/** * */ package xapi.jre.model; import xapi.annotation.inject.SingletonDefault; import xapi.dev.source.CharBuffer; import xapi.io.X_IO; import xapi.log.X_Log; import xapi.model.api.Model; import xapi.model.api.ModelKey; import xapi.model.api.ModelNotFoundException; import xapi.model.api.ModelQuery; import xapi.model.api.ModelQuery.QueryParameter; import xapi.model.api.ModelQueryResult; import xapi.model.service.ModelService; import xapi.platform.JrePlatform; import xapi.source.impl.StringCharIterator; import xapi.time.X_Time; import xapi.util.api.ErrorHandler; import xapi.util.api.ProvidesValue; import xapi.util.api.RemovalHandler; import xapi.util.api.SuccessHandler; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; /** * @author James X. Nelson (james@wetheinter.net, @james) * */ @JrePlatform @SingletonDefault(implFor=ModelService.class) public class ModelServiceJre extends AbstractJreModelService { private File root; @Override @SuppressWarnings({ "unchecked", "rawtypes" }) protected <M extends Model> void doPersist(final String type, final M model, final SuccessHandler<M> 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 = getRoot(callback); if (f == null) { 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); } else { rethrow(e); } return; } key.setId(f.getName()); } else { f = new File(f, key.getId()); } final CharBuffer serialized = serialize(type, model); final File file = f; final Runnable finish = 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); } else { rethrow(e); } } } }; if (isAsync()) { X_Time.runLater(finish); } else { finish.run();; } } protected boolean isAsync() { return false; } @SuppressWarnings("unchecked") @Override public <M extends Model> void load(final Class<M> modelClass, final ModelKey modelKey, final SuccessHandler<M> callback) { File f = getRoot(callback); if (f == null) { 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<RemovalHandler> 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)); } else { rethrow(e); } } finally { handler.remove(); } } }); } } @SuppressWarnings({ "unchecked", "rawtypes" }) public File getRoot(final SuccessHandler<?> callback) { try { return getFilesystemRoot(); } catch (final IOException e) { X_Log.error(getClass(), "Unable to load filesystem root", e); if (callback instanceof ErrorHandler) { ((ErrorHandler) callback).onError(e); } return null; } } @Override public <M extends Model> void query(final Class<M> modelClass, final ModelQuery<M> query, final SuccessHandler<ModelQueryResult<M>> callback) { for (final QueryParameter param : query.getParameters()) { throw new UnsupportedOperationException("The basic, file-backed "+getClass().getName()+" does not support any complex queries"); } // The only query we will support is a parameterless "get all" query File f = getRoot(callback); if (query.getNamespace().length() > 0) { f = new File(f, query.getNamespace()); } final String typeName = getTypeName(modelClass); f = new File(f, typeName); File[] allFiles; if (query.getCursor() == null) { // Yes, listing all files is not going to be very performant; however, this implementation is // far too naive to be used for a production system. It is primarily a proof-of-concept that can // be usable for developing APIs against something that is simple to use and debug allFiles = f.listFiles(); } else { // If there is a cursor, we are continuing a query. allFiles = f.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.compareTo(query.getCursor()) > -1; } }); } final int size = Math.min(query.getPageSize(), allFiles.length); final ArrayList<File> files = new ArrayList<File>(size); for (int i = 0; i < size; i++) { files.add(allFiles[i]); } final ModelQueryResult<M> result = new ModelQueryResult<>(modelClass); if (size < allFiles.length) { result.setCursor(allFiles[size].getName()); } allFiles = null; final ProvidesValue<RemovalHandler> scope = captureScope(); X_Time.runLater(new Runnable() { @Override public void run() { final RemovalHandler handler = scope.get(); String fileResult; try { for (final File file : files) { fileResult = X_IO.toStringUtf8(new FileInputStream(file)); final M model = deserialize(modelClass, new StringCharIterator(fileResult)); result.addModel(model); } callback.onSuccess(result); } catch (final Exception e) { X_Log.error(getClass(), "Unable to load files for query "+query); if (callback instanceof ErrorHandler) { ((ErrorHandler) callback).onError(new RuntimeException("Unable to load files for query "+query)); } else { rethrow(e); } } finally { handler.remove(); } } }); } @Override @SuppressWarnings("rawtypes") public void query(final ModelQuery<Model> query, final SuccessHandler<ModelQueryResult<Model>> callback) { for (final QueryParameter param : query.getParameters()) { throw new UnsupportedOperationException("The basic, file-backed "+getClass().getName()+" does not support any complex queries"); } // The only query we will support is a parameterless "get all" query // This implementation generally sucks, and only exists for very basic usage. // If a file-backed API is truly desired, one should be implemented using proper // indexing, filtering, sorting, etc. And it should use java.nio.File... File f = getRoot(callback); if (query.getNamespace().length() > 0) { f = new File(f, query.getNamespace()); } final ArrayList<File> files = new ArrayList<File>(); final ModelQueryResult<Model> result = new ModelQueryResult<>(null); for (final File type : f.listFiles()) { File[] allFiles; if (query.getCursor() == null) { // Yes, listing all files is not going to be very performant; however, this implementation is // far too naive to be used for a production system. It is primarily a proof-of-concept that can // be usable for developing APIs against something that is simple to use and debug allFiles = type.listFiles(); } else { // If there is a cursor, we are continuing a query. allFiles = type.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.compareTo(query.getCursor()) > -1; } }); } for (int i = 0, m = allFiles.length; i < m; i++) { if (files.size() >= query.getLimit()) { result.setCursor(allFiles[i].getName()); break; } files.add(allFiles[i]); } } final ProvidesValue<RemovalHandler> scope = captureScope(); X_Time.runLater(new Runnable() { @Override public void run() { final RemovalHandler handler = scope.get(); String fileResult; try { for (final File file : files) { fileResult = X_IO.toStringUtf8(new FileInputStream(file)); final Class<? extends Model> type = typeNameToClass.get(file.getParent()); final Model model = deserialize(type, new StringCharIterator(fileResult)); result.addModel(model); } callback.onSuccess(result); } catch (final Exception e) { X_Log.error(getClass(), "Unable to load files for query "+query); if (callback instanceof ErrorHandler) { ((ErrorHandler) callback).onError(new RuntimeException("Unable to load files for query "+query)); } else { rethrow(e); } } 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; } }