blob: ce3f211ffdf1ac7b50c40b18b9cd4f134383b6ea [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013 BSI Business Systems Integration AG.
* 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:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.sdk.extension;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.scout.commons.CollectionUtility;
import org.eclipse.scout.commons.job.JobEx;
import org.eclipse.scout.sdk.Texts;
import org.eclipse.scout.sdk.extensions.runtime.classes.IRuntimeClasses;
import org.eclipse.scout.sdk.internal.ScoutSdk;
import org.eclipse.scout.sdk.util.ast.visitor.DefaultAstVisitor;
import org.eclipse.scout.sdk.util.resources.ResourceUtility;
import org.eclipse.scout.sdk.util.signature.SignatureUtility;
import org.eclipse.scout.sdk.util.type.IMethodFilter;
import org.eclipse.scout.sdk.util.type.MethodFilters;
import org.eclipse.scout.sdk.util.type.TypeFilters;
import org.eclipse.scout.sdk.util.type.TypeUtility;
import org.eclipse.scout.sdk.util.typecache.ICachedTypeHierarchy;
import org.eclipse.scout.sdk.util.typecache.ITypeHierarchy;
/**
* <h3>{@link ExtensionValidation}</h3>
*
* @author Matthias Villiger
* @since 4.2.0
*/
public final class ExtensionValidation {
public static final String INVALID_OPERATION_METHOD_CALL_MARKER_ID = "org.eclipse.scout.sdk.extension.operation.call";
public static final String EXTENSION_VALIDATION_JOB_FAMILY = "extension.validation.job.family";
private static final Object LOCK = new Object();
private static IElementChangedListener listener;
private static volatile Map<String /*method name*/, Map<String /*method id*/, Set<String /* owner sig */>>> chainableMethods;
private ExtensionValidation() {
}
public static synchronized void install() {
if (listener == null) {
listener = new P_ResourceChangeListener();
JavaCore.addElementChangedListener(listener);
}
}
public static synchronized void uninstall() {
if (listener != null) {
JavaCore.removeElementChangedListener(listener);
listener = null;
}
Job.getJobManager().cancel(EXTENSION_VALIDATION_JOB_FAMILY);
synchronized (LOCK) {
chainableMethods = null;
}
}
private static Map<String, Map<String, Set<String>>> getChainableMethods() throws CoreException {
if (chainableMethods == null) {
synchronized (LOCK) {
if (chainableMethods == null) {
chainableMethods = calcChainableMethods();
}
}
}
return chainableMethods;
}
private static Deque<String> getOwners(Set<IMethod> constructorsWithParams, IType chain) throws CoreException {
Deque<String> owners = new LinkedList<String>();
for (IMethod constructor : constructorsWithParams) {
for (ILocalVariable param : constructor.getParameters()) {
String[] paramArgs = Signature.getTypeArguments(param.getTypeSignature());
for (String arg : paramArgs) {
String[] typeArguments = Signature.getTypeArguments(arg);
for (String a : typeArguments) {
if (a.startsWith("+")) {
a = a.substring(1);
}
a = SignatureUtility.ensureSourceTypeParametersAreCorrect(a, chain);
a = Signature.getTypeErasure(a);
if (Signature.getTypeSignatureKind(a) != Signature.TYPE_VARIABLE_SIGNATURE) {
if (SignatureUtility.isUnresolved(a)) {
a = SignatureUtility.getResolvedSignature(a, chain, chain);
}
owners.add(a.replace('$', '.'));
}
}
}
}
}
return owners;
}
private static Map<String, Map<String, Set<String>>> calcChainableMethods() throws CoreException {
Map<String, Map<String, Set<String>>> result = new HashMap<String, Map<String, Set<String>>>();
IType abstractExtensionChain = TypeUtility.getType(IRuntimeClasses.AbstractExtensionChain);
if (!TypeUtility.exists(abstractExtensionChain)) {
return result;
}
ICachedTypeHierarchy chainHierarchy = TypeUtility.getTypeHierarchy(abstractExtensionChain);
Set<IType> allChains = chainHierarchy.getAllSubtypes(abstractExtensionChain, TypeFilters.getMultiTypeFilterAnd(TypeFilters.getClassFilter(), TypeFilters.getFlagsFilter(Flags.AccPublic | Flags.AccStatic)));
IMethodFilter bridgeFilter = new IMethodFilter() {
@Override
public boolean accept(IMethod candidate) throws CoreException {
return !Flags.isBridge(candidate.getFlags());
}
};
for (IType chain : allChains) {
// calc owners based on parameters of constructors
Set<IMethod> constructorsWithParams = TypeUtility.getMethods(chain, MethodFilters.getMultiMethodFilter(new IMethodFilter() {
@Override
public boolean accept(IMethod candidate) throws CoreException {
return candidate.isConstructor() && candidate.getParameters().length > 0;
}
}, bridgeFilter));
Deque<String> owners = getOwners(constructorsWithParams, chain);
if (owners.size() > 1) {
ScoutSdk.logWarning("Multiple owner candidates found for Chain '" + chain.getFullyQualifiedName() + "'.");
}
// calc operation-method-identifiers based on public methods
Set<IMethod> operationMethods = TypeUtility.getMethods(chain, MethodFilters.getMultiMethodFilter(new IMethodFilter() {
@Override
public boolean accept(IMethod candidate) throws CoreException {
String src = candidate.getSource();
if (src == null) {
return false;
}
return !candidate.isConstructor() && src.contains("MethodInvocation") && src.contains("callChain");
}
}, bridgeFilter, MethodFilters.getFlagsFilter(Flags.AccPublic)));
if (operationMethods.size() > 1) {
ScoutSdk.logWarning("Multiple method invocations found for Chain '" + chain.getFullyQualifiedName() + "'.");
}
else if (operationMethods.size() < 1) {
ScoutSdk.logWarning("No method invocations found for Chain '" + chain.getFullyQualifiedName() + "'.");
}
else if (owners.isEmpty()) {
ScoutSdk.logWarning("No owner candidates found for Chain '" + chain.getFullyQualifiedName() + "'.");
}
else {
IMethod m = CollectionUtility.firstElement(operationMethods);
String methodName = m.getElementName();
Map<String, Set<String>> map = result.get(methodName);
if (map == null) {
map = new HashMap<String, Set<String>>();
result.put(methodName, map);
}
String methodId = SignatureUtility.getMethodIdentifier(m);
Set<String> set = map.get(methodId);
if (set == null) {
set = new HashSet<String>();
map.put(methodId, set);
}
set.add(owners.getLast());
}
}
// trim maps
Map<String, Map<String, Set<String>>> returnResult = new HashMap<String, Map<String, Set<String>>>(result.size());
for (Entry<String, Map<String, Set<String>>> a : result.entrySet()) {
Map<String, Set<String>> b = new HashMap<String, Set<String>>(a.getValue().size());
for (Entry<String, Set<String>> c : a.getValue().entrySet()) {
Set<String> d = new HashSet<String>(c.getValue());
b.put(c.getKey(), d);
}
returnResult.put(a.getKey(), b);
}
return returnResult;
}
private static final class P_MarkerCreationJob extends JobEx {
private final CompilationUnit m_ast;
public P_MarkerCreationJob(CompilationUnit ast) {
super("Extension Validation Job");
setSystem(true);
setUser(false);
setPriority(Job.BUILD);
setRule(new P_SchedulingRule());
m_ast = ast;
}
private void createErrorMarker(IResource r, int start, int length, ICompilationUnit icu, String methodName) throws CoreException {
IMarker marker = r.createMarker(INVALID_OPERATION_METHOD_CALL_MARKER_ID);
marker.setAttribute(IMarker.MESSAGE, Texts.get("ChainableMethodCannotBeCalled", methodName));
marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
marker.setAttribute(IMarker.CHAR_START, start);
marker.setAttribute(IMarker.CHAR_END, start + length);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
try {
Document doc = new Document(icu.getSource());
marker.setAttribute(IMarker.LINE_NUMBER, doc.getLineOfOffset(start) + 1);
}
catch (BadLocationException e) {
//nop
}
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
try {
IJavaElement je = m_ast.getJavaElement();
if (!(je instanceof ICompilationUnit)) {
return Status.OK_STATUS;
}
ICompilationUnit icu = (ICompilationUnit) je;
IResource resource = icu.getResource();
if (!ResourceUtility.exists(resource)) {
return Status.OK_STATUS;
}
resource.deleteMarkers(INVALID_OPERATION_METHOD_CALL_MARKER_ID, true, IResource.DEPTH_ZERO);
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
final Map<String, Map<String, Set<String>>> interestingMethods = getChainableMethods();
final Map<IMethod, P_ProblemCandidates> methodsToCheck = new HashMap<IMethod, P_ProblemCandidates>();
m_ast.accept(new DefaultAstVisitor() {
@Override
public boolean visitNode(ASTNode node) {
if (monitor.isCanceled()) {
return false;
}
return super.visitNode(node);
}
@Override
public boolean visit(MethodInvocation node) {
if (monitor.isCanceled()) {
return false;
}
String methodName = node.getName().getIdentifier();
Map<String, Set<String>> map = interestingMethods.get(methodName);
if (map != null) {
IMethodBinding bindings = node.resolveMethodBinding();
if (bindings != null) {
IJavaElement javaElement = bindings.getJavaElement();
if (javaElement instanceof IMethod) {
IMethod m = (IMethod) javaElement;
IType declaringType = getDeclaringTypeOf(node);
if (monitor.isCanceled()) {
return false;
}
if (!isLocalExtension(declaringType, m)) {
try {
String id = SignatureUtility.getMethodIdentifier(m);
Set<String> owners = map.get(id);
if (owners != null) {
if (monitor.isCanceled()) {
return false;
}
P_ProblemCandidates candidate = new P_ProblemCandidates(owners, node.getStartPosition(), node.getLength(), m.getElementName());
methodsToCheck.put(m, candidate);
}
}
catch (CoreException e) {
ScoutSdk.logError("Unable to perform extension validation.", e);
}
}
}
}
}
return super.visit(node);
}
});
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
for (Entry<IMethod, P_ProblemCandidates> entry : methodsToCheck.entrySet()) {
ITypeHierarchy supertypeHierarchy = TypeUtility.getSupertypeHierarchy(entry.getKey().getDeclaringType());
if (supertypeHierarchy != null) {
createErrorMarkerIfNecessary(icu, resource, entry.getValue(), supertypeHierarchy, monitor);
}
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
}
}
catch (Exception e) {
ScoutSdk.logWarning("Unable to check for chainable method calls.", e);
}
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return EXTENSION_VALIDATION_JOB_FAMILY.equals(family);
}
private IType getDeclaringTypeOf(MethodInvocation mi) {
TypeDeclaration td = null;
ASTNode cur = mi;
while (cur != null && td == null) {
if (cur instanceof TypeDeclaration) {
td = (TypeDeclaration) cur;
}
cur = cur.getParent();
}
if (td != null) {
ITypeBinding binding = td.resolveBinding();
if (binding != null) {
IJavaElement javaElement = binding.getJavaElement();
if (TypeUtility.exists(javaElement) && javaElement.getElementType() == IJavaElement.TYPE) {
return (IType) javaElement;
}
}
}
return null;
}
private boolean isLocalExtension(IType declaringTypeOfMethodInvocation, IMethod methodDeclaration) {
if (declaringTypeOfMethodInvocation == null || methodDeclaration == null) {
return false;
}
IType declaringTypeOfLocalExtension = declaringTypeOfMethodInvocation.getDeclaringType();
if (declaringTypeOfLocalExtension == null) {
return false;
}
return declaringTypeOfLocalExtension.equals(methodDeclaration.getDeclaringType());
}
private void createErrorMarkerIfNecessary(ICompilationUnit icu, IResource resource, P_ProblemCandidates problemCandidate, ITypeHierarchy supertypeHierarchy, IProgressMonitor monitor) throws CoreException {
for (String ownerSig : problemCandidate.getOwnerSignatures()) {
IType owner = TypeUtility.getTypeBySignature(ownerSig);
if (TypeUtility.exists(owner) && supertypeHierarchy.contains(owner)) {
createErrorMarker(resource, problemCandidate.getStart(), problemCandidate.getLength(), icu, problemCandidate.getMethodName());
return;
}
if (monitor.isCanceled()) {
return;
}
}
}
}
private static final class P_ProblemCandidates {
private final Set<String> m_ownerSignatures;
private final int m_start;
private final int m_length;
private final String m_methodName;
/**
* @param owner
* @param start
* @param length
*/
public P_ProblemCandidates(Set<String> owners, int start, int length, String methodName) {
m_ownerSignatures = owners;
m_start = start;
m_length = length;
m_methodName = methodName;
}
public Set<String> getOwnerSignatures() {
return m_ownerSignatures;
}
public int getStart() {
return m_start;
}
public int getLength() {
return m_length;
}
public String getMethodName() {
return m_methodName;
}
}
private static final class P_ResourceChangeListener implements IElementChangedListener {
@Override
public void elementChanged(ElementChangedEvent event) {
CompilationUnit compilationUnitAST = visitDelta(event.getDelta());
if (compilationUnitAST != null) {
Job.getJobManager().cancel(EXTENSION_VALIDATION_JOB_FAMILY);
new P_MarkerCreationJob(compilationUnitAST).schedule();
}
}
private CompilationUnit visitDelta(IJavaElementDelta delta) {
CompilationUnit ast = delta.getCompilationUnitAST();
if (ast != null) {
return ast;
}
if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
IJavaElementDelta[] childDeltas = delta.getAffectedChildren();
if (childDeltas != null && childDeltas.length > 0) {
for (int i = 0; i < childDeltas.length; i++) {
ast = visitDelta(childDeltas[i]);
if (ast != null) {
return ast;
}
}
}
}
return null;
}
}
private static final class P_SchedulingRule implements ISchedulingRule {
@Override
public boolean contains(ISchedulingRule rule) {
return rule instanceof P_SchedulingRule;
}
@Override
public boolean isConflicting(ISchedulingRule rule) {
return rule instanceof P_SchedulingRule;
}
}
}