java/sql-dk/src/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java
branchv_0
changeset 128 67f5ff139da0
child 130 8548e21177f9
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/java/sql-dk/src/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java	Sat Jan 04 19:39:35 2014 +0100
     1.3 @@ -0,0 +1,206 @@
     1.4 +/**
     1.5 + * SQL-DK
     1.6 + * Copyright © 2014 František Kučera (frantovo.cz)
     1.7 + *
     1.8 + * This program is free software: you can redistribute it and/or modify
     1.9 + * it under the terms of the GNU General Public License as published by
    1.10 + * the Free Software Foundation, either version 3 of the License, or
    1.11 + * (at your option) any later version.
    1.12 + *
    1.13 + * This program is distributed in the hope that it will be useful,
    1.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    1.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    1.16 + * GNU General Public License for more details.
    1.17 + *
    1.18 + * You should have received a copy of the GNU General Public License
    1.19 + * along with this program. If not, see <http://www.gnu.org/licenses/>.
    1.20 + */
    1.21 +package info.globalcode.sql.dk.formatting;
    1.22 +
    1.23 +import info.globalcode.sql.dk.ColorfulPrintWriter;
    1.24 +import info.globalcode.sql.dk.ColorfulPrintWriter.TerminalColor;
    1.25 +import java.util.Stack;
    1.26 +import javax.xml.namespace.QName;
    1.27 +import static info.globalcode.sql.dk.Functions.isEmpty;
    1.28 +import static info.globalcode.sql.dk.Functions.toHex;
    1.29 +import java.nio.charset.Charset;
    1.30 +import java.util.EmptyStackException;
    1.31 +import java.util.HashMap;
    1.32 +import java.util.LinkedHashMap;
    1.33 +import java.util.Map;
    1.34 +import java.util.Map.Entry;
    1.35 +import java.util.logging.Level;
    1.36 +import java.util.logging.Logger;
    1.37 +
    1.38 +/**
    1.39 + *
    1.40 + * @author Ing. František Kučera (frantovo.cz)
    1.41 + */
    1.42 +public abstract class AbstractXmlFormatter extends AbstractFormatter {
    1.43 +
    1.44 +	private static final Logger log = Logger.getLogger(AbstractXmlFormatter.class.getName());
    1.45 +	public static final String PROPERTY_COLORFUL = "color";
    1.46 +	public static final String PROPERTY_INDENT = "indent";
    1.47 +	private static final TerminalColor ELEMENT_COLOR = TerminalColor.Magenta;
    1.48 +	private static final TerminalColor ATTRIBUTE_NAME_COLOR = TerminalColor.Green;
    1.49 +	private static final TerminalColor ATTRIBUTE_VALUE_COLOR = TerminalColor.Yellow;
    1.50 +	private static final TerminalColor XML_DECLARATION_COLOR = TerminalColor.Red;
    1.51 +	private Stack<QName> treePosition = new Stack<>();
    1.52 +	private final ColorfulPrintWriter out;
    1.53 +	private final String indent;
    1.54 +
    1.55 +	public AbstractXmlFormatter(FormatterContext formatterContext) {
    1.56 +		super(formatterContext);
    1.57 +		boolean colorful = formatterContext.getProperties().getBoolean(PROPERTY_COLORFUL, false);
    1.58 +		out = new ColorfulPrintWriter(formatterContext.getOutputStream(), false, colorful);
    1.59 +		indent = formatterContext.getProperties().getString(PROPERTY_INDENT, "\t");
    1.60 +
    1.61 +		if (!indent.matches("\\s*")) {
    1.62 +			log.log(Level.WARNING, "Setting indent to „{0}“ is weird & freaky; in hex: {1}", new Object[]{indent, toHex(indent.getBytes())});
    1.63 +		}
    1.64 +
    1.65 +	}
    1.66 +
    1.67 +	protected void printStartDocument() {
    1.68 +		out.print(XML_DECLARATION_COLOR, "<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().name() + "\"?>");
    1.69 +	}
    1.70 +
    1.71 +	protected void printEndDocument() {
    1.72 +		out.println();
    1.73 +		out.flush();
    1.74 +		if (!treePosition.empty()) {
    1.75 +			throw new IllegalStateException("Some elements are not closed: " + treePosition);
    1.76 +		}
    1.77 +	}
    1.78 +
    1.79 +	protected void printStartElement(QName element) {
    1.80 +		printStartElement(element, null);
    1.81 +	}
    1.82 +
    1.83 +	protected Map<QName, String> singleAttribute(QName name, String value) {
    1.84 +		Map<QName, String> attributes = new HashMap<>(1);
    1.85 +		attributes.put(name, value);
    1.86 +		return attributes;
    1.87 +	}
    1.88 +
    1.89 +	protected void printStartElement(QName element, Map<QName, String> attributes) {
    1.90 +		printStartElement(element, attributes, false);
    1.91 +	}
    1.92 +
    1.93 +	/**
    1.94 +	 * @param empty whether element should be closed <codfe>… /&gt;</code> (has no content, do not
    1.95 +	 * call {@linkplain #printEndElement()})
    1.96 +	 */
    1.97 +	private void printStartElement(QName element, Map<QName, String> attributes, boolean empty) {
    1.98 +		printIndent();
    1.99 +
   1.100 +		out.print(ELEMENT_COLOR, "<" + toString(element));
   1.101 +
   1.102 +		if (attributes != null) {
   1.103 +			for (Entry<QName, String> attribute : attributes.entrySet()) {
   1.104 +				out.print(" ");
   1.105 +				out.print(ATTRIBUTE_NAME_COLOR, toString(attribute.getKey()));
   1.106 +				out.print("=");
   1.107 +				out.print(ATTRIBUTE_VALUE_COLOR, '"' + escapeXmlAttribute(attribute.getValue()) + '"');
   1.108 +			}
   1.109 +		}
   1.110 +
   1.111 +		if (empty) {
   1.112 +			out.print(ELEMENT_COLOR, "/>");
   1.113 +		} else {
   1.114 +			out.print(ELEMENT_COLOR, ">");
   1.115 +			treePosition.add(element);
   1.116 +		}
   1.117 +
   1.118 +		out.flush();
   1.119 +	}
   1.120 +
   1.121 +	/**
   1.122 +	 * Prints text node wrapped in given element without indenting the text and adding line breaks
   1.123 +	 * (useful for short texts).
   1.124 +	 *
   1.125 +	 * @param attributes use {@linkplain  LinkedHashMap} to preserve attributes order
   1.126 +	 */
   1.127 +	protected void printTextElement(QName element, Map<QName, String> attributes, String text) {
   1.128 +		printStartElement(element, attributes);
   1.129 +		printText(text, false);
   1.130 +		printEndElement(false);
   1.131 +	}
   1.132 +
   1.133 +	protected void printEmptyElement(QName element, Map<QName, String> attributes) {
   1.134 +		printStartElement(element, attributes, true);
   1.135 +	}
   1.136 +
   1.137 +	protected void printEndElement() {
   1.138 +		printEndElement(true);
   1.139 +	}
   1.140 +
   1.141 +	private void printEndElement(boolean indent) {
   1.142 +		try {
   1.143 +			QName name = treePosition.pop();
   1.144 +
   1.145 +			if (indent) {
   1.146 +				printIndent();
   1.147 +			}
   1.148 +
   1.149 +			out.print(ELEMENT_COLOR, "</" + toString(name) + ">");
   1.150 +			out.flush();
   1.151 +
   1.152 +		} catch (EmptyStackException e) {
   1.153 +			throw new IllegalStateException("No more elements to end.", e);
   1.154 +		}
   1.155 +	}
   1.156 +
   1.157 +	protected void printText(String s) {
   1.158 +		printText(s, true);
   1.159 +	}
   1.160 +
   1.161 +	private void printText(String s, boolean indent) {
   1.162 +		if (indent) {
   1.163 +			printIndent();
   1.164 +		}
   1.165 +		out.print(escapeXmlText(s));
   1.166 +		out.flush();
   1.167 +	}
   1.168 +
   1.169 +	protected void printIndent() {
   1.170 +		out.println();
   1.171 +		for (int i = 0; i < treePosition.size(); i++) {
   1.172 +			out.print(indent);
   1.173 +		}
   1.174 +	}
   1.175 +
   1.176 +	protected static QName qname(String name) {
   1.177 +		return new QName(name);
   1.178 +	}
   1.179 +
   1.180 +	protected static QName qname(String prefix, String name) {
   1.181 +		return new QName(null, name, prefix);
   1.182 +	}
   1.183 +
   1.184 +	private String toString(QName name) {
   1.185 +		if (isEmpty(name.getPrefix(), true)) {
   1.186 +			return escapeName(name.getLocalPart());
   1.187 +		} else {
   1.188 +			return escapeName(name.getPrefix()) + ":" + escapeName(name.getLocalPart());
   1.189 +		}
   1.190 +	}
   1.191 +
   1.192 +	private String escapeName(String s) {
   1.193 +		// TODO: avoid ugly values in <name name="…"/>		
   1.194 +		return s;
   1.195 +	}
   1.196 +
   1.197 +	private static String escapeXmlText(String s) {
   1.198 +		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
   1.199 +		// Not needed:
   1.200 +		// return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
   1.201 +	}
   1.202 +
   1.203 +	/**
   1.204 +	 * Expects attribute values enclosed in "quotes" not 'apostrophes'.
   1.205 +	 */
   1.206 +	private static String escapeXmlAttribute(String s) {
   1.207 +		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
   1.208 +	}
   1.209 +}