diff --git a/openide.util/apichanges.xml b/openide.util/apichanges.xml --- a/openide.util/apichanges.xml +++ b/openide.util/apichanges.xml @@ -1457,6 +1457,22 @@ + + + Refactored XML methods from various modules to be added + to XMLUtil. Six new methods added. + + + + + +

+ Refactored XML related methods into XMLUtil. +

+
+ + +
diff --git a/openide.util/manifest.mf b/openide.util/manifest.mf --- a/openide.util/manifest.mf +++ b/openide.util/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.openide.util OpenIDE-Module-Localizing-Bundle: org/openide/util/Bundle.properties -OpenIDE-Module-Specification-Version: 8.2 +OpenIDE-Module-Specification-Version: 8.3 diff --git a/openide.util/src/org/openide/xml/XMLUtil.java b/openide.util/src/org/openide/xml/XMLUtil.java --- a/openide.util/src/org/openide/xml/XMLUtil.java +++ b/openide.util/src/org/openide/xml/XMLUtil.java @@ -45,8 +45,11 @@ import java.io.IOException; import java.io.OutputStream; import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.parsers.DocumentBuilder; @@ -842,4 +845,213 @@ } return doc; } + + /** + * Append a child element to the parent at the specified location. + * + *
    The parent document must be + *
  • a valid document
  • + *
  • order must contain all direct children
  • + *
+ * + * + * The implementation is 'insert before', and since the order of existing elements + * is not validated, it is important to place the new child before + * the existing item instead of after the existing item. + * + * @param parent parent to which the child will be appended + * @param el element to be added + * @param order order of the elements which must be followed + * @throws IllegalArgumentException if the order cannot be followed, either + * a missing existing child or missing new child entry from the desiredAppendOrder + * + * @since 8.3 + */ + public static void appendChildElement(Element parent, Element el, String[] order) throws IllegalArgumentException { + List l = Arrays.asList(order); + int index = l.indexOf(el.getLocalName()); + + // ensure the new new element is contained in the 'order' + if (index == -1) { + throw new IllegalArgumentException("new child element '"+ el.getLocalName() + "' not specified in order " + l); // NOI18N + } + + List elements = findSubElements(parent); + Element insertBefore = null; + + for (Element e : elements) { + int index2 = l.indexOf(e.getLocalName()); + // ensure that all existing elements are in 'order' + if (index2 == -1) { + throw new IllegalArgumentException("Existing child element '" + e.getLocalName() + "' not specified in order " + l); // NOI18N + } + if (index2 > index) { + insertBefore = e; + break; + } + } + + parent.insertBefore(el, insertBefore); + } + + /** + * Find all direct child elements of an element. + * Children which are all-whitespace text nodes or comments are ignored; others cause + * an exception to be thrown. + * @param parent a parent element in a DOM tree + * @return a list of direct child elements (may be empty) + * @throws IllegalArgumentException if there are non-element children besides whitespace + * + * @since 8.3 + */ + public static List findSubElements(Element parent) throws IllegalArgumentException { + NodeList l = parent.getChildNodes(); + List elements = new ArrayList(l.getLength()); + for (int i = 0; i < l.getLength(); i++) { + Node n = l.item(i); + if (n.getNodeType() == Node.ELEMENT_NODE) { + elements.add((Element) n); + } else if (n.getNodeType() == Node.TEXT_NODE) { + String text = ((Text) n).getNodeValue(); + if (text.trim().length() > 0) { + throw new IllegalArgumentException("non-ws text encountered in " + parent + ": " + text); // NOI18N + } + } else if (n.getNodeType() == Node.COMMENT_NODE) { + // OK, ignore + } else { + throw new IllegalArgumentException("unexpected non-element child of " + parent + ": " + n); // NOI18N + } + } + return elements; + } + + /** + * Search for an XML element in the direct children of parent only. + * + * This compares localName (uses nodeName is localName is null) to name, + * and checks the tags namespace with the provided namespace. + * A null namespace will match any namespace. + * + *
    This is differs from the DOM version by: + *
  • not searching recursively
  • + *
  • returns a single result
  • + *
+ * + * @param parent a parent element + * @param name the intended local name + * @param namespace the intended namespace (or null) + * @return the one child element with that name, or null if none + * @throws IllegalArgumentException if there is multiple elements of the same name + * + * @since 8.3 + */ + public static Element findElement(Element parent, String name, String namespace) throws IllegalArgumentException { + Element result = null; + NodeList l = parent.getChildNodes(); + int nodeCount = l.getLength(); + for (int i = 0; i < nodeCount; i++) { + if (l.item(i).getNodeType() == Node.ELEMENT_NODE) { + Node node = l.item(i); + String localName = node.getLocalName(); + localName = localName == null ? node.getNodeName() : localName; + + if (name.equals(localName) + && (namespace == null || namespace.equals(node.getNamespaceURI()))) { + if (result == null) { + result = (Element)node; + } else { + throw new IllegalArgumentException("more than one element with same name found"); + } + } + } + } + return result; + } + + /** + * Extract nested text from a node. + * Currently does not handle coalescing text nodes, CDATA sections, etc. + * @param parent a parent element + * @return the nested text, or null if none was found + * + * @since 8.3 + */ + public static String findText(Node parent) { + NodeList l = parent.getChildNodes(); + for (int i = 0; i < l.getLength(); i++) { + if (l.item(i).getNodeType() == Node.TEXT_NODE) { + Text text = (Text) l.item(i); + return text.getNodeValue(); + } + } + return null; + } + + /** + * Convert an XML fragment from one namespace to another. + * + * @param from + * @param namespace + * @return + * + * @since 8.3 + */ + public static Element translateXML(Element from, String namespace) { + Element to = from.getOwnerDocument().createElementNS(namespace, from.getLocalName()); + NodeList nl = from.getChildNodes(); + int length = nl.getLength(); + for (int i = 0; i < length; i++) { + Node node = nl.item(i); + Node newNode; + if (node.getNodeType() == Node.ELEMENT_NODE) { + newNode = translateXML((Element) node, namespace); + } else { + newNode = node.cloneNode(true); + } + to.appendChild(newNode); + } + NamedNodeMap m = from.getAttributes(); + for (int i = 0; i < m.getLength(); i++) { + Node attr = m.item(i); + to.setAttribute(attr.getNodeName(), attr.getNodeValue()); + } + return to; + } + + /** + * Copy elements from one document to another attaching at the specified element + * and translating the namespace. + * + * @param from copy the children of this element (exclusive) + * @param to where to attach the copied elements + * @param newNamespace destination namespace + * + * @since 8.3 + */ + public static void copyDocument(Element from, Element to, String newNamespace) { + Document doc = to.getOwnerDocument(); + NodeList nl = from.getChildNodes(); + int length = nl.getLength(); + for (int i = 0; i < length; i++) { + Node node = nl.item(i); + Node newNode = null; + if (Node.ELEMENT_NODE == node.getNodeType()) { + Element oldElement = (Element) node; + newNode = doc.createElementNS(newNamespace, oldElement.getTagName()); + NamedNodeMap m = oldElement.getAttributes(); + Element newElement = (Element) newNode; + for (int index = 0; index < m.getLength(); index++) { + Node attr = m.item(index); + newElement.setAttribute(attr.getNodeName(), attr.getNodeValue()); + } + copyDocument(oldElement, newElement, newNamespace); + } else { + newNode = node.cloneNode(true); + newNode = to.getOwnerDocument().importNode(newNode, true); + } + if (newNode != null) { + to.appendChild(newNode); + } + } + } } diff --git a/openide.util/test/unit/src/org/openide/xml/XMLUtilTest.java b/openide.util/test/unit/src/org/openide/xml/XMLUtilTest.java --- a/openide.util/test/unit/src/org/openide/xml/XMLUtilTest.java +++ b/openide.util/test/unit/src/org/openide/xml/XMLUtilTest.java @@ -49,6 +49,7 @@ import java.io.StringReader; import java.lang.ref.Reference; import java.lang.ref.WeakReference; +import java.util.List; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.stream.StreamSource; @@ -497,4 +498,210 @@ assertGC("can collect resolver", resolverRef); } + public void testAppendChildElement() throws Exception { + Document doc = XMLUtil.parse(new InputSource(new StringReader("")), false, true, null, null); + Element parent = doc.createElementNS("unittest", "parent"); + Element newElement1 = doc.createElementNS("unittest", "new_element1"); + Element newElement2 = doc.createElementNS("unittest", "new_element2"); + Element newElement3 = doc.createElementNS("unittest", "new_element3"); + + // append to the default root node (no match in order) + XMLUtil.appendChildElement(parent, newElement1, new String[] { "new_element2", "new_element1" }); + NodeList children = parent.getChildNodes(); + assertEquals(1, children.getLength()); + + // append after the child we just appended + XMLUtil.appendChildElement(parent, newElement2, new String[] { "new_element2", "new_element1" }); + + children = parent.getChildNodes(); + assertEquals(2, children.getLength()); + Node firstChild = parent.getChildNodes().item(0); + Node secondChild = parent.getChildNodes().item(1); + assertEquals("new_element2", firstChild.getNodeName()); + assertEquals("new_element1", secondChild.getNodeName()); + + // failures + + try { + // new child is not in the order list + XMLUtil.appendChildElement(parent, newElement3, new String[] { "new_element2", "new_element1"}); + fail("Expecting IAE"); + } catch (IllegalArgumentException e) { + assertEquals("new child element 'new_element3' not specified in order [new_element2, new_element1]", e.getMessage()); + } + try { + // existing child not in the order list + XMLUtil.appendChildElement(parent, newElement3, new String[] { "new_element3"}); + fail("Expecting IAE"); + } catch (IllegalArgumentException e) { + assertEquals("Existing child element 'new_element2' not specified in order [new_element3]", e.getMessage()); + } + } + + public void testFindSubElements() throws Exception { + Document doc = XMLUtil.parse(new InputSource(new StringReader(" ")), + false, true, null, null); + + Element parent = doc.getDocumentElement(); + assertEquals(5, parent.getChildNodes().getLength()); + List subElements = XMLUtil.findSubElements(parent); + assertEquals(3, subElements.size()); + Element firstChild = subElements.get(0); + Element secondChild = subElements.get(1); + Element thirdChild = subElements.get(2); + + assertEquals("child1", firstChild.getNodeName()); + assertEquals("child2", secondChild.getNodeName()); + assertEquals("child3", thirdChild.getNodeName()); + + Document failureDoc = XMLUtil.parse(new InputSource(new StringReader("Non whitespace")), + false, true, null, null); + Element failedParent = failureDoc.getDocumentElement(); + try { + XMLUtil.findSubElements(failedParent); + fail("expected IAE"); + } catch (IllegalArgumentException e) { } + } + + public void testFindElement() throws Exception { + String xmlDoc = " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + + Document doc = XMLUtil.parse(new InputSource(new StringReader(xmlDoc)), false, true, null, null); + Element parent = doc.getDocumentElement(); + + Element doesNotExist1 = XMLUtil.findElement(parent, "doesNotExist", "h"); + assertNull(doesNotExist1); + + Element doesNotExist2 = XMLUtil.findElement(parent, "doesNotExist", null); + assertNull(doesNotExist2); + + Element noTable2 = XMLUtil.findElement(parent, "table", "h"); + assertNull(noTable2); + + Element noTable3 = XMLUtil.findElement(parent, "table", "f"); + assertNull(noTable3); + + Element table1 = XMLUtil.findElement(parent, "table", "http://www.w3.org/TR/html4/"); + assertNotNull(table1); + + Element table2 = XMLUtil.findElement(parent, "table", "http://www.w3schools.com/furniture"); + assertNotNull(table2); + + Element form1 = XMLUtil.findElement(parent, "form", "http://www.w3.org/TR/html4/"); + assertNotNull(form1); + + Element form2 = XMLUtil.findElement(parent, "form", null); + assertNotNull(form2); + + assertEquals(form1, form2); + + try { + XMLUtil.findElement(parent, "dup", null); + fail("Expected IAE"); + } catch (IllegalArgumentException e) { } + + try { + XMLUtil.findElement(parent, "table", null); + fail("Expected IAE"); + } catch (IllegalArgumentException e) { } + } + + public void testFindText() throws Exception { + Document doc = XMLUtil.parse(new InputSource(new StringReader("Text To Find")), + false, true, null, null); + + Element parent = doc.getDocumentElement(); + String foundText = XMLUtil.findText(parent); + + assertEquals("Text To Find", foundText); + + String notFoundText = XMLUtil.findText(parent.getFirstChild()); + + assertNull(notFoundText); + } + + public void testTranslateXML() throws Exception { + // don't add any whitespace to the first 3 nodes + String xmlDoc = "" + + " " + + " " + + " " + + "
ApplesBananas
" + + " " + + "" + + "
"; + + Document doc = XMLUtil.parse(new InputSource(new StringReader(xmlDoc)), false, true, null, null); + Element parent = doc.getDocumentElement(); + + Element translated = XMLUtil.translateXML((Element) parent.getFirstChild(), "http://www.w3.org/TR/html4/"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLUtil.write(doc, baos, "UTF-8"); + + Document destDoc = XMLUtil.parse(new InputSource(new StringReader("")), false, true, null, null); + + destDoc.importNode(translated, true); + baos = new ByteArrayOutputStream(); + XMLUtil.write(destDoc, baos, "UTF-8"); + + assertEquals("http://www.w3.org/TR/html4/", translated.getNamespaceURI()); + assertEquals(1, translated.getAttributes().getLength()); + + assertEquals("http://www.w3.org/TR/html4/", translated.getFirstChild().getNamespaceURI()); + assertEquals(0, translated.getFirstChild().getAttributes().getLength()); + } + + public void testCopyDocument() throws Exception { + String srcXml = "" + + "" + + " " + + " " + + " " + + " " + + " " + + "
ApplesBananas
" + + " " + + "
"; + + String resultXml = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
ApplesBananas
\n" + + " \n" + + "
\n"; + + Document srcDoc = XMLUtil.parse(new InputSource(new StringReader(srcXml)), false, true, null, null); + Element srcRoot = srcDoc.getDocumentElement(); + + Document dstDoc = XMLUtil.parse(new InputSource(new StringReader("")), false, true, null, null); + + Element dstRoot = dstDoc.getDocumentElement(); + XMLUtil.copyDocument(srcRoot, dstRoot, "http://www.w3.org/TR/html4/"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLUtil.write(dstDoc, baos, "UTF-8"); + assertEquals(resultXml, baos.toString()); + + assertEquals(1, dstDoc.getChildNodes().getLength()); + Element root = dstDoc.getDocumentElement(); + Element tableNode = (Element) root.getFirstChild().getNextSibling(); + + assertEquals("table", tableNode.getNodeName()); + assertEquals("http://www.w3.org/TR/html4/", tableNode.getNamespaceURI()); + assertEquals(1, tableNode.getAttributes().getLength()); + } + }