diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java | 915 |
1 files changed, 915 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java new file mode 100644 index 000000000..145036bf3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java @@ -0,0 +1,915 @@ +/* + * 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()); + } + }; +} |