/*
* Grapht, an open source dependency injector.
* Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt)
* Copyright 2010-2014 Regents of the University of Minnesota
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.grouplens.grapht.solver;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import org.apache.commons.lang3.tuple.Pair;
import org.grouplens.grapht.CachePolicy;
import org.grouplens.grapht.Component;
import org.grouplens.grapht.Dependency;
import org.grouplens.grapht.ResolutionException;
import org.grouplens.grapht.graph.DAGEdge;
import org.grouplens.grapht.graph.DAGNode;
import org.grouplens.grapht.graph.DAGNodeBuilder;
import org.grouplens.grapht.graph.MergePool;
import org.grouplens.grapht.reflect.Desire;
import org.grouplens.grapht.reflect.Satisfaction;
import org.grouplens.grapht.reflect.internal.NullSatisfaction;
import org.grouplens.grapht.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* <p>
* DependencySolver is a utility for resolving Desires into a dependency graph,
* where nodes are shared when permitted by a Satisfaction's dependency
* configuration. It supports qualified and context-aware injection, and
* just-in-time injection if the type has an injectable constructor.
* <p>
* The conceptual binding function used by this solver is represented as a list
* of prioritized {@link BindingFunction functions}. Functions at the start of
* the list are used first, which makes it easy to provide custom functions that
* override default behaviors.
* <p>
* This solver does not support cyclic dependencies because of the possibility
* that a context later on might activate a bind rule that breaks the cycle. To
* ensure termination, it has a maximum context depth that is configurable.
*
* @see DefaultInjector
* @author <a href="http://grouplens.org">GroupLens Research</a>
*/
public class DependencySolver {
private static final Logger logger = LoggerFactory.getLogger(DependencySolver.class);
public static final Component ROOT_SATISFACTION =
Component.create(new NullSatisfaction(Void.TYPE), CachePolicy.NO_PREFERENCE);
/**
* Get an initial injection context.
* @return The context from the initial injection.
*/
public static InjectionContext initialContext() {
return InjectionContext.singleton(ROOT_SATISFACTION.getSatisfaction());
}
/**
* Get a singleton root node for a dependency graph.
* @return A root node for a dependency graph with no resolved objects.
*/
public static DAGNode<Component,Dependency> rootNode() {
return DAGNode.singleton(ROOT_SATISFACTION);
}
private final int maxDepth;
private final CachePolicy defaultPolicy;
private final List<BindingFunction> functions;
private final List<BindingFunction> triggerFunctions;
private DAGNode<Component,Dependency> graph;
private SetMultimap<DAGNode<Component,Dependency>, DAGEdge<Component,Dependency>> backEdges;
private MergePool<Component,Dependency> mergePool;
/**
* Create a DependencySolver that uses the given functions, and max
* depth of the dependency graph.
*
* @param bindFunctions The binding functions that control desire bindings
* @param maxDepth A maximum depth of the graph before it's determined that
* a cycle exists
* @throws IllegalArgumentException if maxDepth is less than 1
* @throws NullPointerException if bindFunctions is null
*/
DependencySolver(List<BindingFunction> bindFunctions,
List<BindingFunction> triggers,
CachePolicy defaultPolicy, int maxDepth) {
Preconditions.notNull("bindFunctions", bindFunctions);
Preconditions.notNull("defaultPolicy", defaultPolicy);
if (maxDepth <= 0) {
throw new IllegalArgumentException("Max depth must be at least 1");
}
this.functions = new ArrayList<BindingFunction>(bindFunctions);
this.triggerFunctions = new ArrayList<BindingFunction>(triggers);
this.maxDepth = maxDepth;
this.defaultPolicy = defaultPolicy;
graph = DAGNode.singleton(ROOT_SATISFACTION);
backEdges = HashMultimap.create();
mergePool = MergePool.create();
logger.info("DependencySolver created, max depth: {}", maxDepth);
}
/**
* Create a new dependency solver builder.
*
* @return A dependency solver builder.
*/
public static DependencySolverBuilder newBuilder() {
return new DependencySolverBuilder();
}
/**
* Get the current full dependency graph. This consists of a synthetic root node with edges
* to the resolutions of all dependencies passed to {@link #resolve(Desire)}.
* @return The resolved dependency graph.
*/
public DAGNode<Component, Dependency> getGraph() {
return graph;
}
/**
* Get the map of back-edges for circular dependencies. Circular dependencies are only allowed
* via provider injection, and only if {@link ProviderBindingFunction} is one of the binding
* functions. In such cases, there will be a back edge from the provider node to the actual
* node being provided, and this map will report that edge.
*
* @return A snapshot of the map of back-edges. This snapshot is entirely independent of the
* back edge map maintained by the dependency solver.
*/
public SetMultimap<DAGNode<Component, Dependency>, DAGEdge<Component, Dependency>> getBackEdges() {
return ImmutableSetMultimap.copyOf(backEdges);
}
/**
* Get the back edge for a particular node and desire, if one exists.
* @return The back edge, or {@code null} if no edge exists.
* @see #getBackEdges()
*/
public synchronized DAGNode<Component, Dependency> getBackEdge(DAGNode<Component, Dependency> parent,
Desire desire) {
Predicate<DAGEdge<?, Dependency>> pred = DAGEdge.labelMatches(Dependency.hasInitialDesire(desire));
return FluentIterable.from(backEdges.get(parent))
.filter(pred)
.first()
.transform(DAGEdge.<Component, Dependency>extractTail())
.orNull();
}
/**
* Get the root node.
* @deprecated Use {@link #getGraph()} instead.
*/
@Deprecated
public DAGNode<Component, Dependency> getRootNode() {
return graph;
}
/**
* Update the dependency graph to include the given desire. An edge from the
* root node to the desire's resolved satisfaction will exist after this is
* finished.
*
* @param desire The desire to include in the graph
*/
public synchronized void resolve(Desire desire) throws ResolutionException {
logger.info("Resolving desire: {}", desire);
Queue<Deferral> deferralQueue = new ArrayDeque<Deferral>();
// before any deferred nodes are processed, we use a synthetic root
// and null original desire since nothing produced this root
deferralQueue.add(new Deferral(rootNode(), initialContext()));
while(!deferralQueue.isEmpty()) {
Deferral current = deferralQueue.poll();
DAGNode<Component, Dependency> parent = current.node;
// deferred nodes are either root - depless - or having deferred dependencies
assert parent.getOutgoingEdges().isEmpty();
if (current.node.getLabel().equals(ROOT_SATISFACTION)) {
Pair<DAGNode<Component, Dependency>, Dependency> rootNode =
resolveFully(desire, current.context, deferralQueue);
// add this to the global graph
graph = DAGNode.copyBuilder(graph)
.addEdge(mergePool.merge(rootNode.getLeft()),
rootNode.getRight())
.build();
} else if (graph.getReachableNodes().contains(parent)) {
// the node needs to be re-scanned. This means that it was not consolidated by
// a previous merge operation. This branch only arises with provider injection.
Satisfaction sat = parent.getLabel().getSatisfaction();
for (Desire d: sat.getDependencies()) {
logger.debug("Attempting to resolve deferred dependency {} of {}", d, sat);
// resolve the dependency
Pair<DAGNode<Component, Dependency>, Dependency> result =
resolveFully(d, current.context, deferralQueue);
// merge it in
DAGNode<Component, Dependency> merged = mergePool.merge(result.getLeft());
// now see if there's a real cycle
if (merged.getReachableNodes().contains(parent)) {
// parent node is referenced from merged, we have a circle!
// that means we need a back edge
backEdges.put(parent, DAGEdge.create(parent, merged, result.getRight()));
} else {
// an edge from parent to merged does not add a cycle
// we have to update graph right away so it's available to merge the next
// dependency
DAGNode<Component, Dependency> newP =
DAGNode.copyBuilder(parent)
.addEdge(merged, result.getRight())
.build();
replaceNode(parent, newP);
parent = newP;
}
}
} else {
// node unreachable - it's a leftover or unneeded deferral
logger.debug("node {} not in graph, ignoring", parent);
}
}
}
private void replaceNode(DAGNode<Component,Dependency> old,
DAGNode<Component,Dependency> repl) {
Map<DAGNode<Component,Dependency>,
DAGNode<Component,Dependency>> memory = Maps.newHashMap();
graph = graph.replaceNode(old, repl, memory);
// loop over a snapshot of the list, replacing nodes
Collection<DAGEdge<Component, Dependency>> oldBackEdges = backEdges.values();
backEdges = HashMultimap.create();
for (DAGEdge<Component,Dependency> edge: oldBackEdges) {
DAGNode<Component,Dependency> newHead, newTail;
newHead = memory.get(edge.getHead());
if (newHead == null) {
newHead = edge.getHead();
}
newTail = memory.get(edge.getTail());
if (newTail == null) {
newTail = edge.getTail();
}
DAGEdge<Component,Dependency> newEdge;
if (newHead.equals(edge.getHead()) && newTail.equals(edge.getTail())) {
newEdge = edge;
} else {
newEdge = DAGEdge.create(newHead, newTail, edge.getLabel());
}
backEdges.put(newHead, newEdge);
}
}
/**
* Rewrite a dependency graph using the rules in this solver. The accumulated global graph and
* back edges are ignored and not modified.
* <p>Graph rewrite walks the graph, looking for nodes to rewrite. If the desire that leads
* to a node is matched by a trigger binding function, then it is resolved using the binding
* functions and replaced with the resulting (merged) node. Rewriting proceeds from the root
* down, but does not consider the children of nodes generated by the rewriting process.</p>
*
* @param graph The graph to rewrite.
* @return A rewritten version of the graph.
*/
public DAGNode<Component,Dependency> rewrite(DAGNode<Component,Dependency> graph) throws ResolutionException {
if (!graph.getLabel().getSatisfaction().getErasedType().equals(Void.TYPE)) {
throw new IllegalArgumentException("only full dependency graphs can be rewritten");
}
logger.debug("rewriting graph with {} nodes", graph.getReachableNodes().size());
// We proceed in three stages.
Map<DAGEdge<Component, Dependency>, DAGEdge<Component,Dependency>> replacementSubtrees =
Maps.newHashMap();
walkGraphForReplacements(graph,
InjectionContext.singleton(graph.getLabel().getSatisfaction()),
replacementSubtrees);
DAGNode<Component, Dependency> stage2 =
graph.transformEdges(Functions.forMap(replacementSubtrees, null));
logger.debug("merging rewritten graph");
// Now we have a graph (stage2) with rewritten subtrees based on trigger rules
// We merge this graph with the original to deduplicate.
MergePool<Component,Dependency> pool = MergePool.create();
pool.merge(graph);
return pool.merge(stage2);
}
/**
* Walk the graph, looking for replacements.
* @param root The node to walk.
* @param context The context leading to this node.
* @param replacements The map of replacements to build. This maps edges to their replacement
* targets and labels.
* @throws ResolutionException If there is a resolution error rewriting the graph.
*/
private void walkGraphForReplacements(DAGNode<Component, Dependency> root,
InjectionContext context,
Map<DAGEdge<Component, Dependency>, DAGEdge<Component, Dependency>> replacements) throws ResolutionException {
assert context.getTailValue().getLeft().equals(root.getLabel().getSatisfaction());
for (DAGEdge<Component, Dependency> edge: root.getOutgoingEdges()) {
logger.debug("considering {} for replacement", edge.getTail().getLabel());
Desire desire = edge.getLabel().getDesireChain().getInitialDesire();
DesireChain chain = DesireChain.singleton(desire);
Pair<DAGNode<Component, Dependency>, Dependency> repl = null;
if (!edge.getLabel().isFixed()) {
for (BindingFunction bf: triggerFunctions) {
BindingResult result = bf.bind(context, chain);
if (result != null) {
// resolve the node
// we could reuse the resolution, but perf savings isn't worth complexity
repl = resolveFully(desire, context, null);
break;
}
}
} else {
logger.debug("{} is fixed, skipping", edge.getTail().getLabel());
}
if (repl == null) {
// no trigger bindings, walk the node's children
InjectionContext next = context.extend(edge.getTail()
.getLabel()
.getSatisfaction(),
edge.getLabel()
.getDesireChain()
.getInitialDesire()
.getInjectionPoint());
walkGraphForReplacements(edge.getTail(), next, replacements);
} else {
// trigger binding, add a replacement
logger.info("replacing {} with {}",
edge.getTail().getLabel(),
repl.getLeft().getLabel());
replacements.put(edge, DAGEdge.create(root, repl.getLeft(), repl.getRight()));
}
}
}
/**
* Resolve a desire and its dependencies, inserting them into the graph.
*
* @param desire The desire to resolve.
* @param context The context of {@code parent}.
* @param deferQueue The queue of node deferrals.
* @throws ResolutionException if there is an error resolving the nodes.
*/
private Pair<DAGNode<Component,Dependency>,Dependency>
resolveFully(Desire desire, InjectionContext context, Queue<Deferral> deferQueue) throws ResolutionException {
// check context depth against max to detect likely dependency cycles
if (context.size() > maxDepth) {
throw new CyclicDependencyException(desire, "Maximum context depth of " + maxDepth + " was reached");
}
// resolve the current node
Resolution result = resolve(desire, context);
InjectionContext newContext = context.extend(result.satisfaction, desire.getInjectionPoint());
DAGNode<Component, Dependency> node;
if (result.deferDependencies) {
// extend node onto deferred queue and skip its dependencies for now
logger.debug("Deferring dependencies of {}", result.satisfaction);
node = DAGNode.singleton(result.makeSatisfaction());
// FIXME Deferred and skippable bindings do not interact well
deferQueue.add(new Deferral(node, newContext));
return Pair.of(node, result.makeDependency());
} else {
return resolveDepsAndMakeNode(deferQueue, result, newContext);
}
}
private Pair<DAGNode<Component, Dependency>,Dependency> resolveDepsAndMakeNode(Queue<Deferral> deferQueue,
Resolution result,
InjectionContext newContext) throws ResolutionException {
DAGNode<Component, Dependency> node;// build up a node with its outgoing edges
DAGNodeBuilder<Component,Dependency> nodeBuilder = DAGNode.newBuilder();
nodeBuilder.setLabel(result.makeSatisfaction());
for (Desire d: result.satisfaction.getDependencies()) {
// complete the sub graph for the given desire
// - the call to resolveFully() is responsible for adding the dependency edges
// so we don't need to process the returned node
logger.debug("Attempting to satisfy dependency {} of {}", d, result.satisfaction);
Pair<DAGNode<Component, Dependency>, Dependency> dep;
try {
dep = resolveFully(d, newContext, deferQueue);
} catch (UnresolvableDependencyException ex) {
if (!d.equals(ex.getDesireChain().getInitialDesire())) {
// this is for some other (deeper) desire, fail
throw ex;
}
// whoops, try to backtrack
Resolution back = result.skippable ? result.backtrack() : null;
if (back != null) {
InjectionContext popped = newContext.getLeading();
InjectionContext forked = InjectionContext.extend(popped, back.satisfaction,
back.desires.getInitialDesire().getInjectionPoint());
return resolveDepsAndMakeNode(deferQueue, back, forked);
} else if (result.backtracked || result.skippable) {
// the result is the result of backtracking, or could be, so make an error at this dependency
throw new UnresolvableDependencyException(result.desires, newContext.getLeading(), ex);
} else {
throw ex;
}
}
nodeBuilder.addEdge(dep);
}
node = nodeBuilder.build();
return Pair.of(node, result.makeDependency());
}
private Resolution resolve(Desire desire, InjectionContext context) throws ResolutionException {
DesireChain chain = DesireChain.singleton(desire);
CachePolicy policy = CachePolicy.NO_PREFERENCE;
boolean fixed = false;
boolean skippable = false;
while(true) {
logger.debug("Current desire: {}", chain.getCurrentDesire());
BindingResult binding = null;
for (BindingFunction bf: functions) {
binding = bf.bind(context, chain);
if (binding != null && !chain.getPreviousDesires().contains(binding.getDesire())) {
// found a binding that hasn't been used before
break;
}
}
boolean defer = false;
boolean terminate = true; // so we stop if there is no binding
if (binding != null) {
// update the desire chain
chain = chain.extend(binding.getDesire());
terminate = binding.terminates(); // binding decides if we stop
defer = binding.isDeferred();
fixed |= binding.isFixed();
skippable = binding.isSkippable();
// upgrade policy if needed
if (binding.getCachePolicy().compareTo(policy) > 0) {
policy = binding.getCachePolicy();
}
}
if (terminate && chain.getCurrentDesire().isInstantiable()) {
logger.info("Satisfied {} with {}", desire, chain.getCurrentDesire().getSatisfaction());
// update cache policy if a specific policy hasn't yet been selected
if (policy.equals(CachePolicy.NO_PREFERENCE)) {
policy = chain.getCurrentDesire().getSatisfaction().getDefaultCachePolicy();
if (policy.equals(CachePolicy.NO_PREFERENCE)) {
policy = defaultPolicy;
}
}
return new Resolution(chain.getCurrentDesire().getSatisfaction(), policy, chain, fixed, defer, skippable, false);
} else if (binding == null) {
// no more desires to process, it cannot be satisfied
throw new UnresolvableDependencyException(chain, context);
}
}
}
/*
* Result tuple for resolve(Desire, InjectionContext)
*/
private static class Resolution {
private final Satisfaction satisfaction;
private final CachePolicy policy;
private final DesireChain desires;
private final boolean fixed;
private final boolean deferDependencies;
private final boolean skippable;
private final boolean backtracked;
public Resolution(Satisfaction satisfaction, CachePolicy policy,
DesireChain desires, boolean fixed,
boolean deferDependencies,
boolean skippable,
boolean backtracked) {
this.satisfaction = satisfaction;
this.policy = policy;
this.desires = desires;
this.fixed = fixed;
this.deferDependencies = deferDependencies;
this.skippable = skippable;
this.backtracked = backtracked;
}
public Component makeSatisfaction() {
return Component.create(satisfaction, policy);
}
public Dependency makeDependency() {
EnumSet<Dependency.Flag> flags = Dependency.Flag.emptySet();
if (fixed) {
flags.add(Dependency.Flag.FIXED);
}
return Dependency.create(desires, flags);
}
/**
* Backtrack this resolution to skip a binding.
*
* @return The backtracked resolution, or {@code null} if the resolution cannot backtrack because doing so
* would result in a non-instantiable resolution.
* @throws IllegalArgumentException if attempting to backtrack would be invalid, because the resolution is
* not skippable.
*/
public Resolution backtrack() {
if (!skippable) {
throw new IllegalArgumentException("unskippable resolution can't backtrack");
}
if (desires.size() <= 1) {
throw new IllegalArgumentException("singleton desire chain can't backtrack");
}
DesireChain shrunk = desires.getPreviousDesireChain();
if (shrunk.getCurrentDesire().isInstantiable()) {
return new Resolution(shrunk.getCurrentDesire().getSatisfaction(),
policy, // FIXME Backtrack the policy
shrunk,
fixed, // FIXME If we allow skippability on non-default bindings, this is wrong
deferDependencies, // FIXME same here
false, true);
} else {
return null;
}
}
@Override
public String toString() {
return "(" + satisfaction + ", " + policy + ")";
}
}
/*
* Deferred results tuple
*/
private static class Deferral {
private final DAGNode<Component, Dependency> node;
private final InjectionContext context;
public Deferral(DAGNode<Component, Dependency> node,
InjectionContext context) {
this.node = node;
this.context = context;
}
}
}