001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.betwixt.io;
018:
019: import java.beans.IntrospectionException;
020: import java.io.BufferedWriter;
021: import java.io.IOException;
022: import java.io.OutputStream;
023: import java.io.OutputStreamWriter;
024: import java.io.UnsupportedEncodingException;
025: import java.io.Writer;
026:
027: import org.apache.commons.betwixt.XMLUtils;
028: import org.apache.commons.betwixt.strategy.MixedContentEncodingStrategy;
029: import org.apache.commons.logging.Log;
030: import org.apache.commons.logging.LogFactory;
031: import org.xml.sax.Attributes;
032: import org.xml.sax.SAXException;
033:
034: /** <p><code>BeanWriter</code> outputs beans as XML to an io stream.</p>
035: *
036: * <p>The output for each bean is an xml fragment
037: * (rather than a well-formed xml-document).
038: * This allows bean representations to be appended to a document
039: * by writing each in turn to the stream.
040: * So to create a well formed xml document,
041: * you'll need to write the prolog to the stream first.
042: * If you append more than one bean to the stream,
043: * then you'll need to add a wrapping root element as well.
044: *
045: * <p> The line ending to be used is set by {@link #setEndOfLine}.
046: *
047: * <p> The output can be formatted (with whitespace) for easy reading
048: * by calling {@link #enablePrettyPrint}.
049: * The output will be indented.
050: * The indent string used is set by {@link #setIndent}.
051: *
052: * <p> Bean graphs can sometimes contain cycles.
053: * Care must be taken when serializing cyclic bean graphs
054: * since this can lead to infinite recursion.
055: * The approach taken by <code>BeanWriter</code> is to automatically
056: * assign an <code>ID</code> attribute value to beans.
057: * When a cycle is encountered,
058: * an element is written that has the <code>IDREF</code> attribute set to the
059: * id assigned earlier.
060: *
061: * <p> The names of the <code>ID</code> and <code>IDREF</code> attributes used
062: * can be customized by the <code>XMLBeanInfo</code>.
063: * The id's used can also be customized by the user
064: * via <code>IDGenerator</code> subclasses.
065: * The implementation used can be set by the <code>IdGenerator</code> property.
066: * BeanWriter defaults to using <code>SequentialIDGenerator</code>
067: * which supplies id values in numeric sequence.
068: *
069: * <p>If generated <code>ID</code> attribute values are not acceptable in the output,
070: * then this can be disabled by setting the <code>WriteIDs</code> property to false.
071: * If a cyclic reference is encountered in this case then a
072: * <code>CyclicReferenceException</code> will be thrown.
073: * When the <code>WriteIDs</code> property is set to false,
074: * it is recommended that this exception is caught by the caller.
075: *
076: *
077: * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
078: * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
079: */
080: public class BeanWriter extends AbstractBeanWriter {
081:
082: /**
083: * Gets the default EOL string.
084: * @return EOL string, not null
085: */
086: private static final String getEOL() {
087: // just wraps call in an exception check for access restricted environments
088: String result = "\n";
089: try {
090: result = System.getProperty("line.separator", "\n");
091: } catch (SecurityException se) {
092: Log log = LogFactory.getLog(BeanWriter.class);
093: log.warn("Cannot load line separator property: "
094: + se.getMessage());
095: log.trace("Caused by: ", se);
096: }
097: return result;
098: }
099:
100: /** Where the output goes */
101: private Writer writer;
102: /** text used for end of lines. Defaults to <code>\n</code>*/
103: private static final String EOL = getEOL();
104: /** text used for end of lines. Defaults to <code>\n</code>*/
105: private String endOfLine = EOL;
106: /** Initial level of indentation (starts at 1 with the first element by default) */
107: private int initialIndentLevel = 1;
108: /** indentation text */
109: private String indent;
110:
111: /** should we flush after writing bean */
112: private boolean autoFlush;
113: /** Log used for logging (Doh!) */
114: private Log log = LogFactory.getLog(BeanWriter.class);
115: /** Has any content (excluding attributes) been written to the current element */
116: private boolean currentElementIsEmpty = false;
117: /** Has the current element written any body text */
118: private boolean currentElementHasBodyText = false;
119: /** Has the last start tag been closed */
120: private boolean closedStartTag = true;
121: /** Should an end tag be added for empty elements? */
122: private boolean addEndTagForEmptyElement = false;
123: /** Current level of indentation */
124: private int indentLevel;
125: /** USed to determine how body content should be encoded before being output*/
126: private MixedContentEncodingStrategy mixedContentEncodingStrategy = MixedContentEncodingStrategy.DEFAULT;
127:
128: /**
129: * <p> Constructor uses <code>System.out</code> for output.</p>
130: */
131: public BeanWriter() {
132: this (System.out);
133: }
134:
135: /**
136: * <p> Constuctor uses given <code>OutputStream</code> for output.</p>
137: *
138: * @param out write out representations to this stream
139: */
140: public BeanWriter(OutputStream out) {
141: this .writer = new BufferedWriter(new OutputStreamWriter(out));
142: this .autoFlush = true;
143: }
144:
145: /**
146: * <p>Constuctor uses given <code>OutputStream</code> for output
147: * and allows encoding to be set.</p>
148: *
149: * @param out write out representations to this stream
150: * @param enc the name of the encoding to be used. This should be compatible
151: * with the encoding types described in <code>java.io</code>
152: * @throws UnsupportedEncodingException if the given encoding is not supported
153: */
154: public BeanWriter(OutputStream out, String enc)
155: throws UnsupportedEncodingException {
156: this .writer = new BufferedWriter(new OutputStreamWriter(out,
157: enc));
158: this .autoFlush = true;
159: }
160:
161: /**
162: * <p> Constructor sets writer used for output.</p>
163: *
164: * @param writer write out representations to this writer
165: */
166: public BeanWriter(Writer writer) {
167: this .writer = writer;
168: }
169:
170: /**
171: * A helper method that allows you to write the XML Declaration.
172: * This should only be called once before you output any beans.
173: *
174: * @param xmlDeclaration is the XML declaration string typically of
175: * the form "<xml version='1.0' encoding='UTF-8' ?>
176: *
177: * @throws IOException when declaration cannot be written
178: */
179: public void writeXmlDeclaration(String xmlDeclaration)
180: throws IOException {
181: writer.write(xmlDeclaration);
182: printLine();
183: }
184:
185: /**
186: * Allows output to be flushed on the underlying output stream
187: *
188: * @throws IOException when the flush cannot be completed
189: */
190: public void flush() throws IOException {
191: writer.flush();
192: }
193:
194: /**
195: * Closes the underlying output stream
196: *
197: * @throws IOException when writer cannot be closed
198: */
199: public void close() throws IOException {
200: writer.close();
201: }
202:
203: /**
204: * Write the given object to the stream (and then flush).
205: *
206: * @param bean write this <code>Object</code> to the stream
207: * @throws IOException if an IO problem causes failure
208: * @throws SAXException if a SAX problem causes failure
209: * @throws IntrospectionException if bean cannot be introspected
210: */
211: public void write(Object bean) throws IOException, SAXException,
212: IntrospectionException {
213:
214: super .write(bean);
215:
216: if (autoFlush) {
217: writer.flush();
218: }
219: }
220:
221: /**
222: * <p> Switch on formatted output.
223: * This sets the end of line and the indent.
224: * The default is adding 2 spaces and a newline
225: */
226: public void enablePrettyPrint() {
227: endOfLine = EOL;
228: indent = " ";
229: }
230:
231: /**
232: * Gets the string used to mark end of lines.
233: *
234: * @return the string used for end of lines
235: */
236: public String getEndOfLine() {
237: return endOfLine;
238: }
239:
240: /**
241: * Sets the string used for end of lines
242: * Produces a warning the specified value contains an invalid whitespace character
243: *
244: * @param endOfLine the <code>String</code to use
245: */
246: public void setEndOfLine(String endOfLine) {
247: this .endOfLine = endOfLine;
248: for (int i = 0; i < endOfLine.length(); i++) {
249: if (!Character.isWhitespace(endOfLine.charAt(i))) {
250: log.warn("Invalid EndOfLine character(s)");
251: break;
252: }
253: }
254:
255: }
256:
257: /**
258: * Gets the initial indent level
259: *
260: * @return the initial level for indentation
261: * @since 0.8
262: */
263: public int getInitialIndentLevel() {
264: return initialIndentLevel;
265: }
266:
267: /**
268: * Sets the initial indent level used for pretty print indents
269: * @param initialIndentLevel use this <code>int</code> to start with
270: * @since 0.8
271: */
272: public void setInitialIndentLevel(int initialIndentLevel) {
273: this .initialIndentLevel = initialIndentLevel;
274: }
275:
276: /**
277: * Gets the indent string
278: *
279: * @return the string used for indentation
280: */
281: public String getIndent() {
282: return indent;
283: }
284:
285: /**
286: * Sets the string used for pretty print indents
287: * @param indent use this <code>string</code> for indents
288: */
289: public void setIndent(String indent) {
290: this .indent = indent;
291: }
292:
293: /**
294: * <p> Set the log implementation used. </p>
295: *
296: * @return a <code>org.apache.commons.logging.Log</code> level constant
297: */
298: public Log getLog() {
299: return log;
300: }
301:
302: /**
303: * <p> Set the log implementation used. </p>
304: *
305: * @param log <code>Log</code> implementation to use
306: */
307: public void setLog(Log log) {
308: this .log = log;
309: }
310:
311: /**
312: * Gets the encoding strategy for mixed content.
313: * This is used to process body content
314: * before it is written to the textual output.
315: * @return the <code>MixedContentEncodingStrategy</code>, not null
316: * @since 0.5
317: */
318: public MixedContentEncodingStrategy getMixedContentEncodingStrategy() {
319: return mixedContentEncodingStrategy;
320: }
321:
322: /**
323: * Sets the encoding strategy for mixed content.
324: * This is used to process body content
325: * before it is written to the textual output.
326: * @param strategy the <code>MixedContentEncodingStrategy</code>
327: * used to process body content, not null
328: * @since 0.5
329: */
330: public void setMixedContentEncodingStrategy(
331: MixedContentEncodingStrategy strategy) {
332: mixedContentEncodingStrategy = strategy;
333: }
334:
335: /**
336: * <p>Should an end tag be added for each empty element?
337: * </p><p>
338: * When this property is false then empty elements will
339: * be written as <code><<em>element-name</em>/gt;</code>.
340: * When this property is true then empty elements will
341: * be written as <code><<em>element-name</em>gt;
342: * </<em>element-name</em>gt;</code>.
343: * </p>
344: * @return true if an end tag should be added
345: */
346: public boolean isEndTagForEmptyElement() {
347: return addEndTagForEmptyElement;
348: }
349:
350: /**
351: * Sets when an an end tag be added for each empty element.
352: * When this property is false then empty elements will
353: * be written as <code><<em>element-name</em>/gt;</code>.
354: * When this property is true then empty elements will
355: * be written as <code><<em>element-name</em>gt;
356: * </<em>element-name</em>gt;</code>.
357: * @param addEndTagForEmptyElement true if an end tag should be
358: * written for each empty element, false otherwise
359: */
360: public void setEndTagForEmptyElement(
361: boolean addEndTagForEmptyElement) {
362: this .addEndTagForEmptyElement = addEndTagForEmptyElement;
363: }
364:
365: // New API
366: //------------------------------------------------------------------------------
367:
368: /**
369: * Writes the start tag for an element.
370: *
371: * @param uri the element's namespace uri
372: * @param localName the element's local name
373: * @param qualifiedName the element's qualified name
374: * @param attr the element's attributes
375: * @throws IOException if an IO problem occurs during writing
376: * @throws SAXException if an SAX problem occurs during writing
377: * @since 0.5
378: */
379: protected void startElement(WriteContext context, String uri,
380: String localName, String qualifiedName, Attributes attr)
381: throws IOException, SAXException {
382: if (!closedStartTag) {
383: writer.write('>');
384: printLine();
385: }
386:
387: indentLevel++;
388:
389: indent();
390: writer.write('<');
391: writer.write(qualifiedName);
392:
393: for (int i = 0; i < attr.getLength(); i++) {
394: writer.write(' ');
395: writer.write(attr.getQName(i));
396: writer.write("=\"");
397: writer.write(XMLUtils
398: .escapeAttributeValue(attr.getValue(i)));
399: writer.write('\"');
400: }
401: closedStartTag = false;
402: currentElementIsEmpty = true;
403: currentElementHasBodyText = false;
404: }
405:
406: /**
407: * Writes the end tag for an element
408: *
409: * @param uri the element's namespace uri
410: * @param localName the element's local name
411: * @param qualifiedName the element's qualified name
412: *
413: * @throws IOException if an IO problem occurs during writing
414: * @throws SAXException if an SAX problem occurs during writing
415: * @since 0.5
416: */
417: protected void endElement(WriteContext context, String uri,
418: String localName, String qualifiedName) throws IOException,
419: SAXException {
420: if (!addEndTagForEmptyElement && !closedStartTag
421: && currentElementIsEmpty) {
422:
423: writer.write("/>");
424: closedStartTag = true;
425:
426: } else {
427:
428: if (addEndTagForEmptyElement && !closedStartTag) {
429: writer.write(">");
430: closedStartTag = true;
431: } else if (!currentElementHasBodyText) {
432: indent();
433: }
434: writer.write("</");
435: writer.write(qualifiedName);
436: writer.write('>');
437:
438: }
439:
440: indentLevel--;
441: printLine();
442:
443: currentElementHasBodyText = false;
444: }
445:
446: /**
447: * Write element body text
448: *
449: * @param text write out this body text
450: * @throws IOException when the stream write fails
451: * @since 0.5
452: */
453: protected void bodyText(WriteContext context, String text)
454: throws IOException {
455: if (text == null) {
456: // XXX This is probably a programming error
457: log.error("[expressBodyText]Body text is null");
458:
459: } else {
460: if (!closedStartTag) {
461: writer.write('>');
462: closedStartTag = true;
463: }
464: writer.write(mixedContentEncodingStrategy.encode(text,
465: context.getCurrentDescriptor()));
466: currentElementIsEmpty = false;
467: currentElementHasBodyText = true;
468: }
469: }
470:
471: /** Writes out an empty line.
472: * Uses current <code>endOfLine</code>.
473: *
474: * @throws IOException when stream write fails
475: */
476: private void printLine() throws IOException {
477: if (endOfLine != null) {
478: writer.write(endOfLine);
479: }
480: }
481:
482: /**
483: * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
484: *
485: * @throws IOException when stream write fails
486: */
487: private void indent() throws IOException {
488: if (indent != null) {
489: for (int i = 1 - initialIndentLevel; i < indentLevel; i++) {
490: writer.write(getIndent());
491: }
492: }
493: }
494:
495: // OLD API (DEPRECATED)
496: //----------------------------------------------------------------------------
497:
498: /** Writes out an empty line.
499: * Uses current <code>endOfLine</code>.
500: *
501: * @throws IOException when stream write fails
502: * @deprecated 0.5 replaced by new SAX inspired API
503: */
504: protected void writePrintln() throws IOException {
505: if (endOfLine != null) {
506: writer.write(endOfLine);
507: }
508: }
509:
510: /**
511: * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
512: *
513: * @throws IOException when stream write fails
514: * @deprecated 0.5 replaced by new SAX inspired API
515: */
516: protected void writeIndent() throws IOException {
517: if (indent != null) {
518: for (int i = 0; i < indentLevel; i++) {
519: writer.write(getIndent());
520: }
521: }
522: }
523:
524: /**
525: * <p>Escape the <code>toString</code> of the given object.
526: * For use as body text.</p>
527: *
528: * @param value escape <code>value.toString()</code>
529: * @return text with escaped delimiters
530: * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue}
531: */
532: protected String escapeBodyValue(Object value) {
533: return XMLUtils.escapeBodyValue(value);
534: }
535:
536: /**
537: * <p>Escape the <code>toString</code> of the given object.
538: * For use in an attribute value.</p>
539: *
540: * @param value escape <code>value.toString()</code>
541: * @return text with characters restricted (for use in attributes) escaped
542: *
543: * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue}
544: */
545: protected String escapeAttributeValue(Object value) {
546: return XMLUtils.escapeAttributeValue(value);
547: }
548:
549: /**
550: * Express an element tag start using given qualified name
551: *
552: * @param qualifiedName the fully qualified name of the element to write
553: * @throws IOException when stream write fails
554: * @deprecated 0.5 replaced by new SAX inspired API
555: */
556: protected void expressElementStart(String qualifiedName)
557: throws IOException {
558: if (qualifiedName == null) {
559: // XXX this indicates a programming error
560: log.fatal("[expressElementStart]Qualified name is null.");
561: throw new RuntimeException("Qualified name is null.");
562: }
563:
564: writePrintln();
565: writeIndent();
566: writer.write('<');
567: writer.write(qualifiedName);
568: }
569:
570: /**
571: * Write a tag close to the stream
572: *
573: * @throws IOException when stream write fails
574: * @deprecated 0.5 replaced by new SAX inspired API
575: */
576: protected void expressTagClose() throws IOException {
577: writer.write('>');
578: }
579:
580: /**
581: * Write an element end tag to the stream
582: *
583: * @param qualifiedName the name of the element
584: * @throws IOException when stream write fails
585: * @deprecated 0.5 replaced by new SAX inspired API
586: */
587: protected void expressElementEnd(String qualifiedName)
588: throws IOException {
589: if (qualifiedName == null) {
590: // XXX this indicates a programming error
591: log.fatal("[expressElementEnd]Qualified name is null.");
592: throw new RuntimeException("Qualified name is null.");
593: }
594:
595: writer.write("</");
596: writer.write(qualifiedName);
597: writer.write('>');
598: }
599:
600: /**
601: * Write an empty element end to the stream
602: *
603: * @throws IOException when stream write fails
604: * @deprecated 0.5 replaced by new SAX inspired API
605: */
606: protected void expressElementEnd() throws IOException {
607: writer.write("/>");
608: }
609:
610: /**
611: * Write element body text
612: *
613: * @param text write out this body text
614: * @throws IOException when the stream write fails
615: * @deprecated 0.5 replaced by new SAX inspired API
616: */
617: protected void expressBodyText(String text) throws IOException {
618: if (text == null) {
619: // XXX This is probably a programming error
620: log.error("[expressBodyText]Body text is null");
621:
622: } else {
623: writer.write(XMLUtils.escapeBodyValue(text));
624: }
625: }
626:
627: /**
628: * Writes an attribute to the stream.
629: *
630: * @param qualifiedName fully qualified attribute name
631: * @param value attribute value
632: * @throws IOException when the stream write fails
633: * @deprecated 0.5 replaced by new SAX inspired API
634: */
635: protected void expressAttribute(String qualifiedName, String value)
636: throws IOException {
637: if (value == null) {
638: // XXX probably a programming error
639: log.error("Null attribute value.");
640: return;
641: }
642:
643: if (qualifiedName == null) {
644: // XXX probably a programming error
645: log.error("Null attribute value.");
646: return;
647: }
648:
649: writer.write(' ');
650: writer.write(qualifiedName);
651: writer.write("=\"");
652: writer.write(XMLUtils.escapeAttributeValue(value));
653: writer.write('\"');
654: }
655:
656: }
|