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