| /* |
| * $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; |
| } |
| } |