| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.ide.eclipse.adt.internal.editors.layout.gle2; |
| |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ID_PREFIX; |
| import static com.android.SdkConstants.NEW_ID_PREFIX; |
| import static com.android.SdkConstants.TOOLS_URI; |
| import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; |
| import com.android.utils.Pair; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.wst.sse.core.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IModelManager; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; |
| import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.InputSource; |
| |
| import java.io.StringReader; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| /** |
| * Various utility methods for manipulating DOM nodes. |
| */ |
| @SuppressWarnings("restriction") // No replacement for restricted XML model yet |
| public class DomUtilities { |
| /** |
| * Finds the nearest common parent of the two given nodes (which could be one of the |
| * two nodes as well) |
| * |
| * @param node1 the first node to test |
| * @param node2 the second node to test |
| * @return the nearest common parent of the two given nodes |
| */ |
| @Nullable |
| public static Node getCommonAncestor(@NonNull Node node1, @NonNull Node node2) { |
| while (node2 != null) { |
| Node current = node1; |
| while (current != null && current != node2) { |
| current = current.getParentNode(); |
| } |
| if (current == node2) { |
| return current; |
| } |
| node2 = node2.getParentNode(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns all elements below the given node (which can be a document, |
| * element, etc). This will include the node itself, if it is an element. |
| * |
| * @param node the node to search from |
| * @return all elements in the subtree formed by the node parameter |
| */ |
| @NonNull |
| public static List<Element> getAllElements(@NonNull Node node) { |
| List<Element> elements = new ArrayList<Element>(64); |
| addElements(node, elements); |
| return elements; |
| } |
| |
| private static void addElements(@NonNull Node node, @NonNull List<Element> elements) { |
| if (node instanceof Element) { |
| elements.add((Element) node); |
| } |
| |
| NodeList childNodes = node.getChildNodes(); |
| for (int i = 0, n = childNodes.getLength(); i < n; i++) { |
| addElements(childNodes.item(i), elements); |
| } |
| } |
| |
| /** |
| * Returns the depth of the given node (with the document node having depth 0, |
| * and the document element having depth 1) |
| * |
| * @param node the node to test |
| * @return the depth in the document |
| */ |
| public static int getDepth(@NonNull Node node) { |
| int depth = -1; |
| while (node != null) { |
| depth++; |
| node = node.getParentNode(); |
| } |
| |
| return depth; |
| } |
| |
| /** |
| * Returns true if the given node has one or more element children |
| * |
| * @param node the node to test for element children |
| * @return true if the node has one or more element children |
| */ |
| public static boolean hasElementChildren(@NonNull Node node) { |
| NodeList children = node.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns the DOM document for the given file |
| * |
| * @param file the XML file |
| * @return the document, or null if not found or not parsed properly (no |
| * errors are generated/thrown) |
| */ |
| @Nullable |
| public static Document getDocument(@NonNull IFile file) { |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| if (modelManager == null) { |
| return null; |
| } |
| try { |
| IStructuredModel model = modelManager.getExistingModelForRead(file); |
| if (model == null) { |
| model = modelManager.getModelForRead(file); |
| } |
| if (model != null) { |
| if (model instanceof IDOMModel) { |
| IDOMModel domModel = (IDOMModel) model; |
| return domModel.getDocument(); |
| } |
| try { |
| } finally { |
| model.releaseFromRead(); |
| } |
| } |
| } catch (Exception e) { |
| // Ignore exceptions. |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the DOM document for the given editor |
| * |
| * @param editor the XML editor |
| * @return the document, or null if not found or not parsed properly (no |
| * errors are generated/thrown) |
| */ |
| @Nullable |
| public static Document getDocument(@NonNull AndroidXmlEditor editor) { |
| IStructuredModel model = editor.getModelForRead(); |
| try { |
| if (model instanceof IDOMModel) { |
| IDOMModel domModel = (IDOMModel) model; |
| return domModel.getDocument(); |
| } |
| } finally { |
| if (model != null) { |
| model.releaseFromRead(); |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| /** |
| * Returns the XML DOM node corresponding to the given offset of the given |
| * document. |
| * |
| * @param document The document to look in |
| * @param offset The offset to look up the node for |
| * @return The node containing the offset, or null |
| */ |
| @Nullable |
| public static Node getNode(@NonNull IDocument document, int offset) { |
| Node node = null; |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| if (modelManager == null) { |
| return null; |
| } |
| try { |
| IStructuredModel model = modelManager.getExistingModelForRead(document); |
| if (model != null) { |
| try { |
| for (; offset >= 0 && node == null; --offset) { |
| node = (Node) model.getIndexedRegion(offset); |
| } |
| } finally { |
| model.releaseFromRead(); |
| } |
| } |
| } catch (Exception e) { |
| // Ignore exceptions. |
| } |
| |
| return node; |
| } |
| |
| /** |
| * Returns the editing context at the given offset, as a pair of parent node and child |
| * node. This is not the same as just calling {@link DomUtilities#getNode} and taking |
| * its parent node, because special care has to be taken to return content element |
| * positions. |
| * <p> |
| * For example, for the XML {@code <foo>^</foo>}, if the caret ^ is inside the foo |
| * element, between the opening and closing tags, then the foo element is the parent, |
| * and the child is null which represents a potential text node. |
| * <p> |
| * If the node is inside an element tag definition (between the opening and closing |
| * bracket) then the child node will be the element and whatever parent (element or |
| * document) will be its parent. |
| * <p> |
| * If the node is in a text node, then the text node will be the child and its parent |
| * element or document node its parent. |
| * <p> |
| * Finally, if the caret is on a boundary of a text node, then the text node will be |
| * considered the child, regardless of whether it is on the left or right of the |
| * caret. For example, in the XML {@code <foo>^ </foo>} and in the XML |
| * {@code <foo> ^</foo>}, in both cases the text node is preferred over the element. |
| * |
| * @param document the document to search in |
| * @param offset the offset to look up |
| * @return a pair of parent and child elements, where either the parent or the child |
| * but not both can be null, and if non null the child.getParentNode() should |
| * return the parent. Note that the method can also return null if no |
| * document or model could be obtained or if the offset is invalid. |
| */ |
| @Nullable |
| public static Pair<Node, Node> getNodeContext(@NonNull IDocument document, int offset) { |
| Node node = null; |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| if (modelManager == null) { |
| return null; |
| } |
| try { |
| IStructuredModel model = modelManager.getExistingModelForRead(document); |
| if (model != null) { |
| try { |
| for (; offset >= 0 && node == null; --offset) { |
| IndexedRegion indexedRegion = model.getIndexedRegion(offset); |
| if (indexedRegion != null) { |
| node = (Node) indexedRegion; |
| |
| if (node.getNodeType() == Node.TEXT_NODE) { |
| return Pair.of(node.getParentNode(), node); |
| } |
| |
| // Look at the structured document to see if |
| // we have the special case where the caret is pointing at |
| // a -potential- text node, e.g. <foo>^</foo> |
| IStructuredDocument doc = model.getStructuredDocument(); |
| IStructuredDocumentRegion region = |
| doc.getRegionAtCharacterOffset(offset); |
| |
| ITextRegion subRegion = region.getRegionAtCharacterOffset(offset); |
| String type = subRegion.getType(); |
| if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) { |
| // Try to return the text node if it's on the left |
| // of this element node, such that replace strings etc |
| // can be computed. |
| Node lastChild = node.getLastChild(); |
| if (lastChild != null) { |
| IndexedRegion previousRegion = (IndexedRegion) lastChild; |
| if (previousRegion.getEndOffset() == offset) { |
| return Pair.of(node, lastChild); |
| } |
| } |
| return Pair.of(node, null); |
| } |
| |
| return Pair.of(node.getParentNode(), node); |
| } |
| } |
| } finally { |
| model.releaseFromRead(); |
| } |
| } |
| } catch (Exception e) { |
| // Ignore exceptions. |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you |
| * indicate whether you want the search to look forwards or backwards. |
| * This is vital when trying to compute a node range. Consider the following |
| * XML fragment: |
| * {@code |
| * <a/><b/>[<c/><d/><e/>]<f/><g/> |
| * } |
| * Suppose we want to locate the nodes in the range indicated by the brackets above. |
| * If we want to search for the node corresponding to the start position, should |
| * we pick the node on its left or the node on its right? Similarly for the end |
| * position. Clearly, we'll need to bias the search towards the right when looking |
| * for the start position, and towards the left when looking for the end position. |
| * The following method lets us do just that. When passed an offset which sits |
| * on the edge of the computed node, it will pick the neighbor based on whether |
| * "forward" is true or false, where forward means searching towards the right |
| * and not forward is obviously towards the left. |
| * @param document the document to search in |
| * @param offset the offset to search for |
| * @param forward if true, search forwards, otherwise search backwards when on node boundaries |
| * @return the node which surrounds the given offset, or the node adjacent to the offset |
| * where the side depends on the forward parameter |
| */ |
| @Nullable |
| public static Node getNode(@NonNull IDocument document, int offset, boolean forward) { |
| Node node = getNode(document, offset); |
| |
| if (node instanceof IndexedRegion) { |
| IndexedRegion region = (IndexedRegion) node; |
| |
| if (!forward && offset <= region.getStartOffset()) { |
| Node left = node.getPreviousSibling(); |
| if (left == null) { |
| left = node.getParentNode(); |
| } |
| |
| node = left; |
| } else if (forward && offset >= region.getEndOffset()) { |
| Node right = node.getNextSibling(); |
| if (right == null) { |
| right = node.getParentNode(); |
| } |
| node = right; |
| } |
| } |
| |
| return node; |
| } |
| |
| /** |
| * Returns a range of elements for the given caret range. Note that the two elements |
| * may not be at the same level so callers may want to perform additional input |
| * filtering. |
| * |
| * @param document the document to search in |
| * @param beginOffset the beginning offset of the range |
| * @param endOffset the ending offset of the range |
| * @return a pair of begin+end elements, or null |
| */ |
| @Nullable |
| public static Pair<Element, Element> getElementRange(@NonNull IDocument document, |
| int beginOffset, int endOffset) { |
| Element beginElement = null; |
| Element endElement = null; |
| Node beginNode = getNode(document, beginOffset, true); |
| Node endNode = beginNode; |
| if (endOffset > beginOffset) { |
| endNode = getNode(document, endOffset, false); |
| } |
| |
| if (beginNode == null || endNode == null) { |
| return null; |
| } |
| |
| // Adjust offsets if you're pointing at text |
| if (beginNode.getNodeType() != Node.ELEMENT_NODE) { |
| // <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/> |
| beginElement = getNextElement(beginNode); |
| if (beginElement == null) { |
| // Might be inside the end of a parent, e.g. |
| // <foo> <bar/> | </foo> => should pick <bar/> |
| beginElement = getPreviousElement(beginNode); |
| if (beginElement == null) { |
| // We must be inside an empty element, |
| // <foo> | </foo> |
| // In that case just pick the parent. |
| beginElement = getParentElement(beginNode); |
| } |
| } |
| } else { |
| beginElement = (Element) beginNode; |
| } |
| |
| if (endNode.getNodeType() != Node.ELEMENT_NODE) { |
| // In the following, | marks the caret position: |
| // <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/> |
| endElement = getPreviousElement(endNode); |
| if (endElement == null) { |
| // Might be inside the beginning of a parent, e.g. |
| // <foo> | <bar/></foo> => should pick <bar/> |
| endElement = getNextElement(endNode); |
| if (endElement == null) { |
| // We must be inside an empty element, |
| // <foo> | </foo> |
| // In that case just pick the parent. |
| endElement = getParentElement(endNode); |
| } |
| } |
| } else { |
| endElement = (Element) endNode; |
| } |
| |
| if (beginElement != null && endElement != null) { |
| return Pair.of(beginElement, endElement); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the next sibling element of the node, or null if there is no such element |
| * |
| * @param node the starting node |
| * @return the next sibling element, or null |
| */ |
| @Nullable |
| public static Element getNextElement(@NonNull Node node) { |
| while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { |
| node = node.getNextSibling(); |
| } |
| |
| return (Element) node; // may be null as well |
| } |
| |
| /** |
| * Returns the previous sibling element of the node, or null if there is no such element |
| * |
| * @param node the starting node |
| * @return the previous sibling element, or null |
| */ |
| @Nullable |
| public static Element getPreviousElement(@NonNull Node node) { |
| while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { |
| node = node.getPreviousSibling(); |
| } |
| |
| return (Element) node; // may be null as well |
| } |
| |
| /** |
| * Returns the closest ancestor element, or null if none |
| * |
| * @param node the starting node |
| * @return the closest parent element, or null |
| */ |
| @Nullable |
| public static Element getParentElement(@NonNull Node node) { |
| while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { |
| node = node.getParentNode(); |
| } |
| |
| return (Element) node; // may be null as well |
| } |
| |
| /** Utility used by {@link #getFreeWidgetId(Element)} */ |
| private static void addLowercaseIds(@NonNull Element root, @NonNull Set<String> seen) { |
| if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) { |
| String id = root.getAttributeNS(ANDROID_URI, ATTR_ID); |
| if (id.startsWith(NEW_ID_PREFIX)) { |
| // See getFreeWidgetId for details on locale |
| seen.add(id.substring(NEW_ID_PREFIX.length()).toLowerCase(Locale.US)); |
| } else if (id.startsWith(ID_PREFIX)) { |
| seen.add(id.substring(ID_PREFIX.length()).toLowerCase(Locale.US)); |
| } else { |
| seen.add(id.toLowerCase(Locale.US)); |
| } |
| } |
| } |
| |
| /** |
| * Returns a suitable new widget id (not including the {@code @id/} prefix) for the |
| * given element, which is guaranteed to be unique in this document |
| * |
| * @param element the element to compute a new widget id for |
| * @param reserved an optional set of extra, "reserved" set of ids that should be |
| * considered taken |
| * @param prefix an optional prefix to use for the generated name, or null to get a |
| * default (which is currently the tag name) |
| * @return a unique id, never null, which does not include the {@code @id/} prefix |
| * @see DescriptorsUtils#getFreeWidgetId |
| */ |
| public static String getFreeWidgetId( |
| @NonNull Element element, |
| @Nullable Set<String> reserved, |
| @Nullable String prefix) { |
| Set<String> ids = new HashSet<String>(); |
| if (reserved != null) { |
| for (String id : reserved) { |
| // Note that we perform locale-independent lowercase checks; in "Image" we |
| // want the lowercase version to be "image", not "?mage" where ? is |
| // the char LATIN SMALL LETTER DOTLESS I. |
| |
| ids.add(id.toLowerCase(Locale.US)); |
| } |
| } |
| addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids); |
| |
| if (prefix == null) { |
| prefix = DescriptorsUtils.getBasename(element.getTagName()); |
| } |
| String generated; |
| int num = 1; |
| do { |
| generated = String.format("%1$s%2$d", prefix, num++); //$NON-NLS-1$ |
| } while (ids.contains(generated.toLowerCase(Locale.US))); |
| |
| return generated; |
| } |
| |
| /** |
| * Returns the element children of the given element |
| * |
| * @param element the parent element |
| * @return a list of child elements, possibly empty but never null |
| */ |
| @NonNull |
| public static List<Element> getChildren(@NonNull Element element) { |
| // Convenience to avoid lots of ugly DOM access casting |
| NodeList children = element.getChildNodes(); |
| // An iterator would have been more natural (to directly drive the child list |
| // iteration) but iterators can't be used in enhanced for loops... |
| List<Element> result = new ArrayList<Element>(children.getLength()); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node node = children.item(i); |
| if (node.getNodeType() == Node.ELEMENT_NODE) { |
| Element child = (Element) node; |
| result.add(child); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Returns true iff the given elements are contiguous siblings |
| * |
| * @param elements the elements to be tested |
| * @return true if the elements are contiguous siblings with no gaps |
| */ |
| public static boolean isContiguous(@NonNull List<Element> elements) { |
| if (elements.size() > 1) { |
| // All elements must be siblings (e.g. same parent) |
| Node parent = elements.get(0).getParentNode(); |
| if (!(parent instanceof Element)) { |
| return false; |
| } |
| for (Element node : elements) { |
| if (parent != node.getParentNode()) { |
| return false; |
| } |
| } |
| |
| // Ensure that the siblings are contiguous; no gaps. |
| // If we've selected all the children of the parent then we don't need |
| // to look. |
| List<Element> siblings = DomUtilities.getChildren((Element) parent); |
| if (siblings.size() != elements.size()) { |
| Set<Element> nodeSet = new HashSet<Element>(elements); |
| boolean inRange = false; |
| int remaining = elements.size(); |
| for (Element node : siblings) { |
| boolean in = nodeSet.contains(node); |
| if (in) { |
| remaining--; |
| if (remaining == 0) { |
| break; |
| } |
| inRange = true; |
| } else if (inRange) { |
| return false; |
| } |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Determines whether two element trees are equivalent. Two element trees are |
| * equivalent if they represent the same DOM structure (elements, attributes, and |
| * children in order). This is almost the same as simply checking whether the String |
| * representations of the two nodes are identical, but this allows for minor |
| * variations that are not semantically significant, such as variations in formatting |
| * or ordering of the element attribute declarations, and the text children are |
| * ignored (this is such that in for example layout where content is only used for |
| * indentation the indentation differences are ignored). Null trees are never equal. |
| * |
| * @param element1 the first element to compare |
| * @param element2 the second element to compare |
| * @return true if the two element hierarchies are logically equal |
| */ |
| public static boolean isEquivalent(@Nullable Element element1, @Nullable Element element2) { |
| if (element1 == null || element2 == null) { |
| return false; |
| } |
| |
| if (!element1.getTagName().equals(element2.getTagName())) { |
| return false; |
| } |
| |
| // Check attribute map |
| NamedNodeMap attributes1 = element1.getAttributes(); |
| NamedNodeMap attributes2 = element2.getAttributes(); |
| |
| List<Attr> attributeNodes1 = new ArrayList<Attr>(); |
| for (int i = 0, n = attributes1.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributes1.item(i); |
| // Ignore tools uri namespace attributes for equivalency test |
| if (TOOLS_URI.equals(attribute.getNamespaceURI())) { |
| continue; |
| } |
| attributeNodes1.add(attribute); |
| } |
| List<Attr> attributeNodes2 = new ArrayList<Attr>(); |
| for (int i = 0, n = attributes2.getLength(); i < n; i++) { |
| Attr attribute = (Attr) attributes2.item(i); |
| // Ignore tools uri namespace attributes for equivalency test |
| if (TOOLS_URI.equals(attribute.getNamespaceURI())) { |
| continue; |
| } |
| attributeNodes2.add(attribute); |
| } |
| |
| if (attributeNodes1.size() != attributeNodes2.size()) { |
| return false; |
| } |
| |
| if (attributes1.getLength() > 0) { |
| Collections.sort(attributeNodes1, ATTRIBUTE_COMPARATOR); |
| Collections.sort(attributeNodes2, ATTRIBUTE_COMPARATOR); |
| for (int i = 0; i < attributeNodes1.size(); i++) { |
| Attr attr1 = attributeNodes1.get(i); |
| Attr attr2 = attributeNodes2.get(i); |
| if (attr1.getLocalName() == null || attr2.getLocalName() == null) { |
| if (!attr1.getName().equals(attr2.getName())) { |
| return false; |
| } |
| } else if (!attr1.getLocalName().equals(attr2.getLocalName())) { |
| return false; |
| } |
| if (!attr1.getValue().equals(attr2.getValue())) { |
| return false; |
| } |
| if (attr1.getNamespaceURI() == null) { |
| if (attr2.getNamespaceURI() != null) { |
| return false; |
| } |
| } else if (attr2.getNamespaceURI() == null) { |
| return false; |
| } else if (!attr1.getNamespaceURI().equals(attr2.getNamespaceURI())) { |
| return false; |
| } |
| } |
| } |
| |
| NodeList children1 = element1.getChildNodes(); |
| NodeList children2 = element2.getChildNodes(); |
| int nextIndex1 = 0; |
| int nextIndex2 = 0; |
| while (true) { |
| while (nextIndex1 < children1.getLength() && |
| children1.item(nextIndex1).getNodeType() != Node.ELEMENT_NODE) { |
| nextIndex1++; |
| } |
| |
| while (nextIndex2 < children2.getLength() && |
| children2.item(nextIndex2).getNodeType() != Node.ELEMENT_NODE) { |
| nextIndex2++; |
| } |
| |
| Element nextElement1 = (Element) (nextIndex1 < children1.getLength() |
| ? children1.item(nextIndex1) : null); |
| Element nextElement2 = (Element) (nextIndex2 < children2.getLength() |
| ? children2.item(nextIndex2) : null); |
| if (nextElement1 == null) { |
| return nextElement2 == null; |
| } else if (nextElement2 == null) { |
| return false; |
| } else if (!isEquivalent(nextElement1, nextElement2)) { |
| return false; |
| } |
| nextIndex1++; |
| nextIndex2++; |
| } |
| } |
| |
| /** |
| * Finds the corresponding element in a document to a given element in another |
| * document. Note that this does <b>not</b> do any kind of equivalence check |
| * (see {@link #isEquivalent(Element, Element)}), and currently the search |
| * is only by id; there is no structural search. |
| * |
| * @param element the element to find an equivalent for |
| * @param document the document to search for an equivalent element in |
| * @return an equivalent element, or null |
| */ |
| @Nullable |
| public static Element findCorresponding(@NonNull Element element, @NonNull Document document) { |
| // Make sure the method is called correctly -- the element is for a different |
| // document than the one we are searching |
| assert element.getOwnerDocument() != document; |
| |
| // First search by id. This allows us to find the corresponding |
| String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); |
| if (id != null && id.length() > 0) { |
| if (id.startsWith(ID_PREFIX)) { |
| id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length()); |
| } |
| |
| return findCorresponding(document.getDocumentElement(), id); |
| } |
| |
| // TODO: Search by structure - look in the document and |
| // find a corresponding element in the same location in the structure, |
| // e.g. 4th child of root, 3rd child, 6th child, then pick node with tag "foo". |
| |
| return null; |
| } |
| |
| /** Helper method for {@link #findCorresponding(Element, Document)} */ |
| @Nullable |
| private static Element findCorresponding(@NonNull Element element, @NonNull String targetId) { |
| String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); |
| if (id != null) { // Work around DOM bug |
| if (id.equals(targetId)) { |
| return element; |
| } else if (id.startsWith(ID_PREFIX)) { |
| id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length()); |
| if (id.equals(targetId)) { |
| return element; |
| } |
| } |
| } |
| |
| NodeList children = element.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node node = children.item(i); |
| if (node.getNodeType() == Node.ELEMENT_NODE) { |
| Element child = (Element) node; |
| Element match = findCorresponding(child, targetId); |
| if (match != null) { |
| return match; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Parses the given XML string as a DOM document, using Eclipse's structured |
| * XML model (which for example allows us to distinguish empty elements |
| * (<foo/>) from elements with no children (<foo></foo>). |
| * |
| * @param xml the XML content to be parsed (must be well formed) |
| * @return the DOM document, or null |
| */ |
| @Nullable |
| public static Document parseStructuredDocument(@NonNull String xml) { |
| IStructuredModel model = createStructuredModel(xml); |
| if (model instanceof IDOMModel) { |
| IDOMModel domModel = (IDOMModel) model; |
| return domModel.getDocument(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Parses the given XML string and builds an Eclipse structured model for it. |
| * |
| * @param xml the XML content to be parsed (must be well formed) |
| * @return the structured model |
| */ |
| @Nullable |
| public static IStructuredModel createStructuredModel(@NonNull String xml) { |
| IStructuredModel model = createEmptyModel(); |
| IStructuredDocument document = model.getStructuredDocument(); |
| model.aboutToChangeModel(); |
| document.set(xml); |
| model.changedModel(); |
| |
| return model; |
| } |
| |
| /** |
| * Creates an empty Eclipse XML model |
| * |
| * @return a new Eclipse XML model |
| */ |
| @NonNull |
| public static IStructuredModel createEmptyModel() { |
| IModelManager modelManager = StructuredModelManager.getModelManager(); |
| return modelManager.createUnManagedStructuredModelFor(ContentTypeID_XML); |
| } |
| |
| /** |
| * Creates an empty Eclipse XML document |
| * |
| * @return an empty Eclipse XML document |
| */ |
| @Nullable |
| public static Document createEmptyDocument() { |
| IStructuredModel model = createEmptyModel(); |
| if (model instanceof IDOMModel) { |
| IDOMModel domModel = (IDOMModel) model; |
| return domModel.getDocument(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Creates an empty non-Eclipse XML document. |
| * This is used when you need to use XML operations not supported by |
| * the Eclipse XML model (such as serialization). |
| * <p> |
| * The new document will not validate, will ignore comments, and will |
| * support namespace. |
| * |
| * @return the new document |
| */ |
| @Nullable |
| public static Document createEmptyPlainDocument() { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| factory.setValidating(false); |
| factory.setIgnoringComments(true); |
| DocumentBuilder builder; |
| try { |
| builder = factory.newDocumentBuilder(); |
| return builder.newDocument(); |
| } catch (ParserConfigurationException e) { |
| AdtPlugin.log(e, null); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Parses the given XML string as a DOM document, using the JDK parser. |
| * The parser does not validate, and is namespace aware. |
| * |
| * @param xml the XML content to be parsed (must be well formed) |
| * @param logParserErrors if true, log parser errors to the log, otherwise |
| * silently return null |
| * @return the DOM document, or null |
| */ |
| @Nullable |
| public static Document parseDocument(@NonNull String xml, boolean logParserErrors) { |
| DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| InputSource is = new InputSource(new StringReader(xml)); |
| factory.setNamespaceAware(true); |
| factory.setValidating(false); |
| try { |
| DocumentBuilder builder = factory.newDocumentBuilder(); |
| return builder.parse(is); |
| } catch (Exception e) { |
| if (logParserErrors) { |
| AdtPlugin.log(e, null); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Can be used to sort attributes by name */ |
| private static final Comparator<Attr> ATTRIBUTE_COMPARATOR = new Comparator<Attr>() { |
| @Override |
| public int compare(Attr a1, Attr a2) { |
| return a1.getName().compareTo(a2.getName()); |
| } |
| }; |
| } |