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 }