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.cocoon.transformation;
18
19 import org.apache.avalon.framework.activity.Disposable;
20 import org.apache.avalon.framework.configuration.Configurable;
21 import org.apache.avalon.framework.configuration.Configuration;
22 import org.apache.avalon.framework.configuration.ConfigurationException;
23 import org.apache.avalon.framework.parameters.Parameters;
24 import org.apache.avalon.framework.service.ServiceException;
25 import org.apache.avalon.framework.service.ServiceManager;
26 import org.apache.avalon.framework.service.Serviceable;
27
28 import org.apache.cocoon.ProcessingException;
29 import org.apache.cocoon.environment.Context;
30 import org.apache.cocoon.environment.ObjectModelHelper;
31 import org.apache.cocoon.environment.Request;
32 import org.apache.cocoon.environment.Response;
33 import org.apache.cocoon.environment.SourceResolver;
34 import org.apache.cocoon.transformation.helpers.ParametersRecorder;
35 import org.apache.cocoon.transformation.helpers.TextRecorder;
36 import org.apache.cocoon.util.ClassUtils;
37 import org.apache.cocoon.util.TraxErrorHandler;
38 import org.apache.cocoon.xml.AttributesImpl;
39 import org.apache.cocoon.xml.ImmutableAttributesImpl;
40 import org.apache.cocoon.xml.IncludeXMLConsumer;
41 import org.apache.cocoon.xml.SaxBuffer;
42 import org.apache.cocoon.xml.XMLConsumer;
43 import org.apache.cocoon.xml.XMLUtils;
44 import org.apache.cocoon.xml.dom.DOMBuilder;
45
46 import org.apache.excalibur.source.SourceParameters;
47 import org.apache.excalibur.xml.sax.XMLizable;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.DocumentFragment;
50 import org.w3c.dom.Node;
51 import org.xml.sax.Attributes;
52 import org.xml.sax.ContentHandler;
53 import org.xml.sax.Locator;
54 import org.xml.sax.SAXException;
55 import org.xml.sax.ext.LexicalHandler;
56
57 import javax.xml.transform.TransformerFactory;
58 import javax.xml.transform.sax.SAXTransformerFactory;
59 import java.io.IOException;
60 import java.util.ArrayList;
61 import java.util.Iterator;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Properties;
65 import java.util.Stack;
66
67 /**
68 * This class is the basis for all transformers. It provides various useful
69 * methods and hooks for implementing own custom transformers.
70 *
71 * <p>The basic behaviour of each transformer consists of the following four
72 * parts:</p>
73 * <ul>
74 * <li>Listen for specific events with a given namespace</li>
75 * <li>Collect information via these events</li>
76 * <li>Process the information</li>
77 * <li>Create new events from the processed information</li>
78 * </ul>
79 *
80 * <p>For all these four purposes the AbstractSAXTransformer offers some
81 * powerful methods and hooks:</p>
82 *
83 * <h3>Namespace handling</h3>
84 * By setting the instance variable namespaceURI to the namespace the
85 * events are filtered and only events with this namespace are send to
86 * the two hooks: <code>startTransformingElement</code> and
87 * <code>endTransformingElement</code>. It is possible to override the default
88 * namespace for the transformer by specifying the parameter "namespaceURI"
89 * in the pipeline. This avoids possible namespace collisions.
90 *
91 * <h3>Recording of information</h3>
92 * There are several methods for recording information, e.g. startRecording(),
93 * startTextRecording() etc. These methods collect information from the xml
94 * stream for further processing.
95 *
96 * <h3>Creating new events</h3>
97 * New events can be easily created with the <code>sendEvents()</code>
98 * method, the <code>sendStartElementEvent()</code> methods, the
99 * <code>sendEndElementEvent()</code> method or the
100 * <code>sendTextEvent()</code> method.
101 *
102 * <h3>Initialization</h3>
103 * Before the document is processed the <code>setupTransforming</code> hook
104 * is invoked.
105 *
106 * @author <a href="mailto:cziegeler@s-und-n.de">Carsten Ziegeler</a>
107 * @version $Id: AbstractSAXTransformer.java 433543 2006-08-22 06:22:54Z crossley $
108 */
109 public abstract class AbstractSAXTransformer extends AbstractTransformer
110 implements Serviceable, Configurable, Disposable {
111
112 /**
113 * Empty immutable attributes (for performance). Use them
114 * whenever creating an element with no attributes.
115 */
116 protected static final Attributes EMPTY_ATTRIBUTES = XMLUtils.EMPTY_ATTRIBUTES;
117
118 /**
119 * The trax <code>TransformerFactory</code> used by this transformer.
120 */
121 private SAXTransformerFactory tfactory;
122
123 /**
124 * Controlls SAX event handling.
125 * If set to true all whitespace events are ignored.
126 */
127 protected boolean ignoreWhitespaces;
128
129 /**
130 * Controlls SAX event handling.
131 * If set to true all characters events containing only whitespaces
132 * are ignored.
133 */
134 protected boolean ignoreEmptyCharacters;
135
136 /**
137 * Controlls SAX event handling.
138 * If this is incremented all events are not forwarded to the next
139 * pipeline component, but the hooks are still called.
140 */
141 protected int ignoreEventsCount;
142
143 /**
144 * Controlls SAX event handling.
145 * If this is greater than zero, the hooks are not called. Attention,
146 * make sure, that you decrement this counter properly as your hooks are
147 * not called anymore!
148 */
149 protected int ignoreHooksCount;
150
151 /**
152 * The namespace used by the transformer for the SAX events filtering.
153 * This either equals to the {@link #defaultNamespaceURI} or to the value
154 * set by the <code>namespaceURI</code> sitemap parameter for the pipeline.
155 * Must never be null.
156 */
157 protected String namespaceURI;
158
159 /**
160 * This is the default namespace used by the transformer.
161 * Implementations should set its value in the constructor.
162 * Must never be null.
163 */
164 protected String defaultNamespaceURI;
165
166 /**
167 * A stack for collecting information.
168 * The stack is important for collection information especially when
169 * the tags can be nested.
170 */
171 protected final Stack stack = new Stack();
172
173 /**
174 * The stack of current used recorders
175 */
176 protected final Stack recorderStack = new Stack();
177
178 /**
179 * The current Request object
180 */
181 protected Request request;
182
183 /**
184 * The current Response object
185 */
186 protected Response response;
187
188 /**
189 * The current Context object
190 */
191 protected Context context;
192
193 /**
194 * The current objectModel of the environment
195 */
196 protected Map objectModel;
197
198 /**
199 * The parameters specified in the sitemap
200 */
201 protected Parameters parameters;
202
203 /**
204 * The source attribute specified in the sitemap
205 */
206 protected String source;
207
208 /**
209 * The Avalon ServiceManager for getting Components
210 */
211 protected ServiceManager manager;
212
213 /**
214 * The SourceResolver for this request
215 */
216 protected SourceResolver resolver;
217
218 /**
219 * Are we already initialized for the current request?
220 */
221 private boolean isInitialized;
222
223 /**
224 * Empty attributes (for performance). This can be used
225 * do create own attributes, but make sure to clean them
226 * afterwords.
227 * @deprecated Use {@link AbstractSAXTransformer#EMPTY_ATTRIBUTES}.
228 */
229 protected Attributes emptyAttributes = EMPTY_ATTRIBUTES;
230
231 /**
232 * The namespaces and their prefixes
233 */
234 private final List namespaces = new ArrayList(5);
235
236 /**
237 * The current prefix for our namespace
238 */
239 private String ourPrefix;
240
241 //
242 // Lifecycle
243 //
244
245 /* (non-Javadoc)
246 * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
247 */
248 public void service(ServiceManager manager) throws ServiceException {
249 this.manager = manager;
250 }
251
252 /* (non-Javadoc)
253 * @see Configurable#configure(Configuration)
254 */
255 public void configure(Configuration configuration) throws ConfigurationException {
256 String tFactoryClass = configuration.getChild("transformer-factory").getValue(null);
257 if (tFactoryClass != null) {
258 try {
259 this.tfactory = (SAXTransformerFactory) ClassUtils.newInstance(tFactoryClass);
260 if (getLogger().isDebugEnabled()) {
261 getLogger().debug("Using transformer factory " + tFactoryClass);
262 }
263 } catch (Exception e) {
264 throw new ConfigurationException("Cannot load transformer factory " + tFactoryClass, e);
265 }
266 } else {
267 // Standard TrAX behaviour
268 this.tfactory = (SAXTransformerFactory) TransformerFactory.newInstance();
269 }
270 tfactory.setErrorListener(new TraxErrorHandler(getLogger()));
271 }
272
273 /* (non-Javadoc)
274 * @see org.apache.cocoon.sitemap.SitemapModelComponent#setup(SourceResolver, Map, String, Parameters)
275 */
276 public void setup(SourceResolver resolver,
277 Map objectModel,
278 String src,
279 Parameters params)
280 throws ProcessingException, SAXException, IOException {
281
282 if (getLogger().isDebugEnabled()) {
283 getLogger().debug("Setup resolver=" + resolver +
284 ", objectModel=" + objectModel +
285 ", src=" + src +
286 ", parameters=" + params);
287 }
288
289 // defaultNamespaceURI should never be null
290 if (this.defaultNamespaceURI == null) {
291 this.defaultNamespaceURI = "";
292 }
293 this.objectModel = objectModel;
294
295 this.request = ObjectModelHelper.getRequest(objectModel);
296 this.response = ObjectModelHelper.getResponse(objectModel);
297 this.context = ObjectModelHelper.getContext(objectModel);
298 this.resolver = resolver;
299 this.parameters = params;
300 this.source = src;
301 this.isInitialized = false;
302
303 // get the current namespace
304 this.namespaceURI = params.getParameter("namespaceURI",
305 this.defaultNamespaceURI);
306
307 this.ignoreHooksCount = 0;
308 this.ignoreEventsCount = 0;
309 this.ignoreWhitespaces = true;
310 this.ignoreEmptyCharacters = false;
311 }
312
313 /* (non-Javadoc)
314 * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
315 */
316 public void recycle() {
317 this.namespaceURI = null;
318 this.objectModel = null;
319 this.request = null;
320 this.response = null;
321 this.context = null;
322 this.resolver = null;
323 this.stack.clear();
324 this.recorderStack.clear();
325 this.parameters = null;
326 this.source = null;
327 this.namespaces.clear();
328 this.ourPrefix = null;
329
330 super.recycle();
331 }
332
333 public void dispose() {
334 this.manager = null;
335 }
336
337 //
338 // SAX ContentHandler methods
339 //
340
341 /**
342 * Process the SAX event.
343 * @see ContentHandler#setDocumentLocator
344 */
345 public void setDocumentLocator(Locator locator) {
346 if (this.ignoreEventsCount == 0) {
347 super.setDocumentLocator(locator);
348 }
349 }
350
351 /**
352 * Process the SAX event. A new document is processed. The hook method
353 * {@link #setupTransforming} is invoked.
354 * @see ContentHandler#startDocument
355 */
356 public void startDocument()
357 throws SAXException {
358 if (!this.isInitialized) {
359 try {
360 setupTransforming();
361 } catch (ProcessingException e) {
362 throw new SAXException("ProcessingException: " + e, e);
363 } catch (IOException e) {
364 throw new SAXException("IOException: " + e, e);
365 }
366 this.isInitialized = true;
367 }
368
369 if (this.ignoreEventsCount == 0) {
370 super.startDocument();
371 }
372 }
373
374 /**
375 * Process the SAX event. The processing of the document is finished.
376 * @see org.xml.sax.ContentHandler#endDocument
377 */
378 public void endDocument()
379 throws SAXException {
380 if (this.ignoreEventsCount == 0) {
381 super.endDocument();
382 }
383 }
384
385 /**
386 * Process the SAX event.
387 * @see org.xml.sax.ContentHandler#startPrefixMapping
388 */
389 public void startPrefixMapping(String prefix, String uri)
390 throws SAXException {
391 if (prefix != null) {
392 this.namespaces.add(new String[] {prefix, uri});
393 }
394 if (namespaceURI.equals(uri)) {
395 this.ourPrefix = prefix;
396 }
397 if (this.ignoreEventsCount == 0) {
398 super.startPrefixMapping(prefix, uri);
399 }
400 }
401
402 /**
403 * Process the SAX event.
404 * @see org.xml.sax.ContentHandler#endPrefixMapping
405 */
406 public void endPrefixMapping(String prefix)
407 throws SAXException {
408
409 if (prefix != null) {
410 // Find and remove the namespace prefix
411 boolean found = false;
412 for (int i = this.namespaces.size() - 1; i >= 0; i--) {
413 final String[] prefixAndUri = (String[]) this.namespaces.get(i);
414 if (prefixAndUri[0].equals(prefix)) {
415 this.namespaces.remove(i);
416 found = true;
417 break;
418 }
419 }
420 if (!found) {
421 throw new SAXException("Namespace for prefix '" + prefix + "' not found.");
422 }
423
424 if (prefix.equals(this.ourPrefix)) {
425 // Reset our current prefix
426 this.ourPrefix = null;
427
428 // Now search if we have a different prefix for our namespace
429 for (int i = this.namespaces.size() - 1; i >= 0; i--) {
430 final String[] prefixAndUri = (String[]) this.namespaces.get(i);
431 if (namespaceURI.equals(prefixAndUri[1])) {
432 this.ourPrefix = prefixAndUri[0];
433 break;
434 }
435 }
436 }
437 }
438
439 if (this.ignoreEventsCount == 0) {
440 super.endPrefixMapping(prefix);
441 }
442 }
443
444 /**
445 * Process the SAX event. The namespace of the event is checked.
446 * If it is the defined namespace for this transformer,
447 * the {@link #startTransformingElement} hook is called.
448 * @see org.xml.sax.ContentHandler#startElement
449 */
450 public void startElement(String uri,
451 String name,
452 String raw,
453 Attributes attr)
454 throws SAXException {
455 if (namespaceURI.equals(uri) && ignoreHooksCount == 0) {
456 // this is our namespace:
457 try {
458 startTransformingElement(uri, name, raw, attr);
459 } catch (ProcessingException e) {
460 throw new SAXException("ProcessingException: " + e, e);
461 } catch (IOException e) {
462 throw new SAXException("IOException occured during processing: " + e, e);
463 }
464 } else {
465 if (ignoreEventsCount == 0) {
466 super.startElement(uri, name, raw, attr);
467 }
468 }
469 }
470
471 /**
472 * Process the SAX event. The namespace of the event is checked.
473 * If it is the defined namespace for this transformer,
474 * the {@link #endTransformingElement} hook is called.
475 * @see org.xml.sax.ContentHandler#endElement
476 */
477 public void endElement(String uri, String name, String raw)
478 throws SAXException {
479 if (namespaceURI.equals(uri) && this.ignoreHooksCount == 0) {
480 // this is our namespace:
481 try {
482 endTransformingElement(uri, name, raw);
483 } catch (ProcessingException e) {
484 throw new SAXException("ProcessingException: " + e, e);
485 } catch (IOException e) {
486 throw new SAXException("IOException occured during processing: " + e, e);
487 }
488 } else {
489 if (ignoreEventsCount == 0) {
490 super.endElement(uri, name, raw);
491 }
492 }
493 }
494
495 /**
496 * Process the SAX event.
497 * @see org.xml.sax.ContentHandler#characters
498 */
499 public void characters(char[] p0, int p1, int p2)
500 throws SAXException {
501 if (this.ignoreEventsCount == 0) {
502 if (this.ignoreEmptyCharacters) {
503 String value = new String(p0, p1, p2);
504 if (value.trim().length() > 0) {
505 super.characters(p0, p1, p2);
506 }
507 } else {
508 super.characters(p0, p1, p2);
509 }
510 }
511 }
512
513 /**
514 * Process the SAX event.
515 * @see org.xml.sax.ContentHandler#ignorableWhitespace
516 */
517 public void ignorableWhitespace(char[] p0, int p1, int p2)
518 throws SAXException {
519 if (ignoreWhitespaces == false && ignoreEventsCount == 0) {
520 super.ignorableWhitespace(p0, p1, p2);
521 }
522 }
523
524 /**
525 * Process the SAX event.
526 * @see ContentHandler#processingInstruction
527 */
528 public void processingInstruction(String target, String data)
529 throws SAXException {
530 if (this.ignoreEventsCount == 0) {
531 super.processingInstruction(target, data);
532 }
533 }
534
535 /**
536 * Process the SAX event.
537 * @see ContentHandler#skippedEntity
538 */
539 public void skippedEntity(String name)
540 throws SAXException {
541 if (this.ignoreEventsCount == 0) {
542 super.skippedEntity(name);
543 }
544 }
545
546 //
547 // SAX LexicalHandler methods
548 //
549
550 /**
551 * @see LexicalHandler#startDTD
552 */
553 public void startDTD(String name, String public_id, String system_id)
554 throws SAXException {
555 if (this.ignoreEventsCount == 0) {
556 super.startDTD(name, public_id, system_id);
557 }
558 }
559
560 /**
561 * @see LexicalHandler#endDTD
562 */
563 public void endDTD() throws SAXException {
564 if (this.ignoreEventsCount == 0) {
565 super.endDTD();
566 }
567 }
568
569 /**
570 * @see LexicalHandler#startEntity
571 */
572 public void startEntity (String name)
573 throws SAXException {
574 if (this.ignoreEventsCount == 0) {
575 super.startEntity(name);
576 }
577 }
578
579 /**
580 * @see LexicalHandler#endEntity
581 */
582 public void endEntity (String name)
583 throws SAXException {
584 if (this.ignoreEventsCount == 0) {
585 super.endEntity(name);
586 }
587 }
588
589 /**
590 * @see LexicalHandler#startCDATA
591 */
592 public void startCDATA() throws SAXException {
593 if (this.ignoreEventsCount == 0) {
594 super.startCDATA();
595 }
596 }
597
598 /**
599 * @see LexicalHandler#endCDATA
600 */
601 public void endCDATA() throws SAXException {
602 if (this.ignoreEventsCount == 0) {
603 super.endCDATA();
604 }
605 }
606
607 /**
608 * @see LexicalHandler#comment
609 */
610 public void comment(char ary[], int start, int length)
611 throws SAXException {
612 if (this.ignoreEventsCount == 0) {
613 super.comment(ary, start, length);
614 }
615 }
616
617
618 /*
619 * Recording of events.
620 * With this method all events are not forwarded to the next component in the pipeline.
621 * They are recorded to create a document fragment.
622 */
623
624 private LexicalHandler originalLexicalHandler;
625 private ContentHandler originalContentHandler;
626
627 /**
628 * Add a new recorder to the recording chain.
629 * Do not invoke this method directly.
630 */
631 protected void addRecorder(XMLConsumer recorder) {
632 if (this.recorderStack.empty()) {
633 // redirect if first (top) recorder
634 this.originalLexicalHandler = this.lexicalHandler;
635 this.originalContentHandler = this.contentHandler;
636 }
637 setContentHandler(recorder);
638 setLexicalHandler(recorder);
639 this.recorderStack.push(recorder);
640 }
641
642 /**
643 * Remove a recorder from the recording chain.
644 * Do not invoke this method directly.
645 */
646 protected Object removeRecorder() {
647 Object recorder = this.recorderStack.pop();
648 if (this.recorderStack.empty() == true) {
649 // undo redirect if no recorder any more
650 setContentHandler(originalContentHandler);
651 setLexicalHandler(originalLexicalHandler);
652 this.originalLexicalHandler = null;
653 this.originalContentHandler = null;
654 } else {
655 XMLConsumer next = (XMLConsumer) recorderStack.peek();
656 setContentHandler(next);
657 setLexicalHandler(next);
658 }
659
660 return recorder;
661 }
662
663 /**
664 * Start recording of SAX events.
665 * All incoming events are recorded and not forwarded. The resulting
666 * XMLizable can be obtained by the matching {@link #endSAXRecording} call.
667 * @since 2.1.5
668 */
669 public void startSAXRecording()
670 throws SAXException {
671 addRecorder(new SaxBuffer());
672 sendStartPrefixMapping();
673 }
674
675 /**
676 * Stop recording of SAX events.
677 * This method returns the resulting XMLizable.
678 * @since 2.1.5
679 */
680 public XMLizable endSAXRecording()
681 throws SAXException {
682 sendEndPrefixMapping();
683 return (XMLizable) removeRecorder();
684 }
685
686 /**
687 * Start recording of a text.
688 * No events forwarded, and all characters events
689 * are collected into a string.
690 */
691 public void startTextRecording()
692 throws SAXException {
693 if (getLogger().isDebugEnabled()) {
694 getLogger().debug("Start text recording");
695 }
696 addRecorder(new TextRecorder());
697 sendStartPrefixMapping();
698 }
699
700 /**
701 * Stop recording of text and return the recorded information.
702 * @return The String, trimmed.
703 */
704 public String endTextRecording()
705 throws SAXException {
706 sendEndPrefixMapping();
707
708 TextRecorder recorder = (TextRecorder) removeRecorder();
709 String text = recorder.getText();
710 if (getLogger().isDebugEnabled()) {
711 getLogger().debug("End text recording. Text=" + text);
712 }
713 return text;
714 }
715
716 /**
717 * Start recording of serialized xml
718 * All events are converted to an xml string which can be retrieved by
719 * endSerializedXMLRecording.
720 * @param format The format for the serialized output. If <CODE>null</CODE>
721 * is specified, the default format is used.
722 */
723 public void startSerializedXMLRecording(Properties format)
724 throws SAXException {
725 if (getLogger().isDebugEnabled()) {
726 getLogger().debug("Start serialized XML recording. Format=" + format);
727 }
728 this.stack.push(format == null? XMLUtils.createPropertiesForXML(false): format);
729 startSAXRecording();
730 }
731
732 /**
733 * Return the serialized xml string.
734 * @return A string containing the recorded xml information, formatted by
735 * the properties passed to the corresponding startSerializedXMLRecording().
736 */
737 public String endSerializedXMLRecording()
738 throws SAXException, ProcessingException {
739 XMLizable xml = endSAXRecording();
740 String text = XMLUtils.serialize(xml, (Properties) this.stack.pop());
741 if (getLogger().isDebugEnabled()) {
742 getLogger().debug("End serialized XML recording. XML=" + text);
743 }
744 return text;
745 }
746
747 /**
748 * Start recording of parameters.
749 * All events are not forwarded and the incoming xml is converted to
750 * parameters. Each toplevel node is a parameter and its text subnodes
751 * form the value.
752 * The Parameters can eiter be retrieved by endParametersRecording().
753 */
754 public void startParametersRecording()
755 throws SAXException {
756 if (getLogger().isDebugEnabled()) {
757 getLogger().debug("Start parameters recording");
758 }
759 addRecorder(new ParametersRecorder());
760 sendStartPrefixMapping();
761 }
762
763 /**
764 * End recording of parameters
765 * If source is null a new parameters object is created, otherwise
766 * the parameters are added to this object.
767 * @param source An optional parameters object.
768 * @return The object containing all parameters.
769 */
770 public SourceParameters endParametersRecording(Parameters source)
771 throws SAXException {
772 sendEndPrefixMapping();
773
774 ParametersRecorder recorder = (ParametersRecorder) this.removeRecorder();
775 SourceParameters parameters = recorder.getParameters(source);
776 if (getLogger().isDebugEnabled()) {
777 getLogger().debug("End parameters recording. Parameters=" + parameters);
778 }
779 return parameters;
780 }
781
782 /**
783 * End recording of parameters
784 * If source is null a new parameters object is created, otherwise
785 * the parameters are added to this object.
786 * @param source An optional parameters object.
787 * @return The object containing all parameters.
788 */
789 public SourceParameters endParametersRecording(SourceParameters source)
790 throws SAXException {
791 sendEndPrefixMapping();
792
793 ParametersRecorder recorder = (ParametersRecorder) removeRecorder();
794 SourceParameters parameters = recorder.getParameters(source);
795 if (getLogger().isDebugEnabled()) {
796 getLogger().debug("End parameters recording. Parameters=" + parameters);
797 }
798 return parameters;
799 }
800
801 /**
802 * Start DOM DocumentFragment recording.
803 * All incoming events are recorded and not forwarded. The resulting
804 * DocumentFragment can be obtained by the matching {@link #endRecording} call.
805 */
806 public void startRecording()
807 throws SAXException {
808 if (getLogger().isDebugEnabled()) {
809 getLogger().debug("Start recording");
810 }
811 DOMBuilder builder = new DOMBuilder(this.tfactory);
812 addRecorder(builder);
813 builder.startDocument();
814 builder.startElement("", "cocoon", "cocoon", EMPTY_ATTRIBUTES);
815 sendStartPrefixMapping();
816 }
817
818 /**
819 * Stop DOM DocumentFragment recording.
820 * This method returns the resulting DocumentFragment, normalized.
821 */
822 public DocumentFragment endRecording()
823 throws SAXException {
824 sendEndPrefixMapping();
825
826 DOMBuilder builder = (DOMBuilder) removeRecorder();
827 builder.endElement("", "cocoon", "cocoon");
828 builder.endDocument();
829
830 // Create Document Fragment
831 final Document doc = builder.getDocument();
832 final DocumentFragment fragment = doc.createDocumentFragment();
833 final Node root = doc.getDocumentElement();
834
835 // Remove empty text nodes and collapse neighbouring text nodes
836 root.normalize();
837
838 // Move all nodes into the fragment
839 boolean space = true;
840 while (root.hasChildNodes()) {
841 Node child = root.getFirstChild();
842 root.removeChild(child);
843
844 // Leave out leading whitespace nodes
845 // FIXME: Why leading spaces are trimmed at all? Why not trailing spaces?
846 if (space && child.getNodeType() == Node.TEXT_NODE
847 && child.getNodeValue().trim().length() == 0) {
848 continue;
849 }
850 space = false;
851
852 fragment.appendChild(child);
853 }
854
855 if (getLogger().isDebugEnabled()) {
856 Object serializedXML = null;
857 try {
858 serializedXML = fragment == null? "null": XMLUtils.serializeNode(fragment, XMLUtils.createPropertiesForXML(false));
859 } catch (ProcessingException ignore) {
860 serializedXML = fragment;
861 }
862 getLogger().debug("End recording. Fragment=" + serializedXML);
863 }
864
865 return fragment;
866 }
867
868 //
869 // Hooks
870 //
871
872 /**
873 * Setup the transformation of an xml document.
874 * This method is called just before the transformation (sending of sax events)
875 * starts. It should be used to initialize setup parameter depending on the
876 * object modell.
877 */
878 public void setupTransforming()
879 throws IOException, ProcessingException, SAXException {
880 if (getLogger().isDebugEnabled()) {
881 getLogger().debug("setupTransforming");
882 }
883 this.stack.clear();
884 this.recorderStack.clear();
885 this.ignoreWhitespaces = true;
886 this.ignoreEmptyCharacters = false;
887 }
888
889 /**
890 * Start processing elements of our namespace.
891 * This hook is invoked for each sax event with our namespace.
892 * @param uri The namespace of the element.
893 * @param name The local name of the element.
894 * @param raw The qualified name of the element.
895 * @param attr The attributes of the element.
896 */
897 public void startTransformingElement(String uri,
898 String name,
899 String raw,
900 Attributes attr)
901 throws ProcessingException, IOException, SAXException {
902 if (this.ignoreEventsCount == 0) {
903 super.startElement(uri, name, raw, attr);
904 }
905 }
906
907 /**
908 * Start processing elements of our namespace.
909 * This hook is invoked for each sax event with our namespace.
910 * @param uri The namespace of the element.
911 * @param name The local name of the element.
912 * @param raw The qualified name of the element.
913 */
914 public void endTransformingElement(String uri,
915 String name,
916 String raw)
917 throws ProcessingException, IOException, SAXException {
918 if (this.ignoreEventsCount == 0) {
919 super.endElement(uri, name, raw);
920 }
921 }
922
923 /**
924 * Send SAX events to the next pipeline component.
925 * The characters event for the given text is send to the next
926 * component in the current pipeline.
927 * @param text The string containing the information.
928 */
929 public void sendTextEvent(String text)
930 throws SAXException {
931 characters(text.toCharArray(), 0, text.length());
932 }
933
934 /**
935 * Send SAX events to the next pipeline component.
936 * The startElement event for the given element is send
937 * to the next component in the current pipeline.
938 * The element has no namespace and no attributes
939 * @param localname The name of the event.
940 */
941 public void sendStartElementEvent(String localname)
942 throws SAXException {
943 startElement("", localname, localname, EMPTY_ATTRIBUTES);
944 }
945
946 /**
947 * Send SAX events to the next pipeline component.
948 * The startElement event for the given element is send
949 * to the next component in the current pipeline.
950 * The element has the namespace of the transformer,
951 * but not attributes
952 * @param localname The name of the event.
953 */
954 public void sendStartElementEventNS(String localname)
955 throws SAXException {
956 startElement(this.namespaceURI,
957 localname, this.ourPrefix + ':' + localname, EMPTY_ATTRIBUTES);
958 }
959
960 /**
961 * Send SAX events to the next pipeline component.
962 * The startElement event for the given element is send
963 * to the next component in the current pipeline.
964 * The element has no namespace.
965 * @param localname The name of the event.
966 * @param attr The Attributes of the element
967 */
968 public void sendStartElementEvent(String localname, Attributes attr)
969 throws SAXException {
970 startElement("", localname, localname, attr);
971 }
972
973 /**
974 * Send SAX events to the next pipeline component.
975 * The startElement event for the given element is send
976 * to the next component in the current pipeline.
977 * The element has the namespace of the transformer.
978 * @param localname The name of the event.
979 * @param attr The Attributes of the element
980 */
981 public void sendStartElementEventNS(String localname, Attributes attr)
982 throws SAXException {
983 startElement(this.namespaceURI,
984 localname, this.ourPrefix + ':' + localname, attr);
985 }
986
987 /**
988 * Send SAX events to the next pipeline component.
989 * The endElement event for the given element is send
990 * to the next component in the current pipeline.
991 * The element has no namespace.
992 * @param localname The name of the event.
993 */
994 public void sendEndElementEvent(String localname)
995 throws SAXException {
996 endElement("", localname, localname);
997 }
998
999 /**
1000 * Send SAX events to the next pipeline component.
1001 * The endElement event for the given element is send
1002 * to the next component in the current pipeline.
1003 * The element has the namespace of the transformer.
1004 * @param localname The name of the event.
1005 */
1006 public void sendEndElementEventNS(String localname)
1007 throws SAXException {
1008 endElement(this.namespaceURI,
1009 localname, this.ourPrefix + ':' + localname);
1010 }
1011
1012 /**
1013 * Send SAX events to the next pipeline component.
1014 * The node is parsed and the events are send to
1015 * the next component in the pipeline.
1016 * @param node The tree to be included.
1017 */
1018 public void sendEvents(Node node)
1019 throws SAXException {
1020 IncludeXMLConsumer.includeNode(node, this, this);
1021 }
1022
1023 /**
1024 * Send SAX events for the <code>SourceParameters</code>.
1025 * For each parametername/value pair an element is
1026 * created with the name of the parameter and the content
1027 * of this element is the value.
1028 */
1029 public void sendParametersEvents(SourceParameters pars)
1030 throws SAXException {
1031
1032 if (pars != null) {
1033 Iterator names = pars.getParameterNames();
1034 while (names.hasNext()) {
1035 final String currentName = (String)names.next();
1036 Iterator values = pars.getParameterValues(currentName);
1037 while (values.hasNext()) {
1038 final String currentValue = (String)values.next();
1039 sendStartElementEvent(currentName);
1040 sendTextEvent(currentValue);
1041 sendEndElementEvent(currentName);
1042 }
1043 }
1044 }
1045 }
1046
1047 /**
1048 * Send all start prefix mapping events to the current content handler
1049 */
1050 protected void sendStartPrefixMapping()
1051 throws SAXException {
1052 final int l = this.namespaces.size();
1053 for (int i = 0; i < l; i++) {
1054 String[] prefixAndUri = (String[]) this.namespaces.get(i);
1055 super.contentHandler.startPrefixMapping(prefixAndUri[0], prefixAndUri[1]);
1056 }
1057 }
1058
1059 /**
1060 * Send all end prefix mapping events to the current content handler
1061 */
1062 protected void sendEndPrefixMapping()
1063 throws SAXException {
1064 final int l = this.namespaces.size();
1065 for (int i = 0; i < l; i++) {
1066 String[] prefixAndUri = (String[]) this.namespaces.get(i);
1067 super.contentHandler.endPrefixMapping(prefixAndUri[0]);
1068 }
1069 }
1070
1071 /**
1072 * Find prefix mapping for the given namespace URI.
1073 * @return Prefix mapping or null if no prefix defined
1074 */
1075 protected String findPrefixMapping(String uri) {
1076 final int l = this.namespaces.size();
1077 for (int i = 0; i < l; i++) {
1078 String[] prefixAndUri = (String[]) this.namespaces.get(i);
1079 if (prefixAndUri[1].equals(uri)) {
1080 return prefixAndUri[0];
1081 }
1082 }
1083
1084 return null;
1085 }
1086
1087 /**
1088 * Helper method to get a modifiable attribute set.
1089 */
1090 protected AttributesImpl getMutableAttributes(Attributes a) {
1091 if ( a instanceof AttributesImpl && !(a instanceof ImmutableAttributesImpl)) {
1092 return (AttributesImpl)a;
1093 }
1094 return new AttributesImpl(a);
1095 }
1096 }