001: /*
002: * Copyright 2002,2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.apache.commons.jelly.tags.core;
017:
018: import java.lang.reflect.InvocationTargetException;
019: import java.util.HashMap;
020: import java.util.HashSet;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.Set;
024:
025: import org.apache.commons.beanutils.BeanUtils;
026: import org.apache.commons.beanutils.PropertyUtils;
027: import org.apache.commons.jelly.JellyTagException;
028: import org.apache.commons.jelly.MapTagSupport;
029: import org.apache.commons.jelly.MissingAttributeException;
030: import org.apache.commons.jelly.XMLOutput;
031: import org.apache.commons.jelly.impl.BeanSource;
032: import org.apache.commons.jelly.util.ClassLoaderUtils;
033:
034: /**
035: * A tag which instantiates an instance of the given class
036: * and then sets the properties on the bean.
037: * The class can be specified via a {@link java.lang.Class} instance or
038: * a String which will be used to load the class using either the current
039: * thread's context class loader or the class loader used to load this
040: * Jelly library.
041: *
042: * This tag can be used it as follows,
043: * <pre>
044: * <j:useBean var="person" class="com.acme.Person" name="James" location="${loc}"/>
045: * <j:useBean var="order" class="${orderClass}" amount="12" price="123.456"/>
046: * </pre>
047: *
048: * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
049: * @version $Revision: 155420 $
050: */
051: public class UseBeanTag extends MapTagSupport implements BeanSource {
052:
053: /** the current bean instance */
054: private Object bean;
055:
056: /** the default class to use if no Class is specified */
057: private Class defaultClass;
058:
059: /**
060: * a Set of Strings of property names to ignore (remove from the
061: * Map of attributes before passing to ConvertUtils)
062: */
063: private Set ignoreProperties;
064:
065: /**
066: * If this tag finds an attribute in the XML that's not
067: * ignored by {@link #ignoreProperties} and isn't a
068: * bean property, should it throw an exception?
069: * @see #setIgnoreUnknownProperties(boolean)
070: */
071: private boolean ignoreUnknownProperties = false;
072:
073: public UseBeanTag() {
074: }
075:
076: public UseBeanTag(Class defaultClass) {
077: this .defaultClass = defaultClass;
078: }
079:
080: // BeanSource interface
081: //-------------------------------------------------------------------------
082:
083: /**
084: * @return the bean that has just been created
085: */
086: public Object getBean() {
087: return bean;
088: }
089:
090: // Tag interface
091: //-------------------------------------------------------------------------
092: public void doTag(XMLOutput output) throws JellyTagException {
093: Map attributes = getAttributes();
094: String var = (String) attributes.get("var");
095: Object classObject = attributes.get("class");
096: addIgnoreProperty("class");
097: addIgnoreProperty("var");
098:
099: try {
100: // this method could return null in derived classes
101: Class theClass = convertToClass(classObject);
102:
103: bean = newInstance(theClass, attributes, output);
104: setBeanProperties(bean, attributes);
105:
106: // invoke body which could result in other properties being set
107: invokeBody(output);
108:
109: processBean(var, bean);
110: } catch (ClassNotFoundException e) {
111: throw new JellyTagException(e);
112: }
113: }
114:
115: // Implementation methods
116: //-------------------------------------------------------------------------
117:
118: /**
119: * Allow derived classes to programatically set the bean
120: */
121: protected void setBean(Object bean) {
122: this .bean = bean;
123: }
124:
125: /**
126: * Attempts to convert the given object to a Class instance.
127: * If the classObject is already a Class it will be returned
128: * otherwise it will be converted to a String and loaded
129: * using the default class loading mechanism.
130: */
131: protected Class convertToClass(Object classObject)
132: throws MissingAttributeException, ClassNotFoundException {
133: if (classObject instanceof Class) {
134: return (Class) classObject;
135: } else if (classObject == null) {
136: Class theClass = getDefaultClass();
137: if (theClass == null) {
138: throw new MissingAttributeException("class");
139: }
140: return theClass;
141: } else {
142: String className = classObject.toString();
143: return loadClass(className);
144: }
145: }
146:
147: /**
148: * Loads the given class using the default class loading mechanism
149: * which is to try use the current Thread's context class loader first
150: * otherise use the class loader which loaded this class.
151: */
152: protected Class loadClass(String className)
153: throws ClassNotFoundException {
154: return ClassLoaderUtils.loadClass(className, getClass());
155: }
156:
157: /**
158: * Creates a new instance of the given class, which by default will invoke the
159: * default constructor.
160: * Derived tags could do something different here.
161: */
162: protected Object newInstance(Class theClass, Map attributes,
163: XMLOutput output) throws JellyTagException {
164: try {
165: return theClass.newInstance();
166: } catch (IllegalAccessException e) {
167: throw new JellyTagException(e.toString());
168: } catch (InstantiationException e) {
169: throw new JellyTagException(e.toString());
170: }
171: }
172:
173: /**
174: * Sets the properties on the bean. Derived tags could implement some custom
175: * type conversion etc.
176: * <p/>
177: * This method ignores all property names in the Set returned by {@link #getIgnorePropertySet()}.
178: */
179: protected void setBeanProperties(Object bean, Map attributes)
180: throws JellyTagException {
181: Map attrsToUse = new HashMap(attributes);
182: attrsToUse.keySet().removeAll(getIgnorePropertySet());
183:
184: validateBeanProperties(bean, attrsToUse);
185:
186: try {
187: BeanUtils.populate(bean, attrsToUse);
188: } catch (IllegalAccessException e) {
189: throw new JellyTagException(
190: "could not set the properties of the bean", e);
191: } catch (InvocationTargetException e) {
192: throw new JellyTagException(
193: "could not set the properties of the bean", e);
194: }
195: }
196:
197: /**
198: * If {@link #isIgnoreUnknownProperties()} returns true, make sure that
199: * every non-ignored ({@see #addIgnoreProperty(String)}) property
200: * matches a writable property on the target bean.
201: * @param bean the bean to validate
202: * @param attributes the list of properties to validate
203: * @throws JellyTagException when a property is not writeable
204: */
205: protected void validateBeanProperties(Object bean, Map attributes)
206: throws JellyTagException {
207: if (!isIgnoreUnknownProperties()) {
208: for (Iterator i = attributes.keySet().iterator(); i
209: .hasNext();) {
210: String attrName = (String) i.next();
211: if (!PropertyUtils.isWriteable(bean, attrName)) {
212: throw new JellyTagException(
213: "No bean property found: " + attrName);
214: }
215: }
216: }
217: }
218:
219: /**
220: * By default this will export the bean using the given variable if it is defined.
221: * This Strategy method allows derived tags to process the beans in different ways
222: * such as to register this bean with its parent tag etc.
223: */
224: protected void processBean(String var, Object bean)
225: throws JellyTagException {
226: if (var != null) {
227: context.setVariable(var, bean);
228: } else {
229: ArgTag parentArg = (ArgTag) (findAncestorWithClass(ArgTag.class));
230: if (null != parentArg) {
231: parentArg.setValue(bean);
232: }
233: }
234: }
235:
236: /**
237: * Allows derived classes to provide a default bean implementation class
238: */
239: protected Class getDefaultClass() {
240: return defaultClass;
241: }
242:
243: /**
244: * Adds a name to the Set of property names that will be skipped when setting
245: * bean properties. In other words, names added here won't be set into the bean
246: * if they're present in the attribute Map.
247: * @param name
248: */
249: protected void addIgnoreProperty(String name) {
250: getIgnorePropertySet().add(name);
251: }
252:
253: /**
254: * @return the Set of property names that should be ignored when setting the
255: * properties of the bean.
256: */
257: protected Set getIgnorePropertySet() {
258: if (ignoreProperties == null) {
259: ignoreProperties = new HashSet();
260: }
261:
262: return ignoreProperties;
263: }
264:
265: /**
266: * @see {@link #setIgnoreUnknownProperties(boolean)}
267: * @return
268: */
269: public boolean isIgnoreUnknownProperties() {
270: return ignoreUnknownProperties;
271: }
272:
273: /**
274: * If this tag finds an attribute in the XML that's not
275: * ignored by {@link #ignoreProperties} and isn't a
276: * bean property, should it throw an exception?
277: * @param ignoreUnknownProperties Sets {@link #ignoreUnknownProperties}.
278: */
279: public void setIgnoreUnknownProperties(boolean ignoreUnknownProps) {
280: this.ignoreUnknownProperties = ignoreUnknownProps;
281: }
282: }
|