1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 * <j:useBean var="person" class="com.acme.Person" name="James" location="${loc}"/>
45 * <j:useBean var="order" class="${orderClass}" amount="12" price="123.456"/>
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
82
83
84 /***
85 * @return the bean that has just been created
86 */
87 public Object getBean() {
88 return bean;
89 }
90
91
92
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
103 Class theClass = convertToClass(classObject);
104
105 bean = newInstance(theClass, attributes, output);
106 setBeanProperties(bean, attributes);
107
108
109 invokeBody(output);
110
111 processBean(var, bean);
112 }
113 catch (ClassNotFoundException e) {
114 throw new JellyTagException(e);
115 }
116 }
117
118
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 }