blob: 6bd0028643c4ea2371fc420810f818586872b8a1 [file] [log] [blame]
/*
* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.views.gxp;
import com.google.common.annotations.VisibleForTesting;
import com.google.gxp.base.GxpContext;
import com.google.gxp.base.MarkupClosure;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Struts2 to GXP adapter. Can be used to write a GXP or create a
* {@link MarkupClosure}. Pulls GXP parameters from Struts2 value stack.
*
* @author Bob Lee
*/
public abstract class AbstractGxp<T extends MarkupClosure> {
ValueStackFactory valueStackFactory;
Map defaultValues = new HashMap();
List<Param> params;
Class gxpClass;
Method writeMethod;
Method getGxpClosureMethod;
boolean hasBodyParam;
protected AbstractGxp(Class gxpClass) {
this(gxpClass, lookupMethodByName(gxpClass, "write"), lookupMethodByName(gxpClass, "getGxpClosure"));
}
protected AbstractGxp(Class gxpClass, Method writeMethod, Method getGxpClosureMethod) {
this.gxpClass = gxpClass;
this.writeMethod = writeMethod;
this.getGxpClosureMethod = getGxpClosureMethod;
this.params = lookupParams();
}
/**
* Writes GXP. Pulls GXP parameters from Struts2's value stack.
*/
public void write(Appendable out, GxpContext gxpContext) {
write(out, gxpContext, null);
}
/**
* Writes GXP. Pulls GXP parameters from Struts2's value stack.
*
* @param overrides parameter map pushed onto the value stack
*/
protected void write(Appendable out, GxpContext gxpContext, Map overrides) {
Object[] args = getArgs(out, gxpContext, overrides);
try {
writeMethod.invoke(getGxpInstance(), args);
} catch (Exception e) {
throw new RuntimeException(createDebugString(args, e), e);
}
}
protected Object[] getArgs(Appendable out, GxpContext gxpContext, Map overrides) {
List<Object> argList = getArgListFromValueStack(overrides);
Object[] args = new Object[argList.size() + 2];
args[0] = out;
args[1] = gxpContext;
int index = 2;
for (Iterator<Object> i = argList.iterator(); i.hasNext(); index++) {
args[index] = i.next();
}
return args;
}
/**
* @return the object on which to call the write and getGxpClosure methods. If
* the methods are static, this can return {@code null}
*/
protected Object getGxpInstance() {
return null;
}
/**
* Creates GXP closure. Pulls GXP parameters from Struts 2 value stack.
*/
public T getGxpClosure() {
return getGxpClosure(null, null);
}
/**
* Creates GXP closure. Pulls GXP parameters from Struts 2 value stack.
*
* @param body is pushed onto the stack if this GXP has a
* {@link MarkupClosure} (or subclass) parameter named "body".
* @param params comes first on the value stack.
*/
@SuppressWarnings("unchecked")
protected T getGxpClosure(T body, Map params) {
final Map overrides = getOverrides(body, params);
Object[] args = getArgListFromValueStack(overrides).toArray();
try {
return (T) getGxpClosureMethod.invoke(getGxpInstance(), args);
} catch (IllegalArgumentException e) {
throw new RuntimeException(createDebugString(args, e), e);
} catch (IllegalAccessException e) {
throw new RuntimeException(createDebugString(args, e), e);
} catch (InvocationTargetException e) {
throw new RuntimeException(createDebugString(args, e), e);
}
}
protected Map getOverrides(T body, Map params) {
final Map overrides = new HashMap();
if (hasBodyParam && body != null) {
overrides.put(Param.BODY_PARAM_NAME, body);
}
if (params != null) {
overrides.putAll(params);
}
return overrides;
}
/**
* Iterates over GXP parameters, pulls value from value stack for each
* parameter, and appends the values to an argument list which will
* be passed to a method on a GXP.
*
* @param overrides parameter map pushed onto the value stack
*/
List getArgListFromValueStack(Map overrides) {
ValueStack valueStack = valueStackFactory.createValueStack(ActionContext.getContext().getValueStack());
// add default values to the bottom of the stack. if no action provides
// a getter for a param, the default value will be used.
valueStack.getRoot().add(this.defaultValues);
// push override parameters onto the stack.
if (overrides != null && !overrides.isEmpty()) {
valueStack.push(overrides);
}
List args = new ArrayList(params.size());
for (Param param : getParams()) {
try {
args.add(valueStack.findValue(param.getName(), param.getType()));
} catch (Exception e) {
throw new RuntimeException("Exception while finding '" + param.getName() + "'.", e);
}
}
return args;
}
/**
* Combines parameter names and types into <code>Param</code> objects.
*/
List<Param> lookupParams() {
List<Param> params = new ArrayList<Param>();
List<String> parameterNames = lookupParameterNames();
List<Class<?>> parameterTypes = lookupParameterTypes();
Iterator<Class<?>> parameterTypeIterator = parameterTypes.iterator();
// If there are more parameter names than parameter types it means that we are
// using instantiable GXPs and there are 1 or more constructor parameters.
// Constructor params will always be first in the list, so just drop an appropriate
// number of elements from the beginning of the list.
if (parameterNames.size() > parameterTypes.size()) {
parameterNames = parameterNames.subList(parameterNames.size() - parameterTypes.size(), parameterNames.size());
}
for (String name : parameterNames) {
Class paramType = parameterTypeIterator.next();
Param param = new Param(gxpClass, name, paramType);
params.add(param);
if (param.isBody()) {
hasBodyParam = true;
}
if (param.isOptional()) {
defaultValues.put(param.getName(), param.getDefaultValue());
}
}
this.defaultValues = Collections.unmodifiableMap(this.defaultValues);
return Collections.unmodifiableList(params);
}
/**
* Gets list of parameter types.
*/
List<Class<?>> lookupParameterTypes() {
List<Class<?>> parameterTypes = Arrays.asList(writeMethod.getParameterTypes());
// skip the first two, gxp_out and gxp_context. they are for internal use.
return parameterTypes.subList(2, parameterTypes.size());
}
/**
* Gets list of parameter names.
*/
List<String> lookupParameterNames() {
try {
return (List<String>) gxpClass.getMethod("getArgList").invoke(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Returns first method with the given name. Should not be used if the
* method is overloaded.
*/
protected static Method lookupMethodByName(Class clazz, String name) {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(name)) {
return methods[i];
}
}
throw new RuntimeException("No " + name + "(...) method found for "
+ clazz.getName() + ".");
}
public Class getGxpClass() {
return this.gxpClass;
}
/**
* Returns list of parameters requested by GXP.
*/
public List<Param> getParams() {
return params;
}
/**
* Returns generated GXP class given an absolute path to a GXP file.
* The current implementation assumes that the GXP and generated Java source
* file share the same name with different extensions.
*/
@VisibleForTesting
public static Class getGxpClassForPath(String gxpPath) {
int offset = (gxpPath.charAt(0) == '/') ? 1 : 0;
String className = gxpPath.substring(offset, gxpPath.length() - 4).replace('/', '.');
try {
return getClassLoader().loadClass(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
static ClassLoader getClassLoader() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return (loader == null) ? ClassLoader.getSystemClassLoader() : loader;
}
/**
* Creates debug String which can be tacked onto an exception.
*/
String createDebugString(Object[] args, Exception exception) {
StringBuffer buffer = new StringBuffer();
printExceptionTraceToBuffer(exception, buffer);
buffer.append("\nException in GXP: ").append(gxpClass.getName()).append(". Params:");
int index = 2;
for (Param param : getParams()) {
try {
Object arg = args[index++];
String typesMatch = "n/a (null)";
if (arg != null) {
if (doesArgumentTypeMatchParamType(param, arg)) {
typesMatch = "YES";
} else {
typesMatch = "NO";
}
}
buffer.append("\n ")
.append(param.toString())
.append(" = ")
.append(arg)
.append("; ")
.append("[types match? ")
.append(typesMatch)
.append("]");
} catch (Exception e) {
buffer.append(" >Error getting information for param # ").append(index).append("< ");
}
}
buffer.append("\nStack trace: ");
return buffer.toString();
}
private void printExceptionTraceToBuffer(Exception e,
StringBuffer buffer) {
StringWriter out = new StringWriter();
e.printStackTrace(new PrintWriter(out));
buffer.append(out.getBuffer().toString());
}
private boolean doesArgumentTypeMatchParamType(Param param, Object arg) {
Class paramType = param.getType();
Class<? extends Object> argClass = arg.getClass();
// TODO(jpelly): Handle all primitive unwrapping (ie, Boolean --> boolean).
if (boolean.class.equals(paramType) && Boolean.class.equals(argClass)) {
return true;
} else if (char.class.equals(paramType) && Character.class.equals(argClass)) {
return true;
}
return paramType.isAssignableFrom(argClass);
}
@Inject
public void setValueStackFactory(ValueStackFactory valueStackFactory) {
this.valueStackFactory = valueStackFactory;
}
}