/*
* Copyright 2011 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.gwt.inject.rebind.resolution;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.inject.rebind.ErrorManager;
import com.google.gwt.inject.rebind.GinjectorBindings;
import com.google.gwt.inject.rebind.binding.Binding;
import com.google.gwt.inject.rebind.binding.Dependency;
import com.google.gwt.inject.rebind.binding.ParentBinding;
import com.google.gwt.inject.rebind.resolution.DependencyExplorer.DependencyExplorerOutput;
import com.google.gwt.inject.rebind.util.Preconditions;
import com.google.gwt.inject.rebind.util.PrettyPrinter;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Finds and reports errors in the dependency information. Removes all optional bindings that can't
* be constructed.
*
* <p>A key is required if there exists a path from a key in the unresolved set of the origin
* Ginjector to the key in question that passes only through required dependencies. A key is
* optional if every path that leads to the key contains an optional dependency.
*
* This detects the following:
* <ul>
* <li>Circular dependencies that do not pass through a Provider or an AsyncProvider. This is a
* fatal error, even for optional keys.
* </li>
* <li>Bindings that cannot be created without causing a double-binding. Specifically, bindings for
* which the key is already bound below, but not exposed to, the Ginjector that needs the key. This
* is only an error for required keys; optional keys are removed.
* </li>
* <li>Bindings that cannot have implicit dependencies created for them. This is only an error if
* the key is required; optional keys are removed.
* </li>
* </ul>
*
* <p>Optional bindings with errors must be removed from the dependency graph before proceeding.
* This is to prevent them from incorrectly constraining the positions for keys that depend on them.
*
* <p>See {@link BindingResolver} for how this fits into the overall algorithm for resolution.
*/
public class UnresolvedBindingValidator {
private final ErrorManager errorManager;
private final EagerCycleFinder cycleFinder;
private final TreeLogger logger;
@Inject
public UnresolvedBindingValidator(EagerCycleFinder cycleFinder, ErrorManager errorManager,
@Assisted TreeLogger logger) {
this.cycleFinder = cycleFinder;
this.errorManager = errorManager;
this.logger = logger;
}
/**
* Returns an {@link InvalidKeys} object containing information about all the errors that we
* discovered in required keys, and the set of all optional bindings that should be removed from
* the graph in order to make it valid.
*/
public InvalidKeys getInvalidKeys(DependencyExplorerOutput output) {
Map<Key<?>, String> allInvalidKeys = getAllInvalidKeys(output);
Set<Key<?>> optionalInvalidKeys = removeOptionalKeys(output, allInvalidKeys);
optionalInvalidKeys = getKeysToRemove(output.getGraph(), optionalInvalidKeys);
return new InvalidKeys(allInvalidKeys, optionalInvalidKeys);
}
/**
* Returns true if the graph is valid (does not have any cycles or problems creating required
* keys). If there are any errors, they will be reported to the global {@link ErrorManager}.
*/
public boolean validate(DependencyExplorerOutput output, InvalidKeys invalidKeys) {
Collection<Map.Entry<Key<?>, String>> invalidRequiredKeys =
invalidKeys.getInvalidRequiredKeys();
for (Map.Entry<Key<?>, String> error : invalidRequiredKeys) {
reportError(output, error.getKey(), error.getValue());
}
return !cycleFinder.findAndReportCycles(output.getGraph()) && invalidRequiredKeys.isEmpty();
}
/**
* Prune all of the invalid optional keys from the graph. After this method, all of the keys
* remaining in the graph are resolvable.
*/
public void pruneInvalidOptional(DependencyExplorerOutput output, InvalidKeys invalidKeys) {
DependencyGraph.GraphPruner prunedGraph = new DependencyGraph.GraphPruner(output.getGraph());
for (Key<?> key : invalidKeys.getInvalidOptionalKeys()) {
prunedGraph.remove(key);
output.removeBinding(key);
}
output.setGraph(prunedGraph.update());
}
/**
* Returns a map from keys that are invalid to errors explaining why each key is invalid.
*/
private Map<Key<?>, String> getAllInvalidKeys(DependencyExplorerOutput output) {
Map<Key<?>, String> invalidKeys = new LinkedHashMap<Key<?>, String>();
// Look for errors in the nodes, and either report the error (if its required) or remove the
// node (if its optional).
for (Entry<Key<?>, String> error : output.getBindingErrors()) {
invalidKeys.put(error.getKey(), "Unable to create or inherit binding: " + error.getValue());
}
GinjectorBindings origin = output.getGraph().getOrigin();
for (Key<?> key : output.getImplicitlyBoundKeys()) {
if (origin.isBoundLocallyInChild(key)) {
GinjectorBindings child = origin.getChildWhichBindsLocally(key);
Binding childBinding = child.getBinding(key);
PrettyPrinter.log(logger, TreeLogger.DEBUG,
"Marking the key %s as bound in the ginjector %s (implicitly), and in the child"
+ " %s (%s)", key, origin, child, childBinding.getContext());
// TODO(schmitt): Determine path to binding in child ginjector (requires
// different DependencyExplorerOutput).
invalidKeys.put(key,
PrettyPrinter.format("Already bound in child Ginjector %s. Consider exposing it?",
child));
}
}
return invalidKeys;
}
/**
* Remove optional keys from the {@code invalidKeys} map, and return the the set of optional keys
* that had problems.
*/
private Set<Key<?>> removeOptionalKeys(
DependencyExplorerOutput output, Map<Key<?>, String> invalidKeys) {
RequiredKeySet requiredKeys = new RequiredKeySet(output.getGraph());
Set<Key<?>> optionalKeys = new LinkedHashSet<Key<?>>();
// We need to use a for loop instead of a foreach loop because we remove
// entries as we go (see the call to iterator.remove() below).
for (Iterator<Map.Entry<Key<?>, String>> iterator = invalidKeys.entrySet().iterator();
iterator.hasNext();) {
Map.Entry<Key<?>, String> entry = iterator.next();
Key<?> key = entry.getKey();
if (!requiredKeys.isRequired(key)) {
PrettyPrinter.log(logger, TreeLogger.DEBUG,
"Removing the optional key %s because it had errors: %s", key, entry.getValue());
optionalKeys.add(key);
iterator.remove();
}
}
return optionalKeys;
}
/**
* Given the set of optional keys that had problems, compute the set of all optional keys that
* should be removed. This may include additional keys, for instance if an optional key requires
* an invalid key, than the optional key should also be removed.
*
* <p>This will only add optional keys to {@code toRemove}. Recall that a key is required iff
* there exists a path to it that passes through only required edges. If key Y is optional, and
* there is a required edge from X -> Y, then X must also be optional. If X were required, by
* our definition Y would also be required.
*/
private Set<Key<?>> getKeysToRemove(DependencyGraph graph, Collection<Key<?>> discovered) {
Set<Key<?>> toRemove = new LinkedHashSet<Key<?>>();
while (!discovered.isEmpty()) {
toRemove.addAll(discovered);
discovered = getRequiredSourcesTargeting(graph, discovered);
discovered.removeAll(toRemove);
}
return toRemove;
}
/**
* Returns all of the source keys that have a required dependency on any key in the target set.
*/
private Collection<Key<?>> getRequiredSourcesTargeting(
DependencyGraph graph, Iterable<Key<?>> targets) {
Collection<Key<?>> requiredSources = new LinkedHashSet<Key<?>>();
for (Key<?> target : targets) {
for (Dependency edge : graph.getDependenciesTargeting(target)) {
if (!edge.isOptional()) {
PrettyPrinter.log(logger, TreeLogger.DEBUG, "Removing the key %s because of %s",
edge.getSource(), edge);
requiredSources.add(edge.getSource());
}
}
}
return requiredSources;
}
private void reportError(DependencyExplorerOutput output, Key<?> key, String error) {
// TODO(dburrows, bchambers): consider better approaches to pretty-printing keys.
List<Dependency> path = new PathFinder()
.onGraph(output.getGraph())
.withOnlyRequiredEdges(true)
.addRoots(Dependency.GINJECTOR)
.addDestinations(key)
.findShortestPath();
errorManager.logError("Error injecting %s: %s%n Path to required node:%n%s",
key, error, path);
}
/**
* Container for information about invalid keys.
*/
public static class InvalidKeys {
private final Map<Key<?>, String> invalidRequiredKeys;
private final Collection<Key<?>> invalidOptionalKeys;
private InvalidKeys(Map<Key<?>, String> invalidRequiredKeys, Set<Key<?>> invalidOptionalKeys) {
this.invalidRequiredKeys = Collections.unmodifiableMap(invalidRequiredKeys);
this.invalidOptionalKeys = Collections.unmodifiableCollection(invalidOptionalKeys);
}
public Set<Entry<Key<?>, String>> getInvalidRequiredKeys() {
return invalidRequiredKeys.entrySet();
}
public Collection<Key<?>> getInvalidOptionalKeys() {
return invalidOptionalKeys;
}
}
public interface Factory {
UnresolvedBindingValidator create(TreeLogger logger);
}
}