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