    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);
    095                        long startTime = System.currentTimeMillis();
    096                        setter.invoke(componentInstance, valueToInvoke);
    097                        componentMonitor.invoked(container,
    098                                                 PropertyApplicator.this,
    099                                                 setter, componentInstance, System.currentTimeMillis() - startTime);
    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    }