package com.softwaremill.common.cdi.objectservice.extension;
import org.jboss.weld.util.reflection.HierarchyDiscovery;
import org.jboss.weld.util.reflection.ParameterizedTypeImpl;
import com.softwaremill.common.cdi.objectservice.OS;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.*;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashSet;
import java.util.Set;
/**
* @author Adam Warski (adam at warski dot org)
*/
public class ObjectServiceExtension implements Serializable, Extension {
/*
An object service is a class which implements the OS interface, and its type parameter can be resolved
to a class.
Registration algorithm:
1. Find annotated types which implement OS.
2. Determine the exact type T of the type parameter. Proceed if it is a concrete class (not a wildcard, abstract
class, etc.)
3. Register a bean with type: OSP<T, OS<T>>, which provides the found service.
Lookup algorithm:
1. Given an object, search for the best object service specification (T, OS<T> pair)
2. Create a new instance of a bean which implements the service
3. Set the object using OS.setServiced()
*/
// For some reason @Inject doesn't work, the field is assigned later
private BeanManager beanManager;
private Set<ObjectServiceSpecification> objectServicesSpecs = new HashSet<ObjectServiceSpecification>();
private Set<ObjectServiceSpecification> objectServicesProviderSpecs = new HashSet<ObjectServiceSpecification>();
private Type getObjectServiceTypeParameter(Iterable<Type> flattenedTypes) {
for (Type flattenedType : flattenedTypes) {
if (flattenedType instanceof ParameterizedType && ((ParameterizedType) flattenedType).getRawType().equals(OS.class)) {
return ((ParameterizedType) flattenedType).getActualTypeArguments()[0];
}
}
return null;
}
public Set<Type> getInterfaceClosure(Class<?> rawType) {
Set<Type> types = new HashSet<Type>();
for (Type t : rawType.getGenericInterfaces()) {
types.addAll(new HierarchyDiscovery(t).getTypeClosure());
}
return types;
}
/**
* Looks for the type parameter of the given annotated type, which has a raw type {@link com.softwaremill.common.cdi.objectservice.OS}.
* @param at The class on which to look for the type parameter.
* @return The type parameter or {@code null}, if no type parameter is specified.
*/
private Type getObjectServiceTypeParameter(AnnotatedType<?> at) {
Type typeParameter = getObjectServiceTypeParameter(getInterfaceClosure(at.getJavaClass()));
if (typeParameter != null) {
return typeParameter;
} else {
return getObjectServiceTypeParameter(at.getTypeClosure());
}
}
private Type[] createWildcards(int count) {
Type[] ret = new Type[count];
for (int i=0; i<count; i++) {
ret[i] = PureWildcardType.INSTANCE;
}
return ret;
}
public <X> void processAnnotatedType(@Observes ProcessAnnotatedType<X> event) {
AnnotatedType<X> at = event.getAnnotatedType();
// Checking if the class implements the OS interface
if (OS.class.isAssignableFrom(at.getJavaClass())) {
// Getting the type parameter type parameter for the OS implementation
Type osTypeParameter = getObjectServiceTypeParameter(at);
// If the type parameter is a type variable, searching for that type variable in the main class declaration.
if (osTypeParameter instanceof TypeVariable) {
String typeVariableName = ((TypeVariable) osTypeParameter).getName();
for (TypeVariable<Class<X>> classTypeVariable : at.getJavaClass().getTypeParameters()) {
if (classTypeVariable.getName().equals(typeVariableName)) {
// Looking at the bound of the variable and if any, assigning it to the found type parameter
if (classTypeVariable.getBounds().length > 0) {
osTypeParameter = classTypeVariable.getBounds()[0];
}
}
}
}
// Checking if the found type variable is a class. If so, registering a new object service.
if (osTypeParameter instanceof Class) {
Class<?> serviceClass = at.getJavaClass();
// Checking if the class directly implements OS.
boolean osFound = false;
for (Class<?> serviceClassInterface : serviceClass.getInterfaces()) {
if (serviceClassInterface.isAssignableFrom(OS.class)) {
osFound = true;
}
}
// If the class has any type parameters, filling them in with wildcards
Type serviceType;
if (serviceClass.getTypeParameters().length > 0) {
serviceType = new ParameterizedTypeImpl(serviceClass, createWildcards(serviceClass.getTypeParameters().length), null);
} else {
serviceType = serviceClass;
}
ObjectServiceSpecification osSpec = new ObjectServiceSpecification((Class<?>) osTypeParameter, serviceType, serviceClass);
// Registering a new spec so that the bean can be found by the provider
objectServicesSpecs.add(osSpec);
if (osFound) {
// Registering a new OSP for class: osTypeParameter and service: serviceType
objectServicesProviderSpecs.add(osSpec);
}
}
}
}
public void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
// Adding OSP beans according to the specifications gathered
for (ObjectServiceSpecification objectServicesSpec : objectServicesProviderSpecs) {
abd.addBean(new ObjectServiceProviderBean(this, objectServicesSpec));
}
// Storing the bean manager
beanManager = bm;
}
/**
* Finds a most specific service specification, given the class of the object and the class of the service.
* @param objClass Class of the object.
* @param serviceClass Class of the service.
* @return A specification of a service for the same object class as the given one, with the most specific
* service class.
*/
private ObjectServiceSpecification serviceSpecForObject(Class<?> objClass, Class<?> serviceClass) {
// TODO: cache results
ObjectServiceSpecification bestSoFar = null;
// Checking all registered specifications
for (ObjectServiceSpecification candidateSpec : objectServicesSpecs) {
// Checking if the checked spec is suitable for the requested one
// Here we allow subclasses of objClass, to make sure that e.g. Hibernate Proxies work. However, this
// will not work if there are object services for non-terminal nodes (non-leaves) in the inheritance tree.
if (candidateSpec.getObjectClass().isAssignableFrom(objClass) &&
serviceClass.isAssignableFrom(candidateSpec.getServiceClass())) {
// Checking if it's better (more specific) than the one currently found
if (bestSoFar == null || bestSoFar.getServiceClass().isAssignableFrom(candidateSpec.getServiceClass())) {
bestSoFar = candidateSpec;
}
// TODO: check if there's no ambiguency (two "best")
}
}
if (bestSoFar == null) {
throw new RuntimeException("No " + serviceClass + " service found for object of class: " + objClass + ".");
}
return bestSoFar;
}
/**
* For the given specification, finds an instance of a bean implementing the given service type. There must be
* exactly one service matching the given type.
* @param spec Specification of the service
* @return An instance of a bean implementing the given service type.
*/
private Object beanFromSpec(ObjectServiceSpecification spec) {
Set<Bean<?>> bestServiceBeans = beanManager.getBeans(spec.getServiceType());
if (bestServiceBeans.size() == 0) {
throw new RuntimeException("No bean found for specification: " + spec);
}
if (bestServiceBeans.size() > 1) {
throw new RuntimeException("Multiple beans (" + bestServiceBeans + ") found for specification:" + spec);
}
// Now we know there's only one
Bean<?> bestServiceBean = bestServiceBeans.iterator().next();
return beanManager.getReference(bestServiceBean,
spec.getServiceType(),
beanManager.createCreationalContext(bestServiceBean));
}
/**
* @param objClass The class of the object
* @param serviceClass The class of the service.
* @return An instance of a bean, which is the most specific implementation of an object service for the given
* object class and service class.
*/
public Object serviceForObject(Class<?> objClass, Class<?> serviceClass) {
ObjectServiceSpecification bestSoFar = serviceSpecForObject(objClass, serviceClass);
return beanFromSpec(bestSoFar);
}
}