package org.jboss.seam.remoting;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import org.jboss.logging.Logger;
import org.jboss.seam.remoting.BeanMetadata.BeanType;
import org.jboss.seam.remoting.annotations.WebRemote;
/**
* Caches BeanMetadata instances
*
* @author Shane Bryzak
*/
@ApplicationScoped
public class MetadataCache
{
private static final Logger log = Logger.getLogger(MetadataCache.class);
private Map<Class<?>,BeanMetadata> metadataCache;
private Map<Class<?>, Set<Class<?>>> beanDependencies;
/**
* A cache of the accessible properties for a bean class
*/
private Map<Class<?>, Map<String,Type>> accessibleProperties = new HashMap<Class<?>, Map<String,Type>>();
@Inject BeanManager beanManager;
public MetadataCache()
{
metadataCache = new HashMap<Class<?>, BeanMetadata>();
beanDependencies = new HashMap<Class<?>, Set<Class<?>>>();
}
public BeanMetadata getMetadata(Class<?> beanClass)
{
if (beanClass == null) throw new IllegalArgumentException("beanClass cannot be null");
if (metadataCache.containsKey(beanClass))
{
return metadataCache.get(beanClass);
}
else
{
synchronized(metadataCache)
{
if (!metadataCache.containsKey(beanClass))
{
metadataCache.put(beanClass, generateBeanMetadata(beanClass));
Set<Class<?>> dependencies = beanDependencies.get(beanClass);
if (dependencies != null)
{
for (Class<?> dependency : dependencies)
{
getMetadata(dependency);
}
}
}
}
return metadataCache.get(beanClass);
}
}
@WebRemote
public Set<BeanMetadata> loadBeans(Set<String> names)
{
Set<BeanMetadata> meta = new HashSet<BeanMetadata>();
for (String name : names)
{
Class<?> beanClass = null;
Set<Bean<?>> beans = beanManager.getBeans(name);
if (!beans.isEmpty())
{
beanClass = beans.iterator().next().getBeanClass();
}
else
{
try
{
beanClass = Class.forName(name);
}
catch (ClassNotFoundException ex)
{
log.error(String.format("Component not found: [%s]", name));
throw new IllegalArgumentException(String.format("Component not found: [%s]", name));
}
}
addBeanDependencies(beanClass, meta);
}
return meta;
}
private void addBeanDependencies(Class<?> beanClass, Set<BeanMetadata> types)
{
BeanMetadata meta = getMetadata(beanClass);
if (types.contains(meta)) return;
types.add(meta);
Set<Class<?>> dependencies = getDependencies(beanClass);
if (dependencies != null)
{
for (Class<?> dependencyClass : dependencies)
{
if (!types.contains(dependencyClass))
{
addBeanDependencies(dependencyClass, types);
}
}
}
}
private BeanMetadata generateBeanMetadata(Class<?> beanClass)
{
BeanType beanType = BeanType.state;
String name = beanClass.getName();
// If any of the methods are annotated with @WebRemote, it's an action bean
for (Method m : beanClass.getDeclaredMethods())
{
if (m.getAnnotation(WebRemote.class) != null)
{
beanType = BeanType.action;
String beanName = beanManager.getBeans(beanClass).iterator().next().getName();
if (beanName != null)
{
name = beanName;
}
break;
}
}
BeanMetadata meta = new BeanMetadata(beanType, name);
if (beanType == BeanType.state)
{
populateMetadataProperties(beanClass, meta);
}
else
{
populateMetadataMethods(beanClass, meta);
}
return meta;
}
private void populateMetadataProperties(Class<?> beanClass, BeanMetadata meta)
{
Map<String,Type> props = getAccessibleProperties(beanClass);
for (String propertyName : props.keySet())
{
Type propertyType = props.get(propertyName);
meta.addProperty(propertyName, getFieldType(propertyType));
addTypeDependency(beanClass, propertyType);
}
}
private void populateMetadataMethods(Class<?> beanClass, BeanMetadata meta)
{
for (Method m : beanClass.getDeclaredMethods())
{
if (m.getAnnotation(WebRemote.class) == null) continue;
meta.addMethod(m.getName(), m.getParameterTypes().length);
addTypeDependency(beanClass, m.getGenericReturnType());
for (int i = 0; i < m.getGenericParameterTypes().length; i++)
{
addTypeDependency(beanClass, m.getGenericParameterTypes()[i]);
}
}
}
private void addTypeDependency(Class<?> beanType, Type dependency)
{
if (!beanDependencies.containsKey(beanType))
{
beanDependencies.put(beanType, new HashSet<Class<?>>());
}
Set<Class<?>> dependencies = beanDependencies.get(beanType);
if (dependencies.contains(dependency)) return;
if (dependency instanceof Class<?>)
{
Class<?> classType = (Class<?>) dependency;
if (classType.isArray())
{
addTypeDependency(beanType, classType.getComponentType());
return;
}
if (classType.getName().startsWith("java.") || classType.isPrimitive()
|| classType.isEnum())
{
return;
}
dependencies.add(classType);
}
else if (dependency instanceof ParameterizedType)
{
for (Type t : ((ParameterizedType) dependency).getActualTypeArguments())
{
addTypeDependency(beanType, t);
}
}
}
/**
* Returns the metadata for the specified bean type and all of its reachable
* dependencies
*
* @param beanType
* @return
*/
public Set<Class<?>> getDependencies(Class<?> beanType)
{
return beanDependencies.get(beanType);
}
/**
* Returns the remoting "type" for a specified class.
*
* @param type Type The type for which to return the remoting type.
* @return String The remoting type for the specified type.
*/
protected String getFieldType(Type type)
{
if (type.equals(String.class)
|| (type instanceof Class<?> && ((Class<?>) type).isEnum())
|| type.equals(BigInteger.class) || type.equals(BigDecimal.class))
{
return "str";
}
else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE))
{
return "bool";
}
else if (type.equals(Short.class) || type.equals(Short.TYPE)
|| type.equals(Integer.class) || type.equals(Integer.TYPE)
|| type.equals(Long.class) || type.equals(Long.TYPE)
|| type.equals(Float.class) || type.equals(Float.TYPE)
|| type.equals(Double.class) || type.equals(Double.TYPE)
|| type.equals(Byte.class) || type.equals(Byte.TYPE))
{
return "number";
}
else if (type instanceof Class<?>)
{
Class<?> cls = (Class<?>) type;
if (Date.class.isAssignableFrom(cls)
|| Calendar.class.isAssignableFrom(cls))
{
return "date";
}
else if (cls.isArray())
{
return "bag";
}
else if (cls.isAssignableFrom(Map.class))
{
return "map";
}
else if (cls.isAssignableFrom(Collection.class))
{
return "bag";
}
}
else if (type instanceof ParameterizedType)
{
ParameterizedType pt = (ParameterizedType) type;
if (pt.getRawType() instanceof Class<?>
&& Map.class.isAssignableFrom((Class<?>) pt.getRawType()))
{
return "map";
}
else if (pt.getRawType() instanceof Class<?>
&& Collection.class.isAssignableFrom((Class<?>) pt.getRawType()))
{
return "bag";
}
}
return "bean";
}
/**
* A helper method, used internally by InterfaceGenerator and also when
* serializing responses. Returns a list of the property names for the
* specified class which should be included in the generated interface for
* the type.
*
* @param cls Class The class to scan for accessible properties
* @return Set<String> A Set containing the accessible property names
*/
public Map<String,Type> getAccessibleProperties(Class<?> cls)
{
if (cls.getName().contains("EnhancerByCGLIB"))
cls = cls.getSuperclass();
if (!accessibleProperties.containsKey(cls))
{
synchronized (accessibleProperties)
{
if (!accessibleProperties.containsKey(cls))
{
Map<String, Type> properties = new HashMap<String, Type>();
Class<?> c = cls;
while (c != null && !c.equals(Object.class))
{
// Scan the declared fields
for (Field f : c.getDeclaredFields())
{
// If we already have this field, continue processing
if (properties.containsKey(f.getName()))
{
continue;
}
// Don't include transient or static fields
if (!Modifier.isTransient(f.getModifiers())
&& !Modifier.isStatic(f.getModifiers()))
{
if (Modifier.isPublic(f.getModifiers()))
{
properties.put(f.getName(), f.getGenericType());
}
else
{
// Look for a public getter method
String fieldName = f.getName().substring(0, 1).toUpperCase()
+ f.getName().substring(1);
String getterName = String.format("get%s", fieldName);
Method getMethod = null;
try
{
getMethod = c.getMethod(getterName);
}
catch (SecurityException ex) { }
catch (NoSuchMethodException ex)
{
// it might be an "is" method...
getterName = String.format("is%s", fieldName);
try
{
getMethod = c.getMethod(getterName);
}
catch (NoSuchMethodException ex2) { }
}
if (getMethod != null && Modifier.isPublic(getMethod.getModifiers()))
{
properties.put(f.getName(), getMethod.getGenericReturnType());
}
else
{
// Last resort, look for a public setter method
String setterName = String.format("set%s", fieldName);
Method setMethod = null;
try
{
setMethod = c.getMethod(setterName, new Class[] { f.getType() });
}
catch (SecurityException ex) { }
catch (NoSuchMethodException ex) { }
if (setMethod != null && Modifier.isPublic(setMethod.getModifiers()))
{
properties.put(f.getName(), setMethod.getGenericParameterTypes()[0]);
}
}
}
}
}
// Scan the declared methods
for (Method m : c.getDeclaredMethods())
{
if (m.getName().startsWith("get") || m.getName().startsWith("is"))
{
int startIdx = m.getName().startsWith("get") ? 3 : 2;
try
{
c.getMethod(String.format("set%s", m.getName()
.substring(startIdx)), m.getReturnType());
}
catch (NoSuchMethodException ex)
{
// If there is no setter method, ignore and continue processing
continue;
}
String propertyName = String.format("%s%s", Character
.toLowerCase(m.getName().charAt(startIdx)), m
.getName().substring(startIdx + 1));
if (!properties.containsKey(propertyName))
{
properties.put(propertyName, m.getGenericReturnType());
}
}
}
// Recursively scan the class hierarchy
c = c.getSuperclass();
}
accessibleProperties.put(cls, properties);
}
}
}
return accessibleProperties.get(cls);
}
}