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