1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.betwixt.io; 18 19 import java.beans.IntrospectionException; 20 import java.io.BufferedWriter; 21 import java.io.IOException; 22 import java.io.OutputStream; 23 import java.io.OutputStreamWriter; 24 import java.io.UnsupportedEncodingException; 25 import java.io.Writer; 26 27 import org.apache.commons.betwixt.XMLUtils; 28 import org.apache.commons.betwixt.strategy.MixedContentEncodingStrategy; 29 import org.apache.commons.logging.Log; 30 import org.apache.commons.logging.LogFactory; 31 import org.xml.sax.Attributes; 32 import org.xml.sax.SAXException; 33 34 /** <p><code>BeanWriter</code> outputs beans as XML to an io stream.</p> 35 * 36 * <p>The output for each bean is an xml fragment 37 * (rather than a well-formed xml-document). 38 * This allows bean representations to be appended to a document 39 * by writing each in turn to the stream. 40 * So to create a well formed xml document, 41 * you'll need to write the prolog to the stream first. 42 * If you append more than one bean to the stream, 43 * then you'll need to add a wrapping root element as well. 44 * 45 * <p> The line ending to be used is set by {@link #setEndOfLine}. 46 * 47 * <p> The output can be formatted (with whitespace) for easy reading 48 * by calling {@link #enablePrettyPrint}. 49 * The output will be indented. 50 * The indent string used is set by {@link #setIndent}. 51 * 52 * <p> Bean graphs can sometimes contain cycles. 53 * Care must be taken when serializing cyclic bean graphs 54 * since this can lead to infinite recursion. 55 * The approach taken by <code>BeanWriter</code> is to automatically 56 * assign an <code>ID</code> attribute value to beans. 57 * When a cycle is encountered, 58 * an element is written that has the <code>IDREF</code> attribute set to the 59 * id assigned earlier. 60 * 61 * <p> The names of the <code>ID</code> and <code>IDREF</code> attributes used 62 * can be customized by the <code>XMLBeanInfo</code>. 63 * The id's used can also be customized by the user 64 * via <code>IDGenerator</code> subclasses. 65 * The implementation used can be set by the <code>IdGenerator</code> property. 66 * BeanWriter defaults to using <code>SequentialIDGenerator</code> 67 * which supplies id values in numeric sequence. 68 * 69 * <p>If generated <code>ID</code> attribute values are not acceptable in the output, 70 * then this can be disabled by setting the <code>WriteIDs</code> property to false. 71 * If a cyclic reference is encountered in this case then a 72 * <code>CyclicReferenceException</code> will be thrown. 73 * When the <code>WriteIDs</code> property is set to false, 74 * it is recommended that this exception is caught by the caller. 75 * 76 * 77 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a> 78 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a> 79 */ 80 public class BeanWriter extends AbstractBeanWriter { 81 82 /** 83 * Gets the default EOL string. 84 * @return EOL string, not null 85 */ 86 private static final String getEOL() { 87 // just wraps call in an exception check for access restricted environments 88 String result = "\n"; 89 try { 90 result = System.getProperty( "line.separator", "\n" ); 91 } catch (SecurityException se) { 92 Log log = LogFactory.getLog( BeanWriter.class ); 93 log.warn("Cannot load line separator property: " + se.getMessage()); 94 log.trace("Caused by: ", se); 95 } 96 return result; 97 } 98 99 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 127 = MixedContentEncodingStrategy.DEFAULT; 128 129 /** 130 * <p> Constructor uses <code>System.out</code> for output.</p> 131 */ 132 public BeanWriter() { 133 this( System.out ); 134 } 135 136 /** 137 * <p> Constuctor uses given <code>OutputStream</code> for output.</p> 138 * 139 * @param out write out representations to this stream 140 */ 141 public BeanWriter(OutputStream out) { 142 this.writer = new BufferedWriter( new OutputStreamWriter( out ) ); 143 this.autoFlush = true; 144 } 145 146 /** 147 * <p>Constuctor uses given <code>OutputStream</code> for output 148 * and allows encoding to be set.</p> 149 * 150 * @param out write out representations to this stream 151 * @param enc the name of the encoding to be used. This should be compatible 152 * with the encoding types described in <code>java.io</code> 153 * @throws UnsupportedEncodingException if the given encoding is not supported 154 */ 155 public BeanWriter(OutputStream out, String enc) throws UnsupportedEncodingException { 156 this.writer = new BufferedWriter( new OutputStreamWriter( out, enc ) ); 157 this.autoFlush = true; 158 } 159 160 /** 161 * <p> Constructor sets writer used for output.</p> 162 * 163 * @param writer write out representations to this writer 164 */ 165 public BeanWriter(Writer writer) { 166 this.writer = writer; 167 } 168 169 /** 170 * A helper method that allows you to write the XML Declaration. 171 * This should only be called once before you output any beans. 172 * 173 * @param xmlDeclaration is the XML declaration string typically of 174 * the form "<xml version='1.0' encoding='UTF-8' ?> 175 * 176 * @throws IOException when declaration cannot be written 177 */ 178 public void writeXmlDeclaration(String xmlDeclaration) throws IOException { 179 writer.write( xmlDeclaration ); 180 printLine(); 181 } 182 183 /** 184 * Allows output to be flushed on the underlying output stream 185 * 186 * @throws IOException when the flush cannot be completed 187 */ 188 public void flush() throws IOException { 189 writer.flush(); 190 } 191 192 /** 193 * Closes the underlying output stream 194 * 195 * @throws IOException when writer cannot be closed 196 */ 197 public void close() throws IOException { 198 writer.close(); 199 } 200 201 /** 202 * Write the given object to the stream (and then flush). 203 * 204 * @param bean write this <code>Object</code> to the stream 205 * @throws IOException if an IO problem causes failure 206 * @throws SAXException if a SAX problem causes failure 207 * @throws IntrospectionException if bean cannot be introspected 208 */ 209 public void write(Object bean) throws IOException, SAXException, IntrospectionException { 210 211 super.write(bean); 212 213 if ( autoFlush ) { 214 writer.flush(); 215 } 216 } 217 218 219 /** 220 * <p> Switch on formatted output. 221 * This sets the end of line and the indent. 222 * The default is adding 2 spaces and a newline 223 */ 224 public void enablePrettyPrint() { 225 endOfLine = EOL; 226 indent = " "; 227 } 228 229 /** 230 * Gets the string used to mark end of lines. 231 * 232 * @return the string used for end of lines 233 */ 234 public String getEndOfLine() { 235 return endOfLine; 236 } 237 238 /** 239 * Sets the string used for end of lines 240 * Produces a warning the specified value contains an invalid whitespace character 241 * 242 * @param endOfLine the <code>String</code to use 243 */ 244 public void setEndOfLine(String endOfLine) { 245 this.endOfLine = endOfLine; 246 for (int i = 0; i < endOfLine.length(); i++) { 247 if (!Character.isWhitespace(endOfLine.charAt(i))) { 248 log.warn("Invalid EndOfLine character(s)"); 249 break; 250 } 251 } 252 253 } 254 255 /** 256 * Gets the initial indent level 257 * 258 * @return the initial level for indentation 259 * @since 0.8 260 */ 261 public int getInitialIndentLevel() { 262 return initialIndentLevel; 263 } 264 265 /** 266 * Sets the initial indent level used for pretty print indents 267 * @param initialIndentLevel use this <code>int</code> to start with 268 * @since 0.8 269 */ 270 public void setInitialIndentLevel(int initialIndentLevel) { 271 this.initialIndentLevel = initialIndentLevel; 272 } 273 274 275 /** 276 * Gets the indent string 277 * 278 * @return the string used for indentation 279 */ 280 public String getIndent() { 281 return indent; 282 } 283 284 /** 285 * Sets the string used for pretty print indents 286 * @param indent use this <code>string</code> for indents 287 */ 288 public void setIndent(String indent) { 289 this.indent = indent; 290 } 291 292 /** 293 * <p> Set the log implementation used. </p> 294 * 295 * @return a <code>org.apache.commons.logging.Log</code> level constant 296 */ 297 public Log getLog() { 298 return log; 299 } 300 301 /** 302 * <p> Set the log implementation used. </p> 303 * 304 * @param log <code>Log</code> implementation to use 305 */ 306 public void setLog( Log log ) { 307 this.log = log; 308 } 309 310 /** 311 * Gets the encoding strategy for mixed content. 312 * This is used to process body content 313 * before it is written to the textual output. 314 * @return the <code>MixedContentEncodingStrategy</code>, not null 315 * @since 0.5 316 */ 317 public MixedContentEncodingStrategy getMixedContentEncodingStrategy() { 318 return mixedContentEncodingStrategy; 319 } 320 321 /** 322 * Sets the encoding strategy for mixed content. 323 * This is used to process body content 324 * before it is written to the textual output. 325 * @param strategy the <code>MixedContentEncodingStrategy</code> 326 * used to process body content, not null 327 * @since 0.5 328 */ 329 public void setMixedContentEncodingStrategy(MixedContentEncodingStrategy strategy) { 330 mixedContentEncodingStrategy = strategy; 331 } 332 333 /** 334 * <p>Should an end tag be added for each empty element? 335 * </p><p> 336 * When this property is false then empty elements will 337 * be written as <code><<em>element-name</em>/gt;</code>. 338 * When this property is true then empty elements will 339 * be written as <code><<em>element-name</em>gt; 340 * </<em>element-name</em>gt;</code>. 341 * </p> 342 * @return true if an end tag should be added 343 */ 344 public boolean isEndTagForEmptyElement() { 345 return addEndTagForEmptyElement; 346 } 347 348 /** 349 * Sets when an an end tag be added for each empty element. 350 * When this property is false then empty elements will 351 * be written as <code><<em>element-name</em>/gt;</code>. 352 * When this property is true then empty elements will 353 * be written as <code><<em>element-name</em>gt; 354 * </<em>element-name</em>gt;</code>. 355 * @param addEndTagForEmptyElement true if an end tag should be 356 * written for each empty element, false otherwise 357 */ 358 public void setEndTagForEmptyElement(boolean addEndTagForEmptyElement) { 359 this.addEndTagForEmptyElement = addEndTagForEmptyElement; 360 } 361 362 363 364 // New API 365 //------------------------------------------------------------------------------ 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( 380 WriteContext context, 381 String uri, 382 String localName, 383 String qualifiedName, 384 Attributes attr) 385 throws 386 IOException, 387 SAXException { 388 if ( !closedStartTag ) { 389 writer.write( '>' ); 390 printLine(); 391 } 392 393 indentLevel++; 394 395 indent(); 396 writer.write( '<' ); 397 writer.write( qualifiedName ); 398 399 for ( int i=0; i< attr.getLength(); i++ ) { 400 writer.write( ' ' ); 401 writer.write( attr.getQName(i) ); 402 writer.write( "=\"" ); 403 writer.write( XMLUtils.escapeAttributeValue( attr.getValue(i) ) ); 404 writer.write( '\"' ); 405 } 406 closedStartTag = false; 407 currentElementIsEmpty = true; 408 currentElementHasBodyText = false; 409 } 410 411 /** 412 * Writes the end tag for an element 413 * 414 * @param uri the element's namespace uri 415 * @param localName the element's local name 416 * @param qualifiedName the element's qualified name 417 * 418 * @throws IOException if an IO problem occurs during writing 419 * @throws SAXException if an SAX problem occurs during writing 420 * @since 0.5 421 */ 422 protected void endElement( 423 WriteContext context, 424 String uri, 425 String localName, 426 String qualifiedName) 427 throws 428 IOException, 429 SAXException { 430 if ( 431 !addEndTagForEmptyElement 432 && !closedStartTag 433 && currentElementIsEmpty ) { 434 435 writer.write( "/>" ); 436 closedStartTag = true; 437 438 } else { 439 440 if ( 441 addEndTagForEmptyElement 442 && !closedStartTag ) { 443 writer.write( ">" ); 444 closedStartTag = true; 445 } 446 else if (!currentElementHasBodyText) { 447 indent(); 448 } 449 writer.write( "</" ); 450 writer.write( qualifiedName ); 451 writer.write( '>' ); 452 453 } 454 455 indentLevel--; 456 printLine(); 457 458 currentElementHasBodyText = false; 459 } 460 461 /** 462 * Write element body text 463 * 464 * @param text write out this body text 465 * @throws IOException when the stream write fails 466 * @since 0.5 467 */ 468 protected void bodyText(WriteContext context, String text) throws IOException { 469 if ( text == null ) { 470 // XXX This is probably a programming error 471 log.error( "[expressBodyText]Body text is null" ); 472 473 } else { 474 if ( !closedStartTag ) { 475 writer.write( '>' ); 476 closedStartTag = true; 477 } 478 writer.write( 479 mixedContentEncodingStrategy.encode( 480 text, 481 context.getCurrentDescriptor()) ); 482 currentElementIsEmpty = false; 483 currentElementHasBodyText = true; 484 } 485 } 486 487 /** Writes out an empty line. 488 * Uses current <code>endOfLine</code>. 489 * 490 * @throws IOException when stream write fails 491 */ 492 private void printLine() throws IOException { 493 if ( endOfLine != null ) { 494 writer.write( endOfLine ); 495 } 496 } 497 498 /** 499 * Writes out <code>indent</code>'s to the current <code>indentLevel</code> 500 * 501 * @throws IOException when stream write fails 502 */ 503 private void indent() throws IOException { 504 if ( indent != null ) { 505 for ( int i = 1 - initialIndentLevel; i < indentLevel; i++ ) { 506 writer.write( getIndent() ); 507 } 508 } 509 } 510 511 // OLD API (DEPRECATED) 512 //---------------------------------------------------------------------------- 513 514 515 /** Writes out an empty line. 516 * Uses current <code>endOfLine</code>. 517 * 518 * @throws IOException when stream write fails 519 * @deprecated 0.5 replaced by new SAX inspired API 520 */ 521 protected void writePrintln() throws IOException { 522 if ( endOfLine != null ) { 523 writer.write( endOfLine ); 524 } 525 } 526 527 /** 528 * Writes out <code>indent</code>'s to the current <code>indentLevel</code> 529 * 530 * @throws IOException when stream write fails 531 * @deprecated 0.5 replaced by new SAX inspired API 532 */ 533 protected void writeIndent() throws IOException { 534 if ( indent != null ) { 535 for ( int i = 0; i < indentLevel; i++ ) { 536 writer.write( getIndent() ); 537 } 538 } 539 } 540 541 /** 542 * <p>Escape the <code>toString</code> of the given object. 543 * For use as body text.</p> 544 * 545 * @param value escape <code>value.toString()</code> 546 * @return text with escaped delimiters 547 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue} 548 */ 549 protected String escapeBodyValue(Object value) { 550 return XMLUtils.escapeBodyValue(value); 551 } 552 553 /** 554 * <p>Escape the <code>toString</code> of the given object. 555 * For use in an attribute value.</p> 556 * 557 * @param value escape <code>value.toString()</code> 558 * @return text with characters restricted (for use in attributes) escaped 559 * 560 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue} 561 */ 562 protected String escapeAttributeValue(Object value) { 563 return XMLUtils.escapeAttributeValue(value); 564 } 565 566 /** 567 * Express an element tag start using given qualified name 568 * 569 * @param qualifiedName the fully qualified name of the element to write 570 * @throws IOException when stream write fails 571 * @deprecated 0.5 replaced by new SAX inspired API 572 */ 573 protected void expressElementStart(String qualifiedName) throws IOException { 574 if ( qualifiedName == null ) { 575 // XXX this indicates a programming error 576 log.fatal( "[expressElementStart]Qualified name is null." ); 577 throw new RuntimeException( "Qualified name is null." ); 578 } 579 580 writePrintln(); 581 writeIndent(); 582 writer.write( '<' ); 583 writer.write( qualifiedName ); 584 } 585 586 /** 587 * Write a tag close to the stream 588 * 589 * @throws IOException when stream write fails 590 * @deprecated 0.5 replaced by new SAX inspired API 591 */ 592 protected void expressTagClose() throws IOException { 593 writer.write( '>' ); 594 } 595 596 /** 597 * Write an element end tag to the stream 598 * 599 * @param qualifiedName the name of the element 600 * @throws IOException when stream write fails 601 * @deprecated 0.5 replaced by new SAX inspired API 602 */ 603 protected void expressElementEnd(String qualifiedName) throws IOException { 604 if (qualifiedName == null) { 605 // XXX this indicates a programming error 606 log.fatal( "[expressElementEnd]Qualified name is null." ); 607 throw new RuntimeException( "Qualified name is null." ); 608 } 609 610 writer.write( "</" ); 611 writer.write( qualifiedName ); 612 writer.write( '>' ); 613 } 614 615 /** 616 * Write an empty element end to the stream 617 * 618 * @throws IOException when stream write fails 619 * @deprecated 0.5 replaced by new SAX inspired API 620 */ 621 protected void expressElementEnd() throws IOException { 622 writer.write( "/>" ); 623 } 624 625 /** 626 * Write element body text 627 * 628 * @param text write out this body text 629 * @throws IOException when the stream write fails 630 * @deprecated 0.5 replaced by new SAX inspired API 631 */ 632 protected void expressBodyText(String text) throws IOException { 633 if ( text == null ) { 634 // XXX This is probably a programming error 635 log.error( "[expressBodyText]Body text is null" ); 636 637 } else { 638 writer.write( XMLUtils.escapeBodyValue(text) ); 639 } 640 } 641 642 /** 643 * Writes an attribute to the stream. 644 * 645 * @param qualifiedName fully qualified attribute name 646 * @param value attribute value 647 * @throws IOException when the stream write fails 648 * @deprecated 0.5 replaced by new SAX inspired API 649 */ 650 protected void expressAttribute( 651 String qualifiedName, 652 String value) 653 throws 654 IOException{ 655 if ( value == null ) { 656 // XXX probably a programming error 657 log.error( "Null attribute value." ); 658 return; 659 } 660 661 if ( qualifiedName == null ) { 662 // XXX probably a programming error 663 log.error( "Null attribute value." ); 664 return; 665 } 666 667 writer.write( ' ' ); 668 writer.write( qualifiedName ); 669 writer.write( "=\"" ); 670 writer.write( XMLUtils.escapeAttributeValue(value) ); 671 writer.write( '\"' ); 672 } 673 674 675 }