/* * This file is part of the Weborganic XMLDoclet library. * * For licensing information please see the file license.txt included in the release. * A copy of this licence can also be found at * http://www.opensource.org/licenses/artistic-license-2.0.php */ package org.weborganic.xmldoclet; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.w3c.tidy.Tidy; import org.w3c.tidy.TidyMessage; import org.w3c.tidy.TidyMessage.Level; import org.w3c.tidy.TidyMessageListener; import com.sun.javadoc.Doc; /** * Represents an XML node. * * @author Christophe Lauret * @author Johan Johansson * @version 24 May 2013 */ public final class XMLNode { /** * Used in the toString method to provide a carriage-return + line-feed. */ private static final String CRLF = System.getProperty("line.separator"); /** * To print nowhere. */ private static final PrintWriter VOID_WRITER = new PrintWriter(new VoidWriter()); /** * The element name. */ private final String _name; /** * The source document the node corresponds to. */ private Doc _doc = null; /** * The namespace URI of all nodes. */ private static final String NAMESPACE_URI = "http://weborganic.org/xmldoclet"; /** * Sets the namespace prefix for this node. */ private String _namespacePrefix = ""; /** * The attributes */ private Map<String, String> _attributes; /** * The child nodes */ private List<XMLNode> _children; /** * The content, which may be include markup. */ private StringBuilder _content; /** * The line in the source. */ private int _line; /** * Constructs the XMLNode. * * @param name The name of the element * @param doc The source java document the node belongs to. */ public XMLNode(String name, Doc doc, int line) { this._name = name; this._doc = doc; this._attributes = new HashMap<String, String>(); this._children = new ArrayList<XMLNode>(); this._content = new StringBuilder(); this._line = line; } /** * Constructs the XMLNode. * * @param name The name of the element * @param doc The source java document the node belongs to. */ public XMLNode(String name, Doc doc) { this(name, doc, -1); } /** * Constructs the XMLNode. * * @param name The name of the element */ public XMLNode(String name) { this(name, null); } /** * Adds an attribute to the node * * @param name the name of the attribute. * @param value the value for the attribute */ public XMLNode attribute(String name, String value) { if (value != null) this._attributes.put(name, value); return this; } /** * Adds an attribute to the node * * @param name the name of the attribute * @param value the value for the attribute */ public XMLNode attribute(String name, boolean value) { this._attributes.put(name, Boolean.toString(value)); return this; } /** * Adds a list of child nodes. * * @param nodes The nodes to add. * @return this node for chaining. */ public XMLNode child(List<XMLNode> nodes) { for (XMLNode node : nodes) { this._children.add(node); node.setDoc(this._doc); } return this; } /** * Adds a child node. * * @param node The node * @return this node for chaining. */ public XMLNode child(XMLNode node) { if (node != null) { this._children.add(node); node.setDoc(this._doc); } return this; } /** * Set the doc for the node and its descendants. * * @param doc the doc */ private void setDoc(Doc doc) { if (doc == null) return; if (this._doc == null) this._doc = doc; for (XMLNode child : this._children) { child.setDoc(doc); } } /** * Adds text to the content of the node. * * @param text The text. * @return this node for chaining. */ public XMLNode text(String text) { if (text != null) this._content.append(text); return this; } /** * Returns the specified attributed. * * @param name The key for the value to be retrieved. * @return The value stored in the attribute hash for the given key. */ public String getAttribute(String name) { return this._attributes.get(name); } /** * Returns the name of the node. * * @param name The name of the node. * @return The name of the node. */ public String getName() { return this._name; } /** * Saves this XML node to the directory specified. * * @param dir the directory to save this node to. * @param name the name of the file * * @param encoding the character encoding used for the output. */ public void save(File dir, String name, Charset encoding, String nsPrefix) { try { String _xmlDeclaration = "<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>" + CRLF; if (nsPrefix != null && !"".equals(nsPrefix)) { this._namespacePrefix = nsPrefix; this.attribute("xmlns:" + this._namespacePrefix, NAMESPACE_URI); this._namespacePrefix = this._namespacePrefix + ":"; } // Write out to the file File file = new File(dir, name); FileOutputStream os = new FileOutputStream(file); BufferedOutputStream bos = new BufferedOutputStream(os); OutputStreamWriter out = new OutputStreamWriter(bos, encoding); out.write(_xmlDeclaration); out.write(this.toString("")); out.close(); } catch (IOException ex) { System.err.println("Could not create '" + dir +File.pathSeparator+ name + "'"); ex.printStackTrace(); } } /** * Converts the XML node to a String. * * @param tabs The tabs used for indentation. * @return the String representation of this node and its children. */ public String toString(String tabs) { StringBuilder out = new StringBuilder(); // Open element out.append(tabs + "<" + this._namespacePrefix + this._name); // Serialise the attributes for (Entry<String, String> att : this._attributes.entrySet()) { out.append(" " + att.getKey() + "=\"" + encodeAttribute(att.getValue()) + "\""); } // Close if empty element (no text node AND no children) if (this._content.length() <= 0 && this._children.isEmpty()) { out.append(" />" + CRLF); return out.toString(); } // Close open tag out.append(">"); if (!this._children.isEmpty()) out.append(CRLF); // This node has text if (this._content.length() > 0) { // Wrapping text in a separate node allows for good presentation of data with out adding extra data. out.append(encode(this._content.toString(), this._doc, this._line)); } // Serialise children for (XMLNode node : this._children) { out.append(node.toString(tabs + "\t")); } // Close element if (!this._children.isEmpty()) out.append(tabs); out.append("</" + this._namespacePrefix + this._name + ">" + CRLF + ("class".equalsIgnoreCase(this._name)? CRLF : "")); return out.toString(); } /** * Encodes strings as XML. Check for <, >, ', ", &. * * @param text The input string. * @param doc The source java document the node belongs to. * @return The encoded string. */ private static String encode(String text, Doc doc, int line) { if (text.indexOf('<') >= 0) { return encodeElement(tidy(text, doc, line)); } else { return encodeElement(text); } } /** * Encodes strings as XML. Check for <, >, ', ", &. * * @param in The input string. * @return The encoded string. */ public static String encodeElement(String in) { final int length = in.length(); StringBuilder out = new StringBuilder(length); for (int i = 0; i < length; i++) { char c = in.charAt(i); switch (c) { case '&': out.append("&"); break; case '>': out.append(">"); break; case '<': out.append("<"); break; default: out.append(c); } } return out.toString(); } /** * Encodes strings as XML. Check for <, >, ', ", &. * * @param in The input string. * @return The encoded string. */ public static String encodeAttribute(String in) { final int length = in.length(); StringBuilder out = new StringBuilder(length); for (int i = 0; i < length; i++) { char c = in.charAt(i); switch (c) { case '\'': out.append("'"); break; case '"': out.append("""); break; case '>': out.append(">"); break; case '<': out.append("<"); break; default: out.append(c); } } return out.toString(); } /** * Tidy the text for inclusion as a comment description. * * @param text the HTML body text to tidy * @param doc The source java document the node belongs to. * @return the tidied HTML */ private static String tidy(String text, Doc doc, int line) { Tidy tidy = new Tidy(); tidy.setXmlOut(true); tidy.setEncloseText(false); tidy.setQuiet(true); tidy.setEscapeCdata(false); tidy.setIndentCdata(false); tidy.setTrimEmptyElements(false); tidy.setDropProprietaryAttributes(false); tidy.setErrout(VOID_WRITER); tidy.setWraplen(0); // Tidy wants a full HTML document... StringBuilder in = new StringBuilder(); in.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""); in.append(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); in.append("<html><head><title>Remove</title></head><body>"); in.append(text); in.append("</body></html>"); // Count new lines int baseline = line; if (line != -1) { baseline = line - countLines(doc.getRawCommentText()) -2; } tidy.setMessageListener(new Listener(doc, baseline)); // Tidy StringWriter w = new StringWriter(); tidy.parse(new StringReader(in.toString()), w); String out = w.toString(); // Get output int start = out.indexOf("<body>"); int end = out.indexOf("</body>"); if (start != -1 && end != -1) { return out.substring(start+6, end); } // Second chance try with XML tidy.setXmlTags(true); in.setLength(0); in.append(text); // Tidy w = new StringWriter(); tidy.parse(new StringReader(in.toString()), w); // Report errors return w.toString(); } private static int countLines(String text) { int lineCount = 0; int i = text.indexOf('\n'); while (i != -1) { lineCount++; i = text.indexOf('\n', i+1); } return lineCount; } /** * A listener to capture errors thrown by tidy. */ private static class Listener implements TidyMessageListener { /** * Error code returned by Tidy for unknown attributes. */ private static final int UNKNOWN_ATTRIBUTE = 48; /** * The current document being processing. */ private final Doc _doc; /** * The line where the source that is being tidied starts. */ private final int _baseline; /** * Creates a new listener for tidy * * @param doc The source java doc * @param baseline The line where the source that is being tidied starts. */ public Listener(Doc doc, int baseline) { this._doc = doc; this._baseline = baseline; } @Override public void messageReceived(TidyMessage message) { int code = message.getErrorCode(); int line = this._baseline >= 0? this._baseline + message.getLine() : -1; if (code != UNKNOWN_ATTRIBUTE) { Level level = message.getLevel(); String prefix = "["+level+"]"+(level == Level.ERROR? " " : " "); System.err.println(prefix+this._doc.toString()+":" +(line != -1? "L"+line+" " : " ")+message.getMessage()); } } }; /** * Does nothing. * * @author Christophe Lauret * @version 9 May 2012 */ private static class VoidWriter extends Writer { @Override public void write(char[] cbuf) { } @Override public void write(int c) { } @Override public void write(String str, int off, int len) { } @Override public void write(char[] cbuf, int off, int len) { } @Override public void flush() { } @Override public void close() { } } }