com.parse.ParseFile.java Source code

Java tutorial

Introduction

Here is the source code for com.parse.ParseFile.java

Source

/*
 * Copyright (c) 2015-present, Parse, LLC.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.parse;

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 ParseFile} is a local representation of a file that is saved to the Parse cloud.
 * <p/>
 * The workflow is to construct a {@code ParseFile} with data and optionally a filename. Then save
 * it and set it as a field on a {@link ParseObject}.
 * <p/>
 * Example:
 * <pre>
 * ParseFile file = new ParseFile("hello".getBytes());
 * file.save();
 *
 * ParseObject object = new ParseObject("TestObject");
 * object.put("file", file);
 * object.save();
 * </pre>
 */
public class ParseFile {

    // We limit the size of ParseFile data to be 10mb.
    /* package */ static final int MAX_FILE_SIZE = 10 * 1048576;

    /* package for tests */ static ParseFileController getFileController() {
        return ParseCorePlugins.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<Void>() {
                    @Override
                    public Void call() throws Exception {
                        progressCallback.done(percentDone);
                        return null;
                    }
                }, ParseExecutors.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 ParseFile}'s data is stored in memory until the {@code ParseFile} 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<Task<?>.TaskCompletionSource> currentTasks = Collections
            .synchronizedSet(new HashSet<Task<?>.TaskCompletionSource>());

