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