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.collect.ImmutableListMultimap;
23  import com.google.common.collect.ListMultimap;
24  import com.google.common.collect.Multimap;
25  import org.apache.commons.lang3.tuple.Pair;
26  import org.grouplens.grapht.ResolutionException;
27  import org.grouplens.grapht.context.ContextMatch;
28  import org.grouplens.grapht.context.ContextMatcher;
29  import org.grouplens.grapht.reflect.QualifierMatcher;
30  import org.grouplens.grapht.util.Preconditions;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import java.util.*;
35  
36  /**
37   * <p>
38   * BindingFunction that uses BindRules created by the fluent API to bind desires
39   * to other desires or satisfactions.
40   * <p>
41   * For more details on context management, see {@link org.grouplens.grapht.context.ContextPattern},
42   * {@link org.grouplens.grapht.context.ContextElementMatcher}, and {@link QualifierMatcher}. This function uses the
43   * context to activate and select BindRules. A number of rules are used to order
44   * applicable BindRules and choose the best. When any of these rules rely on the
45   * current dependency context, the deepest node in the context has the most
46   * influence. Put another way, if contexts were strings, they could be ordered
47   * lexicographically from the right to the left.
48   * <p>
49   * When selecting BindRules to apply to a Desire, BindRules are ordered first by
50   * {@linkplain ContextMatch context match}, then by the ordering defined by the bind rule itself.
51   * <p>
52   * A summary of these rules is that the best specified BindRule is applied,
53   * where the context that the BindRule is activated in has more priority than
54   * the type of the BindRule. If multiple rules tie for best, then the solver
55   * fails with a checked exception.
56   * 
57   * @author <a href="http://grouplens.org">GroupLens Research</a>
58   */
59  public class RuleBasedBindingFunction implements BindingFunction {
60      private static final Map<Object,Set<BindRule>> bindRuleMemory
61              = new WeakHashMap<Object, Set<BindRule>>();
62  
63      private static final Logger logger = LoggerFactory.getLogger(RuleBasedBindingFunction.class);
64      
65      private final ImmutableListMultimap<ContextMatcher, BindRule> rules;
66      
67      public RuleBasedBindingFunction(Multimap<ContextMatcher, BindRule> rules) {
68          Preconditions.notNull("rules", rules);
69          
70          this.rules = ImmutableListMultimap.copyOf(rules);
71      }
72      
73      /**
74       * Get the rules underlying this binding function.
75       * @return The rules used by this BindingFunction
76       */
77      public ListMultimap<ContextMatcher, BindRule> getRules() {
78          return rules;
79      }
80      
81      @Override
82      public BindingResult bind(InjectionContext context, DesireChain desire) throws ResolutionException {
83          // FIXME Build a better way to remember the applied rules
84          Set<BindRule> appliedRules;
85          synchronized (bindRuleMemory) {
86              appliedRules = bindRuleMemory.get(desire.getKey());
87              if (appliedRules == null) {
88                  appliedRules = new HashSet<BindRule>();
89                  bindRuleMemory.put(desire.getKey(), appliedRules);
90              }
91          }
92  
93          // collect all bind rules that apply to this desire
94          List<Pair<ContextMatch, BindRule>> validRules = new ArrayList<Pair<ContextMatch, BindRule>>();
95          for (ContextMatcher matcher: rules.keySet()) {
96              ContextMatch match = matcher.matches(context);
97              if (match != null) {
98                  // the context applies to the current context, so go through all
99                  // bind rules within it and record those that match the desire
100                 for (BindRule br: rules.get(matcher)) {
101                     if (br.matches(desire.getCurrentDesire()) && !appliedRules.contains(br)) {
102                         validRules.add(Pair.of(match, br));
103                         logger.trace("Matching rule, context: {}, rule: {}", matcher, br);
104                     }
105                 }
106             }
107         }
108         
109         if (!validRules.isEmpty()) {
110             // we have a bind rule to apply
111             // pair's ordering is suitable for sorting the bind rules
112             Collections.sort(validRules);
113 
114             if (validRules.size() > 1) {
115                 // must check if other rules are equal to the first
116                 // we find the whole list of dupes for error reporting purposes
117                 List<BindRule> topRules = new ArrayList<BindRule>();
118                 topRules.add(validRules.get(0).getRight());
119                 for (int i = 1; i < validRules.size(); i++) {
120                     if (validRules.get(0).compareTo(validRules.get(i)) == 0) {
121                         topRules.add(validRules.get(i).getRight());
122                     }
123                 }
124                 
125                 if (topRules.size() > 1) {
126                     logger.error("{} bindings for {} in {}", topRules.size(),
127                                  desire, context);
128                     for (BindRule rule: topRules) {
129                         logger.info("matching rule: {}", rule);
130                     }
131                     // additional rules match just as well as the first, so fail
132                     throw new MultipleBindingsException(desire, context, topRules);
133                 }
134             }
135 
136             // apply the bind rule to get a new desire
137             BindRule selectedRule = validRules.get(0).getRight();
138             appliedRules.add(selectedRule);
139             
140             logger.debug("Applying rule: {} to desire: {}", selectedRule, desire);
141             return BindingResult.newBuilder()
142                                 .setDesire(selectedRule.apply(desire.getCurrentDesire()))
143                                 .setCachePolicy(selectedRule.getCachePolicy())
144                                 .setFlags(selectedRule.getFlags())
145                                 .build();
146         }
147         
148         // No rule to apply, so return null to delegate to the next binding function
149         return null;
150     }
151 }