/*
* Copyright (c) 2010 Mysema Ltd.
* All rights reserved.
*
*/
package com.mysema.rdfbean.object;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import com.google.common.base.Strings;
import com.mysema.rdfbean.annotations.ClassMapping;
import com.mysema.rdfbean.annotations.Context;
import com.mysema.rdfbean.annotations.Path;
import com.mysema.rdfbean.annotations.Predicate;
import com.mysema.rdfbean.model.UID;
/**
* MappedClassFactory provides a factory for MappedClass creation
*
* @author tiwe
* @version $Id$
*/
public class MappedClassFactory {
private final Map<Class<?>, MappedClass> mappedClasses = new LinkedHashMap<Class<?>, MappedClass>();
@Nullable
private final String defaultNamespace;
public MappedClassFactory(@Nullable String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
}
private void assignConstructor(Class<?> clazz, MappedClass mappedClass) {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
if (constructors.length == 0) {
return;
}
ConstructorVisitor visitor = new ConstructorVisitor();
try {
if (clazz.getClassLoader() != null) {
InputStream is = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class");
ClassReader cr = new ClassReader(is);
cr.accept(visitor, 0);
visitor.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
Map<Integer, List<String>> paramsMap = new HashMap<Integer, List<String>>();
for (List<String> c : visitor.getConstructors()) {
paramsMap.put(c.size(), c);
}
MappedConstructor defaultConstructor = null;
MappedConstructor mappedConstructor = null;
nextConstructor: for (Constructor<?> constructor : constructors) {
if (constructor.getParameterTypes().length == 0) {
defaultConstructor = new MappedConstructor(constructor);
} else {
List<MappedPath> mappedArguments = new ArrayList<MappedPath>();
List<String> params = paramsMap.get(constructor.getParameterTypes().length);
if (params == null) {
continue;
}
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
MappedPath mappedPath = getPathMapping(mappedClass, constructor, i, params.get(i));
if (mappedPath != null) {
mappedArguments.add(mappedPath);
} else if (mappedArguments.size() > 0) {
throw new IllegalArgumentException("Constructor has unmapped parameters: " + constructor);
} else {
continue nextConstructor;
}
}
if (!mappedArguments.isEmpty()) {
if (mappedConstructor != null) {
throw new IllegalArgumentException(
"Ambiguous mapped constructor: " + constructor);
} else {
mappedConstructor = new MappedConstructor(constructor,
mappedArguments);
}
}
}
}
if (mappedConstructor != null) {
mappedClass.setMappedConstructor(mappedConstructor);
} else if (defaultConstructor != null) {
mappedClass.setMappedConstructor(defaultConstructor);
}
// else {
// // TODO return false?
// }
}
private void collectDynamicFieldProperties(Class<?> clazz, MappedClass mappedClass) {
if (!clazz.isInterface()) {
for (Field field : clazz.getDeclaredFields()) {
FieldProperty property = new FieldProperty(field, mappedClass);
if (property.isDynamic()) {
property.resolve(null);
if (!property.isMap()) {
throw new IllegalArgumentException(
"Only properties type of java.util.Map, can be annotated with @Properties");
}
if (!UID.class.equals(property.getKeyType())) {
throw new IllegalArgumentException(
"Key must be type of com.mysema.rdfbean.model.UID");
} else {
mappedClass.addDynamicProperty(property);
}
}
}
}
}
private void collectFieldPaths(Class<?> clazz, MappedClass mappedClass) {
if (!clazz.isInterface()) {
MappedPath path;
String classNs = mappedClass.getClassNs();
for (Field field : clazz.getDeclaredFields()) {
path = getPathMapping(classNs, field, mappedClass);
if (path != null) {
mappedClass.addMappedPath(path);
}
}
}
}
private void collectMethodPaths(Class<?> clazz, MappedClass mappedClass) {
String classNs = mappedClass.getClassNs();
for (Method method : clazz.getDeclaredMethods()) {
MappedPath path = getPathMapping(classNs, method, mappedClass);
if (path != null) {
mappedClass.addMappedPath(path);
}
}
}
public MappedClass getMappedClass(Class<?> clazz) {
// NOTE: no need to further synchronize access to mappedPaths because
// result is immutable and deterministic, i.e. it does't really matter
// if it gets calculated multiple times
MappedClass mappedClass = mappedClasses.get(clazz);
if (mappedClass == null) {
UID uid = getUID(clazz);
Context context = clazz.getAnnotation(Context.class);
List<MappedClass> superclasses = getMappedSuperClasses(clazz);
mappedClass = new MappedClass(clazz, uid, context != null ? new UID(context.value()) : null, superclasses);
if (!clazz.isEnum()) {
for (MappedClass mappedSuperClass : mappedClass.getMappedSuperClasses()) {
if (mappedSuperClass != null) {
for (MappedPath path : mappedSuperClass.getProperties()) {
MappedProperty<?> property = (MappedProperty<?>) path.getMappedProperty().clone();
property.resolve(mappedClass);
mappedClass.addMappedPath(new MappedPath(property,
path.getPredicatePath(), !mappedClass.equals(property.getDeclaringClass())));
}
}
}
// Collect direct properties (merge with super properties)
collectFieldPaths(clazz, mappedClass);
collectMethodPaths(clazz, mappedClass);
collectDynamicFieldProperties(clazz, mappedClass);
assignConstructor(clazz, mappedClass);
mappedClass.close();
mappedClasses.put(clazz, mappedClass);
}
}
return mappedClass;
}
@Nullable
private MappedPath getMappedPath(MappedProperty<?> property, @Nullable List<MappedPredicate> path) {
property.resolve(null);
if (path != null) {
return new MappedPath(property, path, false);
} else {
if (property.isAnnotatedProperty()) {
return new MappedPath(property, Collections.<MappedPredicate> emptyList(), false);
} else {
return null;
}
}
}
private List<MappedClass> getMappedSuperClasses(Class<?> clazz) {
Class<?> superClass = clazz.getSuperclass();
Class<?>[] ifaces = clazz.getInterfaces();
List<MappedClass> mappedSuperClasses = new ArrayList<MappedClass>(ifaces != null ? ifaces.length + 1 : 1);
if (superClass != null && !Object.class.equals(superClass)) {
if (isProcessedClass(superClass)) {
mappedSuperClasses.add(getMappedClass(superClass));
}
}
if (ifaces != null) {
for (Class<?> iface : ifaces) {
if (isProcessedClass(iface)) {
mappedSuperClasses.add(getMappedClass(iface));
}
}
}
return mappedSuperClasses;
}
@Nullable
private MappedPath getPathMapping(MappedClass mappedClass, Constructor<?> constructor, int parameterIndex, String property) {
boolean reference = mappedClass.hasProperty(property);
ConstructorParameter constructorParameter = new ConstructorParameter(constructor, parameterIndex, mappedClass, property, reference);
if (mappedClass.hasProperty(property)) {
return mappedClass.getMappedPath(property);
} else {
List<MappedPredicate> path = getPredicatePath(mappedClass.getClassNs(), constructorParameter);
return getMappedPath(constructorParameter, path);
}
}
private MappedPath getPathMapping(String classNs, Field field, MappedClass declaringClass) {
FieldProperty property = new FieldProperty(field, declaringClass);
List<MappedPredicate> path = getPredicatePath(classNs, property);
return getMappedPath(property, path);
}
@Nullable
private MappedPath getPathMapping(String classNs, Method method, MappedClass declaringClass) {
MethodProperty property = MethodProperty.getMethodPropertyOrNull(method, declaringClass);
if (property != null) {
List<MappedPredicate> path = getPredicatePath(classNs, property);
return getMappedPath(property, path);
} else {
return null;
}
}
@Nullable
private List<MappedPredicate> getPredicatePath(String classNs, MappedProperty<?> property) {
String parentNs = classNs;
Path path = property.getAnnotation(Path.class);
Predicate[] predicates;
if (path != null) {
if (!Strings.isNullOrEmpty(path.ns())) {
parentNs = path.ns();
}
predicates = path.value();
} else {
Predicate predicate = property.getAnnotation(Predicate.class);
if (predicate != null) {
predicates = new Predicate[] { predicate };
} else {
predicates = null;
}
}
if (predicates != null) {
List<MappedPredicate> predicatePath =
new ArrayList<MappedPredicate>(predicates.length);
boolean first = true;
for (Predicate predicate : predicates) {
predicatePath.add(
new MappedPredicate(parentNs, predicate,
first ? property.getName() : null));
first = false;
}
return predicatePath;
} else {
return null;
}
}
@Nullable
private UID getUID(Class<?> clazz) {
ClassMapping cmap = clazz.getAnnotation(ClassMapping.class);
if (cmap != null) {
String ns = cmap.ns();
if (Strings.isNullOrEmpty(ns)) {
ns = defaultNamespace;
}
String ln = cmap.ln();
if (Strings.isNullOrEmpty(ln)) {
ln = clazz.getSimpleName();
}
if (ns != null) {
return new UID(ns, ln);
} else {
throw new IllegalArgumentException("Namespace needs to be declared in ClassMapping or configuration.");
}
} else {
// NOTE : might be used for autowire etc, doesn't need ClassMapping
// for such cases
return null;
}
}
private boolean isProcessedClass(Class<?> clazz) {
Package pack = clazz.getPackage();
return pack == null || !pack.getName().startsWith("java");
}
}