• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
19 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
20 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
21 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
22 import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML;
23 
24 import com.android.ide.eclipse.adt.AdtPlugin;
25 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
26 import com.android.util.Pair;
27 
28 import org.eclipse.jface.text.IDocument;
29 import org.eclipse.wst.sse.core.StructuredModelManager;
30 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
31 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
32 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
33 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
34 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
35 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
36 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
37 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
38 import org.w3c.dom.Attr;
39 import org.w3c.dom.Document;
40 import org.w3c.dom.Element;
41 import org.w3c.dom.NamedNodeMap;
42 import org.w3c.dom.Node;
43 import org.w3c.dom.NodeList;
44 import org.xml.sax.InputSource;
45 
46 import java.io.StringReader;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Locale;
53 import java.util.Set;
54 
55 import javax.xml.parsers.DocumentBuilder;
56 import javax.xml.parsers.DocumentBuilderFactory;
57 
58 /**
59  * Various utility methods for manipulating DOM nodes.
60  */
61 @SuppressWarnings("restriction") // No replacement for restricted XML model yet
62 public class DomUtilities {
63     private static final String AMPERSAND_ENTITY = "&"; //$NON-NLS-1$
64 
65     /**
66      * Finds the nearest common parent of the two given nodes (which could be one of the
67      * two nodes as well)
68      *
69      * @param node1 the first node to test
70      * @param node2 the second node to test
71      * @return the nearest common parent of the two given nodes
72      */
getCommonAncestor(Node node1, Node node2)73     public static Node getCommonAncestor(Node node1, Node node2) {
74         while (node2 != null) {
75             Node current = node1;
76             while (current != null && current != node2) {
77                 current = current.getParentNode();
78             }
79             if (current == node2) {
80                 return current;
81             }
82             node2 = node2.getParentNode();
83         }
84 
85         return null;
86     }
87 
88     /**
89      * Returns all elements below the given node (which can be a document,
90      * element, etc). This will include the node itself, if it is an element.
91      *
92      * @param node the node to search from
93      * @return all elements in the subtree formed by the node parameter
94      */
getAllElements(Node node)95     public static List<Element> getAllElements(Node node) {
96         List<Element> elements = new ArrayList<Element>(64);
97         addElements(node, elements);
98         return elements;
99     }
100 
addElements(Node node, List<Element> elements)101     private static void addElements(Node node, List<Element> elements) {
102         if (node instanceof Element) {
103             elements.add((Element) node);
104         }
105 
106         NodeList childNodes = node.getChildNodes();
107         for (int i = 0, n = childNodes.getLength(); i < n; i++) {
108             addElements(childNodes.item(i), elements);
109         }
110     }
111 
112     /**
113      * Returns the depth of the given node (with the document node having depth 0,
114      * and the document element having depth 1)
115      *
116      * @param node the node to test
117      * @return the depth in the document
118      */
getDepth(Node node)119     public static int getDepth(Node node) {
120         int depth = -1;
121         while (node != null) {
122             depth++;
123             node = node.getParentNode();
124         }
125 
126         return depth;
127     }
128 
129     /**
130      * Returns true if the given node has one or more element children
131      *
132      * @param node the node to test for element children
133      * @return true if the node has one or more element children
134      */
hasElementChildren(Node node)135     public static boolean hasElementChildren(Node node) {
136         NodeList children = node.getChildNodes();
137         for (int i = 0, n = children.getLength(); i < n; i++) {
138             if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
139                 return true;
140             }
141         }
142 
143         return false;
144     }
145 
146     /**
147      * Returns the XML DOM node corresponding to the given offset of the given
148      * document.
149      *
150      * @param document The document to look in
151      * @param offset The offset to look up the node for
152      * @return The node containing the offset, or null
153      */
getNode(IDocument document, int offset)154     public static Node getNode(IDocument document, int offset) {
155         Node node = null;
156         IModelManager modelManager = StructuredModelManager.getModelManager();
157         if (modelManager == null) {
158             return null;
159         }
160         try {
161             IStructuredModel model = modelManager.getExistingModelForRead(document);
162             if (model != null) {
163                 try {
164                     for (; offset >= 0 && node == null; --offset) {
165                         node = (Node) model.getIndexedRegion(offset);
166                     }
167                 } finally {
168                     model.releaseFromRead();
169                 }
170             }
171         } catch (Exception e) {
172             // Ignore exceptions.
173         }
174 
175         return node;
176     }
177 
178     /**
179      * Returns the editing context at the given offset, as a pair of parent node and child
180      * node. This is not the same as just calling {@link DomUtilities#getNode} and taking
181      * its parent node, because special care has to be taken to return content element
182      * positions.
183      * <p>
184      * For example, for the XML {@code <foo>^</foo>}, if the caret ^ is inside the foo
185      * element, between the opening and closing tags, then the foo element is the parent,
186      * and the child is null which represents a potential text node.
187      * <p>
188      * If the node is inside an element tag definition (between the opening and closing
189      * bracket) then the child node will be the element and whatever parent (element or
190      * document) will be its parent.
191      * <p>
192      * If the node is in a text node, then the text node will be the child and its parent
193      * element or document node its parent.
194      * <p>
195      * Finally, if the caret is on a boundary of a text node, then the text node will be
196      * considered the child, regardless of whether it is on the left or right of the
197      * caret. For example, in the XML {@code <foo>^ </foo>} and in the XML
198      * {@code <foo> ^</foo>}, in both cases the text node is preferred over the element.
199      *
200      * @param document the document to search in
201      * @param offset the offset to look up
202      * @return a pair of parent and child elements, where either the parent or the child
203      *         but not both can be null, and if non null the child.getParentNode() should
204      *         return the parent. Note that the method can also return null if no
205      *         document or model could be obtained or if the offset is invalid.
206      */
getNodeContext(IDocument document, int offset)207     public static Pair<Node, Node> getNodeContext(IDocument document, int offset) {
208         Node node = null;
209         IModelManager modelManager = StructuredModelManager.getModelManager();
210         if (modelManager == null) {
211             return null;
212         }
213         try {
214             IStructuredModel model = modelManager.getExistingModelForRead(document);
215             if (model != null) {
216                 try {
217                     for (; offset >= 0 && node == null; --offset) {
218                         IndexedRegion indexedRegion = model.getIndexedRegion(offset);
219                         if (indexedRegion != null) {
220                             node = (Node) indexedRegion;
221 
222                             if (node.getNodeType() == Node.TEXT_NODE) {
223                                 return Pair.of(node.getParentNode(), node);
224                             }
225 
226                             // Look at the structured document to see if
227                             // we have the special case where the caret is pointing at
228                             // a -potential- text node, e.g. <foo>^</foo>
229                             IStructuredDocument doc = model.getStructuredDocument();
230                             IStructuredDocumentRegion region =
231                                 doc.getRegionAtCharacterOffset(offset);
232 
233                             ITextRegion subRegion = region.getRegionAtCharacterOffset(offset);
234                             String type = subRegion.getType();
235                             if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) {
236                                 // Try to return the text node if it's on the left
237                                 // of this element node, such that replace strings etc
238                                 // can be computed.
239                                 Node lastChild = node.getLastChild();
240                                 if (lastChild != null) {
241                                     IndexedRegion previousRegion = (IndexedRegion) lastChild;
242                                     if (previousRegion.getEndOffset() == offset) {
243                                         return Pair.of(node, lastChild);
244                                     }
245                                 }
246                                 return Pair.of(node, null);
247                             }
248 
249                             return Pair.of(node.getParentNode(), node);
250                         }
251                     }
252                 } finally {
253                     model.releaseFromRead();
254                 }
255             }
256         } catch (Exception e) {
257             // Ignore exceptions.
258         }
259 
260         return null;
261     }
262 
263     /**
264      * Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you
265      * indicate whether you want the search to look forwards or backwards.
266      * This is vital when trying to compute a node range. Consider the following
267      * XML fragment:
268      * {@code
269      *    <a/><b/>[<c/><d/><e/>]<f/><g/>
270      * }
271      * Suppose we want to locate the nodes in the range indicated by the brackets above.
272      * If we want to search for the node corresponding to the start position, should
273      * we pick the node on its left or the node on its right? Similarly for the end
274      * position. Clearly, we'll need to bias the search towards the right when looking
275      * for the start position, and towards the left when looking for the end position.
276      * The following method lets us do just that. When passed an offset which sits
277      * on the edge of the computed node, it will pick the neighbor based on whether
278      * "forward" is true or false, where forward means searching towards the right
279      * and not forward is obviously towards the left.
280      * @param document the document to search in
281      * @param offset the offset to search for
282      * @param forward if true, search forwards, otherwise search backwards when on node boundaries
283      * @return the node which surrounds the given offset, or the node adjacent to the offset
284      *    where the side depends on the forward parameter
285      */
getNode(IDocument document, int offset, boolean forward)286     public static Node getNode(IDocument document, int offset, boolean forward) {
287         Node node = getNode(document, offset);
288 
289         if (node instanceof IndexedRegion) {
290             IndexedRegion region = (IndexedRegion) node;
291 
292             if (!forward && offset <= region.getStartOffset()) {
293                 Node left = node.getPreviousSibling();
294                 if (left == null) {
295                     left = node.getParentNode();
296                 }
297 
298                 node = left;
299             } else if (forward && offset >= region.getEndOffset()) {
300                 Node right = node.getNextSibling();
301                 if (right == null) {
302                     right = node.getParentNode();
303                 }
304                 node = right;
305             }
306         }
307 
308         return node;
309     }
310 
311     /**
312      * Returns a range of elements for the given caret range. Note that the two elements
313      * may not be at the same level so callers may want to perform additional input
314      * filtering.
315      *
316      * @param document the document to search in
317      * @param beginOffset the beginning offset of the range
318      * @param endOffset the ending offset of the range
319      * @return a pair of begin+end elements, or null
320      */
getElementRange(IDocument document, int beginOffset, int endOffset)321     public static Pair<Element, Element> getElementRange(IDocument document, int beginOffset,
322             int endOffset) {
323         Element beginElement = null;
324         Element endElement = null;
325         Node beginNode = getNode(document, beginOffset, true);
326         Node endNode = beginNode;
327         if (endOffset > beginOffset) {
328             endNode = getNode(document, endOffset, false);
329         }
330 
331         if (beginNode == null || endNode == null) {
332             return null;
333         }
334 
335         // Adjust offsets if you're pointing at text
336         if (beginNode.getNodeType() != Node.ELEMENT_NODE) {
337             // <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/>
338             beginElement = getNextElement(beginNode);
339             if (beginElement == null) {
340                 // Might be inside the end of a parent, e.g.
341                 // <foo> <bar/> | </foo> => should pick <bar/>
342                 beginElement = getPreviousElement(beginNode);
343                 if (beginElement == null) {
344                     // We must be inside an empty element,
345                     // <foo> | </foo>
346                     // In that case just pick the parent.
347                     beginElement = getParentElement(beginNode);
348                 }
349             }
350         } else {
351             beginElement = (Element) beginNode;
352         }
353 
354         if (endNode.getNodeType() != Node.ELEMENT_NODE) {
355             // In the following, | marks the caret position:
356             // <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/>
357             endElement = getPreviousElement(endNode);
358             if (endElement == null) {
359                 // Might be inside the beginning of a parent, e.g.
360                 // <foo> | <bar/></foo> => should pick <bar/>
361                 endElement = getNextElement(endNode);
362                 if (endElement == null) {
363                     // We must be inside an empty element,
364                     // <foo> | </foo>
365                     // In that case just pick the parent.
366                     endElement = getParentElement(endNode);
367                 }
368             }
369         } else {
370             endElement = (Element) endNode;
371         }
372 
373         if (beginElement != null && endElement != null) {
374             return Pair.of(beginElement, endElement);
375         }
376 
377         return null;
378     }
379 
380     /**
381      * Returns the next sibling element of the node, or null if there is no such element
382      *
383      * @param node the starting node
384      * @return the next sibling element, or null
385      */
getNextElement(Node node)386     public static Element getNextElement(Node node) {
387         while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
388             node = node.getNextSibling();
389         }
390 
391         return (Element) node; // may be null as well
392     }
393 
394     /**
395      * Returns the previous sibling element of the node, or null if there is no such element
396      *
397      * @param node the starting node
398      * @return the previous sibling element, or null
399      */
getPreviousElement(Node node)400     public static Element getPreviousElement(Node node) {
401         while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
402             node = node.getPreviousSibling();
403         }
404 
405         return (Element) node; // may be null as well
406     }
407 
408     /**
409      * Returns the closest ancestor element, or null if none
410      *
411      * @param node the starting node
412      * @return the closest parent element, or null
413      */
getParentElement(Node node)414     public static Element getParentElement(Node node) {
415         while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
416             node = node.getParentNode();
417         }
418 
419         return (Element) node; // may be null as well
420     }
421 
422     /**
423      * Converts the given attribute value to an XML-attribute-safe value, meaning that
424      * single and double quotes are replaced with their corresponding XML entities.
425      *
426      * @param attrValue the value to be escaped
427      * @return the escaped value
428      */
toXmlAttributeValue(String attrValue)429     public static String toXmlAttributeValue(String attrValue) {
430         for (int i = 0, n = attrValue.length(); i < n; i++) {
431             char c = attrValue.charAt(i);
432             if (c == '"' || c == '\'' || c == '<' || c == '&') {
433                 StringBuilder sb = new StringBuilder(2 * attrValue.length());
434                 appendXmlAttributeValue(sb, attrValue);
435                 return sb.toString();
436             }
437         }
438 
439         return attrValue;
440     }
441 
442     /**
443      * Appends text to the given {@link StringBuilder} and escapes it as required for a
444      * DOM attribute node.
445      *
446      * @param sb the string builder
447      * @param attrValue the attribute value to be appended and escaped
448      */
appendXmlAttributeValue(StringBuilder sb, String attrValue)449     public static void appendXmlAttributeValue(StringBuilder sb, String attrValue) {
450         int n = attrValue.length();
451         // &, ", ' and < are illegal in attributes; see http://www.w3.org/TR/REC-xml/#NT-AttValue
452         // (' legal in a " string and " is legal in a ' string but here we'll stay on the safe
453         // side)
454         for (int i = 0; i < n; i++) {
455             char c = attrValue.charAt(i);
456             if (c == '"') {
457                 sb.append("&quot;"); //$NON-NLS-1$
458             } else if (c == '<') {
459                 sb.append("&lt;"); //$NON-NLS-1$
460             } else if (c == '\'') {
461                 sb.append("&apos;"); //$NON-NLS-1$
462             } else if (c == '&') {
463                 sb.append(AMPERSAND_ENTITY);
464             } else {
465                 sb.append(c);
466             }
467         }
468     }
469 
470     /**
471      * Appends text to the given {@link StringBuilder} and escapes it as required for a
472      * DOM text node.
473      *
474      * @param sb the string builder
475      * @param textValue the text value to be appended and escaped
476      */
appendXmlTextValue(StringBuilder sb, String textValue)477     public static void appendXmlTextValue(StringBuilder sb, String textValue) {
478         for (int i = 0, n = textValue.length(); i < n; i++) {
479             char c = textValue.charAt(i);
480             if (c == '<') {
481                 sb.append("&lt;");  //$NON-NLS-1$
482             } else if (c == '&') {
483                 sb.append(AMPERSAND_ENTITY);
484             } else {
485                 sb.append(c);
486             }
487         }
488     }
489 
490     /** Utility used by {@link #getFreeWidgetId(Element)} */
addLowercaseIds(Element root, Set<String> seen)491     private static void addLowercaseIds(Element root, Set<String> seen) {
492         if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
493             String id = root.getAttributeNS(ANDROID_URI, ATTR_ID);
494             if (id.startsWith(NEW_ID_PREFIX)) {
495                 // See getFreeWidgetId for details on locale
496                 seen.add(id.substring(NEW_ID_PREFIX.length()).toLowerCase(Locale.US));
497             } else if (id.startsWith(ID_PREFIX)) {
498                 seen.add(id.substring(ID_PREFIX.length()).toLowerCase(Locale.US));
499             } else {
500                 seen.add(id.toLowerCase(Locale.US));
501             }
502         }
503     }
504 
505     /**
506      * Returns a suitable new widget id (not including the {@code @id/} prefix) for the
507      * given element, which is guaranteed to be unique in this document
508      *
509      * @param element the element to compute a new widget id for
510      * @param reserved an optional set of extra, "reserved" set of ids that should be
511      *            considered taken
512      * @param prefix an optional prefix to use for the generated name, or null to get a
513      *            default (which is currently the tag name)
514      * @return a unique id, never null, which does not include the {@code @id/} prefix
515      * @see DescriptorsUtils#getFreeWidgetId
516      */
getFreeWidgetId(Element element, Set<String> reserved, String prefix)517     public static String getFreeWidgetId(Element element, Set<String> reserved, String prefix) {
518         Set<String> ids = new HashSet<String>();
519         if (reserved != null) {
520             for (String id : reserved) {
521                 // Note that we perform locale-independent lowercase checks; in "Image" we
522                 // want the lowercase version to be "image", not "?mage" where ? is
523                 // the char LATIN SMALL LETTER DOTLESS I.
524 
525                 ids.add(id.toLowerCase(Locale.US));
526             }
527         }
528         addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
529 
530         if (prefix == null) {
531             prefix = DescriptorsUtils.getBasename(element.getTagName());
532         }
533         String generated;
534         int num = 1;
535         do {
536             generated = String.format("%1$s%2$d", prefix, num++);   //$NON-NLS-1$
537         } while (ids.contains(generated.toLowerCase(Locale.US)));
538 
539         return generated;
540     }
541 
542     /**
543      * Returns the element children of the given element
544      *
545      * @param element the parent element
546      * @return a list of child elements, possibly empty but never null
547      */
getChildren(Element element)548     public static List<Element> getChildren(Element element) {
549         // Convenience to avoid lots of ugly DOM access casting
550         NodeList children = element.getChildNodes();
551         // An iterator would have been more natural (to directly drive the child list
552         // iteration) but iterators can't be used in enhanced for loops...
553         List<Element> result = new ArrayList<Element>(children.getLength());
554         for (int i = 0, n = children.getLength(); i < n; i++) {
555             Node node = children.item(i);
556             if (node.getNodeType() == Node.ELEMENT_NODE) {
557                 Element child = (Element) node;
558                 result.add(child);
559             }
560         }
561 
562         return result;
563     }
564 
565     /**
566      * Returns true iff the given elements are contiguous siblings
567      *
568      * @param elements the elements to be tested
569      * @return true if the elements are contiguous siblings with no gaps
570      */
isContiguous(List<Element> elements)571     public static boolean isContiguous(List<Element> elements) {
572         if (elements.size() > 1) {
573             // All elements must be siblings (e.g. same parent)
574             Node parent = elements.get(0).getParentNode();
575             if (!(parent instanceof Element)) {
576                 return false;
577             }
578             for (Element node : elements) {
579                 if (parent != node.getParentNode()) {
580                     return false;
581                 }
582             }
583 
584             // Ensure that the siblings are contiguous; no gaps.
585             // If we've selected all the children of the parent then we don't need
586             // to look.
587             List<Element> siblings = DomUtilities.getChildren((Element) parent);
588             if (siblings.size() != elements.size()) {
589                 Set<Element> nodeSet = new HashSet<Element>(elements);
590                 boolean inRange = false;
591                 int remaining = elements.size();
592                 for (Element node : siblings) {
593                     boolean in = nodeSet.contains(node);
594                     if (in) {
595                         remaining--;
596                         if (remaining == 0) {
597                             break;
598                         }
599                         inRange = true;
600                     } else if (inRange) {
601                         return false;
602                     }
603                 }
604             }
605         }
606 
607         return true;
608     }
609 
610     /**
611      * Determines whether two element trees are equivalent. Two element trees are
612      * equivalent if they represent the same DOM structure (elements, attributes, and
613      * children in order). This is almost the same as simply checking whether the String
614      * representations of the two nodes are identical, but this allows for minor
615      * variations that are not semantically significant, such as variations in formatting
616      * or ordering of the element attribute declarations, and the text children are
617      * ignored (this is such that in for example layout where content is only used for
618      * indentation the indentation differences are ignored). Null trees are never equal.
619      *
620      * @param element1 the first element to compare
621      * @param element2 the second element to compare
622      * @return true if the two element hierarchies are logically equal
623      */
isEquivalent(Element element1, Element element2)624     public static boolean isEquivalent(Element element1, Element element2) {
625         if (element1 == null || element2 == null) {
626             return false;
627         }
628 
629         if (!element1.getTagName().equals(element2.getTagName())) {
630             return false;
631         }
632 
633         // Check attribute map
634         NamedNodeMap attributes1 = element1.getAttributes();
635         NamedNodeMap attributes2 = element2.getAttributes();
636         if (attributes1.getLength() != attributes2.getLength()) {
637             return false;
638         }
639         if (attributes1.getLength() > 0) {
640             List<Attr> attributeNodes1 = new ArrayList<Attr>();
641             for (int i = 0, n = attributes1.getLength(); i < n; i++) {
642                 attributeNodes1.add((Attr) attributes1.item(i));
643             }
644             List<Attr> attributeNodes2 = new ArrayList<Attr>();
645             for (int i = 0, n = attributes2.getLength(); i < n; i++) {
646                 attributeNodes2.add((Attr) attributes2.item(i));
647             }
648             Collections.sort(attributeNodes1, ATTRIBUTE_COMPARATOR);
649             Collections.sort(attributeNodes2, ATTRIBUTE_COMPARATOR);
650             for (int i = 0; i < attributeNodes1.size(); i++) {
651                 Attr attr1 = attributeNodes1.get(i);
652                 Attr attr2 = attributeNodes2.get(i);
653                 if (attr1.getLocalName() == null || attr2.getLocalName() == null) {
654                     if (!attr1.getName().equals(attr2.getName())) {
655                         return false;
656                     }
657                 } else if (!attr1.getLocalName().equals(attr2.getLocalName())) {
658                     return false;
659                 }
660                 if (!attr1.getValue().equals(attr2.getValue())) {
661                     return false;
662                 }
663                 if (attr1.getNamespaceURI() == null) {
664                     if (attr2.getNamespaceURI() != null) {
665                         return false;
666                     }
667                 } else if (attr2.getNamespaceURI() == null) {
668                     return false;
669                 } else if (!attr1.getNamespaceURI().equals(attr2.getNamespaceURI())) {
670                     return false;
671                 }
672             }
673         }
674 
675         NodeList children1 = element1.getChildNodes();
676         NodeList children2 = element2.getChildNodes();
677         int nextIndex1 = 0;
678         int nextIndex2 = 0;
679         while (true) {
680             while (nextIndex1 < children1.getLength() &&
681                     children1.item(nextIndex1).getNodeType() != Node.ELEMENT_NODE) {
682                 nextIndex1++;
683             }
684 
685             while (nextIndex2 < children2.getLength() &&
686                     children2.item(nextIndex2).getNodeType() != Node.ELEMENT_NODE) {
687                 nextIndex2++;
688             }
689 
690             Element nextElement1 = (Element) (nextIndex1 < children1.getLength()
691                     ? children1.item(nextIndex1) : null);
692             Element nextElement2 = (Element) (nextIndex2 < children2.getLength()
693                     ? children2.item(nextIndex2) : null);
694             if (nextElement1 == null) {
695                 return nextElement2 == null;
696             } else if (nextElement2 == null) {
697                 return false;
698             } else if (!isEquivalent(nextElement1, nextElement2)) {
699                 return false;
700             }
701             nextIndex1++;
702             nextIndex2++;
703         }
704     }
705 
706     /**
707      * Finds the corresponding element in a document to a given element in another
708      * document. Note that this does <b>not</b> do any kind of equivalence check
709      * (see {@link #isEquivalent(Element, Element)}), and currently the search
710      * is only by id; there is no structural search.
711      *
712      * @param element the element to find an equivalent for
713      * @param document the document to search for an equivalent element in
714      * @return an equivalent element, or null
715      */
findCorresponding(Element element, Document document)716     public static Element findCorresponding(Element element, Document document) {
717         // Make sure the method is called correctly -- the element is for a different
718         // document than the one we are searching
719         assert element.getOwnerDocument() != document;
720 
721         // First search by id. This allows us to find the corresponding
722         String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
723         if (id != null && id.length() > 0) {
724             if (id.startsWith(ID_PREFIX)) {
725                 id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
726             }
727 
728             return findCorresponding(document.getDocumentElement(), id);
729         }
730 
731         // TODO: Search by structure - look in the document and
732         // find a corresponding element in the same location in the structure,
733         // e.g. 4th child of root, 3rd child, 6th child, then pick node with tag "foo".
734 
735         return null;
736     }
737 
738     /** Helper method for {@link #findCorresponding(Element, Document)} */
findCorresponding(Element element, String targetId)739     private static Element findCorresponding(Element element, String targetId) {
740         String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
741         if (id != null) { // Work around DOM bug
742             if (id.equals(targetId)) {
743                 return element;
744             } else if (id.startsWith(ID_PREFIX)) {
745                 id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
746                 if (id.equals(targetId)) {
747                     return element;
748                 }
749             }
750         }
751 
752         NodeList children = element.getChildNodes();
753         for (int i = 0, n = children.getLength(); i < n; i++) {
754             Node node = children.item(i);
755             if (node.getNodeType() == Node.ELEMENT_NODE) {
756                 Element child = (Element) node;
757                 Element match = findCorresponding(child, targetId);
758                 if (match != null) {
759                     return match;
760                 }
761             }
762         }
763 
764         return null;
765     }
766 
767     /**
768      * Parses the given XML string as a DOM document, using Eclipse's structured
769      * XML model (which for example allows us to distinguish empty elements
770      * (<foo/>) from elements with no children (<foo></foo>).
771      *
772      * @param xml the XML content to be parsed (must be well formed)
773      * @return the DOM document, or null
774      */
parseStructuredDocument(String xml)775     public static Document parseStructuredDocument(String xml) {
776         IStructuredModel model = createStructuredModel(xml);
777         if (model instanceof IDOMModel) {
778             IDOMModel domModel = (IDOMModel) model;
779             return domModel.getDocument();
780         }
781 
782         return null;
783     }
784 
785     /**
786      * Parses the given XML string and builds an Eclipse structured model for it.
787      *
788      * @param xml the XML content to be parsed (must be well formed)
789      * @return the structured model
790      */
createStructuredModel(String xml)791     public static IStructuredModel createStructuredModel(String xml) {
792         IStructuredModel model = createEmptyModel();
793         IStructuredDocument document = model.getStructuredDocument();
794         model.aboutToChangeModel();
795         document.set(xml);
796         model.changedModel();
797 
798         return model;
799     }
800 
801     /**
802      * Creates an empty Eclipse XML model
803      *
804      * @return a new Eclipse XML model
805      */
createEmptyModel()806     public static IStructuredModel createEmptyModel() {
807         IModelManager modelManager = StructuredModelManager.getModelManager();
808         return modelManager.createUnManagedStructuredModelFor(ContentTypeID_XML);
809     }
810 
811     /**
812      * Creates an empty Eclipse XML document
813      *
814      * @return an empty Eclipse XML document
815      */
createEmptyDocument()816     public static Document createEmptyDocument() {
817         IStructuredModel model = createEmptyModel();
818         if (model instanceof IDOMModel) {
819             IDOMModel domModel = (IDOMModel) model;
820             return domModel.getDocument();
821         }
822 
823         return null;
824     }
825 
826     /**
827      * Parses the given XML string as a DOM document, using the JDK parser.
828      * The parser does not validate, and is namespace aware.
829      *
830      * @param xml the XML content to be parsed (must be well formed)
831      * @param logParserErrors if true, log parser errors to the log, otherwise
832      *            silently return null
833      * @return the DOM document, or null
834      */
parseDocument(String xml, boolean logParserErrors)835     public static Document parseDocument(String xml, boolean logParserErrors) {
836         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
837         InputSource is = new InputSource(new StringReader(xml));
838         factory.setNamespaceAware(true);
839         factory.setValidating(false);
840         try {
841             DocumentBuilder builder = factory.newDocumentBuilder();
842             return builder.parse(is);
843         } catch (Exception e) {
844             if (logParserErrors) {
845                 AdtPlugin.log(e, null);
846             }
847         }
848 
849         return null;
850     }
851 
852     /** Can be used to sort attributes by name */
853     private static final Comparator<Attr> ATTRIBUTE_COMPARATOR = new Comparator<Attr>() {
854         @Override
855         public int compare(Attr a1, Attr a2) {
856             return a1.getName().compareTo(a2.getName());
857         }
858     };
859 }
860