org.jboss.byteman.eclipse.validation.BytemanJavaValidator.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.byteman.eclipse.validation.BytemanJavaValidator.java

Source

/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat and individual contributors as identified
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 * 
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 * 
 * @authors Rebecca Simmonds, Andrew Dinn
 */
package org.jboss.byteman.eclipse.validation;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.CheckType;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.core.search.TypeNameRequestor;

import org.jboss.byteman.eclipse.byteman.AccessExpr;
import org.jboss.byteman.eclipse.byteman.BytemanPackage;
import org.jboss.byteman.eclipse.byteman.Actions;
import org.jboss.byteman.eclipse.byteman.ActionList;
import org.jboss.byteman.eclipse.byteman.Action;
import org.jboss.byteman.eclipse.byteman.BytemanRule;
import org.jboss.byteman.eclipse.byteman.EventClass;
import org.jboss.byteman.eclipse.byteman.EventMethod;
import org.jboss.byteman.eclipse.byteman.Expression;
import org.jboss.byteman.eclipse.byteman.AssignmentExpr;
import org.jboss.byteman.eclipse.byteman.IndexedSpecialVar;
import org.jboss.byteman.eclipse.byteman.ParameterTypes;
import org.jboss.byteman.eclipse.byteman.SimpleName;
import org.jboss.byteman.eclipse.byteman.SpecialVar;
import org.jboss.byteman.eclipse.byteman.Binding;

public class BytemanJavaValidator extends AbstractBytemanJavaValidator {

    /**
     * quick check rule to ensure that return or throw actions only occur at
     * the end of the action list
     * @param actions the parse tree element representing DO  clause
     */
    @Check
    public void checkThrowReturnActionAtEnd(Actions actions) {
        ActionList actionList = actions.getActionList();
        if (actionList != null) {
            EList<Action> children = actionList.getActions();
            int length = children.size();
            for (int i = 0; i < length - 1; i++) {
                Action action = children.get(i);
                if (action instanceof Expression) {
                    Expression expression = (Expression) action;
                    String op = expression.getOp();
                    if (op != null) {
                        if (op.equals("return") || op.equals("RETURN")) {
                            error("RETURN expression must be last in DO actions",
                                    BytemanPackage.eINSTANCE.getActions_ActionList(), i);
                        } else if (op.equals("throw") || op.equals("THROW")) {
                            error("THROW expression must be last in DO actions",
                                    BytemanPackage.eINSTANCE.getActions_ActionList(), i);
                        }
                    }
                }
            }
        }
    }

    /**
     * quick check rule to ensure that the left hand side of an assignment operator
     * is an assignable type i.e. a variable or a field or array access
     * @param assignmentExpr an assignment expression occurring in the rule body
     */
    @Check
    public void checkAssignmentOperand1IsAssignable(AssignmentExpr assignmentExpr) {
        Expression operand1 = assignmentExpr.getOperand1();

        String op = operand1.getOp();
        if (operand1 instanceof SpecialVar) {
            String value = ((SpecialVar) operand1).getValue();
            // we can currently assign $foo and $! but not $this nor any other special vars
            if (op.equals("$") && value.equals("this")) {
                error("Special variable $this is not assignable",
                        BytemanPackage.eINSTANCE.getExpression_Operand1());
            } else if (!op.equals("$!")) {
                error("Special variable " + op + " is not assignable",
                        BytemanPackage.eINSTANCE.getExpression_Operand1());
            }
        } else if (operand1 instanceof IndexedSpecialVar) {
            // cannot assign to $0
            String value = ((IndexedSpecialVar) operand1).getOp().substring(1);
            int idx = Integer.valueOf(value);
            if (idx == 0) {
                error("Indexed parameter variable $" + value + " is not assignable",
                        BytemanPackage.eINSTANCE.getExpression_Operand1());
            } else if (idx < 0) {
                error("Invalid index for parameter variable $" + value,
                        BytemanPackage.eINSTANCE.getExpression_Operand1());
            }
        } else if (operand1 instanceof AccessExpr) {
            // array elements and fields are ok as assignment targets but not method calls
            if (op.equals(".") && ((AccessExpr) operand1).getArgs() != null) {
                error("Invalid target for assignment operation", BytemanPackage.eINSTANCE.getExpression_Operand1());
            }
        } else if (operand1 instanceof SimpleName) {
            // assigning a local bound var is ok
            // n.b. ensuring the var is in scope is done as a separate check
        } else {
            // any other LHS expression is invalid 
            error("Invalid target for assignment operation", BytemanPackage.eINSTANCE.getExpression_Operand1());
        }
    }

