/*
* 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.CircularDependenciesException;
import com.shipdream.lib.poke.exception.PokeException;
import com.shipdream.lib.poke.exception.ProvideException;
import com.shipdream.lib.poke.exception.ProviderMissingException;
import com.shipdream.lib.poke.util.ReflectUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Inject;
/**
* A graph manages how to inject dependencies to target objects.
*/
public class Graph {
public static class IllegalRootComponentException extends PokeException {
public IllegalRootComponentException(String message) {
super(message);
}
}
private List<Monitor> monitors;
private String revisitedNode = null;
private Set<String> visitedInjectNodes = new LinkedHashSet<>();
private Map<Object, Map<String, Set<String>>> visitedFields = new HashMap<>();
private List<Provider.DereferenceListener> dereferenceListeners;
private List<Provider.DisposeListener> disposeListeners;
private Component rootComponent;
/**
* Register {@link Monitor} which will be called the graph is about to inject or release an object
*
* @param monitor The monitor
*/
public void registerMonitor(Monitor monitor) {
if (monitors == null) {
monitors = new CopyOnWriteArrayList<>();
}
monitors.add(monitor);
}
/**
* Register {@link Monitor} which will be called the graph is about to inject or release an object
*
* @param monitor The monitor
*/
public void unregisterMonitor(Monitor monitor) {
if (monitors != null) {
monitors.remove(monitor);
if (monitors.isEmpty()) {
monitors = null;
}
}
}
/**
* Clear {@link Monitor} which will be called the graph is about to inject or release an object
*/
public void clearMonitors() {
if (monitors != null) {
monitors.clear();
monitors = null;
}
}
/**
* Add {@link Component} to the graph.
*
* @param component The root {@link Component} of this graph.
*/
public void setRootComponent(Component component) throws IllegalRootComponentException {
if (component != null && component.getParent() != null) {
throw new IllegalRootComponentException("A component with parent cannot be set as a graph's root component. Make sure the component doesn't parent.");
}
this.rootComponent = component;
}
public Component getRootComponent() {
return rootComponent;
}
/**
* Register {@link Provider.DisposeListener} which will be called when either
* <ul>
* <li>The provider doesn't have a scope instances and a provided instance is dereferenced</li>
* <li>The provider has a scope instances and the provider is dereferenced with 0 reference count</li>
* </ul>
* @param disposeListener The listener
*/
public void registerDisposeListener(Provider.DisposeListener disposeListener) {
if (disposeListeners == null) {
disposeListeners = new CopyOnWriteArrayList<>();
}
disposeListeners.add(disposeListener);
}
/**
* Unregister {@link Provider.DisposeListener} which will be called when either
* <ul>
* <li>The provider doesn't have a scope instances and a provided instance is dereferenced</li>
* <li>The provider has a scope instances and the provider is dereferenced with 0 reference count</li>
* </ul>
*
* @param disposeListener The listener
*/
public void unregisterDisposeListener(Provider.DisposeListener disposeListener) {
if (disposeListeners != null) {
disposeListeners.remove(disposeListener);
if (disposeListeners.isEmpty()) {
disposeListeners = null;
}
}
}
/**
* Clear {@link Provider.DisposeListener}s which will be called when when either
* <ul>
* <li>The provider doesn't have a scope instances and a provided instance is dereferenced</li>
* <li>The provider has a scope instances and the provider is dereferenced with 0 reference count</li>
* </ul>
*/
public void clearDisposeListeners() {
if (disposeListeners != null) {
disposeListeners.clear();
disposeListeners = null;
}
}
/**
* Register {@link Provider.DereferenceListener} which will be called when the provider's
* instance is dereferenced.
*
* @param dereferenceListener The listener
*/
public void registerDereferencedListener(Provider.DereferenceListener dereferenceListener) {
if (dereferenceListeners == null) {
dereferenceListeners = new CopyOnWriteArrayList<>();
}
dereferenceListeners.add(dereferenceListener);
}
/**
* Unregister {@link Provider.DereferenceListener} which will be called when the provider's
* instance is dereferenced.
*
* @param dereferenceListener The listener
*/
public void unregisterDereferencedListener(Provider.DereferenceListener dereferenceListener) {
if (dereferenceListeners != null) {
dereferenceListeners.remove(dereferenceListener);
if (dereferenceListeners.isEmpty()) {
dereferenceListeners = null;
}
}
}
/**
* Clear {@link Provider.DereferenceListener}s which will be called when the provider's
* instance is dereferenced.
*/
public void clearDereferencedListeners() {
if (dereferenceListeners != null) {
dereferenceListeners.clear();
dereferenceListeners = null;
}
}
/**
* Inject all fields annotated by the given injectAnnotation
*
* @param target Whose fields will be injected
* @param injectAnnotation Annotated which a field will be recognize
* @throws ProvideException
*/
public void inject(Object target, Class<? extends Annotation> injectAnnotation)
throws ProvideException, ProviderMissingException, CircularDependenciesException {
if (monitors != null) {
int size = monitors.size();
for (int i = 0; i < size; i++) {
monitors.get(i).onInject(target);
}
}
doInject(target, null, null, null, injectAnnotation);
visitedInjectNodes.clear();
revisitedNode = null;
visitedFields.clear();
}
/**
* Same as {@link #use(Class, Annotation, Class, Consumer)} except using un-qualified injectable type.
*
* @param type The type of the injectable instance
* @param injectAnnotation injectAnnotation
* @param consumer Consume to use the instance
* @throws ProvideException ProvideException
* @throws CircularDependenciesException CircularDependenciesException
* @throws ProviderMissingException ProviderMissingException
*/
public <T> void use(Class<T> type, Class<? extends Annotation> injectAnnotation, Consumer<T> consumer)
throws ProvideException, CircularDependenciesException, ProviderMissingException {
use(type, null, injectAnnotation, consumer);
}
/**
* Use an injectable instance in the scope of {@link Consumer#consume(Object)} without injecting
* it as a field of an object. This method will automatically retain the instance before
* {@link Consumer#consume(Object)} is called and released after it's returned. As a result,
* it doesn't hold the instance like the field marked by {@link Inject} that will retain the
* reference of the instance until {@link #release(Object, Class)} is called. However, in the
* scope of {@link Consumer#consume(Object)} the instance will be held.
* <p>For example,</p>
* <pre>
* private static class Device {
* @MyInject
* private Os os;
* }
*
* final SimpleGraph graph = new SimpleGraph();
* ScopeCache scopeCache = new ScopeCache();
*
* graph.register(Os.class, Android.class, scopeCache);
*
* //OsReferenceCount = 0
* graph.use(Os.class, null, Inject.class, new Consumer<Os>() {
* @Override
* public void consume(Os instance) {
* //First time to create the instance.
* //OsReferenceCount = 1
* }
* });
* //Reference count decremented by use method automatically
* //OsReferenceCount = 0
*
* Device device = new Device();
* graph.inject(device, MyInject.class); //OsReferenceCount = 1
* //New instance created and cached
*
* graph.use(Os.class, null, Inject.class, new Consumer<Os>() {
* @Override
* public void consume(Os instance) {
* //Since reference count is greater than 0, cached instance will be reused
* //OsReferenceCount = 2
* Assert.assertTrue(device.os == instance);
* }
* });
* //Reference count decremented by use method automatically
* //OsReferenceCount = 1
*
* graph.release(device, MyInject.class); //OsReferenceCount = 0
* //Last instance released, so next time a new instance will be created
*
* graph.use(Os.class, null, Inject.class, new Consumer<Os>() {
* @Override
* public void consume(Os instance) {
* //OsReferenceCount = 1
* //Since the cached instance is cleared, the new instance is a newly created one.
* Assert.assertTrue(device.os != instance);
* }
* });
* //Reference count decremented by use method automatically
* //OsReferenceCount = 0
*
* graph.use(Os.class, null, Inject.class, new Consumer<Os>() {
* @Override
* public void consume(Os instance) {
* //OsReferenceCount = 1
* //Since the cached instance is cleared, the new instance is a newly created one.
* Assert.assertTrue(device.os != instance);
* }
* });
* //Reference count decremented by use method automatically
* //OsReferenceCount = 0
* //Cached instance cleared again
*
* graph.use(Os.class, null, Inject.class, new Consumer<Os>() {
* @Override
* public void consume(Os instance) {
* //OsReferenceCount = 1
* graph.inject(device, MyInject.class);
* //Injection will reuse the cached instance and increment the reference count
* //OsReferenceCount = 2
*
* //Since the cached instance is cleared, the new instance is a newly created one.
* Assert.assertTrue(device.os == instance);
* }
* });
* //Reference count decremented by use method automatically
* //OsReferenceCount = 1
*
* graph.release(device, MyInject.class); //OsReferenceCount = 0
* </pre>
*
* @param type The type of the injectable instance
* @param qualifier Qualifier for the injectable instance
* @param injectAnnotation injectAnnotation
* @param consumer Consume to use the instance
* @throws ProvideException ProvideException
* @throws CircularDependenciesException CircularDependenciesException
* @throws ProviderMissingException ProviderMissingException
*/
public <T> void use(Class<T> type, Annotation qualifier,
Class<? extends Annotation> injectAnnotation, Consumer<T> consumer)
throws ProvideException, CircularDependenciesException, ProviderMissingException {
T instance = reference(type, qualifier, injectAnnotation);
consumer.consume(instance);
dereference(instance, type, qualifier, injectAnnotation);
}
private <T> Provider<T> findProvider(Class<T> type, Annotation qualifier) throws ProviderMissingException {
Provider<T> provider = rootComponent.findProvider(type, qualifier);
return provider;
}
/**
* Reference an injectable object and retain it. Use
* {@link #dereference(Object, Class, Annotation, Class)} to dereference it when it's not used
* any more.
*
* @param type the type of the object
* @param qualifier the qualifier
* @param injectAnnotation the inject annotation
* @return
*/
public <T> T reference(Class<T> type, Annotation qualifier, Class<? extends Annotation> injectAnnotation)
throws ProviderMissingException, ProvideException, CircularDependenciesException {
Provider<T> provider = findProvider(type, qualifier);
T instance = provider.get();
doInject(instance, null, type, qualifier, injectAnnotation);
provider.retain();
provider.notifyReferenced(provider, instance);
//Clear visiting records
visitedFields.clear();
visitedInjectNodes.clear();
return instance;
}
/**
* Dereference an injectable object. When it's not referenced by anything else after this
* dereferencing, release its cached instance if possible.
*
* @param instance the instance is to dereference
* @param type the type of the object
* @param qualifier the qualifier
* @param injectAnnotation the inject annotation
*/
public <T> void dereference(T instance, Class<T> type, Annotation qualifier,
Class<? extends Annotation> injectAnnotation) throws ProviderMissingException {
doRelease(instance, null, type, qualifier, injectAnnotation);
Provider<T> provider = findProvider(type, qualifier);
provider.release();
dereferenceProvider(provider, instance);
}
@SuppressWarnings("unchecked")
private void doInject(Object target, Field targetField, Class targetType, Annotation targetQualifier,
Class<? extends Annotation> injectAnnotation)
throws ProvideException, ProviderMissingException, CircularDependenciesException {
boolean circularDetected = false;
Provider targetProvider;
if (targetType != null) {
//Nested injection
circularDetected = recordVisit(targetType, targetQualifier);
targetProvider = findProvider(targetType, targetQualifier);
Object cachedInstance = targetProvider.getCachedInstance();
boolean infiniteCircularInjection = true;
if (circularDetected) {
if (cachedInstance != null) {
infiniteCircularInjection = false;
}
if (infiniteCircularInjection) {
throwCircularDependenciesException();
}
}
}
if (!circularDetected && target != null) {
Class<?> clazz = target.getClass();
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(injectAnnotation)) {
Class fieldType = field.getType();
Annotation fieldQualifier = ReflectUtils.findFirstQualifierInAnnotations(field);
Provider provider = findProvider(fieldType, fieldQualifier);
Object impl = provider.get();
ReflectUtils.setField(target, field, impl);
boolean visited = isFieldVisited(target, targetField, field);
if (!visited) {
doInject(impl, field, fieldType, fieldQualifier, injectAnnotation);
}
provider.retain(target, field);
provider.notifyReferenced(provider, impl);
recordVisitField(target, targetField, field);
}
}
clazz = clazz.getSuperclass();
}
if (targetType != null) {
unrecordVisit(targetType, targetQualifier);
}
}
}
/**
* Release cached instances held by fields of target object. References of instances of the
* instances will be decremented. Once the reference count of a controller reaches 0, it will
* be removed from the instances and raise {@link Provider.DereferenceListener}.
*
* @param target Whose fields will be injected
* @param injectAnnotation Annotated which a field will be recognize
*/
public void release(Object target, Class<? extends Annotation> injectAnnotation) throws ProviderMissingException {
if (monitors != null) {
int size = monitors.size();
for (int i = 0; i < size; i++) {
monitors.get(i).onRelease(target);
}
}
doRelease(target, null, null, null, injectAnnotation);
visitedInjectNodes.clear();
revisitedNode = null;
visitedFields.clear();
}
private void doRelease(Object target, Field targetField, Class targetType, Annotation targetQualifier,
final Class<? extends Annotation> injectAnnotation) throws ProviderMissingException {
Class<?> clazz = target.getClass();
boolean circularDetected = false;
if (targetType != null) {
circularDetected = recordVisit(targetType, targetQualifier);
}
if (!circularDetected) {
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(injectAnnotation)) {
Object fieldValue = ReflectUtils.getFieldValue(target, field);
if (fieldValue != null) {
final Class<?> fieldType = field.getType();
Annotation fieldQualifier = ReflectUtils.findFirstQualifierInAnnotations(field);
Provider provider = findProvider(fieldType, fieldQualifier);
boolean stillReferenced = provider.getReferenceCount(target, field) > 0;
boolean fieldVisited = isFieldVisited(target, targetField, field);
if (!fieldVisited && stillReferenced) {
recordVisitField(target, targetField, field);
doRelease(fieldValue, field, fieldType, fieldQualifier, injectAnnotation);
provider.release(target, field);
dereferenceProvider(provider, fieldValue);
}
}
}
}
clazz = clazz.getSuperclass();
}
if (targetType != null) {
unrecordVisit(targetType, targetQualifier);
}
}
}
private <T> void dereferenceProvider(Provider<T> provider, T instance) {
if (dereferenceListeners != null) {
int listenerSize = dereferenceListeners.size();
for (int i = 0; i < listenerSize; i++) {
dereferenceListeners.get(i).onDereferenced(provider, instance);
}
}
if (disposeListeners != null) {
boolean disposing = false;
if (provider.getScopeCache() == null) {
disposing = true;
} else if (provider.getReferenceCount() == 0) {
disposing = true;
}
if (disposing) {
int listenerSize = disposeListeners.size();
for (int i = 0; i < listenerSize; i++) {
disposeListeners.get(i).onDisposed(provider, instance);
}
}
}
}
/**
* Records the field of a target object is visited
*
* @param object The field holder
* @param objectField The field which holds the object in its parent
* @param field The field of the holder
*/
private void recordVisitField(Object object, Field objectField, Field field) {
Map<String, Set<String>> bag = visitedFields.get(object);
if (bag == null) {
bag = new HashMap<>();
visitedFields.put(object, bag);
}
Set<String> fields = bag.get(objectField);
String objectFiledKey = objectField == null ? "" : objectField.toGenericString();
if (fields == null) {
fields = new HashSet<>();
bag.put(objectFiledKey, fields);
}
fields.add(field.toGenericString());
}
/**
* Indicates whether the field of a target object is visited
*
* @param object The field holder
* @param objectField The field which holds the object in its parent
* @param field The field of the holder
*/
private boolean isFieldVisited(Object object, Field objectField, Field field) {
Map<String, Set<String>> bag = visitedFields.get(object);
if (bag == null) {
return false;
}
String objectFiledKey = objectField == null ? "" : objectField.toGenericString();
Set<String> fields = bag.get(objectFiledKey);
return fields != null && fields.contains(field);
}
private boolean recordVisit(Class classType, Annotation qualifier) {
String key = PokeHelper.makeProviderKey(classType, qualifier);
boolean circularVisitDetected = visitedInjectNodes.contains(key);
if (!circularVisitDetected) {
visitedInjectNodes.add(key);
} else {
revisitedNode = key;
}
return circularVisitDetected;
}
private void unrecordVisit(Class classType, Annotation qualifier) {
String key = PokeHelper.makeProviderKey(classType, qualifier);
visitedInjectNodes.remove(key);
}
/**
* Print readable circular graph
*
* @throws CircularDependenciesException
*/
private void throwCircularDependenciesException()
throws CircularDependenciesException {
String msg = "Circular dependencies found. Check the circular graph below:\n";
boolean firstNode = true;
String tab = " ";
for (String visit : visitedInjectNodes) {
if (!firstNode) {
msg += tab + "->";
tab += tab;
}
msg += visit + "\n";
firstNode = false;
}
msg += tab.substring(2) + "->" + revisitedNode + "\n";
throw new CircularDependenciesException(msg);
}
/**
* Monitor to watch when the graph is about to inject or release an object
*/
public interface Monitor {
/**
* Called when the graph is about to inject dependencies into the given object
*
* @param target The object whose injectable fields have been injected
*/
void onInject(Object target);
/**
* Called when the graph is about to release dependencies from the given object
*
* @param target The object whose injectable fields have been released
*/
void onRelease(Object target);
}
}