/*******************************************************************************
* 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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
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.IGrailsProjectInfo;
import org.grails.ide.eclipse.editor.groovy.elements.ControllerClass;
import org.grails.ide.eclipse.editor.groovy.elements.GrailsWorkspaceCore;
import org.grails.ide.eclipse.editor.groovy.types.PerProjectTypeCache;
/**
* Keeps track of controllers and their actions so that gsp views can
* use their contents to help with type inferencing
*
* @author Andrew Eisenberg
* @since 2.7.0
*/
public class PerProjectControllerCache implements IGrailsProjectInfo {
private class TargetSet {
public TargetSet(ControllerTarget target, Set<ActionTarget> actions) {
this.target = target;
this.actions = actions;
}
final ControllerTarget target;
final Set<ActionTarget> actions;
}
private IProject project;
private IJavaProject javaProject;
// view folder name to a controller cache
private Map<String, ControllerCache> controllerCaches = new HashMap<String, ControllerCache>();
// view folder name to set of field (action) names
private Map<String, TargetSet> controllerToTargets = null;
public void dispose() {
synchronized (GrailsCore.get().getLockForProject(project)) {
project = null;
controllerCaches = null;
controllerToTargets = null;
}
}
public IProject getProject() {
return project;
}
public void setProject(IProject project) {
this.project = project;
if (project != null) {
javaProject = JavaCore.create(project);
}
}
public void projectChanged(GrailsElementKind[] changeKinds,
IResourceDelta change) {
synchronized (GrailsCore.get().getLockForProject(project)) {
boolean foundRelevantChange = false;
for (GrailsElementKind changeKind : changeKinds) {
if (changeKind == GrailsElementKind.CONTROLLER_CLASS) {
foundRelevantChange = true;
break;
}
}
if (foundRelevantChange) {
controllerCaches.clear();
controllerToTargets = null;
}
}
}
public Map<String, ClassNode> findReturnValuesForAction(String viewFolderName, String actionName) {
if (project == null) {
return null;
}
if (actionName.endsWith(".gsp")) {
actionName = actionName.substring(0, actionName.length()-".gsp".length());
}
synchronized (GrailsCore.get().getLockForProject(project)) {
ControllerCache controllerCache = controllerCaches.get(viewFolderName);
if (controllerCache == null) {
controllerCache = createControllerCache(viewFolderName);
if (controllerCache != null) {
controllerCaches.put(viewFolderName, controllerCache);
}
}
if (controllerCache == null) {
return null;
}
Map<String, ClassNode> values = controllerCache.findReturnValuesForAction(actionName);
return values;
}
}
public String findReturnValuesAsDeclarations(String viewFolderName, String actionName) {
// don't do anything for the views at the root view folder
if (viewFolderName.equals("views")) {
return "";
}
Map<String, ClassNode> valuesForAction = findReturnValuesForAction(viewFolderName, actionName);
if (valuesForAction == null) {
return "";
}
StringBuilder sb = new StringBuilder();
for (Entry<String, ClassNode> entry : valuesForAction.entrySet()) {
String className = extractClassName(entry.getValue());
sb.append(className + " " + entry.getKey() + ";\n");
}
return sb.toString();
}
private String extractClassName(ClassNode clazz) {
StringBuilder sb = new StringBuilder();
ClassNode componentType = clazz;
int arrayCount = 0;
while (componentType.getComponentType() != null) {
arrayCount++;
componentType = componentType.getComponentType();
}
// use the wrapper class since it is not legal to have a primitive type as a type argument
sb.append(ClassHelper.getWrapper(componentType).getName());
// now look for type parameters
GenericsType[] tps = clazz.getGenericsTypes();
if (tps != null && tps.length > 0) {
StringBuilder sb2 = new StringBuilder();
boolean doit = false;
sb2.append("<");
for (int i = 0; i < tps.length; i++) {
if (tps[i].isPlaceholder()) {
continue;
}
sb2.append(extractClassName(tps[i].getType()));
sb2.append(",");
doit = true;
}
if (doit) {
sb2.replace(sb2.length()-1, sb2.length(), ">");
sb.append(sb2);
}
}
for (int i = 0; i < arrayCount; i++) {
sb.append("[]");
}
return sb.toString();
}
private ControllerCache createControllerCache(String viewFolderName) {
String controllerName = convertToControllerName(viewFolderName);
if (controllerName == null) {
return null;
}
GroovyCompilationUnit controllerUnit = findControllerUnit(controllerName);
if (controllerUnit == null) {
return null;
}
return new ControllerCache(controllerUnit);
}
private String convertToControllerName(String viewFolderName) {
return String.valueOf(Character.toUpperCase(viewFolderName.charAt(0))) + viewFolderName.substring(1) + "Controller.groovy";
}
private GroovyCompilationUnit findControllerUnit(String controllerName) {
ControllerClass cc = GrailsWorkspaceCore.get().create(javaProject).getControllerClass(controllerName);
if (cc != null) {
return (GroovyCompilationUnit) cc.getCompilationUnit();
}
return null;
}
public Set<ControllerTarget> getAllControllerTargets() {
synchronized (GrailsCore.get().getLockForProject(project)) {
ensureInitialized();
Set<ControllerTarget> targets = new HashSet<ControllerTarget>(controllerToTargets.size(), 1);
for (TargetSet controllerSet : controllerToTargets.values()) {
targets.add(controllerSet.target);
}
return targets;
}
}
public Set<ActionTarget> getActionsForController(String controller) {
synchronized (GrailsCore.get().getLockForProject(project)) {
ensureInitialized();
TargetSet targetSet = controllerToTargets.get(controller);
return targetSet == null ? null : targetSet.actions;
}
}
private void ensureInitialized() {
if (controllerToTargets == null) {
try {
controllerToTargets = new HashMap<String, TargetSet>();
Map<String, ClassNode> allControllers = GrailsWorkspaceCore.get().create(javaProject).findAllControllers();
PerProjectTypeCache typeCache = GrailsCore.get().connect(project, PerProjectTypeCache.class);
for (Entry<String, ClassNode> controllerEntry : allControllers.entrySet()) {
ClassNode controller = controllerEntry.getValue();
String controllerName = extractControllerName(controllerEntry);
ControllerTarget target = new ControllerTarget(controller.getName(), controllerName, javaProject);
Set<ActionTarget> actions = new HashSet<ActionTarget>();
for (PropertyNode prop : controller.getProperties()) {
// assume all public fields are actions
if (!prop.isStatic() && prop.isPublic() && !prop.getName().equals("metaClass")) {
actions.add(new ActionTarget(target, prop.getName(), false));
}
}
// in grails 2.0+, controller actions are now methods. Need to look for those, too
for (MethodNode method : controller.getMethods()) {
// assume all public no-arg methods are actions
if (isMethodAction(method) && controller.equals(method.getDeclaringClass())) {
actions.add(new ActionTarget(target, method.getName(), true));
}
}
controllerToTargets.put(controllerName, new TargetSet(target, actions));
// we don't want to keep this in the type cache in case there is a change
typeCache.clearFromCache(controllerEntry.getValue().getName());
}
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
}
}
private boolean isMethodAction(MethodNode method) {
return !method.isStatic() && method.isPublic() &&
!ignoredMethodNames.contains(method.getName()) && !method.getName().contains("$");
}
private final static Set<String> ignoredMethodNames = new HashSet<String>();
static {
ignoredMethodNames.add("getProperty");
ignoredMethodNames.add("setProperty");
ignoredMethodNames.add("getMetaClass");
ignoredMethodNames.add("setMetaClass");
ignoredMethodNames.add("invokeMethod");
}
/**
* @param controllerEntry
* @return
*/
private String extractControllerName(
Entry<String, ClassNode> controllerEntry) {
String controllerName = controllerEntry.getKey();
int contIndex = controllerName.indexOf("Controller");
if (contIndex > 0) {
controllerName = controllerName.substring(0, contIndex);
}
return controllerName;
}
}