package org.rapidoid.ioc.impl;
import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.cls.Cls;
import org.rapidoid.collection.Coll;
import org.rapidoid.commons.AnyObj;
import org.rapidoid.commons.Deep;
import org.rapidoid.config.Conf;
import org.rapidoid.config.Config;
import org.rapidoid.ioc.*;
import org.rapidoid.lambda.Lmbd;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.log.Log;
import org.rapidoid.scan.Scan;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import org.rapidoid.util.MscOpts;
import javax.annotation.PostConstruct;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/*
* #%L
* rapidoid-inject
* %%
* Copyright (C) 2014 - 2017 Nikolche Mihajlovski and contributors
* %%
* 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.
* #L%
*/
@Authors("Nikolche Mihajlovski")
@Since("5.1.0")
public class IoCContextImpl extends RapidoidThing implements IoCContext {
public IoCContextImpl() {
}
private volatile String name;
private volatile IoCState state = new IoCState();
private volatile BeanProvider beanProvider;
private volatile IoCContextWrapper wrapper;
private final Map<Class<?>, ClassMetadata> metadata = Coll
.autoExpandingMap(new Mapper<Class<?>, ClassMetadata>() {
@Override
public ClassMetadata map(Class<?> clazz) throws Exception {
return new ClassMetadata(clazz);
}
});
@Override
public IoCContext name(String name) {
this.name = name;
return this;
}
@Override
public String name() {
return name;
}
@Override
public synchronized void reset() {
if (!state.isEmpty()) {
Log.info("Resetting IoC context", "context", this);
} else {
Log.debug("Resetting IoC context", "context", this);
}
state.reset();
metadata.clear();
beanProvider = null;
}
private ClassMetadata meta(Class<?> type) {
return metadata.get(type);
}
@Override
public synchronized void manage(Object... classesOrInstances) {
classesOrInstances = AnyObj.flat(classesOrInstances);
List<Class<?>> autoCreate = U.list();
for (Object classOrInstance : classesOrInstances) {
boolean isClass = isClass(classOrInstance);
Class<?> clazz = Cls.toClass(classOrInstance);
if (Msc.matchingProfile(clazz)) {
for (Class<?> interfacee : Cls.getImplementedInterfaces(clazz)) {
addProvider(interfacee, classOrInstance);
}
if (isClass) {
Log.debug("configuring managed class", "class", classOrInstance);
state.providedClasses.add(clazz);
if (!clazz.isInterface() && !clazz.isEnum() && !clazz.isAnnotation()) {
// if the class is annotated, auto-create an instance
if (clazz.getAnnotation(Autocreate.class) != null) {
autoCreate.add(clazz);
}
}
} else {
Object instance = classOrInstance;
Log.debug("configuring provided instance", "instance", instance);
addProvider(clazz, instance);
state.providedInstances.add(instance);
state.instances.add(instance);
}
}
}
for (Class<?> clazz : autoCreate) {
singleton(clazz);
}
}
private void addProvider(Class<?> type, Object provider) {
state.providersByType.get(type).add(provider);
}
@Override
public synchronized <T> T singleton(Class<T> type) {
Log.debug("Singleton", "type", type);
return provideIoCInstanceOf(null, type, null, null, false);
}
@Override
public synchronized boolean autowire(Object target) {
Log.debug("Autowire", "target", target);
return autowire(target, null, null, null);
}
@Override
public synchronized <T> T autowire(T target, Mapper<String, Object> session, Mapper<String, Object> bindings) {
Log.debug("Autowire", "target", target);
autowire(target, null, session, bindings);
return target;
}
@Override
public synchronized <T> T inject(T target) {
Log.debug("Inject", "target", target);
return register(target, null);
}
@Override
public synchronized <T> T inject(T target, Map<String, Object> properties) {
Log.debug("Inject", "target", target, "properties", properties);
return register(target, properties);
}
private <T> T provideSessionValue(Object target, Class<T> type, String name, Mapper<String, Object> session) {
U.notNull(session, "session");
Object value = Lmbd.eval(session, name);
return value != null ? Cls.convert(value, type) : null;
}
private <T> T provideBindValue(Object target, Class<T> type, String name, Mapper<String, Object> bindings) {
U.notNull(bindings, "bindings");
Object value = Lmbd.eval(bindings, name);
return value != null ? Cls.convert(value, type) : null;
}
@SuppressWarnings("unchecked")
private <T> T provideIoCInstanceOf(Object target, Class<T> type, String name,
Map<String, Object> properties, boolean optional) {
if (target != null) {
processMetadata(meta(target.getClass()));
}
if (type != null) {
processMetadata(meta(type));
}
T instance = (T) provideSpecialInstance(type, name);
if (instance == null && name != null) {
instance = provideInstanceByName(target, type, name, properties);
}
if (instance == null) {
instance = provideInstanceByType(type, properties);
}
BeanProvider provider = this.beanProvider;
if (instance == null && provider != null) {
instance = provider.getBean(type, name);
}
if (instance == null && Cls.isAppBeanType(type)) {
instance = provideNewInstanceOf(type, properties);
}
if (!optional) {
if (instance == null) {
if (name != null) {
throw U.rte("Didn't find a value for type '%s' and name '%s'!", type, name);
} else {
throw U.rte("Didn't find a value for type '%s'!", type);
}
}
}
return Cls.isAppBean(instance) ? register(instance, properties) : null;
}
private boolean processMetadata(ClassMetadata meta) {
boolean processed = false;
if (U.notEmpty(meta.typesToManage)) {
manage(meta.typesToManage);
processed = true;
}
if (U.notEmpty(meta.packagesToScan)) {
List<Class<?>> classes = Scan.annotated(IoC.ANNOTATIONS).in(meta.packagesToScan).loadAll();
if (U.notEmpty(classes)) {
manage(classes);
processed = true;
}
}
return processed;
}
@SuppressWarnings("unchecked")
private <T> T provideNewInstanceOf(Class<T> type, Map<String, Object> properties) {
// instantiation if it's real class
if (!type.isInterface() && !type.isEnum() && !type.isAnnotation()) {
if (Msc.matchingProfile(type)) {
T instance = instantiate(type, properties);
return register(instance, properties);
}
}
return null;
}
private <T> T instantiate(Class<T> type, Map<String, Object> properties) {
ClassMetadata meta = meta(type);
if (U.notEmpty(meta.injectableConstructors)) {
U.must(meta.injectableConstructors.size() == 1, "Found more than 1 @Inject-annotated constructor for: %s", type);
Constructor<?> constr = U.single(meta.injectableConstructors);
Class<?>[] paramTypes = constr.getParameterTypes();
Object[] args = new Object[paramTypes.length];
for (int i = 0; i < args.length; i++) {
Class<?> paramType = paramTypes[i];
String paramName = null; // FIXME inject by name
args[i] = provideIoCInstanceOf(null, paramType, paramName, properties, false);
}
return Cls.invoke(constr, args);
} else if (meta.defaultConstructor != null) {
return Cls.invoke(meta.defaultConstructor);
} else {
throw U.illegal("Cannot find a default nor @Inject-annotated constructor for: %s", type);
}
}
private <T> T provideInstanceByType(Class<T> type, Map<String, Object> properties) {
Set<Object> providers = state.providersByType.get(type);
Object provider = null;
for (Object candidate : providers) {
if (provider == null) {
provider = candidate;
} else {
if (isClass(provider) && !isClass(candidate)) {
provider = candidate;
} else if (isClass(provider) || !isClass(candidate)) {
throw U.rte("Found more than 1 candidates for type '%s': %s", type, providers);
}
}
}
if (provider != null) {
return provideFrom(provider, properties);
}
return null;
}
@SuppressWarnings("unchecked")
private <T> T provideFrom(Object provider, Map<String, Object> properties) {
T instance;
if (isClass(provider)) {
instance = provideNewInstanceOf((Class<T>) provider, properties);
} else {
instance = (T) provider;
}
return instance;
}
private boolean isClass(Object obj) {
return obj instanceof Class;
}
private Object provideSpecialInstance(Class<?> type, String name) {
String cls = type.getName();
if (type.equals(IoCContext.class)) {
return U.or(wrapper, this);
}
if (cls.equals("javax.persistence.EntityManager") && MscOpts.hasRapidoidJPA()) {
return OptionalJPAUtil.getSharedContextAwareEntityManagerProxy();
}
if (cls.equals("javax.persistence.EntityManagerFactory") && MscOpts.hasRapidoidJPA()) {
return OptionalJPAUtil.getSharedEntityManagerFactoryProxy();
}
return null;
}
private <T> T provideInstanceByName(Object target, Class<T> type, String name, Map<String, Object> properties) {
T instance = getInjectableByName(target, type, name, properties, false);
if (target != null) {
instance = getInjectableByName(target, type, name, properties, true);
}
if (instance == null) {
instance = getInjectableByName(target, type, name, properties, true);
}
return (T) instance;
}
@SuppressWarnings("unchecked")
private <T> T getInjectableByName(Object target, Class<T> type, String name,
Map<String, Object> properties, boolean useConfig) {
Object instance = properties != null ? properties.get(name) : null;
if (instance == null && target != null && useConfig) {
Config config = Conf.section(target.getClass());
if (type.equals(Boolean.class) || type.equals(boolean.class)) {
instance = config.is(name);
} else {
String opt = config.entry(name).str().getOrNull();
if (opt != null) {
instance = Cls.convert(opt, type);
}
}
}
return (T) instance;
}
private boolean autowire(Object target, Map<String, Object> properties, Mapper<String, Object> session,
Mapper<String, Object> locals) {
Log.debug("Autowiring", "target", target, "session", session, "bindings", locals);
ClassMetadata meta = meta(target.getClass());
boolean processed = processMetadata(meta);
for (Field field : meta.injectableFields) {
boolean optional = isInjectOptional(field);
Object value = provideIoCInstanceOf(target, field.getType(), field.getName(), properties, optional);
Log.debug("Injecting field value", "target", target, "field", field.getName(), "value", value);
if (!optional || value != null) {
Cls.setFieldValue(target, field.getName(), value);
processed = true;
}
}
return processed;
}
private boolean isInjectOptional(Field field) {
Wired wired = field.getAnnotation(Wired.class);
return wired != null && wired.optional();
}
private <T> void invokePostConstruct(T target) {
List<Method> methods = Cls.getMethodsAnnotated(target.getClass(), PostConstruct.class);
for (Method method : methods) {
Cls.invoke(method, target);
}
}
private <T> T register(T target, Map<String, Object> properties) {
U.must(Cls.isAppBean(target), "Not a bean: %s", target);
if (!isManaged(target)) {
add(target);
autowire(target, properties, null, null);
invokePostConstruct(target);
}
return target;
}
private boolean isManaged(Object instance) {
return state.instances.contains(instance) || state.providedInstances.contains(instance);
}
private void add(Object instance) {
Class<?> clazz = instance.getClass();
for (Class<?> interfacee : Cls.getImplementedInterfaces(clazz)) {
addProvider(interfacee, instance);
}
addProvider(clazz, instance);
state.instances.add(instance);
}
@Override
public synchronized boolean remove(Object bean) {
boolean removedProvided = state.providedInstances.remove(bean);
boolean removedInstance = state.instances.remove(bean);
boolean removed = removedProvided || removedInstance;
if (removed) {
Class<?> clazz = bean.getClass();
state.providedClasses.remove(clazz);
metadata.remove(clazz);
for (Map.Entry<Class<?>, Set<Object>> e : state.providersByType.entrySet()) {
Iterator<Object> it = e.getValue().iterator();
while (it.hasNext()) {
Object provider = it.next();
if (Cls.instanceOf(provider, bean.getClass())) {
it.remove();
}
}
}
}
return removed;
}
@Override
public <K, V> Map<K, V> autoExpandingInjectingMap(final Class<V> clazz) {
return Coll.autoExpandingMap(new Mapper<K, V>() {
@Override
public V map(K src) throws Exception {
return inject(Cls.newInstance(clazz));
}
});
}
@Override
public synchronized Object findInstanceOf(String className) {
for (Object instance : state.providedInstances) {
if (instance.getClass().getName().equals(className)) {
return instance;
}
}
for (Map.Entry<Class<?>, Set<Object>> e : state.providersByType.entrySet()) {
for (Object provider : e.getValue()) {
if (provider.getClass().getName().equals(className)) {
return provider;
}
}
}
return null;
}
@Override
public synchronized IoCContextChanges reload(List<Class<?>> modified, List<String> deleted) {
ClassLoader classLoader = !U.isEmpty(modified) ? U.last(modified).getClassLoader() : null;
List<Object> loadedInstances = U.list();
List<Object> removedInstances = U.list();
for (String className : deleted) {
Object bean = findInstanceOf(className);
if (bean != null) {
remove(bean);
removedInstances.add(bean);
} else {
Log.warn("Couldn't find the target class to deregister!", "class", className, "context", this);
}
}
if (classLoader != null) {
List<Object> toRefresh = U.list(state.instances);
toRefresh.removeAll(state.providedInstances);
for (Object oldBean : toRefresh) {
String className = oldBean.getClass().getName();
remove(oldBean);
Class<?> cls;
try {
cls = classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
Log.error("Couldn't find the class to reload!", "class", className);
continue;
}
Object newBean = singleton(cls);
removedInstances.add(oldBean);
loadedInstances.add(newBean);
}
}
for (Class<?> cls : modified) {
Object newBean = singleton(cls);
loadedInstances.add(newBean);
}
return new IoCContextChanges(loadedInstances, removedInstances);
}
@Override
public synchronized Map<String, Object> info() {
return state.info();
}
public synchronized IoCState backup() {
return state.copy();
}
public synchronized void rollback(IoCState backup) {
this.state = backup;
}
@Override
public void beanProvider(BeanProvider beanProvider) {
this.beanProvider = beanProvider;
}
@Override
public Set<Object> getManagedInstances() {
Set<Object> beans = U.set(state.instances);
beans.addAll(state.providedInstances);
return Collections.unmodifiableSet(beans);
}
@Override
public Set<Class<?>> getManagedClasses() {
return Collections.unmodifiableSet(state.providedClasses);
}
@Override
public String toString() {
return Deep.copyOf(state.instances, Msc.TRANSFORM_TO_SIMPLE_CLASS_NAME).toString();
}
void wrapper(IoCContextWrapper wrapper) {
this.wrapper = wrapper;
}
}