View Javadoc

1   /*
2    * Grapht, an open source dependency injector.
3    * Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt)
4    * Copyright 2010-2014 Regents of the University of Minnesota
5    *
6    * This program is free software; you can redistribute it and/or modify
7    * it under the terms of the GNU Lesser General Public License as
8    * published by the Free Software Foundation; either version 2.1 of the
9    * License, or (at your option) any later version.
10   *
11   * This program is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13   * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14   * details.
15   *
16   * You should have received a copy of the GNU General Public License along with
17   * this program; if not, write to the Free Software Foundation, Inc., 51
18   * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19   */
20  package org.grouplens.grapht.solver;
21  
22  import com.google.common.base.Functions;
23  import com.google.common.base.Predicate;
24  import com.google.common.collect.*;
25  import org.apache.commons.lang3.tuple.Pair;
26  import org.grouplens.grapht.CachePolicy;
27  import org.grouplens.grapht.Component;
28  import org.grouplens.grapht.Dependency;
29  import org.grouplens.grapht.ResolutionException;
30  import org.grouplens.grapht.graph.DAGEdge;
31  import org.grouplens.grapht.graph.DAGNode;
32  import org.grouplens.grapht.graph.DAGNodeBuilder;
33  import org.grouplens.grapht.graph.MergePool;
34  import org.grouplens.grapht.reflect.Desire;
35  import org.grouplens.grapht.reflect.Satisfaction;
36  import org.grouplens.grapht.reflect.internal.NullSatisfaction;
37  import org.grouplens.grapht.util.Preconditions;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import java.util.*;
42  
43  /**
44   * <p>
45   * DependencySolver is a utility for resolving Desires into a dependency graph,
46   * where nodes are shared when permitted by a Satisfaction's dependency
47   * configuration. It supports qualified and context-aware injection, and
48   * just-in-time injection if the type has an injectable constructor.
49   * <p>
50   * The conceptual binding function used by this solver is represented as a list
51   * of prioritized {@link BindingFunction functions}. Functions at the start of
52   * the list are used first, which makes it easy to provide custom functions that
53   * override default behaviors.
54   * <p>
55   * This solver does not support cyclic dependencies because of the possibility
56   * that a context later on might activate a bind rule that breaks the cycle. To
57   * ensure termination, it has a maximum context depth that is configurable.
58   * 
59   * @see DefaultInjector
60   * @author <a href="http://grouplens.org">GroupLens Research</a>
61   */
62  public class DependencySolver {
63      private static final Logger logger = LoggerFactory.getLogger(DependencySolver.class);
64      public static final Component ROOT_SATISFACTION =
65              Component.create(new NullSatisfaction(Void.TYPE), CachePolicy.NO_PREFERENCE);
66  
67      /**
68       * Get an initial injection context.
69       * @return The context from the initial injection.
70       */
71      public static InjectionContext initialContext() {
72          return InjectionContext.singleton(ROOT_SATISFACTION.getSatisfaction());
73      }
74  
75      /**
76       * Get a singleton root node for a dependency graph.
77       * @return A root node for a dependency graph with no resolved objects.
78       */
79      public static DAGNode<Component,Dependency> rootNode() {
80          return DAGNode.singleton(ROOT_SATISFACTION);
81      }
82  
83      private final int maxDepth;
84      private final CachePolicy defaultPolicy;
85  
86      private final List<BindingFunction> functions;
87      private final List<BindingFunction> triggerFunctions;
88      
89      private DAGNode<Component,Dependency> graph;
90      private SetMultimap<DAGNode<Component,Dependency>, DAGEdge<Component,Dependency>> backEdges;
91      private MergePool<Component,Dependency> mergePool;
92  
93      /**
94       * Create a DependencySolver that uses the given functions, and max
95       * depth of the dependency graph.
96       * 
97       * @param bindFunctions The binding functions that control desire bindings
98       * @param maxDepth A maximum depth of the graph before it's determined that
99       *            a cycle exists
100      * @throws IllegalArgumentException if maxDepth is less than 1
101      * @throws NullPointerException if bindFunctions is null
102      */
103     DependencySolver(List<BindingFunction> bindFunctions,
104                      List<BindingFunction> triggers,
105                      CachePolicy defaultPolicy, int maxDepth) {
106         Preconditions.notNull("bindFunctions", bindFunctions);
107         Preconditions.notNull("defaultPolicy", defaultPolicy);
108         if (maxDepth <= 0) {
109             throw new IllegalArgumentException("Max depth must be at least 1");
110         }
111         
112         this.functions = new ArrayList<BindingFunction>(bindFunctions);
113         this.triggerFunctions = new ArrayList<BindingFunction>(triggers);
114         this.maxDepth = maxDepth;
115         this.defaultPolicy = defaultPolicy;
116         
117         graph = DAGNode.singleton(ROOT_SATISFACTION);
118         backEdges = HashMultimap.create();
119         mergePool = MergePool.create();
120 
121         logger.info("DependencySolver created, max depth: {}", maxDepth);
122     }
123 
124     /**
125      * Create a new dependency solver builder.
126      *
127      * @return A dependency solver builder.
128      */
129     public static DependencySolverBuilder newBuilder() {
130         return new DependencySolverBuilder();
131     }
132     
133     /**
134      * Get the current full dependency graph. This consists of a synthetic root node with edges
135      * to the resolutions of all dependencies passed to {@link #resolve(Desire)}.
136      * @return The resolved dependency graph.
137      */
138     public DAGNode<Component, Dependency> getGraph() {
139         return graph;
140     }
141 
142     /**
143      * Get the map of back-edges for circular dependencies.  Circular dependencies are only allowed
144      * via provider injection, and only if {@link ProviderBindingFunction} is one of the binding
145      * functions.  In such cases, there will be a back edge from the provider node to the actual
146      * node being provided, and this map will report that edge.
147      *
148      * @return A snapshot of the map of back-edges.  This snapshot is entirely independent of the
149      *         back edge map maintained by the dependency solver.
150      */
151     public SetMultimap<DAGNode<Component, Dependency>, DAGEdge<Component, Dependency>> getBackEdges() {
152         return ImmutableSetMultimap.copyOf(backEdges);
153     }
154 
155     /**
156      * Get the back edge for a particular node and desire, if one exists.
157      * @return The back edge, or {@code null} if no edge exists.
158      * @see #getBackEdges()
159      */
160     public synchronized DAGNode<Component, Dependency> getBackEdge(DAGNode<Component, Dependency> parent,
161                                                                             Desire desire) {
162         Predicate<DAGEdge<?, Dependency>> pred = DAGEdge.labelMatches(Dependency.hasInitialDesire(desire));
163         return FluentIterable.from(backEdges.get(parent))
164                              .filter(pred)
165                              .first()
166                              .transform(DAGEdge.<Component, Dependency>extractTail())
167                              .orNull();
168     }
169 
170     /**
171      * Get the root node.
172      * @deprecated Use {@link #getGraph()} instead.
173      */
174     @Deprecated
175     public DAGNode<Component, Dependency> getRootNode() {
176         return graph;
177     }
178     
179     /**
180      * Update the dependency graph to include the given desire. An edge from the
181      * root node to the desire's resolved satisfaction will exist after this is
182      * finished.
183      * 
184      * @param desire The desire to include in the graph
185      */
186     public synchronized void resolve(Desire desire) throws ResolutionException {
187         logger.info("Resolving desire: {}", desire);
188 
189         Queue<Deferral> deferralQueue = new ArrayDeque<Deferral>();
190 
191         // before any deferred nodes are processed, we use a synthetic root
192         // and null original desire since nothing produced this root
193         deferralQueue.add(new Deferral(rootNode(), initialContext()));
194 
195         while(!deferralQueue.isEmpty()) {
196             Deferral current = deferralQueue.poll();
197             DAGNode<Component, Dependency> parent = current.node;
198             // deferred nodes are either root - depless - or having deferred dependencies
199             assert parent.getOutgoingEdges().isEmpty();
200 
201             if (current.node.getLabel().equals(ROOT_SATISFACTION)) {
202                 Pair<DAGNode<Component, Dependency>, Dependency> rootNode =
203                         resolveFully(desire, current.context, deferralQueue);
204                 // add this to the global graph
205                 graph = DAGNode.copyBuilder(graph)
206                                .addEdge(mergePool.merge(rootNode.getLeft()),
207                                         rootNode.getRight())
208                                .build();
209             } else if (graph.getReachableNodes().contains(parent)) {
210                 // the node needs to be re-scanned.  This means that it was not consolidated by
211                 // a previous merge operation.  This branch only arises with provider injection.
212                 Satisfaction sat = parent.getLabel().getSatisfaction();
213                 for (Desire d: sat.getDependencies()) {
214                     logger.debug("Attempting to resolve deferred dependency {} of {}", d, sat);
215                     // resolve the dependency
216                     Pair<DAGNode<Component, Dependency>, Dependency> result =
217                             resolveFully(d, current.context, deferralQueue);
218                     // merge it in
219                     DAGNode<Component, Dependency> merged = mergePool.merge(result.getLeft());
220                     // now see if there's a real cycle
221                     if (merged.getReachableNodes().contains(parent)) {
222                         // parent node is referenced from merged, we have a circle!
223                         // that means we need a back edge
224                         backEdges.put(parent, DAGEdge.create(parent, merged, result.getRight()));
225                     } else {
226                         // an edge from parent to merged does not add a cycle
227                         // we have to update graph right away so it's available to merge the next
228                         // dependency
229                         DAGNode<Component, Dependency> newP =
230                                 DAGNode.copyBuilder(parent)
231                                        .addEdge(merged, result.getRight())
232                                        .build();
233                         replaceNode(parent, newP);
234                         parent = newP;
235                     }
236                 }
237             } else {
238                 // node unreachable - it's a leftover or unneeded deferral
239                 logger.debug("node {} not in graph, ignoring", parent);
240             }
241         }
242     }
243 
244     private void replaceNode(DAGNode<Component,Dependency> old,
245                              DAGNode<Component,Dependency> repl) {
246         Map<DAGNode<Component,Dependency>,
247                 DAGNode<Component,Dependency>> memory = Maps.newHashMap();
248         graph = graph.replaceNode(old, repl, memory);
249 
250         // loop over a snapshot of the list, replacing nodes
251         Collection<DAGEdge<Component, Dependency>> oldBackEdges = backEdges.values();
252         backEdges = HashMultimap.create();
253         for (DAGEdge<Component,Dependency> edge: oldBackEdges) {
254             DAGNode<Component,Dependency> newHead, newTail;
255             newHead = memory.get(edge.getHead());
256             if (newHead == null) {
257                 newHead = edge.getHead();
258             }
259             newTail = memory.get(edge.getTail());
260             if (newTail == null) {
261                 newTail = edge.getTail();
262             }
263             DAGEdge<Component,Dependency> newEdge;
264             if (newHead.equals(edge.getHead()) && newTail.equals(edge.getTail())) {
265                 newEdge = edge;
266             } else {
267                 newEdge = DAGEdge.create(newHead, newTail, edge.getLabel());
268             }
269             backEdges.put(newHead, newEdge);
270         }
271     }
272 
273     /**
274      * Rewrite a dependency graph using the rules in this solver.  The accumulated global graph and
275      * back edges are ignored and not modified.
276      * <p>Graph rewrite walks the graph, looking for nodes to rewrite.  If the desire that leads
277      * to a node is matched by a trigger binding function, then it is resolved using the binding
278      * functions and replaced with the resulting (merged) node.  Rewriting proceeds from the root
279      * down, but does not consider the children of nodes generated by the rewriting process.</p>
280      *
281      * @param graph The graph to rewrite.
282      * @return A rewritten version of the graph.
283      */
284     public DAGNode<Component,Dependency> rewrite(DAGNode<Component,Dependency> graph) throws ResolutionException {
285         if (!graph.getLabel().getSatisfaction().getErasedType().equals(Void.TYPE)) {
286             throw new IllegalArgumentException("only full dependency graphs can be rewritten");
287         }
288 
289         logger.debug("rewriting graph with {} nodes", graph.getReachableNodes().size());
290         // We proceed in three stages.
291         Map<DAGEdge<Component, Dependency>, DAGEdge<Component,Dependency>> replacementSubtrees =
292                 Maps.newHashMap();
293         walkGraphForReplacements(graph,
294                                  InjectionContext.singleton(graph.getLabel().getSatisfaction()),
295                                  replacementSubtrees);
296 
297         DAGNode<Component, Dependency> stage2 =
298                 graph.transformEdges(Functions.forMap(replacementSubtrees, null));
299 
300         logger.debug("merging rewritten graph");
301         // Now we have a graph (stage2) with rewritten subtrees based on trigger rules
302         // We merge this graph with the original to deduplicate.
303         MergePool<Component,Dependency> pool = MergePool.create();
304         pool.merge(graph);
305         return pool.merge(stage2);
306     }
307 
308     /**
309      * Walk the graph, looking for replacements.
310      * @param root The node to walk.
311      * @param context The context leading to this node.
312      * @param replacements The map of replacements to build. This maps edges to their replacement
313      *                     targets and labels.
314      * @throws ResolutionException If there is a resolution error rewriting the graph.
315      */
316     private void walkGraphForReplacements(DAGNode<Component, Dependency> root,
317                                           InjectionContext context,
318                                           Map<DAGEdge<Component, Dependency>, DAGEdge<Component, Dependency>> replacements) throws ResolutionException {
319         assert context.getTailValue().getLeft().equals(root.getLabel().getSatisfaction());
320         for (DAGEdge<Component, Dependency> edge: root.getOutgoingEdges()) {
321             logger.debug("considering {} for replacement", edge.getTail().getLabel());
322             Desire desire = edge.getLabel().getDesireChain().getInitialDesire();
323             DesireChain chain = DesireChain.singleton(desire);
324             Pair<DAGNode<Component, Dependency>, Dependency> repl = null;
325             if (!edge.getLabel().isFixed()) {
326                 for (BindingFunction bf: triggerFunctions) {
327                     BindingResult result = bf.bind(context, chain);
328                     if (result != null) {
329                         // resolve the node
330                         // we could reuse the resolution, but perf savings isn't worth complexity
331                         repl = resolveFully(desire, context, null);
332                         break;
333                     }
334                 }
335             } else {
336                 logger.debug("{} is fixed, skipping", edge.getTail().getLabel());
337             }
338             if (repl == null) {
339                 // no trigger bindings, walk the node's children
340                 InjectionContext next = context.extend(edge.getTail()
341                                                            .getLabel()
342                                                            .getSatisfaction(),
343                                                        edge.getLabel()
344                                                            .getDesireChain()
345                                                            .getInitialDesire()
346                                                            .getInjectionPoint());
347                 walkGraphForReplacements(edge.getTail(), next, replacements);
348             } else {
349                 // trigger binding, add a replacement
350                 logger.info("replacing {} with {}",
351                             edge.getTail().getLabel(),
352                             repl.getLeft().getLabel());
353                 replacements.put(edge, DAGEdge.create(root, repl.getLeft(), repl.getRight()));
354             }
355         }
356     }
357 
358     /**
359      * Resolve a desire and its dependencies, inserting them into the graph.
360      *
361      * @param desire The desire to resolve.
362      * @param context The context of {@code parent}.
363      * @param deferQueue The queue of node deferrals.
364      * @throws ResolutionException if there is an error resolving the nodes.
365      */
366     private Pair<DAGNode<Component,Dependency>,Dependency>
367     resolveFully(Desire desire, InjectionContext context, Queue<Deferral> deferQueue) throws ResolutionException {
368         // check context depth against max to detect likely dependency cycles
369         if (context.size() > maxDepth) {
370             throw new CyclicDependencyException(desire, "Maximum context depth of " + maxDepth + " was reached");
371         }
372         
373         // resolve the current node
374         Resolution result = resolve(desire, context);
375 
376         InjectionContext newContext = context.extend(result.satisfaction, desire.getInjectionPoint());
377 
378         DAGNode<Component, Dependency> node;
379         if (result.deferDependencies) {
380             // extend node onto deferred queue and skip its dependencies for now
381             logger.debug("Deferring dependencies of {}", result.satisfaction);
382             node = DAGNode.singleton(result.makeSatisfaction());
383             // FIXME Deferred and skippable bindings do not interact well
384             deferQueue.add(new Deferral(node, newContext));
385             return Pair.of(node, result.makeDependency());
386         } else {
387             return resolveDepsAndMakeNode(deferQueue, result, newContext);
388         }
389     }
390 
391     private Pair<DAGNode<Component, Dependency>,Dependency> resolveDepsAndMakeNode(Queue<Deferral> deferQueue,
392                                                                                    Resolution result,
393                                                                                    InjectionContext newContext) throws ResolutionException {
394         DAGNode<Component, Dependency> node;// build up a node with its outgoing edges
395         DAGNodeBuilder<Component,Dependency> nodeBuilder = DAGNode.newBuilder();
396         nodeBuilder.setLabel(result.makeSatisfaction());
397         for (Desire d: result.satisfaction.getDependencies()) {
398             // complete the sub graph for the given desire
399             // - the call to resolveFully() is responsible for adding the dependency edges
400             //   so we don't need to process the returned node
401             logger.debug("Attempting to satisfy dependency {} of {}", d, result.satisfaction);
402             Pair<DAGNode<Component, Dependency>, Dependency> dep;
403             try {
404                 dep = resolveFully(d, newContext, deferQueue);
405             } catch (UnresolvableDependencyException ex) {
406                 if (!d.equals(ex.getDesireChain().getInitialDesire())) {
407                     // this is for some other (deeper) desire, fail
408                     throw ex;
409                 }
410                 // whoops, try to backtrack
411                 Resolution back = result.skippable ? result.backtrack() : null;
412                 if (back != null) {
413                     InjectionContext popped = newContext.getLeading();
414                     InjectionContext forked = InjectionContext.extend(popped, back.satisfaction,
415                                                                       back.desires.getInitialDesire().getInjectionPoint());
416                     return resolveDepsAndMakeNode(deferQueue, back, forked);
417                 } else if (result.backtracked || result.skippable) {
418                     // the result is the result of backtracking, or could be, so make an error at this dependency
419                     throw new UnresolvableDependencyException(result.desires, newContext.getLeading(), ex);
420                 } else {
421                     throw ex;
422                 }
423             }
424             nodeBuilder.addEdge(dep);
425         }
426         node = nodeBuilder.build();
427         return Pair.of(node, result.makeDependency());
428     }
429 
430     private Resolution resolve(Desire desire, InjectionContext context) throws ResolutionException {
431         DesireChain chain = DesireChain.singleton(desire);
432 
433         CachePolicy policy = CachePolicy.NO_PREFERENCE;
434         boolean fixed = false;
435         boolean skippable = false;
436 
437         while(true) {
438             logger.debug("Current desire: {}", chain.getCurrentDesire());
439             
440             BindingResult binding = null;
441             for (BindingFunction bf: functions) {
442                 binding = bf.bind(context, chain);
443                 if (binding != null && !chain.getPreviousDesires().contains(binding.getDesire())) {
444                     // found a binding that hasn't been used before
445                     break;
446                 }
447             }
448             
449             boolean defer = false;
450             boolean terminate = true; // so we stop if there is no binding
451             if (binding != null) {
452                 // update the desire chain
453                 chain = chain.extend(binding.getDesire());
454 
455                 terminate = binding.terminates(); // binding decides if we stop
456                 defer = binding.isDeferred();
457                 fixed |= binding.isFixed();
458                 skippable = binding.isSkippable();
459                 
460                 // upgrade policy if needed
461                 if (binding.getCachePolicy().compareTo(policy) > 0) {
462                     policy = binding.getCachePolicy();
463                 }
464             }
465             
466             if (terminate && chain.getCurrentDesire().isInstantiable()) {
467                 logger.info("Satisfied {} with {}", desire, chain.getCurrentDesire().getSatisfaction());
468                 
469                 // update cache policy if a specific policy hasn't yet been selected
470                 if (policy.equals(CachePolicy.NO_PREFERENCE)) {
471                     policy = chain.getCurrentDesire().getSatisfaction().getDefaultCachePolicy();
472                     if (policy.equals(CachePolicy.NO_PREFERENCE)) {
473                         policy = defaultPolicy;
474                     }
475                 }
476                 
477                 return new Resolution(chain.getCurrentDesire().getSatisfaction(), policy, chain, fixed, defer, skippable, false);
478             } else if (binding == null) {
479                 // no more desires to process, it cannot be satisfied
480                 throw new UnresolvableDependencyException(chain, context);
481             }
482         }
483     }
484     
485     /*
486      * Result tuple for resolve(Desire, InjectionContext)
487      */
488     private static class Resolution {
489         private final Satisfaction satisfaction;
490         private final CachePolicy policy;
491         private final DesireChain desires;
492         private final boolean fixed;
493         private final boolean deferDependencies;
494         private final boolean skippable;
495         private final boolean backtracked;
496 
497         public Resolution(Satisfaction satisfaction, CachePolicy policy, 
498                           DesireChain desires, boolean fixed,
499                           boolean deferDependencies,
500                           boolean skippable,
501                           boolean backtracked) {
502             this.satisfaction = satisfaction;
503             this.policy = policy;
504             this.desires = desires;
505             this.fixed = fixed;
506             this.deferDependencies = deferDependencies;
507             this.skippable = skippable;
508             this.backtracked = backtracked;
509         }
510 
511         public Component makeSatisfaction() {
512             return Component.create(satisfaction, policy);
513         }
514 
515         public Dependency makeDependency() {
516             EnumSet<Dependency.Flag> flags = Dependency.Flag.emptySet();
517             if (fixed) {
518                 flags.add(Dependency.Flag.FIXED);
519             }
520             return Dependency.create(desires, flags);
521         }
522 
523         /**
524          * Backtrack this resolution to skip a binding.
525          *
526          * @return The backtracked resolution, or {@code null} if the resolution cannot backtrack because doing so
527          * would result in a non-instantiable resolution.
528          * @throws IllegalArgumentException if attempting to backtrack would be invalid, because the resolution is
529          *                                  not skippable.
530          */
531         public Resolution backtrack() {
532             if (!skippable) {
533                 throw new IllegalArgumentException("unskippable resolution can't backtrack");
534             }
535             if (desires.size() <= 1) {
536                 throw new IllegalArgumentException("singleton desire chain can't backtrack");
537             }
538             DesireChain shrunk = desires.getPreviousDesireChain();
539             if (shrunk.getCurrentDesire().isInstantiable()) {
540                 return new Resolution(shrunk.getCurrentDesire().getSatisfaction(),
541                                       policy, // FIXME Backtrack the policy
542                                       shrunk,
543                                       fixed,  // FIXME If we allow skippability on non-default bindings, this is wrong
544                                       deferDependencies, // FIXME same here
545                                       false, true);
546             } else {
547                 return null;
548             }
549         }
550 
551         @Override
552         public String toString() {
553             return "(" + satisfaction + ", " + policy + ")";
554         }
555     }
556     
557     /*
558      * Deferred results tuple
559      */
560     private static class Deferral {
561         private final DAGNode<Component, Dependency> node;
562         private final InjectionContext context;
563 
564         public Deferral(DAGNode<Component, Dependency> node,
565                         InjectionContext context) {
566             this.node = node;
567             this.context = context;
568         }
569     }
570 }