/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of the License at the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package org.apereo.portal.xml.stream; import java.util.Deque; import java.util.EnumSet; import java.util.LinkedList; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.XMLEventWriter; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Characters; import javax.xml.stream.events.XMLEvent; import org.apache.commons.lang.StringUtils; /** Adds indentation to an {@link XMLEventWriter} */ public class IndentingXMLEventWriter extends EventWriterDelegate { public static final String NEW_LINE = "\n"; private static final ConcurrentMap<Integer, String> indentCache = new ConcurrentHashMap<Integer, String>(); private enum StackState { WROTE_MARKUP, WROTE_DATA } private final XMLEventFactory xmlEventFactory; private int indentSize = 2; private final Deque<Set<StackState>> scopeState = new LinkedList<Set<StackState>>(); private int depth = 0; // document scope public IndentingXMLEventWriter(XMLEventWriter out) { super(out); xmlEventFactory = XMLEventFactory.newFactory(); scopeState.add(EnumSet.noneOf(StackState.class)); } public void setIndentSize(int indentSize) { this.indentSize = indentSize; } /** @return System.getProperty("line.separator") or {@link #NEW_LINE} if that fails. */ public static String getLineSeparator() { try { return System.getProperty("line.separator"); } catch (SecurityException ignored) { } return NEW_LINE; } @Override public void add(XMLEvent event) throws XMLStreamException { switch (event.getEventType()) { case XMLStreamConstants.CHARACTERS: case XMLStreamConstants.CDATA: case XMLStreamConstants.SPACE: { wrappedWriter.add(event); afterData(); return; } case XMLStreamConstants.START_ELEMENT: { beforeStartElement(); wrappedWriter.add(event); afterStartElement(); return; } case XMLStreamConstants.END_ELEMENT: { beforeEndElement(); wrappedWriter.add(event); afterEndElement(); return; } case XMLStreamConstants.START_DOCUMENT: case XMLStreamConstants.PROCESSING_INSTRUCTION: case XMLStreamConstants.COMMENT: case XMLStreamConstants.DTD: { beforeMarkup(); wrappedWriter.add(event); afterMarkup(); return; } case XMLStreamConstants.END_DOCUMENT: { wrappedWriter.add(event); afterEndDocument(); break; } default: { wrappedWriter.add(event); return; } } } /** Prepare to write markup, by writing a new line and indentation. */ protected void beforeMarkup() { final Set<StackState> state = scopeState.getFirst(); if (!state.contains(StackState.WROTE_DATA) && (depth > 0 || !state.isEmpty())) { final String indent = getIndent(this.depth, this.indentSize); final Characters indentEvent = xmlEventFactory.createCharacters(indent); try { wrappedWriter.add(indentEvent); } catch (XMLStreamException e) { //Ignore exceptions caused by indentation } afterMarkup(); // indentation was written } } /** Note that markup or indentation was written. */ protected void afterMarkup() { final Set<StackState> state = scopeState.getFirst(); state.add(StackState.WROTE_MARKUP); } /** Note that data were written. */ protected void afterData() { final Set<StackState> state = scopeState.getFirst(); state.add(StackState.WROTE_DATA); } /** Prepare to start an element, by allocating stack space. */ protected void beforeStartElement() { beforeMarkup(); } /** Note that an element was started. */ protected void afterStartElement() { afterMarkup(); ++depth; scopeState.push(EnumSet.noneOf(StackState.class)); } /** Prepare to end an element, by writing a new line and indentation. */ protected void beforeEndElement() { final Set<StackState> state = scopeState.getFirst(); // but not data if (depth > 0 && state.contains(StackState.WROTE_MARKUP) && !state.contains(StackState.WROTE_DATA)) { final String indent = this.getIndent(depth - 1, indentSize); final Characters indentEvent = xmlEventFactory.createCharacters(indent); try { wrappedWriter.add(indentEvent); } catch (XMLStreamException e) { //Ignore exceptions caused by indentation } } } /** Note that an element was ended. */ protected void afterEndElement() { if (depth > 0) { --depth; scopeState.pop(); } } /** Note that a document was ended. */ protected void afterEndDocument() { depth = 0; final Set<StackState> state = scopeState.getFirst(); if (state.contains(StackState.WROTE_MARKUP) && !state.contains(StackState.WROTE_DATA)) { // but not data try { final String indent = getLineSeparator() + StringUtils.repeat(" ", 0); final Characters indentEvent = xmlEventFactory.createCharacters(indent); wrappedWriter.add(indentEvent); } catch (Exception ignored) { } } scopeState.clear(); scopeState.push(EnumSet.noneOf(StackState.class)); // start fresh } /** Generate an indentation string for the specified depth and indent size */ protected String getIndent(int depth, int size) { final int length = depth * size; String indent = indentCache.get(length); if (indent == null) { indent = getLineSeparator() + StringUtils.repeat(" ", length); indentCache.put(length, indent); } return indent; } }