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