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 }