1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.jelly.tags.xml;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.Reader;
22 import java.io.StringReader;
23 import java.io.StringWriter;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.util.Iterator;
27 import java.util.List;
28
29 import javax.xml.transform.Result;
30 import javax.xml.transform.Source;
31 import javax.xml.transform.TransformerConfigurationException;
32 import javax.xml.transform.TransformerException;
33 import javax.xml.transform.TransformerFactory;
34 import javax.xml.transform.URIResolver;
35 import javax.xml.transform.sax.SAXResult;
36 import javax.xml.transform.sax.SAXSource;
37 import javax.xml.transform.sax.SAXTransformerFactory;
38 import javax.xml.transform.sax.TransformerHandler;
39 import javax.xml.transform.stream.StreamSource;
40
41 import org.apache.commons.jelly.JellyContext;
42 import org.apache.commons.jelly.JellyException;
43 import org.apache.commons.jelly.JellyTagException;
44 import org.apache.commons.jelly.MissingAttributeException;
45 import org.apache.commons.jelly.Script;
46 import org.apache.commons.jelly.Tag;
47 import org.apache.commons.jelly.XMLOutput;
48 import org.apache.commons.jelly.impl.ScriptBlock;
49 import org.apache.commons.jelly.impl.StaticTagScript;
50 import org.apache.commons.jelly.impl.TagScript;
51 import org.apache.commons.jelly.impl.WeakReferenceWrapperScript;
52 import org.apache.commons.logging.Log;
53 import org.apache.commons.logging.LogFactory;
54 import org.dom4j.Document;
55 import org.dom4j.io.DocumentResult;
56 import org.dom4j.io.DocumentSource;
57 import org.xml.sax.ContentHandler;
58 import org.xml.sax.DTDHandler;
59 import org.xml.sax.EntityResolver;
60 import org.xml.sax.ErrorHandler;
61 import org.xml.sax.InputSource;
62 import org.xml.sax.SAXException;
63 import org.xml.sax.SAXNotRecognizedException;
64 import org.xml.sax.SAXNotSupportedException;
65 import org.xml.sax.XMLReader;
66 import org.xml.sax.ext.LexicalHandler;
67 import org.xml.sax.helpers.XMLReaderFactory;
68
69 /*** A tag which parses some XML, applies an xslt transform to it
70 * and defines a variable with the transformed Document.
71 * The XML can either be specified as its body or can be passed in via the
72 * xml property which can be a Reader, InputStream, URL or String URI.
73 *
74 * The XSL can be passed in via the
75 * xslt property which can be a Reader, InputStream, URL or String URI.
76 *
77 * @author Robert Leftwich
78 * @version $Revision: 1.6 $
79 */
80 public class TransformTag extends ParseTag {
81
82 /*** The Log to which logging calls will be made. */
83 private static final Log log = LogFactory.getLog(TransformTag.class);
84
85 /*** Propert name for lexical handler */
86 private static final String LEXICAL_HANDLER_PROPERTY =
87 "http://xml.org/sax/properties/lexical-handler";
88
89 /*** The xslt to parse, either a String URI, a Reader or InputStream */
90 private Object xslt;
91
92 /*** The xsl transformer factory */
93 private SAXTransformerFactory tf;
94
95 /*** the transformer handler, doing the real work */
96 private TransformerHandler transformerHandler;
97
98 /***
99 * Constructor for TransformTag.
100 */
101 public TransformTag() {
102 super();
103 this.tf = (SAXTransformerFactory) TransformerFactory.newInstance();
104 }
105
106
107
108
109 /***
110 * Process this tag instance
111 *
112 * @param output The pipeline for xml events
113 * @throws Exception - when required attributes are missing
114 */
115 public void doTag(XMLOutput output) throws MissingAttributeException, JellyTagException {
116
117 if (null == this.getXslt()) {
118 throw new MissingAttributeException("The xslt attribute cannot be null");
119 }
120
121
122 this.tf.setURIResolver(createURIResolver());
123
124 try {
125 this.transformerHandler =
126 this.tf.newTransformerHandler(this.getObjAsSAXSource(this.getXslt()));
127 }
128 catch (TransformerConfigurationException e) {
129 throw new JellyTagException(e);
130 }
131
132
133 this.doNestedParamTag(output);
134
135 try {
136
137 XMLReader xmlReader = this.createXMLReader();
138 xmlReader.setContentHandler(this.transformerHandler);
139 xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, this.transformerHandler);
140
141
142 String varName = this.getVar();
143 if (null == varName) {
144
145 this.transformerHandler.setResult(this.createSAXResult(output));
146 xmlReader.parse(this.getXMLInputSource());
147 }
148 else {
149
150 DocumentResult result = new DocumentResult();
151 this.transformerHandler.setResult(result);
152 xmlReader.parse(this.getXMLInputSource());
153
154
155 Document transformedDoc = result.getDocument();
156 this.context.setVariable(varName, transformedDoc);
157 }
158 }
159 catch (SAXException e) {
160 throw new JellyTagException(e);
161 }
162 catch (IOException e) {
163 throw new JellyTagException(e);
164 }
165
166 }
167
168
169
170
171 /***
172 * Gets the source of the XSL which is either a String URI, Reader or
173 * InputStream
174 *
175 * @returns xslt The source of the xslt
176 */
177 public Object getXslt() {
178 return this.xslt;
179 }
180
181 /***
182 * Sets the source of the XSL which is either a String URI, Reader or
183 * InputStream
184 *
185 * @param xslt The source of the xslt
186 */
187 public void setXslt(Object xslt) {
188 this.xslt = xslt;
189 }
190
191 public void setParameterValue(String name, Object value) {
192 this.transformerHandler.getTransformer().setParameter(name, value);
193 }
194
195
196
197
198 /***
199 * Creates a new URI Resolver so that URIs inside the XSLT document can be
200 * resolved using the JellyContext
201 *
202 * @return a URI Resolver for the JellyContext
203 */
204 protected URIResolver createURIResolver() {
205 return new URIResolver() {
206 public Source resolve(String href, String base)
207 throws TransformerException {
208
209 if (log.isDebugEnabled() ) {
210 log.debug( "base: " + base + " href: " + href );
211 }
212
213
214 if (null == href)
215 return null;
216
217
218
219
220
221
222 return new StreamSource(context.getResourceAsStream(href));
223 }
224 };
225 }
226
227 /***
228 * Factory method to create a new SAXResult for the given
229 * XMLOutput so that the output of an XSLT transform will go
230 * directly into the XMLOutput that we are given.
231 *
232 * @param output The destination of the transform output
233 * @return A SAXResult for the transfrom output
234 */
235 protected Result createSAXResult(XMLOutput output) {
236 SAXResult result = new SAXResult(output);
237 result.setLexicalHandler(output);
238 return result;
239 }
240
241 /***
242 * Factory method to create a new XMLReader for this tag
243 * so that the input of the XSLT transform comes from
244 * either the xml var, the nested tag or the tag body.
245 *
246 * @return XMLReader for the transform input
247 * @throws SAXException
248 * If the value of the "org.xml.sax.driver" system property
249 * is null, or if the class cannot be loaded and instantiated.
250 */
251 protected XMLReader createXMLReader() throws SAXException {
252 XMLReader xmlReader = null;
253 Object xmlReaderSourceObj = this.getXml();
254
255
256 if (null == xmlReaderSourceObj) {
257 xmlReader = new TagBodyXMLReader(this);
258 }
259 else {
260 xmlReader = XMLReaderFactory.createXMLReader();
261 }
262
263 return xmlReader;
264 }
265
266 /***
267 * Helper method to get the appropriate xml input source
268 * so that the input of the XSLT transform comes from
269 * either the xml var, the nested tag or the tag body.
270 *
271 * @return InputSource for the transform input
272 */
273 protected InputSource getXMLInputSource() {
274 InputSource xmlInputSource = null;
275 Object xmlInputSourceObj = this.getXml();
276
277
278 if (null == xmlInputSourceObj) {
279 xmlInputSource = new TagBodyInputSource();
280 } else {
281 xmlInputSource = this.getInputSourceFromObj(xmlInputSourceObj);
282 }
283 return xmlInputSource;
284 }
285
286 /***
287 * Helper method to convert the specified object to a SAX source
288 *
289 * @return SAXSource from the source object or null
290 */
291 protected SAXSource getObjAsSAXSource(Object saxSourceObj) {
292 SAXSource saxSource = null;
293 if (null != saxSourceObj) {
294 if (saxSourceObj instanceof Document) {
295 saxSource = new DocumentSource((Document) saxSourceObj);
296 } else {
297 InputSource xmlInputSource =
298 this.getInputSourceFromObj(saxSourceObj);
299 saxSource = new SAXSource(xmlInputSource);
300 }
301 }
302
303 return saxSource;
304 }
305
306 /***
307 * Helper method to get an xml input source for the supplied object
308 *
309 * @return InputSource for the object or null
310 */
311 protected InputSource getInputSourceFromObj(Object sourceObj ) {
312 InputSource xmlInputSource = null;
313 if (sourceObj instanceof Document) {
314 SAXSource saxSource = new DocumentSource((Document) sourceObj);
315 xmlInputSource = saxSource.getInputSource();
316 } else {
317 if (sourceObj instanceof String) {
318 String uri = (String) sourceObj;
319 xmlInputSource = new InputSource(context.getResourceAsStream(uri));
320 }
321 else if (sourceObj instanceof Reader) {
322 xmlInputSource = new InputSource((Reader) sourceObj);
323 }
324 else if (sourceObj instanceof InputStream) {
325 xmlInputSource = new InputSource((InputStream) sourceObj);
326 }
327 else if (sourceObj instanceof URL) {
328 String uri = ((URL) sourceObj).toString();
329 xmlInputSource = new InputSource(context.getResourceAsStream(uri));
330 }
331 else if (sourceObj instanceof File) {
332 try {
333 String uri = ((File) sourceObj).toURL().toString();
334 xmlInputSource = new InputSource(context.getResourceAsStream(uri));
335 }
336 catch (MalformedURLException e) {
337 throw new IllegalArgumentException(
338 "This should never occur. We should always be able to convert a File to a URL" + e );
339 }
340 }
341 else {
342 throw new IllegalArgumentException(
343 "Invalid source argument. Must be a String, Reader, InputStream or URL."
344 + " Was type; "
345 + sourceObj.getClass().getName()
346 + " with value: "
347 + sourceObj);
348 }
349 }
350
351 return xmlInputSource;
352 }
353
354 /***
355 * Helper method to run any nested param tags
356 *
357 * @param output The destination for any SAX output (not actually used)
358 */
359 private void doNestedParamTag(XMLOutput output) throws JellyTagException {
360
361 Script bodyScript = this.getBody();
362
363 if (bodyScript instanceof WeakReferenceWrapperScript) {
364 WeakReferenceWrapperScript wrws = (WeakReferenceWrapperScript) bodyScript;
365 invokeNestedTagsOfType(wrws, ParamTag.class,context,output);
366 }else if (bodyScript instanceof ScriptBlock) {
367 ScriptBlock scriptBlock = (ScriptBlock) bodyScript;
368 List scriptList = scriptBlock.getScriptList();
369 for (Iterator iter = scriptList.iterator(); iter.hasNext(); ) {
370 Script script = (Script) iter.next();
371 if (script instanceof TagScript) {
372
373 Tag tag = null;
374 try {
375 tag = ((TagScript) script).getTag();
376 } catch (JellyException e) {
377 throw new JellyTagException(e);
378 }
379
380 if (tag instanceof ParamTag) {
381 script.run(context, output);
382 }
383
384
385 }
386 }
387 }
388 }
389
390 /*** Locates all child TagScripts, whose tags are of the type
391 * given. These tags are executed with the provided JellyContext and output.
392 * <p/>
393 * <strong>This method is in place
394 * to support specific features in the XML tag library and
395 * shouldn't be used by anyone at all.
396 * This method will be removed in a near-future verison of jelly.</strong>
397 * <p/>
398 *
399 * XXX if possible, this is actually more bogus than "containsScriptType", it must be removed ASAP
400 *
401 * @param clazz Execute all child tags of this type
402 * @param output The output to use when executing the tags.
403 * @throws JellyTagException
404 */
405 public void invokeNestedTagsOfType(WeakReferenceWrapperScript wrws, Class clazz, JellyContext context, XMLOutput output) throws JellyTagException {
406 Object bodyScript = wrws.script();
407
408 if (bodyScript instanceof ScriptBlock) {
409 ScriptBlock scriptBlock = (ScriptBlock) bodyScript;
410 List scriptList = scriptBlock.getScriptList();
411 for (Iterator iter = scriptList.iterator(); iter.hasNext(); ) {
412 Script script = (Script) iter.next();
413 if (script instanceof TagScript) {
414
415 Tag tag = null;
416 try {
417 tag = ((TagScript) script).getTag();
418 } catch (JellyException e) {
419 throw new JellyTagException(e);
420 }
421
422 if (tag instanceof ParamTag) {
423 script.run(context, output);
424 }
425 }
426 }
427 }
428 }
429
430
431 /*** A helper class that converts a transform tag body to an XMLReader
432 * to hide the details of where the input for the transform is obtained
433 *
434 * @author <a href="mailto:robert@leftwich.info">Robert Leftwich</a>
435 * @version $Revision: 1.6 $
436 */
437 private class TagBodyXMLReader implements XMLReader {
438
439 /*** The tag whose body is to be read. */
440 private Tag tag;
441
442 /*** The destination for the sax events generated by the reader. */
443 private XMLOutput xmlOutput;
444
445 /*** Storage for a DTDHandler if set by the user of the reader. */
446 private DTDHandler dtdHandler;
447
448 /*** Storage for a ErrorHandler if set by the user of the reader. */
449 private ErrorHandler errorHandler;
450
451 /*** Storage for a EntityResolver if set by the user of the reader. */
452 private EntityResolver entityResolver;
453
454 /***
455 * Construct an XMLReader for the specified Tag
456 *
457 * @param tag The Tag to convert to an XMLReader
458 */
459 public TagBodyXMLReader(Tag tag)
460 {
461 this.tag = tag;
462 this.xmlOutput = new XMLOutput();
463 }
464
465
466
467
468 /***
469 * Parse an XML source.
470 *
471 * @param input The source of the xml
472 * @throws SAXException -
473 * Any SAX exception, possibly wrapping another exception.
474 * @throws IOException -
475 * An IO exception from the parser, possibly from a byte
476 stream or character stream supplied by the application.
477 */
478 public void parse(InputSource input)
479 throws IOException, SAXException
480 {
481
482 if (input instanceof TagBodyInputSource) {
483 this.doInvokeBody();
484 } else {
485 throw new SAXException("Invalid input source");
486 }
487 }
488
489 /***
490 * Parse an XML source specified by a system id
491 *
492 * @param input The system identifier (URI)
493 * @throws SAXException -
494 * Any SAX exception, possibly wrapping another exception.
495 * @throws IOException -
496 * An IO exception from the parser, possibly from a byte
497 stream or character stream supplied by the application.
498 */
499 public void parse(String systemId)
500 throws IOException, SAXException
501 {
502 this.doInvokeBody();
503 }
504
505
506
507
508 /***
509 * Actually invoke the tag body to generate the SAX events
510 *
511 * @throws SAXException -
512 * Any SAX exception, possibly wrapping another exception.
513 */
514 private void doInvokeBody() throws SAXException {
515 try {
516 if (this.shouldParseBody()) {
517 XMLReader anXMLReader = XMLReaderFactory.createXMLReader();
518 anXMLReader.setContentHandler(this.xmlOutput);
519 anXMLReader.setProperty(LEXICAL_HANDLER_PROPERTY,this.xmlOutput);
520 StringWriter writer = new StringWriter();
521 this.tag.invokeBody(XMLOutput.createXMLOutput(writer));
522 Reader reader = new StringReader(writer.toString());
523 anXMLReader.parse(new InputSource(reader));
524 } else {
525 this.tag.invokeBody(this.xmlOutput);
526 }
527 } catch (Exception ex) {
528 throw new SAXException(ex);
529 }
530 }
531
532 /***
533 * Helper method to determin if nested body needs to be parsed by (an
534 * xml parser, i.e. its only text) to generate SAX events or not
535 *
536 * @return True if tag body should be parsed or false if invoked only
537 * @throws JellyTagException
538 */
539 private boolean shouldParseBody() throws JellyTagException {
540 boolean result = false;
541
542 Script bodyScript = this.tag.getBody();
543
544 if (bodyScript instanceof WeakReferenceWrapperScript) {
545 WeakReferenceWrapperScript wrws = (WeakReferenceWrapperScript) bodyScript;
546 return wrws.containsScriptType(StaticTagScript.class);
547 }
548
549 if (bodyScript instanceof ScriptBlock) {
550 ScriptBlock scriptBlock = (ScriptBlock) bodyScript;
551 List scriptList = scriptBlock.getScriptList();
552 for (Iterator iter = scriptList.iterator(); iter.hasNext(); ) {
553 Script script = (Script) iter.next();
554 if (script instanceof StaticTagScript) {
555 result = true;
556 break;
557 }
558 }
559 }
560 return result;
561 }
562
563
564
565
566 /***
567 * Gets the SAX ContentHandler to feed SAX events into
568 *
569 * @return the SAX ContentHandler to use to feed SAX events into
570 */
571 public ContentHandler getContentHandler() {
572 return this.xmlOutput.getContentHandler();
573 }
574
575 /***
576 * Sets the SAX ContentHandler to feed SAX events into
577 *
578 * @param contentHandler is the ContentHandler to use.
579 * This value cannot be null.
580 */
581 public void setContentHandler(ContentHandler contentHandler) {
582 this.xmlOutput.setContentHandler(contentHandler);
583
584 if (contentHandler instanceof LexicalHandler) {
585 this.xmlOutput.setLexicalHandler((LexicalHandler) contentHandler);
586 }
587 }
588
589 /***
590 * Gets the DTD Handler to feed SAX events into
591 *
592 * @return the DTD Handler to use to feed SAX events into
593 */
594 public DTDHandler getDTDHandler() {
595 return this.dtdHandler;
596 }
597
598 /***
599 * Sets the DTD Handler to feed SAX events into
600 *
601 * @param the DTD Handler to use to feed SAX events into
602 */
603 public void setDTDHandler(DTDHandler dtdHandler) {
604 this.dtdHandler = dtdHandler;
605 }
606
607 /***
608 * Gets the Error Handler to feed SAX events into
609 *
610 * @return the Error Handler to use to feed SAX events into
611 */
612 public ErrorHandler getErrorHandler() {
613 return this.errorHandler;
614 }
615
616 /***
617 * Sets the Error Handler to feed SAX events into
618 *
619 * @param the Error Handler to use to feed SAX events into
620 */
621 public void setErrorHandler(ErrorHandler errorHandler) {
622
623 this.errorHandler = errorHandler;
624 }
625
626 /***
627 * Gets the Entity Resolver to feed SAX events into
628 *
629 * @return the Entity Resolver to use to feed SAX events into
630 */
631 public EntityResolver getEntityResolver() {
632 return this.entityResolver;
633 }
634
635 /***
636 * Sets the Entity Resolver to feed SAX events into
637 *
638 * @param the Entity Resolver to use to feed SAX events into
639 */
640 public void setEntityResolver(EntityResolver entityResolver) {
641 this.entityResolver = entityResolver;
642 }
643
644 /***
645 * Lookup the value of a property
646 *
647 * @param name - The property name, which is a fully-qualified URI.
648 * @return - The current value of the property.
649 * @throws SAXNotRecognizedException -
650 * When the XMLReader does not recognize the property name.
651 * @throws SAXNotSupportedException -
652 * When the XMLReader recognizes the property name but
653 * cannot determine its value at this time.
654 */
655 public Object getProperty(String name)
656 throws SAXNotRecognizedException, SAXNotSupportedException
657 {
658
659 if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
660 return this.xmlOutput.getLexicalHandler();
661 } else {
662
663 return null;
664 }
665 }
666
667 /***
668 * Set the value of a property
669 *
670 * @param name - The property name, which is a fully-qualified URI.
671 * @param value - The property value
672 * @throws SAXNotRecognizedException -
673 * When the XMLReader does not recognize the property name.
674 * @throws SAXNotSupportedException -
675 * When the XMLReader recognizes the property name but
676 * cannot determine its value at this time.
677 */
678 public void setProperty(String name, Object value)
679 throws SAXNotRecognizedException, SAXNotSupportedException
680 {
681
682 if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
683 this.xmlOutput.setLexicalHandler((LexicalHandler) value);
684 }
685 }
686
687 /***
688 * Lookup the value of a feature
689 *
690 * @param name - The feature name, which is a fully-qualified URI.
691 * @return - The current state of the feature (true or false)
692 * @throws SAXNotRecognizedException -
693 * When the XMLReader does not recognize the feature name.
694 * @throws SAXNotSupportedException -
695 * When the XMLReader recognizes the feature name but
696 * cannot determine its value at this time.
697 */
698 public boolean getFeature(String name)
699 throws SAXNotRecognizedException, SAXNotSupportedException
700 {
701
702 return false;
703 }
704
705 /***
706 * Set the value of a feature
707 *
708 * @param name - The feature name, which is a fully-qualified URI.
709 * @param value - The current state of the feature (true or false)
710 * @throws SAXNotRecognizedException -
711 * When the XMLReader does not recognize the feature name.
712 * @throws SAXNotSupportedException -
713 * When the XMLReader recognizes the feature name but
714 * cannot determine its value at this time.
715 */
716 public void setFeature(String name, boolean value)
717 throws SAXNotRecognizedException, SAXNotSupportedException
718 {
719
720 }
721 }
722
723 /*** A marker class used by the TagBodyXMLReader as a sanity check
724 * (i.e. The source is not actually used)
725 *
726 */
727 private class TagBodyInputSource extends InputSource {
728
729 /***
730 * Construct an instance of this marker class
731 */
732 public TagBodyInputSource() {
733 }
734 }
735
736 }