/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.grails.ide.eclipse.editor.groovy.controllers; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.expr.Expression; 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.NamedArgumentListExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.TupleExpression; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.groovy.search.ITypeRequestor; import org.eclipse.jdt.groovy.search.TypeInferencingVisitorFactory; import org.eclipse.jdt.groovy.search.TypeInferencingVisitorWithRequestor; import org.eclipse.jdt.groovy.search.TypeLookupResult; import org.grails.ide.eclipse.core.GrailsCoreActivator; /** * Keeps track of a single controller class, all of its actions, and the names/inferred types of the return values * @author Andrew Eisenberg * @since 2.7.0 */ public class ControllerCache { private class ActionReturnValueRequestor implements ITypeRequestor { private final IMember targetMember; private MapExpression targetMapExpression; Map<String, ClassNode> returnValues; List<String> redirects = null; // FIXADE should be Set private Map<Expression, String> reverse; public ActionReturnValueRequestor(IMember targetMember, Map<String, ClassNode> returnValues) { this.targetMember = targetMember; this.returnValues = returnValues; } public VisitStatus acceptASTNode(ASTNode node, TypeLookupResult result, IJavaElement enclosingElement) { if (enclosingElement.getElementType() != targetMember.getElementType()) { return VisitStatus.CONTINUE; } if (!enclosingElement.equals(targetMember)) { return VisitStatus.CANCEL_MEMBER; } // assume only one return statement if (node instanceof ReturnStatement) { Expression returnExpr = ((ReturnStatement) node).getExpression(); if (returnExpr instanceof MapExpression && ((MapExpression) returnExpr).getMapEntryExpressions() != null) { targetMapExpression = (MapExpression) returnExpr; reverse = new HashMap<Expression, String>(targetMapExpression.getMapEntryExpressions().size()*2); for (MapEntryExpression expr : targetMapExpression.getMapEntryExpressions()) { Expression valueExpression = expr.getValueExpression(); // navigate down to the actual expression that we are interested in while (valueExpression instanceof MethodCallExpression || valueExpression instanceof PropertyExpression) { if (valueExpression instanceof MethodCallExpression) { valueExpression = ((MethodCallExpression) valueExpression).getMethod(); } else if (valueExpression instanceof PropertyExpression) { valueExpression = ((PropertyExpression) valueExpression).getProperty(); } } reverse.put(valueExpression, expr.getKeyExpression().getText()); } } } else if (node instanceof MethodCallExpression) { MethodCallExpression call = (MethodCallExpression) node; if (call.getMethodAsString().equals("redirect")) { // remember the redirect call so that we can traverse the redirect target later Expression args = call.getArguments(); if (args instanceof TupleExpression && ((TupleExpression) args).getExpression(0) instanceof NamedArgumentListExpression) { NamedArgumentListExpression named = (NamedArgumentListExpression) ((TupleExpression) args).getExpression(0); MapEntryExpression entry = named.getMapEntryExpressions().get(0); if (entry.getKeyExpression().getText().equals("action")) { if (redirects == null) { redirects = new LinkedList<String>(); } redirects.add(entry.getValueExpression().getText()); } } } } else if (targetMapExpression != null) { String currentValue = reverse.get(node); if (currentValue != null) { returnValues.put(currentValue, result.type); } } return VisitStatus.CONTINUE; } } private final GroovyCompilationUnit controllerUnit; private final IType controllerType; private Map<String, Map<String, ClassNode>> actionToReturnValue = new HashMap<String, Map<String, ClassNode>>(); public ControllerCache(GroovyCompilationUnit controllerUnit) { this.controllerUnit = controllerUnit; this.controllerType = controllerUnit.findPrimaryType(); } /** * Returns all the return parameters and inferred types for this controller action. Will return null * if the aciton name cannot be found. * @param actionName * @return */ public Map<String, ClassNode> findReturnValuesForAction(String actionName) { Map<String, ClassNode> returnValues = actionToReturnValue.get(actionName); if (returnValues == null) { returnValues = calculateValuesForAction(actionName); } return returnValues; } /** * Here is where the real work gets done. * 1. get the module node * 2. find the relevant action name * 3. run the inferencing engine down that action * 4. make assumption that the last return statement is the one we are interested in * assume that it will be in the form of a {@link MapEntryExpression}. * assume that the keys are strings and the values have inferred types. * @param actionName * @return */ private Map<String, ClassNode> calculateValuesForAction(String actionName) { if (controllerType == null || !controllerType.exists()) { return Collections.emptyMap(); } Map<String, ClassNode> existing = new HashMap<String, ClassNode>(); actionToReturnValue.put(actionName, existing); // Here is a little problem. // in Grails 1.3.7 and earlier, controller actions were defined as fields with closure initializers // in Grails 2.0 and later, they are no-arg methods. // Try looking for both kinds here. First look for method, and if not found, then go for field. // If a controller class has both, then that's a problem. IMember actionMember = findMember(actionName); if (actionMember == null) { return Collections.emptyMap(); } ActionReturnValueRequestor requestor = new ActionReturnValueRequestor(actionMember, existing); TypeInferencingVisitorWithRequestor visitor = new TypeInferencingVisitorFactory().createVisitor(controllerUnit); visitor.visitCompilationUnit(requestor); if (requestor.redirects != null) { for (String redirectedAction : requestor.redirects) { existing.putAll(findReturnValuesForAction(redirectedAction)); } } if (requestor.returnValues != null) { existing.putAll(requestor.returnValues); } return existing; } /** * Finds the associated {@link IJavaElement} for this action name. * @param actionName * @return */ private IMember findMember(String actionName) { // first go for method...can have arbitrary number of parameters IMember member; try { IMethod[] allMethods = controllerType.getMethods(); member = null; for (IMethod maybeMethod : allMethods) { if (maybeMethod.getElementName().equals(actionName)) { member = maybeMethod; break; } } if (member != null) { return member; } } catch (JavaModelException e) { GrailsCoreActivator.log(e); } member = controllerType.getField(actionName); if (member.exists()) { return member; } return null; } }