/**
* Copyright (c) 2010 Stefan Henss.
* 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:
* Stefan Henss - initial API and implementation.
*/
package org.eclipse.recommenders.internal.calls.rcp.templates;
import static com.google.common.collect.Sets.newHashSet;
import static org.eclipse.recommenders.completion.rcp.CompletionContextKey.ENCLOSING_METHOD_FIRST_DECLARATION;
import static org.eclipse.recommenders.internal.calls.rcp.CallCompletionContextFunctions.*;
import static org.eclipse.recommenders.internal.calls.rcp.Constants.TEMPLATES_CATEGORY_ID;
import static org.eclipse.recommenders.internal.calls.rcp.l10n.LogMessages.*;
import static org.eclipse.recommenders.utils.Logs.log;
import static org.eclipse.recommenders.utils.Recommendations.top;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.inject.Inject;
import javax.inject.Provider;
import org.apache.commons.lang3.CharUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.internal.codeassist.ISearchRequestor;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMemberAccess;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedNameReference;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnSingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.SearchableEnvironment;
import org.eclipse.jdt.internal.corext.util.JdtFlags;
import org.eclipse.jdt.internal.corext.util.SuperTypeHierarchyCache;
import org.eclipse.jdt.ui.PreferenceConstants;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.recommenders.calls.ICallModel;
import org.eclipse.recommenders.calls.ICallModel.DefinitionKind;
import org.eclipse.recommenders.calls.ICallModelProvider;
import org.eclipse.recommenders.completion.rcp.CompletionContextKey;
import org.eclipse.recommenders.completion.rcp.DisableContentAssistCategoryJob;
import org.eclipse.recommenders.completion.rcp.ICompletionContextFunction;
import org.eclipse.recommenders.completion.rcp.IRecommendersCompletionContext;
import org.eclipse.recommenders.completion.rcp.RecommendersCompletionContext;
import org.eclipse.recommenders.coordinates.ProjectCoordinate;
import org.eclipse.recommenders.models.UniqueTypeName;
import org.eclipse.recommenders.models.rcp.IProjectCoordinateProvider;
import org.eclipse.recommenders.rcp.IAstProvider;
import org.eclipse.recommenders.rcp.JavaElementResolver;
import org.eclipse.recommenders.utils.Recommendation;
import org.eclipse.recommenders.utils.Recommendations;
import org.eclipse.recommenders.utils.Throws;
import org.eclipse.recommenders.utils.names.IMethodName;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
/**
* Controls the process of template recommendations.
*/
@SuppressWarnings({ "restriction", "rawtypes", "unchecked" })
public class TemplatesCompletionProposalComputer implements IJavaCompletionProposalComputer {
public static enum CompletionMode {
TYPE_NAME,
MEMBER_ACCESS,
THIS
}
private final Provider<IProjectCoordinateProvider> pcProvider;
private final Provider<ICallModelProvider> store;
private final IAstProvider astProvider;
private final JavaElementResolver elementResolver;
private final Map<CompletionContextKey, ICompletionContextFunction> functions;
private IRecommendersCompletionContext rCtx;
private IMethod enclosingMethod;
private Set<IType> candidates;
private String variableName;
private boolean requiresConstructor;
private String methodPrefix;
private CompletionMode mode;
private Image icon;
private ICallModel model;
@Inject
public TemplatesCompletionProposalComputer(Provider<IProjectCoordinateProvider> pcProvider,
Provider<ICallModelProvider> store, IAstProvider astProvider, JavaElementResolver elementResolver,
Map<CompletionContextKey, ICompletionContextFunction> functions) {
this.pcProvider = pcProvider;
this.store = store;
this.astProvider = astProvider;
this.elementResolver = elementResolver;
this.functions = functions;
loadImage();
}
private void loadImage() {
Bundle bundle = FrameworkUtil.getBundle(TemplatesCompletionProposalComputer.class);
icon = null;
if (bundle != null) {
ImageDescriptor desc = AbstractUIPlugin.imageDescriptorFromPlugin(bundle.getSymbolicName(),
"icons/view16/templates-dots.gif"); //$NON-NLS-1$
icon = desc.createImage();
}
}
@VisibleForTesting
public CompletionMode getCompletionMode() {
return mode;
}
@VisibleForTesting
public String getVariableName() {
return variableName;
}
@VisibleForTesting
public String getMethodPrefix() {
return methodPrefix;
}
@Override
public List computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
if (!shouldMakeProposals()) {
return Collections.EMPTY_LIST;
}
rCtx = new RecommendersCompletionContext((JavaContentAssistInvocationContext) context, astProvider, functions);
if (!findEnclosingMethod()) {
return Collections.emptyList();
}
if (!findCompletionMode()) {
return Collections.emptyList();
}
if (!findPotentialTypes()) {
return Collections.emptyList();
}
ProposalBuilder proposalBuilder = new ProposalBuilder(icon, rCtx, elementResolver, variableName);
for (IType t : candidates) {
addPatternsForType(t, proposalBuilder);
}
return proposalBuilder.createProposals();
}
@VisibleForTesting
protected boolean shouldMakeProposals() {
// we only make proposals on non-default content assist lists (2nd, 3rd,...)
String[] excluded = PreferenceConstants.getExcludedCompletionProposalCategories();
Set<String> ex = Sets.newHashSet(excluded);
if (!ex.contains(TEMPLATES_CATEGORY_ID)) {
new DisableContentAssistCategoryJob(TEMPLATES_CATEGORY_ID).schedule();
return false;
}
// we are not on the default tab
return true;
}
private void addPatternsForType(IType t, ProposalBuilder proposalBuilder) {
ProjectCoordinate pc = pcProvider.get().resolve(t).or(ProjectCoordinate.UNKNOWN);
UniqueTypeName name = new UniqueTypeName(pc, elementResolver.toRecType(t));
model = store.get().acquireModel(name).orNull();
try {
if (model == null) {
return;
}
model.setObservedCalls(Sets.<IMethodName>newHashSet());
if (mode == CompletionMode.TYPE_NAME) {
handleTypeNameCompletionRequest(proposalBuilder);
} else {
handleVariableCompletionRequest(proposalBuilder);
}
} finally {
store.get().releaseModel(model);
}
}
private void handleVariableCompletionRequest(ProposalBuilder proposalBuilder) {
// set override-context:
IMethod overrides = rCtx.get(ENCLOSING_METHOD_FIRST_DECLARATION, null);
if (overrides != null) {
IMethodName crOverrides = elementResolver.toRecMethod(overrides)
.or(org.eclipse.recommenders.utils.Constants.UNKNOWN_METHOD);
model.setObservedOverrideContext(crOverrides);
}
// set definition-type and defined-by
model.setObservedDefinitionKind(rCtx.get(RECEIVER_DEF_TYPE, null));
model.setObservedDefiningMethod(rCtx.get(RECEIVER_DEF_BY, null));
// set calls:
model.setObservedCalls(newHashSet(rCtx.get(RECEIVER_CALLS, Collections.<IMethodName>emptyList())));
List<Recommendation<String>> callgroups = getMostLikelyPatternsSortedByProbability(model);
for (Recommendation<String> p : callgroups) {
String patternId = p.getProposal();
model.setObservedPattern(patternId);
Collection<IMethodName> calls = Sets.newTreeSet();
for (Recommendation<IMethodName> r : top(model.recommendCalls(), 100, 0.1d)) {
calls.add(r.getProposal());
}
// patterns with less than two calls are no patterns :)
if (calls.size() < 2) {
continue;
}
PatternRecommendation pattern = new PatternRecommendation(patternId, model.getReceiverType(),
ImmutableSet.copyOf(calls), p.getRelevance());
proposalBuilder.addPattern(pattern);
// }
}
}
private void handleTypeNameCompletionRequest(ProposalBuilder proposalBuilder) {
IMethod overrides = rCtx.get(ENCLOSING_METHOD_FIRST_DECLARATION, null);
model.setObservedDefinitionKind(DefinitionKind.NEW);
if (overrides != null) {
IMethodName crOverrides = elementResolver.toRecMethod(overrides)
.or(org.eclipse.recommenders.utils.Constants.UNKNOWN_METHOD);
model.setObservedOverrideContext(crOverrides);
}
List<Recommendation<String>> callgroups = getMostLikelyPatternsSortedByProbability(model);
for (Recommendation<String> p : callgroups) {
String patternId = p.getProposal();
model.setObservedPattern(patternId);
for (Recommendation<IMethodName> def : Recommendations.top(model.recommendDefinitions(), 100, 0.01d)) {
IMethodName constructor = def.getProposal();
// TODO XXX this looks like a bug in some mining code: we have wring receiver methods here: new
// Label(Composite) for instance occurred in Composite model. Why?
if (!constructor.isInit() || constructor.getDeclaringType() != model.getReceiverType()) {
continue;
}
Collection<IMethodName> calls = getCallsForDefinition(model, constructor);
calls.add(constructor);
// patterns with less than two calls are no patterns :)
if (calls.size() < 2) {
continue;
}
PatternRecommendation pattern = new PatternRecommendation(patternId, model.getReceiverType(),
ImmutableSet.copyOf(calls), p.getRelevance());
proposalBuilder.addPattern(pattern);
}
}
}
private Collection<IMethodName> getCallsForDefinition(ICallModel model, IMethodName definition) {
boolean constructorAdded = false;
TreeSet<IMethodName> calls = Sets.newTreeSet();
List<Recommendation<IMethodName>> rec = Recommendations.top(model.recommendCalls(), 100, 0.1d);
if (rec.isEmpty()) {
return Lists.newLinkedList();
}
if (requiresConstructor && definition.isInit()) {
calls.add(definition);
constructorAdded = true;
}
if (requiresConstructor && !constructorAdded) {
return Lists.newLinkedList();
}
for (Recommendation<IMethodName> pair : rec) {
calls.add(pair.getProposal());
}
if (!containsCallWithMethodPrefix(calls)) {
return Lists.newLinkedList();
}
return calls;
}
private boolean containsCallWithMethodPrefix(Set<IMethodName> calls) {
for (IMethodName call : calls) {
if (call.getName().startsWith(methodPrefix)) {
return true;
}
}
return false;
}
private List<Recommendation<String>> getMostLikelyPatternsSortedByProbability(ICallModel net) {
return Recommendations.top(net.recommendPatterns(), 10, 0.03d);
}
private boolean findCompletionMode() {
variableName = ""; //$NON-NLS-1$
methodPrefix = ""; //$NON-NLS-1$
mode = null;
ASTNode n = rCtx.getCompletionNode().orNull();
if (n instanceof CompletionOnSingleNameReference) {
if (isPotentialClassName((CompletionOnSingleNameReference) n)) {
mode = CompletionMode.TYPE_NAME;
} else {
// eq$ --> receiver is this
mode = CompletionMode.THIS;
methodPrefix = rCtx.getReceiverName();
}
} else if (n instanceof CompletionOnQualifiedNameReference) {
if (isPotentialClassName((CompletionOnQualifiedNameReference) n)) {
mode = CompletionMode.TYPE_NAME;
} else {
mode = CompletionMode.MEMBER_ACCESS;
variableName = rCtx.getReceiverName();
methodPrefix = rCtx.getPrefix();
}
} else if (n instanceof CompletionOnMemberAccess) {
Expression ma = ((CompletionOnMemberAccess) n).receiver;
if (ma.isImplicitThis() || ma.isSuper() || ma.isThis()) {
mode = CompletionMode.THIS;
} else {
mode = CompletionMode.MEMBER_ACCESS;
}
}
return mode != null;
}
private boolean findPotentialTypes() {
if (mode == CompletionMode.TYPE_NAME) {
ASTNode n = rCtx.getCompletionNode().orNull();
CompletionOnSingleNameReference c = null;
if (n instanceof CompletionOnSingleNameReference) {
c = (CompletionOnSingleNameReference) n;
candidates = findTypesBySimpleName(c.token);
}
} else if (mode == CompletionMode.THIS) {
createCandidatesFromOptional(getSupertypeOfThis());
} else {
createCandidatesFromOptional(rCtx.getReceiverType());
}
return candidates != null;
}
private Optional<IType> getSupertypeOfThis() {
IMethod m = rCtx.getEnclosingMethod().orNull();
try {
if (m == null || JdtFlags.isStatic(m)) {
return Optional.absent();
}
IType type = m.getDeclaringType();
ITypeHierarchy hierarchy = SuperTypeHierarchyCache.getTypeHierarchy(type);
return Optional.fromNullable(hierarchy.getSuperclass(type));
} catch (JavaModelException e) {
log(WARNING_FAILED_TO_RESOLVE_SUPER_TYPE, e, m);
return Optional.absent();
} catch (Exception e) {
log(ERROR_FAILED_TO_RESOLVE_SUPER_TYPE, e, m);
return Optional.absent();
}
}
private void createCandidatesFromOptional(Optional<IType> optType) {
if (optType.isPresent()) {
candidates = Sets.newHashSet(optType.get());
}
}
private boolean isPotentialClassName(CompletionOnQualifiedNameReference c) {
char[] name = c.completionIdentifier;
return name != null && name.length > 0 && CharUtils.isAsciiAlphaUpper(name[0]);
}
private boolean isPotentialClassName(CompletionOnSingleNameReference c) {
return c.token != null && c.token.length > 0 && CharUtils.isAsciiAlphaUpper(c.token[0]);
}
private boolean findEnclosingMethod() {
enclosingMethod = rCtx.getEnclosingMethod().orNull();
return enclosingMethod != null;
}
@Override
public void sessionStarted() {
// This particular event is not of interest for us.
}
@Override
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
return Collections.emptyList();
}
@Override
public String getErrorMessage() {
return null;
}
@Override
public void sessionEnded() {
// This particular event is not of interest for us.
}
public Set<IType> findTypesBySimpleName(char[] simpleTypeName) {
final Set<IType> result = Sets.newHashSet();
try {
final JavaProject project = (JavaProject) rCtx.getProject();
SearchableEnvironment environment = project.newSearchableNameEnvironment(DefaultWorkingCopyOwner.PRIMARY);
environment.findExactTypes(simpleTypeName, false, IJavaSearchConstants.TYPE, new ISearchRequestor() {
@Override
public void acceptConstructor(int modifiers, char[] simpleTypeName, int parameterCount,
char[] signature, char[][] parameterTypes, char[][] parameterNames, int typeModifiers,
char[] packageName, int extraFlags, String path, AccessRestriction access) {
}
@Override
public void acceptType(char[] packageName, char[] typeName, char[][] enclosingTypeNames, int modifiers,
AccessRestriction accessRestriction) {
IType res;
try {
res = project.findType(String.valueOf(packageName), String.valueOf(typeName));
if (res != null) {
result.add(res);
}
} catch (JavaModelException e) {
log(ERROR_FAILED_TO_FIND_TYPE, e, packageName, typeName);
}
}
@Override
public void acceptPackage(char[] packageName) {
}
});
} catch (JavaModelException e) {
Throws.throwUnhandledException(e);
}
return result;
}
}