/*******************************************************************************
* 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.actions;
import java.util.List;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
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.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.ui.texteditor.ITextEditor;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.editor.groovy.elements.GrailsWorkspaceCore;
/**
* A hyperlink detector for the URL Mappings file. See {@link #findLink(Statement, int, GroovyCompilationUnit)}
* for a list of links that we look for.
*
* @author Andrew Eisenberg
* @since 2.8.0
*/
public class UrlMappingHyperlinkDetector extends AbstractHyperlinkDetector {
private class NameRegion {
final String name;
final Region region;
NameRegion(String name, Region region) {
super();
this.name = name;
this.region = region;
}
}
public IHyperlink[] detectHyperlinks(ITextViewer textViewer,
IRegion region, boolean canShowMultipleHyperlinks) {
ITextEditor textEditor= (ITextEditor)getAdapter(ITextEditor.class);
if (region == null || !(textEditor instanceof JavaEditor)) {
return null;
}
// IAction openAction= textEditor.getAction("OpenEditor"); //$NON-NLS-1$
// if (!(openAction instanceof SelectionDispatchAction)) {
// return null;
// }
//
ITypeRoot input= EditorUtility.getEditorInputJavaElement(textEditor, false);
if (input == null) {
return null;
}
if (! (input instanceof GroovyCompilationUnit)) {
return null;
}
IResource resource = input.getResource();
// we could get more specific and check to make sure that the file is
// in the proper package and source folder and in a grails project, but I think
// it is useful here to have this functionality more widely available.
if (resource == null || !resource.getName().equals("UrlMappings.groovy")) {
return null;
}
GroovyCompilationUnit unit = (GroovyCompilationUnit) input;
ModuleNode moduleNode = unit.getModuleNode();
if (moduleNode == null) {
return null;
}
ClassNode mappingClass = findMappingsClass(moduleNode);
if (mappingClass == null) {
return null;
}
FieldNode mappings = mappingClass.getField("mappings");
if (mappings == null) {
return null;
}
int offset= region.getOffset();
if (mappings.getStart() > offset || mappings.getEnd() < offset) {
return null;
}
Expression expression = mappings.getInitialExpression();
if (! (expression instanceof ClosureExpression)) {
return null;
}
Statement body = ((ClosureExpression) expression).getCode();
if (! (body instanceof BlockStatement) || ((BlockStatement) body).getStatements() == null) {
return null;
}
// now we know that we have a hyperlink request inside of a UrlMappings.mapping field.
// we can do the real work now.
return findMappingLinks(((BlockStatement) body).getStatements(), offset, unit);
}
private IHyperlink[] findMappingLinks(List<Statement> statements, int offset, GroovyCompilationUnit unit) {
for (Statement statement : statements) {
IHyperlink link = findLink(statement, offset, unit);
if (link != null) {
return new IHyperlink[] { link };
}
}
return null;
}
/**
* Handle these kinds of links:
* <pre>
* "/product"(controller:"product", action:"list") // link support to the controller and the action
* "/product"(controller:"product") // link support only for the controller
* "/help"(controller:"site",view:"help") // link support to the controller and to the view (and maybe to the action as well)
* "403"(view: "/errors/forbidden" // link support to the view
* name personList: "/showPeople" {
* controller = 'person' // link support to the controller
* action = 'list' // link support to the action
* }
* "/showPeople" {
* controller = 'person' // link support to the controller
* action = 'list' // link support to the action
* }
* "/product/$id"(controller:"product"){
* action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
* }
* </pre>
* @param statements
* @param offset
* @return
*/
private IHyperlink findLink(Statement statement, int offset, GroovyCompilationUnit unit) {
if (! (statement instanceof ExpressionStatement)) {
return null;
}
Expression expr = ((ExpressionStatement) statement).getExpression();
if (expr.getStart() > offset || expr.getEnd() < offset) {
return null;
}
if (expr instanceof MethodCallExpression) {
MethodCallExpression call = (MethodCallExpression) expr;
Expression args = call .getArguments();
if (! (args instanceof TupleExpression) || ((TupleExpression) args).getExpressions().size() == 0) {
return null;
}
TupleExpression tuple = (TupleExpression) args;
Expression firstArg = tuple.getExpression(0);
Expression lastArg = tuple.getExpression(tuple.getExpressions().size()-1);
NameRegion[] components;
if (lastArg instanceof ClosureExpression && firstArg == lastArg) {
/* we have something like this:
* "/showPeople" {
* controller = 'person' // link support to the controller
* action = 'list' // link support to the action
* }
*/
components = findLinkComponentsInClosure((ClosureExpression) lastArg, offset);
} else if (firstArg instanceof MapExpression) {
List<MapEntryExpression> mapEntryExpressions = ((MapExpression) firstArg).getMapEntryExpressions();
if (mapEntryExpressions.size() > 0 && mapEntryExpressions.get(0).getValueExpression() instanceof MethodCallExpression) {
MethodCallExpression innerCall = (MethodCallExpression) mapEntryExpressions.get(0).getValueExpression();
if (innerCall.getArguments() instanceof ArgumentListExpression && ((ArgumentListExpression) innerCall.getArguments()).getExpressions().size() == 1 && ((ArgumentListExpression) innerCall.getArguments()).getExpression(0) instanceof ClosureExpression) {
/* we have something like this:
* name showPeople: "/showPeople" {
* controller = 'person' // link support to the controller
* action = 'list' // link support to the action
* }
*/
components = findLinkComponentsInClosure((ClosureExpression) ((ArgumentListExpression) innerCall.getArguments()).getExpression(0), offset);
} else {
components = null;
}
} else {
/* we have something like this:
* "/product"(controller:"product", action:"list") // link support to the controller and the action
*/
components = findLinkComponentsInCall((MapExpression) firstArg, offset);
if (components != null && lastArg instanceof ClosureExpression) {
/* we have something like this:
* "/product/$id"(controller:"product"){
* action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
* }
*/
finishComponents((ClosureExpression) lastArg, components, offset);
}
}
} else {
components = null;
}
if (components != null) {
NameRegion controllerNameRegion = components[0];
NameRegion actionNameRegion = components[1];
NameRegion viewNameRegion = components[2];
// may as well link to all possibilities here
IHyperlink link = null;
if (controllerNameRegion != null) {
IType type = findController(controllerNameRegion.name, unit.getJavaProject());
if (type != null && type.exists()) {
// action name should go first
if (actionNameRegion != null) {
IMember action = findAction(type, actionNameRegion.name);
if (actionNameRegion.region != null && action != null && action.exists()) {
link = new JavaElementHyperlink(actionNameRegion.region, action);
}
}
if (controllerNameRegion.region != null) {
link = new JavaElementHyperlink(controllerNameRegion.region, type);
}
}
}
if (viewNameRegion != null && viewNameRegion.region != null) {
String viewName = viewNameRegion.name;
// add a slash
if (viewName.charAt(0) != '/') {
viewName = "/" + viewName;
}
// add controller name
if (controllerNameRegion != null && ! viewName.startsWith("/" + controllerNameRegion.name + "/")) {
viewName = "/" + controllerNameRegion.name + viewName;
}
// add prefix
if (!viewName.endsWith(".gsp")) {
viewName = viewName + ".gsp";
}
IFile file = unit.getJavaProject().getProject().getFile("grails-app/views" + viewName);
if (file.exists()) {
link = new WorkspaceFileHyperlink(viewNameRegion.region, file);
}
}
return link;
}
}
return null;
}
/**
* find this kind of mapping:
* action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
*
* @param lastArg
* @param components
*/
private void finishComponents(ClosureExpression lastArg,
NameRegion[] components, int offset) {
if (! (lastArg.getCode() instanceof BlockStatement)) {
return;
}
BlockStatement block = (BlockStatement) lastArg.getCode();
for (Statement s : block.getStatements()) {
if (s.getStart() < offset && s.getEnd() > offset) {
if (s instanceof ExpressionStatement) {
Expression expr = ((ExpressionStatement) s).getExpression();
if (expr instanceof BinaryExpression && ((BinaryExpression) expr).getOperation().getText().equals("=")) {
BinaryExpression bexpr = (BinaryExpression) expr;
String mapping = null;
if (bexpr.getLeftExpression().getText().equals("action")) {
mapping = "action";
} else if (bexpr.getLeftExpression().getText().equals("view")) {
mapping = "view";
}
if (mapping != null && bexpr.getRightExpression() instanceof MapExpression) {
MapExpression mexpr = (MapExpression) bexpr.getRightExpression();
for (MapEntryExpression entry : mexpr.getMapEntryExpressions()) {
Expression value = entry.getValueExpression();
if (value.getStart() <= offset && value.getEnd() >= offset) {
NameRegion nr = new NameRegion(value.getText(), new Region(value.getStart(), value.getLength()));
if (mapping.equals("action")) {
components[1] = nr;
} else {
components[2] = nr;
}
}
}
}
}
}
}
}
}
private NameRegion[] findLinkComponentsInClosure(ClosureExpression firstArg,
int offset) {
if (! (firstArg.getCode() instanceof BlockStatement)) {
return null;
}
BlockStatement code = (BlockStatement) firstArg.getCode();
if (code.getStatements() == null) {
return null;
}
NameRegion controllerName = null;
NameRegion actionName = null;
NameRegion viewName = null;
for (Statement state : code.getStatements()) {
if (state instanceof ExpressionStatement) {
if (((ExpressionStatement) state).getExpression() instanceof BinaryExpression) {
BinaryExpression bexpr = (BinaryExpression) ((ExpressionStatement) state).getExpression();
Expression left = bexpr.getLeftExpression();
if (bexpr.getOperation().getText().equals("=") && left instanceof VariableExpression) {
Expression right = bexpr.getRightExpression();
Region region;
if (right.getStart() <= offset && right.getEnd() >= offset) {
region = new Region(right.getStart(), right.getLength());
} else {
region = null;
}
String name = left.getText();
if (name.equals("controller")) {
controllerName = new NameRegion(right.getText(), region);
} else if (name.equals("action")) {
actionName = new NameRegion(right.getText(), region);
} else if (name.equals("view")) {
viewName = new NameRegion(right.getText(), region);
}
}
}
}
}
return new NameRegion[] { controllerName, actionName, viewName };
}
private NameRegion[] findLinkComponentsInCall(MapExpression arguments, int offset) {
NameRegion controllerName = null;
NameRegion actionName = null;
NameRegion viewName = null;
List<MapEntryExpression> entries = arguments.getMapEntryExpressions();
for (MapEntryExpression entry : entries) {
Expression value = entry.getValueExpression();
Region region;
if (value.getStart() <= offset && value.getEnd() >= offset) {
region = new Region(value.getStart(), value.getLength());
} else {
region = null;
}
Expression key = entry.getKeyExpression();
String text = key.getText();
if ("controller".equals(text)) {
controllerName = new NameRegion(value.getText(), region);
} else if ("action".equals(text)) {
actionName = new NameRegion(value.getText(), region);
} else if ("view".equals(text)) {
viewName = new NameRegion(value.getText(), region);
}
}
return new NameRegion[] { controllerName, actionName, viewName };
}
private IType findController(String controllerName, IJavaProject project) {
try {
return GrailsWorkspaceCore.get().create(project).findControllerFromSimpleName(controllerName);
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
return null;
}
private IMember findAction(IType type, String actionName) {
try {
for (IJavaElement child : type.getChildren()) {
if (child.getElementName().equals(actionName)) {
// assume that the first time we find an element with the name, then we have found our match.
return (IMember) child;
}
}
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
return null;
}
private ClassNode findMappingsClass(ModuleNode moduleNode) {
List<ClassNode> classes = moduleNode.getClasses();
for (ClassNode clazz : classes) {
if (clazz.getNameWithoutPackage().equals("UrlMappings")) {
return clazz;
}
}
return null;
}
}