    /**
     * quick check rule which keeps track of which rule we are in
     * @param rule the current rule
     */
    @Check
    public void recordCurrentRuleName(BytemanRule rule) {
        String name = rule.getName();
        getContext().put(RULE_NAME_KEY, name);
    }

    /**
     * quick check which stashes away the name of all bound variables
     * so we can validate subsequent references to them in the current rule
     * @param binding
     */
    @Check
    public void recordLocalNameIsBound(Binding binding) {
        String name = binding.getBindVariable();
        String currentRule = (String) getContext().get(RULE_NAME_KEY);

        String oldBinding = (String) getContext().put(new ContextKey(name), currentRule);

        // don't allow previous bindings for the same rule
        if (oldBinding == currentRule) {
            error("Duplicate binding for rule variable " + name,
                    BytemanPackage.eINSTANCE.getBinding_BindVariable());
        }
    }

    /**
     * quick check which ensures that simple names are bound before they are used
     * within an expression
     * @param expr
     */
    @Check
    public void checkLocalNameIsBound(SimpleName simpleName) {
        String name = simpleName.getValue();
        String currentRule = (String) getContext().get(RULE_NAME_KEY);
        String binding = (String) getContext().get(new ContextKey(name));
        if (binding == null || binding != currentRule) {
            // hmm, this name may be part of a qualified name identifying a static
            // field or method in which case we cannot actually check this until
            // we are in a position to do typechecking. in any other case though
            // we can flag an error

            // so we need to detect a situation where this simple name is the
            // operand1 of a member access expression i.e. the containing
            // expression is  a term of the form simpleName "." xxx
            // in that case this 
            EStructuralFeature feature = simpleName.eContainingFeature();
            EObject container = simpleName.eContainer();
            boolean okSoFar = (container instanceof AccessExpr);
            if (okSoFar) {
                // ok, so its an access expression but let's check it is a
                // member expresson
                AccessExpr accessExpr = (AccessExpr) container;
                okSoFar = accessExpr.getOp().equals(".");
                if (okSoFar) {
                    okSoFar = (feature == BytemanPackage.eINSTANCE.getExpression_Operand1());
                }
            }

            if (!okSoFar) {
                error("Unknown rule variable " + name, BytemanPackage.eINSTANCE.getSimpleName_Value());
            }
        }
    }

