java/sql-dk/src/main/java/info/globalcode/sql/dk/formatting/AbstractXmlFormatter.java
author František Kučera <franta-hg@frantovo.cz>
Thu, 24 Oct 2019 21:43:08 +0200
branchv_0
changeset 250 aae5009bd0af
parent 245 b6ff5b7a8422
permissions -rw-r--r--
fix license version: GNU GPLv3

/**
 * SQL-DK
 * Copyright © 2014 František Kučera (frantovo.cz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package info.globalcode.sql.dk.formatting;

import info.globalcode.sql.dk.ColorfulPrintWriter;
import info.globalcode.sql.dk.ColorfulPrintWriter.TerminalColor;
import java.util.Stack;
import javax.xml.namespace.QName;
import static info.globalcode.sql.dk.Functions.isEmpty;
import static info.globalcode.sql.dk.Functions.toHex;
import info.globalcode.sql.dk.configuration.PropertyDeclaration;
import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL;
import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION;
import java.nio.charset.Charset;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <p>
 * Provides helper methods for printing pretty intended and optionally colorful (syntax highlighted)
 * XML output.
 * </p>
 *
 * <p>
 * Must be used with care – bad usage can lead to invalid XML (e.g. using undeclared namespaces).
 * </p>
 *
 * @author Ing. František Kučera (frantovo.cz)
 */
@PropertyDeclaration(name = COLORFUL, defaultValue = "false", type = Boolean.class, description = COLORFUL_DESCRIPTION)
@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")
@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.")
public abstract class AbstractXmlFormatter extends AbstractFormatter {

	private static final Logger log = Logger.getLogger(AbstractXmlFormatter.class.getName());
	public static final String PROPERTY_INDENT = "indent";
	protected static final String PROPERTY_INDENT_DEFAULT = "\t";
	public static final String PROPERTY_INDENT_TEXT = "indentText";
	private static final TerminalColor ELEMENT_COLOR = TerminalColor.Magenta;
	private static final TerminalColor ATTRIBUTE_NAME_COLOR = TerminalColor.Green;
	private static final TerminalColor ATTRIBUTE_VALUE_COLOR = TerminalColor.Yellow;
	private static final TerminalColor XML_DECLARATION_COLOR = TerminalColor.Red;
	private static final TerminalColor XML_DOCTYPE_COLOR = TerminalColor.Cyan;
	private final Stack<QName> treePosition = new Stack<>();
	private final ColorfulPrintWriter out;
	private final String indent;
	private final boolean indentText;

	public AbstractXmlFormatter(FormatterContext formatterContext) {
		super(formatterContext);
		boolean colorful = formatterContext.getProperties().getBoolean(COLORFUL, false);
		out = new ColorfulPrintWriter(formatterContext.getOutputStream(), false, colorful);
		indent = formatterContext.getProperties().getString(PROPERTY_INDENT, PROPERTY_INDENT_DEFAULT);
		indentText = formatterContext.getProperties().getBoolean(PROPERTY_INDENT_TEXT, true);

		if (!indent.matches("\\s*")) {
			log.log(Level.WARNING, "Setting indent to „{0}“ is weird & freaky; in hex: {1}", new Object[]{indent, toHex(indent.getBytes())});
		}

	}

	protected void printStartDocument() {
		out.print(XML_DECLARATION_COLOR, "<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().name() + "\"?>");
	}

	protected void printDoctype(String doctype) {
		out.print(XML_DOCTYPE_COLOR, "\n<!DOCTYPE " + doctype + ">");
	}

	protected void printEndDocument() {
		out.println();
		out.flush();
		if (!treePosition.empty()) {
			throw new IllegalStateException("Some elements are not closed: " + treePosition);
		}
	}

	protected void printStartElement(QName element) {
		printStartElement(element, null);
	}

	protected Map<QName, String> singleAttribute(QName name, String value) {
		Map<QName, String> attributes = new HashMap<>(2);
		attributes.put(name, value);
		return attributes;
	}

	protected void printStartElement(QName element, Map<QName, String> attributes) {
		printStartElement(element, attributes, false);
	}

	/**
	 * @param empty whether element should be closed <codfe>… /&gt;</code> (has no content, do not
	 * call {@linkplain #printEndElement()})
	 */
	private void printStartElement(QName element, Map<QName, String> attributes, boolean empty) {
		printIndent();

		out.print(ELEMENT_COLOR, "<" + toString(element));

		if (attributes != null) {
			for (Entry<QName, String> attribute : attributes.entrySet()) {
				out.print(" ");
				out.print(ATTRIBUTE_NAME_COLOR, toString(attribute.getKey()));
				out.print("=");
				out.print(ATTRIBUTE_VALUE_COLOR, '"' + escapeXmlAttribute(attribute.getValue()) + '"');
			}
		}

		if (empty) {
			out.print(ELEMENT_COLOR, "/>");
		} else {
			out.print(ELEMENT_COLOR, ">");
			treePosition.add(element);
		}

		out.flush();
	}

	/**
	 * Prints text node wrapped in given element without indenting the text and adding line breaks
	 * (useful for short texts).
	 *
	 * @param attributes use {@linkplain  LinkedHashMap} to preserve attributes order
	 */
	protected void printTextElement(QName element, Map<QName, String> attributes, String text) {
		printStartElement(element, attributes);

		String[] lines = text.split("\\n");

		if (indentText && lines.length > 1) {
			for (String line : lines) {
				printText(line, true);
			}
			printEndElement(true);
		} else {
			/*
			 * line breaks at the end of the text will be eaten – if you need them, use indentText = false
			 */
			if (lines.length == 1 && text.endsWith("\n")) {
				text = text.substring(0, text.length() - 1);
			}

			printText(text, false);
			printEndElement(false);
		}
	}

	protected void printEmptyElement(QName element, Map<QName, String> attributes) {
		printStartElement(element, attributes, true);
	}

	protected void printEndElement() {
		printEndElement(true);
	}

	private void printEndElement(boolean indent) {
		try {
			QName name = treePosition.pop();

			if (indent) {
				printIndent();
			}

			out.print(ELEMENT_COLOR, "</" + toString(name) + ">");
			out.flush();

		} catch (EmptyStackException e) {
			throw new IllegalStateException("No more elements to end.", e);
		}
	}

	protected void printText(String s, boolean indent) {
		if (indent) {
			printIndent();
		}
		out.print(escapeXmlText(s));
		out.flush();
	}

	protected void printIndent() {
		out.println();
		for (int i = 0; i < treePosition.size(); i++) {
			out.print(indent);
		}
	}

	private String toString(QName name) {
		if (isEmpty(name.getPrefix(), true)) {
			return escapeName(name.getLocalPart());
		} else {
			return escapeName(name.getPrefix()) + ":" + escapeName(name.getLocalPart());
		}
	}

	private String escapeName(String s) {
		// TODO: avoid ugly values in <name name="…"/>		
		return s;
	}

	private static String escapeXmlText(String s) {
		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
		// Not needed:
		// return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
	}

	/**
	 * Expects attribute values enclosed in "quotes" not 'apostrophes'.
	 */
	private static String escapeXmlAttribute(String s) {
		return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
	}
}