/*
* Copyright 2006 - 2007 the original author or authors.
*
* Licensed 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.springmodules.xt.model.generator.factory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.log4j.Logger;
import org.springframework.aop.support.AopUtils;
import org.springmodules.xt.model.generator.annotation.ConstructorArg;
import org.springmodules.xt.model.generator.annotation.ConstructorArgType;
import org.springmodules.xt.model.generator.annotation.FactoryMethod;
import org.springmodules.xt.model.generator.annotation.Property;
import org.springmodules.xt.model.generator.annotation.Value;
import org.springmodules.xt.model.generator.support.IllegalArgumentPositionException;
import org.springmodules.xt.model.generator.support.ObjectConstructionException;
import org.springmodules.xt.model.generator.support.ReturnTypeMismatchException;
import org.springmodules.xt.model.introductor.support.IllegalReturnTypeException;
/**
* Java dynamic Proxy-based interceptor for implementing factory generation.
*
* @see org.springmodules.xt.model.generator.factory.DynamicFactoryGenerator
*
* @author Sergio Bossa
*/
public class FactoryGeneratorInterceptor implements InvocationHandler {
private static final Logger logger = Logger.getLogger(FactoryGeneratorInterceptor.class);
private Class productClass;
// Needs to be ordered by argument position:
private TreeMap<Integer, ConstructorArgPair> constructorArgs = new TreeMap<Integer, ConstructorArgPair>();
// No need to be ordered:
private HashMap<String, PropertyPair> properties = new HashMap<String, PropertyPair>();
// General values map, holding both constructor args and properties values:
private HashMap<String, Object> values = new HashMap<String, Object>();
/**
* Constructor.
*
* @param productClass The factory product class, that is, the class of the object created by the factory.
*/
public FactoryGeneratorInterceptor(Class productClass) {
this.productClass = productClass;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (this.isConstructorArg(method)) {
return this.putConstructorArg(args, method);
} else if (this.isPropertySetter(method)) {
return this.putProperty(args, method);
} else if (this.isValueSetter(method)) {
return this.putValue(args, method);
} else if (this.isGetter(method)) {
return this.readProperty(args, method);
} else if (this.isFactoryMethod(method)) {
Class returnType = method.getReturnType();
if (!returnType.isAssignableFrom(this.productClass)) {
throw new ReturnTypeMismatchException("Return type mismatch. Expected assignable from: " + this.productClass + ", found: " + returnType);
} else {
Object product = this.make();
// Fill properties:
for (Map.Entry<String, PropertyPair> entry : this.properties.entrySet()) {
String propertyName = entry.getKey();
Object propertyValue = entry.getValue().getValue();
Property.AccessType propertyAccess = entry.getValue().getAccess();
if (propertyAccess.equals(Property.AccessType.FIELD)) {
Field field = this.productClass.getDeclaredField(propertyName);
field.setAccessible(true);
field.set(product, propertyValue);
} else {
String methodName = new StringBuilder("set").append(StringUtils.capitalize(propertyName)).toString();
Method methodObj = this.productClass.getMethod(methodName, propertyValue.getClass());
methodObj.invoke(product, propertyValue);
}
}
// Return product:
return product;
}
} else {
if (AopUtils.isEqualsMethod(method)) {
return this.doEquals(proxy, args[0]);
} else if (AopUtils.isHashCodeMethod(method)) {
return this.doHashCode(proxy);
} else if (AopUtils.isToStringMethod(method)) {
return this.doToString(proxy);
} else {
// Fail fast:
throw new UnsupportedOperationException("Unsupported method called: " + method.getName());
}
}
}
private boolean isConstructorArg(Method method) {
if (method.getName().startsWith("set") && method.isAnnotationPresent(ConstructorArg.class)) {
return true;
} else {
return false;
}
}
private boolean isPropertySetter(Method method) {
if (method.getName().startsWith("set") && method.isAnnotationPresent(Property.class)) {
return true;
} else {
return false;
}
}
private boolean isValueSetter(Method method) {
if (method.getName().startsWith("set") && method.isAnnotationPresent(Value.class)) {
return true;
} else {
return false;
}
}
private boolean isGetter(Method method) {
if (method.getName().startsWith("get") || method.getName().startsWith("is")) {
return true;
} else {
return false;
}
}
private boolean isFactoryMethod(Method method) {
if (method.isAnnotationPresent(FactoryMethod.class)) {
return true;
} else {
return false;
}
}
private Object putProperty(final Object[] args, final Method method) {
if (args.length != 1) {
throw new IllegalStateException("The setter method " + method.getName() + " must have only one argument!");
} else {
String name = StringUtils.uncapitalize(method.getName().substring(3));
Property annotation = method.getAnnotation(Property.class);
Property.AccessType access = annotation.access();
PropertyPair pair = new PropertyPair();
pair.setValue(args[0]);
pair.setAccess(access);
this.properties.put(name, pair);
this.values.put(name, args[0]);
logger.debug(new StringBuilder("Put property with name and value: ").append(name).append(",").append(args[0]));
return null;
}
}
private Object putValue(final Object[] args, final Method method) {
if (args.length != 1) {
throw new IllegalStateException("The setter method " + method.getName() + " must have only one argument!");
} else {
String name = StringUtils.uncapitalize(method.getName().substring(3));
this.values.put(name, args[0]);
logger.debug(new StringBuilder("Put value property with name and value: ").append(name).append(",").append(args[0]));
return null;
}
}
private Object putConstructorArg(final Object[] args, final Method method) {
if (args.length != 1) {
throw new IllegalStateException("The setter method " + method.getName() + " must have only one argument!");
} else {
String name = StringUtils.uncapitalize(method.getName().substring(3));
ConstructorArg annotation = method.getAnnotation(ConstructorArg.class);
int position = annotation.position();
Class type = null;
if (method.isAnnotationPresent(ConstructorArgType.class)) {
type = method.getAnnotation(ConstructorArgType.class).type();
} else {
type = args[0].getClass();
}
ConstructorArgPair pair = new ConstructorArgPair();
pair.setValue(args[0]);
pair.setType(type);
this.constructorArgs.put(position, pair);
this.values.put(name, args[0]);
logger.debug(new StringBuilder("Put constructor arg with position and value: ").append(position).append(",").append(args[0]));
return null;
}
}
private Object readProperty(final Object[] args, final Method method) {
if (args != null && args.length != 0) {
throw new IllegalStateException("The getter method " + method.getName() + " must have no arguments!");
}
if (method.getReturnType().isPrimitive()) {
throw new IllegalReturnTypeException("Return types cannot be primitives.");
} else {
String name = null;
if (method.getName().startsWith("get")) {
name = StringUtils.uncapitalize(method.getName().substring(3));
} else {
name = StringUtils.uncapitalize(method.getName().substring(2));
}
Object value = values.get(name);
logger.debug(new StringBuilder("Read property with name and value: ").append(name).append(",").append(value));
return value;
}
}
private Object make() {
logger.debug("Making object of class: " + this.productClass);
int argsNr = this.constructorArgs.size();
Object[] argsArray = new Object[argsNr];
Class[] typesArray = new Class[argsNr];
for (Map.Entry<Integer, ConstructorArgPair> entry : this.constructorArgs.entrySet()) {
int position = entry.getKey();
Object value = entry.getValue().getValue();
Class type = entry.getValue().getType();
if (position < 0 || position > argsNr - 1) {
throw new IllegalArgumentPositionException("Illegal position: " + position);
} else {
argsArray[position] = value;
typesArray[position] = type;
}
}
try {
Constructor constructor = this.productClass.getConstructor(typesArray);
Object product = constructor.newInstance(argsArray);
return product;
} catch (NoSuchMethodException ex) {
throw new ObjectConstructionException("No constructor found accepting the following array of types: "
+ ToStringBuilder.reflectionToString(typesArray, ToStringStyle.SIMPLE_STYLE)
+ " Have you correctly set all constructor arguments?", ex);
} catch (InvocationTargetException ex) {
throw new ObjectConstructionException("Exception thrown by the underlying constructor: "
+ ex.getMessage(), ex);
} catch (IllegalAccessException ex) {
throw new ObjectConstructionException("Cannot access a constructor with the following array of types: "
+ ToStringBuilder.reflectionToString(typesArray, ToStringStyle.SIMPLE_STYLE)
+ " Have you correctly set all constructor arguments?", ex);
} catch (InstantiationException ex) {
throw new ObjectConstructionException("Unable to instantiate the following class: "
+ this.productClass, ex);
}
}
private boolean doEquals(Object proxy, Object other) {
if (other != null && Proxy.isProxyClass(other.getClass())) {
return Proxy.getInvocationHandler(proxy) == Proxy.getInvocationHandler(other);
} else {
return false;
}
}
private int doHashCode(Object proxy) {
return Proxy.getInvocationHandler(proxy).hashCode();
}
private String doToString(Object proxy) {
return Proxy.getInvocationHandler(proxy).toString();
}
/*** Inner classes ***/
private class ConstructorArgPair {
private Object value;
private Class type;
public Object getValue() {
return this.value;
}
public void setValue(Object value) {
this.value = value;
}
public Class getType() {
return this.type;
}
public void setType(Class type) {
this.type = type;
}
}
private class PropertyPair {
private Object value;
private Property.AccessType access;
public Object getValue() {
return this.value;
}
public void setValue(Object value) {
this.value = value;
}
public Property.AccessType getAccess() {
return this.access;
}
public void setAccess(Property.AccessType access) {
this.access = access;
}
}
}