001: /* $Id: IndentingXMLEventWriter.java,v 1.2 2004/06/24 22:06:11 kohsuke Exp $
002: *
003: * Copyright (c) 2004, Sun Microsystems, Inc.
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * * Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * * Redistributions in binary form must reproduce the above
014: * copyright notice, this list of conditions and the following
015: * disclaimer in the documentation and/or other materials provided
016: * with the distribution.
017: *
018: * * Neither the name of Sun Microsystems, Inc. nor the names of its
019: * contributors may be used to endorse or promote products derived
020: * from this software without specific prior written permission.
021: *
022: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
023: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
024: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
025: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033: */
034: package javanet.staxutils;
035:
036: import java.io.IOException;
037: import java.io.Writer;
038: import javax.xml.namespace.NamespaceContext;
039: import javax.xml.namespace.QName;
040: import javax.xml.stream.Location;
041: import javax.xml.stream.XMLEventReader;
042: import javax.xml.stream.XMLEventWriter;
043: import javax.xml.stream.XMLStreamConstants;
044: import javax.xml.stream.XMLStreamException;
045: import javax.xml.stream.events.Characters;
046: import javax.xml.stream.events.EndElement;
047: import javax.xml.stream.events.StartElement;
048: import javax.xml.stream.events.XMLEvent;
049:
050: /**
051: * Wraps another {@link XMLEventWriter} and does the indentation.
052: *
053: * <p>
054: * {@link XMLEventWriter} API doesn't provide any portable way of
055: * doing pretty-printing. This {@link XMLEventWriter} filter provides
056: * a portable indentation support by wrapping another {@link XMLEventWriter}
057: * and adding proper {@link Characters} event for indentation.
058: *
059: * <p>
060: * Because whitespace handling in XML is tricky, this is not an
061: * one-size-fit-all indentation engine. Instead, this class is
062: * focused on handling so-called "data-oritented XML" like follows:
063: *
064: * <pre><xmp>
065: * <cards>
066: * <card id="kk.152">
067: * <firstName>Kohsuke</firstName>
068: * <lastName>Kawaguchi</lastName>
069: * </card>
070: * </cards>
071: * </xmp></pre>
072: *
073: * <p>
074: * We'll discuss more about the supported subset of XML later.
075: *
076: * <p>
077: * To use this engine, do as follows:
078: * <pre>
079: * {@link XMLEventWriter} w = xmlOutputFactory.createXMLEventWriter(...);
080: * w = new {@link IndentingXMLEventWriter}(w);
081: *
082: * // start writing
083: * </pre>
084: *
085: * <p>
086: * Use {@link #setIndent(String)} and {@link #setNewLine(String)} to
087: * control the indentation if you want.
088: *
089: *
090: * <h2>What Subset Does This Support?</h2>
091: * <p>
092: * This engine works when the content model of each element is either
093: * element-only or #PCDATA (but not mixed content model.) IOW, it
094: * assumes that the children of any element is either (a) only elements
095: * and no #PCDATA or (b) #PCDATA only and no elements.
096: *
097: * <p>
098: * The engine also tries to handle comments, PIs, and a DOCTYPE decl,
099: * but in general it works only when those things appear in the
100: * element-only content model.
101: *
102: *
103: * <h2>For Maintainers</h2>
104: * <p>
105: * Please don't try to make this class into an almighty indentation class.
106: * I've seen it attempted in Xerces and it's not gonna be pretty.
107: *
108: * <p>
109: * If you come up with an idea of another pretty-printer
110: * that supports another subset, please go ahead and write your own class.
111: *
112: *
113: *
114: * @author
115: * Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
116: */
117: public class IndentingXMLEventWriter implements XMLEventWriter {
118: private final XMLEventWriter core;
119:
120: /**
121: * String used for indentation.
122: */
123: private String indent = " ";
124:
125: /**
126: * String for EOL.
127: */
128: private String newLine;
129:
130: /**
131: * Current nest level.
132: */
133: private int depth = 0;
134:
135: /**
136: * True if the current element has text.
137: */
138: private boolean hasText;
139:
140: /**
141: * {@link XMLEvent} constant that returns the {@link #newLine}.
142: */
143: private final Characters newLineEvent = new CharactersImpl() {
144: public String getData() {
145: return newLine;
146: }
147: };
148:
149: /**
150: * {@link XMLEvent} constant that returns the {@link #indent}.
151: */
152: private final Characters indentEvent = new CharactersImpl() {
153: public String getData() {
154: return indent;
155: }
156: };
157:
158: /**
159: * Partial implementation of {@link Characters} event.
160: */
161: private static abstract class CharactersImpl implements Characters {
162:
163: public boolean isWhiteSpace() {
164: return true;
165: }
166:
167: public boolean isCData() {
168: return false;
169: }
170:
171: public boolean isIgnorableWhiteSpace() {
172: // this is hard call. On one hand, we want the indentation to
173: // get through whatever pipeline, so we are tempted to return false.
174: // also DTD isn't necessarily present.
175: //
176: // But on the other hand, this IS an ignorable whitespace
177: // in its intended meaning.
178: return true;
179: }
180:
181: public int getEventType() {
182: // it's not clear if we are supposed to return SPACES
183: return XMLStreamConstants.CHARACTERS;
184: }
185:
186: public Location getLocation() {
187: // spec isn't clear if we can return null, but it doesn't say we can't.
188: return null;
189: }
190:
191: public boolean isStartElement() {
192: return false;
193: }
194:
195: public boolean isAttribute() {
196: return false;
197: }
198:
199: public boolean isNamespace() {
200: return false;
201: }
202:
203: public boolean isEndElement() {
204: return false;
205: }
206:
207: public boolean isEntityReference() {
208: return false;
209: }
210:
211: public boolean isProcessingInstruction() {
212: return false;
213: }
214:
215: public boolean isCharacters() {
216: return true;
217: }
218:
219: public boolean isStartDocument() {
220: return false;
221: }
222:
223: public boolean isEndDocument() {
224: return false;
225: }
226:
227: public StartElement asStartElement() {
228: return null;
229: }
230:
231: public EndElement asEndElement() {
232: return null;
233: }
234:
235: public Characters asCharacters() {
236: return this ;
237: }
238:
239: public QName getSchemaType() {
240: return null;
241: }
242:
243: public void writeAsEncodedUnicode(Writer writer)
244: throws XMLStreamException {
245: try {
246: // technically we need to do escaping, for we allow
247: // any characters to be used for indent and newLine.
248: // but in practice, who'll use something other than 0x20,0x0D,0x0A,0x08?
249: writer.write(getData());
250: } catch (IOException e) {
251: throw new XMLStreamException(e);
252: }
253: }
254:
255: };
256:
257: public IndentingXMLEventWriter(XMLEventWriter core) {
258: if (core == null)
259: throw new IllegalArgumentException();
260: this .core = core;
261:
262: // get the default line separator
263: try {
264: newLine = System.getProperty("line.separator");
265: } catch (SecurityException e) {
266: // use '\n' if we can't figure it out
267: newLine = "\n";
268: }
269: }
270:
271: /**
272: * Returns the string used for indentation.
273: */
274: public String getIndent() {
275: return indent;
276: }
277:
278: /**
279: * Sets the string used for indentation.
280: *
281: * <p>
282: * By default, this is set to two space chars.
283: *
284: * @param indent
285: * A string like " ", "\\t". Must not be null.
286: */
287: public void setIndent(String indent) {
288: if (indent == null)
289: throw new IllegalArgumentException();
290: this .indent = indent;
291: }
292:
293: /**
294: * Returns the string used for newline.
295: */
296: public String getNewLine() {
297: return newLine;
298: }
299:
300: /**
301: * Sets the string used for newline.
302: *
303: * <p>
304: * By default, this is set to the platform default new line.
305: *
306: * @param newLine
307: * A string like "\\n" or "\\r\\n". Must not be null.
308: */
309: public void setNewLine(String newLine) {
310: if (newLine == null)
311: throw new IllegalArgumentException();
312: this .newLine = newLine;
313: }
314:
315: public void add(XMLEvent event) throws XMLStreamException {
316: switch (event.getEventType()) {
317: case XMLStreamConstants.CHARACTERS:
318: case XMLStreamConstants.CDATA:
319: case XMLStreamConstants.SPACE:
320: if (event.asCharacters().isWhiteSpace())
321: // skip any indentation given by the client
322: // we are running the risk of ignoring the non-ignorable
323: // significant whitespaces, but that's a risk explained
324: // in the class javadoc.
325: return;
326:
327: hasText = true;
328: core.add(event);
329: return;
330:
331: case XMLStreamConstants.START_ELEMENT:
332: newLine();
333: core.add(event);
334: hasText = false;
335: depth++;
336: return;
337:
338: case XMLStreamConstants.END_ELEMENT:
339: depth--;
340: if (!hasText) {
341: newLine();
342: }
343: core.add(event);
344: hasText = false;
345: return;
346:
347: case XMLStreamConstants.PROCESSING_INSTRUCTION:
348: case XMLStreamConstants.COMMENT:
349: case XMLStreamConstants.DTD:
350: // those things can be mixed with text,
351: // and at this point we don't know if text follows this
352: // like <foo><?pi?>text</foo>
353: //
354: // but we make a bold assumption that the those primitives
355: // only appear as a part of the element-only content model.
356: // so we always indent them as:
357: // <foo>
358: // <?pi?>
359: // ...
360: // </foo>
361: if (!hasText) {
362: // if we know that we already had a text, I see no
363: // reason to indent
364: newLine();
365: }
366: core.add(event);
367: return;
368:
369: case XMLStreamConstants.END_DOCUMENT:
370: core.add(event);
371: // some implementation does the buffering by default,
372: // and it prevents the output from appearing.
373: // this has been a confusion for many people.
374: // calling flush wouldn't hurt decent impls, and it
375: // prevent such unnecessary confusion.
376: flush();
377: break;
378:
379: default:
380: core.add(event);
381: return;
382: }
383:
384: }
385:
386: /**
387: * Prints out a new line and indent.
388: */
389: private void newLine() throws XMLStreamException {
390: core.add(newLineEvent);
391: for (int i = 0; i < depth; i++)
392: core.add(indentEvent);
393: }
394:
395: public void add(XMLEventReader reader) throws XMLStreamException {
396: // we can't just delegate to the core
397: // because we need to do indentation.
398: if (reader == null)
399: throw new IllegalArgumentException();
400: while (reader.hasNext()) {
401: add(reader.nextEvent());
402: }
403: }
404:
405: public void close() throws XMLStreamException {
406: core.close();
407: }
408:
409: public void flush() throws XMLStreamException {
410: core.flush();
411: }
412:
413: public NamespaceContext getNamespaceContext() {
414: return core.getNamespaceContext();
415: }
416:
417: public String getPrefix(String uri) throws XMLStreamException {
418: return core.getPrefix(uri);
419: }
420:
421: public void setDefaultNamespace(String uri)
422: throws XMLStreamException {
423: core.setDefaultNamespace(uri);
424: }
425:
426: public void setNamespaceContext(NamespaceContext context)
427: throws XMLStreamException {
428: core.setNamespaceContext(context);
429: }
430:
431: public void setPrefix(String prefix, String uri)
432: throws XMLStreamException {
433: core.setPrefix(prefix, uri);
434: }
435: }
|