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