org.ofbiz.plugin.analysis.Analysis.java Source code

Java tutorial

Introduction

Here is the source code for org.ofbiz.plugin.analysis.Analysis.java

Source

/**
 * Copyright 2008 Anders Hessellund 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id: Analysis.java,v 1.1 2008/01/17 18:48:19 hessellund Exp $
 */
package org.ofbiz.plugin.analysis;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.ofbiz.plugin.Plugin;
import org.ofbiz.plugin.ofbiz.Attribute;
import org.ofbiz.plugin.ofbiz.Project;
import org.ofbiz.plugin.ofbiz.Service;
import org.ofbiz.plugin.parser.AttributeFinder;
import org.ofbiz.plugin.parser.FinderException;

public class Analysis {
    private static boolean invocationRunAlready = false;
    private int invocationMapKeys = 0;;
    private static Map<String, IMarker> serviceInvocationMarkers = new HashMap<String, IMarker>();
    private final AnalysisContext[] contexts;
    private final IJavaProject javaProject;

    private static List<Service> asList(Service service) {
        assert service != null;
        List<Service> list = new ArrayList<Service>();
        list.add(service);
        return list;
    }

    public Analysis(IJavaProject javaProject, Service service, Project p) {
        this(javaProject, Analysis.asList(service), p);
    }

    public Analysis(IJavaProject javaProject, Service service) {
        this(javaProject, service, null);
    }

    public Analysis(IJavaProject javaProject, List<Service> services, Project p) {
        assert javaProject != null;
        assert javaProject.exists();
        assert services != null;
        this.javaProject = javaProject;
        this.contexts = new AnalysisContext[services.size()];
        for (int i = 0; i < services.size(); i++) {
            assert services.get(i) != null;
            this.contexts[i] = new AnalysisContext();
            this.contexts[i].javaProject = javaProject;
            this.contexts[i].service = services.get(i);
        }
    }

