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 */
16package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17
18import static com.android.SdkConstants.ANDROID_URI;
19import static com.android.SdkConstants.ATTR_ID;
20import static com.android.SdkConstants.ID_PREFIX;
21import static com.android.SdkConstants.NEW_ID_PREFIX;
22import static com.android.SdkConstants.TOOLS_URI;
23import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML;
24
25import com.android.annotations.NonNull;
26import com.android.annotations.Nullable;
27import com.android.ide.eclipse.adt.AdtPlugin;
28import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
29import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
30import com.android.utils.Pair;
31
32import org.eclipse.core.resources.IFile;
33import org.eclipse.jface.text.IDocument;
34import org.eclipse.wst.sse.core.StructuredModelManager;
35import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
36import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
37import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
38import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
39import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
40import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
41import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
42import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
43import org.w3c.dom.Attr;
44import org.w3c.dom.Document;
45import org.w3c.dom.Element;
46import org.w3c.dom.NamedNodeMap;
47import org.w3c.dom.Node;
48import org.w3c.dom.NodeList;
49import org.xml.sax.InputSource;
50
51import java.io.StringReader;
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.Comparator;
55import java.util.HashSet;
56import java.util.List;
57import java.util.Locale;
58import java.util.Set;
59
60import javax.xml.parsers.DocumentBuilder;
61import javax.xml.parsers.DocumentBuilderFactory;
62import 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
68public 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
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
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
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     */
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     */
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
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
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
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
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
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
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
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
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
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)} */
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     */
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
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     */
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     */
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
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
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
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
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
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
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
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
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