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