/**
* Copyright (c) 2010, 2013 Darmstadt University of Technology.
* 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:
* Marcel Bruch - initial API and implementation.
*/
package org.eclipse.recommenders.internal.calls.rcp;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Sets.newHashSet;
import static java.lang.Math.max;
import static java.math.RoundingMode.HALF_EVEN;
import static java.text.MessageFormat.format;
import static org.eclipse.recommenders.completion.rcp.CompletionContextKey.ENCLOSING_METHOD_FIRST_DECLARATION;
import static org.eclipse.recommenders.completion.rcp.processable.ProposalTag.RECOMMENDERS_SCORE;
import static org.eclipse.recommenders.internal.calls.rcp.CallCompletionContextFunctions.*;
import static org.eclipse.recommenders.rcp.SharedImages.Images.OVR_STAR;
import static org.eclipse.recommenders.utils.Constants.REASON_NOT_IN_CACHE;
import static org.eclipse.recommenders.utils.Recommendations.top;
import static org.eclipse.recommenders.utils.Result.*;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMemberAccess;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnMessageSend;
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.jface.viewers.IDecoration;
import org.eclipse.recommenders.calls.ICallModel;
import org.eclipse.recommenders.calls.ICallModelProvider;
import org.eclipse.recommenders.completion.rcp.IProposalNameProvider;
import org.eclipse.recommenders.completion.rcp.IRecommendersCompletionContext;
import org.eclipse.recommenders.completion.rcp.processable.IProcessableProposal;
import org.eclipse.recommenders.completion.rcp.processable.OverlayImageProposalProcessor;
import org.eclipse.recommenders.completion.rcp.processable.ProposalProcessorManager;
import org.eclipse.recommenders.completion.rcp.processable.SessionProcessor;
import org.eclipse.recommenders.completion.rcp.processable.SimpleProposalProcessor;
import org.eclipse.recommenders.internal.calls.rcp.l10n.Messages;
import org.eclipse.recommenders.internal.models.rcp.PrefetchModelArchiveJob;
import org.eclipse.recommenders.models.UniqueTypeName;
import org.eclipse.recommenders.models.rcp.IProjectCoordinateProvider;
import org.eclipse.recommenders.rcp.SharedImages;
import org.eclipse.recommenders.utils.Recommendation;
import org.eclipse.recommenders.utils.Recommendations;
import org.eclipse.recommenders.utils.Result;
import org.eclipse.recommenders.utils.names.IMethodName;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.math.DoubleMath;
@SuppressWarnings({ "restriction" })
public class CallCompletionSessionProcessor extends SessionProcessor {
private static final CompletionProposal NULL_PROPOSAL = new CompletionProposal();
private final ImmutableSet<Class<? extends ASTNode>> supportedCompletionRequests = ImmutableSet
.<Class<? extends ASTNode>>of(CompletionOnMemberAccess.class, CompletionOnMessageSend.class,
CompletionOnQualifiedNameReference.class, CompletionOnSingleNameReference.class);
private final Provider<IProjectCoordinateProvider> pcProvider;
private final Provider<ICallModelProvider> modelProvider;
private final IProposalNameProvider methodNameProvider;
private final CallsRcpPreferences prefs;
private final OverlayImageProposalProcessor overlayProcessor;
private IRecommendersCompletionContext ctx;
private Iterable<Recommendation<IMethodName>> recommendations;
private ICallModel model;
private Set<IMethodName> observedCalls;
private Map<Recommendation<IMethodName>, Integer> recommendationsIndex;
@Inject
public CallCompletionSessionProcessor(Provider<IProjectCoordinateProvider> pcProvider,
Provider<ICallModelProvider> modelProvider, IProposalNameProvider methodNameProvider,
CallsRcpPreferences prefs, SharedImages images) {
this.pcProvider = pcProvider;
this.modelProvider = modelProvider;
this.methodNameProvider = methodNameProvider;
this.prefs = prefs;
this.overlayProcessor = new OverlayImageProposalProcessor(images.getDescriptor(OVR_STAR), IDecoration.TOP_LEFT);
}
@Override
public boolean startSession(final IRecommendersCompletionContext context) {
ctx = context;
recommendations = Lists.newLinkedList();
try {
return isCompletionRequestSupported() && findReceiverTypeAndModel() && findRecommendations();
} finally {
releaseModel();
}
}
private boolean isCompletionRequestSupported() {
final ASTNode node = ctx.getCompletionNode().orNull();
if (node == null) {
return false;
} else {
for (Class<? extends ASTNode> supportedCompletionRequest : supportedCompletionRequests) {
if (supportedCompletionRequest.isInstance(node)) {
return true;
}
}
return false;
}
}
private boolean findReceiverTypeAndModel() {
final IType receiverType = ctx.get(RECEIVER_TYPE2, null);
if (receiverType == null) {
return false;
}
Result<UniqueTypeName> res = pcProvider.get().tryToUniqueName(receiverType);
switch (res.getReason()) {
case OK:
model = modelProvider.get().acquireModel(res.get()).orNull();
return model != null;
case REASON_NOT_IN_CACHE:
new PrefetchModelArchiveJob<ICallModel>(receiverType, pcProvider.get(), modelProvider.get()).schedule(200);
case ABSENT:
default:
return false;
}
}
private boolean findRecommendations() {
// set override-context:
IMethod overrides = ctx.get(ENCLOSING_METHOD_FIRST_DECLARATION, null);
if (overrides != null) {
IMethodName crOverrides = pcProvider.get().toName(overrides)
.or(org.eclipse.recommenders.utils.Constants.UNKNOWN_METHOD);
model.setObservedOverrideContext(crOverrides);
}
// set definition-type and defined-by
model.setObservedDefinitionKind(ctx.get(RECEIVER_DEF_TYPE, null));
model.setObservedDefiningMethod(ctx.get(RECEIVER_DEF_BY, null));
observedCalls = newHashSet(ctx.get(RECEIVER_CALLS, Collections.<IMethodName>emptyList()));
model.setObservedCalls(observedCalls);
// read
recommendations = model.recommendCalls();
// filter void methods if needed:
if (ctx.getExpectedTypeSignature().isPresent()) {
recommendations = Recommendations.filterVoid(recommendations);
}
recommendations = top(recommendations, prefs.maxNumberOfProposals,
max(prefs.minProposalPercentage, 0.01) / 100);
calculateProposalRelevanceBoostMap();
return !isEmpty(recommendations) || !observedCalls.isEmpty();
}
private void calculateProposalRelevanceBoostMap() {
recommendationsIndex = Maps.newHashMap();
for (Recommendation<IMethodName> r : recommendations) {
double rel = r.getRelevance() * 100;
int score = 0;
if (rel < 1d) {
int promille = DoubleMath.roundToInt(rel * 100, HALF_EVEN);
score = promille;
} else {
score = 100 + DoubleMath.roundToInt(rel, HALF_EVEN);
}
recommendationsIndex.put(r, score);
}
}
private void releaseModel() {
if (model != null) {
modelProvider.get().releaseModel(model);
}
}
@Override
public void process(final IProcessableProposal proposal) {
final CompletionProposal coreProposal = proposal.getCoreProposal().or(NULL_PROPOSAL);
switch (coreProposal.getKind()) {
case CompletionProposal.METHOD_REF:
case CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER:
case CompletionProposal.METHOD_NAME_REFERENCE:
IMethodName proposedMethod = methodNameProvider.toMethodName(coreProposal).orNull();
if (proposedMethod == null) {
return;
}
final ProposalMatcher matcher = new ProposalMatcher(proposedMethod);
if (prefs.highlightUsedProposals && handleAlreadyUsedProposal(proposal, matcher)) {
return;
}
handleRecommendation(proposal, matcher);
}
}
private boolean handleAlreadyUsedProposal(IProcessableProposal proposal, ProposalMatcher matcher) {
for (IMethodName observed : observedCalls) {
if (matcher.match(observed)) {
final int boost = prefs.changeProposalRelevance ? 1 : 0;
final String label = prefs.decorateProposalText ? Messages.PROPOSAL_LABEL_USED : ""; //$NON-NLS-1$
ProposalProcessorManager manager = proposal.getProposalProcessorManager();
manager.addProcessor(new SimpleProposalProcessor(boost, label));
if (prefs.decorateProposalIcon) {
manager.addProcessor(overlayProcessor);
}
return true;
}
}
return false;
}
private void handleRecommendation(IProcessableProposal proposal, ProposalMatcher matcher) {
for (final Recommendation<IMethodName> call : recommendations) {
final IMethodName crMethod = call.getProposal();
if (!matcher.match(crMethod)) {
continue;
}
Integer score = recommendationsIndex.get(call);
final int boost = prefs.changeProposalRelevance ? 200 + score : 0;
if (boost > 0) {
proposal.setTag(RECOMMENDERS_SCORE, score);
}
String label = ""; //$NON-NLS-1$
if (prefs.decorateProposalText) {
double relevance = call.getRelevance();
String format = relevance < 0.01d ? Messages.PROPOSAL_LABEL_PROMILLE
: Messages.PROPOSAL_LABEL_PERCENTAGE;
label = format(format, relevance);
}
ProposalProcessorManager mgr = proposal.getProposalProcessorManager();
mgr.addProcessor(new SimpleProposalProcessor(boost, label));
if (prefs.decorateProposalIcon) {
mgr.addProcessor(overlayProcessor);
}
// we found the proposal we are looking for. So quit.
return;
}
}
@VisibleForTesting
public ICallModel getModel() {
return model;
}
}