blob: 86b44b5c77afe273fbe3eddbc18c825232c0e8c3 [file] [log] [blame]
/**
* 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:
* <pre><code>
* ScopeChecker scopeChecker = new ScopeChecker(injector);
* scopeChecker.check(Singleton.class, SessionScoped.class, RequestScoped.class);
* </code></pre>
* 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<? extends Annotation> longest, Class<? extends Annotation> nested,
Class<? extends Annotation>... furtherNested) {
Ranker ranker = new Ranker(longest, nested, furtherNested);
Map<Key<?>, 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<Message> 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<Key<?>, 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<Scope> {
private final ImmutableList<Class<? extends Annotation>> scopeAnnotations;
private final ImmutableMap<Scope, Integer> scopeToRank;
private Ranker(Class<? extends Annotation> longest, Class<? extends Annotation> nested,
Class<? extends Annotation>... furtherNested) {
scopeAnnotations = new ImmutableList.Builder<Class<? extends Annotation>>()
.add(longest)
.add(nested)
.addAll(asList(furtherNested))
.build();
ImmutableMap.Builder<Scope, Integer> scopeToRankBuilder = ImmutableMap.builder();
Map<Class<? extends Annotation>, Scope> annotationToScope = injector.getScopeBindings();
int i = 0;
for (Class<? extends Annotation> 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<? extends Annotation> scopeAnnotation) {
throw new AssertionError();
}
public Scope visitNoScoping() {
return Scopes.NO_SCOPE;
}
}
}