/*
* This code is distributed under The GNU Lesser General Public License (LGPLv3)
* Please visit GNU site for LGPLv3 http://www.gnu.org/copyleft/lesser.html
*
* Copyright Denis Pavlov 2009
* Web: http://www.genericdtoassembler.org
* SVN: https://svn.code.sf.net/p/geda-genericdto/code/trunk/
* SVN (mirror): http://geda-genericdto.googlecode.com/svn/trunk/
*/
package com.inspiresoftware.lib.dto.geda.assembler.extension.impl;
import com.inspiresoftware.lib.dto.geda.assembler.SynthesizerUtils;
import com.inspiresoftware.lib.dto.geda.assembler.extension.Cache;
import com.inspiresoftware.lib.dto.geda.assembler.extension.DataReader;
import com.inspiresoftware.lib.dto.geda.assembler.extension.DataWriter;
import com.inspiresoftware.lib.dto.geda.assembler.extension.MethodSynthesizer;
import com.inspiresoftware.lib.dto.geda.exception.GeDAException;
import com.inspiresoftware.lib.dto.geda.exception.GeDARuntimeException;
import com.inspiresoftware.lib.dto.geda.exception.InspectionPropertyNotFoundException;
import com.inspiresoftware.lib.dto.geda.exception.UnableToCreateInstanceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.PropertyDescriptor;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Template for method synthesizer.
*
* @author DPavlov
* @since 1.1.2
*/
public abstract class AbstractMethodSynthesizer extends SynthesizerUtils implements MethodSynthesizer {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMethodSynthesizer.class);
private final Lock readLock = new ReentrantLock();
private final Lock writeLock = new ReentrantLock();
private static final int MAX_COMPILE_TRIES = 3;
private Reference<ClassLoader> loader;
/**
* The <code>int</code> value representing the <code>public</code>
* modifier.
* @see java.lang.reflect.Modifier
*/
private static final int PUBLIC = 0x00000001;
/**
* This method allows to "catch" generated classes by class generators such as Javassist or CGLib.
* The problem with those classes is that some of them are loaded by private ClassLoaders and are
* invisible to other generated classes such as DataReaders and DataWriters. The basic "catch" is
* to track $ sign in the class name that is put by both of those generators.
*
* Typically this problem arrises with lazy initialized fields via proxies in sub entities.
*
* @param method class method
* @return class that declares the method
*/
public static Class< ? > getValidDeclaringClass(final java.lang.reflect.Method method) {
final Class< ? > decl = method.getDeclaringClass();
if (decl.isAnonymousClass() || decl.getName().indexOf('$') == -1) {
return decl;
}
final Class< ? > declS = decl.getSuperclass();
if (declS.equals(Object.class)) {
final Class< ? >[] declIs = decl.getInterfaces();
for (int i = 0; i < declIs.length; i++) {
if (declaringClassIsValid(declIs[i], method)) {
return declIs[i];
}
}
throw new GeDARuntimeException("Unable to identify interface for proxy object");
}
if (declaringClassIsValid(declS, method)) {
return declS;
}
final Class< ? >[] declIs = decl.getInterfaces();
for (int i = 0; i < declIs.length; i++) {
if (declaringClassIsValid(declIs[i], method)) {
return declIs[i];
}
}
throw new GeDARuntimeException("Unable to identify interface for proxy object");
}
private static boolean declaringClassIsValid(final Class< ? > clazz, final java.lang.reflect.Method method) {
try {
clazz.getMethod(method.getName(), method.getParameterTypes());
return true;
} catch (Exception all) {
return false;
}
}
/** DataReaders instances cache. */
private static final Cache<Object> READER_CACHE = new SoftReferenceCache<Object>();
/** DataWriters instances cache. */
private static final Cache<Object> WRITER_CACHE = new SoftReferenceCache<Object>();
/**
* Primitive to wrapper conversion map.
*/
protected static final Map<String, String> PRIMITIVE_TO_WRAPPER = new HashMap<String, String>();
static {
PRIMITIVE_TO_WRAPPER.put("byte", Byte.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("short", Short.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("int", Integer.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("long", Long.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("float", Float.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("double", Double.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("boolean", Boolean.class.getCanonicalName());
PRIMITIVE_TO_WRAPPER.put("char", Character.class.getCanonicalName());
}
/**
* Primitive to wrapper conversion map.
*/
protected static final Map<String, Class< ? >> PRIMITIVE_TO_WRAPPER_CLASS = new HashMap<String, Class< ? >>();
static {
PRIMITIVE_TO_WRAPPER_CLASS.put("byte", Byte.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("short", Short.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("int", Integer.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("long", Long.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("float", Float.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("double", Double.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("boolean", Boolean.class);
PRIMITIVE_TO_WRAPPER_CLASS.put("char", Character.class);
}
/**
* Wrapper to promitive conversion map.
*/
protected static final Map<String, String> WRAPPER_TO_PRIMITIVE = new HashMap<String, String>();
static {
WRAPPER_TO_PRIMITIVE.put("byte", ".byteValue()");
WRAPPER_TO_PRIMITIVE.put("short", ".shortValue()");
WRAPPER_TO_PRIMITIVE.put("int", ".intValue()");
WRAPPER_TO_PRIMITIVE.put("long", ".longValue()");
WRAPPER_TO_PRIMITIVE.put("float", ".floatValue()");
WRAPPER_TO_PRIMITIVE.put("double", ".doubleValue()");
WRAPPER_TO_PRIMITIVE.put("boolean", ".booleanValue()");
WRAPPER_TO_PRIMITIVE.put("char", ".charValue()");
}
/**
* Default constructor synthesizers. This set a template for auto
* class generation. It is a requirement to provide a valid class loader
* to which files will be loaded. This in effect like a visitor pattern
* whereby we generate required file and enhance the current class
* loader by providing new class. The reason for this is that newly
* generated files need access to graph of objects available to class
* loader. Also it allows us to separate our contributions to each class
* loader.
*
* @param classLoader class loader that will be used to load generated classes
*/
public AbstractMethodSynthesizer(final ClassLoader classLoader) {
super();
this.loader = initialiseClassLoaderWeakReference(classLoader);
}
/**
* Hook for sub classes to assign correct class loader reference.
* We must maintain only weak reference to the class loader.
*
* @param classLoader class loader to contribute generated files to.
*/
protected Reference<ClassLoader> initialiseClassLoaderWeakReference(final ClassLoader classLoader) {
return new SoftReference<ClassLoader>(classLoader);
}
/**
* Hook for changing class loader reference.
* Invokes #initialiseClassLoaderWeakReference(getClassLoader()).
*/
protected final void enhanceClassLoader() {
this.loader = initialiseClassLoaderWeakReference(getClassLoader());
}
/**
* @param configuration configuration name
* @param value value to set
* @return true if configuration was set, false if not set or invalid
* @throws GeDAException any exceptions during configuration
*/
public boolean configure(final String configuration, final Object value) throws GeDAException {
return false;
}
/*
* @param cleanUpReaderCycle reader cache clean up cycle
*/
private boolean setCleanUpReaderCycle(final Object cleanUpReaderCycle) throws GeDAException {
return READER_CACHE.configure("cleanUpCycle", cleanUpReaderCycle);
}
/*
* @param cleanUpWriterCycle writer cache clean up cycle
*/
private boolean setCleanUpWriterCycle(final Object cleanUpWriterCycle) throws GeDAException {
return WRITER_CACHE.configure("cleanUpCycle", cleanUpWriterCycle);
}
/**
* Perform reader validation.
*
* @param descriptor descriptor
* @throws InspectionPropertyNotFoundException property not found
* @throws GeDARuntimeException any abnormality
*/
protected void preMakeReaderValidation(final PropertyDescriptor descriptor)
throws InspectionPropertyNotFoundException, GeDARuntimeException {
final Method readMethod = descriptor.getReadMethod();
if (readMethod == null) {
throw new InspectionPropertyNotFoundException("No read method for: ", descriptor.getName());
}
final Class< ? > target = getValidDeclaringClass(readMethod);
if ((target.getModifiers() & PUBLIC) == 0) {
throw new GeDARuntimeException(target.getCanonicalName()
+ " does not have [public] modifier. This will cause IllegalAccessError during runtime.");
}
}
/**
* Simple object to hold plain text representation of return type of reader.
*
* @author denispavlov
*
*/
protected static class ReturnTypeContext {
private final Class< ? > clazz;
private final String methodReturnType;
private final String methodReturnTypePrimitiveName;
/**
* @param clazz class
* @param methodReturnType class of object that represent return type
* @param methodReturnTypePrimitiveName primitive name (or null if this is not primitive)
*/
public ReturnTypeContext(
final Class< ? > clazz,
final String methodReturnType,
final String methodReturnTypePrimitiveName) {
this.clazz = clazz;
this.methodReturnType = methodReturnType;
this.methodReturnTypePrimitiveName = methodReturnTypePrimitiveName;
}
/**
* @return raw class
*/
public Class< ? > getClazz() {
return clazz;
}
/**
* @return class of object that represent return type
*/
public String getMethodReturnType() {
return methodReturnType;
}
/**
* @return primitive name (or null if this is not primitive)
*/
public String getMethodReturnTypePrimitiveName() {
return methodReturnTypePrimitiveName;
}
/**
* @return true if this is a primitive type
*/
public boolean isPrimitive() {
return methodReturnTypePrimitiveName != null;
}
}
/**
* @param readerClassName class name
* @param sourceClassGetterMethodReturnType return type
* @return context
* @throws GeDARuntimeException if unable to determine correct return type
*/
protected final ReturnTypeContext getReturnTypeContext(final String readerClassName, final Type sourceClassGetterMethodReturnType)
throws GeDARuntimeException {
try {
final Class rcl = getClassForType(sourceClassGetterMethodReturnType);
if (rcl.isPrimitive()) {
return new ReturnTypeContext(rcl,
PRIMITIVE_TO_WRAPPER.get(rcl.getCanonicalName()), rcl.getCanonicalName());
}
return new ReturnTypeContext(rcl, rcl.getCanonicalName(), null);
} catch (GeDARuntimeException gre) {
throw new GeDARuntimeException("Unable to determine correct return type from getter method in class: " + readerClassName, gre);
}
}
/** {@inheritDoc} */
public final DataReader synthesizeReader(final PropertyDescriptor descriptor)
throws InspectionPropertyNotFoundException, UnableToCreateInstanceException, GeDARuntimeException {
preMakeReaderValidation(descriptor);
final Method readMethod = descriptor.getReadMethod();
final String sourceClassNameFull = getValidDeclaringClass(readMethod).getCanonicalName();
final String sourceClassGetterMethodName = readMethod.getName();
final String readerClassName = generateClassName("DataReader", sourceClassNameFull, sourceClassGetterMethodName);
DataReader reader;
reader = getFromCacheOrCreateFromClassLoader(readerClassName, READER_CACHE, getClassLoader());
if (reader == null) {
readLock.lock();
final MakeContext ctx = new MakeContext(DataReader.class.getCanonicalName());
try {
do {
reader = makeReaderClass(getClassLoader(), readMethod,
readerClassName, sourceClassNameFull, sourceClassGetterMethodName, readMethod.getGenericReturnType(), ctx);
if (reader == null) {
reader = getFromCacheOrCreateFromClassLoader(readerClassName, READER_CACHE, getClassLoader());
} else {
READER_CACHE.put(readerClassName.hashCode(), reader);
}
} while (reader == null);
} finally {
readLock.unlock();
}
}
return reader;
}
@SuppressWarnings("unchecked")
private <T> T getFromCacheOrCreateFromClassLoader(final String className,
final Cache<Object> cache,
final ClassLoader classLoader)
throws UnableToCreateInstanceException {
Object instance;
instance = cache.get(className.hashCode());
if (instance != null) {
return (T) instance;
}
instance = createInstanceFromClassLoader(classLoader, className);
if (instance != null) {
return (T) instance;
}
return null;
}
/**
* Method to be overridden by specific synthesizer - contains the low level code
* to actually generating Class object.
*
* @param loader class loader
* @param readMethod read method object
* @param readerClassName name of the reader class
* @param sourceClassNameFull name of the class of source object (i.e. whose getter will be invoked)
* @param sourceClassGetterMethodName name of the getter method to be invoked on the source object
* @param sourceClassGetterMethodReturnType class name of the return type to be returned
* @param ctx compilation context. Need to invoke .next() for every unsuccessful compilation attempt.
*
* @return data reader instance.
*
* @throws UnableToCreateInstanceException whenever there is a problem creating an instance of the generated class
* @throws GeDARuntimeException any exceptions during class generation
*/
protected abstract DataReader makeReaderClass(
final ClassLoader loader,
final Method readMethod,
final String readerClassName,
final String sourceClassNameFull,
final String sourceClassGetterMethodName,
final Type sourceClassGetterMethodReturnType,
final MakeContext ctx) throws UnableToCreateInstanceException, GeDARuntimeException;
/**
* Perform writer validation.
*
* @param descriptor descriptor
* @throws InspectionPropertyNotFoundException property not found
* @throws GeDARuntimeException any abnormality
*/
protected void preMakeWriterValidation(final PropertyDescriptor descriptor)
throws InspectionPropertyNotFoundException, GeDARuntimeException {
final Method writeMethod = descriptor.getWriteMethod();
if (writeMethod == null) {
throw new InspectionPropertyNotFoundException("No write method for: ", descriptor.getName());
}
final Class< ? > target = getValidDeclaringClass(writeMethod);
if ((target.getModifiers() & PUBLIC) == 0) {
throw new GeDARuntimeException(target.getCanonicalName()
+ " does not have [public] modifier. This will cause IllegalAccessError during runtime.");
}
}
/**
* Simple object to hold plain text representation of argument type of reader.
*
* @author denispavlov
*
*/
protected class ArgumentTypeContext {
private final Class< ? > clazz;
private final String methodArgType;
private final String methodArgPrimitiveName;
/**
* @param clazz class
* @param methodArgType object class name
* @param methodArgPrimitiveName primitive name (or null if this type is not primitive)
*/
public ArgumentTypeContext(
final Class< ? > clazz,
final String methodArgType,
final String methodArgPrimitiveName) {
this.clazz = clazz;
this.methodArgType = methodArgType;
this.methodArgPrimitiveName = methodArgPrimitiveName;
}
/**
* @return raw class
*/
public Class< ? > getClazz() {
return clazz;
}
/**
* @return object class name
*/
public String getMethodArgType() {
return methodArgType;
}
/**
* @return primitive name (or null if this type is not primitive)
*/
public String getMethodArgPrimitiveName() {
return methodArgPrimitiveName;
}
/**
* @return true if this is a primitive type
*/
public boolean isPrimitive() {
return methodArgPrimitiveName != null;
}
}
/**
* @param sourceClassSetterMethodArgumentClass class name of the argument type passed to setter
* @return context
*/
protected final ArgumentTypeContext getArgumentTypeContext(final Class< ? > sourceClassSetterMethodArgumentClass) {
if (sourceClassSetterMethodArgumentClass.isPrimitive()) {
return new ArgumentTypeContext(sourceClassSetterMethodArgumentClass,
PRIMITIVE_TO_WRAPPER.get(sourceClassSetterMethodArgumentClass.getCanonicalName()),
sourceClassSetterMethodArgumentClass.getCanonicalName());
}
return new ArgumentTypeContext(sourceClassSetterMethodArgumentClass, sourceClassSetterMethodArgumentClass.getCanonicalName(), null);
}
/** {@inheritDoc} */
public final DataWriter synthesizeWriter(final PropertyDescriptor descriptor)
throws InspectionPropertyNotFoundException, UnableToCreateInstanceException, GeDARuntimeException {
preMakeWriterValidation(descriptor);
final Method writeMethod = descriptor.getWriteMethod();
final String classNameFull = getValidDeclaringClass(writeMethod).getCanonicalName();
final String methodName = writeMethod.getName();
final String writerClassName = generateClassName("DataWriter", classNameFull, methodName);
DataWriter writer;
writer = getFromCacheOrCreateFromClassLoader(writerClassName, WRITER_CACHE, getClassLoader());
if (writer == null) {
writeLock.lock();
final MakeContext ctx = new MakeContext(DataWriter.class.getCanonicalName());
try {
do {
writer = makeWriterClass(getClassLoader(), writeMethod,
writerClassName, classNameFull, methodName, writeMethod.getParameterTypes()[0], ctx);
if (writer == null) {
writer = getFromCacheOrCreateFromClassLoader(writerClassName, WRITER_CACHE, getClassLoader());
} else {
WRITER_CACHE.put(writerClassName.hashCode(), writer);
}
} while (writer == null);
} finally {
writeLock.unlock();
}
}
return writer;
}
/**
* Method to be overridden by specific synthesizer - contains the low level code
* to actually generating Class object.
*
* @param loader class loader
* @param writeMethod write method object
* @param writerClassName name of the reader class
* @param sourceClassNameFull name of the class of source object (i.e. whose setter will be invoked)
* @param sourceClassSetterMethodName name of the setter method to be invoked on the source object
* @param sourceClassSetterMethodArgumentClass class name of the argument type passed to setter
* @param ctx compilation context. Need to invoke .next() for every unsuccessful compilation attempt.
*
* @return DataWriter instance
*
* @throws UnableToCreateInstanceException whenever there is a problem creating an instance of the generated class
*/
protected abstract DataWriter makeWriterClass(
final ClassLoader loader,
final Method writeMethod,
final String writerClassName,
final String sourceClassNameFull,
final String sourceClassSetterMethodName,
final Class< ? > sourceClassSetterMethodArgumentClass,
final MakeContext ctx) throws UnableToCreateInstanceException;
/**
* Class loader reference.
*/
protected ClassLoader getClassLoader() {
ClassLoader cl = loader.get();
if (cl == null) { // Cl was garbage collected - something gone really wrong
throw new GeDARuntimeException("Class loader has been gc'ed");
}
return cl;
}
@SuppressWarnings("unchecked")
private <T> T createInstanceFromClassLoader(final ClassLoader cl, final String clazzName)
throws UnableToCreateInstanceException {
try {
final Class< ? > clazz = Class.forName(clazzName, true, cl);
return (T) clazz.newInstance();
} catch (ClassNotFoundException cnfe) {
// That's OK we don't have it
return null;
} catch (Throwable exp) {
throw new UnableToCreateInstanceException(clazzName, "Unable to create instance of: " + clazzName, exp);
}
}
private String generateClassName(final String prefix, final String declaringClass, final String methodName) {
return declaringClass + prefix + "M" + methodName + "ID" + getSynthesizerId();
}
/**
* @return synthesizer id that will be used in class name
*/
protected abstract String getSynthesizerId();
/**
* Inner class to keep track of recursive attempts to compile a class.
*
* @author denispavlov
*
*/
public static final class MakeContext {
private int tryNo;
private final String classType;
/**
* @param classType class type (reader/writer)
*/
public MakeContext(final String classType) {
this.classType = classType;
this.tryNo = 0;
}
/**
* To be used to attempt recovery through classloader instance creation.
* If next counter exceed the number of tries and exception is thrown,
* otherwise another cycle is attempted.
*
* @param exp exception which occured whilst auto generating a class
* @param source source code.
* @throws UnableToCreateInstanceException thrown after number of tries is exceeded.
* wraps the original exception.
*/
public void next(final Exception exp, final String source) throws UnableToCreateInstanceException {
this.tryNo++;
if (this.tryNo > MAX_COMPILE_TRIES) {
throw new UnableToCreateInstanceException(classType,
"Unable to create class type [" + classType + "]\n"
+ "with source:\n============>" + source + "\n<=============", exp);
}
}
}
/** {@inheritDoc} */
public void releaseResources() {
loader.clear();
}
}