package act.db;
/*-
* #%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.App;
import act.plugin.AppServicePlugin;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.osgl.$;
import org.osgl.Osgl;
import org.osgl.exception.NotAppliedException;
import org.osgl.inject.BeanSpec;
import org.osgl.inject.Injector;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.S;
import org.osgl.util.ValueObject;
import java.beans.Transient;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* The `AdaptiveRecord` interface specifies a special {@link Model} in that
* the fields/columns could be implicitly defined by database
*/
public interface AdaptiveRecord<ID_TYPE, MODEL_TYPE extends AdaptiveRecord> extends Model<ID_TYPE, MODEL_TYPE> {
Map<String, Object> internalMap();
/**
* Add or replace a key/val pair into the active record
*
* @param key the key
* @param val the value
* @return the active record instance
*/
MODEL_TYPE putValue(String key, Object val);
/**
* Merge a key/val pair in the active record.
*
* If the key specified does not exists then insert the key/val pair into the record.
*
* If there are existing key/val pair then merge it with the new one:
*
* 1. if the val is simple type or cannot be merged, then replace the existing value with new value
* 2. if the val can be merged, e.g. it is a POJO or another adaptive record, then merge the new value into the old value. Merge shall happen recursively
*
* @param key the key
* @param val the value
* @return the active record instance
*/
MODEL_TYPE mergeValue(String key, Object val);
/**
* Add all key/val pairs from specified kv map into this active record
*
* @param kvMap the key/value pairs
* @return this active record instance
*/
MODEL_TYPE putValues(Map<String, Object> kvMap);
/**
* Merge all key/val pairs from specified kv map into this active record
*
* @param kvMap the key/value pairs
* @return this active record instance
* @see #mergeValue(String, Object)
*/
MODEL_TYPE mergeValues(Map<String, Object> kvMap);
/**
* Get value from the active record by key specified
*
* @param key the key
* @param <T> the generic type of the value
* @return the value or `null` if not found
*/
<T> T getValue(String key);
/**
* Export the key/val pairs from this active record into a map
*
* @return the exported map contains all key/val pairs stored in this active record
*/
Map<String, Object> toMap();
/**
* Get the size of the data stored in the active record
*
* @return the active record size
*/
int size();
/**
* Check if the active records has a value associated with key specified
*
* @param key the key
* @return `true` if there is value associated with the key in the record, or `false` otherwise
*/
boolean containsKey(String key);
/**
* Returns a set of keys that has value stored in the active record
*
* @return the key set
*/
Set<String> keySet();
/**
* Returns a set of entries stored in the active record
*
* @return the entry set
*/
Set<Map.Entry<String, Object>> entrySet();
/**
* Returns a set of entries stored in the active record. For
* field entries, use the field filter specified to check
* if it needs to be added into the return set
*
* @param fieldFilter the function that returns `true` or `false` for
* bean spec of a certain field declared in the class
* @return the entry set with field filter applied
*/
Set<Map.Entry<String, Object>> entrySet($.Function<BeanSpec, Boolean> fieldFilter);
/**
* Returns a Map typed object backed by this active record
*
* @return a Map backed by this active record
*/
Map<String, Object> asMap();
/**
* Returns the meta info of this AdaptiveRecord
*
* @return AdaptiveRecord meta info
*/
@Transient
MetaInfo metaInfo();
class Util {
public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE putValue(MODEL_TYPE ar, String key, Object val) {
Map<String, Object> kv = ar.internalMap();
$.Func2 setter = ar.metaInfo().fieldSetters.get(key);
if (null != setter) {
setter.apply(ar, val);
} else {
kv.put(key, val);
}
return ar;
}
public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE mergeValue(MODEL_TYPE ar, String key, Object val) {
Map<String, Object> kv = ar.internalMap();
$.Func2 merger = ar.metaInfo().fieldMergers.get(key);
if (null != merger) {
merger.apply(ar, val);
} else {
Object v0 = kv.get(key);
kv.put(key, AdaptiveRecord.MetaInfo.merge(v0, val));
}
return ar;
}
public static <MODEL_TYPE extends AdaptiveRecord, T> T getValue(MODEL_TYPE ar, String key) {
Map<String, Object> kv = ar.internalMap();
$.Function getter = ar.metaInfo().fieldGetters.get(key);
if (null != getter) {
return (T) getter.apply(ar);
}
return (T) kv.get(key);
}
public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE putValues(MODEL_TYPE ar, Map<String, Object> map) {
for (Map.Entry<String, Object> entry: map.entrySet()) {
String key = entry.getKey();
if ("id".equals(key)) {
continue;
}
ar.putValue(entry.getKey(), entry.getValue());
}
return ar;
}
public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE mergeValues(MODEL_TYPE ar, Map<String, Object> map) {
for (Map.Entry<String, Object> entry: map.entrySet()) {
ar.mergeValue(entry.getKey(), entry.getValue());
}
return ar;
}
public static <MODEL_TYPE extends AdaptiveRecord, T> boolean containsKey(MODEL_TYPE ar, String key) {
Map<String, Object> kv = ar.internalMap();
return kv.containsKey(key) || ar.metaInfo().getterFieldSpecs.containsKey(key);
}
public static Map<String, Object> toMap(final AdaptiveRecord ar) {
Map<String, Object> kv = ar.internalMap();
Map<String, Object> map = new HashMap<>(kv);
for (Map.Entry<String, $.Function> entry : ar.metaInfo().fieldGetters.entrySet()) {
map.put(entry.getKey(), entry.getValue().apply(ar));
}
return map;
}
private static int fieldsSize(final AdaptiveRecord ar) {
return ar.metaInfo().getterFieldSpecs.size();
}
public static int size(final AdaptiveRecord ar) {
Map<String, Object> kv = ar.internalMap();
return kv.size() + fieldsSize(ar);
}
private static boolean hasFields(AdaptiveRecord ar) {
return !ar.metaInfo().getterFieldSpecs.isEmpty();
}
public static Set<String> keySet(AdaptiveRecord ar) {
Map<String, Object> kv = ar.internalMap();
if (!hasFields(ar)) {
return kv.keySet();
}
Set<String> set = new HashSet<String>(ar.metaInfo().getterFieldSpecs.keySet());
set.addAll(kv.keySet());
return set;
}
public static Set<Map.Entry<String, Object>> entrySet(AdaptiveRecord ar, Osgl.Function<BeanSpec, Boolean> function) {
Map<String, Object> kv = ar.internalMap();
if (!hasFields(ar)) {
return kv.entrySet();
}
Set<Map.Entry<String, Object>> set = new HashSet<Map.Entry<String, Object>>(kv.entrySet());
AdaptiveRecord.MetaInfo metaInfo = ar.metaInfo();
boolean filter = null != function;
for (Map.Entry<String, $.Function> entry: metaInfo.fieldGetters.entrySet()) {
String fieldName = entry.getKey();
if ("kv".equals(fieldName)) {
continue;
}
if (filter) {
BeanSpec field = metaInfo.getterFieldSpecs.get(fieldName);
if (!function.apply(field)) {
continue;
}
}
$.Function getter = entry.getValue();
set.add(new C.Map.Entry(fieldName, getter.apply(ar)));
}
return set;
}
public static Map<String, Object> asMap(final AdaptiveRecord ar) {
final Map<String, Object> kv = ar.internalMap();
// TODO: should we check the field value on size, remove, containsXxx etc methods?
return new Map<String, Object>() {
@Override
public int size() {
return ar.size();
}
@Override
public boolean isEmpty() {
return ar.size() == 0;
}
@Override
public boolean containsKey(Object key) {
return kv.containsKey(key) || ar.metaInfo().getterFieldSpecs.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return kv.containsValue(value);
}
@Override
public Object get(Object key) {
$.Function getter = ar.metaInfo().fieldGetters.get(key);
return null != getter ? getter.apply(this) : kv.get((String)key);
}
@Override
public Object put(String key, Object value) {
$.Func2 setter = ar.metaInfo().fieldSetters.get(key);
if (null != setter) {
Object o = get(key);
setter.apply(this, value);
return o;
}
return kv.put(key, value);
}
@Override
public Object remove(Object key) {
$.Function getter = ar.metaInfo().fieldGetters.get(key);
if (null != getter) {
return null;
} else {
return kv.remove(key);
}
}
@Override
public void putAll(Map<? extends String, ?> m) {
ar.putValues((Map)m);
}
@Override
public void clear() {
kv.clear();
// TODO: should we clear field values?
}
@Override
public Set<String> keySet() {
return ar.keySet();
}
@Override
public Collection<Object> values() {
List<Object> list = new ArrayList<Object>();
list.addAll(kv.values());
for ($.Function getter : ar.metaInfo().fieldGetters.values()) {
list.add(getter.apply(this));
}
return list;
}
@Override
public Set<Entry<String, Object>> entrySet() {
return ar.entrySet();
}
};
}
public static AdaptiveRecord.MetaInfo generateMetaInfo(AdaptiveRecord ar) {
AdaptiveRecord.MetaInfo.Repository r = Act.appServicePluginManager().get(AdaptiveRecord.MetaInfo.Repository.class);
return r.get(ar.getClass(), new $.Transformer<Class<? extends AdaptiveRecord>, AdaptiveRecord.MetaInfo>() {
@Override
public AdaptiveRecord.MetaInfo transform(Class<? extends AdaptiveRecord> aClass) {
return new AdaptiveRecord.MetaInfo(aClass);
}
});
}
}
class MetaInfo {
private Class<? extends AdaptiveRecord> arClass;
public String className;
public Map<String, BeanSpec> getterFieldSpecs;
public Map<String, Class> getterFieldClasses;
public Map<String, BeanSpec> setterFieldSpecs;
public Map<String, Class> setterFieldClasses;
public Map<String, $.Function> fieldGetters;
public Map<String, $.Func2> fieldSetters;
public Map<String, $.Func2> fieldMergers;
public MetaInfo(Class<? extends AdaptiveRecord> clazz) {
this.className = clazz.getName();
this.arClass = clazz;
this.discoverProperties(clazz);
}
@Deprecated
public Class fieldClass(String fieldName) {
Class clazz = setterFieldClasses.get(fieldName);
return null == clazz ? getterFieldClasses.get(fieldName) : clazz;
}
public Class getterFieldClass(String fieldName) {
return getterFieldClasses.get(fieldName);
}
public Class setterFieldClass(String fieldName) {
return setterFieldClasses.get(fieldName);
}
public Type getterFieldType(String fieldName) {
BeanSpec spec = getterFieldSpecs.get(fieldName);
return null == spec ? null : spec.type();
}
public Type setterFieldType(String fieldName) {
BeanSpec spec = setterFieldSpecs.get(fieldName);
return null == spec ? null : spec.type();
}
private void discoverProperties(Class<? extends AdaptiveRecord> clazz) {
getterFieldSpecs = new HashMap<>();
getterFieldClasses = new HashMap<>();
setterFieldSpecs = new HashMap<>();
setterFieldClasses = new HashMap<>();
fieldGetters = new HashMap<>();
fieldSetters = new HashMap<>();
fieldMergers = new HashMap<>();
Injector injector = Act.app().injector();
for (final Method m : clazz.getMethods()) {
String name = propertyName(m);
if (S.blank(name)) {
continue;
} else {
name = S.lowerFirst(name);
if ("idAsStr".equals(name)) {
// special case for MorphiaModel
continue;
}
}
Class returnClass = m.getReturnType();
Type returnType = m.getGenericReturnType();
Class paramClass = null;
Type paramType = null;
Class[] params = m.getParameterTypes();
Type[] paramTypes = m.getGenericParameterTypes();
if (null != params && params.length == 1) {
paramClass = params[0];
paramType = paramTypes[0];
}
Class fieldClass = null == paramClass ? returnClass : paramClass;
Type fieldType = null == paramType ? returnType : paramType;
if (!(fieldType instanceof ParameterizedType)) {
fieldType = fieldClass;
}
if (null == paramClass) {
getterFieldSpecs.put(name, BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector));
getterFieldClasses.put(name, fieldClass);
} else {
BeanSpec existingSpec = setterFieldSpecs.get(name);
if (null != existingSpec) {
// we need to infer the type from field in this case
Field field = $.fieldOf(clazz, name, true);
if (null != field) {
setterFieldSpecs.put(name, BeanSpec.of(field, injector));
setterFieldClasses.put(name, field.getType());
} else {
if (fieldClass == Object.class) {
// ignore
} else if (existingSpec.rawType() == Object.class) {
setterFieldSpecs.put(name, BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector));
setterFieldClasses.put(name, fieldClass);
}
}
} else {
setterFieldSpecs.put(name, BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector));
setterFieldClasses.put(name, fieldClass);
}
}
if (null != paramClass) {
final String fieldName = name;
fieldSetters.put(name, new Osgl.Func2() {
@Override
public Object apply(Object host, Object value) throws NotAppliedException, Osgl.Break {
BeanSpec spec = setterFieldSpecs.get(fieldName);
if (null != value && !spec.isInstance(value)) {
if (value instanceof String) {
value = Act.app().resolverManager().resolve((String)value, spec.rawType());
} else if (value instanceof JSONObject) {
value = JSON.parseObject(((JSONObject) value).toJSONString(), spec.rawType());
}
}
$.invokeVirtual(host, m, value);
return null;
}
});
fieldMergers.put(name, new Osgl.Func2() {
@Override
public Object apply(Object host, Object value) throws NotAppliedException, Osgl.Break {
BeanSpec spec = setterFieldSpecs.get(fieldName);
if (null != value && !spec.isInstance(value)) {
if (value instanceof String) {
value = Act.app().resolverManager().resolve((String)value, spec.rawType());
}
}
$.Function getter = fieldGetters.get(fieldName);
if (null == getter) {
$.invokeVirtual(host, m, value);
return null;
}
Object value0 = getter.apply(host);
value = merge(value0, value);
$.invokeVirtual(host, m, value);
return null;
}
});
} else {
fieldGetters.put(name, new Osgl.F1() {
@Override
public Object apply(Object host) throws NotAppliedException, Osgl.Break {
try {
return m.invoke(host);
} catch (InvocationTargetException e) {
throw E.unexpected(e.getTargetException());
} catch (IllegalAccessException e) {
throw E.unexpected(e);
}
}
});
}
}
}
private String propertyName(Method m) {
String name = m.getName();
if ("getClass".equals(name)) {
return null;
}
Type[] paramTypes = m.getGenericParameterTypes();
if (name.startsWith("set") && void.class == m.getReturnType() && null != paramTypes && paramTypes.length == 1) {
return name.substring(3);
}
boolean isGet = name.startsWith("get");
boolean isIs = name.startsWith("is");
if ((isGet || isIs) && void.class != m.getReturnType() && (null == paramTypes || paramTypes.length == 0)) {
return isGet ? name.substring(3) : name.substring(2);
}
return null;
}
public static Object merge(Object to, Object from) {
if (null == to) {
return from;
}
if (null == from) {
return to;
}
if (canBeMerged(to.getClass())) {
return _merge(to, from);
}
return from;
}
private static Object _merge(Object to, Object from) {
if (to instanceof ValueObject) {
if (from instanceof ValueObject) {
return ValueObject.of(merge(((ValueObject) to).value(), ((ValueObject) from).value()));
}
return ValueObject.of(merge(((ValueObject) to).value(), from));
}
if (to instanceof AdaptiveRecord) {
AdaptiveRecord ar = (AdaptiveRecord) to;
return mergeIntoAdaptiveRecord(ar, from);
}
if (to instanceof Map) {
Map map = (Map) to;
return mergeIntoMap(map, from);
}
if (to instanceof Set) {
Set set = (Set) to;
return mergeIntoSet(set, from);
}
if (to instanceof List) {
List list = (List) to;
return mergeIntoList(list, from);
}
if (to.getClass().isArray()) {
List list = new ArrayList();
int len = Array.getLength(to);
for (int i = 0; i < len; ++i) {
list.add(Array.get(to, i));
}
List list1 = mergeIntoList(list, from);
int sz1 = list1.size();
Object a1 = Array.newInstance(to.getClass().getComponentType(), sz1);
for (int i = 0; i < sz1; ++i) {
Array.set(a1, i, list1.get(i));
}
return a1;
}
return mergeIntoPojo(to, from);
}
private static String getterName(Field field) {
boolean isBoolean = field.getType() == Boolean.class || field.getType() == boolean.class;
return (isBoolean ? "is" : "get") + S.capFirst(field.getName());
}
private static String setterName(Field field) {
return "set" + S.capFirst(field.getName());
}
private static boolean canBeMerged(Class<?> c) {
return !($.isSimpleType(c) || isDateType(c));
}
private static boolean isDateType(Class<?> c) {
String name = c.getSimpleName();
return (name.endsWith("Date") || name.endsWith("DateTime") || name.endsWith("Calendar"));
}
private static AdaptiveRecord mergeIntoAdaptiveRecord(AdaptiveRecord ar, Object value) {
if (value instanceof Map) {
return mergeMapIntoAdaptiveRecord(ar, (Map) value);
}
if (value instanceof AdaptiveRecord) {
return mergeMapIntoAdaptiveRecord(ar, ((AdaptiveRecord) value).asMap());
}
List<Field> fields = $.fieldsOf(value.getClass(), true);
for (Field f : fields) {
f.setAccessible(true);
try {
String fn = f.getName();
Object fv = f.get(value);
Object o0 = ar.getValue(fn);
ar.putValue(fn, merge(o0, fv));
} catch (IllegalAccessException e) {
throw E.unexpected(e, "error merging into adaptive record");
}
}
return ar;
}
private static AdaptiveRecord mergeMapIntoAdaptiveRecord(AdaptiveRecord ar, Map map) {
return ar.putValues(map);
}
private static Map mergeIntoMap(Map map, Object value) {
if (value instanceof Map) {
return mergeMapIntoMap(map, (Map) value);
}
if (value instanceof AdaptiveRecord) {
return mergeMapIntoMap(map, ((AdaptiveRecord) value).asMap());
}
Map retval = new HashMap(map);
List<Field> fields = $.fieldsOf(value.getClass(), true);
for (Field f : fields) {
f.setAccessible(true);
try {
String fn = f.getName();
Object fv = f.get(value);
Object o0 = map.get(fn);
retval.put(fn, merge(o0, fv));
} catch (IllegalAccessException e) {
throw E.unexpected(e, "error merging into adaptive record");
}
}
return retval;
}
private static Map mergeMapIntoMap(Map m0, Map<?, ?> m1) {
Map retval = (Map) Act.injector().get(m0.getClass());
retval.putAll(m0);
for (Map.Entry entry : m1.entrySet()) {
Object k = entry.getKey();
Object v = entry.getValue();
retval.put(k, merge(m0.get(k), v));
}
return retval;
}
private static Set mergeIntoSet(Set set, Object value) {
if (value instanceof Collection) {
return mergeCollectionIntoSet(set, (Collection) value);
}
if (value.getClass().isArray()) {
List list = new ArrayList();
int len = Array.getLength(value);
for (int i = 0; i < len; ++i) {
list.add(Array.get(value, i));
}
return mergeCollectionIntoSet(set, list);
}
throw new IllegalArgumentException("Cannot merge " + value.getClass() + " into Set");
}
private static Set mergeCollectionIntoSet(Set set, Collection col) {
Set set1 = (Set) Act.injector().get(set.getClass());
set1.addAll(col);
return set1;
}
private static List mergeIntoList(List list, Object value) {
if (value instanceof Set) {
return mergeSetIntoList(list, (Set) value);
}
if (value instanceof List) {
return mergeListIntoList(list, (List) value);
}
if (value.getClass().isArray()) {
List list0 = new ArrayList();
int len = Array.getLength(value);
for (int i = 0; i < len; ++i) {
list0.add(Array.get(value, i));
}
return mergeListIntoList(list, list0);
}
throw new IllegalArgumentException("Cannot merge " + value.getClass() + " into List");
}
private static List mergeSetIntoList(List list, Set set) {
List retval = (List) Act.injector().get(list.getClass());
retval.addAll(list);
retval.addAll(set);
return retval;
}
private static List mergeListIntoList(List to, List from) {
List retval = (List) Act.injector().get(to.getClass());
int szTo = to.size();
int szFrom = from.size();
int szMin = Math.min(szTo, szFrom);
for (int i = 0; i < szMin; ++i) {
Object o0 = to.get(i);
Object o1 = from.get(i);
retval.add(merge(o0, o1));
}
if (szTo > szFrom) {
for (int i = szMin; i < szTo; ++i) {
retval.add(to.get(i));
}
} else if (szFrom > szTo) {
for (int i = szMin; i < szFrom; ++i) {
retval.add(from.get(i));
}
}
return retval;
}
private static Object mergeIntoPojo(Object o0, Object o1) {
if (o1 instanceof Map) {
return mergeMapIntoPojo(o0, (Map) o1);
}
if (o1 instanceof AdaptiveRecord) {
return mergeMapIntoPojo(o0, ((AdaptiveRecord) o1).asMap());
}
if (o1 instanceof Collection || o1.getClass().isArray()) {
throw E.unexpected("cannot merge " + o1.getClass() + " into " + o0.getClass());
}
Class<?> c0 = o0.getClass();
List<Field> fields = $.fieldsOf(o1.getClass(), true);
for (Field f : fields) {
f.setAccessible(true);
try {
String fn = f.getName();
Field f0 = $.fieldOf(c0, fn);
if (null == f0) {
continue;
}
Object fv = f.get(o1);
f0.setAccessible(true);
Object fv0 = merge(f0.get(o0), fv);
f0.set(o0, fv0);
} catch (IllegalAccessException e) {
throw E.unexpected(e, "error merging into POJO");
}
}
return o0;
}
private static Object mergeMapIntoPojo(Object o0, Map map) {
List<Field> fields = $.fieldsOf(o0.getClass(), true);
for (Field f : fields) {
String fn = f.getName();
if (map.containsKey(fn)) {
f.setAccessible(true);
try {
Object v = f.get(o0);
f.set(o0, merge(v, map.get(fn)));
} catch (IllegalAccessException e) {
throw E.unexpected(e, "error merging into POJO");
}
}
}
return o0;
}
public static class Repository extends AppServicePlugin {
@Override
protected void applyTo(App app) {
}
private ConcurrentMap<Class<?>, MetaInfo> map = new ConcurrentHashMap<>();
public MetaInfo get(Class<? extends AdaptiveRecord> clazz, $.Function<Class<? extends AdaptiveRecord>, MetaInfo> factory) {
MetaInfo info = map.get(clazz);
if (null == info) {
info = factory.apply(clazz);
map.putIfAbsent(clazz, info);
}
return info;
}
}
}
}