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("""); //$NON-NLS-1$ 458 } else if (c == '<') { 459 sb.append("<"); //$NON-NLS-1$ 460 } else if (c == '\'') { 461 sb.append("'"); //$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("<"); //$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