    private void analyzeOutMap(final AnalysisContext ctx, ControlFlowGraph cfg) throws FinderException {

        //TODO: if the in-map also serves as out-map then the initial keys should be available

        // get mandatory keys for out-map
        final List<String> mandatoryKeys = new ArrayList<String>();
        List<Attribute> attributes = new AttributeFinder(ctx.service).getAttributes();
        for (Attribute attr : attributes) {
            if (!attr.isOptional()
                    && (attr.getMode().getLiteral().equals("OUT") || attr.getMode().getLiteral().equals("INOUT"))) {
                mandatoryKeys.add(attr.getName());
            }
        }

        if (mandatoryKeys.isEmpty())
            return; // skip analysis

        AvailableMapKeys amk = new AvailableMapKeys(cfg);
        amk.computeFixPoint();

        // find all relevant return statements in methodbody
        final List<ReturnStatement> returns = new ArrayList<ReturnStatement>();
        ctx.method.accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement returnStmt) {
                Expression expr = returnStmt.getExpression();
                // basic case, e.g., return out;
                if (expr instanceof SimpleName) {
                    returns.add(returnStmt);
                    return false;
                }
                // special cases
                if (expr instanceof MethodInvocation) {
                    MethodInvocation mi = (MethodInvocation) expr;
                    IMethodBinding binding = mi.resolveMethodBinding();
                    // ignore ServiceUtil.java methods
                    if (binding.getDeclaringClass().getQualifiedName().equals("org.ofbiz.service.ServiceUtil")) {
                        return false;
                    }
                    // handle the toMap methods
                    if (binding.getDeclaringClass().getQualifiedName().equals("org.ofbiz.base.util.UtilMisc")
                            && binding.getName().equals("toMap")) {
                        Set<String> keys = new HashSet<String>();
                        //TODO: handle toMap(String[])
                        for (int i = 0; i < mi.arguments().size(); i += 2) {
                            // collect keys from toMap(..)-call
                            if (mi.arguments().get(i) instanceof StringLiteral) {
                                keys.add(((StringLiteral) mi.arguments().get(i)).getLiteralValue());
                            }
                            // handle special case of service execution errors (ModelService-class)
                            if (mi.arguments().get(i) instanceof QualifiedName
                                    && mi.arguments().get(i + 1) instanceof QualifiedName) {
                                QualifiedName keyQN = (QualifiedName) mi.arguments().get(i);
                                QualifiedName valueQN = (QualifiedName) mi.arguments().get(i + 1);
                                String key = keyQN.resolveConstantExpressionValue().toString();
                                String value = valueQN.resolveConstantExpressionValue().toString();
                                // service execution errors are ignored
                                if (key.equals("responseMessage")
                                        && (value.equals("error") || value.equals("fail"))) {
                                    return false;
                                } else {
                                    keys.add(key);
                                }
                            }
                        }

                        for (String key : mandatoryKeys) {
                            if (!keys.contains(key)) {
                                ctx.error(returnStmt, "Missing mandatory output-parameter: " + key);
                            }
                        }
                        return false;
                    }
                }
                ctx.warn(returnStmt, "Unable to analyze complex returns: " + Util.getFirstLine(returnStmt));
                return false;
            }
        });

        // check key usage for each return statement
        for (ReturnStatement stmt : returns) {
            String variable = ((SimpleName) stmt.getExpression()).getIdentifier();

            // is there a statement on this path in the control flow which
            // uses the out-map as a parameter? e.g., doSomething( out );

            if (amk.isNameUsedInInterproceduralCall(variable)) {
                for (Statement s : amk.getInterproceduralCalls(variable)) {
                    ctx.warn(s, "Unable to analyze interprocedural calls");
                }

            } else {

                // regular analysis

                Set<Pair<String, String>> outSet = amk.getOutSet(stmt);
                for (String key : mandatoryKeys) {
                    if (!outSet.contains(new Pair<String, String>(variable, key))) {
                        if (outSet.contains(new Pair<String, String>(variable, "responseMessage"))
                                || outSet.contains(new Pair<String, String>(variable, "errorMessage"))) {
                            // might be the special case of service execution error (ModelService-class)
                            ctx.warn(stmt, "Might be missing mandatory output parameter: " + key);
                        } else {
                            ctx.error(stmt, "Missing mandatory output-parameter: " + key);
                        }
                    }
                }
            }

        }
    }

    private void analyzeInMap(final AnalysisContext ctx, DefUseChain duc) throws FinderException {
        // second param in serviceimpls is the in-map
        ASTNode inMap = (ASTNode) ctx.method.parameters().get(1);

        // get references to this map
        Set<Statement> refs = duc.findRefsToDef(inMap);

        // get valid keys for in-map
        final List<String> validInKeys = new ArrayList<String>();
        validInKeys.add("userLogin");
        validInKeys.add("locale");
        List<Attribute> attributes = new AttributeFinder(ctx.service).getAttributes();
        for (Attribute attr : attributes) {
            if (attr.getMode().getLiteral().equals("IN") || attr.getMode().getLiteral().equals("INOUT")) {
                validInKeys.add(attr.getName());
            }
        }

        /* RULE: For any execution of the method it should hold that 
         * if in.get(X) is executed then key X must be available in the model.
         */
        for (Statement ref : refs) {

            ref.accept(new ASTVisitor() {
                @Override
                public boolean visit(MethodInvocation invocation) {

                    // resolve binding
                    IMethodBinding binding = invocation.resolveMethodBinding();
                    if (binding == null)
                        throw new RuntimeException(
                                "Unable to resolve binding for " + Util.getFirstLine(invocation));

                    //TODO: what about clear- and put-calls for in-map?

                    // filter out anything but get-calls
                    String methodName = binding.getName();
                    if (methodName == null || !methodName.equals("get"))
                        return super.visit(invocation);

                    // only allow get-calls from java.util.Map
                    String declaringClass = binding.getDeclaringClass().getQualifiedName();
                    if (!declaringClass.equals("java.util.Map"))
                        return super.visit(invocation);

                    // check argument, non-StringLiteral arguments are flagged as errors
                    Object expression = invocation.arguments().get(0);
                    if (!(expression instanceof StringLiteral)) {
                        ctx.warn(invocation, "Cannot analyze expression: " + Util.getFirstLine(expression));
                        return super.visit(invocation);
                    }

                    // check argument, arguments must be valid keys
                    String argument = ((StringLiteral) expression).getEscapedValue().replaceAll("\"", "");
                    if (!validInKeys.contains(argument)) {
                        ctx.error(invocation, "Undeclared input-parameter: " + argument);
                        return super.visit(invocation);
                    }

                    // ok :)
                    return super.visit(invocation);
                }
            });
        }
        invocationRunAlready = true;
    }

    /** runs the analysis
     * @return no of successful analysis
     */
    public int run(boolean resetMarkers) {
        int noOfSuccesfulAnalysis = 0;
        Map<String, Set<AnalysisContext>> location2context = mapLocationsToContexts();
        for (Entry<String, Set<AnalysisContext>> entry : location2context.entrySet()) {
            String location = entry.getKey();
            Set<AnalysisContext> contexts = entry.getValue();
            Plugin.logInfo("checking location " + location, null);
            try {
                IType type = this.javaProject.findType(location);
                if (type == null) {
                    Plugin.logError("  Unable to locate type on build path", null);
                    continue;
                }
                IFile file = (IFile) type.getResource();
                if (file == null) {
                    Plugin.logError("  Unable to retrieve file", null);
                    continue;
                }
                ICompilationUnit icu = type.getCompilationUnit();
                if (icu == null) {
                    Plugin.logError("  Unable to locate ICompilationUnit", null);
                    continue;
                }
                CompilationUnit cu = parse(icu);
                if (cu == null) {
                    Plugin.logError("  Unable to parse", null);
                    continue;
                }
                for (Iterator<AnalysisContext> iter = contexts.iterator(); iter.hasNext();) {
                    AnalysisContext ctx = iter.next();
                    ctx.file = file;
                    ctx.cu = cu;
                    ctx.method = getMethod(ctx.service.getInvoke(), ctx.cu);
                    if (ctx.method == null) {
                        Plugin.logError("  Unable to locate method " + ctx.service.getName(), null);
                        continue;
                    }
                    try {
                        IMarker marker = file.createMarker(Plugin.TEXT_MARKER);
                        marker.setAttribute(IMarker.CHAR_START, ctx.method.getStartPosition());
                        marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO);
                        marker.setAttribute("name", ctx.service.getName());
                        Plugin.logInfo(
                                "Marker created for " + ctx.service.getName() + " in file " + file.getFullPath(),
                                null);
                    } catch (CoreException e) {
                        Plugin.logError(
                                "Unable to create marker for " + ctx.method.getName().getFullyQualifiedName(), e);
                    }
                    // this is an analyzable service, so run analysis
                    boolean ok = false;
                    try {
                        if (resetMarkers) {
                            IMarker[] markers = ctx.file.findMarkers(Plugin.PROBLEM_MARKER, true,
                                    IResource.DEPTH_INFINITE);
                            for (IMarker m : markers) {
                                if (m.getAttribute("name", "").equals(ctx.service.getName())) {
                                    m.delete();
                                }
                            }
                        }
                        ControlFlowGraph cfg = new ControlFlowGraph(ctx.method);
                        ReachingDefinitionAnalysis rda = new ReachingDefinitionAnalysis(cfg);
                        rda.computeFixPoint();
                        DefUseChain duc = new DefUseChain(rda, ctx.method);
                        analyzeInMap(ctx, duc);
                        String returnType = ctx.method.getReturnType2().resolveBinding().getQualifiedName();
                        if (returnType.equals("java.util.Map")) {
                            analyzeOutMap(ctx, cfg);
                        }
                        ok = true;
                        noOfSuccesfulAnalysis++;
                    } catch (AnalysisException ae) {
                        Plugin.logError("  Unable to analyze " + ctx, ae);
                    } finally {
                        Plugin.logInfo(
                                "  Analysis of " + ctx.service.getName() + " " + (ok ? "succeeded" : "failed"),
                                null);
                        ctx.dispose();
                    }
                }
                // clean up 
                cu = null;
                icu = null;
                file = null;
                type = null;
            } catch (Exception e) {
                Plugin.logError("Caught an exception during analysis of location: " + location, e);
            }
        }
        return noOfSuccesfulAnalysis;
    }

    /** create a map from location to a set of (co-located) contexts */
    private Map<String, Set<AnalysisContext>> mapLocationsToContexts() {
        Map<String, Set<AnalysisContext>> location2context = new HashMap<String, Set<AnalysisContext>>();
        int countNonJavaServices = 0;
        for (AnalysisContext ctx : contexts) {
            if (!ctx.service.getEngine().equals("java")) {
                countNonJavaServices++;
                continue;
            }
            assert ctx.service.getLocation() != null;
            assert ctx.service.getInvoke() != null;
            if (location2context.containsKey(ctx.service.getLocation())) {
                Set<AnalysisContext> values = location2context.get(ctx.service.getLocation());
                values.add(ctx);
            } else {
                Set<AnalysisContext> values = new HashSet<AnalysisContext>();
                values.add(ctx);
                location2context.put(ctx.service.getLocation(), values);
            }
        }
        return location2context;
    }

    /** parse using {@link AST.JSL3} */
    private CompilationUnit parse(ICompilationUnit lwUnit) {
        ASTParser parser = ASTParser.newParser(AST.JLS3);
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        parser.setSource(lwUnit); // set source
        parser.setResolveBindings(true); // we need bindings later on
        return (CompilationUnit) parser.createAST(null /* IProgressMonitor */); // parse
    }

    // TODO: getMethod does not handle overload
    private MethodDeclaration getMethod(String name, CompilationUnit cu) {
        return getMethods(cu).get(name);
    }

    /** returns all methods (taking two params) in a given {@link CompilationUnit}*/
    private Map<String, MethodDeclaration> getMethods(final CompilationUnit cu) {
        final Map<String, MethodDeclaration> methods = new HashMap<String, MethodDeclaration>();
        cu.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodDeclaration node) {
                // naive filtering
                if (node.parameters().size() == 2)
                    methods.put(node.getName().toString(), node);
                return false; // skip children
            }
        });
        return methods;
    }

    /** stores all relevant information for the analysis of a single service */
    static class AnalysisContext {
        private void dispose() {
            javaProject = null;
            service = null;
            method = null;
            file = null;
            cu = null;
        }

        private IJavaProject javaProject;
        private Service service;
        private MethodDeclaration method;
        private IFile file;
        private CompilationUnit cu;

        void warn(ASTNode node, String message) {
            mark(node, message, IMarker.SEVERITY_WARNING);
        }

        void error(ASTNode node, String message) {
            mark(node, message, IMarker.SEVERITY_ERROR);
        }

        private void mark(ASTNode node, String message, int type) {
            assert javaProject != null && javaProject.exists();
            assert service != null;
            assert method != null;
            assert file != null && file.exists();
            assert cu != null;
            assert node != null;
            assert message != null;
            try {
                int linenumber = cu.getLineNumber(node.getStartPosition());
                IMarker[] markers = file.findMarkers(Plugin.PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
                for (IMarker m : markers) {
                    String msg = "[" + service.getName() + "] " + message;
                    if (m.getAttribute("name", "").equals(service.getName())
                            && m.getAttribute(IMarker.MESSAGE, "").equals(msg)
                            && m.getAttribute(IMarker.LINE_NUMBER, -1) == linenumber) {
                        // skip marker creation
                        return;
                    }
                }
                IMarker marker = file.createMarker(Plugin.PROBLEM_MARKER);
                marker.setAttribute(IMarker.MESSAGE, "[" + service.getName() + "] " + message);
                marker.setAttribute(IMarker.CHAR_START, node.getStartPosition());
                marker.setAttribute(IMarker.CHAR_END, node.getStartPosition() + node.getLength());
                marker.setAttribute(IMarker.LINE_NUMBER, linenumber);
                marker.setAttribute(IMarker.SEVERITY, type);
                marker.setAttribute("method", method.getName().getFullyQualifiedName());
                marker.setAttribute("name", service.getName());
                assert marker.exists();
            } catch (CoreException ce) {
                throw new AnalysisException("Unable to create markers for file: " + file.getName(), ce);
            }
        }
    }

    public static IMarker getMarkerLookupKey(String key) {
        return serviceInvocationMarkers.get(key);
    }
}