/*******************************************************************************
* Copyright (c) 2004, 2008 John Krasnay and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* John Krasnay - initial API and implementation
*******************************************************************************/
package net.sf.vex.dom;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Arrays;
/**
* Writes a document to an output stream, using a stylesheet to provide
* formatting hints.
*
* <ul>
* <li>Children of an element are indented by a configurable amount.</li>
* <li>Text is wrapped to fit within a configurable width.<li>
* </ul>
*
* <p>Documents are currently saved UTF-8 encoding, with no encoding
* specified in the XML declaration.</p>
*/
public class DocumentWriter {
private IWhitespacePolicy whitespacePolicy;
private String indent;
private int wrapColumn;
/**
* Class constructor.
*/
public DocumentWriter() {
this.indent = " ";
this.wrapColumn = 72;
}
/**
* Escapes special XML characters. Changes '<', '>', and '&' to
* '<', '>' and '&', respectively.
*
* @param s the string to be escaped.
* @return the escaped string
*/
public static String escape(String s) {
StringBuffer sb = new StringBuffer(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '<') {
sb.append("<");
} else if (c == '>') {
sb.append(">");
} else if (c == '&') {
sb.append("&");
} else if (c == '"') {
sb.append(""");
} else if (c == '\'') {
sb.append("'");
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* Returns the indent string. By default this is two spaces.
*/
public String getIndent() {
return this.indent;
}
/**
* Returns the whitespace policy used by this writer.
*/
public IWhitespacePolicy getWhitespacePolicy() {
return whitespacePolicy;
}
/**
* Returns the column at which text should be wrapped. By default this
* is 72.
*/
public int getWrapColumn() {
return this.wrapColumn;
}
/**
* Sets the value of the indent string.
*
* @param indent new value for the indent string.
*/
public void setIndent(String indent) {
this.indent = indent;
}
/**
* Sets the whitespace policy for this writer. The whitespace policy tells
* the writer which elements are block-formatted and which are pre-formatted.
*
* @param whitespacePolicy The whitespacePolicy to set.
*/
public void setWhitespacePolicy(IWhitespacePolicy whitespacePolicy) {
this.whitespacePolicy = whitespacePolicy;
}
/**
* Sets the value of the wrap column.
*
* @param wrapColumn new value for the wrap column.
*/
public void setWrapColumn(int wrapColumn) {
this.wrapColumn = wrapColumn;
}
public void write(Document doc, OutputStream os)
throws IOException {
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
PrintWriter pw = new PrintWriter(osw);
pw.println("<?xml version='1.0'?>");
if (doc.getSystemID() != null) {
StringBuffer sb = new StringBuffer();
sb.append("<!DOCTYPE ");
sb.append(doc.getRootElement().getName());
if (doc.getPublicID() != null) {
sb.append(" PUBLIC");
sb.append(" \"");
sb.append(doc.getPublicID());
sb.append("\"");
} else {
sb.append(" SYSTEM");
}
sb.append(" \"");
sb.append(doc.getSystemID());
sb.append("\">");
pw.println(sb.toString());
}
this.writeNode(doc.getRootElement(), pw, "");
pw.flush();
}
//====================================================== PRIVATE
private void writeNode(Node node,
PrintWriter pw,
String indent) {
if (node instanceof Text) {
TextWrapper wrapper = new TextWrapper();
wrapper.add(escape(node.getText()));
String[] lines = wrapper.wrap(this.wrapColumn - indent.length());
for (int i = 0; i < lines.length; i++) {
pw.print(indent);
pw.println(lines[i]);
}
} else {
Element element = (Element) node;
if (this.whitespacePolicy != null && this.whitespacePolicy.isPre(element)) {
pw.print(indent);
writeNodeNoWrap(node, pw);
pw.println();
return;
}
boolean hasBlockChild = false;
Element[] children = element.getChildElements();
for (int i = 0; i < children.length; i++) {
if (this.whitespacePolicy != null && this.whitespacePolicy.isBlock(children[i])) {
hasBlockChild = true;
break;
}
}
if (hasBlockChild) {
pw.print(indent);
pw.print("<");
pw.print(element.getName());
TextWrapper wrapper = new TextWrapper();
wrapper.addNoSplit(this.getAttributeString(element));
int outdent = indent.length() + 1 + element.getName().length();
String[] lines = wrapper.wrap(this.wrapColumn - outdent);
char[] bigIndent = new char[outdent];
Arrays.fill(bigIndent, ' ');
for (int i = 0; i < lines.length; i++) {
if (i > 0) {
pw.print(bigIndent);
}
pw.print(lines[i]);
if (i < lines.length - 1) {
pw.println();
}
}
pw.println(">");
String childIndent = indent + this.indent;
Node[] content = element.getChildNodes();
for (int i = 0; i < content.length; i++) {
this.writeNode(content[i], pw, childIndent);
}
pw.print(indent);
pw.print("</");
pw.print(element.getName());
pw.println(">");
} else {
TextWrapper wrapper = new TextWrapper();
this.addNode(element, wrapper);
String[] lines = wrapper.wrap(this.wrapColumn-indent.length());
for (int i = 0; i < lines.length; i++) {
pw.print(indent);
pw.println(lines[i]);
}
}
}
}
private void writeNodeNoWrap(Node node, PrintWriter pw) {
if (node instanceof Text) {
pw.print(escape(node.getText()));
} else {
Element element = (Element) node;
pw.print("<");
pw.print(element.getName());
pw.print(this.getAttributeString(element));
pw.print(">");
Node[] content = element.getChildNodes();
for (int i = 0; i < content.length; i++) {
this.writeNodeNoWrap(content[i], pw);
}
pw.print("</");
pw.print(element.getName());
pw.print(">");
}
}
private String attrToString(String name, String value) {
StringBuffer sb = new StringBuffer();
sb.append(" ");
sb.append(name);
sb.append("=\"");
sb.append(escape(value));
sb.append("\"");
return sb.toString();
}
private void addNode(Node node, TextWrapper wrapper) {
if (node instanceof Text) {
wrapper.add(escape(node.getText()));
} else {
Element element = (Element)node;
Node[] content = element.getChildNodes();
String[] attrs = element.getAttributeNames();
Arrays.sort(attrs);
if (attrs.length == 0) {
if (content.length == 0) {
wrapper.add("<" + element.getName() + " />");
} else {
wrapper.add("<" + element.getName() + ">");
}
} else {
Validator validator = element.getDocument().getValidator();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < attrs.length; i++) {
sb.setLength(0);
if (i == 0) {
sb.append("<" + element.getName());
}
if (!attrHasDefaultValue(validator, element, attrs[i])) {
sb.append(attrToString(attrs[i], element.getAttribute(attrs[i])));
}
if (i == attrs.length - 1) {
if (content.length == 0) {
sb.append("/>");
} else {
sb.append(">");
}
}
wrapper.addNoSplit(sb.toString());
}
}
for (int i = 0; i < content.length; i++) {
addNode(content[i], wrapper);
}
if (content.length > 0) {
wrapper.add("</" + element.getName() + ">");
}
}
}
private String getAttributeString(Element element) {
Validator validator = element.getDocument().getValidator();
String[] attrs = element.getAttributeNames();
Arrays.sort(attrs);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < attrs.length; i++) {
if (attrHasDefaultValue(validator, element, attrs[i])) {
continue;
}
sb.append(" ");
sb.append(attrs[i]);
sb.append("=\"");
sb.append(escape(element.getAttribute(attrs[i])));
sb.append("\"");
}
return sb.toString();
}
private static boolean attrHasDefaultValue(Validator validator, Element element, String attribute) {
if (validator != null) {
AttributeDefinition ad = validator.getAttributeDefinition(element.getName(), attribute);
if (ad != null) {
String value = element.getAttribute(attribute);
String defaultValue = ad.getDefaultValue();
return value != null && value.equals(defaultValue);
}
}
return false;
}
}