All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.easimart.EasimartFile Maven / Gradle / Ivy

package co.easimart;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;

import bolts.Continuation;
import bolts.Task;

/**
 * {@code EasimartFile} is a local representation of a file that is saved to the Easimart cloud.
 * 

* The workflow is to construct a {@code EasimartFile} with data and optionally a filename. Then save * it and set it as a field on a {@link EasimartObject}. *

* Example: *

 * EasimartFile file = new EasimartFile("hello".getBytes());
 * file.save();
 *
 * EasimartObject object = new EasimartObject("TestObject");
 * object.put("file", file);
 * object.save();
 * 
*/ public class EasimartFile { // We limit the size of EasimartFile data to be 10mb. /* package */ static final int MAX_FILE_SIZE = 10 * 1048576; /* package for tests */ static EasimartFileController getFileController() { return EasimartCorePlugins.getInstance().getFileController(); } private static ProgressCallback progressCallbackOnMainThread( final ProgressCallback progressCallback) { if (progressCallback == null) { return null; } return new ProgressCallback() { @Override public void done(final Integer percentDone) { Task.call(new Callable() { @Override public Void call() throws Exception { progressCallback.done(percentDone); return null; } }, EasimartExecutors.main()); } }; } /* package */ static class State { /* package */ static class Builder { private String name; private String mimeType; private String url; public Builder() { // do nothing } public Builder(State state) { name = state.name(); mimeType = state.mimeType(); url = state.url(); } public Builder name(String name) { this.name = name; return this; } public Builder mimeType(String mimeType) { this.mimeType = mimeType; return this; } public Builder url(String url) { this.url = url; return this; } public State build() { return new State(this); } } private final String name; private final String contentType; private final String url; private State(Builder builder) { name = builder.name != null ? builder.name : "file"; contentType = builder.mimeType; url = builder.url; } public String name() { return name; } public String mimeType() { return contentType; } public String url() { return url; } } private State state; /** * Staging of {@code EasimartFile}'s data is stored in memory until the {@code EasimartFile} has been * successfully synced with the server. */ /* package for tests */ byte[] data; /* package for tests */ File file; /* package for tests */ final TaskQueue taskQueue = new TaskQueue(); private Set.TaskCompletionSource> currentTasks = Collections.synchronizedSet( new HashSet.TaskCompletionSource>()); /** * Creates a new file from a file pointer. * * @param file * The file. */ public EasimartFile(File file) { this(file, null); } /** * Creates a new file from a file pointer, and content type. Content type will be used instead of * auto-detection by file extension. * * @param file * The file. * @param contentType * The file's content type. */ public EasimartFile(File file, String contentType) { this(new State.Builder().name(file.getName()).mimeType(contentType).build()); if (file.length() > MAX_FILE_SIZE) { throw new IllegalArgumentException(String.format("EasimartFile must be less than %d bytes", MAX_FILE_SIZE)); } this.file = file; } /** * Creates a new file from a byte array, file name, and content type. Content type will be used * instead of auto-detection by file extension. * * @param name * The file's name, ideally with extension. The file name must begin with an alphanumeric * character, and consist of alphanumeric characters, periods, spaces, underscores, or * dashes. * @param data * The file's data. * @param contentType * The file's content type. */ public EasimartFile(String name, byte[] data, String contentType) { this(new State.Builder().name(name).mimeType(contentType).build()); if (data.length > MAX_FILE_SIZE) { throw new IllegalArgumentException(String.format("EasimartFile must be less than %d bytes", MAX_FILE_SIZE)); } this.data = data; } /** * Creates a new file from a byte array. * * @param data * The file's data. */ public EasimartFile(byte[] data) { this(null, data, null); } /** * Creates a new file from a byte array and a name. Giving a name with a proper file extension * (e.g. ".png") is ideal because it allows Easimart to deduce the content type of the file and set * appropriate HTTP headers when it is fetched. * * @param name * The file's name, ideally with extension. The file name must begin with an alphanumeric * character, and consist of alphanumeric characters, periods, spaces, underscores, or * dashes. * @param data * The file's data. */ public EasimartFile(String name, byte[] data) { this(name, data, null); } /** * Creates a new file from a byte array, and content type. Content type will be used instead of * auto-detection by file extension. * * @param data * The file's data. * @param contentType * The file's content type. */ public EasimartFile(byte[] data, String contentType) { this(null, data, contentType); } /* package for tests */ EasimartFile(State state) { this.state = state; } /* package for tests */ State getState() { return state; } /** * The filename. Before save is called, this is just the filename given by the user (if any). * After save is called, that name gets prefixed with a unique identifier. * * @return The file's name. */ public String getName() { return state.name(); } /** * Whether the file still needs to be saved. * * @return Whether the file needs to be saved. */ public boolean isDirty() { return state.url() == null; } /** * Whether the file has available data. */ public boolean isDataAvailable() { return data != null || getFileController().isDataAvailable(state); } /** * This returns the url of the file. It's only available after you save or after you get the file * from a EasimartObject. * * @return The url of the file. */ public String getUrl() { return state.url(); } /** * Saves the file to the Easimart cloud synchronously. */ public void save() throws EasimartException { EasimartTaskUtils.wait(saveInBackground()); } private Task saveAsync(final String sessionToken, final ProgressCallback uploadProgressCallback, Task toAwait, final Task cancellationToken) { // If the file isn't dirty, just return immediately. if (!isDirty()) { return Task.forResult(null); } if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } // Wait for our turn in the queue, then check state to decide whether to no-op. return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (!isDirty()) { return Task.forResult(null); } if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } Task saveTask; if (data != null) { saveTask = getFileController().saveAsync( state, data, sessionToken, progressCallbackOnMainThread(uploadProgressCallback), cancellationToken); } else { saveTask = getFileController().saveAsync( state, file, sessionToken, progressCallbackOnMainThread(uploadProgressCallback), cancellationToken); } return saveTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { state = task.getResult(); // Since we have successfully uploaded the file, we do not need to hold the file pointer // anymore. data = null; file = null; return task.makeVoid(); } }); } }); } /** * Saves the file to the Easimart cloud in a background thread. * `progressCallback` is guaranteed to be called with 100 before saveCallback is called. * * @param uploadProgressCallback * A ProgressCallback that is called periodically with progress updates. * @return A Task that will be resolved when the save completes. */ public Task saveInBackground(final ProgressCallback uploadProgressCallback) { final Task.TaskCompletionSource cts = Task.create(); currentTasks.add(cts); return EasimartUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final String sessionToken = task.getResult(); return saveAsync(sessionToken, uploadProgressCallback, cts.getTask()); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { cts.trySetResult(null); // release currentTasks.remove(cts); return task; } }); } /* package */ Task saveAsync(final String sessionToken, final ProgressCallback uploadProgressCallback, final Task cancellationToken) { return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken); } }); } /** * Saves the file to the Easimart cloud in a background thread. * * @return A Task that will be resolved when the save completes. */ public Task saveInBackground() { return saveInBackground((ProgressCallback) null); } /** * Saves the file to the Easimart cloud in a background thread. * `progressCallback` is guaranteed to be called with 100 before saveCallback is called. * * @param saveCallback * A SaveCallback that gets called when the save completes. * @param progressCallback * A ProgressCallback that is called periodically with progress updates. */ public void saveInBackground(final SaveCallback saveCallback, ProgressCallback progressCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(saveInBackground(progressCallback), saveCallback); } /** * Saves the file to the Easimart cloud in a background thread. * * @param callback * A SaveCallback that gets called when the save completes. */ public void saveInBackground(SaveCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback); } /** * Synchronously gets the data from cache if available or fetches its content from the network. * You probably want to use {@link #getDataInBackground()} instead unless you're already in a * background thread. */ public byte[] getData() throws EasimartException { return EasimartTaskUtils.wait(getDataInBackground()); } /** * Asynchronously gets the data from cache if available or fetches its content from the network. * A {@code ProgressCallback} will be called periodically with progress updates. * * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. * @return A Task that is resolved when the data has been fetched. */ public Task getDataInBackground(final ProgressCallback progressCallback) { final Task.TaskCompletionSource cts = Task.create(); currentTasks.add(cts); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { @Override public byte[] then(Task task) throws Exception { File file = task.getResult(); try { return EasimartFileUtils.readFileToByteArray(file); } catch (IOException e) { // do nothing } return null; } }); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { cts.trySetResult(null); // release currentTasks.remove(cts); return task; } }); } /** * Asynchronously gets the data from cache if available or fetches its content from the network. * * @return A Task that is resolved when the data has been fetched. */ public Task getDataInBackground() { return getDataInBackground((ProgressCallback) null); } /** * Asynchronously gets the data from cache if available or fetches its content from the network. * A {@code ProgressCallback} will be called periodically with progress updates. * A {@code GetDataCallback} will be called when the get completes. * * @param dataCallback * A {@code GetDataCallback} that is called when the get completes. * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. */ public void getDataInBackground(GetDataCallback dataCallback, final ProgressCallback progressCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(getDataInBackground(progressCallback), dataCallback); } /** * Asynchronously gets the data from cache if available or fetches its content from the network. * A {@code GetDataCallback} will be called when the get completes. * * @param dataCallback * A {@code GetDataCallback} that is called when the get completes. */ public void getDataInBackground(GetDataCallback dataCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(getDataInBackground(), dataCallback); } /** * Synchronously gets the file pointer from cache if available or fetches its content from the * network. You probably want to use {@link #getFileInBackground()} instead unless you're already * in a background thread. * Note: The {@link File} location may change without notice and should not be * stored to be accessed later. */ public File getFile() throws EasimartException { return EasimartTaskUtils.wait(getFileInBackground()); } /** * Asynchronously gets the file pointer from cache if available or fetches its content from the * network. The {@code ProgressCallback} will be called periodically with progress updates. * Note: The {@link File} location may change without notice and should not be * stored to be accessed later. * * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. * @return A Task that is resolved when the file pointer of this object has been fetched. */ public Task getFileInBackground(final ProgressCallback progressCallback) { final Task.TaskCompletionSource cts = Task.create(); currentTasks.add(cts); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return fetchInBackground(progressCallback, toAwait, cts.getTask()); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { cts.trySetResult(null); // release currentTasks.remove(cts); return task; } }); } /** * Asynchronously gets the file pointer from cache if available or fetches its content from the * network. * Note: The {@link File} location may change without notice and should not be * stored to be accessed later. * * @return A Task that is resolved when the data has been fetched. */ public Task getFileInBackground() { return getFileInBackground((ProgressCallback) null); } /** * Asynchronously gets the file pointer from cache if available or fetches its content from the * network. The {@code GetFileCallback} will be called when the get completes. * The {@code ProgressCallback} will be called periodically with progress updates. * The {@code ProgressCallback} is guaranteed to be called with 100 before the * {@code GetFileCallback} is called. * Note: The {@link File} location may change without notice and should not be * stored to be accessed later. * * @param fileCallback * A {@code GetFileCallback} that is called when the get completes. * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. */ public void getFileInBackground(GetFileCallback fileCallback, final ProgressCallback progressCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(getFileInBackground(progressCallback), fileCallback); } /** * Asynchronously gets the file pointer from cache if available or fetches its content from the * network. The {@code GetFileCallback} will be called when the get completes. * Note: The {@link File} location may change without notice and should not be * stored to be accessed later. * * @param fileCallback * A {@code GetFileCallback} that is called when the get completes. */ public void getFileInBackground(GetFileCallback fileCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(getFileInBackground(), fileCallback); } /** * Synchronously gets the data stream from cached file if available or fetches its content from * the network, saves the content as cached file and returns the data stream of the cached file. * You probably want to use {@link #getDataStreamInBackground} instead unless you're already in a * background thread. */ public InputStream getDataStream() throws EasimartException { return EasimartTaskUtils.wait(getDataStreamInBackground()); } /** * Asynchronously gets the data stream from cached file if available or fetches its content from * the network, saves the content as cached file and returns the data stream of the cached file. * The {@code ProgressCallback} will be called periodically with progress updates. * * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. * @return A Task that is resolved when the data stream of this object has been fetched. */ public Task getDataStreamInBackground(final ProgressCallback progressCallback) { final Task.TaskCompletionSource cts = Task.create(); currentTasks.add(cts); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { @Override public InputStream then(Task task) throws Exception { return new FileInputStream(task.getResult()); } }); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { cts.trySetResult(null); // release currentTasks.remove(cts); return task; } }); } /** * Asynchronously gets the data stream from cached file if available or fetches its content from * the network, saves the content as cached file and returns the data stream of the cached file. * * @return A Task that is resolved when the data stream has been fetched. */ public Task getDataStreamInBackground() { return getDataStreamInBackground((ProgressCallback) null); } /** * Asynchronously gets the data stream from cached file if available or fetches its content from * the network, saves the content as cached file and returns the data stream of the cached file. * The {@code GetDataStreamCallback} will be called when the get completes. The * {@code ProgressCallback} will be called periodically with progress updates. The * {@code ProgressCallback} is guaranteed to be called with 100 before * {@code GetDataStreamCallback} is called. * * @param dataStreamCallback * A {@code GetDataStreamCallback} that is called when the get completes. * @param progressCallback * A {@code ProgressCallback} that is called periodically with progress updates. */ public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback, final ProgressCallback progressCallback) { EasimartTaskUtils.callbackOnMainThreadAsync( getDataStreamInBackground(progressCallback), dataStreamCallback); } /** * Asynchronously gets the data stream from cached file if available or fetches its content from * the network, saves the content as cached file and returns the data stream of the cached file. * The {@code GetDataStreamCallback} will be called when the get completes. * * @param dataStreamCallback * A {@code GetDataStreamCallback} that is called when the get completes. */ public void getDataStreamInBackground(GetDataStreamCallback dataStreamCallback) { EasimartTaskUtils.callbackOnMainThreadAsync(getDataStreamInBackground(), dataStreamCallback); } private Task fetchInBackground( final ProgressCallback progressCallback, Task toAwait, final Task cancellationToken) { if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } return getFileController().fetchAsync( state, null, progressCallbackOnMainThread(progressCallback), cancellationToken); } }); } /** * Cancels the current network request and callbacks whether it's uploading or fetching data from * the server. */ //TODO (grantland): Deprecate and replace with CancellationToken public void cancel() { Set.TaskCompletionSource> tasks = new HashSet<>(currentTasks); for (Task.TaskCompletionSource tcs : tasks) { tcs.trySetCancelled(); } currentTasks.removeAll(tasks); } /* * Encode/Decode */ @SuppressWarnings("unused") /* package */ EasimartFile(JSONObject json, EasimartDecoder decoder) { this(new State.Builder().name(json.optString("name")).url(json.optString("url")).build()); } /* package */ JSONObject encode() throws JSONException { JSONObject json = new JSONObject(); json.put("__type", "File"); json.put("name", getName()); String url = getUrl(); if (url == null) { throw new IllegalStateException("Unable to encode an unsaved EasimartFile."); } json.put("url", getUrl()); return json; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy