package xapi.jre.model; import xapi.collect.X_Collect; import xapi.collect.api.ClassTo; import xapi.collect.api.Dictionary; import xapi.collect.api.IntTo; import xapi.except.NotYetImplemented; import xapi.fu.Out1; import xapi.model.api.Model; import xapi.model.api.ModelKey; import xapi.model.api.ModelManifest; import xapi.model.api.ModelManifest.MethodData; import xapi.model.api.ModelMethodType; import xapi.model.api.ModelModule; import xapi.model.impl.AbstractModel; import xapi.model.impl.AbstractModelService; import xapi.model.impl.ModelUtil; import xapi.reflect.X_Reflect; import xapi.util.X_Debug; import xapi.util.api.ConvertsTwoValues; import xapi.util.api.ConvertsValue; import xapi.util.api.ProvidesValue; import xapi.util.api.RemovalHandler; import static xapi.util.impl.PairBuilder.entryOf; import javax.inject.Provider; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Iterator; import java.util.Map.Entry; import java.util.Objects; import java.util.function.Supplier; /** * @author James X. Nelson (james@wetheinter.net) * Created on 28/10/15. */ public abstract class AbstractJreModelService extends AbstractModelService { private static ThreadLocal<ModelModule> currentModule = new ThreadLocal<>(); public static RemovalHandler registerModule(final ModelModule module) { currentModule.set(module); return new RemovalHandler() { @Override public void remove() { currentModule.remove(); } }; } public static ProvidesValue<RemovalHandler> captureScope() { final ModelModule module = currentModule.get(); return new ProvidesValue<RemovalHandler>() { @Override public RemovalHandler get() { final ModelModule was = currentModule.get(); currentModule.set(module); return new RemovalHandler() { @Override public void remove() { if (module == currentModule.get()) { if (was == null) { currentModule.remove(); } else { currentModule.set(was); } } } }; } }; } /** * This method, by default, is backed by a ThreadLocal which you can set statically via {@link #registerModule(ModelModule)}. * * Although you can override this to return something else, please be aware that this service is a global application singleton, * so whatever you return is going to be supplied to potentially many threads at once. */ public ModelModule getModelModule() { return currentModule.get(); } /** * @author James X. Nelson (james@wetheinter.net, @james) * */ public class ModelInvocationHandler implements InvocationHandler { final ModelManifest manifest; final Dictionary<String, Object> values; ModelKey key; public ModelInvocationHandler(final Class<? extends Model> modelClass) { this(modelClass, X_Collect.newDictionary()); } public ModelInvocationHandler(final Class<? extends Model> modelClass, final Dictionary<String, Object> values) { this(getOrMakeModelManifest(modelClass), values); } public ModelInvocationHandler(final ModelManifest manifest, final Dictionary<String, Object> values) { this.manifest = manifest; this.values = values; } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { switch (method.getName()) { case "setProperty": if (method.getParameterTypes().length == 2) { values.setValue((String)args[0], args[1]); return proxy; } case "setKey": key = (ModelKey) args[0]; return proxy; case "removeProperty": if (method.getParameterTypes().length == 1) { values.removeValue((String)args[0]); return this; } case "getType": return manifest.getType(); case "getPropertyType": final String name = (String) args[0]; return manifest.getMethodData(name).getType(); case "getPropertyNames": return manifest.getPropertyNames(); case "getProperties": final String[] properties = manifest.getPropertyNames(); return new Itr(properties, values, getDefaultValueProvider(manifest)); case "getKey": return key; case "clear": values.clearValues(); return proxy; case "getProperty": Object result = null; if (method.getParameterTypes().length == 1) { // no default value result = values.getValue((String)args[0]); } else if (method.getParameterTypes().length == 2) { // there is a default value... result = values.getValue((String)args[0]); if (result == null) { if (method.getParameterTypes()[1] == Out1.class) { result = ((Out1)args[1]).out1(); } else { result = args[1]; } } } if (result == null) { return getDefaultValueProvider(manifest).convert((String)args[0]); } return result; case "hashCode": return AbstractModel.hashCodeForModel((Model) proxy); case "equals": return AbstractModel.equalsForModel((Model)proxy, args[0]); case "toString": return AbstractModel.toStringForModel((Model)proxy); } if (method.getDeclaringClass() == Model.class) { throw new UnsupportedOperationException("Unhandled xapi.model.api.Model method: "+method.toGenericString()); } if (method.isDefault()) { Method original = manifest.getModelType().getMethod(method.getName(), method.getParameterTypes()); return X_Reflect.invokeDefaultMethod(original.getDeclaringClass(), method.getName(), method.getParameterTypes(), proxy, args); } final MethodData property = manifest.getMethodData(method.getName()); final ModelMethodType methodType = property.getMethodType(method.getName()); if (methodType == null) { throw new UnsupportedOperationException("Unhandled model method: "+method.toGenericString()); } switch (methodType) { case GET: Object result = values.getValue(property.getName()); if (result == null) { if (method.getParameterTypes().length > 1) { if (args[1] instanceof Provider && !Provider.class.isAssignableFrom(property.getType())) { return ((Provider)args[1]).get(); } try { // Supplier class may not be on classpath for projects < java 8 if (args[1] instanceof Supplier && !Supplier.class.isAssignableFrom(property.getType())) { return ((Provider) args[1]).get(); } } catch (Throwable ignored){} return args[1]; } return getDefaultValueProvider(manifest).convert(property.getName()); } return result; case SET: boolean isFluent = ModelUtil.isFluent(method); result = null; if (method.getParameters().length == 2) { // This is a check-and-set final Object previous = values.getValue(property.getName()); final boolean returnsBoolean = method.getReturnType() == boolean.class; if (Objects.equals(previous, args[0])) { result = values.setValue(property.getName(), args[1]); if (returnsBoolean) { return true; } } if (returnsBoolean) { return false; } } else { result = values.setValue(property.getName(), args[0]); } if (isFluent) { return proxy; } if (method.getReturnType() == null || method.getReturnType() == void.class) { return null; } return result; case ADD: case ADD_ALL: case CLEAR: throw new NotYetImplemented("Method "+method.toGenericString()+" of "+ method.getDeclaringClass()+" is not yet implemented"); case REMOVE: result = null; isFluent = ModelUtil.isFluent(method); if (method.getParameters().length == 2) { // This is a check-and-remove final Object previous = values.getValue(property.getName()); final boolean returnsBoolean = method.getReturnType() == boolean.class; if (Objects.equals(previous, args[0])) { result = values.removeValue(property.getName()); if (returnsBoolean) { return true; } } if (returnsBoolean) { return false; } } else { result = values.removeValue(property.getName()); } if (isFluent) { return proxy; } if (method.getReturnType() == null || method.getReturnType() == void.class) { return null; } return result; } return null; } } protected ConvertsValue<String,Object> getDefaultValueProvider(final ModelManifest manifest) { return new ConvertsValue<String, Object>() { @Override public Object convert(final String from) { final MethodData typeData = manifest.getMethodData(from); if (typeData.getType().isPrimitive()) { return AbstractModel.getPrimitiveValue(typeData.getType()); } else if (typeData.getType().isArray()) { return Array.newInstance(typeData.getType().getComponentType(), 0); } else { maybeInitDefaults(defaultValueProvider); // Handle other default values final ConvertsTwoValues<ModelManifest, MethodData, Object> provider = defaultValueProvider.get(typeData.getType()); if (provider != null) { return provider.convert(manifest, typeData); } } return null; } }; } protected void maybeInitDefaults(ClassTo<ConvertsTwoValues<ModelManifest, MethodData, Object>> defaultValueProvider) { if (defaultValueProvider.isEmpty()) { defaultValueProvider.put(IntTo.class, new ConvertsTwoValues<ModelManifest, MethodData, Object>() { @Override public Object convert(ModelManifest manifest, MethodData method) { final Class[] types = method.getTypeParams(); assert types.length == 1 : "Expected exactly one type argument for IntTo instances"; return X_Collect.newList(types[0]); } }); } } private final class Itr implements Iterable<Entry<String, Object>> { private final String[] keys; private final Dictionary<String, Object> map; private final ConvertsValue<String, Object> defaultValueProvider; private Itr(final String[] keys, final Dictionary<String, Object> map, final ConvertsValue<String, Object> defaultValueProvider) { this.keys = keys; this.map = map; this.defaultValueProvider = defaultValueProvider; } @Override public Iterator<Entry<String, Object>> iterator() { return new Iterator<Entry<String,Object>>() { int pos = 0; @Override public boolean hasNext() { return pos < keys.length; } @Override public Entry<String, Object> next() { final String key = keys[pos]; Object value = map.getValue(key); if (value == null) { value = defaultValueProvider.convert(key); } return entryOf(key, value); } }; } } private final ClassTo<ProvidesValue<Object>> modelFactories; private final ClassTo<ConvertsTwoValues<ModelManifest, MethodData, Object>> defaultValueProvider; private final ClassTo<ModelManifest> modelManifests; @SuppressWarnings("unchecked") protected AbstractJreModelService() { modelManifests = X_Collect.newClassMap(ModelManifest.class); modelFactories = X_Collect.newClassMap(Class.class.cast(ProvidesValue.class)); defaultValueProvider = X_Collect.newClassMap(Class.class.cast(ConvertsTwoValues.class)); } /** * @see xapi.model.impl.AbstractModelService#create(java.lang.Class) */ @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <M extends Model> M create(final Class<M> key) { ProvidesValue factory = modelFactories.get(key); if (factory == null) { factory = createModelFactory(key); modelFactories.put(key, factory); } return (M)factory.get(); } protected <M extends Model> ProvidesValue<M> createModelFactory(final Class<M> modelClass) { // TODO: check for an X_Inject interface definition and prefer that, if possible... if (modelClass.isInterface()) { return new ProvidesValue<M>() { @Override @SuppressWarnings("unchecked") public M get() { return (M) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class<?>[]{modelClass}, newInvocationHandler(modelClass) ); } }; } else { // The type is not an interface. We are boned. throw new NotYetImplemented("Unable to generate class provider for " + modelClass+"; " + "only interface types are supported at this time"); } } protected InvocationHandler newInvocationHandler(final Class<? extends Model> modelClass) { return new ModelInvocationHandler(modelClass); } protected ModelManifest getOrMakeModelManifest(final Class<? extends Model> cls) { final ModelModule module = getModelModule(); if (module != null) { final String typeName = getTypeName(cls); return module.getManifest(typeName); } ModelManifest manifest = modelManifests.get(cls); if (manifest == null) { manifest = ModelUtil.createManifest(cls); modelManifests.put(cls, manifest); } return manifest; } @Override @SuppressWarnings("unchecked") public <M extends Model> Class<M> typeToClass(final String kind) { return (Class<M>) typeNameToClass.get(kind); } protected void rethrow(Exception e) { X_Debug.rethrow(e); } }