/* * eXist Open Source Native XML Database * Copyright (C) 2000-2015 The eXist-db Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ package org.exist.util.serializer; import java.io.Writer; import java.util.ArrayDeque; import java.util.Deque; import java.util.Properties; import javax.xml.transform.OutputKeys; import javax.xml.transform.TransformerException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Namespaces; import org.exist.dom.QName; import org.exist.storage.serializers.EXistOutputKeys; public class IndentingXMLWriter extends XMLWriter { private final static Logger LOG = LogManager.getLogger(IndentingXMLWriter.class); private boolean indent = false; private int indentAmount = 4; private String indentChars = " "; private int level = 0; private boolean afterTag = false; private boolean sameline = false; private boolean whitespacePreserve = false; private Deque<Integer> whitespacePreserveStack = new ArrayDeque<>(); public IndentingXMLWriter() { super(); } /** * @param writer A writer to send the serialized XML output to */ public IndentingXMLWriter(final Writer writer) { super(writer); } @Override public void setWriter(final Writer writer) { super.setWriter(writer); level = 0; afterTag = false; sameline = false; whitespacePreserveStack.clear(); } @Override public void startElement(final String namespaceURI, final String localName, final String qname) throws TransformerException { if (afterTag && !isInlineTag(namespaceURI, localName)) { indent(); } super.startElement(namespaceURI, localName, qname); addIndent(); afterTag = true; sameline = true; } @Override public void startElement(final QName qname) throws TransformerException { if (afterTag && !isInlineTag(qname.getNamespaceURI(), qname.getLocalPart())) { indent(); } super.startElement(qname); addIndent(); afterTag = true; sameline = true; } @Override public void endElement(final String namespaceURI, final String localName, final String qname) throws TransformerException { endIndent(namespaceURI, localName); super.endElement(namespaceURI, localName, qname); popWhitespacePreserve(); // apply ancestor's xml:space value _after_ end element sameline = isInlineTag(namespaceURI, localName); afterTag = true; } @Override public void endElement(final QName qname) throws TransformerException { endIndent(qname.getNamespaceURI(), qname.getLocalPart()); super.endElement(qname); popWhitespacePreserve(); // apply ancestor's xml:space value _after_ end element sameline = isInlineTag(qname.getNamespaceURI(), qname.getLocalPart()); afterTag = true; } @Override public void characters(CharSequence chars) throws TransformerException { final int start = 0; final int length = chars.length(); if (length == 0) { return; // whitespace only: skip } if (length < chars.length()) { chars = chars.subSequence(start, length); // drop whitespace } for (int i = 0; i < chars.length(); i++) { if (chars.charAt(i) == '\n') { sameline = false; } } afterTag = false; super.characters(chars); } @Override public void comment(final CharSequence data) throws TransformerException { super.comment(data); afterTag = true; } @Override public void processingInstruction(final String target, final String data) throws TransformerException { super.processingInstruction(target, data); afterTag = true; } @Override public void documentType(final String name, final String publicId, final String systemId) throws TransformerException { super.documentType(name, publicId, systemId); super.characters("\n"); sameline = false; } @Override public void setOutputProperties(final Properties properties) { super.setOutputProperties(properties); final String option = outputProperties.getProperty(EXistOutputKeys.INDENT_SPACES, "4"); try { indentAmount = Integer.parseInt(option); } catch (final NumberFormatException e) { LOG.warn("Invalid indentation value: '" + option + "'"); } indent = "yes".equals(outputProperties.getProperty(OutputKeys.INDENT, "no")); } @Override public void attribute(final String qname, final String value) throws TransformerException { if ("xml:space".equals(qname)) { pushWhitespacePreserve(value); } super.attribute(qname, value); } @Override public void attribute(final QName qname, final String value) throws TransformerException { if ("xml".equals(qname.getPrefix()) && "space".equals(qname.getLocalPart())) { pushWhitespacePreserve(value); } super.attribute(qname, value); } protected void pushWhitespacePreserve(final String value) { if (value.equals("preserve")) { whitespacePreserve = true; whitespacePreserveStack.push(-level); } else if (value.equals("default")) { whitespacePreserve = false; whitespacePreserveStack.push(level); } } protected void popWhitespacePreserve() { if (!whitespacePreserveStack.isEmpty() && Math.abs(whitespacePreserveStack.peek()) > level) { whitespacePreserveStack.pop(); if (whitespacePreserveStack.isEmpty() || whitespacePreserveStack.peek() >= 0) { whitespacePreserve = false; } else { whitespacePreserve = true; } } } protected boolean isInlineTag(final String namespaceURI, final String localName) { return isMatchTag(namespaceURI, localName); } private boolean isMatchTag(final String namespaceURI, final String localName) { return namespaceURI != null && namespaceURI.equals(Namespaces.EXIST_NS) && localName.equals("match"); } protected void indent() throws TransformerException { if (!indent || whitespacePreserve) { return; } final int spaces = indentAmount * level; while (spaces >= indentChars.length()) { indentChars += indentChars; } super.characters("\n"); super.characters(indentChars.subSequence(0, spaces)); sameline = false; } protected void addIndent() { level++; } protected void endIndent(final String namespaceURI, final String localName) throws TransformerException { level--; if (afterTag && !sameline && !isInlineTag(namespaceURI, localName)) { indent(); } } }