View Javadoc

1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2011, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v1.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  // Contributors:  Georg Lundesgaard
15  package ch.qos.logback.core.joran.util;
16  
17  import java.beans.BeanInfo;
18  import java.beans.IntrospectionException;
19  import java.beans.Introspector;
20  import java.beans.MethodDescriptor;
21  import java.beans.PropertyDescriptor;
22  import java.lang.annotation.Annotation;
23  import java.lang.reflect.Method;
24  
25  import ch.qos.logback.core.joran.spi.DefaultClass;
26  import ch.qos.logback.core.joran.spi.DefaultNestedComponentRegistry;
27  import ch.qos.logback.core.spi.ContextAwareBase;
28  import ch.qos.logback.core.util.AggregationType;
29  import ch.qos.logback.core.util.PropertySetterException;
30  
31  /**
32   * General purpose Object property setter. Clients repeatedly invokes
33   * {@link #setProperty setProperty(name,value)} in order to invoke setters on
34   * the Object specified in the constructor. This class relies on the JavaBeans
35   * {@link Introspector} to analyze the given Object Class using reflection.
36   * 
37   * <p>
38   * Usage:
39   * 
40   * <pre>
41   * PropertySetter ps = new PropertySetter(anObject);
42   * ps.set(&quot;name&quot;, &quot;Joe&quot;);
43   * ps.set(&quot;age&quot;, &quot;32&quot;);
44   * ps.set(&quot;isMale&quot;, &quot;true&quot;);
45   * </pre>
46   * 
47   * will cause the invocations anObject.setName("Joe"), anObject.setAge(32), and
48   * setMale(true) if such methods exist with those signatures. Otherwise an
49   * {@link IntrospectionException} are thrown.
50   * 
51   * @author Anders Kristensen
52   * @author Ceki Gulcu
53   */
54  public class PropertySetter extends ContextAwareBase {
55  
56    protected Object obj;
57    protected Class objClass;
58    protected PropertyDescriptor[] propertyDescriptors;
59    protected MethodDescriptor[] methodDescriptors;
60  
61    /**
62     * Create a new PropertySetter for the specified Object. This is done in
63     * preparation for invoking {@link #setProperty} one or more times.
64     * 
65     * @param obj
66     *          the object for which to set properties
67     */
68    public PropertySetter(Object obj) {
69      this.obj = obj;
70      this.objClass = obj.getClass();
71    }
72  
73    /**
74     * Uses JavaBeans {@link Introspector} to computer setters of object to be
75     * configured.
76     */
77    protected void introspect() {
78      try {
79        BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
80        propertyDescriptors = bi.getPropertyDescriptors();
81        methodDescriptors = bi.getMethodDescriptors();
82      } catch (IntrospectionException ex) {
83        addError("Failed to introspect " + obj + ": " + ex.getMessage());
84        propertyDescriptors = new PropertyDescriptor[0];
85        methodDescriptors = new MethodDescriptor[0];
86      }
87    }
88  
89    /**
90     * Set a property on this PropertySetter's Object. If successful, this method
91     * will invoke a setter method on the underlying Object. The setter is the one
92     * for the specified property name and the value is determined partly from the
93     * setter argument type and partly from the value specified in the call to
94     * this method.
95     * 
96     * <p>
97     * If the setter expects a String no conversion is necessary. If it expects an
98     * int, then an attempt is made to convert 'value' to an int using new
99     * Integer(value). If the setter expects a boolean, the conversion is by new
100    * Boolean(value).
101    * 
102    * @param name
103    *          name of the property
104    * @param value
105    *          String value of the property
106    */
107   public void setProperty(String name, String value) {
108     if (value == null) {
109       return;
110     }
111 
112     name = Introspector.decapitalize(name);
113 
114     PropertyDescriptor prop = getPropertyDescriptor(name);
115 
116     if (prop == null) {
117       addWarn("No such property [" + name + "] in " + objClass.getName() + ".");
118     } else {
119       try {
120         setProperty(prop, name, value);
121       } catch (PropertySetterException ex) {
122         addWarn("Failed to set property [" + name + "] to value \"" + value
123             + "\". ", ex);
124       }
125     }
126   }
127 
128   /**
129    * Set the named property given a {@link PropertyDescriptor}.
130    * 
131    * @param prop
132    *          A PropertyDescriptor describing the characteristics of the
133    *          property to set.
134    * @param name
135    *          The named of the property to set.
136    * @param value
137    *          The value of the property.
138    */
139   public void setProperty(PropertyDescriptor prop, String name, String value)
140       throws PropertySetterException {
141     Method setter = prop.getWriteMethod();
142 
143     if (setter == null) {
144       throw new PropertySetterException("No setter for property [" + name
145           + "].");
146     }
147 
148     Class[] paramTypes = setter.getParameterTypes();
149 
150     if (paramTypes.length != 1) {
151       throw new PropertySetterException("#params for setter != 1");
152     }
153 
154     Object arg;
155 
156     try {
157       arg = StringToObjectConverter.convertArg(this, value, paramTypes[0]);
158     } catch (Throwable t) {
159       throw new PropertySetterException("Conversion to type [" + paramTypes[0]
160           + "] failed. ", t);
161     }
162 
163     if (arg == null) {
164       throw new PropertySetterException("Conversion to type [" + paramTypes[0]
165           + "] failed.");
166     }
167     try {
168       setter.invoke(obj, arg);
169     } catch (Exception ex) {
170       throw new PropertySetterException(ex);
171     }
172   }
173 
174   public AggregationType computeAggregationType(String name) {
175     String cName = capitalizeFirstLetter(name);
176 
177     Method addMethod = findAdderMethod(cName);
178 
179     // if the
180     if (addMethod != null) {
181       AggregationType type = computeRawAggregationType(addMethod);
182       switch (type) {
183       case NOT_FOUND:
184         return AggregationType.NOT_FOUND;
185       case AS_BASIC_PROPERTY:
186         return AggregationType.AS_BASIC_PROPERTY_COLLECTION;
187       case AS_COMPLEX_PROPERTY:
188         return AggregationType.AS_COMPLEX_PROPERTY_COLLECTION;
189       }
190     }
191 
192     Method setterMethod = findSetterMethod(name);
193     if (setterMethod != null) {
194       return computeRawAggregationType(setterMethod);
195     } else {
196       // we have failed
197       return AggregationType.NOT_FOUND;
198     }
199   }
200 
201   private Method findAdderMethod(String name) {
202     name = capitalizeFirstLetter(name);
203     Method adderMethod = getMethod("add" + name);
204     return adderMethod;
205   }
206 
207   private Method findSetterMethod(String name) {
208     String dName = Introspector.decapitalize(name);
209     PropertyDescriptor propertyDescriptor = getPropertyDescriptor(dName);
210     if (propertyDescriptor != null) {
211       return propertyDescriptor.getWriteMethod();
212     } else {
213       return null;
214     }
215   }
216 
217   private Class<?> getParameterClassForMethod(Method method) {
218     if (method == null) {
219       return null;
220     }
221     Class[] classArray = method.getParameterTypes();
222     if (classArray.length != 1) {
223       return null;
224     } else {
225       return classArray[0];
226     }
227   }
228 
229   private AggregationType computeRawAggregationType(Method method) {
230     Class<?> parameterClass = getParameterClassForMethod(method);
231     if (parameterClass == null) {
232       return AggregationType.NOT_FOUND;
233     }
234     if (StringToObjectConverter.canBeBuiltFromSimpleString(parameterClass)) {
235       return AggregationType.AS_BASIC_PROPERTY;
236     } else {
237       return AggregationType.AS_COMPLEX_PROPERTY;
238     }
239   }
240 
241   /**
242    * Can the given clazz instantiable with certainty?
243    * 
244    * @param clazz
245    *          The class to test for instantiability
246    * @return true if clazz can be instantiated, and false otherwise.
247    */
248   private boolean isUnequivocallyInstantiable(Class<?> clazz) {
249     if (clazz.isInterface()) {
250       return false;
251     }
252     // checking for constructors would be more elegant, but in
253     // classes without any declared constructors, Class.getConstructor()
254     // returns null.
255     Object o;
256     try {
257       o = clazz.newInstance();
258       if (o != null) {
259         return true;
260       } else {
261         return false;
262       }
263     } catch (InstantiationException e) {
264       return false;
265     } catch (IllegalAccessException e) {
266       return false;
267     }
268   }
269 
270   public Class getObjClass() {
271     return objClass;
272   }
273 
274   public void addComplexProperty(String name, Object complexProperty) {
275     Method adderMethod = findAdderMethod(name);
276     // first let us use the addXXX method
277     if (adderMethod != null) {
278       Class[] paramTypes = adderMethod.getParameterTypes();
279       if (!isSanityCheckSuccessful(name, adderMethod, paramTypes,
280           complexProperty)) {
281         return;
282       }
283       invokeMethodWithSingleParameterOnThisObject(adderMethod, complexProperty);
284     } else {
285       addError("Could not find method [" + "add" + name + "] in class ["
286           + objClass.getName() + "].");
287     }
288   }
289 
290   void invokeMethodWithSingleParameterOnThisObject(Method method,
291       Object parameter) {
292     Class ccc = parameter.getClass();
293     try {
294       method.invoke(this.obj, parameter);
295     } catch (Exception e) {
296       addError("Could not invoke method " + method.getName() + " in class "
297           + obj.getClass().getName() + " with parameter of type "
298           + ccc.getName(), e);
299     }
300   }
301 
302   public void addBasicProperty(String name, String strValue) {
303 
304     if (strValue == null) {
305       return;
306     }
307 
308     name = capitalizeFirstLetter(name);
309     Method adderMethod = findAdderMethod(name);
310 
311     if (adderMethod == null) {
312       addError("No adder for property [" + name + "].");
313       return;
314     }
315 
316     Class[] paramTypes = adderMethod.getParameterTypes();
317     isSanityCheckSuccessful(name, adderMethod, paramTypes, strValue);
318 
319     Object arg;
320     try {
321       arg = StringToObjectConverter.convertArg(this, strValue, paramTypes[0]);
322     } catch (Throwable t) {
323       addError("Conversion to type [" + paramTypes[0] + "] failed. ", t);
324       return;
325     }
326     if (arg != null) {
327       invokeMethodWithSingleParameterOnThisObject(adderMethod, strValue);
328     }
329   }
330 
331   public void setComplexProperty(String name, Object complexProperty) {
332     String dName = Introspector.decapitalize(name);
333     PropertyDescriptor propertyDescriptor = getPropertyDescriptor(dName);
334 
335     if (propertyDescriptor == null) {
336       addWarn("Could not find PropertyDescriptor for [" + name + "] in "
337           + objClass.getName());
338 
339       return;
340     }
341 
342     Method setter = propertyDescriptor.getWriteMethod();
343 
344     if (setter == null) {
345       addWarn("Not setter method for property [" + name + "] in "
346           + obj.getClass().getName());
347 
348       return;
349     }
350 
351     Class[] paramTypes = setter.getParameterTypes();
352 
353     if (!isSanityCheckSuccessful(name, setter, paramTypes, complexProperty)) {
354       return;
355     }
356     try {
357       invokeMethodWithSingleParameterOnThisObject(setter, complexProperty);
358 
359     } catch (Exception e) {
360       addError("Could not set component " + obj + " for parent component "
361           + obj, e);
362     }
363   }
364 
365   private boolean isSanityCheckSuccessful(String name, Method method,
366       Class<?>[] params, Object complexProperty) {
367     Class ccc = complexProperty.getClass();
368     if (params.length != 1) {
369       addError("Wrong number of parameters in setter method for property ["
370           + name + "] in " + obj.getClass().getName());
371 
372       return false;
373     }
374 
375     if (!params[0].isAssignableFrom(complexProperty.getClass())) {
376       addError("A \"" + ccc.getName() + "\" object is not assignable to a \""
377           + params[0].getName() + "\" variable.");
378       addError("The class \"" + params[0].getName() + "\" was loaded by ");
379       addError("[" + params[0].getClassLoader() + "] whereas object of type ");
380       addError("\"" + ccc.getName() + "\" was loaded by ["
381           + ccc.getClassLoader() + "].");
382       return false;
383     }
384 
385     return true;
386   }
387 
388   private String capitalizeFirstLetter(String name) {
389     return name.substring(0, 1).toUpperCase() + name.substring(1);
390   }
391 
392   protected Method getMethod(String methodName) {
393     if (methodDescriptors == null) {
394       introspect();
395     }
396 
397     for (int i = 0; i < methodDescriptors.length; i++) {
398       if (methodName.equals(methodDescriptors[i].getName())) {
399         return methodDescriptors[i].getMethod();
400       }
401     }
402 
403     return null;
404   }
405 
406   protected PropertyDescriptor getPropertyDescriptor(String name) {
407     if (propertyDescriptors == null) {
408       introspect();
409     }
410 
411     for (int i = 0; i < propertyDescriptors.length; i++) {
412       // System.out.println("Comparing " + name + " against "
413       // + propertyDescriptors[i].getName());
414       if (name.equals(propertyDescriptors[i].getName())) {
415         // System.out.println("matched");
416         return propertyDescriptors[i];
417       }
418     }
419 
420     return null;
421   }
422 
423   public Object getObj() {
424     return obj;
425   }
426 
427   Method getRelevantMethod(String name, AggregationType aggregationType) {
428     String cName = capitalizeFirstLetter(name);
429     Method relevantMethod;
430     if (aggregationType == AggregationType.AS_COMPLEX_PROPERTY_COLLECTION) {
431       relevantMethod = findAdderMethod(cName);
432     } else if (aggregationType == AggregationType.AS_COMPLEX_PROPERTY) {
433       relevantMethod = findSetterMethod(cName);
434     } else {
435       throw new IllegalStateException(aggregationType + " not allowed here");
436     }
437     return relevantMethod;
438   }
439 
440   <T extends Annotation> T getAnnotation(String name, Class<T> annonationClass,
441       Method relevantMethod) {
442 
443     if (relevantMethod != null) {
444       return relevantMethod.getAnnotation(annonationClass);
445     } else {
446       return null;
447     }
448   }
449 
450   Class getDefaultClassNameByAnnonation(String name, Method relevantMethod) {
451     DefaultClass defaultClassAnnon = getAnnotation(name, DefaultClass.class,
452         relevantMethod);
453     if (defaultClassAnnon != null) {
454       Class defaultClass = defaultClassAnnon.value();
455       return defaultClass;
456     }
457     return null;
458   }
459 
460   Class getByConcreteType(String name, Method relevantMethod) {
461 
462     Class<?> paramType = getParameterClassForMethod(relevantMethod);
463     if (paramType == null) {
464       return null;
465     }
466 
467     boolean isUnequivocallyInstantiable = isUnequivocallyInstantiable(paramType);
468     if (isUnequivocallyInstantiable) {
469       return paramType;
470     } else {
471       return null;
472     }
473 
474   }
475 
476   public Class getClassNameViaImplicitRules(String name,
477       AggregationType aggregationType, DefaultNestedComponentRegistry registry) {
478 
479     Class registryResult = registry.findDefaultComponentType(obj.getClass(),
480         name);
481     if (registryResult != null) {
482       return registryResult;
483     }
484     // find the relevant method for the given property name and aggregationType
485     Method relevantMethod = getRelevantMethod(name, aggregationType);
486     if (relevantMethod == null) {
487       return null;
488     }
489     Class byAnnotation = getDefaultClassNameByAnnonation(name, relevantMethod);
490     if (byAnnotation != null) {
491       return byAnnotation;
492     }
493     return getByConcreteType(name, relevantMethod);
494   }
495 
496 }