/* * Copyright 2005-2015 by BerryWorks Software, LLC. All rights reserved. * * This file is part of EDIReader. You may obtain a license for its use directly from * BerryWorks Software, and you may also choose to use this software under the terms of the * GPL version 3. Other products in the EDIReader software suite are available only by licensing * with BerryWorks. Only those files bearing the GPL statement below are available under the GPL. * * EDIReader 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, either version 3 of * the License, or (at your option) any later version. * * EDIReader 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 EDIReader. If not, * see <http://www.gnu.org/licenses/>. */ package com.berryworks.edireader.util; import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; public class XmlFormatter extends FilterWriter { private final static String SEPARATOR = System.getProperty("line.separator"); private final static String INDENT = " "; private char mostRecentCharOfInterest; private String currentIndent = ""; private enum State { NORMAL_TEXT, HOLDING_A_CLOSE, HOLDING_A_CLOSE_FOLLOWED_BY_OPEN } private State state; public XmlFormatter(Writer writer) { super(writer); state = State.NORMAL_TEXT; } @Override public void write(int i) throws IOException { if (i <= 0) { super.write(i); } else { process((char) i); } } private void process(char c) throws IOException { switch (state) { case NORMAL_TEXT: if (c == '>') { state = State.HOLDING_A_CLOSE; break; } else { if (isCharacterOfInterest(c)) { mostRecentCharOfInterest = c; } out.write(c); } break; case HOLDING_A_CLOSE: if (c == '<') { state = State.HOLDING_A_CLOSE_FOLLOWED_BY_OPEN; break; } else { out.write('>'); out.write(c); state = State.NORMAL_TEXT; } break; case HOLDING_A_CLOSE_FOLLOWED_BY_OPEN: out.write('>'); out.write(indentionAsNeeded(c)); out.write('<'); out.write(c); state = State.NORMAL_TEXT; break; } } private String indentionAsNeeded(char currentChar) { if (currentChar == '/') { // ...<def><abc></def... currentIndent = shrink(currentIndent); } else if (mostRecentCharOfInterest == '?') { // <?xml...?><root... mostRecentCharOfInterest = '<'; } else { // ...abc><def... if (mostRecentCharOfInterest == '/') { // do not increase the indent mostRecentCharOfInterest = '<'; } else { currentIndent += INDENT; // expand } } return SEPARATOR + currentIndent; } private boolean isCharacterOfInterest(char c) { return c == '<' || c == '?' || c == '/'; } @Override public void write(char[] chars, int startIndex, int length) throws IOException { for (int i = 0; i < length; i++) { process(chars[startIndex + i]); } } @Override public void write(String text, int startIndex, int length) throws IOException { for (int i = 0; i < length; i++) { process(text.charAt(startIndex + i)); } } @Override public void close() throws IOException { switch (state) { case NORMAL_TEXT: break; case HOLDING_A_CLOSE: out.write('>'); break; case HOLDING_A_CLOSE_FOLLOWED_BY_OPEN: out.write('>'); out.write('<'); break; } super.close(); } public static String format(String xmlText) { String indent = ""; int startLookingAtIndex = 0; while (true) { int i = xmlText.indexOf("><", startLookingAtIndex); if (i < 0) { break; } char peekAhead = xmlText.charAt(i + 2); int delta = changeIndent(xmlText, i, peekAhead); if (delta < 0) { indent = shrink(indent); } else if (delta > 0) { indent += INDENT; } String insertedWhitespace = SEPARATOR + indent; xmlText = xmlText.substring(0, i + 1) + insertedWhitespace + xmlText.substring(i + 1); startLookingAtIndex = i + insertedWhitespace.length() + 1; } return xmlText; } private static String shrink(String indent) { if (indent == null || indent.length() <= INDENT.length()) return ""; return indent.substring(INDENT.length()); } private static int changeIndent(String xmlText, int startingIndex, char peekAhead) { int index = startingIndex; while (--index > 0) { char c = xmlText.charAt(index); switch (c) { case '<': return 1; case '?': return 0; case '/': if (index + 1 == startingIndex) { return -1; } else { return peekAhead == '/' ? -1 : 0; } } } return 0; } private char priorNonWhitespace(String xmlText, int index) { while (true) { if (--index <= 0) return xmlText.charAt(0); char c = xmlText.charAt(index); if (!Character.isWhitespace(c)) return c; } } private boolean isClosing(String xmlText, int index) { while (--index > 0) { char c = xmlText.charAt(index); switch (c) { case '<': return false; case '?': case '/': return true; } } return false; } }