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