
org.grails.compiler.web.ControllerActionTransformer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grails-plugin-controllers Show documentation
Show all versions of grails-plugin-controllers Show documentation
Grails Web Application Framework
/*
* Copyright 2011 SpringSource
*
* Licensed 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.grails.compiler.web;
import static org.grails.compiler.injection.GrailsASTUtils.applyDefaultMethodTarget;
import static org.grails.compiler.injection.GrailsASTUtils.applyMethodTarget;
import static org.grails.compiler.injection.GrailsASTUtils.buildGetMapExpression;
import static org.grails.compiler.injection.GrailsASTUtils.buildGetPropertyExpression;
import static org.grails.compiler.injection.GrailsASTUtils.buildSetPropertyExpression;
import grails.artefact.Artefact;
import grails.artefact.controller.support.AllowedMethodsHelper;
import grails.compiler.DelegatingMethod;
import grails.compiler.ast.AnnotatedClassInjector;
import grails.compiler.ast.AstTransformer;
import grails.compiler.ast.GrailsArtefactClassInjector;
import grails.util.CollectionUtils;
import grails.validation.ASTValidateableHelper;
import grails.validation.DefaultASTValidateableHelper;
import grails.validation.Validateable;
import grails.web.Action;
import grails.web.RequestParameter;
import grails.web.controllers.ControllerMethod;
import grails.util.TypeConvertingMap;
import groovy.lang.Closure;
import java.io.File;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.EmptyExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.CatchStatement;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.IfStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.grails.compiler.injection.GrailsASTUtils;
import org.grails.core.DefaultGrailsControllerClass;
import org.grails.core.artefact.ControllerArtefactHandler;
import org.grails.io.support.GrailsResourceUtils;
import org.grails.plugins.web.controllers.DefaultControllerExceptionHandlerMetaData;
import org.grails.web.databinding.DefaultASTDatabindingHelper;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
/**
* Enhances controller classes by converting closures actions to method actions and binding
* request parameters to action arguments.
*/
/*
class TestController{
//default scope configurable in Config.groovy
static scope = 'singleton'
def peterTheFrenchService
//--------------------------
//allow use of methods as actions
def someAction() {
render 'ata'
}
/ becomes behind the scene :
@Action
def someAction() {
render 'ata'
}
/
//--------------------------
//Compile time transformed to method
def lol2 = {
render 'testxx'
}
/ becomes behind the scene :
@Action def lol2() { render 'testxx' }
/
//--------------------------
def lol4 = { PeterCommand cmd ->
render cmd.a
}
/ becomes behind the scene :
@Action(commandObjects={PeterCommand}) def lol4() {
PeterCommand cmd = new PeterCommand(); bindData(cmd, params)
render 'testxx'
}
/
}
*/
@AstTransformer
public class ControllerActionTransformer implements GrailsArtefactClassInjector, AnnotatedClassInjector {
public static final AnnotationNode DELEGATING_METHOD_ANNOATION = new AnnotationNode(ClassHelper.make(DelegatingMethod.class));
public static Pattern CONTROLLER_PATTERN = Pattern.compile(".+/" +
GrailsResourceUtils.GRAILS_APP_DIR + "/controllers/(.+)Controller\\.groovy");
private static final String ALLOWED_METHODS_HANDLED_ATTRIBUTE_NAME = "ALLOWED_METHODS_HANDLED";
private static final ClassNode OBJECT_CLASS = new ClassNode(Object.class);
public static final AnnotationNode ACTION_ANNOTATION_NODE = new AnnotationNode(
new ClassNode(Action.class));
private static final String ACTION_MEMBER_TARGET = "commandObjects";
public static final String EXCEPTION_HANDLER_META_DATA_FIELD_NAME = "$exceptionHandlerMetaData";
private static final TupleExpression EMPTY_TUPLE = new TupleExpression();
@SuppressWarnings({"unchecked"})
private static final Map TYPE_WRAPPER_CLASS_TO_CONVERSION_METHOD_NAME = CollectionUtils.newMap(
ClassHelper.Integer_TYPE, "int",
ClassHelper.Float_TYPE, "float",
ClassHelper.Long_TYPE, "long",
ClassHelper.Double_TYPE, "double",
ClassHelper.Short_TYPE, "short",
ClassHelper.Boolean_TYPE, "boolean",
ClassHelper.Byte_TYPE, "byte",
ClassHelper.Character_TYPE, "char");
private static List PRIMITIVE_CLASS_NODES = CollectionUtils.newList(
ClassHelper.boolean_TYPE,
ClassHelper.char_TYPE,
ClassHelper.int_TYPE,
ClassHelper.short_TYPE,
ClassHelper.long_TYPE,
ClassHelper.double_TYPE,
ClassHelper.float_TYPE,
ClassHelper.byte_TYPE);
public static final String VOID_TYPE = "void";
public static final String CONVERT_CLOSURES_KEY = "grails.compile.artefacts.closures.convert";
private Boolean converterEnabled;
public ControllerActionTransformer() {
converterEnabled = Boolean.parseBoolean(System.getProperty(CONVERT_CLOSURES_KEY));
}
public String[] getArtefactTypes() {
return new String[]{ControllerArtefactHandler.TYPE};
}
public void performInjection(SourceUnit source, GeneratorContext context, ClassNode classNode) {
// don't inject if already an @Artefact annotation is applied
if(!classNode.getAnnotations(new ClassNode(Artefact.class)).isEmpty()) return;
performInjectionOnAnnotatedClass(source, context, classNode);
}
@Override
public void performInjectionOnAnnotatedClass(SourceUnit source, GeneratorContext context, ClassNode classNode) {
final String className = classNode.getName();
if (className.endsWith(ControllerArtefactHandler.TYPE)) {
processMethods(classNode, source, context);
processClosures(classNode, source, context);
}
}
@Override
public void performInjectionOnAnnotatedClass(SourceUnit source, ClassNode classNode) {
performInjectionOnAnnotatedClass(source,null, classNode);
}
private boolean isExceptionHandlingMethod(MethodNode methodNode) {
boolean isExceptionHandler = false;
if(!methodNode.isPrivate() && methodNode.getName().indexOf("$") == -1) {
Parameter[] parameters = methodNode.getParameters();
if(parameters.length == 1) {
ClassNode parameterTypeClassNode = parameters[0].getType();
isExceptionHandler = parameterTypeClassNode.isDerivedFrom(new ClassNode(Exception.class));
}
}
return isExceptionHandler;
}
private void processMethods(ClassNode classNode, SourceUnit source,
GeneratorContext context) {
List deferredNewMethods = new ArrayList();
for (MethodNode method : classNode.getMethods()) {
if (methodShouldBeConfiguredAsControllerAction(method)) {
final List declaredMethodsWithThisName = classNode.getDeclaredMethods(method.getName());
if(declaredMethodsWithThisName != null) {
final int numberOfNonExceptionHandlerMethodsWithThisName = DefaultGroovyMethods.count((Iterable)declaredMethodsWithThisName, new Closure(this) {
@Override
public Object call(Object object) {
return !isExceptionHandlingMethod((MethodNode) object);
}
}).intValue();
if (numberOfNonExceptionHandlerMethodsWithThisName > 1) {
String message = "Controller actions may not be overloaded. The [" +
method.getName() +
"] action has been overloaded in [" +
classNode.getName() +
"].";
GrailsASTUtils.error(source, method, message);
}
}
MethodNode wrapperMethod = convertToMethodAction(classNode, method, source, context);
if (wrapperMethod != null) {
deferredNewMethods.add(wrapperMethod);
}
}
}
Collection exceptionHandlerMethods = getExceptionHandlerMethods(classNode, source);
final FieldNode exceptionHandlerMetaDataField = classNode.getField(EXCEPTION_HANDLER_META_DATA_FIELD_NAME);
if(exceptionHandlerMetaDataField == null || !exceptionHandlerMetaDataField.getDeclaringClass().equals(classNode)) {
final ListExpression listOfExceptionHandlerMetaData = new ListExpression();
for(final MethodNode exceptionHandlerMethod : exceptionHandlerMethods) {
final Parameter[] parameters = exceptionHandlerMethod.getParameters();
final Parameter firstParameter = parameters[0];
final ClassNode firstParameterTypeClassNode = firstParameter.getType();
final String exceptionHandlerMethodName = exceptionHandlerMethod.getName();
final ArgumentListExpression defaultControllerExceptionHandlerMetaDataCtorArgs = new ArgumentListExpression();
defaultControllerExceptionHandlerMetaDataCtorArgs.addExpression(new ConstantExpression(exceptionHandlerMethodName));
defaultControllerExceptionHandlerMetaDataCtorArgs.addExpression(new ClassExpression(firstParameterTypeClassNode.getPlainNodeReference()));
listOfExceptionHandlerMetaData.addExpression(new ConstructorCallExpression(new ClassNode(DefaultControllerExceptionHandlerMetaData.class), defaultControllerExceptionHandlerMetaDataCtorArgs));
}
classNode.addField(EXCEPTION_HANDLER_META_DATA_FIELD_NAME,
Modifier.STATIC | Modifier.PRIVATE | Modifier.FINAL, new ClassNode(List.class),
listOfExceptionHandlerMetaData);
}
for (MethodNode newMethod : deferredNewMethods) {
classNode.addMethod(newMethod);
}
}
/**
*
* @param method a potential controller action method
* @return true if the method should be configured as a controller action, false otherwise
*/
protected boolean methodShouldBeConfiguredAsControllerAction(final MethodNode method) {
return !method.isStatic() &&
method.isPublic() &&
!method.isAbstract() &&
method.getAnnotations(ACTION_ANNOTATION_NODE.getClassNode()).isEmpty() &&
method.getAnnotations(new ClassNode(ControllerMethod.class)).isEmpty() &&
method.getLineNumber() >= 0 &&
!method.getName().startsWith("$") &&
!method.getReturnType().getName().equals(VOID_TYPE) &&
!isExceptionHandlingMethod(method);
}
protected Collection getExceptionHandlerMethods(final ClassNode classNode, SourceUnit sourceUnit) {
final Map exceptionTypeToHandlerMethodMap = new HashMap();
final List methods = classNode.getMethods();
for(MethodNode methodNode : methods) {
if(isExceptionHandlingMethod(methodNode)) {
final Parameter exceptionParameter = methodNode.getParameters()[0];
final ClassNode exceptionType = exceptionParameter.getType();
if(!exceptionTypeToHandlerMethodMap.containsKey(exceptionType)) {
exceptionTypeToHandlerMethodMap.put(exceptionType, methodNode);
} else {
final MethodNode otherHandlerMethod = exceptionTypeToHandlerMethodMap.get(exceptionType);
final String message = "A controller may not define more than 1 exception handler for a particular exception type. [%s] defines the [%s] and [%s] exception handlers which each accept a [%s] which is not allowed.";
final String formattedMessage = String.format(message, classNode.getName(), otherHandlerMethod.getName(), methodNode.getName(), exceptionType.getName());
GrailsASTUtils.error(sourceUnit, methodNode, formattedMessage);
}
}
}
final ClassNode superClass = classNode.getSuperClass();
if(!superClass.equals(OBJECT_CLASS)) {
final Collection superClassMethods = getExceptionHandlerMethods(superClass, sourceUnit);
for(MethodNode superClassMethod : superClassMethods) {
final Parameter exceptionParameter = superClassMethod.getParameters()[0];
final ClassNode exceptionType = exceptionParameter.getType();
// only add this super class handler if we don't already have
// a handler for this exception type in this class
if(!exceptionTypeToHandlerMethodMap.containsKey(exceptionType)) {
exceptionTypeToHandlerMethodMap.put(exceptionType, superClassMethod);
}
}
}
return exceptionTypeToHandlerMethodMap.values();
}
/**
* Converts a method into a controller action. If the method accepts parameters,
* a no-arg counterpart is created which delegates to the original.
*
* @param classNode The controller class
* @param methodNode The method to be converted
* @return The no-arg wrapper method, or null if none was created.
*/
private MethodNode convertToMethodAction(ClassNode classNode, MethodNode methodNode,
SourceUnit source, GeneratorContext context) {
final ClassNode returnType = methodNode.getReturnType();
Parameter[] parameters = methodNode.getParameters();
for (Parameter param : parameters) {
if (param.hasInitialExpression()) {
String paramName = param.getName();
String methodName = methodNode.getName();
String initialValue = param.getInitialExpression().getText();
String methodDeclaration = methodNode.getText();
String message = "Parameter [%s] to method [%s] has default value [%s]. " +
"Default parameter values are not allowed in controller action methods. ([%s])";
String formattedMessage = String.format(message, paramName, methodName,
initialValue, methodDeclaration);
GrailsASTUtils.error(source, methodNode, formattedMessage);
}
}
MethodNode method = null;
if (methodNode.getParameters().length > 0) {
final BlockStatement methodCode = new BlockStatement();
final BlockStatement codeToHandleAllowedMethods = getCodeToHandleAllowedMethods(classNode, methodNode.getName());
final Statement codeToCallOriginalMethod = addOriginalMethodCall(methodNode, initializeActionParameters(
classNode, methodNode, methodNode.getName(), parameters, source, context));
methodCode.addStatement(codeToHandleAllowedMethods);
methodCode.addStatement(codeToCallOriginalMethod);
method = new MethodNode(
methodNode.getName(),
Modifier.PUBLIC, returnType,
ZERO_PARAMETERS,
EMPTY_CLASS_ARRAY,
methodCode);
GrailsASTUtils.copyAnnotations(methodNode, method);
methodNode.addAnnotation(DELEGATING_METHOD_ANNOATION);
annotateActionMethod(classNode, parameters, method);
wrapMethodBodyWithExceptionHandling(classNode, method);
} else {
annotateActionMethod(classNode, parameters, methodNode);
}
wrapMethodBodyWithExceptionHandling(classNode, methodNode);
return method;
}
private Statement addOriginalMethodCall(MethodNode methodNode, BlockStatement blockStatement) {
if (blockStatement == null) {
return null;
}
final ArgumentListExpression arguments = new ArgumentListExpression();
for (Parameter p : methodNode.getParameters()) {
arguments.addExpression(new VariableExpression(p.getName(), p.getType()));
}
MethodCallExpression callExpression = new MethodCallExpression(
new VariableExpression("this"), methodNode.getName(), arguments);
callExpression.setMethodTarget(methodNode);
blockStatement.addStatement(new ReturnStatement(callExpression));
return blockStatement;
}
private boolean isCommandObjectAction(Parameter[] params) {
return params != null && params.length > 0
&& params[0].getType() != new ClassNode(Object[].class)
&& params[0].getType() != new ClassNode(Object.class);
}
private void processClosures(ClassNode classNode, SourceUnit source, GeneratorContext context) {
List propertyNodes = new ArrayList(classNode.getProperties());
Expression initialExpression;
ClosureExpression closureAction;
for (PropertyNode property : propertyNodes) {
initialExpression = property.getInitialExpression();
if (!property.isStatic() && initialExpression != null &&
initialExpression.getClass().equals(ClosureExpression.class)) {
closureAction = (ClosureExpression) initialExpression;
if (converterEnabled) {
transformClosureToMethod(classNode, closureAction, property, source, context);
} else {
addMethodToInvokeClosure(classNode, property, source, context);
}
}
}
}
protected void addMethodToInvokeClosure(ClassNode controllerClassNode,
PropertyNode closureProperty, SourceUnit source, GeneratorContext context) {
MethodNode method = controllerClassNode.getMethod(closureProperty.getName(), ZERO_PARAMETERS);
if (method == null || !method.getDeclaringClass().equals(controllerClassNode)) {
ClosureExpression closureExpression = (ClosureExpression) closureProperty.getInitialExpression();
final Parameter[] parameters = closureExpression.getParameters();
final BlockStatement newMethodCode = initializeActionParameters(
controllerClassNode, closureProperty, closureProperty.getName(),
parameters, source, context);
final ArgumentListExpression closureInvocationArguments = new ArgumentListExpression();
if (parameters != null) {
for (Parameter p : parameters) {
closureInvocationArguments.addExpression(new VariableExpression(p.getName()));
}
}
final MethodCallExpression methodCallExpression = new MethodCallExpression(
closureExpression, "call", closureInvocationArguments);
newMethodCode.addStatement(new ExpressionStatement(applyMethodTarget(methodCallExpression, Closure.class, Object.class)));
final MethodNode methodNode = new MethodNode(closureProperty.getName(), Modifier.PUBLIC,
new ClassNode(Object.class), ZERO_PARAMETERS, EMPTY_CLASS_ARRAY, newMethodCode);
wrapMethodBodyWithExceptionHandling(controllerClassNode, methodNode);
annotateActionMethod(controllerClassNode, parameters, methodNode);
controllerClassNode.addMethod(methodNode);
}
}
protected void annotateActionMethod(ClassNode controllerClassNode, final Parameter[] parameters, final MethodNode methodNode) {
if (isCommandObjectAction(parameters)) {
ListExpression initArray = new ListExpression();
for (Parameter parameter : parameters) {
initArray.addExpression(new ClassExpression(parameter.getType()));
}
AnnotationNode paramActionAnn = new AnnotationNode(new ClassNode(Action.class));
paramActionAnn.setMember(ACTION_MEMBER_TARGET, initArray);
methodNode.addAnnotation(paramActionAnn);
} else {
methodNode.addAnnotation(ACTION_ANNOTATION_NODE);
}
}
protected BlockStatement getCodeToHandleAllowedMethods(ClassNode controllerClass, String methodName) {
GrailsASTUtils.addEnhancedAnnotation(controllerClass, DefaultGrailsControllerClass.ALLOWED_HTTP_METHODS_PROPERTY);
final BlockStatement checkAllowedMethodsBlock = new BlockStatement();
final PropertyExpression requestPropertyExpression = new PropertyExpression(new VariableExpression("this"), "request");
final FieldNode allowedMethodsField = controllerClass.getField(DefaultGrailsControllerClass.ALLOWED_HTTP_METHODS_PROPERTY);
if(allowedMethodsField != null) {
final Expression initialAllowedMethodsExpression = allowedMethodsField.getInitialExpression();
if(initialAllowedMethodsExpression instanceof MapExpression) {
boolean actionIsRestricted = false;
final MapExpression allowedMethodsMapExpression = (MapExpression) initialAllowedMethodsExpression;
final List allowedMethodsMapEntryExpressions = allowedMethodsMapExpression.getMapEntryExpressions();
for(MapEntryExpression allowedMethodsMapEntryExpression : allowedMethodsMapEntryExpressions) {
final Expression allowedMethodsMapEntryKeyExpression = allowedMethodsMapEntryExpression.getKeyExpression();
if(allowedMethodsMapEntryKeyExpression instanceof ConstantExpression) {
final ConstantExpression allowedMethodsMapKeyConstantExpression = (ConstantExpression) allowedMethodsMapEntryKeyExpression;
final Object allowedMethodsMapKeyValue = allowedMethodsMapKeyConstantExpression.getValue();
if(methodName.equals(allowedMethodsMapKeyValue)) {
actionIsRestricted = true;
break;
}
}
}
if(actionIsRestricted) {
final PropertyExpression responsePropertyExpression = new PropertyExpression(new VariableExpression("this"), "response");
final ArgumentListExpression isAllowedArgumentList = new ArgumentListExpression();
isAllowedArgumentList.addExpression(new ConstantExpression(methodName));
isAllowedArgumentList.addExpression(new PropertyExpression(new VariableExpression("this"), "request"));
isAllowedArgumentList.addExpression(new PropertyExpression(new VariableExpression("this"), DefaultGrailsControllerClass.ALLOWED_HTTP_METHODS_PROPERTY));
final Expression isAllowedMethodCall = new StaticMethodCallExpression(ClassHelper.make(AllowedMethodsHelper.class), "isAllowed", isAllowedArgumentList);
final BooleanExpression isValidRequestMethod = new BooleanExpression(isAllowedMethodCall);
final MethodCallExpression sendErrorMethodCall = new MethodCallExpression(responsePropertyExpression, "sendError", new ConstantExpression(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
final ReturnStatement returnStatement = new ReturnStatement(new ConstantExpression(null));
final BlockStatement blockToSendError = new BlockStatement();
blockToSendError.addStatement(new ExpressionStatement(sendErrorMethodCall));
blockToSendError.addStatement(returnStatement);
final IfStatement ifIsValidRequestMethodStatement = new IfStatement(isValidRequestMethod, new ExpressionStatement(new EmptyExpression()), blockToSendError);
checkAllowedMethodsBlock.addStatement(ifIsValidRequestMethodStatement);
}
}
}
final ArgumentListExpression argumentListExpression = new ArgumentListExpression();
argumentListExpression.addExpression(new ConstantExpression(ALLOWED_METHODS_HANDLED_ATTRIBUTE_NAME));
argumentListExpression.addExpression(new ConstantExpression(methodName));
final Expression setAttributeMethodCall = new MethodCallExpression(requestPropertyExpression, "setAttribute", argumentListExpression);
final BlockStatement codeToExecuteIfAttributeIsNotSet = new BlockStatement();
codeToExecuteIfAttributeIsNotSet.addStatement(new ExpressionStatement(setAttributeMethodCall));
codeToExecuteIfAttributeIsNotSet.addStatement(checkAllowedMethodsBlock);
final BooleanExpression attributeIsSetBooleanExpression = new BooleanExpression(new MethodCallExpression(requestPropertyExpression, "getAttribute", new ArgumentListExpression(new ConstantExpression(ALLOWED_METHODS_HANDLED_ATTRIBUTE_NAME))));
final Statement ifAttributeIsAlreadySetStatement = new IfStatement(attributeIsSetBooleanExpression, new EmptyStatement(), codeToExecuteIfAttributeIsNotSet);
final BlockStatement code = new BlockStatement();
code.addStatement(ifAttributeIsAlreadySetStatement);
return code;
}
/**
* This will wrap the method body in a try catch block which does something
* like this:
*
* try {
* // original method body here
* } catch (Exception $caughtException) {
* Method $method = getExceptionHandlerMethod($caughtException.getClass())
* if($method) {
* return $method.invoke(this, $caughtException)
* } else {
* throw $caughtException
* }
* }
*
* @param methodNode the method to add the try catch block to
*/
protected void wrapMethodBodyWithExceptionHandling(final ClassNode controllerClassNode, final MethodNode methodNode) {
final BlockStatement catchBlockCode = new BlockStatement();
final String caughtExceptionArgumentName = "$caughtException";
final Expression caughtExceptionVariableExpression = new VariableExpression(caughtExceptionArgumentName);
final Expression caughtExceptionTypeExpression = new PropertyExpression(caughtExceptionVariableExpression, "class");
final Expression thisExpression = new VariableExpression("this");
final MethodCallExpression getExceptionHandlerMethodCall = new MethodCallExpression(thisExpression, "getExceptionHandlerMethodFor", caughtExceptionTypeExpression);
applyDefaultMethodTarget(getExceptionHandlerMethodCall, controllerClassNode);
final ClassNode reflectMethodClassNode = new ClassNode(Method.class);
final String exceptionHandlerMethodVariableName = "$method";
final Expression exceptionHandlerMethodExpression = new VariableExpression(exceptionHandlerMethodVariableName, new ClassNode(Method.class));
final Expression declareExceptionHandlerMethod = new DeclarationExpression(
new VariableExpression(exceptionHandlerMethodVariableName, reflectMethodClassNode), Token.newSymbol(Types.EQUALS, 0, 0), getExceptionHandlerMethodCall);
final ArgumentListExpression invokeArguments = new ArgumentListExpression();
invokeArguments.addExpression(thisExpression);
invokeArguments.addExpression(caughtExceptionVariableExpression);
final MethodCallExpression invokeExceptionHandlerMethodExpression = new MethodCallExpression(new VariableExpression(exceptionHandlerMethodVariableName), "invoke", invokeArguments);
applyDefaultMethodTarget(invokeExceptionHandlerMethodExpression, reflectMethodClassNode);
final Statement returnStatement = new ReturnStatement(invokeExceptionHandlerMethodExpression);
final Statement throwCaughtExceptionStatement = new ThrowStatement(caughtExceptionVariableExpression);
final Statement ifExceptionHandlerMethodExistsStatement = new IfStatement(new BooleanExpression(exceptionHandlerMethodExpression), returnStatement, throwCaughtExceptionStatement);
catchBlockCode.addStatement(new ExpressionStatement(declareExceptionHandlerMethod));
catchBlockCode.addStatement(ifExceptionHandlerMethodExistsStatement);
final CatchStatement catchStatement = new CatchStatement(new Parameter(new ClassNode(Exception.class), caughtExceptionArgumentName), catchBlockCode);
final Statement methodBody = methodNode.getCode();
BlockStatement tryBlock = new BlockStatement();
BlockStatement codeToHandleAllowedMethods = getCodeToHandleAllowedMethods(controllerClassNode, methodNode.getName());
tryBlock.addStatement(codeToHandleAllowedMethods);
tryBlock.addStatement(methodBody);
final TryCatchStatement tryCatchStatement = new TryCatchStatement(tryBlock, new EmptyStatement());
tryCatchStatement.addCatch(catchStatement);
final ArgumentListExpression argumentListExpression = new ArgumentListExpression();
argumentListExpression.addExpression(new ConstantExpression(ALLOWED_METHODS_HANDLED_ATTRIBUTE_NAME));
final PropertyExpression requestPropertyExpression = new PropertyExpression(new VariableExpression("this"), "request");
final Expression removeAttributeMethodCall = new MethodCallExpression(requestPropertyExpression, "removeAttribute", argumentListExpression);
final Expression getAttributeMethodCall = new MethodCallExpression(requestPropertyExpression, "getAttribute", new ArgumentListExpression(new ConstantExpression(ALLOWED_METHODS_HANDLED_ATTRIBUTE_NAME)));
final VariableExpression attributeValueExpression = new VariableExpression("$allowed_methods_attribute_value", ClassHelper.make(Object.class));
final Expression initializeAttributeValue = new DeclarationExpression(
attributeValueExpression, Token.newSymbol(Types.EQUALS, 0, 0), getAttributeMethodCall);
final Expression attributeValueMatchesMethodNameExpression = new BinaryExpression(new ConstantExpression(methodNode.getName()),
Token.newSymbol(Types.COMPARE_EQUAL, 0, 0),
attributeValueExpression);
final Statement ifAttributeValueMatchesMethodName =
new IfStatement(new BooleanExpression(attributeValueMatchesMethodNameExpression),
new ExpressionStatement(removeAttributeMethodCall), new EmptyStatement());
final BlockStatement blockToRemoveAttribute = new BlockStatement();
blockToRemoveAttribute.addStatement(new ExpressionStatement(initializeAttributeValue));
blockToRemoveAttribute.addStatement(ifAttributeValueMatchesMethodName);
final TryCatchStatement tryCatchToRemoveAttribute = new TryCatchStatement(blockToRemoveAttribute, new EmptyStatement());
tryCatchToRemoveAttribute.addCatch(new CatchStatement(new Parameter(ClassHelper.make(Exception.class), "$exceptionRemovingAttribute"), new EmptyStatement()));
tryCatchStatement.setFinallyStatement(tryCatchToRemoveAttribute);
methodNode.setCode(tryCatchStatement);
}
protected void transformClosureToMethod(ClassNode classNode, ClosureExpression closureAction,
PropertyNode property, SourceUnit source, GeneratorContext context) {
final MethodNode actionMethod = new MethodNode(property.getName(),
Modifier.PUBLIC, property.getType(), closureAction.getParameters(),
EMPTY_CLASS_ARRAY, closureAction.getCode());
MethodNode convertedMethod = convertToMethodAction(classNode, actionMethod, source, context);
if (convertedMethod != null) {
classNode.addMethod(convertedMethod);
}
classNode.getProperties().remove(property);
classNode.getFields().remove(property.getField());
classNode.addMethod(actionMethod);
}
protected BlockStatement initializeActionParameters(ClassNode classNode, ASTNode actionNode,
String actionName, Parameter[] actionParameters, SourceUnit source,
GeneratorContext context) {
BlockStatement wrapper = new BlockStatement();
ArgumentListExpression mapBindingResultConstructorArgs = new ArgumentListExpression();
mapBindingResultConstructorArgs.addExpression(new ConstructorCallExpression(
new ClassNode(HashMap.class), EMPTY_TUPLE));
mapBindingResultConstructorArgs.addExpression(new ConstantExpression("controller"));
final Expression mapBindingResultConstructorCallExpression = new ConstructorCallExpression(
new ClassNode(MapBindingResult.class), mapBindingResultConstructorArgs);
final Expression errorsAssignmentExpression = buildSetPropertyExpression(new VariableExpression("this", classNode), "errors", classNode, mapBindingResultConstructorCallExpression);
wrapper.addStatement(new ExpressionStatement(errorsAssignmentExpression));
if (actionParameters != null) {
for (Parameter param : actionParameters) {
initializeMethodParameter(classNode, wrapper, actionNode, actionName,
param, source, context);
}
}
return wrapper;
}
protected void initializeMethodParameter(final ClassNode classNode, final BlockStatement wrapper,
final ASTNode actionNode, final String actionName, final Parameter param,
final SourceUnit source, final GeneratorContext context) {
final ClassNode paramTypeClassNode = param.getType();
final String paramName = param.getName();
String requestParameterName = paramName;
List requestParameters = param.getAnnotations(
new ClassNode(RequestParameter.class));
if (requestParameters.size() == 1) {
requestParameterName = requestParameters.get(0).getMember("value").getText();
}
if ((PRIMITIVE_CLASS_NODES.contains(paramTypeClassNode) ||
TYPE_WRAPPER_CLASS_TO_CONVERSION_METHOD_NAME.containsKey(paramTypeClassNode))) {
initializePrimitiveOrTypeWrapperParameter(classNode, wrapper, param, requestParameterName);
} else if (paramTypeClassNode.equals(new ClassNode(String.class))) {
initializeStringParameter(classNode, wrapper, param, requestParameterName);
} else if (!paramTypeClassNode.equals(OBJECT_CLASS)) {
initializeAndValidateCommandObjectParameter(wrapper, classNode, paramTypeClassNode,
actionNode, actionName, paramName, source, context);
}
}
protected void initializeAndValidateCommandObjectParameter(final BlockStatement wrapper,
final ClassNode controllerNode, final ClassNode commandObjectNode,
final ASTNode actionNode, final String actionName, final String paramName,
final SourceUnit source, final GeneratorContext context) {
final DeclarationExpression declareCoExpression = new DeclarationExpression(
new VariableExpression(paramName, commandObjectNode), Token.newSymbol(Types.EQUALS, 0, 0), new EmptyExpression());
wrapper.addStatement(new ExpressionStatement(declareCoExpression));
if(commandObjectNode.isInterface() || Modifier.isAbstract(commandObjectNode.getModifiers())) {
final String warningMessage = "The [" + actionName + "] action in [" +
controllerNode.getName() + "] accepts a parameter of type [" +
commandObjectNode.getName() +
"]. Interface types and abstract class types are not supported as command objects. This parameter will be ignored.";
GrailsASTUtils.warning(source, actionNode, warningMessage);
} else {
initializeCommandObjectParameter(wrapper, commandObjectNode, paramName, source);
@SuppressWarnings("unchecked")
boolean argumentIsValidateable = GrailsASTUtils.hasAnyAnnotations(
commandObjectNode,
grails.persistence.Entity.class,
javax.persistence.Entity.class) ||
commandObjectNode.implementsInterface(ClassHelper.make(Validateable.class));
if (!argumentIsValidateable) {
final ModuleNode commandObjectModule = commandObjectNode.getModule();
if (commandObjectModule != null) {
if (commandObjectModule == controllerNode.getModule() ||
doesModulePathIncludeSubstring(commandObjectModule,
"grails-app" + File.separator + "controllers" + File.separator)) {
final ASTValidateableHelper h = new DefaultASTValidateableHelper();
h.injectValidateableCode(commandObjectNode, false);
argumentIsValidateable = true;
} else if (doesModulePathIncludeSubstring(commandObjectModule,
"grails-app" + File.separator + "domain" + File.separator)) {
argumentIsValidateable = true;
}
}
}
if (argumentIsValidateable) {
final MethodCallExpression validateMethodCallExpression =
new MethodCallExpression(new VariableExpression(paramName), "validate", EMPTY_TUPLE);
final MethodNode validateMethod =
commandObjectNode.getMethod("validate", new Parameter[0]);
if (validateMethod != null) {
validateMethodCallExpression.setMethodTarget(validateMethod);
}
final Statement ifCommandObjectIsNotNullThenValidate = new IfStatement(new BooleanExpression(new VariableExpression(paramName)), new ExpressionStatement(validateMethodCallExpression), new ExpressionStatement(new EmptyExpression()));
wrapper.addStatement(ifCommandObjectIsNotNullThenValidate);
} else {
// try to dynamically invoke the .validate() method if it is available at runtime...
final Expression respondsToValidateMethodCallExpression = new MethodCallExpression(
new VariableExpression(paramName), "respondsTo", new ArgumentListExpression(
new ConstantExpression("validate")));
final Expression validateMethodCallExpression = new MethodCallExpression(
new VariableExpression(paramName), "validate", new ArgumentListExpression());
final Statement ifRespondsToValidateThenValidateStatement = new IfStatement(
new BooleanExpression(respondsToValidateMethodCallExpression),
new ExpressionStatement(validateMethodCallExpression),
new ExpressionStatement(new EmptyExpression()));
final Statement ifCommandObjectIsNotNullThenValidate = new IfStatement(new BooleanExpression(new VariableExpression(paramName)), ifRespondsToValidateThenValidateStatement, new ExpressionStatement(new EmptyExpression()));
wrapper.addStatement(ifCommandObjectIsNotNullThenValidate);
final String warningMessage = "The [" + actionName + "] action accepts a parameter of type [" +
commandObjectNode.getName() +
"] which does not implement grails.validation.Validateable. Data binding will still be applied " +
"to this command object but the instance will not be validateable.";
GrailsASTUtils.warning(source, actionNode, warningMessage);
}
if(GrailsASTUtils.isInnerClassNode(commandObjectNode)) {
final String warningMessage = "The [" + actionName + "] action accepts a parameter of type [" +
commandObjectNode.getName() +
"] which is an inner class. Command object classes should not be inner classes.";
GrailsASTUtils.warning(source, actionNode, warningMessage);
}
else {
new DefaultASTDatabindingHelper().injectDatabindingCode(source, context, commandObjectNode);
}
}
}
protected void initializeCommandObjectParameter(final BlockStatement wrapper,
final ClassNode commandObjectNode, final String paramName, SourceUnit source) {
final ArgumentListExpression initializeCommandObjectArguments = new ArgumentListExpression();
initializeCommandObjectArguments.addExpression(new ClassExpression(commandObjectNode));
initializeCommandObjectArguments.addExpression(new ConstantExpression(paramName));
final MethodCallExpression initializeCommandObjectMethodCall = new MethodCallExpression(new VariableExpression("this"), "initializeCommandObject", initializeCommandObjectArguments);
applyDefaultMethodTarget(initializeCommandObjectMethodCall, commandObjectNode);
final Expression assignCommandObjectToParameter = new BinaryExpression(new VariableExpression(paramName), Token.newSymbol(Types.EQUALS, 0, 0), initializeCommandObjectMethodCall);
wrapper.addStatement(new ExpressionStatement(assignCommandObjectToParameter));
}
/**
* Checks to see if a Module is defined at a path which includes the specified substring
*
* @param moduleNode a ModuleNode
* @param substring The substring to search for
* @return true if moduleNode is defined at a path which includes the specified substring
*/
private boolean doesModulePathIncludeSubstring(ModuleNode moduleNode, final String substring) {
if (moduleNode == null) {
return false;
}
boolean substringFoundInDescription = false;
String commandObjectModuleDescription = moduleNode.getDescription();
if (commandObjectModuleDescription != null) {
substringFoundInDescription = commandObjectModuleDescription.contains(substring);
}
return substringFoundInDescription;
}
protected void initializeStringParameter(final ClassNode classNode, final BlockStatement wrapper, final Parameter param,
final String requestParameterName) {
final ClassNode paramTypeClassNode = param.getType();
final String methodParamName = param.getName();
Expression getParamsExpression = buildGetPropertyExpression(new VariableExpression("this"), "params", classNode);
final Expression paramsContainsKeyMethodArguments = new ArgumentListExpression(
new ConstantExpression(requestParameterName));
final BooleanExpression containsKeyExpression = new BooleanExpression(
applyDefaultMethodTarget(new MethodCallExpression(getParamsExpression, "containsKey", paramsContainsKeyMethodArguments), Map.class));
final Statement initializeParameterStatement = new ExpressionStatement(
new DeclarationExpression(new VariableExpression(
methodParamName, paramTypeClassNode),
Token.newSymbol(Types.EQUALS, 0, 0),
new TernaryExpression(containsKeyExpression, buildGetMapExpression(getParamsExpression, requestParameterName) , new ConstantExpression(null))));
wrapper.addStatement(initializeParameterStatement);
}
protected void initializePrimitiveOrTypeWrapperParameter(final ClassNode classNode, final BlockStatement wrapper,
final Parameter param, final String requestParameterName) {
final ClassNode paramTypeClassNode = param.getType();
final String methodParamName = param.getName();
final Expression defaultValueExpression;
if (paramTypeClassNode.equals(ClassHelper.Boolean_TYPE)) {
defaultValueExpression = new ConstantExpression(false);
} else if (PRIMITIVE_CLASS_NODES.contains(paramTypeClassNode)) {
defaultValueExpression = new ConstantExpression(0);
} else {
defaultValueExpression = new ConstantExpression(null);
}
final ConstantExpression paramConstantExpression = new ConstantExpression(requestParameterName);
final Expression paramsTypeConversionMethodArguments = new ArgumentListExpression(
paramConstantExpression/*, defaultValueExpression*/, new ConstantExpression(null));
final String conversionMethodName;
if (TYPE_WRAPPER_CLASS_TO_CONVERSION_METHOD_NAME.containsKey(paramTypeClassNode)) {
conversionMethodName = TYPE_WRAPPER_CLASS_TO_CONVERSION_METHOD_NAME.get(paramTypeClassNode);
} else {
conversionMethodName = paramTypeClassNode.getName();
}
Expression getParamsExpression = buildGetPropertyExpression(new VariableExpression("this"), "params", classNode);
final MethodCallExpression retrieveConvertedValueExpression = new MethodCallExpression(
getParamsExpression, conversionMethodName, paramsTypeConversionMethodArguments);
Class> defaultValueClass = null; // choose any
if("char".equals(conversionMethodName)) {
// TypeConvertingMap.'char' method has 2 different signatures, choose the one with "Character 'char'(String name, Integer defaultValue)" signature
defaultValueClass = Integer.class;
}
applyMethodTarget(retrieveConvertedValueExpression, TypeConvertingMap.class, null, defaultValueClass);
final Expression paramsContainsKeyMethodArguments = new ArgumentListExpression(paramConstantExpression);
final BooleanExpression containsKeyExpression = new BooleanExpression(
applyDefaultMethodTarget(new MethodCallExpression(getParamsExpression, "containsKey", paramsContainsKeyMethodArguments), Map.class));
final Token equalsToken = Token.newSymbol(Types.EQUALS, 0, 0);
final VariableExpression convertedValueExpression = new VariableExpression(
"___converted_" + methodParamName, new ClassNode(Object.class));
final DeclarationExpression declareConvertedValueExpression = new DeclarationExpression(
convertedValueExpression, equalsToken, new EmptyExpression());
Statement declareVariableStatement = new ExpressionStatement(declareConvertedValueExpression);
wrapper.addStatement(declareVariableStatement);
final VariableExpression methodParamExpression = new VariableExpression(
methodParamName, paramTypeClassNode);
final DeclarationExpression declareParameterVariableStatement = new DeclarationExpression(
methodParamExpression, equalsToken, new EmptyExpression());
declareVariableStatement = new ExpressionStatement(declareParameterVariableStatement);
wrapper.addStatement(declareVariableStatement);
final Expression assignmentExpression = new BinaryExpression(
convertedValueExpression, equalsToken,
new TernaryExpression(containsKeyExpression, retrieveConvertedValueExpression, defaultValueExpression));
wrapper.addStatement(new ExpressionStatement(assignmentExpression));
Expression rejectValueMethodCallExpression = getRejectValueExpression(classNode, methodParamName);
BlockStatement ifConvertedValueIsNullBlockStatement = new BlockStatement();
ifConvertedValueIsNullBlockStatement.addStatement(
new ExpressionStatement(rejectValueMethodCallExpression));
ifConvertedValueIsNullBlockStatement.addStatement(
new ExpressionStatement(new BinaryExpression(
methodParamExpression, equalsToken, defaultValueExpression)));
final BooleanExpression isConvertedValueNullExpression = new BooleanExpression(new BinaryExpression(
convertedValueExpression, Token.newSymbol(Types.COMPARE_EQUAL, 0, 0),
new ConstantExpression(null)));
final ExpressionStatement assignConvertedValueToParamStatement = new ExpressionStatement(
new BinaryExpression(methodParamExpression, equalsToken, convertedValueExpression));
final Statement ifStatement = new IfStatement(isConvertedValueNullExpression,
ifConvertedValueIsNullBlockStatement,
assignConvertedValueToParamStatement);
wrapper.addStatement(new IfStatement(new BooleanExpression(containsKeyExpression),
ifStatement, new ExpressionStatement(new EmptyExpression())));
}
protected Expression getRejectValueExpression(final ClassNode classNode, final String methodParamName) {
ArgumentListExpression rejectValueArgs = new ArgumentListExpression();
rejectValueArgs.addExpression(new ConstantExpression(methodParamName));
rejectValueArgs.addExpression(new ConstantExpression(
"params." + methodParamName + ".conversion.error"));
Expression getErrorsExpression = buildGetPropertyExpression(new VariableExpression("this", classNode), "errors", classNode);
Expression rejectValueMethodCallExpression = applyDefaultMethodTarget(new MethodCallExpression(
getErrorsExpression, "rejectValue", rejectValueArgs), Errors.class);
return rejectValueMethodCallExpression;
}
public void performInjection(SourceUnit source, ClassNode classNode) {
performInjection(source, null, classNode);
}
public boolean shouldInject(URL url) {
return url != null && CONTROLLER_PATTERN.matcher(url.getFile()).find();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy