xapi.jre.model.ModelServiceJre Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xapi-dev Show documentation
Show all versions of xapi-dev Show documentation
Everything needed to run a comprehensive dev environment.
Just type X_ and pick a service from autocomplete;
new dev modules will be added as they are built.
The only dev service not included in the uber jar is xapi-dev-maven,
as it includes all runtime dependencies of maven, adding ~4 seconds to build time,
and 6 megabytes to the final output jar size (without xapi-dev-maven, it's ~1MB).
The newest version!
/**
*
*/
package xapi.jre.model;
import static xapi.util.impl.PairBuilder.entryOf;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
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 xapi.annotation.inject.SingletonDefault;
import xapi.collect.X_Collect;
import xapi.collect.api.ClassTo;
import xapi.collect.api.Dictionary;
import xapi.dev.source.CharBuffer;
import xapi.except.NotYetImplemented;
import xapi.io.X_IO;
import xapi.log.X_Log;
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.api.ModelNotFoundException;
import xapi.model.impl.AbstractModel;
import xapi.model.impl.AbstractModelService;
import xapi.model.impl.ModelUtil;
import xapi.model.service.ModelService;
import xapi.platform.JrePlatform;
import xapi.source.impl.StringCharIterator;
import xapi.time.X_Time;
import xapi.util.api.ConvertsValue;
import xapi.util.api.ErrorHandler;
import xapi.util.api.ProvidesValue;
import xapi.util.api.RemovalHandler;
import xapi.util.api.SuccessHandler;
/**
* @author James X. Nelson ([email protected], @james)
*
*/
@JrePlatform
@SingletonDefault(implFor=ModelService.class)
public class ModelServiceJre extends AbstractModelService {
private static ThreadLocal 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 captureScope() {
final ModelModule module = currentModule.get();
return new ProvidesValue() {
@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);
}
}
}
};
}
};
}
/**
* @author James X. Nelson ([email protected], @james)
*
*/
public class ModelInvocationHandler implements InvocationHandler {
final ModelManifest manifest;
final Dictionary values;
ModelKey key;
public ModelInvocationHandler(final Class modelClass) {
this(modelClass, X_Collect.newDictionary());
}
public ModelInvocationHandler(final Class modelClass, final Dictionary values) {
this(getOrMakeModelManifest(modelClass), values);
}
public ModelInvocationHandler(final ModelManifest manifest, final Dictionary 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, new ConvertsValue() {
@Override
public Object convert(final String from) {
final MethodData typeData = manifest.getMethodData(from);
if (typeData.getType().isPrimitive()) {
return AbstractModel.getPrimitiveValue(typeData.getType());
}
return null;
}
});
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) {
result = args[1];
}
}
if (result == null) {
final MethodData typeData = manifest.getMethodData((String)args[0]);
return AbstractModel.getPrimitiveValue(typeData.getType());
}
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());
}
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 && method.getParameterTypes().length > 1) {
return args[1];
}
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;
}
}
private final class Itr implements Iterable> {
private final String[] keys;
private final Dictionary map;
private final ConvertsValue defaultValueProvider;
private Itr(final String[] keys, final Dictionary map, final ConvertsValue defaultValueProvider) {
this.keys = keys;
this.map = map;
this.defaultValueProvider = defaultValueProvider;
}
@Override
public Iterator> iterator() {
return new Iterator>() {
int pos = 0;
@Override
public boolean hasNext() {
return pos < keys.length;
}
@Override
public Entry next() {
final String key = keys[pos];
Object value = map.getValue(key);
if (value == null) {
value = defaultValueProvider.convert(key);
}
return entryOf(key, value);
}
};
}
}
@SuppressWarnings("unchecked")
private final ClassTo> modelFactories = X_Collect.newClassMap(
Class.class.cast(ProvidesValue.class)
);
private final ClassTo modelManifests = X_Collect.newClassMap(ModelManifest.class);
private File root;
@Override
@SuppressWarnings({
"unchecked", "rawtypes"
})
protected void doPersist(final String type, final M model, final SuccessHandler callback) {
// For simplicity sake, lets use the file system to save our models.
ModelKey key = model.getKey();
if (key == null) {
key = newKey(null, type);
model.setKey(key);
}
File f;
try {
f = getFilesystemRoot();
} catch (final IOException e) {
X_Log.error(getClass(), "Unable to load filesystem root", e);
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(e);
}
return;
}
if (key.getNamespace().length() > 0) {
f = new File(f, key.getNamespace());
}
f = new File(f, key.getKind());
f.mkdirs();
if (key.getId() == null) {
// No id; generate one
try {
f = generateFile(f);
} catch (final IOException e) {
X_Log.error(getClass(), "Unable to save model "+model, e);
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(e);
}
return;
}
key.setId(f.getName());
} else {
f = new File(f, key.getId());
}
final CharBuffer serialized = serialize(type, model);
final File file = f;
X_Time.runLater(new Runnable() {
@Override
public void run() {
try {
if (file.exists()) {
file.delete();
}
final FileOutputStream result = new FileOutputStream(file);
X_IO.drain(result, X_IO.toStreamUtf8(serialized.toString()));
callback.onSuccess(model);
X_Log.info(getClass(), "Saved model to ",file);
} catch (final IOException e) {
X_Log.error(getClass(), "Unable to save model "+model, e);
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(e);
}
}
}
});
}
@SuppressWarnings("unchecked")
@Override
public void load(final Class modelClass, final ModelKey modelKey, final SuccessHandler callback) {
File f;
try {
f = getFilesystemRoot();
} catch (final IOException e) {
X_Log.error(getClass(), "Unable to load filesystem root", e);
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(e);
}
return;
}
if (modelKey.getNamespace().length() > 0) {
f = new File(f, modelKey.getNamespace());
}
f = new File(f, modelKey.getKind());
f = new File(f, modelKey.getId());
if (!f.exists()) {
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(new ModelNotFoundException(modelKey));
return;
}
} else {
final File file = f;
final ProvidesValue scope = captureScope();
X_Time.runLater(new Runnable() {
@Override
public void run() {
final RemovalHandler handler = scope.get();
String result;
try {
result = X_IO.toStringUtf8(new FileInputStream(file));
final M model = deserialize(modelClass, new StringCharIterator(result));
callback.onSuccess(model);
} catch (final Exception e) {
X_Log.error(getClass(), "Unable to load file for model "+modelKey);
if (callback instanceof ErrorHandler) {
((ErrorHandler) callback).onError(new ModelNotFoundException(modelKey));
}
} finally {
handler.remove();
}
}
});
}
}
/**
* @param f
* @return
* @throws IOException
*/
private synchronized File generateFile(File f) throws IOException {
final int size = f.listFiles().length;
f = new File(f, Integer.toString(size));
f.createNewFile();
return f;
}
/**
* @return
* @throws IOException
*/
private File getFilesystemRoot() throws IOException {
if (root == null) {
File temp;
temp = File.createTempFile("ephemeral", "models");
root = new File(temp.getParentFile(), "models");
temp.delete();
root.mkdirs();
}
return root;
}
/**
* @see xapi.model.impl.AbstractModelService#create(java.lang.Class)
*/
@SuppressWarnings({
"unchecked", "rawtypes"
})
@Override
public M create(final Class key) {
ProvidesValue factory = modelFactories.get(key);
if (factory == null) {
factory = createModelFactory(key);
modelFactories.put(key, factory);
}
return (M)factory.get();
}
protected ProvidesValue createModelFactory(final Class modelClass) {
// TODO: check for an X_Inject interface definition and prefer that, if possible...
if (modelClass.isInterface()) {
return new ProvidesValue() {
@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 modelClass) {
return new ModelInvocationHandler(modelClass);
}
protected ModelManifest getOrMakeModelManifest(final Class cls) {
final ModelModule module = currentModule.get();
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;
}
}