Home | History | Annotate | Download | only in gle2
      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
     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