    /**
     * Creates a new file from a file pointer.
     *
     * @param file
     *          The file.
     */
    public ParseFile(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 ParseFile(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("ParseFile 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 ParseFile(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("ParseFile 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 ParseFile(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 Parse 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 ParseFile(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 ParseFile(byte[] data, String contentType) {
        this(null, data, contentType);
    }

    /* package for tests */ ParseFile(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 ParseObject.
     *
     * @return The url of the file.
     */
    public String getUrl() {
        return state.url();
    }

    /**
     * Saves the file to the Parse cloud synchronously.
     */
    public void save() throws ParseException {
        ParseTaskUtils.wait(saveInBackground());
    }

    private Task<Void> saveAsync(final String sessionToken, final ProgressCallback uploadProgressCallback,
            Task<Void> toAwait, final Task<Void> 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<Void, Task<Void>>() {
            @Override
            public Task<Void> then(Task<Void> task) throws Exception {
                if (!isDirty()) {
                    return Task.forResult(null);
                }
                if (cancellationToken != null && cancellationToken.isCancelled()) {
                    return Task.cancelled();
                }

                Task<ParseFile.State> 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<State, Task<Void>>() {
                    @Override
                    public Task<Void> then(Task<State> 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 Parse 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<Void> saveInBackground(final ProgressCallback uploadProgressCallback) {
        final Task<Void>.TaskCompletionSource cts = Task.create();
        currentTasks.add(cts);

        return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
            @Override
            public Task<Void> then(Task<String> task) throws Exception {
                final String sessionToken = task.getResult();
                return saveAsync(sessionToken, uploadProgressCallback, cts.getTask());
            }
        }).continueWithTask(new Continuation<Void, Task<Void>>() {
            @Override
            public Task<Void> then(Task<Void> task) throws Exception {
                cts.trySetResult(null); // release
                currentTasks.remove(cts);
                return task;
            }
        });
    }

    /* package */ Task<Void> saveAsync(final String sessionToken, final ProgressCallback uploadProgressCallback,
            final Task<Void> cancellationToken) {
        return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
            @Override
            public Task<Void> then(Task<Void> toAwait) throws Exception {
                return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken);
            }
        });
    }

    /**
     * Saves the file to the Parse cloud in a background thread.
     *
     * @return A Task that will be resolved when the save completes.
     */
    public Task<Void> saveInBackground() {
        return saveInBackground((ProgressCallback) null);
    }

    /**
     * Saves the file to the Parse 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) {
        ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(progressCallback), saveCallback);
    }

    /**
     * Saves the file to the Parse cloud in a background thread.
     *
     * @param callback
     *          A SaveCallback that gets called when the save completes.
     */
    public void saveInBackground(SaveCallback callback) {
        ParseTaskUtils.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 ParseException {
        return ParseTaskUtils.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<byte[]> getDataInBackground(final ProgressCallback progressCallback) {
        final Task<Void>.TaskCompletionSource cts = Task.create();
        currentTasks.add(cts);

        return taskQueue.enqueue(new Continuation<Void, Task<byte[]>>() {
            @Override
            public Task<byte[]> then(Task<Void> toAwait) throws Exception {
                return fetchInBackground(progressCallback, toAwait, cts.getTask())
                        .onSuccess(new Continuation<File, byte[]>() {
                            @Override
                            public byte[] then(Task<File> task) throws Exception {
                                File file = task.getResult();
                                try {
                                    return ParseFileUtils.readFileToByteArray(file);
                                } catch (IOException e) {
                                    // do nothing
                                }
                                return null;
                            }
                        });
            }
        }).continueWithTask(new Continuation<byte[], Task<byte[]>>() {
            @Override
            public Task<byte[]> then(Task<byte[]> 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<byte[]> 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) {
        ParseTaskUtils.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) {
        ParseTaskUtils.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.
     * <strong>Note: </strong> The {@link File} location may change without notice and should not be
     * stored to be accessed later.
     */
    public File getFile() throws ParseException {
        return ParseTaskUtils.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.
     * <strong>Note: </strong> 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<File> getFileInBackground(final ProgressCallback progressCallback) {
        final Task<Void>.TaskCompletionSource cts = Task.create();
        currentTasks.add(cts);

        return taskQueue.enqueue(new Continuation<Void, Task<File>>() {
            @Override
            public Task<File> then(Task<Void> toAwait) throws Exception {
                return fetchInBackground(progressCallback, toAwait, cts.getTask());
            }
        }).continueWithTask(new Continuation<File, Task<File>>() {
            @Override
            public Task<File> then(Task<File> 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.
     * <strong>Note: </strong> 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<File> 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.
     * <strong>Note: </strong> 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) {
        ParseTaskUtils.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.
     * <strong>Note: </strong> 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) {
        ParseTaskUtils.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 ParseException {
        return ParseTaskUtils.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<InputStream> getDataStreamInBackground(final ProgressCallback progressCallback) {
        final Task<Void>.TaskCompletionSource cts = Task.create();
        currentTasks.add(cts);

        return taskQueue.enqueue(new Continuation<Void, Task<InputStream>>() {
            @Override
            public Task<InputStream> then(Task<Void> toAwait) throws Exception {
                return fetchInBackground(progressCallback, toAwait, cts.getTask())
                        .onSuccess(new Continuation<File, InputStream>() {
                            @Override
                            public InputStream then(Task<File> task) throws Exception {
                                return new FileInputStream(task.getResult());
                            }
                        });
            }
        }).continueWithTask(new Continuation<InputStream, Task<InputStream>>() {
            @Override
            public Task<InputStream> then(Task<InputStream> 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<InputStream> 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) {
        ParseTaskUtils.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) {
        ParseTaskUtils.callbackOnMainThreadAsync(getDataStreamInBackground(), dataStreamCallback);
    }

    private Task<File> fetchInBackground(final ProgressCallback progressCallback, Task<Void> toAwait,
            final Task<Void> cancellationToken) {
        if (cancellationToken != null && cancellationToken.isCancelled()) {
            return Task.cancelled();
        }

        return toAwait.continueWithTask(new Continuation<Void, Task<File>>() {
            @Override
            public Task<File> then(Task<Void> 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<Task<?>.TaskCompletionSource> tasks = new HashSet<>(currentTasks);
        for (Task<?>.TaskCompletionSource tcs : tasks) {
            tcs.trySetCancelled();
        }
        currentTasks.removeAll(tasks);
    }

    /*
     * Encode/Decode
     */
    @SuppressWarnings("unused")
    /* package */ ParseFile(JSONObject json, ParseDecoder 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 ParseFile.");
        }
        json.put("url", getUrl());

        return json;
    }
}