/** * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.inject.util; import com.google.inject.Binding; import com.google.inject.ConfigurationException; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Scope; import com.google.inject.Scopes; import com.google.inject.internal.ImmutableList; import com.google.inject.internal.ImmutableMap; import com.google.inject.internal.Lists; import com.google.inject.internal.Maps; import static com.google.inject.internal.Preconditions.checkArgument; import com.google.inject.spi.BindingScopingVisitor; import com.google.inject.spi.Dependency; import com.google.inject.spi.HasDependencies; import com.google.inject.spi.Message; import com.google.inject.spi.ProviderBinding; import java.lang.annotation.Annotation; import static java.util.Arrays.asList; import java.util.List; import java.util.Map; /** * Inspects an injector for scoping violations. Scoping violations exist whenever a long-lived * object (such as a singleton) depends on a short-lived object (such as a request-scoped object). * To use, create an scope checker and call it's {@code check()} method with your scoping * annotations in decreasing duration: *

 *     ScopeChecker scopeChecker = new ScopeChecker(injector);
 *     scopeChecker.check(Singleton.class, SessionScoped.class, RequestScoped.class);
 * 
* If there are scoping violations in the injector, the call will fail with a detailed {@code * ConfigurationException}. * * @author jessewilson@google.com (Jesse Wilson) */ public class ScopeChecker { private final Injector injector; public ScopeChecker(Injector injector) { this.injector = injector; } /** * Checks this checker's injector for scoping violations. * * @param longest the outermost scope, such as {@code Singleton.class}. * @param nested a scope immediately nested within {@code longest} * @param furtherNested any scopes nested within {@code nested}, in decreasing duration. * @throws ConfigurationException if any violations are found. */ public void check(Class longest, Class nested, Class... furtherNested) { Ranker ranker = new Ranker(longest, nested, furtherNested); Map, Node> nodes = Maps.newHashMap(); // build the graph of node dependencies with scope ranks for (Binding binding : injector.getAllBindings().values()) { Key key = binding.getKey(); Node node = getNode(nodes, key); ranker.rank(binding, node); // explicitly ignore dependencies that come via providers. if (binding instanceof ProviderBinding) { continue; } if (binding instanceof HasDependencies) { HasDependencies hasDependencies = (HasDependencies) binding; for (Dependency dependency : hasDependencies.getDependencies()) { getNode(nodes, dependency.getKey()).addUser(node); } } } // walk through the nodes, pushing effective scopes through dependencies for (Node node : nodes.values()) { node.pushScopeToUsers(); } // on the nodes with dependencies narrower than themselves, print an error List messages = Lists.newArrayList(); for (Node node : nodes.values()) { if (node.isScopedCorrectly()) { continue; } StringBuilder error = new StringBuilder("Illegal scoped dependency: ").append(node); Node dependency = node; do { dependency = dependency.effectiveScopeDependency(); error.append("\n depends on ").append(dependency); } while (!dependency.isEffectiveScopeAppliedScope()); messages.add(new Message(error.toString())); } if (!messages.isEmpty()) { throw new ConfigurationException(messages); } } private Node getNode(Map, Node> nodes, Key key) { Node node = nodes.get(key); if (node == null) { node = new Node(key); nodes.put(key, node); } return node; } /** * Applies the scoping rank to a node. Scopes are stored as integers, and narrower scopes get * greater values. */ private class Ranker implements BindingScopingVisitor { private final ImmutableList> scopeAnnotations; private final ImmutableMap scopeToRank; private Ranker(Class longest, Class nested, Class... furtherNested) { scopeAnnotations = new ImmutableList.Builder>() .add(longest) .add(nested) .addAll(asList(furtherNested)) .build(); ImmutableMap.Builder scopeToRankBuilder = ImmutableMap.builder(); Map, Scope> annotationToScope = injector.getScopeBindings(); int i = 0; for (Class scopeAnnotation : scopeAnnotations) { Scope scope = annotationToScope.get(scopeAnnotation); checkArgument(scope != null, "No scope binding for %s", scopeAnnotation); scopeToRankBuilder.put(scope, i++); } scopeToRank = scopeToRankBuilder.build(); } public void rank(Binding binding, Node node) { Scope scope = binding.acceptScopingVisitor(this); Integer rank = scopeToRank.get(scope); if (rank != null) { node.setScopeRank(rank, scopeAnnotations.get(rank)); } } public Scope visitEagerSingleton() { return Scopes.SINGLETON; } public com.google.inject.Scope visitScope(com.google.inject.Scope scope) { return scope; } public Scope visitScopeAnnotation(Class scopeAnnotation) { throw new AssertionError(); } public Scope visitNoScoping() { return Scopes.NO_SCOPE; } } }