/*
* JBoss, Home of Professional Open Source
* Copyright 2009, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.cdk.templatecompiler.el.types;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.Behavior;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.event.FacesEvent;
import javax.faces.model.DataModel;
import javax.faces.render.Renderer;
import javax.faces.validator.Validator;
import org.richfaces.cdk.CdkClassLoader;
import org.richfaces.cdk.Logger;
import org.richfaces.cdk.templatecompiler.el.ParsingException;
import org.richfaces.cdk.util.ArrayUtils;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
/**
* @author Nick Belaevski
* @author Lukas Fryc
*
*/
public final class TypesFactoryImpl implements TypesFactory {
static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTORS = new PropertyDescriptor[0];
static final ImmutableMap<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_CLASSES_MAP = ImmutableMap.<Class<?>, Class<?>>builder()
.put(Boolean.TYPE, Boolean.class).put(Float.TYPE, Float.class).put(Long.TYPE, Long.class)
.put(Integer.TYPE, Integer.class).put(Short.TYPE, Short.class).put(Byte.TYPE, Byte.class)
.put(Double.TYPE, Double.class).put(Character.TYPE, Character.class).put(Void.TYPE, Void.class).build();
static final ImmutableMap<String, Class<?>> PRIMITIVE_CLASSES_MAP;
static {
Builder<String, Class<?>> builder = ImmutableMap.<String, Class<?>>builder();
for (Class<?> primitiveClass : PRIMITIVE_TO_WRAPPER_CLASSES_MAP.keySet()) {
builder.put(primitiveClass.getName(), primitiveClass);
}
PRIMITIVE_CLASSES_MAP = builder.build();
}
private static final Function<Class<?>, String> PACKAGE_NAME_FUNCTION = new Function<Class<?>, String>() {
@Override
public String apply(Class<?> from) {
return from.getPackage().getName();
}
};
private static final ImmutableCollection<String> GUESS_PACKAGES = ImmutableSet.<String>copyOf(Iterables.transform(
ImmutableSet.<Class<?>>of(UIComponent.class, Behavior.class, Converter.class, Validator.class, FacesContext.class,
Application.class, FacesEvent.class, DataModel.class, Renderer.class, Collection.class, Object.class),
PACKAGE_NAME_FUNCTION));
private final ClassLoader classLoader;
private final Logger log;
private final Map<java.lang.reflect.Type, ELType> reflectionTypesCache = new ConcurrentHashMap<java.lang.reflect.Type, ELType>();
private final Map<String, ELType> refferencedTypesCache = new ConcurrentHashMap<String, ELType>();
private final Map<Class<?>, ClassDataHolder> classDataCache = Maps.newHashMap();
private final TypeParserFactory typeParserFactory;
@Inject
public TypesFactoryImpl(Logger log, CdkClassLoader classLoader, TypeParserFactory typeParserFactory) {
this.log = log;
this.classLoader = classLoader;
this.typeParserFactory = typeParserFactory;
}
private ELType getPlainClassType(Class<?> plainClass) {
ELType plainClassType = reflectionTypesCache.get(plainClass);
if (plainClassType == null) {
plainClassType = new PlainClassType(plainClass);
reflectionTypesCache.put(plainClass, plainClassType);
}
return plainClassType;
}
private ELType getReferencedType(String classCodeString) {
ELType type = refferencedTypesCache.get(classCodeString);
if (type == null) {
type = new ReferencedType(classCodeString);
refferencedTypesCache.put(classCodeString, type);
}
return type;
}
private Class<?> tryLoadClas(String className) throws ClassNotFoundException {
int dotIndex = className.indexOf('.');
if (dotIndex < 0) {
// guess type
for (String guessPackage : GUESS_PACKAGES) {
String guessTypeName = guessPackage + "." + className;
try {
// while by default initialize = true for Class.forName(String) method
// initialize = false used here prevents loading of dependencies that
// are accessible only in runtime, e.g. log initializer from API
// depends on the concrete logger implementation provided in runtime only
return Class.forName(guessTypeName, false, classLoader);
} catch (ClassNotFoundException e) {
// ignore
} catch (LinkageError e) {
if (log.isInfoEnabled()) {
log.info(MessageFormat.format("Class {0} couldn''t be loaded because of: {1}", guessTypeName,
e.getMessage()));
}
}
}
}
Class<?> result = PRIMITIVE_CLASSES_MAP.get(className);
if (result == null) {
try {
// initialize = false here for the same reason as already mentioned for the previous load block
result = Class.forName(className, false, classLoader);
} catch (LinkageError e) {
String errorMessage = MessageFormat.format("Class {0} couldn''t be loaded because of: {1}", className,
e.getMessage());
if (log.isInfoEnabled()) {
log.info(errorMessage);
}
throw new ClassNotFoundException(errorMessage, e);
}
}
return result;
}
/*
* (non-Javadoc)
*
* @see org.richfaces.cdk.templatecompiler.el.types.TypesFactory#getType(java.lang.String)
*/
@Override
public ELType getType(String typeString) {
TypeParser typeParser = typeParserFactory.getInstance(typeString);
if (typeParser.isParseable()) {
final String className = typeParser.getClassName();
ELType baseType;
try {
// NB: loadedClass can have name that differs from className!
Class<?> loadedClas = tryLoadClas(className);
baseType = getType(loadedClas);
} catch (ClassNotFoundException e) {
baseType = getReferencedType(className);
}
if (typeParser.isArray()) {
return new ComplexType(baseType, typeParser.getTypeArguments(), typeParser.getArrayDepth());
} else {
return baseType;
}
} else {
if (log.isWarnEnabled()) {
log.warn(MessageFormat.format("Cannot parse type signature: ''{0}''", typeString));
}
return getReferencedType(typeString);
}
}
@Override
public ELType getGeneratedType(String typeString, ELType superType) {
TypeParser typeParser = typeParserFactory.getInstance(typeString);
if (typeParser.isParseable()) {
final String className = typeParser.getClassName();
ELType generatedType = new GeneratedType(className, superType);
if (typeParser.isArray()) {
return new ComplexType(generatedType, typeParser.getTypeArguments(), typeParser.getArrayDepth());
} else {
return generatedType;
}
} else {
if (log.isWarnEnabled()) {
log.warn(MessageFormat.format("Cannot parse type signature: ''{0}''", typeString));
}
return getReferencedType(typeString);
}
}
ELType createType(java.lang.reflect.Type reflectionType) {
java.lang.reflect.Type[] actualTypeArguments = null;
Class<?> rawType = null;
int arrayDepth = 0;
java.lang.reflect.Type localReflectionType = reflectionType;
while (localReflectionType instanceof GenericArrayType) {
localReflectionType = ((GenericArrayType) localReflectionType).getGenericComponentType();
arrayDepth++;
}
if (localReflectionType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) localReflectionType;
actualTypeArguments = parameterizedType.getActualTypeArguments();
rawType = (Class<?>) parameterizedType.getRawType();
} else if (localReflectionType instanceof Class<?>) {
rawType = (Class<?>) localReflectionType;
}
if (rawType != null) {
while (rawType.isArray()) {
arrayDepth++;
rawType = rawType.getComponentType();
}
ELType[] typeArguments = PlainClassType.NO_TYPES;
if (!ArrayUtils.isEmpty(actualTypeArguments)) {
typeArguments = getTypesArray(actualTypeArguments);
}
ELType clearComponentType = getPlainClassType(rawType);
if (!ArrayUtils.isEmpty(typeArguments) || arrayDepth != 0) {
return new ComplexType(clearComponentType, typeArguments, arrayDepth);
} else {
return clearComponentType;
}
} else {
// TODO better way of handling unknown types
return getReferencedType(reflectionType.toString());
}
}
/*
* (non-Javadoc)
*
* @see org.richfaces.cdk.templatecompiler.el.types.TypesFactory#getType(java.lang.reflect.Type)
*/
@Override
public ELType getType(java.lang.reflect.Type reflectionType) {
ELType result = reflectionTypesCache.get(reflectionType);
if (result == null) {
result = createType(reflectionType);
reflectionTypesCache.put(reflectionType, result);
}
return result;
}
private ELType[] getTypesArray(java.lang.reflect.Type[] reflectionTypes) {
ELType[] types = new ELType[reflectionTypes.length];
for (int i = 0; i < reflectionTypes.length; i++) {
types[i] = getType(reflectionTypes[i]);
}
return types;
}
/**
* Returns wrapper classes for passed-in class. If type is primitive, then corresponding wrapper class is returned (e.g.
* boolean -> Boolean), otherwise does nothing and returns passed-in class.
*
* @return wrapper for primitive types, or passed-in class
*/
static Class<?> getWrapperClass(Class<?> inClazz) {
if (inClazz.isPrimitive()) {
return PRIMITIVE_TO_WRAPPER_CLASSES_MAP.get(inClazz);
} else {
return inClazz;
}
}
private static final class JavaELPropertyDescriptor implements ELPropertyDescriptor {
private final PropertyDescriptor descriptor;
private final String descriptorName;
private final ELType propertyType;
/**
* <p class="changed_added_4_0">
* </p>
*
* @param descriptor
* @param propertyType
* @param descriptorName
*/
public JavaELPropertyDescriptor(PropertyDescriptor descriptor, ELType propertyType) {
this.descriptor = descriptor;
this.propertyType = propertyType;
this.descriptorName = descriptor.getName();
}
@Override
public boolean isWritable() {
return null != descriptor.getWriteMethod();
}
@Override
public boolean isReadable() {
return null != descriptor.getReadMethod();
}
@Override
public String getWriteMethosName() {
return descriptor.getWriteMethod().getName();
}
@Override
public ELType getType() {
return propertyType;
}
@Override
public String getReadMethodName() {
return descriptor.getReadMethod().getName();
}
@Override
public String getName() {
return descriptorName;
}
}
private static final class ClassDataHolder implements ClassVisitor {
private Map<String, ELPropertyDescriptor> resolvedProperties;
private List<Method> resolvedMethods;
private final TypesFactory typesFactory;
public ClassDataHolder(TypesFactory typesFactory) {
super();
this.typesFactory = typesFactory;
this.resolvedProperties = Maps.newHashMap();
this.resolvedMethods = Lists.newArrayList();
}
public Map<String, ELPropertyDescriptor> getResolvedProperties() {
return resolvedProperties;
}
public List<Method> getResolvedMethods() {
return resolvedMethods;
}
@Override
public void visit(Class<?> clazz) throws ParsingException {
PropertyDescriptor[] pds;
Method[] declaredMethods;
try {
pds = getPropertyDescriptors(clazz);
declaredMethods = clazz.getDeclaredMethods();
} catch (LinkageError e) {
throw new ParsingException(e.getMessage(), e);
}
for (PropertyDescriptor descriptor : pds) {
String descriptorName = descriptor.getName();
if (resolvedProperties.get(descriptorName) == null) {
Type reflectionType;
if (null != descriptor.getReadMethod()) {
reflectionType = descriptor.getReadMethod().getGenericReturnType();
} else if (null != descriptor.getWriteMethod()) {
reflectionType = descriptor.getWriteMethod().getGenericParameterTypes()[0];
} else {
reflectionType = descriptor.getPropertyType();
}
ELType propertyType = typesFactory.getType(reflectionType);
ELPropertyDescriptor elDescriptor = new JavaELPropertyDescriptor(descriptor, propertyType);
resolvedProperties.put(descriptorName, elDescriptor);
}
}
resolvedMethods.addAll(Arrays.asList(declaredMethods));
}
}
interface ClassVisitor {
void visit(Class<?> clazz) throws ParsingException;
}
static class ClassWalkingLogic {
private Queue<Class<?>> classesList = new LinkedList<Class<?>>();
private Set<Class<?>> visitedClasses = new HashSet<Class<?>>();
public ClassWalkingLogic(Class<?> clazz) {
super();
this.classesList.add(clazz);
}
public void walk(ClassVisitor visitor) throws ParsingException {
// BFS algorithm
while (!classesList.isEmpty()) {
Class<?> clazz = classesList.remove();
if (visitedClasses.add(clazz)) {
visitor.visit(clazz);
Class<?> superclass = clazz.getSuperclass();
if (superclass != null) {
if (!visitedClasses.contains(superclass)) {
classesList.add(superclass);
}
}
Class<?>[] interfaces = clazz.getInterfaces();
if (interfaces != null) {
for (Class<?> iface : interfaces) {
if (!visitedClasses.contains(iface)) {
classesList.add(iface);
}
}
}
}
}
// While interfaces do not have Object.class in their hierarchy directly,
// implementations of interface are always inherited from Object.
// As methods in this class are primarily designed to work with implementations (beans),
// we are adding Object.class explicitly if it hasn't been visited yet.
if (visitedClasses.add(Object.class)) {
visitor.visit(Object.class);
}
visitedClasses.clear();
}
}
private ClassDataHolder resolveClassPropertiesAndMethods(Class<?> clazz) throws ParsingException {
ClassDataHolder classDataHolder = classDataCache.get(clazz);
if (classDataHolder == null) {
classDataHolder = new ClassDataHolder(this);
new ClassWalkingLogic(clazz).walk(classDataHolder);
classDataCache.put(clazz, classDataHolder);
}
return classDataHolder;
}
/**
* This method return PropertyDescriptor by specified propertyName and clazz.
*
* @param elType - class to search
* @param propertyName - propertyName to search
* @return property descriptor if found.
* @throws ParsingException if error occured.
*/
public ELPropertyDescriptor getPropertyDescriptor(ELType elType, String propertyName) throws ParsingException {
ELPropertyDescriptor propertyDescriptor;
if (elType == null) {
propertyDescriptor = new DummyPropertyDescriptor(propertyName);
} else {
ClassDataHolder classDataHolder = resolveClassPropertiesAndMethods(getClassFromType(elType));
Map<String, ELPropertyDescriptor> resolvedProperties = classDataHolder.getResolvedProperties();
if (resolvedProperties.containsKey(propertyName)) {
propertyDescriptor = resolvedProperties.get(propertyName);
} else {
propertyDescriptor = new DummyPropertyDescriptor(propertyName);
}
}
return propertyDescriptor;
}
/**
* <p>
* Retrieve the property descriptors for the specified class, introspecting and caching them the first time a particular
* bean class is encountered.
* </p>
*
* <p>
* <strong>FIXME</strong> - Does not work with DynaBeans.
* </p>
*
* @param beanClass Bean class for which property descriptors are requested
* @return the property descriptors
* @throws ParsingException if error occured.
*
* @exception IllegalArgumentException if <code>beanClass</code> is null
*/
private static PropertyDescriptor[] getPropertyDescriptors(Class<?> beanClass) throws ParsingException {
if (beanClass == null) {
throw new IllegalArgumentException("No bean class specified");
}
// Look up any cached descriptors for this bean class
PropertyDescriptor[] descriptors = null;
// Introspect the bean and cache the generated descriptors
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(beanClass);
descriptors = beanInfo.getPropertyDescriptors();
} catch (IntrospectionException e) {
return EMPTY_PROPERTY_DESCRIPTORS;
}
if (descriptors == null) {
descriptors = EMPTY_PROPERTY_DESCRIPTORS;
}
return descriptors;
}
private static boolean isMethodVisible(Method method) {
return !Modifier.isPrivate(method.getModifiers());
}
private static Class<?> getClassFromType(ELType elType) {
if (elType == null) {
return Object.class;
} else if (elType instanceof PlainClassType) {
Class<?> clazz = ((PlainClassType) elType).getPlainJavaClass();
return clazz;
} else if (elType instanceof ComplexType) {
return getClassFromType(elType.getRawType());
}
return Object.class;
}
/**
* <p>
* Find an accessible method that matches the given name and has compatible parameters. Compatible parameters mean that
* every method parameter is assignable from the given parameters. In other words, it finds a method with the given name
* that will take the parameters given.
* <p>
*
* <p>
* This method is slightly undeterminstic since it loops through methods names and return the first matching method.
* </p>
*
* <p>
* This method is used by {@link #invokeMethod(Object object, String methodName, Object [] args, Class[] parameterTypes)}.
*
* <p>
* This method can match primitive parameter by passing in wrapper classes. For example, a <code>Boolean</code> will match a
* primitive <code>boolean</code> parameter.
*
* @param elType find method in this class
* @param methodName find method with this name
* @param parameterTypes find method with compatible parameters
* @return The accessible method
* @throws ParsingException if error occured.
*/
public ELType getMatchingVisibleMethodReturnType(ELType elType, final String methodName, ELType... parameterTypes)
throws ParsingException {
if (elType instanceof GeneratedType) {
// use the super type to resolve properties and methods
elType = ((GeneratedType) elType).getSuperType();
}
ClassDataHolder classDataHolder = resolveClassPropertiesAndMethods(getClassFromType(elType));
List<Method> resolvedMethods = classDataHolder.getResolvedMethods();
// search through all methods
int paramSize = parameterTypes.length;
Method bestMatch = null;
for (Method resolvedMethod : resolvedMethods) {
if (isMethodVisible(resolvedMethod) && resolvedMethod.getName().equals(methodName)) {
// compare parameters
ELType[] methodsParams = getTypesArray(resolvedMethod.getParameterTypes());
int methodParamSize = methodsParams.length;
if (methodParamSize == paramSize) {
boolean match = true;
for (int n = 0; n < methodParamSize; n++) {
if (!methodsParams[n].isAssignableFrom(parameterTypes[n])) {
match = false;
break;
}
}
if (match) {
bestMatch = resolvedMethod;
}
}
}
}
if (bestMatch != null) {
return getType(bestMatch.getGenericReturnType());
} else {
return TypesFactory.OBJECT_TYPE;
}
}
}