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&oslash;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    }