java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java
branchv_0
changeset 238 4a1864c3e867
parent 207 2bba68ef47c1
child 245 b6ff5b7a8422
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java	Mon Mar 04 20:15:24 2019 +0100
     1.3 @@ -0,0 +1,241 @@
     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 info.globalcode.sql.dk.configuration.PropertyDeclaration;
    1.30 +import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL;
    1.31 +import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
    1.32 +import java.nio.charset.Charset;
    1.33 +import java.util.EmptyStackException;
    1.34 +import java.util.HashMap;
    1.35 +import java.util.LinkedHashMap;
    1.36 +import java.util.Map;
    1.37 +import java.util.Map.Entry;
    1.38 +import java.util.logging.Level;
    1.39 +import java.util.logging.Logger;
    1.40 +
    1.41 +/**
    1.42 + * <p>
    1.43 + * Provides helper methods for printing pretty intended and optionally colorful (syntax highlighted)
    1.44 + * XML output.
    1.45 + * </p>
    1.46 + *
    1.47 + * <p>
    1.48 + * Must be used with care – bad usage can lead to invalid XML (e.g. using undeclared namespaces).
    1.49 + * </p>
    1.50 + *
    1.51 + * @author Ing. František Kučera (frantovo.cz)
    1.52 + */
    1.53 +@PropertyDeclaration(name = COLORFUL, defaultValue = "false", type = Boolean.class, description = COLORFUL_DESCRIPTION)
    1.54 +@PropertyDeclaration(name = AbstractXmlFormatter.PROPERTY_INDENT, defaultValue = AbstractXmlFormatter.PROPERTY_INDENT_DEFAULT, type = String.class, description = "tab or sequence of spaces used for indentation of nested elements")
    1.55 +@PropertyDeclaration(name = AbstractXmlFormatter.PROPERTY_INDENT_TEXT, defaultValue = "true", type = Boolean.class, description = "whether text with line breaks should be indented; if not original whitespace will be preserved.")
    1.56 +public abstract class AbstractXmlFormatter extends AbstractFormatter {
    1.57 +
    1.58 +	private static final Logger log = Logger.getLogger(AbstractXmlFormatter.class.getName());
    1.59 +	public static final String PROPERTY_INDENT = "indent";
    1.60 +	protected static final String PROPERTY_INDENT_DEFAULT = "\t";
    1.61 +	public static final String PROPERTY_INDENT_TEXT = "indentText";
    1.62 +	private static final TerminalColor ELEMENT_COLOR = TerminalColor.Magenta;
    1.63 +	private static final TerminalColor ATTRIBUTE_NAME_COLOR = TerminalColor.Green;
    1.64 +	private static final TerminalColor ATTRIBUTE_VALUE_COLOR = TerminalColor.Yellow;
    1.65 +	private static final TerminalColor XML_DECLARATION_COLOR = TerminalColor.Red;
    1.66 +	private static final TerminalColor XML_DOCTYPE_COLOR = TerminalColor.Cyan;
    1.67 +	private Stack<QName> treePosition = new Stack<>();
    1.68 +	private final ColorfulPrintWriter out;
    1.69 +	private final String indent;
    1.70 +	private final boolean indentText;
    1.71 +
    1.72 +	public AbstractXmlFormatter(FormatterContext formatterContext) {
    1.73 +		super(formatterContext);
    1.74 +		boolean colorful = formatterContext.getProperties().getBoolean(COLORFUL, false);
    1.75 +		out = new ColorfulPrintWriter(formatterContext.getOutputStream(), false, colorful);
    1.76 +		indent = formatterContext.getProperties().getString(PROPERTY_INDENT, PROPERTY_INDENT_DEFAULT);
    1.77 +		indentText = formatterContext.getProperties().getBoolean(PROPERTY_INDENT_TEXT, true);
    1.78 +
    1.79 +		if (!indent.matches("\\s*")) {
    1.80 +			log.log(Level.WARNING, "Setting indent to „{0}“ is weird & freaky; in hex: {1}", new Object[]{indent, toHex(indent.getBytes())});
    1.81 +		}
    1.82 +
    1.83 +	}
    1.84 +
    1.85 +	protected void printStartDocument() {
    1.86 +		out.print(XML_DECLARATION_COLOR, "<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().name() + "\"?>");
    1.87 +	}
    1.88 +
    1.89 +	protected void printDoctype(String doctype) {
    1.90 +		out.print(XML_DOCTYPE_COLOR, "\n<!DOCTYPE " + doctype + ">");
    1.91 +	}
    1.92 +
    1.93 +	protected void printEndDocument() {
    1.94 +		out.println();
    1.95 +		out.flush();
    1.96 +		if (!treePosition.empty()) {
    1.97 +			throw new IllegalStateException("Some elements are not closed: " + treePosition);
    1.98 +		}
    1.99 +	}
   1.100 +
   1.101 +	protected void printStartElement(QName element) {
   1.102 +		printStartElement(element, null);
   1.103 +	}
   1.104 +
   1.105 +	protected Map<QName, String> singleAttribute(QName name, String value) {
   1.106 +		Map<QName, String> attributes = new HashMap<>(2);
   1.107 +		attributes.put(name, value);
   1.108 +		return attributes;
   1.109 +	}
   1.110 +
   1.111 +	protected void printStartElement(QName element, Map<QName, String> attributes) {
   1.112 +		printStartElement(element, attributes, false);
   1.113 +	}
   1.114 +
   1.115 +	/**
   1.116 +	 * @param empty whether element should be closed <codfe>… /&gt;</code> (has no content, do not
   1.117 +	 * call {@linkplain #printEndElement()})
   1.118 +	 */
   1.119 +	private void printStartElement(QName element, Map<QName, String> attributes, boolean empty) {
   1.120 +		printIndent();
   1.121 +
   1.122 +		out.print(ELEMENT_COLOR, "<" + toString(element));
   1.123 +
   1.124 +		if (attributes != null) {
   1.125 +			for (Entry<QName, String> attribute : attributes.entrySet()) {
   1.126 +				out.print(" ");
   1.127 +				out.print(ATTRIBUTE_NAME_COLOR, toString(attribute.getKey()));
   1.128 +				out.print("=");
   1.129 +				out.print(ATTRIBUTE_VALUE_COLOR, '"' + escapeXmlAttribute(attribute.getValue()) + '"');
   1.130 +			}
   1.131 +		}
   1.132 +
   1.133 +		if (empty) {
   1.134 +			out.print(ELEMENT_COLOR, "/>");
   1.135 +		} else {
   1.136 +			out.print(ELEMENT_COLOR, ">");
   1.137 +			treePosition.add(element);
   1.138 +		}
   1.139 +
   1.140 +		out.flush();
   1.141 +	}
   1.142 +
   1.143 +	/**
   1.144 +	 * Prints text node wrapped in given element without indenting the text and adding line breaks
   1.145 +	 * (useful for short texts).
   1.146 +	 *
   1.147 +	 * @param attributes use {@linkplain  LinkedHashMap} to preserve attributes order
   1.148 +	 */
   1.149 +	protected void printTextElement(QName element, Map<QName, String> attributes, String text) {
   1.150 +		printStartElement(element, attributes);
   1.151 +
   1.152 +		String[] lines = text.split("\\n");
   1.153 +
   1.154 +		if (indentText && lines.length > 1) {
   1.155 +			for (String line : lines) {
   1.156 +				printText(line, true);
   1.157 +			}
   1.158 +			printEndElement(true);
   1.159 +		} else {
   1.160 +			/*
   1.161 +			 * line breaks at the end of the text will be eaten – if you need them, use indentText = false
   1.162 +			 */
   1.163 +			if (lines.length == 1 && text.endsWith("\n")) {
   1.164 +				text = text.substring(0, text.length() - 1);
   1.165 +			}
   1.166 +
   1.167 +			printText(text, false);
   1.168 +			printEndElement(false);
   1.169 +		}
   1.170 +	}
   1.171 +
   1.172 +	protected void printEmptyElement(QName element, Map<QName, String> attributes) {
   1.173 +		printStartElement(element, attributes, true);
   1.174 +	}
   1.175 +
   1.176 +	protected void printEndElement() {
   1.177 +		printEndElement(true);
   1.178 +	}
   1.179 +
   1.180 +	private void printEndElement(boolean indent) {
   1.181 +		try {
   1.182 +			QName name = treePosition.pop();
   1.183 +
   1.184 +			if (indent) {
   1.185 +				printIndent();
   1.186 +			}
   1.187 +
   1.188 +			out.print(ELEMENT_COLOR, "</" + toString(name) + ">");
   1.189 +			out.flush();
   1.190 +
   1.191 +		} catch (EmptyStackException e) {
   1.192 +			throw new IllegalStateException("No more elements to end.", e);
   1.193 +		}
   1.194 +	}
   1.195 +
   1.196 +	protected void printText(String s, boolean indent) {
   1.197 +		if (indent) {
   1.198 +			printIndent();
   1.199 +		}
   1.200 +		out.print(escapeXmlText(s));
   1.201 +		out.flush();
   1.202 +	}
   1.203 +
   1.204 +	protected void printIndent() {
   1.205 +		out.println();
   1.206 +		for (int i = 0; i < treePosition.size(); i++) {
   1.207 +			out.print(indent);
   1.208 +		}
   1.209 +	}
   1.210 +
   1.211 +	protected static QName qname(String name) {
   1.212 +		return new QName(name);
   1.213 +	}
   1.214 +
   1.215 +	protected static QName qname(String prefix, String name) {
   1.216 +		return new QName(null, name, prefix);
   1.217 +	}
   1.218 +
   1.219 +	private String toString(QName name) {
   1.220 +		if (isEmpty(name.getPrefix(), true)) {
   1.221 +			return escapeName(name.getLocalPart());
   1.222 +		} else {
   1.223 +			return escapeName(name.getPrefix()) + ":" + escapeName(name.getLocalPart());
   1.224 +		}
   1.225 +	}
   1.226 +
   1.227 +	private String escapeName(String s) {
   1.228 +		// TODO: avoid ugly values in <name name="…"/>		
   1.229 +		return s;
   1.230 +	}
   1.231 +
   1.232 +	private static String escapeXmlText(String s) {
   1.233 +		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
   1.234 +		// Not needed:
   1.235 +		// return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
   1.236 +	}
   1.237 +
   1.238 +	/**
   1.239 +	 * Expects attribute values enclosed in "quotes" not 'apostrophes'.
   1.240 +	 */
   1.241 +	private static String escapeXmlAttribute(String s) {
   1.242 +		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
   1.243 +	}
   1.244 +}