001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.xml.xdm.visitor;
043:
044: import java.util.ArrayList;
045: import java.util.Collections;
046: import java.util.HashMap;
047: import java.util.List;
048: import java.util.Map;
049: import java.util.StringTokenizer;
050: import javax.xml.namespace.NamespaceContext;
051: import javax.xml.xpath.XPath;
052: import javax.xml.xpath.XPathConstants;
053: import javax.xml.xpath.XPathExpressionException;
054: import javax.xml.xpath.XPathFactory;
055: import org.netbeans.modules.xml.xdm.nodes.Attribute;
056: import org.netbeans.modules.xml.xdm.nodes.Document;
057: import org.netbeans.modules.xml.xdm.nodes.Element;
058: import org.netbeans.modules.xml.xdm.nodes.Node;
059: import org.w3c.dom.NodeList;
060:
061: /**
062: * Finder for nodes from a document using arbitrary XPath expression.
063: *
064: * @author Nam Nguyen
065: */
066: public class XPathFinder extends ChildVisitor {
067:
068: /**
069: * @return an xpath that can be used to retrieve the given node using #findNode
070: * @param root root node
071: * @param target Element or Attribute node to generate the xpath for.
072: */
073: public static String getXpath(Document root, Node target) {
074: if (!(target instanceof Element)
075: && !(target instanceof Attribute)) {
076: throw new IllegalArgumentException(
077: "Can only get XPath expression for Element or Attribute");
078: }
079: List<Node> result = new PathFromRootVisitor().findPath(root,
080: target);
081: StringBuffer sb = new StringBuffer(128);
082: Element parent = null;
083: for (int j = result.size() - 1; j >= 0; j--) {
084: Node n = result.get(j);
085: boolean isElement = n instanceof Element;
086: boolean isAttribute = n instanceof Attribute;
087: if (!isElement && !isAttribute) {
088: continue;
089: }
090: sb.append(SEP);
091: int index = isElement ? xpathIndexOf(parent, (Element) n)
092: : -1;
093: String prefix = n.getPrefix();
094: String namespace = n.lookupNamespaceURI(prefix);
095: if (isAttribute) {
096: sb.append(AT);
097: }
098: if (prefix != null) {
099: sb.append(prefix);
100: sb.append(':');
101: }
102: sb.append(n.getLocalName());
103: if (isElement && index > 0) {
104: sb.append(BRACKET0);
105: sb.append(String.valueOf(index));
106: sb.append(BRACKET1);
107: }
108: if (isAttribute) {
109: break;
110: }
111: parent = (Element) n;
112: }
113: return sb.toString();
114: }
115:
116: /**
117: * @return the node correspond to specified xpath, if provided xpath result
118: * in more than one nodes, only the first on get returned.
119: * @param root context in which to find the node.
120: * @param xpath location path for the attribute or element.
121: */
122: public Node findNode(Document root, String xpath) {
123: List<Node> nodes = findNodes(root, xpath);
124: return nodes.size() > 0 ? nodes.get(0) : null;
125: }
126:
127: /**
128: * @return the nodes correspond to specified xpath.
129: * @param root context in which to find the node.
130: * @param xpath location path for the attribute or element.
131: */
132: public List<Node> findNodes(Document root, String xpathExpression) {
133: if (root == null || root.getDocumentElement() == null) {
134: return Collections.emptyList();
135: }
136:
137: init(root, xpathExpression);
138: if (!isReadyForEvaluation()) {
139: return new ArrayList<Node>();
140: }
141:
142: XPath xpath = XPathFactory.newInstance().newXPath();
143: xpath.setNamespaceContext(getNamespaceContext());
144: NodeList result = null;
145: try {
146: result = (NodeList) xpath.evaluate(getFixedUpXpath(), root,
147: XPathConstants.NODESET);
148: } catch (XPathExpressionException e) {
149: throw new RuntimeException(e.getCause()
150: .getLocalizedMessage());
151: }
152: assert (result != null);
153: List<Node> ret = new ArrayList<Node>();
154: for (int i = 0; i < result.getLength(); i++) {
155: ret.add((Node) result.item(i));
156: }
157: return ret;
158: }
159:
160: // 0-based index for xpath
161: private static int xpathIndexOf(Node parentNode, Element child) {
162: assert (child != null);
163: if (!(parentNode instanceof Element || parentNode instanceof Document)) {
164: return -1;
165: }
166:
167: NodeList children = parentNode.getChildNodes();
168: int index = 0;
169: String namespace = child.getNamespaceURI();
170: String name = child.getLocalName();
171: for (int i = 0; i < children.getLength(); i++) {
172: if (!(children.item(i) instanceof Element)) {
173: continue;
174: }
175: Element n = (Element) children.item(i);
176: if (namespace != null
177: && !namespace.equals(n.getNamespaceURI())
178: || namespace == null && n.getNamespaceURI() != null) {
179: continue;
180: }
181:
182: if (name.equals(n.getLocalName())) {
183: index++;
184: }
185:
186: if (n.getId() == child.getId()) {
187: return index;
188: }
189: }
190:
191: return -1;
192: }
193:
194: private void init(Document root, String xpath) {
195: currentParent = root;
196: initTokens(xpath);
197: ((Element) root.getDocumentElement()).accept(this );
198: }
199:
200: private void initTokens(String xpath) {
201: tokens = new ArrayList<XPathSegment>();
202: StringTokenizer tokenizer = new StringTokenizer(xpath, SEP);
203: while (tokenizer.hasMoreTokens()) {
204: String token = tokenizer.nextToken();
205: tokens.add(new XPathSegment(token));
206: }
207: }
208:
209: private static class XPathSegment {
210: private final String token;
211: private String prefix;
212: private String localPart;
213: int index = -1;
214: private String remaining;
215: private boolean isAttribute;
216: private boolean hasConditions;
217:
218: XPathSegment(String token) {
219: this .token = token;
220: parse();
221: }
222:
223: private void parse() {
224: StringTokenizer tokenizer = new StringTokenizer(token, ":[");
225: List<String> parts = new ArrayList<String>();
226: while (tokenizer.hasMoreTokens()) {
227: String t = tokenizer.nextToken();
228: parts.add(t);
229: }
230: if (parts.size() == 1) {
231: prefix = "";
232: localPart = parts.get(0);
233: remaining = "";
234: } else if (parts.size() == 3) {
235: prefix = parts.get(0);
236: localPart = parts.get(1);
237: remaining = parts.get(2);
238: } else if (parts.size() == 2) {
239: String part0 = parts.get(0);
240: if (token.charAt(part0.length()) == ':') {
241: prefix = part0;
242: localPart = parts.get(1);
243: remaining = "";
244: } else {
245: localPart = parts.get(0);
246: remaining = parts.get(1);
247: prefix = "";
248: }
249: }
250: if (prefix != null && prefix.startsWith(AT)) {
251: isAttribute = true;
252: prefix = prefix.substring(1);
253: } else if (localPart.startsWith(AT)) {
254: isAttribute = true;
255: localPart = localPart.substring(1);
256: }
257: if (!remaining.equals("")) {
258: String indexString = remaining.substring(0, remaining
259: .length() - 1);
260: try {
261: index = Integer.parseInt(indexString);
262: } catch (NumberFormatException ex) {
263: hasConditions = true;
264: }
265: remaining = BRACKET0 + remaining;
266: }
267: }
268:
269: public boolean checkTypeAndName(Node e, Node parent) {
270: if (e instanceof Element) {
271: if (isAttribute())
272: return false;
273: if (hasPlainIndex()
274: && getIndex() != xpathIndexOf(parent,
275: (Element) e)) {
276: return false;
277: }
278: } else if (e instanceof Attribute) {
279: if (!isAttribute())
280: return false;
281: } else {
282: return false;
283: }
284: if (!e.getLocalName().equals(getLocalPart())) {
285: return false;
286: }
287: return true;
288: }
289:
290: public void addToXpath(StringBuffer xpath, String prefix) {
291: xpath.append(SEP);
292: if (isAttribute()) {
293: xpath.append(AT);
294: }
295: xpath.append(prefix);
296: xpath.append(COLON);
297: xpath.append(localPart);
298: if (hasConditions() && !prefix.startsWith(XPNS)) {
299: String prefixing = AT + prefix + COLON;
300: //TODO: a better RE to skip those already prefixed
301: xpath.append(remaining.replaceAll(AT, prefixing));
302: } else {
303: xpath.append(remaining);
304: }
305: }
306:
307: public void addToXpath(StringBuffer xpath) {
308: xpath.append(SEP);
309: xpath.append(token);
310: }
311:
312: public String getPrefix() {
313: return prefix;
314: }
315:
316: public String getLocalPart() {
317: return localPart;
318: }
319:
320: public boolean hasPlainIndex() {
321: return index > -1;
322: }
323:
324: public boolean hasConditions() {
325: return hasConditions;
326: }
327:
328: public int getIndex() {
329: return index;
330: }
331:
332: public String getRemaining() {
333: return remaining;
334: }
335:
336: public String getToken() {
337: return token;
338: }
339:
340: public boolean isAttribute() {
341: return isAttribute;
342: }
343:
344: public String toString() {
345: return token;
346: }
347: }
348:
349: protected void visitNode(Node e) {
350: if (tokens.size() == 0) {
351: done = true;
352: return;
353: }
354:
355: XPathSegment segment = tokens.get(0);
356: if (!segment.checkTypeAndName(e, currentParent)) {
357: return; // not matched
358: }
359:
360: String currentNamespace = e.getNamespaceURI();
361: if (currentNamespace != null) {
362: String prefix = segment.getPrefix();
363: if (prefix.length() > 0) {
364: String namespace = e.lookupNamespaceURI(prefix);
365: if (namespace == null) {
366: namespace = namespaces.get(prefix);
367: }
368: if (namespace != null
369: && !currentNamespace.equals(namespace)) {
370: return; // not matched
371: }
372: segment.addToXpath(fixedUpXpath);
373: } else {
374: // no prefix, make up one
375: prefix = prefixes.get(currentNamespace);
376: if (prefix == null) {
377: prefix = XPNS
378: + String.valueOf(countNamespaceSuffix++);
379: }
380: segment.addToXpath(fixedUpXpath, prefix);
381: }
382: // push current namespace context
383: prefixes.put(currentNamespace, prefix);
384: namespaces.put(prefix, currentNamespace);
385: } else {
386: segment.addToXpath(fixedUpXpath);
387: }
388:
389: tokens.remove(0);
390: currentParent = e;
391: super .visitNode(e);
392: }
393:
394: public boolean isReadyForEvaluation() {
395: return done;
396: }
397:
398: public NamespaceContext getNamespaceContext() {
399: return new HashNamespaceResolver(namespaces, prefixes);
400: }
401:
402: private String getFixedUpXpath() {
403: return fixedUpXpath.toString();
404: }
405:
406: private boolean done = false;
407: private Node currentParent = null;
408: private Map<String, String> namespaces = new HashMap<String, String>();
409: private Map<String, String> prefixes = new HashMap<String, String>();
410: private List<XPathSegment> tokens;
411: private StringBuffer fixedUpXpath = new StringBuffer();
412:
413: public static final String SEP = "/"; //NOI18N
414: public static final String AT = "@"; //NOI18N
415: public static final String COLON = ":"; //NOI18N
416: public static final String BRACKET0 = "["; //NOI18N
417: public static final String BRACKET1 = "]"; //NOI18N
418: public static final String XPNS = "xpns"; //NOI18N
419: private int countNamespaceSuffix = 0;
420: }
|