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