/* * Copyright 2016 Kejun Xia * * 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.shipdream.lib.poke; import com.shipdream.lib.poke.exception.PokeException; import com.shipdream.lib.poke.exception.ProvideException; import com.shipdream.lib.poke.exception.ProviderConflictException; import com.shipdream.lib.poke.exception.ProviderMissingException; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Qualifier; /** * <p> * A component is used to group {@link Provider}s. A {@link Provider} can be {@link #register(Provider)}ed * to a component to provide injection candidate. Component can also {@link #register(Object)} * multiple providers by methods provider annotated by {@link Provides} in the object. * </p> * * <p> * A component can have a cache to manage created injection candidates. If the cache is enabled, * the injection candidates are singleton in the scope of the component. When the cache is disabled, * the component will always create new instances from its managed providers. * </p> * * <p> * Components can be group in a tree structure by attaching a child component to a parent component. * In a component tree, injection candidates are unique by the combination of class type and qualifier. * When duplicate providers for the same class type and qualifier are found, a {@link ProviderConflictException} * will be thrown. * </p> * * <p> * To override a provider to the graph the component or its component tree is attaching to, use * {@link #attach(Component, boolean)} with true as the second parameter. Then the last attached * component will a conflicting class type and qualifier will be used until it's {@link #detach(Component)} * </p> */ public class Component { /** * Thrown when detach a component from a parent it doesn't belong to. */ public static class MismatchDetachException extends PokeException { public MismatchDetachException(String message) { super(message); } } /** * Thrown when assigning parent to an attached component already has a parent component. */ public static class MultiParentException extends PokeException { public MultiParentException(String message) { super(message); } } private final String name; protected ScopeCache scopeCache; final Map<String, Component> componentLocator = new HashMap<>(); Map<String, List<Component>> overriddenChain; protected final Map<String, Provider> providers = new HashMap<>(); private Component parentComponent; private List<Component> childrenComponents; /** * Construct an unnamed component with a cache. See {@link #Component(String, boolean)} */ public Component() { this(null, true); } /** * Construct a component with instance cache and the given name. See {@link #Component(String, boolean)} * @param name The name of the component. */ public Component(String name) { this(name, true); } /** * Construct a component without a name. * @param enableCache indicates whether this component cache created instances. When the component * has a cache, all instances created by the providers managed by this component * will be singleton until the component is destroyed or replaced in the * component tree. Otherwise, the component always generates new instances * for injections. */ public Component(boolean enableCache) { this(null, enableCache); } /** * Construct a component. * @param name name of the component used to identify the component. * @param enableCache indicates whether this component cache created instances. When the component * has a cache, all instances created by the providers managed by this component * will be singleton until the component is destroyed or replaced in the * component tree. Otherwise, the component always generates new instances * for injections. */ public Component(String name, boolean enableCache) { if (enableCache) { scopeCache = new ScopeCache(); } else { scopeCache = null; } this.name = name; } /** * @return The name of the component */ public String getName() { return name; } public Map<String, Object> getCache() { if (scopeCache == null) { return null; } else { return scopeCache.instances; } } /** * @return The parent component */ public Component getParent() { return parentComponent; } /** * @return The list of the children components */ public List<Component> getChildrenComponents() { return childrenComponents; } /** * //TODO: document how component scope instances will override provider's * Register a {@link Provider}. When allowOverride = false, it allows to register overriding * binding against the same type and {@link Qualifier} and <b>last wins</b>, otherwise * {@link ProviderConflictException} will be thrown. * * @param provider The provider * @return this instance * @throws ProviderConflictException Thrown when duplicate registries detected against the same * type and qualifier. */ public Component register(@NotNull Provider provider) throws ProviderConflictException { addProvider(provider); return this; } /** * Unregister provider. If there is an overridden type registered already, only unregister the * overridden binding. The original one will be unregistered if the this method is called * again against to the type and qualifier associated with the provider. * * @param provider The provider that has the type and qualifier to unregister against * @return this instance * * @throws ProviderMissingException Thrown when the provider with the given type and qualifier * cannot be found under this component */ public Component unregister(Provider provider) throws ProviderMissingException { return unregister(provider.type(), provider.getQualifier()); } /** * Find the provider in this {@link Component} and its descents. If the provider is found, * detach it from its associated {@link Component}. After this point, the provider will use its * own scope instances. * @param type The type of the provider * @param qualifier The qualifier of the provider * @return this instance * * @throws ProviderMissingException Thrown when the provider with the given type and qualifier * cannot be found under this component */ public <T> Component unregister(Class<T> type, Annotation qualifier) throws ProviderMissingException { //Detach corresponding provider from it's component Provider<T> provider = findProvider(type, qualifier); provider.setComponent(null); String key = PokeHelper.makeProviderKey(type, qualifier); Component targetComponent = getRootComponent().componentLocator.get(key); targetComponent.providers.remove(key); if (targetComponent.scopeCache != null) { targetComponent.scopeCache.removeInstance(PokeHelper.makeProviderKey(type, qualifier)); } Component root = getRootComponent(); //Remove it from root component's locator root.componentLocator.remove(key); return this; } /** * <p> * Register component where methods annotated by {@link Provides} will be registered as * injection providers. When allowOverride = false, it allows to register overriding * binding against the same type and {@link Qualifier} and <b>last wins</b>, otherwise * {@link ProviderConflictException} will be thrown. * </p> * * <pre> * component.register(new Object(){ @ Provides @ EventBusV public EventBus createEventBusV() { return eventBusV; } }); * </pre> * * @param providerHolder The object with methods marked by {@link Provides} to provide injectable * instances * @return this instance * @throws ProvideException Thrown when exception occurs during providers creating instances * @throws ProviderConflictException Thrown when duplicate registries detected against the same * type and qualifier. */ public Component register(Object providerHolder) throws ProvideException, ProviderConflictException { Method[] methods = providerHolder.getClass().getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(Provides.class)) { registerProvides(providerHolder, method); } } return this; } /** * Unregister component where methods annotated by {@link Provides} will be registered as * injection providers. * * @param providerHolder The object with methods marked by {@link Provides} to provide injectable * instances * @return this instance * * @throws ProviderMissingException Thrown when the any provider in the provider holder with * the given type and qualifier cannot be found under this component */ public Component unregister(Object providerHolder) throws ProviderMissingException { Method[] methods = providerHolder.getClass().getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(Provides.class)) { Class<?> returnType = method.getReturnType(); if (returnType != void.class) { Annotation qualifier = null; Annotation[] annotations = method.getAnnotations(); for (Annotation a : annotations) { if (a.annotationType().isAnnotationPresent(Qualifier.class)) { qualifier = a; break; } } unregister(returnType, qualifier); } } } return this; } /** * Attach a component to this component. The root of the component tree this component belongs * to will be able to find all providers registered to the child component. * @param childComponent The component to be added as a child component * @throws MultiParentException Thrown when the child component has had a parent already. * @throws ProviderConflictException Thrown when the child component has provider has been * registered to component tree this component belongs to. */ public void attach(@NotNull Component childComponent) throws MultiParentException, ProviderConflictException { attach(childComponent, false); } /** * Attach a component to this component. The root of the component tree this component belongs * to will be able to find all providers registered to the child component. * @param childComponent The component to be added as a child component * @throws MultiParentException Thrown when the child component has had a parent already. * @throws ProviderConflictException Thrown when the child component has provider has been * registered to component tree this component belongs to. */ public void attach(@NotNull Component childComponent, boolean allowOverride) throws MultiParentException, ProviderConflictException { if (childComponent.parentComponent != null) { String msg = String.format("The attaching component(%s) has a parent already. Remove its parent before attaching it", childComponent.name == null ? "unnamed" : childComponent.name); throw new MultiParentException(msg); } //Merge the child component locator to the root component Component root = getRootComponent(); if (childComponent.parentComponent == null) { //Child component was a root component } Set<String> addedKeys = new HashSet<>(); Iterator<Map.Entry<String, Component>> iterator = childComponent.componentLocator.entrySet().iterator(); while(iterator.hasNext()) { Map.Entry<String, Component> entry = iterator.next(); String key = entry.getKey(); //check conflict if override is not allowed if (root.componentLocator.containsKey(key)) { if (!allowOverride) { for (String k : addedKeys) { root.componentLocator.remove(k); } throw new ProviderConflictException( String.format("Type(%s) in the adding child component(%s) has been added " + "to rootComponent(%s) or its attached child components.", key, childComponent.getComponentId(), root.getComponentId())); } else { if (root.overriddenChain == null) { root.overriddenChain = new HashMap<>(); } List<Component> chain = root.overriddenChain.get(key); if (chain == null) { chain = new ArrayList<>(); root.overriddenChain.put(key, chain); } chain.add(childComponent); } } root.componentLocator.put(key, entry.getValue()); iterator.remove(); addedKeys.add(key); } //Update tree nodes childComponent.parentComponent = this; if (childrenComponents == null) { childrenComponents = new ArrayList<>(); } childrenComponents.add(childComponent); } private String getComponentId() { if (name != null) { return name; } else { if (providers.size() == 0) { return "Unnamed empty Component"; } StringBuilder sb = new StringBuilder(); sb.append("Unnamed Component containing type(s):"); int max = 5; int count = 0; for (Provider provider : providers.values()) { if (count > 0) { sb.append(", "); } sb.append(provider.type().getSimpleName()); count ++; if (count > max) { break; } } return sb.toString(); } } /** * Detach the child component from this component. Once a component is detached the component * tree won't use this component to locate suitable injection candidates and all its cached * instances will be removed * @param childComponent The child component to detach * @throws MismatchDetachException thrown when the child component was not attached to this component */ public void detach(@NotNull Component childComponent) throws MismatchDetachException { if (childComponent.parentComponent != this) { String msg = String.format("The child component(%s) doesn't belong to component(%s)", childComponent.name == null ? "unnamed" : childComponent.getComponentId(), getComponentId()); throw new MismatchDetachException(msg); } //Update tree nodes childComponent.parentComponent = null; if (childrenComponents != null) { childrenComponents.remove(childComponent); } //Disband the child component locator from the root component and return the keys to child //component itself Component root = getRootComponent(); for (String key : childComponent.providers.keySet()) { //Find all keys in itself childComponent.componentLocator.put(key, childComponent); //TODO: need to restructure child components overriding priority, say child //has c1, c2 overriding a key before detaching only root is aware of them. After detaching //childComponent should perform as a root component so it needs to know how to manage them List<Component> chain = null; if (root.overriddenChain != null) { chain = root.overriddenChain.get(key); } if (chain != null) { if (!chain.isEmpty()) { Component removingItem = null; int size = chain.size(); for (int i = size - 1; i >= 0 ; i--) { Component c = chain.get(i); if (c == childComponent) { //Delete the first found removingItem = c; break; } } chain.remove(removingItem); } //notify root component that child component is surrendering managing the key if (chain.isEmpty()) { //No overridden any more if (root.providers.containsKey(key)) { //Root is managing the key itself root.componentLocator.put(key, root); } else { //Nobody is managing the key root.componentLocator.remove(key); } } else { //Second last overriding component takes over the management of the key root.componentLocator.put(key, chain.get(chain.size() - 1)); } } else { root.componentLocator.remove(key); } } } private Component getRootComponent() { Component root = this; while (root.parentComponent != null) { root = root.parentComponent; } return root; } /** * Find the provider specified by the type and qualifier. It will look through the providers * registered to this component and all its children components'. * @param type The type the provider is associated with * @param qualifier The qualifier the provider is associated with * @return The provider * @throws ProviderMissingException Thrown when the provider can't be found */ protected <T> Provider<T> findProvider(Class<T> type, Annotation qualifier) throws ProviderMissingException { String key = PokeHelper.makeProviderKey(type, qualifier); Component targetComponent = getRootComponent().componentLocator.get(key); Provider provider = null; if (targetComponent != null) { provider = targetComponent.providers.get(key); } if (provider == null) { String msg = String.format("Provider(%s) cannot be found", key); throw new ProviderMissingException(msg); } else { return provider; } } /** * Add provider to this component and notify the root component this component is * managing the provider. * @param provider The adding provider * @throws ProviderConflictException Thrown when a provider bound to same type and qualifier is * added in the component tree that this component is in. */ private <T> void addProvider(@NotNull Provider<T> provider) throws ProviderConflictException { Class<T> type = provider.type(); Annotation qualifier = provider.getQualifier(); String key = PokeHelper.makeProviderKey(type, qualifier); addNewKeyToComponent(key, this); provider.setComponent(this); providers.put(key, provider); } /** * Add key to the component locator of the component. This component and the the component tree * root's component locator will both be updated. * @param key The key to add * @param component The component whose providers directly contain the key * @throws ProviderConflictException The key has been added to the component or the component tree */ private void addNewKeyToComponent(String key, Component component) throws ProviderConflictException { Component root = getRootComponent(); if (componentLocator.keySet().contains(key)) { String msg = String.format("Type %s has already been registered " + "in this component(%s).", key, getComponentId()); throw new ProviderConflictException(msg); } if (root != this && root.componentLocator.keySet().contains(key)) { String msg = String.format("\nClass type %s cannot be registered to component(%s)\nsince it's " + "already been registered in its root component(%s).\n\nYou can prepare a child " + "component and register providers to it first. Then attach the child component\nto the " + "component tree with allowOverridden flag set true", key, getComponentId(), getRootComponent().getComponentId()); throw new ProviderConflictException(msg); } //Only put it to root component's locator root.componentLocator.put(key, component); } private void registerProvides(final Object providerHolder, final Method method) throws ProvideException, ProviderConflictException { Class<?> returnType = method.getReturnType(); if (returnType == void.class) { throw new ProvideException(String.format("Provides method %s must not return void.", method.getName())); } else { Annotation[] annotations = method.getAnnotations(); Annotation qualifier = null; for (Annotation a : annotations) { Class<? extends Annotation> annotationType = a.annotationType(); if (annotationType.isAnnotationPresent(Qualifier.class)) { if (qualifier != null) { throw new ProvideException("Only one Qualifier is supported for Provide method. " + String.format("Found multiple qualifier %s and %s for method %s", qualifier.getClass().getName(), a.getClass().getName(), method.getName())); } qualifier = a; } } Provider provider = new MethodProvider(returnType, qualifier, scopeCache, providerHolder, method); register(provider); } } /** * Method provider to extract providers from object's methods annotated by inject annotation */ static class MethodProvider extends Provider { private final Object providerHolder; private final Method method; @SuppressWarnings("unchecked") MethodProvider(Class type, Annotation qualifier, ScopeCache scopeCache, Object providerHolder, Method method) { super(type, qualifier, scopeCache); this.providerHolder = providerHolder; this.method = method; } @Override protected Object createInstance() throws ProvideException { try { boolean accessible = method.isAccessible(); if (!accessible) { method.setAccessible(true); } Object obj = method.invoke(providerHolder); method.setAccessible(accessible); return obj; } catch (IllegalAccessException e) { throw new ProvideException(String.format("Provides method %s must " + "be accessible.", method.getName()), e); // $COVERAGE-IGNORE$ } catch (InvocationTargetException e) { throw new ProvideException(String.format("Provides method %s is not able " + "to be invoked against %s.", method.getName(), providerHolder.getClass().getName()), e); // $COVERAGE-IGNORE$ } } } }