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