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.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     // Tag interface
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         // set a resolver to locate uri
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         // run any nested param tags
133         this.doNestedParamTag(output);
134 
135         try {
136             // get a reader to provide SAX events to transformer
137             XMLReader xmlReader = this.createXMLReader();
138             xmlReader.setContentHandler(this.transformerHandler);
139             xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY, this.transformerHandler);
140 
141             // handle result differently, depending on if var is specified
142             String varName = this.getVar();
143             if (null == varName) {
144                 // pass the result of the transform out as SAX events
145                 this.transformerHandler.setResult(this.createSAXResult(output));
146                 xmlReader.parse(this.getXMLInputSource());
147             }
148             else {
149                 // pass the result of the transform out as a document
150                 DocumentResult result = new DocumentResult();
151                 this.transformerHandler.setResult(result);
152                 xmlReader.parse(this.getXMLInputSource());
153 
154                 // output the result as a variable
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     // Properties
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     // Implementation methods
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                 // pass if we don't have a systemId
214                 if (null == href)
215                     return null;
216 
217                 // @todo
218                 // #### this is a pretty simplistic implementation.
219                 // #### we should really handle this better such that if
220                 // #### base is specified as an absolute URL
221                 // #### we trim the end off it and append href
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         // if no xml source specified then get from body
255         // otherwise convert it to a SAX source
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         // if no xml source specified then get from tag body
277         // otherwise convert it to an input source
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         // find any nested param tags and run them
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                 } // instanceof
426             } // for
427         } // if
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         // Methods
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             // safety check that we are being used correctly
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         // Helper methods
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             // check to see if we need to parse the body or just invoke it
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         // Properties
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             // often classes will implement LexicalHandler as well
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             // save the error handler
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             // respond to the lexical handler request
659             if (name.equalsIgnoreCase(LEXICAL_HANDLER_PROPERTY)) {
660                 return this.xmlOutput.getLexicalHandler();
661             } else {
662                 // do nothing
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             // respond to the lexical handler setting
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             // do nothing
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             // do nothing
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 }