/******************************************************************************* * 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.elements; import groovyjarjarasm.asm.Opcodes; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.jdt.groovy.model.GroovyCompilationUnit; import org.eclipse.core.resources.IFolder; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.groovy.search.AbstractSimplifiedTypeLookup.TypeAndDeclaration; import org.eclipse.jdt.groovy.search.TypeLookupResult.TypeConfidence; import org.eclipse.jdt.groovy.search.VariableScope; import org.grails.ide.eclipse.core.internal.plugins.GrailsCore; import org.grails.ide.eclipse.core.internal.plugins.GrailsElementKind; import org.grails.ide.eclipse.core.internal.plugins.PerProjectPluginCache; import org.grails.ide.eclipse.core.model.ContributedMethod; import org.grails.ide.eclipse.core.model.ContributedProperty; import org.grails.ide.eclipse.core.util.GrailsNameUtils; import org.grails.ide.eclipse.editor.groovy.controllers.PerProjectControllerCache; import org.grails.ide.eclipse.editor.groovy.types.PerProjectMemberCache; /** * @author Andrew Eisenberg * @author Christian Dupuis * @author Nieraj Singh * @created Nov 23, 2009 */ @SuppressWarnings("nls") public class ControllerClass extends AbstractGrailsElement implements INavigableGrailsElement { /** * A wrapper around object class that knows about the return values of the controller action */ class ControllerActionClass extends ClassNode { private final String action; private List<FieldNode> extraFields; public ControllerActionClass(String action) { super(VariableScope.MAP_CLASS_NODE.getName(), VariableScope.MAP_CLASS_NODE.getModifiers(), VariableScope.MAP_CLASS_NODE.getSuperClass()); this.action = action; this.isPrimaryNode = false; this.setRedirect(VariableScope.MAP_CLASS_NODE); if (cache == null) { cache = GrailsCore.get().connect(ControllerClass.this.getCompilationUnit().getJavaProject().getProject(), PerProjectControllerCache.class); } } @Override public FieldNode getField(String name) { List<FieldNode> fields = internalGetFields(); for (FieldNode field : fields) { if (field.getName().equals(name)) { return field; } } return null; } @Override public List<FieldNode> getFields() { return internalGetFields(); } @Override public FieldNode getDeclaredField(String name) { return getField(name); } @Override public PropertyNode getProperty(String name) { FieldNode field = getField(name); if (field != null) { PropertyNode propertyNode = new PropertyNode(field, ACC_PUBLIC, null, null); propertyNode.setDeclaringClass(getGroovyClass()); return propertyNode; } else { return null; } } @Override public List<PropertyNode> getProperties() { // consider caching List<FieldNode> fields = internalGetFields(); List<PropertyNode> properties = new ArrayList<PropertyNode>(fields.size()); for (FieldNode field : fields) { PropertyNode p = new PropertyNode(field, ACC_PUBLIC, null, null); p.setDeclaringClass(getGroovyClass()); properties.add(p); } return properties; } private List<FieldNode> internalGetFields() { if (extraFields == null) { // prevent infinite recursion by setting to empty list first extraFields = Collections.emptyList(); if (cache != null) { Map<String, ClassNode> mappedValues = cache.findReturnValuesForAction(getLogicalName(), action); if (mappedValues != null) { extraFields = new ArrayList<FieldNode>(mappedValues.size()); for (Entry<String, ClassNode> entry : mappedValues.entrySet()) { String name = entry.getKey(); ClassNode type = entry.getValue(); FieldNode f = new FieldNode(name, ACC_PUBLIC, type, getGroovyClass(), null); f.setDeclaringClass(getGroovyClass()); extraFields.add(f); } } } } return extraFields; } } private static final Set<String> extraMethods = new HashSet<String>(); public static final String CONTROLLER = "Controller"; static { extraMethods.add("getTemplateUri"); extraMethods.add("getViewUri"); extraMethods.add("bindData"); extraMethods.add("chain"); extraMethods.add("render"); extraMethods.add("redirect"); // standard actions...make sure they exist extraMethods.add("show"); extraMethods.add("create"); extraMethods.add("list"); extraMethods.add("index"); } private DomainClass cachedDomainClass; private PerProjectPluginCache pluginCache; private PerProjectMemberCache memberCache; private PerProjectControllerCache cache; ControllerClass(GroovyCompilationUnit unit) { super(unit); pluginCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectPluginCache.class); memberCache = GrailsCore.get().connect(unit.getJavaProject().getProject(), PerProjectMemberCache.class); } public GrailsElementKind getKind() { return GrailsElementKind.CONTROLLER_CLASS; } public DomainClass getDomainClass() { if (cachedDomainClass != null) { return cachedDomainClass; } String origName = unit.getElementName(); return DomainClass.getDomainClassForElement(unit, origName.substring(0,origName.lastIndexOf(CONTROLLER))); } public TagLibClass getTagLibClass() { String origName = unit.getElementName(); return TagLibClass.getTagLibClassForElement(unit, origName.substring(0,origName.lastIndexOf(CONTROLLER))); } public ServiceClass getServiceClass() { String origName = unit.getElementName(); return ServiceClass.getServiceClassForElement(unit, origName.substring(0,origName.lastIndexOf(CONTROLLER))); } public TestClass getTestClass() { return TestClass.getTestClassForElement(this, unit, getPrimaryTypeName()); } public ControllerClass getControllerClass() { return this; } public Map<String, ClassNode> getExtraControllerReferences() { return memberCache.getExtraControllerReferences(); } public boolean isSpecialMethodReference(String name) { return extraMethods.contains(name); } public TypeAndDeclaration lookupTypeAndDeclaration(ClassNode declaringType, String name, VariableScope scope) { // if this is a controller action, then must wrap the result in // a special type that knows the return values of the action AnnotatedNode action = this.getControllerAction(name); if (action != null) { return new TypeAndDeclaration(new ControllerActionClass(name), action, getGroovyClass(), "Controller Action", // must override the default LOOSELY_INFERRED confidence TypeConfidence.INFERRED); } // special controller references Map<String, ClassNode> extraControllerReferences = getExtraControllerReferences(); ClassNode type = extraControllerReferences.get(name); if (type != null) { AnnotatedNode declaration; if (type.getName().equals(VariableScope.VOID_WRAPPER_CLASS_NODE)) { declaration = new MethodNode(name, Opcodes.ACC_PUBLIC, type, new Parameter[0], new ClassNode[0], null); declaration.setDeclaringClass(getGroovyClass()); } else { declaration = new FieldNode(name, Opcodes.ACC_PUBLIC, type, getGroovyClass(), null); declaration.setDeclaringClass(getGroovyClass()); } return new TypeAndDeclaration(type, declaration, declaringType); } // Only used in 1.3.7 and earlier Map<String, Set<ContributedMethod>> contributedMethods = pluginCache.getAllControllerMethods(); if (contributedMethods.containsKey(name)) { ClassNode returnType = contributedMethods.get(name).iterator().next().getReturnType(); if (returnType == null) { returnType = getGroovyClass(); if (returnType == null) { returnType = VariableScope.OBJECT_CLASS_NODE; } } return new TypeAndDeclaration(returnType == null ? getGroovyClass() : returnType, declaringType); } // Only used in 1.3.7 and earlier // now look at properties and methods contributed by plugins Map<String, ContributedProperty> contributedProperties = pluginCache.getAllControllerProperties(); if (contributedProperties.containsKey(name)) { ClassNode contribType = contributedProperties.get(name).getType(); if (contribType == null) { contribType = getGroovyClass(); if (contribType == null) { contribType = VariableScope.OBJECT_CLASS_NODE; } } return new TypeAndDeclaration(contribType, declaringType); } return null; } public Map<String, Set<ContributedMethod>> getAllContributedMethods() { return pluginCache.getAllControllerMethods(); } public Map<String, ContributedProperty> getAllContributedProperties() { return pluginCache.getAllControllerProperties(); } public void initializeTypeLookup(VariableScope scope) { ClassNode node = getGroovyClass(); if (node != null) { populateInjectedServices(scope); for (Entry<String, ClassNode> entry : memberCache.getExtraControllerReferences().entrySet()) { scope.addVariable(entry.getKey(), entry.getValue(), node); } } } public IFolder getGSPFolder() { DomainClass d = getDomainClass(); return d != null ? d.getGSPFolder() : null; } public String getAssociatedDomainClassName() { String className = getClassName(); int cIndex = className.lastIndexOf(CONTROLLER); className = className.substring(0, cIndex); return className; } public String getClassName() { return getGroovyClass().getName(); } public String getLogicalName() { return GrailsNameUtils.getLogicalPropertyName(getClassName(), CONTROLLER); } public IType getType() throws JavaModelException { String className = getClassName(); for (IType candidate : unit.getAllTypes()) { if (candidate.getFullyQualifiedName().equals(className)) { return candidate; } } return null; } /** * Finds the controller action with the given name. Looks for both method style and property style actions. * @param name the action name * @return {@link AnnotatedNode} corresponding to the action declaration, or null if not an action */ public AnnotatedNode getControllerAction(String name) { List<MethodNode> methods = getGroovyClass().getMethods(name); if (methods != null && methods.size() > 0) { // arbitrarily choose first method. I don't think it is possible to have overloaded controller actions MethodNode method = methods.get(0); if (method != null && !method.isStatic() && method.getReturnType().equals(VariableScope.OBJECT_CLASS_NODE)) { return method; } } PropertyNode property = getGroovyClass().getProperty(name); if (property != null && !property.isStatic() && !isServiceReference(property) && property.getType().equals(VariableScope.OBJECT_CLASS_NODE) && property.getInitialExpression() instanceof ClosureExpression) { return property; } return null; } public boolean isControllerAction(String name) { return getControllerAction(name) != null; } /** * Finds a corresponding Controller class for the given type name * @param unit {@link ICompilationUnit} of the original class * @param typeName simple name of the original class * @return a corresponding Controller class */ public static ControllerClass getControllerClassForElement(ICompilationUnit unit, String typeName) { String controllerName = typeName + "Controller.groovy"; //$NON-NLS-1$ String packageName = unit.getParent().getElementName(); IJavaProject javaProject = unit.getJavaProject(); GrailsProject gp = GrailsWorkspaceCore.get().create(javaProject); return gp.getControllerClass(packageName, controllerName); } }