• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.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      */
getCommonAncestor(Node node1, Node node2)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      */
getDepth(Node node)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      */
hasElementChildren(Node node)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      */
getNode(IDocument document, int offset)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      */
getNodeContext(IDocument document, int offset)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      */
getNode(IDocument document, int offset, boolean forward)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      */
getElementRange(IDocument document, int beginOffset, int endOffset)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      */
getNextElement(Node node)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      */
getPreviousElement(Node node)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      */
getParentElement(Node node)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      */
toXmlAttributeValue(String attrValue)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      */
appendXmlAttributeValue(StringBuilder sb, String attrValue)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      */
appendXmlTextValue(StringBuilder sb, String textValue)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)} */
addLowercaseIds(Element root, Set<String> seen)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      */
getFreeWidgetId(Element element, Set<String> reserved, String prefix)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      */
getChildren(Element element)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      */
isContiguous(List<Element> elements)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      */
isEquivalent(Element element1, Element element2)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      */
findCorresponding(Element element, Document document)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)} */
findCorresponding(Element element, String targetId)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      */
parseStructuredDocument(String xml)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      */
createStructuredModel(String xml)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      */
parseDocument(String xml, boolean logParserErrors)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