001/* 002 * Copyright 2001 (C) MetaStuff, Ltd. All Rights Reserved. 003 * 004 * This software is open source. 005 * See the bottom of this file for the licence. 006 * 007 * $Id: SAXWriter.java,v 1.12 2001/09/13 12:51:30 jstrachan Exp $ 008 */ 009 010package org.dom4j.io; 011 012import java.io.IOException; 013import java.util.HashMap; 014import java.util.Iterator; 015import java.util.List; 016import java.util.Map; 017 018import org.dom4j.Attribute; 019import org.dom4j.Branch; 020import org.dom4j.CDATA; 021import org.dom4j.CharacterData; 022import org.dom4j.Comment; 023import org.dom4j.DocumentType; 024import org.dom4j.Document; 025import org.dom4j.Element; 026import org.dom4j.Entity; 027import org.dom4j.Namespace; 028import org.dom4j.ProcessingInstruction; 029import org.dom4j.Text; 030 031import org.dom4j.tree.NamespaceStack; 032 033import org.xml.sax.Attributes; 034import org.xml.sax.ContentHandler; 035import org.xml.sax.DTDHandler; 036import org.xml.sax.EntityResolver; 037import org.xml.sax.ErrorHandler; 038import org.xml.sax.InputSource; 039import org.xml.sax.Locator; 040import org.xml.sax.SAXException; 041import org.xml.sax.SAXNotRecognizedException; 042import org.xml.sax.SAXNotSupportedException; 043import org.xml.sax.XMLFilter; 044import org.xml.sax.XMLReader; 045import org.xml.sax.ext.LexicalHandler; 046import org.xml.sax.helpers.AttributesImpl; 047import org.xml.sax.helpers.LocatorImpl; 048 049/** <p><code>SAXWriter</code> writes a DOM4J tree to a SAX ContentHandler.</p> 050 * 051 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan</a> 052 * @version $Revision: 1.12 $ 053 */ 054public class SAXWriter implements XMLReader { 055 056 protected static final String[] LEXICAL_HANDLER_NAMES = { 057 "http://xml.org/sax/properties/lexical-handler", 058 "http://xml.org/sax/handlers/LexicalHandler" 059 }; 060 061 protected static String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes"; 062 protected static String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces"; 063 064 /** <code>ContentHandler</code> to which SAX events are raised */ 065 private ContentHandler contentHandler; 066 067 /** code>DTDHandler</code> fired when a document has a DTD */ 068 private DTDHandler dtdHandler; 069 070 /** code>EntityResolver</code> fired when a document has a DTD */ 071 private EntityResolver entityResolver; 072 073 private ErrorHandler errorHandler; 074 075 /** code>LexicalHandler</code> fired on Entity and CDATA sections */ 076 private LexicalHandler lexicalHandler; 077 078 /** code>AttributesImpl</code> used when generating the Attributes */ 079 private AttributesImpl attributes = new AttributesImpl(); 080 081 /** Stores the features */ 082 private Map features = new HashMap(); 083 084 /** Stores the properties */ 085 private Map properties = new HashMap(); 086 087 /** Whether namespace declarations are exported as attributes or not */ 088 private boolean declareNamespaceAttributes; 089 090 091 public SAXWriter() { 092 properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.FALSE ); 093 properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.TRUE ); 094 } 095 096 public SAXWriter(ContentHandler contentHandler) { 097 this(); 098 this.contentHandler = contentHandler; 099 } 100 101 public SAXWriter( 102 ContentHandler contentHandler, 103 EntityResolver entityResolver, 104 LexicalHandler lexicalHandler 105 ) { 106 this(); 107 this.contentHandler = contentHandler; 108 this.entityResolver = entityResolver; 109 this.lexicalHandler = lexicalHandler; 110 } 111 112 113 /** Generates SAX events for the given Document and all its content 114 * 115 * @param document is the Document to parse 116 * @throw SAXException if there is a SAX error processing the events 117 */ 118 public void write(Document document) throws SAXException { 119 if (document != null) { 120 checkForNullHandlers(); 121 122 documentLocator(document); 123 startDocument(); 124 entityResolver(document); 125 dtdHandler(document); 126 127 writeContent( document, new NamespaceStack() ); 128 endDocument(); 129 } 130 } 131 132 133 134 /** Generates SAX events for the given Element and all its content 135 * 136 * @param element is the Element to parse 137 * @throw SAXException if there is a SAX error processing the events 138 */ 139 public void write( Element element ) throws SAXException { 140 write( element, new NamespaceStack() ); 141 } 142 143 /** Generates SAX events for the given text 144 * 145 * @param text is the text to send to the SAX ContentHandler 146 * @throw SAXException if there is a SAX error processing the events 147 */ 148 public void write( String text ) throws SAXException { 149 if ( text != null ) { 150 char[] chars = text.toCharArray(); 151 contentHandler.characters( chars, 0, chars.length ); 152 } 153 } 154 155 /** Generates SAX events for the given CDATA 156 * 157 * @param cdata is the CDATA to parse 158 * @throw SAXException if there is a SAX error processing the events 159 */ 160 public void write( CDATA cdata ) throws SAXException { 161 String text = cdata.getText(); 162 if ( lexicalHandler != null ) { 163 lexicalHandler.startCDATA(); 164 write( text ); 165 lexicalHandler.endCDATA(); 166 } 167 else { 168 write( text ); 169 } 170 } 171 172 /** Generates SAX events for the given Comment 173 * 174 * @param comment is the Comment to parse 175 * @throw SAXException if there is a SAX error processing the events 176 */ 177 public void write( Comment comment ) throws SAXException { 178 if ( lexicalHandler != null ) { 179 String text = comment.getText(); 180 char[] chars = text.toCharArray(); 181 lexicalHandler.comment( chars, 0, chars.length ); 182 } 183 } 184 185 /** Generates SAX events for the given Entity 186 * 187 * @param e is the Entity to parse 188 * @throw SAXException if there is a SAX error processing the events 189 */ 190 public void write( Entity entity ) throws SAXException { 191 String text = entity.getText(); 192 if ( lexicalHandler != null ) { 193 String name = entity.getName(); 194 lexicalHandler.startEntity(name); 195 write( text ); 196 lexicalHandler.endEntity(name); 197 } 198 else { 199 write( text ); 200 } 201 } 202 203 /** Generates SAX events for the given ProcessingInstruction 204 * 205 * @param pi is the ProcessingInstruction to parse 206 * @throw SAXException if there is a SAX error processing the events 207 */ 208 public void write( ProcessingInstruction pi ) throws SAXException { 209 String target = pi.getTarget(); 210 String text = pi.getText(); 211 contentHandler.processingInstruction(target, text); 212 } 213 214 215 216 /** Should namespace declarations be converted to "xmlns" attributes. This property 217 * defaults to <code>false</code> as per the SAX specification. 218 * This property is set via the SAX feature "http://xml.org/sax/features/namespace-prefixes" 219 */ 220 public boolean isDeclareNamespaceAttributes() { 221 return declareNamespaceAttributes; 222 } 223 224 /** Sets whether namespace declarations should be exported as "xmlns" attributes or not. 225 * This property is set from the SAX feature "http://xml.org/sax/features/namespace-prefixes" 226 */ 227 public void setDeclareNamespaceAttributes(boolean declareNamespaceAttributes) { 228 this.declareNamespaceAttributes = declareNamespaceAttributes; 229 } 230 231 232 233 // XMLReader methods 234 //------------------------------------------------------------------------- 235 236 /** @return the <code>ContentHandler</code> called when SAX events 237 * are raised 238 */ 239 public ContentHandler getContentHandler() { 240 return contentHandler; 241 } 242 243 /** Sets the <code>ContentHandler</code> called when SAX events 244 * are raised 245 * 246 * @param contentHandler is the <code>ContentHandler</code> called when SAX events 247 * are raised 248 */ 249 public void setContentHandler(ContentHandler contentHandler) { 250 this.contentHandler = contentHandler; 251 } 252 253 254 /** @return the <code>DTDHandler</code> 255 */ 256 public DTDHandler getDTDHandler() { 257 return dtdHandler; 258 } 259 260 /** Sets the <code>DTDHandler</code>. 261 */ 262 public void setDTDHandler(DTDHandler dtdHandler) { 263 this.dtdHandler = dtdHandler; 264 } 265 266 /** @return the <code>ErrorHandler</code> 267 */ 268 public ErrorHandler getErrorHandler() { 269 return errorHandler; 270 } 271 272 /** Sets the <code>ErrorHandler</code>. 273 */ 274 public void setErrorHandler(ErrorHandler errorHandler) { 275 this.errorHandler = errorHandler; 276 } 277 278 /** @return the <code>EntityResolver</code> used when a Document contains 279 * a DTD 280 */ 281 public EntityResolver getEntityResolver() { 282 return entityResolver; 283 } 284 285 /** Sets the <code>EntityResolver</code> . 286 * 287 * @param entityResolver is the <code>EntityResolver</code> 288 */ 289 public void setEntityResolver(EntityResolver entityResolver) { 290 this.entityResolver = entityResolver; 291 } 292 293 /** @return the <code>LexicalHandler</code> used when a Document contains 294 * a DTD 295 */ 296 public LexicalHandler getLexicalHandler() { 297 return lexicalHandler; 298 } 299 300 /** Sets the <code>LexicalHandler</code> . 301 * 302 * @param entityResolver is the <code>LexicalHandler</code> 303 */ 304 public void setLexicalHandler(LexicalHandler lexicalHandler) { 305 this.lexicalHandler = lexicalHandler; 306 } 307 308 309 /** Sets the <code>XMLReader</code> used to write SAX events to 310 * 311 * @param xmlReader is the <code>XMLReader</code> 312 */ 313 public void setXMLReader(XMLReader xmlReader) { 314 setContentHandler( xmlReader.getContentHandler() ); 315 setDTDHandler( xmlReader.getDTDHandler() ); 316 setEntityResolver( xmlReader.getEntityResolver() ); 317 setErrorHandler( xmlReader.getErrorHandler() ); 318 } 319 320 /** Looks up the value of a feature. 321 */ 322 public boolean getFeature(String name) 323 throws SAXNotRecognizedException, SAXNotSupportedException { 324 Boolean answer = (Boolean) features.get(name); 325 return answer != null && answer.booleanValue(); 326 } 327 328 /** This implementation does actually use any features but just 329 * stores them for later retrieval 330 */ 331 public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { 332 if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) { 333 setDeclareNamespaceAttributes( value ); 334 } 335 else if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) { 336 if ( ! value ) { 337 throw new SAXNotSupportedException(name + ". namespace feature is always supported in dom4j." ); 338 } 339 } 340 features.put(name, (value) ? Boolean.TRUE : Boolean.FALSE ); 341 } 342 343 /** Sets the given SAX property 344 */ 345 public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { 346 for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) { 347 if (LEXICAL_HANDLER_NAMES[i].equals(name)) { 348 setLexicalHandler((LexicalHandler) value); 349 return; 350 } 351 } 352 properties.put(name, value); 353 } 354 355 /** Gets the given SAX property 356 */ 357 public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { 358 for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) { 359 if (LEXICAL_HANDLER_NAMES[i].equals(name)) { 360 return getLexicalHandler(); 361 } 362 } 363 return properties.get(name); 364 } 365 366 367 368 369 /** This method is not supported. 370 */ 371 public void parse(String systemId) throws SAXNotSupportedException { 372 throw new SAXNotSupportedException( 373 "This XMLReader can only accept <dom4j> InputSource objects" 374 ); 375 } 376 377 378 /** Parses an XML document. 379 * This method can only accept DocumentInputSource inputs 380 * otherwise a {@link SAXNotSupportedException} exception is thrown. 381 * 382 * @throws SAXNotSupportedException 383 * if the input source is not wrapping a dom4j document 384 */ 385 public void parse(InputSource input) throws SAXException { 386 if (input instanceof DocumentInputSource) { 387 DocumentInputSource documentInput = (DocumentInputSource) input; 388 Document document = documentInput.getDocument(); 389 write( document ); 390 } 391 else { 392 throw new SAXNotSupportedException( 393 "This XMLReader can only accept <dom4j> InputSource objects" 394 ); 395 } 396 } 397 398 399 400 // Implementation methods 401 //------------------------------------------------------------------------- 402 403 protected void writeContent( Branch branch, NamespaceStack namespaceStack ) throws SAXException { 404 for ( Iterator iter = branch.nodeIterator(); iter.hasNext(); ) { 405 Object object = iter.next(); 406 if ( object instanceof Element ) { 407 write( (Element) object, namespaceStack ); 408 } 409 else if ( object instanceof CharacterData ) { 410 if ( object instanceof Text ) { 411 Text text = (Text) object; 412 write( text.getText() ); 413 } 414 else if ( object instanceof CDATA ) { 415 write( (CDATA) object ); 416 } 417 else if ( object instanceof Comment ) { 418 write( (Comment) object ); 419 } 420 else { 421 throw new SAXException( "Invalid Node in DOM4J content: " + object ); 422 } 423 } 424 else if ( object instanceof String ) { 425 write( (String) object ); 426 } 427 else if ( object instanceof Entity ) { 428 write( (Entity) object ); 429 } 430 else if ( object instanceof ProcessingInstruction ) { 431 write( (ProcessingInstruction) object ); 432 } 433 else if ( object instanceof Namespace ) { 434 // namespaceStack are written via write of element 435 } 436 else { 437 throw new SAXException( "Invalid Node in DOM4J content: " + object ); 438 } 439 } 440 } 441 442 /** The {@link Locator} is only really useful when parsing a textual 443 * document as its main purpose is to identify the line and column number. 444 * Since we are processing an in memory tree which will probably have 445 * its line number information removed, we'll just use -1 for the line 446 * and column numbers. 447 */ 448 protected void documentLocator(Document document) throws SAXException { 449 LocatorImpl locator = new LocatorImpl(); 450 451 String publicID = null; 452 String systemID = null; 453 DocumentType docType = document.getDocType(); 454 if (docType != null) { 455 publicID = docType.getPublicID(); 456 systemID = docType.getSystemID(); 457 } 458 locator.setPublicId(publicID); 459 locator.setSystemId(systemID); 460 locator.setLineNumber(-1); 461 locator.setColumnNumber(-1); 462 463 contentHandler.setDocumentLocator( locator ); 464 } 465 466 protected void entityResolver(Document document) throws SAXException { 467 if (entityResolver != null) { 468 DocumentType docType = document.getDocType(); 469 if (docType != null) { 470 String publicID = docType.getPublicID(); 471 String systemID = docType.getSystemID(); 472 473 if ( publicID != null || systemID != null ) { 474 try { 475 entityResolver.resolveEntity( publicID, systemID ); 476 } 477 catch (IOException e) { 478 throw new SAXException( 479 "Could not resolve entity publicID: " 480 + publicID + " systemID: " + systemID, e 481 ); 482 } 483 } 484 } 485 } 486 } 487 488 489 /** We do not yet support DTD or XML Schemas so this method does nothing 490 * right now. 491 */ 492 protected void dtdHandler(Document document) throws SAXException { 493 } 494 495 protected void startDocument() throws SAXException { 496 contentHandler.startDocument(); 497 } 498 499 protected void endDocument() throws SAXException { 500 contentHandler.endDocument(); 501 } 502 503 protected void write( Element element, NamespaceStack namespaceStack ) throws SAXException { 504 int stackSize = namespaceStack.size(); 505 AttributesImpl namespaceAttributes = startPrefixMapping(element, namespaceStack); 506 startElement(element, namespaceAttributes); 507 writeContent(element, namespaceStack); 508 endElement(element); 509 endPrefixMapping(namespaceStack, stackSize); 510 } 511 512 /** Fires a SAX startPrefixMapping event for all the namespaceStack 513 * which have just come into scope 514 */ 515 protected AttributesImpl startPrefixMapping( Element element, NamespaceStack namespaceStack ) throws SAXException { 516 AttributesImpl namespaceAttributes = null; 517 List declaredNamespaces = element.declaredNamespaces(); 518 for ( int i = 0, size = declaredNamespaces.size(); i < size ; i++ ) { 519 Namespace namespace = (Namespace) declaredNamespaces.get(i); 520 if ( ! isIgnoreableNamespace( namespace, namespaceStack ) ) { 521 namespaceStack.push( namespace ); 522 contentHandler.startPrefixMapping( 523 namespace.getPrefix(), namespace.getURI() 524 ); 525 namespaceAttributes = addNamespaceAttribute( namespaceAttributes, namespace ); 526 } 527 } 528 return namespaceAttributes; 529 } 530 531 /** Fires a SAX endPrefixMapping event for all the namespaceStack which 532 * have gone out of scope 533 */ 534 protected void endPrefixMapping( NamespaceStack namespaceStack, int stackSize ) throws SAXException { 535 while ( namespaceStack.size() > stackSize ) { 536 Namespace namespace = namespaceStack.pop(); 537 if ( namespace != null ) { 538 contentHandler.endPrefixMapping( namespace.getPrefix() ); 539 } 540 } 541 } 542 543 544 protected void startElement( Element element, AttributesImpl namespaceAttributes ) throws SAXException { 545 contentHandler.startElement( 546 element.getNamespaceURI(), 547 element.getName(), 548 element.getQualifiedName(), 549 createAttributes( element, namespaceAttributes ) 550 ); 551 } 552 553 protected void endElement( Element element ) throws SAXException { 554 contentHandler.endElement( 555 element.getNamespaceURI(), 556 element.getName(), 557 element.getQualifiedName() 558 ); 559 } 560 561 protected Attributes createAttributes( Element element, Attributes namespaceAttributes ) throws SAXException { 562 attributes.clear(); 563 if ( namespaceAttributes != null ) { 564 attributes.setAttributes( namespaceAttributes ); 565 } 566 567 for ( Iterator iter = element.attributeIterator(); iter.hasNext(); ) { 568 Attribute attribute = (Attribute) iter.next(); 569 attributes.addAttribute( 570 attribute.getNamespaceURI(), 571 attribute.getName(), 572 attribute.getQualifiedName(), 573 "CDATA", 574 attribute.getValue() 575 ); 576 } 577 return attributes; 578 } 579 580 /** If isDelcareNamespaceAttributes() is enabled then this method will add the 581 * given namespace declaration to the supplied attributes object, creating one if 582 * it does not exist. 583 */ 584 protected AttributesImpl addNamespaceAttribute( AttributesImpl namespaceAttributes, Namespace namespace ) { 585 if ( declareNamespaceAttributes ) { 586 if ( namespaceAttributes == null ) { 587 namespaceAttributes = new AttributesImpl(); 588 } 589 String prefix = namespace.getPrefix(); 590 String qualifiedName = "xmlns"; 591 if ( prefix != null && prefix.length() > 0 ) { 592 qualifiedName = "xmlns:" + prefix; 593 } 594 String uri = ""; 595 String localName = prefix; 596 String type = "CDATA"; 597 String value = namespace.getURI(); 598 599 namespaceAttributes.addAttribute( uri, localName, qualifiedName, type, value ); 600 } 601 return namespaceAttributes; 602 } 603 604 605 /** @return true if the given namespace is an ignorable namespace 606 * (such as Namespace.NO_NAMESPACE or Namespace.XML_NAMESPACE) or if the 607 * namespace has already been declared in the current scope 608 */ 609 protected boolean isIgnoreableNamespace( Namespace namespace, NamespaceStack namespaceStack ) { 610 if ( namespace.equals( Namespace.NO_NAMESPACE ) || namespace.equals( Namespace.XML_NAMESPACE ) ) { 611 return true; 612 } 613 String uri = namespace.getURI(); 614 if ( uri == null || uri.length() <= 0 ) { 615 return true; 616 } 617 return namespaceStack.contains( namespace ); 618 } 619 620 /** Ensures non-null content handlers? 621 */ 622 protected void checkForNullHandlers() { 623 } 624 625} 626 627 628 629 630/* 631 * Redistribution and use of this software and associated documentation 632 * ("Software"), with or without modification, are permitted provided 633 * that the following conditions are met: 634 * 635 * 1. Redistributions of source code must retain copyright 636 * statements and notices. Redistributions must also contain a 637 * copy of this document. 638 * 639 * 2. Redistributions in binary form must reproduce the 640 * above copyright notice, this list of conditions and the 641 * following disclaimer in the documentation and/or other 642 * materials provided with the distribution. 643 * 644 * 3. The name "DOM4J" must not be used to endorse or promote 645 * products derived from this Software without prior written 646 * permission of MetaStuff, Ltd. For written permission, 647 * please contact dom4j-info@metastuff.com. 648 * 649 * 4. Products derived from this Software may not be called "DOM4J" 650 * nor may "DOM4J" appear in their names without prior written 651 * permission of MetaStuff, Ltd. DOM4J is a registered 652 * trademark of MetaStuff, Ltd. 653 * 654 * 5. Due credit should be given to the DOM4J Project 655 * (http://dom4j.org/). 656 * 657 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS 658 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT 659 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 660 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 661 * METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 662 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 663 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 664 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 665 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 666 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 667 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 668 * OF THE POSSIBILITY OF SUCH DAMAGE. 669 * 670 * Copyright 2001 (C) MetaStuff, Ltd. All Rights Reserved. 671 * 672 * $Id: SAXWriter.java,v 1.12 2001/09/13 12:51:30 jstrachan Exp $ 673 */