/**
* 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.calls;
import static com.google.common.base.Optional.*;
import static com.google.common.collect.Collections2.transform;
import static com.google.common.collect.ImmutableSet.copyOf;
import static org.eclipse.recommenders.utils.Constants.*;
import static org.eclipse.recommenders.utils.Recommendation.newRecommendation;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.recommenders.commons.bayesnet.BayesianNetwork;
import org.eclipse.recommenders.jayes.BayesNet;
import org.eclipse.recommenders.jayes.BayesNode;
import org.eclipse.recommenders.jayes.inference.jtree.JunctionTreeAlgorithm;
import org.eclipse.recommenders.jayes.inference.jtree.JunctionTreeBuilder;
import org.eclipse.recommenders.jayes.io.IBayesNetReader;
import org.eclipse.recommenders.jayes.io.jbif.JayesBifReader;
import org.eclipse.recommenders.jayes.util.triangulation.MinDegree;
import org.eclipse.recommenders.utils.Constants;
import org.eclipse.recommenders.utils.IOUtils;
import org.eclipse.recommenders.utils.Nullable;
import org.eclipse.recommenders.utils.Recommendation;
import org.eclipse.recommenders.utils.names.IFieldName;
import org.eclipse.recommenders.utils.names.IMethodName;
import org.eclipse.recommenders.utils.names.ITypeName;
import org.eclipse.recommenders.utils.names.VmMethodName;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.collect.Lists;
/**
* A thin wrapper around a {@link BayesianNetwork} for recommending method calls.
* <p>
* The Bayesian network is expected to follow the structure specified below:
* <ul>
* <li>every node must have at least <b>2 states</b>!
* <li>the first state is supposed to be a dummy state. Call it like {@link Constants#DUMMY_METHOD}
* <li>the second state <b>may</b> to be a dummy state too if no valuable other state could be found.
* </ul>
* <ul>
* <li><b>callgroup node (formerly called pattern node):</b>
* <ul>
* <li>node name: {@link Constants#N_NODEID_CALL_GROUPS}
* <li>state names: no constraints. Recommended schema is to use 'p'#someNumber.
* </ul>
* <li><b>context node:</b>
* <ul>
* <li>node name: {@link Constants#N_NODEID_CONTEXT}
* <li>state names: fully-qualified method names as returned by {@link IMethodName#getIdentifier()}.
* </ul>
* <li><b>definition node:</b>
* <ul>
* <li>node name: {@link Constants#N_NODEID_DEF}
* <li>state names: fully-qualified names as returned by {@link IMethodName#getIdentifier()} or
* {@link IFieldName#getIdentifier()}.
* </ul>
* <li><b>definition kind node:</b>
* <ul>
* <li>node name: {@link Constants#N_NODEID_DEF_KIND}
* <li>state names: one of {@link DefinitionKind}, i.e., METHOD_RETURN, NEW, FIELD, PARAMETER, THIS, UNKNOWN, or ANY
* </ul>
* <li><b>method call node:</b>
* <ul>
* <li>node name: {@link IMethodName#getIdentifier()}
* <li>state names: {@link Constants#N_STATE_TRUE} or {@link Constants#N_STATE_FALSE}
* </ul>
* </ul>
*/
@SuppressWarnings("deprecation")
@Beta
public class JayesCallModel implements ICallModel {
public static ICallModel load(InputStream is, ITypeName type) throws IOException {
BayesNet net = getModel(is, type);
return new JayesCallModel(type, net);
}
private static BayesNet getModel(InputStream is, ITypeName type) throws IOException {
IBayesNetReader rdr = new JayesBifReader(is);
try {
return rdr.read();
} finally {
IOUtils.closeQuietly(rdr);
}
}
private static final class StringToMethodNameFunction implements Function<String, IMethodName> {
@Override
public IMethodName apply(final String input) {
return VmMethodName.get(input);
}
}
private final BayesNet net;
private final BayesNode callgroupNode;
private final BayesNode overridesNode;
private final BayesNode definedByNode;
private final BayesNode defKindNode;
private final JunctionTreeAlgorithm junctionTree;
private final ITypeName typeName;
private final Map<IMethodName, BayesNode> callNodes;
private static final List<String> SPECIAL_NODES = Arrays.asList(N_NODEID_CONTEXT, N_NODEID_CALL_GROUPS,
N_NODEID_DEF_KIND, N_NODEID_DEF);
public JayesCallModel(final ITypeName name, final BayesNet net) {
this.net = net;
this.typeName = name;
this.callNodes = new HashMap<IMethodName, BayesNode>();
this.junctionTree = new JunctionTreeAlgorithm();
junctionTree.setJunctionTreeBuilder(JunctionTreeBuilder.forHeuristic(new MinDegree()));
junctionTree.setNetwork(net);
overridesNode = net.getNode(N_NODEID_CONTEXT);
callgroupNode = net.getNode(N_NODEID_CALL_GROUPS);
defKindNode = net.getNode(N_NODEID_DEF_KIND);
definedByNode = net.getNode(N_NODEID_DEF);
setCallNodes(net);
}
private void setCallNodes(BayesNet net) {
for (BayesNode bayesNode : net.getNodes()) {
String name = bayesNode.getName();
if (!SPECIAL_NODES.contains(name)) {
VmMethodName vmMethodName = VmMethodName.get(name);
callNodes.put(vmMethodName, bayesNode);
}
}
}
private Optional<IMethodName> computeMethodNameFromState(final BayesNode node) {
String stateId = junctionTree.getEvidence().get(node);
if (stateId == null) {
return absent();
}
return Optional.<IMethodName>of(VmMethodName.get(stateId));
}
@Override
public ImmutableSet<IMethodName> getKnownCalls() {
return ImmutableSet.<IMethodName>builder().addAll(callNodes.keySet()).build();
}
@Override
public ImmutableSet<IMethodName> getKnownOverrideContexts() {
Collection<IMethodName> tmp = transform(overridesNode.getOutcomes(), new StringToMethodNameFunction());
return copyOf(tmp);
}
@Override
public ImmutableSet<IMethodName> getKnownDefiningMethods() {
Collection<IMethodName> tmp = transform(definedByNode.getOutcomes(), new StringToMethodNameFunction());
return copyOf(tmp);
}
@Override
public ImmutableSet<DefinitionKind> getKnownDefinitionKinds() {
Builder<DefinitionKind> b = ImmutableSet.builder();
for (String s : defKindNode.getOutcomes()) {
b.add(DefinitionKind.valueOf(s));
}
return b.build();
}
@Override
public ImmutableSet<String> getKnownPatterns() {
return copyOf(callgroupNode.getOutcomes());
}
@Override
public ImmutableSet<IMethodName> getObservedCalls() {
Builder<IMethodName> builder = ImmutableSet.<IMethodName>builder();
Map<BayesNode, String> evidence = junctionTree.getEvidence();
for (Entry<IMethodName, BayesNode> pair : callNodes.entrySet()) {
BayesNode node = pair.getValue();
IMethodName method = pair.getKey();
if (evidence.containsKey(node) && evidence.get(node).equals(Constants.N_STATE_TRUE)
// remove the NULL that may have been introduced by
// res.add(compute...)
&& !VmMethodName.NULL.equals(method)) {
builder.add(method);
}
}
return builder.build();
}
@Override
public Optional<IMethodName> getObservedDefiningMethod() {
return computeMethodNameFromState(definedByNode);
}
@Override
public Optional<IMethodName> getObservedOverrideContext() {
return computeMethodNameFromState(overridesNode);
}
@Override
public Optional<DefinitionKind> getObservedDefinitionKind() {
String stateId = junctionTree.getEvidence().get(defKindNode);
if (stateId == null) {
return absent();
}
return of(DefinitionKind.valueOf(stateId));
}
@Override
public List<Recommendation<IMethodName>> recommendCalls() {
List<Recommendation<IMethodName>> recs = Lists.newLinkedList();
for (Entry<IMethodName, BayesNode> entry : callNodes.entrySet()) {
IMethodName method = entry.getKey();
BayesNode bayesNode = entry.getValue();
boolean isAlreadyUsedAsEvidence = junctionTree.getEvidence().containsKey(bayesNode);
if (!isAlreadyUsedAsEvidence) {
int indexForTrue = bayesNode.getOutcomeIndex(N_STATE_TRUE);
double[] probabilities = junctionTree.getBeliefs(bayesNode);
double probability = probabilities[indexForTrue];
recs.add(newRecommendation(method, probability));
}
}
return recs;
}
@Override
public List<Recommendation<IMethodName>> recommendDefinitions() {
List<Recommendation<IMethodName>> recs = Lists.newLinkedList();
double[] beliefs = junctionTree.getBeliefs(definedByNode);
for (int i = definedByNode.getOutcomeCount(); i-- > 0;) {
if (beliefs[i] > 0.01d) {
String outcomeName = definedByNode.getOutcomeName(i);
if (outcomeName.equals(NONE_METHOD.getIdentifier())) {
continue;
}
if (outcomeName.equals(UNKNOWN_METHOD.getIdentifier())) {
continue;
}
VmMethodName definition = VmMethodName.get(outcomeName);
Recommendation<IMethodName> r = newRecommendation(definition, beliefs[i]);
recs.add(r);
}
}
return recs;
}
@Override
public List<Recommendation<String>> recommendPatterns() {
List<Recommendation<String>> recs = Lists.newLinkedList();
double[] probs = junctionTree.getBeliefs(callgroupNode);
for (String outcome : callgroupNode.getOutcomes()) {
int probIndex = callgroupNode.getOutcomeIndex(outcome);
double p = probs[probIndex];
recs.add(newRecommendation(outcome, p));
}
return recs;
}
@Override
public ITypeName getReceiverType() {
return typeName;
}
@Override
public void reset() {
junctionTree.getEvidence().clear();
}
@Override
public boolean setObservedCalls(final Set<IMethodName> calls) {
for (IMethodName m : getObservedCalls()) {
// clear previously called methods by setting observation state to NULL
setCalled(m, null);
}
if (calls == null) {
return true;
}
boolean pass = true;
for (IMethodName m : calls) {
pass &= setCalled(m, N_STATE_TRUE);
}
// explicitly set the "no-method" used node to false:
// TODO we disabled this for testing purpose:
// pass &= setCalled(Constants.NO_METHOD, N_STATE_FALSE);
return pass;
}
@Override
public boolean setObservedDefiningMethod(@Nullable final IMethodName newDefinition) {
if (newDefinition == null) {
junctionTree.removeEvidence(definedByNode);
return true;
}
// else:
String identifier = newDefinition.getIdentifier();
boolean contains = definedByNode.getOutcomes().contains(identifier);
if (contains) {
junctionTree.addEvidence(definedByNode, identifier);
}
return contains;
}
@Override
public boolean setObservedOverrideContext(@Nullable final IMethodName newEnclosingMethod) {
if (newEnclosingMethod == null) {
junctionTree.removeEvidence(overridesNode);
return true;
}
// else:
String id = newEnclosingMethod.getIdentifier();
boolean contains = overridesNode.getOutcomes().contains(id);
if (contains) {
junctionTree.addEvidence(overridesNode, id);
}
return contains;
}
@Override
public boolean setObservedDefinitionKind(@Nullable final DefinitionKind newDef) {
if (newDef == null) {
junctionTree.removeEvidence(defKindNode);
return true;
}
// else:
String identifier = newDef.toString();
boolean contains = defKindNode.getOutcomes().contains(identifier);
if (contains) {
junctionTree.addEvidence(defKindNode, identifier);
}
return contains;
}
@Override
public boolean setObservedPattern(@Nullable final String patternName) {
if (patternName == null) {
junctionTree.removeEvidence(callgroupNode);
return true;
}
// else:
boolean contains = callgroupNode.getOutcomes().contains(patternName);
if (contains) {
junctionTree.addEvidence(callgroupNode, patternName);
}
return contains;
}
private boolean setCalled(final IMethodName m, @Nullable final String state) {
IMethodName rebased = VmMethodName.rebase(typeName, m);
BayesNode node = net.getNode(rebased.getIdentifier());
if (node == null) {
return false;
}
if (state == null) {
junctionTree.removeEvidence(node);
} else {
junctionTree.addEvidence(node, state);
}
return true;
}
}