java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java
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>… /></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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
1.234 + // Not needed:
1.235 + // return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
1.243 + }
1.244 +}