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 }