/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2009-12 Ben Fry and Casey Reas This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 2. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.data; import java.io.*; import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; import processing.core.PApplet; /** * This is the base class used for the Processing XML library, * representing a single node of an XML tree. */ public class XML implements Serializable { /** The internal representation, a DOM node. */ protected Node node; /** Cached locally because it's used often. */ protected String name; /** The parent element. */ protected XML parent; /** Child elements, once loaded. */ protected XML[] children; protected XML() { } /** * Begin parsing XML data passed in from a PApplet. This code * wraps exception handling, for more advanced exception handling, * use the constructor that takes a Reader or InputStream. */ public XML(PApplet parent, String filename) { this(parent.createReader(filename)); } public XML(Reader reader) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // Prevent 503 errors from www.w3.org factory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); // without a validating DTD, this doesn't do anything since it doesn't know what is ignorable // factory.setIgnoringElementContentWhitespace(true); factory.setExpandEntityReferences(false); // factory.setExpandEntityReferences(true); // factory.setCoalescing(true); // builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder builder = factory.newDocumentBuilder(); // builder.setEntityResolver() // SAXParserFactory spf = SAXParserFactory.newInstance(); // spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // SAXParser p = spf.newSAXParser(); // builder = DocumentBuilderFactory.newDocumentBuilder(); // builder = new SAXBuilder(); // builder.setValidation(validating); // print(dataPath("1broke.html"), System.out); // Document document = builder.parse(dataPath("1_alt.html")); Document document = builder.parse(new InputSource(reader)); node = document.getDocumentElement(); name = node.getNodeName(); // NodeList nodeList = document.getDocumentElement().getChildNodes(); // for (int i = 0; i < nodeList.getLength(); i++) { // } // print(createWriter("data/1_alt_reparse.html"), document.getDocumentElement(), 0); } catch (ParserConfigurationException pce) { pce.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); } catch (SAXException e2) { e2.printStackTrace(); } } // TODO is there a more efficient way of doing this? wow. // i.e. can we use one static document object for all PNodeXML objects? public XML(String name) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); node = document.createElement(name); this.name = name; this.parent = null; } catch (ParserConfigurationException e) { e.printStackTrace(); } } // public PNodeXML(String name, PNode parent) { // PNodeXML pxml = PNodeXML.parse("<" + name + ">"); // this.node = pxml.node; // this.name = name; // this.parent = parent; // } protected XML(XML parent, Node node) { this.node = node; this.parent = parent; this.name = node.getNodeName(); } static public XML parse(String xml) { return new XML(new StringReader(xml)); } public boolean save(OutputStream output) { return save(PApplet.createWriter(output)); } public boolean save(File file) { return save(PApplet.createWriter(file)); } public boolean save(PrintWriter output) { output.print(toString(2)); output.flush(); return true; } /** * Returns the parent element. This method returns null for the root * element. */ public XML getParent() { return this.parent; } protected Node getNode() { return node; } /** * Returns the full name (i.e. the name including an eventual namespace * prefix) of the element. * @return the name, or null if the element only contains #PCDATA. */ public String getName() { return name; } public void setName(String newName) { Document document = node.getOwnerDocument(); node = document.renameNode(node, null, newName); name = node.getNodeName(); } /** * Returns the name of the element (without namespace prefix). * @return the name, or null if the element only contains #PCDATA. */ public String getLocalName() { return node.getLocalName(); } /** * Honey, can you just check on the kids? Thanks. */ protected void checkChildren() { if (children == null) { NodeList kids = node.getChildNodes(); int childCount = kids.getLength(); children = new XML[childCount]; for (int i = 0; i < childCount; i++) { children[i] = new XML(this, kids.item(i)); } } } /** * Returns the number of children. * @return the count. */ public int getChildCount() { checkChildren(); return children.length; } /** * Put the names of all children into an array. Same as looping through * each child and calling getName() on each XMLElement. */ public String[] listChildren() { // NodeList children = node.getChildNodes(); // int childCount = children.getLength(); // String[] outgoing = new String[childCount]; // for (int i = 0; i < childCount; i++) { // Node kid = children.item(i); // if (kid.getNodeType() == Node.ELEMENT_NODE) { // outgoing[i] = kid.getNodeName(); // } // otherwise just leave him null // } checkChildren(); String[] outgoing = new String[children.length]; for (int i = 0; i < children.length; i++) { outgoing[i] = children[i].getName(); } return outgoing; } /** * Returns an array containing all the child elements. */ public XML[] getChildren() { // NodeList children = node.getChildNodes(); // int childCount = children.getLength(); // XMLElement[] kids = new XMLElement[childCount]; // for (int i = 0; i < childCount; i++) { // Node kid = children.item(i); // kids[i] = new XMLElement(this, kid); // } // return kids; checkChildren(); return children; } /** * Quick accessor for an element at a particular index. * @author processing.org */ public XML getChild(int index) { checkChildren(); return children[index]; } /** * Get a child by its name or path. * @param name element name or path/to/element * @return the first matching element */ public XML getChild(String name) { if (name.indexOf('/') != -1) { return getChildRecursive(PApplet.split(name, '/'), 0); } int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { XML kid = getChild(i); String kidName = kid.getName(); if (kidName != null && kidName.equals(name)) { return kid; } } return null; } /** * Internal helper function for getChild(String). * @param items result of splitting the query on slashes * @param offset where in the items[] array we're currently looking * @return matching element or null if no match * @author processing.org */ protected XML getChildRecursive(String[] items, int offset) { // if it's a number, do an index instead if (Character.isDigit(items[offset].charAt(0))) { XML kid = getChild(Integer.parseInt(items[offset])); if (offset == items.length-1) { return kid; } else { return kid.getChildRecursive(items, offset+1); } } int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { XML kid = getChild(i); String kidName = kid.getName(); if (kidName != null && kidName.equals(items[offset])) { if (offset == items.length-1) { return kid; } else { return kid.getChildRecursive(items, offset+1); } } } return null; } /** * Get any children that match this name or path. Similar to getChild(), * but will grab multiple matches rather than only the first. * @param name element name or path/to/element * @return array of child elements that match * @author processing.org */ public XML[] getChildren(String name) { if (name.indexOf('/') != -1) { return getChildrenRecursive(PApplet.split(name, '/'), 0); } // if it's a number, do an index instead // (returns a single element array, since this will be a single match if (Character.isDigit(name.charAt(0))) { return new XML[] { getChild(Integer.parseInt(name)) }; } int childCount = getChildCount(); XML[] matches = new XML[childCount]; int matchCount = 0; for (int i = 0; i < childCount; i++) { XML kid = getChild(i); String kidName = kid.getName(); if (kidName != null && kidName.equals(name)) { matches[matchCount++] = kid; } } return (XML[]) PApplet.subset(matches, 0, matchCount); } protected XML[] getChildrenRecursive(String[] items, int offset) { if (offset == items.length-1) { return getChildren(items[offset]); } XML[] matches = getChildren(items[offset]); XML[] outgoing = new XML[0]; for (int i = 0; i < matches.length; i++) { XML[] kidMatches = matches[i].getChildrenRecursive(items, offset+1); outgoing = (XML[]) PApplet.concat(outgoing, kidMatches); } return outgoing; } public XML addChild(String tag) { Document document = node.getOwnerDocument(); Node newChild = document.createElement(tag); return appendChild(newChild); } public XML addChild(XML child) { Document document = node.getOwnerDocument(); Node newChild = document.importNode(child.getNode(), true); return appendChild(newChild); } /** Internal handler to add the node structure. */ protected XML appendChild(Node newNode) { node.appendChild(newNode); XML newbie = new XML(this, newNode); if (children != null) { children = (XML[]) PApplet.concat(children, new XML[] { newbie }); } return newbie; } public void removeChild(XML kid) { node.removeChild(kid.node); children = null; // TODO not efficient } /** * Returns the number of attributes. */ public int getAttributeCount() { return node.getAttributes().getLength(); } /** * Get a list of the names for all of the attributes for this node. */ public String[] listAttributes() { NamedNodeMap nnm = node.getAttributes(); String[] outgoing = new String[nnm.getLength()]; for (int i = 0; i < outgoing.length; i++) { outgoing[i] = nnm.item(i).getNodeName(); } return outgoing; } /** * Returns whether an attribute exists. */ public boolean hasAttribute(String name) { return (node.getAttributes().getNamedItem(name) != null); } /** * Returns the value of an attribute. * @param name the non-null name of the attribute. * @return the value, or null if the attribute does not exist. */ // public String getAttribute(String name) { // return this.getAttribute(name, null); // } /** * Returns the value of an attribute. * * @param name the non-null full name of the attribute. * @param defaultValue the default value of the attribute. * * @return the value, or defaultValue if the attribute does not exist. */ // public String getAttribute(String name, String defaultValue) { // Node attr = node.getAttributes().getNamedItem(name); // return (attr == null) ? defaultValue : attr.getNodeValue(); // } public String getString(String name) { return getString(name, null); } public String getString(String name, String defaultValue) { Node attr = node.getAttributes().getNamedItem(name); return (attr == null) ? defaultValue : attr.getNodeValue(); } public void setString(String name, String value) { ((Element) node).setAttribute(name, value); } public int getInt(String name) { return getInt(name, 0); } public void setInt(String name, int value) { setString(name, String.valueOf(value)); } /** * Returns the value of an attribute. * * @param name the non-null full name of the attribute. * @param defaultValue the default value of the attribute. * * @return the value, or defaultValue if the attribute does not exist. */ public int getInt(String name, int defaultValue) { String value = getString(name); return (value == null) ? defaultValue : Integer.parseInt(value); } /** * Returns the value of an attribute, or zero if not present. */ public float getFloat(String name) { return getFloat(name, 0); } /** * Returns the value of an attribute. * * @param name the non-null full name of the attribute. * @param defaultValue the default value of the attribute. * * @return the value, or defaultValue if the attribute does not exist. */ public float getFloat(String name, float defaultValue) { String value = getString(name); return (value == null) ? defaultValue : Float.parseFloat(value); } public void setFloat(String name, float value) { setString(name, String.valueOf(value)); } public double getDouble(String name) { return getDouble(name, 0); } /** * Returns the value of an attribute. * * @param name the non-null full name of the attribute. * @param defaultValue the default value of the attribute. * * @return the value, or defaultValue if the attribute does not exist. */ public double getDouble(String name, double defaultValue) { String value = getString(name); return (value == null) ? defaultValue : Double.parseDouble(value); } public void setDouble(String name, double value) { setString(name, String.valueOf(value)); } /** * Return the #PCDATA content of the element. If the element has a * combination of #PCDATA content and child elements, the #PCDATA * sections can be retrieved as unnamed child objects. In this case, * this method returns null. * * @return the content. */ public String getContent() { return node.getTextContent(); } public void setContent(String text) { node.setTextContent(text); } public String toString() { return toString(2); } public String toString(int indent) { try { // node.normalize(); // does nothing useful DOMSource dumSource = new DOMSource(node); // entities = doctype.getEntities() TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); // if this is the root, output the decl, if not, hide it if (parent != null) { transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); // } else { // transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); } // transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "sample.dtd"); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); // transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "yes"); // huh? // transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, // "-//W3C//DTD XHTML 1.0 Transitional//EN"); // transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, // "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"); // transformer.setOutputProperty(OutputKeys.ENCODING,"ISO-8859-1"); // transformer.setOutputProperty(OutputKeys.ENCODING,"UTF8"); transformer.setOutputProperty(OutputKeys.ENCODING,"UTF-8"); // transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS // indent by default, but sometimes this needs to be turned off if (indent != 0) { transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent)); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); } // Properties p = transformer.getOutputProperties(); // for (Object key : p.keySet()) { // System.out.println(key + " -> " + p.get(key)); // } StringWriter sw = new StringWriter(); StreamResult sr = new StreamResult(sw); transformer.transform(dumSource, sr); return sw.toString(); // Document document = node.getOwnerDocument(); // OutputFormat format = new OutputFormat(document); // format.setLineWidth(65); // format.setIndenting(true); // format.setIndent(2); // StringWriter sw = new StringWriter(); // XMLSerializer serializer = new XMLSerializer(sw, format); // serializer.serialize(document); // return sw.toString(); } catch (Exception e) { e.printStackTrace(); } return null; // DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // try { // DocumentBuilder builder = factory.newDocumentBuilder(); // //builder.get //// Document document = builder. // // } catch (ParserConfigurationException e) { // e.printStackTrace(); // } // Document doc = new DocumentImpl(); // return node.toString(); // TransformerFactory transfac = TransformerFactory.newInstance(); // Transformer trans = transfac.newTransformer(); // trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); // trans.setOutputProperty(OutputKeys.INDENT, "yes"); // // //create string from xml tree // StringWriter sw = new StringWriter(); // StreamResult result = new StreamResult(sw); //// Document doc = // DOMSource source = new DOMSource(doc); // trans.transform(source, result); // String xmlString = sw.toString(); } // static final String HEADER = ""; // // public void write(PrintWriter writer) { // writer.println(HEADER); // writer.print(toString(2)); // } }