package org.cdlib.xtf.util; /** * Copyright (c) 2004, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ import java.io.StringReader; import java.util.LinkedList; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import net.sf.saxon.Configuration; import net.sf.saxon.om.AllElementStripper; import net.sf.saxon.om.NodeInfo; import net.sf.saxon.tinytree.TinyBuilder; import net.sf.saxon.trans.XPathException; //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** This class provides a simple way to produce indented XML with matched * begin and end tags. <br><br> * * To use this class, you basically do the following: <br><br> * * <code> * * XMLFormatter formatter = new XMLFormatter(); * * formatter.tabSize(4); // (Defaults to 2 spaces if not specified) * formatter.procInstr( "xml version=\"1.0\" encoding=\"utf-8\"" ) * formatter.beginTag( "tagName", "tagAttr=\"value\"" ) * formatter.text( "A bunch of text within a tag." ) + newLine(2) * formatter.endTag(); * String result = formatter.toString(); * </code> <br><br> * * This will produce an XML string with the following contents: <br><br> * * <?xml version="1.0" encoding="utf-8"?> * * <tagName tagAttr="value"> * * A bunch of text within a tag. * * </tagName> */ public class XMLFormatter { /** Buffer to accumulate the results in */ private StringBuffer buf = new StringBuffer(); /** Default amount to indent when {@link #tab()} is called. */ public int defaultTabSize = 2; ////////////////////////////////////////////////////////////////////////////// /** Return whether or not a blank line will automatically be inserted after * each new tag. <br><br> * * The default behavior is to insert a blank line after each tag. To change * this behavior, call the * {@link XMLFormatter#blankLineAfterTag(boolean) blankLineAfterTag(boolean enable) } * function. <br><br> * * @return * <code>true</code> - Blank lines will be inserted after each new tag. * <code>false</code> - No blank lines will be inserted after each new tag. */ public boolean blankLineAfterTag() { return mBlankLineAfterTag; } // public blankLineAfterTag() ////////////////////////////////////////////////////////////////////////////// /** Set whether or not a blank line will automatically be inserted after * each new tag. <br><br> * * @param enable Enable (<code>true</code>) or disable (<code>false</code>) * automatic blank line insertion after each tag. * * * @return * The value of the blank line flag just prior to this call. */ public boolean blankLineAfterTag(boolean enable) { // Get the previous blank line flag. boolean oldBlankLineAfterTag = mBlankLineAfterTag; // Set the new blank line flag value. mBlankLineAfterTag = enable; // Return the previous value to the caller. return oldBlankLineAfterTag; } // public blankLineAfterTag( boolean enable ) ////////////////////////////////////////////////////////////////////////////// /** Return the current tab size used for indenting nested tags. <br><br> * * By default, the tab size is set to 2 spaces. To change the indent size, * call the {@link XMLFormatter#tabSize(int) tabSize(int newTabSize) } * method. * * @return * The number of spaces each a nested line will be indented from its * container. */ public int tabSize() { return tabSize; } // public tabSize() ////////////////////////////////////////////////////////////////////////////// /** Set the tab size used for indenting nested tags. <br><br> * * @param newTabSize The new tab size (in spaces) to indent a nested tag * from its containing tag. * * @return * The number of spaces each a nested line will be indented from its * container. */ public int tabSize(int newTabSize) { // Get hold of the previous tab size. int oldTabSize = tabSize; // Set the new tab size, and limit it to between 0 and 8 spaces per // nesting. // tabSize = newTabSize; if (tabSize < 0) tabSize = 0; if (tabSize > 8) tabSize = 8; // Return the previous indent size to the caller. return oldTabSize; } // public tabSize( int newTabSize ) ////////////////////////////////////////////////////////////////////////////// /** Add a string containing a properly indented begin tag consisting * only of the tag name. <br><br> * * @param tagName The name of the tag to create. */ public void beginTag(String tagName) { // Close any previous tag that's hanging open. closeTagStart(); // If no tag name was specified, simply return. if (tagName == null || tagName.length() == 0) return; // Indent, and write the begin tag name between angle brackets. buf.append(spaces.substring(0, tabCount)); buf.append("<"); buf.append(tagName); // Remember that we have a tag currently open. It will be closed by // any operation except adding an attribute. // tagStartOpen = true; // Add the tag name to the current nesting stack so that the // endTag() method can properly close the tag when called. // tagStack.addLast(tagName); // Indent subsequent output to lie within this tag. tab(); } // public beginTag( String tagName ) ////////////////////////////////////////////////////////////////////////////// /** * If there has been a beginTag(), we need to be sure and add the closing * ">" before doing anything else. */ private void closeTagStart() { // If there's no tag open, simply return. if (!tagStartOpen) return; // Emit the closing bracket and a newline. buf.append(">\n"); // All done. tagStartOpen = false; } // private closeTag() ////////////////////////////////////////////////////////////////////////////// /** Format a tag attribute from an attribute name and an associated string * value. */ public void attr(String attName, String attValue) { buf.append(" "); buf.append(attName); buf.append("=\""); buf.append(escapeText(attValue)); buf.append("\""); } // public attr() ////////////////////////////////////////////////////////////////////////////// /** Format a tag attribute from an attribute name and an associated integer * value. */ public void attr(String attName, int attValue) { attr(attName, Integer.toString(attValue)); } // public attr() ////////////////////////////////////////////////////////////////////////////// /** Format a tag attribute from an attribute name and an associated * floating-point value. */ public void attr(String attName, float attValue) { attr(attName, Float.toString(attValue)); } // public attr() ////////////////////////////////////////////////////////////////////////////// /** Format a string containing a properly indented begin tag consisting * of a tag name and a list of attributes. <br><br> * * @param tagName The name of the tag to create. * * @param tagAtts A string of attributes to tadd to the tag. * * @.notes * Use the {@link XMLFormatter#attr(String, String) attr() } method * and its cousins to simplify constructing attribute name/value * pairs. */ public void beginTag(String tagName, String tagAtts) { // Close any previous tag that's hanging open. closeTagStart(); // If no tag name was specified, simply return. if (tagName == null || tagName.length() == 0) return; // If the tag attributes string is empty, have the simple beginTag() // function do the work. // if (tagAtts == null || tagAtts.length() == 0) { beginTag(tagName); return; } // Format up the tag name and attributes between angle brackets. Start // with the angle bracket and tag name. // buf.append(spaces.substring(0, tabCount)); buf.append("<"); buf.append(tagName); // If the tag string doesn't start with a space, add one to separate // the first attribute from the tag name. // if (tagAtts.charAt(0) != ' ') buf.append(" "); // Then add the closing angle bracket and we're done. buf.append(tagAtts); buf.append(">\n"); // Add the tag name to the current nesting stack so that the // endTag() method can properly close the tag when called. // tagStack.addLast(tagName); // If the user wants a blank line after each tag, put one in. if (mBlankLineAfterTag) buf.append("\n"); // Indent subsequent output to lie within this tag. tab(); } // public beginTag( String tagName, String tagAtts ) ////////////////////////////////////////////////////////////////////////////// /** Add a string containing a properly indented end tag for the * closest open tag (if any.) */ public void endTag() { // Close any previous tag that's hanging open. closeTagStart(); // If there are no open tags left, simply return; if (tagStack.isEmpty()) return; // Undo the current indent level. untab(); // Format up the end-tag. buf.append(spaces.substring(0, tabCount)); buf.append("</"); buf.append((String)(tagStack.removeLast())); buf.append(">\n"); // If the user wants a blank line after each tag, add one. if (mBlankLineAfterTag) buf.append("\n"); } // public endTag() ////////////////////////////////////////////////////////////////////////////// /** Add a string containing properly indented end tags for any * remaining open tags. */ public void endAllTags() { // Close any previous tag that's hanging open. closeTagStart(); // While there are open tags left, end each one. while (!tagStack.isEmpty()) endTag(); } // public endAllTags() ////////////////////////////////////////////////////////////////////////////// /** Format an element tag. * * @param tagStr The string to place in the tag. */ public void tag(String tagStr) { // Close any previous tag that's hanging open. closeTagStart(); // Indent and assemble the specified tag. buf.append(spaces.substring(0, tabCount)); buf.append("<"); buf.append(tagStr); buf.append("/>\n"); // If the user wants a blank line after the tag, add one. if (mBlankLineAfterTag) buf.append("\n"); } // public tag() ////////////////////////////////////////////////////////////////////////////// /** Format a processing instruction tag at the current level of indentation. * * @param procStr The processing instruction string to place in the tag. */ public void procInstr(String procStr) { // Close any previous tag that's hanging open. closeTagStart(); // Format up the processing instruction tag. buf.append(spaces.substring(0, tabCount)); buf.append("<?"); buf.append(procStr); buf.append("?>\n"); // If the user wants a blank line after each tag, put one in. if (mBlankLineAfterTag) buf.append("\n"); } // procInstr( String procStr ) ////////////////////////////////////////////////////////////////////////////// /** Format a string of text at the current level of indentation. * * @param str The text to indent. */ public void text(String str) { // Close any previous tag that's hanging open. closeTagStart(); // If no text was passed by the caller, simply return. if (str == null || str.length() == 0) return; // Escape any special characters. str = escapeText(str); // Otherwise, indent the text, and add it to the buffer. buf.append(spaces.substring(0, tabCount)); buf.append(str); } // public text( String str ) ////////////////////////////////////////////////////////////////////////////// /** Format a string containing text broken across multiple indented * lines less than or equal to a maximum line length. <br><br> * * @param str The string to break across multiple lines. * * @param maxWidth The maximum width for each line. */ public void text(String str, int maxWidth) { // Close any previous tag that's hanging open. closeTagStart(); // Escape any special characters. str = escapeText(str); // If the caller didn't pass any text in, simply return. if (str == null) return; // If no maximum width was specified by the caller, call the simple // text function to do the work. // if (maxWidth <= 0) { text(str); return; } // While the remaining text is longer than the maximum width... while (str != null) { // Trim leading and trailing spaces from the the string. str = str.trim(); // Start with the necessary indentation. buf.append(spaces.substring(0, tabCount)); // Look for the first space/carriage-return/line-feed at or // before the maximum width. // int spaceIdx = str.lastIndexOf(' ', maxWidth); int crIdx = str.indexOf('\r'); int lfIdx = str.indexOf('\n'); // Assume we'll break the string at the location of the space. int breakIdx = spaceIdx; // If we found a carriage-return, and it's within the max line // width, have it override the space as the break point. // if (crIdx > -1 && crIdx < maxWidth) { breakIdx = crIdx; // If we also found a line-feed, and it's before the carriage // return, have it override the carriage-return as the break // point. // if (lfIdx > -1 && lfIdx < crIdx) breakIdx = lfIdx; } // We didn't find a carriage return. But if found a line-feed // and it's within the maximum line width, have it override the // space as the break point. // else if (lfIdx > -1 && lfIdx < maxWidth) breakIdx = lfIdx; // If no break was found, simply use the string as is, and // consider the job done. // if (breakIdx == -1) { buf.append(str); str = null; } // If the break was the first character in the // remaining text, skip it and go 'round again. // else if (breakIdx == 0) str = str.substring(1); // For the break at any other location.... else { // Tack on the text up to break character. buf.append(str.substring(0, breakIdx)); buf.append(" \n"); // If the break was not the last character in the // remaining string, chop off the part we just // added to the output string and go 'round again. // if (breakIdx < str.length() - 1) str = str.substring(breakIdx + 1); // Otherwise, there's nothing left in the original // source string, so flag that we're finished. // else str = null; } // else( space at any other location ) } // while( str != null && str.length() > maxWidth ) } // public text( String str, int maxWidth ) ////////////////////////////////////////////////////////////////////////////// /** Adds a text string, unformatted and unescaped, directly to the buffer. * <br><br> * * @param str The string to add to the buffer. */ public void rawText(String str) { // Close any previous tag that's hanging open. closeTagStart(); // And add the string, with no escaping, formatting, etc. buf.append(str); } // public rawText( String str ) ////////////////////////////////////////////////////////////////////////////// /** Add a single new-line. * */ public void newLine() { buf.append("\n"); } ////////////////////////////////////////////////////////////////////////////// /** Add a specified number of new-lines. * * @param lineCount The number of new-lines to add. * */ public void newLine(int lineCount) { // If the caller wants more new lines than we can provide, default // to the maximum we can provide. // if (lineCount > newLines.length()) lineCount = newLines.length(); // Add a string containing the specified number of new lines. buf.append(newLines.substring(0, lineCount)); } // public newLine( int lineCount ) ////////////////////////////////////////////////////////////////////////////// /** Adds a string containing one or two new-lines depending on whether * the user wants blank lines after tags or not. * */ public void newLineAfterText() { newLine(mBlankLineAfterTag ? 2 : 1); } // public newLineAfterText() ////////////////////////////////////////////////////////////////////////////// /** Get the current tab indent level (in spaces). */ public int tabCount() { return tabCount; } ////////////////////////////////////////////////////////////////////////////// /** Indent by the current tab size for all subsequent calls to FormatXML * output functions. */ private void tab() { // Increment the indent amount based on the current tab size. tabCount += tabSize; // And ensure that we don't indent more than the the number of // spaces available in the indent source string. // if (tabCount > spaces.length()) tabCount = spaces.length(); } // public tab() ////////////////////////////////////////////////////////////////////////////// /** Un-indent by the current tab size for all subsequent calls to FormatXML * output functions. */ private void untab() { // If the current indentation is greater than the current tab size, // actually unindent. Otherwise, unindent as far as possible, but not // back past zero. // if (tabCount > tabSize) tabCount -= tabSize; else tabCount = 0; } // public untab() ////////////////////////////////////////////////////////////////////////////// /** * Change any XML-special characters to their ampersand equivalents. * * @param text Text to scan * @return Modified version with escaped characters. */ public static String escapeText(String text) { if (text.indexOf('&') >= 0) text = text.replaceAll("&", "&"); if (text.indexOf('<') >= 0) text = text.replaceAll("<", "<"); if (text.indexOf('>') >= 0) text = text.replaceAll(">", ">"); if (text.indexOf('\"') >= 0) text = text.replaceAll("\"", """); return text; } // public escapeText() ////////////////////////////////////////////////////////////////////////////// /** Get the formatted results as a string. */ public String toString() { return buf.toString(); } // public toString() ////////////////////////////////////////////////////////////////////////////// /** Get the results as a Saxon-compatible Source. */ public Source toSource() { return new StreamSource(new StringReader(buf.toString())); } // public toSource() ////////////////////////////////////////////////////////////////////////////// /** Get the results as a Saxon NodeInfo. */ public NodeInfo toNode(Configuration config) { String strVersion = buf.toString(); StreamSource src = new StreamSource(new StringReader(strVersion)); try { return TinyBuilder.build(src, new AllElementStripper(), config); } catch (XPathException e) { throw new RuntimeException(e); } } // public toNode() ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /** Used for tabbing */ private static final String spaces = " " + " " + " " + " " + " " + " " + " " + " "; /** Used for multiple blank line output */ private static final String newLines = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; /** Stack of current tag nestings */ private LinkedList tagStack = new LinkedList(); /** Current tab level for this thread */ private int tabCount = 0; /** Is there currently a begin tag open? */ private boolean tagStartOpen = false; /** Automatically insert blank lines after tags */ private boolean mBlankLineAfterTag = true; /** Amount to indent when {@link #tab()} is called. */ private int tabSize = defaultTabSize; }