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: 165507 $
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, expression);
174     }
175 
176     /***
177      * Strips off the name of a script to create a new context URL
178      * FIXME: Copied from JellyContext
179      */
180     private URL getJellyContextURL(URL url) throws MalformedURLException {
181         String text = url.toString();
182         int idx = text.lastIndexOf('/');
183         text = text.substring(0, idx + 1);
184         return new URL(text);
185     }
186 
187     // Script interface
188     //-------------------------------------------------------------------------
189 
190     /*** Evaluates the body of a tag */
191     public void run(JellyContext context, XMLOutput output) throws JellyTagException {
192         URL rootURL = context.getRootURL();
193         URL currentURL = context.getCurrentURL();
194         try {
195             Tag tag = getTag(context);
196             if ( tag == null ) {
197                 return;
198             }
199             tag.setContext(context);
200             setContextURLs(context);
201 
202             if ( tag instanceof DynaTag ) {
203                 DynaTag dynaTag = (DynaTag) tag;
204 
205                 // ### probably compiling this to 2 arrays might be quicker and smaller
206                 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
207                     Map.Entry entry = (Map.Entry) iter.next();
208                     String name = (String) entry.getKey();
209                     Expression expression = (Expression) entry.getValue();
210 
211                     Class type = dynaTag.getAttributeType(name);
212                     Object value = null;
213                     if (type != null && type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
214                         value = expression;
215                     }
216                     else {
217                         value = expression.evaluateRecurse(context);
218                     }
219                     dynaTag.setAttribute(name, value);
220                 }
221             }
222             else {
223                 // treat the tag as a bean
224                 DynaBean dynaBean = new ConvertingWrapDynaBean( tag );
225                 for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
226                     Map.Entry entry = (Map.Entry) iter.next();
227                     String name = (String) entry.getKey();
228                     Expression expression = (Expression) entry.getValue();
229 
230                     DynaProperty property = dynaBean.getDynaClass().getDynaProperty(name);
231                     if (property == null) {
232                         throw new JellyException("This tag does not understand the '" + name + "' attribute" );
233                     }
234                     Class type = property.getType();
235 
236                     Object value = null;
237                     if (type.isAssignableFrom(Expression.class) && !type.isAssignableFrom(Object.class)) {
238                         value = expression;
239                     }
240                     else {
241                         value = expression.evaluateRecurse(context);
242                     }
243                     dynaBean.set(name, value);
244                 }
245             }
246 
247             tag.doTag(output);
248             if (output != null) {
249                 output.flush();
250             }
251         }
252         catch (JellyTagException e) {
253             handleException(e);
254         } catch (JellyException e) {
255             handleException(e);
256         } catch (IOException e) {
257             handleException(e);
258         } catch (RuntimeException e) {
259             handleException(e);
260         }
261         catch (Error e) {
262            /*
263             * Not sure if we should be converting errors to exceptions,
264             * but not trivial to remove because JUnit tags throw
265             * Errors in the normal course of operation.  Hmm...
266             */
267             handleException(e);
268         } finally {
269             context.setRootURL(rootURL);
270             context.setCurrentURL(currentURL);
271         }
272 
273     }
274 
275     /***
276      * Set the context's root and current URL if not present
277      * @param context
278      * @throws JellyTagException
279      */
280     protected void setContextURLs(JellyContext context) throws JellyTagException {
281         if ((context.getCurrentURL() == null || context.getRootURL() == null) && scriptURL != null)
282         {
283             if (context.getRootURL() == null) context.setRootURL(scriptURL);
284             if (context.getCurrentURL() == null) context.setCurrentURL(scriptURL);
285         }
286     }
287 
288     // Properties
289     //-------------------------------------------------------------------------
290 
291     /***
292      * @return the tag to be evaluated, creating it lazily if required.
293      */
294     public Tag getTag(JellyContext context) throws JellyException {
295         Thread t = Thread.currentThread();
296         Tag tag = (Tag) threadLocalTagCache.get(t);
297         if ( tag == null ) {
298             tag = createTag();
299             if ( tag != null ) {
300                 threadLocalTagCache.put(t,tag);
301                 configureTag(tag,context);
302             }
303         }
304         return tag;
305     }
306 
307     /***
308      * Returns the Factory of Tag instances.
309      * @return the factory
310      */
311     public TagFactory getTagFactory() {
312         return tagFactory;
313     }
314 
315     /***
316      * Sets the Factory of Tag instances.
317      * @param tagFactory The factory to set
318      */
319     public void setTagFactory(TagFactory tagFactory) {
320         this.tagFactory = tagFactory;
321     }
322 
323     /***
324      * Returns the parent.
325      * @return TagScript
326      */
327     public TagScript getParent() {
328         return parent;
329     }
330 
331     /***
332      * Returns the tagBody.
333      * @return Script
334      */
335     public Script getTagBody() {
336         return tagBody;
337     }
338 
339     /***
340      * Sets the parent.
341      * @param parent The parent to set
342      */
343     public void setParent(TagScript parent) {
344         this.parent = parent;
345     }
346 
347     /***
348      * Sets the tagBody.
349      * @param tagBody The tagBody to set
350      */
351     public void setTagBody(Script tagBody) {
352         this.tagBody = tagBody;
353     }
354 
355     /***
356      * @return the Jelly file which caused the problem
357      */
358     public String getFileName() {
359         return fileName;
360     }
361 
362     /***
363      * Sets the Jelly file which caused the problem
364      */
365     public void setFileName(String fileName) {
366         this.fileName = fileName;
367         try
368         {
369             this.scriptURL = getJellyContextURL(new URL(fileName));
370         } catch (MalformedURLException e) {
371             log.debug("error setting script url", e);
372         }
373     }
374 
375 
376     /***
377      * @return the element name which caused the problem
378      */
379     public String getElementName() {
380         return elementName;
381     }
382 
383     /***
384      * Sets the element name which caused the problem
385      */
386     public void setElementName(String elementName) {
387         this.elementName = elementName;
388     }
389     /***
390      * @return the line number of the tag
391      */
392     public int getLineNumber() {
393         return lineNumber;
394     }
395 
396     /***
397      * Sets the line number of the tag
398      */
399     public void setLineNumber(int lineNumber) {
400         this.lineNumber = lineNumber;
401     }
402 
403     /***
404      * @return the column number of the tag
405      */
406     public int getColumnNumber() {
407         return columnNumber;
408     }
409 
410     /***
411      * Sets the column number of the tag
412      */
413     public void setColumnNumber(int columnNumber) {
414         this.columnNumber = columnNumber;
415     }
416 
417     /***
418      * Returns the SAX attributes of this tag
419      * @return Attributes
420      */
421     public Attributes getSaxAttributes() {
422         return saxAttributes;
423     }
424 
425     /***
426      * Sets the SAX attributes of this tag
427      * @param saxAttributes The saxAttributes to set
428      */
429     public void setSaxAttributes(Attributes saxAttributes) {
430         this.saxAttributes = saxAttributes;
431     }
432 
433     /***
434      * Returns the local, non namespaced XML name of this tag
435      * @return String
436      */
437     public String getLocalName() {
438         return localName;
439     }
440 
441     /***
442      * Sets the local, non namespaced name of this tag.
443      * @param localName The localName to set
444      */
445     public void setLocalName(String localName) {
446         this.localName = localName;
447     }
448 
449 
450     /***
451      * Returns the namespace context of this tag. This is all the prefixes
452      * in scope in the document where this tag is used which are mapped to
453      * their namespace URIs.
454      *
455      * @return a Map with the keys are namespace prefixes and the values are
456      * namespace URIs.
457      */
458     public synchronized Map getNamespaceContext() {
459         if (namespaceContext == null) {
460             if (parent != null) {
461                 namespaceContext = getParent().getNamespaceContext();
462                 if (tagNamespacesMap != null && !tagNamespacesMap.isEmpty()) {
463                     // create a new child context
464                     Hashtable newContext = new Hashtable(namespaceContext.size()+1);
465                     newContext.putAll(namespaceContext);
466                     newContext.putAll(tagNamespacesMap);
467                     namespaceContext = newContext;
468                 }
469             }
470             else {
471                 namespaceContext = tagNamespacesMap;
472                 if (namespaceContext == null) {
473                     namespaceContext = new Hashtable();
474                 }
475             }
476         }
477         return namespaceContext;
478     }
479 
480     // Implementation methods
481     //-------------------------------------------------------------------------
482 
483     /***
484      * Factory method to create a new Tag instance.
485      * The default implementation is to delegate to the TagFactory
486      */
487     protected Tag createTag() throws JellyException {
488         if ( tagFactory != null) {
489             return tagFactory.createTag(localName, getSaxAttributes());
490         }
491         return null;
492     }
493 
494 	
495     /***
496      * Compiles a newly created tag if required, sets its parent and body.
497      */
498     protected void configureTag(Tag tag, JellyContext context) throws JellyException {
499         if (tag instanceof CompilableTag) {
500             ((CompilableTag) tag).compile();
501         }
502         Tag parentTag = null;
503         if ( parent != null ) {
504             parentTag = parent.getTag(context);
505         }
506         tag.setParent( parentTag );
507         tag.setBody( tagBody );
508 
509         if (tag instanceof NamespaceAwareTag) {
510             NamespaceAwareTag naTag = (NamespaceAwareTag) tag;
511             naTag.setNamespaceContext(getNamespaceContext());
512         }
513         if (tag instanceof LocationAware) {
514             applyLocation((LocationAware) tag);
515         }
516     }
517 
518 
519     /***
520      * Allows the script to set the tag instance to be used, such as in a StaticTagScript
521      * when a StaticTag is switched with a DynamicTag
522      */
523     protected void setTag(Tag tag, JellyContext context) {
524         Thread t = Thread.currentThread();
525         threadLocalTagCache.put(t,tag);
526     }
527 
528     /***
529      * Output the new namespace prefixes used for this element
530      */
531     protected void startNamespacePrefixes(XMLOutput output) throws SAXException {
532         if ( tagNamespacesMap != null ) {
533             for ( Iterator iter = tagNamespacesMap.entrySet().iterator(); iter.hasNext(); ) {
534                 Map.Entry entry = (Map.Entry) iter.next();
535                 String prefix = (String) entry.getKey();
536                 String uri = (String) entry.getValue();
537                 output.startPrefixMapping(prefix, uri);
538             }
539         }
540     }
541 
542     /***
543      * End the new namespace prefixes mapped for the current element
544      */
545     protected void endNamespacePrefixes(XMLOutput output) throws SAXException {
546         if ( tagNamespacesMap != null ) {
547             for ( Iterator iter = tagNamespacesMap.keySet().iterator(); iter.hasNext(); ) {
548                 String prefix = (String) iter.next();
549                 output.endPrefixMapping(prefix);
550             }
551         }
552     }
553 
554     /***
555      * Converts the given value to the required type.
556      *
557      * @param value is the value to be converted. This will not be null
558      * @param requiredType the type that the value should be converted to
559      */
560     protected Object convertType(Object value, Class requiredType)
561         throws JellyException {
562         if (requiredType.isInstance(value)) {
563             return value;
564         }
565         if (value instanceof String) {
566             return ConvertUtils.convert((String) value, requiredType);
567         }
568         return value;
569     }
570 
571     /***
572      * Creates a new Jelly exception, adorning it with location information
573      */
574     protected JellyException createJellyException(String reason) {
575         return new JellyException(
576             reason, fileName, elementName, columnNumber, lineNumber
577         );
578     }
579 
580     /***
581      * Creates a new Jelly exception, adorning it with location information
582      */
583     protected JellyException createJellyException(String reason, Exception cause) {
584         if (cause instanceof JellyException) {
585             return (JellyException) cause;
586         }
587 
588         if (cause instanceof InvocationTargetException) {
589             return new JellyException(
590                 reason,
591                 ((InvocationTargetException) cause).getTargetException(),
592                 fileName,
593                 elementName,
594                 columnNumber,
595                 lineNumber);
596         }
597         return new JellyException(
598             reason, cause, fileName, elementName, columnNumber, lineNumber
599         );
600     }
601 
602     /***
603      * A helper method to handle this Jelly exception.
604      * This method adorns the JellyException with location information
605      * such as adding line number information etc.
606      */
607     protected void handleException(JellyTagException e) throws JellyTagException {
608         if (log.isTraceEnabled()) {
609             log.trace( "Caught exception: " + e, e );
610         }
611 
612         applyLocation(e);
613 
614         throw e;
615     }
616 
617     /***
618      * A helper method to handle this Jelly exception.
619      * This method adorns the JellyException with location information
620      * such as adding line number information etc.
621      */
622     protected void handleException(JellyException e) throws JellyTagException {
623         if (log.isTraceEnabled()) {
624             log.trace( "Caught exception: " + e, e );
625         }
626 
627         applyLocation(e);
628 
629         throw new JellyTagException(e);
630     }
631 
632     protected void applyLocation(LocationAware locationAware) {
633         if (locationAware.getLineNumber() == -1) {
634             locationAware.setColumnNumber(columnNumber);
635             locationAware.setLineNumber(lineNumber);
636         }
637         if ( locationAware.getFileName() == null ) {
638             locationAware.setFileName( fileName );
639         }
640         if ( locationAware.getElementName() == null ) {
641             locationAware.setElementName( elementName );
642         }
643     }
644 
645     /***
646      * A helper method to handle this non-Jelly exception.
647      * This method will rethrow the exception, wrapped in a JellyException
648      * while adding line number information etc.
649      */
650     protected void handleException(Exception e) throws JellyTagException {
651         if (log.isTraceEnabled()) {
652             log.trace( "Caught exception: " + e, e );
653         }
654 
655         if (e instanceof LocationAware) {
656             applyLocation((LocationAware) e);
657         }
658 
659         if ( e instanceof JellyException ) {
660             e.fillInStackTrace();
661         }
662 
663         if ( e instanceof InvocationTargetException) {
664             throw new JellyTagException( ((InvocationTargetException)e).getTargetException(),
665                                       fileName,
666                                       elementName,
667                                       columnNumber,
668                                       lineNumber );
669         }
670 
671         throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
672     }
673 
674     /***
675      * A helper method to handle this non-Jelly exception.
676      * This method will rethrow the exception, wrapped in a JellyException
677      * while adding line number information etc.
678      *
679      * Is this method wise?
680      */
681     protected void handleException(Error e) throws Error, JellyTagException {
682         if (log.isTraceEnabled()) {
683             log.trace( "Caught exception: " + e, e );
684         }
685 
686         if (e instanceof LocationAware) {
687             applyLocation((LocationAware) e);
688         }
689 
690         throw new JellyTagException(e, fileName, elementName, columnNumber, lineNumber);
691     }
692 }