001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014// Contributors:  Georg Lundesgaard
015package ch.qos.logback.core.joran.util;
016
017import java.lang.annotation.Annotation;
018import java.lang.reflect.InvocationTargetException;
019import java.lang.reflect.Method;
020
021import ch.qos.logback.core.joran.spi.DefaultClass;
022import ch.qos.logback.core.joran.spi.DefaultNestedComponentRegistry;
023import ch.qos.logback.core.joran.util.beans.BeanDescription;
024import ch.qos.logback.core.joran.util.beans.BeanDescriptionCache;
025import ch.qos.logback.core.joran.util.beans.BeanUtil;
026import ch.qos.logback.core.spi.ContextAwareBase;
027import ch.qos.logback.core.util.AggregationType;
028import ch.qos.logback.core.util.PropertySetterException;
029
030/**
031 * General purpose Object property setter. Clients repeatedly invokes
032 * {@link #setProperty setProperty(name,value)} in order to invoke setters on
033 * the Object specified in the constructor. This class relies on reflection to
034 * analyze the given Object Class.
035 *
036 * <p>
037 * Usage:
038 *
039 * <pre>
040 * PropertySetter ps = new PropertySetter(anObject);
041 * ps.set(&quot;name&quot;, &quot;Joe&quot;);
042 * ps.set(&quot;age&quot;, &quot;32&quot;);
043 * ps.set(&quot;isMale&quot;, &quot;true&quot;);
044 * </pre>
045 *
046 * will cause the invocations anObject.setName("Joe"), anObject.setAge(32), and
047 * setMale(true) if such methods exist with those signatures. Otherwise an
048 * {@link PropertySetterException} is thrown.
049 *
050 * @author Anders Kristensen
051 * @author Ceki Gulcu
052 */
053public class PropertySetter extends ContextAwareBase {
054
055    protected final Object obj;
056    protected final Class<?> objClass;
057    protected final BeanDescription beanDescription;
058
059    /**
060     * Create a new PropertySetter for the specified Object. This is done in
061     * preparation for invoking {@link #setProperty} one or more times.
062     *
063     * @param obj the object for which to set properties
064     */
065    public PropertySetter(BeanDescriptionCache beanDescriptionCache, Object obj) {
066        this.obj = obj;
067        this.objClass = obj.getClass();
068        this.beanDescription = beanDescriptionCache.getBeanDescription(objClass);
069    }
070
071    /**
072     * Set a property on this PropertySetter's Object. If successful, this method
073     * will invoke a setter method on the underlying Object. The setter is the one
074     * for the specified property name and the value is determined partly from the
075     * setter argument type and partly from the value specified in the call to this
076     * method.
077     *
078     * <p>
079     * If the setter expects a String no conversion is necessary. If it expects an
080     * int, then an attempt is made to convert 'value' to an int using new
081     * Integer(value). If the setter expects a boolean, the conversion is by new
082     * Boolean(value).
083     *
084     * @param name  name of the property
085     * @param value String value of the property
086     */
087    public void setProperty(String name, String value) {
088        if (value == null) {
089            return;
090        }
091        Method setter = findSetterMethod(name);
092        if (setter == null) {
093            addWarn("No setter for property [" + name + "] in " + objClass.getName() + ".");
094        } else {
095            try {
096                setProperty(setter, name, value);
097            } catch (PropertySetterException ex) {
098                addWarn("Failed to set property [" + name + "] to value \"" + value + "\". ", ex);
099            }
100        }
101    }
102
103    /**
104     * Set the named property given a {@link PropertyDescriptor}.
105     *
106     * @param prop  A PropertyDescriptor describing the characteristics of the
107     *              property to set.
108     * @param name  The named of the property to set.
109     * @param value The value of the property.
110     */
111    private void setProperty(Method setter, String name, String value) throws PropertySetterException {
112        Class<?>[] paramTypes = setter.getParameterTypes();
113
114        Object arg;
115
116        try {
117            arg = StringToObjectConverter.convertArg(this, value, paramTypes[0]);
118        } catch (Throwable t) {
119            throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed. ", t);
120        }
121
122        if (arg == null) {
123            throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed.");
124        }
125        try {
126            setter.invoke(obj, arg);
127        } catch (Exception ex) {
128            throw new PropertySetterException(ex);
129        }
130    }
131
132    public AggregationType computeAggregationType(String name) {
133        String cName = capitalizeFirstLetter(name);
134
135        Method addMethod = findAdderMethod(cName);
136
137        if (addMethod != null) {
138            AggregationType type = computeRawAggregationType(addMethod);
139            switch (type) {
140            case NOT_FOUND:
141                return AggregationType.NOT_FOUND;
142            case AS_BASIC_PROPERTY:
143                return AggregationType.AS_BASIC_PROPERTY_COLLECTION;
144
145            case AS_COMPLEX_PROPERTY:
146                return AggregationType.AS_COMPLEX_PROPERTY_COLLECTION;
147            case AS_BASIC_PROPERTY_COLLECTION:
148            case AS_COMPLEX_PROPERTY_COLLECTION:
149                addError("Unexpected AggregationType " + type);
150            }
151        }
152
153        Method setter = findSetterMethod(name);
154        if (setter != null) {
155            return computeRawAggregationType(setter);
156        } else {
157            // we have failed
158            return AggregationType.NOT_FOUND;
159        }
160    }
161
162    private Method findAdderMethod(String name) {
163        String propertyName = BeanUtil.toLowerCamelCase(name);
164        return beanDescription.getAdder(propertyName);
165    }
166
167    private Method findSetterMethod(String name) {
168        String propertyName = BeanUtil.toLowerCamelCase(name);
169        return beanDescription.getSetter(propertyName);
170    }
171
172    private Class<?> getParameterClassForMethod(Method method) {
173        if (method == null) {
174            return null;
175        }
176        Class<?>[] classArray = method.getParameterTypes();
177        if (classArray.length != 1) {
178            return null;
179        } else {
180            return classArray[0];
181        }
182    }
183
184    private AggregationType computeRawAggregationType(Method method) {
185        Class<?> parameterClass = getParameterClassForMethod(method);
186        if (parameterClass == null) {
187            return AggregationType.NOT_FOUND;
188        }
189        if (StringToObjectConverter.canBeBuiltFromSimpleString(parameterClass)) {
190            return AggregationType.AS_BASIC_PROPERTY;
191        } else {
192            return AggregationType.AS_COMPLEX_PROPERTY;
193        }
194    }
195
196    /**
197     * Can the given clazz instantiable with certainty?
198     *
199     * @param clazz The class to test for instantiability
200     * @return true if clazz can be instantiated, and false otherwise.
201     */
202    private boolean isUnequivocallyInstantiable(Class<?> clazz) {
203        if (clazz.isInterface()) {
204            return false;
205        }
206        // checking for constructors would be more elegant, but in
207        // classes without any declared constructors, Class.getConstructor()
208        // returns null.
209        Object o;
210        try {
211            o = clazz.getDeclaredConstructor().newInstance();
212            if (o != null) {
213                return true;
214            } else {
215                return false;
216            }
217        } catch (InstantiationException | IllegalAccessException | InvocationTargetException
218                | NoSuchMethodException e) {
219            return false;
220        }
221    }
222
223    public Class<?> getObjClass() {
224        return objClass;
225    }
226
227    public void addComplexProperty(String name, Object complexProperty) {
228        Method adderMethod = findAdderMethod(name);
229        // first let us use the addXXX method
230        if (adderMethod != null) {
231            Class<?>[] paramTypes = adderMethod.getParameterTypes();
232            if (!isSanityCheckSuccessful(name, adderMethod, paramTypes, complexProperty)) {
233                return;
234            }
235            invokeMethodWithSingleParameterOnThisObject(adderMethod, complexProperty);
236        } else {
237            addError("Could not find method [" + "add" + name + "] in class [" + objClass.getName() + "].");
238        }
239    }
240
241    void invokeMethodWithSingleParameterOnThisObject(Method method, Object parameter) {
242        Class<?> ccc = parameter.getClass();
243        try {
244            method.invoke(this.obj, parameter);
245        } catch (Exception e) {
246            addError("Could not invoke method " + method.getName() + " in class " + obj.getClass().getName()
247                    + " with parameter of type " + ccc.getName(), e);
248        }
249    }
250
251    public void addBasicProperty(String name, String strValue) {
252
253        if (strValue == null) {
254            return;
255        }
256
257        name = capitalizeFirstLetter(name);
258        Method adderMethod = findAdderMethod(name);
259
260        if (adderMethod == null) {
261            addError("No adder for property [" + name + "].");
262            return;
263        }
264
265        Class<?>[] paramTypes = adderMethod.getParameterTypes();
266        isSanityCheckSuccessful(name, adderMethod, paramTypes, strValue);
267
268        Object arg;
269        try {
270            arg = StringToObjectConverter.convertArg(this, strValue, paramTypes[0]);
271        } catch (Throwable t) {
272            addError("Conversion to type [" + paramTypes[0] + "] failed. ", t);
273            return;
274        }
275        if (arg != null) {
276            invokeMethodWithSingleParameterOnThisObject(adderMethod, strValue);
277        }
278    }
279
280    public void setComplexProperty(String name, Object complexProperty) {
281        Method setter = findSetterMethod(name);
282
283        if (setter == null) {
284            addWarn("Not setter method for property [" + name + "] in " + obj.getClass().getName());
285
286            return;
287        }
288
289        Class<?>[] paramTypes = setter.getParameterTypes();
290
291        if (!isSanityCheckSuccessful(name, setter, paramTypes, complexProperty)) {
292            return;
293        }
294        try {
295            invokeMethodWithSingleParameterOnThisObject(setter, complexProperty);
296
297        } catch (Exception e) {
298            addError("Could not set component " + obj + " for parent component " + obj, e);
299        }
300    }
301
302    private boolean isSanityCheckSuccessful(String name, Method method, Class<?>[] params, Object complexProperty) {
303        Class<?> ccc = complexProperty.getClass();
304        if (params.length != 1) {
305            addError("Wrong number of parameters in setter method for property [" + name + "] in "
306                    + obj.getClass().getName());
307
308            return false;
309        }
310
311        if (!params[0].isAssignableFrom(complexProperty.getClass())) {
312            addError("A \"" + ccc.getName() + "\" object is not assignable to a \"" + params[0].getName()
313                    + "\" variable.");
314            addError("The class \"" + params[0].getName() + "\" was loaded by ");
315            addError("[" + params[0].getClassLoader() + "] whereas object of type ");
316            addError("\"" + ccc.getName() + "\" was loaded by [" + ccc.getClassLoader() + "].");
317            return false;
318        }
319
320        return true;
321    }
322
323    private String capitalizeFirstLetter(String name) {
324        return name.substring(0, 1).toUpperCase() + name.substring(1);
325    }
326
327    public Object getObj() {
328        return obj;
329    }
330
331    Method getRelevantMethod(String name, AggregationType aggregationType) {
332        Method relevantMethod;
333        if (aggregationType == AggregationType.AS_COMPLEX_PROPERTY_COLLECTION) {
334            relevantMethod = findAdderMethod(name);
335        } else if (aggregationType == AggregationType.AS_COMPLEX_PROPERTY) {
336            relevantMethod = findSetterMethod(name);
337        } else {
338            throw new IllegalStateException(aggregationType + " not allowed here");
339        }
340        return relevantMethod;
341    }
342
343    <T extends Annotation> T getAnnotation(String name, Class<T> annonationClass, Method relevantMethod) {
344
345        if (relevantMethod != null) {
346            return relevantMethod.getAnnotation(annonationClass);
347        } else {
348            return null;
349        }
350    }
351
352    Class<?> getDefaultClassNameByAnnonation(String name, Method relevantMethod) {
353        DefaultClass defaultClassAnnon = getAnnotation(name, DefaultClass.class, relevantMethod);
354        if (defaultClassAnnon != null) {
355            return defaultClassAnnon.value();
356        }
357        return null;
358    }
359
360    Class<?> getByConcreteType(String name, Method relevantMethod) {
361
362        Class<?> paramType = getParameterClassForMethod(relevantMethod);
363        if (paramType == null) {
364            return null;
365        }
366
367        boolean isUnequivocallyInstantiable = isUnequivocallyInstantiable(paramType);
368        if (isUnequivocallyInstantiable) {
369            return paramType;
370        } else {
371            return null;
372        }
373
374    }
375
376    public Class<?> getClassNameViaImplicitRules(String name, AggregationType aggregationType,
377            DefaultNestedComponentRegistry registry) {
378
379        Class<?> registryResult = registry.findDefaultComponentType(obj.getClass(), name);
380        if (registryResult != null) {
381            return registryResult;
382        }
383        // find the relevant method for the given property name and aggregationType
384        Method relevantMethod = getRelevantMethod(name, aggregationType);
385        if (relevantMethod == null) {
386            return null;
387        }
388        Class<?> byAnnotation = getDefaultClassNameByAnnonation(name, relevantMethod);
389        if (byAnnotation != null) {
390            return byAnnotation;
391        }
392        return getByConcreteType(name, relevantMethod);
393    }
394
395}