blob: 499bd198933a431b86273f0b6c4d7e36d0ad48bf [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.xbean.recipe;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.xbean.propertyeditor.PropertyEditorRegistry;
import org.apache.xbean.recipe.ReflectionUtil.*;
/**
* @version $Rev: 6688 $ $Date: 2005-12-29T02:08:29.200064Z $
*/
public class ObjectRecipe extends AbstractRecipe {
private String typeName;
private Class typeClass;
private String factoryMethod;
private List<String> constructorArgNames;
private List<Class<?>> constructorArgTypes;
private PropertyEditorRegistry registry;
private final LinkedHashMap<Property,Object> properties = new LinkedHashMap<Property,Object>();
private final EnumSet<Option> options = EnumSet.of(Option.FIELD_INJECTION);
private final Map<String,Object> unsetProperties = new LinkedHashMap<String,Object>();
public ObjectRecipe(Class typeClass) {
this(typeClass, null, null, null, null);
}
public ObjectRecipe(Class typeClass, String factoryMethod) {
this(typeClass, factoryMethod, null, null, null);
}
public ObjectRecipe(Class typeClass, Map<String,Object> properties) {
this(typeClass, null, null, null, properties);
}
public ObjectRecipe(Class typeClass, String[] constructorArgNames) {
this(typeClass, null, constructorArgNames, null, null);
}
public ObjectRecipe(Class typeClass, String[] constructorArgNames, Class[] constructorArgTypes) {
this(typeClass, null, constructorArgNames, constructorArgTypes, null);
}
public ObjectRecipe(Class type, String factoryMethod, String[] constructorArgNames) {
this(type, factoryMethod, constructorArgNames, null, null);
}
public ObjectRecipe(Class type, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes) {
this(type, factoryMethod, constructorArgNames, constructorArgTypes, null);
}
public ObjectRecipe(Class typeClass, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes, Map<String,Object> properties) {
this.typeClass = typeClass;
this.factoryMethod = factoryMethod;
this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null;
this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null;
if (properties != null) {
setAllProperties(properties);
}
}
public ObjectRecipe(String typeName) {
this(typeName, null, null, null, null);
}
public ObjectRecipe(String typeName, String factoryMethod) {
this(typeName, factoryMethod, null, null, null);
}
public ObjectRecipe(String typeName, Map<String,Object> properties) {
this(typeName, null, null, null, properties);
}
public ObjectRecipe(String typeName, String[] constructorArgNames) {
this(typeName, null, constructorArgNames, null, null);
}
public ObjectRecipe(String typeName, String[] constructorArgNames, Class[] constructorArgTypes) {
this(typeName, null, constructorArgNames, constructorArgTypes, null);
}
public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames) {
this(typeName, factoryMethod, constructorArgNames, null, null);
}
public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes) {
this(typeName, factoryMethod, constructorArgNames, constructorArgTypes, null);
}
public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes, Map<String,Object> properties) {
this.typeName = typeName;
this.factoryMethod = factoryMethod;
this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null;
this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null;
if (properties != null) {
setAllProperties(properties);
}
}
public void allow(Option option){
options.add(option);
}
public void disallow(Option option){
options.remove(option);
}
public Set<Option> getOptions() {
return Collections.unmodifiableSet(options);
}
public List<String> getConstructorArgNames() {
return constructorArgNames;
}
public void setConstructorArgNames(String[] constructorArgNames) {
this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null;
}
public void setConstructorArgNames(List<String> constructorArgNames) {
this.constructorArgNames = constructorArgNames;
}
public List<Class<?>> getConstructorArgTypes() {
return constructorArgTypes;
}
public void setConstructorArgTypes(Class[] constructorArgTypes) {
this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null;
}
public void setConstructorArgTypes(List<? extends Class<?>> constructorArgTypes) {
this.constructorArgTypes = new ArrayList<Class<?>>(constructorArgTypes);
}
public String getFactoryMethod() {
return factoryMethod;
}
public void setFactoryMethod(String factoryMethod) {
this.factoryMethod = factoryMethod;
}
public Object getProperty(String name) {
Object value = properties.get(new Property(name));
return value;
}
public Map<String, Object> getProperties() {
LinkedHashMap<String, Object> properties = new LinkedHashMap<String, Object>();
for (Map.Entry<Property, Object> entry : this.properties.entrySet()) {
properties.put(entry.getKey().name, entry.getValue());
}
return properties;
}
public void setProperty(String name, Object value) {
setProperty(new Property(name), value);
}
public void setFieldProperty(String name, Object value){
setProperty(new FieldProperty(name), value);
options.add(Option.FIELD_INJECTION);
}
public void setMethodProperty(String name, Object value){
setProperty(new SetterProperty(name), value);
}
public void setAutoMatchProperty(String type, Object value){
setProperty(new AutoMatchProperty(type), value);
}
public void setCompoundProperty(String name, Object value) {
setProperty(new CompoundProperty(name), value);
}
private void setProperty(Property key, Object value) {
if (value instanceof UnsetPropertiesRecipe) {
allow(Option.IGNORE_MISSING_PROPERTIES);
}
properties.put(key, value);
}
public void setAllProperties(Map<?,?> map) {
if (map == null) throw new NullPointerException("map is null");
for (Map.Entry<?, ?> entry : map.entrySet()) {
String name = (String) entry.getKey();
Object value = entry.getValue();
setProperty(name, value);
}
}
public Map<String,Object> getUnsetProperties() {
return unsetProperties;
}
public List<Recipe> getNestedRecipes() {
List<Recipe> nestedRecipes = new ArrayList<Recipe>(properties.size());
for (Object o : properties.values()) {
if (o instanceof Recipe) {
Recipe recipe = (Recipe) o;
nestedRecipes.add(recipe);
}
}
return nestedRecipes;
}
public List<Recipe> getConstructorRecipes() {
// find the factory that will be used to create the class instance
Factory factory = findFactory(Object.class);
// if we are NOT using an instance factory to create the object
// (we have a factory method and it is not a static factory method)
if (factoryMethod != null && !(factory instanceof StaticFactory)) {
// only include recipes used in the construcor args
List<String> parameterNames = factory.getParameterNames();
List<Recipe> nestedRecipes = new ArrayList<Recipe>(parameterNames.size());
for (Map.Entry<Property, Object> entry : properties.entrySet()) {
if (parameterNames.contains(entry.getKey().name) && entry.getValue() instanceof Recipe) {
Recipe recipe = (Recipe) entry.getValue();
nestedRecipes.add(recipe);
}
}
return nestedRecipes;
} else {
// when there is an instance factory all nested recipes are used in the constructor
return getNestedRecipes();
}
}
public boolean canCreate(Type type) {
Class myType = getType();
return RecipeHelper.isAssignable(type, myType) || RecipeHelper.isAssignable(type, myType);
}
protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException {
unsetProperties.clear();
//
// load the type class
Class typeClass = getType();
//
// clone the properties so they can be used again
Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
//
// create the instance
Factory factory = findFactory(expectedType);
Object[] parameters = extractConstructorArgs(propertyValues, factory);
Object instance = factory.create(parameters);
//
// add to execution context if name is specified
if (getName() != null) {
ExecutionContext.getContext().addObject(getName(), instance);
}
//
// set the properties
setProperties(propertyValues, instance, instance.getClass());
//
// call instance factory method
// if we have a factory method name and did not find a static factory,
// then we have an instance factory
if (factoryMethod != null && !(factory instanceof StaticFactory)) {
// find the instance factory method
Method instanceFactory = ReflectionUtil.findInstanceFactory(instance.getClass(), factoryMethod, null);
try {
instance = instanceFactory.invoke(instance);
} catch (Exception e) {
Throwable t = e;
if (e instanceof InvocationTargetException) {
InvocationTargetException invocationTargetException = (InvocationTargetException) e;
if (invocationTargetException.getCause() != null) {
t = invocationTargetException.getCause();
}
}
throw new ConstructionException("Error calling instance factory method: " + instanceFactory, t);
}
}
return instance;
}
public void setProperties(Object instance) throws ConstructionException {
unsetProperties.clear();
// clone the properties so they can be used again
Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
setProperties(propertyValues, instance, instance.getClass());
}
public Class setStaticProperties() throws ConstructionException {
unsetProperties.clear();
// load the type class
Class typeClass = getType();
// verify that it is a class we can construct
if (!Modifier.isPublic(typeClass.getModifiers())) {
throw new ConstructionException("Class is not public: " + typeClass.getName());
}
if (Modifier.isInterface(typeClass.getModifiers())) {
throw new ConstructionException("Class is an interface: " + typeClass.getName());
}
if (Modifier.isAbstract(typeClass.getModifiers())) {
throw new ConstructionException("Class is abstract: " + typeClass.getName());
}
// clone the properties so they can be used again
Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
setProperties(propertyValues, null, typeClass);
return typeClass;
}
public Class getType() {
if (typeClass != null || typeName != null) {
Class type = typeClass;
if (type == null) {
try {
type = RecipeHelper.loadClass(typeName);
} catch (ClassNotFoundException e) {
throw new ConstructionException("Type class could not be found: " + typeName);
}
}
return type;
}
return null;
}
public void setRegistry(final PropertyEditorRegistry registry) {
this.registry = registry;
}
private void setProperties(Map<Property, Object> propertyValues, Object instance, Class clazz) {
// set remaining properties
for (Map.Entry<Property, Object> entry : RecipeHelper.prioritizeProperties(propertyValues)) {
Property propertyName = entry.getKey();
Object propertyValue = entry.getValue();
setProperty(instance, clazz, propertyName, propertyValue);
}
}
private void setProperty(Object instance, Class clazz, Property propertyName, Object propertyValue) {
List<Member> members = new ArrayList<Member>();
try {
if (propertyName instanceof SetterProperty){
List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options, registry);
for (Method setter : setters) {
MethodMember member = new MethodMember(setter);
members.add(member);
}
} else if (propertyName instanceof FieldProperty){
FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options, registry));
members.add(member);
} else if (propertyName instanceof AutoMatchProperty){
MissingAccessorException noField = null;
if (options.contains(Option.FIELD_INJECTION)) {
List<Field> fieldsByType = null;
try {
fieldsByType = ReflectionUtil.findAllFieldsByType(clazz, propertyValue, options, registry);
FieldMember member = new FieldMember(fieldsByType.iterator().next());
members.add(member);
} catch (MissingAccessorException e) {
noField = e;
}
// if we got more then one matching field, that is an immidate error
if (fieldsByType != null && fieldsByType.size() > 1) {
List<String> matches = new ArrayList<String>();
for (Field field : fieldsByType) {
matches.add(field.getName());
}
throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one field: " + matches, 0);
}
}
// if we didn't find any fields, try the setters
if (members.isEmpty()) {
List<Method> settersByType;
try {
settersByType = ReflectionUtil.findAllSettersByType(clazz, propertyValue, options, registry);
MethodMember member = new MethodMember(settersByType.iterator().next());
members.add(member);
} catch (MissingAccessorException noSetter) {
throw (noField == null || noSetter.getMatchLevel() > noField.getMatchLevel())? noSetter: noField;
}
// if we got more then one matching field, that is an immidate error
if (settersByType != null && settersByType.size() > 1) {
List<String> matches = new ArrayList<String>();
for (Method setter : settersByType) {
matches.add(setter.getName());
}
throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one setter: " + matches, 0);
}
}
} else if (propertyName instanceof CompoundProperty) {
String[] names = propertyName.name.split("\\.");
for (int i = 0; i < names.length - 1; i++) {
Method getter = ReflectionUtil.findGetter(clazz, names[i], options);
if (getter != null) {
try {
instance = getter.invoke(instance);
clazz = instance.getClass();
} catch (Exception e) {
Throwable t = e;
if (e instanceof InvocationTargetException) {
InvocationTargetException invocationTargetException = (InvocationTargetException) e;
if (invocationTargetException.getCause() != null) {
t = invocationTargetException.getCause();
}
}
throw new ConstructionException("Error setting property: " + names[i], t);
}
} else {
throw new ConstructionException("No getter for " + names[i] + " property");
}
}
List<Method> setters = ReflectionUtil.findAllSetters(clazz, names[names.length - 1], propertyValue, options, registry);
for (Method setter : setters) {
MethodMember member = new MethodMember(setter);
members.add(member);
}
} else {
// add setter members
MissingAccessorException noSetter = null;
try {
List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options, registry);
for (Method setter : setters) {
MethodMember member = new MethodMember(setter);
members.add(member);
}
} catch (MissingAccessorException e) {
noSetter = e;
if (!options.contains(Option.FIELD_INJECTION)) {
throw noSetter;
}
}
if (options.contains(Option.FIELD_INJECTION)) {
try {
FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options, registry));
members.add(member);
} catch (MissingAccessorException noField) {
if (members.isEmpty()) {
throw (noSetter == null || noField.getMatchLevel() > noSetter.getMatchLevel())? noField: noSetter;
}
}
}
}
} catch (MissingAccessorException e) {
if (options.contains(Option.IGNORE_MISSING_PROPERTIES)) {
unsetProperties.put(propertyName.name, propertyValue);
return;
}
throw e;
}
ConstructionException conversionException = null;
for (Member member : members) {
// convert the value to type of setter/field
try {
propertyValue = RecipeHelper.convert(member.getType(), propertyValue, false, registry);
} catch (Exception e) {
// save off first conversion exception, in case setting failed
if (conversionException == null) {
String valueType = propertyValue == null ? "null" : propertyValue.getClass().getName();
String memberType = member.getType() instanceof Class ? ((Class) member.getType()).getName() : member.getType().toString();
conversionException = new ConstructionException("Unable to convert property value" +
" from " + valueType +
" to " + memberType +
" for injection " + member, e);
}
continue;
}
try {
// set value
member.setValue(instance, propertyValue);
} catch (Exception e) {
Throwable t = e;
if (e instanceof InvocationTargetException) {
InvocationTargetException invocationTargetException = (InvocationTargetException) e;
if (invocationTargetException.getCause() != null) {
t = invocationTargetException.getCause();
}
}
throw new ConstructionException("Error setting property: " + member, t);
}
// value set successfully
return;
}
throw conversionException;
}
private Factory findFactory(Type expectedType) {
Class type = getType();
//
// attempt to find a static factory
if (factoryMethod != null) {
try {
StaticFactory staticFactory = ReflectionUtil.findStaticFactory(
type,
factoryMethod,
constructorArgNames,
constructorArgTypes,
getProperties().keySet(),
options);
return staticFactory;
} catch (MissingFactoryMethodException ignored) {
}
}
//
// factory was not found, look for a constuctor
// if expectedType is a subclass of the assigned type, we create
// the sub class instead
Class consturctorClass;
if (RecipeHelper.isAssignable(type, expectedType)) {
consturctorClass = RecipeHelper.toClass(expectedType);
} else {
consturctorClass = type;
}
ConstructorFactory constructor = ReflectionUtil.findConstructor(
consturctorClass,
constructorArgNames,
constructorArgTypes,
getProperties().keySet(),
options);
return constructor;
}
private Object[] extractConstructorArgs(Map propertyValues, Factory factory) {
List<String> parameterNames = factory.getParameterNames();
List<Type> parameterTypes = factory.getParameterTypes();
Object[] parameters = new Object[parameterNames.size()];
for (int i = 0; i < parameterNames.size(); i++) {
Property name = new Property(parameterNames.get(i));
Type type = parameterTypes.get(i);
Object value;
if (propertyValues.containsKey(name)) {
value = propertyValues.remove(name);
if (!RecipeHelper.isInstance(type, value) && !RecipeHelper.isConvertable(type, value, registry)) {
throw new ConstructionException("Invalid and non-convertable constructor parameter type: " +
"name=" + name + ", " +
"index=" + i + ", " +
"expected=" + RecipeHelper.toClass(type).getName() + ", " +
"actual=" + (value == null ? "null" : value.getClass().getName()));
}
value = RecipeHelper.convert(type, value, false, registry);
} else {
value = getDefaultValue(RecipeHelper.toClass(type));
}
parameters[i] = value;
}
return parameters;
}
private static Object getDefaultValue(Class type) {
if (type.equals(Boolean.TYPE)) {
return Boolean.FALSE;
} else if (type.equals(Character.TYPE)) {
return (char) 0;
} else if (type.equals(Byte.TYPE)) {
return (byte) 0;
} else if (type.equals(Short.TYPE)) {
return (short) 0;
} else if (type.equals(Integer.TYPE)) {
return 0;
} else if (type.equals(Long.TYPE)) {
return (long) 0;
} else if (type.equals(Float.TYPE)) {
return (float) 0;
} else if (type.equals(Double.TYPE)) {
return (double) 0;
}
return null;
}
public static interface Member {
Type getType();
void setValue(Object instance, Object value) throws Exception;
}
public static class MethodMember implements Member {
private final Method setter;
public MethodMember(Method method) {
this.setter = method;
}
public Type getType() {
return setter.getGenericParameterTypes()[0];
}
public void setValue(Object instance, Object value) throws Exception {
setter.invoke(instance, value);
}
public String toString() {
return setter.toString();
}
}
public static class FieldMember implements Member {
private final Field field;
public FieldMember(Field field) {
this.field = field;
}
public Type getType() {
return field.getGenericType();
}
public void setValue(Object instance, Object value) throws Exception {
field.set(instance, value);
}
public String toString() {
return field.toString();
}
}
public static class Property {
private final String name;
public Property(String name) {
if (name == null) throw new NullPointerException("name is null");
this.name = name;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (o instanceof String){
return this.name.equals(o);
}
if (o instanceof Property) {
Property property = (Property) o;
return this.name.equals(property.name);
}
return false;
}
public int hashCode() {
return name.hashCode();
}
public String toString() {
return name;
}
}
public static class SetterProperty extends Property {
public SetterProperty(String name) {
super(name);
}
public int hashCode() {
return super.hashCode()+2;
}
public String toString() {
return "[setter] "+super.toString();
}
}
public static class FieldProperty extends Property {
public FieldProperty(String name) {
super(name);
}
public int hashCode() {
return super.hashCode()+1;
}
public String toString() {
return "[field] "+ super.toString();
}
}
public static class AutoMatchProperty extends Property {
public AutoMatchProperty(String type) {
super(type);
}
public int hashCode() {
return super.hashCode()+1;
}
public String toString() {
return "[auto-match] "+ super.toString();
}
}
public static class CompoundProperty extends Property {
public CompoundProperty(String type) {
super(type);
}
public int hashCode() {
return super.hashCode()+1;
}
public String toString() {
return "[compound] "+ super.toString();
}
}
}