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