View Javadoc

1   /*
2    * Copyright 2002,2004 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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     // Tag interface
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         // set a resolver to locate uri
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         // run any nested param tags
132         this.doNestedParamTag(output);
133 
134         try {
135             // get a reader to provide SAX events to transformer
136             XMLReader xmlReader = this.createXMLReader();
137             xmlReader.setContentHandler(this.transformerHandler);
138             xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, this.transformerHandler);
139 
140             // handle result differently, depending on if var is specified
141             String varName = this.getVar();
142             if (null == varName) {
143                 // pass the result of the transform out as SAX events
144                 this.transformerHandler.setResult(this.createSAXResult(output));
145                 xmlReader.parse(this.getXMLInputSource());
146             }
147             else {
148                 // pass the result of the transform out as a document
149                 DocumentResult result = new DocumentResult();
150                 this.transformerHandler.setResult(result);
151                 xmlReader.parse(this.getXMLInputSource());
152 
153                 // output the result as a variable
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     // Properties
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     // Implementation methods
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                 // pass if we don't have a systemId
213                 if (null == href)
214                     return null;
215 
216                 // @todo
217                 // #### this is a pretty simplistic implementation.
218                 // #### we should really handle this better such that if
219                 // #### base is specified as an absolute URL
220                 // #### we trim the end off it and append href
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         // if no xml source specified then get from body
254         // otherwise convert it to a SAX source
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         // if no xml source specified then get from tag body
276         // otherwise convert it to an input source
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         // find any nested param tags and run them
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         // Methods
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             // safety check that we are being used correctly
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         // Helper methods
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             // check to see if we need to parse the body or just invoke it
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         // Properties
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             // often classes will implement LexicalHandler as well
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             // save the error handler
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             // respond to the lexical handler request
610             if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
611                 return this.xmlOutput.getLexicalHandler();
612             } else {
613                 // do nothing
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             // respond to the lexical handler setting
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             // do nothing
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             // do nothing
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 }