package com.fluentinterface.proxy;
import com.fluentinterface.annotation.Sets;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.fluentinterface.utils.ConversionUtils.translateFromPrimitive;
/**
* A dynamic proxy which will build a bean of the target type upon calls to the implemented interface.
*/
public class BuilderProxy implements InvocationHandler {
private static final Pattern BUILDER_METHOD_PROPERTY_PATTERN = Pattern.compile("[a-z]+([A-Z].*)");
private Class proxied;
private Class builtClass;
private BuilderDelegate builderDelegate;
private AttributeAccessStrategy attributeAccessStrategy;
private Map<String, Object> propertiesToSet;
public BuilderProxy(Class builderInterface, Class builtClass, BuilderDelegate builderDelegate,
AttributeAccessStrategy attributeAccessStrategy) {
this.proxied = builderInterface;
this.builtClass = builtClass;
this.builderDelegate = builderDelegate;
this.attributeAccessStrategy = attributeAccessStrategy;
this.propertiesToSet = new LinkedHashMap<String, Object>();
}
public Object invoke(Object target, Method method, Object[] params) throws Throwable {
if (isFluentSetter(method)) {
String propertyBeingSet = extractPropertyNameFrom(method);
Object valueForProperty = params[0];
if (!hasProperty(builtClass, propertyBeingSet)) {
throw new IllegalStateException(String.format(
"Method [%s] on [%s] corresponds to unknown property [%s] on built class [%s]",
method.getName(), proxied, propertyBeingSet, builtClass)
);
}
propertiesToSet.put(propertyBeingSet, valueForProperty);
return target;
} else if (isBuildMethod(method)) {
params = extractVarArgsIfNeeded(params);
return createInstanceFromProperties(params);
}
throw new IllegalStateException("Unrecognized builder method: " + method);
}
private Object[] extractVarArgsIfNeeded(Object[] params) {
if (params != null
&& params.length == 1
&& params[params.length - 1].getClass().isArray()) {
return (Object[]) params[params.length - 1];
}
return params;
}
private boolean hasProperty(Class<?> builtClass, String propertyName) {
return attributeAccessStrategy.hasProperty(builtClass, propertyName);
}
private Object createInstanceFromProperties(Object[] params) throws Exception {
buildIfBuilderInstances(params);
Constructor<?> constructor = findMatchingConstructor(params);
Object instance = constructor.newInstance(params);
for (Map.Entry<String, Object> entry : propertiesToSet.entrySet()) {
String property = entry.getKey();
Object value = entry.getValue();
setTargetProperty(instance, property, value);
}
return instance;
}
private Constructor findMatchingConstructor(Object[] params) throws NoSuchMethodException {
if (params == null || params.length == 0) {
// use default (empty) constructor
return builtClass.getConstructor();
}
Class<?>[] paramTypes = extractTypesFromValues(params);
List<Constructor<?>> candidates = findCandidateConstructors(paramTypes);
if (candidates.isEmpty()) {
throw new IllegalArgumentException(String.format(
"No constructor found on class [%s] that matches signature (%s)",
builtClass, Arrays.toString(paramTypes)));
} else if (candidates.size() > 1) {
throw new IllegalArgumentException(String.format(
"Found %s constructors matching signature (%s) on class [%s], which is too ambiguous to proceed.",
candidates.size(), Arrays.toString(paramTypes), builtClass));
} else {
return candidates.get(0);
}
}
private Class<?>[] extractTypesFromValues(Object[] params) {
Class<?>[] paramTypes = new Class<?>[params.length];
for (int i = 0; i < params.length; i++) {
Object param = params[i];
paramTypes[i] = (param == null) ? null : param.getClass();
}
return paramTypes;
}
private List<Constructor<?>> findCandidateConstructors(Class<?>[] paramTypes) {
Constructor<?>[] allConstructors = builtClass.getDeclaredConstructors();
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>();
for (Constructor<?> constructor : allConstructors) {
Class<?>[] constructorParamTypes = constructor.getParameterTypes();
if (constructorParamTypes.length != paramTypes.length) {
continue;
}
// if all param types match constructor argument types (null always matching), then consider as candidate
if (typesAreCompatible(paramTypes, constructorParamTypes)) {
candidates.add(constructor);
}
}
return candidates;
}
private boolean typesAreCompatible(Class<?>[] paramTypes, Class<?>[] constructorParamTypes) {
boolean matches = true;
for (int i = 0; i < paramTypes.length; i++) {
Class<?> paramType = paramTypes[i];
if (paramType != null) {
Class<?> inputParamType = translateFromPrimitive(paramType);
Class<?> constructorParamType = translateFromPrimitive(constructorParamTypes[i]);
if (!inputParamType.isAssignableFrom(constructorParamType)) {
matches = false;
break;
}
}
}
return matches;
}
private void setTargetProperty(Object target, String property, Object value) throws Exception {
Class targetPropertyType = attributeAccessStrategy.getPropertyType(target, property);
if (value != null) {
Collection<Object> valueAsCollection = convertToCollectionIfMultiValued(value);
if (valueAsCollection != null) {
valueAsCollection = buildBuildersInCollection(valueAsCollection);
value = transformCollectionToTargetTypeIfPossible(value, valueAsCollection, targetPropertyType);
} else {
value = buildIfBuilderInstance(value);
}
}
attributeAccessStrategy.setPropertyValue(target, property, value);
}
private Object transformCollectionToTargetTypeIfPossible(Object originalValue, Collection<Object> valueAsCollection,
Class targetPropertyType) throws InstantiationException, IllegalAccessException {
if (targetPropertyType.isArray()) {
return collectionToArray(valueAsCollection, targetPropertyType);
}
Collection<Object> targetValue = createCollectionOfType(targetPropertyType);
if (targetValue != null) {
targetValue.addAll(valueAsCollection);
return targetValue;
}
return originalValue;
}
private boolean hasBuilderDelegate() {
return (builderDelegate != null);
}
private Collection<Object> buildBuildersInCollection(Collection<Object> collectionWithBuilders) {
if (!hasBuilderDelegate()) {
return collectionWithBuilders;
}
Collection<Object> transformed = new ArrayList<Object>(collectionWithBuilders.size());
for (Object element : collectionWithBuilders) {
element = buildIfBuilderInstance(element);
transformed.add(element);
}
return transformed;
}
private void buildIfBuilderInstances(Object[] params) {
for (int i = 0; i < params.length; i++) {
params[i] = buildIfBuilderInstance(params[i]);
}
}
@SuppressWarnings("unchecked")
private Object buildIfBuilderInstance(Object value) {
if (!hasBuilderDelegate()) {
return value;
}
if (builderDelegate.isBuilderInstance(value)) {
return builderDelegate.build(value);
}
return value;
}
@SuppressWarnings("unchecked")
private Collection<Object> convertToCollectionIfMultiValued(Object value) {
Class valueClass = value.getClass();
Collection<Object> valueAsCollection = null;
if (valueClass.isArray()) {
valueAsCollection = arrayToCollection(value);
} else if (isCollection(valueClass)) {
valueAsCollection = (Collection) value;
}
return valueAsCollection;
}
private Object collectionToArray(Collection<Object> valueAsCollection, Class targetPropertyType) {
Class arrayElementsType = targetPropertyType.getComponentType();
int arraySize = valueAsCollection.size();
Object createdArray = Array.newInstance(arrayElementsType, arraySize);
int idx = 0;
for (Object arrayElement : valueAsCollection) {
Array.set(createdArray, idx++, arrayElement);
}
return createdArray;
}
private Collection<Object> arrayToCollection(Object array) {
if (!array.getClass().isArray()) {
throw new IllegalArgumentException(String.format("[%s] is not an array.", array));
}
int arrayLength = Array.getLength(array);
List<Object> converted = new ArrayList<Object>(arrayLength);
for (int i = 0; i < arrayLength; i++) {
Object currentElement = Array.get(array, i);
converted.add(currentElement);
}
return converted;
}
private boolean isCollection(Class clazz) {
return Collection.class.isAssignableFrom(clazz);
}
@SuppressWarnings("unchecked")
private Collection<Object> createCollectionOfType(Class clazz) throws IllegalAccessException, InstantiationException {
if (!isCollection(clazz)) {
throw new IllegalArgumentException(String.format("Class [%s] is not a collection.", clazz));
}
if (clazz.isInterface()) {
if (SortedSet.class.isAssignableFrom(clazz)) {
return new TreeSet<Object>();
} else if (Set.class.isAssignableFrom(clazz)) {
return new HashSet<Object>();
} else if (List.class.isAssignableFrom(clazz)) {
return new ArrayList<Object>();
}
return null;
}
return (Collection<Object>) clazz.newInstance();
}
private String extractPropertyNameFrom(Method method) {
String propertyName = extractPropertyNameFromSetsAnnotation(method);
if (propertyName != null) {
return propertyName;
}
String methodName = method.getName();
Matcher propertyNameMatcher = BUILDER_METHOD_PROPERTY_PATTERN.matcher(methodName);
if (propertyNameMatcher.matches()) {
propertyName = propertyNameMatcher.group(1);
if (propertyName != null) {
return uncapitalize(propertyName);
}
}
throw new IllegalStateException(String.format(
"Method [%s] does not seem to represent a setter for a property", methodName));
}
private String extractPropertyNameFromSetsAnnotation(Method method) {
Sets setsAnnotation = method.getAnnotation(Sets.class);
return setsAnnotation != null ? setsAnnotation.property() : null;
}
private boolean isBuildMethod(Method method) {
if (hasBuilderDelegate()) {
return builderDelegate.isBuildMethod(method);
}
return method.getReturnType() == Object.class;
}
private boolean isFluentSetter(Method method) {
return method.getParameterTypes().length == 1
&& method.getReturnType().isAssignableFrom(this.proxied)
&& !this.isBuildMethod(method);
}
private String uncapitalize(String source) {
if (source == null || source.isEmpty()) {
return source;
}
if (source.length() == 1) {
return source.toLowerCase();
}
return source.substring(0, 1).toLowerCase() + source.substring(1);
}
}