Skip to content

Add runtime support for Set-components #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 10, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.components;

import com.google.firebase.inject.Provider;
import java.util.Set;

abstract class AbstractComponentContainer implements ComponentContainer {
@Override
Expand All @@ -25,4 +26,9 @@ public <T> T get(Class<T> anInterface) {
}
return provider.get();
}

@Override
public <T> Set<T> setOf(Class<T> anInterface) {
return setOfProvider(anInterface).get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.google.android.gms.common.annotation.KeepForSdk;
import com.google.firebase.inject.Provider;
import java.util.Set;

/** Provides a means to retrieve instances of requested classes/interfaces. */
@KeepForSdk
Expand All @@ -25,4 +26,10 @@ public interface ComponentContainer {

@KeepForSdk
<T> Provider<T> getProvider(Class<T> anInterface);

@KeepForSdk
<T> Set<T> setOf(Class<T> anInterface);

@KeepForSdk
<T> Provider<Set<T>> setOfProvider(Class<T> anInterface);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

/**
Expand All @@ -32,8 +34,10 @@
* Component}s via {@link #get(Class)} method.
*/
public class ComponentRuntime extends AbstractComponentContainer {
private final List<Component<?>> components;
private static final Provider<Set<Object>> EMPTY_PROVIDER = Collections::emptySet;
private final Map<Component<?>, Lazy<?>> components = new HashMap<>();
private final Map<Class<?>, Lazy<?>> lazyInstanceMap = new HashMap<>();
private final Map<Class<?>, Lazy<Set<?>>> lazySetMap = new HashMap<>();
private final EventBus eventBus;

/**
Expand All @@ -53,12 +57,50 @@ public ComponentRuntime(
}
Collections.addAll(componentsToAdd, additionalComponents);

components = Collections.unmodifiableList(ComponentSorter.sorted(componentsToAdd));
CycleDetector.detect(componentsToAdd);

for (Component<?> component : components) {
for (Component<?> component : componentsToAdd) {
register(component);
}
validateDependencies();

processSetComponents();
}

/** Populates lazySetMap to make set components available for consumption via set dependencies. */
private void processSetComponents() {
Map<Class<?>, Set<Lazy<?>>> setIndex = new HashMap<>();
for (Map.Entry<Component<?>, Lazy<?>> entry : components.entrySet()) {
Component<?> component = entry.getKey();

// only process set components.
if (component.isValue()) {
continue;
}

Lazy<?> lazy = entry.getValue();

for (Class<?> anInterface : component.getProvidedInterfaces()) {
if (!setIndex.containsKey(anInterface)) {
setIndex.put(anInterface, new HashSet<>());
}
setIndex.get(anInterface).add(lazy);
}
}

for (Map.Entry<Class<?>, Set<Lazy<?>>> entry : setIndex.entrySet()) {
Set<Lazy<?>> lazies = entry.getValue();
lazySetMap.put(
entry.getKey(),
new Lazy<>(
() -> {
Set<Object> set = new HashSet<>();
for (Lazy<?> lazy : lazies) {
set.add(lazy.get());
}
return Collections.unmodifiableSet(set);
}));
}
}

@Override
Expand All @@ -68,6 +110,16 @@ public <T> Provider<T> getProvider(Class<T> anInterface) {
return (Provider<T>) lazyInstanceMap.get(anInterface);
}

@Override
@SuppressWarnings("unchecked")
public <T> Provider<Set<T>> setOfProvider(Class<T> anInterface) {
Lazy<Set<?>> lazy = lazySetMap.get(anInterface);
if (lazy != null) {
return (Provider<Set<T>>) (Provider<?>) lazy;
}
return (Provider<Set<T>>) (Provider<?>) EMPTY_PROVIDER;
}

/**
* Initializes all eager components.
*
Expand All @@ -76,10 +128,12 @@ public <T> Provider<T> getProvider(Class<T> anInterface) {
* <p>Note: the method is idempotent.
*/
public void initializeEagerComponents(boolean isDefaultApp) {
for (Component<?> component : components) {
for (Map.Entry<Component<?>, Lazy<?>> entry : components.entrySet()) {
Component<?> component = entry.getKey();
Lazy<?> lazy = entry.getValue();

if (component.isAlwaysEager() || (component.isEagerInDefaultApp() && isDefaultApp)) {
// at least one interface is guarenteed to be provided by a component.
get(component.getProvidedInterfaces().iterator().next());
lazy.get();
}
}

Expand All @@ -88,15 +142,20 @@ public void initializeEagerComponents(boolean isDefaultApp) {

private <T> void register(Component<T> component) {
Lazy<T> lazy =
new Lazy<>(component.getFactory(), new RestrictedComponentContainer(component, this));
new Lazy<>(
() -> component.getFactory().create(new RestrictedComponentContainer(component, this)));

components.put(component, lazy);
if (!component.isValue()) {
return;
}
for (Class<? super T> anInterface : component.getProvidedInterfaces()) {
lazyInstanceMap.put(anInterface, lazy);
}
}

private void validateDependencies() {
for (Component<?> component : components) {
for (Component<?> component : components.keySet()) {
for (Dependency dependency : component.getDependencies()) {
if (dependency.isRequired() && !lazyInstanceMap.containsKey(dependency.getInterface())) {
throw new MissingDependencyException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,42 @@
package com.google.firebase.components;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Implementation of topological sort. */
class ComponentSorter {
/** Cycle detector for the {@link Component} dependency graph. */
class CycleDetector {
private static class Dep {
private final Class<?> anInterface;
private final boolean set;

private Dep(Class<?> anInterface, boolean set) {
this.anInterface = anInterface;
this.set = set;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Dep) {
Dep dep = (Dep) obj;
return dep.anInterface.equals(anInterface) && dep.set == set;
}
return false;
}

@Override
public int hashCode() {
int h = 1000003;
h ^= anInterface.hashCode();
h *= 1000003;
h ^= Boolean.valueOf(set).hashCode();
return h;
}
}

private static class ComponentNode {
private final Component<?> component;
private final Set<ComponentNode> dependencies = new HashSet<>();
Expand Down Expand Up @@ -63,22 +90,21 @@ boolean isLeaf() {
}

/**
* Given a list of components, returns a sorted permutation of it.
* Detect a dependency cycle among provided {@link Component}s.
*
* @param components Components to sort.
* @return Sorted list of components.
* @param components Components to detect cycle between.
* @throws IllegalArgumentException thrown if multiple components implement the same interface.
* @throws DependencyCycleException thrown if a dependency cycle between components is detected.
*/
static List<Component<?>> sorted(List<Component<?>> components) {
static void detect(List<Component<?>> components) {
Set<ComponentNode> graph = toGraph(components);
Set<ComponentNode> roots = getRoots(graph);

List<Component<?>> result = new ArrayList<>();
int numVisited = 0;
while (!roots.isEmpty()) {
ComponentNode node = roots.iterator().next();
roots.remove(node);
result.add(node.getComponent());
numVisited++;

for (ComponentNode dependent : node.getDependencies()) {
dependent.removeDependent(node);
Expand All @@ -88,11 +114,10 @@ static List<Component<?>> sorted(List<Component<?>> components) {
}
}

// If there is no dependency cycle in the graph, the size of the resulting component list will
// be equal to the original list, meaning that we were able to sort all components.
if (result.size() == components.size()) {
Collections.reverse(result);
return result;
// If there is no dependency cycle in the graph, the number of visited nodes will be equal to
// the original list.
if (numVisited == components.size()) {
return;
}

// Otherwise there is a cycle.
Expand All @@ -107,34 +132,49 @@ static List<Component<?>> sorted(List<Component<?>> components) {
}

private static Set<ComponentNode> toGraph(List<Component<?>> components) {
Map<Class<?>, ComponentNode> componentIndex = new HashMap<>(components.size());
Map<Dep, Set<ComponentNode>> componentIndex = new HashMap<>(components.size());
for (Component<?> component : components) {
ComponentNode node = new ComponentNode(component);
for (Class<?> anInterface : component.getProvidedInterfaces()) {
if (componentIndex.put(anInterface, node) != null) {
Dep cmp = new Dep(anInterface, !component.isValue());
if (!componentIndex.containsKey(cmp)) {
componentIndex.put(cmp, new HashSet<>());
}
Set<ComponentNode> nodes = componentIndex.get(cmp);
if (!nodes.isEmpty() && !cmp.set) {
throw new IllegalArgumentException(
String.format("Multiple components provide %s.", anInterface));
}
nodes.add(node);
}
}

for (ComponentNode component : componentIndex.values()) {
for (Dependency dependency : component.getComponent().getDependencies()) {
if (!dependency.isDirectInjection()) {
continue;
}

ComponentNode depComponent = componentIndex.get(dependency.getInterface());
// Missing dependencies are skipped for the purposes of the sort as there is no component to
// sort.
if (depComponent != null) {
component.addDependency(depComponent);
depComponent.addDependent(component);
for (Set<ComponentNode> componentNodes : componentIndex.values()) {
for (ComponentNode node : componentNodes) {
for (Dependency dependency : node.getComponent().getDependencies()) {
if (!dependency.isDirectInjection()) {
continue;
}

Set<ComponentNode> depComponents =
componentIndex.get(new Dep(dependency.getInterface(), dependency.isSet()));
if (depComponents == null) {
continue;
}
for (ComponentNode depComponent : depComponents) {
node.addDependency(depComponent);
depComponent.addDependent(node);
}
}
}
}

return new HashSet<>(componentIndex.values());
HashSet<ComponentNode> result = new HashSet<>();
for (Set<ComponentNode> componentNodes : componentIndex.values()) {
result.addAll(componentNodes);
}

return result;
}

private static Set<ComponentNode> getRoots(Set<ComponentNode> components) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ class Lazy<T> implements Provider<T> {
this.instance = instance;
}

/** Creates a lazy backed by a {@link ComponentFactory} and {@link ComponentContainer}. */
Lazy(ComponentFactory<T> factory, ComponentContainer container) {
provider = () -> factory.create(container);
Lazy(Provider<T> provider) {
this.provider = provider;
}

/** Returns the initialized value. */
Expand All @@ -59,7 +58,10 @@ public T get() {
}
}
}
return (T) result;

@SuppressWarnings("unchecked")
T tResult = (T) result;
return tResult;
}

@VisibleForTesting
Expand Down
Loading