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