/*
* Copyright 2008-2011 the original author or authors.
*
* 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.nominanuda.zen.obj.wrap;
import android.util.Pair;
import com.nominanuda.zen.common.Check;
import com.nominanuda.zen.obj.Arr;
import com.nominanuda.zen.obj.JsonType;
import com.nominanuda.zen.obj.Obj;
import com.nominanuda.zen.obj.Stru;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import static com.nominanuda.zen.obj.wrap.Wrap.WF;
import static java.util.Arrays.asList;
class WrapperInvocationHandler implements InvocationHandler {
private final JSONObject o;
private final Class<?> role;
private final Set<Method> roleMethods = new HashSet<Method>();
private static final HashSet<Method> nonRoleMethods = new HashSet<Method>();
static {
nonRoleMethods.addAll(asList(Object.class.getDeclaredMethods()));
nonRoleMethods.addAll(asList(ObjWrapper.class.getDeclaredMethods()));
nonRoleMethods.addAll(asList(Obj.class.getDeclaredMethods()));
nonRoleMethods.addAll(asList(Stru.class.getDeclaredMethods()));
nonRoleMethods.addAll(asList(Map.class.getDeclaredMethods()));
nonRoleMethods.addAll(asList(Iterable.class.getDeclaredMethods()));
}
public WrapperInvocationHandler(JSONObject o, Class<?> role) {
this.role = role;
this.o = o != null ? o : new Obj();
for (Method m : role.getMethods()) {
if (!nonRoleMethods.contains(m)) {
roleMethods.add(m);
}
}
}
@SuppressWarnings("unchecked")
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
try {
if ("unwrap".equals(name)) {
return o;
} else if ("as".equals(name)) {
Object enhancementMethods = args[1];
InvocationHandler eh = new EnhancedInvocationHandler(enhancementMethods, role, proxy);
return Proxy.newProxyInstance(enhancementMethods.getClass().getClassLoader(), new Class[] { (Class<?>) args[0] }, eh);
} else if (roleMethods.contains(method)) {
Class<?> type = method.getReturnType();
int argsL = (args == null ? 0 : args.length);
if (argsL == 0) { // any getter
if (Collection.class.isAssignableFrom(type)) { // collection getter
JSONArray arr = o.optJSONArray(name);
if (arr != null) {
Collection<Object> coll = type.isInterface()
? new LinkedList<>()
: (Collection<Object>) type.newInstance();
Class<?> itemType = null;
try {
itemType = getCollectionReturnComponentType(method);
} catch (Exception e) {
// dynamic mode on
}
for (int i = 0, l = arr.length(); i < l; i++) {
Object val = arr.isNull(i) ? null : arr.opt(i); // as per issue https://issuetracker.google.com/issues/36924550
if (itemType == null && val == null) {
coll.add(null);
} else {
if (itemType == null) {
itemType = val.getClass();
}
coll.add(fromObjValue(val, itemType));
}
}
return coll;
} else {
return null;
}
} else if (Map.class.isAssignableFrom(type)) { // map getter
JSONObject obj = o.optJSONObject(name);
if (obj != null) {
if (type.isInterface()) {
type = LinkedHashMap.class;
}
Map<String, Object> map = (Map<String, Object>) type.newInstance();
Class<?> itemType = null;
try {
Pair<Class<?>, Class<?>> keyValTypes = getMapReturnComponentTypes(method);
itemType = keyValTypes.second;
} catch (Exception e) {
// dynamic mode on
}
Iterator<String> i = obj.keys();
while (i.hasNext()) {
String key = i.next();
Object val = obj.isNull(key) ? null : obj.opt(key); // as per issue https://issuetracker.google.com/issues/36924550
if (itemType == null && val == null) {
map.put(key, null);
} else {
if (itemType == null) {
itemType = val.getClass();
}
map.put(key, fromObjValue(val, itemType));
}
}
return map;
} else {
return null;
}
} else { // simple getter
return fromObjValue(o.isNull(name) ? null : o.opt(name), type); // as per issue https://issuetracker.google.com/issues/36924550
}
} else if (argsL == 1) { // setter
o.put(name, toObjValue(args[0]));
return proxy;
} else { // bail out
throw new RuntimeException();
}
} else if ("equals".equals(name) && args.length == 1) { // allows [Proxy].equals([Proxy])
return args[0] == proxy;
} else {
return method.invoke(o, args);
}
} catch (InvocationTargetException e) {
throw Check.ifNull((Exception) e.getCause(), e);
}
}
private Class<?> getCollectionReturnComponentType(Method method) throws ClassNotFoundException {
Cls cls = method.getAnnotation(Cls.class);
if (cls != null) {
return cls.value();
}
Type t = method.getGenericReturnType();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] actualTypeArgs = pt.getActualTypeArguments();
if (actualTypeArgs != null && actualTypeArgs.length == 1) {
try {
String name = actualTypeArgs[0].toString();
return Class.forName(name.substring(name.indexOf(' ') + 1)); // remove "interface " in front of name
} catch (ClassNotFoundException e) {
throw e;
}
}
}
throw new ClassNotFoundException("could not determine generic return type for method " + method.toString());
}
private Pair<Class<?>, Class<?>> getMapReturnComponentTypes(Method method) throws ClassNotFoundException {
Cls cls = method.getAnnotation(Cls.class);
if (cls != null) {
return new Pair<Class<?>, Class<?>>(String.class, cls.value());
}
Type t = method.getGenericReturnType();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] actualTypeArgs = pt.getActualTypeArguments();
if (actualTypeArgs != null && actualTypeArgs.length == 2) {
try {
String name0 = actualTypeArgs[0].toString();
String name1 = actualTypeArgs[1].toString();
return new Pair<>(
Class.forName(name0.substring(name0.indexOf(' ') + 1)), // remove "interface " in front of name
Class.forName(name1.substring(name1.indexOf(' ') + 1)) // remove "interface " in front of name
);
} catch (ClassNotFoundException e) {
throw e;
}
}
}
throw new ClassNotFoundException("could not determine generic return type for method " + method.toString());
}
private Object fromObjValue(Object v, Class<?> type) {
if (null == v) {
return null;
} else if (Boolean.TYPE.equals(type)) { // expected boolean
return Boolean.TRUE.equals((Boolean) v); // force true/false (also when v == null)
} else if (JsonType.isNullablePrimitive(v)) {
if (Double.class.equals(type) || double.class.equals(type)) {
return ((Number) v).doubleValue();
} else if (Float.class.equals(type) || float.class.equals(type)) {
return ((Number) v).floatValue();
} else if (Integer.class.equals(type) || int.class.equals(type)) {
return ((Number) v).intValue();
} else if (Long.class.equals(type) || long.class.equals(type)) {
return ((Number) v).longValue();
} else {
return v;
}
} else if (JsonType.isJSONObject(v)) {
JSONObject o = (JSONObject) v;
WrapType wrapType = type.getAnnotation(WrapType.class);
if (wrapType != null) {
return createBuilder(wrapType, type).apply(o);
} else if (WrapperItemFactory.class.isAssignableFrom(type)) {
try {
return findWrapMethod(type).invoke(null, v);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new RuntimeException(e);
}
} else if (ObjWrapper.class.isAssignableFrom(type)) { // sub object
return WF.wrap(o, type);
} else if (Obj.class.isAssignableFrom(type)) { // obj
return Obj.make(o);
} else {
throw new IllegalArgumentException("cannot convert value:" + v + " to type:" + type.getName());
}
} else {
throw new IllegalArgumentException("cannot convert value:" + v + " to type:" + type.getName());
}
}
private static final Map<Class<?>, Builder> BUILDERS_CACHE = new HashMap<>();
private class Builder {
private final String field;
private final Map<String, Class<?>> typeMap = new HashMap<>();
private Builder(WrapType wrapType, Class<?> type) {
field = wrapType.field();
String[] values = wrapType.values();
Class<?>[] types = wrapType.types();
for (int i = 0; i < values.length; i++) {
typeMap.put(values[i], types[i]);
}
}
private Object apply(JSONObject o) {
return WF.wrap(o, typeMap.get(o.opt(field)));
}
}
private Builder createBuilder(WrapType wrapType, Class<?> type) {
Builder builder = BUILDERS_CACHE.get(type);
if (builder == null) {
builder = new Builder(wrapType, type);
BUILDERS_CACHE.put(type, builder);
}
return builder;
}
private Method findWrapMethod(Class<?> type) throws NoSuchMethodException {
try {
return type.getMethod("wrap", Obj.class);
} catch (NoSuchMethodException e) {
for (Class<?> ancestor : type.getInterfaces()) {
if (WrapperItemFactory.class.isAssignableFrom(ancestor)) {
try {
return findWrapMethod(ancestor);
} catch (NoSuchMethodException e1) {
}
}
}
throw new NoSuchMethodException();
}
}
@SuppressWarnings("unchecked")
private Object toObjValue(Object v) throws JSONException {
if (v instanceof Collection) {
Arr arr = new Arr();
for (Object o : (Collection<Object>) v) {
arr.put(toObjValue(o));
}
return arr;
} else if (v instanceof Map) {
Obj obj = new Obj();
for (Entry<String, Object> entry : ((Map<String, Object>) v).entrySet()) {
obj.put(entry.getKey(), toObjValue(entry.getValue()));
}
return obj;
} else if (v instanceof ObjWrapper) { // sub object
return ((ObjWrapper) v).unwrap();
}
return v;
}
}