001: /*****************************************************************************
002: * Copyright (C) PicoContainer Organization. All rights reserved. *
003: * ------------------------------------------------------------------------- *
004: * The software in this package is published under the terms of the BSD *
005: * style license a copy of which has been included with this distribution in *
006: * the LICENSE.txt file. *
007: * *
008: * Original code by *
009: *****************************************************************************/package org.picocontainer.behaviors;
010:
011: import java.beans.PropertyEditor;
012: import java.beans.PropertyEditorManager;
013: import java.io.File;
014: import java.lang.reflect.Method;
015: import java.net.MalformedURLException;
016: import java.net.URL;
017: import java.util.Map;
018: import java.util.Set;
019: import java.util.HashMap;
020: import java.security.AccessController;
021: import java.security.PrivilegedAction;
022:
023: import org.picocontainer.ComponentAdapter;
024: import org.picocontainer.ComponentMonitor;
025: import org.picocontainer.PicoContainer;
026: import org.picocontainer.PicoCompositionException;
027: import org.picocontainer.PicoClassNotFoundException;
028: import org.picocontainer.injectors.SetterInjector;
029: import org.picocontainer.behaviors.AbstractBehavior;
030: import org.picocontainer.behaviors.Cached;
031:
032: /**
033: * Decorating component adapter that can be used to set additional properties
034: * on a component in a bean style. These properties must be managed manually
035: * by the user of the API, and will not be managed by PicoContainer. This class
036: * is therefore <em>not</em> the same as {@link SetterInjector},
037: * which is a true Setter Injection adapter.
038: * <p/>
039: * This adapter is mostly handy for setting various primitive properties via setters;
040: * it is also able to set javabean properties by discovering an appropriate
041: * {@link PropertyEditor} and using its <code>setAsText</code> method.
042: * <p/>
043: * <em>
044: * Note that this class doesn't cache instances. If you want caching,
045: * use a {@link Cached} around this one.
046: * </em>
047: *
048: * @author Aslak Hellesøy
049: * @author Mauro Talevi
050: */
051: public class PropertyApplicator<T> extends AbstractBehavior<T> {
052: private Map<String, String> properties;
053: private transient Map<String, Method> setters = null;
054:
055: /**
056: * Construct a PropertyApplicator.
057: *
058: * @param delegate the wrapped {@link ComponentAdapter}
059: * @throws PicoCompositionException {@inheritDoc}
060: */
061: public PropertyApplicator(ComponentAdapter<T> delegate)
062: throws PicoCompositionException {
063: super (delegate);
064: }
065:
066: /**
067: * Get a component instance and set given property values.
068: *
069: * @return the component instance with any properties of the properties map set.
070: * @throws PicoCompositionException {@inheritDoc}
071: * @throws PicoCompositionException {@inheritDoc}
072: * @throws org.picocontainer.PicoCompositionException
073: * {@inheritDoc}
074: * @see #setProperties(Map)
075: */
076: public T getComponentInstance(PicoContainer container)
077: throws PicoCompositionException {
078: final T componentInstance = super
079: .getComponentInstance(container);
080: if (setters == null) {
081: setters = getSetters(getComponentImplementation());
082: }
083:
084: if (properties != null) {
085: ComponentMonitor componentMonitor = currentMonitor();
086: Set<String> propertyNames = properties.keySet();
087: for (String propertyName : propertyNames) {
088: final Object propertyValue = properties
089: .get(propertyName);
090: Method setter = setters.get(propertyName);
091:
092: Object valueToInvoke = this .getSetterParameter(
093: propertyName, propertyValue, componentInstance,
094: container);
095:
096: try {
097: componentMonitor.invoking(container,
098: PropertyApplicator.this , setter,
099: componentInstance);
100: long startTime = System.currentTimeMillis();
101: setter.invoke(componentInstance, valueToInvoke);
102: componentMonitor.invoked(container,
103: PropertyApplicator.this , setter,
104: componentInstance, System
105: .currentTimeMillis()
106: - startTime);
107: } catch (final Exception e) {
108: componentMonitor.invocationFailed(setter,
109: componentInstance, e);
110: throw new PicoCompositionException(
111: "Failed to set property " + propertyName
112: + " to " + propertyValue + ": "
113: + e.getMessage(), e);
114: }
115: }
116: }
117: return componentInstance;
118: }
119:
120: public String getDescriptor() {
121: return "PropertyApplied";
122: }
123:
124: private Map<String, Method> getSetters(Class<?> clazz) {
125: Map<String, Method> result = new HashMap<String, Method>();
126: Method[] methods = getMethods(clazz);
127: for (Method method : methods) {
128: if (isSetter(method)) {
129: result.put(getPropertyName(method), method);
130: }
131: }
132: return result;
133: }
134:
135: private Method[] getMethods(final Class<?> clazz) {
136: return (Method[]) AccessController
137: .doPrivileged(new PrivilegedAction<Object>() {
138: public Object run() {
139: return clazz.getMethods();
140: }
141: });
142: }
143:
144: private String getPropertyName(Method method) {
145: final String name = method.getName();
146: String result = name.substring(3);
147: if (result.length() > 1
148: && !Character.isUpperCase(result.charAt(1))) {
149: result = "" + Character.toLowerCase(result.charAt(0))
150: + result.substring(1);
151: } else if (result.length() == 1) {
152: result = result.toLowerCase();
153: }
154: return result;
155: }
156:
157: private boolean isSetter(Method method) {
158: final String name = method.getName();
159: return name.length() > 3 && name.startsWith("set")
160: && method.getParameterTypes().length == 1;
161: }
162:
163: private Object convertType(PicoContainer container, Method setter,
164: String propertyValue) {
165: if (propertyValue == null) {
166: return null;
167: }
168: Class<?> type = setter.getParameterTypes()[0];
169: String typeName = type.getName();
170:
171: Object result = convert(typeName, propertyValue, Thread
172: .currentThread().getContextClassLoader());
173:
174: if (result == null) {
175:
176: // check if the propertyValue is a key of a component in the container
177: // if so, the typeName of the component and the setters parameter typeName
178: // have to be compatible
179:
180: // TODO: null check only because of test-case, otherwise null is impossible
181: if (container != null) {
182: Object component = container
183: .getComponent(propertyValue);
184: if (component != null
185: && type.isAssignableFrom(component.getClass())) {
186: return component;
187: }
188: }
189: }
190: return result;
191: }
192:
193: /**
194: * Converts a String value of a named type to an object.
195: * Works with primitive wrappers, String, File, URL types, or any type that has
196: * an appropriate {@link PropertyEditor}.
197: *
198: * @param typeName name of the type
199: * @param value its value
200: * @param classLoader used to load a class if typeName is "class" or "java.lang.Class" (ignored otherwise)
201: * @return instantiated object or null if the type was unknown/unsupported
202: */
203: public static Object convert(String typeName, String value,
204: ClassLoader classLoader) {
205: if (typeName.equals(Boolean.class.getName())
206: || typeName.equals(boolean.class.getName())) {
207: return Boolean.valueOf(value);
208: } else if (typeName.equals(Byte.class.getName())
209: || typeName.equals(byte.class.getName())) {
210: return Byte.valueOf(value);
211: } else if (typeName.equals(Short.class.getName())
212: || typeName.equals(short.class.getName())) {
213: return Short.valueOf(value);
214: } else if (typeName.equals(Integer.class.getName())
215: || typeName.equals(int.class.getName())) {
216: return Integer.valueOf(value);
217: } else if (typeName.equals(Long.class.getName())
218: || typeName.equals(long.class.getName())) {
219: return Long.valueOf(value);
220: } else if (typeName.equals(Float.class.getName())
221: || typeName.equals(float.class.getName())) {
222: return Float.valueOf(value);
223: } else if (typeName.equals(Double.class.getName())
224: || typeName.equals(double.class.getName())) {
225: return Double.valueOf(value);
226: } else if (typeName.equals(Character.class.getName())
227: || typeName.equals(char.class.getName())) {
228: return value.toCharArray()[0];
229: } else if (typeName.equals(String.class.getName())
230: || typeName.equals("string")) {
231: return value;
232: } else if (typeName.equals(File.class.getName())
233: || typeName.equals("file")) {
234: return new File(value);
235: } else if (typeName.equals(URL.class.getName())
236: || typeName.equals("url")) {
237: try {
238: return new URL(value);
239: } catch (MalformedURLException e) {
240: throw new PicoCompositionException(e);
241: }
242: } else if (typeName.equals(Class.class.getName())
243: || typeName.equals("class")) {
244: return loadClass(classLoader, value);
245: } else {
246: final Class<?> clazz = loadClass(classLoader, typeName);
247: final PropertyEditor editor = PropertyEditorManager
248: .findEditor(clazz);
249: if (editor != null) {
250: editor.setAsText(value);
251: return editor.getValue();
252: }
253: }
254: return null;
255: }
256:
257: private static Class<?> loadClass(ClassLoader classLoader,
258: String typeName) {
259: try {
260: return classLoader.loadClass(typeName);
261: } catch (ClassNotFoundException e) {
262: throw new PicoClassNotFoundException(typeName, e);
263: }
264: }
265:
266: /**
267: * Sets the bean property values that should be set upon creation.
268: *
269: * @param properties bean properties
270: */
271: public void setProperties(Map<String, String> properties) {
272: this .properties = properties;
273: }
274:
275: /**
276: * Converts and validates the given property value to an appropriate object
277: * for calling the bean's setter.
278: * @param propertyName String the property name on the component that
279: * we will be setting the value to.
280: * @param propertyValue Object the property value that we've been given. It
281: * may need conversion to be formed into the value we need for the
282: * component instance setter.
283: * @param componentInstance the component that we're looking to provide
284: * the setter to.
285: * @return Object: the final converted object that can
286: * be used in the setter.
287: * @param container
288: */
289: private Object getSetterParameter(final String propertyName,
290: final Object propertyValue, final Object componentInstance,
291: PicoContainer container) {
292:
293: if (propertyValue == null) {
294: return null;
295: }
296:
297: Method setter = setters.get(propertyName);
298:
299: //We can assume that there is only one object (as per typical setters)
300: //because the Setter introspector does that job for us earlier.
301: Class<?> setterParameter = setter.getParameterTypes()[0];
302:
303: Object convertedValue;
304:
305: Class<? extends Object> givenParameterClass = propertyValue
306: .getClass();
307:
308: //
309: //If property value is a string or a true primative then convert it to whatever
310: //we need. (String will convert to string).
311: //
312: convertedValue = convertType(container, setter, propertyValue
313: .toString());
314:
315: //Otherwise, check the parameter type to make sure we can
316: //assign it properly.
317: if (convertedValue == null) {
318: if (setterParameter.isAssignableFrom(givenParameterClass)) {
319: convertedValue = propertyValue;
320: } else {
321: throw new ClassCastException("Setter: "
322: + setter.getName() + " for addComponent: "
323: + componentInstance.toString()
324: + " can only take objects of: "
325: + setterParameter.getName() + " instead got: "
326: + givenParameterClass.getName());
327: }
328: }
329: return convertedValue;
330: }
331:
332: public void setProperty(String name, String value) {
333: if (properties == null) {
334: properties = new HashMap<String, String>();
335: }
336: properties.put(name, value);
337: }
338:
339: }
|