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