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