org.grails.ide.eclipse.editor.actions.UrlMappingHyperlinkDetector.java Source code

Java tutorial

Introduction

Here is the source code for org.grails.ide.eclipse.editor.actions.UrlMappingHyperlinkDetector.java

Source

/*******************************************************************************
 * 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
 * https://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;
    }
}