001: /*
002: * Copyright (C) 2004, 2005, 2006 Joe Walnes.
003: * Copyright (C) 2006, 2007, 2008 XStream Committers.
004: * All rights reserved.
005: *
006: * The software in this package is published under the terms of the BSD
007: * style license a copy of which has been included with this distribution in
008: * the LICENSE.txt file.
009: *
010: * Created on 07. March 2004 by Joe Walnes
011: */
012: package com.thoughtworks.xstream.io.xml;
013:
014: import com.thoughtworks.xstream.core.util.FastStack;
015: import com.thoughtworks.xstream.core.util.QuickWriter;
016: import com.thoughtworks.xstream.io.StreamException;
017:
018: import java.io.Writer;
019:
020: /**
021: * A simple writer that outputs XML in a pretty-printed indented stream.
022: * <p>
023: * By default, the chars <code><pre>
024: * & < > " ' \r
025: * </pre></code> are escaped and replaced with a suitable XML entity. To alter this behavior, override
026: * the the {@link #writeText(com.thoughtworks.xstream.core.util.QuickWriter, String)} and
027: * {@link #writeAttributeValue(com.thoughtworks.xstream.core.util.QuickWriter, String)} methods.
028: * </p>
029: * <p>
030: * Note: Depending on the XML version some characters cannot be written. Especially a 0
031: * character is never valid in XML, neither directly nor as entity nor within CDATA. However, this writer
032: * works by default in a quirks mode, where it will write any character at least as character entity (even
033: * a null character). You may switch into XML_1_1 mode (which supports most characters) or XML_1_0
034: * that does only support a very limited number of control characters. See XML specification for version
035: * <a href="http://www.w3.org/TR/2006/REC-xml-20060816/#charsets">1.0</a> or
036: * <a href="http://www.w3.org/TR/2006/REC-xml11-20060816/#charsets">1.1</a>. If a character is
037: * not supported, a {@link StreamException} is thrown. Select a proper parser implementation that
038: * respects the version in the XML header (the Xpp3 parser will also read character entities of normally
039: * invalid characters).
040: * </p>
041: *
042: * @author Joe Walnes
043: * @author Jörg Schaible
044: */
045: public class PrettyPrintWriter extends AbstractXmlWriter {
046:
047: public static int XML_QUIRKS = -1;
048: public static int XML_1_0 = 0;
049: public static int XML_1_1 = 1;
050:
051: private final QuickWriter writer;
052: private final FastStack elementStack = new FastStack(16);
053: private final char[] lineIndenter;
054: private final int mode;
055:
056: private boolean tagInProgress;
057: protected int depth;
058: private boolean readyForNewLine;
059: private boolean tagIsEmpty;
060: private String newLine;
061:
062: private static final char[] NULL = "�".toCharArray();
063: private static final char[] AMP = "&".toCharArray();
064: private static final char[] LT = "<".toCharArray();
065: private static final char[] GT = ">".toCharArray();
066: private static final char[] CR = "
".toCharArray();
067: private static final char[] QUOT = """.toCharArray();
068: private static final char[] APOS = "'".toCharArray();
069: private static final char[] CLOSE = "</".toCharArray();
070:
071: private PrettyPrintWriter(Writer writer, int mode,
072: char[] lineIndenter, XmlFriendlyReplacer replacer,
073: String newLine) {
074: super (replacer);
075: this .writer = new QuickWriter(writer);
076: this .lineIndenter = lineIndenter;
077: this .newLine = newLine;
078: this .mode = mode;
079: if (mode < XML_QUIRKS || mode > XML_1_1) {
080: throw new IllegalArgumentException("Not a valid XML mode");
081: }
082: }
083:
084: /**
085: * @since 1.2
086: * @deprecated since 1.3
087: */
088: public PrettyPrintWriter(Writer writer, char[] lineIndenter,
089: String newLine, XmlFriendlyReplacer replacer) {
090: this (writer, XML_QUIRKS, lineIndenter, replacer, newLine);
091: }
092:
093: /**
094: * @since 1.3
095: */
096: public PrettyPrintWriter(Writer writer, int mode,
097: char[] lineIndenter, XmlFriendlyReplacer replacer) {
098: this (writer, mode, lineIndenter, replacer, "\n");
099: }
100:
101: /**
102: * @deprecated since 1.3
103: */
104: public PrettyPrintWriter(Writer writer, char[] lineIndenter,
105: String newLine) {
106: this (writer, lineIndenter, newLine, new XmlFriendlyReplacer());
107: }
108:
109: /**
110: * @since 1.3
111: */
112: public PrettyPrintWriter(Writer writer, int mode,
113: char[] lineIndenter) {
114: this (writer, mode, lineIndenter, new XmlFriendlyReplacer());
115: }
116:
117: public PrettyPrintWriter(Writer writer, char[] lineIndenter) {
118: this (writer, lineIndenter, "\n");
119: }
120:
121: /**
122: * @deprecated since 1.3
123: */
124: public PrettyPrintWriter(Writer writer, String lineIndenter,
125: String newLine) {
126: this (writer, lineIndenter.toCharArray(), newLine);
127: }
128:
129: /**
130: * @since 1.3
131: */
132: public PrettyPrintWriter(Writer writer, int mode,
133: String lineIndenter) {
134: this (writer, mode, lineIndenter.toCharArray());
135: }
136:
137: public PrettyPrintWriter(Writer writer, String lineIndenter) {
138: this (writer, lineIndenter.toCharArray());
139: }
140:
141: /**
142: * @since 1.3
143: */
144: public PrettyPrintWriter(Writer writer, int mode,
145: XmlFriendlyReplacer replacer) {
146: this (writer, mode, new char[] { ' ', ' ' }, replacer);
147: }
148:
149: public PrettyPrintWriter(Writer writer, XmlFriendlyReplacer replacer) {
150: this (writer, new char[] { ' ', ' ' }, "\n", replacer);
151: }
152:
153: /**
154: * @since 1.3
155: */
156: public PrettyPrintWriter(Writer writer, int mode) {
157: this (writer, mode, new char[] { ' ', ' ' });
158: }
159:
160: public PrettyPrintWriter(Writer writer) {
161: this (writer, new char[] { ' ', ' ' });
162: }
163:
164: public void startNode(String name) {
165: String escapedName = escapeXmlName(name);
166: tagIsEmpty = false;
167: finishTag();
168: writer.write('<');
169: writer.write(escapedName);
170: elementStack.push(escapedName);
171: tagInProgress = true;
172: depth++;
173: readyForNewLine = true;
174: tagIsEmpty = true;
175: }
176:
177: public void startNode(String name, Class clazz) {
178: startNode(name);
179: }
180:
181: public void setValue(String text) {
182: readyForNewLine = false;
183: tagIsEmpty = false;
184: finishTag();
185:
186: writeText(writer, text);
187: }
188:
189: public void addAttribute(String key, String value) {
190: writer.write(' ');
191: writer.write(escapeXmlName(key));
192: writer.write('=');
193: writer.write('\"');
194: writeAttributeValue(writer, value);
195: writer.write('\"');
196: }
197:
198: protected void writeAttributeValue(QuickWriter writer, String text) {
199: writeText(text);
200: }
201:
202: protected void writeText(QuickWriter writer, String text) {
203: writeText(text);
204: }
205:
206: private void writeText(String text) {
207: int length = text.length();
208: for (int i = 0; i < length; i++) {
209: char c = text.charAt(i);
210: switch (c) {
211: case '\0':
212: if (mode == XML_QUIRKS) {
213: this .writer.write(NULL);
214: } else {
215: throw new StreamException(
216: "Invalid character 0x0 in XML stream");
217: }
218: break;
219: case '&':
220: this .writer.write(AMP);
221: break;
222: case '<':
223: this .writer.write(LT);
224: break;
225: case '>':
226: this .writer.write(GT);
227: break;
228: case '"':
229: this .writer.write(QUOT);
230: break;
231: case '\'':
232: this .writer.write(APOS);
233: break;
234: case '\r':
235: this .writer.write(CR);
236: break;
237: case '\t':
238: case '\n':
239: this .writer.write(c);
240: break;
241: default:
242: if (Character.isDefined(c)
243: && !Character.isISOControl(c)) {
244: if (mode != XML_QUIRKS) {
245: if (c > '\ud7ff' && c < '\ue000') {
246: throw new StreamException(
247: "Invalid character 0x"
248: + Integer.toHexString(c)
249: + " in XML stream");
250: }
251: }
252: this .writer.write(c);
253: } else {
254: if (mode == XML_1_0) {
255: if (c < 9 || c == '\u000b' || c == '\u000c'
256: || c == '\u000e' || c == '\u000f') {
257: throw new StreamException(
258: "Invalid character 0x"
259: + Integer.toHexString(c)
260: + " in XML 1.0 stream");
261: }
262: }
263: if (mode != XML_QUIRKS) {
264: if (c == '\ufffe' || c == '\uffff') {
265: throw new StreamException(
266: "Invalid character 0x"
267: + Integer.toHexString(c)
268: + " in XML stream");
269: }
270: }
271: this .writer.write("&#x");
272: this .writer.write(Integer.toHexString(c));
273: this .writer.write(';');
274: }
275: }
276: }
277: }
278:
279: public void endNode() {
280: depth--;
281: if (tagIsEmpty) {
282: writer.write('/');
283: readyForNewLine = false;
284: finishTag();
285: elementStack.popSilently();
286: } else {
287: finishTag();
288: writer.write(CLOSE);
289: writer.write((String) elementStack.pop());
290: writer.write('>');
291: }
292: readyForNewLine = true;
293: if (depth == 0) {
294: writer.flush();
295: }
296: }
297:
298: private void finishTag() {
299: if (tagInProgress) {
300: writer.write('>');
301: }
302: tagInProgress = false;
303: if (readyForNewLine) {
304: endOfLine();
305: }
306: readyForNewLine = false;
307: tagIsEmpty = false;
308: }
309:
310: protected void endOfLine() {
311: writer.write(getNewLine());
312: for (int i = 0; i < depth; i++) {
313: writer.write(lineIndenter);
314: }
315: }
316:
317: public void flush() {
318: writer.flush();
319: }
320:
321: public void close() {
322: writer.close();
323: }
324:
325: protected String getNewLine() {
326: return newLine;
327: }
328: }
|