    /**
     * quick check which ensures that the target class or interface is known in the workspace 
     * @param eventClass
     * 
     * TODO -- decide whether we need to make this check NORMAL rather than FAST
     * TODO -- n.b. that will also require making other dependent type checks NORMAL
     */
    @Check(CheckType.FAST)
    public void checkClassClause(EventClass eventClass) {
        boolean isInterface = eventClass.getKeyword().equalsIgnoreCase("INTERFACE");
        String fullName = eventClass.getName();
        String cachedResultKey = (isInterface ? "interface " : "class ") + fullName;
        List<TypeSearchResult> cachedResults = (List<TypeSearchResult>) getContext().get(cachedResultKey);
        if (cachedResults == null) {
            String typeName = fullName;
            String packageName = null;
            // n.b. unlike Java Byteman uses $ to separate embedded subclasses
            int dollarIndex = typeName.lastIndexOf('$');
            int dotIndex = typeName.lastIndexOf('.');
            if (dollarIndex > 0 && dollarIndex < dotIndex) {
                // hmm foo.bar$baz.mumble is not allowed
                // must be foo.bar.baz$mumble
                if (isInterface) {
                    error("invalid trigger interface " + fullName, BytemanPackage.eINSTANCE.getEventClass_Name());
                } else {
                    error("invalid trigger class " + fullName, BytemanPackage.eINSTANCE.getEventClass_Name());
                }
                return;
            }
            if (dollarIndex > 0) {
                // ok, we should be able to search for foo.bar.baz$mumble by passing
                // foo.bar as the package name and baz.mumble as the type name
                // but eclipse is not playing ball. So, we have to pass it foo.bar.baz as the package name and
                // mumble as the type name.
                typeName = typeName.replace('$', '.');
                dotIndex = dollarIndex;
            }

            int len = typeName.length();

            if (dotIndex > 0 && dotIndex < len - 1) {
                packageName = typeName.substring(0, dotIndex);
                typeName = typeName.substring(dotIndex + 1);
            }

            SearchEngine searchEngine = new SearchEngine();
            final List<TypeSearchResult> results = new ArrayList<TypeSearchResult>();
            // if a match contains embedded types and the original used a dot as separator then
            // we have to reject the match since Byteman requires use of $ to identify the embedding
            final boolean acceptEmbeddedTypes = (dollarIndex > 0 || dotIndex < 0);
            TypeNameRequestor requestor = new TypeNameRequestor() {
                public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName,
                        char[][] enclosingTypeNames, String path) {
                    if (acceptEmbeddedTypes || enclosingTypeNames.length == 0) {
                        results.add(new TypeSearchResult(modifiers, packageName, simpleTypeName, enclosingTypeNames,
                                path));
                    }
                }
            };
            char[] packageNameChars = (packageName == null ? null : packageName.toCharArray());
            char[] typeNameChars = typeName.toCharArray();

            IJavaSearchScope searchScope = SearchEngine.createWorkspaceScope();

            try {
                searchEngine.searchAllTypeNames(packageNameChars,
                        SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE, typeNameChars,
                        SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE,
                        (isInterface ? IJavaSearchConstants.INTERFACE : IJavaSearchConstants.CLASS), searchScope,
                        requestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, null);
            } catch (JavaModelException e) {
                // TODO : ho hum probably should not happen but just ignore for now
            }

            // if we have any matches then we are ok otherwise we flag the classname
            // as invalid

            if (results.isEmpty()) {
                if (isInterface) {
                    error("unknown trigger interface " + fullName, BytemanPackage.eINSTANCE.getEventClass_Name());
                } else {
                    error("unknown trigger class " + fullName, BytemanPackage.eINSTANCE.getEventClass_Name());
                }
                // erase any current rule class
                getContext().remove(RULE_CLASS_KEY);
            } else {
                // save this result as the current rule class and cache it
                // so we don't have to repeat the lookup
                getContext().put(RULE_CLASS_KEY, results);
                getContext().put(cachedResultKey, results);
            }
        } else {
            // install this result as the current rule class
            getContext().put(RULE_CLASS_KEY, cachedResults);
        }
    }

    @Check(CheckType.FAST)
    public void checkMethodClause(EventMethod eventMethod) {
        // delete any previous cache of possible trigger methods
        getContext().remove(RULE_METHOD_KEY);

        String methodName = eventMethod.getName();
        // we can only check the method if we already know the possible classes
        List<TypeSearchResult> types = (List<TypeSearchResult>) getContext().get(RULE_CLASS_KEY);
        if (types == null) {
            error("unknown trigger method " + methodName, BytemanPackage.eINSTANCE.getEventMethod_Name());
        } else if (methodName.equals("<clinit>")) {
            // cannot verify presence of <clinit> via search
            // just check it has no arguments
            ParameterTypes parameterTypes = eventMethod.getParameterTypes();
            if (parameterTypes != null) {
                EList<String> paramTypeNames = parameterTypes.getParamTypeNames();
                int paramTypeCount = paramTypeNames.size();

                if (paramTypeCount != 0) {
                    error("invalid parameter types for class initializer" + methodName,
                            BytemanPackage.eINSTANCE.getEventMethod_ParameterTypes());
                }
            }
        } else {
            boolean isConstructor = methodName.equals("<init>");
            // look for methods on each of the possible types
            ParameterTypes parameterTypes = eventMethod.getParameterTypes();
            EList<String> paramTypeNames;
            int paramTypeCount;
            if (parameterTypes != null) {
                paramTypeNames = parameterTypes.getParamTypeNames();
                paramTypeCount = paramTypeNames.size();
            } else {
                paramTypeNames = null;
                // -1 indicates any method with the relevant signature will do
                // whereas 0 indicates an empty parameter list ()
                paramTypeCount = -1;
            }

            final List<MethodSearchResult> methods = new ArrayList<MethodSearchResult>();
            // accumulate matching methods

            SearchEngine searchEngine = new SearchEngine();
            IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
            SearchParticipant[] participants = new SearchParticipant[] {
                    SearchEngine.getDefaultSearchParticipant() };

            for (TypeSearchResult result : types) {
                String typeName = result.name.replace('$', '.');

                // now build type qualified method name
                StringBuilder builder = new StringBuilder();
                builder.append(typeName);
                // append method name to type name except when it is a constructor or class initializer
                if (!isConstructor) {
                    builder.append('.');
                    builder.append(methodName);
                }
                if (paramTypeCount >= 0) {
                    String separator = "";
                    builder.append("(");
                    for (int i = 0; i < paramTypeCount; i++) {
                        builder.append(separator);
                        String paramTypeName = paramTypeNames.get(i);
                        // ho hum eclipse doesn't like $ to separate embedded types
                        if (paramTypeName.indexOf('$') > 0) {
                            paramTypeName = paramTypeName.replace('$', '.');
                        }
                        builder.append(paramTypeName);
                        separator = ",";
                    }
                    builder.append(")");
                }
                final String stringPattern = builder.toString();
                int searchFor = (isConstructor ? IJavaSearchConstants.CONSTRUCTOR : IJavaSearchConstants.METHOD);
                int limitTo = IJavaSearchConstants.DECLARATIONS;
                int matchType = SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
                SearchPattern pattern = SearchPattern.createPattern(stringPattern, searchFor, limitTo, matchType);
                final TypeSearchResult typeResult = result;
                SearchRequestor requestor = new SearchRequestor() {
                    @Override
                    public void acceptSearchMatch(SearchMatch match) throws CoreException {
                        // only accept if we have an accurate match
                        if (match.getAccuracy() == SearchMatch.A_ACCURATE) {
                            MethodSearchResult methodResult = new MethodSearchResult(stringPattern, typeResult,
                                    match);
                            methods.add(methodResult);
                        }
                    }
                };
                try {
                    searchEngine.search(pattern, participants, scope, requestor, null);
                } catch (CoreException e) {
                    // TODO : ho hum not sure if this will happen when we have
                    // no such method or because something is wrong with paths etc
                    // but just ignore for now
                }
            }

            // if we have no matches then plant an error

            if (methods.isEmpty()) {
                error("unknown trigger method " + methodName, BytemanPackage.eINSTANCE.getEventMethod_Name());
            } else {
                // cache details of potential trigger methods for current rule
                getContext().put(RULE_METHOD_KEY, methods);
            }
        }
    }

    /**
     *    wrapper class used to hold results of type name searches
     */
    private class TypeSearchResult {
        int modifiers;
        char[] packageName;
        char[] simpleTypeName;
        char[][] enclosingTypeNames;
        String path;
        String name;

        TypeSearchResult(int modifiers, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames,
                String path) {
            this.modifiers = modifiers;
            this.packageName = packageName;
            this.simpleTypeName = simpleTypeName;
            this.enclosingTypeNames = enclosingTypeNames;
            this.path = path;
            StringBuilder builder = new StringBuilder();
            if (packageName != null && packageName.length > 0) {
                builder.append(packageName);
                builder.append('.');
            }
            for (char[] enclosingTypeName : enclosingTypeNames) {
                builder.append(enclosingTypeName);
                builder.append('$');
            }
            builder.append(simpleTypeName);
            name = builder.toString();
        }
    }

    /**
     *    wrapper class used to hold results of method searches
     */
    private class MethodSearchResult {
        TypeSearchResult typeSearchResult;
        String methodSpec;
        SearchMatch match;

        MethodSearchResult(String methodSpec, TypeSearchResult typeSearchResult, SearchMatch match) {
            this.methodSpec = methodSpec;
            this.typeSearchResult = typeSearchResult;
            this.match = match;
        }
    }

    /**
     * key used to identify validation context entry for the name of the current RULE
     * 
     * associated value is a String
     */
    private final static ContextKey RULE_NAME_KEY = new ContextKey("RULE:");
    /**
        * key used to identify validation context entry for the list of possible targets
        * for the name supplied in the CLASS clause of the current RULE
        * 
        * associated value is a List<TypeSearchResult>
        */
    private final static ContextKey RULE_CLASS_KEY = new ContextKey("CLASS:");
    /**
     * key used to identify validation context entry for the list of possible targets
     * for the name supplied in the METHOD clause
     * 
     * associated value is a List<MethodSearchResult>
     */
    private final static ContextKey RULE_METHOD_KEY = new ContextKey("METHOD:");

    // wrapper key used to identify values stored in the context -- using this to
    // wrap the String keys avoids us overwriting anything stored there by eclipse

    private static class ContextKey {
        private String name;

        ContextKey(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object object) {
            if (object instanceof ContextKey) {
                return ((ContextKey) object).name.equals(name);
            }

            return false;
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }
    }
}