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 }