package org.jboss.seam.remoting;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
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.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jboss.seam.Component;
import org.jboss.seam.ComponentType;
import org.jboss.seam.Seam;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.remoting.WebRemote;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.servlet.ContextualHttpServletRequest;
import org.jboss.seam.util.EJB;
import org.jboss.seam.util.Reflections;
import org.jboss.seam.web.ServletContexts;
/**
* Generates JavaScript interface code.
*
* @author Shane Bryzak
*/
public class InterfaceGenerator extends BaseRequestHandler implements RequestHandler
{
private static final LogProvider log = Logging.getLogProvider(InterfaceGenerator.class);
/**
* Maintain a cache of the accessible fields
*/
private static Map<Class,Set<String>> accessibleProperties = new HashMap<Class,Set<String>>();
/**
* A cache of component interfaces, keyed by component name.
*/
private Map<String,byte[]> interfaceCache = new HashMap<String,byte[]>();
/**
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @throws Exception
*/
public void handle(final HttpServletRequest request, final HttpServletResponse response)
throws Exception
{
new ContextualHttpServletRequest(request)
{
@Override
public void process() throws Exception
{
ServletContexts.instance().setRequest(request);
if (request.getQueryString() == null)
{
throw new ServletException("Invalid request - no component specified");
}
Set<Component> components = new HashSet<Component>();
Set<Type> types = new HashSet<Type>();
response.setContentType("text/javascript");
Enumeration e = request.getParameterNames();
while (e.hasMoreElements())
{
String componentName = ((String) e.nextElement()).trim();
Component component = Component.forName(componentName);
if (component == null)
{
try
{
Class c = Reflections.classForName(componentName);
appendClassSource(response.getOutputStream(), c, types);
}
catch (ClassNotFoundException ex)
{
log.error(String.format("Component not found: [%s]", componentName));
throw new ServletException("Invalid request - component not found.");
}
}
else
{
components.add(component);
}
}
generateComponentInterface(components, response.getOutputStream(), types);
}
}.run();
}
/**
* Generates the JavaScript code required to invoke the methods of a component/s.
*
* @param components Component[] The components to generate javascript for
* @param out OutputStream The OutputStream to write the generated javascript to
* @throws IOException Thrown if there is an error writing to the OutputStream
*/
public void generateComponentInterface(Set<Component> components, OutputStream out, Set<Type> types)
throws IOException
{
for (Component c : components)
{
if (c != null)
{
if (!interfaceCache.containsKey(c.getName()))
{
synchronized (interfaceCache)
{
if (!interfaceCache.containsKey(c.getName()))
{
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
appendComponentSource(bOut, c, types);
interfaceCache.put(c.getName(), bOut.toByteArray());
}
}
}
out.write(interfaceCache.get(c.getName()));
}
}
}
/**
* 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
* @return List
*/
public static Set<String> getAccessibleProperties(Class cls)
{
/** @todo This is a hack to get the "real" class - find out if there is
an API method in CGLIB that can be used instead */
if (cls.getName().contains("EnhancerByCGLIB"))
cls = cls.getSuperclass();
if (!accessibleProperties.containsKey(cls))
{
synchronized(accessibleProperties)
{
if (!accessibleProperties.containsKey(cls))
{
Set<String> properties = new HashSet<String>();
Class c = cls;
while (c != null && !c.equals(Object.class))
{
for (Field f : c.getDeclaredFields())
{
if (!Modifier.isTransient(f.getModifiers()) &&
!Modifier.isStatic(f.getModifiers()))
{
String fieldName = f.getName().substring(0, 1).toUpperCase() +
f.getName().substring(1);
String getterName = String.format("get%s", fieldName);
String setterName = String.format("set%s", fieldName);
Method getMethod = null;
Method setMethod = 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)
{ /* don't care */}
}
try
{
setMethod = c.getMethod(setterName, new Class[] {f.getType()});
}
catch (SecurityException ex)
{}
catch (NoSuchMethodException ex)
{ /* don't care */}
if (Modifier.isPublic(f.getModifiers()) ||
(getMethod != null &&
Modifier.isPublic(getMethod.getModifiers()) ||
(setMethod != null &&
Modifier.isPublic(setMethod.getModifiers()))))
{
properties.add(f.getName());
}
}
}
//
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)
{
continue;
}
String propertyName = String.format("%s%s",
Character.toLowerCase(m.getName().charAt(startIdx)),
m.getName().substring(startIdx + 1));
if (!properties.contains(propertyName))
properties.add(propertyName);
}
}
c = c.getSuperclass();
}
accessibleProperties.put(cls, properties);
}
}
}
return accessibleProperties.get(cls);
}
/**
* Appends component interface code to an outputstream for a specified component.
*
* @param out OutputStream The OutputStream to write to
* @param component Component The component to generate an interface for
* @param types Set A list of types that have already been generated for this
* request. If this component has already been generated (i.e. it is in the list)
* then it won't be generated again
* @throws IOException If there is an error writing to the OutputStream.
*/
private void appendComponentSource(OutputStream out, Component component, Set<Type> types)
throws IOException
{
StringBuilder componentSrc = new StringBuilder();
Set<Class> componentTypes = new HashSet<Class>();
if (component.getType().isSessionBean() &&
component.getBusinessInterfaces().size() > 0)
{
for (Class c : component.getBusinessInterfaces())
{
// Use the Local interface
if (c.isAnnotationPresent(EJB.LOCAL))
{
componentTypes.add(c);
break;
}
}
if (componentTypes.isEmpty())
throw new RuntimeException(String.format(
"Type cannot be determined for component [%s]. Please ensure that it has a local interface.", component));
}
else if (component.getType().equals(ComponentType.ENTITY_BEAN))
{
appendTypeSource(out, component.getBeanClass(), types);
return;
}
else if (component.getType().equals(ComponentType.JAVA_BEAN))
{
// Check if any of the methods are annotated with @WebRemote, and if so
// treat it as an "action" component instead of a type component
for (Method m : component.getBeanClass().getDeclaredMethods())
{
if (m.getAnnotation(WebRemote.class) != null)
{
componentTypes.add(component.getBeanClass());
break;
}
}
if (componentTypes.isEmpty())
{
appendTypeSource(out, component.getBeanClass(), types);
return;
}
}
else
{
componentTypes.add(component.getBeanClass());
}
// If types already contains all the component types, then return
boolean foundNew = false;
for (Class type : componentTypes)
{
if (!types.contains(type))
{
foundNew = true;
break;
}
}
if (!foundNew) return;
if (component.getName().contains("."))
{
componentSrc.append("Seam.Remoting.createNamespace('");
componentSrc.append(component.getName().substring(0, component.getName().lastIndexOf('.')));
componentSrc.append("');\n");
}
componentSrc.append("Seam.Remoting.type.");
componentSrc.append(component.getName());
componentSrc.append(" = function() {\n");
componentSrc.append(" this.__callback = new Object();\n");
for (Class type : componentTypes)
{
if (types.contains(type))
{
break;
}
else
{
types.add(type);
for (Method m : type.getDeclaredMethods())
{
if (m.getAnnotation(WebRemote.class) == null) continue;
// Append the return type to the source block
appendTypeSource(out, m.getGenericReturnType(), types);
componentSrc.append(" Seam.Remoting.type.");
componentSrc.append(component.getName());
componentSrc.append(".prototype.");
componentSrc.append(m.getName());
componentSrc.append(" = function(");
// Insert parameters p0..pN
for (int i = 0; i < m.getGenericParameterTypes().length; i++)
{
appendTypeSource(out, m.getGenericParameterTypes()[i], types);
if (i > 0) componentSrc.append(", ");
componentSrc.append("p");
componentSrc.append(i);
}
if (m.getGenericParameterTypes().length > 0) componentSrc.append(", ");
componentSrc.append("callback, exceptionHandler) {\n");
componentSrc.append(" return Seam.Remoting.execute(this, \"");
componentSrc.append(m.getName());
componentSrc.append("\", [");
for (int i = 0; i < m.getParameterTypes().length; i++)
{
if (i > 0) componentSrc.append(", ");
componentSrc.append("p");
componentSrc.append(i);
}
componentSrc.append("], callback, exceptionHandler);\n");
componentSrc.append(" }\n");
}
}
componentSrc.append("}\n");
// Set the component name
componentSrc.append("Seam.Remoting.type.");
componentSrc.append(component.getName());
componentSrc.append(".__name = \"");
componentSrc.append(component.getName());
componentSrc.append("\";\n\n");
// Register the component
componentSrc.append("Seam.Component.register(Seam.Remoting.type.");
componentSrc.append(component.getName());
componentSrc.append(");\n\n");
out.write(componentSrc.toString().getBytes());
}
}
/**
* Append Javascript interface code for a specified class to a block of code.
*
* @param source StringBuilder The code block to append to
* @param type Class The type to generate a Javascript interface for
* @param types Set A list of the types already generated (only include each type once).
*/
private void appendTypeSource(OutputStream out, Type type, Set<Type> types)
throws IOException
{
if (type instanceof Class)
{
Class classType = (Class) type;
if (classType.isArray())
{
appendTypeSource(out, classType.getComponentType(), types);
return;
}
if (classType.getName().startsWith("java.") ||
types.contains(type) || classType.isPrimitive())
return;
// Keep track of which types we've already added
types.add(type);
appendClassSource(out, classType, types);
}
else if (type instanceof ParameterizedType)
{
for (Type t : ((ParameterizedType) type).getActualTypeArguments())
appendTypeSource(out, t, types);
}
}
/**
* Appends the interface code for a non-component class to an OutputStream.
*
* @param out OutputStream
* @param classType Class
* @param types Set
* @throws IOException
*/
private void appendClassSource(OutputStream out, Class classType, Set<Type> types)
throws IOException
{
// Don't generate interfaces for enums
if (classType.isEnum())
return;
StringBuilder typeSource = new StringBuilder();
// Determine whether this class is a component; if so, use its name
// otherwise use its class name.
String componentName = Seam.getComponentName(classType);
if (componentName == null)
componentName = classType.getName();
String typeName = componentName.replace('.', '$');
typeSource.append("Seam.Remoting.type.");
typeSource.append(typeName);
typeSource.append(" = function() {\n");
StringBuilder fields = new StringBuilder();
StringBuilder accessors = new StringBuilder();
StringBuilder mutators = new StringBuilder();
Map<String,String> metadata = new HashMap<String,String>();
String getMethodName = null;
String setMethodName = null;
for ( String propertyName : getAccessibleProperties(classType) )
{
Type propertyType = null;
Field f = null;
try
{
f = classType.getDeclaredField(propertyName);
propertyType = f.getGenericType();
}
catch (NoSuchFieldException ex)
{
setMethodName = String.format("set%s%s",
Character.toUpperCase(propertyName.charAt(0)),
propertyName.substring(1));
try
{
getMethodName = String.format("get%s%s",
Character.toUpperCase(propertyName.charAt(0)),
propertyName.substring(1));
propertyType = classType.getMethod(getMethodName).getGenericReturnType();
}
catch (NoSuchMethodException ex2)
{
try
{
getMethodName = String.format("is%s%s",
Character.toUpperCase(propertyName.charAt(0)),
propertyName.substring(1));
propertyType = classType.getMethod(getMethodName).getGenericReturnType();
}
catch (NoSuchMethodException ex3)
{
// ???
continue;
}
}
}
appendTypeSource(out, propertyType, types);
// Include types referenced by generic declarations
if (propertyType instanceof ParameterizedType)
{
for (Type t : ((ParameterizedType) propertyType).getActualTypeArguments())
{
if (t instanceof Class)
appendTypeSource(out, t, types);
}
}
if (f != null)
{
String fieldName = propertyName.substring(0, 1).toUpperCase() +
propertyName.substring(1);
String getterName = String.format("get%s", fieldName);
String setterName = String.format("set%s", fieldName);
try
{
classType.getMethod(getterName);
getMethodName = getterName;
}
catch (SecurityException ex){}
catch (NoSuchMethodException ex)
{
getterName = String.format("is%s", fieldName);
try
{
if (Modifier.isPublic(classType.getMethod(getterName).getModifiers()))
getMethodName = getterName;
}
catch (NoSuchMethodException ex2)
{ /* don't care */}
}
try
{
if (Modifier.isPublic(classType.getMethod(setterName, f.getType()).getModifiers()))
setMethodName = setterName;
}
catch (SecurityException ex) {}
catch (NoSuchMethodException ex) { /* don't care */}
}
// Construct the list of fields.
if (getMethodName != null || setMethodName != null)
{
metadata.put(propertyName, getFieldType(propertyType));
fields.append(" this.");
fields.append(propertyName);
fields.append(" = undefined;\n");
if (getMethodName != null)
{
accessors.append(" Seam.Remoting.type.");
accessors.append(typeName);
accessors.append(".prototype.");
accessors.append(getMethodName);
accessors.append(" = function() { return this.");
accessors.append(propertyName);
accessors.append("; }\n");
}
if (setMethodName != null)
{
mutators.append(" Seam.Remoting.type.");
mutators.append(typeName);
mutators.append(".prototype.");
mutators.append(setMethodName);
mutators.append(" = function(");
mutators.append(propertyName);
mutators.append(") { this.");
mutators.append(propertyName);
mutators.append(" = ");
mutators.append(propertyName);
mutators.append("; }\n");
}
}
}
typeSource.append(fields);
typeSource.append(accessors);
typeSource.append(mutators);
typeSource.append("}\n\n");
// Append the type name
typeSource.append("Seam.Remoting.type.");
typeSource.append(typeName);
typeSource.append(".__name = \"");
typeSource.append(componentName);
typeSource.append("\";\n");
// Append the metadata
typeSource.append("Seam.Remoting.type.");
typeSource.append(typeName);
typeSource.append(".__metadata = [\n");
boolean first = true;
for (String key : metadata.keySet())
{
if (!first)
typeSource.append(",\n");
typeSource.append(" {field: \"");
typeSource.append(key);
typeSource.append("\", type: \"");
typeSource.append(metadata.get(key));
typeSource.append("\"}");
first = false;
}
typeSource.append("];\n\n");
// Register the type under Seam.Component if it is a component, otherwise
// register it under Seam.Remoting
if (classType.isAnnotationPresent(Name.class))
typeSource.append("Seam.Component.register(Seam.Remoting.type.");
else
typeSource.append("Seam.Remoting.registerType(Seam.Remoting.type.");
typeSource.append(typeName);
typeSource.append(");\n\n");
out.write(typeSource.toString().getBytes());
}
/**
* Returns the remoting "type" for a specified class.
*
* @param type Class
* @return String
*/
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";
}
}