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.impl;
17  
18  import java.io.IOException;
19  import java.lang.reflect.InvocationTargetException;
20  import java.net.MalformedURLException;
21  import java.net.URL;
22  import java.util.Hashtable;
23  import java.util.Iterator;
24  import java.util.Map;
25  
26  import org.apache.commons.beanutils.ConvertingWrapDynaBean;
27  import org.apache.commons.beanutils.ConvertUtils;
28  import org.apache.commons.beanutils.DynaBean;
29  import org.apache.commons.beanutils.DynaProperty;
30  
31  import org.apache.commons.jelly.CompilableTag;
32  import org.apache.commons.jelly.JellyContext;
33  import org.apache.commons.jelly.JellyException;
34  import org.apache.commons.jelly.JellyTagException;
35  import org.apache.commons.jelly.DynaTag;
36  import org.apache.commons.jelly.LocationAware;
37  import org.apache.commons.jelly.NamespaceAwareTag;
38  import org.apache.commons.jelly.Script;
39  import org.apache.commons.jelly.Tag;
40  import org.apache.commons.jelly.XMLOutput;
41  import org.apache.commons.jelly.expression.Expression;
42  
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  import org.xml.sax.Attributes;
47  import org.xml.sax.Locator;
48  import org.xml.sax.SAXException;
49  
50  /***
51   * <p><code>TagScript</code> is a Script that evaluates a custom tag.</p>
52   *
53   * <b>Note</b> that this class should be re-entrant and used
54   * concurrently by multiple threads.
55   *
56   * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
57   * @version $Revision: 1.46 $
58   */
59  public class TagScript implements Script {
60  
61      /*** The Log to which logging calls will be made. */
62      private static final Log log = LogFactory.getLog(TagScript.class);
63  
64      /***
65       * Thread local storage for the tag used by the current thread.
66       * This allows us to pool tag instances, per thread to reduce object construction
67       * over head, if we need it.
68       *
69       * Note that we could use the stack and create a new tag for each invocation
70       * if we made a slight change to the Script API to pass in the parent tag.
71       */
72      private ThreadLocal tagHolder = new ThreadLocal();
73  
74      /*** The attribute expressions that are created */
75      protected Map attributes = new Hashtable();
76  
77      /*** the optional namespaces Map of prefix -> URI of this single Tag */
78      private Map tagNamespacesMap;
79  
80      /***
81       * The optional namespace context mapping all prefixes -> URIs in scope
82       * at the point this tag is used.
83       * This Map is only created lazily if it is required by the NamespaceAwareTag.
84       */
85      private Map namespaceContext;
86  
87      /*** the Jelly file which caused the problem */
88      private String fileName;
89  
90      /*** the qualified element name which caused the problem */
91      private String elementName;
92  
93      /*** the local (non-namespaced) tag name */
94      private String localName;
95  
96      /*** the line number of the tag */
97      private int lineNumber = -1;
98  
99      /*** the column number of the tag */
100     private int columnNumber = -1;
101 
102     /*** the factory of Tag instances */
103     private TagFactory tagFactory;
104 
105     /*** the body script used for this tag */
106     private Script tagBody;
107 
108     /*** the parent TagScript */
109     private TagScript parent;
110 
111     /*** the SAX attributes */
112     private Attributes saxAttributes;
113     
114     /*** the url of the script when parsed */
115     private URL scriptURL = null;
116 
117     /***
118      * @return a new TagScript based on whether
119      * the given Tag class is a bean tag or DynaTag
120      */
121     public static TagScript newInstance(Class tagClass) {
122         TagFactory factory = new DefaultTagFactory(tagClass);
123         return new TagScript(factory);
124     }
125 
126     public TagScript() {
127     }
128 
129     public TagScript(TagFactory tagFactory) {
130         this.tagFactory = tagFactory;
131     }
132 
133     public String toString() {
134         return super.toString() + "[tag=" + elementName + ";at=" + lineNumber + ":" + columnNumber + "]";
135     }
136 
137     /***
138      * Compiles the tags body
139      */
140     public Script compile() throws JellyException {
141         if (tagBody != null) {
142             tagBody = tagBody.compile();
143         }
144         return this;
145     }
146 
147     /***
148      * Sets the optional namespaces prefix -> URI map of
149      * the namespaces attached to this Tag
150      */
151     public void setTagNamespacesMap(Map tagNamespacesMap) {
152         // lets check that this is a thread-safe map
153         if ( ! (tagNamespacesMap instanceof Hashtable) ) {
154             tagNamespacesMap = new Hashtable( tagNamespacesMap );
155         }
156         this.tagNamespacesMap = tagNamespacesMap;
157     }
158 
159     /***
160      * Configures this TagScript from the SAX Locator, setting the column
161      * and line numbers
162      */
163     public void setLocator(Locator locator) {
164         setLineNumber( locator.getLineNumber() );
165         setColumnNumber( locator.getColumnNumber() );
166     }
167 
168 
169     /*** Add an initialization attribute for the tag.
170      * This method must be called after the setTag() method
171      */
172     public void addAttribute(String name, Expression expression) {
173         if (log.isDebugEnabled()) {
174             log.debug("adding attribute name: " + name + " expression: " + expression);
175         }
176         attributes.put(name, expression);
177     }
178 
179     /***
180      * Strips off the name of a script to create a new context URL
181      * FIXME: Copied from JellyContext
182      */
183     private URL getJellyContextURL(URL url) throws MalformedURLException {
184         String text = url.toString();
185         int idx = text.lastIndexOf('/');
186         text = text.substring(0, idx + 1);
187         return new URL(text);
188     }
189 
190     // Script interface
191     //-------------------------------------------------------------------------
192 
193     /*** Evaluates the body of a tag */
194     public void run(JellyContext context, XMLOutput output) throws JellyTagException {
195         URL rootURL = context.getRootURL();
196         URL currentURL = context.getCurrentURL();
197         if ( ! context.isCacheTags() ) {
198             clearTag();
199         }
200         try {
201             Tag tag = getTag();
202             if ( tag == null ) {
203                 return;
204             }
205             tag.setContext(context);
206             setContextURLs(context);
207 
208             if ( tag instanceof DynaTag ) {
209                 DynaTag dynaTag = (DynaTag) tag;
210 
211                 // ### probably compiling this to 2 arrays might be quicker and smaller
212                 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
213                     Map.Entry entry = (Map.Entry) iter.next();
214                     String name = (String) entry.getKey();
215                     Expression expression = (Expression) entry.getValue();
216 
217                     Class type = dynaTag.getAttributeType(name);
218                     Object value = null;
219                     if (type != null && type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
220                         value = expression;
221                     }
222                     else {
223                         value = expression.evaluateRecurse(context);
224                     }
225                     dynaTag.setAttribute(name, value);
226                 }
227             }
228             else {
229                 // treat the tag as a bean
230                 DynaBean dynaBean = new ConvertingWrapDynaBean( tag );
231                 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
232                     Map.Entry entry = (Map.Entry) iter.next();
233                     String name = (String) entry.getKey();
234                     Expression expression = (Expression) entry.getValue();
235 
236                     DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
237                     if (property == null) {
238                         throw new JellyException("This tag does not understand the '" + name + "' attribute" );
239                     }
240                     Class type = property.getType();
241 
242                     Object value = null;
243                     if (type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
244                         value = expression;
245                     }
246                     else {
247                         value = expression.evaluateRecurse(context);
248                     }
249                     dynaBean.set(name, value);
250                 }
251             }
252 
253             tag.doTag(output);
254             output.flush();
255         }
256         catch (JellyTagException e) {
257             handleException(e);
258         } catch (JellyException e) {
259             handleException(e);
260         } catch (IOException e) {
261             handleException(e);
262         } catch (RuntimeException e) {
263             handleException(e);
264         }
265         catch (Error e) {
266            /*
267             * Not sure if we should be converting errors to exceptions,
268             * but not trivial to remove because JUnit tags throw
269             * Errors in the normal course of operation.  Hmm...
270             */
271             handleException(e);
272         } finally {
273             context.setRootURL(rootURL);
274             context.setCurrentURL(currentURL);
275         }
276 
277     }
278 
279     /***
280      * Set the context's root and current URL if not present
281      * @param context
282      * @throws JellyTagException
283      */
284     protected void setContextURLs(JellyContext context) throws JellyTagException {
285         if ((context.getCurrentURL() == null || context.getRootURL() == null) && scriptURL != null)
286         {
287             if (context.getRootURL() == null) context.setRootURL(scriptURL);
288             if (context.getCurrentURL() == null) context.setCurrentURL(scriptURL);
289         }
290     }
291 
292     // Properties
293     //-------------------------------------------------------------------------
294 
295     /***
296      * @return the tag to be evaluated, creating it lazily if required.
297      */
298     public Tag getTag() throws JellyException {
299         Tag tag = (Tag) tagHolder.get();
300         if ( tag == null ) {
301             tag = createTag();
302             if ( tag != null ) {
303                 tagHolder.set(tag);
304             }
305         }
306         configureTag(tag);
307         return tag;
308     }
309 
310     /***
311      * Returns the Factory of Tag instances.
312      * @return the factory
313      */
314     public TagFactory getTagFactory() {
315         return tagFactory;
316     }
317 
318     /***
319      * Sets the Factory of Tag instances.
320      * @param tagFactory The factory to set
321      */
322     public void setTagFactory(TagFactory tagFactory) {
323         this.tagFactory = tagFactory;
324     }
325 
326     /***
327      * Returns the parent.
328      * @return TagScript
329      */
330     public TagScript getParent() {
331         return parent;
332     }
333 
334     /***
335      * Returns the tagBody.
336      * @return Script
337      */
338     public Script getTagBody() {
339         return tagBody;
340     }
341 
342     /***
343      * Sets the parent.
344      * @param parent The parent to set
345      */
346     public void setParent(TagScript parent) {
347         this.parent = parent;
348     }
349 
350     /***
351      * Sets the tagBody.
352      * @param tagBody The tagBody to set
353      */
354     public void setTagBody(Script tagBody) {
355         this.tagBody = tagBody;
356     }
357 
358     /***
359      * @return the Jelly file which caused the problem
360      */
361     public String getFileName() {
362         return fileName;
363     }
364 
365     /***
366      * Sets the Jelly file which caused the problem
367      */
368     public void setFileName(String fileName) {
369         this.fileName = fileName;
370         try
371         {
372             this.scriptURL = getJellyContextURL(new URL(fileName));
373         } catch (MalformedURLException e) {
374             log.debug("error setting script url", e);
375         }
376     }
377 
378 
379     /***
380      * @return the element name which caused the problem
381      */
382     public String getElementName() {
383         return elementName;
384     }
385 
386     /***
387      * Sets the element name which caused the problem
388      */
389     public void setElementName(String elementName) {
390         this.elementName = elementName;
391     }
392     /***
393      * @return the line number of the tag
394      */
395     public int getLineNumber() {
396         return lineNumber;
397     }
398 
399     /***
400      * Sets the line number of the tag
401      */
402     public void setLineNumber(int lineNumber) {
403         this.lineNumber = lineNumber;
404     }
405 
406     /***
407      * @return the column number of the tag
408      */
409     public int getColumnNumber() {
410         return columnNumber;
411     }
412 
413     /***
414      * Sets the column number of the tag
415      */
416     public void setColumnNumber(int columnNumber) {
417         this.columnNumber = columnNumber;
418     }
419 
420     /***
421      * Returns the SAX attributes of this tag
422      * @return Attributes
423      */
424     public Attributes getSaxAttributes() {
425         return saxAttributes;
426     }
427 
428     /***
429      * Sets the SAX attributes of this tag
430      * @param saxAttributes The saxAttributes to set
431      */
432     public void setSaxAttributes(Attributes saxAttributes) {
433         this.saxAttributes = saxAttributes;
434     }
435 
436     /***
437      * Returns the local, non namespaced XML name of this tag
438      * @return String
439      */
440     public String getLocalName() {
441         return localName;
442     }
443 
444     /***
445      * Sets the local, non namespaced name of this tag.
446      * @param localName The localName to set
447      */
448     public void setLocalName(String localName) {
449         this.localName = localName;
450     }
451 
452 
453     /***
454      * Returns the namespace context of this tag. This is all the prefixes
455      * in scope in the document where this tag is used which are mapped to
456      * their namespace URIs.
457      *
458      * @return a Map with the keys are namespace prefixes and the values are
459      * namespace URIs.
460      */
461     public synchronized Map getNamespaceContext() {
462         if (namespaceContext == null) {
463             if (parent != null) {
464                 namespaceContext = getParent().getNamespaceContext();
465                 if (tagNamespacesMap != null && !tagNamespacesMap.isEmpty()) {
466                     // create a new child context
467                     Hashtable newContext = new Hashtable(namespaceContext.size()+1);
468                     newContext.putAll(namespaceContext);
469                     newContext.putAll(tagNamespacesMap);
470                     namespaceContext = newContext;
471                 }
472             }
473             else {
474                 namespaceContext = tagNamespacesMap;
475                 if (namespaceContext == null) {
476                     namespaceContext = new Hashtable();
477                 }
478             }
479         }
480         return namespaceContext;
481     }
482 
483     // Implementation methods
484     //-------------------------------------------------------------------------
485 
486     /***
487      * Factory method to create a new Tag instance.
488      * The default implementation is to delegate to the TagFactory
489      */
490     protected Tag createTag() throws JellyException {
491         if ( tagFactory != null) {
492             return tagFactory.createTag(localName, getSaxAttributes());
493         }
494         return null;
495     }
496 
497 
498     /***
499      * Compiles a newly created tag if required, sets its parent and body.
500      */
501     protected void configureTag(Tag tag) throws JellyException {
502         if (tag instanceof CompilableTag) {
503             ((CompilableTag) tag).compile();
504         }
505         Tag parentTag = null;
506         if ( parent != null ) {
507             parentTag = parent.getTag();
508         }
509         tag.setParent( parentTag );
510         tag.setBody( new WeakReferenceWrapperScript(tagBody) );
511         //tag.setBody( tagBody );
512 
513         if (tag instanceof NamespaceAwareTag) {
514             NamespaceAwareTag naTag = (NamespaceAwareTag) tag;
515             naTag.setNamespaceContext(getNamespaceContext());
516         }
517         if (tag instanceof LocationAware) {
518             applyLocation((LocationAware) tag);
519         }
520     }
521 
522     /***
523      * Flushes the current cached tag so that it will be created, lazily, next invocation
524      */
525     protected void clearTag() {
526         tagHolder.set(null);
527     }
528 
529     /***
530      * Allows the script to set the tag instance to be used, such as in a StaticTagScript
531      * when a StaticTag is switched with a DynamicTag
532      */
533     protected void setTag(Tag tag) {
534         tagHolder.set(tag);
535     }
536 
537     /***
538      * Output the new namespace prefixes used for this element
539      */
540     protected void startNamespacePrefixes(XMLOutput output) throws SAXException {
541         if ( tagNamespacesMap != null ) {
542             for ( Iterator iter = tagNamespacesMap.entrySet().iterator(); iter.hasNext(); ) {
543                 Map.Entry entry = (Map.Entry) iter.next();
544                 String prefix = (String) entry.getKey();
545                 String uri = (String) entry.getValue();
546                 output.startPrefixMapping(prefix, uri);
547             }
548         }
549     }
550 
551     /***
552      * End the new namespace prefixes mapped for the current element
553      */
554     protected void endNamespacePrefixes(XMLOutput output) throws SAXException {
555         if ( tagNamespacesMap != null ) {
556             for ( Iterator iter = tagNamespacesMap.keySet().iterator(); iter.hasNext(); ) {
557                 String prefix = (String) iter.next();
558                 output.endPrefixMapping(prefix);
559             }
560         }
561     }
562 
563     /***
564      * Converts the given value to the required type.
565      *
566      * @param value is the value to be converted. This will not be null
567      * @param requiredType the type that the value should be converted to
568      */
569     protected Object convertType(Object value, Class requiredType)
570         throws JellyException {
571         if (requiredType.isInstance(value)) {
572             return value;
573         }
574         if (value instanceof String) {
575             return ConvertUtils.convert((String) value, requiredType);
576         }
577         return value;
578     }
579 
580     /***
581      * Creates a new Jelly exception, adorning it with location information
582      */
583     protected JellyException createJellyException(String reason) {
584         return new JellyException(
585             reason, fileName, elementName, columnNumber, lineNumber
586         );
587     }
588 
589     /***
590      * Creates a new Jelly exception, adorning it with location information
591      */
592     protected JellyException createJellyException(String reason, Exception cause) {
593         if (cause instanceof JellyException) {
594             return (JellyException) cause;
595         }
596 
597         if (cause instanceof InvocationTargetException) {
598             return new JellyException(
599                 reason,
600                 ((InvocationTargetException) cause).getTargetException(),
601                 fileName,
602                 elementName,
603                 columnNumber,
604                 lineNumber);
605         }
606         return new JellyException(
607             reason, cause, fileName, elementName, columnNumber, lineNumber
608         );
609     }
610 
611     /***
612      * A helper method to handle this Jelly exception.
613      * This method adorns the JellyException with location information
614      * such as adding line number information etc.
615      */
616     protected void handleException(JellyTagException e) throws JellyTagException {
617         if (log.isTraceEnabled()) {
618             log.trace( "Caught exception: " + e, e );
619         }
620 
621         applyLocation(e);
622 
623         throw e;
624     }
625 
626     /***
627      * A helper method to handle this Jelly exception.
628      * This method adorns the JellyException with location information
629      * such as adding line number information etc.
630      */
631     protected void handleException(JellyException e) throws JellyTagException {
632         if (log.isTraceEnabled()) {
633             log.trace( "Caught exception: " + e, e );
634         }
635 
636         applyLocation(e);
637 
638         throw new JellyTagException(e);
639     }
640 
641     protected void applyLocation(LocationAware locationAware) {
642         if (locationAware.getLineNumber() == -1) {
643             locationAware.setColumnNumber(columnNumber);
644             locationAware.setLineNumber(lineNumber);
645         }
646         if ( locationAware.getFileName() == null ) {
647             locationAware.setFileName( fileName );
648         }
649         if ( locationAware.getElementName() == null ) {
650             locationAware.setElementName( elementName );
651         }
652     }
653 
654     /***
655      * A helper method to handle this non-Jelly exception.
656      * This method will rethrow the exception, wrapped in a JellyException
657      * while adding line number information etc.
658      */
659     protected void handleException(Exception e) throws JellyTagException {
660         if (log.isTraceEnabled()) {
661             log.trace( "Caught exception: " + e, e );
662         }
663 
664         if (e instanceof LocationAware) {
665             applyLocation((LocationAware) e);
666         }
667 
668         if ( e instanceof JellyException ) {
669             e.fillInStackTrace();
670         }
671 
672         if ( e instanceof InvocationTargetException) {
673             throw new JellyTagException( ((InvocationTargetException)e).getTargetException(),
674                                       fileName,
675                                       elementName,
676                                       columnNumber,
677                                       lineNumber );
678         }
679 
680         throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
681     }
682 
683     /***
684      * A helper method to handle this non-Jelly exception.
685      * This method will rethrow the exception, wrapped in a JellyException
686      * while adding line number information etc.
687      *
688      * Is this method wise?
689      */
690     protected void handleException(Error e) throws Error, JellyTagException {
691         if (log.isTraceEnabled()) {
692             log.trace( "Caught exception: " + e, e );
693         }
694 
695         if (e instanceof LocationAware) {
696             applyLocation((LocationAware) e);
697         }
698 
699         throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
700     }
701 }