package act.inject.param;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* 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%
*/
import act.Act;
import act.app.ActionContext;
import act.app.App;
import act.app.data.BinderManager;
import act.app.data.StringValueResolverManager;
import act.controller.ActionMethodParamAnnotationHandler;
import act.inject.DefaultValue;
import act.inject.DependencyInjector;
import act.inject.SessionVariable;
import act.inject.genie.DependentScope;
import act.inject.genie.GenieInjector;
import act.inject.genie.RequestScope;
import act.inject.genie.SessionScope;
import act.util.ActContext;
import act.util.DestroyableBase;
import org.osgl.$;
import org.osgl.exception.UnexpectedException;
import org.osgl.inject.BeanSpec;
import org.osgl.inject.InjectException;
import org.osgl.inject.util.AnnotationUtil;
import org.osgl.inject.util.ArrayLoader;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.mvc.annotation.Bind;
import org.osgl.mvc.annotation.Param;
import org.osgl.mvc.result.Result;
import org.osgl.mvc.util.Binder;
import org.osgl.util.*;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.New;
import javax.inject.Named;
import javax.inject.Provider;
import javax.validation.*;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Manage {@link ParamValueLoader} grouped by Method
*/
public abstract class ParamValueLoaderService extends DestroyableBase {
protected Logger logger = LogManager.get(getClass());
private static final ParamValueLoader[] DUMB = new ParamValueLoader[0];
private static final ThreadLocal<ParamTree> PARAM_TREE = new ThreadLocal<ParamTree>();
private static final ParamValueLoader RESULT_LOADER = new ParamValueLoader() {
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
return context.attribute(ActionContext.ATTR_RESULT);
}
@Override
public String bindName() {
return null;
}
};
private static final ParamValueLoader EXCEPTION_LOADED = new ParamValueLoader() {
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
return context.attribute(ActionContext.ATTR_EXCEPTION);
}
@Override
public String bindName() {
return null;
}
};
// contains field names that should be waived when looking for value loader
private static final Set<String> fieldBlackList = new HashSet<>();
StringValueResolverManager resolverManager;
BinderManager binderManager;
DependencyInjector<?> injector;
ConcurrentMap<Method, ParamValueLoader[]> methodRegistry = new ConcurrentHashMap<>();
Map<Method, Boolean> methodValidationConstraintLookup = new HashMap();
ConcurrentMap<Class, Map<Field, ParamValueLoader>> fieldRegistry = new ConcurrentHashMap<Class, Map<Field, ParamValueLoader>>();
ConcurrentMap<Class, ParamValueLoader> classRegistry = new ConcurrentHashMap<Class, ParamValueLoader>();
private ConcurrentMap<$.T2<Type, Annotation[]>, ParamValueLoader> paramRegistry = new ConcurrentHashMap<$.T2<Type, Annotation[]>, ParamValueLoader>();
private ConcurrentMap<BeanSpec, Map<Class<? extends Annotation>, ActionMethodParamAnnotationHandler>> annoHandlers = new ConcurrentHashMap<BeanSpec, Map<Class<? extends Annotation>, ActionMethodParamAnnotationHandler>>();
private Map<Class<? extends Annotation>, ActionMethodParamAnnotationHandler> allAnnotationHandlers;
private Validator validator;
private volatile ExecutableValidator executableValidator;
public ParamValueLoaderService(App app) {
resolverManager = app.resolverManager();
binderManager = app.binderManager();
injector = app.injector();
allAnnotationHandlers = new HashMap<>();
List<ActionMethodParamAnnotationHandler> list = Act.pluginManager().pluginList(ActionMethodParamAnnotationHandler.class);
for (ActionMethodParamAnnotationHandler h : list) {
Set<Class<? extends Annotation>> set = h.listenTo();
for (Class<? extends Annotation> c : set) {
allAnnotationHandlers.put(c, h);
}
}
}
@Override
protected void releaseResources() {
DestroyableBase.Util.tryDestroyAll(classRegistry.values(), ApplicationScoped.class);
DestroyableBase.Util.tryDestroyAll(paramRegistry.values(), ApplicationScoped.class);
}
public Object loadHostBean(Class beanClass, ActContext<?> ctx) {
ParamValueLoader loader = classRegistry.get(beanClass);
if (null == loader) {
loader = findBeanLoader(beanClass);
classRegistry.putIfAbsent(beanClass, loader);
}
return loader.load(null, ctx, false);
}
public Object[] loadMethodParams(Object host, Method method, ActContext ctx) {
try {
ParamValueLoader[] loaders = methodRegistry.get(method);
Boolean hasValidationConstraint = methodValidationConstraintLookup.get(method);
if (null == loaders) {
$.Var<Boolean> boolBag = $.var(Boolean.FALSE);
Class hostClass = null == host ? null : host.getClass();
loaders = findMethodParamLoaders(method, hostClass, boolBag);
methodRegistry.putIfAbsent(method, loaders);
hasValidationConstraint = boolBag.get();
if (hasValidationConstraint && null == host) {
logger.error("Cannot validate static method: %s", method);
hasValidationConstraint = false;
}
methodValidationConstraintLookup.put(method, hasValidationConstraint);
}
int sz = loaders.length;
Object[] params = new Object[sz];
for (int i = 0; i < sz; ++i) {
params[i] = loaders[i].load(null, ctx, false);
}
if (null != hasValidationConstraint && hasValidationConstraint) {
Set<ConstraintViolation> violations = $.cast(executableValidator().validateParameters(host, method, params));
if (!violations.isEmpty()) {
Map<String, ConstraintViolation> map = new HashMap<>();
for (ConstraintViolation v : violations) {
S.Buffer buf = ctx.strBuf();
for (Path.Node node : v.getPropertyPath()) {
if (node.getKind() == ElementKind.METHOD) {
continue;
} else if (node.getKind() == ElementKind.PARAMETER) {
Path.ParameterNode pnode = node.as(Path.ParameterNode.class);
int paramIdx = pnode.getParameterIndex();
ParamValueLoader ploader = loaders[paramIdx];
buf.append(ploader.bindName());
} else if (node.getKind() == ElementKind.PROPERTY) {
buf.append(".").append(node.toString());
}
}
map.put(buf.toString(), v);
}
ctx.addViolations(map);
}
}
return params;
} finally {
PARAM_TREE.remove();
}
}
protected <T> ParamValueLoader findBeanLoader(Class<T> beanClass) {
final Provider<T> provider = injector.getProvider(beanClass);
final Map<Field, ParamValueLoader> loaders = fieldLoaders(beanClass);
final boolean hasField = !loaders.isEmpty();
final $.Var<Boolean> hasValidateConstraint = $.var();
ParamValueLoader loader = new ParamValueLoader() {
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
if (null == bean) {
bean = provider.get();
}
if (!hasField) {
return bean;
}
try {
for (Map.Entry<Field, ParamValueLoader> entry : loaders.entrySet()) {
Field field = entry.getKey();
ParamValueLoader loader = entry.getValue();
Object fieldValue = loader.load(null, context, noDefaultValue);
if (null != fieldValue) {
field.set(bean, fieldValue);
} else {
fieldValue = field.get(bean);
}
if (hasValidationConstraint(BeanSpec.of(field, injector))) {
hasValidateConstraint.set(true);
}
}
} catch (IllegalAccessException e) {
throw new InjectException(e);
}
return bean;
}
@Override
public String bindName() {
return null;
}
};
return decorate(loader, BeanSpec.of(beanClass, injector), beanClass.getDeclaredAnnotations(), false, true);
}
private boolean shouldWaive(Field field) {
int modifiers = field.getModifiers();
return Modifier.isStatic(modifiers)
|| Modifier.isTransient(modifiers)
|| noBind(field.getDeclaringClass())
|| field.isAnnotationPresent(NoBind.class)
|| fieldBlackList.contains(field.getName())
|| Object.class.equals(field.getDeclaringClass())
|| field.getDeclaringClass().isAnnotationPresent(NoBind.class);
}
private ConcurrentMap<Class, Boolean> noBindCache = new ConcurrentHashMap<>();
private boolean noBind(Class c) {
Boolean b = noBindCache.get(c);
if (null != b) {
return b;
}
Annotation[] aa = c.getDeclaredAnnotations();
if (null != aa) {
for (Annotation a: aa) {
if (a.annotationType() == NoBind.class) {
noBindCache.putIfAbsent(c, true);
return true;
}
}
}
noBindCache.putIfAbsent(c, false);
return false;
}
private <T> Map<Field, ParamValueLoader> fieldLoaders(Class<T> beanClass) {
Map<Field, ParamValueLoader> fieldLoaders = fieldRegistry.get(beanClass);
if (null == fieldLoaders) {
fieldLoaders = new HashMap<>();
for (Field field : $.fieldsOf(beanClass, true)) {
if (shouldWaive(field)) {
continue;
}
Type type = field.getGenericType();
Annotation[] annotations = field.getAnnotations();
BeanSpec spec = BeanSpec.of(type, annotations, field.getName(), injector);
ParamValueLoader loader = paramValueLoaderOf(spec);
boolean provided = (loader instanceof ProvidedValueLoader);
if (null != loader && !provided) {
fieldLoaders.put(field, loader);
}
}
fieldRegistry.putIfAbsent(beanClass, fieldLoaders);
}
return fieldLoaders;
}
protected ParamValueLoader[] findMethodParamLoaders(Method method, Class host, $.Var<Boolean> hasValidationConstraint) {
Type[] types = method.getGenericParameterTypes();
int sz = types.length;
if (0 == sz) {
return DUMB;
}
ParamValueLoader[] loaders = new ParamValueLoader[sz];
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < sz; ++i) {
String name = paramName(i);
Type type = types[i];
if (type instanceof TypeVariable) {
TypeVariable var = $.cast(type);
if (null != host) {
type = Generics.buildTypeParamImplLookup(host).get(var.getName());
}
if (null == type) {
throw new UnexpectedException("Cannot infer param type: %s", var.getName());
}
}
BeanSpec spec = BeanSpec.of(type, annotations[i], name, injector);
if (hasValidationConstraint(spec)) {
hasValidationConstraint.set(true);
}
ParamValueLoader loader = paramValueLoaderOf(spec);
if (null == loader) {
throw new UnexpectedException("Cannot find param value loader for param: " + spec);
}
loaders[i] = loader;
}
return loaders;
}
private ParamValueLoader paramValueLoaderOf(BeanSpec spec) {
return paramValueLoaderOf(spec, null);
}
private ParamValueLoader paramValueLoaderOf(BeanSpec spec, String bindName) {
Class<?> rawType = spec.rawType();
if (Result.class.isAssignableFrom(rawType)) {
return RESULT_LOADER;
} else if (Exception.class.isAssignableFrom(rawType)) {
return EXCEPTION_LOADED;
}
Type type = spec.type();
Annotation[] annotations = spec.allAnnotations();
$.T2<Type, Annotation[]> key = $.T2(type, annotations);
ParamValueLoader loader = paramRegistry.get(key);
if (null == loader) {
loader = findLoader(spec, type, annotations, bindName);
// Cannot use spec as the key here because
// spec does not compare Scoped annotation
if (null != loader) {
paramRegistry.putIfAbsent(key, loader);
}
}
return loader;
}
protected abstract ParamValueLoader findContextSpecificLoader(
String bindName,
Class rawType,
BeanSpec spec,
Type type,
Annotation[] annotations
);
protected final ParamValueLoader binder(BeanSpec spec, String bindName) {
Class rawType = spec.rawType();
ParamValueLoader loader = null;
{
Bind bind = spec.getAnnotation(Bind.class);
if (null != bind) {
for (Class<? extends Binder> binderClass : bind.value()) {
Binder binder = injector.get(binderClass);
if (rawType.isAssignableFrom(binder.targetType())) {
loader = new BoundedValueLoader(binder, bindName);
break;
}
}
}
}
if (null == loader) {
Annotation[] aa = spec.allAnnotations();
for (Annotation a : aa) {
Bind bind = AnnotationUtil.tagAnnotation(a, Bind.class);
if (null != bind) {
for (Class<? extends Binder> binderClass : bind.value()) {
Binder binder = injector.get(binderClass);
binder.attributes($.evaluate(a));
if (rawType.isAssignableFrom(binder.targetType())) {
loader = new BoundedValueLoader(binder, bindName);
break;
}
}
}
}
}
if (null == loader) {
Binder binder = binderManager.binder(rawType);
if (null != binder) {
loader = new BoundedValueLoader(binder, bindName);
}
}
return loader;
}
protected String paramName(int i) {
return null;
}
protected boolean supportJsonDecorator() {
return false;
}
private ParamValueLoader findLoader(
BeanSpec spec,
Type type,
Annotation[] annotations,
String bindName
) {
if (provided(spec, injector)) {
return ProvidedValueLoader.get(spec, injector);
}
if (null != filter(annotations, NoBind.class)) {
return null;
}
if (null == bindName) {
bindName = bindName(annotations, spec.name());
}
Class rawType = spec.rawType();
ParamValueLoader loader = findContextSpecificLoader(bindName, rawType, spec, type, annotations);
if (null == loader) {
return null;
}
return decorate(loader, spec, annotations, supportJsonDecorator(), false);
}
ParamValueLoader buildLoader(final ParamKey key, final Type type, BeanSpec targetSpec) {
Class rawType = BeanSpec.rawTypeOf(type);
if (rawType.isArray()) {
return buildArrayLoader(key, rawType.getComponentType(), targetSpec);
}
if (Collection.class.isAssignableFrom(rawType)) {
Type elementType = Object.class;
if (type instanceof ParameterizedType) {
elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
}
return buildCollectionLoader(key, rawType, elementType, targetSpec);
}
if (Map.class.isAssignableFrom(rawType)) {
Class<?> mapClass = rawType;
Type mapType = type;
boolean canProceed = false;
Type[] typeParams = null;
while (true) {
if (mapType instanceof ParameterizedType) {
typeParams = ((ParameterizedType) mapType).getActualTypeArguments();
if (typeParams.length == 2) {
canProceed = true;
break;
}
}
boolean foundInInterfaces = false;
Type[] ta = mapClass.getGenericInterfaces();
if (ta.length > 0) {
mapType = null;
for (Type t : ta) {
if (t instanceof ParameterizedType) {
if (Map.class.isAssignableFrom((Class) ((ParameterizedType) t).getRawType())) {
mapType = t;
mapClass = mapClass.getSuperclass();
foundInInterfaces = true;
break;
}
}
}
}
if (!foundInInterfaces) {
mapType = mapClass.getGenericSuperclass();
mapClass = mapClass.getSuperclass();
}
if (mapClass == Object.class) {
break;
}
}
E.unexpectedIf(!canProceed, "Cannot load Map type parameter loader: no generic type info available");
Type keyType = typeParams[0];
Type valType = typeParams[1];
return buildMapLoader(key, rawType, keyType, valType, targetSpec);
}
return buildPojoLoader(key, rawType);
}
private ParamValueLoader buildArrayLoader(
final ParamKey key,
final Type elementType,
final BeanSpec targetSpec
) {
final CollectionLoader collectionLoader = new CollectionLoader(key, ArrayList.class, elementType, targetSpec, injector, this);
return new ParamValueLoader() {
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
List list = new ArrayList();
if (null != bean) {
int len = Array.getLength(bean);
for (int i = 0; i < len; ++i) {
list.add(Array.get(bean, i));
}
}
list = (List) collectionLoader.load(list, context, false);
return null == list ? null : ArrayLoader.listToArray(list, BeanSpec.rawTypeOf(elementType));
}
@Override
public String bindName() {
return key.toString();
}
};
}
private ParamValueLoader buildCollectionLoader(
final ParamKey key,
final Class<? extends Collection> collectionClass,
final Type elementType,
BeanSpec targetSpec
) {
return new CollectionLoader(key, collectionClass, elementType, targetSpec, injector, this);
}
private ParamValueLoader buildMapLoader(
final ParamKey key,
final Class<? extends Map> mapClass,
final Type keyType,
final Type valType,
final BeanSpec targetSpec
) {
return new MapLoader(key, mapClass, keyType, valType, targetSpec, injector, this);
}
static ParamTree paramTree() {
return PARAM_TREE.get();
}
static ParamTree ensureParamTree(ActContext context) {
ParamTree tree = PARAM_TREE.get();
if (null == tree) {
tree = new ParamTree();
tree.build(context);
PARAM_TREE.set(tree);
}
return tree;
}
private ParamValueLoader buildPojoLoader(final ParamKey key, final Class type) {
final List<FieldLoader> fieldLoaders = fieldLoaders(key, type);
return new ParamValueLoader() {
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
final $.Var<Object> beanBag = $.var(bean);
$.Factory<Object> beanSource = new $.Factory<Object>() {
@Override
public Object create() {
Object bean = beanBag.get();
if (null == bean) {
try {
bean = injector.get(type);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new InjectException(e, "cannot instantiate %s", type);
}
}
beanBag.set(bean);
return bean;
}
};
for (FieldLoader fl : fieldLoaders) {
fl.applyTo(beanSource, context);
}
return beanBag.get();
}
@Override
public String bindName() {
return key.toString();
}
};
}
private ParamValueLoader findLoader(ParamKey paramKey, Field field) {
BeanSpec spec = BeanSpec.of(field.getGenericType(), field.getDeclaredAnnotations(), injector);
Annotation[] annotations = field.getDeclaredAnnotations();
if (provided(spec, injector)) {
return ProvidedValueLoader.get(spec, injector);
}
String name = null;
Named named = filter(annotations, Named.class);
if (null != named) {
name = named.value();
}
if (S.blank(name)) {
name = field.getName();
}
ParamKey key = paramKey.child(name);
Class fieldType = field.getType();
StringValueResolver resolver = resolverManager.resolver(fieldType, spec);
if (null != resolver) {
DefaultValue def = field.getAnnotation(DefaultValue.class);
return new StringValueResolverValueLoader(key, resolver, null, def, fieldType);
}
return buildLoader(key, field.getGenericType(), spec);
}
private List<FieldLoader> fieldLoaders(ParamKey key, Class type) {
Class<?> current = type;
List<FieldLoader> fieldLoaders = C.newList();
while (null != current && !current.equals(Object.class)) {
for (Field field : current.getDeclaredFields()) {
if (shouldWaive(field)) {
continue;
}
field.setAccessible(true);
fieldLoaders.add(fieldLoader(key, field));
}
current = current.getSuperclass();
}
return fieldLoaders;
}
private FieldLoader fieldLoader(ParamKey key, Field field) {
return new FieldLoader(field, findLoader(key, field));
}
static <T extends Annotation> T filter(Annotation[] annotations, Class<T> annoType) {
for (Annotation annotation : annotations) {
if (annoType == annotation.annotationType()) {
return (T) annotation;
}
}
return null;
}
private ParamValueLoader decorate(
final ParamValueLoader loader,
final BeanSpec spec,
final Annotation[] annotations,
boolean useJsonDecorator,
boolean useValidationDecorator
) {
final ParamValueLoader jsonDecorated = useJsonDecorator ? new JsonParamValueLoader(loader, spec, injector) : loader;
ParamValueLoader validationDecorated = jsonDecorated;
if (useValidationDecorator) {
validationDecorated = new ParamValueLoader() {
private Validator validator = Act.app().getInstance(Validator.class);
@Override
public Object load(Object bean, ActContext<?> context, boolean noDefaultValue) {
Object object = jsonDecorated.load(bean, context, noDefaultValue);
Set<ConstraintViolation> violations = $.cast(validator.validate(object));
if (!violations.isEmpty()) {
Map<String, ConstraintViolation> map = new HashMap<>();
for (ConstraintViolation v : violations) {
map.put(v.getPropertyPath().toString(), v);
}
context.addViolations(map);
}
return object;
}
@Override
public String bindName() {
return jsonDecorated.bindName();
}
};
}
return new ScopedParamValueLoader(validationDecorated, spec, scopeCacheSupport(annotations));
}
private boolean hasValidationConstraint(BeanSpec spec) {
for (Annotation anno : spec.allAnnotations()) {
Class<? extends Annotation> annoType = anno.annotationType();
if (Valid.class == annoType || annoType.isAnnotationPresent(Constraint.class)) {
return true;
}
}
return false;
}
private Map<Class<? extends Annotation>, ActionMethodParamAnnotationHandler> paramAnnoHandlers(BeanSpec spec) {
Map<Class<? extends Annotation>, ActionMethodParamAnnotationHandler> handlers = annoHandlers.get(spec);
if (null != handlers) {
return handlers;
}
handlers = new HashMap<>();
for (Annotation annotation : spec.allAnnotations()) {
Class<? extends Annotation> c = annotation.annotationType();
ActionMethodParamAnnotationHandler h = allAnnotationHandlers.get(c);
if (null != h) {
handlers.put(c, h);
}
}
annoHandlers.putIfAbsent(spec, handlers);
return handlers;
}
private ExecutableValidator executableValidator() {
if (null == executableValidator) {
synchronized (this) {
if (null == executableValidator) {
validator = Act.getInstance(Validator.class);
executableValidator = validator.forExecutables();
}
}
}
return executableValidator;
}
private static ScopeCacheSupport scopeCacheSupport(Annotation[] annotations) {
if (null != filter(annotations, RequestScoped.class) ||
null != filter(annotations, org.osgl.inject.annotation.RequestScoped.class)) {
return RequestScope.INSTANCE;
} else if (sessionScoped(annotations)) {
return SessionScope.INSTANCE;
} else if (null != filter(annotations, Dependent.class) ||
null != filter(annotations, New.class)) {
return DependentScope.INSTANCE;
}
// Default to Request Scope
return RequestScope.INSTANCE;
}
static boolean sessionScoped(Annotation[] annotations) {
return null != filter(annotations, SessionScoped.class)
|| null != filter(annotations, org.osgl.inject.annotation.SessionScoped.class)
|| null != filter(annotations, SessionVariable.class);
}
public static void waiveFields(String... fieldNames) {
fieldBlackList.addAll(C.listOf(fieldNames));
}
public static String bindName(Annotation[] annotations, String defVal) {
String name = tryFindBindName(annotations, defVal);
E.illegalStateIf(null == name, "Cannot find bind name");
return name;
}
static String tryFindBindName(Annotation[] annotations, String defVal) {
Param param = filter(annotations, Param.class);
if (null != param && S.notBlank(param.value())) {
return param.value();
}
Bind bind = filter(annotations, Bind.class);
if (null != bind && S.notBlank(bind.model())) {
return bind.model();
}
Named named = filter(annotations, Named.class);
if (null != named && S.notBlank(named.value())) {
return named.value();
}
if (S.notBlank(defVal)) {
return defVal;
}
return null;
}
public static String bindName(BeanSpec beanSpec) {
return bindName(beanSpec.allAnnotations(), beanSpec.name());
}
public static boolean provided(BeanSpec beanSpec, DependencyInjector<?> injector) {
GenieInjector genieInjector = $.cast(injector);
return genieInjector.injectable(beanSpec);
}
public static boolean noBindOrProvided(BeanSpec beanSpec, DependencyInjector<?> injector) {
return null != beanSpec.getAnnotation(NoBind.class) || provided(beanSpec, injector) || beanSpec.isInstanceOf(Throwable.class);
}
}