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