1
2
3
4
5
6
7
8
9
10 package org.dom4j.io;
11
12 import java.io.IOException;
13 import java.util.HashMap;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Map;
17
18 import org.dom4j.Attribute;
19 import org.dom4j.Branch;
20 import org.dom4j.CDATA;
21 import org.dom4j.CharacterData;
22 import org.dom4j.Comment;
23 import org.dom4j.Document;
24 import org.dom4j.DocumentType;
25 import org.dom4j.Element;
26 import org.dom4j.Entity;
27 import org.dom4j.Namespace;
28 import org.dom4j.Node;
29 import org.dom4j.ProcessingInstruction;
30 import org.dom4j.Text;
31 import org.dom4j.tree.NamespaceStack;
32 import org.xml.sax.Attributes;
33 import org.xml.sax.ContentHandler;
34 import org.xml.sax.DTDHandler;
35 import org.xml.sax.EntityResolver;
36 import org.xml.sax.ErrorHandler;
37 import org.xml.sax.InputSource;
38 import org.xml.sax.SAXException;
39 import org.xml.sax.SAXNotRecognizedException;
40 import org.xml.sax.SAXNotSupportedException;
41 import org.xml.sax.XMLReader;
42 import org.xml.sax.ext.LexicalHandler;
43 import org.xml.sax.helpers.AttributesImpl;
44 import org.xml.sax.helpers.LocatorImpl;
45
46 /*** <p><code>SAXWriter</code> writes a DOM4J tree to a SAX ContentHandler.</p>
47 *
48 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan</a>
49 * @version $Revision: 1.22 $
50 */
51 public class SAXWriter implements XMLReader {
52
53 protected static final String[] LEXICAL_HANDLER_NAMES = {
54 "http://xml.org/sax/properties/lexical-handler",
55 "http://xml.org/sax/handlers/LexicalHandler"
56 };
57
58 protected static String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes";
59 protected static String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces";
60
61 /*** <code>ContentHandler</code> to which SAX events are raised */
62 private ContentHandler contentHandler;
63
64 /*** code>DTDHandler</code> fired when a document has a DTD */
65 private DTDHandler dtdHandler;
66
67 /*** code>EntityResolver</code> fired when a document has a DTD */
68 private EntityResolver entityResolver;
69
70 private ErrorHandler errorHandler;
71
72 /*** code>LexicalHandler</code> fired on Entity and CDATA sections */
73 private LexicalHandler lexicalHandler;
74
75 /*** code>AttributesImpl</code> used when generating the Attributes */
76 private AttributesImpl attributes = new AttributesImpl();
77
78 /*** Stores the features */
79 private Map features = new HashMap();
80
81 /*** Stores the properties */
82 private Map properties = new HashMap();
83
84 /*** Whether namespace declarations are exported as attributes or not */
85 private boolean declareNamespaceAttributes;
86
87
88 public SAXWriter() {
89 properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.FALSE );
90 properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.TRUE );
91 }
92
93 public SAXWriter(ContentHandler contentHandler) {
94 this();
95 this.contentHandler = contentHandler;
96 }
97
98 public SAXWriter(
99 ContentHandler contentHandler,
100 LexicalHandler lexicalHandler
101 ) {
102 this();
103 this.contentHandler = contentHandler;
104 this.lexicalHandler = lexicalHandler;
105 }
106
107 public SAXWriter(
108 ContentHandler contentHandler,
109 LexicalHandler lexicalHandler,
110 EntityResolver entityResolver
111 ) {
112 this();
113 this.contentHandler = contentHandler;
114 this.lexicalHandler = lexicalHandler;
115 this.entityResolver = entityResolver;
116 }
117
118
119 /***
120 * A polymorphic method to write any Node to this SAX stream
121 */
122 public void write(Node node) throws SAXException {
123 int nodeType = node.getNodeType();
124 switch (nodeType) {
125 case Node.ELEMENT_NODE:
126 write((Element) node);
127 break;
128 case Node.ATTRIBUTE_NODE:
129 write((Attribute) node);
130 break;
131 case Node.TEXT_NODE:
132 write(node.getText());
133 break;
134 case Node.CDATA_SECTION_NODE:
135 write((CDATA) node);
136 break;
137 case Node.ENTITY_REFERENCE_NODE:
138 write((Entity) node);
139 break;
140 case Node.PROCESSING_INSTRUCTION_NODE:
141 write((ProcessingInstruction) node);
142 break;
143 case Node.COMMENT_NODE:
144 write((Comment) node);
145 break;
146 case Node.DOCUMENT_NODE:
147 write((Document) node);
148 break;
149 case Node.DOCUMENT_TYPE_NODE:
150 write((DocumentType) node);
151 break;
152 case Node.NAMESPACE_NODE:
153
154
155 break;
156 default:
157 throw new SAXException( "Invalid node type: " + node );
158 }
159 }
160
161
162 /*** Generates SAX events for the given Document and all its content
163 *
164 * @param document is the Document to parse
165 * @throws SAXException if there is a SAX error processing the events
166 */
167 public void write(Document document) throws SAXException {
168 if (document != null) {
169 checkForNullHandlers();
170
171 documentLocator(document);
172 startDocument();
173 entityResolver(document);
174 dtdHandler(document);
175
176 writeContent( document, new NamespaceStack() );
177 endDocument();
178 }
179 }
180
181
182
183 /*** Generates SAX events for the given Element and all its content
184 *
185 * @param element is the Element to parse
186 * @throws SAXException if there is a SAX error processing the events
187 */
188 public void write( Element element ) throws SAXException {
189 write( element, new NamespaceStack() );
190 }
191
192
193 /*** <p>Writes the opening tag of an {@link Element},
194 * including its {@link Attribute}s
195 * but without its content.</p>
196 *
197 * @param element <code>Element</code> to output.
198 */
199 public void writeOpen(Element element) throws SAXException {
200 startElement(element, null);
201 }
202
203 /*** <p>Writes the closing tag of an {@link Element}</p>
204 *
205 * @param element <code>Element</code> to output.
206 */
207 public void writeClose(Element element) throws SAXException {
208 endElement(element);
209 }
210
211 /*** Generates SAX events for the given text
212 *
213 * @param text is the text to send to the SAX ContentHandler
214 * @throws SAXException if there is a SAX error processing the events
215 */
216 public void write( String text ) throws SAXException {
217 if ( text != null ) {
218 char[] chars = text.toCharArray();
219 contentHandler.characters( chars, 0, chars.length );
220 }
221 }
222
223 /*** Generates SAX events for the given CDATA
224 *
225 * @param cdata is the CDATA to parse
226 * @throws SAXException if there is a SAX error processing the events
227 */
228 public void write( CDATA cdata ) throws SAXException {
229 String text = cdata.getText();
230 if ( lexicalHandler != null ) {
231 lexicalHandler.startCDATA();
232 write( text );
233 lexicalHandler.endCDATA();
234 }
235 else {
236 write( text );
237 }
238 }
239
240 /*** Generates SAX events for the given Comment
241 *
242 * @param comment is the Comment to parse
243 * @throws SAXException if there is a SAX error processing the events
244 */
245 public void write( Comment comment ) throws SAXException {
246 if ( lexicalHandler != null ) {
247 String text = comment.getText();
248 char[] chars = text.toCharArray();
249 lexicalHandler.comment( chars, 0, chars.length );
250 }
251 }
252
253 /*** Generates SAX events for the given Entity
254 *
255 * @param entity is the Entity to parse
256 * @throws SAXException if there is a SAX error processing the events
257 */
258 public void write( Entity entity ) throws SAXException {
259 String text = entity.getText();
260 if ( lexicalHandler != null ) {
261 String name = entity.getName();
262 lexicalHandler.startEntity(name);
263 write( text );
264 lexicalHandler.endEntity(name);
265 }
266 else {
267 write( text );
268 }
269 }
270
271 /*** Generates SAX events for the given ProcessingInstruction
272 *
273 * @param pi is the ProcessingInstruction to parse
274 * @throws SAXException if there is a SAX error processing the events
275 */
276 public void write( ProcessingInstruction pi ) throws SAXException {
277 String target = pi.getTarget();
278 String text = pi.getText();
279 contentHandler.processingInstruction(target, text);
280 }
281
282
283
284 /*** Should namespace declarations be converted to "xmlns" attributes. This property
285 * defaults to <code>false</code> as per the SAX specification.
286 * This property is set via the SAX feature "http://xml.org/sax/features/namespace-prefixes"
287 */
288 public boolean isDeclareNamespaceAttributes() {
289 return declareNamespaceAttributes;
290 }
291
292 /*** Sets whether namespace declarations should be exported as "xmlns" attributes or not.
293 * This property is set from the SAX feature "http://xml.org/sax/features/namespace-prefixes"
294 */
295 public void setDeclareNamespaceAttributes(boolean declareNamespaceAttributes) {
296 this.declareNamespaceAttributes = declareNamespaceAttributes;
297 }
298
299
300
301
302
303
304 /*** @return the <code>ContentHandler</code> called when SAX events
305 * are raised
306 */
307 public ContentHandler getContentHandler() {
308 return contentHandler;
309 }
310
311 /*** Sets the <code>ContentHandler</code> called when SAX events
312 * are raised
313 *
314 * @param contentHandler is the <code>ContentHandler</code> called when SAX events
315 * are raised
316 */
317 public void setContentHandler(ContentHandler contentHandler) {
318 this.contentHandler = contentHandler;
319 }
320
321
322 /*** @return the <code>DTDHandler</code>
323 */
324 public DTDHandler getDTDHandler() {
325 return dtdHandler;
326 }
327
328 /*** Sets the <code>DTDHandler</code>.
329 */
330 public void setDTDHandler(DTDHandler dtdHandler) {
331 this.dtdHandler = dtdHandler;
332 }
333
334 /*** @return the <code>ErrorHandler</code>
335 */
336 public ErrorHandler getErrorHandler() {
337 return errorHandler;
338 }
339
340 /*** Sets the <code>ErrorHandler</code>.
341 */
342 public void setErrorHandler(ErrorHandler errorHandler) {
343 this.errorHandler = errorHandler;
344 }
345
346 /*** @return the <code>EntityResolver</code> used when a Document contains
347 * a DTD
348 */
349 public EntityResolver getEntityResolver() {
350 return entityResolver;
351 }
352
353 /*** Sets the <code>EntityResolver</code> .
354 *
355 * @param entityResolver is the <code>EntityResolver</code>
356 */
357 public void setEntityResolver(EntityResolver entityResolver) {
358 this.entityResolver = entityResolver;
359 }
360
361 /*** @return the <code>LexicalHandler</code> used when a Document contains
362 * a DTD
363 */
364 public LexicalHandler getLexicalHandler() {
365 return lexicalHandler;
366 }
367
368 /*** Sets the <code>LexicalHandler</code> .
369 *
370 * @param lexicalHandler is the <code>LexicalHandler</code>
371 */
372 public void setLexicalHandler(LexicalHandler lexicalHandler) {
373 this.lexicalHandler = lexicalHandler;
374 }
375
376
377 /*** Sets the <code>XMLReader</code> used to write SAX events to
378 *
379 * @param xmlReader is the <code>XMLReader</code>
380 */
381 public void setXMLReader(XMLReader xmlReader) {
382 setContentHandler( xmlReader.getContentHandler() );
383 setDTDHandler( xmlReader.getDTDHandler() );
384 setEntityResolver( xmlReader.getEntityResolver() );
385 setErrorHandler( xmlReader.getErrorHandler() );
386 }
387
388 /*** Looks up the value of a feature.
389 */
390 public boolean getFeature(String name)
391 throws SAXNotRecognizedException, SAXNotSupportedException {
392 Boolean answer = (Boolean) features.get(name);
393 return answer != null && answer.booleanValue();
394 }
395
396 /*** This implementation does actually use any features but just
397 * stores them for later retrieval
398 */
399 public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
400 if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) {
401 setDeclareNamespaceAttributes( value );
402 }
403 else if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) {
404 if ( ! value ) {
405 throw new SAXNotSupportedException(name + ". namespace feature is always supported in dom4j." );
406 }
407 }
408 features.put(name, (value) ? Boolean.TRUE : Boolean.FALSE );
409 }
410
411 /*** Sets the given SAX property
412 */
413 public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
414 for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) {
415 if (LEXICAL_HANDLER_NAMES[i].equals(name)) {
416 setLexicalHandler((LexicalHandler) value);
417 return;
418 }
419 }
420 properties.put(name, value);
421 }
422
423 /*** Gets the given SAX property
424 */
425 public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException {
426 for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) {
427 if (LEXICAL_HANDLER_NAMES[i].equals(name)) {
428 return getLexicalHandler();
429 }
430 }
431 return properties.get(name);
432 }
433
434
435
436
437 /*** This method is not supported.
438 */
439 public void parse(String systemId) throws SAXNotSupportedException {
440 throw new SAXNotSupportedException(
441 "This XMLReader can only accept <dom4j> InputSource objects"
442 );
443 }
444
445
446 /*** Parses an XML document.
447 * This method can only accept DocumentInputSource inputs
448 * otherwise a {@link SAXNotSupportedException} exception is thrown.
449 *
450 * @throws SAXNotSupportedException
451 * if the input source is not wrapping a dom4j document
452 */
453 public void parse(InputSource input) throws SAXException {
454 if (input instanceof DocumentInputSource) {
455 DocumentInputSource documentInput = (DocumentInputSource) input;
456 Document document = documentInput.getDocument();
457 write( document );
458 }
459 else {
460 throw new SAXNotSupportedException(
461 "This XMLReader can only accept <dom4j> InputSource objects"
462 );
463 }
464 }
465
466
467
468
469
470
471 protected void writeContent( Branch branch, NamespaceStack namespaceStack ) throws SAXException {
472 for ( Iterator iter = branch.nodeIterator(); iter.hasNext(); ) {
473 Object object = iter.next();
474 if ( object instanceof Element ) {
475 write( (Element) object, namespaceStack );
476 }
477 else if ( object instanceof CharacterData ) {
478 if ( object instanceof Text ) {
479 Text text = (Text) object;
480 write( text.getText() );
481 }
482 else if ( object instanceof CDATA ) {
483 write( (CDATA) object );
484 }
485 else if ( object instanceof Comment ) {
486 write( (Comment) object );
487 }
488 else {
489 throw new SAXException( "Invalid Node in DOM4J content: " + object + " of type: " + object.getClass() );
490 }
491 }
492 else if ( object instanceof String ) {
493 write( (String) object );
494 }
495 else if ( object instanceof Entity ) {
496 write( (Entity) object );
497 }
498 else if ( object instanceof ProcessingInstruction ) {
499 write( (ProcessingInstruction) object );
500 }
501 else if ( object instanceof Namespace ) {
502
503 }
504 else {
505 throw new SAXException( "Invalid Node in DOM4J content: " + object );
506 }
507 }
508 }
509
510 /*** The {@link org.xml.sax.Locator} is only really useful when parsing a textual
511 * document as its main purpose is to identify the line and column number.
512 * Since we are processing an in memory tree which will probably have
513 * its line number information removed, we'll just use -1 for the line
514 * and column numbers.
515 */
516 protected void documentLocator(Document document) throws SAXException {
517 LocatorImpl locator = new LocatorImpl();
518
519 String publicID = null;
520 String systemID = null;
521 DocumentType docType = document.getDocType();
522 if (docType != null) {
523 publicID = docType.getPublicID();
524 systemID = docType.getSystemID();
525 }
526 if ( publicID != null ) {
527 locator.setPublicId(publicID);
528 }
529 if ( systemID != null ) {
530 locator.setSystemId(systemID);
531 }
532
533 locator.setLineNumber(-1);
534 locator.setColumnNumber(-1);
535
536 contentHandler.setDocumentLocator( locator );
537 }
538
539 protected void entityResolver(Document document) throws SAXException {
540 if (entityResolver != null) {
541 DocumentType docType = document.getDocType();
542 if (docType != null) {
543 String publicID = docType.getPublicID();
544 String systemID = docType.getSystemID();
545
546 if ( publicID != null || systemID != null ) {
547 try {
548 entityResolver.resolveEntity( publicID, systemID );
549 }
550 catch (IOException e) {
551 throw new SAXException(
552 "Could not resolve entity publicID: "
553 + publicID + " systemID: " + systemID, e
554 );
555 }
556 }
557 }
558 }
559 }
560
561
562 /*** We do not yet support DTD or XML Schemas so this method does nothing
563 * right now.
564 */
565 protected void dtdHandler(Document document) throws SAXException {
566 }
567
568 protected void startDocument() throws SAXException {
569 contentHandler.startDocument();
570 }
571
572 protected void endDocument() throws SAXException {
573 contentHandler.endDocument();
574 }
575
576 protected void write( Element element, NamespaceStack namespaceStack ) throws SAXException {
577 int stackSize = namespaceStack.size();
578 AttributesImpl namespaceAttributes = startPrefixMapping(element, namespaceStack);
579 startElement(element, namespaceAttributes);
580 writeContent(element, namespaceStack);
581 endElement(element);
582 endPrefixMapping(namespaceStack, stackSize);
583 }
584
585 /*** Fires a SAX startPrefixMapping event for all the namespaceStack
586 * which have just come into scope
587 */
588 protected AttributesImpl startPrefixMapping( Element element, NamespaceStack namespaceStack ) throws SAXException {
589 AttributesImpl namespaceAttributes = null;
590
591
592 Namespace elementNamespace = element.getNamespace();
593 if ((elementNamespace != null) && !isIgnoreableNamespace(elementNamespace, namespaceStack)) {
594 namespaceStack.push(elementNamespace);
595 contentHandler.startPrefixMapping(
596 elementNamespace.getPrefix(), elementNamespace.getURI());
597 namespaceAttributes = addNamespaceAttribute(namespaceAttributes, elementNamespace);
598 }
599
600 List declaredNamespaces = element.declaredNamespaces();
601 for ( int i = 0, size = declaredNamespaces.size(); i < size ; i++ ) {
602 Namespace namespace = (Namespace) declaredNamespaces.get(i);
603 if ( ! isIgnoreableNamespace( namespace, namespaceStack ) ) {
604 namespaceStack.push( namespace );
605 contentHandler.startPrefixMapping(
606 namespace.getPrefix(), namespace.getURI()
607 );
608 namespaceAttributes = addNamespaceAttribute( namespaceAttributes, namespace );
609 }
610 }
611 return namespaceAttributes;
612 }
613
614 /*** Fires a SAX endPrefixMapping event for all the namespaceStack which
615 * have gone out of scope
616 */
617 protected void endPrefixMapping( NamespaceStack namespaceStack, int stackSize ) throws SAXException {
618 while ( namespaceStack.size() > stackSize ) {
619 Namespace namespace = namespaceStack.pop();
620 if ( namespace != null ) {
621 contentHandler.endPrefixMapping( namespace.getPrefix() );
622 }
623 }
624 }
625
626
627 protected void startElement( Element element, AttributesImpl namespaceAttributes ) throws SAXException {
628 contentHandler.startElement(
629 element.getNamespaceURI(),
630 element.getName(),
631 element.getQualifiedName(),
632 createAttributes( element, namespaceAttributes )
633 );
634 }
635
636 protected void endElement( Element element ) throws SAXException {
637 contentHandler.endElement(
638 element.getNamespaceURI(),
639 element.getName(),
640 element.getQualifiedName()
641 );
642 }
643
644 protected Attributes createAttributes( Element element, Attributes namespaceAttributes ) throws SAXException {
645 attributes.clear();
646 if ( namespaceAttributes != null ) {
647 attributes.setAttributes( namespaceAttributes );
648 }
649
650 for ( Iterator iter = element.attributeIterator(); iter.hasNext(); ) {
651 Attribute attribute = (Attribute) iter.next();
652 attributes.addAttribute(
653 attribute.getNamespaceURI(),
654 attribute.getName(),
655 attribute.getQualifiedName(),
656 "CDATA",
657 attribute.getValue()
658 );
659 }
660 return attributes;
661 }
662
663 /*** If isDelcareNamespaceAttributes() is enabled then this method will add the
664 * given namespace declaration to the supplied attributes object, creating one if
665 * it does not exist.
666 */
667 protected AttributesImpl addNamespaceAttribute( AttributesImpl namespaceAttributes, Namespace namespace ) {
668 if ( declareNamespaceAttributes ) {
669 if ( namespaceAttributes == null ) {
670 namespaceAttributes = new AttributesImpl();
671 }
672 String prefix = namespace.getPrefix();
673 String qualifiedName = "xmlns";
674 if ( prefix != null && prefix.length() > 0 ) {
675 qualifiedName = "xmlns:" + prefix;
676 }
677 String uri = "";
678 String localName = prefix;
679 String type = "CDATA";
680 String value = namespace.getURI();
681
682 namespaceAttributes.addAttribute( uri, localName, qualifiedName, type, value );
683 }
684 return namespaceAttributes;
685 }
686
687
688 /*** @return true if the given namespace is an ignorable namespace
689 * (such as Namespace.NO_NAMESPACE or Namespace.XML_NAMESPACE) or if the
690 * namespace has already been declared in the current scope
691 */
692 protected boolean isIgnoreableNamespace( Namespace namespace, NamespaceStack namespaceStack ) {
693 if ( namespace.equals( Namespace.NO_NAMESPACE ) || namespace.equals( Namespace.XML_NAMESPACE ) ) {
694 return true;
695 }
696 String uri = namespace.getURI();
697 if ( uri == null || uri.length() <= 0 ) {
698 return true;
699 }
700 return namespaceStack.contains( namespace );
701 }
702
703 /*** Ensures non-null content handlers?
704 */
705 protected void checkForNullHandlers() {
706 }
707
708 }
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756