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.tags.core;
    17
    18  import java.lang.reflect.InvocationTargetException;
    19  import java.util.HashMap;
    20  import java.util.HashSet;
    21  import java.util.Iterator;
    22  import java.util.Map;
    23  import java.util.Set;
    24
    25  import org.apache.commons.beanutils.BeanUtils;
    26  import org.apache.commons.beanutils.PropertyUtils;
    27  import org.apache.commons.jelly.JellyTagException;
    28  import org.apache.commons.jelly.MapTagSupport;
    29  import org.apache.commons.jelly.MissingAttributeException;
    30  import org.apache.commons.jelly.XMLOutput;
    31  import org.apache.commons.jelly.impl.BeanSource;
    32  import org.apache.commons.jelly.util.ClassLoaderUtils;
    33
    34  /***
    35   * A tag which instantiates an instance of the given class
    36   * and then sets the properties on the bean.
    37   * The class can be specified via a {@link java.lang.Class} instance or
    38   * a String which will be used to load the class using either the current
    39   * thread's context class loader or the class loader used to load this
    40   * Jelly library.
    41   *
    42   * This tag can be used it as follows,
    43   * <pre>
    44   * &lt;j:useBean var="person" class="com.acme.Person" name="James" location="${loc}"/&gt;
    45   * &lt;j:useBean var="order" class="${orderClass}" amount="12" price="123.456"/&gt;
    46   * </pre>
    47   *
    48   * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
    49   * @version $Revision: 155420 $
    50   */
    51  public class UseBeanTag extends MapTagSupport implements BeanSource {
    52
    53      /*** the current bean instance */
    54      private Object bean;
    55
    56      /*** the default class to use if no Class is specified */
    57      private Class defaultClass;
    58
    59      /***
    60       * a Set of Strings of property names to ignore (remove from the
    61       * Map of attributes before passing to ConvertUtils)
    62       */
    63      private Set ignoreProperties;
    64
    65      /***
    66       * If this tag finds an attribute in the XML that's not
    67       * ignored by {@link #ignoreProperties} and isn't a
    68       * bean property, should it throw an exception?
    69       * @see #setIgnoreUnknownProperties(boolean)
    70       */
    71      private boolean ignoreUnknownProperties = false;
    72
    73
    74      public UseBeanTag() {
    75      }
    76
    77      public UseBeanTag(Class defaultClass) {
    78          this.defaultClass = defaultClass;
    79      }
    80
    81      // BeanSource interface
    82      //-------------------------------------------------------------------------
    83
    84      /***
    85       * @return the bean that has just been created
    86       */
    87      public Object getBean() {
    88          return bean;
    89      }
    90
    91
    92      // Tag interface
    93      //-------------------------------------------------------------------------
    94      public void doTag(XMLOutput output) throws JellyTagException {
    95          Map attributes = getAttributes();
    96          String var = (String) attributes.get( "var" );
    97          Object classObject = attributes.get( "class" );
    98          addIgnoreProperty("class");
    99          addIgnoreProperty("var");
    100
    101         try {
    102             // this method could return null in derived classes
    103             Class theClass = convertToClass(classObject);
    104
    105             bean = newInstance(theClass, attributes, output);
    106             setBeanProperties(bean, attributes);
    107
    108             // invoke body which could result in other properties being set
    109             invokeBody(output);
    110
    111             processBean(var, bean);
    112         }
    113         catch (ClassNotFoundException e) {
    114             throw new JellyTagException(e);
    115         }
    116     }
    117
    118     // Implementation methods
    119     //-------------------------------------------------------------------------
    120
    121     /***
    122      * Allow derived classes to programatically set the bean
    123      */
    124     protected void setBean(Object bean) {
    125         this.bean = bean;
    126     }
    127
    128     /***
    129      * Attempts to convert the given object to a Class instance.
    130      * If the classObject is already a Class it will be returned
    131      * otherwise it will be converted to a String and loaded
    132      * using the default class loading mechanism.
    133      */
    134     protected Class convertToClass(Object classObject)
    135     throws MissingAttributeException, ClassNotFoundException {
    136         if (classObject instanceof Class) {
    137             return (Class) classObject;
    138         }
    139         else if ( classObject == null ) {
    140             Class theClass = getDefaultClass();
    141             if (theClass == null) {
    142                 throw new MissingAttributeException("class");
    143             }
    144             return theClass;
    145         }
    146         else {
    147             String className = classObject.toString();
    148             return loadClass(className);
    149         }
    150     }
    151
    152     /***
    153      * Loads the given class using the default class loading mechanism
    154      * which is to try use the current Thread's context class loader first
    155      * otherise use the class loader which loaded this class.
    156      */
    157     protected Class loadClass(String className) throws ClassNotFoundException {
    158         return ClassLoaderUtils.loadClass(className, getClass());
    159     }
    160
    161     /***
    162      * Creates a new instance of the given class, which by default will invoke the
    163      * default constructor.
    164      * Derived tags could do something different here.
    165      */
    166     protected Object newInstance(Class theClass, Map attributes, XMLOutput output)
    167     throws JellyTagException {
    168         try {
    169             return theClass.newInstance();
    170         } catch (IllegalAccessException e) {
    171             throw new JellyTagException(e.toString());
    172         } catch (InstantiationException e) {
    173             throw new JellyTagException(e.toString());
    174         }
    175     }
    176
    177     /***
    178      * Sets the properties on the bean. Derived tags could implement some custom
    179      * type conversion etc.
    180      * <p/>
    181      * This method ignores all property names in the Set returned by {@link #getIgnorePropertySet()}.
    182      */
    183     protected void setBeanProperties(Object bean, Map attributes) throws JellyTagException {
    184         Map attrsToUse = new HashMap(attributes);
    185         attrsToUse.keySet().removeAll(getIgnorePropertySet());
    186
    187         validateBeanProperties(bean, attrsToUse);
    188
    189         try {
    190             BeanUtils.populate(bean, attrsToUse);
    191         } catch (IllegalAccessException e) {
    192             throw new JellyTagException("could not set the properties of the bean",e);
    193         } catch (InvocationTargetException e) {
    194             throw new JellyTagException("could not set the properties of the bean",e);
    195         }
    196     }
    197
    198     /***
    199      * If {@link #isIgnoreUnknownProperties()} returns true, make sure that
    200      * every non-ignored ({@see #addIgnoreProperty(String)}) property
    201      * matches a writable property on the target bean.
    202      * @param bean the bean to validate
    203      * @param attributes the list of properties to validate
    204      * @throws JellyTagException when a property is not writeable
    205      */
    206     protected void validateBeanProperties(Object bean, Map attributes) throws JellyTagException {
    207         if (!isIgnoreUnknownProperties()) {
    208             for (Iterator i=attributes.keySet().iterator();i.hasNext();) {
    209                 String attrName = (String)i.next();
    210                 if (! PropertyUtils.isWriteable(bean, attrName)) {
    211                     throw new JellyTagException("No bean property found: " + attrName);
    212                 }
    213             }
    214         }
    215     }
    216
    217     /***
    218      * By default this will export the bean using the given variable if it is defined.
    219      * This Strategy method allows derived tags to process the beans in different ways
    220      * such as to register this bean with its parent tag etc.
    221      */
    222     protected void processBean(String var, Object bean) throws JellyTagException {
    223         if (var != null) {
    224             context.setVariable(var, bean);
    225         }
    226         else {
    227             ArgTag parentArg = (ArgTag)(findAncestorWithClass(ArgTag.class));
    228             if(null != parentArg) {
    229                 parentArg.setValue(bean);
    230             }
    231         }
    232     }
    233
    234     /***
    235      * Allows derived classes to provide a default bean implementation class
    236      */
    237     protected Class getDefaultClass() {
    238         return defaultClass;
    239     }
    240
    241     /***
    242      * Adds a name to the Set of property names that will be skipped when setting
    243      * bean properties. In other words, names added here won't be set into the bean
    244      * if they're present in the attribute Map.
    245      * @param name
    246      */
    247     protected void addIgnoreProperty(String name) {
    248         getIgnorePropertySet().add(name);
    249     }
    250
    251     /***
    252      * @return the Set of property names that should be ignored when setting the
    253      * properties of the bean.
    254      */
    255     protected Set getIgnorePropertySet() {
    256         if (ignoreProperties == null) {
    257             ignoreProperties = new HashSet();
    258         }
    259
    260         return ignoreProperties;
    261     }
    262
    263     /***
    264      * @see {@link #setIgnoreUnknownProperties(boolean)}
    265      * @return
    266      */
    267     public boolean isIgnoreUnknownProperties() {
    268         return ignoreUnknownProperties;
    269     }
    270
    271     /***
    272      * If this tag finds an attribute in the XML that's not
    273      * ignored by {@link #ignoreProperties} and isn't a
    274      * bean property, should it throw an exception?
    275      * @param ignoreUnknownProperties Sets {@link #ignoreUnknownProperties}.
    276      */
    277     public void setIgnoreUnknownProperties(boolean ignoreUnknownProps) {
    278         this.ignoreUnknownProperties = ignoreUnknownProps;
    279     }
    280 }