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 *****************************************************************************/
010package org.picocontainer.behaviors;
011
012import java.beans.PropertyEditor;
013import java.beans.PropertyEditorManager;
014import java.io.File;
015import java.lang.reflect.Method;
016import java.lang.reflect.Type;
017import java.net.MalformedURLException;
018import java.net.URL;
019import java.util.Map;
020import java.util.Set;
021import java.util.HashMap;
022import java.security.AccessController;
023import java.security.PrivilegedAction;
024
025import org.picocontainer.ComponentAdapter;
026import org.picocontainer.ComponentMonitor;
027import org.picocontainer.PicoContainer;
028import org.picocontainer.PicoCompositionException;
029import org.picocontainer.PicoClassNotFoundException;
030import org.picocontainer.injectors.SetterInjector;
031import org.picocontainer.behaviors.AbstractBehavior;
032import 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&oslash;y
051 * @author Mauro Talevi
052 */
053@SuppressWarnings("serial")
054public 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}