diff --git a/.codecov.yml b/.codecov.yml index ac4fc988a..bb0d21868 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,7 +4,7 @@ coverage: range: "45...100" status: - project: + project: default: target: 45% patch: yes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..a56f34ec6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle +on: + push: + branches: + - master + pull_request: + branches: + - '**' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 8 + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'adopt' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - run: pip install --user codecov + - run: mkdir "$ANDROID_HOME/licenses" || true + - run: echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" + - run: ./gradlew clean jacocoTestReport \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9bff4cc2c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -branches: - only: - - master - - /^\d+\.\d+\.\d+$/ # regex - -language: android - -jdk: - - openjdk8 - -before_install: - - pip install --user codecov - - mkdir "$ANDROID_HOME/licenses" || true - - echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> "$ANDROID_HOME/licenses/android-sdk-license" - -script: - - ./gradlew clean testDebugUnitTest jacocoTestReport - -after_success: - - ./gradlew coveralls - - codecov - -cache: - directories: - - $HOME/.gradle - - $HOME/.m2/repository diff --git a/bolts-tasks/build.gradle b/bolts-tasks/build.gradle index 22bcb8bdb..b86e9794a 100644 --- a/bolts-tasks/build.gradle +++ b/bolts-tasks/build.gradle @@ -6,22 +6,11 @@ apply plugin: 'java' apply plugin: 'maven' -configurations { - provided -} - -sourceSets { - main { - compileClasspath += configurations.provided - } -} - dependencies { - provided 'com.google.android:android:4.1.1.4' - testImplementation 'junit:junit:4.12' + compileOnly 'com.google.android:android:4.1.1.4' + testImplementation 'junit:junit:4.13.2' } - javadoc.options.addStringOption('Xdoclint:none', '-quiet') task sourcesJar(type: Jar) { @@ -29,7 +18,7 @@ task sourcesJar(type: Jar) { from sourceSets.main.allJava } -task javadocJar (type: Jar, dependsOn: javadoc) { +task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from javadoc.destinationDir } @@ -45,10 +34,6 @@ artifacts { apply plugin: 'jacoco' -jacoco { - toolVersion = '0.7.1.201405082137' -} - jacocoTestReport { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." @@ -59,3 +44,8 @@ jacocoTestReport { } //endregion + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} \ No newline at end of file diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/AggregateException.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/AggregateException.java index 8535c1ca5..bb8c9c77d 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/AggregateException.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/AggregateException.java @@ -23,7 +23,7 @@ public class AggregateException extends Exception { private static final String DEFAULT_MESSAGE = "There were multiple errors."; - private List innerThrowables; + private final List innerThrowables; /** * Constructs a new {@code AggregateException} with the current stack trace, the specified detail @@ -103,7 +103,7 @@ public void printStackTrace(PrintWriter err) { */ @Deprecated public List getErrors() { - List errors = new ArrayList(); + List errors = new ArrayList<>(); if (innerThrowables == null) { return errors; } @@ -123,7 +123,7 @@ public List getErrors() { */ @Deprecated public Throwable[] getCauses() { - return innerThrowables.toArray(new Throwable[innerThrowables.size()]); + return innerThrowables.toArray(new Throwable[0]); } } diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/AndroidExecutors.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/AndroidExecutors.java index 45ee7769a..19612f42c 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/AndroidExecutors.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/AndroidExecutors.java @@ -34,14 +34,8 @@ */ /* package */ final class AndroidExecutors { + /* package */ static final long KEEP_ALIVE_TIME = 1L; private static final AndroidExecutors INSTANCE = new AndroidExecutors(); - - private final Executor uiThread; - - private AndroidExecutors() { - uiThread = new UIThreadExecutor(); - } - /** * Nexus 5: Quad-Core * Moto X: Dual-Core @@ -55,7 +49,11 @@ private AndroidExecutors() { private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); /* package */ static final int CORE_POOL_SIZE = CPU_COUNT + 1; /* package */ static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1; - /* package */ static final long KEEP_ALIVE_TIME = 1L; + private final Executor uiThread; + + private AndroidExecutors() { + uiThread = new UIThreadExecutor(); + } /** * Creates a proper Cached Thread Pool. Tasks will reuse cached threads if available @@ -72,7 +70,7 @@ public static ExecutorService newCachedThreadPool() { CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, - new LinkedBlockingQueue()); + new LinkedBlockingQueue<>()); allowCoreThreadTimeout(executor, true); @@ -95,7 +93,7 @@ public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, - new LinkedBlockingQueue(), + new LinkedBlockingQueue<>(), threadFactory); allowCoreThreadTimeout(executor, true); diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/BoltsExecutors.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/BoltsExecutors.java index 866013cbd..5b9e7b04d 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/BoltsExecutors.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/BoltsExecutors.java @@ -18,15 +18,6 @@ /* package */ final class BoltsExecutors { private static final BoltsExecutors INSTANCE = new BoltsExecutors(); - - private static boolean isAndroidRuntime() { - String javaRuntimeName = System.getProperty("java.runtime.name"); - if (javaRuntimeName == null) { - return false; - } - return javaRuntimeName.toLowerCase(Locale.US).contains("android"); - } - private final ExecutorService background; private final ScheduledExecutorService scheduled; private final Executor immediate; @@ -39,6 +30,14 @@ private BoltsExecutors() { immediate = new ImmediateExecutor(); } + private static boolean isAndroidRuntime() { + String javaRuntimeName = System.getProperty("java.runtime.name"); + if (javaRuntimeName == null) { + return false; + } + return javaRuntimeName.toLowerCase(Locale.US).contains("android"); + } + /** * An {@link java.util.concurrent.Executor} that executes tasks in parallel. */ @@ -69,7 +68,7 @@ static Executor immediate() { */ private static class ImmediateExecutor implements Executor { private static final int MAX_DEPTH = 15; - private ThreadLocal executionDepth = new ThreadLocal<>(); + private final ThreadLocal executionDepth = new ThreadLocal<>(); /** * Increments the depth. diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationToken.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationToken.java index d9214f86c..e23d79040 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationToken.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationToken.java @@ -64,6 +64,6 @@ public String toString() { return String.format(Locale.US, "%s@%s[cancellationRequested=%s]", getClass().getName(), Integer.toHexString(hashCode()), - Boolean.toString(tokenSource.isCancellationRequested())); + tokenSource.isCancellationRequested()); } } diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java index b654a33f0..9db6c82b3 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java @@ -105,14 +105,11 @@ private void cancelAfter(long delay, TimeUnit timeUnit) { cancelScheduledCancellation(); if (delay != -1) { - scheduledCancellation = executor.schedule(new Runnable() { - @Override - public void run() { - synchronized (lock) { - scheduledCancellation = null; - } - cancel(); + scheduledCancellation = executor.schedule(() -> { + synchronized (lock) { + scheduledCancellation = null; } + cancel(); }, delay, timeUnit); } } @@ -187,7 +184,7 @@ public String toString() { return String.format(Locale.US, "%s@%s[cancellationRequested=%s]", getClass().getName(), Integer.toHexString(hashCode()), - Boolean.toString(isCancellationRequested())); + isCancellationRequested()); } // This method makes no attempt to perform any synchronization itself - you should ensure diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java index 25732908d..e51f9cf6c 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java @@ -30,62 +30,22 @@ public class Task { * An {@link java.util.concurrent.Executor} that executes tasks in parallel. */ public static final ExecutorService BACKGROUND_EXECUTOR = BoltsExecutors.background(); - - /** - * An {@link java.util.concurrent.Executor} that executes tasks in the current thread unless - * the stack runs too deep, at which point it will delegate to {@link Task#BACKGROUND_EXECUTOR} in - * order to trim the stack. - */ - private static final Executor IMMEDIATE_EXECUTOR = BoltsExecutors.immediate(); - /** * An {@link java.util.concurrent.Executor} that executes tasks on the UI thread. */ public static final Executor UI_THREAD_EXECUTOR = AndroidExecutors.uiThread(); - /** - * Interface for handlers invoked when a failed {@code Task} is about to be - * finalized, but the exception has not been consumed. - * - *

The handler will execute in the GC thread, so if the handler needs to do - * anything time consuming or complex it is a good idea to fire off a {@code Task} - * to handle the exception. - * - * @see #getUnobservedExceptionHandler - * @see #setUnobservedExceptionHandler + * An {@link java.util.concurrent.Executor} that executes tasks in the current thread unless + * the stack runs too deep, at which point it will delegate to {@link Task#BACKGROUND_EXECUTOR} in + * order to trim the stack. */ - public interface UnobservedExceptionHandler { - /** - * Method invoked when the given task has an unobserved exception. - *

Any exception thrown by this method will be ignored. - * - * @param t the task - * @param e the exception - */ - void unobservedException(Task t, UnobservedTaskException e); - } - + private static final Executor IMMEDIATE_EXECUTOR = BoltsExecutors.immediate(); + private static final Task TASK_NULL = new Task<>(null); + private static final Task TASK_TRUE = new Task<>((Boolean) true); + private static final Task TASK_FALSE = new Task<>((Boolean) false); + private static final Task TASK_CANCELLED = new Task<>(true); // null unless explicitly set private static volatile UnobservedExceptionHandler unobservedExceptionHandler; - - /** - * Returns the handler invoked when a task has an unobserved - * exception or {@code null}. - */ - public static UnobservedExceptionHandler getUnobservedExceptionHandler() { - return unobservedExceptionHandler; - } - - /** - * Set the handler invoked when a task has an unobserved exception. - * - * @param eh the object to use as an unobserved exception handler. If - * null then unobserved exceptions will be ignored. - */ - public static void setUnobservedExceptionHandler(UnobservedExceptionHandler eh) { - unobservedExceptionHandler = eh; - } - private final Object lock = new Object(); private boolean complete; private boolean cancelled; @@ -111,90 +71,29 @@ private Task(boolean cancelled) { } /** - * @deprecated Please use {@link com.parse.boltsinternal.TaskCompletionSource ()} instead. - */ - public static Task.TaskCompletionSource create() { - Task task = new Task<>(); - return task.new TaskCompletionSource(); - } - - /** - * @return {@code true} if the task completed (has a result, an error, or was cancelled. - * {@code false} otherwise. - */ - public boolean isCompleted() { - synchronized (lock) { - return complete; - } - } - - /** - * @return {@code true} if the task was cancelled, {@code false} otherwise. - */ - public boolean isCancelled() { - synchronized (lock) { - return cancelled; - } - } - - /** - * @return {@code true} if the task has an error, {@code false} otherwise. - */ - public boolean isFaulted() { - synchronized (lock) { - return getError() != null; - } - } - - /** - * @return The result of the task, if set. {@code null} otherwise. - */ - public TResult getResult() { - synchronized (lock) { - return result; - } - } - - /** - * @return The error for the task, if set. {@code null} otherwise. + * Returns the handler invoked when a task has an unobserved + * exception or {@code null}. */ - public Exception getError() { - synchronized (lock) { - if (error != null) { - errorHasBeenObserved = true; - if (unobservedErrorNotifier != null) { - unobservedErrorNotifier.setObserved(); - unobservedErrorNotifier = null; - } - } - return error; - } + public static UnobservedExceptionHandler getUnobservedExceptionHandler() { + return unobservedExceptionHandler; } /** - * Blocks until the task is complete. + * Set the handler invoked when a task has an unobserved exception. + * + * @param eh the object to use as an unobserved exception handler. If + * null then unobserved exceptions will be ignored. */ - public void waitForCompletion() throws InterruptedException { - synchronized (lock) { - if (!isCompleted()) { - lock.wait(); - } - } + public static void setUnobservedExceptionHandler(UnobservedExceptionHandler eh) { + unobservedExceptionHandler = eh; } /** - * Blocks until the task is complete or times out. - * - * @return {@code true} if the task completed (has a result, an error, or was cancelled). - * {@code false} otherwise. + * @deprecated Please use {@link com.parse.boltsinternal.TaskCompletionSource ()} instead. */ - public boolean waitForCompletion(long duration, TimeUnit timeUnit) throws InterruptedException { - synchronized (lock) { - if (!isCompleted()) { - lock.wait(timeUnit.toMillis(duration)); - } - return isCompleted(); - } + public static Task.TaskCompletionSource create() { + Task task = new Task<>(); + return task.new TaskCompletionSource(); } /** @@ -263,54 +162,20 @@ static Task delay(long delay, ScheduledExecutorService executor, final Can } final com.parse.boltsinternal.TaskCompletionSource tcs = new com.parse.boltsinternal.TaskCompletionSource<>(); - final ScheduledFuture scheduled = executor.schedule(new Runnable() { - @Override - public void run() { - tcs.trySetResult(null); - } + final ScheduledFuture scheduled = executor.schedule(() -> { + tcs.trySetResult(null); }, delay, TimeUnit.MILLISECONDS); if (cancellationToken != null) { - cancellationToken.register(new Runnable() { - @Override - public void run() { - scheduled.cancel(true); - tcs.trySetCancelled(); - } + cancellationToken.register(() -> { + scheduled.cancel(true); + tcs.trySetCancelled(); }); } return tcs.getTask(); } - /** - * Makes a fluent cast of a Task's result possible, avoiding an extra continuation just to cast - * the type of the result. - */ - public Task cast() { - @SuppressWarnings("unchecked") - Task task = (Task) this; - return task; - } - - /** - * Turns a Task into a Task, dropping any result. - */ - public Task makeVoid() { - return this.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - if (task.isCancelled()) { - return Task.cancelled(); - } - if (task.isFaulted()) { - return Task.forError(task.getError()); - } - return Task.forResult(null); - } - }); - } - /** * Invokes the callable on a background thread, returning a Task to represent the operation. *

@@ -345,21 +210,18 @@ public static Task call(final Callable callable, Exe final CancellationToken ct) { final com.parse.boltsinternal.TaskCompletionSource tcs = new com.parse.boltsinternal.TaskCompletionSource<>(); try { - executor.execute(new Runnable() { - @Override - public void run() { - if (ct != null && ct.isCancellationRequested()) { - tcs.setCancelled(); - return; - } + executor.execute(() -> { + if (ct != null && ct.isCancellationRequested()) { + tcs.setCancelled(); + return; + } - try { - tcs.setResult(callable.call()); - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } + try { + tcs.setResult(callable.call()); + } catch (CancellationException e) { + tcs.setCancelled(); + } catch (Exception e) { + tcs.setError(e); } }); } catch (Exception e) { @@ -406,16 +268,13 @@ public static Task> whenAnyResult(Collection task : tasks) { - task.continueWith(new Continuation() { - @Override - public Void then(Task task) { - if (isAnyTaskComplete.compareAndSet(false, true)) { - firstCompleted.setResult(task); - } else { - Throwable ensureObserved = task.getError(); - } - return null; + task.continueWith((Continuation) task1 -> { + if (isAnyTaskComplete.compareAndSet(false, true)) { + firstCompleted.setResult(task1); + } else { + Throwable ensureObserved = task1.getError(); } + return null; }); } return firstCompleted.getTask(); @@ -442,16 +301,13 @@ public static Task> whenAny(Collection> tasks) { final AtomicBoolean isAnyTaskComplete = new AtomicBoolean(false); for (Task task : tasks) { - ((Task) task).continueWith(new Continuation() { - @Override - public Void then(Task task) { - if (isAnyTaskComplete.compareAndSet(false, true)) { - firstCompleted.setResult(task); - } else { - Throwable ensureObserved = task.getError(); - } - return null; + ((Task) task).continueWith((Continuation) task1 -> { + if (isAnyTaskComplete.compareAndSet(false, true)) { + firstCompleted.setResult(task1); + } else { + Throwable ensureObserved = task1.getError(); } + return null; }); } return firstCompleted.getTask(); @@ -483,19 +339,16 @@ public Void then(Task task) { * @return A Task that will resolve to {@code List<TResult>} when all the tasks are resolved. */ public static Task> whenAllResult(final Collection> tasks) { - return whenAll(tasks).onSuccess(new Continuation>() { - @Override - public List then(Task task) throws Exception { - if (tasks.size() == 0) { - return Collections.emptyList(); - } + return whenAll(tasks).onSuccess(task -> { + if (tasks.size() == 0) { + return Collections.emptyList(); + } - List results = new ArrayList<>(); - for (Task individualTask : tasks) { - results.add(individualTask.getResult()); - } - return results; + List results = new ArrayList<>(); + for (Task individualTask : tasks) { + results.add(individualTask.getResult()); } + return results; }); } @@ -533,43 +386,236 @@ public static Task whenAll(Collection> tasks) { for (Task task : tasks) { @SuppressWarnings("unchecked") Task t = (Task) task; - t.continueWith(new Continuation() { - @Override - public Void then(Task task) { - if (task.isFaulted()) { - synchronized (errorLock) { - causes.add(task.getError()); - } + t.continueWith((Continuation) task1 -> { + if (task1.isFaulted()) { + synchronized (errorLock) { + causes.add(task1.getError()); } + } - if (task.isCancelled()) { - isCancelled.set(true); - } + if (task1.isCancelled()) { + isCancelled.set(true); + } - if (count.decrementAndGet() == 0) { - if (causes.size() != 0) { - if (causes.size() == 1) { - allFinished.setError(causes.get(0)); - } else { - Exception error = new AggregateException( - String.format("There were %d exceptions.", causes.size()), - causes); - allFinished.setError(error); - } - } else if (isCancelled.get()) { - allFinished.setCancelled(); + if (count.decrementAndGet() == 0) { + if (causes.size() != 0) { + if (causes.size() == 1) { + allFinished.setError(causes.get(0)); } else { - allFinished.setResult(null); + Exception error = new AggregateException( + String.format("There were %d exceptions.", causes.size()), + causes); + allFinished.setError(error); } + } else if (isCancelled.get()) { + allFinished.setCancelled(); + } else { + allFinished.setResult(null); } - return null; } + return null; }); } return allFinished.getTask(); } + /** + * Handles the non-async (i.e. the continuation doesn't return a Task) continuation case, passing + * the results of the given Task through to the given continuation and using the results of that + * call to set the result of the TaskContinuationSource. + * + * @param tcs The TaskContinuationSource that will be orchestrated by this call. + * @param continuation The non-async continuation. + * @param task The task being completed. + * @param executor The executor to use when running the continuation (allowing the continuation to be + * scheduled on a different thread). + */ + private static void completeImmediately( + final com.parse.boltsinternal.TaskCompletionSource tcs, + final Continuation continuation, final Task task, + Executor executor, final CancellationToken ct) { + try { + executor.execute(() -> { + if (ct != null && ct.isCancellationRequested()) { + tcs.setCancelled(); + return; + } + + try { + TContinuationResult result = continuation.then(task); + tcs.setResult(result); + } catch (CancellationException e) { + tcs.setCancelled(); + } catch (Exception e) { + tcs.setError(e); + } + }); + } catch (Exception e) { + tcs.setError(new ExecutorException(e)); + } + } + + /** + * Handles the async (i.e. the continuation does return a Task) continuation case, passing the + * results of the given Task through to the given continuation to get a new Task. The + * TaskCompletionSource's results are only set when the new Task has completed, unwrapping the + * results of the task returned by the continuation. + * + * @param tcs The TaskContinuationSource that will be orchestrated by this call. + * @param continuation The async continuation. + * @param task The task being completed. + * @param executor The executor to use when running the continuation (allowing the continuation to be + * scheduled on a different thread). + */ + private static void completeAfterTask( + final com.parse.boltsinternal.TaskCompletionSource tcs, + final Continuation> continuation, + final Task task, final Executor executor, + final CancellationToken ct) { + try { + executor.execute(() -> { + if (ct != null && ct.isCancellationRequested()) { + tcs.setCancelled(); + return; + } + + try { + Task result = continuation.then(task); + if (result == null) { + tcs.setResult(null); + } else { + result.continueWith((Continuation) task1 -> { + if (ct != null && ct.isCancellationRequested()) { + tcs.setCancelled(); + return null; + } + + if (task1.isCancelled()) { + tcs.setCancelled(); + } else if (task1.isFaulted()) { + tcs.setError(task1.getError()); + } else { + tcs.setResult(task1.getResult()); + } + return null; + }); + } + } catch (CancellationException e) { + tcs.setCancelled(); + } catch (Exception e) { + tcs.setError(e); + } + }); + } catch (Exception e) { + tcs.setError(new ExecutorException(e)); + } + } + + /** + * @return {@code true} if the task completed (has a result, an error, or was cancelled. + * {@code false} otherwise. + */ + public boolean isCompleted() { + synchronized (lock) { + return complete; + } + } + + /** + * @return {@code true} if the task was cancelled, {@code false} otherwise. + */ + public boolean isCancelled() { + synchronized (lock) { + return cancelled; + } + } + + /** + * @return {@code true} if the task has an error, {@code false} otherwise. + */ + public boolean isFaulted() { + synchronized (lock) { + return getError() != null; + } + } + + /** + * @return The result of the task, if set. {@code null} otherwise. + */ + public TResult getResult() { + synchronized (lock) { + return result; + } + } + + /** + * @return The error for the task, if set. {@code null} otherwise. + */ + public Exception getError() { + synchronized (lock) { + if (error != null) { + errorHasBeenObserved = true; + if (unobservedErrorNotifier != null) { + unobservedErrorNotifier.setObserved(); + unobservedErrorNotifier = null; + } + } + return error; + } + } + + /** + * Blocks until the task is complete. + */ + public void waitForCompletion() throws InterruptedException { + synchronized (lock) { + if (!isCompleted()) { + lock.wait(); + } + } + } + + /** + * Blocks until the task is complete or times out. + * + * @return {@code true} if the task completed (has a result, an error, or was cancelled). + * {@code false} otherwise. + */ + public boolean waitForCompletion(long duration, TimeUnit timeUnit) throws InterruptedException { + synchronized (lock) { + if (!isCompleted()) { + lock.wait(timeUnit.toMillis(duration)); + } + return isCompleted(); + } + } + + /** + * Makes a fluent cast of a Task's result possible, avoiding an extra continuation just to cast + * the type of the result. + */ + public Task cast() { + @SuppressWarnings("unchecked") + Task task = (Task) this; + return task; + } + + /** + * Turns a Task into a Task, dropping any result. + */ + public Task makeVoid() { + return this.continueWithTask(task -> { + if (task.isCancelled()) { + return Task.cancelled(); + } + if (task.isFaulted()) { + return Task.forError(task.getError()); + } + return Task.forResult(null); + }); + } + /** * Continues a task with the equivalent of a Task-based while loop, where the body of the loop is * a task continuation. @@ -606,19 +652,16 @@ public Task continueWhile(final Callable predicate, final CancellationToken ct) { final Capture>> predicateContinuation = new Capture<>(); - predicateContinuation.set(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - if (ct != null && ct.isCancellationRequested()) { - return Task.cancelled(); - } + predicateContinuation.set(task -> { + if (ct != null && ct.isCancellationRequested()) { + return Task.cancelled(); + } - if (predicate.call()) { - return Task.forResult(null).onSuccessTask(continuation, executor) - .onSuccessTask(predicateContinuation.get(), executor); - } - return Task.forResult(null); + if (predicate.call()) { + return Task.forResult(null).onSuccessTask(continuation, executor) + .onSuccessTask(predicateContinuation.get(), executor); } + return Task.forResult(null); }); return makeVoid().continueWithTask(predicateContinuation.get(), executor); } @@ -646,12 +689,9 @@ public Task continueWith( synchronized (lock) { completed = this.isCompleted(); if (!completed) { - this.continuations.add(new Continuation() { - @Override - public Void then(Task task) { - completeImmediately(tcs, continuation, task, executor, ct); - return null; - } + this.continuations.add(task -> { + completeImmediately(tcs, continuation, task, executor, ct); + return null; }); } } @@ -700,12 +740,9 @@ public Task continueWithTask( synchronized (lock) { completed = this.isCompleted(); if (!completed) { - this.continuations.add(new Continuation() { - @Override - public Void then(Task task) { - completeAfterTask(tcs, continuation, task, executor, ct); - return null; - } + this.continuations.add(task -> { + completeAfterTask(tcs, continuation, task, executor, ct); + return null; }); } } @@ -749,20 +786,17 @@ public Task onSuccess( public Task onSuccess( final Continuation continuation, Executor executor, final CancellationToken ct) { - return continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (ct != null && ct.isCancellationRequested()) { - return Task.cancelled(); - } + return continueWithTask(task -> { + if (ct != null && ct.isCancellationRequested()) { + return Task.cancelled(); + } - if (task.isFaulted()) { - return Task.forError(task.getError()); - } else if (task.isCancelled()) { - return Task.cancelled(); - } else { - return task.continueWith(continuation); - } + if (task.isFaulted()) { + return Task.forError(task.getError()); + } else if (task.isCancelled()) { + return Task.cancelled(); + } else { + return task.continueWith(continuation); } }, executor); } @@ -801,20 +835,17 @@ public Task onSuccessTask( public Task onSuccessTask( final Continuation> continuation, Executor executor, final CancellationToken ct) { - return continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (ct != null && ct.isCancellationRequested()) { - return Task.cancelled(); - } + return continueWithTask(task -> { + if (ct != null && ct.isCancellationRequested()) { + return Task.cancelled(); + } - if (task.isFaulted()) { - return Task.forError(task.getError()); - } else if (task.isCancelled()) { - return Task.cancelled(); - } else { - return task.continueWithTask(continuation); - } + if (task.isFaulted()) { + return Task.forError(task.getError()); + } else if (task.isCancelled()) { + return Task.cancelled(); + } else { + return task.continueWithTask(continuation); } }, executor); } @@ -838,107 +869,6 @@ public Task onSuccessTask( return onSuccessTask(continuation, IMMEDIATE_EXECUTOR, ct); } - /** - * Handles the non-async (i.e. the continuation doesn't return a Task) continuation case, passing - * the results of the given Task through to the given continuation and using the results of that - * call to set the result of the TaskContinuationSource. - * - * @param tcs The TaskContinuationSource that will be orchestrated by this call. - * @param continuation The non-async continuation. - * @param task The task being completed. - * @param executor The executor to use when running the continuation (allowing the continuation to be - * scheduled on a different thread). - */ - private static void completeImmediately( - final com.parse.boltsinternal.TaskCompletionSource tcs, - final Continuation continuation, final Task task, - Executor executor, final CancellationToken ct) { - try { - executor.execute(new Runnable() { - @Override - public void run() { - if (ct != null && ct.isCancellationRequested()) { - tcs.setCancelled(); - return; - } - - try { - TContinuationResult result = continuation.then(task); - tcs.setResult(result); - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } - } - }); - } catch (Exception e) { - tcs.setError(new ExecutorException(e)); - } - } - - /** - * Handles the async (i.e. the continuation does return a Task) continuation case, passing the - * results of the given Task through to the given continuation to get a new Task. The - * TaskCompletionSource's results are only set when the new Task has completed, unwrapping the - * results of the task returned by the continuation. - * - * @param tcs The TaskContinuationSource that will be orchestrated by this call. - * @param continuation The async continuation. - * @param task The task being completed. - * @param executor The executor to use when running the continuation (allowing the continuation to be - * scheduled on a different thread). - */ - private static void completeAfterTask( - final com.parse.boltsinternal.TaskCompletionSource tcs, - final Continuation> continuation, - final Task task, final Executor executor, - final CancellationToken ct) { - try { - executor.execute(new Runnable() { - @Override - public void run() { - if (ct != null && ct.isCancellationRequested()) { - tcs.setCancelled(); - return; - } - - try { - Task result = continuation.then(task); - if (result == null) { - tcs.setResult(null); - } else { - result.continueWith(new Continuation() { - @Override - public Void then(Task task) { - if (ct != null && ct.isCancellationRequested()) { - tcs.setCancelled(); - return null; - } - - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(task.getResult()); - } - return null; - } - }); - } - } catch (CancellationException e) { - tcs.setCancelled(); - } catch (Exception e) { - tcs.setError(e); - } - } - }); - } catch (Exception e) { - tcs.setError(new ExecutorException(e)); - } - } - private void runContinuations() { synchronized (lock) { for (Continuation continuation : continuations) { @@ -1005,6 +935,28 @@ private void runContinuations() { } } + /** + * Interface for handlers invoked when a failed {@code Task} is about to be + * finalized, but the exception has not been consumed. + * + *

The handler will execute in the GC thread, so if the handler needs to do + * anything time consuming or complex it is a good idea to fire off a {@code Task} + * to handle the exception. + * + * @see #getUnobservedExceptionHandler + * @see #setUnobservedExceptionHandler + */ + public interface UnobservedExceptionHandler { + /** + * Method invoked when the given task has an unobserved exception. + *

Any exception thrown by this method will be ignored. + * + * @param t the task + * @param e the exception + */ + void unobservedException(Task t, UnobservedTaskException e); + } + /** * @deprecated Please use {@link com.parse.boltsinternal.TaskCompletionSource} instead. */ @@ -1013,9 +965,4 @@ public class TaskCompletionSource extends com.parse.boltsinternal.TaskCompletion /* package */ TaskCompletionSource() { } } - - private static Task TASK_NULL = new Task<>(null); - private static Task TASK_TRUE = new Task<>((Boolean) true); - private static Task TASK_FALSE = new Task<>((Boolean) false); - private static Task TASK_CANCELLED = new Task(true); } diff --git a/bolts-tasks/src/test/java/com/parse/boltsinternal/CancellationTest.java b/bolts-tasks/src/test/java/com/parse/boltsinternal/CancellationTest.java index bc390acbe..78190518c 100644 --- a/bolts-tasks/src/test/java/com/parse/boltsinternal/CancellationTest.java +++ b/bolts-tasks/src/test/java/com/parse/boltsinternal/CancellationTest.java @@ -111,12 +111,7 @@ public void testTokenCallsRegisteredActionWhenCancelled() { CancellationToken token = cts.getToken(); final Capture result = new Capture<>(); - token.register(new Runnable() { - @Override - public void run() { - result.set("Run!"); - } - }); + token.register(() -> result.set("Run!")); assertNull(result.get()); @@ -133,12 +128,7 @@ public void testCancelledTokenCallsRegisteredActionImmediately() { cts.cancel(); - token.register(new Runnable() { - @Override - public void run() { - result.set("Run!"); - } - }); + token.register(() -> result.set("Run!")); assertNotNull(result.get()); } @@ -150,18 +140,8 @@ public void testTokenDoesNotCallUnregisteredAction() { final Capture result1 = new Capture<>(); final Capture result2 = new Capture<>(); - CancellationTokenRegistration registration1 = token.register(new Runnable() { - @Override - public void run() { - result1.set("Run!"); - } - }); - token.register(new Runnable() { - @Override - public void run() { - result2.set("Run!"); - } - }); + CancellationTokenRegistration registration1 = token.register(() -> result1.set("Run!")); + token.register(() -> result2.set("Run!")); registration1.close(); @@ -176,11 +156,8 @@ public void testCloseCancellationTokenSource() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.getToken(); - token.register(new Runnable() { - @Override - public void run() { - // Do nothing - } + token.register(() -> { + // Do nothing }); cts.close(); diff --git a/bolts-tasks/src/test/java/com/parse/boltsinternal/TaskTest.java b/bolts-tasks/src/test/java/com/parse/boltsinternal/TaskTest.java index 8d096dd46..45052a219 100644 --- a/bolts-tasks/src/test/java/com/parse/boltsinternal/TaskTest.java +++ b/bolts-tasks/src/test/java/com/parse/boltsinternal/TaskTest.java @@ -16,7 +16,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -29,7 +28,7 @@ public class TaskTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private void runTaskTest(Callable> callable) { try { @@ -117,52 +116,38 @@ public void testSynchronousContinuation() { final Task error = Task.forError(new RuntimeException()); final Task cancelled = Task.cancelled(); - complete.continueWith(new Continuation() { - public Void then(Task task) { - assertEquals(complete, task); - assertTrue(task.isCompleted()); - assertEquals(5, task.getResult().intValue()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - return null; - } + complete.continueWith((Continuation) task -> { + assertEquals(complete, task); + assertTrue(task.isCompleted()); + assertEquals(5, task.getResult().intValue()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + return null; }); - error.continueWith(new Continuation() { - public Void then(Task task) { - assertEquals(error, task); - assertTrue(task.isCompleted()); - assertTrue(task.getError() instanceof RuntimeException); - assertTrue(task.isFaulted()); - assertFalse(task.isCancelled()); - return null; - } + error.continueWith((Continuation) task -> { + assertEquals(error, task); + assertTrue(task.isCompleted()); + assertTrue(task.getError() instanceof RuntimeException); + assertTrue(task.isFaulted()); + assertFalse(task.isCancelled()); + return null; }); - cancelled.continueWith(new Continuation() { - public Void then(Task task) { - assertEquals(cancelled, task); - assertTrue(cancelled.isCompleted()); - assertFalse(cancelled.isFaulted()); - assertTrue(cancelled.isCancelled()); - return null; - } + cancelled.continueWith((Continuation) task -> { + assertEquals(cancelled, task); + assertTrue(cancelled.isCompleted()); + assertFalse(cancelled.isFaulted()); + assertTrue(cancelled.isCancelled()); + return null; }); } @Test public void testSynchronousChaining() { Task first = Task.forResult(1); - Task second = first.continueWith(new Continuation() { - public Integer then(Task task) { - return 2; - } - }); - Task third = second.continueWithTask(new Continuation>() { - public Task then(Task task) { - return Task.forResult(3); - } - }); + Task second = first.continueWith(task -> 2); + Task third = second.continueWithTask(task -> Task.forResult(3)); assertTrue(first.isCompleted()); assertTrue(second.isCompleted()); assertTrue(third.isCompleted()); @@ -174,10 +159,8 @@ public Task then(Task task) { @Test public void testSynchronousCancellation() { Task first = Task.forResult(1); - Task second = first.continueWith(new Continuation() { - public Integer then(Task task) { - throw new CancellationException(); - } + Task second = first.continueWith(task -> { + throw new CancellationException(); }); assertTrue(first.isCompleted()); assertTrue(second.isCancelled()); @@ -189,11 +172,9 @@ public void testSynchronousContinuationTokenAlreadyCancelled() { final Capture continuationRun = new Capture<>(false); cts.cancel(); Task first = Task.forResult(1); - Task second = first.continueWith(new Continuation() { - public Integer then(Task task) { - continuationRun.set(true); - return 2; - } + Task second = first.continueWith(task -> { + continuationRun.set(true); + return 2; }, cts.getToken()); assertTrue(first.isCompleted()); assertTrue(second.isCancelled()); @@ -203,10 +184,8 @@ public Integer then(Task task) { @Test public void testSynchronousTaskCancellation() { Task first = Task.forResult(1); - Task second = first.continueWithTask(new Continuation>() { - public Task then(Task task) { - throw new CancellationException(); - } + Task second = first.continueWithTask(task -> { + throw new CancellationException(); }); assertTrue(first.isCompleted()); assertTrue(second.isCancelled()); @@ -214,21 +193,13 @@ public Task then(Task task) { @Test public void testBackgroundCall() { - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.callInBackground(new Callable() { - public Integer call() throws Exception { - Thread.sleep(100); - return 5; - } - }).continueWith(new Continuation() { - public Void then(Task task) { - assertEquals(5, task.getResult().intValue()); - return null; - } - }); - } - }); + runTaskTest(() -> Task.callInBackground(() -> { + Thread.sleep(100); + return 5; + }).continueWith((Continuation) task -> { + assertEquals(5, task.getResult().intValue()); + return null; + })); } @Test @@ -238,16 +209,13 @@ public void testBackgroundCallTokenCancellation() { final Capture waitingToBeCancelled = new Capture<>(false); final Object cancelLock = new Object(); - Task task = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - synchronized (cancelLock) { - waitingToBeCancelled.set(true); - cancelLock.wait(); - } - ct.throwIfCancellationRequested(); - return 5; + Task task = Task.callInBackground(() -> { + synchronized (cancelLock) { + waitingToBeCancelled.set(true); + cancelLock.wait(); } + ct.throwIfCancellationRequested(); + return 5; }); while (true) { @@ -279,30 +247,20 @@ public void testBackgroundCallTokenAlreadyCancelled() { final CancellationTokenSource cts = new CancellationTokenSource(); cts.cancel(); - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.callInBackground(new Callable() { - public Integer call() throws Exception { - Thread.sleep(100); - return 5; - } - }, cts.getToken()).continueWith(new Continuation() { - public Void then(Task task) { - assertTrue(task.isCancelled()); - return null; - } - }); - } - }); + runTaskTest(() -> Task.callInBackground(() -> { + Thread.sleep(100); + return 5; + }, cts.getToken()).continueWith((Continuation) task -> { + assertTrue(task.isCancelled()); + return null; + })); } @Test public void testBackgroundCallWaiting() throws Exception { - Task task = Task.callInBackground(new Callable() { - public Integer call() throws Exception { - Thread.sleep(100); - return 5; - } + Task task = Task.callInBackground(() -> { + Thread.sleep(100); + return 5; }); task.waitForCompletion(); assertTrue(task.isCompleted()); @@ -313,14 +271,12 @@ public Integer call() throws Exception { public void testBackgroundCallWaitingWithTimeouts() throws Exception { final Object sync = new Object(); - Task task = Task.callInBackground(new Callable() { - public Integer call() throws Exception { - synchronized (sync) { - sync.wait(); - Thread.sleep(100); - } - return 5; + Task task = Task.callInBackground(() -> { + synchronized (sync) { + sync.wait(); + Thread.sleep(100); } + return 5; }); // wait -> timeout assertFalse(task.waitForCompletion(100, TimeUnit.MILLISECONDS)); @@ -336,11 +292,9 @@ public Integer call() throws Exception { @Test public void testBackgroundCallWaitingOnError() throws Exception { - Task task = Task.callInBackground(new Callable() { - public Integer call() throws Exception { - Thread.sleep(100); - throw new RuntimeException(); - } + Task task = Task.callInBackground(() -> { + Thread.sleep(100); + throw new RuntimeException(); }); task.waitForCompletion(); assertTrue(task.isCompleted()); @@ -349,17 +303,10 @@ public Integer call() throws Exception { @Test public void testBackgroundCallWaitOnCancellation() throws Exception { - Task task = Task.callInBackground(new Callable() { - public Integer call() throws Exception { - Thread.sleep(100); - return 5; - } - }).continueWithTask(new Continuation>() { - - public Task then(Task task) { - return Task.cancelled(); - } - }); + Task task = Task.callInBackground(() -> { + Thread.sleep(100); + return 5; + }).continueWithTask(task1 -> Task.cancelled()); task.waitForCompletion(); assertTrue(task.isCompleted()); assertTrue(task.isCancelled()); @@ -367,51 +314,32 @@ public Task then(Task task) { @Test public void testBackgroundError() { - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.callInBackground(new Callable() { - public Integer call() throws Exception { - throw new IllegalStateException(); - } - }).continueWith(new Continuation() { - public Void then(Task task) { - assertTrue(task.isFaulted()); - assertTrue(task.getError() instanceof IllegalStateException); - return null; - } - }); - } - }); + runTaskTest(() -> Task.callInBackground((Callable) () -> { + throw new IllegalStateException(); + }).continueWith((Continuation) task -> { + assertTrue(task.isFaulted()); + assertTrue(task.getError() instanceof IllegalStateException); + return null; + })); } @Test public void testBackgroundCancellation() { - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.callInBackground(new Callable() { - public Void call() throws Exception { - throw new CancellationException(); - } - }).continueWith(new Continuation() { - public Void then(Task task) { - assertTrue(task.isCancelled()); - return null; - } - }); - } - }); + runTaskTest(() -> Task.callInBackground((Callable) () -> { + throw new CancellationException(); + }).continueWith((Continuation) task -> { + assertTrue(task.isCancelled()); + return null; + })); } @Test public void testUnobservedError() throws InterruptedException { try { final Object sync = new Object(); - Task.setUnobservedExceptionHandler(new Task.UnobservedExceptionHandler() { - @Override - public void unobservedException(Task t, UnobservedTaskException e) { - synchronized (sync) { - sync.notify(); - } + Task.setUnobservedExceptionHandler((t, e) -> { + synchronized (sync) { + sync.notify(); } }); @@ -428,11 +356,8 @@ public void unobservedException(Task t, UnobservedTaskException e) { // runs in a separate method to ensure it is out of scope. private void startFailedTask() throws InterruptedException { - Task.call(new Callable() { - @Override - public Object call() throws Exception { - throw new RuntimeException(); - } + Task.call(() -> { + throw new RuntimeException(); }).waitForCompletion(); } @@ -447,206 +372,152 @@ public void testWhenAllNoTasks() { @Test public void testWhenAnyResultFirstSuccess() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteSuccess = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - Thread.sleep(50); - return 10; - } - }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteSuccess); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAnyResult(tasks).continueWith(new Continuation, Void>() { - @Override - public Void then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteSuccess, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertFalse(task.getResult().isCancelled()); - assertFalse(task.getResult().isFaulted()); - assertEquals(10, (int) task.getResult().getResult()); - return null; - } - }); - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteSuccess = Task.callInBackground(() -> { + Thread.sleep(50); + return 10; + }); + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteSuccess); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAnyResult(tasks).continueWith((Continuation, Void>) task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteSuccess, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertFalse(task.getResult().isCancelled()); + assertFalse(task.getResult().isFaulted()); + assertEquals(10, (int) task.getResult().getResult()); + return null; + }); }); } @Test public void testWhenAnyFirstSuccess() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteSuccess = Task.callInBackground(new Callable() { - @Override - public String call() throws Exception { - Thread.sleep(50); - return "SUCCESS"; - } - }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteSuccess); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAny(tasks).continueWith(new Continuation, Object>() { - @Override - public Object then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteSuccess, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertFalse(task.getResult().isCancelled()); - assertFalse(task.getResult().isFaulted()); - assertEquals("SUCCESS", task.getResult().getResult()); - return null; - } - }); - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteSuccess = Task.callInBackground(() -> { + Thread.sleep(50); + return "SUCCESS"; + }); + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteSuccess); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAny(tasks).continueWith(task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteSuccess, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertFalse(task.getResult().isCancelled()); + assertFalse(task.getResult().isFaulted()); + assertEquals("SUCCESS", task.getResult().getResult()); + return null; + }); }); } @Test public void testWhenAnyResultFirstError() { final Exception error = new RuntimeException("This task failed."); - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteError = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - Thread.sleep(50); - throw error; - } - }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteError); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAnyResult(tasks).continueWith(new Continuation, Object>() { - @Override - public Object then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteError, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertFalse(task.getResult().isCancelled()); - assertTrue(task.getResult().isFaulted()); - assertEquals(error, task.getResult().getError()); - return null; - } - }); - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteError = Task.callInBackground(() -> { + Thread.sleep(50); + throw error; + }); + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteError); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAnyResult(tasks).continueWith(task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteError, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertFalse(task.getResult().isCancelled()); + assertTrue(task.getResult().isFaulted()); + assertEquals(error, task.getResult().getError()); + return null; + }); }); } @Test public void testWhenAnyFirstError() { final Exception error = new RuntimeException("This task failed."); - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteError = Task.callInBackground(new Callable() { - @Override - public String call() throws Exception { - Thread.sleep(50); - throw error; - } - }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteError); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAny(tasks).continueWith(new Continuation, Object>() { - @Override - public Object then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteError, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertFalse(task.getResult().isCancelled()); - assertTrue(task.getResult().isFaulted()); - assertEquals(error, task.getResult().getError()); - return null; - } - }); - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteError = Task.callInBackground(() -> { + Thread.sleep(50); + throw error; + }); + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteError); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAny(tasks).continueWith(task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteError, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertFalse(task.getResult().isCancelled()); + assertTrue(task.getResult().isFaulted()); + assertEquals(error, task.getResult().getError()); + return null; + }); }); } @Test public void testWhenAnyResultFirstCancelled() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteCancelled = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - Thread.sleep(50); - throw new CancellationException(); - } - }); + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteCancelled = Task.callInBackground(() -> { + Thread.sleep(50); + throw new CancellationException(); + }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteCancelled); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAnyResult(tasks).continueWith(new Continuation, Object>() { - @Override - public Object then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteCancelled, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertTrue(task.getResult().isCancelled()); - assertFalse(task.getResult().isFaulted()); - return null; - } - }); - } + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteCancelled); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAnyResult(tasks).continueWith(task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteCancelled, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertTrue(task.getResult().isCancelled()); + assertFalse(task.getResult().isFaulted()); + return null; + }); }); } @Test public void testWhenAnyFirstCancelled() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - final Task firstToCompleteCancelled = Task.callInBackground(new Callable() { - @Override - public String call() throws Exception { - Thread.sleep(50); - throw new CancellationException(); - } - }); - tasks.addAll(launchTasksWithRandomCompletions(5)); - tasks.add(firstToCompleteCancelled); - tasks.addAll(launchTasksWithRandomCompletions(5)); - return Task.whenAny(tasks).continueWith(new Continuation, Object>() { - @Override - public Object then(Task> task) throws Exception { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(firstToCompleteCancelled, task.getResult()); - assertTrue(task.getResult().isCompleted()); - assertTrue(task.getResult().isCancelled()); - assertFalse(task.getResult().isFaulted()); - return null; - } - }); - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + final Task firstToCompleteCancelled = Task.callInBackground(() -> { + Thread.sleep(50); + throw new CancellationException(); + }); + tasks.addAll(launchTasksWithRandomCompletions(5)); + tasks.add(firstToCompleteCancelled); + tasks.addAll(launchTasksWithRandomCompletions(5)); + return Task.whenAny(tasks).continueWith(task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(firstToCompleteCancelled, task.getResult()); + assertTrue(task.getResult().isCompleted()); + assertTrue(task.getResult().isCancelled()); + assertFalse(task.getResult().isFaulted()); + return null; + }); }); } @@ -663,18 +534,15 @@ public Object then(Task> task) throws Exception { private Collection> launchTasksWithRandomCompletions(int numberOfTasksToLaunch) { final ArrayList> tasks = new ArrayList<>(); for (int i = 0; i < numberOfTasksToLaunch; i++) { - Task task = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - Thread.sleep((long) (500 + (Math.random() * 100))); - double rand = Math.random(); - if (rand >= 0.7) { - throw new RuntimeException("This task failed."); - } else if (rand >= 0.4) { - throw new CancellationException(); - } - return (int) (Math.random() * 1000); + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (500 + (Math.random() * 100))); + double rand = Math.random(); + if (rand >= 0.7) { + throw new RuntimeException("This task failed."); + } else if (rand >= 0.4) { + throw new CancellationException(); } + return (int) (Math.random() * 1000); }); tasks.add(task); } @@ -683,34 +551,25 @@ public Integer call() throws Exception { @Test public void testWhenAllSuccess() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - Task task = Task.callInBackground(new Callable() { - @Override - public Void call() throws Exception { - Thread.sleep((long) (Math.random() * 100)); - return null; - } - }); - tasks.add(task); - } - return Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - - for (Task t : tasks) { - assertTrue(t.isCompleted()); - } - return null; - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (Math.random() * 100)); + return null; }); + tasks.add(task); } + return Task.whenAll(tasks).continueWith((Continuation) task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + + for (Task t : tasks) { + assertTrue(t.isCompleted()); + } + return null; + }); }); } @@ -718,41 +577,32 @@ public Void then(Task task) { public void testWhenAllOneError() { final Exception error = new RuntimeException("This task failed."); - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - final int number = i; - Task task = Task.callInBackground(new Callable() { - @Override - public Void call() throws Exception { - Thread.sleep((long) (Math.random() * 100)); - if (number == 10) { - throw error; - } - return null; - } - }); - tasks.add(task); - } - return Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - assertTrue(task.isCompleted()); - assertTrue(task.isFaulted()); - assertFalse(task.isCancelled()); - - assertFalse(task.getError() instanceof AggregateException); - assertEquals(error, task.getError()); - - for (Task t : tasks) { - assertTrue(t.isCompleted()); - } - return null; + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + final int number = i; + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (Math.random() * 100)); + if (number == 10) { + throw error; } + return null; }); + tasks.add(task); } + return Task.whenAll(tasks).continueWith((Continuation) task -> { + assertTrue(task.isCompleted()); + assertTrue(task.isFaulted()); + assertFalse(task.isCancelled()); + + assertFalse(task.getError() instanceof AggregateException); + assertEquals(error, task.getError()); + + for (Task t : tasks) { + assertTrue(t.isCompleted()); + } + return null; + }); }); } @@ -761,94 +611,76 @@ public void testWhenAllTwoErrors() { final Exception error0 = new RuntimeException("This task failed (0)."); final Exception error1 = new RuntimeException("This task failed (1)."); - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - final int number = i; - Task task = Task.callInBackground(new Callable() { - @Override - public Void call() throws Exception { - Thread.sleep((long) (number * 10)); - if (number == 10) { - throw error0; - } else if (number == 11) { - throw error1; - } - return null; - } - }); - tasks.add(task); - } - return Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - assertTrue(task.isCompleted()); - assertTrue(task.isFaulted()); - assertFalse(task.isCancelled()); - - assertTrue(task.getError() instanceof AggregateException); - assertEquals(2, ((AggregateException) task.getError()).getInnerThrowables().size()); - assertEquals(error0, ((AggregateException) task.getError()).getInnerThrowables().get(0)); - assertEquals(error1, ((AggregateException) task.getError()).getInnerThrowables().get(1)); - assertEquals(error0, task.getError().getCause()); - - for (Task t : tasks) { - assertTrue(t.isCompleted()); - } - return null; - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + final int number = i; + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (number * 10)); + if (number == 10) { + throw error0; + } else if (number == 11) { + throw error1; + } + return null; }); + tasks.add(task); } + return Task.whenAll(tasks).continueWith((Continuation) task -> { + assertTrue(task.isCompleted()); + assertTrue(task.isFaulted()); + assertFalse(task.isCancelled()); + + assertTrue(task.getError() instanceof AggregateException); + assertEquals(2, ((AggregateException) task.getError()).getInnerThrowables().size()); + assertEquals(error0, ((AggregateException) task.getError()).getInnerThrowables().get(0)); + assertEquals(error1, ((AggregateException) task.getError()).getInnerThrowables().get(1)); + assertEquals(error0, task.getError().getCause()); + + for (Task t : tasks) { + assertTrue(t.isCompleted()); + } + return null; + }); }); } @Test public void testWhenAllCancel() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final ArrayList> tasks = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - - final int number = i; - Task.callInBackground(new Callable() { - @Override - public Void call() throws Exception { - Thread.sleep((long) (Math.random() * 100)); - if (number == 10) { - tcs.setCancelled(); - } else { - tcs.setResult(null); - } - return null; - } - }); - - tasks.add(tcs.getTask()); - } - return Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertTrue(task.isCancelled()); - - for (Task t : tasks) { - assertTrue(t.isCompleted()); - } - return null; - } + runTaskTest(() -> { + final ArrayList> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + final TaskCompletionSource tcs = new TaskCompletionSource<>(); + + final int number = i; + Task.callInBackground((Callable) () -> { + Thread.sleep((long) (Math.random() * 100)); + if (number == 10) { + tcs.setCancelled(); + } else { + tcs.setResult(null); + } + return null; }); + + tasks.add(tcs.getTask()); } + return Task.whenAll(tasks).continueWith((Continuation) task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertTrue(task.isCancelled()); + + for (Task t : tasks) { + assertTrue(t.isCompleted()); + } + return null; + }); }); } @Test public void testWhenAllResultNoTasks() { - Task> task = Task.whenAllResult(new ArrayList>()); + Task> task = Task.whenAllResult(new ArrayList<>()); assertTrue(task.isCompleted()); assertFalse(task.isCancelled()); @@ -858,82 +690,59 @@ public void testWhenAllResultNoTasks() { @Test public void testWhenAllResultSuccess() { - runTaskTest(new Callable>() { - @Override - public Task call() throws Exception { - final List> tasks = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - final int number = (i + 1); - Task task = Task.callInBackground(new Callable() { - @Override - public Integer call() throws Exception { - Thread.sleep((long) (Math.random() * 100)); - return number; - } - }); - tasks.add(task); - } - return Task.whenAllResult(tasks).continueWith(new Continuation, Void>() { - @Override - public Void then(Task> task) { - assertTrue(task.isCompleted()); - assertFalse(task.isFaulted()); - assertFalse(task.isCancelled()); - assertEquals(tasks.size(), task.getResult().size()); - - for (int i = 0; i < tasks.size(); i++) { - Task t = tasks.get(i); - assertTrue(t.isCompleted()); - assertEquals(t.getResult(), task.getResult().get(i)); - } - - return null; - } + runTaskTest(() -> { + final List> tasks = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + final int number = (i + 1); + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (Math.random() * 100)); + return number; }); + tasks.add(task); } + return Task.whenAllResult(tasks).continueWith((Continuation, Void>) task -> { + assertTrue(task.isCompleted()); + assertFalse(task.isFaulted()); + assertFalse(task.isCancelled()); + assertEquals(tasks.size(), task.getResult().size()); + + for (int i = 0; i < tasks.size(); i++) { + Task t = tasks.get(i); + assertTrue(t.isCompleted()); + assertEquals(t.getResult(), task.getResult().get(i)); + } + + return null; + }); }); } @Test public void testAsyncChaining() { - runTaskTest(new Callable>() { - public Task call() throws Exception { - final ArrayList sequence = new ArrayList<>(); - Task result = Task.forResult(null); + runTaskTest(() -> { + final ArrayList sequence = new ArrayList<>(); + Task result = Task.forResult(null); + for (int i = 0; i < 20; i++) { + final int taskNumber = i; + result = result.continueWithTask(task -> Task.callInBackground(() -> { + sequence.add(taskNumber); + return null; + })); + } + result = result.continueWith(task -> { + assertEquals(20, sequence.size()); for (int i = 0; i < 20; i++) { - final int taskNumber = i; - result = result.continueWithTask(new Continuation>() { - public Task then(Task task) { - return Task.callInBackground(new Callable() { - public Void call() throws Exception { - sequence.add(taskNumber); - return null; - } - }); - } - }); + assertEquals(i, sequence.get(i).intValue()); } - result = result.continueWith(new Continuation() { - public Void then(Task task) { - assertEquals(20, sequence.size()); - for (int i = 0; i < 20; i++) { - assertEquals(i, sequence.get(i).intValue()); - } - return null; - } - }); - return result; - } + return null; + }); + return result; }); } @Test public void testOnSuccess() { - Continuation continuation = new Continuation() { - public Integer then(Task task) { - return task.getResult() + 1; - } - }; + Continuation continuation = task -> task.getResult() + 1; Task complete = Task.forResult(5).onSuccess(continuation); Task error = Task.forError(new IllegalStateException()).onSuccess( continuation); @@ -956,11 +765,7 @@ public Integer then(Task task) { @Test public void testOnSuccessTask() { - Continuation> continuation = new Continuation>() { - public Task then(Task task) { - return Task.forResult(task.getResult() + 1); - } - }; + Continuation> continuation = task -> Task.forResult(task.getResult() + 1); Task complete = Task.forResult(5).onSuccessTask(continuation); Task error = Task.forError(new IllegalStateException()).onSuccessTask( continuation); @@ -984,103 +789,56 @@ public Task then(Task task) { @Test public void testContinueWhile() { final AtomicInteger count = new AtomicInteger(0); - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.forResult(null).continueWhile(new Callable() { - public Boolean call() throws Exception { - return count.get() < 10; - } - }, new Continuation>() { - public Task then(Task task) throws Exception { - count.incrementAndGet(); - return null; - } - }).continueWith(new Continuation() { - public Void then(Task task) throws Exception { - assertEquals(10, count.get()); - return null; - } - }); - } - }); + runTaskTest(() -> Task.forResult(null).continueWhile(() -> count.get() < 10, task -> { + count.incrementAndGet(); + return null; + }).continueWith((Continuation) task -> { + assertEquals(10, count.get()); + return null; + })); } @Test public void testContinueWhileAsync() { final AtomicInteger count = new AtomicInteger(0); - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.forResult(null).continueWhile(new Callable() { - public Boolean call() throws Exception { - return count.get() < 10; - } - }, new Continuation>() { - public Task then(Task task) throws Exception { - count.incrementAndGet(); - return null; - } - }, Executors.newCachedThreadPool()).continueWith(new Continuation() { - public Void then(Task task) throws Exception { - assertEquals(10, count.get()); - return null; - } - }); - } - }); + runTaskTest(() -> Task.forResult(null).continueWhile(() -> count.get() < 10, task -> { + count.incrementAndGet(); + return null; + }, Executors.newCachedThreadPool()).continueWith((Continuation) task -> { + assertEquals(10, count.get()); + return null; + })); } @Test public void testContinueWhileAsyncCancellation() { final AtomicInteger count = new AtomicInteger(0); final CancellationTokenSource cts = new CancellationTokenSource(); - runTaskTest(new Callable>() { - public Task call() throws Exception { - return Task.forResult(null).continueWhile(new Callable() { - public Boolean call() throws Exception { - return count.get() < 10; - } - }, new Continuation>() { - public Task then(Task task) - throws Exception { - if (count.incrementAndGet() == 5) { - cts.cancel(); - } - return null; - } - }, Executors.newCachedThreadPool(), - cts.getToken()).continueWith(new Continuation() { - public Void then(Task task) throws Exception { - assertTrue(task.isCancelled()); - assertEquals(5, count.get()); - return null; + runTaskTest(() -> Task.forResult(null).continueWhile(() -> count.get() < 10, task -> { + if (count.incrementAndGet() == 5) { + cts.cancel(); } - }); - } - }); + return null; + }, Executors.newCachedThreadPool(), + cts.getToken()).continueWith((Continuation) task -> { + assertTrue(task.isCancelled()); + assertEquals(5, count.get()); + return null; + })); } @Test public void testCallWithBadExecutor() { final RuntimeException exception = new RuntimeException("BAD EXECUTORS"); - Task.call(new Callable() { - public Integer call() throws Exception { - return 1; - } - }, new Executor() { - @Override - public void execute(Runnable command) { - throw exception; - } - }).continueWith(new Continuation() { - @Override - public Object then(Task task) throws Exception { - assertTrue(task.isFaulted()); - assertTrue(task.getError() instanceof ExecutorException); - assertEquals(exception, task.getError().getCause()); + Task.call(() -> 1, command -> { + throw exception; + }).continueWith(task -> { + assertTrue(task.isFaulted()); + assertTrue(task.getError() instanceof ExecutorException); + assertEquals(exception, task.getError().getCause()); - return null; - } + return null; }); } @@ -1088,29 +846,14 @@ public Object then(Task task) throws Exception { public void testContinueWithBadExecutor() { final RuntimeException exception = new RuntimeException("BAD EXECUTORS"); - Task.call(new Callable() { - public Integer call() throws Exception { - return 1; - } - }).continueWith(new Continuation() { - @Override - public Integer then(Task task) throws Exception { - return task.getResult(); - } - }, new Executor() { - @Override - public void execute(Runnable command) { - throw exception; - } - }).continueWith(new Continuation() { - @Override - public Object then(Task task) throws Exception { - assertTrue(task.isFaulted()); - assertTrue(task.getError() instanceof ExecutorException); - assertEquals(exception, task.getError().getCause()); + Task.call(() -> 1).continueWith(Task::getResult, command -> { + throw exception; + }).continueWith(task -> { + assertTrue(task.isFaulted()); + assertTrue(task.getError() instanceof ExecutorException); + assertEquals(exception, task.getError().getCause()); - return null; - } + return null; }); } @@ -1118,29 +861,14 @@ public Object then(Task task) throws Exception { public void testContinueWithTaskAndBadExecutor() { final RuntimeException exception = new RuntimeException("BAD EXECUTORS"); - Task.call(new Callable() { - public Integer call() throws Exception { - return 1; - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - return task; - } - }, new Executor() { - @Override - public void execute(Runnable command) { - throw exception; - } - }).continueWith(new Continuation() { - @Override - public Object then(Task task) throws Exception { - assertTrue(task.isFaulted()); - assertTrue(task.getError() instanceof ExecutorException); - assertEquals(exception, task.getError().getCause()); + Task.call(() -> 1).continueWithTask(task -> task, command -> { + throw exception; + }).continueWith(task -> { + assertTrue(task.isFaulted()); + assertTrue(task.getError() instanceof ExecutorException); + assertEquals(exception, task.getError().getCause()); - return null; - } + return null; }); } diff --git a/build.gradle b/build.gradle index 586f69f2c..0f5256192 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,17 @@ buildscript { - ext.kotlin_version = "1.4.10" + ext.kotlin_version = "1.5.20" + ext.jacocoVersion = '0.8.7' repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:3.6.2" + classpath "com.android.tools.build:gradle:4.2.2" classpath "org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.3" classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jacoco:org.jacoco.core:$jacocoVersion" + classpath "com.dicedmelon.gradle:jacoco-android:0.1.5" } } @@ -28,8 +31,8 @@ task clean(type: Delete) { } ext { - compileSdkVersion = 29 + compileSdkVersion = 30 minSdkVersion = 14 - targetSdkVersion = 29 + targetSdkVersion = 30 } diff --git a/coroutines/build.gradle b/coroutines/build.gradle index 00e93ae2a..cad4cb37c 100644 --- a/coroutines/build.gradle +++ b/coroutines/build.gradle @@ -29,14 +29,14 @@ android { } ext { - coroutinesVersion = "1.3.9" + coroutinesVersion = "1.5.0" } dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/coroutines/src/main/java/com/parse/coroutines/ParseQueryCoroutinesBuilder.kt b/coroutines/src/main/java/com/parse/coroutines/ParseQueryCoroutinesBuilder.kt index 2030acfc0..8b2d30988 100644 --- a/coroutines/src/main/java/com/parse/coroutines/ParseQueryCoroutinesBuilder.kt +++ b/coroutines/src/main/java/com/parse/coroutines/ParseQueryCoroutinesBuilder.kt @@ -14,7 +14,7 @@ fun CoroutineScope.launchQuery( query: ParseQuery, context: CoroutineContext = EmptyCoroutineContext, block: suspend ParseQueryOperation.() -> Unit -) : Job { +): Job { return launch(context) { block.invoke(ParseQueryOperationImpl(query)) } diff --git a/coroutines/src/main/java/com/parse/coroutines/ParseQueryOperationImpl.kt b/coroutines/src/main/java/com/parse/coroutines/ParseQueryOperationImpl.kt index 4a6a8f53a..cdf41b612 100644 --- a/coroutines/src/main/java/com/parse/coroutines/ParseQueryOperationImpl.kt +++ b/coroutines/src/main/java/com/parse/coroutines/ParseQueryOperationImpl.kt @@ -3,7 +3,8 @@ package com.parse.coroutines import com.parse.ParseObject import com.parse.ParseQuery -class ParseQueryOperationImpl(private val query: ParseQuery) : ParseQueryOperation { +class ParseQueryOperationImpl(private val query: ParseQuery) : + ParseQueryOperation { override suspend fun find(): List = query.findInternal() diff --git a/facebook/build.gradle b/facebook/build.gradle index 08627f308..c517677f7 100644 --- a/facebook/build.gradle +++ b/facebook/build.gradle @@ -23,9 +23,9 @@ dependencies { api "com.facebook.android:facebook-login:6.3.0" implementation project(":parse") - testImplementation "junit:junit:4.13" - testImplementation "org.mockito:mockito-core:1.10.19" - testImplementation "org.robolectric:robolectric:3.8" + testImplementation "junit:junit:4.13.2" + testImplementation "org.mockito:mockito-core:3.6.28" + testImplementation "org.robolectric:robolectric:4.6" } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/facebook/src/main/java/com/parse/facebook/FacebookController.java b/facebook/src/main/java/com/parse/facebook/FacebookController.java index 8a3b85937..cef2ae3c6 100644 --- a/facebook/src/main/java/com/parse/facebook/FacebookController.java +++ b/facebook/src/main/java/com/parse/facebook/FacebookController.java @@ -23,6 +23,7 @@ import com.facebook.FacebookSdk; import com.facebook.login.LoginManager; import com.facebook.login.LoginResult; +import com.parse.boltsinternal.Task; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -36,10 +37,10 @@ import java.util.Set; import java.util.SimpleTimeZone; -import com.parse.boltsinternal.Task; - class FacebookController { + // Used as default activityCode. From FacebookSdk.java. + public static final int DEFAULT_AUTH_ACTIVITY_CODE = 0xface; /** * Precise date format required for auth expiration data. */ @@ -47,28 +48,18 @@ class FacebookController { new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); private static final DateFormat IMPRECISE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - - static { - PRECISE_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT")); - IMPRECISE_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT")); - } - - // Used as default activityCode. From FacebookSdk.java. - public static final int DEFAULT_AUTH_ACTIVITY_CODE = 0xface; - private static final String KEY_USER_ID = "id"; private static final String KEY_ACCESS_TOKEN = "access_token"; private static final String KEY_EXPIRATION_DATE = "expiration_date"; private static final String KEY_REFRESH_DATE = "last_refresh_date"; private static final String KEY_PERMISSIONS = "permissions"; - // Mirrors com.facebook.internal.LoginAuthorizationType.java - public enum LoginAuthorizationType { - READ, PUBLISH + static { + PRECISE_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT")); + IMPRECISE_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "GMT")); } private final FacebookSdkDelegate facebookSdkDelegate; - private CallbackManager callbackManager; FacebookController(FacebookSdkDelegate facebookSdkDelegate) { @@ -212,7 +203,7 @@ public void setAuthData(Map authData) String permissionsCommaDelineated = authData.get(KEY_PERMISSIONS); Set permissions = null; if (permissionsCommaDelineated != null && !permissionsCommaDelineated.isEmpty()) { - String permissionsArray[] = permissionsCommaDelineated.split(","); + String[] permissionsArray = permissionsCommaDelineated.split(","); permissions = new HashSet<>(Arrays.asList(permissionsArray)); } @@ -229,20 +220,6 @@ public void setAuthData(Map authData) facebookSdkDelegate.setCurrentAccessToken(accessToken); } - /* package */ interface FacebookSdkDelegate { - void initialize(Context context, int callbackRequestCodeOffset); - - String getApplicationId(); - - AccessToken getCurrentAccessToken(); - - void setCurrentAccessToken(AccessToken token); - - CallbackManager createCallbackManager(); - - LoginManager getLoginManager(); - } - /** * Convert String representation of a date into Date object. *

@@ -262,6 +239,25 @@ private Date parseDateString(String source) throws java.text.ParseException { } } + // Mirrors com.facebook.internal.LoginAuthorizationType.java + public enum LoginAuthorizationType { + READ, PUBLISH + } + + /* package */ interface FacebookSdkDelegate { + void initialize(Context context, int callbackRequestCodeOffset); + + String getApplicationId(); + + AccessToken getCurrentAccessToken(); + + void setCurrentAccessToken(AccessToken token); + + CallbackManager createCallbackManager(); + + LoginManager getLoginManager(); + } + private static class FacebookSdkDelegateImpl implements FacebookSdkDelegate { @Override public void initialize(Context context, int callbackRequestCodeOffset) { diff --git a/facebook/src/main/java/com/parse/facebook/ParseFacebookUtils.java b/facebook/src/main/java/com/parse/facebook/ParseFacebookUtils.java index 354714545..fd152d4a7 100644 --- a/facebook/src/main/java/com/parse/facebook/ParseFacebookUtils.java +++ b/facebook/src/main/java/com/parse/facebook/ParseFacebookUtils.java @@ -20,14 +20,13 @@ import com.parse.ParseException; import com.parse.ParseUser; import com.parse.SaveCallback; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; import java.util.Collection; import java.util.Collections; import java.util.Map; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * Provides a set of utilities for using Parse with Facebook. *

@@ -80,6 +79,10 @@ public final class ParseFacebookUtils { static FacebookController controller; static ParseUserDelegate userDelegate = new ParseUserDelegateImpl(); + private ParseFacebookUtils() { + // do nothing + } + /** * @param user A {@link com.parse.ParseUser} object. * @return {@code true} if the user is linked to a Facebook account. @@ -113,15 +116,12 @@ public static void initialize(Context context) { public static void initialize(Context context, int callbackRequestCodeOffset) { synchronized (lock) { getController().initialize(context, callbackRequestCodeOffset); - userDelegate.registerAuthenticationCallback(AUTH_TYPE, new AuthenticationCallback() { - @Override - public boolean onRestore(Map authData) { - try { - getController().setAuthData(authData); - return true; - } catch (Exception e) { - return false; - } + userDelegate.registerAuthenticationCallback(AUTH_TYPE, authData -> { + try { + getController().setAuthData(authData); + return true; + } catch (Exception e) { + return false; } }); isInitialized = true; @@ -146,6 +146,8 @@ private static FacebookController getController() { } } + //region Log In + /** * The method that should be called from the Activity's or Fragment's onActivityResult method. * @@ -163,8 +165,6 @@ public static boolean onActivityResult(int requestCode, int resultCode, Intent d } } - //region Log In - /** * Log in using a Facebook account using authorization credentials that have already been * obtained. @@ -293,6 +293,10 @@ public static Task logInWithPublishPermissionsInBackground(Fragment f logInWithPublishPermissionsInBackground(fragment, permissions), callback, true); } + //endregion + + //region Link + private static Task logInAsync(Activity activity, Fragment fragment, Collection permissions, FacebookController.LoginAuthorizationType authorizationType) { checkInitialization(); @@ -301,18 +305,9 @@ private static Task logInAsync(Activity activity, Fragment fragment, } return getController().authenticateAsync( - activity, fragment, authorizationType, permissions).onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) throws Exception { - return userDelegate.logInWithInBackground(AUTH_TYPE, task.getResult()); - } - }); + activity, fragment, authorizationType, permissions).onSuccessTask(task -> userDelegate.logInWithInBackground(AUTH_TYPE, task.getResult())); } - //endregion - - //region Link - /** * Link an existing Parse user with a Facebook account using authorization credentials that have * already been obtained. @@ -452,6 +447,10 @@ public static Task linkWithPublishPermissionsInBackground(ParseUser user, linkWithPublishPermissionsInBackground(user, fragment, permissions), callback, true); } + //endregion + + //region Unlink + private static Task linkAsync(final ParseUser user, Activity activity, Fragment fragment, Collection permissions, FacebookController.LoginAuthorizationType authorizationType) { checkInitialization(); @@ -460,18 +459,9 @@ private static Task linkAsync(final ParseUser user, Activity activity, Fra } return getController().authenticateAsync( - activity, fragment, authorizationType, permissions).onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) throws Exception { - return user.linkWithInBackground(AUTH_TYPE, task.getResult()); - } - }); + activity, fragment, authorizationType, permissions).onSuccessTask(task -> user.linkWithInBackground(AUTH_TYPE, task.getResult())); } - //endregion - - //region Unlink - /** * Unlink a user from a Facebook account. This will save the user's data. * @@ -483,6 +473,10 @@ public static Task unlinkInBackground(ParseUser user) { return user.unlinkFromInBackground(AUTH_TYPE); } + //endregion + + //region TaskUtils + /** * Unlink a user from a Facebook account. This will save the user's data. * @@ -494,10 +488,6 @@ public static Task unlinkInBackground(ParseUser user, SaveCallback callbac return callbackOnMainThreadAsync(unlinkInBackground(user), callback, false); } - //endregion - - //region TaskUtils - /** * Calls the callback after a task completes on the main thread, returning a Task that completes * with the same result as the input task after the callback has been run. @@ -516,6 +506,8 @@ private static Task callbackOnMainThreadAsync( return callbackOnMainThreadInternalAsync(task, callback, reportCancellation); } + //endregion + /** * Calls the callback after a task completes on the main thread, returning a Task that completes * with the same result as the input task after the callback has been run. If reportCancellation @@ -527,50 +519,38 @@ private static Task callbackOnMainThreadInternalAsync( return task; } final Task.TaskCompletionSource tcs = Task.create(); - task.continueWith(new Continuation() { - @Override - public Void then(final Task task) throws Exception { - if (task.isCancelled() && !reportCancellation) { - tcs.setCancelled(); - return null; - } - Task.UI_THREAD_EXECUTOR.execute(new Runnable() { - @Override - public void run() { - try { - Exception error = task.getError(); - if (error != null && !(error instanceof ParseException)) { - error = new ParseException(error); - } - if (callback instanceof SaveCallback) { - ((SaveCallback) callback).done((ParseException) error); - } else if (callback instanceof LogInCallback) { - ((LogInCallback) callback).done( - (ParseUser) task.getResult(), (ParseException) error); - } - } finally { - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(task.getResult()); - } - } - } - }); + task.continueWith((Continuation) task1 -> { + if (task1.isCancelled() && !reportCancellation) { + tcs.setCancelled(); return null; } + Task.UI_THREAD_EXECUTOR.execute(() -> { + try { + Exception error = task1.getError(); + if (error != null && !(error instanceof ParseException)) { + error = new ParseException(error); + } + if (callback instanceof SaveCallback) { + ((SaveCallback) callback).done((ParseException) error); + } else if (callback instanceof LogInCallback) { + ((LogInCallback) callback).done( + (ParseUser) task1.getResult(), (ParseException) error); + } + } finally { + if (task1.isCancelled()) { + tcs.setCancelled(); + } else if (task1.isFaulted()) { + tcs.setError(task1.getError()); + } else { + tcs.setResult(task1.getResult()); + } + } + }); + return null; }); return tcs.getTask(); } - //endregion - - private ParseFacebookUtils() { - // do nothing - } - interface ParseUserDelegate { void registerAuthenticationCallback(String authType, AuthenticationCallback callback); diff --git a/facebook/src/test/java/com/parse/facebook/FacebookControllerTest.java b/facebook/src/test/java/com/parse/facebook/FacebookControllerTest.java index 5abcf5e34..af9a4dd28 100644 --- a/facebook/src/test/java/com/parse/facebook/FacebookControllerTest.java +++ b/facebook/src/test/java/com/parse/facebook/FacebookControllerTest.java @@ -20,6 +20,7 @@ import com.facebook.FacebookException; import com.facebook.login.LoginManager; import com.facebook.login.LoginResult; +import com.parse.boltsinternal.Task; import org.junit.After; import org.junit.Before; @@ -41,8 +42,6 @@ import java.util.Set; import java.util.TimeZone; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; @@ -54,7 +53,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -@Config(manifest=Config.NONE) +@Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class FacebookControllerTest { @@ -92,7 +91,7 @@ public void testGetAuthData() { FacebookController controller = new FacebookController(null); Calendar calendar = new GregorianCalendar(2015, 6, 3); - Set permissions = new HashSet(); + Set permissions = new HashSet<>(); permissions.add("user_friends"); permissions.add("profile"); diff --git a/facebook/src/test/java/com/parse/facebook/ParseFacebookUtilsTest.java b/facebook/src/test/java/com/parse/facebook/ParseFacebookUtilsTest.java index 058adc7d9..88839567d 100644 --- a/facebook/src/test/java/com/parse/facebook/ParseFacebookUtilsTest.java +++ b/facebook/src/test/java/com/parse/facebook/ParseFacebookUtilsTest.java @@ -17,6 +17,7 @@ import com.facebook.AccessToken; import com.parse.AuthenticationCallback; import com.parse.ParseUser; +import com.parse.boltsinternal.Task; import org.junit.After; import org.junit.Before; @@ -33,14 +34,14 @@ import java.util.LinkedList; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -50,7 +51,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@Config(manifest=Config.NONE) +@Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class ParseFacebookUtilsTest { @@ -179,7 +180,7 @@ public void testLogInWithActivityAndReadPermissions() { doLogInWith( mock(Activity.class), null, - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.READ); } @@ -188,7 +189,7 @@ public void testLogInWithFragmentAndReadPermissions() { doLogInWith( null, mock(Fragment.class), - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.READ); } @@ -197,7 +198,7 @@ public void testLogInWithActivityAndPublishPermissions() { doLogInWith( mock(Activity.class), null, - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.PUBLISH); } @@ -206,7 +207,7 @@ public void testLogInWithFragmentAndPublishPermissions() { doLogInWith( null, mock(Fragment.class), - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.PUBLISH); } @@ -220,10 +221,10 @@ private void doLogInWith( Map authData = new HashMap<>(); when(controller.authenticateAsync( - any(Activity.class), - any(Fragment.class), + nullable(Activity.class), + nullable(Fragment.class), any(FacebookController.LoginAuthorizationType.class), - anyListOf(String.class))).thenReturn(Task.forResult(authData)); + anyList())).thenReturn(Task.forResult(authData)); ParseFacebookUtils.isInitialized = true; ParseUser user = mock(ParseUser.class); @@ -261,7 +262,7 @@ public void testLinkWithAccessToken() { ParseUser user = mock(ParseUser.class); when(user.linkWithInBackground(anyString(), anyMapOf(String.class, String.class))) - .thenReturn(Task.forResult(null)); + .thenReturn(Task.forResult(null)); AccessToken token = TestUtils.newAccessToken(); Task task = ParseFacebookUtils.linkInBackground(user, token); verify(controller).getAuthData(token); @@ -274,7 +275,7 @@ public void testLinkWithActivityAndReadPermissions() { doLinkWith( mock(Activity.class), null, - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.READ); } @@ -283,7 +284,7 @@ public void testLinkWithFragmentAndReadPermissions() { doLinkWith( null, mock(Fragment.class), - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.READ); } @@ -292,7 +293,7 @@ public void testLinkWithActivityAndPublishPermissions() { doLinkWith( mock(Activity.class), null, - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.PUBLISH); } @@ -301,7 +302,7 @@ public void testLinkWithFragmentAndPublishPermissions() { doLinkWith( null, mock(Fragment.class), - new LinkedList(), + new LinkedList<>(), FacebookController.LoginAuthorizationType.PUBLISH); } @@ -315,15 +316,15 @@ private void doLinkWith( Map authData = new HashMap<>(); when(controller.authenticateAsync( - any(Activity.class), - any(Fragment.class), + nullable(Activity.class), + nullable(Fragment.class), any(FacebookController.LoginAuthorizationType.class), - anyListOf(String.class))).thenReturn(Task.forResult(authData)); + anyList())).thenReturn(Task.forResult(authData)); ParseFacebookUtils.isInitialized = true; ParseUser user = mock(ParseUser.class); - when(user.linkWithInBackground(anyString(), anyMapOf(String.class, String.class))) - .thenReturn(Task.forResult(null)); + when(user.linkWithInBackground(anyString(), anyMap())) + .thenReturn(Task.forResult(null)); Task task; if (FacebookController.LoginAuthorizationType.PUBLISH.equals(type)) { if (activity != null) { @@ -350,7 +351,7 @@ private void doLinkWith( @Test public void testUnlinkInBackground() { ParseUser user = mock(ParseUser.class); - when(user.unlinkFromInBackground(anyString())).thenReturn(Task.forResult(null)); + when(user.unlinkFromInBackground(anyString())).thenReturn(Task.forResult(null)); ParseFacebookUtils.isInitialized = true; ParseFacebookUtils.unlinkInBackground(user); diff --git a/fcm/build.gradle b/fcm/build.gradle index 48b8ff812..48bf63331 100644 --- a/fcm/build.gradle +++ b/fcm/build.gradle @@ -28,8 +28,8 @@ android { } dependencies { - api "com.google.firebase:firebase-messaging:20.1.5" + api "com.google.firebase:firebase-messaging:22.0.0" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/fcm/src/main/AndroidManifest.xml b/fcm/src/main/AndroidManifest.xml index cbc863652..2edc06cc3 100644 --- a/fcm/src/main/AndroidManifest.xml +++ b/fcm/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/fcm/src/main/java/com/parse/fcm/ParseFCM.java b/fcm/src/main/java/com/parse/fcm/ParseFCM.java index 5efd90bad..47ad16b49 100644 --- a/fcm/src/main/java/com/parse/fcm/ParseFCM.java +++ b/fcm/src/main/java/com/parse/fcm/ParseFCM.java @@ -9,9 +9,7 @@ package com.parse.fcm; import com.parse.PLog; -import com.parse.ParseException; import com.parse.ParseInstallation; -import com.parse.SaveCallback; public class ParseFCM { @@ -32,14 +30,11 @@ public static void register(String token) { installation.setDeviceToken(token); //even though this is FCM, calling it gcm will work on the backend installation.setPushType(PUSH_TYPE); - installation.saveInBackground(new SaveCallback() { - @Override - public void done(ParseException e) { - if (e == null) { - PLog.d(ParseFCM.TAG, "FCM token saved to installation"); - } else { - PLog.e(ParseFCM.TAG, "FCM token upload failed", e); - } + installation.saveInBackground(e -> { + if (e == null) { + PLog.d(ParseFCM.TAG, "FCM token saved to installation"); + } else { + PLog.e(ParseFCM.TAG, "FCM token upload failed", e); } }); } diff --git a/gcm/build.gradle b/gcm/build.gradle index 4da70e8f2..cd7bbfa6f 100644 --- a/gcm/build.gradle +++ b/gcm/build.gradle @@ -32,9 +32,9 @@ android { dependencies { // last version for GCM to be supported - api "com.google.android.gms:play-services-gcm:12.0.1" + api "com.google.android.gms:play-services-gcm:17.0.0" api "com.firebase:firebase-jobdispatcher:0.8.6" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCM.java b/gcm/src/main/java/com/parse/gcm/ParseGCM.java index 699e5015c..7009bc496 100644 --- a/gcm/src/main/java/com/parse/gcm/ParseGCM.java +++ b/gcm/src/main/java/com/parse/gcm/ParseGCM.java @@ -10,6 +10,7 @@ import android.content.Context; import android.os.Bundle; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -31,7 +32,7 @@ public class ParseGCM { /** * Register your app to start receiving GCM pushes * - * @param context context + * @param context context */ public static void register(@NonNull Context context) { //kicks off the background job diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java b/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java index 113d935f7..8c5c2e058 100644 --- a/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java +++ b/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java @@ -20,11 +20,10 @@ import com.google.android.gms.iid.InstanceID; import com.parse.PLog; import com.parse.ParseInstallation; +import com.parse.boltsinternal.Task; import java.util.concurrent.Callable; -import com.parse.boltsinternal.Task; - /** * Handles saving the GCM token to the Parse Installation */ @@ -56,26 +55,23 @@ static Job createJob(FirebaseJobDispatcher dispatcher, String gcmSenderId) { public boolean onStartJob(final JobParameters job) { PLog.d(ParseGCM.TAG, "Updating GCM token"); - Task.callInBackground(new Callable() { - @Override - public Void call() { - try { - InstanceID instanceID = InstanceID.getInstance(getApplicationContext()); - String senderId = job.getExtras().getString(KEY_GCM_SENDER_ID); - String token = instanceID.getToken(senderId, - GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); - ParseInstallation installation = ParseInstallation.getCurrentInstallation(); - installation.setDeviceToken(token); - //even though this is FCM, calling it gcm will work on the backend - installation.setPushType(PUSH_TYPE); - installation.save(); - PLog.d(ParseGCM.TAG, "GCM registration success"); - } catch (Exception e) { - PLog.e(ParseGCM.TAG, "GCM registration failed", e); - jobFinished(job, true); - } - return null; + Task.callInBackground((Callable) () -> { + try { + InstanceID instanceID = InstanceID.getInstance(getApplicationContext()); + String senderId = job.getExtras().getString(KEY_GCM_SENDER_ID); + String token = instanceID.getToken(senderId, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + installation.setDeviceToken(token); + //even though this is FCM, calling it gcm will work on the backend + installation.setPushType(PUSH_TYPE); + installation.save(); + PLog.d(ParseGCM.TAG, "GCM registration success"); + } catch (Exception e) { + PLog.e(ParseGCM.TAG, "GCM registration failed", e); + jobFinished(job, true); } + return null; }); return true; // Answers the question: "Is there still work going on?" } diff --git a/google/build.gradle b/google/build.gradle index 32a100232..6605d7de5 100644 --- a/google/build.gradle +++ b/google/build.gradle @@ -21,9 +21,9 @@ android { } dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - api "com.google.android.gms:play-services-auth:18.0.0" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + api "com.google.android.gms:play-services-auth:19.0.0" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt index fde0bf3a6..69c5b89af 100644 --- a/google/src/main/java/com/parse/google/ParseGoogleUtils.kt +++ b/google/src/main/java/com/parse/google/ParseGoogleUtils.kt @@ -3,8 +3,6 @@ package com.parse.google import android.app.Activity import android.content.Context import android.content.Intent -import com.parse.boltsinternal.Continuation -import com.parse.boltsinternal.Task import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -13,6 +11,8 @@ import com.parse.LogInCallback import com.parse.ParseException import com.parse.ParseUser import com.parse.SaveCallback +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task /** * Provides a set of utilities for using Parse with Google. @@ -67,7 +67,10 @@ object ParseGoogleUtils { this.currentCallback = callback val googleSignInClient = buildGoogleSignInClient(activity) this.googleSignInClient = googleSignInClient - activity.startActivityForResult(googleSignInClient.signInIntent, REQUEST_CODE_GOOGLE_SIGN_IN) + activity.startActivityForResult( + googleSignInClient.signInIntent, + REQUEST_CODE_GOOGLE_SIGN_IN + ) } /** @@ -138,32 +141,32 @@ object ParseGoogleUtils { private fun handleSignInResult(result: Intent) { GoogleSignIn.getSignedInAccountFromIntent(result) - .addOnSuccessListener { googleAccount -> - onSignedIn(googleAccount) - } - .addOnFailureListener { exception -> - onSignInCallbackResult(null, exception) - } + .addOnSuccessListener { googleAccount -> + onSignedIn(googleAccount) + } + .addOnFailureListener { exception -> + onSignInCallbackResult(null, exception) + } } private fun onSignedIn(account: GoogleSignInAccount) { googleSignInClient?.signOut()?.addOnCompleteListener {} val authData: Map = getAuthData(account) ParseUser.logInWithInBackground(AUTH_TYPE, authData) - .continueWith { task -> - when { - task.isCompleted -> { - val user = task.result - onSignInCallbackResult(user, null) - } - task.isFaulted -> { - onSignInCallbackResult(null, task.error) - } - else -> { - onSignInCallbackResult(null, null) - } + .continueWith { task -> + when { + task.isCompleted -> { + val user = task.result + onSignInCallbackResult(user, null) + } + task.isFaulted -> { + onSignInCallbackResult(null, task.error) + } + else -> { + onSignInCallbackResult(null, null) } } + } } private fun getAuthData(account: GoogleSignInAccount): Map { @@ -186,10 +189,10 @@ object ParseGoogleUtils { private fun buildGoogleSignInClient(context: Context): GoogleSignInClient { val signInOptions = GoogleSignInOptions.Builder() - .requestId() - .requestEmail() - .requestIdToken(clientId) - .build() + .requestId() + .requestEmail() + .requestIdToken(clientId) + .build() return GoogleSignIn.getClient(context, signInOptions) } @@ -198,7 +201,8 @@ object ParseGoogleUtils { * with the same result as the input task after the callback has been run. */ private fun callbackOnMainThreadAsync( - task: Task, callback: SaveCallback, reportCancellation: Boolean): Task { + task: Task, callback: SaveCallback, reportCancellation: Boolean + ): Task { return callbackOnMainThreadInternalAsync(task, callback, reportCancellation) } @@ -208,7 +212,8 @@ object ParseGoogleUtils { * is false, the callback will not be called if the task was cancelled. */ private fun callbackOnMainThreadInternalAsync( - task: Task, callback: Any?, reportCancellation: Boolean): Task { + task: Task, callback: Any?, reportCancellation: Boolean + ): Task { if (callback == null) { return task } @@ -228,7 +233,8 @@ object ParseGoogleUtils { callback.done(error as? ParseException) } else if (callback is LogInCallback) { callback.done( - task.result as? ParseUser, error as? ParseException) + task.result as? ParseUser, error as? ParseException + ) } } finally { when { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0ebb3108e..68ca99ac4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip diff --git a/ktx/build.gradle b/ktx/build.gradle index b60b1fcd1..ba9b5fcf1 100644 --- a/ktx/build.gradle +++ b/ktx/build.gradle @@ -33,8 +33,8 @@ android { } dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/ktx/src/main/java/com/parse/ktx/ParseQuery.kt b/ktx/src/main/java/com/parse/ktx/ParseQuery.kt index 410586838..7aa989f42 100644 --- a/ktx/src/main/java/com/parse/ktx/ParseQuery.kt +++ b/ktx/src/main/java/com/parse/ktx/ParseQuery.kt @@ -2,11 +2,7 @@ package com.parse.ktx -import com.parse.ParseException -import com.parse.ParseGeoPoint -import com.parse.ParseObject -import com.parse.ParsePolygon -import com.parse.ParseQuery +import com.parse.* import kotlin.reflect.KProperty /** @@ -15,7 +11,7 @@ import kotlin.reflect.KProperty * Note that this will modify the current limit of the query */ @Throws(ParseException::class) -inline fun ParseQuery.findAll(): List { +inline fun ParseQuery.findAll(): List { limit = ParseQuery.MAX_LIMIT val list = mutableListOf() try { @@ -29,7 +25,7 @@ inline fun ParseQuery.findAll(): List { } } return list - } catch (ex : ParseException) { + } catch (ex: ParseException) { if (ex.code == ParseException.OBJECT_NOT_FOUND) { return list } @@ -75,28 +71,40 @@ fun ParseQuery.selectKeys(keys: Collection> /** * @see ParseQuery.whereContainedIn */ -inline fun ParseQuery.whereContainedIn(key: KProperty, values: Collection): ParseQuery { +inline fun ParseQuery.whereContainedIn( + key: KProperty, + values: Collection +): ParseQuery { return whereContainedIn(key.name, values) } /** * @see ParseQuery.whereContains */ -inline fun ParseQuery.whereContains(key: KProperty, substring: String): ParseQuery { +inline fun ParseQuery.whereContains( + key: KProperty, + substring: String +): ParseQuery { return whereContains(key.name, substring) } /** * @see ParseQuery.whereContainsAll */ -inline fun ParseQuery.whereContainsAll(key: KProperty, values: Collection): ParseQuery { +inline fun ParseQuery.whereContainsAll( + key: KProperty, + values: Collection +): ParseQuery { return whereContainsAll(key.name, values) } /** * @see ParseQuery.whereContainsAllStartsWith */ -inline fun ParseQuery.whereContainsAllStartsWith(key: KProperty, values: Collection): ParseQuery { +inline fun ParseQuery.whereContainsAllStartsWith( + key: KProperty, + values: Collection +): ParseQuery { return whereContainsAllStartsWith(key.name, values) } @@ -110,28 +118,41 @@ inline fun ParseQuery.whereDoesNotExist(key: KProperty ParseQuery.whereDoesNotMatchKeyInQuery(key: KProperty, keyInQuery: KProperty, query: ParseQuery): ParseQuery { +inline fun ParseQuery.whereDoesNotMatchKeyInQuery( + key: KProperty, + keyInQuery: KProperty, + query: ParseQuery +): ParseQuery { return whereDoesNotMatchKeyInQuery(key.name, keyInQuery.name, query) } /** * @see ParseQuery.whereDoesNotMatchQuery */ -inline fun ParseQuery.whereDoesNotMatchQuery(key: KProperty, query: ParseQuery): ParseQuery { +inline fun ParseQuery.whereDoesNotMatchQuery( + key: KProperty, + query: ParseQuery +): ParseQuery { return whereDoesNotMatchQuery(key.name, query) } /** * @see ParseQuery.whereEndsWith */ -inline fun ParseQuery.whereEndsWith(key: KProperty, suffix: String): ParseQuery { +inline fun ParseQuery.whereEndsWith( + key: KProperty, + suffix: String +): ParseQuery { return whereEndsWith(key.name, suffix) } /** * @see ParseQuery.whereEqualTo */ -inline fun ParseQuery.whereEqualTo(key: KProperty, value: Any?): ParseQuery { +inline fun ParseQuery.whereEqualTo( + key: KProperty, + value: Any? +): ParseQuery { return whereEqualTo(key.name, value) } @@ -145,139 +166,205 @@ inline fun ParseQuery.whereExists(key: KProperty): Pa /** * @see ParseQuery.whereFullText */ -inline fun ParseQuery.whereFullText(key: KProperty, text: String): ParseQuery { +inline fun ParseQuery.whereFullText( + key: KProperty, + text: String +): ParseQuery { return whereFullText(key.name, text) } /** * @see ParseQuery.whereGreaterThan */ -inline fun ParseQuery.whereGreaterThan(key: KProperty, value: Any): ParseQuery { +inline fun ParseQuery.whereGreaterThan( + key: KProperty, + value: Any +): ParseQuery { return whereGreaterThan(key.name, value) } /** * @see ParseQuery.whereGreaterThanOrEqualTo */ -inline fun ParseQuery.whereGreaterThanOrEqualTo(key: KProperty, value: Any): ParseQuery { +inline fun ParseQuery.whereGreaterThanOrEqualTo( + key: KProperty, + value: Any +): ParseQuery { return whereGreaterThanOrEqualTo(key.name, value) } /** * @see ParseQuery.whereLessThan */ -inline fun ParseQuery.whereLessThan(key: KProperty, value: Any): ParseQuery { +inline fun ParseQuery.whereLessThan( + key: KProperty, + value: Any +): ParseQuery { return whereLessThan(key.name, value) } /** * @see ParseQuery.whereLessThanOrEqualTo */ -inline fun ParseQuery.whereLessThanOrEqualTo(key: KProperty, value: Any): ParseQuery { +inline fun ParseQuery.whereLessThanOrEqualTo( + key: KProperty, + value: Any +): ParseQuery { return whereLessThanOrEqualTo(key.name, value) } /** * @see ParseQuery.whereMatches */ -inline fun ParseQuery.whereMatches(key: KProperty, regex: String): ParseQuery { +inline fun ParseQuery.whereMatches( + key: KProperty, + regex: String +): ParseQuery { return whereMatches(key.name, regex) } /** * @see ParseQuery.whereMatches */ -inline fun ParseQuery.whereMatches(key: KProperty, regex: String, modifiers: String): ParseQuery { +inline fun ParseQuery.whereMatches( + key: KProperty, + regex: String, + modifiers: String +): ParseQuery { return whereMatches(key.name, regex, modifiers) } /** * @see ParseQuery.whereMatchesKeyInQuery */ -inline fun ParseQuery.whereMatchesKeyInQuery(key: KProperty, keyInQuery: KProperty, query: ParseQuery): ParseQuery { +inline fun ParseQuery.whereMatchesKeyInQuery( + key: KProperty, + keyInQuery: KProperty, + query: ParseQuery +): ParseQuery { return whereMatchesKeyInQuery(key.name, keyInQuery.name, query) } /** * @see ParseQuery.whereMatchesQuery */ -inline fun ParseQuery.whereMatchesQuery(key: KProperty, query: ParseQuery): ParseQuery { +inline fun ParseQuery.whereMatchesQuery( + key: KProperty, + query: ParseQuery +): ParseQuery { return whereMatchesQuery(key.name, query) } /** * @see ParseQuery.whereNear */ -inline fun ParseQuery.whereNear(key: KProperty, point: ParseGeoPoint): ParseQuery { +inline fun ParseQuery.whereNear( + key: KProperty, + point: ParseGeoPoint +): ParseQuery { return whereNear(key.name, point) } /** * @see ParseQuery.whereNotContainedIn */ -inline fun ParseQuery.whereNotContainedIn(key: KProperty, values: Collection): ParseQuery { +inline fun ParseQuery.whereNotContainedIn( + key: KProperty, + values: Collection +): ParseQuery { return whereNotContainedIn(key.name, values) } /** * @see ParseQuery.whereNotEqualTo */ -inline fun ParseQuery.whereNotEqualTo(key: KProperty, value: Any?): ParseQuery { +inline fun ParseQuery.whereNotEqualTo( + key: KProperty, + value: Any? +): ParseQuery { return whereNotEqualTo(key.name, value) } /** * @see ParseQuery.wherePolygonContains */ -inline fun ParseQuery.wherePolygonContains(key: KProperty, point: ParseGeoPoint): ParseQuery { +inline fun ParseQuery.wherePolygonContains( + key: KProperty, + point: ParseGeoPoint +): ParseQuery { return wherePolygonContains(key.name, point) } /** * @see ParseQuery.whereStartsWith */ -inline fun ParseQuery.whereStartsWith(key: KProperty, prefix: String): ParseQuery { +inline fun ParseQuery.whereStartsWith( + key: KProperty, + prefix: String +): ParseQuery { return whereStartsWith(key.name, prefix) } /** * @see ParseQuery.whereWithinGeoBox */ -inline fun ParseQuery.whereWithinGeoBox(key: KProperty, southwest: ParseGeoPoint, northeast: ParseGeoPoint): ParseQuery { +inline fun ParseQuery.whereWithinGeoBox( + key: KProperty, + southwest: ParseGeoPoint, + northeast: ParseGeoPoint +): ParseQuery { return whereWithinGeoBox(key.name, southwest, northeast) } /** * @see ParseQuery.whereWithinKilometers */ -inline fun ParseQuery.whereWithinKilometers(key: KProperty, point: ParseGeoPoint, maxDistance: Double): ParseQuery { +inline fun ParseQuery.whereWithinKilometers( + key: KProperty, + point: ParseGeoPoint, + maxDistance: Double +): ParseQuery { return whereWithinKilometers(key.name, point, maxDistance) } /** * @see ParseQuery.whereWithinMiles */ -inline fun ParseQuery.whereWithinMiles(key: KProperty, point: ParseGeoPoint, maxDistance: Double): ParseQuery { +inline fun ParseQuery.whereWithinMiles( + key: KProperty, + point: ParseGeoPoint, + maxDistance: Double +): ParseQuery { return whereWithinMiles(key.name, point, maxDistance) } /** * @see ParseQuery.whereWithinPolygon */ -inline fun ParseQuery.whereWithinPolygon(key: KProperty, points: List): ParseQuery { +inline fun ParseQuery.whereWithinPolygon( + key: KProperty, + points: List +): ParseQuery { return whereWithinPolygon(key.name, points) } /** * @see ParseQuery.whereWithinPolygon */ -inline fun ParseQuery.whereWithinPolygon(key: KProperty, polygon: ParsePolygon): ParseQuery { +inline fun ParseQuery.whereWithinPolygon( + key: KProperty, + polygon: ParsePolygon +): ParseQuery { return whereWithinPolygon(key.name, polygon) } /** * @see ParseQuery.whereWithinRadians */ -inline fun ParseQuery.whereWithinRadians(key: KProperty, point: ParseGeoPoint, maxDistance: Double): ParseQuery { +inline fun ParseQuery.whereWithinRadians( + key: KProperty, + point: ParseGeoPoint, + maxDistance: Double +): ParseQuery { return whereWithinRadians(key.name, point, maxDistance) } diff --git a/ktx/src/main/java/com/parse/ktx/delegates/BytesParseDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/BytesParseDelegate.kt index d611980d9..c28a46502 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/BytesParseDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/BytesParseDelegate.kt @@ -16,7 +16,7 @@ class BytesParseDelegate(private val name: String?) { } operator fun setValue(parseObject: ParseObject, property: KProperty<*>, value: ByteArray?) { - parseObject.putOrIgnore(name ?:property.name, value) + parseObject.putOrIgnore(name ?: property.name, value) } } diff --git a/ktx/src/main/java/com/parse/ktx/delegates/EnumParseDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/EnumParseDelegate.kt index 44a17f159..f764a5d92 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/EnumParseDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/EnumParseDelegate.kt @@ -3,6 +3,7 @@ package com.parse.ktx.delegates import com.parse.ParseObject +import java.util.* import kotlin.reflect.KProperty /** @@ -12,21 +13,24 @@ import kotlin.reflect.KProperty * convert again to upper case to find correspondent local enum. */ class EnumParseDelegate>( - private val name: String?, - private val default: T?, - private val enumClass: Class + private val name: String?, + private val default: T?, + private val enumClass: Class ) { operator fun getValue(parseObject: ParseObject, property: KProperty<*>): T { return try { - java.lang.Enum.valueOf(enumClass, parseObject.getString(name ?: property.name)!!.toUpperCase()) + java.lang.Enum.valueOf( + enumClass, + parseObject.getString(name ?: property.name)!!.uppercase(Locale.getDefault()) + ) } catch (e: Exception) { default ?: throw e } } operator fun setValue(parseObject: ParseObject, property: KProperty<*>, t: T) { - parseObject.put(name ?: property.name, t.name.toLowerCase()) + parseObject.put(name ?: property.name, t.name.lowercase(Locale.getDefault())) } } @@ -35,10 +39,12 @@ class EnumParseDelegate>( * Returns a [Enum] property delegate for [ParseObject]s. This uses custom implementation for get * to retrieve a local version of the your enum and [ParseObject.put]. */ -inline fun > enumAttribute(default: T? = null) = EnumParseDelegate(null, default, T::class.java) +inline fun > enumAttribute(default: T? = null) = + EnumParseDelegate(null, default, T::class.java) /** * Returns a [Enum] property delegate for [ParseObject]s. This uses custom implementation for get * to retrieve a local version of the your enum and [ParseObject.put]. */ -inline fun > enumAttribute(name: String? = null, default: T? = null) = EnumParseDelegate(name, default, T::class.java) +inline fun > enumAttribute(name: String? = null, default: T? = null) = + EnumParseDelegate(name, default, T::class.java) diff --git a/ktx/src/main/java/com/parse/ktx/delegates/IntParseDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/IntParseDelegate.kt index 06d9a102e..9b4e517a3 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/IntParseDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/IntParseDelegate.kt @@ -15,7 +15,7 @@ class IntParseDelegate(private val name: String?) { } operator fun setValue(parseObject: ParseObject, property: KProperty<*>, value: Int) { - parseObject.put(name ?:property.name, value) + parseObject.put(name ?: property.name, value) } } diff --git a/ktx/src/main/java/com/parse/ktx/delegates/ListParseDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/ListParseDelegate.kt index e0d0939bd..f8846c054 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/ListParseDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/ListParseDelegate.kt @@ -15,7 +15,11 @@ class ListParseDelegate(private val name: String?) { return parseObject.getList(name ?: property.name) as? MutableList } - operator fun setValue(parseObject: ParseObject, property: KProperty<*>, value: MutableList?) { + operator fun setValue( + parseObject: ParseObject, + property: KProperty<*>, + value: MutableList? + ) { parseObject.putOrIgnore(name ?: property.name, value) } diff --git a/ktx/src/main/java/com/parse/ktx/delegates/MapParseDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/MapParseDelegate.kt index 046118dd3..80f851f32 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/MapParseDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/MapParseDelegate.kt @@ -11,11 +11,18 @@ import kotlin.reflect.KProperty */ class MapParseDelegate(private val name: String?) { - operator fun getValue(parseObject: ParseObject, property: KProperty<*>): MutableMap? { + operator fun getValue( + parseObject: ParseObject, + property: KProperty<*> + ): MutableMap? { return parseObject.getMap(name ?: property.name) as? MutableMap } - operator fun setValue(parseObject: ParseObject, property: KProperty<*>, value: MutableMap?) { + operator fun setValue( + parseObject: ParseObject, + property: KProperty<*>, + value: MutableMap? + ) { parseObject.putOrIgnore(name ?: property.name, value) } diff --git a/ktx/src/main/java/com/parse/ktx/delegates/ParseRelationDelegate.kt b/ktx/src/main/java/com/parse/ktx/delegates/ParseRelationDelegate.kt index 2529fc33a..dd7318eaa 100644 --- a/ktx/src/main/java/com/parse/ktx/delegates/ParseRelationDelegate.kt +++ b/ktx/src/main/java/com/parse/ktx/delegates/ParseRelationDelegate.kt @@ -12,7 +12,7 @@ import kotlin.reflect.KProperty class ParseRelationDelegate(private val name: String?) { operator fun getValue(parseObject: ParseObject, property: KProperty<*>): ParseRelation { - return parseObject.getRelation(name ?: property.name) + return parseObject.getRelation(name ?: property.name) } } @@ -21,4 +21,5 @@ class ParseRelationDelegate(private val name: String?) { * Returns a [ParseRelation] property delegate for [ParseObject]s. * This uses [ParseObject.getRelation]. */ -inline fun relationAttribute(name: String? = null) = ParseRelationDelegate(name) +inline fun relationAttribute(name: String? = null) = + ParseRelationDelegate(name) diff --git a/parse/build.gradle b/parse/build.gradle index 303f7a27a..c9298945f 100644 --- a/parse/build.gradle +++ b/parse/build.gradle @@ -1,5 +1,5 @@ apply plugin: "com.android.library" -apply plugin: "com.github.kt3k.coveralls" +apply plugin: "kotlin-android" android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -12,6 +12,12 @@ android { consumerProguardFiles "release-proguard.pro" } + testOptions { + unitTests { + includeAndroidResources = true + } + } + packagingOptions { exclude '**/BuildConfig.class' } @@ -33,57 +39,41 @@ ext { } dependencies { - api "androidx.annotation:annotation:1.1.0" - api "androidx.core:core:1.2.0" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + api "androidx.annotation:annotation:1.2.0" + api "androidx.core:core-ktx:1.6.0" api "com.squareup.okhttp3:okhttp:$okhttpVersion" api project(':bolts-tasks') - testImplementation "org.robolectric:robolectric:3.8" + testImplementation 'junit:junit:4.13.2' + testImplementation "org.robolectric:robolectric:4.6" testImplementation "org.skyscreamer:jsonassert:1.5.0" - testImplementation "org.mockito:mockito-core:1.10.19" + testImplementation "org.mockito:mockito-core:3.6.28" testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" } //region Code Coverage -apply plugin: "jacoco" +apply plugin: "com.dicedmelon.gradle.jacoco-android" jacoco { - toolVersion "0.7.1.201405082137" + toolVersion = "0.8.6" } -task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { - group = "Reporting" - description = "Generate Jacoco coverage reports" - - classDirectories = fileTree( - dir: "${buildDir}/intermediates/classes/debug", - excludes: ['**/R.class', - '**/R$*.class', - '**/*$ViewInjector*.*', - '**/BuildConfig.*', - '**/Manifest*.*'] - ) - - sourceDirectories = files("${buildDir.parent}/src/main/java") - additionalSourceDirs = files([ - "${buildDir}/generated/source/buildConfig/debug", - "${buildDir}/generated/source/r/debug" - ]) - executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec") - - reports { - xml.enabled = true - html.enabled = true - } +jacocoAndroidUnitTestReport { + csv.enabled false + html.enabled true + xml.enabled true } //endregion //region Coveralls -coveralls.jacocoReportPath = "${buildDir}/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" +apply plugin: "com.github.kt3k.coveralls" + +coveralls.jacocoReportPath = "${buildDir}/jacoco/jacoco.xml" //endregion -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/parse/src/main/java/com/parse/AbstractQueryController.java b/parse/src/main/java/com/parse/AbstractQueryController.java deleted file mode 100644 index 6e54e272b..000000000 --- a/parse/src/main/java/com/parse/AbstractQueryController.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 java.util.List; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -/** - * {@code AbstractParseQueryController} is an abstract implementation of - * {@link ParseQueryController}, which implements {@link ParseQueryController#getFirstAsync}. - */ -abstract class AbstractQueryController implements ParseQueryController { - - @Override - public Task getFirstAsync(ParseQuery.State state, ParseUser user, - Task cancellationToken) { - return findAsync(state, user, cancellationToken).continueWith(new Continuation, T>() { - @Override - public T then(Task> task) throws Exception { - if (task.isFaulted()) { - throw task.getError(); - } - if (task.getResult() != null && task.getResult().size() > 0) { - return task.getResult().get(0); - } - throw new ParseException(ParseException.OBJECT_NOT_FOUND, "no results found for query"); - } - }); - } -} diff --git a/parse/src/main/java/com/parse/AbstractQueryController.kt b/parse/src/main/java/com/parse/AbstractQueryController.kt new file mode 100644 index 000000000..9144ab48b --- /dev/null +++ b/parse/src/main/java/com/parse/AbstractQueryController.kt @@ -0,0 +1,36 @@ +/* + * 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 com.parse.boltsinternal.Task + +/** + * `AbstractParseQueryController` is an abstract implementation of + * [ParseQueryController], which implements [ParseQueryController.getFirstAsync]. + */ +internal abstract class AbstractQueryController : ParseQueryController { + override fun getFirstAsync( + state: ParseQuery.State, user: ParseUser?, + cancellationToken: Task? + ): Task { + return findAsync(state, user, cancellationToken).continueWith { task: Task?> -> + if (task.isFaulted) { + throw task.error + } + + task.result?.let { + if (it.isNotEmpty()) { + return@continueWith it[0] + } + } + + throw ParseException(ParseException.OBJECT_NOT_FOUND, "no results found for query") + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/AuthenticationCallback.java b/parse/src/main/java/com/parse/AuthenticationCallback.kt similarity index 56% rename from parse/src/main/java/com/parse/AuthenticationCallback.java rename to parse/src/main/java/com/parse/AuthenticationCallback.kt index a4980d3a2..00a9c8645 100644 --- a/parse/src/main/java/com/parse/AuthenticationCallback.java +++ b/parse/src/main/java/com/parse/AuthenticationCallback.kt @@ -6,24 +6,23 @@ * 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 java.util.Map; +package com.parse /** * Provides a general interface for delegation of third party authentication callbacks. */ -public interface AuthenticationCallback { +interface AuthenticationCallback { /** * Called when restoring third party authentication credentials that have been serialized, * such as session keys, etc. - *

- * Note: This will be executed on a background thread. * - * @param authData The auth data for the provider. This value may be {@code null} when - * unlinking an account. - * @return {@code true} iff the {@code authData} was successfully synchronized or {@code false} - * if user should no longer be associated because of bad {@code authData}. + * + * **Note:** This will be executed on a background thread. + * + * @param authData The auth data for the provider. This value may be `null` when + * unlinking an account. + * @return `true` if the `authData` was successfully synchronized or `false` + * if user should no longer be associated because of bad `authData`. */ - boolean onRestore(Map authData); -} + fun onRestore(authData: Map = emptyMap()): Boolean +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/CacheQueryController.java b/parse/src/main/java/com/parse/CacheQueryController.java deleted file mode 100644 index 70cc2cfa9..000000000 --- a/parse/src/main/java/com/parse/CacheQueryController.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.util.List; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class CacheQueryController extends AbstractQueryController { - - private final NetworkQueryController networkController; - - public CacheQueryController(NetworkQueryController network) { - networkController = network; - } - - @Override - public Task> findAsync( - final ParseQuery.State state, - final ParseUser user, - final Task cancellationToken) { - final String sessionToken = user != null ? user.getSessionToken() : null; - CommandDelegate> callbacks = new CommandDelegate>() { - @Override - public Task> runOnNetworkAsync() { - return networkController.findAsync(state, sessionToken, cancellationToken); - } - - @Override - public Task> runFromCacheAsync() { - return findFromCacheAsync(state, sessionToken); - } - }; - return runCommandWithPolicyAsync(callbacks, state.cachePolicy()); - } - - @Override - public Task countAsync( - final ParseQuery.State state, - final ParseUser user, - final Task cancellationToken) { - final String sessionToken = user != null ? user.getSessionToken() : null; - CommandDelegate callbacks = new CommandDelegate() { - @Override - public Task runOnNetworkAsync() { - return networkController.countAsync(state, sessionToken, cancellationToken); - } - - @Override - public Task runFromCacheAsync() { - return countFromCacheAsync(state, sessionToken); - } - }; - return runCommandWithPolicyAsync(callbacks, state.cachePolicy()); - } - - /** - * Retrieves the results of the last time {@link ParseQuery#find()} was called on a query - * identical to this one. - * - * @param sessionToken The user requesting access. - * @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no - * cache for this query. - */ - private Task> findFromCacheAsync( - final ParseQuery.State state, String sessionToken) { - final String cacheKey = ParseRESTQueryCommand.findCommand(state, sessionToken).getCacheKey(); - return Task.call(new Callable>() { - @Override - public List call() throws Exception { - JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()); - if (cached == null) { - throw new ParseException(ParseException.CACHE_MISS, "results not cached"); - } - try { - return networkController.convertFindResponse(state, cached); - } catch (JSONException e) { - throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json"); - } - } - }, Task.BACKGROUND_EXECUTOR); - } - - /** - * Retrieves the results of the last time {@link ParseQuery#count()} was called on a query - * identical to this one. - * - * @param sessionToken The user requesting access. - * @return A list of {@link ParseObject}s corresponding to this query. Returns null if there is no - * cache for this query. - */ - private Task countFromCacheAsync( - final ParseQuery.State state, String sessionToken) { - final String cacheKey = ParseRESTQueryCommand.countCommand(state, sessionToken).getCacheKey(); - return Task.call(new Callable() { - @Override - public Integer call() throws Exception { - JSONObject cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()); - if (cached == null) { - throw new ParseException(ParseException.CACHE_MISS, "results not cached"); - } - try { - return cached.getInt("count"); - } catch (JSONException e) { - throw new ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json"); - } - } - }, Task.BACKGROUND_EXECUTOR); - } - - private Task runCommandWithPolicyAsync(final CommandDelegate c, - ParseQuery.CachePolicy policy) { - switch (policy) { - case IGNORE_CACHE: - case NETWORK_ONLY: - return c.runOnNetworkAsync(); - case CACHE_ONLY: - return c.runFromCacheAsync(); - case CACHE_ELSE_NETWORK: - return c.runFromCacheAsync().continueWithTask(new Continuation>() { - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") - @Override - public Task then(Task task) { - if (task.getError() instanceof ParseException) { - return c.runOnNetworkAsync(); - } - return task; - } - }); - case NETWORK_ELSE_CACHE: - return c.runOnNetworkAsync().continueWithTask(new Continuation>() { - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") - @Override - public Task then(Task task) { - Exception error = task.getError(); - if (error instanceof ParseException && - ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { - return c.runFromCacheAsync(); - } - // Either the query succeeded, or there was an an error with the query, not the - // network - return task; - } - }); - case CACHE_THEN_NETWORK: - throw new RuntimeException( - "You cannot use the cache policy CACHE_THEN_NETWORK with find()"); - default: - throw new RuntimeException("Unknown cache policy: " + policy); - } - } - - /** - * A callback that will be used to tell runCommandWithPolicy how to perform the command on the - * network and form the cache. - */ - private interface CommandDelegate { - // Fetches data from the network. - Task runOnNetworkAsync(); - - // Fetches data from the cache. - Task runFromCacheAsync(); - } -} diff --git a/parse/src/main/java/com/parse/CacheQueryController.kt b/parse/src/main/java/com/parse/CacheQueryController.kt new file mode 100644 index 000000000..7a56663c1 --- /dev/null +++ b/parse/src/main/java/com/parse/CacheQueryController.kt @@ -0,0 +1,140 @@ +/* + * 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 com.parse.ParseQuery.CachePolicy +import com.parse.boltsinternal.Task +import org.json.JSONException + +internal class CacheQueryController(private val networkController: NetworkQueryController) : + AbstractQueryController() { + override fun findAsync( + state: ParseQuery.State, + user: ParseUser?, + cancellationToken: Task? + ): Task> { + val sessionToken = user?.sessionToken + val callbacks: CommandDelegate> = object : CommandDelegate> { + override fun runOnNetworkAsync(): Task> { + return networkController.findAsync(state, sessionToken, cancellationToken) + } + + override fun runFromCacheAsync(): Task> { + return findFromCacheAsync(state, sessionToken) + } + } + return runCommandWithPolicyAsync(callbacks, state.cachePolicy()) + } + + override fun countAsync( + state: ParseQuery.State, + user: ParseUser?, + cancellationToken: Task? + ): Task { + val sessionToken = user?.sessionToken + val callbacks: CommandDelegate = object : CommandDelegate { + override fun runOnNetworkAsync(): Task { + return networkController.countAsync(state, sessionToken, cancellationToken) + } + + override fun runFromCacheAsync(): Task { + return countFromCacheAsync(state, sessionToken) + } + } + return runCommandWithPolicyAsync(callbacks, state.cachePolicy()) + } + + /** + * Retrieves the results of the last time [ParseQuery.find] was called on a query + * identical to this one. + * + * @param sessionToken The user requesting access. + * @return A list of [ParseObject]s corresponding to this query. Returns null if there is no + * cache for this query. + */ + private fun findFromCacheAsync( + state: ParseQuery.State, sessionToken: String? + ): Task> { + val cacheKey = ParseRESTQueryCommand.findCommand(state, sessionToken).cacheKey + return Task.call({ + val cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()) + ?: throw ParseException(ParseException.CACHE_MISS, "results not cached") + try { + return@call networkController.convertFindResponse(state, cached) + } catch (e: JSONException) { + throw ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json") + } + }, Task.BACKGROUND_EXECUTOR) + } + + /** + * Retrieves the results of the last time [ParseQuery.count] was called on a query + * identical to this one. + * + * @param sessionToken The user requesting access. + * @return A list of [ParseObject]s corresponding to this query. Returns null if there is no + * cache for this query. + */ + private fun countFromCacheAsync( + state: ParseQuery.State, sessionToken: String? + ): Task { + val cacheKey = ParseRESTQueryCommand.countCommand(state, sessionToken).cacheKey + return Task.call({ + val cached = ParseKeyValueCache.jsonFromKeyValueCache(cacheKey, state.maxCacheAge()) + ?: throw ParseException(ParseException.CACHE_MISS, "results not cached") + try { + return@call cached.getInt("count") + } catch (e: JSONException) { + throw ParseException(ParseException.CACHE_MISS, "the cache contains corrupted json") + } + }, Task.BACKGROUND_EXECUTOR) + } + + private fun runCommandWithPolicyAsync( + c: CommandDelegate, + policy: CachePolicy + ): Task { + return when (policy) { + CachePolicy.IGNORE_CACHE, CachePolicy.NETWORK_ONLY -> c.runOnNetworkAsync() + CachePolicy.CACHE_ONLY -> c.runFromCacheAsync() + CachePolicy.CACHE_ELSE_NETWORK -> c.runFromCacheAsync() + .continueWithTask { task: Task -> + if (task.error is ParseException) { + return@continueWithTask c.runOnNetworkAsync() + } + task + } + CachePolicy.NETWORK_ELSE_CACHE -> c.runOnNetworkAsync() + .continueWithTask { task: Task -> + val error = task.error + if (error is ParseException && + error.code == ParseException.CONNECTION_FAILED + ) { + return@continueWithTask c.runFromCacheAsync() + } + task + } + CachePolicy.CACHE_THEN_NETWORK -> throw RuntimeException( + "You cannot use the cache policy CACHE_THEN_NETWORK with find()" + ) + } + } + + /** + * A callback that will be used to tell runCommandWithPolicy how to perform the command on the + * network and form the cache. + */ + private interface CommandDelegate { + // Fetches data from the network. + fun runOnNetworkAsync(): Task + + // Fetches data from the cache. + fun runFromCacheAsync(): Task + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/CachedCurrentInstallationController.java b/parse/src/main/java/com/parse/CachedCurrentInstallationController.java deleted file mode 100644 index 350a5154a..000000000 --- a/parse/src/main/java/com/parse/CachedCurrentInstallationController.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class CachedCurrentInstallationController - implements ParseCurrentInstallationController { - - /* package */ static final String TAG = "com.parse.CachedCurrentInstallationController"; - - /* - * Note about lock ordering: - * - * You must NOT acquire the ParseInstallation instance mutex (the "mutex" field in ParseObject) - * while holding this current installation lock. (We used to use the ParseInstallation.class lock, - * but moved on to an explicit lock object since anyone could acquire the ParseInstallation.class - * lock as ParseInstallation is a public class.) Acquiring the instance mutex while holding this - * current installation lock will lead to a deadlock. - */ - private final Object mutex = new Object(); - - private final TaskQueue taskQueue = new TaskQueue(); - - private final ParseObjectStore store; - private final InstallationId installationId; - - // The "current installation" is the installation for this device. Protected by - // mutex. - /* package for test */ ParseInstallation currentInstallation; - - public CachedCurrentInstallationController( - ParseObjectStore store, InstallationId installationId) { - this.store = store; - this.installationId = installationId; - } - - @Override - public Task setAsync(final ParseInstallation installation) { - if (!isCurrent(installation)) { - return Task.forResult(null); - } - - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.setAsync(installation); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - installationId.set(installation.getInstallationId()); - return task; - } - }, ParseExecutors.io()); - } - }); - } - - @Override - public Task getAsync() { - synchronized (mutex) { - if (currentInstallation != null) { - return Task.forResult(currentInstallation); - } - } - - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (mutex) { - if (currentInstallation != null) { - return Task.forResult(currentInstallation); - } - } - - return store.getAsync().continueWith(new Continuation() { - @Override - public ParseInstallation then(Task task) { - ParseInstallation current = task.getResult(); - if (current == null) { - current = ParseObject.create(ParseInstallation.class); - current.updateDeviceInfo(installationId); - } else { - installationId.set(current.getInstallationId()); - PLog.v(TAG, "Successfully deserialized Installation object"); - } - - synchronized (mutex) { - currentInstallation = current; - } - return current; - } - }, ParseExecutors.io()); - } - }); - } - }); - } - - @Override - public Task existsAsync() { - synchronized (mutex) { - if (currentInstallation != null) { - return Task.forResult(true); - } - } - - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.existsAsync(); - } - }); - } - }); - } - - @Override - public void clearFromMemory() { - synchronized (mutex) { - currentInstallation = null; - } - } - - @Override - public void clearFromDisk() { - synchronized (mutex) { - currentInstallation = null; - } - try { - installationId.clear(); - ParseTaskUtils.wait(store.deleteAsync()); - } catch (ParseException e) { - // ignored - } - } - - @Override - public boolean isCurrent(ParseInstallation installation) { - synchronized (mutex) { - return currentInstallation == installation; - } - } -} diff --git a/parse/src/main/java/com/parse/CachedCurrentInstallationController.kt b/parse/src/main/java/com/parse/CachedCurrentInstallationController.kt new file mode 100644 index 000000000..aa412e15f --- /dev/null +++ b/parse/src/main/java/com/parse/CachedCurrentInstallationController.kt @@ -0,0 +1,110 @@ +/* + * 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 com.parse.PLog.v +import com.parse.boltsinternal.Task + +internal class CachedCurrentInstallationController( + private val store: ParseObjectStore, + private val installationId: InstallationId +) : ParseCurrentInstallationController { + /* + * Note about lock ordering: + * + * You must NOT acquire the ParseInstallation instance mutex (the "mutex" field in ParseObject) + * while holding this current installation lock. (We used to use the ParseInstallation.class lock, + * but moved on to an explicit lock object since anyone could acquire the ParseInstallation.class + * lock as ParseInstallation is a public class.) Acquiring the instance mutex while holding this + * current installation lock will lead to a deadlock. + */ + private val mutex = Any() + private val taskQueue = TaskQueue() + + // The "current installation" is the installation for this device. Protected by + // mutex. + /* package for test */ + @JvmField + var currentInstallation: ParseInstallation? = null + + override fun setAsync(installation: ParseInstallation): Task { + return if (!isCurrent(installation)) { + Task.forResult(null) + } else taskQueue.enqueue { toAwait: Task -> + toAwait.continueWithTask { store.setAsync(installation) } + .continueWithTask( + { task: Task -> + installationId.set(installation.installationId!!) + task + }, ParseExecutors.io() + ) + } + } + + override fun getAsync(): Task { + synchronized(mutex) { + if (currentInstallation != null) { + return Task.forResult(currentInstallation) + } + } + return taskQueue.enqueue { toAwait: Task -> + toAwait.continueWithTask { + synchronized(mutex) { + if (currentInstallation != null) { + return@continueWithTask Task.forResult(currentInstallation!!) + } + } + store.getAsync.continueWith({ task1: Task -> + var current = task1.result + if (current == null) { + current = ParseObject.create(ParseInstallation::class.java) + current.updateDeviceInfo(installationId) + } else { + installationId.set(current.installationId!!) + v(TAG, "Successfully deserialized Installation object") + } + synchronized(mutex) { currentInstallation = current } + current + }, ParseExecutors.io()) + } + } + } + + override fun existsAsync(): Task { + synchronized(mutex) { + if (currentInstallation != null) { + return Task.forResult(true) + } + } + return taskQueue.enqueue { toAwait: Task -> toAwait.continueWithTask { store.existsAsync() } } + } + + override fun clearFromMemory() { + synchronized(mutex) { currentInstallation = null } + } + + override fun clearFromDisk() { + synchronized(mutex) { currentInstallation = null } + try { + installationId.clear() + ParseTaskUtils.wait(store.deleteAsync()) + } catch (e: ParseException) { + // ignored + } + } + + override fun isCurrent(installation: ParseInstallation): Boolean { + synchronized(mutex) { return currentInstallation === installation } + } + + companion object { + /* package */ + const val TAG = "com.parse.CachedCurrentInstallationController" + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/CachedCurrentUserController.java b/parse/src/main/java/com/parse/CachedCurrentUserController.java deleted file mode 100644 index 2db9f7a36..000000000 --- a/parse/src/main/java/com/parse/CachedCurrentUserController.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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 java.util.Arrays; -import java.util.Map; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class CachedCurrentUserController implements ParseCurrentUserController { - - /** - * Lock used to synchronize current user modifications and access. - *

- * Note about lock ordering: - *

- * You must NOT acquire the ParseUser instance mutex (the "mutex" field in ParseObject) while - * holding this static initialization lock. Doing so will cause a deadlock. - */ - private final Object mutex = new Object(); - private final TaskQueue taskQueue = new TaskQueue(); - - private final ParseObjectStore store; - - /* package */ ParseUser currentUser; - // Whether currentUser is known to match the serialized version on disk. This is useful for saving - // a filesystem check if you try to load currentUser frequently while there is none on disk. - /* package */ boolean currentUserMatchesDisk = false; - - public CachedCurrentUserController(ParseObjectStore store) { - this.store = store; - } - - @Override - public Task setAsync(final ParseUser user) { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser oldCurrentUser; - synchronized (mutex) { - oldCurrentUser = currentUser; - } - - if (oldCurrentUser != null && oldCurrentUser != user) { - // We don't need to revoke the token since we're not explicitly calling logOut - // We don't need to remove persisted files since we're overwriting them - return oldCurrentUser.logOutAsync(false).continueWith(new Continuation() { - @Override - public Void then(Task task) { - return null; // ignore errors - } - }); - } - return task; - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - user.setIsCurrentUser(true); - return user.synchronizeAllAuthDataAsync(); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.setAsync(user).continueWith(new Continuation() { - @Override - public Void then(Task task) { - synchronized (mutex) { - currentUserMatchesDisk = !task.isFaulted(); - currentUser = user; - } - return null; - } - }); - } - }); - } - }); - } - - @Override - public Task setIfNeededAsync(ParseUser user) { - synchronized (mutex) { - if (!user.isCurrentUser() || currentUserMatchesDisk) { - return Task.forResult(null); - } - } - - return setAsync(user); - } - - @Override - public Task getAsync() { - return getAsync(ParseUser.isAutomaticUserEnabled()); - } - - @Override - public Task existsAsync() { - synchronized (mutex) { - if (currentUser != null) { - return Task.forResult(true); - } - } - - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.existsAsync(); - } - }); - } - }); - } - - @Override - public boolean isCurrent(ParseUser user) { - synchronized (mutex) { - return currentUser == user; - } - } - - @Override - public void clearFromMemory() { - synchronized (mutex) { - currentUser = null; - currentUserMatchesDisk = false; - } - } - - @Override - public void clearFromDisk() { - synchronized (mutex) { - currentUser = null; - currentUserMatchesDisk = false; - } - try { - ParseTaskUtils.wait(store.deleteAsync()); - } catch (ParseException e) { - // ignored - } - } - - @Override - public Task getCurrentSessionTokenAsync() { - return getAsync(false).onSuccess(new Continuation() { - @Override - public String then(Task task) { - ParseUser user = task.getResult(); - return user != null ? user.getSessionToken() : null; - } - }); - } - - @Override - public Task logOutAsync() { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - // We can parallelize disk and network work, but only after we restore the current user from - // disk. - final Task userTask = getAsync(false); - return Task.whenAll(Arrays.asList(userTask, toAwait)).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - Task logOutTask = userTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser user = task.getResult(); - if (user == null) { - return task.cast(); - } - return user.logOutAsync(); - } - }); - - Task diskTask = store.deleteAsync().continueWith(new Continuation() { - @Override - public Void then(Task task) { - boolean deleted = !task.isFaulted(); - synchronized (mutex) { - currentUserMatchesDisk = deleted; - currentUser = null; - } - return null; - } - }); - return Task.whenAll(Arrays.asList(logOutTask, diskTask)); - } - }); - } - }); - } - - @Override - public Task getAsync(final boolean shouldAutoCreateUser) { - synchronized (mutex) { - if (currentUser != null) { - return Task.forResult(currentUser); - } - } - - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task ignored) { - ParseUser current; - boolean matchesDisk; - synchronized (mutex) { - current = currentUser; - matchesDisk = currentUserMatchesDisk; - } - - if (current != null) { - return Task.forResult(current); - } - - if (matchesDisk) { - if (shouldAutoCreateUser) { - return Task.forResult(lazyLogIn()); - } - return null; - } - - return store.getAsync().continueWith(new Continuation() { - @Override - public ParseUser then(Task task) { - ParseUser current = task.getResult(); - boolean matchesDisk = !task.isFaulted(); - - synchronized (mutex) { - currentUser = current; - currentUserMatchesDisk = matchesDisk; - } - - if (current != null) { - synchronized (current.mutex) { - current.setIsCurrentUser(true); - } - return current; - } - - if (shouldAutoCreateUser) { - return lazyLogIn(); - } - return null; - } - }); - } - }); - } - }); - } - - private ParseUser lazyLogIn() { - Map authData = ParseAnonymousUtils.getAuthData(); - return lazyLogIn(ParseAnonymousUtils.AUTH_TYPE, authData); - } - - /* package for tests */ ParseUser lazyLogIn(String authType, Map authData) { - // Note: if authType != ParseAnonymousUtils.AUTH_TYPE the user is not "lazy". - ParseUser user = ParseObject.create(ParseUser.class); - synchronized (user.mutex) { - user.setIsCurrentUser(true); - user.putAuthData(authType, authData); - } - - synchronized (mutex) { - currentUserMatchesDisk = false; - currentUser = user; - } - - return user; - } -} diff --git a/parse/src/main/java/com/parse/CachedCurrentUserController.kt b/parse/src/main/java/com/parse/CachedCurrentUserController.kt new file mode 100644 index 000000000..a408a85cc --- /dev/null +++ b/parse/src/main/java/com/parse/CachedCurrentUserController.kt @@ -0,0 +1,204 @@ +/* + * 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 com.parse.boltsinternal.Task +import java.util.* + +internal class CachedCurrentUserController(private val store: ParseObjectStore) : + ParseCurrentUserController { + /** + * Lock used to synchronize current user modifications and access. + * + * + * Note about lock ordering: + * + * + * You must NOT acquire the ParseUser instance mutex (the "mutex" field in ParseObject) while + * holding this static initialization lock. Doing so will cause a deadlock. + */ + private val mutex = Any() + private val taskQueue = TaskQueue() + + /* package */ + @JvmField + var currentUser: ParseUser? = null + + // Whether currentUser is known to match the serialized version on disk. This is useful for saving + // a filesystem check if you try to load currentUser frequently while there is none on disk. + /* package */ + @JvmField + var currentUserMatchesDisk = false + override fun setAsync(user: ParseUser): Task { + return taskQueue.enqueue { toAwait: Task -> + toAwait.continueWithTask { task: Task -> + var oldCurrentUser: ParseUser? + synchronized(mutex) { oldCurrentUser = currentUser } + if (oldCurrentUser != null && oldCurrentUser !== user) { + // We don't need to revoke the token since we're not explicitly calling logOut + // We don't need to remove persisted files since we're overwriting them + return@continueWithTask oldCurrentUser!!.logOutAsync(false) + .continueWith { + null // ignore errors + } + } + task + }.onSuccessTask { + user.setIsCurrentUser(true) + user.synchronizeAllAuthDataAsync() + }.onSuccessTask { + store.setAsync(user).continueWith { task: Task -> + synchronized(mutex) { + currentUserMatchesDisk = !task.isFaulted + currentUser = user + } + null + } + } + } + } + + override fun setIfNeededAsync(user: ParseUser): Task { + synchronized(mutex) { + if (!user.isCurrentUser || currentUserMatchesDisk) { + return Task.forResult(null) + } + } + return setAsync(user) + } + + override fun getAsync(): Task { + return getAsync(ParseUser.isAutomaticUserEnabled()) + } + + override fun existsAsync(): Task { + synchronized(mutex) { + if (currentUser != null) { + return Task.forResult(true) + } + } + return taskQueue.enqueue { toAwait: Task -> toAwait.continueWithTask { store.existsAsync() } } + } + + override fun isCurrent(user: ParseUser): Boolean { + synchronized(mutex) { return currentUser === user } + } + + override fun clearFromMemory() { + synchronized(mutex) { + currentUser = null + currentUserMatchesDisk = false + } + } + + override fun clearFromDisk() { + synchronized(mutex) { + currentUser = null + currentUserMatchesDisk = false + } + try { + ParseTaskUtils.wait(store.deleteAsync()) + } catch (e: ParseException) { + // ignored + } + } + + override fun getCurrentSessionTokenAsync(): Task { + return getAsync(false).onSuccess { task: Task -> + val user = task.result + user?.sessionToken + } + } + + override fun logOutAsync(): Task { + return taskQueue.enqueue { toAwait: Task? -> + // We can parallelize disk and network work, but only after we restore the current user from + // disk. + val userTask = getAsync(false) + Task.whenAll(listOf(userTask, toAwait)).continueWithTask { + val logOutTask = userTask.onSuccessTask { task1: Task -> + val user = task1.result ?: return@onSuccessTask task1.cast() + user.logOutAsync() + } + val diskTask = store.deleteAsync().continueWith { task: Task -> + val deleted = !task.isFaulted + synchronized(mutex) { + currentUserMatchesDisk = deleted + currentUser = null + } + null + } + Task.whenAll(listOf(logOutTask, diskTask)) + } + } + } + + override fun getAsync(shouldAutoCreateUser: Boolean): Task { + synchronized(mutex) { + if (currentUser != null) { + return Task.forResult(currentUser) + } + } + return taskQueue.enqueue { toAwait: Task -> + toAwait.continueWithTask { + var current: ParseUser? + var matchesDisk: Boolean + synchronized(mutex) { + current = currentUser + matchesDisk = currentUserMatchesDisk + } + if (current != null) { + return@continueWithTask Task.forResult(current) + } + if (matchesDisk) { + if (shouldAutoCreateUser) { + return@continueWithTask Task.forResult(lazyLogIn()) + } + return@continueWithTask null + } + store.getAsync.continueWith { task: Task -> + val current1 = task.result + val matchesDisk1 = !task.isFaulted + synchronized(mutex) { + currentUser = current1 + currentUserMatchesDisk = matchesDisk1 + } + if (current1 != null) { + synchronized(current1.mutex) { current1.setIsCurrentUser(true) } + return@continueWith current1 + } + if (shouldAutoCreateUser) { + return@continueWith lazyLogIn() + } + null + } + } + } + } + + private fun lazyLogIn(): ParseUser { + val authData = ParseAnonymousUtils.getAuthData() + return lazyLogIn(ParseAnonymousUtils.AUTH_TYPE, authData) + } + + /* package for tests */ + fun lazyLogIn(authType: String?, authData: Map?): ParseUser { + // Note: if authType != ParseAnonymousUtils.AUTH_TYPE the user is not "lazy". + val user = ParseObject.create(ParseUser::class.java) + synchronized(user.mutex) { + user.setIsCurrentUser(true) + user.putAuthData(authType, authData) + } + synchronized(mutex) { + currentUserMatchesDisk = false + currentUser = user + } + return user + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ConfigCallback.java b/parse/src/main/java/com/parse/ConfigCallback.java deleted file mode 100644 index 3fff9adfd..000000000 --- a/parse/src/main/java/com/parse/ConfigCallback.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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; - -/** - * A {@code ConfigCallback} is used to run code after {@link ParseConfig#getInBackground()} is used - * to fetch a new configuration object from the server in a background thread. - *

- * The easiest way to use a {@code ConfigCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the fetch is complete. - * The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- *

- * ParseConfig.getInBackground(new ConfigCallback() {
- *   public void done(ParseConfig config, ParseException e) {
- *     if (e == null) {
- *       configFetchSuccess(object);
- *     } else {
- *       configFetchFailed(e);
- *     }
- *   }
- * });
- * 
- */ -public interface ConfigCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param config A new {@code ParseConfig} instance from the server, or {@code null} if it did not - * succeed. - * @param e The exception raised by the fetch, or {@code null} if it succeeded. - */ - @Override - void done(ParseConfig config, ParseException e); -} diff --git a/parse/src/main/java/com/parse/ConfigCallback.kt b/parse/src/main/java/com/parse/ConfigCallback.kt new file mode 100644 index 000000000..6da817de9 --- /dev/null +++ b/parse/src/main/java/com/parse/ConfigCallback.kt @@ -0,0 +1,43 @@ +/* + * 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 + +/** + * A `ConfigCallback` is used to run code after [ParseConfig.getInBackground] is used + * to fetch a new configuration object from the server in a background thread. + * + * + * The easiest way to use a `ConfigCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the fetch is complete. + * The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + *
+ * ParseConfig.getInBackground(new ConfigCallback() {
+ * public void done(ParseConfig config, ParseException e) {
+ * if (e == null) {
+ * configFetchSuccess(object);
+ * } else {
+ * configFetchFailed(e);
+ * }
+ * }
+ * });
+
* + */ +internal interface ConfigCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param config A new `ParseConfig` instance from the server, or `null` if it did not + * succeed. + * @param e The exception raised by the fetch, or `null` if it succeeded. + */ + override fun done(config: ParseConfig?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ConnectivityNotifier.java b/parse/src/main/java/com/parse/ConnectivityNotifier.java deleted file mode 100644 index f5c7e8227..000000000 --- a/parse/src/main/java/com/parse/ConnectivityNotifier.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ReceiverCallNotAllowedException; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -class ConnectivityNotifier extends BroadcastReceiver { - private static final String TAG = "com.parse.ConnectivityNotifier"; - private static final ConnectivityNotifier singleton = new ConnectivityNotifier(); - private final Object lock = new Object(); - private Set listeners = new HashSet<>(); - private boolean hasRegisteredReceiver = false; - - public static ConnectivityNotifier getNotifier(Context context) { - singleton.tryToRegisterForNetworkStatusNotifications(context); - return singleton; - } - - public static boolean isConnected(Context context) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager == null) { - return false; - } - - NetworkInfo network = connectivityManager.getActiveNetworkInfo(); - return network != null && network.isConnected(); - } - - public void addListener(ConnectivityListener delegate) { - synchronized (lock) { - listeners.add(delegate); - } - } - - public void removeListener(ConnectivityListener delegate) { - synchronized (lock) { - listeners.remove(delegate); - } - } - - private boolean tryToRegisterForNetworkStatusNotifications(Context context) { - synchronized (lock) { - if (hasRegisteredReceiver) { - return true; - } - - try { - if (context == null) { - return false; - } - context = context.getApplicationContext(); - context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - hasRegisteredReceiver = true; - return true; - } catch (ReceiverCallNotAllowedException e) { - // In practice, this only happens with the push service, which will trigger a retry soon afterwards. - PLog.v(TAG, "Cannot register a broadcast receiver because the executing " + - "thread is currently in a broadcast receiver. Will try again later."); - return false; - } - } - } - - @Override - public void onReceive(Context context, Intent intent) { - List listenersCopy; - synchronized (lock) { - listenersCopy = new ArrayList<>(listeners); - } - for (ConnectivityListener delegate : listenersCopy) { - delegate.networkConnectivityStatusChanged(context, intent); - } - } - - public interface ConnectivityListener { - void networkConnectivityStatusChanged(Context context, Intent intent); - } -} diff --git a/parse/src/main/java/com/parse/ConnectivityNotifier.kt b/parse/src/main/java/com/parse/ConnectivityNotifier.kt new file mode 100644 index 000000000..9b2c0633e --- /dev/null +++ b/parse/src/main/java/com/parse/ConnectivityNotifier.kt @@ -0,0 +1,79 @@ +/* + * 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 android.content.* +import android.net.ConnectivityManager +import com.parse.PLog.v +import java.util.* + +internal class ConnectivityNotifier : BroadcastReceiver() { + private val lock = Any() + private val listeners: MutableSet = HashSet() + private var hasRegisteredReceiver = false + fun addListener(delegate: ConnectivityListener) { + synchronized(lock) { listeners.add(delegate) } + } + + fun removeListener(delegate: ConnectivityListener) { + synchronized(lock) { listeners.remove(delegate) } + } + + private fun tryToRegisterForNetworkStatusNotifications(context: Context): Boolean { + synchronized(lock) { + return if (hasRegisteredReceiver) { + true + } else try { + context.applicationContext.registerReceiver( + this, + IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + ) + hasRegisteredReceiver = true + true + } catch (e: ReceiverCallNotAllowedException) { + // In practice, this only happens with the push service, which will trigger a retry soon afterwards. + v( + TAG, "Cannot register a broadcast receiver because the executing " + + "thread is currently in a broadcast receiver. Will try again later." + ) + false + } + } + } + + override fun onReceive(context: Context, intent: Intent) { + var listenersCopy: List + synchronized(lock) { listenersCopy = ArrayList(listeners) } + for (delegate in listenersCopy) { + delegate.networkConnectivityStatusChanged(context, intent) + } + } + + interface ConnectivityListener { + fun networkConnectivityStatusChanged(context: Context?, intent: Intent?) + } + + companion object { + private const val TAG = "com.parse.ConnectivityNotifier" + private val singleton = ConnectivityNotifier() + @JvmStatic + fun getNotifier(context: Context): ConnectivityNotifier { + singleton.tryToRegisterForNetworkStatusNotifications(context) + return singleton + } + + @JvmStatic + fun isConnected(context: Context): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetworkInfo + return network != null && network.isConnected + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/CountCallback.java b/parse/src/main/java/com/parse/CountCallback.kt similarity index 57% rename from parse/src/main/java/com/parse/CountCallback.java rename to parse/src/main/java/com/parse/CountCallback.kt index 1c6345cc3..fb9665530 100644 --- a/parse/src/main/java/com/parse/CountCallback.java +++ b/parse/src/main/java/com/parse/CountCallback.kt @@ -6,41 +6,44 @@ * 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; +package com.parse /** - * A {@code CountCallback} is used to run code after a {@link ParseQuery} is used to count objects + * A `CountCallback` is used to run code after a [ParseQuery] is used to count objects * matching a query in a background thread. - *

- * The easiest way to use a {@code CountCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the count is complete. - * The {@code done} function will be run in the UI thread, while the count happens in a + * + * + * The easiest way to use a `CountCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the count is complete. + * The `done` function will be run in the UI thread, while the count happens in a * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- * For example, this sample code counts objects of class {@code "MyClass"}. It calls a + * + * + * For example, this sample code counts objects of class `"MyClass"`. It calls a * different function depending on whether the count succeeded or not. - *

+ * + * *

  * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
  * query.countInBackground(new CountCallback() {
- *   public void done(int count, ParseException e) {
- *     if (e == null) {
- *       objectsWereCountedSuccessfully(count);
- *     } else {
- *       objectCountingFailed();
- *     }
- *   }
+ * public void done(int count, ParseException e) {
+ * if (e == null) {
+ * objectsWereCountedSuccessfully(count);
+ * } else {
+ * objectCountingFailed();
+ * }
+ * }
  * });
- * 
+ * */ // FYI, this does not extend ParseCallback2 since the first param is `int`, which can't be used // in a generic. -public interface CountCallback { +interface CountCallback { /** * Override this function with the code you want to run after the count is complete. * * @param count The number of objects matching the query, or -1 if it failed. * @param e The exception raised by the count, or null if it succeeded. */ - void done(int count, ParseException e); -} + fun done(count: Int, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/DeleteCallback.java b/parse/src/main/java/com/parse/DeleteCallback.java deleted file mode 100644 index 5aa34862a..000000000 --- a/parse/src/main/java/com/parse/DeleteCallback.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -/** - * A {@code DeleteCallback} is used to run code after saving a {@link ParseObject} in a background - * thread. - *

- * The easiest way to use a {@code DeleteCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the delete is complete. - * The {@code done} function will be run in the UI thread, while the delete happens in a - * background thread. This ensures that the UI does not freeze while the delete happens. - *

- * For example, this sample code deletes the object {@code myObject} and calls a different - * function depending on whether the save succeeded or not. - *

- *

- * myObject.deleteInBackground(new DeleteCallback() {
- *   public void done(ParseException e) {
- *     if (e == null) {
- *       myObjectWasDeletedSuccessfully();
- *     } else {
- *       myObjectDeleteDidNotSucceed();
- *     }
- *   }
- * });
- * 
- */ -public interface DeleteCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the delete is complete. - * - * @param e The exception raised by the delete, or {@code null} if it succeeded. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/DeleteCallback.kt b/parse/src/main/java/com/parse/DeleteCallback.kt new file mode 100644 index 000000000..725e56d68 --- /dev/null +++ b/parse/src/main/java/com/parse/DeleteCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 com.parse.ParseCallback1 + +/** + * A `DeleteCallback` is used to run code after saving a [ParseObject] in a background + * thread. + * + * + * The easiest way to use a `DeleteCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the delete is complete. + * The `done` function will be run in the UI thread, while the delete happens in a + * background thread. This ensures that the UI does not freeze while the delete happens. + * + * + * For example, this sample code deletes the object `myObject` and calls a different + * function depending on whether the save succeeded or not. + * + * + *
+ * myObject.deleteInBackground(new DeleteCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * myObjectWasDeletedSuccessfully();
+ * } else {
+ * myObjectDeleteDidNotSucceed();
+ * }
+ * }
+ * });
+
* + */ +interface DeleteCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the delete is complete. + * + * @param e The exception raised by the delete, or `null` if it succeeded. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/EventuallyPin.java b/parse/src/main/java/com/parse/EventuallyPin.java deleted file mode 100644 index d3fdada11..000000000 --- a/parse/src/main/java/com/parse/EventuallyPin.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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 com.parse.http.ParseHttpRequest; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -/** - * Properties - * - time - * Used for sort order when querying for all EventuallyPins - * - type - * TYPE_SAVE or TYPE_DELETE - * - object - * The object that the operation should notify when complete - * - operationSetUUID - * The operationSet to be completed - * - sessionToken - * The user that instantiated the operation - */ -@ParseClassName("_EventuallyPin") -class EventuallyPin extends ParseObject { - - public static final String PIN_NAME = "_eventuallyPin"; - - public static final int TYPE_SAVE = 1; - public static final int TYPE_DELETE = 2; - public static final int TYPE_COMMAND = 3; - - public EventuallyPin() { - super("_EventuallyPin"); - } - - public static Task pinEventuallyCommand(ParseObject object, - ParseRESTCommand command) { - int type = TYPE_COMMAND; - JSONObject json = null; - if (command.httpPath.startsWith("classes")) { - if (command.method == ParseHttpRequest.Method.POST || - command.method == ParseHttpRequest.Method.PUT) { - type = TYPE_SAVE; - } else if (command.method == ParseHttpRequest.Method.DELETE) { - type = TYPE_DELETE; - } - } else { - json = command.toJSONObject(); - } - return pinEventuallyCommand( - type, - object, - command.getOperationSetUUID(), - command.getSessionToken(), - json); - } - - /** - * @param type Type of the command: TYPE_SAVE, TYPE_DELETE, TYPE_COMMAND - * @param obj (Optional) Object the operation is being executed on. Required for TYPE_SAVE and - * TYPE_DELETE. - * @param operationSetUUID (Optional) UUID of the ParseOperationSet that is paired with the ParseCommand. - * Required for TYPE_SAVE and TYPE_DELETE. - * @param sessionToken (Optional) The sessionToken for the command. Required for TYPE_SAVE and TYPE_DELETE. - * @param command (Optional) JSON representation of the ParseCommand. Required for TYPE_COMMAND. - * @return Returns a task that is resolved when the command is pinned. - */ - private static Task pinEventuallyCommand(int type, ParseObject obj, - String operationSetUUID, String sessionToken, JSONObject command) { - final EventuallyPin pin = new EventuallyPin(); - pin.put("uuid", UUID.randomUUID().toString()); - pin.put("time", new Date()); - pin.put("type", type); - if (obj != null) { - pin.put("object", obj); - } - if (operationSetUUID != null) { - pin.put("operationSetUUID", operationSetUUID); - } - if (sessionToken != null) { - pin.put("sessionToken", sessionToken); - } - if (command != null) { - pin.put("command", command); - } - return pin.pinInBackground(PIN_NAME).continueWith(new Continuation() { - @Override - public EventuallyPin then(Task task) { - return pin; - } - }); - } - - public static Task> findAllPinned() { - return findAllPinned(null); - } - - public static Task> findAllPinned(Collection excludeUUIDs) { - ParseQuery query = new ParseQuery<>(EventuallyPin.class) - .fromPin(PIN_NAME) - .ignoreACLs() - .orderByAscending("time"); - - if (excludeUUIDs != null) { - query.whereNotContainedIn("uuid", excludeUUIDs); - } - - // We need pass in a null user because we don't want the query to fetch the current user - // from LDS. - return query.findInBackground().onSuccessTask(new Continuation, Task>>() { - @Override - public Task> then(Task> task) { - final List pins = task.getResult(); - List> tasks = new ArrayList<>(); - - for (EventuallyPin pin : pins) { - ParseObject object = pin.getObject(); - if (object != null) { - tasks.add(object.fetchFromLocalDatastoreAsync().makeVoid()); - } - } - - return Task.whenAll(tasks).continueWithTask(new Continuation>>() { - @Override - public Task> then(Task task) { - return Task.forResult(pins); - } - }); - } - }); - } - - @Override - boolean needsDefaultACL() { - return false; - } - - public String getUUID() { - return getString("uuid"); - } - - public int getType() { - return getInt("type"); - } - - public ParseObject getObject() { - return getParseObject("object"); - } - - public String getOperationSetUUID() { - return getString("operationSetUUID"); - } - - public String getSessionToken() { - return getString("sessionToken"); - } - - public ParseRESTCommand getCommand() throws JSONException { - JSONObject json = getJSONObject("command"); - ParseRESTCommand command = null; - if (ParseRESTCommand.isValidCommandJSONObject(json)) { - command = ParseRESTCommand.fromJSONObject(json); - } else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) { - // do nothing - } else { - throw new JSONException("Failed to load command from JSON."); - } - return command; - } -} diff --git a/parse/src/main/java/com/parse/EventuallyPin.kt b/parse/src/main/java/com/parse/EventuallyPin.kt new file mode 100644 index 000000000..42f28c6c8 --- /dev/null +++ b/parse/src/main/java/com/parse/EventuallyPin.kt @@ -0,0 +1,154 @@ +/* + * 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 com.parse.ParseRESTCommand.Companion.fromJSONObject +import com.parse.ParseRESTCommand.Companion.isValidCommandJSONObject +import com.parse.ParseRESTCommand.Companion.isValidOldFormatCommandJSONObject +import com.parse.boltsinternal.Task +import com.parse.http.ParseHttpRequest +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +/** + * Properties + * - time + * Used for sort order when querying for all EventuallyPins + * - type + * TYPE_SAVE or TYPE_DELETE + * - object + * The object that the operation should notify when complete + * - operationSetUUID + * The operationSet to be completed + * - sessionToken + * The user that instantiated the operation + */ +@ParseClassName("_EventuallyPin") +internal class EventuallyPin : ParseObject("_EventuallyPin") { + public override fun needsDefaultACL(): Boolean { + return false + } + + val uUID: String? + get() = getString("uuid") + val type: Int + get() = getInt("type") + val `object`: ParseObject? + get() = getParseObject("object") + val operationSetUUID: String? + get() = getString("operationSetUUID") + val sessionToken: String? + get() = getString("sessionToken") + + @get:Throws(JSONException::class) + val command: ParseRESTCommand? + get() { + val json = getJSONObject("command") + var command: ParseRESTCommand? = null + if (isValidCommandJSONObject(json!!)) { + command = fromJSONObject(json) + } else if (!isValidOldFormatCommandJSONObject(json)) { + throw JSONException("Failed to load command from JSON.") + } + return command + } + + companion object { + const val PIN_NAME = "_eventuallyPin" + const val TYPE_SAVE = 1 + const val TYPE_DELETE = 2 + const val TYPE_COMMAND = 3 + + fun pinEventuallyCommand( + `object`: ParseObject?, + command: ParseRESTCommand + ): Task { + var type = TYPE_COMMAND + var json: JSONObject? = null + if (command.httpPath!!.startsWith("classes")) { + if (command.method == ParseHttpRequest.Method.POST || + command.method == ParseHttpRequest.Method.PUT + ) { + type = TYPE_SAVE + } else if (command.method == ParseHttpRequest.Method.DELETE) { + type = TYPE_DELETE + } + } else { + json = command.toJSONObject() + } + return pinEventuallyCommand( + type, + `object`, + command.operationSetUUID, + command.sessionToken, + json + ) + } + + /** + * @param type Type of the command: TYPE_SAVE, TYPE_DELETE, TYPE_COMMAND + * @param obj (Optional) Object the operation is being executed on. Required for TYPE_SAVE and + * TYPE_DELETE. + * @param operationSetUUID (Optional) UUID of the ParseOperationSet that is paired with the ParseCommand. + * Required for TYPE_SAVE and TYPE_DELETE. + * @param sessionToken (Optional) The sessionToken for the command. Required for TYPE_SAVE and TYPE_DELETE. + * @param command (Optional) JSON representation of the ParseCommand. Required for TYPE_COMMAND. + * @return Returns a task that is resolved when the command is pinned. + */ + private fun pinEventuallyCommand( + type: Int, obj: ParseObject?, + operationSetUUID: String?, sessionToken: String?, command: JSONObject? + ): Task { + val pin = EventuallyPin() + pin.put("uuid", UUID.randomUUID().toString()) + pin.put("time", Date()) + pin.put("type", type) + if (obj != null) { + pin.put("object", obj) + } + if (operationSetUUID != null) { + pin.put("operationSetUUID", operationSetUUID) + } + if (sessionToken != null) { + pin.put("sessionToken", sessionToken) + } + if (command != null) { + pin.put("command", command) + } + return pin.pinInBackground(PIN_NAME).continueWith { pin } + } + + @JvmStatic + @JvmOverloads + fun findAllPinned(excludeUUIDs: Collection? = null): Task> { + val query = ParseQuery(EventuallyPin::class.java) + .fromPin(PIN_NAME) + .ignoreACLs() + .orderByAscending("time") + if (excludeUUIDs != null) { + query.whereNotContainedIn("uuid", excludeUUIDs) + } + + // We need pass in a null user because we don't want the query to fetch the current user + // from LDS. + return query.findInBackground().onSuccessTask { task: Task> -> + val pins = task.result + val tasks: MutableList> = ArrayList() + for (pin in pins) { + val `object` = pin.`object` + if (`object` != null) { + tasks.add(`object`.fetchFromLocalDatastoreAsync()!!.makeVoid()) + } + } + Task.whenAll(tasks).continueWithTask { Task.forResult(pins) } + } + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/FileObjectStore.java b/parse/src/main/java/com/parse/FileObjectStore.java deleted file mode 100644 index 8982434f6..000000000 --- a/parse/src/main/java/com/parse/FileObjectStore.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.IOException; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Task; - -class FileObjectStore implements ParseObjectStore { - - private final String className; - private final File file; - private final ParseObjectCurrentCoder coder; - - public FileObjectStore(Class clazz, File file, ParseObjectCurrentCoder coder) { - this(getSubclassingController().getClassName(clazz), file, coder); - } - public FileObjectStore(String className, File file, ParseObjectCurrentCoder coder) { - this.className = className; - this.file = file; - this.coder = coder; - } - - private static ParseObjectSubclassingController getSubclassingController() { - return ParseCorePlugins.getInstance().getSubclassingController(); - } - - /** - * Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. - * - * @param coder Current coder to encode the ParseObject. - * @param current ParseObject which needs to be saved to disk. - * @param file The file to save the object to. - * @see #getFromDisk(ParseObjectCurrentCoder, File, ParseObject.State.Init) - */ - private static void saveToDisk( - ParseObjectCurrentCoder coder, ParseObject current, File file) { - JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get()); - try { - ParseFileUtils.writeJSONObjectToFile(file, json); - } catch (IOException e) { - //TODO(grantland): We should do something if this fails... - } - } - - /** - * Retrieves a {@code ParseObject} from a file on disk in /2/ format. - * - * @param coder Current coder to decode the ParseObject. - * @param file The file to retrieve the object from. - * @param builder An empty builder which is used to generate a empty state and rebuild a ParseObject. - * @return The {@code ParseObject} that was retrieved. If the file wasn't found, or the contents - * of the file is an invalid {@code ParseObject}, returns {@code null}. - * @see #saveToDisk(ParseObjectCurrentCoder, ParseObject, File) - */ - private static T getFromDisk( - ParseObjectCurrentCoder coder, File file, ParseObject.State.Init builder) { - JSONObject json; - try { - json = ParseFileUtils.readFileToJSONObject(file); - } catch (IOException | JSONException e) { - return null; - } - - ParseObject.State newState = coder.decode(builder, json, ParseDecoder.get()) - .isComplete(true) - .build(); - return ParseObject.from(newState); - } - - @Override - public Task setAsync(final T object) { - return Task.call(new Callable() { - @Override - public Void call() { - saveToDisk(coder, object, file); - //TODO (grantland): check to see if this failed? We currently don't for legacy reasons. - return null; - } - }, ParseExecutors.io()); - } - - @Override - public Task getAsync() { - return Task.call(new Callable() { - @Override - public T call() { - if (!file.exists()) { - return null; - } - return getFromDisk(coder, file, ParseObject.State.newBuilder(className)); - } - }, ParseExecutors.io()); - } - - @Override - public Task existsAsync() { - return Task.call(new Callable() { - @Override - public Boolean call() { - return file.exists(); - } - }, ParseExecutors.io()); - } - - @Override - public Task deleteAsync() { - return Task.call(new Callable() { - @Override - public Void call() { - if (file.exists() && !ParseFileUtils.deleteQuietly(file)) { - throw new RuntimeException("Unable to delete"); - } - - return null; - } - }, ParseExecutors.io()); - } -} diff --git a/parse/src/main/java/com/parse/FileObjectStore.kt b/parse/src/main/java/com/parse/FileObjectStore.kt new file mode 100644 index 000000000..2f846cd64 --- /dev/null +++ b/parse/src/main/java/com/parse/FileObjectStore.kt @@ -0,0 +1,104 @@ +/* + * 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 com.parse.PointerEncoder.Companion.get +import com.parse.boltsinternal.Task +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.IOException + +internal class FileObjectStore( + private val className: String, + private val file: File, + private val coder: ParseObjectCurrentCoder +) : ParseObjectStore { + constructor(clazz: Class?, file: File, coder: ParseObjectCurrentCoder) : this( + subclassingController.getClassName(clazz), file, coder + ) + + override fun setAsync(`object`: T): Task { + return Task.call({ + saveToDisk(coder, `object`, file) + null + }, ParseExecutors.io()) + } + + override val getAsync: Task + get() = Task.call({ + if (!file.exists()) { + return@call null + } + getFromDisk(coder, file, ParseObject.State.newBuilder(className)) + }, ParseExecutors.io()) + + override fun existsAsync(): Task { + return Task.call({ file.exists() }, ParseExecutors.io()) + } + + override fun deleteAsync(): Task { + return Task.call({ + if (file.exists() && !ParseFileUtils.deleteQuietly(file)) { + throw RuntimeException("Unable to delete") + } + null + }, ParseExecutors.io()) + } + + companion object { + private val subclassingController: ParseObjectSubclassingController + get() = ParseCorePlugins.getInstance().subclassingController + + /** + * Saves the `ParseObject` to the a file on disk as JSON in /2/ format. + * + * @param coder Current coder to encode the ParseObject. + * @param current ParseObject which needs to be saved to disk. + * @param file The file to save the object to. + * @see .getFromDisk + */ + private fun saveToDisk( + coder: ParseObjectCurrentCoder, current: ParseObject, file: File + ) { + val json = coder.encode(current.state, null, get())!! + try { + ParseFileUtils.writeJSONObjectToFile(file, json) + } catch (e: IOException) { + //TODO(grantland): We should do something if this fails... + } + } + + /** + * Retrieves a `ParseObject` from a file on disk in /2/ format. + * + * @param coder Current coder to decode the ParseObject. + * @param file The file to retrieve the object from. + * @param builder An empty builder which is used to generate a empty state and rebuild a ParseObject. + * @return The `ParseObject` that was retrieved. If the file wasn't found, or the contents + * of the file is an invalid `ParseObject`, returns `null`. + * @see .saveToDisk + */ + private fun getFromDisk( + coder: ParseObjectCurrentCoder, file: File, builder: ParseObject.State.Init<*> + ): T? { + val json: JSONObject = try { + ParseFileUtils.readFileToJSONObject(file) + } catch (e: IOException) { + return null + } catch (e: JSONException) { + return null + } + val newState: ParseObject.State = coder.decode(builder, json, ParseDecoder.get()) + .isComplete(true) + .build() + return ParseObject.from(newState) + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/FindCallback.java b/parse/src/main/java/com/parse/FindCallback.java deleted file mode 100644 index 63f5a4d2f..000000000 --- a/parse/src/main/java/com/parse/FindCallback.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 java.util.List; - -/** - * A {@code FindCallback} is used to run code after a {@link ParseQuery} is used to fetch a list of - * {@link ParseObject}s in a background thread. - *

- * The easiest way to use a {@code FindCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the fetch is complete. - * The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- * For example, this sample code fetches all objects of class {@code "MyClass"}. It calls a - * different function depending on whether the fetch succeeded or not. - *

- *

- * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
- * query.findInBackground(new FindCallback<ParseObject>() {
- *   public void done(List<ParseObject> objects, ParseException e) {
- *     if (e == null) {
- *       objectsWereRetrievedSuccessfully(objects);
- *     } else {
- *       objectRetrievalFailed();
- *     }
- *   }
- * });
- * 
- */ -public interface FindCallback extends ParseCallback2, ParseException> { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param objects The objects that were retrieved, or null if it did not succeed. - * @param e The exception raised by the save, or null if it succeeded. - */ - @Override - void done(List objects, ParseException e); -} diff --git a/parse/src/main/java/com/parse/FindCallback.kt b/parse/src/main/java/com/parse/FindCallback.kt new file mode 100644 index 000000000..9a64a75b2 --- /dev/null +++ b/parse/src/main/java/com/parse/FindCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * A `FindCallback` is used to run code after a [ParseQuery] is used to fetch a list of + * [ParseObject]s in a background thread. + * + * + * The easiest way to use a `FindCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the fetch is complete. + * The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + * For example, this sample code fetches all objects of class `"MyClass"`. It calls a + * different function depending on whether the fetch succeeded or not. + * + * + *
+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.findInBackground(new FindCallback<ParseObject>() {
+ * public void done(List<ParseObject> objects, ParseException e) {
+ * if (e == null) {
+ * objectsWereRetrievedSuccessfully(objects);
+ * } else {
+ * objectRetrievalFailed();
+ * }
+ * }
+ * });
+
* + */ +interface FindCallback : ParseCallback2?, ParseException?> { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param objects The objects that were retrieved, or null if it did not succeed. + * @param e The exception raised by the save, or null if it succeeded. + */ + override fun done(objects: List?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/FunctionCallback.java b/parse/src/main/java/com/parse/FunctionCallback.java deleted file mode 100644 index c7538ae77..000000000 --- a/parse/src/main/java/com/parse/FunctionCallback.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -/** - * A {@code FunctionCallback} is used to run code after {@link ParseCloud#callFunction} is used to - * run a Cloud Function in a background thread. - *

- * The easiest way to use a {@code FunctionCallback} is through an anonymous inner class. Override - * the {@code done} function to specify what the callback should do after the cloud function is - * complete. The {@code done} function will be run in the UI thread, while the fetch happens in - * a background thread. This ensures that the UI does not freeze while the fetch happens. - *

- * For example, this sample code calls a cloud function {@code "MyFunction"} with - * {@code params} and calls a different function depending on whether the function succeeded. - *

- *

- * ParseCloud.callFunctionInBackground("MyFunction"new, params, FunctionCallback() {
- *   public void done(ParseObject object, ParseException e) {
- *     if (e == null) {
- *       cloudFunctionSucceeded(object);
- *     } else {
- *       cloudFunctionFailed();
- *     }
- *   }
- * });
- * 
- * - * @param The type of object returned by the Cloud Function. - */ -public interface FunctionCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the cloud function is complete. - * - * @param object The object that was returned by the cloud function. - * @param e The exception raised by the cloud call, or {@code null} if it succeeded. - */ - @Override - void done(T object, ParseException e); -} diff --git a/parse/src/main/java/com/parse/FunctionCallback.kt b/parse/src/main/java/com/parse/FunctionCallback.kt new file mode 100644 index 000000000..cb8505a1d --- /dev/null +++ b/parse/src/main/java/com/parse/FunctionCallback.kt @@ -0,0 +1,48 @@ +/* + * 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 + +/** + * A `FunctionCallback` is used to run code after [ParseCloud.callFunction] is used to + * run a Cloud Function in a background thread. + * + * + * The easiest way to use a `FunctionCallback` is through an anonymous inner class. Override + * the `done` function to specify what the callback should do after the cloud function is + * complete. The `done` function will be run in the UI thread, while the fetch happens in + * a background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + * For example, this sample code calls a cloud function `"MyFunction"` with + * `params` and calls a different function depending on whether the function succeeded. + * + * + *
+ * ParseCloud.callFunctionInBackground("MyFunction"new, params, FunctionCallback() {
+ * public void done(ParseObject object, ParseException e) {
+ * if (e == null) {
+ * cloudFunctionSucceeded(object);
+ * } else {
+ * cloudFunctionFailed();
+ * }
+ * }
+ * });
+
* + * + * @param The type of object returned by the Cloud Function. + */ +internal interface FunctionCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the cloud function is complete. + * + * @param object The object that was returned by the cloud function. + * @param e The exception raised by the cloud call, or `null` if it succeeded. + */ + override fun done(`object`: T?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/GetCallback.java b/parse/src/main/java/com/parse/GetCallback.java deleted file mode 100644 index 386bb7370..000000000 --- a/parse/src/main/java/com/parse/GetCallback.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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; - -/** - * A {@code GetCallback} is used to run code after a {@link ParseQuery} is used to fetch a - * {@link ParseObject} in a background thread. - *

- * The easiest way to use a {@code GetCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the fetch is complete. - * The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- * For example, this sample code fetches an object of class {@code "MyClass"} and id - * {@code myId}. It calls a different function depending on whether the fetch succeeded or not. - *

- *

- * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
- * query.getInBackground(myId, new GetCallback<ParseObject>() {
- *   public void done(ParseObject object, ParseException e) {
- *     if (e == null) {
- *       objectWasRetrievedSuccessfully(object);
- *     } else {
- *       objectRetrievalFailed();
- *     }
- *   }
- * });
- * 
- */ -public interface GetCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param object The object that was retrieved, or {@code null} if it did not succeed. - * @param e The exception raised by the fetch, or {@code null} if it succeeded. - */ - @Override - void done(T object, ParseException e); -} diff --git a/parse/src/main/java/com/parse/GetCallback.kt b/parse/src/main/java/com/parse/GetCallback.kt new file mode 100644 index 000000000..dc000672c --- /dev/null +++ b/parse/src/main/java/com/parse/GetCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * A `GetCallback` is used to run code after a [ParseQuery] is used to fetch a + * [ParseObject] in a background thread. + * + * + * The easiest way to use a `GetCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the fetch is complete. + * The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + * For example, this sample code fetches an object of class `"MyClass"` and id + * `myId`. It calls a different function depending on whether the fetch succeeded or not. + * + * + *
+ * ParseQuery<ParseObject> query = ParseQuery.getQuery("MyClass");
+ * query.getInBackground(myId, new GetCallback<ParseObject>() {
+ * public void done(ParseObject object, ParseException e) {
+ * if (e == null) {
+ * objectWasRetrievedSuccessfully(object);
+ * } else {
+ * objectRetrievalFailed();
+ * }
+ * }
+ * });
+
* + */ +interface GetCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param object The object that was retrieved, or `null` if it did not succeed. + * @param e The exception raised by the fetch, or `null` if it succeeded. + */ + override fun done(`object`: T?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/GetDataCallback.java b/parse/src/main/java/com/parse/GetDataCallback.java deleted file mode 100644 index 2e9e6fd50..000000000 --- a/parse/src/main/java/com/parse/GetDataCallback.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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; - -/** - * A {@code GetDataCallback} is used to run code after a {@link ParseFile} fetches its data on a - * background thread. - *

- * The easiest way to use a {@code GetDataCallback} is through an anonymous inner class. Override - * the {@code done} function to specify what the callback should do after the fetch is complete. - * The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- *

- * file.getDataInBackground(new GetDataCallback() {
- *   public void done(byte[] data, ParseException e) {
- *     // ...
- *   }
- * });
- * 
- */ -public interface GetDataCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param data The data that was retrieved, or {@code null} if it did not succeed. - * @param e The exception raised by the fetch, or {@code null} if it succeeded. - */ - @Override - void done(byte[] data, ParseException e); -} - diff --git a/parse/src/main/java/com/parse/GetDataCallback.kt b/parse/src/main/java/com/parse/GetDataCallback.kt new file mode 100644 index 000000000..35bdac384 --- /dev/null +++ b/parse/src/main/java/com/parse/GetDataCallback.kt @@ -0,0 +1,38 @@ +/* + * 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 + +/** + * A `GetDataCallback` is used to run code after a [ParseFile] fetches its data on a + * background thread. + * + * + * The easiest way to use a `GetDataCallback` is through an anonymous inner class. Override + * the `done` function to specify what the callback should do after the fetch is complete. + * The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + *
+ * file.getDataInBackground(new GetDataCallback() {
+ * public void done(byte[] data, ParseException e) {
+ * // ...
+ * }
+ * });
+
* + */ +internal interface GetDataCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param data The data that was retrieved, or `null` if it did not succeed. + * @param e The exception raised by the fetch, or `null` if it succeeded. + */ + override fun done(data: ByteArray?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/GetDataStreamCallback.java b/parse/src/main/java/com/parse/GetDataStreamCallback.java deleted file mode 100644 index fc36389af..000000000 --- a/parse/src/main/java/com/parse/GetDataStreamCallback.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 java.io.InputStream; - -/** - * A {@code GetDataStreamCallback} is used to run code after a {@link ParseFile} fetches its data on - * a background thread. - *

- * The easiest way to use a {@code GetDataStreamCallback} is through an anonymous inner class. - * Override the {@code done} function to specify what the callback should do after the fetch is - * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- *

- * file.getDataStreamInBackground(new GetDataStreamCallback() {
- *   public void done(InputSteam input, ParseException e) {
- *     // ...
- *   }
- * });
- * 
- */ -public interface GetDataStreamCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param input The data that was retrieved, or {@code null} if it did not succeed. - * @param e The exception raised by the fetch, or {@code null} if it succeeded. - */ - @Override - void done(InputStream input, ParseException e); -} diff --git a/parse/src/main/java/com/parse/GetDataStreamCallback.kt b/parse/src/main/java/com/parse/GetDataStreamCallback.kt new file mode 100644 index 000000000..896b82dd6 --- /dev/null +++ b/parse/src/main/java/com/parse/GetDataStreamCallback.kt @@ -0,0 +1,40 @@ +/* + * 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 java.io.InputStream + +/** + * A `GetDataStreamCallback` is used to run code after a [ParseFile] fetches its data on + * a background thread. + * + * + * The easiest way to use a `GetDataStreamCallback` is through an anonymous inner class. + * Override the `done` function to specify what the callback should do after the fetch is + * complete. The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + *
+ * file.getDataStreamInBackground(new GetDataStreamCallback() {
+ * public void done(InputSteam input, ParseException e) {
+ * // ...
+ * }
+ * });
+
* + */ +internal interface GetDataStreamCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param input The data that was retrieved, or `null` if it did not succeed. + * @param e The exception raised by the fetch, or `null` if it succeeded. + */ + override fun done(input: InputStream?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/GetFileCallback.java b/parse/src/main/java/com/parse/GetFileCallback.java deleted file mode 100644 index f412a1ab5..000000000 --- a/parse/src/main/java/com/parse/GetFileCallback.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 java.io.File; - -/** - * A {@code GetFileCallback} is used to run code after a {@link ParseFile} fetches its data on - * a background thread. - *

- * The easiest way to use a {@code GetFileCallback} is through an anonymous inner class. - * Override the {@code done} function to specify what the callback should do after the fetch is - * complete. The {@code done} function will be run in the UI thread, while the fetch happens in a - * background thread. This ensures that the UI does not freeze while the fetch happens. - *

- *

- * file.getFileInBackground(new GetFileCallback() {
- *   public void done(File file, ParseException e) {
- *     // ...
- *   }
- * });
- * 
- */ -public interface GetFileCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the fetch is complete. - * - * @param file The data that was retrieved, or {@code null} if it did not succeed. - * @param e The exception raised by the fetch, or {@code null} if it succeeded. - */ - @Override - void done(File file, ParseException e); -} diff --git a/parse/src/main/java/com/parse/GetFileCallback.kt b/parse/src/main/java/com/parse/GetFileCallback.kt new file mode 100644 index 000000000..08dfee330 --- /dev/null +++ b/parse/src/main/java/com/parse/GetFileCallback.kt @@ -0,0 +1,40 @@ +/* + * 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 java.io.File + +/** + * A `GetFileCallback` is used to run code after a [ParseFile] fetches its data on + * a background thread. + * + * + * The easiest way to use a `GetFileCallback` is through an anonymous inner class. + * Override the `done` function to specify what the callback should do after the fetch is + * complete. The `done` function will be run in the UI thread, while the fetch happens in a + * background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + *
+ * file.getFileInBackground(new GetFileCallback() {
+ * public void done(File file, ParseException e) {
+ * // ...
+ * }
+ * });
+
* + */ +internal interface GetFileCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the fetch is complete. + * + * @param file The data that was retrieved, or `null` if it did not succeed. + * @param e The exception raised by the fetch, or `null` if it succeeded. + */ + override fun done(file: File?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/InstallationId.java b/parse/src/main/java/com/parse/InstallationId.java deleted file mode 100644 index a82d6e001..000000000 --- a/parse/src/main/java/com/parse/InstallationId.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.UUID; - -/** - * Since we cannot save dirty ParseObjects to disk and we must be able to persist UUIDs across - * restarts even if the ParseInstallation is not saved, we use this legacy file still as a - * bootstrapping environment as well until the full ParseInstallation is cached to disk. - *

- * TODO: Allow dirty objects to be saved to disk. - */ -/* package */ class InstallationId { - - private static final String TAG = "InstallationId"; - - private final Object lock = new Object(); - private final File file; - private String installationId; - - public InstallationId(File file) { - this.file = file; - } - - /** - * Loads the installationId from memory, then tries to loads the legacy installationId from disk - * if it is present, or creates a new random UUID. - */ - public String get() { - synchronized (lock) { - if (installationId == null) { - try { - installationId = ParseFileUtils.readFileToString(file, "UTF-8"); - } catch (FileNotFoundException e) { - PLog.i(TAG, "Couldn't find existing installationId file. Creating one instead."); - } catch (IOException e) { - PLog.e(TAG, "Unexpected exception reading installation id from disk", e); - } - } - - if (installationId == null) { - setInternal(UUID.randomUUID().toString()); - } - } - - return installationId; - } - - /** - * Sets the installationId and persists it to disk. - */ - public void set(String newInstallationId) { - synchronized (lock) { - if (ParseTextUtils.isEmpty(newInstallationId) - || newInstallationId.equals(get())) { - return; - } - setInternal(newInstallationId); - } - } - - private void setInternal(String newInstallationId) { - synchronized (lock) { - try { - ParseFileUtils.writeStringToFile(file, newInstallationId, "UTF-8"); - } catch (IOException e) { - PLog.e(TAG, "Unexpected exception writing installation id to disk", e); - } - - installationId = newInstallationId; - } - } - - /* package for tests */ void clear() { - synchronized (lock) { - installationId = null; - ParseFileUtils.deleteQuietly(file); - } - } -} diff --git a/parse/src/main/java/com/parse/InstallationId.kt b/parse/src/main/java/com/parse/InstallationId.kt new file mode 100644 index 000000000..3719dde70 --- /dev/null +++ b/parse/src/main/java/com/parse/InstallationId.kt @@ -0,0 +1,88 @@ +/* + * 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 com.parse.PLog.e +import com.parse.PLog.i +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* + +/** + * Since we cannot save dirty ParseObjects to disk and we must be able to persist UUIDs across + * restarts even if the ParseInstallation is not saved, we use this legacy file still as a + * bootstrapping environment as well until the full ParseInstallation is cached to disk. + * + * + * TODO: Allow dirty objects to be saved to disk. + */ +class InstallationId(private val file: File) { + private val lock = Any() + private var installationId: String? = null + + /** + * Loads the installationId from memory, then tries to loads the legacy installationId from disk + * if it is present, or creates a new random UUID. + */ + fun get(): String? { + synchronized(lock) { + if (installationId == null) { + try { + installationId = ParseFileUtils.readFileToString(file, "UTF-8") + } catch (e: FileNotFoundException) { + i(TAG, "Couldn't find existing installationId file. Creating one instead.") + } catch (e: IOException) { + e(TAG, "Unexpected exception reading installation id from disk", e) + } + } + if (installationId == null) { + setInternal(UUID.randomUUID().toString()) + } + } + return installationId + } + + /** + * Sets the installationId and persists it to disk. + */ + fun set(newInstallationId: String) { + synchronized(lock) { + if (ParseTextUtils.isEmpty(newInstallationId) + || newInstallationId == get() + ) { + return + } + setInternal(newInstallationId) + } + } + + private fun setInternal(newInstallationId: String) { + synchronized(lock) { + try { + ParseFileUtils.writeStringToFile(file, newInstallationId, "UTF-8") + } catch (e: IOException) { + e(TAG, "Unexpected exception writing installation id to disk", e) + } + installationId = newInstallationId + } + } + + /* package for tests */ + fun clear() { + synchronized(lock) { + installationId = null + ParseFileUtils.deleteQuietly(file) + } + } + + companion object { + private const val TAG = "InstallationId" + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/KnownParseObjectDecoder.java b/parse/src/main/java/com/parse/KnownParseObjectDecoder.java deleted file mode 100644 index db86f03d1..000000000 --- a/parse/src/main/java/com/parse/KnownParseObjectDecoder.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 java.util.Map; - -/** - * A subclass of ParseDecoder which can keep ParseObject that - * has been fetched instead of creating a new instance. - */ -class KnownParseObjectDecoder extends ParseDecoder { - private Map fetchedObjects; - - public KnownParseObjectDecoder(Map fetchedObjects) { - super(); - this.fetchedObjects = fetchedObjects; - } - - /** - * If the object has been fetched, the fetched object will be returned. Otherwise a - * new created object will be returned. - */ - @Override - protected ParseObject decodePointer(String className, String objectId) { - if (fetchedObjects != null && fetchedObjects.containsKey(objectId)) { - return fetchedObjects.get(objectId); - } - return super.decodePointer(className, objectId); - } -} diff --git a/parse/src/main/java/com/parse/KnownParseObjectDecoder.kt b/parse/src/main/java/com/parse/KnownParseObjectDecoder.kt new file mode 100644 index 000000000..b943887ba --- /dev/null +++ b/parse/src/main/java/com/parse/KnownParseObjectDecoder.kt @@ -0,0 +1,26 @@ +/* + * 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 + +/** + * A subclass of `ParseDecoder` which can keep `ParseObject` that + * has been fetched instead of creating a new instance. + */ +internal class KnownParseObjectDecoder(private val fetchedObjects: Map?) : + ParseDecoder() { + /** + * If the object has been fetched, the fetched object will be returned. Otherwise a + * new created object will be returned. + */ + override fun decodePointer(className: String?, objectId: String?): ParseObject? { + return if (fetchedObjects != null && fetchedObjects.containsKey(objectId)) { + fetchedObjects[objectId] + } else super.decodePointer(className, objectId) + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/Lists.java b/parse/src/main/java/com/parse/Lists.java deleted file mode 100644 index 3e70becec..000000000 --- a/parse/src/main/java/com/parse/Lists.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2007 The Guava Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.parse; - -import java.util.AbstractList; -import java.util.List; - -/** - * Static utility methods pertaining to {@link List} instances. - *

- *

See the Guava User Guide article on - * {@code Lists}. - * - * @author Kevin Bourrillion - * @author Mike Bostock - * @author Louis Wasserman - * @since 2.0 - */ -class Lists { - - /** - * Returns consecutive sublists of a list, each of the same size (the final list may be smaller). - * For example, partitioning a list containing [a, b, c, d, e] with a partition size of 3 yields - * [[a, b, c], [d, e]] -- an outer list containing two inner lists of three and two elements, all - * in the original order. - *

- * The outer list is unmodifiable, but reflects the latest state of the source list. The inner - * lists are sublist views of the original list, produced on demand using List.subList(int, int), - * and are subject to all the usual caveats about modification as explained in that API. - * - * @param list the list to return consecutive sublists of - * @param size the desired size of each sublist (the last may be smaller) - * @return a list of consecutive sublists - */ - /* package */ - static List> partition(List list, int size) { - return new Partition<>(list, size); - } - - private static class Partition extends AbstractList> { - - private final List list; - private final int size; - - public Partition(List list, int size) { - this.list = list; - this.size = size; - } - - @Override - public List get(int location) { - int start = location * size; - int end = Math.min(start + size, list.size()); - return list.subList(start, end); - } - - @Override - public int size() { - return (int) Math.ceil((double) list.size() / size); - } - } -} diff --git a/parse/src/main/java/com/parse/LocalIdManager.java b/parse/src/main/java/com/parse/LocalIdManager.java deleted file mode 100644 index 53c1461ec..000000000 --- a/parse/src/main/java/com/parse/LocalIdManager.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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.IOException; -import java.util.Random; - -/** - * Manages a set of local ids and possible mappings to global Parse objectIds. This class is - * thread-safe. - */ -class LocalIdManager { - - // Path to the local id storage on disk. - private final File diskPath; - // Random generator for inventing new ids. - private final Random random; - - /** - * Creates a new LocalIdManager with default options. - */ - /* package for tests */ LocalIdManager(File root) { - diskPath = new File(root, "LocalId"); - random = new Random(); - } - - /** - * Returns true if localId has the right basic format for a local id. - */ - private boolean isLocalId(String localId) { - if (!localId.startsWith("local_")) { - return false; - } - for (int i = 6; i < localId.length(); ++i) { - char c = localId.charAt(i); - if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f')) { - return false; - } - } - return true; - } - - /** - * Grabs one entry in the local id map off the disk. - */ - private synchronized MapEntry getMapEntry(String localId) { - if (!isLocalId(localId)) { - throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); - } - - try { - JSONObject json = ParseFileUtils.readFileToJSONObject(new File(diskPath, localId)); - - MapEntry entry = new MapEntry(); - entry.retainCount = json.optInt("retainCount", 0); - entry.objectId = json.optString("objectId", null); - return entry; - } catch (IOException | JSONException e) { - return new MapEntry(); - } - } - - /** - * Writes one entry to the local id map on disk. - */ - private synchronized void putMapEntry(String localId, MapEntry entry) { - if (!isLocalId(localId)) { - throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); - } - - JSONObject json = new JSONObject(); - try { - json.put("retainCount", entry.retainCount); - if (entry.objectId != null) { - json.put("objectId", entry.objectId); - } - } catch (JSONException je) { - throw new IllegalStateException("Error creating local id map entry.", je); - } - - File file = new File(diskPath, localId); - if (!diskPath.exists()) { - diskPath.mkdirs(); - } - - try { - ParseFileUtils.writeJSONObjectToFile(file, json); - } catch (IOException e) { - //TODO (grantland): We should do something if this fails... - } - } - - /** - * Removes an entry from the local id map on disk. - */ - private synchronized void removeMapEntry(String localId) { - if (!isLocalId(localId)) { - throw new IllegalStateException("Tried to get invalid local id: \"" + localId + "\"."); - } - File file = new File(diskPath, localId); - ParseFileUtils.deleteQuietly(file); - } - - /** - * Creates a new local id. - */ - synchronized String createLocalId() { - long localIdNumber = random.nextLong(); - String localId = "local_" + Long.toHexString(localIdNumber); - - if (!isLocalId(localId)) { - throw new IllegalStateException("Generated an invalid local id: \"" + localId + "\". " - + "This should never happen. Open a bug at https://github.com/parse-community/parse-server"); - } - - return localId; - } - - /** - * Increments the retain count of a local id on disk. - */ - synchronized void retainLocalIdOnDisk(String localId) { - MapEntry entry = getMapEntry(localId); - entry.retainCount++; - putMapEntry(localId, entry); - } - - /** - * Decrements the retain count of a local id on disk. If the retain count hits zero, the id is - * forgotten forever. - */ - synchronized void releaseLocalIdOnDisk(String localId) { - MapEntry entry = getMapEntry(localId); - entry.retainCount--; - - if (entry.retainCount > 0) { - putMapEntry(localId, entry); - } else { - removeMapEntry(localId); - } - } - - /** - * Returns the objectId associated with a given local id. Returns null if no objectId is yet known - * for the local id. - */ - synchronized String getObjectId(String localId) { - MapEntry entry = getMapEntry(localId); - return entry.objectId; - } - - /** - * Sets the objectId associated with a given local id. - */ - synchronized void setObjectId(String localId, String objectId) { - MapEntry entry = getMapEntry(localId); - if (entry.retainCount > 0) { - if (entry.objectId != null) { - throw new IllegalStateException( - "Tried to set an objectId for a localId that already has one."); - } - entry.objectId = objectId; - putMapEntry(localId, entry); - } - } - - /** - * Clears all local ids from the map. Returns true is the cache was already empty. - */ - synchronized boolean clear() throws IOException { - String[] files = diskPath.list(); - if (files == null) { - return false; - } - if (files.length == 0) { - return false; - } - for (String fileName : files) { - File file = new File(diskPath, fileName); - if (!file.delete()) { - throw new IOException("Unable to delete file " + fileName + " in localId cache."); - } - } - return true; - } - - /** - * Internal class representing all the information we know about a local id. - */ - private static class MapEntry { - String objectId; - int retainCount; - } -} diff --git a/parse/src/main/java/com/parse/LocalIdManager.kt b/parse/src/main/java/com/parse/LocalIdManager.kt new file mode 100644 index 000000000..6731c09f0 --- /dev/null +++ b/parse/src/main/java/com/parse/LocalIdManager.kt @@ -0,0 +1,188 @@ +/* + * 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.IOException +import java.util.* + +/** + * Manages a set of local ids and possible mappings to global Parse objectIds. This class is + * thread-safe. + */ +internal class LocalIdManager(root: File?) { + // Path to the local id storage on disk. + private val diskPath: File = File(root, "LocalId") + + // Random generator for inventing new ids. + private val random: Random = Random() + + /** + * Returns true if localId has the right basic format for a local id. + */ + private fun isLocalId(localId: String): Boolean { + if (!localId.startsWith("local_")) { + return false + } + for (i in 6 until localId.length) { + val c = localId[i] + if (c !in '0'..'9' && c !in 'a'..'f') { + return false + } + } + return true + } + + /** + * Grabs one entry in the local id map off the disk. + */ + @Synchronized + private fun getMapEntry(localId: String): MapEntry { + check(isLocalId(localId)) { "Tried to get invalid local id: \"$localId\"." } + return try { + val json = ParseFileUtils.readFileToJSONObject(File(diskPath, localId)) + val entry = MapEntry() + entry.retainCount = json.optInt("retainCount", 0) + entry.objectId = json.optString("objectId", null) + entry + } catch (e: IOException) { + MapEntry() + } catch (e: JSONException) { + MapEntry() + } + } + + /** + * Writes one entry to the local id map on disk. + */ + @Synchronized + private fun putMapEntry(localId: String, entry: MapEntry) { + check(isLocalId(localId)) { "Tried to get invalid local id: \"$localId\"." } + val json = JSONObject() + try { + json.put("retainCount", entry.retainCount) + if (entry.objectId != null) { + json.put("objectId", entry.objectId) + } + } catch (je: JSONException) { + throw IllegalStateException("Error creating local id map entry.", je) + } + val file = File(diskPath, localId) + if (!diskPath.exists()) { + diskPath.mkdirs() + } + try { + ParseFileUtils.writeJSONObjectToFile(file, json) + } catch (e: IOException) { + //TODO (grantland): We should do something if this fails... + } + } + + /** + * Removes an entry from the local id map on disk. + */ + @Synchronized + private fun removeMapEntry(localId: String) { + check(isLocalId(localId)) { "Tried to get invalid local id: \"$localId\"." } + val file = File(diskPath, localId) + ParseFileUtils.deleteQuietly(file) + } + + /** + * Creates a new local id. + */ + @Synchronized + fun createLocalId(): String { + val localIdNumber = random.nextLong() + val localId = "local_" + java.lang.Long.toHexString(localIdNumber) + check(isLocalId(localId)) { + ("Generated an invalid local id: \"" + localId + "\". " + + "This should never happen. Open a bug at https://github.com/parse-community/parse-server") + } + return localId + } + + /** + * Increments the retain count of a local id on disk. + */ + @Synchronized + fun retainLocalIdOnDisk(localId: String) { + val entry = getMapEntry(localId) + entry.retainCount++ + putMapEntry(localId, entry) + } + + /** + * Decrements the retain count of a local id on disk. If the retain count hits zero, the id is + * forgotten forever. + */ + @Synchronized + fun releaseLocalIdOnDisk(localId: String) { + val entry = getMapEntry(localId) + entry.retainCount-- + if (entry.retainCount > 0) { + putMapEntry(localId, entry) + } else { + removeMapEntry(localId) + } + } + + /** + * Returns the objectId associated with a given local id. Returns null if no objectId is yet known + * for the local id. + */ + @Synchronized + fun getObjectId(localId: String): String? { + val entry = getMapEntry(localId) + return entry.objectId + } + + /** + * Sets the objectId associated with a given local id. + */ + @Synchronized + fun setObjectId(localId: String, objectId: String?) { + val entry = getMapEntry(localId) + if (entry.retainCount > 0) { + check(entry.objectId == null) { "Tried to set an objectId for a localId that already has one." } + entry.objectId = objectId + putMapEntry(localId, entry) + } + } + + /** + * Clears all local ids from the map. Returns true is the cache was already empty. + */ + @Synchronized + @Throws(IOException::class) + fun clear(): Boolean { + val files = diskPath.list() ?: return false + if (files.isEmpty()) { + return false + } + for (fileName in files) { + val file = File(diskPath, fileName) + if (!file.delete()) { + throw IOException("Unable to delete file $fileName in localId cache.") + } + } + return true + } + + /** + * Internal class representing all the information we know about a local id. + */ + private class MapEntry { + var objectId: String? = null + var retainCount = 0 + } + +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/LocationCallback.java b/parse/src/main/java/com/parse/LocationCallback.java deleted file mode 100644 index 48e497210..000000000 --- a/parse/src/main/java/com/parse/LocationCallback.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - - -/** - * A {@code LocationCallback} is used to run code after a Location has been fetched by - * {@link com.parse.ParseGeoPoint#getCurrentLocationInBackground(long, android.location.Criteria)}. - *

- * The easiest way to use a {@code LocationCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the location has been - * fetched. The {@code done} function will be run in the UI thread, while the location check - * happens in a background thread. This ensures that the UI does not freeze while the fetch happens. - *

- * For example, this sample code defines a timeout for fetching the user's current location, and - * provides a callback. Within the callback, the success and failure cases are handled differently. - *

- *

- * ParseGeoPoint.getCurrentLocationAsync(1000, new LocationCallback() {
- *   public void done(ParseGeoPoint geoPoint, ParseException e) {
- *     if (e == null) {
- *       // do something with your new ParseGeoPoint
- *     } else {
- *       // handle your error
- *       e.printStackTrace();
- *     }
- *   }
- * });
- * 
- */ -public interface LocationCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the location fetch is complete. - * - * @param geoPoint The {@link ParseGeoPoint} returned by the location fetch. - * @param e The exception raised by the location fetch, or {@code null} if it succeeded. - */ - @Override - void done(ParseGeoPoint geoPoint, ParseException e); -} diff --git a/parse/src/main/java/com/parse/LocationCallback.kt b/parse/src/main/java/com/parse/LocationCallback.kt new file mode 100644 index 000000000..29e322b1e --- /dev/null +++ b/parse/src/main/java/com/parse/LocationCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * A `LocationCallback` is used to run code after a Location has been fetched by + * [com.parse.ParseGeoPoint.getCurrentLocationInBackground]. + * + * + * The easiest way to use a `LocationCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the location has been + * fetched. The `done` function will be run in the UI thread, while the location check + * happens in a background thread. This ensures that the UI does not freeze while the fetch happens. + * + * + * For example, this sample code defines a timeout for fetching the user's current location, and + * provides a callback. Within the callback, the success and failure cases are handled differently. + * + * + *
+ * ParseGeoPoint.getCurrentLocationAsync(1000, new LocationCallback() {
+ * public void done(ParseGeoPoint geoPoint, ParseException e) {
+ * if (e == null) {
+ * // do something with your new ParseGeoPoint
+ * } else {
+ * // handle your error
+ * e.printStackTrace();
+ * }
+ * }
+ * });
+
* + */ +internal interface LocationCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the location fetch is complete. + * + * @param geoPoint The [ParseGeoPoint] returned by the location fetch. + * @param e The exception raised by the location fetch, or `null` if it succeeded. + */ + override fun done(geoPoint: ParseGeoPoint?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/LocationNotifier.java b/parse/src/main/java/com/parse/LocationNotifier.java deleted file mode 100644 index 0db252ac9..000000000 --- a/parse/src/main/java/com/parse/LocationNotifier.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 android.content.Context; -import android.location.Criteria; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; - -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -/** - * LocationNotifier is a wrapper around fetching the current device's location. It looks for the GPS - * and Network LocationProviders by default (printStackTrace()'ing if, for example, the app doesn't - * have the correct permissions in its AndroidManifest.xml). This class is intended to be used for a - * single location update. - *

- * When testing, if a fakeLocation is provided (via setFakeLocation()), we don't wait for the - * LocationManager to fire or for the timer to run out; instead, we build a local LocationListener, - * then call the onLocationChanged() method manually. - */ -class LocationNotifier { - private static Location fakeLocation = null; - - /** - * Asynchronously gets the current location of the device. - *

- * This will request location updates from the best provider that match the given criteria - * and return the first location received. - *

- * You can customize the criteria to meet your specific needs. - * * For higher accuracy, you can set {@link Criteria#setAccuracy(int)}, however result in longer - * times for a fix. - * * For better battery efficiency and faster location fixes, you can set - * {@link Criteria#setPowerRequirement(int)}, however, this will result in lower accuracy. - * - * @param context The context used to request location updates. - * @param timeout The number of milliseconds to allow before timing out. - * @param criteria The application criteria for selecting a location provider. - * @see android.location.LocationManager#getBestProvider(android.location.Criteria, boolean) - * @see android.location.LocationManager#requestLocationUpdates(String, long, float, android.location.LocationListener) - */ - /* package */ - static Task getCurrentLocationAsync(Context context, - long timeout, Criteria criteria) { - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - final Capture> timeoutFuture = new Capture<>(); - final LocationManager manager = - (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - final LocationListener listener = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - if (location == null) { - return; - } - - timeoutFuture.get().cancel(true); - - tcs.trySetResult(location); - manager.removeUpdates(this); - } - - @Override - public void onProviderDisabled(String provider) { - } - - @Override - public void onProviderEnabled(String provider) { - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - } - }; - - timeoutFuture.set(ParseExecutors.scheduled().schedule(new Runnable() { - @Override - public void run() { - tcs.trySetError(new ParseException(ParseException.TIMEOUT, "Location fetch timed out.")); - manager.removeUpdates(listener); - } - }, timeout, TimeUnit.MILLISECONDS)); - - String provider = manager.getBestProvider(criteria, true); - if (provider != null) { - manager.requestLocationUpdates(provider, /* minTime */ 0, /* minDistance */ 0.0f, listener); - } - - if (fakeLocation != null) { - listener.onLocationChanged(fakeLocation); - } - - return tcs.getTask(); - } - - /** - * Helper method for testing. - */ - /* package */ - static void setFakeLocation(Location location) { - fakeLocation = location; - } -} diff --git a/parse/src/main/java/com/parse/LocationNotifier.kt b/parse/src/main/java/com/parse/LocationNotifier.kt new file mode 100644 index 000000000..fc8165c01 --- /dev/null +++ b/parse/src/main/java/com/parse/LocationNotifier.kt @@ -0,0 +1,103 @@ +/* + * 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 android.content.Context +import android.location.Criteria +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import com.parse.boltsinternal.Capture +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +/** + * LocationNotifier is a wrapper around fetching the current device's location. It looks for the GPS + * and Network LocationProviders by default (printStackTrace()'ing if, for example, the app doesn't + * have the correct permissions in its AndroidManifest.xml). This class is intended to be used for a + * *single* location update. + * + * + * When testing, if a fakeLocation is provided (via setFakeLocation()), we don't wait for the + * LocationManager to fire or for the timer to run out; instead, we build a local LocationListener, + * then call the onLocationChanged() method manually. + */ +internal object LocationNotifier { + private var fakeLocation: Location? = null + + /** + * Asynchronously gets the current location of the device. + * + * + * This will request location updates from the best provider that match the given criteria + * and return the first location received. + * + * + * You can customize the criteria to meet your specific needs. + * * For higher accuracy, you can set [Criteria.setAccuracy], however result in longer + * times for a fix. + * * For better battery efficiency and faster location fixes, you can set + * [Criteria.setPowerRequirement], however, this will result in lower accuracy. + * + * @param context The context used to request location updates. + * @param timeout The number of milliseconds to allow before timing out. + * @param criteria The application criteria for selecting a location provider. + * @see android.location.LocationManager.getBestProvider + * @see android.location.LocationManager.requestLocationUpdates + */ + /* package */ + @JvmStatic + fun getCurrentLocationAsync( + context: Context, + timeout: Long, criteria: Criteria? + ): Task { + val tcs = TaskCompletionSource() + val timeoutFuture = Capture>() + val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val listener: LocationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + timeoutFuture.get().cancel(true) + tcs.trySetResult(location) + manager.removeUpdates(this) + } + + override fun onProviderDisabled(provider: String) {} + override fun onProviderEnabled(provider: String) {} + override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {} + } + timeoutFuture.set(ParseExecutors.scheduled().schedule({ + tcs.trySetError(ParseException(ParseException.TIMEOUT, "Location fetch timed out.")) + manager.removeUpdates(listener) + }, timeout, TimeUnit.MILLISECONDS)) + val provider = manager.getBestProvider(criteria!!, true) + if (provider != null) { + manager.requestLocationUpdates( + provider, /* minTime */ + 0, /* minDistance */ + 0.0f, + listener + ) + } + if (fakeLocation != null) { + listener.onLocationChanged(fakeLocation!!) + } + return tcs.task + } + + /** + * Helper method for testing. + */ + /* package */ + fun setFakeLocation(location: Location?) { + fakeLocation = location + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/LockSet.java b/parse/src/main/java/com/parse/LockSet.java deleted file mode 100644 index 46978e117..000000000 --- a/parse/src/main/java/com/parse/LockSet.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 java.util.Collection; -import java.util.Comparator; -import java.util.Set; -import java.util.TreeSet; -import java.util.WeakHashMap; -import java.util.concurrent.locks.Lock; - -class LockSet { - private static final WeakHashMap stableIds = new WeakHashMap<>(); - private static long nextStableId = 0L; - - private final Set locks; - - public LockSet(Collection locks) { - this.locks = new TreeSet<>(new Comparator() { - @Override - public int compare(Lock lhs, Lock rhs) { - Long lhsId = getStableId(lhs); - Long rhsId = getStableId(rhs); - return lhsId.compareTo(rhsId); - } - }); - this.locks.addAll(locks); - } - - private static Long getStableId(Lock lock) { - synchronized (stableIds) { - if (stableIds.containsKey(lock)) { - return stableIds.get(lock); - } - long id = nextStableId++; - stableIds.put(lock, id); - return id; - } - } - - public void lock() { - for (Lock l : locks) { - l.lock(); - } - } - - public void unlock() { - for (Lock l : locks) { - l.unlock(); - } - } -} diff --git a/parse/src/main/java/com/parse/LockSet.kt b/parse/src/main/java/com/parse/LockSet.kt new file mode 100644 index 000000000..44e4f8c97 --- /dev/null +++ b/parse/src/main/java/com/parse/LockSet.kt @@ -0,0 +1,51 @@ +/* + * 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 java.util.* +import java.util.concurrent.locks.Lock + +internal class LockSet(locks: Collection?) { + private val locks: MutableSet + fun lock() { + for (l in locks) { + l.lock() + } + } + + fun unlock() { + for (l in locks) { + l.unlock() + } + } + + companion object { + private val stableIds = WeakHashMap() + private var nextStableId = 0L + private fun getStableId(lock: Lock): Long? { + synchronized(stableIds) { + if (stableIds.containsKey(lock)) { + return stableIds[lock] + } + val id = nextStableId++ + stableIds[lock] = id + return id + } + } + } + + init { + this.locks = TreeSet { lhs: Lock, rhs: Lock -> + val lhsId = getStableId(lhs) + val rhsId = getStableId(rhs) + lhsId!!.compareTo(rhsId!!) + } + this.locks.addAll(locks!!) + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/LogInCallback.java b/parse/src/main/java/com/parse/LogInCallback.java deleted file mode 100644 index 2f322c44f..000000000 --- a/parse/src/main/java/com/parse/LogInCallback.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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; - -/** - * A {@code LogInCallback} is used to run code after logging in a user. - *

- * The easiest way to use a {@code LogInCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the login is complete. - * The {@code done} function will be run in the UI thread, while the login happens in a - * background thread. This ensures that the UI does not freeze while the save happens. - *

- * For example, this sample code logs in a user and calls a different function depending on whether - * the login succeeded or not. - *

- *

- * ParseUser.logInInBackground("username", "password", new LogInCallback() {
- *   public void done(ParseUser user, ParseException e) {
- *     if (e == null && user != null) {
- *       loginSuccessful();
- *     } else if (user == null) {
- *       usernameOrPasswordIsInvalid();
- *     } else {
- *       somethingWentWrong();
- *     }
- *   }
- * });
- * 
- */ -public interface LogInCallback extends ParseCallback2 { - /** - * Override this function with the code you want to run after the save is complete. - * - * @param user The user that logged in, if the username and password is valid. - * @param e The exception raised by the login, or {@code null} if it succeeded. - */ - @Override - void done(ParseUser user, ParseException e); -} diff --git a/parse/src/main/java/com/parse/LogInCallback.kt b/parse/src/main/java/com/parse/LogInCallback.kt new file mode 100644 index 000000000..3d8369612 --- /dev/null +++ b/parse/src/main/java/com/parse/LogInCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * A `LogInCallback` is used to run code after logging in a user. + * + * + * The easiest way to use a `LogInCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the login is complete. + * The `done` function will be run in the UI thread, while the login happens in a + * background thread. This ensures that the UI does not freeze while the save happens. + * + * + * For example, this sample code logs in a user and calls a different function depending on whether + * the login succeeded or not. + * + * + *
+ * ParseUser.logInInBackground("username", "password", new LogInCallback() {
+ * public void done(ParseUser user, ParseException e) {
+ * if (e == null && user != null) {
+ * loginSuccessful();
+ * } else if (user == null) {
+ * usernameOrPasswordIsInvalid();
+ * } else {
+ * somethingWentWrong();
+ * }
+ * }
+ * });
+
* + */ +internal interface LogInCallback : ParseCallback2 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param user The user that logged in, if the username and password is valid. + * @param e The exception raised by the login, or `null` if it succeeded. + */ + override fun done(user: ParseUser?, e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/LogOutCallback.java b/parse/src/main/java/com/parse/LogOutCallback.java deleted file mode 100644 index 6a2c08cd9..000000000 --- a/parse/src/main/java/com/parse/LogOutCallback.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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; - -/** - * A {@code LogOutCallback} is used to run code after logging out a user. - *

- * The easiest way to use a {@code LogOutCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the login is complete. - * The {@code done} function will be run in the UI thread, while the login happens in a - * background thread. This ensures that the UI does not freeze while the save happens. - *

- * For example, this sample code logs out a user and calls a different function depending on whether - * the log out succeeded or not. - *

- *

- * ParseUser.logOutInBackground(new LogOutCallback() {
- *   public void done(ParseException e) {
- *     if (e == null) {
- *       logOutSuccessful();
- *     } else {
- *       somethingWentWrong();
- *     }
- *   }
- * });
- * 
- */ -public interface LogOutCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the save is complete. - * - * @param e The exception raised by the log out, or {@code null} if it succeeded. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/LogOutCallback.kt b/parse/src/main/java/com/parse/LogOutCallback.kt new file mode 100644 index 000000000..b9b7c7b2a --- /dev/null +++ b/parse/src/main/java/com/parse/LogOutCallback.kt @@ -0,0 +1,44 @@ +/* + * 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 + +/** + * A `LogOutCallback` is used to run code after logging out a user. + * + * + * The easiest way to use a `LogOutCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the login is complete. + * The `done` function will be run in the UI thread, while the login happens in a + * background thread. This ensures that the UI does not freeze while the save happens. + * + * + * For example, this sample code logs out a user and calls a different function depending on whether + * the log out succeeded or not. + * + * + *
+ * ParseUser.logOutInBackground(new LogOutCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * logOutSuccessful();
+ * } else {
+ * somethingWentWrong();
+ * }
+ * }
+ * });
+
* + */ +internal interface LogOutCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param e The exception raised by the log out, or `null` if it succeeded. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ManifestInfo.java b/parse/src/main/java/com/parse/ManifestInfo.java deleted file mode 100644 index 1474e5edf..000000000 --- a/parse/src/main/java/com/parse/ManifestInfo.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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 android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.os.Bundle; - -import java.util.ArrayList; -import java.util.List; - -/** - * A utility class for retrieving app metadata such as the app name, default icon, whether or not - * the app declares the correct permissions for push, etc. - */ -public class ManifestInfo { - private static final String TAG = "com.parse.ManifestInfo"; - - private static final Object lock = new Object(); - /* package */ static int versionCode = -1; - /* package */ static String versionName = null; - private static int iconId = 0; - private static String displayName = null; - - /** - * Returns the version code for this app, as specified by the android:versionCode attribute in the - * element of the manifest. - */ - public static int getVersionCode() { - synchronized (lock) { - if (versionCode == -1) { - try { - versionCode = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionCode; - } catch (NameNotFoundException e) { - PLog.e(TAG, "Couldn't find info about own package", e); - } - } - } - - return versionCode; - } - - /** - * Returns the version name for this app, as specified by the android:versionName attribute in the - * element of the manifest. - */ - public static String getVersionName() { - synchronized (lock) { - if (versionName == null) { - try { - versionName = getPackageManager().getPackageInfo(getContext().getPackageName(), 0).versionName; - } catch (NameNotFoundException e) { - PLog.e(TAG, "Couldn't find info about own package", e); - versionName = "unknown"; - } - if (versionName == null) { - // Some contexts, such as instrumentation tests can always have this value - // return as null. We will change to "unknown" for this case as well, so that - // an exception isn't thrown for adding a null header later. - versionName = "unknown"; - } - } - } - - return versionName; - } - - /** - * Returns the display name of the app used by the app launcher, as specified by the android:label - * attribute in the element of the manifest. - */ - public static String getDisplayName(Context context) { - synchronized (lock) { - if (displayName == null) { - ApplicationInfo appInfo = context.getApplicationInfo(); - displayName = context.getPackageManager().getApplicationLabel(appInfo).toString(); - } - } - - return displayName; - } - - /** - * Returns the default icon id used by this application, as specified by the android:icon - * attribute in the element of the manifest. - */ - public static int getIconId() { - synchronized (lock) { - if (iconId == 0) { - iconId = getContext().getApplicationInfo().icon; - } - } - return iconId; - } - - /** - * Returns a list of ResolveInfo objects corresponding to the BroadcastReceivers with Intent Filters - * specifying the given action within the app's package. - */ - /* package */ - static List getIntentReceivers(String... actions) { - Context context = getContext(); - PackageManager pm = context.getPackageManager(); - String packageName = context.getPackageName(); - List list = new ArrayList<>(); - - for (String action : actions) { - list.addAll(pm.queryBroadcastReceivers( - new Intent(action), - PackageManager.GET_INTENT_FILTERS)); - } - - for (int i = list.size() - 1; i >= 0; --i) { - String receiverPackageName = list.get(i).activityInfo.packageName; - if (!receiverPackageName.equals(packageName)) { - list.remove(i); - } - } - return list; - } - - private static Context getContext() { - return Parse.getApplicationContext(); - } - - private static PackageManager getPackageManager() { - return getContext().getPackageManager(); - } - - private static ApplicationInfo getApplicationInfo(Context context, int flags) { - try { - return context.getPackageManager().getApplicationInfo(context.getPackageName(), flags); - } catch (NameNotFoundException e) { - return null; - } - } - - /** - * @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise null. - */ - public static Bundle getApplicationMetadata(Context context) { - ApplicationInfo info = getApplicationInfo(context, PackageManager.GET_META_DATA); - if (info != null) { - return info.metaData; - } - return null; - } -} diff --git a/parse/src/main/java/com/parse/ManifestInfo.kt b/parse/src/main/java/com/parse/ManifestInfo.kt new file mode 100644 index 000000000..156fd2a9e --- /dev/null +++ b/parse/src/main/java/com/parse/ManifestInfo.kt @@ -0,0 +1,152 @@ +/* + * 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 android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.os.Bundle +import com.parse.PLog.e +import java.util.* + +/** + * A utility class for retrieving app metadata such as the app name, default icon, whether or not + * the app declares the correct permissions for push, etc. + */ +object ManifestInfo { + private const val TAG = "com.parse.ManifestInfo" + private val lock = Any() + + /** + * Returns the version code for this app, as specified by the android:versionCode attribute in the + * element of the manifest. + */ + var versionCode = -1 + get() { + synchronized(lock) { + if (versionCode == -1) { + try { + return packageManager.getPackageInfo(context.packageName, 0).versionCode + } catch (e: PackageManager.NameNotFoundException) { + e(TAG, "Couldn't find info about own package", e) + } + } + } + return field + } + + /** + * Returns the version name for this app, as specified by the android:versionName attribute in the + * element of the manifest. + */ + var versionName: String? = null + get() { + synchronized(lock) { + if (versionName == null) { + versionName = try { + packageManager.getPackageInfo(context.packageName, 0).versionName + } catch (e: PackageManager.NameNotFoundException) { + e(TAG, "Couldn't find info about own package", e) + "unknown" + } + if (versionName == null) { + // Some contexts, such as instrumentation tests can always have this value + // return as null. We will change to "unknown" for this case as well, so that + // an exception isn't thrown for adding a null header later. + versionName = "unknown" + } + } + } + return field + } + + /** + * Returns the default icon id used by this application, as specified by the android:icon + * attribute in the element of the manifest. + */ + @JvmStatic + var iconId = 0 + get() { + synchronized(lock) { + if (field == 0) { + field = context.applicationInfo.icon + } + } + return field + } + private set + + /** + * Returns the display name of the app used by the app launcher, as specified by the android:label + * attribute in the element of the manifest. + */ + private val displayName: String? + get() { + synchronized(lock) { + if (displayName == null) { + val appInfo = context.applicationInfo + return context.packageManager.getApplicationLabel(appInfo).toString() + } + } + return displayName + } + + + /** + * Returns a list of ResolveInfo objects corresponding to the BroadcastReceivers with Intent Filters + * specifying the given action within the app's package. + */ + /* package */ + @JvmStatic + fun getIntentReceivers(vararg actions: String?): List { + val context = context + val pm = context.packageManager + val packageName = context.packageName + val list: MutableList = ArrayList() + for (action in actions) { + list.addAll( + pm.queryBroadcastReceivers( + Intent(action), + PackageManager.GET_INTENT_FILTERS + ) + ) + } + for (i in list.indices.reversed()) { + val receiverPackageName = list[i].activityInfo.packageName + if (receiverPackageName != packageName) { + list.removeAt(i) + } + } + return list + } + + private val context: Context + get() = Parse.getApplicationContext() + private val packageManager: PackageManager + get() = context.packageManager + + private fun getApplicationInfo(context: Context, flags: Int): ApplicationInfo? { + return try { + context.packageManager.getApplicationInfo(context.packageName, flags) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + /** + * @return A [Bundle] if meta-data is specified in AndroidManifest, otherwise null. + */ + @JvmStatic + fun getApplicationMetadata(context: Context): Bundle? { + val info = getApplicationInfo(context, PackageManager.GET_META_DATA) + return info?.metaData + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/NetworkObjectController.java b/parse/src/main/java/com/parse/NetworkObjectController.java deleted file mode 100644 index 0c668a1fd..000000000 --- a/parse/src/main/java/com/parse/NetworkObjectController.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class NetworkObjectController implements ParseObjectController { - - private ParseHttpClient client; - private ParseObjectCoder coder; - - public NetworkObjectController(ParseHttpClient client) { - this.client = client; - this.coder = ParseObjectCoder.get(); - } - - @Override - public Task fetchAsync( - final ParseObject.State state, String sessionToken, final ParseDecoder decoder) { - final ParseRESTCommand command = ParseRESTObjectCommand.getObjectCommand( - state.objectId(), - state.className(), - sessionToken); - - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseObject.State then(Task task) { - JSONObject result = task.getResult(); - // Copy and clear to create an new empty instance of the same type as `state` - ParseObject.State.Init builder = state.newBuilder().clear(); - return coder.decode(builder, result, decoder) - .isComplete(true) - .build(); - } - }); - } - - @Override - public Task saveAsync( - final ParseObject.State state, - final ParseOperationSet operations, - String sessionToken, - final ParseDecoder decoder) { - /* - * Get the JSON representation of the object, and use some of the information to construct the - * command. - */ - JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); - - ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand( - state, - objectJSON, - sessionToken); - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseObject.State then(Task task) { - JSONObject result = task.getResult(); - // Copy and clear to create an new empty instance of the same type as `state` - ParseObject.State.Init builder = state.newBuilder().clear(); - return coder.decode(builder, result, decoder) - .isComplete(false) - .build(); - } - }); - } - - @Override - public List> saveAllAsync( - List states, - List operationsList, - String sessionToken, - List decoders) { - int batchSize = states.size(); - - List commands = new ArrayList<>(batchSize); - ParseEncoder encoder = PointerEncoder.get(); - for (int i = 0; i < batchSize; i++) { - ParseObject.State state = states.get(i); - ParseOperationSet operations = operationsList.get(i); - JSONObject objectJSON = coder.encode(state, operations, encoder); - - ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand( - state, objectJSON, sessionToken); - commands.add(command); - } - - final List> batchTasks = - ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken); - - final List> tasks = new ArrayList<>(batchSize); - for (int i = 0; i < batchSize; i++) { - final ParseObject.State state = states.get(i); - final ParseDecoder decoder = decoders.get(i); - tasks.add(batchTasks.get(i).onSuccess(new Continuation() { - @Override - public ParseObject.State then(Task task) { - JSONObject result = task.getResult(); - // Copy and clear to create an new empty instance of the same type as `state` - ParseObject.State.Init builder = state.newBuilder().clear(); - return coder.decode(builder, result, decoder) - .isComplete(false) - .build(); - } - })); - } - return tasks; - } - - @Override - public Task deleteAsync(ParseObject.State state, String sessionToken) { - ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand( - state, sessionToken); - - return command.executeAsync(client).makeVoid(); - } - - @Override - public List> deleteAllAsync( - List states, String sessionToken) { - int batchSize = states.size(); - - List commands = new ArrayList<>(batchSize); - for (int i = 0; i < batchSize; i++) { - ParseObject.State state = states.get(i); - ParseRESTObjectCommand command = ParseRESTObjectCommand.deleteObjectCommand( - state, sessionToken); - commands.add(command); - } - - final List> batchTasks = - ParseRESTObjectBatchCommand.executeBatch(client, commands, sessionToken); - - List> tasks = new ArrayList<>(batchSize); - for (int i = 0; i < batchSize; i++) { - tasks.add(batchTasks.get(i).makeVoid()); - } - return tasks; - } -} diff --git a/parse/src/main/java/com/parse/NetworkObjectController.kt b/parse/src/main/java/com/parse/NetworkObjectController.kt new file mode 100644 index 000000000..a010ec0db --- /dev/null +++ b/parse/src/main/java/com/parse/NetworkObjectController.kt @@ -0,0 +1,130 @@ +/* + * 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 com.parse.ParseRESTObjectBatchCommand.Companion.executeBatch +import com.parse.boltsinternal.Task +import org.json.JSONObject +import java.util.* + +internal class NetworkObjectController(private val client: ParseHttpClient) : + ParseObjectController { + private val coder: ParseObjectCoder = ParseObjectCoder.get() + + override fun fetchAsync( + state: ParseObject.State, sessionToken: String, decoder: ParseDecoder + ): Task { + val command: ParseRESTCommand = ParseRESTObjectCommand.getObjectCommand( + state.objectId(), + state.className(), + sessionToken + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + // Copy and clear to create an new empty instance of the same type as `state` + val builder = state.newBuilder>().clear() + coder.decode(builder, result!!, decoder) + .isComplete(true) + .build() + } + } + + override fun saveAsync( + state: ParseObject.State, + operations: ParseOperationSet, + sessionToken: String, + decoder: ParseDecoder + ): Task { + /* + * Get the JSON representation of the object, and use some of the information to construct the + * command. + */ + val objectJSON = coder.encode(state, operations, PointerEncoder.get()) + val command = ParseRESTObjectCommand.saveObjectCommand( + state, + objectJSON, + sessionToken + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + // Copy and clear to create an new empty instance of the same type as `state` + val builder = state.newBuilder>().clear() + coder.decode(builder, result!!, decoder) + .isComplete(false) + .build() + } + } + + override fun saveAllAsync( + states: List, + operationsList: List, + sessionToken: String, + decoders: List + ): List> { + val batchSize = states.size + val commands: MutableList = ArrayList(batchSize) + val encoder: ParseEncoder = PointerEncoder.get() + for (i in 0 until batchSize) { + val state = states[i] + val operations = operationsList[i] + val objectJSON = coder.encode(state, operations, encoder) + val command = ParseRESTObjectCommand.saveObjectCommand( + state, objectJSON, sessionToken + ) + commands.add(command) + } + val batchTasks = executeBatch( + client, commands, sessionToken + ) + val tasks: MutableList> = ArrayList(batchSize) + for (i in 0 until batchSize) { + val state = states[i] + val decoder = decoders[i] + tasks.add(batchTasks[i].onSuccess { task: Task -> + val result = task.result + // Copy and clear to create an new empty instance of the same type as `state` + val builder = state.newBuilder>().clear() + coder.decode(builder, result!!, decoder) + .isComplete(false) + .build() + }) + } + return tasks + } + + override fun deleteAsync(state: ParseObject.State, sessionToken: String): Task { + val command = ParseRESTObjectCommand.deleteObjectCommand( + state, sessionToken + ) + return command.executeAsync(client).makeVoid() + } + + override fun deleteAllAsync( + states: List, sessionToken: String + ): List> { + val batchSize = states.size + val commands: MutableList = ArrayList(batchSize) + for (i in 0 until batchSize) { + val state = states[i] + val command = ParseRESTObjectCommand.deleteObjectCommand( + state, sessionToken + ) + commands.add(command) + } + val batchTasks = executeBatch( + client, commands, sessionToken + ) + val tasks: MutableList> = ArrayList(batchSize) + for (i in 0 until batchSize) { + tasks.add(batchTasks[i].makeVoid()) + } + return tasks + } + +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/NetworkQueryController.java b/parse/src/main/java/com/parse/NetworkQueryController.java deleted file mode 100644 index 9721038c7..000000000 --- a/parse/src/main/java/com/parse/NetworkQueryController.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class NetworkQueryController extends AbstractQueryController { - - private static final String TAG = "NetworkQueryController"; - - private final ParseHttpClient restClient; - - public NetworkQueryController(ParseHttpClient restClient) { - this.restClient = restClient; - } - - @Override - public Task> findAsync( - ParseQuery.State state, ParseUser user, Task cancellationToken) { - String sessionToken = user != null ? user.getSessionToken() : null; - return findAsync(state, sessionToken, cancellationToken); - } - - @Override - public Task countAsync( - ParseQuery.State state, ParseUser user, Task cancellationToken) { - String sessionToken = user != null ? user.getSessionToken() : null; - return countAsync(state, sessionToken, cancellationToken); - } - - /** - * Retrieves a list of {@link ParseObject}s that satisfy this query from the source. - * - * @return A list of all {@link ParseObject}s obeying the conditions set in this query. - */ - /* package */ Task> findAsync( - final ParseQuery.State state, - String sessionToken, - Task ct) { - final long queryStart = System.nanoTime(); - - final ParseRESTCommand command = ParseRESTQueryCommand.findCommand(state, sessionToken); - - final long querySent = System.nanoTime(); - return command.executeAsync(restClient, ct).onSuccess(new Continuation>() { - @Override - public List then(Task task) throws Exception { - JSONObject json = task.getResult(); - // Cache the results, unless we are ignoring the cache - ParseQuery.CachePolicy policy = state.cachePolicy(); - if (policy != null && (policy != ParseQuery.CachePolicy.IGNORE_CACHE)) { - ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), json.toString()); - } - - long queryReceived = System.nanoTime(); - - List response = convertFindResponse(state, task.getResult()); - - long objectsParsed = System.nanoTime(); - - if (json.has("trace")) { - Object serverTrace = json.get("trace"); - PLog.d("ParseQuery", - String.format("Query pre-processing took %f seconds\n" + - "%s\n" + - "Client side parsing took %f seconds\n", - (querySent - queryStart) / (1000.0f * 1000.0f), - serverTrace, - (objectsParsed - queryReceived) / (1000.0f * 1000.0f))); - } - return response; - } - }, Task.BACKGROUND_EXECUTOR); - } - - /* package */ Task countAsync( - final ParseQuery.State state, - String sessionToken, - Task ct) { - final ParseRESTCommand command = ParseRESTQueryCommand.countCommand(state, sessionToken); - - return command.executeAsync(restClient, ct).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // Cache the results, unless we are ignoring the cache - ParseQuery.CachePolicy policy = state.cachePolicy(); - if (policy != null && policy != ParseQuery.CachePolicy.IGNORE_CACHE) { - JSONObject result = task.getResult(); - ParseKeyValueCache.saveToKeyValueCache(command.getCacheKey(), result.toString()); - } - return task; - } - }, Task.BACKGROUND_EXECUTOR).onSuccess(new Continuation() { - @Override - public Integer then(Task task) { - // Convert response - return task.getResult().optInt("count"); - } - }); - } - - // Converts the JSONArray that represents the results of a find command to an - // ArrayList. - /* package */ List convertFindResponse(ParseQuery.State state, - JSONObject response) throws JSONException { - ArrayList answer = new ArrayList<>(); - JSONArray results = response.getJSONArray("results"); - if (results == null) { - PLog.d(TAG, "null results in find response"); - } else { - String resultClassName = response.optString("className", null); - if (resultClassName == null) { - resultClassName = state.className(); - } - for (int i = 0; i < results.length(); ++i) { - JSONObject data = results.getJSONObject(i); - T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys()); - answer.add(object); - - /* - * If there was a $relatedTo constraint on the query, then add any results to the list of - * known objects in the relation for offline caching - */ - ParseQuery.RelationConstraint relation = - (ParseQuery.RelationConstraint) state.constraints().get("$relatedTo"); - if (relation != null) { - relation.getRelation().addKnownObject(object); - } - } - } - - return answer; - } -} diff --git a/parse/src/main/java/com/parse/NetworkQueryController.kt b/parse/src/main/java/com/parse/NetworkQueryController.kt new file mode 100644 index 000000000..255e73c40 --- /dev/null +++ b/parse/src/main/java/com/parse/NetworkQueryController.kt @@ -0,0 +1,141 @@ +/* + * 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 com.parse.PLog.d +import com.parse.ParseDecoder.Companion.get +import com.parse.ParseQuery.CachePolicy +import com.parse.ParseQuery.RelationConstraint +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +internal class NetworkQueryController(private val restClient: ParseHttpClient) : + AbstractQueryController() { + override fun findAsync( + state: ParseQuery.State, user: ParseUser?, cancellationToken: Task? + ): Task> { + val sessionToken = user?.sessionToken + return findAsync(state, sessionToken, cancellationToken) + } + + override fun countAsync( + state: ParseQuery.State, + user: ParseUser?, + cancellationToken: Task? + ): Task { + val sessionToken = user?.sessionToken + return countAsync(state, sessionToken, cancellationToken) + } + + /** + * Retrieves a list of [ParseObject]s that satisfy this query from the source. + * + * @return A list of all [ParseObject]s obeying the conditions set in this query. + */ + /* package */ + fun findAsync( + state: ParseQuery.State, + sessionToken: String?, + ct: Task? + ): Task> { + val queryStart = System.nanoTime() + val command: ParseRESTCommand = ParseRESTQueryCommand.findCommand(state, sessionToken) + val querySent = System.nanoTime() + return command.executeAsync(restClient, ct) + .onSuccess(Continuation { task: Task -> + val json = task.result + // Cache the results, unless we are ignoring the cache + val policy = state.cachePolicy() + if (policy != null && policy != CachePolicy.IGNORE_CACHE) { + ParseKeyValueCache.saveToKeyValueCache(command.cacheKey, json.toString()) + } + val queryReceived = System.nanoTime() + val response: List = convertFindResponse(state, task.result) + val objectsParsed = System.nanoTime() + if (json.has("trace")) { + val serverTrace = json["trace"] + d( + "ParseQuery", String.format( + """ + Query pre-processing took %f seconds + %s + Client side parsing took %f seconds + + """.trimIndent(), + (querySent - queryStart) / (1000.0f * 1000.0f), + serverTrace, + (objectsParsed - queryReceived) / (1000.0f * 1000.0f) + ) + ) + } + response + }, Task.BACKGROUND_EXECUTOR) + } + + /* package */ + fun countAsync( + state: ParseQuery.State, + sessionToken: String?, + ct: Task? + ): Task { + val command: ParseRESTCommand = ParseRESTQueryCommand.countCommand(state, sessionToken) + return command.executeAsync(restClient, ct) + .onSuccessTask(Continuation { task: Task -> + // Cache the results, unless we are ignoring the cache + val policy = state.cachePolicy() + if (policy != null && policy != CachePolicy.IGNORE_CACHE) { + val result = task.result + ParseKeyValueCache.saveToKeyValueCache(command.cacheKey, result.toString()) + } + task + }, Task.BACKGROUND_EXECUTOR) + .onSuccess { task: Task -> task.result.optInt("count") } + } + + // Converts the JSONArray that represents the results of a find command to an + // ArrayList. + /* package */ + @Throws(JSONException::class) + fun convertFindResponse( + state: ParseQuery.State, + response: JSONObject + ): List { + val answer = ArrayList() + val results = response.optJSONArray("results") + if (results == null) { + d(TAG, "null results in find response") + } else { + var resultClassName = response.optString("className", null) + if (resultClassName == null) { + resultClassName = state.className() + } + for (i in 0 until results.length()) { + val data = results.getJSONObject(i) + val `object`: T = + ParseObject.fromJSON(data, resultClassName, get(), state.selectedKeys()) + answer.add(`object`) + + /* + * If there was a $relatedTo constraint on the query, then add any results to the list of + * known objects in the relation for offline caching + */ + val relation = state.constraints()["\$relatedTo"] as RelationConstraint? + relation?.relation?.addKnownObject(`object`) + } + } + return answer + } + + companion object { + private const val TAG = "NetworkQueryController" + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/NetworkSessionController.java b/parse/src/main/java/com/parse/NetworkSessionController.java deleted file mode 100644 index 6a7607c36..000000000 --- a/parse/src/main/java/com/parse/NetworkSessionController.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.JSONObject; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class NetworkSessionController implements ParseSessionController { - - private final ParseHttpClient client; - private final ParseObjectCoder coder; - - public NetworkSessionController(ParseHttpClient client) { - this.client = client; - this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject - } - - @Override - public Task getSessionAsync(String sessionToken) { - ParseRESTSessionCommand command = - ParseRESTSessionCommand.getCurrentSessionCommand(sessionToken); - - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseObject.State then(Task task) { - JSONObject result = task.getResult(); - return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get()) - .isComplete(true) - .build(); - } - }); - } - - @Override - public Task revokeAsync(String sessionToken) { - return ParseRESTSessionCommand.revoke(sessionToken) - .executeAsync(client) - .makeVoid(); - } - - @Override - public Task upgradeToRevocable(String sessionToken) { - ParseRESTSessionCommand command = - ParseRESTSessionCommand.upgradeToRevocableSessionCommand(sessionToken); - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseObject.State then(Task task) { - JSONObject result = task.getResult(); - return coder.decode(new ParseObject.State.Builder("_Session"), result, ParseDecoder.get()) - .isComplete(true) - .build(); - } - }); - } -} diff --git a/parse/src/main/java/com/parse/NetworkSessionController.kt b/parse/src/main/java/com/parse/NetworkSessionController.kt new file mode 100644 index 000000000..5a44acb5a --- /dev/null +++ b/parse/src/main/java/com/parse/NetworkSessionController.kt @@ -0,0 +1,48 @@ +/* + * 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 com.parse.ParseObjectCoder.Companion.get +import com.parse.boltsinternal.Task +import org.json.JSONObject + +internal class NetworkSessionController(private val client: ParseHttpClient) : + ParseSessionController { + private val coder: ParseObjectCoder = get() + + override fun getSessionAsync(sessionToken: String): Task { + val command = ParseRESTSessionCommand.getCurrentSessionCommand(sessionToken) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseObject.State.Builder("_Session"), result!!, ParseDecoder.get()) + .isComplete(true) + .build() + } + } + + override fun revokeAsync(sessionToken: String): Task { + return ParseRESTSessionCommand.revoke(sessionToken) + .executeAsync(client) + .makeVoid() + } + + override fun upgradeToRevocable(sessionToken: String): Task { + val command = ParseRESTSessionCommand.upgradeToRevocableSessionCommand(sessionToken) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseObject.State.Builder("_Session"), result!!, ParseDecoder.get()) + .isComplete(true) + .build() + } + } + + init { + // TODO(grantland): Inject + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/NetworkUserController.java b/parse/src/main/java/com/parse/NetworkUserController.java deleted file mode 100644 index 317fe3eaf..000000000 --- a/parse/src/main/java/com/parse/NetworkUserController.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.JSONObject; - -import java.util.Map; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class NetworkUserController implements ParseUserController { - - private static final int STATUS_CODE_CREATED = 201; - - private final ParseHttpClient client; - private final ParseObjectCoder coder; - private final boolean revocableSession; - - public NetworkUserController(ParseHttpClient client) { - this(client, false); - } - - public NetworkUserController(ParseHttpClient client, boolean revocableSession) { - this.client = client; - this.coder = ParseObjectCoder.get(); // TODO(grantland): Inject - this.revocableSession = revocableSession; - } - - @Override - public Task signUpAsync( - final ParseObject.State state, - ParseOperationSet operations, - String sessionToken) { - JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); - ParseRESTCommand command = ParseRESTUserCommand.signUpUserCommand( - objectJSON, sessionToken, revocableSession); - - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - JSONObject result = task.getResult(); - return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) - .isComplete(false) - .isNew(true) - .build(); - } - }); - } - - //region logInAsync - - @Override - public Task logInAsync( - String username, String password) { - ParseRESTCommand command = ParseRESTUserCommand.logInUserCommand( - username, password, revocableSession); - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - JSONObject result = task.getResult(); - - return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) - .isComplete(true) - .build(); - } - }); - } - - @Override - public Task logInAsync( - ParseUser.State state, ParseOperationSet operations) { - JSONObject objectJSON = coder.encode(state, operations, PointerEncoder.get()); - final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand( - objectJSON, state.sessionToken(), revocableSession); - - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - JSONObject result = task.getResult(); - - // TODO(grantland): Does the server really respond back with complete object data if the - // object isn't new? - boolean isNew = command.getStatusCode() == STATUS_CODE_CREATED; - boolean isComplete = !isNew; - - return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) - .isComplete(isComplete) - .isNew(isNew) - .build(); - } - }); - } - - @Override - public Task logInAsync( - final String authType, final Map authData) { - final ParseRESTUserCommand command = ParseRESTUserCommand.serviceLogInUserCommand( - authType, authData, revocableSession); - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - JSONObject result = task.getResult(); - - return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) - .isComplete(true) - .isNew(command.getStatusCode() == STATUS_CODE_CREATED) - .putAuthData(authType, authData) - .build(); - } - }); - } - - //endregion - - @Override - public Task getUserAsync(String sessionToken) { - ParseRESTCommand command = ParseRESTUserCommand.getCurrentUserCommand(sessionToken); - return command.executeAsync(client).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - JSONObject result = task.getResult(); - - return coder.decode(new ParseUser.State.Builder(), result, ParseDecoder.get()) - .isComplete(true) - .build(); - } - }); - } - - @Override - public Task requestPasswordResetAsync(String email) { - ParseRESTCommand command = ParseRESTUserCommand.resetPasswordResetCommand(email); - return command.executeAsync(client).makeVoid(); - } -} diff --git a/parse/src/main/java/com/parse/NetworkUserController.kt b/parse/src/main/java/com/parse/NetworkUserController.kt new file mode 100644 index 000000000..9c509f631 --- /dev/null +++ b/parse/src/main/java/com/parse/NetworkUserController.kt @@ -0,0 +1,110 @@ +/* + * 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 com.parse.ParseRESTUserCommand +import com.parse.boltsinternal.Task +import org.json.JSONObject + +internal class NetworkUserController @JvmOverloads constructor( + private val client: ParseHttpClient, + private val revocableSession: Boolean = false +) : ParseUserController { + private val coder: ParseObjectCoder = ParseObjectCoder.get() + + override fun signUpAsync( + state: ParseObject.State, + operations: ParseOperationSet, + sessionToken: String + ): Task { + val objectJSON = coder.encode(state, operations, PointerEncoder.get()) + val command: ParseRESTCommand = ParseRESTUserCommand.signUpUserCommand( + objectJSON, sessionToken, revocableSession + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseUser.State.Builder(), result!!, ParseDecoder.get()) + .isComplete(false) + .isNew(true) + .build() + } + } + + //region logInAsync + override fun logInAsync( + username: String, password: String + ): Task { + val command: ParseRESTCommand = ParseRESTUserCommand.logInUserCommand( + username, password, revocableSession + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseUser.State.Builder(), result!!, ParseDecoder.get()) + .isComplete(true) + .build() + } + } + + override fun logInAsync( + state: ParseUser.State, operations: ParseOperationSet + ): Task { + val objectJSON = coder.encode(state, operations, PointerEncoder.get()) + val command = ParseRESTUserCommand.serviceLogInUserCommand( + objectJSON, state.sessionToken(), revocableSession + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + + // TODO(grantland): Does the server really respond back with complete object data if the + // object isn't new? + val isNew = command.statusCode == STATUS_CODE_CREATED + val isComplete = !isNew + coder.decode(ParseUser.State.Builder(), result!!, ParseDecoder.get()) + .isComplete(isComplete) + .isNew(isNew) + .build() + } + } + + override fun logInAsync( + authType: String, authData: Map + ): Task { + val command = ParseRESTUserCommand.serviceLogInUserCommand( + authType, authData, revocableSession + ) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseUser.State.Builder(), result!!, ParseDecoder.get()) + .isComplete(true) + .isNew(command.statusCode == STATUS_CODE_CREATED) + .putAuthData(authType, authData) + .build() + } + } + + //endregion + override fun getUserAsync(sessionToken: String): Task { + val command: ParseRESTCommand = ParseRESTUserCommand.getCurrentUserCommand(sessionToken) + return command.executeAsync(client).onSuccess { task: Task -> + val result = task.result + coder.decode(ParseUser.State.Builder(), result!!, ParseDecoder.get()) + .isComplete(true) + .build() + } + } + + override fun requestPasswordResetAsync(email: String): Task { + val command: ParseRESTCommand = ParseRESTUserCommand.resetPasswordResetCommand(email) + return command.executeAsync(client).makeVoid() + } + + companion object { + private const val STATUS_CODE_CREATED = 201 + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/NoObjectsEncoder.java b/parse/src/main/java/com/parse/NoObjectsEncoder.java deleted file mode 100644 index e5c4005ac..000000000 --- a/parse/src/main/java/com/parse/NoObjectsEncoder.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.JSONObject; - -/** - * Throws an exception if someone attempts to encode a {@code ParseObject}. - */ -class NoObjectsEncoder extends ParseEncoder { - - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final NoObjectsEncoder INSTANCE = new NoObjectsEncoder(); - - public static NoObjectsEncoder get() { - return INSTANCE; - } - - @Override - public JSONObject encodeRelatedObject(ParseObject object) { - throw new IllegalArgumentException("ParseObjects not allowed here"); - } -} diff --git a/parse/src/main/java/com/parse/NoObjectsEncoder.kt b/parse/src/main/java/com/parse/NoObjectsEncoder.kt new file mode 100644 index 000000000..690beaf9c --- /dev/null +++ b/parse/src/main/java/com/parse/NoObjectsEncoder.kt @@ -0,0 +1,30 @@ +/* + * 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.JSONObject + +/** + * Throws an exception if someone attempts to encode a `ParseObject`. + */ +internal class NoObjectsEncoder : ParseEncoder() { + override fun encodeRelatedObject(`object`: ParseObject): JSONObject { + throw IllegalArgumentException("ParseObjects not allowed here") + } + + companion object { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = NoObjectsEncoder() + @JvmStatic + fun get(): NoObjectsEncoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/Numbers.java b/parse/src/main/java/com/parse/Numbers.java deleted file mode 100644 index 27b212dd9..000000000 --- a/parse/src/main/java/com/parse/Numbers.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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; - -/** - * Static utility methods pertaining to {@link Number} instances. - */ -class Numbers { - - /** - * Add two {@link Number} instances. - */ - /* package */ - static Number add(Number first, Number second) { - if (first instanceof Double || second instanceof Double) { - return first.doubleValue() + second.doubleValue(); - } else if (first instanceof Float || second instanceof Float) { - return first.floatValue() + second.floatValue(); - } else if (first instanceof Long || second instanceof Long) { - return first.longValue() + second.longValue(); - } else if (first instanceof Integer || second instanceof Integer) { - return first.intValue() + second.intValue(); - } else if (first instanceof Short || second instanceof Short) { - return first.shortValue() + second.shortValue(); - } else if (first instanceof Byte || second instanceof Byte) { - return first.byteValue() + second.byteValue(); - } else { - throw new RuntimeException("Unknown number type."); - } - } - - /** - * Subtract two {@link Number} instances. - */ - /* package */ - static Number subtract(Number first, Number second) { - if (first instanceof Double || second instanceof Double) { - return first.doubleValue() - second.doubleValue(); - } else if (first instanceof Float || second instanceof Float) { - return first.floatValue() - second.floatValue(); - } else if (first instanceof Long || second instanceof Long) { - return first.longValue() - second.longValue(); - } else if (first instanceof Integer || second instanceof Integer) { - return first.intValue() - second.intValue(); - } else if (first instanceof Short || second instanceof Short) { - return first.shortValue() - second.shortValue(); - } else if (first instanceof Byte || second instanceof Byte) { - return first.byteValue() - second.byteValue(); - } else { - throw new RuntimeException("Unknown number type."); - } - } - - /** - * Compare two {@link Number} instances. - */ - /* package */ - static int compare(Number first, Number second) { - if (first instanceof Double || second instanceof Double) { - return (int) Math.signum(first.doubleValue() - second.doubleValue()); - } else if (first instanceof Float || second instanceof Float) { - return (int) Math.signum(first.floatValue() - second.floatValue()); - } else if (first instanceof Long || second instanceof Long) { - long diff = first.longValue() - second.longValue(); - return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0); - } else if (first instanceof Integer || second instanceof Integer) { - return first.intValue() - second.intValue(); - } else if (first instanceof Short || second instanceof Short) { - return first.shortValue() - second.shortValue(); - } else if (first instanceof Byte || second instanceof Byte) { - return first.byteValue() - second.byteValue(); - } else { - throw new RuntimeException("Unknown number type."); - } - } -} diff --git a/parse/src/main/java/com/parse/Numbers.kt b/parse/src/main/java/com/parse/Numbers.kt new file mode 100644 index 000000000..a38121f80 --- /dev/null +++ b/parse/src/main/java/com/parse/Numbers.kt @@ -0,0 +1,85 @@ +/* + * 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 kotlin.math.sign + +/** + * Static utility methods pertaining to [Number] instances. + */ +internal object Numbers { + /** + * Add two [Number] instances. + */ + /* package */ + @JvmStatic + fun add(first: Number, second: Number): Number { + return if (first is Double || second is Double) { + first.toDouble() + second.toDouble() + } else if (first is Float || second is Float) { + first.toFloat() + second.toFloat() + } else if (first is Long || second is Long) { + first.toLong() + second.toLong() + } else if (first is Int || second is Int) { + first.toInt() + second.toInt() + } else if (first is Short || second is Short) { + first.toShort() + second.toShort() + } else if (first is Byte || second is Byte) { + first.toByte() + second.toByte() + } else { + throw RuntimeException("Unknown number type.") + } + } + + /** + * Subtract two [Number] instances. + */ + /* package */ + fun subtract(first: Number, second: Number): Number { + return if (first is Double || second is Double) { + first.toDouble() - second.toDouble() + } else if (first is Float || second is Float) { + first.toFloat() - second.toFloat() + } else if (first is Long || second is Long) { + first.toLong() - second.toLong() + } else if (first is Int || second is Int) { + first.toInt() - second.toInt() + } else if (first is Short || second is Short) { + first.toShort() - second.toShort() + } else if (first is Byte || second is Byte) { + first.toByte() - second.toByte() + } else { + throw RuntimeException("Unknown number type.") + } + } + + /** + * Compare two [Number] instances. + */ + /* package */ + @JvmStatic + fun compare(first: Number, second: Number): Int { + return if (first is Double || second is Double) { + sign(first.toDouble() - second.toDouble()).toInt() + } else if (first is Float || second is Float) { + sign(first.toFloat() - second.toFloat()).toInt() + } else if (first is Long || second is Long) { + val diff = first.toLong() - second.toLong() + if (diff < 0) -1 else if (diff > 0) 1 else 0 + } else if (first is Int || second is Int) { + first.toInt() - second.toInt() + } else if (first is Short || second is Short) { + first.toShort() - second.toShort() + } else if (first is Byte || second is Byte) { + first.toByte() - second.toByte() + } else { + throw RuntimeException("Unknown number type.") + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/OfflineObjectStore.java b/parse/src/main/java/com/parse/OfflineObjectStore.java deleted file mode 100644 index afbd610e9..000000000 --- a/parse/src/main/java/com/parse/OfflineObjectStore.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 java.util.Arrays; -import java.util.List; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class OfflineObjectStore implements ParseObjectStore { - - private final String className; - private final String pinName; - private final ParseObjectStore legacy; - public OfflineObjectStore(Class clazz, String pinName, ParseObjectStore legacy) { - this(getSubclassingController().getClassName(clazz), pinName, legacy); - } - public OfflineObjectStore(String className, String pinName, ParseObjectStore legacy) { - this.className = className; - this.pinName = pinName; - this.legacy = legacy; - } - - private static ParseObjectSubclassingController getSubclassingController() { - return ParseCorePlugins.getInstance().getSubclassingController(); - } - - private static Task migrate( - final ParseObjectStore from, final ParseObjectStore to) { - return from.getAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final T object = task.getResult(); - if (object == null) { - return task; - } - - return Task.whenAll(Arrays.asList( - from.deleteAsync(), - to.setAsync(object) - )).continueWith(new Continuation() { - @Override - public T then(Task task) { - return object; - } - }); - } - }); - } - - @Override - public Task setAsync(final T object) { - return ParseObject.unpinAllInBackground(pinName).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return object.pinInBackground(pinName, false); - } - }); - } - - @Override - public Task getAsync() { - // We need to set `ignoreACLs` since we can't use ACLs without the current user. - ParseQuery query = ParseQuery.getQuery(className) - .fromPin(pinName) - .ignoreACLs(); - return query.findInBackground().onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - List results = task.getResult(); - if (results != null) { - if (results.size() == 1) { - return Task.forResult(results.get(0)); - } else { - return ParseObject.unpinAllInBackground(pinName).cast(); - } - } - return Task.forResult(null); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - T ldsObject = task.getResult(); - if (ldsObject != null) { - return task; - } - - return migrate(legacy, OfflineObjectStore.this).cast(); - } - }); - } - - @Override - public Task existsAsync() { - // We need to set `ignoreACLs` since we can't use ACLs without the current user. - ParseQuery query = ParseQuery.getQuery(className) - .fromPin(pinName) - .ignoreACLs(); - return query.countInBackground().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - boolean exists = task.getResult() == 1; - if (exists) { - return Task.forResult(true); - } - return legacy.existsAsync(); - } - }); - } - - @Override - public Task deleteAsync() { - final Task ldsTask = ParseObject.unpinAllInBackground(pinName); - return Task.whenAll(Arrays.asList( - legacy.deleteAsync(), - ldsTask - )).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We only really care about the result of unpinning. - return ldsTask; - } - }); - } -} diff --git a/parse/src/main/java/com/parse/OfflineObjectStore.kt b/parse/src/main/java/com/parse/OfflineObjectStore.kt new file mode 100644 index 000000000..15b0bb794 --- /dev/null +++ b/parse/src/main/java/com/parse/OfflineObjectStore.kt @@ -0,0 +1,104 @@ +/* + * 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 com.parse.ParseQuery +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import java.util.* + +internal class OfflineObjectStore( + private val className: String, + private val pinName: String?, + private val legacy: ParseObjectStore +) : ParseObjectStore { + constructor(clazz: Class?, pinName: String?, legacy: ParseObjectStore) : this( + subclassingController.getClassName(clazz), pinName, legacy + ) { + } + + override fun setAsync(`object`: T): Task { + return ParseObject.unpinAllInBackground(pinName).continueWithTask { + `object`.pinInBackground( + pinName!!, false + ) + } + } + + // We need to set `ignoreACLs` since we can't use ACLs without the current user. + override val getAsync: Task + get() { + // We need to set `ignoreACLs` since we can't use ACLs without the current user. + val query = ParseQuery.getQuery(className) + .fromPin(pinName) + .ignoreACLs() + return query.findInBackground().onSuccessTask { task: Task?> -> + val results = task.result + if (results != null) { + if (results.size == 1) { + return@onSuccessTask Task.forResult(results[0]) + } else { + return@onSuccessTask ParseObject.unpinAllInBackground(pinName).cast() + } + } + Task.forResult(null) + }.onSuccessTask { + val ldsObject = it.result + if (ldsObject != null) { + return@onSuccessTask it.cast() + } + migrate(legacy, this@OfflineObjectStore).cast() + } + } + + override fun existsAsync(): Task { + // We need to set `ignoreACLs` since we can't use ACLs without the current user. + val query = ParseQuery.getQuery(className) + .fromPin(pinName) + .ignoreACLs() + return query.countInBackground().onSuccessTask { task: Task -> + val exists = task.result == 1 + if (exists) { + return@onSuccessTask Task.forResult(true) + } + legacy.existsAsync() + } + } + + override fun deleteAsync(): Task { + val ldsTask = ParseObject.unpinAllInBackground( + pinName + ) + return Task.whenAll( + listOf( + legacy.deleteAsync(), + ldsTask + ) + ).continueWithTask { ldsTask } + } + + companion object { + private val subclassingController: ParseObjectSubclassingController + get() = ParseCorePlugins.getInstance().subclassingController + + private fun migrate( + from: ParseObjectStore, to: ParseObjectStore + ): Task { + return from.getAsync.onSuccessTask { task: Task -> + val `object` = task.result ?: return@onSuccessTask task + Task.whenAll( + listOf( + from.deleteAsync(), + to.setAsync(`object`) + ) + ).continueWith { `object` } + } + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/OfflineQueryController.java b/parse/src/main/java/com/parse/OfflineQueryController.java deleted file mode 100644 index a678f8130..000000000 --- a/parse/src/main/java/com/parse/OfflineQueryController.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 java.util.List; - -import com.parse.boltsinternal.Task; - -class OfflineQueryController extends AbstractQueryController { - - private final OfflineStore offlineStore; - private final ParseQueryController networkController; - - public OfflineQueryController(OfflineStore store, ParseQueryController network) { - offlineStore = store; - networkController = network; - } - - @Override - public Task> findAsync( - ParseQuery.State state, - ParseUser user, - Task cancellationToken) { - if (state.isFromLocalDatastore()) { - return offlineStore.findFromPinAsync(state.pinName(), state, user); - } else { - return networkController.findAsync(state, user, cancellationToken); - } - } - - @Override - public Task countAsync( - ParseQuery.State state, - ParseUser user, - Task cancellationToken) { - if (state.isFromLocalDatastore()) { - return offlineStore.countFromPinAsync(state.pinName(), state, user); - } else { - return networkController.countAsync(state, user, cancellationToken); - } - } -} diff --git a/parse/src/main/java/com/parse/OfflineQueryController.kt b/parse/src/main/java/com/parse/OfflineQueryController.kt new file mode 100644 index 000000000..beb211428 --- /dev/null +++ b/parse/src/main/java/com/parse/OfflineQueryController.kt @@ -0,0 +1,40 @@ +/* + * 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 com.parse.boltsinternal.Task + +internal class OfflineQueryController( + private val offlineStore: OfflineStore, + private val networkController: ParseQueryController +) : AbstractQueryController() { + override fun findAsync( + state: ParseQuery.State, + user: ParseUser?, + cancellationToken: Task? + ): Task> { + return if (state.isFromLocalDatastore) { + offlineStore.findFromPinAsync(state.pinName(), state, user)!! + } else { + networkController.findAsync(state, user, cancellationToken) + } + } + + override fun countAsync( + state: ParseQuery.State, + user: ParseUser?, + cancellationToken: Task? + ): Task { + return if (state.isFromLocalDatastore) { + offlineStore.countFromPinAsync(state.pinName(), state, user)!! + } else { + networkController.countAsync(state, user, cancellationToken) + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/OfflineQueryLogic.java b/parse/src/main/java/com/parse/OfflineQueryLogic.java deleted file mode 100644 index 59e512655..000000000 --- a/parse/src/main/java/com/parse/OfflineQueryLogic.java +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * 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 com.parse.ParseQuery.KeyConstraints; -import com.parse.ParseQuery.QueryConstraints; -import com.parse.ParseQuery.RelationConstraint; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -class OfflineQueryLogic { - private final OfflineStore store; - - /* package */ OfflineQueryLogic(OfflineStore store) { - this.store = store; - } - - /** - * Returns an Object's value for a given key, handling any special keys like objectId. Also - * handles dot-notation for traversing into objects. - */ - private static Object getValue(Object container, String key) throws ParseException { - return getValue(container, key, 0); - } - - private static Object getValue(Object container, String key, int depth) throws ParseException { - if (key.contains(".")) { - String[] parts = key.split("\\.", 2); - Object value = getValue(container, parts[0], depth + 1); - /* - * Only Maps and JSONObjects can be dotted into for getting values, so we should reject - * anything like ParseObjects and arrays. - */ - if (!(value == null || value == JSONObject.NULL || value instanceof Map || value instanceof JSONObject)) { - // Technically, they can search inside the REST representation of some nested objects. - if (depth > 0) { - Object restFormat = null; - try { - restFormat = PointerEncoder.get().encode(value); - } catch (Exception e) { - // Well, if we couldn't encode it, it's not searchable. - } - if (restFormat instanceof JSONObject) { - return getValue(restFormat, parts[1], depth + 1); - } - } - throw new ParseException(ParseException.INVALID_QUERY, String.format("Key %s is invalid.", - key)); - } - return getValue(value, parts[1], depth + 1); - } - - if (container instanceof ParseObject) { - final ParseObject object = (ParseObject) container; - - // The object needs to have been fetched already if we are going to sort by one of its fields. - if (!object.isDataAvailable()) { - throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s", - key)); - } - - // Handle special keys for ParseObjects. - switch (key) { - case "objectId": - return object.getObjectId(); - case "createdAt": - case "_created_at": - return object.getCreatedAt(); - case "updatedAt": - case "_updated_at": - return object.getUpdatedAt(); - default: - return object.get(key); - } - - } else if (container instanceof JSONObject) { - return ((JSONObject) container).opt(key); - - } else if (container instanceof Map) { - return ((Map) container).get(key); - - } else if (container == JSONObject.NULL) { - return null; - - } else if (container == null) { - return null; - - } else { - throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s", key)); - } - } - - /** - * General purpose compareTo that figures out the right types to use. The arguments should be - * atomic values to compare, such as Dates, Strings, or Numbers -- not composite objects or - * arrays. - */ - private static int compareTo(Object lhs, Object rhs) { - boolean lhsIsNullOrUndefined = (lhs == JSONObject.NULL || lhs == null); - boolean rhsIsNullOrUndefined = (rhs == JSONObject.NULL || rhs == null); - - if (lhsIsNullOrUndefined || rhsIsNullOrUndefined) { - if (!lhsIsNullOrUndefined) { - return 1; - } else if (!rhsIsNullOrUndefined) { - return -1; - } else { - return 0; - } - } else if (lhs instanceof Date && rhs instanceof Date) { - return ((Date) lhs).compareTo((Date) rhs); - } else if (lhs instanceof String && rhs instanceof String) { - return ((String) lhs).compareTo((String) rhs); - } else if (lhs instanceof Number && rhs instanceof Number) { - return Numbers.compare((Number) lhs, (Number) rhs); - } else { - throw new IllegalArgumentException( - String.format("Cannot compare %s against %s", lhs, rhs)); - } - } - - /** - * Returns true if decider returns true for any value in the given list. - */ - private static boolean compareList(Object constraint, List values, Decider decider) { - for (Object value : values) { - if (decider.decide(constraint, value)) { - return true; - } - } - return false; - } - - /** - * Returns true if decider returns true for any value in the given list. - */ - private static boolean compareArray(Object constraint, JSONArray values, Decider decider) { - for (int i = 0; i < values.length(); ++i) { - try { - if (decider.decide(constraint, values.get(i))) { - return true; - } - } catch (JSONException e) { - // This can literally never happen. - throw new RuntimeException(e); - } - } - return false; - } - - /** - * Returns true if the decider returns true for the given value and the given constraint. This - * method handles Mongo's logic where an item can match either an item itself, or any item within - * the item, if the item is an array. - */ - private static boolean compare(Object constraint, Object value, Decider decider) { - if (value instanceof List) { - return compareList(constraint, (List) value, decider); - } else if (value instanceof JSONArray) { - return compareArray(constraint, (JSONArray) value, decider); - } else { - return decider.decide(constraint, value); - } - } - - /** - * Implements simple equality constraints. This emulates Mongo's behavior where "equals" can mean - * array containment. - */ - private static boolean matchesEqualConstraint(Object constraint, Object value) { - if (constraint == null || value == null) { - return constraint == value; - } - - if (constraint instanceof Number && value instanceof Number) { - return compareTo(constraint, value) == 0; - } - - if (constraint instanceof ParseGeoPoint && value instanceof ParseGeoPoint) { - ParseGeoPoint lhs = (ParseGeoPoint) constraint; - ParseGeoPoint rhs = (ParseGeoPoint) value; - return lhs.getLatitude() == rhs.getLatitude() - && lhs.getLongitude() == rhs.getLongitude(); - } - - if (constraint instanceof ParsePolygon && value instanceof ParsePolygon) { - ParsePolygon lhs = (ParsePolygon) constraint; - ParsePolygon rhs = (ParsePolygon) value; - return lhs.equals(rhs); - } - - Decider decider; - if (isStartsWithRegex(constraint)) { - decider = new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - return ((String) value).matches(((KeyConstraints) constraint).get("$regex").toString()); - } - }; - } else { - decider = new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - return constraint.equals(value); - } - }; - } - - return compare(constraint, value, decider); - } - - /** - * Matches $ne constraints. - */ - private static boolean matchesNotEqualConstraint(Object constraint, Object value) { - return !matchesEqualConstraint(constraint, value); - } - - /** - * Matches $lt constraints. - */ - private static boolean matchesLessThanConstraint(Object constraint, Object value) { - return compare(constraint, value, new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - return compareTo(constraint, value) > 0; - } - }); - } - - /** - * Matches $lte constraints. - */ - private static boolean matchesLessThanOrEqualToConstraint(Object constraint, Object value) { - return compare(constraint, value, new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - return compareTo(constraint, value) >= 0; - } - }); - } - - /** - * Matches $gt constraints. - */ - private static boolean matchesGreaterThanConstraint(Object constraint, Object value) { - return compare(constraint, value, new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - return compareTo(constraint, value) < 0; - } - }); - } - - /** - * Matches $gte constraints. - */ - private static boolean matchesGreaterThanOrEqualToConstraint(Object constraint, Object value) { - return compare(constraint, value, new Decider() { - @Override - public boolean decide(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - return compareTo(constraint, value) <= 0; - } - }); - } - - /** - * Matches $in constraints. - * $in returns true if the intersection of value and constraint is not an empty set. - */ - private static boolean matchesInConstraint(Object constraint, Object value) { - if (constraint instanceof Collection) { - for (Object requiredItem : (Collection) constraint) { - if (matchesEqualConstraint(requiredItem, value)) { - return true; - } - } - return false; - } - throw new IllegalArgumentException("Constraint type not supported for $in queries."); - } - - /** - * Matches $nin constraints. - */ - private static boolean matchesNotInConstraint(Object constraint, Object value) { - return !matchesInConstraint(constraint, value); - } - - /** - * Matches $all constraints. - */ - private static boolean matchesAllConstraint(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - - if (!(value instanceof Collection)) { - throw new IllegalArgumentException("Value type not supported for $all queries."); - } - - if (constraint instanceof Collection) { - if (isAnyValueRegexStartsWith((Collection) constraint)) { - constraint = cleanRegexStartsWith((Collection) constraint); - if (constraint == null) { - throw new IllegalArgumentException("All values in $all queries must be of starting with regex or non regex."); - } - } - - for (Object requiredItem : (Collection) constraint) { - if (!matchesEqualConstraint(requiredItem, value)) { - return false; - } - } - return true; - } - throw new IllegalArgumentException("Constraint type not supported for $all queries."); - } - - /** - * Check if any of the collection constraints is a regex to match strings that starts with another - * string. - */ - private static boolean isAnyValueRegexStartsWith(Collection constraints) { - for (Object constraint : constraints) { - if (isStartsWithRegex(constraint)) { - return true; - } - } - - return false; - } - - /** - * Cleans all regex constraints. If any of the constraints is not a regex, then null is returned. - * All values in a $all constraint must be a starting with another string regex. - */ - private static Collection cleanRegexStartsWith(Collection constraints) { - ArrayList cleanedValues = new ArrayList<>(); - for (Object constraint : constraints) { - if (!(constraint instanceof KeyConstraints)) { - return null; - } - - KeyConstraints cleanedRegex = cleanRegexStartsWith((KeyConstraints) constraint); - if (cleanedRegex == null) { - return null; - } - - cleanedValues.add(cleanedRegex); - } - - return cleanedValues; - } - - /** - * Creates a regex pattern to match a substring at the beginning of another string. - *

- * If given string is not a regex to match a string at the beginning of another string, then null - * is returned. - */ - private static KeyConstraints cleanRegexStartsWith(KeyConstraints regex) { - if (!isStartsWithRegex(regex)) { - return null; - } - - // remove all instances of \Q and \E from the remaining text & escape single quotes - String literalizedString = ((String) regex.get("$regex")) - .replaceAll("([^\\\\])(\\\\E)", "$1") - .replaceAll("([^\\\\])(\\\\Q)", "$1") - .replaceAll("^\\\\E", "") - .replaceAll("^\\\\Q", "") - .replaceAll("([^'])'", "$1''") - .replaceAll("^'([^'])", "''$1"); - - regex.put("$regex", literalizedString + ".*"); - return regex; - } - - /** - * Check if given constraint is a regex to match strings that starts with another string. - */ - private static boolean isStartsWithRegex(Object constraint) { - if (constraint == null || !(constraint instanceof KeyConstraints)) { - return false; - } - - KeyConstraints keyConstraints = (KeyConstraints) constraint; - return keyConstraints.size() == 1 && keyConstraints.containsKey("$regex") && - ((String) keyConstraints.get("$regex")).startsWith("^"); - } - - /** - * Matches $regex constraints. - */ - private static boolean matchesRegexConstraint(Object constraint, Object value, String options) - throws ParseException { - if (value == null || value == JSONObject.NULL) { - return false; - } - - if (options == null) { - options = ""; - } - - if (!options.matches("^[imxs]*$")) { - throw new ParseException(ParseException.INVALID_QUERY, String.format( - "Invalid regex options: %s", options)); - } - - int flags = 0; - if (options.contains("i")) { - flags = flags | Pattern.CASE_INSENSITIVE; - } - if (options.contains("m")) { - flags = flags | Pattern.MULTILINE; - } - if (options.contains("x")) { - flags = flags | Pattern.COMMENTS; - } - if (options.contains("s")) { - flags = flags | Pattern.DOTALL; - } - - String regex = (String) constraint; - Pattern pattern = Pattern.compile(regex, flags); - Matcher matcher = pattern.matcher((String) value); - return matcher.find(); - } - - /** - * Matches $exists constraints. - */ - private static boolean matchesExistsConstraint(Object constraint, Object value) { - /* - * In the Android SDK, null means "undefined", and JSONObject.NULL means "null". - */ - if (constraint != null && (Boolean) constraint) { - return value != null && value != JSONObject.NULL; - } else { - return value == null || value == JSONObject.NULL; - } - } - - /** - * Matches $nearSphere constraints. - */ - private static boolean matchesNearSphereConstraint(Object constraint, Object value, - Double maxDistance) { - if (value == null || value == JSONObject.NULL) { - return false; - } - if (maxDistance == null) { - return true; - } - ParseGeoPoint point1 = (ParseGeoPoint) constraint; - ParseGeoPoint point2 = (ParseGeoPoint) value; - return point1.distanceInRadiansTo(point2) <= maxDistance; - } - - /** - * Matches $within constraints. - */ - private static boolean matchesWithinConstraint(Object constraint, Object value) - throws ParseException { - if (value == null || value == JSONObject.NULL) { - return false; - } - - @SuppressWarnings("unchecked") - HashMap> constraintMap = - (HashMap>) constraint; - ArrayList box = constraintMap.get("$box"); - ParseGeoPoint southwest = box.get(0); - ParseGeoPoint northeast = box.get(1); - ParseGeoPoint target = (ParseGeoPoint) value; - - if (northeast.getLongitude() < southwest.getLongitude()) { - throw new ParseException(ParseException.INVALID_QUERY, - "whereWithinGeoBox queries cannot cross the International Date Line."); - } - if (northeast.getLatitude() < southwest.getLatitude()) { - throw new ParseException(ParseException.INVALID_QUERY, - "The southwest corner of a geo box must be south of the northeast corner."); - } - if (northeast.getLongitude() - southwest.getLongitude() > 180) { - throw new ParseException(ParseException.INVALID_QUERY, - "Geo box queries larger than 180 degrees in longitude are not supported. " - + "Please check point order."); - } - - return (target.getLatitude() >= southwest.getLatitude() - && target.getLatitude() <= northeast.getLatitude() - && target.getLongitude() >= southwest.getLongitude() - && target.getLongitude() <= northeast.getLongitude()); - } - - /** - * Matches $geoIntersects constraints. - */ - private static boolean matchesGeoIntersectsConstraint(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - - @SuppressWarnings("unchecked") - HashMap constraintMap = - (HashMap) constraint; - ParseGeoPoint point = constraintMap.get("$point"); - ParsePolygon target = (ParsePolygon) value; - return target.containsPoint(point); - } - - /** - * Matches $geoWithin constraints. - */ - private static boolean matchesGeoWithinConstraint(Object constraint, Object value) { - if (value == null || value == JSONObject.NULL) { - return false; - } - - @SuppressWarnings("unchecked") - HashMap> constraintMap = - (HashMap>) constraint; - List points = constraintMap.get("$polygon"); - ParsePolygon polygon = new ParsePolygon(points); - ParseGeoPoint point = (ParseGeoPoint) value; - return polygon.containsPoint(point); - } - - /** - * Returns true iff the given value matches the given operator and constraint. - * - * @throws UnsupportedOperationException if the operator is not one this function can handle. - */ - private static boolean matchesStatelessConstraint(String operator, Object constraint, - Object value, KeyConstraints allKeyConstraints) throws ParseException { - switch (operator) { - case "$ne": - return matchesNotEqualConstraint(constraint, value); - - case "$lt": - return matchesLessThanConstraint(constraint, value); - - case "$lte": - return matchesLessThanOrEqualToConstraint(constraint, value); - - case "$gt": - return matchesGreaterThanConstraint(constraint, value); - - case "$gte": - return matchesGreaterThanOrEqualToConstraint(constraint, value); - - case "$in": - return matchesInConstraint(constraint, value); - - case "$nin": - return matchesNotInConstraint(constraint, value); - - case "$all": - return matchesAllConstraint(constraint, value); - - case "$regex": - String regexOptions = (String) allKeyConstraints.get("$options"); - return matchesRegexConstraint(constraint, value, regexOptions); - - case "$options": - // No need to do anything. This is handled by $regex. - return true; - - case "$exists": - return matchesExistsConstraint(constraint, value); - - case "$nearSphere": - Double maxDistance = (Double) allKeyConstraints.get("$maxDistance"); - return matchesNearSphereConstraint(constraint, value, maxDistance); - - case "$maxDistance": - // No need to do anything. This is handled by $nearSphere. - return true; - - case "$within": - return matchesWithinConstraint(constraint, value); - - case "$geoWithin": - return matchesGeoWithinConstraint(constraint, value); - - case "$geoIntersects": - return matchesGeoIntersectsConstraint(constraint, value); - - default: - throw new UnsupportedOperationException(String.format( - "The offline store does not yet support the %s operator.", operator)); - } - } - - /** - * Returns true iff the object is visible based on its read ACL and the given user objectId. - */ - /* package */ - static boolean hasReadAccess(ParseUser user, T object) { - if (user == object) { - return true; - } - - ParseACL acl = object.getACL(); - if (acl == null) { - return true; - } - if (acl.getPublicReadAccess()) { - return true; - } - return user != null && acl.getReadAccess(user); - // TODO: Implement roles. - } - - /** - * Returns true iff the object is visible based on its read ACL and the given user objectId. - */ - /* package */ - static boolean hasWriteAccess(ParseUser user, T object) { - if (user == object) { - return true; - } - - ParseACL acl = object.getACL(); - if (acl == null) { - return true; - } - if (acl.getPublicWriteAccess()) { - return true; - } - return user != null && acl.getWriteAccess(user); - // TODO: Implement roles. - } - - /** - * Sorts the given array based on the parameters of the given query. - */ - /* package */ - static void sort(List results, ParseQuery.State state) - throws ParseException { - final List keys = state.order(); - // Do some error checking just for maximum compatibility with the server. - for (String key : state.order()) { - if (!key.matches("^-?[A-Za-z][A-Za-z0-9_]*$")) { - if (!"_created_at".equals(key) && !"_updated_at".equals(key)) { - throw new ParseException(ParseException.INVALID_KEY_NAME, String.format( - "Invalid key name: \"%s\".", key)); - } - } - } - - // See if there's a $nearSphere constraint that will override the other sort parameters. - String mutableNearSphereKey = null; - ParseGeoPoint mutableNearSphereValue = null; - for (String queryKey : state.constraints().keySet()) { - Object queryKeyConstraints = state.constraints().get(queryKey); - if (queryKeyConstraints instanceof KeyConstraints) { - KeyConstraints keyConstraints = (KeyConstraints) queryKeyConstraints; - if (keyConstraints.containsKey("$nearSphere")) { - mutableNearSphereKey = queryKey; - mutableNearSphereValue = (ParseGeoPoint) keyConstraints.get("$nearSphere"); - } - } - } - final String nearSphereKey = mutableNearSphereKey; - final ParseGeoPoint nearSphereValue = mutableNearSphereValue; - - // If there's nothing to sort based on, then don't do anything. - if (keys.size() == 0 && mutableNearSphereKey == null) { - return; - } - - /* - * TODO(klimt): Test whether we allow dotting into objects for sorting. - */ - - Collections.sort(results, new Comparator() { - @Override - public int compare(T lhs, T rhs) { - if (nearSphereKey != null) { - ParseGeoPoint lhsPoint; - ParseGeoPoint rhsPoint; - try { - lhsPoint = (ParseGeoPoint) getValue(lhs, nearSphereKey); - rhsPoint = (ParseGeoPoint) getValue(rhs, nearSphereKey); - } catch (ParseException e) { - throw new RuntimeException(e); - } - - // GeoPoints can't be null if there's a $nearSphere. - double lhsDistance = lhsPoint.distanceInRadiansTo(nearSphereValue); - double rhsDistance = rhsPoint.distanceInRadiansTo(nearSphereValue); - if (lhsDistance != rhsDistance) { - return (lhsDistance - rhsDistance > 0) ? 1 : -1; - } - } - - for (String key : keys) { - boolean descending = false; - if (key.startsWith("-")) { - descending = true; - key = key.substring(1); - } - - Object lhsValue; - Object rhsValue; - try { - lhsValue = getValue(lhs, key); - rhsValue = getValue(rhs, key); - } catch (ParseException e) { - throw new RuntimeException(e); - } - - int result; - try { - result = compareTo(lhsValue, rhsValue); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(String.format("Unable to sort by key %s.", key), e); - } - if (result != 0) { - return descending ? -result : result; - } - } - return 0; - } - }); - } - - /** - * Makes sure that the object specified by path, relative to container, is fetched. - */ - private static Task fetchIncludeAsync( - final OfflineStore store, - final Object container, - final String path, - final ParseSQLiteDatabase db) { - // If there's no object to include, that's fine. - if (container == null) { - return Task.forResult(null); - } - - // If the container is a list or array, fetch all the sub-items. - if (container instanceof Collection) { - Collection collection = (Collection) container; - // We do the fetches in series because it makes it easier to fail on the first error. - Task task = Task.forResult(null); - for (final Object item : collection) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return fetchIncludeAsync(store, item, path, db); - } - }); - } - return task; - } else if (container instanceof JSONArray) { - final JSONArray array = (JSONArray) container; - // We do the fetches in series because it makes it easier to fail on the first error. - Task task = Task.forResult(null); - for (int i = 0; i < array.length(); ++i) { - final int index = i; - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - return fetchIncludeAsync(store, array.get(index), path, db); - } - }); - } - return task; - } - - // If we've reached the end of the path, then actually do the fetch. - if (path == null) { - if (JSONObject.NULL.equals(container)) { - // Accept JSONObject.NULL value in included field. We swallow it silently instead of - // throwing an exception. - return Task.forResult(null); - } else if (container instanceof ParseObject) { - ParseObject object = (ParseObject) container; - return store.fetchLocallyAsync(object, db).makeVoid(); - } else { - return Task.forError(new ParseException( - ParseException.INVALID_NESTED_KEY, "include is invalid for non-ParseObjects")); - } - } - - // Descend into the container and try again. - - String[] parts = path.split("\\.", 2); - final String key = parts[0]; - final String rest = (parts.length > 1 ? parts[1] : null); - - // Make sure the container is fetched. - return Task.forResult(null).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (container instanceof ParseObject) { - // Make sure this object is fetched before descending into it. - return fetchIncludeAsync(store, container, null, db).onSuccess(new Continuation() { - @Override - public Object then(Task task) { - return ((ParseObject) container).get(key); - } - }); - } else if (container instanceof Map) { - return Task.forResult(((Map) container).get(key)); - } else if (container instanceof JSONObject) { - return Task.forResult(((JSONObject) container).opt(key)); - } else if (JSONObject.NULL.equals(container)) { - // Accept JSONObject.NULL value in included field. We swallow it silently instead of - // throwing an exception. - return null; - } else { - return Task.forError(new IllegalStateException("include is invalid")); - } - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return fetchIncludeAsync(store, task.getResult(), rest, db); - } - }); - } - - /** - * Makes sure all of the objects included by the given query get fetched. - */ - /* package */ - static Task fetchIncludesAsync( - final OfflineStore store, - final T object, - ParseQuery.State state, - final ParseSQLiteDatabase db) { - Set includes = state.includes(); - // We do the fetches in series because it makes it easier to fail on the first error. - Task task = Task.forResult(null); - for (final String include : includes) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return fetchIncludeAsync(store, object, include, db); - } - }); - } - return task; - } - - /** - * Creates a matcher that handles $inQuery constraints. - */ - private ConstraintMatcher createInQueryMatcher(ParseUser user, - Object constraint, final String key) { - // TODO(grantland): Convert builder to state t6941155 - @SuppressWarnings("unchecked") - ParseQuery.State query = ((ParseQuery.State.Builder) constraint).build(); - return new SubQueryMatcher(user, query) { - @Override - protected boolean matches(T object, List results) throws ParseException { - Object value = getValue(object, key); - return matchesInConstraint(results, value); - } - }; - } - - /** - * Creates a matcher that handles $notInQuery constraints. - */ - private ConstraintMatcher createNotInQueryMatcher(ParseUser user, - Object constraint, final String key) { - final ConstraintMatcher inQueryMatcher = createInQueryMatcher(user, constraint, key); - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - return inQueryMatcher.matchesAsync(object, db).onSuccess(new Continuation() { - @Override - public Boolean then(Task task) { - return !task.getResult(); - } - }); - } - }; - } - - /** - * Creates a matcher that handles $select constraints. - */ - private ConstraintMatcher createSelectMatcher(ParseUser user, - Object constraint, final String key) { - Map constraintMap = (Map) constraint; - // TODO(grantland): Convert builder to state t6941155 - @SuppressWarnings("unchecked") - ParseQuery.State query = ((ParseQuery.State.Builder) constraintMap.get("query")).build(); - final String resultKey = (String) constraintMap.get("key"); - return new SubQueryMatcher(user, query) { - @Override - protected boolean matches(T object, List results) throws ParseException { - Object value = getValue(object, key); - for (T result : results) { - Object resultValue = getValue(result, resultKey); - if (matchesEqualConstraint(value, resultValue)) { - return true; - } - } - return false; - } - }; - } - - /** - * Creates a matcher that handles $dontSelect constraints. - */ - private ConstraintMatcher createDontSelectMatcher(ParseUser user, - Object constraint, final String key) { - final ConstraintMatcher selectMatcher = createSelectMatcher(user, constraint, key); - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - return selectMatcher.matchesAsync(object, db).onSuccess(new Continuation() { - @Override - public Boolean then(Task task) { - return !task.getResult(); - } - }); - } - }; - } - - /* - * Creates a matcher for a particular constraint operator. - */ - private ConstraintMatcher createMatcher(ParseUser user, - final String operator, final Object constraint, final String key, - final KeyConstraints allKeyConstraints) { - switch (operator) { - case "$inQuery": - return createInQueryMatcher(user, constraint, key); - - case "$notInQuery": - return createNotInQueryMatcher(user, constraint, key); - - case "$select": - return createSelectMatcher(user, constraint, key); - - case "$dontSelect": - return createDontSelectMatcher(user, constraint, key); - - default: - /* - * All of the other operators we know about are stateless, so return a simple matcher. - */ - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - try { - Object value = getValue(object, key); - return Task.forResult(matchesStatelessConstraint(operator, constraint, value, - allKeyConstraints)); - } catch (ParseException e) { - return Task.forError(e); - } - } - }; - } - } - - /** - * Handles $or queries. - */ - private ConstraintMatcher createOrMatcher(ParseUser user, - ArrayList queries) { - // Make a list of all the matchers to OR together. - final ArrayList> matchers = new ArrayList<>(); - for (QueryConstraints constraints : queries) { - ConstraintMatcher matcher = createMatcher(user, constraints); - matchers.add(matcher); - } - /* - * Now OR together the constraints for each query. - */ - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(final T object, final ParseSQLiteDatabase db) { - Task task = Task.forResult(false); - for (final ConstraintMatcher matcher : matchers) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.getResult()) { - return task; - } - return matcher.matchesAsync(object, db); - } - }); - } - return task; - } - }; - - } - - /** - * Returns a ConstraintMatcher that return true iff the object matches QueryConstraints. This - * takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we - * want to reuse them whenever possible. - */ - private ConstraintMatcher createMatcher(ParseUser user, - QueryConstraints queryConstraints) { - // Make a list of all the matchers to AND together. - final ArrayList> matchers = new ArrayList<>(); - for (final String key : queryConstraints.keySet()) { - final Object queryConstraintValue = queryConstraints.get(key); - - if (key.equals("$or")) { - /* - * A set of queries to be OR-ed together. - */ - @SuppressWarnings("unchecked") - ConstraintMatcher matcher = - createOrMatcher(user, (ArrayList) queryConstraintValue); - matchers.add(matcher); - - } else if (queryConstraintValue instanceof KeyConstraints) { - /* - * It's a set of constraints that should be AND-ed together. - */ - KeyConstraints keyConstraints = (KeyConstraints) queryConstraintValue; - for (String operator : keyConstraints.keySet()) { - final Object keyConstraintValue = keyConstraints.get(operator); - ConstraintMatcher matcher = - createMatcher(user, operator, keyConstraintValue, key, keyConstraints); - matchers.add(matcher); - } - - } else if (queryConstraintValue instanceof RelationConstraint) { - /* - * It's a $relatedTo constraint. - */ - final RelationConstraint relation = (RelationConstraint) queryConstraintValue; - matchers.add(new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - return Task.forResult(relation.getRelation().hasKnownObject(object)); - } - }); - - } else { - /* - * It's not a set of constraints, so it's just a value to compare against. - */ - matchers.add(new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - Object objectValue; - try { - objectValue = getValue(object, key); - } catch (ParseException e) { - return Task.forError(e); - } - return Task.forResult(matchesEqualConstraint(queryConstraintValue, objectValue)); - } - }); - } - } - - /* - * Now AND together the constraints for each key. - */ - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(final T object, final ParseSQLiteDatabase db) { - Task task = Task.forResult(true); - for (final ConstraintMatcher matcher : matchers) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - if (!task.getResult()) { - return task; - } - return matcher.matchesAsync(object, db); - } - }); - } - return task; - } - }; - } - - /** - * Returns a ConstraintMatcher that return true iff the object matches the given query's - * constraints. This takes in a SQLiteDatabase connection because SQLite is finicky about nesting - * connections, so we want to reuse them whenever possible. - * - * @param state The query. - * @param user The user we are testing ACL access for. - * @param Subclass of ParseObject. - * @return A new instance of ConstraintMatcher. - */ - /* package */ ConstraintMatcher createMatcher( - ParseQuery.State state, final ParseUser user) { - final boolean ignoreACLs = state.ignoreACLs(); - final ConstraintMatcher constraintMatcher = createMatcher(user, state.constraints()); - - return new ConstraintMatcher(user) { - @Override - public Task matchesAsync(T object, ParseSQLiteDatabase db) { - if (!ignoreACLs && !hasReadAccess(user, object)) { - return Task.forResult(false); - } - return constraintMatcher.matchesAsync(object, db); - } - }; - } - - /** - * A decider decides whether the given value matches the given constraint. - */ - private interface Decider { - boolean decide(Object constraint, Object value); - } - - /** - * A query is converted into a complex hierarchy of ConstraintMatchers that evaluate whether a - * ParseObject matches each part of the query. This is done because some parts of the query (such - * as $inQuery) are much more efficient if we can do some preprocessing. This makes some parts of - * the query matching stateful. - */ - /* package */ abstract class ConstraintMatcher { - - /* package */ final ParseUser user; - - public ConstraintMatcher(ParseUser user) { - this.user = user; - } - - /* package */ - abstract Task matchesAsync(T object, ParseSQLiteDatabase db); - } - - private abstract class SubQueryMatcher extends ConstraintMatcher { - private final ParseQuery.State subQuery; - private Task> subQueryResults = null; - - public SubQueryMatcher(ParseUser user, ParseQuery.State subQuery) { - super(user); - this.subQuery = subQuery; - } - - @Override - public Task matchesAsync(final T object, ParseSQLiteDatabase db) { - /* - * As an optimization, we do this lazily. Then we may not have to do it at all, if this part - * of the query gets short-circuited. - */ - if (subQueryResults == null) { - //TODO (grantland): We need to pass through the original pin we were limiting the parent - // query on. - subQueryResults = store.findAsync(subQuery, user, null, db); - } - return subQueryResults.onSuccess(new Continuation, Boolean>() { - @Override - public Boolean then(Task> task) throws ParseException { - return matches(object, task.getResult()); - } - }); - } - - protected abstract boolean matches(T object, List results) throws ParseException; - } -} diff --git a/parse/src/main/java/com/parse/OfflineQueryLogic.kt b/parse/src/main/java/com/parse/OfflineQueryLogic.kt new file mode 100644 index 000000000..c4381e161 --- /dev/null +++ b/parse/src/main/java/com/parse/OfflineQueryLogic.kt @@ -0,0 +1,1128 @@ +/* + * 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 com.parse.Numbers.compare +import com.parse.ParseQuery.* +import com.parse.PointerEncoder.Companion.get +import com.parse.boltsinternal.Task +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.* +import java.util.regex.Pattern + +internal class OfflineQueryLogic /* package */(private val store: OfflineStore) { + /** + * Creates a matcher that handles $inQuery constraints. + */ + private fun createInQueryMatcher( + user: ParseUser, + constraint: Any?, key: String + ): ConstraintMatcher { + // TODO(grantland): Convert builder to state t6941155 + val query = (constraint as State.Builder?)!!.build() + return object : SubQueryMatcher(user, query) { + @Throws(ParseException::class) + override fun matches(`object`: T, results: List?): Boolean { + val value = getValue(`object`, key) + return matchesInConstraint(results, value) + } + } + } + + /** + * Creates a matcher that handles $notInQuery constraints. + */ + private fun createNotInQueryMatcher( + user: ParseUser, + constraint: Any?, key: String + ): ConstraintMatcher { + val inQueryMatcher: ConstraintMatcher = createInQueryMatcher(user, constraint, key) + return object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + return inQueryMatcher.matchesAsync(`object`, db) + .onSuccess { task: Task -> !task.result } + } + } + } + + /** + * Creates a matcher that handles $select constraints. + */ + private fun createSelectMatcher( + user: ParseUser, + constraint: Any?, key: String + ): ConstraintMatcher { + val constraintMap = constraint as Map<*, *> + // TODO(grantland): Convert builder to state t6941155 + val query = (constraintMap["query"] as State.Builder?)!!.build() + val resultKey = constraintMap["key"] as String? + return object : SubQueryMatcher(user, query) { + @Throws(ParseException::class) + override fun matches(`object`: T, results: List?): Boolean { + val value = getValue(`object`, key) + for (result in results!!) { + val resultValue = getValue(result, resultKey) + if (matchesEqualConstraint(value, resultValue)) { + return true + } + } + return false + } + } + } + + /** + * Creates a matcher that handles $dontSelect constraints. + */ + private fun createDontSelectMatcher( + user: ParseUser, + constraint: Any?, key: String + ): ConstraintMatcher { + val selectMatcher: ConstraintMatcher = createSelectMatcher(user, constraint, key) + return object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + return selectMatcher.matchesAsync(`object`, db) + .onSuccess { task: Task -> !task.result } + } + } + } + + /* + * Creates a matcher for a particular constraint operator. + */ + private fun createMatcher( + user: ParseUser, + operator: String, constraint: Any?, key: String, + allKeyConstraints: KeyConstraints + ): ConstraintMatcher { + return when (operator) { + "\$inQuery" -> createInQueryMatcher(user, constraint, key) + "\$notInQuery" -> createNotInQueryMatcher(user, constraint, key) + "\$select" -> createSelectMatcher(user, constraint, key) + "\$dontSelect" -> createDontSelectMatcher(user, constraint, key) + else -> /* + * All of the other operators we know about are stateless, so return a simple matcher. + */object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + return try { + val value = getValue(`object`, key) + Task.forResult( + matchesStatelessConstraint( + operator, constraint, value, + allKeyConstraints + ) + ) + } catch (e: ParseException) { + Task.forError(e) + } + } + } + } + } + + /** + * Handles $or queries. + */ + private fun createOrMatcher( + user: ParseUser, + queries: ArrayList? + ): ConstraintMatcher { + // Make a list of all the matchers to OR together. + val matchers = ArrayList>() + for (constraints in queries!!) { + val matcher: ConstraintMatcher = createMatcher(user, constraints) + matchers.add(matcher) + } + /* + * Now OR together the constraints for each query. + */return object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + var task = Task.forResult(false) + for (matcher in matchers) { + task = task.onSuccessTask { task1: Task -> + if (task1.result) { + return@onSuccessTask task1 + } + matcher.matchesAsync(`object`, db) + } + } + return task + } + } + } + + /** + * Returns a ConstraintMatcher that return true iff the object matches QueryConstraints. This + * takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we + * want to reuse them whenever possible. + */ + private fun createMatcher( + user: ParseUser, + queryConstraints: QueryConstraints + ): ConstraintMatcher { + // Make a list of all the matchers to AND together. + val matchers = ArrayList>() + for (key in queryConstraints.keys) { + val queryConstraintValue = queryConstraints[key] + if (key == "\$or") { + /* + * A set of queries to be OR-ed together. + */ + val matcher: ConstraintMatcher = + createOrMatcher(user, queryConstraintValue as ArrayList?) + matchers.add(matcher) + } else if (queryConstraintValue is KeyConstraints) { + /* + * It's a set of constraints that should be AND-ed together. + */ + for (operator in queryConstraintValue.keys) { + val keyConstraintValue = queryConstraintValue[operator] + val matcher: ConstraintMatcher = + createMatcher(user, operator, keyConstraintValue, key, queryConstraintValue) + matchers.add(matcher) + } + } else if (queryConstraintValue is RelationConstraint) { + /* + * It's a $relatedTo constraint. + */ + matchers.add(object : ConstraintMatcher(user) { + override fun matchesAsync( + `object`: T, + db: ParseSQLiteDatabase? + ): Task { + return Task.forResult(queryConstraintValue.relation.hasKnownObject(`object`)) + } + }) + } else { + /* + * It's not a set of constraints, so it's just a value to compare against. + */ + matchers.add(object : ConstraintMatcher(user) { + override fun matchesAsync( + `object`: T, + db: ParseSQLiteDatabase? + ): Task { + val objectValue: Any? = try { + getValue(`object`, key) + } catch (e: ParseException) { + return Task.forError(e) + } + return Task.forResult( + matchesEqualConstraint( + queryConstraintValue, + objectValue + ) + ) + } + }) + } + } + + /* + * Now AND together the constraints for each key. + */return object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + var task = Task.forResult(true) + for (matcher in matchers) { + task = task.onSuccessTask { task1: Task -> + if (!task1.result) { + return@onSuccessTask task1 + } + matcher.matchesAsync(`object`, db) + } + } + return task + } + } + } + + /** + * Returns a ConstraintMatcher that return true iff the object matches the given query's + * constraints. This takes in a SQLiteDatabase connection because SQLite is finicky about nesting + * connections, so we want to reuse them whenever possible. + * + * @param state The query. + * @param user The user we are testing ACL access for. + * @param Subclass of ParseObject. + * @return A new instance of ConstraintMatcher. + */ + /* package */ + fun createMatcher( + state: State, user: ParseUser + ): ConstraintMatcher { + val ignoreACLs = state.ignoreACLs() + val constraintMatcher: ConstraintMatcher = createMatcher(user, state.constraints()) + return object : ConstraintMatcher(user) { + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + return if (!ignoreACLs && !hasReadAccess(user, `object`)) { + Task.forResult(false) + } else constraintMatcher.matchesAsync(`object`, db) + } + } + } + + /** + * A decider decides whether the given value matches the given constraint. + */ + private interface Decider { + fun decide(constraint: Any?, value: Any?): Boolean + } + + /** + * A query is converted into a complex hierarchy of ConstraintMatchers that evaluate whether a + * ParseObject matches each part of the query. This is done because some parts of the query (such + * as $inQuery) are much more efficient if we can do some preprocessing. This makes some parts of + * the query matching stateful. + */ + /* package */ + internal abstract class ConstraintMatcher( /* package */ + val user: ParseUser + ) { + /* package */ + abstract fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task + } + + private abstract inner class SubQueryMatcher( + user: ParseUser, + private val subQuery: ParseQuery.State + ) : ConstraintMatcher(user) { + private var subQueryResults: Task?>? = null + override fun matchesAsync(`object`: T, db: ParseSQLiteDatabase?): Task { + /* + * As an optimization, we do this lazily. Then we may not have to do it at all, if this part + * of the query gets short-circuited. + */ + if (subQueryResults == null) { + //TODO (grantland): We need to pass through the original pin we were limiting the parent + // query on. + subQueryResults = store.findAsync(subQuery, user, null, db!!) + } + return subQueryResults!!.onSuccess { task: Task?> -> + matches( + `object`, + task.result + ) + } + } + + @Throws(ParseException::class) + protected abstract fun matches(`object`: T, results: List?): Boolean + } + + companion object { + /** + * Returns an Object's value for a given key, handling any special keys like objectId. Also + * handles dot-notation for traversing into objects. + */ + @Throws(ParseException::class) + private fun getValue(container: T, key: String?): Any? { + return getValue(container, key, 0) + } + + @Throws(ParseException::class) + private fun getValue(container: Any?, key: String?, depth: Int): Any? { + if (key!!.contains(".")) { + val parts = key.split("\\.").toTypedArray() + val value = getValue(container, parts[0], depth + 1) + /* + * Only Maps and JSONObjects can be dotted into for getting values, so we should reject + * anything like ParseObjects and arrays. + */if (!(value == null || value === JSONObject.NULL || value is Map<*, *> || value is JSONObject)) { + // Technically, they can search inside the REST representation of some nested objects. + if (depth > 0) { + var restFormat: Any? = null + try { + restFormat = get().encode(value) + } catch (e: Exception) { + // Well, if we couldn't encode it, it's not searchable. + } + if (restFormat is JSONObject) { + return getValue(restFormat, parts[1], depth + 1) + } + } + throw ParseException( + ParseException.INVALID_QUERY, String.format( + "Key %s is invalid.", + key + ) + ) + } + return getValue(value, parts[1], depth + 1) + } + return when { + container is ParseObject -> { + + // The object needs to have been fetched already if we are going to sort by one of its fields. + if (!container.isDataAvailable()) { + throw ParseException( + ParseException.INVALID_NESTED_KEY, String.format( + "Bad key: %s", + key + ) + ) + } + when (key) { + "objectId" -> container.objectId + "createdAt", "_created_at" -> container.createdAt + "updatedAt", "_updated_at" -> container.updatedAt + else -> container[key] + } + } + container is JSONObject -> { + container.opt(key) + } + container is Map<*, *> -> { + container[key] + } + container === JSONObject.NULL -> { + null + } + container == null -> { + null + } + else -> { + throw ParseException( + ParseException.INVALID_NESTED_KEY, + String.format("Bad key: %s", key) + ) + } + } + } + + /** + * General purpose compareTo that figures out the right types to use. The arguments should be + * atomic values to compare, such as Dates, Strings, or Numbers -- not composite objects or + * arrays. + */ + private fun compareTo(lhs: Any?, rhs: Any?): Int { + val lhsIsNullOrUndefined = lhs === JSONObject.NULL || lhs == null + val rhsIsNullOrUndefined = rhs === JSONObject.NULL || rhs == null + return if (lhsIsNullOrUndefined || rhsIsNullOrUndefined) { + if (!lhsIsNullOrUndefined) { + 1 + } else if (!rhsIsNullOrUndefined) { + -1 + } else { + 0 + } + } else if (lhs is Date && rhs is Date) { + lhs.compareTo(rhs as Date?) + } else if (lhs is String && rhs is String) { + lhs.compareTo((rhs as String?)!!) + } else if (lhs is Number && rhs is Number) { + compare((lhs as Number?)!!, (rhs as Number?)!!) + } else { + throw IllegalArgumentException( + String.format( + "Cannot compare %s against %s", + lhs, + rhs + ) + ) + } + } + + /** + * Returns true if decider returns true for any value in the given list. + */ + private fun compareList(constraint: Any?, values: List<*>, decider: Decider): Boolean { + for (value in values) { + if (decider.decide(constraint, value)) { + return true + } + } + return false + } + + /** + * Returns true if decider returns true for any value in the given list. + */ + private fun compareArray(constraint: Any?, values: JSONArray, decider: Decider): Boolean { + for (i in 0 until values.length()) { + try { + if (decider.decide(constraint, values[i])) { + return true + } + } catch (e: JSONException) { + // This can literally never happen. + throw RuntimeException(e) + } + } + return false + } + + /** + * Returns true if the decider returns true for the given value and the given constraint. This + * method handles Mongo's logic where an item can match either an item itself, or any item within + * the item, if the item is an array. + */ + private fun compare(constraint: Any?, value: Any?, decider: Decider): Boolean { + return if (value is List<*>) { + compareList( + constraint, + value, + decider + ) + } else if (value is JSONArray) { + compareArray(constraint, value, decider) + } else { + decider.decide(constraint, value) + } + } + + /** + * Implements simple equality constraints. This emulates Mongo's behavior where "equals" can mean + * array containment. + */ + private fun matchesEqualConstraint(constraint: Any?, value: Any?): Boolean { + if (constraint == null || value == null) { + return constraint === value + } + if (constraint is Number && value is Number) { + return compareTo(constraint, value) == 0 + } + if (constraint is ParseGeoPoint && value is ParseGeoPoint) { + return (constraint.latitude == value.latitude + && constraint.longitude == value.longitude) + } + if (constraint is ParsePolygon && value is ParsePolygon) { + return constraint == value + } + val decider: Decider + if (isStartsWithRegex(constraint)) { + decider = object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + return (value as String).matches( + Regex((constraint as KeyConstraints)["\$regex"].toString()) + ) + } + } + } else { + decider = object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + return value?.equals(constraint) ?: false + } + } + } + return compare(constraint, value, decider) + } + + /** + * Matches $ne constraints. + */ + private fun matchesNotEqualConstraint(constraint: Any?, value: Any?): Boolean { + return !matchesEqualConstraint(constraint, value) + } + + /** + * Matches $lt constraints. + */ + private fun matchesLessThanConstraint(constraint: Any?, value: Any?): Boolean { + return compare(constraint, value, object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + return compareTo(constraint, value) > 0 + } + }) + } + + /** + * Matches $lte constraints. + */ + private fun matchesLessThanOrEqualToConstraint(constraint: Any?, value: Any?): Boolean { + return compare(constraint, value, object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + return compareTo(constraint, value) >= 0 + } + }) + } + + /** + * Matches $gt constraints. + */ + private fun matchesGreaterThanConstraint(constraint: Any?, value: Any?): Boolean { + return compare(constraint, value, object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + return compareTo(constraint, value) < 0 + } + }) + } + + /** + * Matches $gte constraints. + */ + private fun matchesGreaterThanOrEqualToConstraint(constraint: Any?, value: Any?): Boolean { + return compare(constraint, value, object : Decider { + override fun decide(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + return compareTo(constraint, value) <= 0 + } + }) + } + + /** + * Matches $in constraints. + * $in returns true if the intersection of value and constraint is not an empty set. + */ + private fun matchesInConstraint(constraint: Any?, value: Any?): Boolean { + if (constraint is Collection<*>) { + for (requiredItem in constraint) { + if (matchesEqualConstraint(requiredItem, value)) { + return true + } + } + return false + } + throw IllegalArgumentException("Constraint type not supported for \$in queries.") + } + + /** + * Matches $nin constraints. + */ + private fun matchesNotInConstraint(constraint: Any?, value: Any?): Boolean { + return !matchesInConstraint(constraint, value) + } + + /** + * Matches $all constraints. + */ + private fun matchesAllConstraint(constraint: Any?, value: Any?): Boolean { + var constraint = constraint + if (value == null || value === JSONObject.NULL) { + return false + } + require(value is Collection<*>) { "Value type not supported for \$all queries." } + if (constraint is Collection<*>) { + if (isAnyValueRegexStartsWith(constraint)) { + constraint = cleanRegexStartsWith(constraint) + requireNotNull(constraint) { "All values in \$all queries must be of starting with regex or non regex." } + } + for (requiredItem in constraint as Collection<*>) { + if (!matchesEqualConstraint(requiredItem, value)) { + return false + } + } + return true + } + throw IllegalArgumentException("Constraint type not supported for \$all queries.") + } + + /** + * Check if any of the collection constraints is a regex to match strings that starts with another + * string. + */ + private fun isAnyValueRegexStartsWith(constraints: Collection<*>): Boolean { + for (constraint in constraints) { + if (isStartsWithRegex(constraint)) { + return true + } + } + return false + } + + /** + * Cleans all regex constraints. If any of the constraints is not a regex, then null is returned. + * All values in a $all constraint must be a starting with another string regex. + */ + private fun cleanRegexStartsWith(constraints: Collection<*>): Collection<*>? { + val cleanedValues = ArrayList() + for (constraint in constraints) { + if (constraint !is KeyConstraints) { + return null + } + val cleanedRegex = cleanRegexStartsWith(constraint) + ?: return null + cleanedValues.add(cleanedRegex) + } + return cleanedValues + } + + /** + * Creates a regex pattern to match a substring at the beginning of another string. + * + * + * If given string is not a regex to match a string at the beginning of another string, then null + * is returned. + */ + private fun cleanRegexStartsWith(regex: KeyConstraints): KeyConstraints? { + if (!isStartsWithRegex(regex)) { + return null + } + + // remove all instances of \Q and \E from the remaining text & escape single quotes + val literalizedString = (regex["\$regex"] as String) + .replace("([^\\\\])(\\\\E)".toRegex(), "$1") + .replace("([^\\\\])(\\\\Q)".toRegex(), "$1") + .replace("^\\\\E".toRegex(), "") + .replace("^\\\\Q".toRegex(), "") + .replace("([^'])'".toRegex(), "$1''") + .replace("^'([^'])".toRegex(), "''$1") + regex["\$regex"] = "$literalizedString.*" + return regex + } + + /** + * Check if given constraint is a regex to match strings that starts with another string. + */ + private fun isStartsWithRegex(constraint: Any?): Boolean { + if (constraint == null || constraint !is KeyConstraints) { + return false + } + val keyConstraints = constraint + return keyConstraints.size == 1 && keyConstraints.containsKey("\$regex") && + (keyConstraints["\$regex"] as String?)!!.startsWith("^") + } + + /** + * Matches $regex constraints. + */ + @Throws(ParseException::class) + private fun matchesRegexConstraint( + constraint: Any?, + value: Any?, + options: String = "" + ): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + if (!options.matches(Regex("^[imxs]*$"))) { + throw ParseException( + ParseException.INVALID_QUERY, String.format( + "Invalid regex options: %s", options + ) + ) + } + var flags = 0 + if (options.contains("i")) { + flags = flags or Pattern.CASE_INSENSITIVE + } + if (options.contains("m")) { + flags = flags or Pattern.MULTILINE + } + if (options.contains("x")) { + flags = flags or Pattern.COMMENTS + } + if (options.contains("s")) { + flags = flags or Pattern.DOTALL + } + val regex = constraint as String? + val pattern = Pattern.compile(regex, flags) + val matcher = pattern.matcher(value as String?) + return matcher.find() + } + + /** + * Matches $exists constraints. + */ + private fun matchesExistsConstraint(constraint: Any?, value: Any?): Boolean { + /* + * In the Android SDK, null means "undefined", and JSONObject.NULL means "null". + */ + return if (constraint != null && constraint as Boolean) { + value != null && value !== JSONObject.NULL + } else { + value == null || value === JSONObject.NULL + } + } + + /** + * Matches $nearSphere constraints. + */ + private fun matchesNearSphereConstraint( + constraint: Any?, value: Any?, + maxDistance: Double? + ): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + if (maxDistance == null) { + return true + } + val point1 = constraint as ParseGeoPoint? + val point2 = value as ParseGeoPoint + return point1!!.distanceInRadiansTo(point2) <= maxDistance + } + + /** + * Matches $within constraints. + */ + @Throws(ParseException::class) + private fun matchesWithinConstraint(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + val constraintMap = constraint as HashMap>? + val box = constraintMap!!["\$box"]!! + val southwest = box[0] + val northeast = box[1] + val target = value as ParseGeoPoint + if (northeast.longitude < southwest.longitude) { + throw ParseException( + ParseException.INVALID_QUERY, + "whereWithinGeoBox queries cannot cross the International Date Line." + ) + } + if (northeast.latitude < southwest.latitude) { + throw ParseException( + ParseException.INVALID_QUERY, + "The southwest corner of a geo box must be south of the northeast corner." + ) + } + if (northeast.longitude - southwest.longitude > 180) { + throw ParseException( + ParseException.INVALID_QUERY, + "Geo box queries larger than 180 degrees in longitude are not supported. " + + "Please check point order." + ) + } + return target.latitude >= southwest.latitude && target.latitude <= northeast.latitude && target.longitude >= southwest.longitude && target.longitude <= northeast.longitude + } + + /** + * Matches $geoIntersects constraints. + */ + private fun matchesGeoIntersectsConstraint(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + val constraintMap = constraint as HashMap? + val point = constraintMap!!["\$point"] + val target = value as ParsePolygon + return target.containsPoint(point) + } + + /** + * Matches $geoWithin constraints. + */ + private fun matchesGeoWithinConstraint(constraint: Any?, value: Any?): Boolean { + if (value == null || value === JSONObject.NULL) { + return false + } + val constraintMap = constraint as HashMap>? + val points = constraintMap!!["\$polygon"]!! + val polygon = ParsePolygon(points) + val point = value as ParseGeoPoint + return polygon.containsPoint(point) + } + + /** + * Returns true iff the given value matches the given operator and constraint. + * + * @throws UnsupportedOperationException if the operator is not one this function can handle. + */ + @Throws(ParseException::class) + private fun matchesStatelessConstraint( + operator: String, constraint: Any?, + value: Any?, allKeyConstraints: KeyConstraints + ): Boolean { + return when (operator) { + "\$ne" -> matchesNotEqualConstraint(constraint, value) + "\$lt" -> matchesLessThanConstraint(constraint, value) + "\$lte" -> matchesLessThanOrEqualToConstraint(constraint, value) + "\$gt" -> matchesGreaterThanConstraint(constraint, value) + "\$gte" -> matchesGreaterThanOrEqualToConstraint( + constraint, + value + ) + "\$in" -> matchesInConstraint(constraint, value) + "\$nin" -> matchesNotInConstraint(constraint, value) + "\$all" -> matchesAllConstraint(constraint, value) + "\$regex" -> { + val regexOptions = allKeyConstraints["\$options"] as String + matchesRegexConstraint(constraint, value, regexOptions) + } + "\$options" -> // No need to do anything. This is handled by $regex. + true + "\$exists" -> matchesExistsConstraint(constraint, value) + "\$nearSphere" -> { + val maxDistance = allKeyConstraints["\$maxDistance"] as Double? + matchesNearSphereConstraint(constraint, value, maxDistance) + } + "\$maxDistance" -> // No need to do anything. This is handled by $nearSphere. + true + "\$within" -> matchesWithinConstraint(constraint, value) + "\$geoWithin" -> matchesGeoWithinConstraint(constraint, value) + "\$geoIntersects" -> matchesGeoIntersectsConstraint( + constraint, + value + ) + else -> throw UnsupportedOperationException( + String.format( + "The offline store does not yet support the %s operator.", operator + ) + ) + } + } + + /** + * Returns true iff the object is visible based on its read ACL and the given user objectId. + */ + /* package */ + @JvmStatic + fun hasReadAccess(user: ParseUser?, `object`: T): Boolean { + if (user === `object`) { + return true + } + val acl = `object`!!.getACL() ?: return true + return if (acl.publicReadAccess) { + true + } else user != null && acl.getReadAccess(user) + // TODO: Implement roles. + } + + /** + * Returns true iff the object is visible based on its read ACL and the given user objectId. + */ + /* package */ + @JvmStatic + fun hasWriteAccess(user: ParseUser?, `object`: T): Boolean { + if (user === `object`) { + return true + } + val acl = `object`!!.getACL() ?: return true + return if (acl.publicWriteAccess) { + true + } else user != null && acl.getWriteAccess(user) + // TODO: Implement roles. + } + + /** + * Sorts the given array based on the parameters of the given query. + */ + /* package */ + @JvmStatic + @Throws(ParseException::class) + fun sort(results: List?, state: State) { + val keys = state.order() + // Do some error checking just for maximum compatibility with the server. + for (key in state.order()) { + if (!key.matches(Regex("^-?[A-Za-z][A-Za-z0-9_]*$"))) { + if ("_created_at" != key && "_updated_at" != key) { + throw ParseException( + ParseException.INVALID_KEY_NAME, String.format( + "Invalid key name: \"%s\".", key + ) + ) + } + } + } + + // See if there's a $nearSphere constraint that will override the other sort parameters. + var mutableNearSphereKey: String? = null + var mutableNearSphereValue: ParseGeoPoint? = null + for (queryKey in state.constraints().keys) { + val queryKeyConstraints = state.constraints()[queryKey] + if (queryKeyConstraints is KeyConstraints) { + if (queryKeyConstraints.containsKey("\$nearSphere")) { + mutableNearSphereKey = queryKey + mutableNearSphereValue = + queryKeyConstraints["\$nearSphere"] as ParseGeoPoint? + } + } + } + val nearSphereKey = mutableNearSphereKey + val nearSphereValue = mutableNearSphereValue + + // If there's nothing to sort based on, then don't do anything. + if (keys.size == 0 && mutableNearSphereKey == null) { + return + } + + /* + * TODO(klimt): Test whether we allow dotting into objects for sorting. + */ + results?.sortedWith(kotlin.Comparator { lhs: T, rhs: T -> + if (nearSphereKey != null) { + val lhsPoint: ParseGeoPoint + val rhsPoint: ParseGeoPoint + try { + lhsPoint = getValue(lhs, nearSphereKey) as ParseGeoPoint + rhsPoint = getValue(rhs, nearSphereKey) as ParseGeoPoint + } catch (e: ParseException) { + throw RuntimeException(e) + } + + // GeoPoints can't be null if there's a $nearSphere. + val lhsDistance = lhsPoint.distanceInRadiansTo(nearSphereValue) + val rhsDistance = rhsPoint.distanceInRadiansTo(nearSphereValue) + if (lhsDistance != rhsDistance) { + return@Comparator if (lhsDistance - rhsDistance > 0) 1 else -1 + } + } + + for (key in keys) { + var subKey: String? = null + + var descending = false + + if (key.startsWith("-")) { + descending = true + subKey = key.substring(1) + } + var lhsValue: Any? + var rhsValue: Any? + + try { + lhsValue = getValue(lhs, subKey) + rhsValue = getValue(rhs, subKey) + } catch (e: ParseException) { + throw RuntimeException(e) + } + + val result: Int = try { + compareTo(lhsValue, rhsValue) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException( + String.format( + "Unable to sort by key %s.", + subKey + ), e + ) + } + if (result != 0) { + return@Comparator if (descending) -result else result + } + } + 0 + }) + } + + /** + * Makes sure that the object specified by path, relative to container, is fetched. + */ + private fun fetchIncludeAsync( + store: OfflineStore, + container: Any?, + path: String?, + db: ParseSQLiteDatabase + ): Task { + // If there's no object to include, that's fine. + if (container == null) { + return Task.forResult(null) + } + + // If the container is a list or array, fetch all the sub-items. + if (container is Collection<*>) { + // We do the fetches in series because it makes it easier to fail on the first error. + var task = Task.forResult(null) + for (item in container) { + task = task.onSuccessTask { task1: Task? -> + fetchIncludeAsync( + store, + item, + path, + db + ) + } + } + return task + } else if (container is JSONArray) { + val array = container + // We do the fetches in series because it makes it easier to fail on the first error. + var task = Task.forResult(null) + for (i in 0 until array.length()) { + task = task.onSuccessTask { task12: Task? -> + fetchIncludeAsync( + store, + array[i], + path, + db + ) + } + } + return task + } + + // If we've reached the end of the path, then actually do the fetch. + if (path == null) { + return if (JSONObject.NULL == container) { + // Accept JSONObject.NULL value in included field. We swallow it silently instead of + // throwing an exception. + Task.forResult(null) + } else if (container is ParseObject) { + store.fetchLocallyAsync(container, db).makeVoid() + } else { + Task.forError( + ParseException( + ParseException.INVALID_NESTED_KEY, + "include is invalid for non-ParseObjects" + ) + ) + } + } + + // Descend into the container and try again. + val parts = path.split("\\.").toTypedArray() + val key = parts[0] + val rest = if (parts.size > 1) parts[1] else null + + // Make sure the container is fetched. + return Task.forResult(null).continueWithTask { task: Task? -> + if (container is ParseObject) { + // Make sure this object is fetched before descending into it. + return@continueWithTask fetchIncludeAsync( + store, + container, + null, + db + ).onSuccess { task13: Task? -> container[key] } + } else if (container is Map<*, *>) { + return@continueWithTask Task.forResult(container[key]) + } else if (container is JSONObject) { + return@continueWithTask Task.forResult(container.opt(key)) + } else if (JSONObject.NULL == container) { + // Accept JSONObject.NULL value in included field. We swallow it silently instead of + // throwing an exception. + return@continueWithTask null + } else { + return@continueWithTask Task.forError(IllegalStateException("include is invalid")) + } + }.onSuccessTask { task: Task -> fetchIncludeAsync(store, task.result, rest, db) } + } + + /** + * Makes sure all of the objects included by the given query get fetched. + */ + /* package */ + @JvmStatic + fun fetchIncludesAsync( + store: OfflineStore, + `object`: T, + state: State, + db: ParseSQLiteDatabase + ): Task { + val includes = state.includes() + // We do the fetches in series because it makes it easier to fail on the first error. + var task = Task.forResult(null) + for (include in includes) { + task = task.onSuccessTask { + fetchIncludeAsync( + store, + `object`, + include, + db + ) + } + } + return task + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/OfflineStore.java b/parse/src/main/java/com/parse/OfflineStore.java deleted file mode 100644 index 1dd23be15..000000000 --- a/parse/src/main/java/com/parse/OfflineStore.java +++ /dev/null @@ -1,1552 +0,0 @@ -/* - * 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 android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.text.TextUtils; -import android.util.Pair; - -import com.parse.OfflineQueryLogic.ConstraintMatcher; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.WeakHashMap; - -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -class OfflineStore { - - /** - * SQLite has a max of 999 SQL variables in a single statement. - */ - private static final int MAX_SQL_VARIABLES = 999; - // Lock for all members of the store. - final private Object lock = new Object(); - // Helper for accessing the database. - final private OfflineSQLiteOpenHelper helper; - /** - * In-memory map of UUID -> ParseObject. This is used so that we can always return the same - * instance for a given object. The only objects in this map are ones that are in the database. - */ - final private WeakValueHashMap uuidToObjectMap = new WeakValueHashMap<>(); - /** - * In-memory map of ParseObject -> UUID. This is used to that when we see an unsaved ParseObject - * that's already in the database, we can update the same record in the database. It stores a Task - * instead of the String, because one thread may want to reserve the spot. Once the task is - * finished, there will be a row for this UUID in the database. - */ - final private WeakHashMap> objectToUuidMap = new WeakHashMap<>(); - /** - * In-memory set of ParseObjects that have been fetched from the local database already. If the - * object is in the map, a fetch of it has been started. If the value is a finished task, then the - * fetch was completed. - */ - final private WeakHashMap> fetchedObjects = new WeakHashMap<>(); - /** - * In-memory map of (className, objectId) -> ParseObject. This is used so that we can always - * return the same instance for a given object. Objects in this map may or may not be in the - * database. - */ - private final WeakValueHashMap, ParseObject> - classNameAndObjectIdToObjectMap = new WeakValueHashMap<>(); - - /** - * Used by the static method to create the singleton. - */ - /* package */ OfflineStore(Context context) { - this(new OfflineSQLiteOpenHelper(context)); - } - - /* package */ OfflineStore(OfflineSQLiteOpenHelper helper) { - this.helper = helper; - } - - /** - * Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object - * and adds a new row to the database for the object with no data. - */ - private Task getOrCreateUUIDAsync(final ParseObject object, ParseSQLiteDatabase db) { - final String newUUID = UUID.randomUUID().toString(); - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - - synchronized (lock) { - Task uuidTask = objectToUuidMap.get(object); - if (uuidTask != null) { - return uuidTask; - } - - // The object doesn't have a UUID yet, so we're gonna have to make one. - objectToUuidMap.put(object, tcs.getTask()); - uuidToObjectMap.put(newUUID, object); - fetchedObjects.put(object, tcs.getTask().onSuccess(new Continuation() { - @Override - public ParseObject then(Task task) { - return object; - } - })); - } - - /* - * We need to put a placeholder row in the database so that later on, the save can just be an - * update. This could be a pointer to an object that itself never gets saved offline, in which - * case the consumer will just have to deal with that. - */ - ContentValues values = new ContentValues(); - values.put(OfflineSQLiteOpenHelper.KEY_UUID, newUUID); - values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, object.getClassName()); - db.insertOrThrowAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values).continueWith( - new Continuation() { - @Override - public Void then(Task task) { - // This will signal that the UUID does represent a row in the database. - tcs.setResult(newUUID); - return null; - } - }); - - return tcs.getTask(); - } - - /** - * Gets an unfetched pointer to an object in the db, based on its uuid. The object may or may not - * be in memory, but it must be in the database. If it is already in memory, that instance will be - * returned. Since this is only for creating pointers to objects that are referenced by other - * objects in the data store, that's a fair assumption. - * - * @param uuid The object to retrieve. - * @param db The database instance to retrieve from. - * @return The object with that UUID. - */ - private Task getPointerAsync(final String uuid, - ParseSQLiteDatabase db) { - synchronized (lock) { - @SuppressWarnings("unchecked") - T existing = (T) uuidToObjectMap.get(uuid); - if (existing != null) { - return Task.forResult(existing); - } - } - - /* - * We want to just return the pointer, but we have to look in the database to know if there's - * something with this classname and object id already. - */ - - String[] select = {OfflineSQLiteOpenHelper.KEY_CLASS_NAME, OfflineSQLiteOpenHelper.KEY_OBJECT_ID}; - String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; - String[] args = {uuid}; - return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess( - new Continuation() { - @Override - public T then(Task task) { - Cursor cursor = task.getResult(); - cursor.moveToFirst(); - if (cursor.isAfterLast()) { - cursor.close(); - throw new IllegalStateException("Attempted to find non-existent uuid " + uuid); - } - - synchronized (lock) { - // We need to check again since another task might have come around and added it to - // the map. - //TODO (grantland): Maybe we should insert a Task that is resolved when the query - // completes like we do in getOrCreateUUIDAsync? - @SuppressWarnings("unchecked") - T existing = (T) uuidToObjectMap.get(uuid); - if (existing != null) { - return existing; - } - - String className = cursor.getString(0); - String objectId = cursor.getString(1); - cursor.close(); - @SuppressWarnings("unchecked") - T pointer = (T) ParseObject.createWithoutData(className, objectId); - /* - * If it doesn't have an objectId, we don't really need the UUID, and this simplifies - * some other logic elsewhere if we only update the map for new objects. - */ - if (objectId == null) { - uuidToObjectMap.put(uuid, pointer); - objectToUuidMap.put(pointer, Task.forResult(uuid)); - } - return pointer; - } - } - }); - } - - /** - * Runs a ParseQuery against the store's contents. - * - * @return The objects that match the query's constraints. - */ - /* package for OfflineQueryLogic */ Task> findAsync( - ParseQuery.State query, - ParseUser user, - ParsePin pin, - ParseSQLiteDatabase db) { - return findAsync(query, user, pin, false, db); - } - - /** - * Runs a ParseQuery against the store's contents. May cause any instances of T to get fetched - * from the offline database. TODO(klimt): Should the query consider objects that are in memory, - * but not in the offline store? - * - * @param query The query. - * @param user The user making the query. - * @param pin (Optional) The pin we are querying across. If null, all pins. - * @param isCount True if we are doing a count. - * @param db The SQLiteDatabase. - * @param Subclass of ParseObject. - * @return The objects that match the query's constraints. - */ - private Task> findAsync( - final ParseQuery.State query, - final ParseUser user, - final ParsePin pin, - final boolean isCount, - final ParseSQLiteDatabase db) { - /* - * This is currently unused, but is here to allow future querying across objects that are in the - * process of being deleted eventually. - */ - final boolean includeIsDeletingEventually = false; - - final OfflineQueryLogic queryLogic = new OfflineQueryLogic(this); - - final List results = new ArrayList<>(); - - Task queryTask; - if (pin == null) { - String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS; - String[] select = {OfflineSQLiteOpenHelper.KEY_UUID}; - String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?"; - if (!includeIsDeletingEventually) { - where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0"; - } - String[] args = {query.className()}; - - queryTask = db.queryAsync(table, select, where, args); - } else { - Task uuidTask = objectToUuidMap.get(pin); - if (uuidTask == null) { - // Pin was never saved locally, therefore there won't be any results. - return Task.forResult(results); - } - - queryTask = uuidTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String uuid = task.getResult(); - - String table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + " A " + - " INNER JOIN " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + " B " + - " ON A." + OfflineSQLiteOpenHelper.KEY_UUID + "=B." + OfflineSQLiteOpenHelper.KEY_UUID; - String[] select = {"A." + OfflineSQLiteOpenHelper.KEY_UUID}; - String where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" + - " AND " + OfflineSQLiteOpenHelper.KEY_KEY + "=?"; - if (!includeIsDeletingEventually) { - where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0"; - } - String[] args = {query.className(), uuid}; - - return db.queryAsync(table, select, where, args); - } - }); - } - - return queryTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - Cursor cursor = task.getResult(); - List uuids = new ArrayList<>(); - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - uuids.add(cursor.getString(0)); - } - cursor.close(); - - // Find objects that match the where clause. - final ConstraintMatcher matcher = queryLogic.createMatcher(query, user); - - Task checkedAllObjects = Task.forResult(null); - for (final String uuid : uuids) { - final Capture object = new Capture<>(); - - checkedAllObjects = checkedAllObjects.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getPointerAsync(uuid, db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - object.set(task.getResult()); - return fetchLocallyAsync(object.get(), db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - if (!object.get().isDataAvailable()) { - return Task.forResult(false); - } - return matcher.matchesAsync(object.get(), db); - } - }).onSuccess(new Continuation() { - @Override - public Void then(Task task) { - if (task.getResult()) { - results.add(object.get()); - } - return null; - } - }); - } - - return checkedAllObjects; - } - }).onSuccessTask(new Continuation>>() { - @Override - public Task> then(Task task) throws Exception { - // Sort by any sort operators. - OfflineQueryLogic.sort(results, query); - - // Apply the skip. - List trimmedResults = results; - int skip = query.skip(); - if (!isCount && skip >= 0) { - skip = Math.min(query.skip(), trimmedResults.size()); - trimmedResults = trimmedResults.subList(skip, trimmedResults.size()); - } - - // Trim to the limit. - int limit = query.limit(); - if (!isCount && limit >= 0 && trimmedResults.size() > limit) { - trimmedResults = trimmedResults.subList(0, limit); - } - - // Fetch the includes. - Task fetchedIncludesTask = Task.forResult(null); - for (final T object : trimmedResults) { - fetchedIncludesTask = fetchedIncludesTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return OfflineQueryLogic.fetchIncludesAsync(OfflineStore.this, object, query, db); - } - }); - } - - final List finalTrimmedResults = trimmedResults; - return fetchedIncludesTask.onSuccess(new Continuation>() { - @Override - public List then(Task task) { - return finalTrimmedResults; - } - }); - } - }); - } - - /** - * Gets the data for the given object from the offline database. Returns a task that will be - * completed if data for the object was available. If the object is not in the cache, the task - * will be faulted, with a CACHE_MISS error. - * - * @param object The object to fetch. - * @param db A database connection to use. - */ - /* package for OfflineQueryLogic */ Task fetchLocallyAsync( - final T object, - final ParseSQLiteDatabase db) { - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - Task uuidTask; - - synchronized (lock) { - if (fetchedObjects.containsKey(object)) { - /* - * The object has already been fetched from the offline store, so any data that's in there - * is already reflected in the in-memory version. There's nothing more to do. - */ - //noinspection unchecked - return (Task) fetchedObjects.get(object); - } - - /* - * Put a placeholder so that anyone else who attempts to fetch this object will just wait for - * this call to finish doing it. - */ - //noinspection unchecked - fetchedObjects.put(object, (Task) tcs.getTask()); - - uuidTask = objectToUuidMap.get(object); - } - String className = object.getClassName(); - String objectId = object.getObjectId(); - - /* - * If this gets set, then it will contain data from the offline store that needs to be merged - * into the existing object in memory. - */ - Task jsonStringTask = Task.forResult(null); - - if (objectId == null) { - // This Object has never been saved to Parse. - if (uuidTask == null) { - /* - * This object was not pulled from the data store or previously saved to it, so there's - * nothing that can be fetched from it. This isn't an error, because it's really convenient - * to try to fetch objects from the offline store just to make sure they are up-to-date, and - * we shouldn't force developers to specially handle this case. - */ - } else { - /* - * This object is a new ParseObject that is known to the data store, but hasn't been - * fetched. The only way this could happen is if the object had previously been stored in - * the offline store, then the object was removed from memory (maybe by rebooting), and then - * a object with a pointer to it was fetched, so we only created the pointer. We need to - * pull the data out of the database using the UUID. - */ - final String[] select = {OfflineSQLiteOpenHelper.KEY_JSON}; - final String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; - final Capture uuid = new Capture<>(); - jsonStringTask = uuidTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - uuid.set(task.getResult()); - String[] args = {uuid.get()}; - return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args); - } - }).onSuccess(new Continuation() { - @Override - public String then(Task task) { - Cursor cursor = task.getResult(); - cursor.moveToFirst(); - if (cursor.isAfterLast()) { - cursor.close(); - throw new IllegalStateException("Attempted to find non-existent uuid " + uuid.get()); - } - String json = cursor.getString(0); - cursor.close(); - - return json; - } - }); - } - } else { - if (uuidTask != null) { - /* - * This object is an existing ParseObject, and we must've already pulled its data out of the - * offline store, or else we wouldn't know its UUID. This should never happen. - */ - tcs.setError(new IllegalStateException("This object must have already been " - + "fetched from the local datastore, but isn't marked as fetched.")); - synchronized (lock) { - // Forget we even tried to fetch this object, so that retries will actually... retry. - fetchedObjects.remove(object); - } - return tcs.getTask(); - } - - /* - * We've got a pointer to an existing ParseObject, but we've never pulled its data out of the - * offline store. Since fetching from the server forces a fetch from the offline store, that - * means this is a pointer. We need to try to find any existing entry for this object in the - * database. - */ - String[] select = {OfflineSQLiteOpenHelper.KEY_JSON, OfflineSQLiteOpenHelper.KEY_UUID}; - String where = - String.format("%s = ? AND %s = ?", OfflineSQLiteOpenHelper.KEY_CLASS_NAME, - OfflineSQLiteOpenHelper.KEY_OBJECT_ID); - String[] args = {className, objectId}; - jsonStringTask = - db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args).onSuccess( - new Continuation() { - @Override - public String then(Task task) throws Exception { - Cursor cursor = task.getResult(); - cursor.moveToFirst(); - if (cursor.isAfterLast()) { - /* - * This is a pointer that came from Parse that references an object that has - * never been saved in the offline store before. This just means there's no data - * in the store that needs to be merged into the object. - */ - cursor.close(); - throw new ParseException(ParseException.CACHE_MISS, - "This object is not available in the offline cache."); - } - - // we should fetch its data and record its UUID for future reference. - String jsonString = cursor.getString(0); - String newUUID = cursor.getString(1); - cursor.close(); - - synchronized (lock) { - /* - * It's okay to put this object into the uuid map. No one will try to fetch - * it, because it's already in the fetchedObjects map. And no one will try to - * save to it without fetching it first, so everything should be just fine. - */ - objectToUuidMap.put(object, Task.forResult(newUUID)); - uuidToObjectMap.put(newUUID, object); - } - - return jsonString; - } - }); - } - - return jsonStringTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String jsonString = task.getResult(); - if (jsonString == null) { - /* - * This means we tried to fetch an object from the database that was never actually saved - * locally. This probably means that its parent object was saved locally and we just - * created a pointer to this object. This should be considered a cache miss. - */ - return Task.forError(new ParseException(ParseException.CACHE_MISS, - "Attempted to fetch an object offline which was never saved to the offline cache.")); - } - final JSONObject json; - try { - /* - * We can assume that whatever is in the database is the last known server state. The only - * things to maintain from the in-memory object are any changes since the object was last - * put in the database. - */ - json = new JSONObject(jsonString); - } catch (JSONException e) { - return Task.forError(e); - } - - // Fetch all the offline objects before we decode. - final Map> offlineObjects = new HashMap<>(); - - (new ParseTraverser() { - @Override - protected boolean visit(Object object) { - if (object instanceof JSONObject - && ((JSONObject) object).optString("__type").equals("OfflineObject")) { - String uuid = ((JSONObject) object).optString("uuid"); - offlineObjects.put(uuid, getPointerAsync(uuid, db)); - } - return true; - } - }).setTraverseParseObjects(false).setYieldRoot(false).traverse(json); - - return Task.whenAll(offlineObjects.values()).onSuccess(new Continuation() { - @Override - public Void then(Task task) { - object.mergeREST(object.getState(), json, new OfflineDecoder(offlineObjects)); - return null; - } - }); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(object); - } - return tcs.getTask(); - } - }); - } - - /** - * Gets the data for the given object from the offline database. Returns a task that will be - * completed if data for the object was available. If the object is not in the cache, the task - * will be faulted, with a CACHE_MISS error. - * - * @param object The object to fetch. - */ - /* package */ Task fetchLocallyAsync(final T object) { - return runWithManagedConnection(new SQLiteDatabaseCallable>() { - @Override - public Task call(ParseSQLiteDatabase db) { - return fetchLocallyAsync(object, db); - } - }); - } - - /** - * Stores a single object in the local database. If the object is a pointer, isn't dirty, and has - * an objectId already, it may not be saved, since it would provide no useful data. - * - * @param object The object to save. - * @param db A database connection to use. - */ - private Task saveLocallyAsync( - final String key, final ParseObject object, final ParseSQLiteDatabase db) { - // If this is just a clean, unfetched pointer known to Parse, then there is nothing to save. - if (object.getObjectId() != null && !object.isDataAvailable() && !object.hasChanges() - && !object.hasOutstandingOperations()) { - return Task.forResult(null); - } - - final Capture uuidCapture = new Capture<>(); - - // Make sure we have a UUID for the object to be saved. - return getOrCreateUUIDAsync(object, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String uuid = task.getResult(); - uuidCapture.set(uuid); - return updateDataForObjectAsync(uuid, object, db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ContentValues values = new ContentValues(); - values.put(OfflineSQLiteOpenHelper.KEY_KEY, key); - values.put(OfflineSQLiteOpenHelper.KEY_UUID, uuidCapture.get()); - return db.insertWithOnConflict(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, values, - SQLiteDatabase.CONFLICT_IGNORE); - } - }); - } - - /** - * Stores an object (and optionally, every object it points to recursively) in the local database. - * If any of the objects have not been fetched from Parse, they will not be stored. However, if - * they have changed data, the data will be retained. To get the objects back later, you can use a - * ParseQuery with a cache policy that uses the local cache, or you can create an unfetched - * pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. If you modify - * the object after saving it locally, such as by fetching it or saving it, those changes will - * automatically be applied to the cache. - *

- * Any objects previously stored with the same key will be removed from the local database. - * - * @param object Root object - * @param includeAllChildren {@code true} to recursively save all pointers. - * @param db DB connection - * @return A Task that will be resolved when saving is complete - */ - private Task saveLocallyAsync( - final ParseObject object, final boolean includeAllChildren, final ParseSQLiteDatabase db) { - final ArrayList objectsInTree = new ArrayList<>(); - // Fetch all objects locally in case they are being re-added - if (!includeAllChildren) { - objectsInTree.add(object); - } else { - (new ParseTraverser() { - @Override - protected boolean visit(Object object) { - if (object instanceof ParseObject) { - objectsInTree.add((ParseObject) object); - } - return true; - } - }).setYieldRoot(true).setTraverseParseObjects(true).traverse(object); - } - - return saveLocallyAsync(object, objectsInTree, db); - } - - private Task saveLocallyAsync( - final ParseObject object, List children, final ParseSQLiteDatabase db) { - final List objects = children != null - ? new ArrayList<>(children) - : new ArrayList(); - if (!objects.contains(object)) { - objects.add(object); - } - - // Call saveLocallyAsync for each of them individually. - final List> tasks = new ArrayList<>(); - for (ParseObject obj : objects) { - tasks.add(fetchLocallyAsync(obj, db).makeVoid()); - } - - return Task.whenAll(tasks).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return objectToUuidMap.get(object); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String uuid = task.getResult(); - if (uuid == null) { - // The root object was never stored in the offline store, so nothing to unpin. - return null; - } - - // Delete all objects locally corresponding to the key we're trying to use in case it was - // used before (overwrite) - return unpinAsync(uuid, db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getOrCreateUUIDAsync(object, db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String uuid = task.getResult(); - - // Call saveLocallyAsync for each of them individually. - final List> tasks = new ArrayList<>(); - for (ParseObject obj : objects) { - tasks.add(saveLocallyAsync(uuid, obj, db)); - } - - return Task.whenAll(tasks); - } - }); - } - - private Task unpinAsync(final ParseObject object, final ParseSQLiteDatabase db) { - Task uuidTask = objectToUuidMap.get(object); - if (uuidTask == null) { - // The root object was never stored in the offline store, so nothing to unpin. - return Task.forResult(null); - } - return uuidTask.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - final String uuid = task.getResult(); - if (uuid == null) { - // The root object was never stored in the offline store, so nothing to unpin. - return Task.forResult(null); - } - return unpinAsync(uuid, db); - } - }); - } - - private Task unpinAsync(final String key, final ParseSQLiteDatabase db) { - final List uuidsToDelete = new LinkedList<>(); - // A continueWithTask that ends with "return task" is essentially a try-finally. - return Task.forResult((Void) null).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1 - String sql = "SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + - " WHERE " + OfflineSQLiteOpenHelper.KEY_KEY + "=? AND " + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + - " SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + - " GROUP BY " + OfflineSQLiteOpenHelper.KEY_UUID + - " HAVING COUNT(" + OfflineSQLiteOpenHelper.KEY_UUID + ")=1" + - ")"; - String[] args = {key}; - return db.rawQueryAsync(sql, args); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // DELETE FROM Objects - - Cursor cursor = task.getResult(); - while (cursor.moveToNext()) { - uuidsToDelete.add(cursor.getString(0)); - } - cursor.close(); - - return deleteObjects(uuidsToDelete, db); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // DELETE FROM Dependencies - String where = OfflineSQLiteOpenHelper.KEY_KEY + "=?"; - String[] args = {key}; - return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args); - } - }).onSuccess(new Continuation() { - @Override - public Void then(Task task) { - synchronized (lock) { - // Remove uuids from memory - for (String uuid : uuidsToDelete) { - ParseObject object = uuidToObjectMap.get(uuid); - if (object != null) { - objectToUuidMap.remove(object); - uuidToObjectMap.remove(uuid); - } - } - } - return null; - } - }); - } - - private Task deleteObjects(final List uuids, final ParseSQLiteDatabase db) { - if (uuids.size() <= 0) { - return Task.forResult(null); - } - - // SQLite has a max 999 SQL variables in a statement, so we need to split it up into manageable - // chunks. We can do this because we're already in a transaction. - if (uuids.size() > MAX_SQL_VARIABLES) { - return deleteObjects(uuids.subList(0, MAX_SQL_VARIABLES), db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return deleteObjects(uuids.subList(MAX_SQL_VARIABLES, uuids.size()), db); - } - }); - } - - String[] placeholders = new String[uuids.size()]; - for (int i = 0; i < placeholders.length; i++) { - placeholders[i] = "?"; - } - String where = OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + TextUtils.join(",", placeholders) + ")"; - // dynamic args - String[] args = uuids.toArray(new String[uuids.size()]); - return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args); - } - - /** - * Takes an object that has been fetched from the database before and updates it with whatever - * data is in memory. This will only be used when data comes back from the server after a fetch or - * a save. - */ - /* package */ Task updateDataForObjectAsync(final ParseObject object) { - Task fetched; - // Make sure the object is fetched. - synchronized (lock) { - fetched = fetchedObjects.get(object); - if (fetched == null) { - return Task.forError(new IllegalStateException( - "An object cannot be updated if it wasn't fetched.")); - } - } - return fetched.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - // Catch CACHE_MISS - //noinspection ThrowableResultOfMethodCallIgnored - if (task.getError() instanceof ParseException - && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) { - return Task.forResult(null); - } - return task.makeVoid(); - } - - return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseSQLiteDatabase db = task.getResult(); - return db.beginTransactionAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return updateDataForObjectAsync(object, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return db.setTransactionSuccessfulAsync(); - } - }).continueWithTask(new Continuation>() { - // } finally { - @Override - public Task then(Task task) { - db.endTransactionAsync(); - db.closeAsync(); - return task; - } - }); - } - }); - } - }); - } - }); - } - - private Task updateDataForObjectAsync( - final ParseObject object, - final ParseSQLiteDatabase db) { - // Make sure the object has a UUID. - Task uuidTask; - synchronized (lock) { - uuidTask = objectToUuidMap.get(object); - if (uuidTask == null) { - // It was fetched, but it has no UUID. That must mean it isn't actually in the database. - return Task.forResult(null); - } - } - return uuidTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String uuid = task.getResult(); - return updateDataForObjectAsync(uuid, object, db); - } - }); - } - - private Task updateDataForObjectAsync( - final String uuid, - final ParseObject object, - final ParseSQLiteDatabase db) { - // Now actually encode the object as JSON. - OfflineEncoder encoder = new OfflineEncoder(db); - final JSONObject json = object.toRest(encoder); - - return encoder.whenFinished().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - // Put the JSON in the database. - String className = object.getClassName(); - String objectId = object.getObjectId(); - int isDeletingEventually = json.getInt(ParseObject.KEY_IS_DELETING_EVENTUALLY); - - final ContentValues values = new ContentValues(); - values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, className); - values.put(OfflineSQLiteOpenHelper.KEY_JSON, json.toString()); - if (objectId != null) { - values.put(OfflineSQLiteOpenHelper.KEY_OBJECT_ID, objectId); - } - values.put(OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); - String where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?"; - String[] args = {uuid}; - return db.updateAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values, where, args).makeVoid(); - } - }); - } - - /* package */ Task deleteDataForObjectAsync(final ParseObject object) { - return helper.getWritableDatabaseAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseSQLiteDatabase db = task.getResult(); - return db.beginTransactionAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return deleteDataForObjectAsync(object, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return db.setTransactionSuccessfulAsync(); - } - }).continueWithTask(new Continuation>() { - // } finally { - @Override - public Task then(Task task) { - db.endTransactionAsync(); - db.closeAsync(); - return task; - } - }); - } - }); - } - }); - } - - private Task deleteDataForObjectAsync(final ParseObject object, final ParseSQLiteDatabase db) { - final Capture uuid = new Capture<>(); - - // Make sure the object has a UUID. - Task uuidTask; - synchronized (lock) { - uuidTask = objectToUuidMap.get(object); - if (uuidTask == null) { - // It was fetched, but it has no UUID. That must mean it isn't actually in the database. - return Task.forResult(null); - } - } - uuidTask = uuidTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - uuid.set(task.getResult()); - return task; - } - }); - - // If the object was the root of a pin, unpin it. - Task unpinTask = uuidTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // Find all the roots for this object. - String[] select = {OfflineSQLiteOpenHelper.KEY_KEY}; - String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; - String[] args = {uuid.get()}; - return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, select, where, args); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // Try to unpin this object from the pin label if it's a root of the ParsePin. - Cursor cursor = task.getResult(); - List uuids = new ArrayList<>(); - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { - uuids.add(cursor.getString(0)); - } - cursor.close(); - - List> tasks = new ArrayList<>(); - for (final String uuid : uuids) { - Task unpinTask = getPointerAsync(uuid, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParsePin pin = (ParsePin) task.getResult(); - return fetchLocallyAsync(pin, db); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - ParsePin pin = task.getResult(); - - List modified = pin.getObjects(); - if (modified == null || !modified.contains(object)) { - return task.makeVoid(); - } - - modified.remove(object); - if (modified.size() == 0) { - return unpinAsync(uuid, db); - } - - pin.setObjects(modified); - return saveLocallyAsync(pin, true, db); - } - }); - tasks.add(unpinTask); - } - - return Task.whenAll(tasks); - } - }); - - // Delete the object from the Local Datastore in case it wasn't the root of a pin. - return unpinTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; - String[] args = {uuid.get()}; - return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String where = OfflineSQLiteOpenHelper.KEY_UUID + "=?"; - String[] args = {uuid.get()}; - return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (lock) { - // Clean up - //TODO (grantland): we should probably clean up uuidToObjectMap and objectToUuidMap, but - // getting the uuid requires a task and things might get a little funky... - fetchedObjects.remove(object); - } - return task; - } - }); - } - - private Task getParsePin(final String name, ParseSQLiteDatabase db) { - ParseQuery.State query = new ParseQuery.State.Builder<>(ParsePin.class) - .whereEqualTo(ParsePin.KEY_NAME, name) - .build(); - - /* We need to call directly to the OfflineStore since we don't want/need a user to query for - * ParsePins - */ - return findAsync(query, null, null, db).onSuccess(new Continuation, ParsePin>() { - @Override - public ParsePin then(Task> task) { - ParsePin pin = null; - if (task.getResult() != null && task.getResult().size() > 0) { - pin = task.getResult().get(0); - } - - //TODO (grantland): What do we do if there are more than 1 result? - - if (pin == null) { - pin = ParseObject.create(ParsePin.class); - pin.setName(name); - } - return pin; - } - }); - } - - //region ParsePin - - /* package */ Task pinAllObjectsAsync( - final String name, - final List objects, - final boolean includeChildren) { - return runWithManagedTransaction(new SQLiteDatabaseCallable>() { - @Override - public Task call(ParseSQLiteDatabase db) { - return pinAllObjectsAsync(name, objects, includeChildren, db); - } - }); - } - - private Task pinAllObjectsAsync( - final String name, - final List objects, - final boolean includeChildren, - final ParseSQLiteDatabase db) { - if (objects == null || objects.size() == 0) { - return Task.forResult(null); - } - - return getParsePin(name, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParsePin pin = task.getResult(); - - //TODO (grantland): change to use relations. currently the related PO are only getting saved - // offline as pointers. -// ParseRelation relation = pin.getRelation(KEY_OBJECTS); -// relation.add(object); - - // Hack to store collections in a pin - List modified = pin.getObjects(); - if (modified == null) { - modified = new ArrayList(objects); - } else { - for (ParseObject object : objects) { - if (!modified.contains(object)) { - modified.add(object); - } - } - } - pin.setObjects(modified); - - if (includeChildren) { - return saveLocallyAsync(pin, true, db); - } - return saveLocallyAsync(pin, pin.getObjects(), db); - } - }); - } - - /* package */ Task unpinAllObjectsAsync( - final String name, - final List objects) { - return runWithManagedTransaction(new SQLiteDatabaseCallable>() { - @Override - public Task call(ParseSQLiteDatabase db) { - return unpinAllObjectsAsync(name, objects, db); - } - }); - } - - private Task unpinAllObjectsAsync( - String name, - final List objects, - final ParseSQLiteDatabase db) { - if (objects == null || objects.size() == 0) { - return Task.forResult(null); - } - - return getParsePin(name, db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParsePin pin = task.getResult(); - - //TODO (grantland): change to use relations. currently the related PO are only getting saved - // offline as pointers. -// ParseRelation relation = pin.getRelation(KEY_OBJECTS); -// relation.remove(object); - - // Hack to store collections in a pin - List modified = pin.getObjects(); - if (modified == null) { - // Unpin a pin that doesn't exist. Wat? - return Task.forResult(null); - } - - modified.removeAll(objects); - if (modified.size() == 0) { - return unpinAsync(pin, db); - } - pin.setObjects(modified); - - return saveLocallyAsync(pin, true, db); - } - }); - } - - /* package */ Task unpinAllObjectsAsync(final String name) { - return runWithManagedTransaction(new SQLiteDatabaseCallable>() { - @Override - public Task call(ParseSQLiteDatabase db) { - return unpinAllObjectsAsync(name, db); - } - }); - } - - private Task unpinAllObjectsAsync(final String name, final ParseSQLiteDatabase db) { - return getParsePin(name, db).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - return task.makeVoid(); - } - ParsePin pin = task.getResult(); - return unpinAsync(pin, db); - } - }); - } - - /* package */ Task> findFromPinAsync( - final String name, - final ParseQuery.State state, - final ParseUser user) { - return runWithManagedConnection(new SQLiteDatabaseCallable>>() { - @Override - public Task> call(ParseSQLiteDatabase db) { - return findFromPinAsync(name, state, user, db); - } - }); - } - - private Task> findFromPinAsync( - final String name, - final ParseQuery.State state, - final ParseUser user, - final ParseSQLiteDatabase db) { - Task task; - if (name != null) { - task = getParsePin(name, db); - } else { - task = Task.forResult(null); - } - return task.onSuccessTask(new Continuation>>() { - @Override - public Task> then(Task task) { - ParsePin pin = task.getResult(); - return findAsync(state, user, pin, false, db); - } - }); - } - - /* package */ Task countFromPinAsync( - final String name, - final ParseQuery.State state, - final ParseUser user) { - return runWithManagedConnection(new SQLiteDatabaseCallable>() { - @Override - public Task call(ParseSQLiteDatabase db) { - return countFromPinAsync(name, state, user, db); - } - }); - } - - private Task countFromPinAsync( - final String name, - final ParseQuery.State state, - final ParseUser user, - final ParseSQLiteDatabase db) { - Task task; - if (name != null) { - task = getParsePin(name, db); - } else { - task = Task.forResult(null); - } - return task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParsePin pin = task.getResult(); - return findAsync(state, user, pin, true, db).onSuccess(new Continuation, Integer>() { - @Override - public Integer then(Task> task) { - return task.getResult().size(); - } - }); - } - }); - } - - /** - * This should be called by the ParseObject constructor notify the store that there is an object - * with this className and objectId. - */ - /* package */ void registerNewObject(ParseObject object) { - synchronized (lock) { - String objectId = object.getObjectId(); - if (objectId != null) { - String className = object.getClassName(); - Pair classNameAndObjectId = Pair.create(className, objectId); - classNameAndObjectIdToObjectMap.put(classNameAndObjectId, object); - } - } - } - - //endregion - - //region Single Instance - - /* package */ void unregisterObject(ParseObject object) { - synchronized (lock) { - String objectId = object.getObjectId(); - if (objectId != null) { - classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), objectId)); - } - } - } - - /** - * This should only ever be called from ParseObject.createWithoutData(). - * - * @return a pair of ParseObject and Boolean. The ParseObject is the object. The Boolean is true - * iff the object was newly created. - */ - /* package */ ParseObject getObject(String className, String objectId) { - if (objectId == null) { - throw new IllegalStateException("objectId cannot be null."); - } - - Pair classNameAndObjectId = Pair.create(className, objectId); - // This lock should never be held by anyone doing disk or database access. - synchronized (lock) { - return classNameAndObjectIdToObjectMap.get(classNameAndObjectId); - } - } - - /** - * When an object is finished saving, it gets an objectId. Then it should call this method to - * clean up the bookeeping around ids. - */ - /* package */ void updateObjectId(ParseObject object, String oldObjectId, String newObjectId) { - if (oldObjectId != null) { - if (oldObjectId.equals(newObjectId)) { - return; - } - /* - * Special case for re-saving installation if it was deleted on the server - * @see ParseInstallation#saveAsync(String, Task) - */ - if (object instanceof ParseInstallation - && newObjectId == null) { - synchronized (lock) { - classNameAndObjectIdToObjectMap.remove(Pair.create(object.getClassName(), oldObjectId)); - } - return; - } else { - throw new RuntimeException("objectIds cannot be changed in offline mode."); - } - } - - String className = object.getClassName(); - Pair classNameAndNewObjectId = Pair.create(className, newObjectId); - - synchronized (lock) { - // See if there's already an entry for the new object id. - ParseObject existing = classNameAndObjectIdToObjectMap.get(classNameAndNewObjectId); - if (existing != null && existing != object) { - throw new RuntimeException("Attempted to change an objectId to one that's " - + "already known to the Offline Store."); - } - - // Okay, all clear to add the new reference. - classNameAndObjectIdToObjectMap.put(classNameAndNewObjectId, object); - } - } - - /** - * Wraps SQLite operations with a managed SQLite connection. - */ - private Task runWithManagedConnection(final SQLiteDatabaseCallable> callable) { - return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseSQLiteDatabase db = task.getResult(); - return callable.call(db).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - db.closeAsync(); - return task; - } - }); - } - }); - } - - /** - * Wraps SQLite operations with a managed SQLite connection and transaction. - */ - private Task runWithManagedTransaction(final SQLiteDatabaseCallable> callable) { - return helper.getWritableDatabaseAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseSQLiteDatabase db = task.getResult(); - return db.beginTransactionAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return callable.call(db).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return db.setTransactionSuccessfulAsync(); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - db.endTransactionAsync(); - db.closeAsync(); - return task; - } - }); - } - }); - } - }); - } - - //endregion - - /** - * Clears all in-memory caches so that data must be retrieved from disk. - */ - void simulateReboot() { - synchronized (lock) { - uuidToObjectMap.clear(); - objectToUuidMap.clear(); - classNameAndObjectIdToObjectMap.clear(); - fetchedObjects.clear(); - } - } - - /** - * Clears the database on disk. - */ - void clearDatabase(Context context) { - helper.clearDatabase(context); - } - - private interface SQLiteDatabaseCallable { - T call(ParseSQLiteDatabase db); - } - - /* - * Methods for testing. - */ - - /** - * Extends the normal JSON -> ParseObject decoding to also deal with placeholders for new objects - * that have been saved offline. - */ - private class OfflineDecoder extends ParseDecoder { - // A map of UUID -> Task that will be finished once the given ParseObject is loaded. - // The Tasks should all be finished before decode is called. - private Map> offlineObjects; - - private OfflineDecoder(Map> offlineObjects) { - this.offlineObjects = offlineObjects; - } - - @Override - public Object decode(Object object) { - // If we see an offline id, make sure to decode it. - if (object instanceof JSONObject - && ((JSONObject) object).optString("__type").equals("OfflineObject")) { - String uuid = ((JSONObject) object).optString("uuid"); - return offlineObjects.get(uuid).getResult(); - } - - /* - * Embedded objects can't show up here, because we never stored them that way offline. - */ - - return super.decode(object); - } - } - - /** - * An encoder that can encode objects that are available offline. After using this encoder, you - * must call whenFinished() and wait for its result to be finished before the results of the - * encoding will be valid. - */ - private class OfflineEncoder extends ParseEncoder { - private final Object tasksLock = new Object(); - private ParseSQLiteDatabase db; - private ArrayList> tasks = new ArrayList<>(); - - /** - * Creates an encoder. - * - * @param db A database connection to use. - */ - public OfflineEncoder(ParseSQLiteDatabase db) { - this.db = db; - } - - /** - * The results of encoding an object with this encoder will not be valid until the task returned - * by this method is finished. - */ - public Task whenFinished() { - return Task.whenAll(tasks).continueWithTask(new Continuation>() { - @Override - public Task then(Task ignore) { - synchronized (tasksLock) { - // It might be better to return an aggregate error here. - for (Task task : tasks) { - if (task.isFaulted() || task.isCancelled()) { - return task; - } - } - tasks.clear(); - return Task.forResult(null); - } - } - }); - } - - /** - * Implements an encoding strategy for Parse Objects that uses offline ids when necessary. - */ - @Override - public JSONObject encodeRelatedObject(ParseObject object) { - try { - if (object.getObjectId() != null) { - JSONObject result = new JSONObject(); - result.put("__type", "Pointer"); - result.put("objectId", object.getObjectId()); - result.put("className", object.getClassName()); - return result; - } - - final JSONObject result = new JSONObject(); - result.put("__type", "OfflineObject"); - synchronized (tasksLock) { - tasks.add(getOrCreateUUIDAsync(object, db).onSuccess(new Continuation() { - @Override - public Void then(Task task) throws Exception { - result.put("uuid", task.getResult()); - return null; - } - })); - } - return result; - } catch (JSONException e) { - // This can literally never happen. - throw new RuntimeException(e); - } - } - } -} diff --git a/parse/src/main/java/com/parse/OfflineStore.kt b/parse/src/main/java/com/parse/OfflineStore.kt new file mode 100644 index 000000000..ffb548c27 --- /dev/null +++ b/parse/src/main/java/com/parse/OfflineStore.kt @@ -0,0 +1,1348 @@ +/* + * 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 android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.text.TextUtils +import android.util.Pair +import com.parse.ParseObject.Companion.create +import com.parse.boltsinternal.Capture +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +internal open class OfflineStore /* package */( // Helper for accessing the database. + private val helper: OfflineSQLiteOpenHelper +) { + // Lock for all members of the store. + private val lock = Any() + + /** + * In-memory map of UUID -> ParseObject. This is used so that we can always return the same + * instance for a given object. The only objects in this map are ones that are in the database. + */ + private val uuidToObjectMap = WeakValueHashMap() + + /** + * In-memory map of ParseObject -> UUID. This is used to that when we see an unsaved ParseObject + * that's already in the database, we can update the same record in the database. It stores a Task + * instead of the String, because one thread may want to reserve the spot. Once the task is + * finished, there will be a row for this UUID in the database. + */ + private val objectToUuidMap = WeakHashMap>() + + /** + * In-memory set of ParseObjects that have been fetched from the local database already. If the + * object is in the map, a fetch of it has been started. If the value is a finished task, then the + * fetch was completed. + */ + private val fetchedObjects = WeakHashMap>() + + /** + * In-memory map of (className, objectId) -> ParseObject. This is used so that we can always + * return the same instance for a given object. Objects in this map may or may not be in the + * database. + */ + private val classNameAndObjectIdToObjectMap = + WeakValueHashMap, ParseObject>() + + /** + * Used by the static method to create the singleton. + */ + /* package */ + constructor(context: Context?) : this(OfflineSQLiteOpenHelper(context)) {} + + /** + * Gets the UUID for the given object, if it has one. Otherwise, creates a new UUID for the object + * and adds a new row to the database for the object with no data. + */ + private fun getOrCreateUUIDAsync(`object`: ParseObject, db: ParseSQLiteDatabase): Task { + val newUUID = UUID.randomUUID().toString() + val tcs = TaskCompletionSource() + synchronized(lock) { + val uuidTask = objectToUuidMap[`object`] + if (uuidTask != null) { + return uuidTask + } + + // The object doesn't have a UUID yet, so we're gonna have to make one. + objectToUuidMap[`object`] = tcs.task + uuidToObjectMap.put(newUUID, `object`) + fetchedObjects.put(`object`, tcs.task.onSuccess { `object` }) + } + + /* + * We need to put a placeholder row in the database so that later on, the save can just be an + * update. This could be a pointer to an object that itself never gets saved offline, in which + * case the consumer will just have to deal with that. + */ + val values = ContentValues() + values.put(OfflineSQLiteOpenHelper.KEY_UUID, newUUID) + values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, `object`.className) + db.insertOrThrowAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values).continueWith { + // This will signal that the UUID does represent a row in the database. + tcs.setResult(newUUID) + null + } + return tcs.task + } + + /** + * Gets an unfetched pointer to an object in the db, based on its uuid. The object may or may not + * be in memory, but it must be in the database. If it is already in memory, that instance will be + * returned. Since this is only for creating pointers to objects that are referenced by other + * objects in the data store, that's a fair assumption. + * + * @param uuid The object to retrieve. + * @param db The database instance to retrieve from. + * @return The object with that UUID. + */ + private fun getPointerAsync( + uuid: String, + db: ParseSQLiteDatabase + ): Task { + synchronized(lock) { + val existing = uuidToObjectMap[uuid] as T? + if (existing != null) { + return Task.forResult(existing) + } + } + + /* + * We want to just return the pointer, but we have to look in the database to know if there's + * something with this classname and object id already. + */ + val select = + arrayOf(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, OfflineSQLiteOpenHelper.KEY_OBJECT_ID) + val where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?" + val args = arrayOf(uuid) + return db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args) + .onSuccess { task: Task -> + val cursor = task.result + cursor.moveToFirst() + if (cursor.isAfterLast) { + cursor.close() + throw IllegalStateException("Attempted to find non-existent uuid $uuid") + } + synchronized(lock) { + + // We need to check again since another task might have come around and added it to + // the map. + //TODO (grantland): Maybe we should insert a Task that is resolved when the query + // completes like we do in getOrCreateUUIDAsync? + val existing = uuidToObjectMap[uuid] as T? + if (existing != null) { + return@onSuccess existing + } + val className = cursor.getString(0) + val objectId = cursor.getString(1) + cursor.close() + val pointer = ParseObject.createWithoutData(className, objectId) as T? + /* + * If it doesn't have an objectId, we don't really need the UUID, and this simplifies + * some other logic elsewhere if we only update the map for new objects. + */if (objectId == null) { + uuidToObjectMap.put(uuid, pointer) + objectToUuidMap[pointer] = Task.forResult(uuid) + } + return@onSuccess pointer + } + } + } + + /** + * Runs a ParseQuery against the store's contents. + * + * @return The objects that match the query's constraints. + */ + /* package for OfflineQueryLogic */ + fun findAsync( + query: ParseQuery.State, + user: ParseUser?, + pin: ParsePin?, + db: ParseSQLiteDatabase + ): Task?> { + return findAsync(query, user, pin, false, db) + } + + /** + * Runs a ParseQuery against the store's contents. May cause any instances of T to get fetched + * from the offline database. TODO(klimt): Should the query consider objects that are in memory, + * but not in the offline store? + * + * @param query The query. + * @param user The user making the query. + * @param pin (Optional) The pin we are querying across. If null, all pins. + * @param isCount True if we are doing a count. + * @param db The SQLiteDatabase. + * @param Subclass of ParseObject. + * @return The objects that match the query's constraints. + */ + private fun findAsync( + query: ParseQuery.State, + user: ParseUser?, + pin: ParsePin?, + isCount: Boolean, + db: ParseSQLiteDatabase + ): Task?> { + /* + * This is currently unused, but is here to allow future querying across objects that are in the + * process of being deleted eventually. + */ + val includeIsDeletingEventually = false + val queryLogic = OfflineQueryLogic(this) + val results: MutableList = ArrayList() + val queryTask: Task + if (pin == null) { + val table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + val select = arrayOf(OfflineSQLiteOpenHelper.KEY_UUID) + var where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" + if (!includeIsDeletingEventually) { + where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0" + } + val args = arrayOf(query.className()) + queryTask = db.queryAsync(table, select, where, args) + } else { + val uuidTask = objectToUuidMap[pin] + ?: // Pin was never saved locally, therefore there won't be any results. + return Task.forResult(results) + queryTask = uuidTask.onSuccessTask { task: Task -> + val uuid = task.result + val table = OfflineSQLiteOpenHelper.TABLE_OBJECTS + " A " + + " INNER JOIN " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + " B " + + " ON A." + OfflineSQLiteOpenHelper.KEY_UUID + "=B." + OfflineSQLiteOpenHelper.KEY_UUID + val select = arrayOf("A." + OfflineSQLiteOpenHelper.KEY_UUID) + var where = OfflineSQLiteOpenHelper.KEY_CLASS_NAME + "=?" + + " AND " + OfflineSQLiteOpenHelper.KEY_KEY + "=?" + if (!includeIsDeletingEventually) { + where += " AND " + OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY + "=0" + } + val args = arrayOf(query.className(), uuid) + db.queryAsync(table, select, where, args) + } + } + return queryTask.onSuccessTask { task: Task -> + val cursor = task.result + val uuids: MutableList = ArrayList() + cursor.moveToFirst() + while (!cursor.isAfterLast) { + uuids.add(cursor.getString(0)) + cursor.moveToNext() + } + cursor.close() + + // Find objects that match the where clause. + val matcher = queryLogic.createMatcher(query, user) + var checkedAllObjects = Task.forResult(null) + for (uuid in uuids) { + val `object` = Capture() + checkedAllObjects = + checkedAllObjects.onSuccessTask(Continuation> { + getPointerAsync( + uuid, + db + ) + }).onSuccessTask { task15: Task -> + `object`.set(task15.result) + fetchLocallyAsync(`object`.get(), db) + }.onSuccessTask { + if (!`object`.get()!!.isDataAvailable()) { + return@onSuccessTask Task.forResult(false) + } + matcher.matchesAsync(`object`.get(), db) + }.onSuccess { task13: Task -> + if (task13.result) { + results.add(`object`.get()) + } + null + } + } + checkedAllObjects + }.onSuccessTask { task: Task? -> + // Sort by any sort operators. + OfflineQueryLogic.sort(results, query) + + // Apply the skip. + var trimmedResults: List = results + var skip = query.skip() + if (!isCount && skip >= 0) { + skip = Math.min(query.skip(), trimmedResults.size) + trimmedResults = trimmedResults.subList(skip, trimmedResults.size) + } + + // Trim to the limit. + val limit = query.limit() + if (!isCount && limit >= 0 && trimmedResults.size > limit) { + trimmedResults = trimmedResults.subList(0, limit) + } + + // Fetch the includes. + var fetchedIncludesTask = Task.forResult(null) + for (`object` in trimmedResults) { + fetchedIncludesTask = fetchedIncludesTask.onSuccessTask { task12: Task? -> + OfflineQueryLogic.fetchIncludesAsync( + this@OfflineStore, + `object`, + query, + db + ) + } + } + val finalTrimmedResults = trimmedResults + fetchedIncludesTask.onSuccess { task1: Task? -> finalTrimmedResults } + } + } + + /** + * Gets the data for the given object from the offline database. Returns a task that will be + * completed if data for the object was available. If the object is not in the cache, the task + * will be faulted, with a CACHE_MISS error. + * + * @param object The object to fetch. + * @param db A database connection to use. + */ + /* package for OfflineQueryLogic */ + fun fetchLocallyAsync( + `object`: T, + db: ParseSQLiteDatabase + ): Task { + val tcs = TaskCompletionSource() + var uuidTask: Task? + synchronized(lock) { + if (fetchedObjects.containsKey(`object`)) { + /* + * The object has already been fetched from the offline store, so any data that's in there + * is already reflected in the in-memory version. There's nothing more to do. + */ + return fetchedObjects[`object`] as Task + } + + /* + * Put a placeholder so that anyone else who attempts to fetch this object will just wait for + * this call to finish doing it. + */fetchedObjects[`object`] = tcs.task as Task + uuidTask = objectToUuidMap[`object`] + } + val className = `object`.className + val objectId = `object`.objectId + + /* + * If this gets set, then it will contain data from the offline store that needs to be merged + * into the existing object in memory. + */ + var jsonStringTask = Task.forResult(null) + if (objectId == null) { + // This Object has never been saved to Parse. + if (uuidTask == null) { + /* + * This object was not pulled from the data store or previously saved to it, so there's + * nothing that can be fetched from it. This isn't an error, because it's really convenient + * to try to fetch objects from the offline store just to make sure they are up-to-date, and + * we shouldn't force developers to specially handle this case. + */ + } else { + /* + * This object is a new ParseObject that is known to the data store, but hasn't been + * fetched. The only way this could happen is if the object had previously been stored in + * the offline store, then the object was removed from memory (maybe by rebooting), and then + * a object with a pointer to it was fetched, so we only created the pointer. We need to + * pull the data out of the database using the UUID. + */ + val select = arrayOf(OfflineSQLiteOpenHelper.KEY_JSON) + val where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?" + val uuid = Capture() + jsonStringTask = uuidTask!!.onSuccessTask { task: Task -> + uuid.set(task.result) + val args = arrayOf(uuid.get()) + db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args) + }.onSuccess { task: Task -> + val cursor = task.result + cursor.moveToFirst() + if (cursor.isAfterLast) { + cursor.close() + throw IllegalStateException("Attempted to find non-existent uuid " + uuid.get()) + } + val json = cursor.getString(0) + cursor.close() + json + } + } + } else { + if (uuidTask != null) { + /* + * This object is an existing ParseObject, and we must've already pulled its data out of the + * offline store, or else we wouldn't know its UUID. This should never happen. + */ + tcs.setError( + IllegalStateException( + "This object must have already been " + + "fetched from the local datastore, but isn't marked as fetched." + ) + ) + synchronized(lock) { + // Forget we even tried to fetch this object, so that retries will actually... retry. + fetchedObjects.remove(`object`) + } + return tcs.task + } + + /* + * We've got a pointer to an existing ParseObject, but we've never pulled its data out of the + * offline store. Since fetching from the server forces a fetch from the offline store, that + * means this is a pointer. We need to try to find any existing entry for this object in the + * database. + */ + val select = arrayOf(OfflineSQLiteOpenHelper.KEY_JSON, OfflineSQLiteOpenHelper.KEY_UUID) + val where = String.format( + "%s = ? AND %s = ?", OfflineSQLiteOpenHelper.KEY_CLASS_NAME, + OfflineSQLiteOpenHelper.KEY_OBJECT_ID + ) + val args = arrayOf(className, objectId) + jsonStringTask = + db.queryAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, select, where, args) + .onSuccess { task: Task -> + val cursor = task.result + cursor.moveToFirst() + if (cursor.isAfterLast) { + /* + * This is a pointer that came from Parse that references an object that has + * never been saved in the offline store before. This just means there's no data + * in the store that needs to be merged into the object. + */ + cursor.close() + throw ParseException( + ParseException.CACHE_MISS, + "This object is not available in the offline cache." + ) + } + + // we should fetch its data and record its UUID for future reference. + val jsonString = cursor.getString(0) + val newUUID = cursor.getString(1) + cursor.close() + synchronized(lock) { + + /* + * It's okay to put this object into the uuid map. No one will try to fetch + * it, because it's already in the fetchedObjects map. And no one will try to + * save to it without fetching it first, so everything should be just fine. + */objectToUuidMap[`object`] = Task.forResult(newUUID) + uuidToObjectMap.put(newUUID, `object`) + } + jsonString + } + } + return jsonStringTask.onSuccessTask(Continuation { task: Task -> + val jsonString = task.result + ?: /* + * This means we tried to fetch an object from the database that was never actually saved + * locally. This probably means that its parent object was saved locally and we just + * created a pointer to this object. This should be considered a cache miss. + */ + return@Continuation Task.forError( + ParseException( + ParseException.CACHE_MISS, + "Attempted to fetch an object offline which was never saved to the offline cache." + ) + ) + val json: JSONObject = try { + /* + * We can assume that whatever is in the database is the last known server state. The only + * things to maintain from the in-memory object are any changes since the object was last + * put in the database. + */ + JSONObject(jsonString) + } catch (e: JSONException) { + return@Continuation Task.forError(e) + } + + // Fetch all the offline objects before we decode. + val offlineObjects: MutableMap> = HashMap() + object : ParseTraverser() { + override fun visit(object1: Any): Boolean { + if (object1 is JSONObject + && object1.optString("__type") == "OfflineObject" + ) { + val uuid = object1.optString("uuid") + offlineObjects[uuid] = getPointerAsync(uuid, db) + } + return true + } + }.setTraverseParseObjects(false).setYieldRoot(false).traverse(json) + Task.whenAll(offlineObjects.values).onSuccess { task1: Task? -> + `object`.mergeREST(`object`.state, json, OfflineDecoder(offlineObjects)) + null + } + } as Continuation>).continueWithTask { task: Task -> + if (task.isCancelled) { + tcs.setCancelled() + } else if (task.isFaulted) { + tcs.setError(task.error) + } else { + tcs.setResult(`object`) + } + tcs.task + } + } + + /** + * Gets the data for the given object from the offline database. Returns a task that will be + * completed if data for the object was available. If the object is not in the cache, the task + * will be faulted, with a CACHE_MISS error. + * + * @param object The object to fetch. + */ + /* package */ + fun fetchLocallyAsync(`object`: T): Task { + return runWithManagedConnection(object : SQLiteDatabaseCallable> { + override fun call(db: ParseSQLiteDatabase): Task { + return fetchLocallyAsync( + `object`, + db + ) + } + }) + } + + /** + * Stores a single object in the local database. If the object is a pointer, isn't dirty, and has + * an objectId already, it may not be saved, since it would provide no useful data. + * + * @param object The object to save. + * @param db A database connection to use. + */ + private fun saveLocallyAsync( + key: String, `object`: ParseObject, db: ParseSQLiteDatabase + ): Task { + // If this is just a clean, unfetched pointer known to Parse, then there is nothing to save. + if (`object`.objectId != null && !`object`.isDataAvailable() && !`object`.hasChanges() + && !`object`.hasOutstandingOperations() + ) { + return Task.forResult(null) + } + val uuidCapture = Capture() + + // Make sure we have a UUID for the object to be saved. + return getOrCreateUUIDAsync(`object`, db).onSuccessTask { task: Task -> + val uuid = task.result + uuidCapture.set(uuid) + updateDataForObjectAsync(uuid, `object`, db) + }.onSuccessTask { task: Task? -> + val values = ContentValues() + values.put(OfflineSQLiteOpenHelper.KEY_KEY, key) + values.put(OfflineSQLiteOpenHelper.KEY_UUID, uuidCapture.get()) + db.insertWithOnConflict( + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, values, + SQLiteDatabase.CONFLICT_IGNORE + ) + } + } + + /** + * Stores an object (and optionally, every object it points to recursively) in the local database. + * If any of the objects have not been fetched from Parse, they will not be stored. However, if + * they have changed data, the data will be retained. To get the objects back later, you can use a + * ParseQuery with a cache policy that uses the local cache, or you can create an unfetched + * pointer with ParseObject.createWithoutData() and then call fetchFromLocalDatastore() on it. If you modify + * the object after saving it locally, such as by fetching it or saving it, those changes will + * automatically be applied to the cache. + * + * + * Any objects previously stored with the same key will be removed from the local database. + * + * @param object Root object + * @param includeAllChildren `true` to recursively save all pointers. + * @param db DB connection + * @return A Task that will be resolved when saving is complete + */ + private fun saveLocallyAsync( + `object`: ParseObject, includeAllChildren: Boolean, db: ParseSQLiteDatabase + ): Task { + val objectsInTree = ArrayList() + // Fetch all objects locally in case they are being re-added + if (!includeAllChildren) { + objectsInTree.add(`object`) + } else { + object : ParseTraverser() { + override fun visit(`object`: Any): Boolean { + if (`object` is ParseObject) { + objectsInTree.add(`object`) + } + return true + } + }.setYieldRoot(true).setTraverseParseObjects(true).traverse(`object`) + } + return saveLocallyAsync(`object`, objectsInTree, db) + } + + private fun saveLocallyAsync( + `object`: ParseObject, children: List?, db: ParseSQLiteDatabase + ): Task { + val objects: MutableList = + if (children != null) ArrayList(children) else ArrayList() + if (!objects.contains(`object`)) { + objects.add(`object`) + } + + // Call saveLocallyAsync for each of them individually. + val tasks: MutableList> = ArrayList() + for (obj in objects) { + tasks.add(fetchLocallyAsync(obj, db).makeVoid()) + } + return Task.whenAll(tasks) + .continueWithTask { task: Task? -> objectToUuidMap[`object`] } + .onSuccessTask { task: Task -> + val uuid = task.result + ?: // The root object was never stored in the offline store, so nothing to unpin. + return@onSuccessTask null + unpinAsync(uuid, db) + }.onSuccessTask { task: Task? -> getOrCreateUUIDAsync(`object`, db) } + .onSuccessTask { task: Task -> + val uuid = task.result + + // Call saveLocallyAsync for each of them individually. + val tasks1: MutableList> = ArrayList() + for (obj in objects) { + tasks1.add(saveLocallyAsync(uuid, obj, db)) + } + Task.whenAll(tasks1) + } + } + + private fun unpinAsync(`object`: ParseObject, db: ParseSQLiteDatabase): Task { + val uuidTask = objectToUuidMap[`object`] + ?: // The root object was never stored in the offline store, so nothing to unpin. + return Task.forResult(null) + return uuidTask.continueWithTask { task: Task -> + val uuid = task.result + ?: // The root object was never stored in the offline store, so nothing to unpin. + return@continueWithTask Task.forResult(null) + unpinAsync(uuid, db) + } + } + + private fun unpinAsync(key: String, db: ParseSQLiteDatabase): Task { + val uuidsToDelete: MutableList = LinkedList() + // A continueWithTask that ends with "return task" is essentially a try-finally. + return Task.forResult(null as Void?).continueWithTask { task: Task? -> + // Fetch all uuids from Dependencies for key=? grouped by uuid having a count of 1 + val sql = + "SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + + " WHERE " + OfflineSQLiteOpenHelper.KEY_KEY + "=? AND " + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + + " SELECT " + OfflineSQLiteOpenHelper.KEY_UUID + " FROM " + OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES + + " GROUP BY " + OfflineSQLiteOpenHelper.KEY_UUID + + " HAVING COUNT(" + OfflineSQLiteOpenHelper.KEY_UUID + ")=1" + + ")" + val args = arrayOf(key) + db.rawQueryAsync(sql, args) + }.onSuccessTask { task: Task -> + // DELETE FROM Objects + val cursor = task.result + while (cursor.moveToNext()) { + uuidsToDelete.add(cursor.getString(0)) + } + cursor.close() + deleteObjects(uuidsToDelete, db) + }.onSuccessTask { task: Task? -> + // DELETE FROM Dependencies + val where = OfflineSQLiteOpenHelper.KEY_KEY + "=?" + val args = arrayOf(key) + db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args) + }.onSuccess { task: Task? -> + synchronized(lock) { + // Remove uuids from memory + for (uuid in uuidsToDelete) { + val `object` = uuidToObjectMap[uuid] + if (`object` != null) { + objectToUuidMap.remove(`object`) + uuidToObjectMap.remove(uuid) + } + } + } + null + } + } + + private fun deleteObjects(uuids: List, db: ParseSQLiteDatabase): Task { + if (uuids.size <= 0) { + return Task.forResult(null) + } + + // SQLite has a max 999 SQL variables in a statement, so we need to split it up into manageable + // chunks. We can do this because we're already in a transaction. + if (uuids.size > MAX_SQL_VARIABLES) { + return deleteObjects( + uuids.subList(0, MAX_SQL_VARIABLES), + db + ).onSuccessTask { task: Task? -> + deleteObjects( + uuids.subList( + MAX_SQL_VARIABLES, uuids.size + ), db + ) + } + } + val placeholders = arrayOfNulls(uuids.size) + for (i in placeholders.indices) { + placeholders[i] = "?" + } + val where = + OfflineSQLiteOpenHelper.KEY_UUID + " IN (" + TextUtils.join(",", placeholders) + ")" + // dynamic args + val args = uuids.toTypedArray() + return db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args) + } + + /** + * Takes an object that has been fetched from the database before and updates it with whatever + * data is in memory. This will only be used when data comes back from the server after a fetch or + * a save. + */ + /* package */ + fun updateDataForObjectAsync(`object`: ParseObject): Task { + var fetched: Task? + // Make sure the object is fetched. + synchronized(lock) { + fetched = fetchedObjects[`object`] + if (fetched == null) { + return Task.forError( + IllegalStateException( + "An object cannot be updated if it wasn't fetched." + ) + ) + } + } + return fetched!!.continueWithTask { task: Task -> + if (task.isFaulted) { + // Catch CACHE_MISS + if (task.error is ParseException + && (task.error as ParseException).code == ParseException.CACHE_MISS + ) { + return@continueWithTask Task.forResult(null) + } + return@continueWithTask task.makeVoid() + } + helper.writableDatabaseAsync.continueWithTask { task14: Task -> + val db = task14.result + db.beginTransactionAsync().onSuccessTask { task13: Task? -> + updateDataForObjectAsync( + `object`, + db + ).onSuccessTask { task12: Task? -> db.setTransactionSuccessfulAsync() } + .continueWithTask { task1: Task? -> + db.endTransactionAsync() + db.closeAsync() + task1 + } + } + } + } + } + + private fun updateDataForObjectAsync( + `object`: ParseObject, + db: ParseSQLiteDatabase + ): Task { + // Make sure the object has a UUID. + var uuidTask: Task? + synchronized(lock) { + uuidTask = objectToUuidMap[`object`] + if (uuidTask == null) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return Task.forResult(null) + } + } + return uuidTask!!.onSuccessTask { task: Task -> + val uuid = task.result + updateDataForObjectAsync(uuid, `object`, db) + } + } + + private fun updateDataForObjectAsync( + uuid: String, + `object`: ParseObject, + db: ParseSQLiteDatabase + ): Task { + // Now actually encode the object as JSON. + val encoder = OfflineEncoder(db) + val json = `object`.toRest(encoder) + return encoder.whenFinished().onSuccessTask { task: Task? -> + // Put the JSON in the database. + val className = `object`.className + val objectId = `object`.objectId + val isDeletingEventually = json.getInt(ParseObject.KEY_IS_DELETING_EVENTUALLY) + val values = ContentValues() + values.put(OfflineSQLiteOpenHelper.KEY_CLASS_NAME, className) + values.put(OfflineSQLiteOpenHelper.KEY_JSON, json.toString()) + if (objectId != null) { + values.put(OfflineSQLiteOpenHelper.KEY_OBJECT_ID, objectId) + } + values.put(OfflineSQLiteOpenHelper.KEY_IS_DELETING_EVENTUALLY, isDeletingEventually) + val where = OfflineSQLiteOpenHelper.KEY_UUID + " = ?" + val args = arrayOf(uuid) + db.updateAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, values, where, args).makeVoid() + } + } + + /* package */ + fun deleteDataForObjectAsync(`object`: ParseObject): Task { + return helper.writableDatabaseAsync.continueWithTask { task: Task -> + val db = task.result + db.beginTransactionAsync().onSuccessTask { task13: Task? -> + deleteDataForObjectAsync( + `object`, + db + ).onSuccessTask { task12: Task? -> db.setTransactionSuccessfulAsync() } + .continueWithTask { task1: Task? -> + db.endTransactionAsync() + db.closeAsync() + task1 + } + } + } + } + + private fun deleteDataForObjectAsync( + `object`: ParseObject, + db: ParseSQLiteDatabase + ): Task { + val uuid = Capture() + + // Make sure the object has a UUID. + var uuidTask: Task? + synchronized(lock) { + uuidTask = objectToUuidMap[`object`] + if (uuidTask == null) { + // It was fetched, but it has no UUID. That must mean it isn't actually in the database. + return Task.forResult(null) + } + } + uuidTask = uuidTask!!.onSuccessTask { task: Task -> + uuid.set(task.result) + task + } + + // If the object was the root of a pin, unpin it. + val unpinTask = uuidTask!!.onSuccessTask { + // Find all the roots for this object. + val select = arrayOf(OfflineSQLiteOpenHelper.KEY_KEY) + val where = OfflineSQLiteOpenHelper.KEY_UUID + "=?" + val args = arrayOf(uuid.get()) + db.queryAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, select, where, args) + }.onSuccessTask { task: Task -> + // Try to unpin this object from the pin label if it's a root of the ParsePin. + val cursor = task.result + val uuids: MutableList = ArrayList() + cursor.moveToFirst() + while (!cursor.isAfterLast) { + uuids.add(cursor.getString(0)) + cursor.moveToNext() + } + cursor.close() + val tasks: MutableList> = ArrayList() + for (uuid1 in uuids) { + val unpinTask1 = getPointerAsync( + uuid1, + db + ).onSuccessTask { task12: Task -> + val pin = task12.result as ParsePin + fetchLocallyAsync(pin, db) + }.continueWithTask { task1: Task -> + val pin = task1.result + val modified = pin.objects + if (modified == null || !modified.contains(`object`)) { + return@continueWithTask task1.makeVoid() + } + modified.remove(`object`) + if (modified.size == 0) { + return@continueWithTask unpinAsync(uuid1, db) + } + pin.objects = modified + saveLocallyAsync(pin, true, db) + } + tasks.add(unpinTask1) + } + Task.whenAll(tasks) + } + + // Delete the object from the Local Datastore in case it wasn't the root of a pin. + return unpinTask.onSuccessTask { task: Task? -> + val where = OfflineSQLiteOpenHelper.KEY_UUID + "=?" + val args = arrayOf(uuid.get()) + db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_DEPENDENCIES, where, args) + }.onSuccessTask { task: Task? -> + val where = OfflineSQLiteOpenHelper.KEY_UUID + "=?" + val args = arrayOf(uuid.get()) + db.deleteAsync(OfflineSQLiteOpenHelper.TABLE_OBJECTS, where, args) + }.onSuccessTask { task: Task? -> + synchronized(lock) { + // Clean up + //TODO (grantland): we should probably clean up uuidToObjectMap and objectToUuidMap, but + // getting the uuid requires a task and things might get a little funky... + fetchedObjects.remove(`object`) + } + task + } + } + + private fun getParsePin(name: String, db: ParseSQLiteDatabase): Task { + val query = ParseQuery.State.Builder( + ParsePin::class.java + ) + .whereEqualTo(ParsePin.KEY_NAME, name) + .build() + + /* We need to call directly to the OfflineStore since we don't want/need a user to query for + * ParsePins + */return findAsync(query, null, null, db).onSuccess { task: Task?> -> + var pin: ParsePin? = null + if (task.result != null && task.result!!.isNotEmpty()) { + pin = task.result!![0] + } + + //TODO (grantland): What do we do if there are more than 1 result? + if (pin == null) { + pin = create(ParsePin::class.java) + pin.name = name + } + pin + } + } + + //region ParsePin + /* package */ + fun pinAllObjectsAsync( + name: String, + objects: List, + includeChildren: Boolean + ): Task { + return runWithManagedTransaction(object : SQLiteDatabaseCallable> { + override fun call(db: ParseSQLiteDatabase): Task { + return pinAllObjectsAsync( + name, + objects, + includeChildren, + db + ) + } + }) + } + + private fun pinAllObjectsAsync( + name: String, + objects: List?, + includeChildren: Boolean, + db: ParseSQLiteDatabase + ): Task { + return if (objects == null || objects.isEmpty()) { + Task.forResult(null) + } else getParsePin(name, db).onSuccessTask { task: Task -> + val pin = task.result + + //TODO (grantland): change to use relations. currently the related PO are only getting saved + // offline as pointers. +// ParseRelation relation = pin.getRelation(KEY_OBJECTS); +// relation.add(object); + + // Hack to store collections in a pin + var modified = pin.objects + if (modified == null) { + modified = ArrayList(objects) + } else { + for (`object` in objects) { + if (!modified.contains(`object`)) { + modified.add(`object`) + } + } + } + pin.objects = modified + if (includeChildren) { + return@onSuccessTask saveLocallyAsync(pin, true, db) + } + saveLocallyAsync(pin, pin.objects, db) + } + } + + /* package */ + fun unpinAllObjectsAsync( + name: String, + objects: List + ): Task { + return runWithManagedTransaction(object : SQLiteDatabaseCallable> { + override fun call(db: ParseSQLiteDatabase): Task { + return unpinAllObjectsAsync( + name, + objects, + db + ) + } + }) + } + + private fun unpinAllObjectsAsync( + name: String, + objects: List?, + db: ParseSQLiteDatabase + ): Task { + return if (objects == null || objects.isEmpty()) { + Task.forResult(null) + } else getParsePin(name, db).onSuccessTask { task: Task -> + val pin = task.result + + //TODO (grantland): change to use relations. currently the related PO are only getting saved + // offline as pointers. +// ParseRelation relation = pin.getRelation(KEY_OBJECTS); +// relation.remove(object); + + // Hack to store collections in a pin + val modified = pin.objects + ?: // Unpin a pin that doesn't exist. Wat? + return@onSuccessTask Task.forResult(null) + modified.removeAll(objects) + if (modified.size == 0) { + return@onSuccessTask unpinAsync(pin, db) + } + pin.objects = modified + saveLocallyAsync(pin, true, db) + } + } + + /* package */ + fun unpinAllObjectsAsync(name: String): Task { + return runWithManagedTransaction(object : SQLiteDatabaseCallable> { + override fun call(db: ParseSQLiteDatabase): Task { + return unpinAllObjectsAsync( + name, + db + ) + } + }) + } + + private fun unpinAllObjectsAsync(name: String, db: ParseSQLiteDatabase): Task { + return getParsePin(name, db).continueWithTask { task: Task -> + if (task.isFaulted) { + return@continueWithTask task.makeVoid() + } + val pin = task.result + unpinAsync(pin, db) + } + } + + /* package */ + open fun findFromPinAsync( + name: String?, + state: ParseQuery.State, + user: ParseUser? + ): Task>? { + return runWithManagedConnection(object : SQLiteDatabaseCallable>> { + override fun call(db: ParseSQLiteDatabase): Task> { + return findFromPinAsync( + name, + state, + user, + db + ) + } + }) + } + + private fun findFromPinAsync( + name: String?, + state: ParseQuery.State, + user: ParseUser?, + db: ParseSQLiteDatabase + ): Task> { + val task: Task = name?.let { getParsePin(it, db) } ?: Task.forResult(null) + return task.onSuccessTask { task1: Task -> + val pin = task1.result + findAsync(state, user, pin, false, db) + } + } + + /* package */ + open fun countFromPinAsync( + name: String?, + state: ParseQuery.State, + user: ParseUser? + ): Task? { + return runWithManagedConnection(object : SQLiteDatabaseCallable> { + override fun call(db: ParseSQLiteDatabase): Task { + return countFromPinAsync( + name, + state, + user, + db + ) + } + }) + } + + private fun countFromPinAsync( + name: String?, + state: ParseQuery.State, + user: ParseUser?, + db: ParseSQLiteDatabase + ): Task { + val task: Task = name?.let { getParsePin(it, db) } ?: Task.forResult(null) + return task.onSuccessTask { task12: Task -> + val pin = task12.result + findAsync( + state, + user, + pin, + true, + db + ).onSuccess { task1: Task?> -> task1.result!!.size } + } + } + + /** + * This should be called by the ParseObject constructor notify the store that there is an object + * with this className and objectId. + */ + /* package */ + fun registerNewObject(`object`: ParseObject) { + synchronized(lock) { + val objectId = `object`.objectId + if (objectId != null) { + val className = `object`.className + val classNameAndObjectId = Pair.create(className, objectId) + classNameAndObjectIdToObjectMap.put(classNameAndObjectId, `object`) + } + } + } + + //endregion + //region Single Instance + /* package */ + fun unregisterObject(`object`: ParseObject) { + synchronized(lock) { + val objectId = `object`.objectId + if (objectId != null) { + classNameAndObjectIdToObjectMap.remove(Pair.create(`object`.className, objectId)) + } + } + } + + /** + * This should only ever be called from ParseObject.createWithoutData(). + * + * @return a pair of ParseObject and Boolean. The ParseObject is the object. The Boolean is true + * iff the object was newly created. + */ + /* package */ + fun getObject(className: String?, objectId: String?): ParseObject? { + checkNotNull(objectId) { "objectId cannot be null." } + val classNameAndObjectId = Pair.create(className, objectId) + // This lock should never be held by anyone doing disk or database access. + synchronized(lock) { return classNameAndObjectIdToObjectMap[classNameAndObjectId] } + } + + /** + * When an object is finished saving, it gets an objectId. Then it should call this method to + * clean up the bookeeping around ids. + */ + /* package */ + fun updateObjectId(`object`: ParseObject, oldObjectId: String?, newObjectId: String?) { + if (oldObjectId != null) { + if (oldObjectId == newObjectId) { + return + } + /* + * Special case for re-saving installation if it was deleted on the server + * @see ParseInstallation#saveAsync(String, Task) + */if (`object` is ParseInstallation + && newObjectId == null + ) { + synchronized(lock) { + classNameAndObjectIdToObjectMap.remove( + Pair.create( + `object`.className, + oldObjectId + ) + ) + } + return + } else { + throw RuntimeException("objectIds cannot be changed in offline mode.") + } + } + val className = `object`.className + val classNameAndNewObjectId = Pair.create(className, newObjectId) + synchronized(lock) { + + // See if there's already an entry for the new object id. + val existing = classNameAndObjectIdToObjectMap[classNameAndNewObjectId] + if (existing != null && existing !== `object`) { + throw RuntimeException( + "Attempted to change an objectId to one that's " + + "already known to the Offline Store." + ) + } + + // Okay, all clear to add the new reference. + classNameAndObjectIdToObjectMap.put(classNameAndNewObjectId, `object`) + } + } + + /** + * Wraps SQLite operations with a managed SQLite connection. + */ + private fun runWithManagedConnection(callable: SQLiteDatabaseCallable>): Task { + return helper.writableDatabaseAsync.onSuccessTask { task: Task -> + val db = task.result + callable.call(db).continueWithTask { task1: Task? -> + db.closeAsync() + task1 + } + } + } + + /** + * Wraps SQLite operations with a managed SQLite connection and transaction. + */ + private fun runWithManagedTransaction(callable: SQLiteDatabaseCallable>): Task { + return helper.writableDatabaseAsync.onSuccessTask { task: Task -> + val db = task.result + db.beginTransactionAsync().onSuccessTask { + callable.call(db) + .onSuccessTask { db.setTransactionSuccessfulAsync() } + .continueWithTask { task1: Task? -> + db.endTransactionAsync() + db.closeAsync() + task1 + } + } + } + } + //endregion + /** + * Clears all in-memory caches so that data must be retrieved from disk. + */ + fun simulateReboot() { + synchronized(lock) { + uuidToObjectMap.clear() + objectToUuidMap.clear() + classNameAndObjectIdToObjectMap.clear() + fetchedObjects.clear() + } + } + + /** + * Clears the database on disk. + */ + fun clearDatabase(context: Context?) { + helper.clearDatabase(context) + } + + private interface SQLiteDatabaseCallable { + fun call(db: ParseSQLiteDatabase): T + } + /* + * Methods for testing. + */ + /** + * Extends the normal JSON -> ParseObject decoding to also deal with placeholders for new objects + * that have been saved offline. + */ + private class OfflineDecoder( // A map of UUID -> Task that will be finished once the given ParseObject is loaded. + // The Tasks should all be finished before decode is called. + private val offlineObjects: Map> + ) : ParseDecoder() { + override fun decode(`object`: Any): Any? { + // If we see an offline id, make sure to decode it. + if (`object` is JSONObject + && `object`.optString("__type") == "OfflineObject" + ) { + val uuid = `object`.optString("uuid") + return offlineObjects[uuid]!!.result + } + + /* + * Embedded objects can't show up here, because we never stored them that way offline. + */return super.decode(`object`) + } + } + + /** + * An encoder that can encode objects that are available offline. After using this encoder, you + * must call whenFinished() and wait for its result to be finished before the results of the + * encoding will be valid. + */ + private inner class OfflineEncoder + /** + * Creates an encoder. + * + * @param db A database connection to use. + */(private val db: ParseSQLiteDatabase) : ParseEncoder() { + private val tasksLock = Any() + private val tasks = ArrayList>() + + /** + * The results of encoding an object with this encoder will not be valid until the task returned + * by this method is finished. + */ + fun whenFinished(): Task { + return Task.whenAll(tasks).continueWithTask { ignore: Task? -> + synchronized(tasksLock) { + + // It might be better to return an aggregate error here. + for (task in tasks) { + if (task.isFaulted || task.isCancelled) { + return@continueWithTask task + } + } + tasks.clear() + return@continueWithTask Task.forResult(null) + } + } + } + + /** + * Implements an encoding strategy for Parse Objects that uses offline ids when necessary. + */ + override fun encodeRelatedObject(`object`: ParseObject): JSONObject { + return try { + if (`object`.objectId != null) { + val result = JSONObject() + result.put("__type", "Pointer") + result.put("objectId", `object`.objectId) + result.put("className", `object`.className) + return result + } + val result = JSONObject() + result.put("__type", "OfflineObject") + synchronized(tasksLock) { + tasks.add(getOrCreateUUIDAsync(`object`, db).onSuccess { task: Task -> + result.put("uuid", task.result) + null + }) + } + result + } catch (e: JSONException) { + // This can literally never happen. + throw RuntimeException(e) + } + } + } + + companion object { + /** + * SQLite has a max of 999 SQL variables in a single statement. + */ + private const val MAX_SQL_VARIABLES = 999 + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/PLog.java b/parse/src/main/java/com/parse/PLog.java deleted file mode 100644 index 697079225..000000000 --- a/parse/src/main/java/com/parse/PLog.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 android.annotation.SuppressLint; -import android.util.Log; - -/** - * Parse Logger. See {@link #setLogLevel(int)} - */ -@SuppressWarnings("WeakerAccess") -public class PLog { - - @SuppressWarnings("unused") - public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE; - - private static int logLevel = Integer.MAX_VALUE; - - /** - * Returns the level of logging that will be displayed. - */ - public static int getLogLevel() { - return logLevel; - } - - /** - * Sets the level of logging to display, where each level includes all those below it. The default - * level is {@link #LOG_LEVEL_NONE}. Please ensure this is set to {@link Log#ERROR} - * or {@link #LOG_LEVEL_NONE} before deploying your app to ensure no sensitive information is - * logged. The levels are: - *

    - *
  • {@link Log#VERBOSE}
  • - *
  • {@link Log#DEBUG}
  • - *
  • {@link Log#INFO}
  • - *
  • {@link Log#WARN}
  • - *
  • {@link Log#ERROR}
  • - *
  • {@link #LOG_LEVEL_NONE}
  • - *
- * - * @param logLevel The level of logcat logging that Parse should do. - */ - public static void setLogLevel(int logLevel) { - PLog.logLevel = logLevel; - } - - @SuppressLint("WrongConstant") - private static void log(int messageLogLevel, String tag, String message, Throwable tr) { - if (messageLogLevel >= logLevel) { - if (tr == null) { - Log.println(logLevel, tag, message); - } else { - Log.println(logLevel, tag, message + '\n' + Log.getStackTraceString(tr)); - } - } - } - - public static void v(String tag, String message, Throwable tr) { - log(Log.VERBOSE, tag, message, tr); - } - - public static void v(String tag, String message) { - v(tag, message, null); - } - - public static void d(String tag, String message, Throwable tr) { - log(Log.DEBUG, tag, message, tr); - } - - public static void d(String tag, String message) { - d(tag, message, null); - } - - public static void i(String tag, String message, Throwable tr) { - log(Log.INFO, tag, message, tr); - } - - public static void i(String tag, String message) { - i(tag, message, null); - } - - public static void w(String tag, String message, Throwable tr) { - log(Log.WARN, tag, message, tr); - } - - public static void w(String tag, String message) { - w(tag, message, null); - } - - public static void e(String tag, String message, Throwable tr) { - log(Log.ERROR, tag, message, tr); - } - - public static void e(String tag, String message) { - e(tag, message, null); - } -} diff --git a/parse/src/main/java/com/parse/PLog.kt b/parse/src/main/java/com/parse/PLog.kt new file mode 100644 index 000000000..39be5bdfb --- /dev/null +++ b/parse/src/main/java/com/parse/PLog.kt @@ -0,0 +1,82 @@ +/* + * 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 android.annotation.SuppressLint +import android.util.Log + +/** + * Parse Logger. See [.setLogLevel] + */ +object PLog { + const val LOG_LEVEL_NONE = Int.MAX_VALUE + /** + * Returns the level of logging that will be displayed. + */ + /** + * Sets the level of logging to display, where each level includes all those below it. The default + * level is [.LOG_LEVEL_NONE]. Please ensure this is set to [Log.ERROR] + * or [.LOG_LEVEL_NONE] before deploying your app to ensure no sensitive information is + * logged. The levels are: + * + * * [Log.VERBOSE] + * * [Log.DEBUG] + * * [Log.INFO] + * * [Log.WARN] + * * [Log.ERROR] + * * [.LOG_LEVEL_NONE] + * + * + * @param logLevel The level of logcat logging that Parse should do. + */ + @JvmStatic + var logLevel = Int.MAX_VALUE + @SuppressLint("WrongConstant") + private fun log(messageLogLevel: Int, tag: String, message: String, tr: Throwable?) { + if (messageLogLevel >= logLevel) { + if (tr == null) { + Log.println(logLevel, tag, message) + } else { + Log.println( + logLevel, tag, """ + $message + ${Log.getStackTraceString(tr)} + """.trimIndent() + ) + } + } + } + + @JvmOverloads + fun v(tag: String, message: String, tr: Throwable? = null) { + log(Log.VERBOSE, tag, message, tr) + } + + @JvmOverloads + fun d(tag: String, message: String, tr: Throwable? = null) { + log(Log.DEBUG, tag, message, tr) + } + + @JvmOverloads + fun i(tag: String, message: String, tr: Throwable? = null) { + log(Log.INFO, tag, message, tr) + } + + @JvmStatic + @JvmOverloads + fun w(tag: String, message: String, tr: Throwable? = null) { + log(Log.WARN, tag, message, tr) + } + + @JvmStatic + @JvmOverloads + fun e(tag: String, message: String, tr: Throwable? = null) { + log(Log.ERROR, tag, message, tr) + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/Parse.java b/parse/src/main/java/com/parse/Parse.java index 9525c711d..226616f04 100644 --- a/parse/src/main/java/com/parse/Parse.java +++ b/parse/src/main/java/com/parse/Parse.java @@ -11,10 +11,13 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Log; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; import java.io.File; import java.io.FileOutputStream; @@ -22,13 +25,13 @@ import java.io.RandomAccessFile; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; import okhttp3.OkHttpClient; /** @@ -159,12 +162,9 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) { // application. checkCacheApplicationId(); final Context context = configuration.context; - Task.callInBackground(new Callable() { - @Override - public Void call() { - getEventuallyQueue(context); - return null; - } + Task.callInBackground((Callable) () -> { + getEventuallyQueue(context); + return null; }); ParseFieldOperations.registerDefaultDecoders(); @@ -176,13 +176,10 @@ public Void call() { "com.parse.push.intent.OPEN, com.parse.push.intent.DELETE"); } - ParseUser.getCurrentUserAsync().makeVoid().continueWith(new Continuation() { - @Override - public Void then(Task task) { - // Prime config in the background - ParseConfig.getCurrentConfig(); - return null; - } + ParseUser.getCurrentUserAsync().makeVoid().continueWith((Continuation) task -> { + // Prime config in the background + ParseConfig.getCurrentConfig(); + return null; }, Task.BACKGROUND_EXECUTOR); dispatchOnParseInitialized(); @@ -195,6 +192,15 @@ public Void then(Task task) { //region Server URL + /** + * Returns the current server URL. + */ + public static @Nullable + String getServer() { + URL server = ParseRESTCommand.server; + return server == null ? null : server.toString(); + } + /** * Sets the server URL. The local client cache is not cleared. *

@@ -206,6 +212,7 @@ public Void then(Task task) { * The new server URL must point to a Parse Server that connects to the same database. * Otherwise, issues may arise related to locally cached data or delayed methods such as * {@link ParseObject#saveEventually()}. + * * @param server The server URL to set. */ public static void setServer(@NonNull String server) { @@ -216,20 +223,14 @@ public static void setServer(@NonNull String server) { } } - /** - * Returns the current server URL. - */ - public static @Nullable String getServer() { - URL server = ParseRESTCommand.server; - return server == null ? null : server.toString(); - } - /** * Validates the server URL. + * * @param server The server URL to validate. * @return The validated server URL. */ - private static @Nullable String validateServerUrl(@Nullable String server) { + private static @Nullable + String validateServerUrl(@Nullable String server) { // Add an extra trailing slash so that Parse REST commands include // the path as part of the server URL (i.e. http://api.myhost.com/parse) @@ -359,7 +360,7 @@ static void checkCacheApplicationId() { byte[] bytes = new byte[(int) f.length()]; f.readFully(bytes); f.close(); - String diskApplicationId = new String(bytes, "UTF-8"); + String diskApplicationId = new String(bytes, Charset.forName("UTF-8")); matches = diskApplicationId.equals(applicationId); } catch (IOException e) { // Hmm, the applicationId file was malformed or something. Assume it @@ -380,7 +381,7 @@ static void checkCacheApplicationId() { applicationIdFile = new File(dir, "applicationId"); try { FileOutputStream out = new FileOutputStream(applicationIdFile); - out.write(applicationId.getBytes("UTF-8")); + out.write(applicationId.getBytes(Charset.forName("UTF-8"))); out.close(); } catch (IOException e) { // Nothing we can really do about it. @@ -566,6 +567,7 @@ public static final class Configuration { final boolean localDataStoreEnabled; final OkHttpClient.Builder clientBuilder; final int maxRetries; + private Configuration(Builder builder) { this.context = builder.context; this.applicationId = builder.applicationId; @@ -580,7 +582,7 @@ private Configuration(Builder builder) { * Allows for simple constructing of a {@code Configuration} object. */ public static final class Builder { - private Context context; + private final Context context; private String applicationId; private String clientKey; private String server; diff --git a/parse/src/main/java/com/parse/ParseACL.java b/parse/src/main/java/com/parse/ParseACL.java index b68d48a1b..74420a0bf 100644 --- a/parse/src/main/java/com/parse/ParseACL.java +++ b/parse/src/main/java/com/parse/ParseACL.java @@ -57,6 +57,7 @@ public ParseACL[] newArray(int size) { public ParseACL() { // do nothing } + /** * Creates a copy of {@code acl}. * diff --git a/parse/src/main/java/com/parse/ParseAnalytics.java b/parse/src/main/java/com/parse/ParseAnalytics.java index b153b46c7..04d76ca2f 100644 --- a/parse/src/main/java/com/parse/ParseAnalytics.java +++ b/parse/src/main/java/com/parse/ParseAnalytics.java @@ -10,6 +10,9 @@ import android.content.Intent; +import com.parse.boltsinternal.Capture; +import com.parse.boltsinternal.Task; + import org.json.JSONException; import org.json.JSONObject; @@ -18,10 +21,6 @@ import java.util.LinkedHashMap; import java.util.Map; -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The {@code ParseAnalytics} class provides an interface to Parse's logging and analytics backend. * Methods will return immediately and cache requests (+ timestamps) to be handled "eventually." @@ -66,12 +65,9 @@ public static Task trackAppOpenedInBackground(Intent intent) { } } } - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - return getAnalyticsController().trackAppOpenedInBackground(pushHash.get(), sessionToken); - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + String sessionToken = task.getResult(); + return getAnalyticsController().trackAppOpenedInBackground(pushHash.get(), sessionToken); }); } @@ -171,12 +167,9 @@ public static Task trackEventInBackground(final String name, ? Collections.unmodifiableMap(new HashMap<>(dimensions)) : null; - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - return getAnalyticsController().trackEventInBackground(name, dimensionsCopy, sessionToken); - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + String sessionToken = task.getResult(); + return getAnalyticsController().trackEventInBackground(name, dimensionsCopy, sessionToken); }); } diff --git a/parse/src/main/java/com/parse/ParseAnalyticsController.java b/parse/src/main/java/com/parse/ParseAnalyticsController.java index 61c180a23..5533779e7 100644 --- a/parse/src/main/java/com/parse/ParseAnalyticsController.java +++ b/parse/src/main/java/com/parse/ParseAnalyticsController.java @@ -8,15 +8,15 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import java.util.Map; -import com.parse.boltsinternal.Task; - class ParseAnalyticsController { - /* package for test */ ParseEventuallyQueue eventuallyQueue; + /* package for test */ final ParseEventuallyQueue eventuallyQueue; public ParseAnalyticsController(ParseEventuallyQueue eventuallyQueue) { this.eventuallyQueue = eventuallyQueue; diff --git a/parse/src/main/java/com/parse/ParseAnonymousUtils.java b/parse/src/main/java/com/parse/ParseAnonymousUtils.java index 10be8cc42..6634bbb73 100644 --- a/parse/src/main/java/com/parse/ParseAnonymousUtils.java +++ b/parse/src/main/java/com/parse/ParseAnonymousUtils.java @@ -8,12 +8,12 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import java.util.HashMap; import java.util.Map; import java.util.UUID; -import com.parse.boltsinternal.Task; - /** * Provides utility functions for working with Anonymously logged-in users. Anonymous users have * some unique characteristics: diff --git a/parse/src/main/java/com/parse/ParseAuthenticationManager.java b/parse/src/main/java/com/parse/ParseAuthenticationManager.java index f145a148f..9b0dc1ba7 100644 --- a/parse/src/main/java/com/parse/ParseAuthenticationManager.java +++ b/parse/src/main/java/com/parse/ParseAuthenticationManager.java @@ -8,12 +8,10 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; class ParseAuthenticationManager { @@ -44,15 +42,12 @@ public void register(final String authType, AuthenticationCallback callback) { } // Synchronize the current user with the auth callback. - controller.getAsync(false).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser user = task.getResult(); - if (user != null) { - return user.synchronizeAuthDataAsync(authType); - } - return null; + controller.getAsync(false).onSuccessTask(task -> { + ParseUser user = task.getResult(); + if (user != null) { + return user.synchronizeAuthDataAsync(authType); } + return null; }); } @@ -64,12 +59,7 @@ public Task restoreAuthenticationAsync(String authType, final Map() { - @Override - public Boolean call() { - return callback.onRestore(authData); - } - }, ParseExecutors.io()); + return Task.call(() -> callback.onRestore(authData), ParseExecutors.io()); } public Task deauthenticateAsync(String authType) { @@ -78,12 +68,9 @@ public Task deauthenticateAsync(String authType) { callback = this.callbacks.get(authType); } if (callback != null) { - return Task.call(new Callable() { - @Override - public Void call() { - callback.onRestore(null); - return null; - } + return Task.call(() -> { + callback.onRestore(null); + return null; }, ParseExecutors.io()); } return Task.forResult(null); diff --git a/parse/src/main/java/com/parse/ParseByteArrayHttpBody.java b/parse/src/main/java/com/parse/ParseByteArrayHttpBody.java index 60c68247c..e6bb5dbc1 100644 --- a/parse/src/main/java/com/parse/ParseByteArrayHttpBody.java +++ b/parse/src/main/java/com/parse/ParseByteArrayHttpBody.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; class ParseByteArrayHttpBody extends ParseHttpBody { /* package */ final byte[] content; @@ -22,7 +23,7 @@ class ParseByteArrayHttpBody extends ParseHttpBody { public ParseByteArrayHttpBody(String content, String contentType) throws UnsupportedEncodingException { - this(content.getBytes("UTF-8"), contentType); + this(content.getBytes(StandardCharsets.UTF_8), contentType); } public ParseByteArrayHttpBody(byte[] content, String contentType) { diff --git a/parse/src/main/java/com/parse/ParseCallback1.java b/parse/src/main/java/com/parse/ParseCallback1.java deleted file mode 100644 index 5ad73211b..000000000 --- a/parse/src/main/java/com/parse/ParseCallback1.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; - -/** - * A {@code ParseCallback} is used to do something after a background task completes. End users will - * use a specific subclass of {@code ParseCallback}. - */ -interface ParseCallback1 { - /** - * {@code done(t)} must be overridden when you are doing a background operation. It is called - * when the background operation completes. - *

- * If the operation is successful, {@code t} will be {@code null}. - *

- * If the operation was unsuccessful, {@code t} will contain information about the operation - * failure. - * - * @param t Generally an {@link Throwable} that was thrown by the operation, if there was any. - */ - void done(T t); -} diff --git a/parse/src/main/java/com/parse/ParseCallback1.kt b/parse/src/main/java/com/parse/ParseCallback1.kt new file mode 100644 index 000000000..f43059ec1 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseCallback1.kt @@ -0,0 +1,30 @@ +/* + * 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 + +/** + * A `ParseCallback` is used to do something after a background task completes. End users will + * use a specific subclass of `ParseCallback`. + */ +interface ParseCallback1 { + /** + * `done(t)` must be overridden when you are doing a background operation. It is called + * when the background operation completes. + * + * + * If the operation is successful, `t` will be `null`. + * + * + * If the operation was unsuccessful, `t` will contain information about the operation + * failure. + * + * @param t Generally an [Throwable] that was thrown by the operation, if there was any. + */ + fun done(t: T?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseCallback2.java b/parse/src/main/java/com/parse/ParseCallback2.java deleted file mode 100644 index 4db0585e5..000000000 --- a/parse/src/main/java/com/parse/ParseCallback2.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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; - -/** - * A {@code ParseCallback} is used to do something after a background task completes. End users will - * use a specific subclass of {@code ParseCallback}. - */ -interface ParseCallback2 { - /** - * {@code done(t1, t2)} must be overridden when you are doing a background operation. It is called - * when the background operation completes. - *

- * If the operation is successful, {@code t1} will contain the results and {@code t2} will be - * {@code null}. - *

- * If the operation was unsuccessful, {@code t1} will be {@code null} and {@code t2} will contain - * information about the operation failure. - * - * @param t1 Generally the results of the operation. - * @param t2 Generally an {@link Throwable} that was thrown by the operation, if there was any. - */ - void done(T1 t1, T2 t2); -} diff --git a/parse/src/main/java/com/parse/ParseCallback2.kt b/parse/src/main/java/com/parse/ParseCallback2.kt new file mode 100644 index 000000000..c4bb82ac8 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseCallback2.kt @@ -0,0 +1,32 @@ +/* + * 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 + +/** + * A `ParseCallback` is used to do something after a background task completes. End users will + * use a specific subclass of `ParseCallback`. + */ +interface ParseCallback2 { + /** + * `done(t1, t2)` must be overridden when you are doing a background operation. It is called + * when the background operation completes. + * + * + * If the operation is successful, `t1` will contain the results and `t2` will be + * `null`. + * + * + * If the operation was unsuccessful, `t1` will be `null` and `t2` will contain + * information about the operation failure. + * + * @param t1 Generally the results of the operation. + * @param t2 Generally an [Throwable] that was thrown by the operation, if there was any. + */ + fun done(t1: T1?, t2: T2?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseCloud.java b/parse/src/main/java/com/parse/ParseCloud.java index 05dd315bd..4a24bf77a 100644 --- a/parse/src/main/java/com/parse/ParseCloud.java +++ b/parse/src/main/java/com/parse/ParseCloud.java @@ -10,12 +10,11 @@ import androidx.annotation.NonNull; +import com.parse.boltsinternal.Task; + import java.util.List; import java.util.Map; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The ParseCloud class defines provides methods for interacting with Parse Cloud Functions. A Cloud * Function can be called with {@link #callFunctionInBackground(String, Map, FunctionCallback)} @@ -61,12 +60,9 @@ static ParseCloudCodeController getCloudCodeController() { */ public static Task callFunctionInBackground(@NonNull final String name, @NonNull final Map params) { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - return getCloudCodeController().callFunctionInBackground(name, params, sessionToken); - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + String sessionToken = task.getResult(); + return getCloudCodeController().callFunctionInBackground(name, params, sessionToken); }); } diff --git a/parse/src/main/java/com/parse/ParseCloudCodeController.java b/parse/src/main/java/com/parse/ParseCloudCodeController.java index 3703178f7..0b7078346 100644 --- a/parse/src/main/java/com/parse/ParseCloudCodeController.java +++ b/parse/src/main/java/com/parse/ParseCloudCodeController.java @@ -8,13 +8,12 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import java.util.Map; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - class ParseCloudCodeController { /* package for test */ final ParseHttpClient restClient; @@ -29,13 +28,10 @@ public Task callFunctionInBackground(final String name, name, params, sessionToken); - return command.executeAsync(restClient).onSuccess(new Continuation() { - @Override - public T then(Task task) { - @SuppressWarnings("unchecked") - T result = (T) convertCloudResponse(task.getResult()); - return result; - } + return command.executeAsync(restClient).onSuccess(task -> { + @SuppressWarnings("unchecked") + T result = (T) convertCloudResponse(task.getResult()); + return result; }); } diff --git a/parse/src/main/java/com/parse/ParseCommandCache.java b/parse/src/main/java/com/parse/ParseCommandCache.java deleted file mode 100644 index 17e4412cc..000000000 --- a/parse/src/main/java/com/parse/ParseCommandCache.java +++ /dev/null @@ -1,675 +0,0 @@ -/* - * 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 android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.concurrent.Callable; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -/** - * ParseCommandCache manages an on-disk cache of commands to be executed, and a thread with a - * standard run loop that executes the commands. There should only ever be one instance of this - * class, because multiple instances would be running separate threads trying to read and execute - * the same commands. - */ -class ParseCommandCache extends ParseEventuallyQueue { - private static final String TAG = "com.parse.ParseCommandCache"; - // Lock guards access to the file system and all of the instance variables above. It is static so - // that if somehow there are two instances of ParseCommandCache, they won't step on each others' - // toes while using the file system. A thread with lock should *not* try to get runningLock, under - // penalty of deadlock. Only the run loop (runLoop) thread should ever wait on this lock. Other - // threads should notify on this lock whenever the run loop should wake up and try to execute more - // commands. - private static final Object lock = new Object(); - // order. - private static int filenameCounter = 0; // Appended to temp file names so we know their creation - // Guards access to running. Gets a broadcast whenever running changes. A thread should only wait - // on runningLock if it's sure the value of running is going to change. Only the run loop - // (runLoop) thread should ever notify on runningLock. It's perfectly fine for a thread that has - // runningLock to then also try to acquire the other lock. - private final Object runningLock; - private final ParseHttpClient httpClient; - ConnectivityNotifier notifier; - ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() { - @Override - public void networkConnectivityStatusChanged(Context context, Intent intent) { - final boolean connectionLost = - intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); - final boolean isConnected = ConnectivityNotifier.isConnected(context); - - /* - Hack to avoid blocking the UI thread with disk I/O - - setConnected uses the same lock we use for synchronizing disk I/O, so there's a possibility - that we can block the UI thread on disk I/O, so we're going to bump the lock usage to a - different thread. - - TODO(grantland): Convert to TaskQueue, similar to ParsePinningEventuallyQueue - */ - Task.call(new Callable() { - @Override - public Void call() { - if (connectionLost) { - setConnected(false); - } else { - setConnected(isConnected); - } - return null; - } - }, ParseExecutors.io()); - } - }; - private File cachePath; // Where the cache is stored on disk. - private int timeoutMaxRetries = 5; // Don't retry more than 5 times before assuming disconnection. - private double timeoutRetryWaitSeconds = 600.0f; // Wait 10 minutes before retrying after network - // timeout. - private int maxCacheSizeBytes = 10 * 1024 * 1024; // Don't consume more than N bytes of storage. - // processed by the run loop? - private boolean shouldStop; // Should the run loop thread processing the disk cache continue? - private boolean unprocessedCommandsExist; // Has a command been added which hasn't yet been - // Map of filename to TaskCompletionSource, for all commands that are in the queue from this run - // of the program. This is necessary so that the original objects can be notified after their - // saves complete. - private HashMap> pendingTasks = new HashMap<>(); - private boolean running; // Is the run loop executing commands from the disk cache running? - private Logger log; // Why is there a custom logger? To prevent Mockito deadlock! - - public ParseCommandCache(Context context, ParseHttpClient client) { - setConnected(false); - - shouldStop = false; - running = false; - - runningLock = new Object(); - httpClient = client; - - log = Logger.getLogger(TAG); - - cachePath = getCacheDir(); - - if (!Parse.hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) { - // The command cache only works if the user has granted us permission to monitor the network. - return; - } - - setConnected(ConnectivityNotifier.isConnected(context)); - notifier = ConnectivityNotifier.getNotifier(context); - notifier.addListener(listener); - - resume(); - } - - private static File getCacheDir() { - // Construct the path to the cache directory. - File cacheDir = new File(Parse.getParseDir(), "CommandCache"); - cacheDir.mkdirs(); - - return cacheDir; - } - - public static int getPendingCount() { - synchronized (lock) { - String[] files = getCacheDir().list(); - return files == null ? 0 : files.length; - } - } - - @Override - public void onDestroy() { - //TODO (grantland): pause #6484855 - - notifier.removeListener(listener); - } - - // Set the maximum number of times to retry before assuming disconnection. - @SuppressWarnings("unused") - public void setTimeoutMaxRetries(int tries) { - synchronized (lock) { - timeoutMaxRetries = tries; - } - } - - // Sets the amount of time to wait before retrying after network timeout. - public void setTimeoutRetryWaitSeconds(double seconds) { - synchronized (lock) { - timeoutRetryWaitSeconds = seconds; - } - } - - // Sets the maximum amount of storage space this cache can consume. - public void setMaxCacheSizeBytes(int bytes) { - synchronized (lock) { - maxCacheSizeBytes = bytes; - } - } - - // Starts the run loop thread running. - public void resume() { - synchronized (runningLock) { - if (!running) { - new Thread("ParseCommandCache.runLoop()") { - @Override - public void run() { - runLoop(); - } - }.start(); - try { - runningLock.wait(); - } catch (InterruptedException e) { - // Someone told this thread to stop. - synchronized (lock) { - shouldStop = true; - lock.notifyAll(); - } - } - } - } - } - - // Stops the run loop thread from processing commands until resume is called. - // When this function returns, the run loop has stopped. - public void pause() { - synchronized (runningLock) { - if (running) { - synchronized (lock) { - shouldStop = true; - lock.notifyAll(); - } - } - while (running) { - try { - runningLock.wait(); - } catch (InterruptedException e) { - // Someone told this thread to stop while it was already waiting to - // finish... - // Ignore them and continue waiting. - } - } - } - } - - /** - * Removes a file from the file system and any internal caches. - */ - private void removeFile(File file) { - synchronized (lock) { - // Remove the data in memory for this command. - pendingTasks.remove(file); - - // Release all the localIds referenced by the command. - // Read one command from the cache. - JSONObject json; - try { - json = ParseFileUtils.readFileToJSONObject(file); - - ParseRESTCommand command = commandFromJSON(json); - command.releaseLocalIds(); - } catch (Exception e) { - // Well, we did our best. We'll just have to leak a localId. - } - - // Delete the command file itself. - ParseFileUtils.deleteQuietly(file); - } - } - - /** - * Makes this command cache forget all the state it keeps during a single run of the app. This is - * only for testing purposes. - */ - void simulateReboot() { - synchronized (lock) { - pendingTasks.clear(); - } - } - - /** - * Fakes an object update notification for use in tests. This is used by saveEventually to make it - * look like test code has updated an object through the command cache even if it actually - * avoided executing update by determining the object wasn't dirty. - */ - void fakeObjectUpdate() { - notifyTestHelper(TestHelper.COMMAND_ENQUEUED); - notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); - notifyTestHelper(TestHelper.OBJECT_UPDATED); - } - - @Override - public Task enqueueEventuallyAsync(ParseRESTCommand command, - ParseObject object) { - return enqueueEventuallyAsync(command, false, object); - } - - /** - * Attempts to run the given command and any pending commands. Adds the command to the pending set - * if it can't be run yet. - * - * @param command - The command to run. - * @param preferOldest - When the disk is full, if preferOldest, drop new commands. Otherwise, the oldest - * commands will be deleted to make room. - * @param object - See runEventually. - */ - private Task enqueueEventuallyAsync(ParseRESTCommand command, boolean preferOldest, - ParseObject object) { - Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE); - TaskCompletionSource tcs = new TaskCompletionSource<>(); - byte[] json; - try { - // If this object doesn't have an objectId yet, store the localId so we can remap it to the - // objectId after the save completes. - if (object != null && object.getObjectId() == null) { - command.setLocalId(object.getOrCreateLocalId()); - } - JSONObject jsonObject = command.toJSONObject(); - json = jsonObject.toString().getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - log.log(Level.WARNING, "UTF-8 isn't supported. This shouldn't happen.", e); - } - notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); - return Task.forResult(null); - } - - // If this object by itself is larger than the full disk cache, then don't - // even bother trying. - if (json.length > maxCacheSizeBytes) { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - log.warning("Unable to save command for later because it's too big."); - } - notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); - return Task.forResult(null); - } - - synchronized (lock) { - try { - // Is there enough free storage space? - String[] fileNames = cachePath.list(); - if (fileNames != null) { - Arrays.sort(fileNames); - int size = 0; - for (String fileName : fileNames) { - File file = new File(cachePath, fileName); - // Should be safe to convert long to int, because we don't allow - // files larger than 2GB. - size += (int) file.length(); - } - size += json.length; - if (size > maxCacheSizeBytes) { - if (preferOldest) { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - log.warning("Unable to save command for later because storage is full."); - } - return Task.forResult(null); - } else { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - log.warning("Deleting old commands to make room in command cache."); - } - int indexToDelete = 0; - while (size > maxCacheSizeBytes && indexToDelete < fileNames.length) { - File file = new File(cachePath, fileNames[indexToDelete++]); - size -= (int) file.length(); - removeFile(file); - } - } - } - } - - // Get the current time to store in the filename, so that we process them in order. - String prefix1 = Long.toHexString(System.currentTimeMillis()); - if (prefix1.length() < 16) { - char[] zeroes = new char[16 - prefix1.length()]; - Arrays.fill(zeroes, '0'); - prefix1 = new String(zeroes) + prefix1; - } - - // Then add another incrementing number in case we enqueue items faster than the system's - // time granularity. - String prefix2 = Integer.toHexString(filenameCounter++); - if (prefix2.length() < 8) { - char[] zeroes = new char[8 - prefix2.length()]; - Arrays.fill(zeroes, '0'); - prefix2 = new String(zeroes) + prefix2; - } - - String prefix = "CachedCommand_" + prefix1 + "_" + prefix2 + "_"; - - // Get a unique filename to store this command in. - File path = File.createTempFile(prefix, "", cachePath); - - // Write the command to that file. - pendingTasks.put(path, tcs); - command.retainLocalIds(); - ParseFileUtils.writeByteArrayToFile(path, json); - - notifyTestHelper(TestHelper.COMMAND_ENQUEUED); - - unprocessedCommandsExist = true; - } catch (IOException e) { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - log.log(Level.WARNING, "Unable to save command for later.", e); - } - } finally { - lock.notifyAll(); - } - } - return tcs.getTask(); - } - - /** - * Returns the number of commands currently in the set waiting to be run. - */ - @Override - public int pendingCount() { - return getPendingCount(); - } - - /** - * Gets rid of all pending commands. - */ - public void clear() { - synchronized (lock) { - File[] files = cachePath.listFiles(); - if (files == null) { - return; - } - for (File file : files) { - removeFile(file); - } - pendingTasks.clear(); - } - } - - /** - * Manually sets the network connection status. - */ - public void setConnected(boolean connected) { - synchronized (lock) { - if (isConnected() != connected) { - if (connected) { - lock.notifyAll(); - } - } - super.setConnected(connected); - } - } - - /** - * This is kind of like ParseTaskUtils.wait(), except that it gives up the CommandCache's lock - * while the task is running, and reclaims it before returning. - */ - private T waitForTaskWithoutLock(Task task) throws ParseException { - synchronized (lock) { - final Capture finished = new Capture<>(false); - task.continueWith(new Continuation() { - @Override - public Void then(Task task) { - finished.set(true); - synchronized (lock) { - lock.notifyAll(); - } - return null; - } - }, Task.BACKGROUND_EXECUTOR); - while (!finished.get()) { - try { - lock.wait(); - } catch (InterruptedException ie) { - shouldStop = true; - } - } - return ParseTaskUtils.wait(task); // Just to get the return value and maybe rethrow. - } - } - - /** - * Attempts to run every command in the disk queue in order, synchronously. If there is no network - * connection, returns immediately without doing anything. If there is supposedly a connection, - * but parse can't be reached, waits timeoutRetryWaitSeconds before retrying up to - * retriesRemaining times. Blocks until either there's a connection, or the retries are exhausted. - * If any command fails, just deletes it and moves on to the next one. - */ - private void maybeRunAllCommandsNow(int retriesRemaining) { - synchronized (lock) { - unprocessedCommandsExist = false; - - if (!isConnected()) { - // There's no way to do work when there's no network connection. - return; - } - - String[] fileNames = cachePath.list(); - if (fileNames == null || fileNames.length == 0) { - return; - } - Arrays.sort(fileNames); - for (String fileName : fileNames) { - final File file = new File(cachePath, fileName); - - // Read one command from the cache. - JSONObject json; - try { - json = ParseFileUtils.readFileToJSONObject(file); - } catch (FileNotFoundException e) { - // This shouldn't really be possible. - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "File disappeared from cache while being read.", e); - } - continue; - } catch (IOException e) { - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "Unable to read contents of file in cache.", e); - } - removeFile(file); - continue; - } catch (JSONException e) { - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "Error parsing JSON found in cache.", e); - } - removeFile(file); - continue; - } - - // Convert the command from a string. - final ParseRESTCommand command; - final TaskCompletionSource tcs = - pendingTasks.containsKey(file) ? pendingTasks.get(file) : null; - - try { - command = commandFromJSON(json); - } catch (JSONException e) { - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "Unable to create ParseCommand from JSON.", e); - } - removeFile(file); - continue; - } - - try { - Task commandTask; - if (command == null) { - commandTask = Task.forResult(null); - if (tcs != null) { - tcs.setResult(null); - } - notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED); - } else { - commandTask = command.executeAsync(httpClient).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - String localId = command.getLocalId(); - Exception error = task.getError(); - if (error != null) { - if (error instanceof ParseException - && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { - // do nothing - } else { - if (tcs != null) { - tcs.setError(error); - } - } - return task; - } - - JSONObject json = task.getResult(); - if (tcs != null) { - tcs.setResult(json); - } else if (localId != null) { - // If this command created a new objectId, add it to the map. - String objectId = json.optString("objectId", null); - if (objectId != null) { - ParseCorePlugins.getInstance() - .getLocalIdManager().setObjectId(localId, objectId); - } - } - return task; - } - }); - } - - waitForTaskWithoutLock(commandTask); - if (tcs != null) { - waitForTaskWithoutLock(tcs.getTask()); - } - - // The command succeeded. Remove it from the cache. - removeFile(file); - notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); - } catch (ParseException e) { - if (e.getCode() == ParseException.CONNECTION_FAILED) { - if (retriesRemaining > 0) { - // Reachability says we have a network connection, but we can't actually contact - // Parse. Wait N minutes, or until we get signaled again before doing anything else. - if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { - log.info("Network timeout in command cache. Waiting for " + timeoutRetryWaitSeconds - + " seconds and then retrying " + retriesRemaining + " times."); - } - long currentTime = System.currentTimeMillis(); - long waitUntil = currentTime + (long) (timeoutRetryWaitSeconds * 1000); - while (currentTime < waitUntil) { - // We haven't waited long enough, but if we lost the connection, - // or should stop, just quit. - if (!isConnected() || shouldStop) { - if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { - log.info("Aborting wait because runEventually thread should stop."); - } - return; - } - try { - lock.wait(waitUntil - currentTime); - } catch (InterruptedException ie) { - shouldStop = true; - } - currentTime = System.currentTimeMillis(); - if (currentTime < (waitUntil - (long) (timeoutRetryWaitSeconds * 1000))) { - // This situation should be impossible, so it must mean the clock changed. - currentTime = (waitUntil - (long) (timeoutRetryWaitSeconds * 1000)); - } - } - maybeRunAllCommandsNow(retriesRemaining - 1); - } else { - setConnected(false); - - notifyTestHelper(TestHelper.NETWORK_DOWN); - } - } else { - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "Failed to run command.", e); - } - // Delete the command from the cache, even though it failed. - // Otherwise, we'll just keep trying it forever. - removeFile(file); - notifyTestHelper(TestHelper.COMMAND_FAILED, e); - } - } - } - } - } - - /** - * The main function of the run loop thread. This function basically loops forever (unless pause - * is called). On each iteration, if it hasn't been told to stop, it calls maybeRunAllCommandsNow - * to try to execute everything queued up on disk. Then it waits until it gets signaled again by - * lock.notify(). Usually that happens as a result of either (1) Parse being initialized, (2) - * runEventually being called, or (3) the OS notifying that the network connection has been - * re-established. - */ - private void runLoop() { - if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { - log.info("Parse command cache has started processing queued commands."); - } - // Make sure we marked as running. - synchronized (runningLock) { - if (running) { - // Don't run this thread more than once. - return; - } else { - running = true; - runningLock.notifyAll(); - } - } - - boolean shouldRun; - synchronized (lock) { - shouldRun = !(shouldStop || Thread.interrupted()); - } - while (shouldRun) { - synchronized (lock) { - try { - maybeRunAllCommandsNow(timeoutMaxRetries); - if (!shouldStop) { - try { - /* - * If an unprocessed command was added, avoid waiting because we want - * maybeRunAllCommandsNow to run at least once to potentially process that command. - */ - if (!unprocessedCommandsExist) { - lock.wait(); - } - } catch (InterruptedException e) { - shouldStop = true; - } - } - } catch (Exception e) { - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - log.log(Level.SEVERE, "saveEventually thread had an error.", e); - } - } finally { - shouldRun = !shouldStop; - } - } - } - - synchronized (runningLock) { - running = false; - runningLock.notifyAll(); - } - if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { - log.info("saveEventually thread has stopped processing commands."); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseCommandCache.kt b/parse/src/main/java/com/parse/ParseCommandCache.kt new file mode 100644 index 000000000..8af3f3b1d --- /dev/null +++ b/parse/src/main/java/com/parse/ParseCommandCache.kt @@ -0,0 +1,642 @@ +/* + * 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 android.Manifest +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import com.parse.ConnectivityNotifier.Companion.getNotifier +import com.parse.ConnectivityNotifier.Companion.isConnected +import com.parse.ConnectivityNotifier.ConnectivityListener +import com.parse.boltsinternal.Capture +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.charset.Charset +import java.util.* +import java.util.logging.Level +import java.util.logging.Logger + + +@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") +private fun Any.wait() = (this as Object).wait() +private fun Any.wait(timeout: Long) = (this as Object).wait(timeout) +private fun Any.notifyAll() = (this as Object).notifyAll() +private fun Any.notify() = (this as Object).notify() + +/** + * ParseCommandCache manages an on-disk cache of commands to be executed, and a thread with a + * standard run loop that executes the commands. There should only ever be one instance of this + * class, because multiple instances would be running separate threads trying to read and execute + * the same commands. + */ +internal class ParseCommandCache(context: Context?, client: ParseHttpClient) : + ParseEventuallyQueue() { + private val listener: ConnectivityListener = object : ConnectivityListener { + override fun networkConnectivityStatusChanged(context: Context?, intent: Intent?) { + val connectionLost = intent?.getBooleanExtra( + ConnectivityManager.EXTRA_NO_CONNECTIVITY, false + ) ?: false + val isConnected = isConnected(context!!) + + /* + Hack to avoid blocking the UI thread with disk I/O + + setConnected uses the same lock we use for synchronizing disk I/O, so there's a possibility + that we can block the UI thread on disk I/O, so we're going to bump the lock usage to a + different thread. + + TODO(grantland): Convert to TaskQueue, similar to ParsePinningEventuallyQueue + */Task.call({ + if (connectionLost) { + this@ParseCommandCache.isConnected = false + } else { + this@ParseCommandCache.isConnected = connectionLost + } + null + }, ParseExecutors.io()) + } + } + + // Guards access to running. Gets a broadcast whenever running changes. A thread should only wait + // on runningLock if it's sure the value of running is going to change. Only the run loop + // (runLoop) thread should ever notify on runningLock. It's perfectly fine for a thread that has + // runningLock to then also try to acquire the other lock. + private val runningLock: Any + private val httpClient: ParseHttpClient + private val cachePath // Where the cache is stored on disk. + : File + + // Map of filename to TaskCompletionSource, for all commands that are in the queue from this run + // of the program. This is necessary so that the original objects can be notified after their + // saves complete. + private val pendingTasks = HashMap>() + private val log // Why is there a custom logger? To prevent Mockito deadlock! + : Logger + private lateinit var notifier: ConnectivityNotifier + private var timeoutMaxRetries = + 5 // Don't retry more than 5 times before assuming disconnection. + private var timeoutRetryWaitSeconds = 600.0 // Wait 10 minutes before retrying after network + + // timeout. + private var maxCacheSizeBytes = 10 * 1024 * 1024 // Don't consume more than N bytes of storage. + + // processed by the run loop? + private var shouldStop // Should the run loop thread processing the disk cache continue? + : Boolean + private var unprocessedCommandsExist // Has a command been added which hasn't yet been + = false + private var running // Is the run loop executing commands from the disk cache running? + : Boolean + + override fun onDestroy() { + //TODO (grantland): pause #6484855 + notifier.removeListener(listener) + } + + // Set the maximum number of times to retry before assuming disconnection. + fun setTimeoutMaxRetries(tries: Int) { + synchronized(lock) { timeoutMaxRetries = tries } + } + + // Sets the amount of time to wait before retrying after network timeout. + override fun setTimeoutRetryWaitSeconds(seconds: Double) { + synchronized(lock) { timeoutRetryWaitSeconds = seconds } + } + + // Sets the maximum amount of storage space this cache can consume. + override fun setMaxCacheSizeBytes(bytes: Int) { + synchronized(lock) { maxCacheSizeBytes = bytes } + } + + // Starts the run loop thread running. + override fun resume() { + synchronized(runningLock) { + if (!running) { + object : Thread("ParseCommandCache.runLoop()") { + override fun run() { + runLoop() + } + }.start() + try { + runningLock.wait() + } catch (e: InterruptedException) { + // Someone told this thread to stop. + synchronized(lock) { + shouldStop = true + lock.notifyAll() + } + } + } + } + } + + // Stops the run loop thread from processing commands until resume is called. + // When this function returns, the run loop has stopped. + override fun pause() { + synchronized(runningLock) { + if (running) { + synchronized(lock) { + shouldStop = true + lock.notifyAll() + } + } + while (running) { + try { + runningLock.wait() + } catch (e: InterruptedException) { + // Someone told this thread to stop while it was already waiting to + // finish... + // Ignore them and continue waiting. + } + } + } + } + + /** + * Removes a file from the file system and any internal caches. + */ + private fun removeFile(file: File) { + synchronized(lock) { + + // Remove the data in memory for this command. + pendingTasks.remove(file) + + // Release all the localIds referenced by the command. + // Read one command from the cache. + val json: JSONObject + try { + json = ParseFileUtils.readFileToJSONObject(file) + val command = commandFromJSON(json) + command?.releaseLocalIds() + } catch (e: Exception) { + // Well, we did our best. We'll just have to leak a localId. + } + + // Delete the command file itself. + ParseFileUtils.deleteQuietly(file) + } + } + + /** + * Makes this command cache forget all the state it keeps during a single run of the app. This is + * only for testing purposes. + */ + public override fun simulateReboot() { + synchronized(lock) { pendingTasks.clear() } + } + + /** + * Fakes an object update notification for use in tests. This is used by saveEventually to make it + * look like test code has updated an object through the command cache even if it actually + * avoided executing update by determining the object wasn't dirty. + */ + public override fun fakeObjectUpdate() { + notifyTestHelper(TestHelper.COMMAND_ENQUEUED) + notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL) + notifyTestHelper(TestHelper.OBJECT_UPDATED) + } + + override fun enqueueEventuallyAsync( + command: ParseRESTCommand, + `object`: ParseObject? + ): Task { + return enqueueEventuallyAsync(command, false, `object`) + } + + /** + * Attempts to run the given command and any pending commands. Adds the command to the pending set + * if it can't be run yet. + * + * @param command - The command to run. + * @param preferOldest - When the disk is full, if preferOldest, drop new commands. Otherwise, the oldest + * commands will be deleted to make room. + * @param object - See runEventually. + */ + private fun enqueueEventuallyAsync( + command: ParseRESTCommand, preferOldest: Boolean, + `object`: ParseObject? + ): Task { + Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE) + val tcs = TaskCompletionSource() + val json: ByteArray + // If this object doesn't have an objectId yet, store the localId so we can remap it to the + // objectId after the save completes. + if (`object` != null && `object`.objectId == null) { + command.localId = `object`.getOrCreateLocalId() + } + val jsonObject = command.toJSONObject() + json = jsonObject.toString().toByteArray(Charset.forName("UTF-8")) + + // If this object by itself is larger than the full disk cache, then don't + // even bother trying. + if (json.size > maxCacheSizeBytes) { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + log.warning("Unable to save command for later because it's too big.") + } + notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED) + return Task.forResult(null) + } + synchronized(lock) { + try { + // Is there enough free storage space? + val fileNames = cachePath.list() + if (fileNames != null) { + Arrays.sort(fileNames) + var size = 0 + for (fileName in fileNames) { + val file = File(cachePath, fileName) + // Should be safe to convert long to int, because we don't allow + // files larger than 2GB. + size += file.length().toInt() + } + size += json.size + if (size > maxCacheSizeBytes) { + if (preferOldest) { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + log.warning("Unable to save command for later because storage is full.") + } + return Task.forResult(null) + } else { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + log.warning("Deleting old commands to make room in command cache.") + } + var indexToDelete = 0 + while (size > maxCacheSizeBytes && indexToDelete < fileNames.size) { + val file = File(cachePath, fileNames[indexToDelete++]) + size -= file.length().toInt() + removeFile(file) + } + } + } + } + + // Get the current time to store in the filename, so that we process them in order. + var prefix1 = java.lang.Long.toHexString(System.currentTimeMillis()) + if (prefix1.length < 16) { + val zeroes = CharArray(16 - prefix1.length) + Arrays.fill(zeroes, '0') + prefix1 = String(zeroes) + prefix1 + } + + // Then add another incrementing number in case we enqueue items faster than the system's + // time granularity. + var prefix2 = Integer.toHexString(filenameCounter++) + if (prefix2.length < 8) { + val zeroes = CharArray(8 - prefix2.length) + Arrays.fill(zeroes, '0') + prefix2 = String(zeroes) + prefix2 + } + val prefix = "CachedCommand_" + prefix1 + "_" + prefix2 + "_" + + // Get a unique filename to store this command in. + val path = File.createTempFile(prefix, "", cachePath) + + // Write the command to that file. + pendingTasks[path] = tcs + command.retainLocalIds() + ParseFileUtils.writeByteArrayToFile(path, json) + notifyTestHelper(TestHelper.COMMAND_ENQUEUED) + unprocessedCommandsExist = true + } catch (e: IOException) { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + log.log(Level.WARNING, "Unable to save command for later.", e) + } + } finally { + lock.notifyAll() + } + } + return tcs.task + } + + /** + * Returns the number of commands currently in the set waiting to be run. + */ + override fun pendingCount(): Int { + return pendingCount + } + + /** + * Gets rid of all pending commands. + */ + override fun clear() { + synchronized(lock) { + val files = cachePath.listFiles() ?: return + for (file in files) { + removeFile(file) + } + pendingTasks.clear() + } + } + + override var isConnected: Boolean = false + get() = super.isConnected + set(value) { + synchronized(lock) { + if (isConnected != value) { + if (value) { + lock.notifyAll() + } + } + field = value + } + } + + /** + * This is kind of like ParseTaskUtils.wait(), except that it gives up the CommandCache's lock + * while the task is running, and reclaims it before returning. + */ + @Throws(ParseException::class) + private fun waitForTaskWithoutLock(task: Task): T { + synchronized(lock) { + val finished = Capture(false) + task.continueWith(Continuation { task1: Task? -> + finished.set(true) + synchronized(lock) { lock.notifyAll() } + null + }, Task.BACKGROUND_EXECUTOR) + while (!finished.get()) { + try { + lock.wait() + } catch (ie: InterruptedException) { + shouldStop = true + } + } + return ParseTaskUtils.wait(task) // Just to get the return value and maybe rethrow. + } + } + + /** + * Attempts to run every command in the disk queue in order, synchronously. If there is no network + * connection, returns immediately without doing anything. If there is supposedly a connection, + * but parse can't be reached, waits timeoutRetryWaitSeconds before retrying up to + * retriesRemaining times. Blocks until either there's a connection, or the retries are exhausted. + * If any command fails, just deletes it and moves on to the next one. + */ + private fun maybeRunAllCommandsNow(retriesRemaining: Int) { + synchronized(lock) { + unprocessedCommandsExist = false + if (!isConnected) { + // There's no way to do work when there's no network connection. + return + } + val fileNames = cachePath.list() + if (fileNames == null || fileNames.isEmpty()) { + return + } + Arrays.sort(fileNames) + for (fileName in fileNames) { + val file = File(cachePath, fileName) + + // Read one command from the cache. + val json: JSONObject? = try { + ParseFileUtils.readFileToJSONObject(file) + } catch (e: FileNotFoundException) { + // This shouldn't really be possible. + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "File disappeared from cache while being read.", e) + } + continue + } catch (e: IOException) { + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "Unable to read contents of file in cache.", e) + } + removeFile(file) + continue + } catch (e: JSONException) { + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "Error parsing JSON found in cache.", e) + } + removeFile(file) + continue + } + + // Convert the command from a string. + val tcs: TaskCompletionSource? = + if (pendingTasks.containsKey(file)) pendingTasks[file] else null + val command: ParseRESTCommand? = try { + commandFromJSON(json) + } catch (e: JSONException) { + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "Unable to create ParseCommand from JSON.", e) + } + removeFile(file) + continue + } + try { + var commandTask: Task + if (command == null) { + commandTask = Task.forResult(null) + tcs?.setResult(null) + notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED) + } else { + commandTask = command.executeAsync(httpClient) + .continueWithTask { task: Task -> + val localId = command.localId + val error = task.error + if (error != null) { + if (error is ParseException + && error.code == ParseException.CONNECTION_FAILED + ) { + // do nothing + } else { + tcs?.setError(error) + } + return@continueWithTask task + } + val json1 = task.result + if (tcs != null) { + tcs.setResult(json1) + } else if (localId != null) { + // If this command created a new objectId, add it to the map. + val objectId = json1!!.optString("objectId", null) + if (objectId != null) { + ParseCorePlugins.getInstance() + .localIdManager.setObjectId(localId, objectId) + } + } + task + } + } + waitForTaskWithoutLock(commandTask) + if (tcs != null) { + waitForTaskWithoutLock(tcs.task) + } + + // The command succeeded. Remove it from the cache. + removeFile(file) + notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL) + } catch (e: ParseException) { + if (e.code == ParseException.CONNECTION_FAILED) { + if (retriesRemaining > 0) { + // Reachability says we have a network connection, but we can't actually contact + // Parse. Wait N minutes, or until we get signaled again before doing anything else. + if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { + log.info( + "Network timeout in command cache. Waiting for " + timeoutRetryWaitSeconds + + " seconds and then retrying " + retriesRemaining + " times." + ) + } + var currentTime = System.currentTimeMillis() + val waitUntil = currentTime + (timeoutRetryWaitSeconds * 1000).toLong() + while (currentTime < waitUntil) { + // We haven't waited long enough, but if we lost the connection, + // or should stop, just quit. + if (!isConnected || shouldStop) { + if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { + log.info("Aborting wait because runEventually thread should stop.") + } + return + } + try { + lock.wait(waitUntil - currentTime) + } catch (ie: InterruptedException) { + shouldStop = true + } + currentTime = System.currentTimeMillis() + if (currentTime < waitUntil - (timeoutRetryWaitSeconds * 1000).toLong()) { + // This situation should be impossible, so it must mean the clock changed. + currentTime = + waitUntil - (timeoutRetryWaitSeconds * 1000).toLong() + } + } + maybeRunAllCommandsNow(retriesRemaining - 1) + } else { + isConnected = false + notifyTestHelper(TestHelper.NETWORK_DOWN) + } + } else { + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "Failed to run command.", e) + } + // Delete the command from the cache, even though it failed. + // Otherwise, we'll just keep trying it forever. + removeFile(file) + notifyTestHelper(TestHelper.COMMAND_FAILED, e) + } + } + } + } + } + + /** + * The main function of the run loop thread. This function basically loops forever (unless pause + * is called). On each iteration, if it hasn't been told to stop, it calls maybeRunAllCommandsNow + * to try to execute everything queued up on disk. Then it waits until it gets signaled again by + * lock.notify(). Usually that happens as a result of either (1) Parse being initialized, (2) + * runEventually being called, or (3) the OS notifying that the network connection has been + * re-established. + */ + private fun runLoop() { + if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { + log.info("Parse command cache has started processing queued commands.") + } + // Make sure we marked as running. + synchronized(runningLock) { + if (running) { + // Don't run this thread more than once. + return + } else { + running = true + runningLock.notifyAll() + } + } + var shouldRun: Boolean + synchronized(lock) { shouldRun = !(shouldStop || Thread.interrupted()) } + while (shouldRun) { + synchronized(lock) { + try { + maybeRunAllCommandsNow(timeoutMaxRetries) + if (!shouldStop) { + try { + /* + * If an unprocessed command was added, avoid waiting because we want + * maybeRunAllCommandsNow to run at least once to potentially process that command. + */ + if (!unprocessedCommandsExist) { + lock.wait() + } + } catch (e: InterruptedException) { + shouldStop = true + } + } + } catch (e: Exception) { + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + log.log(Level.SEVERE, "saveEventually thread had an error.", e) + } + } finally { + shouldRun = !shouldStop + } + } + } + synchronized(runningLock) { + running = false + runningLock.notifyAll() + } + if (Parse.LOG_LEVEL_INFO >= Parse.getLogLevel()) { + log.info("saveEventually thread has stopped processing commands.") + } + } + + companion object { + private const val TAG = "com.parse.ParseCommandCache" + + // Lock guards access to the file system and all of the instance variables above. It is static so + // that if somehow there are two instances of ParseCommandCache, they won't step on each others' + // toes while using the file system. A thread with lock should *not* try to get runningLock, under + // penalty of deadlock. Only the run loop (runLoop) thread should ever wait on this lock. Other + // threads should notify on this lock whenever the run loop should wake up and try to execute more + // commands. + private val lock = Any() + + // order. + private var filenameCounter = 0 // Appended to temp file names so we know their creation + + // Construct the path to the cache directory. + private val cacheDir: File + get() { + // Construct the path to the cache directory. + val cacheDir = File(Parse.getParseDir(), "CommandCache") + cacheDir.mkdirs() + return cacheDir + } + @JvmStatic + val pendingCount: Int + get() { + synchronized(lock) { + val files = cacheDir.list() + return files?.size ?: 0 + } + } + } + + init { + isConnected = false + shouldStop = false + running = false + runningLock = Any() + httpClient = client + log = Logger.getLogger(TAG) + cachePath = cacheDir + + if (Parse.hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) { + // The command cache only works if the user has granted us permission to monitor the network. + isConnected = isConnected(context!!) + notifier = getNotifier(context) + notifier.addListener(listener) + resume() + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseConfig.java b/parse/src/main/java/com/parse/ParseConfig.java index e985cde3d..6d1d39c6d 100644 --- a/parse/src/main/java/com/parse/ParseConfig.java +++ b/parse/src/main/java/com/parse/ParseConfig.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONArray; import org.json.JSONObject; @@ -17,9 +19,6 @@ import java.util.List; import java.util.Map; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The {@code ParseConfig} is a local representation of configuration data that can be set from the * Parse dashboard. @@ -34,7 +33,7 @@ public class ParseConfig { } /* package */ ParseConfig() { - params = Collections.unmodifiableMap(new HashMap()); + params = Collections.unmodifiableMap(new HashMap<>()); } /* package for tests */ @@ -87,26 +86,13 @@ public static void getInBackground(ConfigCallback callback) { * @return A Task that is resolved when the fetch completes. */ public static Task getInBackground() { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return getAsync(toAwait); - } - }); + return taskQueue.enqueue(ParseConfig::getAsync); } private static Task getAsync(final Task toAwait) { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return getConfigController().getAsync(sessionToken); - } - }); - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + final String sessionToken = task.getResult(); + return toAwait.continueWithTask(task1 -> getConfigController().getAsync(sessionToken)); }); } diff --git a/parse/src/main/java/com/parse/ParseConfigController.java b/parse/src/main/java/com/parse/ParseConfigController.java index 6c8707498..4982bf8b7 100644 --- a/parse/src/main/java/com/parse/ParseConfigController.java +++ b/parse/src/main/java/com/parse/ParseConfigController.java @@ -8,15 +8,14 @@ */ package com.parse; -import org.json.JSONObject; - -import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; +import org.json.JSONObject; + class ParseConfigController { private final ParseHttpClient restClient; - private ParseCurrentConfigController currentConfigController; + private final ParseCurrentConfigController currentConfigController; public ParseConfigController(ParseHttpClient restClient, ParseCurrentConfigController currentConfigController) { @@ -30,19 +29,11 @@ public ParseConfigController(ParseHttpClient restClient, public Task getAsync(String sessionToken) { final ParseRESTCommand command = ParseRESTConfigCommand.fetchConfigCommand(sessionToken); - return command.executeAsync(restClient).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - JSONObject result = task.getResult(); - - final ParseConfig config = ParseConfig.decode(result, ParseDecoder.get()); - return currentConfigController.setCurrentConfigAsync(config).continueWith(new Continuation() { - @Override - public ParseConfig then(Task task) { - return config; - } - }); - } + return command.executeAsync(restClient).onSuccessTask(task -> { + JSONObject result = task.getResult(); + + final ParseConfig config = ParseConfig.decode(result, ParseDecoder.get()); + return currentConfigController.setCurrentConfigAsync(config).continueWith(task1 -> config); }); } } diff --git a/parse/src/main/java/com/parse/ParseCorePlugins.java b/parse/src/main/java/com/parse/ParseCorePlugins.java index ea5b8a79f..8101faea6 100644 --- a/parse/src/main/java/com/parse/ParseCorePlugins.java +++ b/parse/src/main/java/com/parse/ParseCorePlugins.java @@ -19,28 +19,29 @@ class ParseCorePlugins { /* package */ static final String PIN_CURRENT_INSTALLATION = "_currentInstallation"; /* package */ static final String FILENAME_CURRENT_CONFIG = "currentConfig"; private static final ParseCorePlugins INSTANCE = new ParseCorePlugins(); - private AtomicReference objectController = new AtomicReference<>(); - private AtomicReference userController = new AtomicReference<>(); - private AtomicReference sessionController = new AtomicReference<>(); + private final AtomicReference objectController = new AtomicReference<>(); + private final AtomicReference userController = new AtomicReference<>(); + private final AtomicReference sessionController = new AtomicReference<>(); // TODO(mengyan): Inject into ParseUserInstanceController - private AtomicReference currentUserController = + private final AtomicReference currentUserController = new AtomicReference<>(); // TODO(mengyan): Inject into ParseInstallationInstanceController - private AtomicReference currentInstallationController = + private final AtomicReference currentInstallationController = new AtomicReference<>(); - private AtomicReference authenticationController = + private final AtomicReference authenticationController = new AtomicReference<>(); - private AtomicReference queryController = new AtomicReference<>(); - private AtomicReference fileController = new AtomicReference<>(); - private AtomicReference analyticsController = new AtomicReference<>(); - private AtomicReference cloudCodeController = new AtomicReference<>(); - private AtomicReference configController = new AtomicReference<>(); - private AtomicReference pushController = new AtomicReference<>(); - private AtomicReference pushChannelsController = + private final AtomicReference queryController = new AtomicReference<>(); + private final AtomicReference fileController = new AtomicReference<>(); + private final AtomicReference analyticsController = new AtomicReference<>(); + private final AtomicReference cloudCodeController = new AtomicReference<>(); + private final AtomicReference configController = new AtomicReference<>(); + private final AtomicReference pushController = new AtomicReference<>(); + private final AtomicReference pushChannelsController = new AtomicReference<>(); - private AtomicReference defaultACLController = new AtomicReference<>(); - private AtomicReference localIdManager = new AtomicReference<>(); - private AtomicReference subclassingController = new AtomicReference<>(); + private final AtomicReference defaultACLController = new AtomicReference<>(); + private final AtomicReference localIdManager = new AtomicReference<>(); + private final AtomicReference subclassingController = new AtomicReference<>(); + private ParseCorePlugins() { // do nothing } diff --git a/parse/src/main/java/com/parse/ParseCurrentConfigController.java b/parse/src/main/java/com/parse/ParseCurrentConfigController.java index a78d7dd9b..3e429f4cf 100644 --- a/parse/src/main/java/com/parse/ParseCurrentConfigController.java +++ b/parse/src/main/java/com/parse/ParseCurrentConfigController.java @@ -8,50 +8,43 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Task; class ParseCurrentConfigController { private final Object currentConfigMutex = new Object(); + private final File currentConfigFile; /* package for test */ ParseConfig currentConfig; - private File currentConfigFile; public ParseCurrentConfigController(File currentConfigFile) { this.currentConfigFile = currentConfigFile; } public Task setCurrentConfigAsync(final ParseConfig config) { - return Task.call(new Callable() { - @Override - public Void call() { - synchronized (currentConfigMutex) { - currentConfig = config; - saveToDisk(config); - } - return null; + return Task.call(() -> { + synchronized (currentConfigMutex) { + currentConfig = config; + saveToDisk(config); } + return null; }, ParseExecutors.io()); } public Task getCurrentConfigAsync() { - return Task.call(new Callable() { - @Override - public ParseConfig call() { - synchronized (currentConfigMutex) { - if (currentConfig == null) { - ParseConfig config = getFromDisk(); - currentConfig = (config != null) ? config : new ParseConfig(); - } + return Task.call(() -> { + synchronized (currentConfigMutex) { + if (currentConfig == null) { + ParseConfig config = getFromDisk(); + currentConfig = (config != null) ? config : new ParseConfig(); } - return currentConfig; } + return currentConfig; }, ParseExecutors.io()); } diff --git a/parse/src/main/java/com/parse/ParseDecoder.java b/parse/src/main/java/com/parse/ParseDecoder.java deleted file mode 100644 index 2cb8a1634..000000000 --- a/parse/src/main/java/com/parse/ParseDecoder.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 android.util.Base64; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * A {@code ParseDecoder} can be used to transform JSON data structures into actual objects, such as - * {@link ParseObject}s. - * - * @see com.parse.ParseEncoder - */ -public class ParseDecoder { - - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final ParseDecoder INSTANCE = new ParseDecoder(); - - protected ParseDecoder() { - // do nothing - } - - public static ParseDecoder get() { - return INSTANCE; - } - - /* package */ List convertJSONArrayToList(JSONArray array) { - List list = new ArrayList<>(); - for (int i = 0; i < array.length(); ++i) { - list.add(decode(array.opt(i))); - } - return list; - } - - /* package */ Map convertJSONObjectToMap(JSONObject object) { - Map outputMap = new HashMap<>(); - Iterator it = object.keys(); - while (it.hasNext()) { - String key = it.next(); - Object value = object.opt(key); - outputMap.put(key, decode(value)); - } - return outputMap; - } - - /** - * Gets the ParseObject another object points to. By default a new - * object will be created. - */ - protected ParseObject decodePointer(String className, String objectId) { - return ParseObject.createWithoutData(className, objectId); - } - - public Object decode(Object object) { - if (object instanceof JSONArray) { - return convertJSONArrayToList((JSONArray) object); - } - - if (object == JSONObject.NULL) { - return null; - } - - if (!(object instanceof JSONObject)) { - return object; - } - - JSONObject jsonObject = (JSONObject) object; - - String opString = jsonObject.optString("__op", null); - if (opString != null) { - try { - return ParseFieldOperations.decode(jsonObject, this); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - String typeString = jsonObject.optString("__type", null); - if (typeString == null) { - return convertJSONObjectToMap(jsonObject); - } - - if (typeString.equals("Date")) { - String iso = jsonObject.optString("iso"); - return ParseDateFormat.getInstance().parse(iso); - } - - if (typeString.equals("Bytes")) { - String base64 = jsonObject.optString("base64"); - return Base64.decode(base64, Base64.NO_WRAP); - } - - if (typeString.equals("Pointer")) { - return decodePointer(jsonObject.optString("className"), - jsonObject.optString("objectId")); - } - - if (typeString.equals("File")) { - return new ParseFile(jsonObject, this); - } - - if (typeString.equals("GeoPoint")) { - double latitude, longitude; - try { - latitude = jsonObject.getDouble("latitude"); - longitude = jsonObject.getDouble("longitude"); - } catch (JSONException e) { - throw new RuntimeException(e); - } - return new ParseGeoPoint(latitude, longitude); - } - - if (typeString.equals("Polygon")) { - List coordinates = new ArrayList<>(); - try { - JSONArray array = jsonObject.getJSONArray("coordinates"); - for (int i = 0; i < array.length(); ++i) { - JSONArray point = array.getJSONArray(i); - coordinates.add(new ParseGeoPoint(point.getDouble(0), point.getDouble(1))); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - return new ParsePolygon(coordinates); - } - - if (typeString.equals("Object")) { - return ParseObject.fromJSON(jsonObject, null, this); - } - - if (typeString.equals("Relation")) { - return new ParseRelation<>(jsonObject, this); - } - - if (typeString.equals("OfflineObject")) { - throw new RuntimeException("An unexpected offline pointer was encountered."); - } - - return null; - } -} diff --git a/parse/src/main/java/com/parse/ParseDecoder.kt b/parse/src/main/java/com/parse/ParseDecoder.kt new file mode 100644 index 000000000..cc5ed86a5 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseDecoder.kt @@ -0,0 +1,135 @@ +/* + * 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 android.util.Base64 +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +/** + * A `ParseDecoder` can be used to transform JSON data structures into actual objects, such as + * [ParseObject]s. + * + * @see com.parse.ParseEncoder + */ +open class ParseDecoder protected constructor() { + /* package */ + fun convertJSONArrayToList(array: JSONArray): List { + val list: MutableList = ArrayList() + for (i in 0 until array.length()) { + list.add(decode(array.opt(i))) + } + return list + } + + /* package */ + fun convertJSONObjectToMap(`object`: JSONObject): Map { + val outputMap: MutableMap = HashMap() + val it = `object`.keys() + while (it.hasNext()) { + val key = it.next() + val value = `object`.opt(key) + outputMap[key] = decode(value) + } + return outputMap + } + + /** + * Gets the `ParseObject` another object points to. By default a new + * object will be created. + */ + protected open fun decodePointer(className: String?, objectId: String?): ParseObject? { + return ParseObject.createWithoutData(className, objectId) + } + + open fun decode(`object`: Any): Any? { + if (`object` is JSONArray) { + return convertJSONArrayToList(`object`) + } + if (`object` === JSONObject.NULL) { + return null + } + if (`object` !is JSONObject) { + return `object` + } + val opString = `object`.optString("__op", null) + if (opString != null) { + return try { + ParseFieldOperations.decode(`object`, this) + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + val typeString = `object`.optString("__type", null) + ?: return convertJSONObjectToMap(`object`) + if (typeString == "Date") { + val iso = `object`.optString("iso") + return ParseDateFormat.getInstance().parse(iso) + } + if (typeString == "Bytes") { + val base64 = `object`.optString("base64") + return Base64.decode(base64, Base64.NO_WRAP) + } + if (typeString == "Pointer") { + return decodePointer( + `object`.optString("className"), + `object`.optString("objectId") + ) + } + if (typeString == "File") { + return ParseFile(`object`, this) + } + if (typeString == "GeoPoint") { + val latitude: Double + val longitude: Double + try { + latitude = `object`.getDouble("latitude") + longitude = `object`.getDouble("longitude") + } catch (e: JSONException) { + throw RuntimeException(e) + } + return ParseGeoPoint(latitude, longitude) + } + if (typeString == "Polygon") { + val coordinates: MutableList = ArrayList() + try { + val array = `object`.getJSONArray("coordinates") + for (i in 0 until array.length()) { + val point = array.getJSONArray(i) + coordinates.add(ParseGeoPoint(point.getDouble(0), point.getDouble(1))) + } + } catch (e: JSONException) { + throw RuntimeException(e) + } + return ParsePolygon(coordinates) + } + if (typeString == "Object") { + return ParseObject.fromJSON(`object`, null, this) + } + if (typeString == "Relation") { + return ParseRelation(`object`, this) + } + if (typeString == "OfflineObject") { + throw RuntimeException("An unexpected offline pointer was encountered.") + } + return null + } + + companion object { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = ParseDecoder() + @JvmStatic + fun get(): ParseDecoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseEncoder.java b/parse/src/main/java/com/parse/ParseEncoder.java deleted file mode 100644 index bc1d3f535..000000000 --- a/parse/src/main/java/com/parse/ParseEncoder.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 android.util.Base64; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * A {@code ParseEncoder} can be used to transform objects such as {@link ParseObject}s into JSON - * data structures. - * - * @see com.parse.ParseDecoder - */ -public abstract class ParseEncoder { - - /* package */ - static boolean isValidType(Object value) { - return value instanceof String - || value instanceof Number - || value instanceof Boolean - || value instanceof Date - || value instanceof List - || value instanceof Map - || value instanceof byte[] - || value == JSONObject.NULL - || value instanceof ParseObject - || value instanceof ParseACL - || value instanceof ParseFile - || value instanceof ParseGeoPoint - || value instanceof ParsePolygon - || value instanceof ParseRelation; - } - - public Object encode(Object object) { - try { - if (object instanceof ParseObject) { - return encodeRelatedObject((ParseObject) object); - } - - // TODO(grantland): Remove once we disallow mutable nested queries t6941155 - if (object instanceof ParseQuery.State.Builder) { - ParseQuery.State.Builder builder = (ParseQuery.State.Builder) object; - return encode(builder.build()); - } - - if (object instanceof ParseQuery.State) { - ParseQuery.State state = (ParseQuery.State) object; - return state.toJSON(this); - } - - if (object instanceof Date) { - return encodeDate((Date) object); - } - - if (object instanceof byte[]) { - JSONObject json = new JSONObject(); - json.put("__type", "Bytes"); - json.put("base64", Base64.encodeToString((byte[]) object, Base64.NO_WRAP)); - return json; - } - - if (object instanceof ParseFile) { - return ((ParseFile) object).encode(); - } - - if (object instanceof ParseGeoPoint) { - ParseGeoPoint point = (ParseGeoPoint) object; - JSONObject json = new JSONObject(); - json.put("__type", "GeoPoint"); - json.put("latitude", point.getLatitude()); - json.put("longitude", point.getLongitude()); - return json; - } - - if (object instanceof ParsePolygon) { - ParsePolygon polygon = (ParsePolygon) object; - JSONObject json = new JSONObject(); - json.put("__type", "Polygon"); - json.put("coordinates", polygon.coordinatesToJSONArray()); - return json; - } - - if (object instanceof ParseACL) { - ParseACL acl = (ParseACL) object; - return acl.toJSONObject(this); - } - - if (object instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) object; - JSONObject json = new JSONObject(); - for (Map.Entry pair : map.entrySet()) { - json.put(pair.getKey(), encode(pair.getValue())); - } - return json; - } - - if (object instanceof Collection) { - JSONArray array = new JSONArray(); - for (Object item : (Collection) object) { - array.put(encode(item)); - } - return array; - } - - if (object instanceof ParseRelation) { - ParseRelation relation = (ParseRelation) object; - return relation.encodeToJSON(this); - } - - if (object instanceof ParseFieldOperation) { - return ((ParseFieldOperation) object).encode(this); - } - - if (object instanceof ParseQuery.RelationConstraint) { - return ((ParseQuery.RelationConstraint) object).encode(this); - } - - if (object == null) { - return JSONObject.NULL; - } - - } catch (JSONException e) { - throw new RuntimeException(e); - } - - // String, Number, Boolean, - if (isValidType(object)) { - return object; - } - - throw new IllegalArgumentException("invalid type for ParseObject: " - + object.getClass().toString()); - } - - protected abstract JSONObject encodeRelatedObject(ParseObject object); - - protected JSONObject encodeDate(Date date) { - JSONObject object = new JSONObject(); - String iso = ParseDateFormat.getInstance().format(date); - try { - object.put("__type", "Date"); - object.put("iso", iso); - } catch (JSONException e) { - // This should not happen - throw new RuntimeException(e); - } - return object; - } -} diff --git a/parse/src/main/java/com/parse/ParseEncoder.kt b/parse/src/main/java/com/parse/ParseEncoder.kt new file mode 100644 index 000000000..12e5a253f --- /dev/null +++ b/parse/src/main/java/com/parse/ParseEncoder.kt @@ -0,0 +1,141 @@ +/* + * 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 android.util.Base64 +import com.parse.ParseQuery.RelationConstraint +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +/** + * A `ParseEncoder` can be used to transform objects such as [ParseObject]s into JSON + * data structures. + * + * @see com.parse.ParseDecoder + */ +abstract class ParseEncoder { + fun encode(`object`: Any?): Any { + try { + if (`object` is ParseObject) { + return encodeRelatedObject(`object`) + } + + // TODO(grantland): Remove once we disallow mutable nested queries t6941155 + if (`object` is ParseQuery.State.Builder<*>) { + return encode(`object`.build()) + } + if (`object` is ParseQuery.State<*>) { + return `object`.toJSON(this) + } + if (`object` is Date) { + return encodeDate(`object` as Date?) + } + if (`object` is ByteArray) { + val json = JSONObject() + json.put("__type", "Bytes") + json.put("base64", Base64.encodeToString(`object` as ByteArray?, Base64.NO_WRAP)) + return json + } + if (`object` is ParseFile) { + return `object`.encode() + } + if (`object` is ParseGeoPoint) { + val json = JSONObject() + json.put("__type", "GeoPoint") + json.put("latitude", `object`.latitude) + json.put("longitude", `object`.longitude) + return json + } + if (`object` is ParsePolygon) { + val json = JSONObject() + json.put("__type", "Polygon") + json.put("coordinates", `object`.coordinatesToJSONArray()) + return json + } + if (`object` is ParseACL) { + return `object`.toJSONObject(this) + } + if (`object` is Map<*, *>) { + val map = `object` as Map + val json = JSONObject() + for ((key, value) in map) { + json.put(key, encode(value)) + } + return json + } + if (`object` is Collection<*>) { + val array = JSONArray() + for (item in `object`) { + array.put(encode(item)) + } + return array + } + if (`object` is ParseRelation<*>) { + return `object`.encodeToJSON(this) + } + if (`object` is ParseFieldOperation) { + return `object`.encode(this) + } + if (`object` is RelationConstraint) { + return `object`.encode(this) + } + if (`object` == null) { + return JSONObject.NULL + } + } catch (e: JSONException) { + throw RuntimeException(e) + } + + // String, Number, Boolean, + if (isValidType(`object`)) { + return `object` + } + throw IllegalArgumentException( + "invalid type for ParseObject: " + + `object`.javaClass.toString() + ) + } + + abstract fun encodeRelatedObject(`object`: ParseObject): JSONObject + + protected fun encodeDate(date: Date?): JSONObject { + val `object` = JSONObject() + val iso = ParseDateFormat.getInstance().format(date) + try { + `object`.put("__type", "Date") + `object`.put("iso", iso) + } catch (e: JSONException) { + // This should not happen + throw RuntimeException(e) + } + return `object` + } + + companion object { + /* package */ + @JvmStatic + fun isValidType(value: Any): Boolean { + return (value is String + || value is Number + || value is Boolean + || value is Date + || value is List<*> + || value is Map<*, *> + || value is ByteArray + || value === JSONObject.NULL || value is ParseObject + || value is ParseACL + || value is ParseFile + || value is ParseGeoPoint + || value is ParsePolygon + || value is ParseRelation<*>) + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseEventuallyQueue.java b/parse/src/main/java/com/parse/ParseEventuallyQueue.java deleted file mode 100644 index 8152b6b64..000000000 --- a/parse/src/main/java/com/parse/ParseEventuallyQueue.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * 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 android.util.SparseArray; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -import com.parse.boltsinternal.Task; - -/* package */ abstract class ParseEventuallyQueue { - - private boolean isConnected; - - /** - * Gets notifications of various events happening in the command cache, so that tests can be - * synchronized. - */ - private TestHelper testHelper; - - public abstract void onDestroy(); - - public boolean isConnected() { - return isConnected; - } - - public void setConnected(boolean connected) { - isConnected = connected; - } - - public abstract int pendingCount(); - - public void setTimeoutRetryWaitSeconds(double seconds) { - // do nothing - } - - public void setMaxCacheSizeBytes(int bytes) { - // do nothing - } - - /** - * See class TestHelper below. - */ - public TestHelper getTestHelper() { - if (testHelper == null) { - testHelper = new TestHelper(); - } - return testHelper; - } - - protected void notifyTestHelper(int event) { - notifyTestHelper(event, null); - } - - protected void notifyTestHelper(int event, Throwable t) { - if (testHelper != null) { - testHelper.notify(event, t); - } - } - - public abstract void pause(); - - public abstract void resume(); - - /** - * Attempts to run the given command and any pending commands. Adds the command to the pending set - * if it can't be run yet. - * - * @param command - The command to run. - * @param object - If this command references an unsaved object, we need to remove any previous command - * referencing that unsaved object. Otherwise, it will get created over and over again. - * So object is a reference to the object, if it has no objectId. Otherwise, it can be - * null. - */ - public abstract Task enqueueEventuallyAsync(ParseRESTCommand command, - ParseObject object); - - protected ParseRESTCommand commandFromJSON(JSONObject json) - throws JSONException { - ParseRESTCommand command = null; - if (ParseRESTCommand.isValidCommandJSONObject(json)) { - command = ParseRESTCommand.fromJSONObject(json); - } else if (ParseRESTCommand.isValidOldFormatCommandJSONObject(json)) { - // do nothing - } else { - throw new JSONException("Failed to load command from JSON."); - } - return command; - } - - /* package */ Task waitForOperationSetAndEventuallyPin(ParseOperationSet operationSet, - EventuallyPin eventuallyPin) { - return Task.forResult(null); - } - - /* package */ - abstract void simulateReboot(); - - /** - * Gets rid of all pending commands. - */ - public abstract void clear(); - - /** - * Fakes an object update notification for use in tests. This is used by saveEventually to make it - * look like test code has updated an object through the command cache even if it actually - * avoided executing update by determining the object wasn't dirty. - */ - void fakeObjectUpdate() { - if (testHelper != null) { - testHelper.notify(TestHelper.COMMAND_ENQUEUED); - testHelper.notify(TestHelper.COMMAND_SUCCESSFUL); - testHelper.notify(TestHelper.OBJECT_UPDATED); - } - } - - /** - * Gets notifications of various events happening in the command cache, so that tests can be - * synchronized. See ParseCommandCacheTest for examples of how to use this. - */ - public static class TestHelper { - public static final int COMMAND_SUCCESSFUL = 1; - public static final int COMMAND_FAILED = 2; - public static final int COMMAND_ENQUEUED = 3; - public static final int COMMAND_NOT_ENQUEUED = 4; - public static final int OBJECT_UPDATED = 5; - public static final int OBJECT_REMOVED = 6; - public static final int NETWORK_DOWN = 7; - public static final int COMMAND_OLD_FORMAT_DISCARDED = 8; - private static final int MAX_EVENTS = 1000; - private SparseArray events = new SparseArray<>(); - - private TestHelper() { - clear(); - } - - public static String getEventString(int event) { - switch (event) { - case COMMAND_SUCCESSFUL: - return "COMMAND_SUCCESSFUL"; - case COMMAND_FAILED: - return "COMMAND_FAILED"; - case COMMAND_ENQUEUED: - return "COMMAND_ENQUEUED"; - case COMMAND_NOT_ENQUEUED: - return "COMMAND_NOT_ENQUEUED"; - case OBJECT_UPDATED: - return "OBJECT_UPDATED"; - case OBJECT_REMOVED: - return "OBJECT_REMOVED"; - case NETWORK_DOWN: - return "NETWORK_DOWN"; - case COMMAND_OLD_FORMAT_DISCARDED: - return "COMMAND_OLD_FORMAT_DISCARDED"; - default: - throw new IllegalStateException("Encountered unknown event: " + event); - } - } - - public void clear() { - events.clear(); - events.put(COMMAND_SUCCESSFUL, new Semaphore(MAX_EVENTS)); - events.put(COMMAND_FAILED, new Semaphore(MAX_EVENTS)); - events.put(COMMAND_ENQUEUED, new Semaphore(MAX_EVENTS)); - events.put(COMMAND_NOT_ENQUEUED, new Semaphore(MAX_EVENTS)); - events.put(OBJECT_UPDATED, new Semaphore(MAX_EVENTS)); - events.put(OBJECT_REMOVED, new Semaphore(MAX_EVENTS)); - events.put(NETWORK_DOWN, new Semaphore(MAX_EVENTS)); - events.put(COMMAND_OLD_FORMAT_DISCARDED, new Semaphore(MAX_EVENTS)); - for (int i = 0; i < events.size(); i++) { - int event = events.keyAt(i); - events.get(event).acquireUninterruptibly(MAX_EVENTS); - } - } - - public int unexpectedEvents() { - int sum = 0; - for (int i = 0; i < events.size(); i++) { - int event = events.keyAt(i); - sum += events.get(event).availablePermits(); - } - return sum; - } - - public List getUnexpectedEvents() { - List unexpectedEvents = new ArrayList<>(); - for (int i = 0; i < events.size(); i++) { - int event = events.keyAt(i); - if (events.get(event).availablePermits() > 0) { - unexpectedEvents.add(getEventString(event)); - } - } - return unexpectedEvents; - } - - public void notify(int event) { - notify(event, null); - } - - public void notify(int event, Throwable t) { - events.get(event).release(); - } - - public boolean waitFor(int event) { - return waitFor(event, 1); - } - - public boolean waitFor(int event, int permits) { - try { - return events.get(event).tryAcquire(permits, 10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - return false; - } - } - } -} diff --git a/parse/src/main/java/com/parse/ParseEventuallyQueue.kt b/parse/src/main/java/com/parse/ParseEventuallyQueue.kt new file mode 100644 index 000000000..f1db1eb7a --- /dev/null +++ b/parse/src/main/java/com/parse/ParseEventuallyQueue.kt @@ -0,0 +1,198 @@ +/* + * 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 android.util.SparseArray +import com.parse.ParseRESTCommand.Companion.fromJSONObject +import com.parse.ParseRESTCommand.Companion.isValidCommandJSONObject +import com.parse.ParseRESTCommand.Companion.isValidOldFormatCommandJSONObject +import com.parse.boltsinternal.Task +import org.json.JSONException +import org.json.JSONObject +import java.util.* +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit + +/* package */ +internal abstract class ParseEventuallyQueue { + open var isConnected = false + + /** + * Gets notifications of various events happening in the command cache, so that tests can be + * synchronized. + */ + private val testHelper = TestHelper() + abstract fun onDestroy() + abstract fun pendingCount(): Int + open fun setTimeoutRetryWaitSeconds(seconds: Double) { + // do nothing + } + + open fun setMaxCacheSizeBytes(bytes: Int) { + // do nothing + } + + internal fun notifyTestHelper(event: Int, t: Throwable? = null) { + testHelper.notify(event, t) + } + + abstract fun pause() + abstract fun resume() + + /** + * Attempts to run the given command and any pending commands. Adds the command to the pending set + * if it can't be run yet. + * + * @param command - The command to run. + * @param object - If this command references an unsaved object, we need to remove any previous command + * referencing that unsaved object. Otherwise, it will get created over and over again. + * So object is a reference to the object, if it has no objectId. Otherwise, it can be + * null. + */ + abstract fun enqueueEventuallyAsync( + command: ParseRESTCommand, + `object`: ParseObject? + ): Task + + @Throws(JSONException::class) + protected fun commandFromJSON(json: JSONObject?): ParseRESTCommand? { + var command: ParseRESTCommand? = null + + json?.let { + when { + isValidCommandJSONObject(json) -> { + command = fromJSONObject(json) + } + isValidOldFormatCommandJSONObject(json) -> { + // do nothing + } + else -> { + throw JSONException("Failed to load command from JSON.") + } + } + } + + return command + } + + /* package */ + open fun waitForOperationSetAndEventuallyPin( + operationSet: ParseOperationSet?, + eventuallyPin: EventuallyPin? + ): Task? { + return Task.forResult(null) + } + + /* package */ + abstract fun simulateReboot() + + /** + * Gets rid of all pending commands. + */ + abstract fun clear() + + /** + * Fakes an object update notification for use in tests. This is used by saveEventually to make it + * look like test code has updated an object through the command cache even if it actually + * avoided executing update by determining the object wasn't dirty. + */ + open fun fakeObjectUpdate() { + testHelper.notify(TestHelper.COMMAND_ENQUEUED) + testHelper.notify(TestHelper.COMMAND_SUCCESSFUL) + testHelper.notify(TestHelper.OBJECT_UPDATED) + } + + /** + * Gets notifications of various events happening in the command cache, so that tests can be + * synchronized. See ParseCommandCacheTest for examples of how to use this. + */ + class TestHelper { + private val events = SparseArray() + fun clear() { + events.clear() + events.put(COMMAND_SUCCESSFUL, Semaphore(MAX_EVENTS)) + events.put(COMMAND_FAILED, Semaphore(MAX_EVENTS)) + events.put(COMMAND_ENQUEUED, Semaphore(MAX_EVENTS)) + events.put(COMMAND_NOT_ENQUEUED, Semaphore(MAX_EVENTS)) + events.put(OBJECT_UPDATED, Semaphore(MAX_EVENTS)) + events.put(OBJECT_REMOVED, Semaphore(MAX_EVENTS)) + events.put(NETWORK_DOWN, Semaphore(MAX_EVENTS)) + events.put(COMMAND_OLD_FORMAT_DISCARDED, Semaphore(MAX_EVENTS)) + for (i in 0 until events.size()) { + val event = events.keyAt(i) + events[event].acquireUninterruptibly(MAX_EVENTS) + } + } + + fun unexpectedEvents(): Int { + var sum = 0 + for (i in 0 until events.size()) { + val event = events.keyAt(i) + sum += events[event].availablePermits() + } + return sum + } + + val unexpectedEvents: List + get() { + val unexpectedEvents: MutableList = ArrayList() + for (i in 0 until events.size()) { + val event = events.keyAt(i) + if (events[event].availablePermits() > 0) { + unexpectedEvents.add(getEventString(event)) + } + } + return unexpectedEvents + } + + @JvmOverloads + fun notify(event: Int, t: Throwable? = null) { + events[event].release() + } + + @JvmOverloads + fun waitFor(event: Int, permits: Int = 1): Boolean { + return try { + events[event].tryAcquire(permits, 10, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + e.printStackTrace() + false + } + } + + companion object { + const val COMMAND_SUCCESSFUL = 1 + const val COMMAND_FAILED = 2 + const val COMMAND_ENQUEUED = 3 + const val COMMAND_NOT_ENQUEUED = 4 + const val OBJECT_UPDATED = 5 + const val OBJECT_REMOVED = 6 + const val NETWORK_DOWN = 7 + const val COMMAND_OLD_FORMAT_DISCARDED = 8 + private const val MAX_EVENTS = 1000 + fun getEventString(event: Int): String { + return when (event) { + COMMAND_SUCCESSFUL -> "COMMAND_SUCCESSFUL" + COMMAND_FAILED -> "COMMAND_FAILED" + COMMAND_ENQUEUED -> "COMMAND_ENQUEUED" + COMMAND_NOT_ENQUEUED -> "COMMAND_NOT_ENQUEUED" + OBJECT_UPDATED -> "OBJECT_UPDATED" + OBJECT_REMOVED -> "OBJECT_REMOVED" + NETWORK_DOWN -> "NETWORK_DOWN" + COMMAND_OLD_FORMAT_DISCARDED -> "COMMAND_OLD_FORMAT_DISCARDED" + else -> throw IllegalStateException("Encountered unknown event: $event") + } + } + } + + init { + clear() + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseException.java b/parse/src/main/java/com/parse/ParseException.java index 1b2124379..60c582e53 100644 --- a/parse/src/main/java/com/parse/ParseException.java +++ b/parse/src/main/java/com/parse/ParseException.java @@ -200,7 +200,7 @@ public class ParseException extends Exception { */ public static final int UNSUPPORTED_SERVICE = 252; private static final long serialVersionUID = 1; - private int code; + private final int code; /** * Construct a new ParseException with a particular error code. diff --git a/parse/src/main/java/com/parse/ParseExecutors.java b/parse/src/main/java/com/parse/ParseExecutors.java index e0a0e7d57..6f6f4cc10 100644 --- a/parse/src/main/java/com/parse/ParseExecutors.java +++ b/parse/src/main/java/com/parse/ParseExecutors.java @@ -8,11 +8,11 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; -import com.parse.boltsinternal.Task; - class ParseExecutors { private static final Object SCHEDULED_EXECUTOR_LOCK = new Object(); diff --git a/parse/src/main/java/com/parse/ParseFieldOperations.java b/parse/src/main/java/com/parse/ParseFieldOperations.java index b78246730..c08155357 100644 --- a/parse/src/main/java/com/parse/ParseFieldOperations.java +++ b/parse/src/main/java/com/parse/ParseFieldOperations.java @@ -20,7 +20,7 @@ */ /* package */ final class ParseFieldOperations { // A map of all known decoders. - private static Map opDecoderMap = new HashMap<>(); + private static final Map opDecoderMap = new HashMap<>(); private ParseFieldOperations() { } diff --git a/parse/src/main/java/com/parse/ParseFile.java b/parse/src/main/java/com/parse/ParseFile.java index edf745d72..65929a5d1 100644 --- a/parse/src/main/java/com/parse/ParseFile.java +++ b/parse/src/main/java/com/parse/ParseFile.java @@ -11,6 +11,10 @@ import android.os.Parcel; import android.os.Parcelable; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.json.JSONException; import org.json.JSONObject; @@ -23,10 +27,6 @@ import java.util.Set; import java.util.concurrent.Callable; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - /** * {@code ParseFile} is a local representation of a file that is saved to the Parse cloud. *

@@ -57,6 +57,8 @@ public ParseFile[] newArray(int size) { } }; /* package for tests */ final TaskQueue taskQueue = new TaskQueue(); + private final Set> currentTasks = Collections.synchronizedSet( + new HashSet<>()); /** * Staging of {@code ParseFile}'s data is stored in memory until the {@code ParseFile} has been * successfully synced with the server. @@ -64,8 +66,6 @@ public ParseFile[] newArray(int size) { /* package for tests */ byte[] data; /* package for tests */ File file; private State state; - private Set> currentTasks = Collections.synchronizedSet( - new HashSet>()); /** * Creates a new file from a file pointer. @@ -75,6 +75,7 @@ public ParseFile[] newArray(int size) { 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. @@ -186,18 +187,10 @@ private static ProgressCallback progressCallbackOnMainThread( return null; } - return new ProgressCallback() { - @Override - public void done(final Integer percentDone) { - Task.call(new Callable() { - @Override - public Void call() { - progressCallback.done(percentDone); - return null; - } - }, ParseExecutors.main()); - } - }; + return percentDone -> Task.call((Callable) () -> { + progressCallback.done(percentDone); + return null; + }, ParseExecutors.main()); } /* package for tests */ State getState() { @@ -259,45 +252,39 @@ private Task saveAsync(final String sessionToken, } // 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) { - 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) { - 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(); - } - }); + return toAwait.continueWithTask(task -> { + 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(task1 -> { + state = task1.getResult(); + // Since we have successfully uploaded the file, we do not need to hold the file pointer + // anymore. + data = null; + file = null; + return task1.makeVoid(); + }); }); } @@ -312,30 +299,19 @@ public Task saveInBackground(final ProgressCallback uploadProgressCallback final TaskCompletionSource cts = new TaskCompletionSource<>(); currentTasks.add(cts); - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return saveAsync(sessionToken, uploadProgressCallback, cts.getTask()); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - cts.trySetResult(null); // release - currentTasks.remove(cts); - return task; - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + final String sessionToken = task.getResult(); + return saveAsync(sessionToken, uploadProgressCallback, cts.getTask()); + }).continueWithTask(task -> { + 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) { - return saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken); - } - }); + return taskQueue.enqueue(toAwait -> saveAsync(sessionToken, uploadProgressCallback, toAwait, cancellationToken)); } /** @@ -388,29 +364,18 @@ public Task getDataInBackground(final ProgressCallback progressCallback) final TaskCompletionSource cts = new TaskCompletionSource<>(); currentTasks.add(cts); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(new Continuation() { - @Override - public byte[] then(Task task) { - File file = task.getResult(); - try { - return ParseFileUtils.readFileToByteArray(file); - } catch (IOException e) { - // do nothing - } - return null; - } - }); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - cts.trySetResult(null); // release - currentTasks.remove(cts); - return task; + return taskQueue.enqueue(toAwait -> fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(task -> { + File file = task.getResult(); + try { + return ParseFileUtils.readFileToByteArray(file); + } catch (IOException e) { + // do nothing } + return null; + })).continueWithTask(task -> { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; }); } @@ -470,18 +435,10 @@ public Task getFileInBackground(final ProgressCallback progressCallback) { final TaskCompletionSource cts = new TaskCompletionSource<>(); currentTasks.add(cts); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return fetchInBackground(progressCallback, toAwait, cts.getTask()); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - cts.trySetResult(null); // release - currentTasks.remove(cts); - return task; - } + return taskQueue.enqueue(toAwait -> fetchInBackground(progressCallback, toAwait, cts.getTask())).continueWithTask(task -> { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; }); } @@ -548,23 +505,10 @@ public Task getDataStreamInBackground(final ProgressCallback progre final TaskCompletionSource cts = new TaskCompletionSource<>(); currentTasks.add(cts); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - 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) { - cts.trySetResult(null); // release - currentTasks.remove(cts); - return task; - } + return taskQueue.enqueue((Continuation>) toAwait -> fetchInBackground(progressCallback, toAwait, cts.getTask()).onSuccess(task -> new FileInputStream(task.getResult()))).continueWithTask(task -> { + cts.trySetResult(null); // release + currentTasks.remove(cts); + return task; }); } @@ -614,18 +558,15 @@ private Task fetchInBackground( return Task.cancelled(); } - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } - return getFileController().fetchAsync( - state, - null, - progressCallbackOnMainThread(progressCallback), - cancellationToken); + return toAwait.continueWithTask(task -> { + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); } + return getFileController().fetchAsync( + state, + null, + progressCallbackOnMainThread(progressCallback), + cancellationToken); }); } @@ -685,6 +626,7 @@ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { 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; diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index a6ba29316..be15a9a9e 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -8,18 +8,15 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import org.json.JSONObject; import java.io.File; import java.io.IOException; -import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - // TODO(grantland): Create ParseFileController interface class ParseFileController { @@ -104,24 +101,21 @@ public Task saveAsync( uploadProgressCallback, null, cancellationToken - ).onSuccess(new Continuation() { - @Override - public ParseFile.State then(Task task) throws Exception { - JSONObject result = task.getResult(); - ParseFile.State newState = new ParseFile.State.Builder(state) - .name(result.getString("name")) - .url(result.getString("url")) - .build(); - - // Write data to cache - try { - ParseFileUtils.writeByteArrayToFile(getCacheFile(newState), data); - } catch (IOException e) { - // do nothing - } - - return newState; + ).onSuccess(task -> { + JSONObject result = task.getResult(); + ParseFile.State newState = new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.writeByteArrayToFile(getCacheFile(newState), data); + } catch (IOException e) { + // do nothing } + + return newState; }, ParseExecutors.io()); } @@ -150,24 +144,21 @@ public Task saveAsync( uploadProgressCallback, null, cancellationToken - ).onSuccess(new Continuation() { - @Override - public ParseFile.State then(Task task) throws Exception { - JSONObject result = task.getResult(); - ParseFile.State newState = new ParseFile.State.Builder(state) - .name(result.getString("name")) - .url(result.getString("url")) - .build(); - - // Write data to cache - try { - ParseFileUtils.copyFile(file, getCacheFile(newState)); - } catch (IOException e) { - // do nothing - } - - return newState; + ).onSuccess(task -> { + JSONObject result = task.getResult(); + ParseFile.State newState = new ParseFile.State.Builder(state) + .name(result.getString("name")) + .url(result.getString("url")) + .build(); + + // Write data to cache + try { + ParseFileUtils.copyFile(file, getCacheFile(newState)); + } catch (IOException e) { + // do nothing } + + return newState; }, ParseExecutors.io()); } @@ -180,59 +171,48 @@ public Task fetchAsync( return Task.cancelled(); } final File cacheFile = getCacheFile(state); - return Task.call(new Callable() { - @Override - public Boolean call() { - return cacheFile.exists(); + return Task.call(cacheFile::exists, ParseExecutors.io()).continueWithTask(task -> { + boolean result = task.getResult(); + if (result) { + return Task.forResult(cacheFile); } - }, ParseExecutors.io()).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - boolean result = task.getResult(); - if (result) { - return Task.forResult(cacheFile); - } + if (cancellationToken != null && cancellationToken.isCancelled()) { + return Task.cancelled(); + } + + // Generate the temp file path for caching ParseFile content based on ParseFile's url + // The reason we do not write to the cacheFile directly is because there is no way we can + // verify if a cacheFile is complete or not. If download is interrupted in the middle, next + // time when we download the ParseFile, since cacheFile has already existed, we will return + // this incomplete cacheFile + final File tempFile = getTempFile(state); + + // network + final ParseFileRequest request = + new ParseFileRequest(ParseHttpRequest.Method.GET, state.url(), tempFile); + + // We do not need to delete the temp file since we always try to overwrite it + return request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken).continueWithTask(task1 -> { + // If the top-level task was cancelled, don't actually set the data -- just move on. if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); + throw new CancellationException(); + } + if (task1.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + return task1.cast(); } - // Generate the temp file path for caching ParseFile content based on ParseFile's url - // The reason we do not write to the cacheFile directly is because there is no way we can - // verify if a cacheFile is complete or not. If download is interrupted in the middle, next - // time when we download the ParseFile, since cacheFile has already existed, we will return - // this incomplete cacheFile - final File tempFile = getTempFile(state); - - // network - final ParseFileRequest request = - new ParseFileRequest(ParseHttpRequest.Method.GET, state.url(), tempFile); - - // We do not need to delete the temp file since we always try to overwrite it - return request.executeAsync( - fileClient(), - null, - downloadProgressCallback, - cancellationToken).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - // If the top-level task was cancelled, don't actually set the data -- just move on. - if (cancellationToken != null && cancellationToken.isCancelled()) { - throw new CancellationException(); - } - if (task.isFaulted()) { - ParseFileUtils.deleteQuietly(tempFile); - return task.cast(); - } - - // Since we give the cacheFile pointer to developers, it is not safe to guarantee - // cacheFile always does not exist here, so it is better to delete it manually, - // otherwise moveFile may throw an exception. - ParseFileUtils.deleteQuietly(cacheFile); - ParseFileUtils.moveFile(tempFile, cacheFile); - return Task.forResult(cacheFile); - } - }, ParseExecutors.io()); - } + // Since we give the cacheFile pointer to developers, it is not safe to guarantee + // cacheFile always does not exist here, so it is better to delete it manually, + // otherwise moveFile may throw an exception. + ParseFileUtils.deleteQuietly(cacheFile); + ParseFileUtils.moveFile(tempFile, cacheFile); + return Task.forResult(cacheFile); + }, ParseExecutors.io()); }); } } diff --git a/parse/src/main/java/com/parse/ParseFileRequest.java b/parse/src/main/java/com/parse/ParseFileRequest.java index 49eaf4d07..24f8e1301 100644 --- a/parse/src/main/java/com/parse/ParseFileRequest.java +++ b/parse/src/main/java/com/parse/ParseFileRequest.java @@ -8,15 +8,13 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Task; /** * Request returns a byte array of the response and provides a callback the progress of the data @@ -48,34 +46,31 @@ protected Task onResponseAsync(final ParseHttpResponse response, return null; } - return Task.call(new Callable() { - @Override - public Void call() throws Exception { - long totalSize = response.getTotalSize(); - long downloadedSize = 0; - InputStream responseStream = null; - FileOutputStream tempFileStream = null; - try { - responseStream = response.getContent(); - tempFileStream = ParseFileUtils.openOutputStream(tempFile); + return Task.call(() -> { + long totalSize = response.getTotalSize(); + long downloadedSize = 0; + InputStream responseStream = null; + FileOutputStream tempFileStream = null; + try { + responseStream = response.getContent(); + tempFileStream = ParseFileUtils.openOutputStream(tempFile); - int nRead; - byte[] data = new byte[32 << 10]; // 32KB + int nRead; + byte[] data = new byte[32 << 10]; // 32KB - while ((nRead = responseStream.read(data, 0, data.length)) != -1) { - tempFileStream.write(data, 0, nRead); - downloadedSize += nRead; - if (downloadProgressCallback != null && totalSize != -1) { - int progressToReport = - Math.round((float) downloadedSize / (float) totalSize * 100.0f); - downloadProgressCallback.done(progressToReport); - } + while ((nRead = responseStream.read(data, 0, data.length)) != -1) { + tempFileStream.write(data, 0, nRead); + downloadedSize += nRead; + if (downloadProgressCallback != null && totalSize != -1) { + int progressToReport = + Math.round((float) downloadedSize / (float) totalSize * 100.0f); + downloadProgressCallback.done(progressToReport); } - return null; - } finally { - ParseIOUtils.closeQuietly(responseStream); - ParseIOUtils.closeQuietly(tempFileStream); } + return null; + } finally { + ParseIOUtils.closeQuietly(responseStream); + ParseIOUtils.closeQuietly(tempFileStream); } }, ParseExecutors.io()); } diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java index 046f16a6e..c45a6bebd 100644 --- a/parse/src/main/java/com/parse/ParseFileUtils.java +++ b/parse/src/main/java/com/parse/ParseFileUtils.java @@ -28,6 +28,7 @@ import java.io.OutputStream; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * General file manipulation utilities. @@ -537,7 +538,7 @@ public static JSONObject readFileToJSONObject(File file) throws IOException, JSO * Writes a {@link JSONObject} to a file creating the file if it does not exist. */ public static void writeJSONObjectToFile(File file, JSONObject json) throws IOException { - ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes(Charset.forName("UTF-8"))); + ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes(StandardCharsets.UTF_8)); } //endregion diff --git a/parse/src/main/java/com/parse/ParseGeoPoint.java b/parse/src/main/java/com/parse/ParseGeoPoint.java index 87e1d12c7..d27a151e6 100644 --- a/parse/src/main/java/com/parse/ParseGeoPoint.java +++ b/parse/src/main/java/com/parse/ParseGeoPoint.java @@ -13,11 +13,10 @@ import android.os.Parcel; import android.os.Parcelable; -import java.util.Locale; - -import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; +import java.util.Locale; + /** * {@code ParseGeoPoint} represents a latitude / longitude point that may be associated with a key * in a {@link ParseObject} or used as a reference point for geo queries. This allows proximity @@ -46,8 +45,8 @@ public ParseGeoPoint[] newArray(int size) { return new ParseGeoPoint[size]; } }; - static double EARTH_MEAN_RADIUS_KM = 6371.0; - static double EARTH_MEAN_RADIUS_MILE = 3958.8; + static final double EARTH_MEAN_RADIUS_KM = 6371.0; + static final double EARTH_MEAN_RADIUS_MILE = 3958.8; private double latitude = 0.0; private double longitude = 0.0; @@ -120,12 +119,9 @@ public static Task getCurrentLocationInBackground(long timeout) { criteria.setAccuracy(Criteria.NO_REQUIREMENT); criteria.setPowerRequirement(Criteria.NO_REQUIREMENT); return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria) - .onSuccess(new Continuation() { - @Override - public ParseGeoPoint then(Task task) { - Location location = task.getResult(); - return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); - } + .onSuccess(task -> { + Location location = task.getResult(); + return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); }); } @@ -167,12 +163,9 @@ public static void getCurrentLocationInBackground(long timeout, LocationCallback */ public static Task getCurrentLocationInBackground(long timeout, Criteria criteria) { return LocationNotifier.getCurrentLocationAsync(Parse.getApplicationContext(), timeout, criteria) - .onSuccess(new Continuation() { - @Override - public ParseGeoPoint then(Task task) { - Location location = task.getResult(); - return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); - } + .onSuccess(task -> { + Location location = task.getResult(); + return new ParseGeoPoint(location.getLatitude(), location.getLongitude()); }); } diff --git a/parse/src/main/java/com/parse/ParseHttpClient.java b/parse/src/main/java/com/parse/ParseHttpClient.java index affe8673f..912810a7c 100644 --- a/parse/src/main/java/com/parse/ParseHttpClient.java +++ b/parse/src/main/java/com/parse/ParseHttpClient.java @@ -34,7 +34,7 @@ */ class ParseHttpClient { - private OkHttpClient okHttpClient; + private final OkHttpClient okHttpClient; private boolean hasExecuted; ParseHttpClient(@Nullable OkHttpClient.Builder builder) { @@ -161,7 +161,7 @@ Request getRequest(ParseHttpRequest parseRequest) { private static class ParseOkHttpRequestBody extends RequestBody { - private ParseHttpBody parseBody; + private final ParseHttpBody parseBody; ParseOkHttpRequestBody(ParseHttpBody parseBody) { this.parseBody = parseBody; diff --git a/parse/src/main/java/com/parse/ParseInstallation.java b/parse/src/main/java/com/parse/ParseInstallation.java deleted file mode 100644 index 86b853945..000000000 --- a/parse/src/main/java/com/parse/ParseInstallation.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * 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 android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.text.TextUtils; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -/** - * The {@code ParseInstallation} is a local representation of installation data that can be saved - * and retrieved from the Parse cloud. - */ -@ParseClassName("_Installation") -public class ParseInstallation extends ParseObject { - /* package */ static final String KEY_CHANNELS = "channels"; - private static final String TAG = "com.parse.ParseInstallation"; - private static final String KEY_OBJECT_ID = "objectId"; - private static final String KEY_INSTALLATION_ID = "installationId"; - private static final String KEY_DEVICE_TYPE = "deviceType"; - private static final String KEY_APP_NAME = "appName"; - private static final String KEY_APP_IDENTIFIER = "appIdentifier"; - private static final String KEY_PARSE_VERSION = "parseVersion"; - private static final String KEY_DEVICE_TOKEN = "deviceToken"; - private static final String KEY_PUSH_TYPE = "pushType"; - private static final String KEY_TIME_ZONE = "timeZone"; - private static final String KEY_LOCALE = "localeIdentifier"; - private static final String KEY_APP_VERSION = "appVersion"; - private static final List READ_ONLY_FIELDS = Collections.unmodifiableList( - Arrays.asList(KEY_DEVICE_TYPE, KEY_INSTALLATION_ID, KEY_DEVICE_TOKEN, KEY_PUSH_TYPE, - KEY_TIME_ZONE, KEY_LOCALE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION, - KEY_APP_IDENTIFIER, KEY_OBJECT_ID)); - - public ParseInstallation() { - // do nothing - } - - // TODO(mengyan): Inject into ParseInstallationInstanceController - /* package */ - static ParseCurrentInstallationController getCurrentInstallationController() { - return ParseCorePlugins.getInstance().getCurrentInstallationController(); - } - - public static ParseInstallation getCurrentInstallation() { - try { - return ParseTaskUtils.wait( - getCurrentInstallationController().getAsync()); - } catch (ParseException e) { - // In order to have backward compatibility, we swallow the exception silently. - return null; - } - } - - /** - * Constructs a query for {@code ParseInstallation}. - *

- * Note: We only allow the following types of queries for installations: - *

-     * query.get(objectId)
-     * query.whereEqualTo("installationId", value)
-     * query.whereMatchesKeyInQuery("installationId", keyInQuery, query)
-     * 
- *

- * You can add additional query clauses, but one of the above must appear as a top-level - * {@code AND} clause in the query. - * - * @see com.parse.ParseQuery#getQuery(Class) - */ - public static ParseQuery getQuery() { - return ParseQuery.getQuery(ParseInstallation.class); - } - - /** - * Returns the unique ID of this installation. - * - * @return A UUID that represents this device. - */ - public String getInstallationId() { - return getString(KEY_INSTALLATION_ID); - } - - @Override - public void setObjectId(String newObjectId) { - throw new RuntimeException("Installation's objectId cannot be changed"); - } - - @Override - /* package */ boolean needsDefaultACL() { - return false; - } - - @Override - /* package */ boolean isKeyMutable(String key) { - return !READ_ONLY_FIELDS.contains(key); - } - - @Override - /* package */ void updateBeforeSave() { - super.updateBeforeSave(); - if (getCurrentInstallationController().isCurrent(ParseInstallation.this)) { - updateTimezone(); - updateVersionInfo(); - updateDeviceInfo(); - updateLocaleIdentifier(); - } - } - - @Override - /* package */ Task fetchAsync( - final String sessionToken, final Task toAwait) { - synchronized (mutex) { - // Because the Service and the global currentInstallation are different objects, we may not - // have the same ObjectID (we never will at bootstrap). The server has a special hack for - // _Installation where save with an existing InstallationID will merge Object IDs - Task result; - if (getObjectId() == null) { - result = saveAsync(sessionToken, toAwait); - } else { - result = Task.forResult(null); - } - return result.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return ParseInstallation.super.fetchAsync(sessionToken, toAwait); - } - }); - } - } - - @Override - /* package */ Task saveAsync(final String sessionToken, final Task toAwait) { - return super.saveAsync(sessionToken, toAwait).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // Retry the fetch as a save operation because this Installation was deleted on the server. - if (task.getError() != null - && task.getError() instanceof ParseException) { - int errCode = ((ParseException) task.getError()).getCode(); - if (errCode == ParseException.OBJECT_NOT_FOUND - || (errCode == ParseException.MISSING_REQUIRED_FIELD_ERROR && getObjectId() == null)) { - synchronized (mutex) { - setState(new State.Builder(getState()).objectId(null).build()); - markAllFieldsDirty(); - return ParseInstallation.super.saveAsync(sessionToken, toAwait); - } - } - } - return task; - } - }); - } - - @Override - /* package */ Task handleSaveResultAsync(ParseObject.State result, - ParseOperationSet operationsBeforeSave) { - Task task = super.handleSaveResultAsync(result, operationsBeforeSave); - - if (result == null) { // Failure - return task; - } - - return task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getCurrentInstallationController().setAsync(ParseInstallation.this); - } - }); - } - - @Override - /* package */ Task handleFetchResultAsync(final ParseObject.State newState) { - return super.handleFetchResultAsync(newState).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getCurrentInstallationController().setAsync(ParseInstallation.this); - } - }); - } - - // Android documentation states that getID may return one of many forms: America/LosAngeles, - // GMT-, or code. We only accept the first on the server, so for now we will not upload - // time zones from devices reporting other formats. - private void updateTimezone() { - String zone = TimeZone.getDefault().getID(); - if ((zone.indexOf('/') > 0 || zone.equals("GMT")) && !zone.equals(get(KEY_TIME_ZONE))) { - performPut(KEY_TIME_ZONE, zone); - } - } - - private void updateVersionInfo() { - synchronized (mutex) { - try { - Context context = Parse.getApplicationContext(); - String packageName = context.getPackageName(); - PackageManager pm = context.getPackageManager(); - PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); - String appVersion = String.valueOf(pkgInfo.versionCode); - String appName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString(); - - if (packageName != null && !packageName.equals(get(KEY_APP_IDENTIFIER))) { - performPut(KEY_APP_IDENTIFIER, packageName); - } - if (appName != null && !appName.equals(get(KEY_APP_NAME))) { - performPut(KEY_APP_NAME, appName); - } - if (appVersion != null && !appVersion.equals(get(KEY_APP_VERSION))) { - performPut(KEY_APP_VERSION, appVersion); - } - } catch (PackageManager.NameNotFoundException e) { - PLog.w(TAG, "Cannot load package info; will not be saved to installation"); - } - } - } - - /* - * Save locale in the following format: - * [language code]-[country code] - * - * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by - * ISO 639-1. - * The country codes are two-letter uppercase ISO country codes (such as "US") as defined by - * ISO 3166-1. - * - * Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language - * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This - * rewriting happens even if you construct your own {@code Locale} object, not just for - * instances returned by the various lookup methods. - */ - private void updateLocaleIdentifier() { - final Locale locale = Locale.getDefault(); - - String language = locale.getLanguage(); - String country = locale.getCountry(); - - if (TextUtils.isEmpty(language)) { - return; - } - - // rewrite depreciated two-letter codes - if (language.equals("iw")) language = "he"; // Hebrew - if (language.equals("in")) language = "id"; // Indonesian - if (language.equals("ji")) language = "yi"; // Yiddish - - String localeString = language; - - if (!TextUtils.isEmpty(country)) { - localeString = String.format(Locale.US, "%s-%s", language, country); - } - - if (!localeString.equals(get(KEY_LOCALE))) { - performPut(KEY_LOCALE, localeString); - } - } - - // TODO(mengyan): Move to ParseInstallationInstanceController - /* package */ void updateDeviceInfo() { - updateDeviceInfo(ParsePlugins.get().installationId()); - } - - /* package */ void updateDeviceInfo(InstallationId installationId) { - /* - * If we don't have an installationId, use the one that comes from the installationId file on - * disk. This should be impossible since we set the installationId in setDefaultValues. - */ - if (!has(KEY_INSTALLATION_ID)) { - performPut(KEY_INSTALLATION_ID, installationId.get()); - } - String deviceType = "android"; - if (!deviceType.equals(get(KEY_DEVICE_TYPE))) { - performPut(KEY_DEVICE_TYPE, deviceType); - } - } - - public String getPushType() { - return super.getString(KEY_PUSH_TYPE); - } - - public void setPushType(String pushType) { - if (pushType != null) { - performPut(KEY_PUSH_TYPE, pushType); - } - } - - /* package */ void removePushType() { - performRemove(KEY_PUSH_TYPE); - } - - public String getDeviceToken() { - return super.getString(KEY_DEVICE_TOKEN); - } - - public void setDeviceToken(String deviceToken) { - if (deviceToken != null && deviceToken.length() > 0) { - performPut(KEY_DEVICE_TOKEN, deviceToken); - } - } - - /* package */ void removeDeviceToken() { - performRemove(KEY_DEVICE_TOKEN); - } -} diff --git a/parse/src/main/java/com/parse/ParseInstallation.kt b/parse/src/main/java/com/parse/ParseInstallation.kt new file mode 100644 index 000000000..6adf22615 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseInstallation.kt @@ -0,0 +1,296 @@ +/* + * 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 android.content.pm.PackageManager +import android.text.TextUtils +import com.parse.PLog.w +import kotlin.jvm.JvmOverloads +import com.parse.ParseTaskUtils +import com.parse.ParseQuery +import com.parse.boltsinternal.Task +import java.lang.RuntimeException +import java.util.* + +/** + * The `ParseInstallation` is a local representation of installation data that can be saved + * and retrieved from the Parse cloud. + */ +@ParseClassName("_Installation") +class ParseInstallation : ParseObject() { + /** + * Returns the unique ID of this installation. + * + * @return A UUID that represents this device. + */ + val installationId: String? + get() = getString(KEY_INSTALLATION_ID) + + override fun /* package */needsDefaultACL(): Boolean { + return false + } + + override fun /* package */isKeyMutable(key: String?): Boolean { + return !READ_ONLY_FIELDS.contains(key) + } + + override fun /* package */updateBeforeSave() { + super.updateBeforeSave() + if (currentInstallationController.isCurrent(this@ParseInstallation)) { + updateTimezone() + updateVersionInfo() + updateDeviceInfo() + updateLocaleIdentifier() + } + } + + override fun /* package */ fetchAsync( + sessionToken: String?, toAwait: Task + ): Task? { + synchronized(mutex) { + + // Because the Service and the global currentInstallation are different objects, we may not + // have the same ObjectID (we never will at bootstrap). The server has a special hack for + // _Installation where save with an existing InstallationID will merge Object IDs + val result: Task? = if (objectId == null) { + saveAsync(sessionToken, toAwait) + } else { + Task.forResult(null) + } + return result!!.onSuccessTask { + super@ParseInstallation.fetchAsync( + sessionToken, + toAwait + ) + } + } + } + + override fun saveAsync( + sessionToken: String?, + toAwait: Task? + ): Task? { + return super.saveAsync(sessionToken, toAwait)!!.continueWithTask { task: Task -> + // Retry the fetch as a save operation because this Installation was deleted on the server. + if (task.error != null + && task.error is ParseException + ) { + val errCode = (task.error as ParseException).code + if (errCode == ParseException.OBJECT_NOT_FOUND + || errCode == ParseException.MISSING_REQUIRED_FIELD_ERROR && objectId == null + ) { + synchronized(mutex) { + state = State.Builder(state).objectId(null).build() + markAllFieldsDirty() + return@continueWithTask super@ParseInstallation.saveAsync( + sessionToken, + toAwait + ) + } + } + } + task + } + } + + override fun /* package */handleSaveResultAsync( + result: State?, + operationsBeforeSave: ParseOperationSet + ): Task { + val task = super.handleSaveResultAsync(result, operationsBeforeSave) + return if (result == null) { // Failure + task + } else task.onSuccessTask { + currentInstallationController.setAsync( + this@ParseInstallation + ) + } + } + + override fun handleFetchResultAsync(result: State): Task? { + return super.handleFetchResultAsync(result)!! + .onSuccessTask { currentInstallationController.setAsync(this@ParseInstallation) } + } + + // Android documentation states that getID may return one of many forms: America/LosAngeles, + // GMT-, or code. We only accept the first on the server, so for now we will not upload + // time zones from devices reporting other formats. + private fun updateTimezone() { + val zone = TimeZone.getDefault().id + if ((zone.indexOf('/') > 0 || zone == "GMT") && zone != get(KEY_TIME_ZONE)) { + performPut(KEY_TIME_ZONE, zone) + } + } + + private fun updateVersionInfo() { + synchronized(mutex) { + try { + val context = Parse.getApplicationContext() + val packageName = context.packageName + val pm = context.packageManager + val pkgInfo = pm.getPackageInfo(packageName!!, 0) + val appVersion = pkgInfo.versionCode.toString() + val appName = + pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString() + if (packageName != null && packageName != get(KEY_APP_IDENTIFIER)) { + performPut(KEY_APP_IDENTIFIER, packageName) + } + if (appName != null && appName != get(KEY_APP_NAME)) { + performPut(KEY_APP_NAME, appName) + } + if (appVersion != null && appVersion != get(KEY_APP_VERSION)) { + performPut(KEY_APP_VERSION, appVersion) + } + } catch (e: PackageManager.NameNotFoundException) { + w(TAG, "Cannot load package info; will not be saved to installation") + } + } + } + + /* + * Save locale in the following format: + * [language code]-[country code] + * + * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + * The country codes are two-letter uppercase ISO country codes (such as "US") as defined by + * ISO 3166-1. + * + * Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language + * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This + * rewriting happens even if you construct your own {@code Locale} object, not just for + * instances returned by the various lookup methods. + */ + private fun updateLocaleIdentifier() { + val locale = Locale.getDefault() + var language = locale.language + val country = locale.country + if (TextUtils.isEmpty(language)) { + return + } + + // rewrite depreciated two-letter codes + if (language == "iw") language = "he" // Hebrew + if (language == "in") language = "id" // Indonesian + if (language == "ji") language = "yi" // Yiddish + var localeString = language + if (!TextUtils.isEmpty(country)) { + localeString = String.format(Locale.US, "%s-%s", language, country) + } + if (localeString != get(KEY_LOCALE)) { + performPut(KEY_LOCALE, localeString) + } + } + + /* package */ // TODO(mengyan): Move to ParseInstallationInstanceController + /* package */ + @JvmOverloads + fun updateDeviceInfo(installationId: InstallationId = ParsePlugins.get().installationId()) { + /* + * If we don't have an installationId, use the one that comes from the installationId file on + * disk. This should be impossible since we set the installationId in setDefaultValues. + */ + if (!has(KEY_INSTALLATION_ID)) { + performPut(KEY_INSTALLATION_ID, installationId.get()!!) + } + val deviceType = "android" + if (deviceType != get(KEY_DEVICE_TYPE)) { + performPut(KEY_DEVICE_TYPE, deviceType) + } + } + + var pushType: String? + get() = super.getString(KEY_PUSH_TYPE) + set(pushType) { + if (pushType != null) { + performPut(KEY_PUSH_TYPE, pushType) + } + } + + /* package */ + fun removePushType() { + performRemove(KEY_PUSH_TYPE) + } + + var deviceToken: String? + get() = super.getString(KEY_DEVICE_TOKEN) + set(deviceToken) { + if (deviceToken != null && deviceToken.isNotEmpty()) { + performPut(KEY_DEVICE_TOKEN, deviceToken) + } + } + + /* package */ + fun removeDeviceToken() { + performRemove(KEY_DEVICE_TOKEN) + } + + companion object { + /* package */ + const val KEY_CHANNELS = "channels" + private const val TAG = "com.parse.ParseInstallation" + private const val KEY_OBJECT_ID = "objectId" + private const val KEY_INSTALLATION_ID = "installationId" + private const val KEY_DEVICE_TYPE = "deviceType" + private const val KEY_APP_NAME = "appName" + private const val KEY_APP_IDENTIFIER = "appIdentifier" + private const val KEY_PARSE_VERSION = "parseVersion" + private const val KEY_DEVICE_TOKEN = "deviceToken" + private const val KEY_PUSH_TYPE = "pushType" + private const val KEY_TIME_ZONE = "timeZone" + private const val KEY_LOCALE = "localeIdentifier" + private const val KEY_APP_VERSION = "appVersion" + private val READ_ONLY_FIELDS = Collections.unmodifiableList( + listOf( + KEY_DEVICE_TYPE, KEY_INSTALLATION_ID, KEY_DEVICE_TOKEN, KEY_PUSH_TYPE, + KEY_TIME_ZONE, KEY_LOCALE, KEY_APP_VERSION, KEY_APP_NAME, KEY_PARSE_VERSION, + KEY_APP_IDENTIFIER, KEY_OBJECT_ID + ) + ) + + // TODO(mengyan): Inject into ParseInstallationInstanceController + /* package */ + @JvmStatic + internal val currentInstallationController: ParseCurrentInstallationController + get() = ParseCorePlugins.getInstance().currentInstallationController + + // In order to have backward compatibility, we swallow the exception silently. + @JvmStatic + val currentInstallation: ParseInstallation? + get() = try { + ParseTaskUtils.wait( + currentInstallationController.async + ) + } catch (e: ParseException) { + // In order to have backward compatibility, we swallow the exception silently. + null + } + + /** + * Constructs a query for `ParseInstallation`. + * + * + * **Note:** We only allow the following types of queries for installations: + *

+         * query.get(objectId)
+         * query.whereEqualTo("installationId", value)
+         * query.whereMatchesKeyInQuery("installationId", keyInQuery, query)
+        
* + * + * + * You can add additional query clauses, but one of the above must appear as a top-level + * `AND` clause in the query. + * + * @see com.parse.ParseQuery.getQuery + */ + @JvmStatic + val query: ParseQuery + get() = ParseQuery.getQuery(ParseInstallation::class.java) + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseJSONUtils.java b/parse/src/main/java/com/parse/ParseJSONUtils.java index 5e6371ed4..02081250a 100644 --- a/parse/src/main/java/com/parse/ParseJSONUtils.java +++ b/parse/src/main/java/com/parse/ParseJSONUtils.java @@ -8,8 +8,6 @@ */ package com.parse; -import androidx.annotation.NonNull; - import org.json.JSONException; import org.json.JSONObject; @@ -48,13 +46,7 @@ public static JSONObject create(JSONObject copyFrom, Collection excludes */ public static Iterable keys(JSONObject object) { final JSONObject finalObject = object; - return new Iterable() { - @NonNull - @Override - public Iterator iterator() { - return finalObject.keys(); - } - }; + return finalObject::keys; } /** diff --git a/parse/src/main/java/com/parse/ParseKeyValueCache.java b/parse/src/main/java/com/parse/ParseKeyValueCache.java index 20ad10f8c..9c9571ae9 100644 --- a/parse/src/main/java/com/parse/ParseKeyValueCache.java +++ b/parse/src/main/java/com/parse/ParseKeyValueCache.java @@ -14,11 +14,10 @@ import org.json.JSONObject; import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.Comparator; import java.util.Date; /** @@ -81,12 +80,7 @@ static int size() { private static File getKeyValueCacheFile(String key) { final String suffix = '.' + key; - File[] matches = getKeyValueCacheDir().listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String filename) { - return filename.endsWith(suffix); - } - }); + File[] matches = getKeyValueCacheDir().listFiles((dir, filename) -> filename.endsWith(suffix)); return (matches == null || matches.length == 0) ? null : matches[0]; } @@ -134,7 +128,7 @@ static void saveToKeyValueCache(String key, String value) { } File f = createKeyValueCacheFile(key); try { - ParseFileUtils.writeByteArrayToFile(f, value.getBytes("UTF-8")); + ParseFileUtils.writeByteArrayToFile(f, value.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { // do nothing } @@ -162,15 +156,12 @@ static void saveToKeyValueCache(String key, String value) { // Sometimes (i.e. tests) the time of lastModified isn't granular enough, // so we resort // to sorting by the file name which is always prepended with time in ms - Arrays.sort(files, new Comparator() { - @Override - public int compare(File f1, File f2) { - int dateCompare = Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); - if (dateCompare != 0) { - return dateCompare; - } else { - return f1.getName().compareTo(f2.getName()); - } + Arrays.sort(files, (f1, f2) -> { + int dateCompare = Long.compare(f1.lastModified(), f2.lastModified()); + if (dateCompare != 0) { + return dateCompare; + } else { + return f1.getName().compareTo(f2.getName()); } }); @@ -222,7 +213,7 @@ static String loadFromKeyValueCache(final String key, final long maxAgeMilliseco byte[] bytes = new byte[(int) f.length()]; f.readFully(bytes); f.close(); - return new String(bytes, "UTF-8"); + return new String(bytes, StandardCharsets.UTF_8); } catch (IOException e) { PLog.e(TAG, "error reading from cache", e); return null; diff --git a/parse/src/main/java/com/parse/ParseObject.java b/parse/src/main/java/com/parse/ParseObject.java deleted file mode 100644 index d00526522..000000000 --- a/parse/src/main/java/com/parse/ParseObject.java +++ /dev/null @@ -1,4219 +0,0 @@ -/* - * 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 android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Lock; - -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -/** - * The {@code ParseObject} is a local representation of data that can be saved and retrieved from - * the Parse cloud. - *

- * The basic workflow for creating new data is to construct a new {@code ParseObject}, use - * {@link #put(String, Object)} to fill it with data, and then use {@link #saveInBackground()} to - * persist to the cloud. - *

- * The basic workflow for accessing existing data is to use a {@link ParseQuery} to specify which - * existing data to retrieve. - */ -@SuppressWarnings({"unused", "WeakerAccess"}) -public class ParseObject implements Parcelable { - /** - * Default name for pinning if not specified. - * - * @see #pin() - * @see #unpin() - */ - public static final String DEFAULT_PIN = "_default"; - static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; - private static final String AUTO_CLASS_NAME = "_Automatic"; - private static final String TAG = "ParseObject"; - /* - REST JSON Keys - */ - public static final String KEY_OBJECT_ID = "objectId"; - public static final String KEY_CREATED_AT = "createdAt"; - public static final String KEY_UPDATED_AT = "updatedAt"; - private static final String KEY_CLASS_NAME = "className"; - private static final String KEY_ACL = "ACL"; - /* - Internal JSON Keys - Used to store internal data when persisting {@code ParseObject}s locally. - */ - private static final String KEY_COMPLETE = "__complete"; - private static final String KEY_OPERATIONS = "__operations"; - // Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s - // correctly, and helps constructing the {@code State.availableKeys()} set. - private static final String KEY_SELECTED_KEYS = "__selectedKeys"; - // Because Grantland messed up naming this... We'll only try to read from this for backward - // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete - // and not check after a while - private static final String KEY_IS_DELETING_EVENTUALLY_OLD = "isDeletingEventually"; - private static final ThreadLocal isCreatingPointerForObjectId = - new ThreadLocal() { - @Override - protected String initialValue() { - return null; - } - }; - /* - * This is used only so that we can pass it to createWithoutData as the objectId to make it create - * an un-fetched pointer that has no objectId. This is useful only in the context of the offline - * store, where you can have an un-fetched pointer for an object that can later be fetched from the - * store. - */ - private static final String NEW_OFFLINE_OBJECT_ID_PLACEHOLDER = - "*** Offline Object ***"; - public final static Creator CREATOR = new Creator() { - @Override - public ParseObject createFromParcel(Parcel source) { - return ParseObject.createFromParcel(source, new ParseObjectParcelDecoder()); - } - - @Override - public ParseObject[] newArray(int size) { - return new ParseObject[size]; - } - }; - final Object mutex = new Object(); - final TaskQueue taskQueue = new TaskQueue(); - final LinkedList operationSetQueue; - // Cached State - private final Map estimatedData; - private final ParseMulticastDelegate saveEvent = new ParseMulticastDelegate<>(); - String localId; - boolean isDeleted; - boolean isDeleting; // Since delete ops are queued, we don't need a counter. - //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. - int isDeletingEventually; - private State state; - private boolean ldsEnabledWhenParceling; - - /** - * The base class constructor to call in subclasses. Uses the class name specified with the - * {@link ParseClassName} annotation on the subclass. - */ - protected ParseObject() { - this(AUTO_CLASS_NAME); - } - - /** - * Constructs a new {@code ParseObject} with no data in it. A {@code ParseObject} constructed in - * this way will not have an objectId and will not persist to the database until {@link #save()} - * is called. - *

- * Class names must be alphanumerical plus underscore, and start with a letter. It is recommended - * to name classes in PascalCaseLikeThis. - * - * @param theClassName The className for this {@code ParseObject}. - */ - public ParseObject(String theClassName) { - // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the - // right thing with subclasses. It's ugly and terrible, but it does provide the development - // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the - // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? - String objectIdForPointer = isCreatingPointerForObjectId.get(); - - if (theClassName == null) { - throw new IllegalArgumentException( - "You must specify a Parse class name when creating a new ParseObject."); - } - if (AUTO_CLASS_NAME.equals(theClassName)) { - theClassName = getSubclassingController().getClassName(getClass()); - } - - // If this is supposed to be created by a factory but wasn't, throw an exception. - if (!getSubclassingController().isSubclassValid(theClassName, getClass())) { - throw new IllegalArgumentException( - "You must create this type of ParseObject using ParseObject.create() or the proper subclass."); - } - - operationSetQueue = new LinkedList<>(); - operationSetQueue.add(new ParseOperationSet()); - estimatedData = new HashMap<>(); - - State.Init builder = newStateBuilder(theClassName); - // When called from new, assume hasData for the whole object is true. - if (objectIdForPointer == null) { - setDefaultValues(); - builder.isComplete(true); - } else { - if (!objectIdForPointer.equals(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER)) { - builder.objectId(objectIdForPointer); - } - builder.isComplete(false); - } - // This is a new untouched object, we don't need cache rebuilding, etc. - state = builder.build(); - - OfflineStore store = Parse.getLocalDatastore(); - if (store != null) { - store.registerNewObject(this); - } - } - - private static ParseObjectController getObjectController() { - return ParseCorePlugins.getInstance().getObjectController(); - } - - private static LocalIdManager getLocalIdManager() { - return ParseCorePlugins.getInstance().getLocalIdManager(); - } - - private static ParseObjectSubclassingController getSubclassingController() { - return ParseCorePlugins.getInstance().getSubclassingController(); - } - - /** - * Creates a new {@code ParseObject} based upon a class name. If the class name is a special type - * (e.g. for {@code ParseUser}), then the appropriate type of {@code ParseObject} is returned. - * - * @param className The class of object to create. - * @return A new {@code ParseObject} for the given class name. - */ - public static ParseObject create(String className) { - return getSubclassingController().newInstance(className); - } - - /** - * Creates a new {@code ParseObject} based upon a subclass type. Note that the object will be - * created based upon the {@link ParseClassName} of the given subclass type. For example, calling - * create(ParseUser.class) may create an instance of a custom subclass of {@code ParseUser}. - * - * @param subclass The class of object to create. - * @return A new {@code ParseObject} based upon the class name of the given subclass type. - */ - @SuppressWarnings("unchecked") - public static T create(Class subclass) { - return (T) create(getSubclassingController().getClassName(subclass)); - } - - /** - * Creates a reference to an existing {@code ParseObject} for use in creating associations between - * {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return - * {@code false} until {@link #fetchIfNeeded()} or {@link #fetch()} has been called. No network - * request will be made. - * - * @param className The object's class. - * @param objectId The object id for the referenced object. - * @return A {@code ParseObject} without data. - */ - public static ParseObject createWithoutData(String className, String objectId) { - OfflineStore store = Parse.getLocalDatastore(); - try { - if (objectId == null) { - isCreatingPointerForObjectId.set(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER); - } else { - isCreatingPointerForObjectId.set(objectId); - } - ParseObject object = null; - if (store != null && objectId != null) { - object = store.getObject(className, objectId); - } - - if (object == null) { - object = create(className); - if (object.hasChanges()) { - throw new IllegalStateException( - "A ParseObject subclass default constructor must not make changes " - + "to the object that cause it to be dirty." - ); - } - } - - return object; - - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException("Failed to create instance of subclass.", e); - } finally { - isCreatingPointerForObjectId.set(null); - } - } - - /** - * Creates a reference to an existing {@code ParseObject} for use in creating associations between - * {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return - * {@code false} until {@link #fetchIfNeeded()} or {@link #fetch()} has been called. No network - * request will be made. - * - * @param subclass The {@code ParseObject} subclass to create. - * @param objectId The object id for the referenced object. - * @return A {@code ParseObject} without data. - */ - @SuppressWarnings({"unused", "unchecked"}) - public static T createWithoutData(Class subclass, String objectId) { - return (T) createWithoutData(getSubclassingController().getClassName(subclass), objectId); - } - - /** - * Registers a custom subclass type with the Parse SDK, enabling strong-typing of those - * {@code ParseObject}s whenever they appear. Subclasses must specify the {@link ParseClassName} - * annotation and have a default constructor. - * - * @param subclass The subclass type to register. - */ - public static void registerSubclass(Class subclass) { - getSubclassingController().registerSubclass(subclass); - } - - /* package for tests */ - static void unregisterSubclass(Class subclass) { - getSubclassingController().unregisterSubclass(subclass); - } - - /** - * Adds a task to the queue for all of the given objects. - */ - static Task enqueueForAll(final List objects, - Continuation> taskStart) { - // The task that will be complete when all of the child queues indicate they're ready to start. - final TaskCompletionSource readyToStart = new TaskCompletionSource<>(); - - // First, we need to lock the mutex for the queue for every object. We have to hold this - // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so - // that saves actually get executed in the order they were setup by taskStart(). - // The locks have to be sorted so that we always acquire them in the same order. - // Otherwise, there's some risk of deadlock. - List locks = new ArrayList<>(objects.size()); - for (ParseObject obj : objects) { - locks.add(obj.taskQueue.getLock()); - } - LockSet lock = new LockSet(locks); - - lock.lock(); - try { - // The task produced by TaskStart - final Task fullTask; - try { - // By running this immediately, we allow everything prior to toAwait to run before waiting - // for all of the queues on all of the objects. - fullTask = taskStart.then(readyToStart.getTask()); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - - // Add fullTask to each of the objects' queues. - final List> childTasks = new ArrayList<>(); - for (ParseObject obj : objects) { - obj.taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task task) { - childTasks.add(task); - return fullTask; - } - }); - } - - // When all of the objects' queues are ready, signal fullTask that it's ready to go on. - Task.whenAll(childTasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - readyToStart.setResult(null); - return null; - } - }); - return fullTask; - } finally { - lock.unlock(); - } - } - - /** - * Converts a {@code ParseObject.State} to a {@code ParseObject}. - * - * @param state The {@code ParseObject.State} to convert from. - * @return A {@code ParseObject} instance. - */ - static T from(ParseObject.State state) { - @SuppressWarnings("unchecked") - T object = (T) ParseObject.createWithoutData(state.className(), state.objectId()); - synchronized (object.mutex) { - State newState; - if (state.isComplete()) { - newState = state; - } else { - newState = object.getState().newBuilder().apply(state).build(); - } - object.setState(newState); - } - return object; - } - - /** - * Creates a new {@code ParseObject} based on data from the Parse server. - * - * @param json The object's data. - * @param defaultClassName The className of the object, if none is in the JSON. - * @param decoder Delegate for knowing how to decode the values in the JSON. - * @param selectedKeys Set of keys selected when quering for this object. If none, the object is assumed to - * be complete, i.e. this is all the data for the object on the server. - */ - public static T fromJSON(JSONObject json, String defaultClassName, - ParseDecoder decoder, - Set selectedKeys) { - if (selectedKeys != null && !selectedKeys.isEmpty()) { - JSONArray keys = new JSONArray(selectedKeys); - try { - json.put(KEY_SELECTED_KEYS, keys); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - return fromJSON(json, defaultClassName, decoder); - } - - /** - * Creates a new {@code ParseObject} based on data from the Parse server. - * - * @param json The object's data. It is assumed to be complete, unless the JSON has the - * {@link #KEY_SELECTED_KEYS} key. - * @param defaultClassName The className of the object, if none is in the JSON. - * @param decoder Delegate for knowing how to decode the values in the JSON. - */ - public static T fromJSON(JSONObject json, String defaultClassName, - ParseDecoder decoder) { - String className = json.optString(KEY_CLASS_NAME, defaultClassName); - if (className == null) { - return null; - } - String objectId = json.optString(KEY_OBJECT_ID, null); - boolean isComplete = !json.has(KEY_SELECTED_KEYS); - @SuppressWarnings("unchecked") - T object = (T) ParseObject.createWithoutData(className, objectId); - State newState = object.mergeFromServer(object.getState(), json, decoder, isComplete); - object.setState(newState); - return object; - } - - //region Getter/Setter helper methods - - /** - * Method used by parse server webhooks implementation to convert raw JSON to Parse Object - *

- * Method is used by parse server webhooks implementation to create a - * new {@code ParseObject} from the incoming json payload. The method is different from - * {@link #fromJSON(JSONObject, String, ParseDecoder, Set)} ()} in that it calls - * {@link #build(JSONObject, ParseDecoder)} which populates operation queue - * rather then the server data from the incoming JSON, as at external server the incoming - * JSON may not represent the actual server data. Also it handles - * {@link ParseFieldOperations} separately. - * - * @param json The object's data. - * @param decoder Delegate for knowing how to decode the values in the JSON. - */ - - static T fromJSONPayload( - JSONObject json, ParseDecoder decoder) { - String className = json.optString(KEY_CLASS_NAME); - if (className == null || ParseTextUtils.isEmpty(className)) { - return null; - } - String objectId = json.optString(KEY_OBJECT_ID, null); - @SuppressWarnings("unchecked") - T object = (T) ParseObject.createWithoutData(className, objectId); - object.build(json, decoder); - return object; - } - - /** - * This deletes all of the objects from the given List. - */ - private static Task deleteAllAsync( - final List objects, final String sessionToken) { - if (objects.size() == 0) { - return Task.forResult(null); - } - - // Create a list of unique objects based on objectIds - int objectCount = objects.size(); - final List uniqueObjects = new ArrayList<>(objectCount); - final HashSet idSet = new HashSet<>(); - for (int i = 0; i < objectCount; i++) { - ParseObject obj = objects.get(i); - if (!idSet.contains(obj.getObjectId())) { - idSet.add(obj.getObjectId()); - uniqueObjects.add(obj); - } - } - - return enqueueForAll(uniqueObjects, new Continuation>() { - @Override - public Task then(Task toAwait) { - return deleteAllAsync(uniqueObjects, sessionToken, toAwait); - } - }); - } - - private static Task deleteAllAsync( - final List uniqueObjects, final String sessionToken, Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - int objectCount = uniqueObjects.size(); - List states = new ArrayList<>(objectCount); - for (int i = 0; i < objectCount; i++) { - ParseObject object = uniqueObjects.get(i); - object.validateDelete(); - states.add(object.getState()); - } - List> batchTasks = getObjectController().deleteAllAsync(states, sessionToken); - - List> tasks = new ArrayList<>(objectCount); - for (int i = 0; i < objectCount; i++) { - Task batchTask = batchTasks.get(i); - final T object = uniqueObjects.get(i); - tasks.add(batchTask.onSuccessTask(new Continuation>() { - @Override - public Task then(final Task batchTask) { - return object.handleDeleteResultAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return batchTask; - } - }); - } - })); - } - return Task.whenAll(tasks); - } - }); - } - - /** - * Deletes each object in the provided list. This is faster than deleting each object individually - * because it batches the requests. - * - * @param objects The objects to delete. - * @throws ParseException Throws an exception if the server returns an error or is inaccessible. - */ - public static void deleteAll(List objects) throws ParseException { - ParseTaskUtils.wait(deleteAllInBackground(objects)); - } - - /** - * Deletes each object in the provided list. This is faster than deleting each object individually - * because it batches the requests. - * - * @param objects The objects to delete. - * @param callback The callback method to execute when completed. - */ - public static void deleteAllInBackground(List objects, DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(deleteAllInBackground(objects), callback); - } - - /** - * Deletes each object in the provided list. This is faster than deleting each object individually - * because it batches the requests. - * - * @param objects The objects to delete. - * @return A {@link Task} that is resolved when deleteAll completes. - */ - public static Task deleteAllInBackground(final List objects) { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - return deleteAllAsync(objects, sessionToken); - } - }); - } - - /** - * Finds all of the objects that are reachable from child, including child itself, and adds them - * to the given mutable array. It traverses arrays and json objects. - * - * @param node An kind object to search for children. - * @param dirtyChildren The array to collect the {@code ParseObject}s into. - * @param dirtyFiles The array to collect the {@link ParseFile}s into. - * @param alreadySeen The set of all objects that have already been seen. - * @param alreadySeenNew The set of new objects that have already been seen since the last existing object. - */ - private static void collectDirtyChildren(Object node, - final Collection dirtyChildren, - final Collection dirtyFiles, - final Set alreadySeen, - final Set alreadySeenNew) { - - new ParseTraverser() { - @Override - protected boolean visit(Object node) { - // If it's a file, then add it to the list if it's dirty. - if (node instanceof ParseFile) { - if (dirtyFiles == null) { - return true; - } - - ParseFile file = (ParseFile) node; - if (file.getUrl() == null) { - dirtyFiles.add(file); - } - return true; - } - - // If it's anything other than a file, then just continue; - if (!(node instanceof ParseObject)) { - return true; - } - - if (dirtyChildren == null) { - return true; - } - - // For files, we need to handle recursion manually to find cycles of new objects. - ParseObject object = (ParseObject) node; - Set seen = alreadySeen; - Set seenNew = alreadySeenNew; - - // Check for cycles of new objects. Any such cycle means it will be - // impossible to save this collection of objects, so throw an exception. - if (object.getObjectId() != null) { - seenNew = new HashSet<>(); - } else { - if (seenNew.contains(object)) { - throw new RuntimeException("Found a circular dependency while saving."); - } - seenNew = new HashSet<>(seenNew); - seenNew.add(object); - } - - // Check for cycles of any object. If this occurs, then there's no - // problem, but we shouldn't recurse any deeper, because it would be - // an infinite recursion. - if (seen.contains(object)) { - return true; - } - seen = new HashSet<>(seen); - seen.add(object); - - // Recurse into this object's children looking for dirty children. - // We only need to look at the child object's current estimated data, - // because that's the only data that might need to be saved now. - collectDirtyChildren(object.estimatedData, dirtyChildren, dirtyFiles, seen, seenNew); - - if (object.isDirty(false)) { - dirtyChildren.add(object); - } - - return true; - } - }.setYieldRoot(true).traverse(node); - } - - //endregion - - /** - * Helper version of collectDirtyChildren so that callers don't have to add the internally used - * parameters. - */ - private static void collectDirtyChildren(Object node, Collection dirtyChildren, - Collection dirtyFiles) { - collectDirtyChildren(node, dirtyChildren, dirtyFiles, - new HashSet(), - new HashSet()); - } - - /** - * This saves all of the objects and files reachable from the given object. It does its work in - * multiple waves, saving as many as possible in each wave. If there's ever an error, it just - * gives up, sets error, and returns NO. - */ - private static Task deepSaveAsync(final Object object, final String sessionToken) { - Set objects = new HashSet<>(); - Set files = new HashSet<>(); - collectDirtyChildren(object, objects, files); - - // This has to happen separately from everything else because ParseUser.save() is - // special-cased to work for lazy users, but new users can't be created by - // ParseMultiCommand's regular save. - Set users = new HashSet<>(); - for (ParseObject o : objects) { - if (o instanceof ParseUser) { - ParseUser user = (ParseUser) o; - if (user.isLazy()) { - users.add((ParseUser) o); - } - } - } - objects.removeAll(users); - - // objects will need to wait for files to be complete since they may be nested children. - final AtomicBoolean filesComplete = new AtomicBoolean(false); - List> tasks = new ArrayList<>(); - for (ParseFile file : files) { - tasks.add(file.saveAsync(sessionToken, null, null)); - } - Task filesTask = Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - filesComplete.set(true); - return null; - } - }); - - // objects will need to wait for users to be complete since they may be nested children. - final AtomicBoolean usersComplete = new AtomicBoolean(false); - tasks = new ArrayList<>(); - for (final ParseUser user : users) { - tasks.add(user.saveAsync(sessionToken)); - } - Task usersTask = Task.whenAll(tasks).continueWith(new Continuation() { - @Override - public Void then(Task task) { - usersComplete.set(true); - return null; - } - }); - - final Capture> remaining = new Capture<>(objects); - Task objectsTask = Task.forResult(null).continueWhile(new Callable() { - @Override - public Boolean call() { - return remaining.get().size() > 0; - } - }, new Continuation>() { - @Override - public Task then(Task task) { - // Partition the objects into two sets: those that can be save immediately, - // and those that rely on other objects to be created first. - final List current = new ArrayList<>(); - final Set nextBatch = new HashSet<>(); - for (ParseObject obj : remaining.get()) { - if (obj.canBeSerialized()) { - current.add(obj); - } else { - nextBatch.add(obj); - } - } - remaining.set(nextBatch); - - if (current.size() == 0 && filesComplete.get() && usersComplete.get()) { - // We do cycle-detection when building the list of objects passed to this function, so - // this should never get called. But we should check for it anyway, so that we get an - // exception instead of an infinite loop. - throw new RuntimeException("Unable to save a ParseObject with a relation to a cycle."); - } - - // Package all save commands together - if (current.size() == 0) { - return Task.forResult(null); - } - - return enqueueForAll(current, new Continuation>() { - @Override - public Task then(Task toAwait) { - return saveAllAsync(current, sessionToken, toAwait); - } - }); - } - }); - - return Task.whenAll(Arrays.asList(filesTask, usersTask, objectsTask)); - } - - private static Task saveAllAsync( - final List uniqueObjects, final String sessionToken, Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - int objectCount = uniqueObjects.size(); - List states = new ArrayList<>(objectCount); - List operationsList = new ArrayList<>(objectCount); - List decoders = new ArrayList<>(objectCount); - for (int i = 0; i < objectCount; i++) { - ParseObject object = uniqueObjects.get(i); - object.updateBeforeSave(); - object.validateSave(); - - states.add(object.getState()); - operationsList.add(object.startSave()); - final Map fetchedObjects = object.collectFetchedObjects(); - decoders.add(new KnownParseObjectDecoder(fetchedObjects)); - } - List> batchTasks = getObjectController().saveAllAsync( - states, operationsList, sessionToken, decoders); - - List> tasks = new ArrayList<>(objectCount); - for (int i = 0; i < objectCount; i++) { - Task batchTask = batchTasks.get(i); - final T object = uniqueObjects.get(i); - final ParseOperationSet operations = operationsList.get(i); - tasks.add(batchTask.continueWithTask(new Continuation>() { - @Override - public Task then(final Task batchTask) { - ParseObject.State result = batchTask.getResult(); // will be null on failure - return object.handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted() || task.isCancelled()) { - return task; - } - - // We still want to propagate batchTask errors - return batchTask.makeVoid(); - } - }); - } - })); - } - return Task.whenAll(tasks); - } - }); - } - - /** - * Saves each object in the provided list. This is faster than saving each object individually - * because it batches the requests. - * - * @param objects The objects to save. - * @throws ParseException Throws an exception if the server returns an error or is inaccessible. - */ - public static void saveAll(List objects) throws ParseException { - ParseTaskUtils.wait(saveAllInBackground(objects)); - } - - /** - * Saves each object in the provided list to the server in a background thread. This is preferable - * to using saveAll, unless your code is already running from a background thread. - * - * @param objects The objects to save. - * @param callback {@code callback.done(e)} is called when the save completes. - */ - public static void saveAllInBackground(List objects, SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(saveAllInBackground(objects), callback); - } - - /** - * Saves each object in the provided list to the server in a background thread. This is preferable - * to using saveAll, unless your code is already running from a background thread. - * - * @param objects The objects to save. - * @return A {@link Task} that is resolved when saveAll completes. - */ - public static Task saveAllInBackground(final List objects) { - return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser current = task.getResult(); - if (current == null) { - return Task.forResult(null); - } - if (!current.isLazy()) { - return Task.forResult(current.getSessionToken()); - } - - // The current user is lazy/unresolved. If it is attached to any of the objects via ACL, - // we'll need to resolve/save it before proceeding. - for (ParseObject object : objects) { - if (!object.isDataAvailable(KEY_ACL)) { - continue; - } - final ParseACL acl = object.getACL(false); - if (acl == null) { - continue; - } - final ParseUser user = acl.getUnresolvedUser(); - if (user != null && user.isCurrentUser()) { - // We only need to find one, since there's only one current user. - return user.saveAsync(null).onSuccess(new Continuation() { - @Override - public String then(Task task) { - if (acl.hasUnresolvedUser()) { - throw new IllegalStateException("ACL has an unresolved ParseUser. " - + "Save or sign up before attempting to serialize the ACL."); - } - return user.getSessionToken(); - } - }); - } - } - - // There were no objects with ACLs pointing to unresolved users. - return Task.forResult(null); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return deepSaveAsync(objects, sessionToken); - } - }); - } - - /** - * Fetches all the objects that don't have data in the provided list in the background. - * - * @param objects The list of objects to fetch. - * @return A {@link Task} that is resolved when fetchAllIfNeeded completes. - */ - public static Task> fetchAllIfNeededInBackground( - final List objects) { - return fetchAllAsync(objects, true); - } - - /** - * Fetches all the objects that don't have data in the provided list. - * - * @param objects The list of objects to fetch. - * @return The list passed in for convenience. - * @throws ParseException Throws an exception if the server returns an error or is inaccessible. - */ - public static List fetchAllIfNeeded(List objects) - throws ParseException { - return ParseTaskUtils.wait(fetchAllIfNeededInBackground(objects)); - } - - //region LDS-processing methods. - - /** - * Fetches all the objects that don't have data in the provided list in the background. - * - * @param objects The list of objects to fetch. - * @param callback {@code callback.done(result, e)} is called when the fetch completes. - */ - public static void fetchAllIfNeededInBackground(final List objects, - FindCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(fetchAllIfNeededInBackground(objects), callback); - } - - private static Task> fetchAllAsync( - final List objects, final boolean onlyIfNeeded) { - return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>>() { - @Override - public Task> then(Task task) { - final ParseUser user = task.getResult(); - return enqueueForAll(objects, new Continuation>>() { - @Override - public Task> then(Task task) { - return fetchAllAsync(objects, user, onlyIfNeeded, task); - } - }); - } - }); - } - - /** - * @param onlyIfNeeded If enabled, will only fetch if the object has an objectId and - * !isDataAvailable, otherwise it requires objectIds and will fetch regardless - * of data availability. - */ - // TODO(grantland): Convert to ParseUser.State - private static Task> fetchAllAsync( - final List objects, final ParseUser user, final boolean onlyIfNeeded, Task toAwait) { - if (objects.size() == 0) { - return Task.forResult(objects); - } - - List objectIds = new ArrayList<>(); - String className = null; - for (T object : objects) { - if (onlyIfNeeded && object.isDataAvailable()) { - continue; - } - - if (className != null && !object.getClassName().equals(className)) { - throw new IllegalArgumentException("All objects should have the same class"); - } - className = object.getClassName(); - - String objectId = object.getObjectId(); - if (objectId != null) { - objectIds.add(object.getObjectId()); - } else if (!onlyIfNeeded) { - throw new IllegalArgumentException("All objects must exist on the server"); - } - } - - if (objectIds.size() == 0) { - return Task.forResult(objects); - } - - final ParseQuery query = ParseQuery.getQuery(className) - .whereContainedIn(KEY_OBJECT_ID, objectIds) - .setLimit(objectIds.size()); - return toAwait.continueWithTask(new Continuation>>() { - @Override - public Task> then(Task task) { - return query.findAsync(query.getBuilder().build(), user, null); - } - }).onSuccess(new Continuation, List>() { - @Override - public List then(Task> task) throws Exception { - Map resultMap = new HashMap<>(); - for (T o : task.getResult()) { - resultMap.put(o.getObjectId(), o); - } - for (T object : objects) { - if (onlyIfNeeded && object.isDataAvailable()) { - continue; - } - - T newObject = resultMap.get(object.getObjectId()); - if (newObject == null) { - throw new ParseException( - ParseException.OBJECT_NOT_FOUND, - "Object id " + object.getObjectId() + " does not exist"); - } - if (!Parse.isLocalDatastoreEnabled()) { - // We only need to merge if LDS is disabled, since single instance will do the merging - // for us. - object.mergeFromObject(newObject); - } - } - return objects; - } - }); - } - - //endregion - - /** - * Fetches all the objects in the provided list in the background. - * - * @param objects The list of objects to fetch. - * @return A {@link Task} that is resolved when fetch completes. - */ - public static Task> fetchAllInBackground(final List objects) { - return fetchAllAsync(objects, false); - } - - /** - * Fetches all the objects in the provided list. - * - * @param objects The list of objects to fetch. - * @return The list passed in. - * @throws ParseException Throws an exception if the server returns an error or is inaccessible. - */ - public static List fetchAll(List objects) throws ParseException { - return ParseTaskUtils.wait(fetchAllInBackground(objects)); - } - - /** - * Fetches all the objects in the provided list in the background. - * - * @param objects The list of objects to fetch. - * @param callback {@code callback.done(result, e)} is called when the fetch completes. - */ - public static void fetchAllInBackground(List objects, - FindCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(fetchAllInBackground(objects), callback); - } - - /** - * Registers the Parse-provided {@code ParseObject} subclasses. Do this here in a real method rather than - * as part of a static initializer because doing this in a static initializer can lead to - * deadlocks - */ - - static void registerParseSubclasses() { - registerSubclass(ParseUser.class); - registerSubclass(ParseRole.class); - registerSubclass(ParseInstallation.class); - registerSubclass(ParseSession.class); - - registerSubclass(ParsePin.class); - registerSubclass(EventuallyPin.class); - } - - - static void unregisterParseSubclasses() { - unregisterSubclass(ParseUser.class); - unregisterSubclass(ParseRole.class); - unregisterSubclass(ParseInstallation.class); - unregisterSubclass(ParseSession.class); - - unregisterSubclass(ParsePin.class); - unregisterSubclass(EventuallyPin.class); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * - * @param name the name - * @param objects the objects to be pinned - * @param callback the callback - * @see #unpinAllInBackground(String, java.util.List, DeleteCallback) - */ - public static void pinAllInBackground(String name, - List objects, SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(name, objects), callback); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * - * @param name the name - * @param objects the objects to be pinned - * @return A {@link Task} that is resolved when pinning all completes. - * @see #unpinAllInBackground(String, java.util.List) - */ - public static Task pinAllInBackground(final String name, - final List objects) { - return pinAllInBackground(name, objects, true); - } - - private static Task pinAllInBackground(final String name, - final List objects, final boolean includeAllChildren) { - if (!Parse.isLocalDatastoreEnabled()) { - throw new IllegalStateException("Method requires Local Datastore. " + - "Please refer to `Parse#enableLocalDatastore(Context)`."); - } - - Task task = Task.forResult(null); - - // Resolve and persist unresolved users attached via ACL, similarly how we do in saveAsync - for (final ParseObject object : objects) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - if (!object.isDataAvailable(KEY_ACL)) { - return Task.forResult(null); - } - - final ParseACL acl = object.getACL(false); - if (acl == null) { - return Task.forResult(null); - } - - ParseUser user = acl.getUnresolvedUser(); - if (user == null || !user.isCurrentUser()) { - return Task.forResult(null); - } - - return ParseUser.pinCurrentUserIfNeededAsync(user); - } - }); - } - - return task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return Parse.getLocalDatastore().pinAllObjectsAsync( - name != null ? name : DEFAULT_PIN, - objects, - includeAllChildren); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - // Hack to emulate persisting current user on disk after a save like in ParseUser#saveAsync - // Note: This does not persist current user if it's a child object of `objects`, it probably - // should, but we can't unless we do something similar to #deepSaveAsync. - if (ParseCorePlugins.PIN_CURRENT_USER.equals(name)) { - return task; - } - for (ParseObject object : objects) { - if (object instanceof ParseUser) { - final ParseUser user = (ParseUser) object; - if (user.isCurrentUser()) { - return ParseUser.pinCurrentUserIfNeededAsync(user); - } - } - } - return task; - } - }); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * {@link #fetchFromLocalDatastore()} on it. - * - * @param name the name - * @param objects the objects to be pinned - * @throws ParseException exception if fails - * @see #unpinAll(String, java.util.List) - */ - public static void pinAll(String name, - List objects) throws ParseException { - ParseTaskUtils.wait(pinAllInBackground(name, objects)); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * - * @param objects the objects to be pinned - * @param callback the callback - * @see #unpinAllInBackground(java.util.List, DeleteCallback) - * @see #DEFAULT_PIN - */ - public static void pinAllInBackground(List objects, - SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(DEFAULT_PIN, objects), callback); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * - * @param objects the objects to be pinned - * @return A {@link Task} that is resolved when pinning all completes. - * @see #unpinAllInBackground(java.util.List) - * @see #DEFAULT_PIN - */ - public static Task pinAllInBackground(List objects) { - return pinAllInBackground(DEFAULT_PIN, objects); - } - - /** - * Stores the objects and every object they point to in the local datastore, recursively. If - * those other objects have not been fetched from Parse, they will not be stored. However, if they - * have changed data, all of the changes will be retained. To get the objects back later, you can - * use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it. - * - * @param objects the objects to be pinned - * @throws ParseException exception if fails - * @see #unpinAll(java.util.List) - * @see #DEFAULT_PIN - */ - public static void pinAll(List objects) throws ParseException { - ParseTaskUtils.wait(pinAllInBackground(DEFAULT_PIN, objects)); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @param objects the objects - * @param callback the callback - * @see #pinAllInBackground(String, java.util.List, SaveCallback) - */ - public static void unpinAllInBackground(String name, List objects, - DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name, objects), callback); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @param objects the objects - * @return A {@link Task} that is resolved when unpinning all completes. - * @see #pinAllInBackground(String, java.util.List) - */ - public static Task unpinAllInBackground(String name, - List objects) { - if (!Parse.isLocalDatastoreEnabled()) { - throw new IllegalStateException("Method requires Local Datastore. " + - "Please refer to `Parse#enableLocalDatastore(Context)`."); - } - if (name == null) { - name = DEFAULT_PIN; - } - return Parse.getLocalDatastore().unpinAllObjectsAsync(name, objects); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @param objects the objects - * @throws ParseException exception if fails - * @see #pinAll(String, java.util.List) - */ - public static void unpinAll(String name, - List objects) throws ParseException { - ParseTaskUtils.wait(unpinAllInBackground(name, objects)); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param objects the objects - * @param callback the callback - * @see #pinAllInBackground(java.util.List, SaveCallback) - * @see #DEFAULT_PIN - */ - public static void unpinAllInBackground(List objects, - DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(DEFAULT_PIN, objects), callback); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param objects the objects - * @return A {@link Task} that is resolved when unpinning all completes. - * @see #pinAllInBackground(java.util.List) - * @see #DEFAULT_PIN - */ - public static Task unpinAllInBackground(List objects) { - return unpinAllInBackground(DEFAULT_PIN, objects); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param objects the objects - * @throws ParseException exception if fails - * @see #pinAll(java.util.List) - * @see #DEFAULT_PIN - */ - public static void unpinAll(List objects) throws ParseException { - ParseTaskUtils.wait(unpinAllInBackground(DEFAULT_PIN, objects)); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @param callback the callback - * @see #pinAll(String, java.util.List) - */ - public static void unpinAllInBackground(String name, DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name), callback); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @return A {@link Task} that is resolved when unpinning all completes. - * @see #pinAll(String, java.util.List) - */ - public static Task unpinAllInBackground(String name) { - if (!Parse.isLocalDatastoreEnabled()) { - throw new IllegalStateException("Method requires Local Datastore. " + - "Please refer to `Parse#enableLocalDatastore(Context)`."); - } - if (name == null) { - name = DEFAULT_PIN; - } - return Parse.getLocalDatastore().unpinAllObjectsAsync(name); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param name the name - * @throws ParseException exception if fails - * @see #pinAll(String, java.util.List) - */ - public static void unpinAll(String name) throws ParseException { - ParseTaskUtils.wait(unpinAllInBackground(name)); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @param callback the callback - * @see #pinAllInBackground(java.util.List, SaveCallback) - * @see #DEFAULT_PIN - */ - public static void unpinAllInBackground(DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(), callback); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @return A {@link Task} that is resolved when unpinning all completes. - * @see #pinAllInBackground(java.util.List, SaveCallback) - * @see #DEFAULT_PIN - */ - public static Task unpinAllInBackground() { - return unpinAllInBackground(DEFAULT_PIN); - } - - /** - * Removes the objects and every object they point to in the local datastore, recursively. - * - * @throws ParseException exception if fails - * @see #pinAll(java.util.List) - * @see #DEFAULT_PIN - */ - public static void unpinAll() throws ParseException { - ParseTaskUtils.wait(unpinAllInBackground()); - } - - - static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) { - String className = source.readString(); - String objectId = source.readByte() == 1 ? source.readString() : null; - // Create empty object (might be the same instance if LDS is enabled) - // and pass to decoder before unparceling child objects in State - ParseObject object = createWithoutData(className, objectId); - if (decoder instanceof ParseObjectParcelDecoder) { - ((ParseObjectParcelDecoder) decoder).addKnownObject(object); - } - State state = State.createFromParcel(source, decoder); - object.setState(state); - if (source.readByte() == 1) object.localId = source.readString(); - if (source.readByte() == 1) object.isDeleted = true; - // If object.ldsEnabledWhenParceling is true, we got this from OfflineStore. - // There is no need to restore operations in that case. - boolean restoreOperations = !object.ldsEnabledWhenParceling; - ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); - if (restoreOperations) { - for (String key : set.keySet()) { - ParseFieldOperation op = set.get(key); - object.performOperation(key, op); // Update ops and estimatedData - } - } - Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); - object.onRestoreInstanceState(bundle); - return object; - } - - State.Init newStateBuilder(String className) { - return new State.Builder(className); - } - - State getState() { - synchronized (mutex) { - return state; - } - } - - /** - * Updates the current state of this object as well as updates our in memory cached state. - * - * @param newState The new state. - */ - void setState(State newState) { - synchronized (mutex) { - setState(newState, true); - } - } - - private void setState(State newState, boolean notifyIfObjectIdChanges) { - synchronized (mutex) { - String oldObjectId = state.objectId(); - String newObjectId = newState.objectId(); - - state = newState; - - if (notifyIfObjectIdChanges && !ParseTextUtils.equals(oldObjectId, newObjectId)) { - notifyObjectIdChanged(oldObjectId, newObjectId); - } - - rebuildEstimatedData(); - } - } - - /** - * Accessor to the class name. - */ - public String getClassName() { - synchronized (mutex) { - return state.className(); - } - } - - /** - * This reports time as the server sees it, so that if you make changes to a {@code ParseObject}, then - * wait a while, and then call {@link #save()}, the updated time will be the time of the - * {@link #save()} call rather than the time the object was changed locally. - * - * @return The last time this object was updated on the server. - */ - public Date getUpdatedAt() { - long updatedAt = getState().updatedAt(); - return updatedAt > 0 - ? new Date(updatedAt) - : null; - } - - /** - * This reports time as the server sees it, so that if you create a {@code ParseObject}, then wait a - * while, and then call {@link #save()}, the creation time will be the time of the first - * {@link #save()} call rather than the time the object was created locally. - * - * @return The first time this object was saved on the server. - */ - public Date getCreatedAt() { - long createdAt = getState().createdAt(); - return createdAt > 0 - ? new Date(createdAt) - : null; - } - - /** - * Returns a set view of the keys contained in this object. This does not include createdAt, - * updatedAt, authData, or objectId. It does include things like username and ACL. - */ - public Set keySet() { - synchronized (mutex) { - return Collections.unmodifiableSet(estimatedData.keySet()); - } - } - - /** - * Copies all of the operations that have been performed on another object since its last save - * onto this one. - */ - void copyChangesFrom(ParseObject other) { - synchronized (mutex) { - ParseOperationSet operations = other.operationSetQueue.getFirst(); - for (String key : operations.keySet()) { - performOperation(key, operations.get(key)); - } - } - } - - void mergeFromObject(ParseObject other) { - synchronized (mutex) { - // If they point to the same instance, we don't need to merge. - if (this == other) { - return; - } - - State copy = other.getState().newBuilder().build(); - - // We don't want to notify if an objectId changed here since we utilize this method to merge - // an anonymous current user with a new ParseUser instance that's calling signUp(). This - // doesn't make any sense and we should probably remove that code in ParseUser. - // Otherwise, there shouldn't be any objectId changes here since this method is only otherwise - // used in fetchAll. - setState(copy, false); - } - } - - /** - * Clears changes to this object's {@code key} made since the last call to {@link #save()} or - * {@link #saveInBackground()}. - * - * @param key The {@code key} to revert changes for. - */ - public void revert(String key) { - synchronized (mutex) { - if (isDirty(key)) { - currentOperations().remove(key); - rebuildEstimatedData(); - } - } - } - - /** - * Clears any changes to this object made since the last call to {@link #save()} or - * {@link #saveInBackground()}. - */ - public void revert() { - synchronized (mutex) { - if (isDirty()) { - currentOperations().clear(); - rebuildEstimatedData(); - } - } - } - - /** - * Deep traversal on this object to grab a copy of any object referenced by this object. These - * instances may have already been fetched, and we don't want to lose their data when refreshing - * or saving. - * - * @return the map mapping from objectId to {@code ParseObject} which has been fetched. - */ - private Map collectFetchedObjects() { - final Map fetchedObjects = new HashMap<>(); - ParseTraverser traverser = new ParseTraverser() { - @Override - protected boolean visit(Object object) { - if (object instanceof ParseObject) { - ParseObject parseObj = (ParseObject) object; - State state = parseObj.getState(); - if (state.objectId() != null && state.isComplete()) { - fetchedObjects.put(state.objectId(), parseObj); - } - } - return true; - } - }; - traverser.traverse(estimatedData); - return fetchedObjects; - } - - /** - * Helper method called by {@link #fromJSONPayload(JSONObject, ParseDecoder)} - *

- * The method helps webhooks implementation to build Parse object from raw JSON payload. - * It is different from {@link #mergeFromServer(State, JSONObject, ParseDecoder, boolean)} - * as the method saves the key value pairs (other than className, objectId, updatedAt and - * createdAt) in the operation queue rather than the server data. It also handles - * {@link ParseFieldOperations} differently. - * - * @param json : JSON object to be converted to Parse object - * @param decoder : Decoder to be used for Decoding JSON - */ - void build(JSONObject json, ParseDecoder decoder) { - try { - State.Builder builder = new State.Builder(state) - .isComplete(true); - - builder.clear(); - - Iterator keys = json.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - /* - __className: Used by fromJSONPayload, should be stripped out by the time it gets here... - */ - if (key.equals(KEY_CLASS_NAME)) { - continue; - } - if (key.equals(KEY_OBJECT_ID)) { - String newObjectId = json.getString(key); - builder.objectId(newObjectId); - continue; - } - if (key.equals(KEY_CREATED_AT)) { - builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - if (key.equals(KEY_UPDATED_AT)) { - builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - - Object value = json.get(key); - Object decodedObject = decoder.decode(value); - if (decodedObject instanceof ParseFieldOperation) { - performOperation(key, (ParseFieldOperation) decodedObject); - } else { - put(key, decodedObject); - } - } - - setState(builder.build()); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - /** - * Merges from JSON in REST format. - * Updates this object with data from the server. - * - * @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder) - */ - State mergeFromServer( - State state, JSONObject json, ParseDecoder decoder, boolean completeData) { - try { - // If server data is complete, consider this object to be fetched. - State.Init builder = state.newBuilder(); - if (completeData) { - builder.clear(); - } - builder.isComplete(state.isComplete() || completeData); - - Iterator keys = json.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - /* - __type: Returned by queries and cloud functions to designate body is a ParseObject - __className: Used by fromJSON, should be stripped out by the time it gets here... - */ - if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) { - continue; - } - if (key.equals(KEY_OBJECT_ID)) { - String newObjectId = json.getString(key); - builder.objectId(newObjectId); - continue; - } - if (key.equals(KEY_CREATED_AT)) { - builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - if (key.equals(KEY_UPDATED_AT)) { - builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - if (key.equals(KEY_ACL)) { - ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder); - builder.put(KEY_ACL, acl); - continue; - } - if (key.equals(KEY_SELECTED_KEYS)) { - JSONArray safeKeys = json.getJSONArray(key); - if (safeKeys.length() > 0) { - Collection set = new HashSet<>(); - for (int i = 0; i < safeKeys.length(); i++) { - // Don't add nested keys. - String safeKey = safeKeys.getString(i); - if (safeKey.contains(".")) safeKey = safeKey.split("\\.")[0]; - set.add(safeKey); - } - builder.availableKeys(set); - } - continue; - } - - Object value = json.get(key); - if (value instanceof JSONObject && json.has(KEY_SELECTED_KEYS)) { - // This might be a ParseObject. Pass selected keys to understand if it is complete. - JSONArray selectedKeys = json.getJSONArray(KEY_SELECTED_KEYS); - JSONArray nestedKeys = new JSONArray(); - for (int i = 0; i < selectedKeys.length(); i++) { - String nestedKey = selectedKeys.getString(i); - if (nestedKey.startsWith(key + ".")) - nestedKeys.put(nestedKey.substring(key.length() + 1)); - } - if (nestedKeys.length() > 0) { - ((JSONObject) value).put(KEY_SELECTED_KEYS, nestedKeys); - } - } - Object decodedObject = decoder.decode(value); - builder.put(key, decodedObject); - } - - return builder.build(); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - /** - * Convert to REST JSON for persisting in LDS. - * - * @see #mergeREST(State, org.json.JSONObject, ParseDecoder) - */ - JSONObject toRest(ParseEncoder encoder) { - State state; - List operationSetQueueCopy; - synchronized (mutex) { - // mutex needed to lock access to state and operationSetQueue and operationSetQueue & children - // are mutable - state = getState(); - - // operationSetQueue is a List of Lists, so we'll need custom copying logic - int operationSetQueueSize = operationSetQueue.size(); - operationSetQueueCopy = new ArrayList<>(operationSetQueueSize); - for (int i = 0; i < operationSetQueueSize; i++) { - ParseOperationSet original = operationSetQueue.get(i); - ParseOperationSet copy = new ParseOperationSet(original); - operationSetQueueCopy.add(copy); - } - } - return toRest(state, operationSetQueueCopy, encoder); - } - - JSONObject toRest( - State state, List operationSetQueue, ParseEncoder objectEncoder) { - // Public data goes in dataJSON; special fields go in objectJSON. - JSONObject json = new JSONObject(); - - try { - // REST JSON (State) - json.put(KEY_CLASS_NAME, state.className()); - if (state.objectId() != null) { - json.put(KEY_OBJECT_ID, state.objectId()); - } - if (state.createdAt() > 0) { - json.put(KEY_CREATED_AT, - ParseDateFormat.getInstance().format(new Date(state.createdAt()))); - } - if (state.updatedAt() > 0) { - json.put(KEY_UPDATED_AT, - ParseDateFormat.getInstance().format(new Date(state.updatedAt()))); - } - for (String key : state.keySet()) { - Object value = state.get(key); - json.put(key, objectEncoder.encode(value)); - } - - // Internal JSON - //TODO(klimt): We'll need to rip all this stuff out and put it somewhere else if we start - // using the REST api and want to send data to Parse. - json.put(KEY_COMPLETE, state.isComplete()); - json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); - JSONArray availableKeys = new JSONArray(state.availableKeys()); - json.put(KEY_SELECTED_KEYS, availableKeys); - - // Operation Set Queue - JSONArray operations = new JSONArray(); - for (ParseOperationSet operationSet : operationSetQueue) { - operations.put(operationSet.toRest(objectEncoder)); - } - json.put(KEY_OPERATIONS, operations); - - } catch (JSONException e) { - throw new RuntimeException("could not serialize object to JSON"); - } - - return json; - } - - /** - * Merge with REST JSON from LDS. - * - * @see #toRest(ParseEncoder) - */ - void mergeREST(State state, JSONObject json, ParseDecoder decoder) { - ArrayList saveEventuallyOperationSets = new ArrayList<>(); - - synchronized (mutex) { - try { - boolean isComplete = json.getBoolean(KEY_COMPLETE); - isDeletingEventually = ParseJSONUtils.getInt(json, Arrays.asList( - KEY_IS_DELETING_EVENTUALLY, - KEY_IS_DELETING_EVENTUALLY_OLD - )); - JSONArray operations = json.getJSONArray(KEY_OPERATIONS); - { - ParseOperationSet newerOperations = currentOperations(); - operationSetQueue.clear(); - - // Add and enqueue any saveEventually operations, roll forward any other operation sets - // (operation sets here are generally failed/incomplete saves). - ParseOperationSet current = null; - for (int i = 0; i < operations.length(); i++) { - JSONObject operationSetJSON = operations.getJSONObject(i); - ParseOperationSet operationSet = ParseOperationSet.fromRest(operationSetJSON, decoder); - - if (operationSet.isSaveEventually()) { - if (current != null) { - operationSetQueue.add(current); - current = null; - } - saveEventuallyOperationSets.add(operationSet); - operationSetQueue.add(operationSet); - continue; - } - - if (current != null) { - operationSet.mergeFrom(current); - } - current = operationSet; - } - if (current != null) { - operationSetQueue.add(current); - } - - // Merge the changes that were previously in memory into the updated object. - currentOperations().mergeFrom(newerOperations); - } - - // We only want to merge server data if we our updatedAt is null (we're unsaved or from - // #createWithoutData) or if the JSON's updatedAt is newer than ours. - boolean mergeServerData = false; - if (state.updatedAt() < 0) { - mergeServerData = true; - } else if (json.has(KEY_UPDATED_AT)) { - Date otherUpdatedAt = ParseDateFormat.getInstance().parse(json.getString(KEY_UPDATED_AT)); - if (new Date(state.updatedAt()).compareTo(otherUpdatedAt) < 0) { - mergeServerData = true; - } - } - - if (mergeServerData) { - // Clean up internal json keys - JSONObject mergeJSON = ParseJSONUtils.create(json, Arrays.asList( - KEY_COMPLETE, KEY_IS_DELETING_EVENTUALLY, KEY_IS_DELETING_EVENTUALLY_OLD, - KEY_OPERATIONS - )); - State newState = mergeFromServer(state, mergeJSON, decoder, isComplete); - setState(newState); - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - // We cannot modify the taskQueue inside synchronized (mutex). - for (ParseOperationSet operationSet : saveEventuallyOperationSets) { - enqueueSaveEventuallyOperationAsync(operationSet); - } - } - - private boolean hasDirtyChildren() { - synchronized (mutex) { - // We only need to consider the currently estimated children here, - // because they're the only ones that might need to be saved in a - // subsequent call to save, which is the meaning of "dirtiness". - List unsavedChildren = new ArrayList<>(); - collectDirtyChildren(estimatedData, unsavedChildren, null); - return unsavedChildren.size() > 0; - } - } - - /** - * Whether any key-value pair in this object (or its children) has been added/updated/removed and - * not saved yet. - * - * @return Whether this object has been altered and not saved yet. - */ - public boolean isDirty() { - return this.isDirty(true); - } - - boolean isDirty(boolean considerChildren) { - synchronized (mutex) { - return (isDeleted || getObjectId() == null || hasChanges() || (considerChildren && hasDirtyChildren())); - } - } - - boolean hasChanges() { - synchronized (mutex) { - return currentOperations().size() > 0; - } - } - - /** - * Returns {@code true} if this {@code ParseObject} has operations in operationSetQueue that - * haven't been completed yet, {@code false} if there are no operations in the operationSetQueue. - */ - boolean hasOutstandingOperations() { - synchronized (mutex) { - // > 1 since 1 is for unsaved changes. - return operationSetQueue.size() > 1; - } - } - - /** - * Whether a value associated with a key has been added/updated/removed and not saved yet. - * - * @param key The key to check for - * @return Whether this key has been altered and not saved yet. - */ - public boolean isDirty(String key) { - synchronized (mutex) { - return currentOperations().containsKey(key); - } - } - - /** - * Accessor to the object id. An object id is assigned as soon as an object is saved to the - * server. The combination of a className and an objectId uniquely identifies an object in your - * application. - * - * @return The object id. - */ - public String getObjectId() { - synchronized (mutex) { - return state.objectId(); - } - } - - /** - * Setter for the object id. In general you do not need to use this. However, in some cases this - * can be convenient. For example, if you are serializing a {@code ParseObject} yourself and wish - * to recreate it, you can use this to recreate the {@code ParseObject} exactly. - */ - public void setObjectId(String newObjectId) { - synchronized (mutex) { - String oldObjectId = state.objectId(); - if (ParseTextUtils.equals(oldObjectId, newObjectId)) { - return; - } - - // We don't need to use setState since it doesn't affect our cached state. - state = state.newBuilder().objectId(newObjectId).build(); - notifyObjectIdChanged(oldObjectId, newObjectId); - } - } - - /** - * Returns the localId, which is used internally for serializing relations to objects that don't - * yet have an objectId. - */ - String getOrCreateLocalId() { - synchronized (mutex) { - if (localId == null) { - if (state.objectId() != null) { - throw new IllegalStateException( - "Attempted to get a localId for an object with an objectId."); - } - localId = getLocalIdManager().createLocalId(); - } - return localId; - } - } - - // Sets the objectId without marking dirty. - private void notifyObjectIdChanged(String oldObjectId, String newObjectId) { - synchronized (mutex) { - // The offline store may throw if this object already had a different objectId. - OfflineStore store = Parse.getLocalDatastore(); - if (store != null) { - store.updateObjectId(this, oldObjectId, newObjectId); - } - - if (localId != null) { - getLocalIdManager().setObjectId(localId, newObjectId); - localId = null; - } - } - } - - private ParseRESTObjectCommand currentSaveEventuallyCommand( - ParseOperationSet operations, ParseEncoder objectEncoder, String sessionToken) { - State state = getState(); - - /* - * Get the JSON representation of the object, and use some of the information to construct the - * command. - */ - JSONObject objectJSON = toJSONObjectForSaving(state, operations, objectEncoder); - - return ParseRESTObjectCommand.saveObjectCommand( - state, - objectJSON, - sessionToken); - } - - /** - * Converts a {@code ParseObject} to a JSON representation for saving to Parse. - *

- *

-     * {
-     *   data: { // objectId plus any ParseFieldOperations },
-     *   classname: class name for the object
-     * }
-     * 
- *

- * updatedAt and createdAt are not included. only dirty keys are represented in the data. - * - * @see #mergeFromServer(State state, org.json.JSONObject, ParseDecoder, boolean) - */ - // Currently only used by saveEventually - JSONObject toJSONObjectForSaving( - T state, ParseOperationSet operations, ParseEncoder objectEncoder) { - JSONObject objectJSON = new JSONObject(); - - try { - // Serialize the data - for (String key : operations.keySet()) { - ParseFieldOperation operation = operations.get(key); - objectJSON.put(key, objectEncoder.encode(operation)); - - // TODO(grantland): Use cached value from hashedObjects if it's a set operation. - } - - if (state.objectId() != null) { - objectJSON.put(KEY_OBJECT_ID, state.objectId()); - } - } catch (JSONException e) { - throw new RuntimeException("could not serialize object to JSON"); - } - - return objectJSON; - } - - /** - * Handles the result of {@code save}. - *

- * Should be called on success or failure. - */ - // TODO(grantland): Remove once we convert saveEventually and ParseUser.signUp/resolveLaziness - // to controllers - Task handleSaveResultAsync( - final JSONObject result, final ParseOperationSet operationsBeforeSave) { - ParseObject.State newState = null; - - if (result != null) { // Success - synchronized (mutex) { - final Map fetchedObjects = collectFetchedObjects(); - ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); - newState = ParseObjectCoder.get().decode(getState().newBuilder().clear(), result, decoder) - .isComplete(false) - .build(); - } - } - - return handleSaveResultAsync(newState, operationsBeforeSave); - } - - /** - * Handles the result of {@code save}. - *

- * Should be called on success or failure. - */ - Task handleSaveResultAsync( - final ParseObject.State result, final ParseOperationSet operationsBeforeSave) { - Task task = Task.forResult(null); - - /* - * If this object is in the offline store, then we need to make sure that we pull in any dirty - * changes it may have before merging the server data into it. - */ - final OfflineStore store = Parse.getLocalDatastore(); - if (store != null) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.fetchLocallyAsync(ParseObject.this).makeVoid(); - } - }); - } - - final boolean success = result != null; - synchronized (mutex) { - // Find operationsBeforeSave in the queue so that we can remove it and move to the next - // operation set. - ListIterator opIterator = - operationSetQueue.listIterator(operationSetQueue.indexOf(operationsBeforeSave)); - opIterator.next(); - opIterator.remove(); - - if (!success) { - // Merge the data from the failed save into the next save. - ParseOperationSet nextOperation = opIterator.next(); - nextOperation.mergeFrom(operationsBeforeSave); - if (store != null) { - task = task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - return Task.forResult(null); - } else { - return store.updateDataForObjectAsync(ParseObject.this); - } - } - }); - } - return task; - } - } - - // fetchLocallyAsync will return an error if this object isn't in the LDS yet and that's ok - task = task.continueWith(new Continuation() { - @Override - public Void then(Task task) { - synchronized (mutex) { - State newState; - if (result.isComplete()) { - // Result is complete, so just replace - newState = result; - } else { - // Result is incomplete, so we'll need to apply it to the current state - newState = getState().newBuilder() - .apply(operationsBeforeSave) - .apply(result) - .build(); - } - setState(newState); - } - return null; - } - }); - - if (store != null) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.updateDataForObjectAsync(ParseObject.this); - } - }); - } - - task = task.onSuccess(new Continuation() { - @Override - public Void then(Task task) { - saveEvent.invoke(ParseObject.this, null); - return null; - } - }); - - return task; - } - - ParseOperationSet startSave() { - synchronized (mutex) { - ParseOperationSet currentOperations = currentOperations(); - operationSetQueue.addLast(new ParseOperationSet()); - return currentOperations; - } - } - - void validateSave() { - // do nothing - } - - /** - * Saves this object to the server. Typically, you should use {@link #saveInBackground} instead of - * this, unless you are managing your own threading. - * - * @throws ParseException Throws an exception if the server is inaccessible. - */ - public final void save() throws ParseException { - ParseTaskUtils.wait(saveInBackground()); - } - - /** - * Saves this object to the server in a background thread. This is preferable to using {@link #save()}, - * unless your code is already running from a background thread. - * - * @return A {@link Task} that is resolved when the save completes. - */ - public final Task saveInBackground() { - return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser current = task.getResult(); - if (current == null) { - return Task.forResult(null); - } - if (!current.isLazy()) { - return Task.forResult(current.getSessionToken()); - } - - // The current user is lazy/unresolved. If it is attached to us via ACL, we'll need to - // resolve/save it before proceeding. - if (!isDataAvailable(KEY_ACL)) { - return Task.forResult(null); - } - final ParseACL acl = getACL(false); - if (acl == null) { - return Task.forResult(null); - } - final ParseUser user = acl.getUnresolvedUser(); - if (user == null || !user.isCurrentUser()) { - return Task.forResult(null); - } - return user.saveAsync(null).onSuccess(new Continuation() { - @Override - public String then(Task task) { - if (acl.hasUnresolvedUser()) { - throw new IllegalStateException("ACL has an unresolved ParseUser. " - + "Save or sign up before attempting to serialize the ACL."); - } - return user.getSessionToken(); - } - }); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return saveAsync(sessionToken); - } - }); - } - - Task saveAsync(final String sessionToken) { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return saveAsync(sessionToken, toAwait); - } - }); - } - - Task saveAsync(final String sessionToken, final Task toAwait) { - if (!isDirty()) { - return Task.forResult(null); - } - - final ParseOperationSet operations; - synchronized (mutex) { - updateBeforeSave(); - validateSave(); - operations = startSave(); - } - - Task task; - synchronized (mutex) { - // Recursively save children - - /* - * TODO(klimt): Why is this estimatedData and not... I mean, what if a child is - * removed after save is called, but before the unresolved user gets resolved? It - * won't get saved. - */ - task = deepSaveAsync(estimatedData, sessionToken); - } - - return task.onSuccessTask( - TaskQueue.waitFor(toAwait) - ).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final Map fetchedObjects = collectFetchedObjects(); - ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); - return getObjectController().saveAsync(getState(), operations, sessionToken, decoder); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(final Task saveTask) { - ParseObject.State result = saveTask.getResult(); - return handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted() || task.isCancelled()) { - return task; - } - - // We still want to propagate saveTask errors - return saveTask.makeVoid(); - } - }); - } - }); - } - - // Currently only used by ParsePinningEventuallyQueue for saveEventually due to the limitation in - // ParseCommandCache that it can only return JSONObject result. - Task saveAsync( - ParseHttpClient client, - final ParseOperationSet operationSet, - String sessionToken) { - final ParseRESTCommand command = - currentSaveEventuallyCommand(operationSet, PointerEncoder.get(), sessionToken); - return command.executeAsync(client); - } - - /** - * Saves this object to the server in a background thread. This is preferable to using {@link #save()}, - * unless your code is already running from a background thread. - * - * @param callback {@code callback.done(e)} is called when the save completes. - */ - public final void saveInBackground(SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback); - } - - void validateSaveEventually() throws ParseException { - // do nothing - } - - /** - * Saves this object to the server at some unspecified time in the future, even if Parse is - * currently inaccessible. Use this when you may not have a solid network connection, and don't - * need to know when the save completes. If there is some problem with the object such that it - * can't be saved, it will be silently discarded. Objects saved with this method will be stored - * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately - * if possible. Otherwise, they will be sent the next time a network connection is available. - * Objects saved this way will persist even after the app is closed, in which case they will be - * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, - * subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old - * saves to be silently discarded until the connection can be re-established, and the queued - * objects can be saved. - * - * @param callback - A callback which will be called if the save completes before the app exits. - */ - public final void saveEventually(SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(saveEventually(), callback); - } - - /** - * Saves this object to the server at some unspecified time in the future, even if Parse is - * currently inaccessible. Use this when you may not have a solid network connection, and don't - * need to know when the save completes. If there is some problem with the object such that it - * can't be saved, it will be silently discarded. Objects saved with this method will be stored - * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately - * if possible. Otherwise, they will be sent the next time a network connection is available. - * Objects saved this way will persist even after the app is closed, in which case they will be - * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, - * subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old - * saves to be silently discarded until the connection can be re-established, and the queued - * objects can be saved. - * - * @return A {@link Task} that is resolved when the save completes. - */ - public final Task saveEventually() { - if (!isDirty()) { - Parse.getEventuallyQueue().fakeObjectUpdate(); - return Task.forResult(null); - } - - final ParseOperationSet operationSet; - final ParseRESTCommand command; - final Task runEventuallyTask; - - synchronized (mutex) { - updateBeforeSave(); - try { - validateSaveEventually(); - } catch (ParseException e) { - return Task.forError(e); - } - - // TODO(klimt): Once we allow multiple saves on an object, this - // should be collecting dirty children from the estimate based on - // whatever data is going to be sent by this saveEventually, which - // won't necessarily be the current estimatedData. We should resolve - // this when the multiple save code is added. - List unsavedChildren = new ArrayList<>(); - collectDirtyChildren(estimatedData, unsavedChildren, null); - - String localId = null; - if (getObjectId() == null) { - localId = getOrCreateLocalId(); - } - - operationSet = startSave(); - operationSet.setIsSaveEventually(true); - - //TODO (grantland): Convert to async - final String sessionToken = ParseUser.getCurrentSessionToken(); - - // See [1] - command = currentSaveEventuallyCommand(operationSet, PointerOrLocalIdEncoder.get(), - sessionToken); - - // TODO: Make this logic make sense once we have deepSaveEventually - command.setLocalId(localId); - - // Mark the command with a UUID so that we can match it up later. - command.setOperationSetUUID(operationSet.getUUID()); - - // Ensure local ids are retained before saveEventually-ing children - command.retainLocalIds(); - - for (ParseObject object : unsavedChildren) { - object.saveEventually(); - } - - } - - // We cannot modify the taskQueue inside synchronized (mutex). - ParseEventuallyQueue cache = Parse.getEventuallyQueue(); - runEventuallyTask = cache.enqueueEventuallyAsync(command, ParseObject.this); - enqueueSaveEventuallyOperationAsync(operationSet); - - // Release the extra retained local ids. - command.releaseLocalIds(); - - Task handleSaveResultTask; - if (Parse.isLocalDatastoreEnabled()) { - // ParsePinningEventuallyQueue calls handleSaveEventuallyResultAsync directly. - handleSaveResultTask = runEventuallyTask.makeVoid(); - } else { - handleSaveResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - JSONObject json = task.getResult(); - return handleSaveEventuallyResultAsync(json, operationSet); - } - }); - } - return handleSaveResultTask; - } - - /** - * Enqueues the saveEventually ParseOperationSet in {@link #taskQueue}. - */ - private void enqueueSaveEventuallyOperationAsync(final ParseOperationSet operationSet) { - if (!operationSet.isSaveEventually()) { - throw new IllegalStateException( - "This should only be used to enqueue saveEventually operation sets"); - } - - taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseEventuallyQueue cache = Parse.getEventuallyQueue(); - return cache.waitForOperationSetAndEventuallyPin(operationSet, null).makeVoid(); - } - }); - } - }); - } - - /** - * Handles the result of {@code saveEventually}. - *

- * In addition to normal save handling, this also notifies the saveEventually test helper. - *

- * Should be called on success or failure. - */ - Task handleSaveEventuallyResultAsync( - JSONObject json, ParseOperationSet operationSet) { - final boolean success = json != null; - Task handleSaveResultTask = handleSaveResultAsync(json, operationSet); - - return handleSaveResultTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - if (success) { - Parse.getEventuallyQueue() - .notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_UPDATED); - } - return task; - } - }); - } - - /** - * Called by {@link #saveInBackground()} and {@link #saveEventually(SaveCallback)} - * and guaranteed to be thread-safe. Subclasses can override this method to do any custom updates - * before an object gets saved. - */ - void updateBeforeSave() { - // do nothing - } - - /** - * Deletes this object from the server at some unspecified time in the future, even if Parse is - * currently inaccessible. Use this when you may not have a solid network connection, and don't - * need to know when the delete completes. If there is some problem with the object such that it - * can't be deleted, the request will be silently discarded. Delete requests made with this method - * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be - * sent immediately if possible. Otherwise, they will be sent the next time a network connection - * is available. Delete instructions saved this way will persist even after the app is closed, in - * which case they will be sent the next time the app is opened. If more than 10MB of commands are - * waiting to be sent, subsequent calls to {@code #deleteEventually()} or - * {@link #saveEventually()} will cause old instructions to be silently discarded until the - * connection can be re-established, and the queued objects can be saved. - * - * @param callback - A callback which will be called if the delete completes before the app exits. - */ - public final void deleteEventually(DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(deleteEventually(), callback); - } - - /** - * Deletes this object from the server at some unspecified time in the future, even if Parse is - * currently inaccessible. Use this when you may not have a solid network connection, and don't - * need to know when the delete completes. If there is some problem with the object such that it - * can't be deleted, the request will be silently discarded. Delete requests made with this method - * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be - * sent immediately if possible. Otherwise, they will be sent the next time a network connection - * is available. Delete instructions saved this way will persist even after the app is closed, in - * which case they will be sent the next time the app is opened. If more than 10MB of commands are - * waiting to be sent, subsequent calls to {@code #deleteEventually()} or - * {@link #saveEventually()} will cause old instructions to be silently discarded until the - * connection can be re-established, and the queued objects can be saved. - * - * @return A {@link Task} that is resolved when the delete completes. - */ - public final Task deleteEventually() { - final ParseRESTCommand command; - final Task runEventuallyTask; - synchronized (mutex) { - validateDelete(); - isDeletingEventually += 1; - - String localId = null; - if (getObjectId() == null) { - localId = getOrCreateLocalId(); - } - - // TODO(grantland): Convert to async - final String sessionToken = ParseUser.getCurrentSessionToken(); - - // See [1] - command = ParseRESTObjectCommand.deleteObjectCommand( - getState(), sessionToken); - command.setLocalId(localId); - - runEventuallyTask = Parse.getEventuallyQueue().enqueueEventuallyAsync(command, ParseObject.this); - } - - Task handleDeleteResultTask; - if (Parse.isLocalDatastoreEnabled()) { - // ParsePinningEventuallyQueue calls handleDeleteEventuallyResultAsync directly. - handleDeleteResultTask = runEventuallyTask.makeVoid(); - } else { - handleDeleteResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return handleDeleteEventuallyResultAsync(); - } - }); - } - - return handleDeleteResultTask; - } - - /** - * Handles the result of {@code deleteEventually}. - *

- * Should only be called on success. - */ - Task handleDeleteEventuallyResultAsync() { - synchronized (mutex) { - isDeletingEventually -= 1; - } - Task handleDeleteResultTask = handleDeleteResultAsync(); - - return handleDeleteResultTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - Parse.getEventuallyQueue() - .notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_REMOVED); - return task; - } - }); - } - - /** - * Handles the result of {@code fetch}. - *

- * Should only be called on success. - */ - Task handleFetchResultAsync(final ParseObject.State result) { - Task task = Task.forResult(null); - - /* - * If this object is in the offline store, then we need to make sure that we pull in any dirty - * changes it may have before merging the server data into it. - */ - final OfflineStore store = Parse.getLocalDatastore(); - if (store != null) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.fetchLocallyAsync(ParseObject.this).makeVoid(); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // Catch CACHE_MISS - if (task.getError() instanceof ParseException - && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) { - return null; - } - return task; - } - }); - } - - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (mutex) { - State newState; - if (result.isComplete()) { - // Result is complete, so just replace - newState = result; - } else { - // Result is incomplete, so we'll need to apply it to the current state - newState = getState().newBuilder().apply(result).build(); - } - setState(newState); - } - return null; - } - }); - - if (store != null) { - task = task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return store.updateDataForObjectAsync(ParseObject.this); - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // Catch CACHE_MISS - if (task.getError() instanceof ParseException - && ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) { - return null; - } - return task; - } - }); - } - - return task; - } - - /** - * Fetches this object with the data from the server. Call this whenever you want the state of the - * object to reflect exactly what is on the server. - * - * @return The {@code ParseObject} that was fetched. - * @throws ParseException Throws an exception if the server is inaccessible. - */ - public T fetch() throws ParseException { - return ParseTaskUtils.wait(this.fetchInBackground()); - } - - @SuppressWarnings("unchecked") - Task fetchAsync( - final String sessionToken, Task toAwait) { - return toAwait.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - State state; - Map fetchedObjects; - synchronized (mutex) { - state = getState(); - fetchedObjects = collectFetchedObjects(); - } - ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects); - return getObjectController().fetchAsync(state, sessionToken, decoder); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseObject.State result = task.getResult(); - return handleFetchResultAsync(result); - } - }).onSuccess(new Continuation() { - @Override - public T then(Task task) { - return (T) ParseObject.this; - } - }); - } - - /** - * Fetches this object with the data from the server in a background thread. This is preferable to - * using fetch(), unless your code is already running from a background thread. - * - * @return A {@link Task} that is resolved when fetch completes. - */ - public final Task fetchInBackground() { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return fetchAsync(sessionToken, toAwait); - } - }); - } - }); - } - - /** - * Fetches this object with the data from the server in a background thread. This is preferable to - * using fetch(), unless your code is already running from a background thread. - * - * @param callback {@code callback.done(object, e)} is called when the fetch completes. - */ - public final void fetchInBackground(GetCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(this.fetchInBackground(), callback); - } - - /** - * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), - * fetches this object with the data from the server in a background thread. This is preferable to - * using {@link #fetchIfNeeded()}, unless your code is already running from a background thread. - * - * @return A {@link Task} that is resolved when fetch completes. - */ - public final Task fetchIfNeededInBackground() { - if (isDataAvailable()) { - return Task.forResult((T) this); - } - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - if (isDataAvailable()) { - return Task.forResult((T) ParseObject.this); - } - return fetchAsync(sessionToken, toAwait); - } - }); - } - }); - - } - - /** - * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), - * fetches this object with the data from the server. - * - * @return The fetched {@code ParseObject}. - * @throws ParseException Throws an exception if the server is inaccessible. - */ - public T fetchIfNeeded() throws ParseException { - return ParseTaskUtils.wait(this.fetchIfNeededInBackground()); - } - - /** - * If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), - * fetches this object with the data from the server in a background thread. This is preferable to - * using {@link #fetchIfNeeded()}, unless your code is already running from a background thread. - * - * @param callback {@code callback.done(object, e)} is called when the fetch completes. - */ - public final void fetchIfNeededInBackground(GetCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(this.fetchIfNeededInBackground(), callback); - } - - // Validates the delete method - void validateDelete() { - // do nothing - } - - private Task deleteAsync(final String sessionToken, Task toAwait) { - validateDelete(); - - return toAwait.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - isDeleting = true; - if (state.objectId() == null) { - return task.cast(); // no reason to call delete since it doesn't exist - } - return deleteAsync(sessionToken); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return handleDeleteResultAsync(); - } - }).continueWith(new Continuation() { - @Override - public Void then(Task task) throws Exception { - isDeleting = false; - if (task.isFaulted()) { - throw task.getError(); - } - return null; - } - }); - } - - //TODO (grantland): I'm not sure we want direct access to this. All access to `delete` should - // enqueue on the taskQueue... - Task deleteAsync(String sessionToken) { - return getObjectController().deleteAsync(getState(), sessionToken); - } - - /** - * Handles the result of {@code delete}. - *

- * Should only be called on success. - */ - Task handleDeleteResultAsync() { - Task task = Task.forResult(null); - - synchronized (mutex) { - isDeleted = true; - } - - final OfflineStore store = Parse.getLocalDatastore(); - if (store != null) { - task = task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (mutex) { - if (isDeleted) { - store.unregisterObject(ParseObject.this); - return store.deleteDataForObjectAsync(ParseObject.this); - } else { - return store.updateDataForObjectAsync(ParseObject.this); - } - } - } - }); - } - - return task; - } - - /** - * Deletes this object on the server in a background thread. This is preferable to using - * {@link #delete()}, unless your code is already running from a background thread. - * - * @return A {@link Task} that is resolved when delete completes. - */ - public final Task deleteInBackground() { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final String sessionToken = task.getResult(); - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return deleteAsync(sessionToken, toAwait); - } - }); - } - }); - } - - /** - * Deletes this object on the server. This does not delete or destroy the object locally. - * - * @throws ParseException Throws an error if the object does not exist or if the internet fails. - */ - public final void delete() throws ParseException { - ParseTaskUtils.wait(deleteInBackground()); - } - - /** - * Deletes this object on the server in a background thread. This is preferable to using - * {@link #delete()}, unless your code is already running from a background thread. - * - * @param callback {@code callback.done(e)} is called when the save completes. - */ - public final void deleteInBackground(DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(deleteInBackground(), callback); - } - - /** - * Returns {@code true} if this object can be serialized for saving. - */ - private boolean canBeSerialized() { - synchronized (mutex) { - final Capture result = new Capture<>(true); - - // This method is only used for batching sets of objects for saveAll - // and when saving children automatically. Since it's only used to - // determine whether or not save should be called on them, it only - // needs to examine their current values, so we use estimatedData. - new ParseTraverser() { - @Override - protected boolean visit(Object value) { - if (value instanceof ParseFile) { - ParseFile file = (ParseFile) value; - if (file.isDirty()) { - result.set(false); - } - } - - if (value instanceof ParseObject) { - ParseObject object = (ParseObject) value; - if (object.getObjectId() == null) { - result.set(false); - } - } - - // Continue to traverse only if it can still be serialized. - return result.get(); - } - }.setYieldRoot(false).setTraverseParseObjects(true).traverse(this); - - return result.get(); - } - } - - /** - * Return the operations that will be sent in the next call to save. - */ - private ParseOperationSet currentOperations() { - synchronized (mutex) { - return operationSetQueue.getLast(); - } - } - - /** - * Updates the estimated values in the map based on the given set of ParseFieldOperations. - */ - private void applyOperations(ParseOperationSet operations, Map map) { - for (String key : operations.keySet()) { - ParseFieldOperation operation = operations.get(key); - Object oldValue = map.get(key); - Object newValue = operation.apply(oldValue, key); - if (newValue != null) { - map.put(key, newValue); - } else { - map.remove(key); - } - } - } - - /** - * Regenerates the estimatedData map from the serverData and operations. - */ - private void rebuildEstimatedData() { - synchronized (mutex) { - estimatedData.clear(); - for (String key : state.keySet()) { - estimatedData.put(key, state.get(key)); - } - for (ParseOperationSet operations : operationSetQueue) { - applyOperations(operations, estimatedData); - } - } - } - - void markAllFieldsDirty() { - synchronized (mutex) { - for (String key : state.keySet()) { - performPut(key, state.get(key)); - } - } - } - - /** - * performOperation() is like {@link #put(String, Object)} but instead of just taking a new value, - * it takes a ParseFieldOperation that modifies the value. - */ - void performOperation(String key, ParseFieldOperation operation) { - synchronized (mutex) { - Object oldValue = estimatedData.get(key); - Object newValue = operation.apply(oldValue, key); - if (newValue != null) { - estimatedData.put(key, newValue); - } else { - estimatedData.remove(key); - } - - ParseFieldOperation oldOperation = currentOperations().get(key); - ParseFieldOperation newOperation = operation.mergeWithPrevious(oldOperation); - currentOperations().put(key, newOperation); - } - } - - /** - * Add a key-value pair to this object. It is recommended to name keys in - * camelCaseLikeThis. - * - * @param key Keys must be alphanumerical plus underscore, and start with a letter. - * @param value Values may be numerical, {@link String}, {@link JSONObject}, {@link JSONArray}, - * {@link JSONObject#NULL}, or other {@code ParseObject}s. value may not be {@code null}. - */ - public void put(@NonNull String key, @NonNull Object value) { - checkKeyIsMutable(key); - - performPut(key, value); - } - - void performPut(String key, Object value) { - if (key == null) { - throw new IllegalArgumentException("key may not be null."); - } - - if (value == null) { - throw new IllegalArgumentException("value may not be null."); - } - - if (value instanceof JSONObject) { - ParseDecoder decoder = ParseDecoder.get(); - value = decoder.convertJSONObjectToMap((JSONObject) value); - } else if (value instanceof JSONArray) { - ParseDecoder decoder = ParseDecoder.get(); - value = decoder.convertJSONArrayToList((JSONArray) value); - } - - if (!ParseEncoder.isValidType(value)) { - throw new IllegalArgumentException("invalid type for value: " + value.getClass().toString()); - } - - performOperation(key, new ParseSetOperation(value)); - } - - /** - * Whether this object has a particular key. Same as {@link #containsKey(String)}. - * - * @param key The key to check for - * @return Whether this object contains the key - */ - public boolean has(@NonNull String key) { - return containsKey(key); - } - - /** - * Atomically increments the given key by 1. - * - * @param key The key to increment. - */ - public void increment(@NonNull String key) { - increment(key, 1); - } - - /** - * Atomically increments the given key by the given number. - * - * @param key The key to increment. - * @param amount The amount to increment by. - */ - public void increment(@NonNull String key, @NonNull Number amount) { - ParseIncrementOperation operation = new ParseIncrementOperation(amount); - performOperation(key, operation); - } - - /** - * Atomically adds an object to the end of the array associated with a given key. - * - * @param key The key. - * @param value The object to add. - */ - public void add(@NonNull String key, Object value) { - this.addAll(key, Collections.singletonList(value)); - } - - /** - * Atomically adds the objects contained in a {@code Collection} to the end of the array - * associated with a given key. - * - * @param key The key. - * @param values The objects to add. - */ - public void addAll(@NonNull String key, Collection values) { - ParseAddOperation operation = new ParseAddOperation(values); - performOperation(key, operation); - } - - /** - * Atomically adds an object to the array associated with a given key, only if it is not already - * present in the array. The position of the insert is not guaranteed. - * - * @param key The key. - * @param value The object to add. - */ - public void addUnique(@NonNull String key, Object value) { - this.addAllUnique(key, Collections.singletonList(value)); - } - - /** - * Atomically adds the objects contained in a {@code Collection} to the array associated with a - * given key, only adding elements which are not already present in the array. The position of the - * insert is not guaranteed. - * - * @param key The key. - * @param values The objects to add. - */ - public void addAllUnique(@NonNull String key, Collection values) { - ParseAddUniqueOperation operation = new ParseAddUniqueOperation(values); - performOperation(key, operation); - } - - /** - * Removes a key from this object's data if it exists. - * - * @param key The key to remove. - */ - public void remove(@NonNull String key) { - checkKeyIsMutable(key); - - performRemove(key); - } - - void performRemove(String key) { - synchronized (mutex) { - Object object = get(key); - - if (object != null) { - performOperation(key, ParseDeleteOperation.getInstance()); - } - } - } - - /** - * Atomically removes all instances of the objects contained in a {@code Collection} from the - * array associated with a given key. To maintain consistency with the Java Collection API, there - * is no method removing all instances of a single object. Instead, you can call - * {@code parseObject.removeAll(key, Arrays.asList(value))}. - * - * @param key The key. - * @param values The objects to remove. - */ - public void removeAll(@NonNull String key, Collection values) { - checkKeyIsMutable(key); - - ParseRemoveOperation operation = new ParseRemoveOperation(values); - performOperation(key, operation); - } - - private void checkKeyIsMutable(String key) { - if (!isKeyMutable(key)) { - throw new IllegalArgumentException("Cannot modify `" + key - + "` property of an " + getClassName() + " object."); - } - } - - boolean isKeyMutable(String key) { - return true; - } - - /** - * Whether this object has a particular key. Same as {@link #has(String)}. - * - * @param key The key to check for - * @return Whether this object contains the key - */ - public boolean containsKey(@NonNull String key) { - synchronized (mutex) { - return estimatedData.containsKey(key); - } - } - - /** - * Access a {@link String} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link String}. - */ - @Nullable - public String getString(@NonNull String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof String)) { - return null; - } - return (String) value; - } - } - - /** - * Access a {@code byte[]} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@code byte[]}. - */ - @Nullable - public byte[] getBytes(String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof byte[])) { - return null; - } - - return (byte[]) value; - } - } - - /** - * Access a {@link Number} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link Number}. - */ - @Nullable - public Number getNumber(String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof Number)) { - return null; - } - return (Number) value; - } - } - - /** - * Access a {@link JSONArray} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link JSONArray}. - */ - @Nullable - public JSONArray getJSONArray(String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - - if (value instanceof List) { - value = PointerOrLocalIdEncoder.get().encode(value); - } - - if (!(value instanceof JSONArray)) { - return null; - } - return (JSONArray) value; - } - } - - /** - * Access a {@link List} value. - * - * @param key The key to access the value for - * @return {@code null} if there is no such key or if the value can't be converted to a - * {@link List}. - */ - @Nullable - public List getList(String key) { - synchronized (mutex) { - Object value = estimatedData.get(key); - if (!(value instanceof List)) { - return null; - } - @SuppressWarnings("unchecked") - List returnValue = (List) value; - return returnValue; - } - } - - /** - * Access a {@link Map} value - * - * @param key The key to access the value for - * @return {@code null} if there is no such key or if the value can't be converted to a - * {@link Map}. - */ - @Nullable - public Map getMap(String key) { - synchronized (mutex) { - Object value = estimatedData.get(key); - if (!(value instanceof Map)) { - return null; - } - @SuppressWarnings("unchecked") - Map returnValue = (Map) value; - return returnValue; - } - } - - /** - * Access a {@link JSONObject} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link JSONObject}. - */ - @Nullable - public JSONObject getJSONObject(String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - - if (value instanceof Map) { - value = PointerOrLocalIdEncoder.get().encode(value); - } - - if (!(value instanceof JSONObject)) { - return null; - } - - return (JSONObject) value; - } - } - - /** - * Access an {@code int} value. - * - * @param key The key to access the value for. - * @return {@code 0} if there is no such key or if it is not a {@code int}. - */ - public int getInt(@NonNull String key) { - Number number = getNumber(key); - if (number == null) { - return 0; - } - return number.intValue(); - } - - /** - * Access a {@code double} value. - * - * @param key The key to access the value for. - * @return {@code 0} if there is no such key or if it is not a {@code double}. - */ - public double getDouble(@NonNull String key) { - Number number = getNumber(key); - if (number == null) { - return 0; - } - return number.doubleValue(); - } - - /** - * Access a {@code long} value. - * - * @param key The key to access the value for. - * @return {@code 0} if there is no such key or if it is not a {@code long}. - */ - public long getLong(@NonNull String key) { - Number number = getNumber(key); - if (number == null) { - return 0; - } - return number.longValue(); - } - - /** - * Access a {@code boolean} value. - * - * @param key The key to access the value for. - * @return {@code false} if there is no such key or if it is not a {@code boolean}. - */ - public boolean getBoolean(@NonNull String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof Boolean)) { - return false; - } - return (Boolean) value; - } - } - - /** - * Access a {@link Date} value. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link Date}. - */ - @Nullable - public Date getDate(@NonNull String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof Date)) { - return null; - } - return (Date) value; - } - } - - /** - * Access a {@code ParseObject} value. This function will not perform a network request. Unless the - * {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling - * {@link #fetchIfNeeded()} or {@link #fetch()}), {@link #isDataAvailable()} will return - * {@code false}. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@code ParseObject}. - */ - @Nullable - public ParseObject getParseObject(@NonNull String key) { - Object value = get(key); - if (!(value instanceof ParseObject)) { - return null; - } - return (ParseObject) value; - } - - /** - * Access a {@link ParseUser} value. This function will not perform a network request. Unless the - * {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling - * {@link #fetchIfNeeded()} or {@link #fetch()}), {@link #isDataAvailable()} will return - * {@code false}. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if the value is not a {@link ParseUser}. - */ - @Nullable - public ParseUser getParseUser(@NonNull String key) { - Object value = get(key); - if (!(value instanceof ParseUser)) { - return null; - } - return (ParseUser) value; - } - - /** - * Access a {@link ParseFile} value. This function will not perform a network request. Unless the - * {@link ParseFile} has been downloaded (e.g. by calling {@link ParseFile#getData()}), - * {@link ParseFile#isDataAvailable()} will return {@code false}. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key or if it is not a {@link ParseFile}. - */ - @Nullable - public ParseFile getParseFile(@NonNull String key) { - Object value = get(key); - if (!(value instanceof ParseFile)) { - return null; - } - return (ParseFile) value; - } - - /** - * Access a {@link ParseGeoPoint} value. - * - * @param key The key to access the value for - * @return {@code null} if there is no such key or if it is not a {@link ParseGeoPoint}. - */ - @Nullable - public ParseGeoPoint getParseGeoPoint(@NonNull String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof ParseGeoPoint)) { - return null; - } - return (ParseGeoPoint) value; - } - } - - /** - * Access a {@link ParsePolygon} value. - * - * @param key The key to access the value for - * @return {@code null} if there is no such key or if it is not a {@link ParsePolygon}. - */ - @Nullable - public ParsePolygon getParsePolygon(@NonNull String key) { - synchronized (mutex) { - checkGetAccess(key); - Object value = estimatedData.get(key); - if (!(value instanceof ParsePolygon)) { - return null; - } - return (ParsePolygon) value; - } - } - - /** - * Access the {@link ParseACL} governing this object. - */ - @Nullable - public ParseACL getACL() { - return getACL(true); - } - - /** - * Set the {@link ParseACL} governing this object. - */ - public void setACL(ParseACL acl) { - put(KEY_ACL, acl); - } - - private ParseACL getACL(boolean mayCopy) { - synchronized (mutex) { - checkGetAccess(KEY_ACL); - Object acl = estimatedData.get(KEY_ACL); - if (acl == null) { - return null; - } - if (!(acl instanceof ParseACL)) { - throw new RuntimeException("only ACLs can be stored in the ACL key"); - } - if (mayCopy && ((ParseACL) acl).isShared()) { - ParseACL copy = new ParseACL((ParseACL) acl); - estimatedData.put(KEY_ACL, copy); - return copy; - } - return (ParseACL) acl; - } - } - - /** - * Gets whether the {@code ParseObject} has been fetched. - * - * @return {@code true} if the {@code ParseObject} is new or has been fetched or refreshed. {@code false} - * otherwise. - */ - public boolean isDataAvailable() { - synchronized (mutex) { - return state.isComplete(); - } - } - - /** - * Gets whether the {@code ParseObject} specified key has been fetched. - * This means the property can be accessed safely. - * - * @return {@code true} if the {@code ParseObject} key is new or has been fetched or refreshed. {@code false} - * otherwise. - */ - public boolean isDataAvailable(@NonNull String key) { - synchronized (mutex) { - // Fallback to estimatedData to include dirty changes. - return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key); - } - } - - /** - * Access or create a {@link ParseRelation} value for a key - * - * @param key The key to access the relation for. - * @return the ParseRelation object if the relation already exists for the key or can be created - * for this key. - */ - @NonNull - public ParseRelation getRelation(@NonNull String key) { - synchronized (mutex) { - // All the sanity checking is done when add or remove is called on the relation. - Object value = estimatedData.get(key); - if (value instanceof ParseRelation) { - @SuppressWarnings("unchecked") - ParseRelation relation = (ParseRelation) value; - relation.ensureParentAndKey(this, key); - return relation; - } else { - ParseRelation relation = new ParseRelation<>(this, key); - /* - * We put the relation into the estimated data so that we'll get the same instance later, - * which may have known objects cached. If we rebuildEstimatedData, then this relation will - * be lost, and we'll get a new one. That's okay, because any cached objects it knows about - * must be replayable from the operations in the queue. If there were any objects in this - * relation that weren't still in the queue, then they would be in the copy of the - * ParseRelation that's in the serverData, so we would have gotten that instance instead. - */ - estimatedData.put(key, relation); - return relation; - } - } - } - - /** - * Access a value. In most cases it is more convenient to use a helper function such as - * {@link #getString(String)} or {@link #getInt(String)}. - * - * @param key The key to access the value for. - * @return {@code null} if there is no such key. - */ - @Nullable - public Object get(@NonNull String key) { - synchronized (mutex) { - if (key.equals(KEY_ACL)) { - return getACL(); - } - - checkGetAccess(key); - Object value = estimatedData.get(key); - - // A relation may be deserialized without a parent or key. - // Either way, make sure it's consistent. - if (value instanceof ParseRelation) { - ((ParseRelation) value).ensureParentAndKey(this, key); - } - - return value; - } - } - - private void checkGetAccess(String key) { - if (!isDataAvailable(key)) { - throw new IllegalStateException( - "ParseObject has no data for '" + key + "'. Call fetchIfNeeded() to get the data."); - } - } - - public boolean hasSameId(ParseObject other) { - synchronized (mutex) { - return this.getClassName() != null && this.getObjectId() != null - && this.getClassName().equals(other.getClassName()) - && this.getObjectId().equals(other.getObjectId()); - } - } - - void registerSaveListener(GetCallback callback) { - synchronized (mutex) { - saveEvent.subscribe(callback); - } - } - - void unregisterSaveListener(GetCallback callback) { - synchronized (mutex) { - saveEvent.unsubscribe(callback); - } - } - - /** - * Called when a non-pointer is being created to allow additional initialization to occur. - */ - void setDefaultValues() { - if (needsDefaultACL() && ParseACL.getDefaultACL() != null) { - this.setACL(ParseACL.getDefaultACL()); - } - } - - /** - * Determines whether this object should get a default ACL. Override in subclasses to turn off - * default ACLs. - */ - boolean needsDefaultACL() { - return true; - } - - /** - * Loads data from the local datastore into this object, if it has not been fetched from the - * server already. If the object is not stored in the local datastore, this method with do - * nothing. - */ - @SuppressWarnings("unchecked") - Task fetchFromLocalDatastoreAsync() { - if (!Parse.isLocalDatastoreEnabled()) { - throw new IllegalStateException("Method requires Local Datastore. " + - "Please refer to `Parse#enableLocalDatastore(Context)`."); - } - return Parse.getLocalDatastore().fetchLocallyAsync((T) this); - } - - /** - * Loads data from the local datastore into this object, if it has not been fetched from the - * server already. If the object is not stored in the local datastore, this method with do - * nothing. - */ - public void fetchFromLocalDatastoreInBackground(GetCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(this.fetchFromLocalDatastoreAsync(), callback); - } - - /** - * Loads data from the local datastore into this object, if it has not been fetched from the - * server already. If the object is not stored in the local datastore, this method with throw a - * CACHE_MISS exception. - * - * @throws ParseException exception if fails - */ - public void fetchFromLocalDatastore() throws ParseException { - ParseTaskUtils.wait(fetchFromLocalDatastoreAsync()); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @param callback the callback - * @see #unpinInBackground(String, DeleteCallback) - */ - public void pinInBackground(String name, SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(name), callback); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @return A {@link Task} that is resolved when pinning completes. - * @see #unpinInBackground(String) - */ - public Task pinInBackground(String name) { - return pinAllInBackground(name, Collections.singletonList(this)); - } - - Task pinInBackground(String name, boolean includeAllChildren) { - return pinAllInBackground(name, Collections.singletonList(this), includeAllChildren); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @throws ParseException exception if fails - * @see #unpin(String) - */ - public void pin(String name) throws ParseException { - ParseTaskUtils.wait(pinInBackground(name)); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @param callback the callback - * @see #unpinInBackground(DeleteCallback) - * @see #DEFAULT_PIN - */ - public void pinInBackground(SaveCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(), callback); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @return A {@link Task} that is resolved when pinning completes. - * @see #unpinInBackground() - * @see #DEFAULT_PIN - */ - public Task pinInBackground() { - return pinAllInBackground(DEFAULT_PIN, Collections.singletonList(this)); - } - - /** - * Stores the object and every object it points to in the local datastore, recursively. If those - * other objects have not been fetched from Parse, they will not be stored. However, if they have - * changed data, all of the changes will be retained. To get the objects back later, you can use - * {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with - * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on - * it. - * - * @throws ParseException exception if fails - * @see #unpin() - * @see #DEFAULT_PIN - */ - public void pin() throws ParseException { - ParseTaskUtils.wait(pinInBackground()); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @param callback the callback - * @see #pinInBackground(String, SaveCallback) - */ - public void unpinInBackground(String name, DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(name), callback); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @return A {@link Task} that is resolved when unpinning completes. - * @see #pinInBackground(String) - */ - public Task unpinInBackground(String name) { - return unpinAllInBackground(name, Collections.singletonList(this)); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @see #pin(String) - */ - public void unpin(String name) throws ParseException { - ParseTaskUtils.wait(unpinInBackground(name)); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @param callback the callback - * @see #pinInBackground(SaveCallback) - * @see #DEFAULT_PIN - */ - public void unpinInBackground(DeleteCallback callback) { - ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(), callback); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @return A {@link Task} that is resolved when unpinning completes. - * @see #pinInBackground() - * @see #DEFAULT_PIN - */ - public Task unpinInBackground() { - return unpinAllInBackground(DEFAULT_PIN, Collections.singletonList(this)); - } - - /** - * Removes the object and every object it points to in the local datastore, recursively. - * - * @see #pin() - * @see #DEFAULT_PIN - */ - public void unpin() throws ParseException { - ParseTaskUtils.wait(unpinInBackground()); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - writeToParcel(dest, new ParseObjectParcelEncoder(this)); - } - - void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { - synchronized (mutex) { - // Developer warnings. - ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled(); - boolean saving = hasOutstandingOperations(); - boolean deleting = isDeleting || isDeletingEventually > 0; - if (saving) { - PLog.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + - "going on. If recovered from LDS, the unparceled object will be internally updated when " + - "these tasks end. If not, it will act as if these tasks have failed. This means that " + - "the subsequent call to save() will update again the same keys, and this is dangerous " + - "for certain operations, like increment(). To avoid inconsistencies, wait for operations " + - "to end before parceling."); - } - if (deleting) { - PLog.w(TAG, "About to parcel a ParseObject while a delete / deleteEventually operation is " + - "going on. If recovered from LDS, the unparceled object will be internally updated when " + - "these tasks end. If not, it will assume it's not deleted, and might incorrectly " + - "return false for isDirty(). To avoid inconsistencies, wait for operations to end " + - "before parceling."); - } - // Write className and id first, regardless of state. - dest.writeString(getClassName()); - String objectId = getObjectId(); - dest.writeByte(objectId != null ? (byte) 1 : 0); - if (objectId != null) dest.writeString(objectId); - // Write state and other members - state.writeToParcel(dest, encoder); - dest.writeByte(localId != null ? (byte) 1 : 0); - if (localId != null) dest.writeString(localId); - dest.writeByte(isDeleted ? (byte) 1 : 0); - // Care about dirty changes and ongoing tasks. - ParseOperationSet set; - if (hasOutstandingOperations()) { - // There's more than one set. Squash the queue, creating copies - // to preserve the original queue when LDS is enabled. - set = new ParseOperationSet(); - for (ParseOperationSet operationSet : operationSetQueue) { - ParseOperationSet copy = new ParseOperationSet(operationSet); - copy.mergeFrom(set); - set = copy; - } - } else { - set = operationSetQueue.getLast(); - } - set.setIsSaveEventually(false); - set.toParcel(dest, encoder); - // Pass a Bundle to subclasses. - Bundle bundle = new Bundle(); - onSaveInstanceState(bundle); - dest.writeBundle(bundle); - } - } - - /** - * Called when parceling this ParseObject. - * Subclasses can put values into the provided {@link Bundle} and receive them later - * {@link #onRestoreInstanceState(Bundle)}. Note that internal fields are already parceled by - * the framework. - * - * @param outState Bundle to host extra values - */ - protected void onSaveInstanceState(Bundle outState) { - } - - /** - * Called when unparceling this ParseObject. - * Subclasses can read values from the provided {@link Bundle} that were previously put - * during {@link #onSaveInstanceState(Bundle)}. At this point the internal state is already - * recovered. - * - * @param savedState Bundle to read the values from - */ - protected void onRestoreInstanceState(Bundle savedState) { - } - - static class State { - - private final String className; - private final String objectId; - private final long createdAt; - private final long updatedAt; - private final Map serverData; - private final Set availableKeys; - private final boolean isComplete; - - State(Init builder) { - className = builder.className; - objectId = builder.objectId; - createdAt = builder.createdAt; - updatedAt = builder.updatedAt > 0 - ? builder.updatedAt - : createdAt; - serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData)); - isComplete = builder.isComplete; - availableKeys = Collections.synchronizedSet(builder.availableKeys); - } - - State(Parcel parcel, String clazz, ParseParcelDecoder decoder) { - className = clazz; // Already read - objectId = parcel.readByte() == 1 ? parcel.readString() : null; - createdAt = parcel.readLong(); - long updated = parcel.readLong(); - updatedAt = updated > 0 ? updated : createdAt; - int size = parcel.readInt(); - HashMap map = new HashMap<>(); - for (int i = 0; i < size; i++) { - String key = parcel.readString(); - Object obj = decoder.decode(parcel); - map.put(key, obj); - } - serverData = Collections.unmodifiableMap(map); - isComplete = parcel.readByte() == 1; - List available = new ArrayList<>(); - parcel.readStringList(available); - availableKeys = new HashSet<>(available); - } - - public static Init newBuilder(String className) { - if ("_User".equals(className)) { - return new ParseUser.State.Builder(); - } - return new Builder(className); - } - - - static State createFromParcel(Parcel source, ParseParcelDecoder decoder) { - String className = source.readString(); - if ("_User".equals(className)) { - return new ParseUser.State(source, className, decoder); - } - return new State(source, className, decoder); - } - - @SuppressWarnings("unchecked") - public > T newBuilder() { - return (T) new Builder(this); - } - - public String className() { - return className; - } - - public String objectId() { - return objectId; - } - - public long createdAt() { - return createdAt; - } - - public long updatedAt() { - return updatedAt; - } - - public boolean isComplete() { - return isComplete; - } - - public Object get(String key) { - return serverData.get(key); - } - - public Set keySet() { - return serverData.keySet(); - } - - // Available keys for this object. With respect to keySet(), this includes also keys that are - // undefined in the server, but that should be accessed without throwing. - // These extra keys come e.g. from ParseQuery.selectKeys(). Selected keys must be available to - // get() methods even if undefined, for consistency with complete objects. - // For a complete object, this set is equal to keySet(). - public Set availableKeys() { - return availableKeys; - } - - protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { - dest.writeString(className); - dest.writeByte(objectId != null ? (byte) 1 : 0); - if (objectId != null) { - dest.writeString(objectId); - } - dest.writeLong(createdAt); - dest.writeLong(updatedAt); - dest.writeInt(serverData.size()); - Set keys = serverData.keySet(); - for (String key : keys) { - dest.writeString(key); - encoder.encode(serverData.get(key), dest); - } - dest.writeByte(isComplete ? (byte) 1 : 0); - dest.writeStringList(new ArrayList<>(availableKeys)); - } - - @Override - public String toString() { - return String.format(Locale.US, "%s@%s[" + - "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + - "serverData=%s, availableKeys=%s]", - getClass().getName(), - Integer.toHexString(hashCode()), - className, - objectId, - createdAt, - updatedAt, - isComplete, - serverData, - availableKeys); - } - - static abstract class Init { - - private final String className; - Map serverData = new HashMap<>(); - private String objectId; - private long createdAt = -1; - private long updatedAt = -1; - private boolean isComplete; - private Set availableKeys = new HashSet<>(); - - public Init(String className) { - this.className = className; - } - - Init(State state) { - className = state.className(); - objectId = state.objectId(); - createdAt = state.createdAt(); - updatedAt = state.updatedAt(); - availableKeys = Collections.synchronizedSet(new HashSet<>(state.availableKeys())); - for (String key : state.keySet()) { - serverData.put(key, state.get(key)); - availableKeys.add(key); - } - isComplete = state.isComplete(); - } - - - abstract T self(); - - - abstract S build(); - - public T objectId(String objectId) { - this.objectId = objectId; - return self(); - } - - public T createdAt(Date createdAt) { - this.createdAt = createdAt.getTime(); - return self(); - } - - public T createdAt(long createdAt) { - this.createdAt = createdAt; - return self(); - } - - public T updatedAt(Date updatedAt) { - this.updatedAt = updatedAt.getTime(); - return self(); - } - - public T updatedAt(long updatedAt) { - this.updatedAt = updatedAt; - return self(); - } - - public T isComplete(boolean complete) { - isComplete = complete; - return self(); - } - - public T put(String key, Object value) { - serverData.put(key, value); - availableKeys.add(key); - return self(); - } - - public T remove(String key) { - serverData.remove(key); - return self(); - } - - public T availableKeys(Collection keys) { - availableKeys.addAll(keys); - return self(); - } - - public T clear() { - objectId = null; - createdAt = -1; - updatedAt = -1; - isComplete = false; - serverData.clear(); - availableKeys.clear(); - return self(); - } - - /** - * Applies a {@code State} on top of this {@code Builder} instance. - * - * @param other The {@code State} to apply over this instance. - * @return A new {@code Builder} instance. - */ - public T apply(State other) { - if (other.objectId() != null) { - objectId(other.objectId()); - } - if (other.createdAt() > 0) { - createdAt(other.createdAt()); - } - if (other.updatedAt() > 0) { - updatedAt(other.updatedAt()); - } - isComplete(isComplete || other.isComplete()); - for (String key : other.keySet()) { - put(key, other.get(key)); - } - availableKeys(other.availableKeys()); - return self(); - } - - public T apply(ParseOperationSet operations) { - for (String key : operations.keySet()) { - ParseFieldOperation operation = operations.get(key); - Object oldValue = serverData.get(key); - Object newValue = operation.apply(oldValue, key); - if (newValue != null) { - put(key, newValue); - } else { - remove(key); - } - } - return self(); - } - } - - static class Builder extends Init { - - public Builder(String className) { - super(className); - } - - public Builder(State state) { - super(state); - } - - @Override - Builder self() { - return this; - } - - public State build() { - return new State(this); - } - } - } - -} - -// [1] Normally we should only construct the command from state when it's our turn in the -// taskQueue so that new objects will have an updated objectId from previous saves. -// We can't do this for save/deleteEventually since this will break the promise that we'll -// try to run the command eventually, since our process might die before it's our turn in -// the taskQueue. -// This seems like this will only be a problem for new objects that are saved & -// save/deleteEventually'd at the same time, as the first will create 2 objects and the second -// the delete might fail. diff --git a/parse/src/main/java/com/parse/ParseObject.kt b/parse/src/main/java/com/parse/ParseObject.kt new file mode 100644 index 000000000..db75eff67 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseObject.kt @@ -0,0 +1,3853 @@ +/* +* 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 android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import com.parse.PLog.w +import com.parse.ParseEncoder.Companion.isValidType +import com.parse.TaskQueue.Companion.waitFor +import com.parse.boltsinternal.Capture +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.Lock +import kotlin.collections.Collection +import kotlin.collections.Iterator +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.MutableCollection +import kotlin.collections.MutableList +import kotlin.collections.MutableMap +import kotlin.collections.MutableSet +import kotlin.collections.Set +import kotlin.collections.listOf +import kotlin.collections.set +import kotlin.collections.toTypedArray + +/** + * The `ParseObject` is a local representation of data that can be saved and retrieved from + * the Parse cloud. + * + * + * The basic workflow for creating new data is to construct a new `ParseObject`, use + * [.put] to fill it with data, and then use [.saveInBackground] to + * persist to the cloud. + * + * + * The basic workflow for accessing existing data is to use a [ParseQuery] to specify which + * existing data to retrieve. + */ +open class ParseObject(theClassName: String) : Parcelable { + val mutex = Any() + private val taskQueue = TaskQueue() + private val operationSetQueue: LinkedList + + // Cached State + private val estimatedData: MutableMap + private val saveEvent = ParseMulticastDelegate() + @JvmField + var localId: String? = null + @JvmField + var isDeleted = false + @JvmField + var isDeleting // Since delete ops are queued, we don't need a counter. + = false + + //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. + var isDeletingEventually = 0 + internal var state: State = State.newBuilder(theClassName).build() + get() {synchronized(mutex) { return field }} + set(value) {synchronized(mutex) { setState(value, true) }} + private var ldsEnabledWhenParceling = false + + /** + * The base class constructor to call in subclasses. Uses the class name specified with the + * [ParseClassName] annotation on the subclass. + */ + protected constructor() : this(AUTO_CLASS_NAME) + + internal open fun newStateBuilder(className: String): State.Init<*> { + return State.Builder(className) + } + + /** + * Updates the current state of this object as well as updates our in memory cached state. + * + * @param newState The new state. + */ + private fun setState(newState: State, notifyIfObjectIdChanges: Boolean) { + synchronized(mutex) { + val oldObjectId = state.objectId() + val newObjectId = newState.objectId() + state = newState + if (notifyIfObjectIdChanges && !ParseTextUtils.equals(oldObjectId, newObjectId)) { + notifyObjectIdChanged(oldObjectId, newObjectId) + } + rebuildEstimatedData() + } + } + + /** + * Accessor to the class name. + */ + val className: String? + get() { + synchronized(mutex) { return state.className() } + } + + /** + * This reports time as the server sees it, so that if you make changes to a `ParseObject`, then + * wait a while, and then call [.save], the updated time will be the time of the + * [.save] call rather than the time the object was changed locally. + * + * @return The last time this object was updated on the server. + */ + val updatedAt: Date? + get() { + val updatedAt = state.updatedAt() + return if (updatedAt > 0) Date(updatedAt) else null + } + + /** + * This reports time as the server sees it, so that if you create a `ParseObject`, then wait a + * while, and then call [.save], the creation time will be the time of the first + * [.save] call rather than the time the object was created locally. + * + * @return The first time this object was saved on the server. + */ + val createdAt: Date? + get() { + val createdAt = state.createdAt() + return if (createdAt > 0) Date(createdAt) else null + } + + /** + * Returns a set view of the keys contained in this object. This does not include createdAt, + * updatedAt, authData, or objectId. It does include things like username and ACL. + */ + fun keySet(): Set { + synchronized(mutex) { return Collections.unmodifiableSet(estimatedData.keys) } + } + + /** + * Copies all of the operations that have been performed on another object since its last save + * onto this one. + */ + fun copyChangesFrom(other: ParseObject) { + synchronized(mutex) { + val operations = other.operationSetQueue.first + for (key in operations.keys) { + performOperation(key, operations[key]) + } + } + } + + fun mergeFromObject(other: ParseObject) { + synchronized(mutex) { + + // If they point to the same instance, we don't need to merge. + if (this === other) { + return + } + val copy = other.state.newBuilder>().build() + + // We don't want to notify if an objectId changed here since we utilize this method to merge + // an anonymous current user with a new ParseUser instance that's calling signUp(). This + // doesn't make any sense and we should probably remove that code in ParseUser. + // Otherwise, there shouldn't be any objectId changes here since this method is only otherwise + // used in fetchAll. + setState(copy, false) + } + } + + /** + * Clears changes to this object's `key` made since the last call to [.save] or + * [.saveInBackground]. + * + * @param key The `key` to revert changes for. + */ + fun revert(key: String) { + synchronized(mutex) { + if (isDirty(key)) { + currentOperations().remove(key) + rebuildEstimatedData() + } + } + } + + /** + * Clears any changes to this object made since the last call to [.save] or + * [.saveInBackground]. + */ + fun revert() { + synchronized(mutex) { + if (isDirty) { + currentOperations().clear() + rebuildEstimatedData() + } + } + } + + /** + * Deep traversal on this object to grab a copy of any object referenced by this object. These + * instances may have already been fetched, and we don't want to lose their data when refreshing + * or saving. + * + * @return the map mapping from objectId to `ParseObject` which has been fetched. + */ + private fun collectFetchedObjects(): Map { + val fetchedObjects: MutableMap = HashMap() + val traverser: ParseTraverser = object : ParseTraverser() { + override fun visit(`object`: Any): Boolean { + if (`object` is ParseObject) { + val state = `object`.state + if (state.objectId() != null && state.isComplete) { + fetchedObjects[state.objectId()] = `object` + } + } + return true + } + } + traverser.traverse(estimatedData) + return fetchedObjects + } + + /** + * Helper method called by [.fromJSONPayload] + * + * + * The method helps webhooks implementation to build Parse object from raw JSON payload. + * It is different from [.mergeFromServer] + * as the method saves the key value pairs (other than className, objectId, updatedAt and + * createdAt) in the operation queue rather than the server data. It also handles + * [ParseFieldOperations] differently. + * + * @param json : JSON object to be converted to Parse object + * @param decoder : Decoder to be used for Decoding JSON + */ + fun build(json: JSONObject, decoder: ParseDecoder) { + try { + val builder = State.Builder(state) + .isComplete(true) + builder.clear() + val keys: Iterator<*> = json.keys() + while (keys.hasNext()) { + val key = keys.next() as String + /* + __className: Used by fromJSONPayload, should be stripped out by the time it gets here... + */if (key == KEY_CLASS_NAME) { + continue + } + if (key == KEY_OBJECT_ID) { + val newObjectId = json.getString(key) + builder.objectId(newObjectId) + continue + } + if (key == KEY_CREATED_AT) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + if (key == KEY_UPDATED_AT) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + val value = json[key] + val decodedObject = decoder.decode(value) + if (decodedObject is ParseFieldOperation) { + performOperation(key, decodedObject as ParseFieldOperation?) + } else { + put(key, decodedObject!!) + } + } + state = builder.build() + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + /** + * Merges from JSON in REST format. + * Updates this object with data from the server. + * + * @see .toJSONObjectForSaving + */ + internal fun mergeFromServer( + state: State?, json: JSONObject, decoder: ParseDecoder, completeData: Boolean + ): State { + return try { + // If server data is complete, consider this object to be fetched. + val builder = state!!.newBuilder>() + if (completeData) { + builder.clear() + } + builder.isComplete(state.isComplete || completeData) + val keys: Iterator<*> = json.keys() + while (keys.hasNext()) { + val key = keys.next() as String + /* + __type: Returned by queries and cloud functions to designate body is a ParseObject + __className: Used by fromJSON, should be stripped out by the time it gets here... + */ + if (key == "__type" || key == KEY_CLASS_NAME) { + continue + } + if (key == KEY_OBJECT_ID) { + val newObjectId = json.getString(key) + builder.objectId(newObjectId) + continue + } + if (key == KEY_CREATED_AT) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + if (key == KEY_UPDATED_AT) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + if (key == KEY_ACL) { + val acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder) + builder.put(KEY_ACL, acl) + continue + } + if (key == KEY_SELECTED_KEYS) { + val safeKeys = json.getJSONArray(key) + if (safeKeys.length() > 0) { + val set: MutableCollection = HashSet() + for (i in 0 until safeKeys.length()) { + // Don't add nested keys. + var safeKey = safeKeys.getString(i) + if (safeKey.contains(".")) safeKey = + safeKey.split("\\.").toTypedArray()[0] + set.add(safeKey) + } + builder.availableKeys(set.toSet()) + } + continue + } + val value = json[key] + if (value is JSONObject && json.has(KEY_SELECTED_KEYS)) { + // This might be a ParseObject. Pass selected keys to understand if it is complete. + val selectedKeys = json.getJSONArray(KEY_SELECTED_KEYS) + val nestedKeys = JSONArray() + for (i in 0 until selectedKeys.length()) { + val nestedKey = selectedKeys.getString(i) + if (nestedKey.startsWith("$key.")) nestedKeys.put(nestedKey.substring(key.length + 1)) + } + if (nestedKeys.length() > 0) { + value.put(KEY_SELECTED_KEYS, nestedKeys) + } + } + val decodedObject = decoder.decode(value) + builder.put(key, decodedObject) + } + builder.build() + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + /** + * Convert to REST JSON for persisting in LDS. + * + * @see .mergeREST + */ + fun toRest(encoder: ParseEncoder): JSONObject { + var state: State? + var operationSetQueueCopy: MutableList + synchronized(mutex) { + + // mutex needed to lock access to state and operationSetQueue and operationSetQueue & children + // are mutable + state = this.state + + // operationSetQueue is a List of Lists, so we'll need custom copying logic + val operationSetQueueSize = operationSetQueue.size + operationSetQueueCopy = ArrayList(operationSetQueueSize) + for (i in 0 until operationSetQueueSize) { + val original = operationSetQueue[i] + val copy = ParseOperationSet(original) + operationSetQueueCopy.add(copy) + } + } + return toRest(state, operationSetQueueCopy, encoder) + } + + internal fun toRest( + state: State?, operationSetQueue: List, objectEncoder: ParseEncoder + ): JSONObject { + // Public data goes in dataJSON; special fields go in objectJSON. + val json = JSONObject() + try { + // REST JSON (State) + json.put(KEY_CLASS_NAME, state!!.className()) + if (state.objectId() != null) { + json.put(KEY_OBJECT_ID, state.objectId()) + } + if (state.createdAt() > 0) { + json.put( + KEY_CREATED_AT, + ParseDateFormat.getInstance().format(Date(state.createdAt())) + ) + } + if (state.updatedAt() > 0) { + json.put( + KEY_UPDATED_AT, + ParseDateFormat.getInstance().format(Date(state.updatedAt())) + ) + } + for (key in state.keySet()) { + val value = state[key] + json.put(key, objectEncoder.encode(value)) + } + + // Internal JSON + //TODO(klimt): We'll need to rip all this stuff out and put it somewhere else if we start + // using the REST api and want to send data to Parse. + json.put(KEY_COMPLETE, state.isComplete) + json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually) + val availableKeys = JSONArray(state.availableKeys()) + json.put(KEY_SELECTED_KEYS, availableKeys) + + // Operation Set Queue + val operations = JSONArray() + for (operationSet in operationSetQueue) { + operations.put(operationSet.toRest(objectEncoder)) + } + json.put(KEY_OPERATIONS, operations) + } catch (e: JSONException) { + throw RuntimeException("could not serialize object to JSON") + } + return json + } + + /** + * Merge with REST JSON from LDS. + * + * @see .toRest + */ + internal fun mergeREST(state: State, json: JSONObject, decoder: ParseDecoder) { + val saveEventuallyOperationSets = ArrayList() + synchronized(mutex) { + try { + val isComplete = json.getBoolean(KEY_COMPLETE) + isDeletingEventually = ParseJSONUtils.getInt( + json, listOf( + KEY_IS_DELETING_EVENTUALLY, + KEY_IS_DELETING_EVENTUALLY_OLD + ) + ) + val operations = json.getJSONArray(KEY_OPERATIONS) + run { + val newerOperations = currentOperations() + operationSetQueue.clear() + + // Add and enqueue any saveEventually operations, roll forward any other operation sets + // (operation sets here are generally failed/incomplete saves). + var current: ParseOperationSet? = null + for (i in 0 until operations.length()) { + val operationSetJSON = operations.getJSONObject(i) + val operationSet = ParseOperationSet.fromRest(operationSetJSON, decoder) + if (operationSet.isSaveEventually) { + if (current != null) { + operationSetQueue.add(current) + current = null + } + saveEventuallyOperationSets.add(operationSet) + operationSetQueue.add(operationSet) + continue + } + if (current != null) { + operationSet.mergeFrom(current) + } + current = operationSet + } + if (current != null) { + operationSetQueue.add(current) + } + + // Merge the changes that were previously in memory into the updated object. + currentOperations().mergeFrom(newerOperations) + } + + // We only want to merge server data if we our updatedAt is null (we're unsaved or from + // #createWithoutData) or if the JSON's updatedAt is newer than ours. + var mergeServerData = false + if (state.updatedAt() < 0) { + mergeServerData = true + } else if (json.has(KEY_UPDATED_AT)) { + val otherUpdatedAt = ParseDateFormat.getInstance().parse( + json.getString( + KEY_UPDATED_AT + ) + ) + if (Date(state.updatedAt()) < otherUpdatedAt) { + mergeServerData = true + } + } + if (mergeServerData) { + // Clean up internal json keys + val mergeJSON = ParseJSONUtils.create( + json, listOf( + KEY_COMPLETE, + KEY_IS_DELETING_EVENTUALLY, + KEY_IS_DELETING_EVENTUALLY_OLD, + KEY_OPERATIONS + ) + ) + val newState = mergeFromServer(state, mergeJSON, decoder, isComplete) + this.state = newState + } + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + // We cannot modify the taskQueue inside synchronized (mutex). + for (operationSet in saveEventuallyOperationSets) { + enqueueSaveEventuallyOperationAsync(operationSet) + } + } + + private fun hasDirtyChildren(): Boolean { + synchronized(mutex) { + + // We only need to consider the currently estimated children here, + // because they're the only ones that might need to be saved in a + // subsequent call to save, which is the meaning of "dirtiness". + val unsavedChildren: MutableList = ArrayList() + collectDirtyChildren(estimatedData, unsavedChildren, null) + return unsavedChildren.size > 0 + } + } + + /** + * Whether any key-value pair in this object (or its children) has been added/updated/removed and + * not saved yet. + * + * @return Whether this object has been altered and not saved yet. + */ + val isDirty: Boolean + get() = this.isDirty(true) + + fun isDirty(considerChildren: Boolean): Boolean { + synchronized(mutex) { return isDeleted || objectId == null || hasChanges() || considerChildren && hasDirtyChildren() } + } + + fun hasChanges(): Boolean { + synchronized(mutex) { return currentOperations().size > 0 } + } + + /** + * Returns `true` if this `ParseObject` has operations in operationSetQueue that + * haven't been completed yet, `false` if there are no operations in the operationSetQueue. + */ + fun hasOutstandingOperations(): Boolean { + synchronized(mutex) { + // > 1 since 1 is for unsaved changes. + return operationSetQueue.size > 1 + } + } + + /** + * Whether a value associated with a key has been added/updated/removed and not saved yet. + * + * @param key The key to check for + * @return Whether this key has been altered and not saved yet. + */ + fun isDirty(key: String): Boolean { + synchronized(mutex) { return currentOperations().containsKey(key) } + } + /** + * Accessor to the object id. An object id is assigned as soon as an object is saved to the + * server. The combination of a className and an objectId uniquely identifies an object in your + * application. + * + * @return The object id. + */// We don't need to use setState since it doesn't affect our cached state. + /** + * Setter for the object id. In general you do not need to use this. However, in some cases this + * can be convenient. For example, if you are serializing a `ParseObject` yourself and wish + * to recreate it, you can use this to recreate the `ParseObject` exactly. + */ + val objectId: String? + get() { + synchronized(mutex) { return state.objectId() } + } + + /** + * Returns the localId, which is used internally for serializing relations to objects that don't + * yet have an objectId. + */ + fun getOrCreateLocalId(): String { + synchronized(mutex) { + if (localId == null) { + check(state.objectId() == null) { "Attempted to get a localId for an object with an objectId." } + localId = localIdManager.createLocalId() + } + return localId!! + } + } + + // Sets the objectId without marking dirty. + private fun notifyObjectIdChanged(oldObjectId: String?, newObjectId: String?) { + synchronized(mutex) { + + // The offline store may throw if this object already had a different objectId. + val store = Parse.getLocalDatastore() + store?.updateObjectId(this, oldObjectId, newObjectId) + if (localId != null) { + localIdManager.setObjectId(localId!!, newObjectId) + localId = null + } + } + } + + private fun currentSaveEventuallyCommand( + operations: ParseOperationSet, objectEncoder: ParseEncoder, sessionToken: String + ): ParseRESTObjectCommand { + val state = this.state + + /* + * Get the JSON representation of the object, and use some of the information to construct the + * command. + */ + val objectJSON = toJSONObjectForSaving(state, operations, objectEncoder) + return ParseRESTObjectCommand.saveObjectCommand( + state, + objectJSON, + sessionToken + ) + } + + /** + * Converts a `ParseObject` to a JSON representation for saving to Parse. + * + * + *

+     * {
+     * data: { // objectId plus any ParseFieldOperations },
+     * classname: class name for the object
+     * }
+    
* + * + * + * updatedAt and createdAt are not included. only dirty keys are represented in the data. + * + * @see .mergeFromServer + */ + // Currently only used by saveEventually + private fun toJSONObjectForSaving( + state: T, operations: ParseOperationSet, objectEncoder: ParseEncoder + ): JSONObject { + val objectJSON = JSONObject() + try { + // Serialize the data + for (key in operations.keys) { + val operation = operations[key] + objectJSON.put(key, objectEncoder.encode(operation)) + + // TODO(grantland): Use cached value from hashedObjects if it's a set operation. + } + if (state!!.objectId() != null) { + objectJSON.put(KEY_OBJECT_ID, state.objectId()) + } + } catch (e: JSONException) { + throw RuntimeException("could not serialize object to JSON") + } + return objectJSON + } + + /** + * Handles the result of `save`. + * + * + * Should be called on success or failure. + */ + // TODO(grantland): Remove once we convert saveEventually and ParseUser.signUp/resolveLaziness + // to controllers + internal fun handleSaveResultAsync( + result: JSONObject?, operationsBeforeSave: ParseOperationSet + ): Task { + var newState: State? = null + if (result != null) { // Success + synchronized(mutex) { + val fetchedObjects = collectFetchedObjects() + val decoder: ParseDecoder = KnownParseObjectDecoder(fetchedObjects) + newState = ParseObjectCoder.get() + .decode(state.newBuilder>().clear(), result, decoder) + .isComplete(false) + .build() + } + } + return handleSaveResultAsync(newState, operationsBeforeSave) + } + + /** + * Handles the result of `save`. + * + * + * Should be called on success or failure. + */ + internal open fun handleSaveResultAsync( + result: State?, operationsBeforeSave: ParseOperationSet + ): Task { + var task = Task.forResult(null) + + /* + * If this object is in the offline store, then we need to make sure that we pull in any dirty + * changes it may have before merging the server data into it. + */ + val store = Parse.getLocalDatastore() + if (store != null) { + task = task.onSuccessTask { + store.fetchLocallyAsync(this@ParseObject).makeVoid() + } + } + val success = result != null + synchronized(mutex) { + + // Find operationsBeforeSave in the queue so that we can remove it and move to the next + // operation set. + val opIterator = + operationSetQueue.listIterator(operationSetQueue.indexOf(operationsBeforeSave)) + opIterator.next() + opIterator.remove() + if (!success) { + // Merge the data from the failed save into the next save. + val nextOperation = opIterator.next() + nextOperation.mergeFrom(operationsBeforeSave) + if (store != null) { + task = task.continueWithTask { task14: Task -> + if (task14.isFaulted) { + return@continueWithTask Task.forResult(null) + } else { + return@continueWithTask store.updateDataForObjectAsync(this@ParseObject) + } + } + } + return task + } + } + + // fetchLocallyAsync will return an error if this object isn't in the LDS yet and that's ok + task = task.continueWith { + synchronized(mutex) { + val newState: State = if (result!!.isComplete) { + // Result is complete, so just replace + result + } else { + // Result is incomplete, so we'll need to apply it to the current state + state.newBuilder>() + .apply(operationsBeforeSave) + .apply(result) + .build() + } + state = newState + } + null + } + if (store != null) { + task = + task.onSuccessTask { store.updateDataForObjectAsync(this@ParseObject) } + } + task = task.onSuccess { + saveEvent.invoke(this@ParseObject, null) + null + } + return task + } + + internal fun startSave(): ParseOperationSet { + synchronized(mutex) { + val currentOperations = currentOperations() + operationSetQueue.addLast(ParseOperationSet()) + return currentOperations + } + } + + open fun validateSave() { + // do nothing + } + + /** + * Saves this object to the server. Typically, you should use [.saveInBackground] instead of + * this, unless you are managing your own threading. + * + * @throws ParseException Throws an exception if the server is inaccessible. + */ + @Throws(ParseException::class) + fun save() { + ParseTaskUtils.wait(saveInBackground()) + } + + /** + * Saves this object to the server in a background thread. This is preferable to using [.save], + * unless your code is already running from a background thread. + * + * @return A [Task] that is resolved when the save completes. + */ + fun saveInBackground(): Task { + return ParseUser.getCurrentUserAsync().onSuccessTask { task: Task -> + val current = task.result + ?: return@onSuccessTask Task.forResult(null) + if (!current.isLazy) { + return@onSuccessTask Task.forResult(current.sessionToken) + } + + // The current user is lazy/unresolved. If it is attached to us via ACL, we'll need to + // resolve/save it before proceeding. + if (!isDataAvailable(KEY_ACL)) { + return@onSuccessTask Task.forResult(null) + } + val acl = getACL(false) + ?: return@onSuccessTask Task.forResult(null) + val user = acl.unresolvedUser + if (user == null || !user.isCurrentUser) { + return@onSuccessTask Task.forResult(null) + } + user.saveAsync(null).onSuccess { + check(!acl.hasUnresolvedUser()) { + ("ACL has an unresolved ParseUser. " + + "Save or sign up before attempting to serialize the ACL.") + } + user.sessionToken + } + }.onSuccessTask { task: Task -> + val sessionToken = task.result + saveAsync(sessionToken) + } + } + + fun saveAsync(sessionToken: String?): Task { + return taskQueue.enqueue { toAwait: Task? -> + saveAsync( + sessionToken, + toAwait + ) + } + } + + open fun saveAsync(sessionToken: String?, toAwait: Task?): Task? { + if (!isDirty) { + return Task.forResult(null) + } + val operations: ParseOperationSet + synchronized(mutex) { + updateBeforeSave() + validateSave() + operations = startSave() + } + var task: Task + synchronized(mutex) { + // Recursively save children + + /* + * TODO(klimt): Why is this estimatedData and not... I mean, what if a child is + * removed after save is called, but before the unresolved user gets resolved? It + * won't get saved. + */task = deepSaveAsync(estimatedData, sessionToken) + } + return task.onSuccessTask( + waitFor(toAwait!!) + ).onSuccessTask { + val fetchedObjects = collectFetchedObjects() + val decoder: ParseDecoder = KnownParseObjectDecoder(fetchedObjects) + objectController.saveAsync(state, operations, sessionToken, decoder) + }.continueWithTask { saveTask: Task -> + val result = saveTask.result + handleSaveResultAsync(result, operations).continueWithTask { task1: Task -> + if (task1.isFaulted || task1.isCancelled) { + return@continueWithTask task1 + } + saveTask.makeVoid() + } + } + } + + // Currently only used by ParsePinningEventuallyQueue for saveEventually due to the limitation in + // ParseCommandCache that it can only return JSONObject result. + internal fun saveAsync( + client: ParseHttpClient?, + operationSet: ParseOperationSet, + sessionToken: String + ): Task { + val command: ParseRESTCommand = + currentSaveEventuallyCommand(operationSet, PointerEncoder.get(), sessionToken) + return command.executeAsync(client!!) + } + + /** + * Saves this object to the server in a background thread. This is preferable to using [.save], + * unless your code is already running from a background thread. + * + * @param callback `callback.done(e)` is called when the save completes. + */ + fun saveInBackground(callback: SaveCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback) + } + + @Throws(ParseException::class) + open fun validateSaveEventually() { + // do nothing + } + + /** + * Saves this object to the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the save completes. If there is some problem with the object such that it + * can't be saved, it will be silently discarded. Objects saved with this method will be stored + * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately + * if possible. Otherwise, they will be sent the next time a network connection is available. + * Objects saved this way will persist even after the app is closed, in which case they will be + * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, + * subsequent calls to `#saveEventually()` or [.deleteEventually] will cause old + * saves to be silently discarded until the connection can be re-established, and the queued + * objects can be saved. + * + * @param callback - A callback which will be called if the save completes before the app exits. + */ + fun saveEventually(callback: SaveCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(saveEventually(), callback) + } + + /** + * Saves this object to the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the save completes. If there is some problem with the object such that it + * can't be saved, it will be silently discarded. Objects saved with this method will be stored + * locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately + * if possible. Otherwise, they will be sent the next time a network connection is available. + * Objects saved this way will persist even after the app is closed, in which case they will be + * sent the next time the app is opened. If more than 10MB of data is waiting to be sent, + * subsequent calls to `#saveEventually()` or [.deleteEventually] will cause old + * saves to be silently discarded until the connection can be re-established, and the queued + * objects can be saved. + * + * @return A [Task] that is resolved when the save completes. + */ + fun saveEventually(): Task { + if (!isDirty) { + Parse.getEventuallyQueue().fakeObjectUpdate() + return Task.forResult(null) + } + val operationSet: ParseOperationSet + val command: ParseRESTCommand + val runEventuallyTask: Task + synchronized(mutex) { + updateBeforeSave() + try { + validateSaveEventually() + } catch (e: ParseException) { + return Task.forError(e) + } + + // TODO(klimt): Once we allow multiple saves on an object, this + // should be collecting dirty children from the estimate based on + // whatever data is going to be sent by this saveEventually, which + // won't necessarily be the current estimatedData. We should resolve + // this when the multiple save code is added. + val unsavedChildren: MutableList = ArrayList() + collectDirtyChildren(estimatedData, unsavedChildren, null) + var localId: String? = null + if (objectId == null) { + localId = getOrCreateLocalId() + } + operationSet = startSave() + operationSet.isSaveEventually = true + + //TODO (grantland): Convert to async + val sessionToken = ParseUser.getCurrentSessionToken() + + // See [1] + command = currentSaveEventuallyCommand( + operationSet, PointerOrLocalIdEncoder.get(), + sessionToken + ) + + // TODO: Make this logic make sense once we have deepSaveEventually + command.localId = localId + + // Mark the command with a UUID so that we can match it up later. + command.operationSetUUID = operationSet.uuid + + // Ensure local ids are retained before saveEventually-ing children + command.retainLocalIds() + for (`object` in unsavedChildren) { + `object`.saveEventually() + } + } + + // We cannot modify the taskQueue inside synchronized (mutex). + val cache = Parse.getEventuallyQueue() + runEventuallyTask = cache.enqueueEventuallyAsync(command, this@ParseObject) + enqueueSaveEventuallyOperationAsync(operationSet) + + // Release the extra retained local ids. + command.releaseLocalIds() + val handleSaveResultTask: Task + handleSaveResultTask = if (Parse.isLocalDatastoreEnabled()) { + // ParsePinningEventuallyQueue calls handleSaveEventuallyResultAsync directly. + runEventuallyTask.makeVoid() + } else { + runEventuallyTask.onSuccessTask { task: Task -> + val json = task.result + handleSaveEventuallyResultAsync(json, operationSet) + } + } + return handleSaveResultTask + } + + /** + * Enqueues the saveEventually ParseOperationSet in [.taskQueue]. + */ + private fun enqueueSaveEventuallyOperationAsync(operationSet: ParseOperationSet) { + check(operationSet.isSaveEventually) { "This should only be used to enqueue saveEventually operation sets" } + taskQueue.enqueue { toAwait: Task -> + toAwait.continueWithTask { + val cache = Parse.getEventuallyQueue() + cache.waitForOperationSetAndEventuallyPin(operationSet, null)?.makeVoid() + } + } + } + + /** + * Handles the result of `saveEventually`. + * + * + * In addition to normal save handling, this also notifies the saveEventually test helper. + * + * + * Should be called on success or failure. + */ + private fun handleSaveEventuallyResultAsync( + json: JSONObject?, operationSet: ParseOperationSet + ): Task { + val success = json != null + val handleSaveResultTask = handleSaveResultAsync(json, operationSet) + return handleSaveResultTask.onSuccessTask { task: Task? -> + if (success) { + Parse.getEventuallyQueue() + .notifyTestHelper(ParseEventuallyQueue.TestHelper.OBJECT_UPDATED) + } + task + } + } + + /** + * Called by [.saveInBackground] and [.saveEventually] + * and guaranteed to be thread-safe. Subclasses can override this method to do any custom updates + * before an object gets saved. + */ + internal open fun updateBeforeSave() { + // do nothing + } + + /** + * Deletes this object from the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the delete completes. If there is some problem with the object such that it + * can't be deleted, the request will be silently discarded. Delete requests made with this method + * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be + * sent immediately if possible. Otherwise, they will be sent the next time a network connection + * is available. Delete instructions saved this way will persist even after the app is closed, in + * which case they will be sent the next time the app is opened. If more than 10MB of commands are + * waiting to be sent, subsequent calls to `#deleteEventually()` or + * [.saveEventually] will cause old instructions to be silently discarded until the + * connection can be re-established, and the queued objects can be saved. + * + * @param callback - A callback which will be called if the delete completes before the app exits. + */ + fun deleteEventually(callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteEventually(), callback) + } + + /** + * Deletes this object from the server at some unspecified time in the future, even if Parse is + * currently inaccessible. Use this when you may not have a solid network connection, and don't + * need to know when the delete completes. If there is some problem with the object such that it + * can't be deleted, the request will be silently discarded. Delete requests made with this method + * will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be + * sent immediately if possible. Otherwise, they will be sent the next time a network connection + * is available. Delete instructions saved this way will persist even after the app is closed, in + * which case they will be sent the next time the app is opened. If more than 10MB of commands are + * waiting to be sent, subsequent calls to `#deleteEventually()` or + * [.saveEventually] will cause old instructions to be silently discarded until the + * connection can be re-established, and the queued objects can be saved. + * + * @return A [Task] that is resolved when the delete completes. + */ + fun deleteEventually(): Task { + val command: ParseRESTCommand + val runEventuallyTask: Task + synchronized(mutex) { + validateDelete() + isDeletingEventually += 1 + var localId: String? = null + if (objectId == null) { + localId = getOrCreateLocalId() + } + + // TODO(grantland): Convert to async + val sessionToken = ParseUser.getCurrentSessionToken() + + // See [1] + command = ParseRESTObjectCommand.deleteObjectCommand( + state, sessionToken + ) + command.localId = localId + runEventuallyTask = + Parse.getEventuallyQueue().enqueueEventuallyAsync(command, this@ParseObject) + } + return if (Parse.isLocalDatastoreEnabled()) { + // ParsePinningEventuallyQueue calls handleDeleteEventuallyResultAsync directly. + runEventuallyTask.makeVoid() + } else { + runEventuallyTask.onSuccessTask { handleDeleteEventuallyResultAsync() } + } + } + + /** + * Handles the result of `deleteEventually`. + * + * + * Should only be called on success. + */ + fun handleDeleteEventuallyResultAsync(): Task { + synchronized(mutex) { isDeletingEventually -= 1 } + val handleDeleteResultTask = handleDeleteResultAsync() + return handleDeleteResultTask.onSuccessTask { task: Task? -> + Parse.getEventuallyQueue() + .notifyTestHelper(ParseEventuallyQueue.TestHelper.OBJECT_REMOVED) + task + } + } + + /** + * Handles the result of `fetch`. + * + * + * Should only be called on success. + */ + internal open fun handleFetchResultAsync(result: State): Task? { + var task = Task.forResult(null) + + /* + * If this object is in the offline store, then we need to make sure that we pull in any dirty + * changes it may have before merging the server data into it. + */ + val store = Parse.getLocalDatastore() + if (store != null) { + task = task.onSuccessTask { + store.fetchLocallyAsync(this@ParseObject).makeVoid() + } + .continueWithTask { task14: Task -> + // Catch CACHE_MISS + if (task14.error is ParseException + && (task14.error as ParseException).code == ParseException.CACHE_MISS + ) { + return@continueWithTask null + } + task14 + } + } + task = task.onSuccessTask { + synchronized(mutex) { + val newState: State = if (result.isComplete) { + // Result is complete, so just replace + result + } else { + // Result is incomplete, so we'll need to apply it to the current state + state.newBuilder>().apply(result).build() + } + state = newState + } + null + } + if (store != null) { + task = + task.onSuccessTask { store.updateDataForObjectAsync(this@ParseObject) } + .continueWithTask { task: Task -> + // Catch CACHE_MISS + if (task.error is ParseException + && (task.error as ParseException).code == ParseException.CACHE_MISS + ) { + return@continueWithTask null + } + task + } + } + return task + } + + /** + * Fetches this object with the data from the server. Call this whenever you want the state of the + * object to reflect exactly what is on the server. + * + * @return The `ParseObject` that was fetched. + * @throws ParseException Throws an exception if the server is inaccessible. + */ + @Throws(ParseException::class) + open fun fetch(): T { + return ParseTaskUtils.wait(this.fetchInBackground()) + } + + open fun fetchAsync( + sessionToken: String?, toAwait: Task + ): Task? { + return toAwait.onSuccessTask { + var fetchedObjects: Map + synchronized(mutex) { + fetchedObjects = collectFetchedObjects() + } + val decoder: ParseDecoder = KnownParseObjectDecoder(fetchedObjects) + objectController.fetchAsync(state, sessionToken, decoder) + }.onSuccessTask { task: Task -> + val result = task.result + handleFetchResultAsync(result) + }.onSuccess { this@ParseObject as T } + } + + /** + * Fetches this object with the data from the server in a background thread. This is preferable to + * using fetch(), unless your code is already running from a background thread. + * + * @return A [Task] that is resolved when fetch completes. + */ + fun fetchInBackground(): Task { + return ParseUser.getCurrentSessionTokenAsync() + .onSuccessTask { task: Task -> + val sessionToken = task.result + taskQueue.enqueue { toAwait: Task -> + fetchAsync( + sessionToken, + toAwait + ) + } + } + } + + /** + * Fetches this object with the data from the server in a background thread. This is preferable to + * using fetch(), unless your code is already running from a background thread. + * + * @param callback `callback.done(object, e)` is called when the fetch completes. + */ + fun fetchInBackground(callback: GetCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(this.fetchInBackground(), callback) + } + + /** + * If this `ParseObject` has not been fetched (i.e. [.isDataAvailable] returns `false`), + * fetches this object with the data from the server in a background thread. This is preferable to + * using [.fetchIfNeeded], unless your code is already running from a background thread. + * + * @return A [Task] that is resolved when fetch completes. + */ + fun fetchIfNeededInBackground(): Task { + return if (isDataAvailable()) { + Task.forResult(this as T) + } else ParseUser.getCurrentSessionTokenAsync() + .onSuccessTask { task: Task -> + val sessionToken = task.result + taskQueue.enqueue { toAwait: Task -> + if (isDataAvailable()) { + return@enqueue Task.forResult(this@ParseObject as T) + } + fetchAsync(sessionToken, toAwait) + } + } + } + + /** + * If this `ParseObject` has not been fetched (i.e. [.isDataAvailable] returns `false`), + * fetches this object with the data from the server. + * + * @return The fetched `ParseObject`. + * @throws ParseException Throws an exception if the server is inaccessible. + */ + @Throws(ParseException::class) + open fun fetchIfNeeded(): T { + return ParseTaskUtils.wait(this.fetchIfNeededInBackground()) + } + + /** + * If this `ParseObject` has not been fetched (i.e. [.isDataAvailable] returns `false`), + * fetches this object with the data from the server in a background thread. This is preferable to + * using [.fetchIfNeeded], unless your code is already running from a background thread. + * + * @param callback `callback.done(object, e)` is called when the fetch completes. + */ + fun fetchIfNeededInBackground(callback: GetCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(this.fetchIfNeededInBackground(), callback) + } + + // Validates the delete method + open fun validateDelete() { + // do nothing + } + + private fun deleteAsync(sessionToken: String?, toAwait: Task): Task { + validateDelete() + return toAwait.onSuccessTask { task: Task -> + isDeleting = true + if (state.objectId() == null) { + return@onSuccessTask task.cast() // no reason to call delete since it doesn't exist + } + deleteAsync(sessionToken) + }.onSuccessTask { task: Task? -> handleDeleteResultAsync() } + .continueWith { task: Task -> + isDeleting = false + if (task.isFaulted) { + throw task.error + } + null + } + } + + //TODO (grantland): I'm not sure we want direct access to this. All access to `delete` should + // enqueue on the taskQueue... + fun deleteAsync(sessionToken: String?): Task { + return objectController.deleteAsync(state, sessionToken) + } + + /** + * Handles the result of `delete`. + * + * + * Should only be called on success. + */ + fun handleDeleteResultAsync(): Task { + var task = Task.forResult(null) + synchronized(mutex) { isDeleted = true } + val store = Parse.getLocalDatastore() + if (store != null) { + task = task.continueWithTask { + synchronized(mutex) { + if (isDeleted) { + store.unregisterObject(this@ParseObject) + return@continueWithTask store.deleteDataForObjectAsync(this@ParseObject) + } else { + return@continueWithTask store.updateDataForObjectAsync(this@ParseObject) + } + } + } + } + return task + } + + /** + * Deletes this object on the server in a background thread. This is preferable to using + * [.delete], unless your code is already running from a background thread. + * + * @return A [Task] that is resolved when delete completes. + */ + fun deleteInBackground(): Task { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask { task: Task -> + val sessionToken = task.result + taskQueue.enqueue { toAwait: Task -> + deleteAsync( + sessionToken, + toAwait + ) + } + } + } + + /** + * Deletes this object on the server. This does not delete or destroy the object locally. + * + * @throws ParseException Throws an error if the object does not exist or if the internet fails. + */ + @Throws(ParseException::class) + fun delete() { + ParseTaskUtils.wait(deleteInBackground()) + } + + /** + * Deletes this object on the server in a background thread. This is preferable to using + * [.delete], unless your code is already running from a background thread. + * + * @param callback `callback.done(e)` is called when the save completes. + */ + fun deleteInBackground(callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteInBackground(), callback) + } + + /** + * Returns `true` if this object can be serialized for saving. + */ + private fun canBeSerialized(): Boolean { + synchronized(mutex) { + val result = Capture(true) + + // This method is only used for batching sets of objects for saveAll + // and when saving children automatically. Since it's only used to + // determine whether or not save should be called on them, it only + // needs to examine their current values, so we use estimatedData. + object : ParseTraverser() { + override fun visit(value: Any): Boolean { + if (value is ParseFile) { + if (value.isDirty) { + result.set(false) + } + } + if (value is ParseObject) { + if (value.objectId == null) { + result.set(false) + } + } + + // Continue to traverse only if it can still be serialized. + return result.get() + } + }.setYieldRoot(false).setTraverseParseObjects(true).traverse(this) + return result.get() + } + } + + /** + * Return the operations that will be sent in the next call to save. + */ + private fun currentOperations(): ParseOperationSet { + synchronized(mutex) { return operationSetQueue.last } + } + + /** + * Updates the estimated values in the map based on the given set of ParseFieldOperations. + */ + private fun applyOperations(operations: ParseOperationSet, map: MutableMap) { + for (key in operations.keys) { + val operation = operations[key] + val oldValue = map[key] + val newValue = operation!!.apply(oldValue, key) + if (newValue != null) { + map[key] = newValue + } else { + map.remove(key) + } + } + } + + /** + * Regenerates the estimatedData map from the serverData and operations. + */ + private fun rebuildEstimatedData() { + synchronized(mutex) { + estimatedData.clear() + for (key in state.keySet()) { + estimatedData[key] = state[key] + } + for (operations in operationSetQueue) { + applyOperations(operations, estimatedData) + } + } + } + + fun markAllFieldsDirty() { + synchronized(mutex) { + for (key in state.keySet()) { + performPut(key, state[key]!!) + } + } + } + + /** + * performOperation() is like [.put] but instead of just taking a new value, + * it takes a ParseFieldOperation that modifies the value. + */ + internal fun performOperation(key: String, operation: ParseFieldOperation?) { + synchronized(mutex) { + val oldValue = estimatedData[key] + val newValue = operation!!.apply(oldValue, key) + if (newValue != null) { + estimatedData[key] = newValue + } else { + estimatedData.remove(key) + } + val oldOperation = currentOperations()[key] + val newOperation = operation.mergeWithPrevious(oldOperation) + currentOperations().put(key, newOperation) + } + } + + /** + * Add a key-value pair to this object. It is recommended to name keys in + * `camelCaseLikeThis`. + * + * @param key Keys must be alphanumerical plus underscore, and start with a letter. + * @param value Values may be numerical, [String], [JSONObject], [JSONArray], + * [JSONObject.NULL], or other `ParseObject`s. value may not be `null`. + */ + open fun put(key: String, value: Any) { + checkKeyIsMutable(key) + performPut(key, value) + } + + fun performPut(key: String, value: Any) { + lateinit var valueToAdd: Any + + if (value is JSONObject) { + val decoder = ParseDecoder.get() + valueToAdd = decoder.convertJSONObjectToMap(value) + } else if (value is JSONArray) { + val decoder = ParseDecoder.get() + valueToAdd = decoder.convertJSONArrayToList(value) + } + require(isValidType(valueToAdd)) { "invalid type for value: " + value.javaClass.toString() } + performOperation(key, ParseSetOperation(valueToAdd)) + } + + /** + * Whether this object has a particular key. Same as [.containsKey]. + * + * @param key The key to check for + * @return Whether this object contains the key + */ + fun has(key: String): Boolean { + return containsKey(key) + } + /** + * Atomically increments the given key by the given number. + * + * @param key The key to increment. + * @param amount The amount to increment by. + */ + /** + * Atomically increments the given key by 1. + * + * @param key The key to increment. + */ + @JvmOverloads + fun increment(key: String, amount: Number = 1) { + val operation = ParseIncrementOperation(amount) + performOperation(key, operation) + } + + /** + * Atomically adds an object to the end of the array associated with a given key. + * + * @param key The key. + * @param value The object to add. + */ + fun add(key: String, value: Any) { + this.addAll(key, listOf(value)) + } + + /** + * Atomically adds the objects contained in a `Collection` to the end of the array + * associated with a given key. + * + * @param key The key. + * @param values The objects to add. + */ + fun addAll(key: String, values: Collection<*>?) { + val operation = ParseAddOperation(values) + performOperation(key, operation) + } + + /** + * Atomically adds an object to the array associated with a given key, only if it is not already + * present in the array. The position of the insert is not guaranteed. + * + * @param key The key. + * @param value The object to add. + */ + fun addUnique(key: String, value: Any) { + addAllUnique(key, listOf(value)) + } + + /** + * Atomically adds the objects contained in a `Collection` to the array associated with a + * given key, only adding elements which are not already present in the array. The position of the + * insert is not guaranteed. + * + * @param key The key. + * @param values The objects to add. + */ + fun addAllUnique(key: String, values: Collection<*>?) { + val operation = ParseAddUniqueOperation(values) + performOperation(key, operation) + } + + /** + * Removes a key from this object's data if it exists. + * + * @param key The key to remove. + */ + open fun remove(key: String) { + checkKeyIsMutable(key) + performRemove(key) + } + + fun performRemove(key: String) { + synchronized(mutex) { + val `object` = get(key) + if (`object` != null) { + performOperation(key, ParseDeleteOperation.getInstance()) + } + } + } + + /** + * Atomically removes all instances of the objects contained in a `Collection` from the + * array associated with a given key. To maintain consistency with the Java Collection API, there + * is no method removing all instances of a single object. Instead, you can call + * `parseObject.removeAll(key, Arrays.asList(value))`. + * + * @param key The key. + * @param values The objects to remove. + */ + fun removeAll(key: String, values: Collection<*>?) { + checkKeyIsMutable(key) + val operation = ParseRemoveOperation(values) + performOperation(key, operation) + } + + private fun checkKeyIsMutable(key: String) { + require(isKeyMutable(key)) { + ("Cannot modify `" + key + + "` property of an " + className + " object.") + } + } + + internal open fun isKeyMutable(key: String?): Boolean { + return true + } + + /** + * Whether this object has a particular key. Same as [.has]. + * + * @param key The key to check for + * @return Whether this object contains the key + */ + fun containsKey(key: String): Boolean { + synchronized(mutex) { return estimatedData.containsKey(key) } + } + + /** + * Access a [String] value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [String]. + */ + fun getString(key: String): String? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is String) { + null + } else value + } + } + + /** + * Access a `byte[]` value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a `byte[]`. + */ + fun getBytes(key: String): ByteArray? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is ByteArray) { + null + } else value + } + } + + /** + * Access a [Number] value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [Number]. + */ + fun getNumber(key: String): Number? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is Number) { + null + } else value + } + } + + /** + * Access a [JSONArray] value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [JSONArray]. + */ + fun getJSONArray(key: String): JSONArray? { + synchronized(mutex) { + checkGetAccess(key) + var value = estimatedData[key] + if (value is List<*>) { + value = PointerOrLocalIdEncoder.get().encode(value) + } + return if (value !is JSONArray) { + null + } else value + } + } + + /** + * Access a [List] value. + * + * @param key The key to access the value for + * @return `null` if there is no such key or if the value can't be converted to a + * [List]. + */ + fun getList(key: String?): List? { + synchronized(mutex) { + val value = estimatedData[key] as? List<*> ?: return null + return value as List + } + } + + /** + * Access a [Map] value + * + * @param key The key to access the value for + * @return `null` if there is no such key or if the value can't be converted to a + * [Map]. + */ + fun getMap(key: String?): Map? { + synchronized(mutex) { + val value = estimatedData[key] as? Map<*, *> ?: return null + return value as Map + } + } + + /** + * Access a [JSONObject] value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [JSONObject]. + */ + fun getJSONObject(key: String): JSONObject? { + synchronized(mutex) { + checkGetAccess(key) + var value = estimatedData[key] + if (value is Map<*, *>) { + value = PointerOrLocalIdEncoder.get().encode(value) + } + return if (value !is JSONObject) { + null + } else value + } + } + + /** + * Access an `int` value. + * + * @param key The key to access the value for. + * @return `0` if there is no such key or if it is not a `int`. + */ + fun getInt(key: String): Int { + val number = getNumber(key) ?: return 0 + return number.toInt() + } + + /** + * Access a `double` value. + * + * @param key The key to access the value for. + * @return `0` if there is no such key or if it is not a `double`. + */ + fun getDouble(key: String): Double { + val number = getNumber(key) ?: return 0.0 + return number.toDouble() + } + + /** + * Access a `long` value. + * + * @param key The key to access the value for. + * @return `0` if there is no such key or if it is not a `long`. + */ + fun getLong(key: String): Long { + val number = getNumber(key) ?: return 0 + return number.toLong() + } + + /** + * Access a `boolean` value. + * + * @param key The key to access the value for. + * @return `false` if there is no such key or if it is not a `boolean`. + */ + fun getBoolean(key: String): Boolean { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is Boolean) { + false + } else value + } + } + + /** + * Access a [Date] value. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [Date]. + */ + fun getDate(key: String): Date? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is Date) { + null + } else value + } + } + + /** + * Access a `ParseObject` value. This function will not perform a network request. Unless the + * `ParseObject` has been downloaded (e.g. by a [ParseQuery.include] or by calling + * [.fetchIfNeeded] or [.fetch]), [.isDataAvailable] will return + * `false`. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a `ParseObject`. + */ + fun getParseObject(key: String): ParseObject? { + val value = get(key) + return if (value !is ParseObject) { + null + } else value + } + + /** + * Access a [ParseUser] value. This function will not perform a network request. Unless the + * `ParseObject` has been downloaded (e.g. by a [ParseQuery.include] or by calling + * [.fetchIfNeeded] or [.fetch]), [.isDataAvailable] will return + * `false`. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if the value is not a [ParseUser]. + */ + fun getParseUser(key: String): ParseUser? { + val value = get(key) + return if (value !is ParseUser) { + null + } else value + } + + /** + * Access a [ParseFile] value. This function will not perform a network request. Unless the + * [ParseFile] has been downloaded (e.g. by calling [ParseFile.getData]), + * [ParseFile.isDataAvailable] will return `false`. + * + * @param key The key to access the value for. + * @return `null` if there is no such key or if it is not a [ParseFile]. + */ + fun getParseFile(key: String): ParseFile? { + val value = get(key) + return if (value !is ParseFile) { + null + } else value + } + + /** + * Access a [ParseGeoPoint] value. + * + * @param key The key to access the value for + * @return `null` if there is no such key or if it is not a [ParseGeoPoint]. + */ + fun getParseGeoPoint(key: String): ParseGeoPoint? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is ParseGeoPoint) { + null + } else value + } + } + + /** + * Access a [ParsePolygon] value. + * + * @param key The key to access the value for + * @return `null` if there is no such key or if it is not a [ParsePolygon]. + */ + fun getParsePolygon(key: String): ParsePolygon? { + synchronized(mutex) { + checkGetAccess(key) + val value = estimatedData[key] + return if (value !is ParsePolygon) { + null + } else value + } + } + + /** + * Access the [ParseACL] governing this object. + */ + fun getACL(): ParseACL? { + return getACL(true) + } + + /** + * Set the [ParseACL] governing this object. + */ + fun setACL(acl: ParseACL) { + put(KEY_ACL, acl) + } + + private fun getACL(mayCopy: Boolean): ParseACL? { + synchronized(mutex) { + checkGetAccess(KEY_ACL) + val acl = (estimatedData[KEY_ACL] ?: return null) as? ParseACL + ?: throw RuntimeException("only ACLs can be stored in the ACL key") + if (mayCopy && acl.isShared) { + val copy = ParseACL(acl) + estimatedData[KEY_ACL] = copy + return copy + } + return acl + } + } + + /** + * Gets whether the `ParseObject` has been fetched. + * + * @return `true` if the `ParseObject` is new or has been fetched or refreshed. `false` + * otherwise. + */ + fun isDataAvailable(): Boolean { + synchronized(mutex) { return state.isComplete } + } + + /** + * Gets whether the `ParseObject` specified key has been fetched. + * This means the property can be accessed safely. + * + * @return `true` if the `ParseObject` key is new or has been fetched or refreshed. `false` + * otherwise. + */ + fun isDataAvailable(key: String): Boolean { + synchronized(mutex) { + // Fallback to estimatedData to include dirty changes. + return isDataAvailable() || state.availableKeys() + .contains(key) || estimatedData.containsKey(key) + } + } + + /** + * Access or create a [ParseRelation] value for a key + * + * @param key The key to access the relation for. + * @return the ParseRelation object if the relation already exists for the key or can be created + * for this key. + */ + fun getRelation(key: String): ParseRelation { + synchronized(mutex) { + + // All the sanity checking is done when add or remove is called on the relation. + val value = estimatedData[key] + return if (value is ParseRelation<*>) { + val relation = value as ParseRelation + relation.ensureParentAndKey(this, key) + relation + } else { + val relation = ParseRelation(this, key) + /* + * We put the relation into the estimated data so that we'll get the same instance later, + * which may have known objects cached. If we rebuildEstimatedData, then this relation will + * be lost, and we'll get a new one. That's okay, because any cached objects it knows about + * must be replayable from the operations in the queue. If there were any objects in this + * relation that weren't still in the queue, then they would be in the copy of the + * ParseRelation that's in the serverData, so we would have gotten that instance instead. + */estimatedData[key] = relation + relation + } + } + } + + /** + * Access a value. In most cases it is more convenient to use a helper function such as + * [.getString] or [.getInt]. + * + * @param key The key to access the value for. + * @return `null` if there is no such key. + */ + operator fun get(key: String): Any? { + synchronized(mutex) { + if (key == KEY_ACL) { + return getACL() + } + checkGetAccess(key) + val value = estimatedData[key] + + // A relation may be deserialized without a parent or key. + // Either way, make sure it's consistent. + if (value is ParseRelation<*>) { + value.ensureParentAndKey(this, key) + } + return value + } + } + + private fun checkGetAccess(key: String) { + check(isDataAvailable(key)) { "ParseObject has no data for '$key'. Call fetchIfNeeded() to get the data." } + } + + fun hasSameId(other: ParseObject): Boolean { + synchronized(mutex) { return className != null && objectId != null && className == other.className && objectId == other.objectId } + } + + fun registerSaveListener(callback: GetCallback?) { + synchronized(mutex) { saveEvent.subscribe(callback) } + } + + fun unregisterSaveListener(callback: GetCallback?) { + synchronized(mutex) { saveEvent.unsubscribe(callback) } + } + + /** + * Called when a non-pointer is being created to allow additional initialization to occur. + */ + internal open fun setDefaultValues() { + if (needsDefaultACL() && ParseACL.getDefaultACL() != null) { + setACL(ParseACL.getDefaultACL()) + } + } + + /** + * Determines whether this object should get a default ACL. Override in subclasses to turn off + * default ACLs. + */ + internal open fun needsDefaultACL(): Boolean { + return true + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with do + * nothing. + */ + open fun fetchFromLocalDatastoreAsync(): Task? { + check(Parse.isLocalDatastoreEnabled()) { + "Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`." + } + return Parse.getLocalDatastore().fetchLocallyAsync(this as T) + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with do + * nothing. + */ + fun fetchFromLocalDatastoreInBackground(callback: GetCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(fetchFromLocalDatastoreAsync(), callback) + } + + /** + * Loads data from the local datastore into this object, if it has not been fetched from the + * server already. If the object is not stored in the local datastore, this method with throw a + * CACHE_MISS exception. + * + * @throws ParseException exception if fails + */ + @Throws(ParseException::class) + fun fetchFromLocalDatastore() { + ParseTaskUtils.wait(fetchFromLocalDatastoreAsync()) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @param callback the callback + * @see .unpinInBackground + */ + fun pinInBackground(name: String = DEFAULT_PIN, callback: SaveCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(name), callback) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @return A [Task] that is resolved when pinning completes. + * @see .unpinInBackground + */ + fun pinInBackground(name: String = DEFAULT_PIN): Task { + return pinAllInBackground(name, listOf(this)) + } + + fun pinInBackground(name: String = DEFAULT_PIN, includeAllChildren: Boolean): Task { + return pinAllInBackground(name, listOf(this), includeAllChildren) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @throws ParseException exception if fails + * @see .unpin + */ + @Throws(ParseException::class) + fun pin(name: String = DEFAULT_PIN) { + ParseTaskUtils.wait(pinInBackground(name)) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @param callback the callback + * @see .unpinInBackground + * @see .DEFAULT_PIN + */ + fun pinInBackground(callback: SaveCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(), callback) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @return A [Task] that is resolved when pinning completes. + * @see .unpinInBackground + * @see .DEFAULT_PIN + */ + fun pinInBackground(): Task { + return pinAllInBackground(DEFAULT_PIN, listOf(this)) + } + + /** + * Stores the object and every object it points to in the local datastore, recursively. If those + * other objects have not been fetched from Parse, they will not be stored. However, if they have + * changed data, all of the changes will be retained. To get the objects back later, you can use + * [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on + * it. + * + * @throws ParseException exception if fails + * @see .unpin + * @see .DEFAULT_PIN + */ + @Throws(ParseException::class) + fun pin() { + ParseTaskUtils.wait(pinInBackground()) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @param callback the callback + * @see .pinInBackground + */ + fun unpinInBackground(name: String = DEFAULT_PIN, callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(name), callback) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @return A [Task] that is resolved when unpinning completes. + * @see .pinInBackground + */ + fun unpinInBackground(name: String = DEFAULT_PIN): Task { + return unpinAllInBackground(name, listOf(this)) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see .pin + */ + @Throws(ParseException::class) + fun unpin(name: String = DEFAULT_PIN) { + ParseTaskUtils.wait(unpinInBackground(name)) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @param callback the callback + * @see .pinInBackground + * @see .DEFAULT_PIN + */ + fun unpinInBackground(callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(), callback) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @return A [Task] that is resolved when unpinning completes. + * @see .pinInBackground + * @see .DEFAULT_PIN + */ + fun unpinInBackground(): Task { + return unpinAllInBackground(DEFAULT_PIN, listOf(this)) + } + + /** + * Removes the object and every object it points to in the local datastore, recursively. + * + * @see .pin + * @see .DEFAULT_PIN + */ + @Throws(ParseException::class) + fun unpin() { + ParseTaskUtils.wait(unpinInBackground()) + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + writeToParcel(dest, ParseObjectParcelEncoder(this)) + } + + fun writeToParcel(dest: Parcel, encoder: ParseParcelEncoder) { + synchronized(mutex) { + + // Developer warnings. + ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled() + val saving = hasOutstandingOperations() + val deleting = isDeleting || isDeletingEventually > 0 + if (saving) { + w( + TAG, + "About to parcel a ParseObject while a save / saveEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will act as if these tasks have failed. This means that " + + "the subsequent call to save() will update again the same keys, and this is dangerous " + + "for certain operations, like increment(). To avoid inconsistencies, wait for operations " + + "to end before parceling." + ) + } + if (deleting) { + w( + TAG, + "About to parcel a ParseObject while a delete / deleteEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will assume it's not deleted, and might incorrectly " + + "return false for isDirty(). To avoid inconsistencies, wait for operations to end " + + "before parceling." + ) + } + // Write className and id first, regardless of state. + dest.writeString(className) + val objectId = objectId + dest.writeByte(if (objectId != null) 1.toByte() else 0) + if (objectId != null) dest.writeString(objectId) + // Write state and other members + state.writeToParcel(dest, encoder) + dest.writeByte(if (localId != null) 1.toByte() else 0) + if (localId != null) dest.writeString(localId) + dest.writeByte(if (isDeleted) 1.toByte() else 0) + // Care about dirty changes and ongoing tasks. + var set: ParseOperationSet + if (hasOutstandingOperations()) { + // There's more than one set. Squash the queue, creating copies + // to preserve the original queue when LDS is enabled. + set = ParseOperationSet() + for (operationSet in operationSetQueue) { + val copy = ParseOperationSet(operationSet) + copy.mergeFrom(set) + set = copy + } + } else { + set = operationSetQueue.last + } + set.isSaveEventually = false + set.toParcel(dest, encoder) + // Pass a Bundle to subclasses. + val bundle = Bundle() + onSaveInstanceState(bundle) + dest.writeBundle(bundle) + } + } + + /** + * Called when parceling this ParseObject. + * Subclasses can put values into the provided [Bundle] and receive them later + * [.onRestoreInstanceState]. Note that internal fields are already parceled by + * the framework. + * + * @param outState Bundle to host extra values + */ + protected open fun onSaveInstanceState(outState: Bundle?) {} + + /** + * Called when unparceling this ParseObject. + * Subclasses can read values from the provided [Bundle] that were previously put + * during [.onSaveInstanceState]. At this point the internal state is already + * recovered. + * + * @param savedState Bundle to read the values from + */ + protected open fun onRestoreInstanceState(savedState: Bundle?) {} + open class State { + private val className: String? + private val objectId: String? + private val createdAt: Long + private val updatedAt: Long + private val serverData: Map + private val availableKeys: Set + val isComplete: Boolean + + constructor(builder: Init<*>) { + className = builder.className + objectId = builder.objectId + createdAt = builder.createdAt + updatedAt = if (builder.updatedAt > 0) builder.updatedAt else createdAt + serverData = Collections.unmodifiableMap(HashMap(builder.serverData.toMap())) + isComplete = builder.isComplete + availableKeys = builder.availableKeys.toSet() + } + + private constructor(parcel: Parcel, clazz: String?, decoder: ParseParcelDecoder) { + className = clazz // Already read + objectId = if (parcel.readByte().toInt() == 1) parcel.readString() else null + createdAt = parcel.readLong() + val updated = parcel.readLong() + updatedAt = if (updated > 0) updated else createdAt + val size = parcel.readInt() + val map = HashMap() + for (i in 0 until size) { + val key = parcel.readString()!! + val obj = decoder.decode(parcel)!! + map[key] = obj + } + serverData = map.toMap() + isComplete = parcel.readByte().toInt() == 1 + val available: List = ArrayList() + parcel.readStringList(available) + availableKeys = HashSet(available) + } + + open fun ?> newBuilder(): T { + return Builder(this) as T + } + + fun className(): String? { + return className + } + + fun objectId(): String? { + return objectId + } + + fun createdAt(): Long { + return createdAt + } + + fun updatedAt(): Long { + return updatedAt + } + + operator fun get(key: String): Any? { + return serverData[key] + } + + fun keySet(): Set { + return serverData.keys + } + + // Available keys for this object. With respect to keySet(), this includes also keys that are + // undefined in the server, but that should be accessed without throwing. + // These extra keys come e.g. from ParseQuery.selectKeys(). Selected keys must be available to + // get() methods even if undefined, for consistency with complete objects. + // For a complete object, this set is equal to keySet(). + fun availableKeys(): Set { + return availableKeys + } + + open fun writeToParcel(dest: Parcel, encoder: ParseParcelEncoder) { + dest.writeString(className) + dest.writeByte(if (objectId != null) 1.toByte() else 0) + if (objectId != null) { + dest.writeString(objectId) + } + dest.writeLong(createdAt) + dest.writeLong(updatedAt) + dest.writeInt(serverData.size) + val keys = serverData.keys + for (key in keys) { + dest.writeString(key) + encoder.encode(serverData[key], dest) + } + dest.writeByte(if (isComplete) 1.toByte() else 0) + dest.writeStringList(ArrayList(availableKeys)) + } + + override fun toString(): String { + return String.format( + Locale.US, "%s@%s[" + + "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + + "serverData=%s, availableKeys=%s]", + javaClass.name, + Integer.toHexString(hashCode()), + className, + objectId, + createdAt, + updatedAt, + isComplete, + serverData, + availableKeys + ) + } + + abstract class Init> { + @JvmField + val serverData: MutableMap = HashMap() + val className: String? + var objectId: String? = null + var createdAt: Long = -1 + var updatedAt: Long = -1 + var isComplete = false + var availableKeys: MutableSet = HashSet() + + constructor(className: String?) { + this.className = className + } + + constructor(state: State) { + className = state.className() + objectId = state.objectId() + createdAt = state.createdAt() + updatedAt = state.updatedAt() + availableKeys = Collections.synchronizedSet( + HashSet( + state.availableKeys() + ) + ) + for (key in state.keySet()) { + serverData[key] = state[key] + availableKeys.add(key) + } + isComplete = state.isComplete + } + + abstract fun self(): T + abstract fun build(): S + fun objectId(objectId: String?): T { + this.objectId = objectId + return self() + } + + fun createdAt(createdAt: Date): T { + this.createdAt = createdAt.time + return self() + } + + fun createdAt(createdAt: Long): T { + this.createdAt = createdAt + return self() + } + + fun updatedAt(updatedAt: Date): T { + this.updatedAt = updatedAt.time + return self() + } + + fun updatedAt(updatedAt: Long): T { + this.updatedAt = updatedAt + return self() + } + + fun isComplete(complete: Boolean): T { + isComplete = complete + return self() + } + + fun put(key: String, value: Any?): T { + serverData[key] = value + availableKeys.add(key) + return self() + } + + fun remove(key: String): T { + serverData.remove(key) + return self() + } + + fun availableKeys(keys: Set = emptySet()): T { + availableKeys.addAll(keys) + return self() + } + + fun clear(): T { + objectId = null + createdAt = -1 + updatedAt = -1 + isComplete = false + serverData.clear() + availableKeys.clear() + return self() + } + + /** + * Applies a `State` on top of this `Builder` instance. + * + * @param other The `State` to apply over this instance. + * @return A new `Builder` instance. + */ + open fun apply(other: State): T { + if (other.objectId() != null) { + objectId(other.objectId()) + } + if (other.createdAt() > 0) { + createdAt(other.createdAt()) + } + if (other.updatedAt() > 0) { + updatedAt(other.updatedAt()) + } + isComplete(isComplete || other.isComplete) + for (key in other.keySet()) { + put(key, other[key]) + } + availableKeys(other.availableKeys()) + return self() + } + + fun apply(operations: ParseOperationSet): T { + for (key in operations.keys) { + val operation = operations[key] + val oldValue = serverData[key] + val newValue = operation!!.apply(oldValue, key) + newValue?.let { put(key, it) } ?: remove(key) + } + return self() + } + } + + internal class Builder : Init { + constructor(className: String) : super(className) + constructor(state: State) : super(state) + + override fun self(): Builder { + return this + } + + override fun build(): S { + return State(this) as S + } + } + + companion object { + @JvmStatic + fun newBuilder(className: String): Init<*> { + return if ("_User" == className) { + ParseUser.State.Builder() + } else Builder(className) + } + + @JvmStatic + internal fun createFromParcel(source: Parcel, decoder: ParseParcelDecoder): State { + val className = source.readString() + return if ("_User" == className) { + ParseUser.State(source, className, decoder) + } else State(source, className, decoder) + } + } + } + + companion object { + /** + * Default name for pinning if not specified. + * + * @see .pin + * @see .unpin + */ + const val DEFAULT_PIN = "_default" + + /* + REST JSON Keys + */ + const val KEY_OBJECT_ID = "objectId" + const val KEY_CREATED_AT = "createdAt" + const val KEY_UPDATED_AT = "updatedAt" + const val KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually" + private const val AUTO_CLASS_NAME = "_Automatic" + private const val TAG = "ParseObject" + private const val KEY_CLASS_NAME = "className" + private const val KEY_ACL = "ACL" + + /* + Internal JSON Keys - Used to store internal data when persisting {@code ParseObject}s locally. + */ + private const val KEY_COMPLETE = "__complete" + private const val KEY_OPERATIONS = "__operations" + + // Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s + // correctly, and helps constructing the {@code State.availableKeys()} set. + private const val KEY_SELECTED_KEYS = "__selectedKeys" + + // Because Grantland messed up naming this... We'll only try to read from this for backward + // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete + // and not check after a while + private const val KEY_IS_DELETING_EVENTUALLY_OLD = "isDeletingEventually" + private val isCreatingPointerForObjectId: ThreadLocal = + object : ThreadLocal() { + override fun initialValue(): String? { + return null + } + } + + /* + * This is used only so that we can pass it to createWithoutData as the objectId to make it create + * an un-fetched pointer that has no objectId. This is useful only in the context of the offline + * store, where you can have an un-fetched pointer for an object that can later be fetched from the + * store. + */ + private const val NEW_OFFLINE_OBJECT_ID_PLACEHOLDER = "*** Offline Object ***" + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ParseObject { + return createFromParcel(source, ParseObjectParcelDecoder()) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + private val objectController: ParseObjectController + get() = ParseCorePlugins.getInstance().objectController + private val localIdManager: LocalIdManager + get() = ParseCorePlugins.getInstance().localIdManager + private val subclassingController: ParseObjectSubclassingController + get() = ParseCorePlugins.getInstance().subclassingController + + /** + * Creates a new `ParseObject` based upon a class name. If the class name is a special type + * (e.g. for `ParseUser`), then the appropriate type of `ParseObject` is returned. + * + * @param className The class of object to create. + * @return A new `ParseObject` for the given class name. + */ + fun create(className: String?): ParseObject { + return subclassingController.newInstance(className) + } + + /** + * Creates a new `ParseObject` based upon a subclass type. Note that the object will be + * created based upon the [ParseClassName] of the given subclass type. For example, calling + * create(ParseUser.class) may create an instance of a custom subclass of `ParseUser`. + * + * @param subclass The class of object to create. + * @return A new `ParseObject` based upon the class name of the given subclass type. + */ + @JvmStatic + fun create(subclass: Class?): T { + return create(subclassingController.getClassName(subclass)) as T + } + + /** + * Creates a reference to an existing `ParseObject` for use in creating associations between + * `ParseObject`s. Calling [.isDataAvailable] on this object will return + * `false` until [.fetchIfNeeded] or [.fetch] has been called. No network + * request will be made. + * + * @param className The object's class. + * @param objectId The object id for the referenced object. + * @return A `ParseObject` without data. + */ + fun createWithoutData(className: String?, objectId: String?): ParseObject? { + val store = Parse.getLocalDatastore() + return try { + if (objectId == null) { + isCreatingPointerForObjectId.set(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER) + } else { + isCreatingPointerForObjectId.set(objectId) + } + var `object`: ParseObject? = null + if (store != null && objectId != null) { + `object` = store.getObject(className, objectId) + } + if (`object` == null) { + `object` = create(className) + check(!`object`.hasChanges()) { + ("A ParseObject subclass default constructor must not make changes " + + "to the object that cause it to be dirty.") + } + } + `object` + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw RuntimeException("Failed to create instance of subclass.", e) + } finally { + isCreatingPointerForObjectId.set(null) + } + } + + /** + * Creates a reference to an existing `ParseObject` for use in creating associations between + * `ParseObject`s. Calling [.isDataAvailable] on this object will return + * `false` until [.fetchIfNeeded] or [.fetch] has been called. No network + * request will be made. + * + * @param subclass The `ParseObject` subclass to create. + * @param objectId The object id for the referenced object. + * @return A `ParseObject` without data. + */ + fun createWithoutData(subclass: Class?, objectId: String?): T? { + return createWithoutData(subclassingController.getClassName(subclass), objectId) as T? + } + + /** + * Registers a custom subclass type with the Parse SDK, enabling strong-typing of those + * `ParseObject`s whenever they appear. Subclasses must specify the [ParseClassName] + * annotation and have a default constructor. + * + * @param subclass The subclass type to register. + */ + @JvmStatic + fun registerSubclass(subclass: Class?) { + subclassingController.registerSubclass(subclass) + } + + /* package for tests */ + @JvmStatic + fun unregisterSubclass(subclass: Class?) { + subclassingController.unregisterSubclass(subclass) + } + + /** + * Adds a task to the queue for all of the given objects. + */ + fun enqueueForAll( + objects: List, + taskStart: Continuation?> + ): Task? { + // The task that will be complete when all of the child queues indicate they're ready to start. + val readyToStart = TaskCompletionSource() + + // First, we need to lock the mutex for the queue for every object. We have to hold this + // from at least when taskStart() is called to when obj.taskQueue enqueue is called, so + // that saves actually get executed in the order they were setup by taskStart(). + // The locks have to be sorted so that we always acquire them in the same order. + // Otherwise, there's some risk of deadlock. + val locks: MutableList = ArrayList(objects.size) + for (obj in objects) { + locks.add(obj.taskQueue.lock) + } + val lock = LockSet(locks) + lock.lock() + return try { + // The task produced by TaskStart + val fullTask: Task? = try { + // By running this immediately, we allow everything prior to toAwait to run before waiting + // for all of the queues on all of the objects. + taskStart.then(readyToStart.task) + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw RuntimeException(e) + } + + // Add fullTask to each of the objects' queues. + val childTasks: MutableList> = ArrayList() + for (obj in objects) { + obj.taskQueue.enqueue { task: Task -> + childTasks.add(task) + fullTask + } + } + + // When all of the objects' queues are ready, signal fullTask that it's ready to go on. + Task.whenAll(childTasks) + .continueWith({ + readyToStart.setResult(null) + null + }) + fullTask + } finally { + lock.unlock() + } + } + + /** + * Converts a `ParseObject.State` to a `ParseObject`. + * + * @param state The `ParseObject.State` to convert from. + * @return A `ParseObject` instance. + */ + @JvmStatic + internal fun from(state: State): T { + val `object` = createWithoutData(state.className(), state.objectId()) as T? + synchronized(`object`!!.mutex) { + val newState: State = if (state.isComplete) { + state + } else { + `object`.state.newBuilder>().apply(state) + .build() + } + `object`.state = newState + } + return `object` + } + + /** + * Creates a new `ParseObject` based on data from the Parse server. + * + * @param json The object's data. + * @param defaultClassName The className of the object, if none is in the JSON. + * @param decoder Delegate for knowing how to decode the values in the JSON. + * @param selectedKeys Set of keys selected when quering for this object. If none, the object is assumed to + * be complete, i.e. this is all the data for the object on the server. + */ + fun fromJSON( + json: JSONObject, defaultClassName: String?, + decoder: ParseDecoder, + selectedKeys: Set? + ): T { + if (selectedKeys != null && selectedKeys.isNotEmpty()) { + val keys = JSONArray(selectedKeys) + try { + json.put(KEY_SELECTED_KEYS, keys) + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + return fromJSON(json, defaultClassName, decoder)!! + } + + /** + * Creates a new `ParseObject` based on data from the Parse server. + * + * @param json The object's data. It is assumed to be complete, unless the JSON has the + * [.KEY_SELECTED_KEYS] key. + * @param defaultClassName The className of the object, if none is in the JSON. + * @param decoder Delegate for knowing how to decode the values in the JSON. + */ + fun fromJSON( + json: JSONObject, defaultClassName: String?, + decoder: ParseDecoder + ): T? { + val className = json.optString(KEY_CLASS_NAME, defaultClassName) + ?: return null + val objectId = json.optString(KEY_OBJECT_ID, null) + val isComplete = !json.has(KEY_SELECTED_KEYS) + val `object` = createWithoutData(className, objectId) as T? + val newState = + `object`!!.mergeFromServer(`object`.state, json, decoder, isComplete) + `object`.state = newState + return `object` + } + //region Getter/Setter helper methods + /** + * Method used by parse server webhooks implementation to convert raw JSON to Parse Object + * + * + * Method is used by parse server webhooks implementation to create a + * new `ParseObject` from the incoming json payload. The method is different from + * [.fromJSON] ()} in that it calls + * [.build] which populates operation queue + * rather then the server data from the incoming JSON, as at external server the incoming + * JSON may not represent the actual server data. Also it handles + * [ParseFieldOperations] separately. + * + * @param json The object's data. + * @param decoder Delegate for knowing how to decode the values in the JSON. + */ + @JvmStatic + fun fromJSONPayload( + json: JSONObject, decoder: ParseDecoder + ): T? { + val className = json.optString(KEY_CLASS_NAME) + if (className == null || ParseTextUtils.isEmpty(className)) { + return null + } + val objectId = json.optString(KEY_OBJECT_ID, null) + val `object` = createWithoutData(className, objectId) as T? + `object`!!.build(json, decoder) + return `object` + } + + /** + * This deletes all of the objects from the given List. + */ + private fun deleteAllAsync( + objects: List, sessionToken: String + ): Task? { + if (objects.isEmpty()) { + return Task.forResult(null) + } + + // Create a list of unique objects based on objectIds + val objectCount = objects.size + val uniqueObjects: MutableList = ArrayList(objectCount) + val idSet = HashSet() + for (i in 0 until objectCount) { + val obj: ParseObject = objects[i] + if (!idSet.contains(obj.objectId)) { + idSet.add(obj.objectId) + uniqueObjects.add(obj) + } + } + return enqueueForAll(uniqueObjects) { toAwait: Task -> + deleteAllAsync( + uniqueObjects, + sessionToken, + toAwait + ) + } + } + + private fun deleteAllAsync( + uniqueObjects: List, sessionToken: String, toAwait: Task + ): Task { + return toAwait.continueWithTask { + val objectCount = uniqueObjects.size + val states: MutableList = ArrayList(objectCount) + for (i in 0 until objectCount) { + val `object`: ParseObject = uniqueObjects[i] + `object`.validateDelete() + states.add(`object`.state) + } + val batchTasks = objectController.deleteAllAsync(states, sessionToken) + val tasks: MutableList> = ArrayList(objectCount) + for (i in 0 until objectCount) { + val batchTask = batchTasks[i] + val `object` = uniqueObjects[i] + tasks.add(batchTask.onSuccessTask { batchTask1: Task? -> + `object`.handleDeleteResultAsync() + .continueWithTask { batchTask1 } + }) + } + Task.whenAll(tasks) + } + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects The objects to delete. + * @throws ParseException Throws an exception if the server returns an error or is inaccessible. + */ + @Throws(ParseException::class) + fun deleteAll(objects: List) { + ParseTaskUtils.wait(deleteAllInBackground(objects)) + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects The objects to delete. + * @param callback The callback method to execute when completed. + */ + fun deleteAllInBackground(objects: List, callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(deleteAllInBackground(objects), callback) + } + + /** + * Deletes each object in the provided list. This is faster than deleting each object individually + * because it batches the requests. + * + * @param objects The objects to delete. + * @return A [Task] that is resolved when deleteAll completes. + */ + fun deleteAllInBackground(objects: List): Task { + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask { task: Task -> + val sessionToken = task.result + deleteAllAsync(objects, sessionToken) + } + } + /** + * Finds all of the objects that are reachable from child, including child itself, and adds them + * to the given mutable array. It traverses arrays and json objects. + * + * @param node An kind object to search for children. + * @param dirtyChildren The array to collect the `ParseObject`s into. + * @param dirtyFiles The array to collect the [ParseFile]s into. + * @param alreadySeen The set of all objects that have already been seen. + * @param alreadySeenNew The set of new objects that have already been seen since the last existing object. + */ + //endregion + /** + * Helper version of collectDirtyChildren so that callers don't have to add the internally used + * parameters. + */ + private fun collectDirtyChildren( + node: Any, + dirtyChildren: MutableCollection?, + dirtyFiles: MutableCollection?, + alreadySeen: MutableSet = + HashSet(), + alreadySeenNew: MutableSet = + HashSet() + ) { + object : ParseTraverser() { + override fun visit(node: Any): Boolean { + // If it's a file, then add it to the list if it's dirty. + if (node is ParseFile) { + if (dirtyFiles == null) { + return true + } + val file = node + if (file.url == null) { + dirtyFiles.add(file) + } + return true + } + + // If it's anything other than a file, then just continue; + if (node !is ParseObject) { + return true + } + if (dirtyChildren == null) { + return true + } + + // For files, we need to handle recursion manually to find cycles of new objects. + val `object` = node + var seen = alreadySeen + var seenNew = alreadySeenNew + + // Check for cycles of new objects. Any such cycle means it will be + // impossible to save this collection of objects, so throw an exception. + if (`object`.objectId != null) { + seenNew = HashSet() + } else { + if (seenNew.contains(`object`)) { + throw RuntimeException("Found a circular dependency while saving.") + } + seenNew = HashSet(seenNew) + seenNew.add(`object`) + } + + // Check for cycles of any object. If this occurs, then there's no + // problem, but we shouldn't recurse any deeper, because it would be + // an infinite recursion. + if (seen.contains(`object`)) { + return true + } + seen = HashSet(seen) + seen.add(`object`) + + // Recurse into this object's children looking for dirty children. + // We only need to look at the child object's current estimated data, + // because that's the only data that might need to be saved now. + collectDirtyChildren( + `object`.estimatedData, + dirtyChildren, + dirtyFiles, + seen, + seenNew + ) + if (`object`.isDirty(false)) { + dirtyChildren.add(`object`) + } + return true + } + }.setYieldRoot(true).traverse(node) + } + + /** + * This saves all of the objects and files reachable from the given object. It does its work in + * multiple waves, saving as many as possible in each wave. If there's ever an error, it just + * gives up, sets error, and returns NO. + */ + private fun deepSaveAsync(`object`: Any, sessionToken: String?): Task { + val objects: MutableSet = HashSet() + val files: MutableSet = HashSet() + collectDirtyChildren(`object`, objects, files) + + // This has to happen separately from everything else because ParseUser.save() is + // special-cased to work for lazy users, but new users can't be created by + // ParseMultiCommand's regular save. + val users: MutableSet = HashSet() + for (o in objects) { + if (o is ParseUser) { + if (o.isLazy) { + users.add(o) + } + } + } + objects.removeAll(users) + + // objects will need to wait for files to be complete since they may be nested children. + val filesComplete = AtomicBoolean(false) + var tasks: MutableList?> = ArrayList() + for (file in files) { + tasks.add(file.saveAsync(sessionToken, null, null)) + } + val filesTask = Task.whenAll(tasks).continueWith { task: Task? -> + filesComplete.set(true) + null + } + + // objects will need to wait for users to be complete since they may be nested children. + val usersComplete = AtomicBoolean(false) + tasks = ArrayList() + for (user in users) { + tasks.add(user.saveAsync(sessionToken)) + } + val usersTask = Task.whenAll(tasks).continueWith { task: Task? -> + usersComplete.set(true) + null + } + val remaining = Capture>(objects) + val objectsTask = Task.forResult(null).continueWhile( + { remaining.get().isNotEmpty() }) { + // Partition the objects into two sets: those that can be save immediately, + // and those that rely on other objects to be created first. + val current: MutableList = ArrayList() + val nextBatch: MutableSet = HashSet() + for (obj in remaining.get()) { + if (obj.canBeSerialized()) { + current.add(obj) + } else { + nextBatch.add(obj) + } + } + remaining.set(nextBatch) + if (current.size == 0 && filesComplete.get() && usersComplete.get()) { + // We do cycle-detection when building the list of objects passed to this function, so + // this should never get called. But we should check for it anyway, so that we get an + // exception instead of an infinite loop. + throw RuntimeException("Unable to save a ParseObject with a relation to a cycle.") + } + + // Package all save commands together + if (current.size == 0) { + return@continueWhile Task.forResult(null) + } + enqueueForAll(current) { toAwait: Task -> + saveAllAsync( + current, + sessionToken, + toAwait + ) + } + } + return Task.whenAll(Arrays.asList(filesTask, usersTask, objectsTask)) + } + + private fun saveAllAsync( + uniqueObjects: List, sessionToken: String?, toAwait: Task + ): Task { + return toAwait.continueWithTask { + val objectCount = uniqueObjects.size + val states: MutableList = ArrayList(objectCount) + val operationsList: MutableList = ArrayList(objectCount) + val decoders: MutableList = ArrayList(objectCount) + for (i in 0 until objectCount) { + val `object`: ParseObject = uniqueObjects[i] + `object`.updateBeforeSave() + `object`.validateSave() + states.add(`object`.state) + operationsList.add(`object`.startSave()) + val fetchedObjects = `object`.collectFetchedObjects() + decoders.add(KnownParseObjectDecoder(fetchedObjects)) + } + val batchTasks = objectController.saveAllAsync( + states, operationsList, sessionToken, decoders + ) + val tasks: MutableList> = ArrayList(objectCount) + for (i in 0 until objectCount) { + val batchTask = batchTasks[i] + val `object` = uniqueObjects[i] + val operations = operationsList[i] + tasks.add(batchTask.continueWithTask { batchTask1: Task -> + val result = batchTask1.result // will be null on failure + `object`.handleSaveResultAsync(result, operations) + .continueWithTask { task1: Task -> + if (task1.isFaulted || task1.isCancelled) { + return@continueWithTask task1 + } + batchTask1.makeVoid() + } + }) + } + Task.whenAll(tasks) + } + } + + /** + * Saves each object in the provided list. This is faster than saving each object individually + * because it batches the requests. + * + * @param objects The objects to save. + * @throws ParseException Throws an exception if the server returns an error or is inaccessible. + */ + @Throws(ParseException::class) + fun saveAll(objects: List) { + ParseTaskUtils.wait(saveAllInBackground(objects)) + } + + /** + * Saves each object in the provided list to the server in a background thread. This is preferable + * to using saveAll, unless your code is already running from a background thread. + * + * @param objects The objects to save. + * @param callback `callback.done(e)` is called when the save completes. + */ + fun saveAllInBackground(objects: List, callback: SaveCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(saveAllInBackground(objects), callback) + } + + /** + * Saves each object in the provided list to the server in a background thread. This is preferable + * to using saveAll, unless your code is already running from a background thread. + * + * @param objects The objects to save. + * @return A [Task] that is resolved when saveAll completes. + */ + fun saveAllInBackground(objects: List): Task { + return ParseUser.getCurrentUserAsync().onSuccessTask { task: Task -> + val current = task.result + ?: return@onSuccessTask Task.forResult(null) + if (!current.isLazy) { + return@onSuccessTask Task.forResult(current.sessionToken) + } + + // The current user is lazy/unresolved. If it is attached to any of the objects via ACL, + // we'll need to resolve/save it before proceeding. + for (`object` in objects) { + if (!`object`.isDataAvailable(KEY_ACL)) { + continue + } + val acl = `object`.getACL(false) ?: continue + val user = acl.unresolvedUser + if (user != null && user.isCurrentUser) { + // We only need to find one, since there's only one current user. + return@onSuccessTask user.saveAsync(null).onSuccess { task1: Task? -> + check(!acl.hasUnresolvedUser()) { + ("ACL has an unresolved ParseUser. " + + "Save or sign up before attempting to serialize the ACL.") + } + user.sessionToken + } + } + } + Task.forResult(null) + }.onSuccessTask { task: Task -> + val sessionToken = task.result + deepSaveAsync(objects, sessionToken) + } + } + + /** + * Fetches all the objects that don't have data in the provided list in the background. + * + * @param objects The list of objects to fetch. + * @return A [Task] that is resolved when fetchAllIfNeeded completes. + */ + fun fetchAllIfNeededInBackground( + objects: List + ): Task?> { + return fetchAllAsync(objects, true) + } + + /** + * Fetches all the objects that don't have data in the provided list. + * + * @param objects The list of objects to fetch. + * @return The list passed in for convenience. + * @throws ParseException Throws an exception if the server returns an error or is inaccessible. + */ + @Throws(ParseException::class) + fun fetchAllIfNeeded(objects: List): List { + return ParseTaskUtils.wait(fetchAllIfNeededInBackground(objects))!! + } + //region LDS-processing methods. + /** + * Fetches all the objects that don't have data in the provided list in the background. + * + * @param objects The list of objects to fetch. + * @param callback `callback.done(result, e)` is called when the fetch completes. + */ + fun fetchAllIfNeededInBackground( + objects: List, + callback: FindCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync( + fetchAllIfNeededInBackground(objects), + callback + ) + } + + private fun fetchAllAsync( + objects: List, onlyIfNeeded: Boolean + ): Task?> { + return ParseUser.getCurrentUserAsync() + .onSuccessTask { task: Task -> + val user = task.result + enqueueForAll( + objects + ) { task: Task -> + fetchAllAsync( + objects, + user, + onlyIfNeeded, + task + ) + } + } + } + + /** + * @param onlyIfNeeded If enabled, will only fetch if the object has an objectId and + * !isDataAvailable, otherwise it requires objectIds and will fetch regardless + * of data availability. + */ + // TODO(grantland): Convert to ParseUser.State + private fun fetchAllAsync( + objects: List, user: ParseUser?, onlyIfNeeded: Boolean, toAwait: Task + ): Task?> { + if (objects.isEmpty()) { + return Task.forResult(objects) + } + val objectIds: MutableList = ArrayList() + var className: String? = null + for (`object` in objects) { + if (onlyIfNeeded && `object`!!.isDataAvailable()) { + continue + } + require(!(className != null && `object`!!.className != className)) { "All objects should have the same class" } + className = `object`!!.className + val objectId = `object`.objectId + if (objectId != null) { + objectIds.add(`object`.objectId) + } else require(onlyIfNeeded) { "All objects must exist on the server" } + } + if (objectIds.size == 0) { + return Task.forResult(objects) + } + val query = ParseQuery.getQuery(className!!) + .whereContainedIn(KEY_OBJECT_ID, objectIds) + .setLimit(objectIds.size) + return toAwait.continueWithTask { + query.findAsync( + query.builder.build(), + user, + null + ) + } + .onSuccess { task: Task> -> + val resultMap: MutableMap = HashMap() + for (o in task.result) { + resultMap[o!!.objectId] = o + } + for (`object` in objects) { + if (onlyIfNeeded && `object`!!.isDataAvailable()) { + continue + } + val newObject = resultMap[`object`!!.objectId] + ?: throw ParseException( + ParseException.OBJECT_NOT_FOUND, + "Object id " + `object`.objectId + " does not exist" + ) + if (!Parse.isLocalDatastoreEnabled()) { + // We only need to merge if LDS is disabled, since single instance will do the merging + // for us. + `object`.mergeFromObject(newObject) + } + } + objects + } + } + //endregion + /** + * Fetches all the objects in the provided list in the background. + * + * @param objects The list of objects to fetch. + * @return A [Task] that is resolved when fetch completes. + */ + fun fetchAllInBackground(objects: List): Task?> { + return fetchAllAsync(objects, false) + } + + /** + * Fetches all the objects in the provided list. + * + * @param objects The list of objects to fetch. + * @return The list passed in. + * @throws ParseException Throws an exception if the server returns an error or is inaccessible. + */ + @Throws(ParseException::class) + fun fetchAll(objects: List): List { + return ParseTaskUtils.wait(fetchAllInBackground(objects))!! + } + + /** + * Fetches all the objects in the provided list in the background. + * + * @param objects The list of objects to fetch. + * @param callback `callback.done(result, e)` is called when the fetch completes. + */ + fun fetchAllInBackground( + objects: List, + callback: FindCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync(fetchAllInBackground(objects), callback) + } + + /** + * Registers the Parse-provided `ParseObject` subclasses. Do this here in a real method rather than + * as part of a static initializer because doing this in a static initializer can lead to + * deadlocks + */ + @JvmStatic + fun registerParseSubclasses() { + registerSubclass(ParseUser::class.java) + registerSubclass(ParseRole::class.java) + registerSubclass(ParseInstallation::class.java) + registerSubclass(ParseSession::class.java) + registerSubclass(ParsePin::class.java) + registerSubclass(EventuallyPin::class.java) + } + + @JvmStatic + fun unregisterParseSubclasses() { + unregisterSubclass(ParseUser::class.java) + unregisterSubclass(ParseRole::class.java) + unregisterSubclass(ParseInstallation::class.java) + unregisterSubclass(ParseSession::class.java) + unregisterSubclass(ParsePin::class.java) + unregisterSubclass(EventuallyPin::class.java) + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * + * @param name the name + * @param objects the objects to be pinned + * @param callback the callback + * @see .unpinAllInBackground + */ + fun pinAllInBackground( + name: String = DEFAULT_PIN, + objects: List, callback: SaveCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(name, objects), callback) + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * + * @param name the name + * @param objects the objects to be pinned + * @return A [Task] that is resolved when pinning all completes. + * @see .unpinAllInBackground + */ + fun pinAllInBackground( + name: String = DEFAULT_PIN, + objects: List + ): Task { + return pinAllInBackground(name, objects, true) + } + + private fun pinAllInBackground( + name: String?, + objects: List, includeAllChildren: Boolean + ): Task { + check(Parse.isLocalDatastoreEnabled()) { + "Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`." + } + var task = Task.forResult(null) + + // Resolve and persist unresolved users attached via ACL, similarly how we do in saveAsync + for (`object` in objects) { + task = task.onSuccessTask { + if (!`object`.isDataAvailable(KEY_ACL)) { + return@onSuccessTask Task.forResult(null) + } + val acl = `object`.getACL(false) + ?: return@onSuccessTask Task.forResult(null) + val user = acl.unresolvedUser + if (user == null || !user.isCurrentUser) { + return@onSuccessTask Task.forResult(null) + } + ParseUser.pinCurrentUserIfNeededAsync(user) + } + } + return task.onSuccessTask { + Parse.getLocalDatastore().pinAllObjectsAsync( + name ?: DEFAULT_PIN, + objects, + includeAllChildren + ) + }.onSuccessTask { task1: Task? -> + // Hack to emulate persisting current user on disk after a save like in ParseUser#saveAsync + // Note: This does not persist current user if it's a child object of `objects`, it probably + // should, but we can't unless we do something similar to #deepSaveAsync. + if (ParseCorePlugins.PIN_CURRENT_USER == name) { + return@onSuccessTask task1 + } + for (`object` in objects) { + if (`object` is ParseUser) { + val user = `object` as ParseUser + if (user.isCurrentUser) { + return@onSuccessTask ParseUser.pinCurrentUserIfNeededAsync(user) + } + } + } + task1 + } + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * [.fetchFromLocalDatastore] on it. + * + * @param name the name + * @param objects the objects to be pinned + * @throws ParseException exception if fails + * @see .unpinAll + */ + @Throws(ParseException::class) + fun pinAll( + name: String = DEFAULT_PIN, + objects: List + ) { + ParseTaskUtils.wait(pinAllInBackground(name, objects)) + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * + * @param objects the objects to be pinned + * @param callback the callback + * @see .unpinAllInBackground + * @see .DEFAULT_PIN + */ + fun pinAllInBackground( + objects: List, + callback: SaveCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync( + pinAllInBackground(DEFAULT_PIN, objects), + callback + ) + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * + * @param objects the objects to be pinned + * @return A [Task] that is resolved when pinning all completes. + * @see .unpinAllInBackground + * @see .DEFAULT_PIN + */ + fun pinAllInBackground(objects: List): Task { + return pinAllInBackground(DEFAULT_PIN, objects) + } + + /** + * Stores the objects and every object they point to in the local datastore, recursively. If + * those other objects have not been fetched from Parse, they will not be stored. However, if they + * have changed data, all of the changes will be retained. To get the objects back later, you can + * use [ParseQuery.fromLocalDatastore], or you can create an unfetched pointer with + * [.createWithoutData] and then call [.fetchFromLocalDatastore] on it. + * + * @param objects the objects to be pinned + * @throws ParseException exception if fails + * @see .unpinAll + * @see .DEFAULT_PIN + */ + @Throws(ParseException::class) + fun pinAll(objects: List) { + ParseTaskUtils.wait(pinAllInBackground(DEFAULT_PIN, objects)) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @param objects the objects + * @param callback the callback + * @see .pinAllInBackground + */ + fun unpinAllInBackground( + name: String = DEFAULT_PIN, objects: List?, + callback: DeleteCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name, objects), callback) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @param objects the objects + * @return A [Task] that is resolved when unpinning all completes. + * @see .pinAllInBackground + */ + fun unpinAllInBackground( + name: String = DEFAULT_PIN, + objects: List? + ): Task { + check(Parse.isLocalDatastoreEnabled()) { + "Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`." + } + return Parse.getLocalDatastore().unpinAllObjectsAsync(name, objects) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @param objects the objects + * @throws ParseException exception if fails + * @see .pinAll + */ + @Throws(ParseException::class) + fun unpinAll( + name: String = DEFAULT_PIN, + objects: List? + ) { + ParseTaskUtils.wait(unpinAllInBackground(name, objects)) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param objects the objects + * @param callback the callback + * @see .pinAllInBackground + * @see .DEFAULT_PIN + */ + fun unpinAllInBackground( + objects: List?, + callback: DeleteCallback? + ) { + ParseTaskUtils.callbackOnMainThreadAsync( + unpinAllInBackground(DEFAULT_PIN, objects), + callback + ) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param objects the objects + * @return A [Task] that is resolved when unpinning all completes. + * @see .pinAllInBackground + * @see .DEFAULT_PIN + */ + fun unpinAllInBackground(objects: List?): Task { + return unpinAllInBackground(DEFAULT_PIN, objects) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param objects the objects + * @throws ParseException exception if fails + * @see .pinAll + * @see .DEFAULT_PIN + */ + @Throws(ParseException::class) + fun unpinAll(objects: List?) { + ParseTaskUtils.wait(unpinAllInBackground(DEFAULT_PIN, objects)) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @param callback the callback + * @see .pinAll + */ + fun unpinAllInBackground(name: String?, callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name), callback) + } + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @return A [Task] that is resolved when unpinning all completes. + * @see .pinAll + */ + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @return A [Task] that is resolved when unpinning all completes. + * @see .pinAllInBackground + * @see .DEFAULT_PIN + */ + @JvmOverloads + fun unpinAllInBackground(name: String? = DEFAULT_PIN): Task { + check(Parse.isLocalDatastoreEnabled()) { + "Method requires Local Datastore. " + + "Please refer to `Parse#enableLocalDatastore(Context)`." + } + return Parse.getLocalDatastore().unpinAllObjectsAsync(name) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param name the name + * @throws ParseException exception if fails + * @see .pinAll + */ + @Throws(ParseException::class) + fun unpinAll(name: String?) { + ParseTaskUtils.wait(unpinAllInBackground(name)) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @param callback the callback + * @see .pinAllInBackground + * @see .DEFAULT_PIN + */ + fun unpinAllInBackground(callback: DeleteCallback?) { + ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(), callback) + } + + /** + * Removes the objects and every object they point to in the local datastore, recursively. + * + * @throws ParseException exception if fails + * @see .pinAll + * @see .DEFAULT_PIN + */ + @Throws(ParseException::class) + fun unpinAll() { + ParseTaskUtils.wait(unpinAllInBackground()) + } + + @JvmStatic + internal fun createFromParcel(source: Parcel, decoder: ParseParcelDecoder): ParseObject { + val className = source.readString() + val objectId = if (source.readByte().toInt() == 1) source.readString() else null + // Create empty object (might be the same instance if LDS is enabled) + // and pass to decoder before unparceling child objects in State + val `object` = createWithoutData(className, objectId) + if (decoder is ParseObjectParcelDecoder) { + decoder.addKnownObject(`object`) + } + val state = State.createFromParcel(source, decoder) + `object`!!.state = state + if (source.readByte().toInt() == 1) `object`.localId = source.readString() + if (source.readByte().toInt() == 1) `object`.isDeleted = true + // If object.ldsEnabledWhenParceling is true, we got this from OfflineStore. + // There is no need to restore operations in that case. + val restoreOperations = !`object`.ldsEnabledWhenParceling + val set = ParseOperationSet.fromParcel(source, decoder) + if (restoreOperations) { + for (key in set.keys) { + val op = set[key] + `object`.performOperation(key, op) // Update ops and estimatedData + } + } + val bundle = source.readBundle(ParseObject::class.java.classLoader) + `object`.onRestoreInstanceState(bundle) + return `object` + } + } + + /** + * Constructs a new `ParseObject` with no data in it. A `ParseObject` constructed in + * this way will not have an objectId and will not persist to the database until [.save] + * is called. + * + * + * Class names must be alphanumerical plus underscore, and start with a letter. It is recommended + * to name classes in `PascalCaseLikeThis`. + * + * @param theClassName The className for this `ParseObject`. + */ + init { + // We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the + // right thing with subclasses. It's ugly and terrible, but it does provide the development + // experience we generally want, so... yeah. Sorry to whomever has to deal with this in the + // future. I pinky-swear we won't make a habit of this -- you believe me, don't you? + var theClassName = theClassName + val objectIdForPointer = isCreatingPointerForObjectId.get() + requireNotNull(theClassName) { "You must specify a Parse class name when creating a new ParseObject." } + if (AUTO_CLASS_NAME == theClassName) { + theClassName = subclassingController.getClassName(javaClass) + } + + // If this is supposed to be created by a factory but wasn't, throw an exception. + require( + subclassingController.isSubclassValid( + theClassName, + javaClass + ) + ) { "You must create this type of ParseObject using ParseObject.create() or the proper subclass." } + operationSetQueue = LinkedList() + operationSetQueue.add(ParseOperationSet()) + estimatedData = HashMap() + val builder = newStateBuilder(theClassName) + // When called from new, assume hasData for the whole object is true. + if (objectIdForPointer == null) { + setDefaultValues() + builder.isComplete(true) + } else { + if (objectIdForPointer != NEW_OFFLINE_OBJECT_ID_PLACEHOLDER) { + builder.objectId(objectIdForPointer) + } + builder.isComplete(false) + } + // This is a new untouched object, we don't need cache rebuilding, etc. + state = builder.build() + val store = Parse.getLocalDatastore() + store?.registerNewObject(this) + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseObjectCoder.java b/parse/src/main/java/com/parse/ParseObjectCoder.java deleted file mode 100644 index 1e973e80c..000000000 --- a/parse/src/main/java/com/parse/ParseObjectCoder.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.util.Iterator; - -/** - * Handles encoding/decoding ParseObjects to/from REST JSON. - */ -class ParseObjectCoder { - - private static final String KEY_OBJECT_ID = "objectId"; - private static final String KEY_CLASS_NAME = "className"; - private static final String KEY_ACL = "ACL"; - private static final String KEY_CREATED_AT = "createdAt"; - private static final String KEY_UPDATED_AT = "updatedAt"; - - private static final ParseObjectCoder INSTANCE = new ParseObjectCoder(); - - /* package */ ParseObjectCoder() { - // do nothing - } - - public static ParseObjectCoder get() { - return INSTANCE; - } - - /** - * Converts a {@code ParseObject.State} to REST JSON for saving. - *

- * Only dirty keys from {@code operations} are represented in the data. Non-dirty keys such as - * {@code updatedAt}, {@code createdAt}, etc. are not included. - * - * @param state {@link ParseObject.State} of the type of {@link ParseObject} that will be returned. - * Properties are completely ignored. - * @param operations Dirty operations that are to be saved. - * @param encoder Encoder instance that will be used to encode the request. - * @return A REST formatted {@link JSONObject} that will be used for saving. - */ - public JSONObject encode( - T state, ParseOperationSet operations, ParseEncoder encoder) { - JSONObject objectJSON = new JSONObject(); - - try { - // Serialize the data - for (String key : operations.keySet()) { - ParseFieldOperation operation = operations.get(key); - objectJSON.put(key, encoder.encode(operation)); - - // TODO(grantland): Use cached value from hashedObjects if it's a set operation. - } - - if (state.objectId() != null) { - objectJSON.put(KEY_OBJECT_ID, state.objectId()); - } - } catch (JSONException e) { - throw new RuntimeException("could not serialize object to JSON"); - } - - return objectJSON; - } - - /** - * Converts REST JSON response to {@link ParseObject.State.Init}. - *

- * This returns Builder instead of a State since we'll probably want to set some additional - * properties on it after decoding such as {@link ParseObject.State.Init#isComplete()}, etc. - * - * @param builder A {@link ParseObject.State.Init} instance that will have the server JSON applied - * (mutated) to it. This will generally be a instance created by clearing a mutable - * copy of a {@link ParseObject.State} to ensure it's an instance of the correct - * subclass: {@code state.newBuilder().clear()} - * @param json JSON response in REST format from the server. - * @param decoder Decoder instance that will be used to decode the server response. - * @return The same Builder instance passed in after the JSON is applied. - */ - public > T decode( - T builder, JSONObject json, ParseDecoder decoder) { - try { - Iterator keys = json.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - /* - __type: Returned by queries and cloud functions to designate body is a ParseObject - __className: Used by fromJSON, should be stripped out by the time it gets here... - */ - if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) { - continue; - } - if (key.equals(KEY_OBJECT_ID)) { - String newObjectId = json.getString(key); - builder.objectId(newObjectId); - continue; - } - if (key.equals(KEY_CREATED_AT)) { - builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - if (key.equals(KEY_UPDATED_AT)) { - builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))); - continue; - } - if (key.equals(KEY_ACL)) { - ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder); - builder.put(KEY_ACL, acl); - continue; - } - - Object value = json.get(key); - Object decodedObject = decoder.decode(value); - builder.put(key, decodedObject); - } - - return builder; - } catch (JSONException e) { - throw new RuntimeException(e); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseObjectCoder.kt b/parse/src/main/java/com/parse/ParseObjectCoder.kt new file mode 100644 index 000000000..9d1211a3f --- /dev/null +++ b/parse/src/main/java/com/parse/ParseObjectCoder.kt @@ -0,0 +1,124 @@ +/* + * 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 + +/** + * Handles encoding/decoding ParseObjects to/from REST JSON. + */ +internal open class ParseObjectCoder /* package */ { + /** + * Converts a `ParseObject.State` to REST JSON for saving. + * + * + * Only dirty keys from `operations` are represented in the data. Non-dirty keys such as + * `updatedAt`, `createdAt`, etc. are not included. + * + * @param state [ParseObject.State] of the type of [ParseObject] that will be returned. + * Properties are completely ignored. + * @param operations Dirty operations that are to be saved. + * @param encoder Encoder instance that will be used to encode the request. + * @return A REST formatted [JSONObject] that will be used for saving. + */ + open fun encode( + state: T, operations: ParseOperationSet?, encoder: ParseEncoder + ): JSONObject? { + val objectJSON = JSONObject() + try { + // Serialize the data + operations?.let { + for (key in it.keys) { + val operation = it[key] + objectJSON.put(key, encoder.encode(operation)) + + // TODO(grantland): Use cached value from hashedObjects if it's a set operation. + } + } + + if (state.objectId() != null) { + objectJSON.put(KEY_OBJECT_ID, state.objectId()) + } + } catch (e: JSONException) { + throw RuntimeException("could not serialize object to JSON") + } + return objectJSON + } + + /** + * Converts REST JSON response to [ParseObject.State.Init]. + * + * + * This returns Builder instead of a State since we'll probably want to set some additional + * properties on it after decoding such as [ParseObject.State.Init.isComplete], etc. + * + * @param builder A [ParseObject.State.Init] instance that will have the server JSON applied + * (mutated) to it. This will generally be a instance created by clearing a mutable + * copy of a [ParseObject.State] to ensure it's an instance of the correct + * subclass: `state.newBuilder().clear()` + * @param json JSON response in REST format from the server. + * @param decoder Decoder instance that will be used to decode the server response. + * @return The same Builder instance passed in after the JSON is applied. + */ + open fun > decode( + builder: T, json: JSONObject, decoder: ParseDecoder + ): T { + return try { + val keys: Iterator<*> = json.keys() + while (keys.hasNext()) { + val key = keys.next() as String + /* + __type: Returned by queries and cloud functions to designate body is a ParseObject + __className: Used by fromJSON, should be stripped out by the time it gets here... + */ + if (key == "__type" || key == KEY_CLASS_NAME) { + continue + } + if (key == KEY_OBJECT_ID) { + val newObjectId = json.getString(key) + builder.objectId(newObjectId) + continue + } + if (key == KEY_CREATED_AT) { + builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + if (key == KEY_UPDATED_AT) { + builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key))) + continue + } + if (key == KEY_ACL) { + val acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder) + builder.put(KEY_ACL, acl) + continue + } + val value = json[key] + val decodedObject = decoder.decode(value) + builder.put(key, decodedObject) + } + builder + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + companion object { + private const val KEY_OBJECT_ID = "objectId" + private const val KEY_CLASS_NAME = "className" + private const val KEY_ACL = "ACL" + private const val KEY_CREATED_AT = "createdAt" + private const val KEY_UPDATED_AT = "updatedAt" + private val INSTANCE = ParseObjectCoder() + @JvmStatic + fun get(): ParseObjectCoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseObjectController.java b/parse/src/main/java/com/parse/ParseObjectController.java index 9a2204bff..655070d80 100644 --- a/parse/src/main/java/com/parse/ParseObjectController.java +++ b/parse/src/main/java/com/parse/ParseObjectController.java @@ -8,10 +8,10 @@ */ package com.parse; -import java.util.List; - import com.parse.boltsinternal.Task; +import java.util.List; + interface ParseObjectController { Task fetchAsync( diff --git a/parse/src/main/java/com/parse/ParseObjectCurrentCoder.java b/parse/src/main/java/com/parse/ParseObjectCurrentCoder.java deleted file mode 100644 index 2a3c8ace2..000000000 --- a/parse/src/main/java/com/parse/ParseObjectCurrentCoder.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * 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.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Date; -import java.util.Iterator; - -/** - * Handles encoding/decoding ParseObjects to/from /2 format JSON. /2 format json is only used for - * persisting current ParseObject(currentInstallation, currentUser) to disk when LDS is not enabled. - */ -class ParseObjectCurrentCoder extends ParseObjectCoder { - - /* - /2 format JSON Keys - */ - private static final String KEY_OBJECT_ID = "objectId"; - private static final String KEY_CLASS_NAME = "classname"; - private static final String KEY_CREATED_AT = "createdAt"; - private static final String KEY_UPDATED_AT = "updatedAt"; - private static final String KEY_ACL = "ACL"; - private static final String KEY_DATA = "data"; - - /* - Old serialized JSON keys - */ - private static final String KEY_OLD_OBJECT_ID = "id"; - private static final String KEY_OLD_CREATED_AT = "created_at"; - private static final String KEY_OLD_UPDATED_AT = "updated_at"; - private static final String KEY_OLD_POINTERS = "pointers"; - - private static final ParseObjectCurrentCoder INSTANCE = - new ParseObjectCurrentCoder(); - - /* package */ ParseObjectCurrentCoder() { - // do nothing - } - - public static ParseObjectCurrentCoder get() { - return INSTANCE; - } - - /** - * Converts a {@code ParseObject} to /2/ JSON representation suitable for saving to disk. - *

- *

-     * {
-     *   data: {
-     *     // data fields, including objectId, createdAt, updatedAt
-     *   },
-     *   classname: class name for the object,
-     *   operations: { } // operations per field
-     * }
-     * 
- *

- * All keys are included, regardless of whether they are dirty. - * - * @see #decode(ParseObject.State.Init, JSONObject, ParseDecoder) - */ - @Override - public JSONObject encode( - T state, ParseOperationSet operations, ParseEncoder encoder) { - if (operations != null) { - throw new IllegalArgumentException("Parameter ParseOperationSet is not null"); - } - - // Public data goes in dataJSON; special fields go in objectJSON. - JSONObject objectJSON = new JSONObject(); - JSONObject dataJSON = new JSONObject(); - - try { - // Serialize the data - for (String key : state.keySet()) { - Object object = state.get(key); - dataJSON.put(key, encoder.encode(object)); - - // TODO(grantland): Use cached value from hashedObjects, but only if we're not dirty. - } - - if (state.createdAt() > 0) { - dataJSON.put(KEY_CREATED_AT, - ParseDateFormat.getInstance().format(new Date(state.createdAt()))); - } - if (state.updatedAt() > 0) { - dataJSON.put(KEY_UPDATED_AT, - ParseDateFormat.getInstance().format(new Date(state.updatedAt()))); - } - if (state.objectId() != null) { - dataJSON.put(KEY_OBJECT_ID, state.objectId()); - } - - objectJSON.put(KEY_DATA, dataJSON); - objectJSON.put(KEY_CLASS_NAME, state.className()); - } catch (JSONException e) { - throw new RuntimeException("could not serialize object to JSON"); - } - - return objectJSON; - } - - /** - * Decodes from /2/ JSON. - *

- * This is only used to read ParseObjects stored on disk in JSON. - * - * @see #encode(ParseObject.State, ParseOperationSet, ParseEncoder) - */ - @Override - public > T decode( - T builder, JSONObject json, ParseDecoder decoder) { - try { - // The handlers for id, created_at, updated_at, and pointers are for - // backward compatibility with old serialized users. - if (json.has(KEY_OLD_OBJECT_ID)) { - String newObjectId = json.getString(KEY_OLD_OBJECT_ID); - builder.objectId(newObjectId); - } - if (json.has(KEY_OLD_CREATED_AT)) { - String createdAtString = - json.getString(KEY_OLD_CREATED_AT); - if (createdAtString != null) { - builder.createdAt(ParseImpreciseDateFormat.getInstance().parse(createdAtString)); - } - } - if (json.has(KEY_OLD_UPDATED_AT)) { - String updatedAtString = - json.getString(KEY_OLD_UPDATED_AT); - if (updatedAtString != null) { - builder.updatedAt(ParseImpreciseDateFormat.getInstance().parse(updatedAtString)); - } - } - if (json.has(KEY_OLD_POINTERS)) { - JSONObject newPointers = - json.getJSONObject(KEY_OLD_POINTERS); - Iterator keys = newPointers.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - JSONArray pointerArray = newPointers.getJSONArray(key); - builder.put(key, ParseObject.createWithoutData(pointerArray.optString(0), - pointerArray.optString(1))); - } - } - - JSONObject data = json.optJSONObject(KEY_DATA); - if (data != null) { - Iterator keys = data.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); - switch (key) { - case KEY_OBJECT_ID: - String newObjectId = data.getString(key); - builder.objectId(newObjectId); - break; - case KEY_CREATED_AT: - builder.createdAt(ParseDateFormat.getInstance().parse(data.getString(key))); - break; - case KEY_UPDATED_AT: - builder.updatedAt(ParseDateFormat.getInstance().parse(data.getString(key))); - break; - case KEY_ACL: - ParseACL acl = ParseACL.createACLFromJSONObject(data.getJSONObject(key), decoder); - builder.put(KEY_ACL, acl); - break; - default: - Object value = data.get(key); - Object decodedObject = decoder.decode(value); - builder.put(key, decodedObject); - } - } - } - return builder; - } catch (JSONException e) { - throw new RuntimeException(e); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseObjectCurrentCoder.kt b/parse/src/main/java/com/parse/ParseObjectCurrentCoder.kt new file mode 100644 index 000000000..927a3e822 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseObjectCurrentCoder.kt @@ -0,0 +1,180 @@ +/* + * 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.util.* + +/** + * Handles encoding/decoding ParseObjects to/from /2 format JSON. /2 format json is only used for + * persisting current ParseObject(currentInstallation, currentUser) to disk when LDS is not enabled. + */ +internal open class ParseObjectCurrentCoder /* package */ + : ParseObjectCoder() { + /** + * Converts a `ParseObject` to /2/ JSON representation suitable for saving to disk. + * + * + *

+     * {
+     * data: {
+     * // data fields, including objectId, createdAt, updatedAt
+     * },
+     * classname: class name for the object,
+     * operations: { } // operations per field
+     * }
+    
* + * + * + * All keys are included, regardless of whether they are dirty. + * + * @see .decode + */ + override fun encode( + state: T, operations: ParseOperationSet?, encoder: ParseEncoder + ): JSONObject? { + // Public data goes in dataJSON; special fields go in objectJSON. + val objectJSON = JSONObject() + val dataJSON = JSONObject() + try { + // Serialize the data + for (key in state.keySet()) { + val `object` = state[key] + dataJSON.put(key, encoder.encode(`object`)) + + // TODO(grantland): Use cached value from hashedObjects, but only if we're not dirty. + } + if (state.createdAt() > 0) { + dataJSON.put( + KEY_CREATED_AT, + ParseDateFormat.getInstance().format(Date(state.createdAt())) + ) + } + if (state.updatedAt() > 0) { + dataJSON.put( + KEY_UPDATED_AT, + ParseDateFormat.getInstance().format(Date(state.updatedAt())) + ) + } + if (state.objectId() != null) { + dataJSON.put(KEY_OBJECT_ID, state.objectId()) + } + objectJSON.put(KEY_DATA, dataJSON) + objectJSON.put(KEY_CLASS_NAME, state.className()) + } catch (e: JSONException) { + throw RuntimeException("could not serialize object to JSON") + } + return objectJSON + } + + /** + * Decodes from /2/ JSON. + * + * + * This is only used to read ParseObjects stored on disk in JSON. + * + * @see .encode + */ + override fun > decode( + builder: T, json: JSONObject, decoder: ParseDecoder + ): T { + return try { + // The handlers for id, created_at, updated_at, and pointers are for + // backward compatibility with old serialized users. + if (json.has(KEY_OLD_OBJECT_ID)) { + val newObjectId = json.getString(KEY_OLD_OBJECT_ID) + builder.objectId(newObjectId) + } + if (json.has(KEY_OLD_CREATED_AT)) { + val createdAtString = json.getString(KEY_OLD_CREATED_AT) + builder.createdAt( + ParseImpreciseDateFormat.getInstance().parse(createdAtString) + ) + } + if (json.has(KEY_OLD_UPDATED_AT)) { + val updatedAtString = json.getString(KEY_OLD_UPDATED_AT) + builder.updatedAt( + ParseImpreciseDateFormat.getInstance().parse(updatedAtString) + ) + } + if (json.has(KEY_OLD_POINTERS)) { + val newPointers = json.getJSONObject(KEY_OLD_POINTERS) + val keys: Iterator<*> = newPointers.keys() + while (keys.hasNext()) { + val key = keys.next() as String + val pointerArray = newPointers.getJSONArray(key) + builder.put( + key, ParseObject.createWithoutData( + pointerArray.optString(0), + pointerArray.optString(1) + ) + ) + } + } + val data = json.optJSONObject(KEY_DATA) + if (data != null) { + val keys: Iterator<*> = data.keys() + while (keys.hasNext()) { + val key = keys.next() as String + when (key) { + KEY_OBJECT_ID -> { + val newObjectId = data.getString(key) + builder.objectId(newObjectId) + } + KEY_CREATED_AT -> builder.createdAt( + ParseDateFormat.getInstance().parse(data.getString(key)) + ) + KEY_UPDATED_AT -> builder.updatedAt( + ParseDateFormat.getInstance().parse(data.getString(key)) + ) + KEY_ACL -> { + val acl = + ParseACL.createACLFromJSONObject(data.getJSONObject(key), decoder) + builder.put(KEY_ACL, acl) + } + else -> { + val value = data[key] + val decodedObject = decoder.decode(value) + builder.put(key, decodedObject) + } + } + } + } + builder + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + companion object { + /* + /2 format JSON Keys + */ + private const val KEY_OBJECT_ID = "objectId" + private const val KEY_CLASS_NAME = "classname" + private const val KEY_CREATED_AT = "createdAt" + private const val KEY_UPDATED_AT = "updatedAt" + private const val KEY_ACL = "ACL" + private const val KEY_DATA = "data" + + /* + Old serialized JSON keys + */ + private const val KEY_OLD_OBJECT_ID = "id" + private const val KEY_OLD_CREATED_AT = "created_at" + private const val KEY_OLD_UPDATED_AT = "updated_at" + private const val KEY_OLD_POINTERS = "pointers" + private val INSTANCE = ParseObjectCurrentCoder() + @JvmStatic + fun get(): ParseObjectCurrentCoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseObjectParcelDecoder.java b/parse/src/main/java/com/parse/ParseObjectParcelDecoder.java index c9a16caf7..09c589458 100644 --- a/parse/src/main/java/com/parse/ParseObjectParcelDecoder.java +++ b/parse/src/main/java/com/parse/ParseObjectParcelDecoder.java @@ -14,7 +14,7 @@ */ /* package */ class ParseObjectParcelDecoder extends ParseParcelDecoder { - private Map objects = new HashMap<>(); + private final Map objects = new HashMap<>(); public ParseObjectParcelDecoder() { } diff --git a/parse/src/main/java/com/parse/ParseObjectParcelEncoder.java b/parse/src/main/java/com/parse/ParseObjectParcelEncoder.java index 4d3634c0a..de9a408f7 100644 --- a/parse/src/main/java/com/parse/ParseObjectParcelEncoder.java +++ b/parse/src/main/java/com/parse/ParseObjectParcelEncoder.java @@ -13,7 +13,7 @@ */ /* package */ class ParseObjectParcelEncoder extends ParseParcelEncoder { - private Set ids = new HashSet<>(); + private final Set ids = new HashSet<>(); public ParseObjectParcelEncoder() { } diff --git a/parse/src/main/java/com/parse/ParseObjectStore.java b/parse/src/main/java/com/parse/ParseObjectStore.kt similarity index 55% rename from parse/src/main/java/com/parse/ParseObjectStore.java rename to parse/src/main/java/com/parse/ParseObjectStore.kt index 38f1e72f7..7ef1ab94c 100644 --- a/parse/src/main/java/com/parse/ParseObjectStore.java +++ b/parse/src/main/java/com/parse/ParseObjectStore.kt @@ -6,17 +6,13 @@ * 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; +package com.parse -import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.Task -interface ParseObjectStore { - - Task getAsync(); - - Task setAsync(T object); - - Task existsAsync(); - - Task deleteAsync(); -} +internal interface ParseObjectStore { + val getAsync: Task + fun setAsync(`object`: T): Task + fun existsAsync(): Task + fun deleteAsync(): Task +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseOperationSet.java b/parse/src/main/java/com/parse/ParseOperationSet.java deleted file mode 100644 index f61737c4a..000000000 --- a/parse/src/main/java/com/parse/ParseOperationSet.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 android.os.Parcel; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.UUID; - -/** - * A set of field-level operations that can be performed on an object, corresponding to one command. - * For example, all of the data for a single call to save() will be packaged here. It is assumed - * that the ParseObject that owns the operations handles thread-safety. - */ -class ParseOperationSet extends HashMap { - private static final long serialVersionUID = 1L; - - private static final String REST_KEY_IS_SAVE_EVENTUALLY = "__isSaveEventually"; - private static final String REST_KEY_UUID = "__uuid"; - - // A unique id for this operation set. - private final String uuid; - - // Does this set correspond to a call to saveEventually? - private boolean isSaveEventually = false; - - /** - * Creates a new operation set with a random UUID. - */ - public ParseOperationSet() { - this(UUID.randomUUID().toString()); - } - - public ParseOperationSet(ParseOperationSet operations) { - super(operations); - uuid = operations.getUUID(); - isSaveEventually = operations.isSaveEventually; - } - - /** - * Creates a new operation set with the given UUID. - */ - private ParseOperationSet(String uuid) { - this.uuid = uuid; - } - - /** - * The inverse of toRest. Creates a new OperationSet from the given JSON. - */ - public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) - throws JSONException { - // Copy the json object to avoid making changes to the old object - Iterator keysIter = json.keys(); - String[] keys = new String[json.length()]; - int index = 0; - while (keysIter.hasNext()) { - String key = keysIter.next(); - keys[index++] = key; - } - - JSONObject jsonCopy = new JSONObject(json, keys); - String uuid = (String) jsonCopy.remove(REST_KEY_UUID); - ParseOperationSet operationSet = - (uuid == null ? new ParseOperationSet() : new ParseOperationSet(uuid)); - - boolean isSaveEventually = jsonCopy.optBoolean(REST_KEY_IS_SAVE_EVENTUALLY); - jsonCopy.remove(REST_KEY_IS_SAVE_EVENTUALLY); - operationSet.setIsSaveEventually(isSaveEventually); - - Iterator opKeys = jsonCopy.keys(); - while (opKeys.hasNext()) { - String opKey = (String) opKeys.next(); - Object value = decoder.decode(jsonCopy.get(opKey)); - ParseFieldOperation fieldOp; - if (opKey.equals("ACL")) { - value = ParseACL.createACLFromJSONObject(jsonCopy.getJSONObject(opKey), decoder); - } - if (value instanceof ParseFieldOperation) { - fieldOp = (ParseFieldOperation) value; - } else { - fieldOp = new ParseSetOperation(value); - } - operationSet.put(opKey, fieldOp); - } - - return operationSet; - } - - /* package */ - static ParseOperationSet fromParcel(Parcel source, ParseParcelDecoder decoder) { - ParseOperationSet set = new ParseOperationSet(source.readString()); - set.setIsSaveEventually(source.readByte() == 1); - int size = source.readInt(); - for (int i = 0; i < size; i++) { - String key = source.readString(); - ParseFieldOperation op = (ParseFieldOperation) decoder.decode(source); - set.put(key, op); - } - return set; - } - - public String getUUID() { - return uuid; - } - - public void setIsSaveEventually(boolean value) { - isSaveEventually = value; - } - - public boolean isSaveEventually() { - return isSaveEventually; - } - - /** - * Merges the changes from the given operation set into this one. Most typically, this is what - * happens when a save fails and changes need to be rolled into the next save. - */ - public void mergeFrom(ParseOperationSet other) { - for (String key : other.keySet()) { - ParseFieldOperation operation1 = other.get(key); - ParseFieldOperation operation2 = get(key); - if (operation2 != null) { - operation2 = operation2.mergeWithPrevious(operation1); - } else { - operation2 = operation1; - } - put(key, operation2); - } - } - - /** - * Converts this operation set into its REST format for serializing to LDS. - */ - public JSONObject toRest(ParseEncoder objectEncoder) throws JSONException { - JSONObject operationSetJSON = new JSONObject(); - for (String key : keySet()) { - ParseFieldOperation op = get(key); - operationSetJSON.put(key, op.encode(objectEncoder)); - } - - operationSetJSON.put(REST_KEY_UUID, uuid); - if (isSaveEventually) { - operationSetJSON.put(REST_KEY_IS_SAVE_EVENTUALLY, true); - } - return operationSetJSON; - } - - /** - * Parcels this operation set into a Parcel with the given encoder. - */ - /* package */ void toParcel(Parcel dest, ParseParcelEncoder encoder) { - dest.writeString(uuid); - dest.writeByte(isSaveEventually ? (byte) 1 : 0); - dest.writeInt(size()); - for (String key : keySet()) { - dest.writeString(key); - encoder.encode(get(key), dest); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseOperationSet.kt b/parse/src/main/java/com/parse/ParseOperationSet.kt new file mode 100644 index 000000000..808b6450e --- /dev/null +++ b/parse/src/main/java/com/parse/ParseOperationSet.kt @@ -0,0 +1,148 @@ +/* + * 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 android.os.Parcel +import org.json.JSONException +import org.json.JSONObject +import java.util.* + +/** + * A set of field-level operations that can be performed on an object, corresponding to one command. + * For example, all of the data for a single call to save() will be packaged here. It is assumed + * that the ParseObject that owns the operations handles thread-safety. + */ +class ParseOperationSet : HashMap { + // A unique id for this operation set. + val uuid: String + + // Does this set correspond to a call to saveEventually? + var isSaveEventually = false + + /** + * Creates a new operation set with a random UUID. + */ + constructor() : this(UUID.randomUUID().toString()) + constructor(operations: ParseOperationSet) : super(operations) { + uuid = operations.uuid + isSaveEventually = operations.isSaveEventually + } + + /** + * Creates a new operation set with the given UUID. + */ + private constructor(uuid: String) { + this.uuid = uuid + } + + /** + * Merges the changes from the given operation set into this one. Most typically, this is what + * happens when a save fails and changes need to be rolled into the next save. + */ + fun mergeFrom(other: ParseOperationSet) { + for (key in other.keys) { + val operation1 = other[key] + var operation2 = get(key) + operation2 = if (operation2 != null) { + operation2.mergeWithPrevious(operation1) + } else { + operation1 + } + if (operation2 != null) { + put(key, operation2) + } + } + } + + /** + * Converts this operation set into its REST format for serializing to LDS. + */ + @Throws(JSONException::class) + fun toRest(objectEncoder: ParseEncoder?): JSONObject { + val operationSetJSON = JSONObject() + for (key in keys) { + val op = get(key) + operationSetJSON.put(key, op!!.encode(objectEncoder)) + } + operationSetJSON.put(REST_KEY_UUID, uuid) + if (isSaveEventually) { + operationSetJSON.put(REST_KEY_IS_SAVE_EVENTUALLY, true) + } + return operationSetJSON + } + + /** + * Parcels this operation set into a Parcel with the given encoder. + */ + /* package */ + fun toParcel(dest: Parcel, encoder: ParseParcelEncoder) { + dest.writeString(uuid) + dest.writeByte(if (isSaveEventually) 1.toByte() else 0) + dest.writeInt(size) + for (key in keys) { + dest.writeString(key) + encoder.encode(get(key), dest) + } + } + + companion object { + private const val serialVersionUID = 1L + private const val REST_KEY_IS_SAVE_EVENTUALLY = "__isSaveEventually" + private const val REST_KEY_UUID = "__uuid" + + /** + * The inverse of toRest. Creates a new OperationSet from the given JSON. + */ + @Throws(JSONException::class) + fun fromRest(json: JSONObject, decoder: ParseDecoder): ParseOperationSet { + // Copy the json object to avoid making changes to the old object + val keysIter = json.keys() + val keys = arrayOfNulls(json.length()) + var index = 0 + while (keysIter.hasNext()) { + val key = keysIter.next() + keys[index++] = key + } + val jsonCopy = JSONObject(json, keys) + val uuid = jsonCopy.remove(REST_KEY_UUID) as String + val operationSet = ParseOperationSet(uuid) + val isSaveEventually = jsonCopy.optBoolean(REST_KEY_IS_SAVE_EVENTUALLY) + jsonCopy.remove(REST_KEY_IS_SAVE_EVENTUALLY) + operationSet.isSaveEventually = isSaveEventually + val opKeys: Iterator<*> = jsonCopy.keys() + while (opKeys.hasNext()) { + val opKey = opKeys.next() as String + var value = decoder.decode(jsonCopy[opKey]) + if (opKey == "ACL") { + value = ParseACL.createACLFromJSONObject(jsonCopy.getJSONObject(opKey), decoder) + } + val fieldOp: ParseFieldOperation = if (value is ParseFieldOperation) { + value + } else { + ParseSetOperation(value) + } + operationSet[opKey] = fieldOp + } + return operationSet + } + + /* package */ + fun fromParcel(source: Parcel, decoder: ParseParcelDecoder): ParseOperationSet { + val set = ParseOperationSet(source.readString()!!) + set.isSaveEventually = source.readByte().toInt() == 1 + val size = source.readInt() + for (i in 0 until size) { + val key = source.readString()!! + val op = decoder.decode(source) as ParseFieldOperation + set[key] = op + } + return set + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseParcelDecoder.java b/parse/src/main/java/com/parse/ParseParcelDecoder.java deleted file mode 100644 index dedbd870b..000000000 --- a/parse/src/main/java/com/parse/ParseParcelDecoder.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 android.os.Parcel; - -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * A {@code ParseParcelableDecoder} can be used to unparcel objects such as - * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. - *

- * This is capable of decoding objects and pointers to them. - * However, for improved behavior in the case of {@link ParseObject}s, use the stateful - * implementation {@link ParseObjectParcelDecoder}. - * - * @see ParseParcelEncoder - * @see ParseObjectParcelDecoder - */ -/* package */ class ParseParcelDecoder { - - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final ParseParcelDecoder INSTANCE = new ParseParcelDecoder(); - - public static ParseParcelDecoder get() { - return INSTANCE; - } - - public Object decode(Parcel source) { - String type = source.readString(); - switch (type) { - - case ParseParcelEncoder.TYPE_OBJECT: - return decodeParseObject(source); - - case ParseParcelEncoder.TYPE_POINTER: - return decodePointer(source); - - case ParseParcelEncoder.TYPE_DATE: - String iso = source.readString(); - return ParseDateFormat.getInstance().parse(iso); - - case ParseParcelEncoder.TYPE_BYTES: - byte[] bytes = new byte[source.readInt()]; - source.readByteArray(bytes); - return bytes; - - case ParseParcelEncoder.TYPE_OP: - return ParseFieldOperations.decode(source, this); - - case ParseParcelEncoder.TYPE_FILE: - return new ParseFile(source, this); - - case ParseParcelEncoder.TYPE_GEOPOINT: - return new ParseGeoPoint(source, this); - - case ParseParcelEncoder.TYPE_POLYGON: - return new ParsePolygon(source, this); - - case ParseParcelEncoder.TYPE_ACL: - return new ParseACL(source, this); - - case ParseParcelEncoder.TYPE_RELATION: - return new ParseRelation<>(source, this); - - case ParseParcelEncoder.TYPE_MAP: - int size = source.readInt(); - Map map = new HashMap<>(size); - for (int i = 0; i < size; i++) { - map.put(source.readString(), decode(source)); - } - return map; - - case ParseParcelEncoder.TYPE_COLLECTION: - int length = source.readInt(); - List list = new ArrayList<>(length); - for (int i = 0; i < length; i++) { - list.add(i, decode(source)); - } - return list; - - case ParseParcelEncoder.TYPE_JSON_NULL: - return JSONObject.NULL; - - case ParseParcelEncoder.TYPE_NULL: - return null; - - case ParseParcelEncoder.TYPE_NATIVE: - return source.readValue(null); // No need for a class loader. - - default: - throw new RuntimeException("Could not unparcel objects from this Parcel."); - - } - } - - protected ParseObject decodeParseObject(Parcel source) { - return ParseObject.createFromParcel(source, this); - } - - protected ParseObject decodePointer(Parcel source) { - // By default, use createWithoutData. Overriden in subclass. - return ParseObject.createWithoutData(source.readString(), source.readString()); - } - -} diff --git a/parse/src/main/java/com/parse/ParseParcelDecoder.kt b/parse/src/main/java/com/parse/ParseParcelDecoder.kt new file mode 100644 index 000000000..e53104cb8 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseParcelDecoder.kt @@ -0,0 +1,97 @@ +/* + * 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 android.os.Parcel +import org.json.JSONObject +import java.util.* +import kotlin.collections.ArrayList + +/** + * A `ParseParcelableDecoder` can be used to unparcel objects such as + * [com.parse.ParseObject] from a [android.os.Parcel]. + * + * + * This is capable of decoding objects and pointers to them. + * However, for improved behavior in the case of [ParseObject]s, use the stateful + * implementation [ParseObjectParcelDecoder]. + * + * @see ParseParcelEncoder + * + * @see ParseObjectParcelDecoder + */ +/* package */ +internal open class ParseParcelDecoder { + fun decode(source: Parcel): Any? { + val type = source.readString() + return when (type) { + ParseParcelEncoder.TYPE_OBJECT -> decodeParseObject(source) + ParseParcelEncoder.TYPE_POINTER -> decodePointer(source) + ParseParcelEncoder.TYPE_DATE -> { + val iso = source.readString() + ParseDateFormat.getInstance().parse(iso) + } + ParseParcelEncoder.TYPE_BYTES -> { + val bytes = ByteArray(source.readInt()) + source.readByteArray(bytes) + bytes + } + ParseParcelEncoder.TYPE_OP -> ParseFieldOperations.decode(source, this) + ParseParcelEncoder.TYPE_FILE -> ParseFile(source, this) + ParseParcelEncoder.TYPE_GEOPOINT -> ParseGeoPoint(source, this) + ParseParcelEncoder.TYPE_POLYGON -> ParsePolygon(source, this) + ParseParcelEncoder.TYPE_ACL -> ParseACL(source, this) + ParseParcelEncoder.TYPE_RELATION -> ParseRelation(source, this) + ParseParcelEncoder.TYPE_MAP -> { + val size = source.readInt() + val map: MutableMap = + HashMap(size) + var i = 0 + while (i < size) { + map[source.readString()] = decode(source) + i++ + } + map + } + ParseParcelEncoder.TYPE_COLLECTION -> { + val length = source.readInt() + val list = ArrayList(length) + var i = 0 + while (i < length) { + list.add(i, decode(source)) + i++ + } + list + } + ParseParcelEncoder.TYPE_JSON_NULL -> JSONObject.NULL + ParseParcelEncoder.TYPE_NULL -> null + ParseParcelEncoder.TYPE_NATIVE -> source.readValue(null) // No need for a class loader. + else -> throw RuntimeException("Could not unparcel objects from this Parcel.") + } + } + + protected fun decodeParseObject(source: Parcel?): ParseObject { + return ParseObject.createFromParcel(source!!, this) + } + + protected open fun decodePointer(source: Parcel): ParseObject? { + // By default, use createWithoutData. Overriden in subclass. + return ParseObject.createWithoutData(source.readString(), source.readString()) + } + + companion object { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = ParseParcelDecoder() + @JvmStatic + fun get(): ParseParcelDecoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseParcelEncoder.java b/parse/src/main/java/com/parse/ParseParcelEncoder.java deleted file mode 100644 index 15686e8c9..000000000 --- a/parse/src/main/java/com/parse/ParseParcelEncoder.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 android.os.Parcel; - -import org.json.JSONObject; - -import java.util.Collection; -import java.util.Date; -import java.util.Map; - -/** - * A {@code ParseParcelableEncoder} can be used to parcel objects into a {@link android.os.Parcel}. - *

- * This is capable of parceling {@link ParseObject}s, but the result can likely be a - * {@link StackOverflowError} due to circular references in the objects tree. - * When needing to parcel {@link ParseObject}, use the stateful {@link ParseObjectParcelEncoder}. - * - * @see ParseParcelDecoder - * @see ParseObjectParcelEncoder - */ -/* package */ class ParseParcelEncoder { - - /* package */ final static String TYPE_OBJECT = "Object"; - /* package */ final static String TYPE_POINTER = "Pointer"; - /* package */ final static String TYPE_DATE = "Date"; - /* package */ final static String TYPE_BYTES = "Bytes"; - /* package */ final static String TYPE_ACL = "Acl"; - /* package */ final static String TYPE_RELATION = "Relation"; - /* package */ final static String TYPE_MAP = "Map"; - /* package */ final static String TYPE_COLLECTION = "Collection"; - /* package */ final static String TYPE_JSON_NULL = "JsonNull"; - /* package */ final static String TYPE_NULL = "Null"; - /* package */ final static String TYPE_NATIVE = "Native"; - /* package */ final static String TYPE_OP = "Operation"; - /* package */ final static String TYPE_FILE = "File"; - /* package */ final static String TYPE_GEOPOINT = "GeoPoint"; - /* package */ final static String TYPE_POLYGON = "Polygon"; - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final ParseParcelEncoder INSTANCE = new ParseParcelEncoder(); - - public static ParseParcelEncoder get() { - return INSTANCE; - } - - private static boolean isValidType(Object value) { - // This encodes to parcel what ParseEncoder does for JSON - return ParseEncoder.isValidType(value); - } - - public void encode(Object object, Parcel dest) { - try { - if (object instanceof ParseObject) { - // By default, encode as a full ParseObject. Overriden in sublasses. - encodeParseObject((ParseObject) object, dest); - - } else if (object instanceof Date) { - dest.writeString(TYPE_DATE); - dest.writeString(ParseDateFormat.getInstance().format((Date) object)); - - } else if (object instanceof byte[]) { - dest.writeString(TYPE_BYTES); - byte[] bytes = (byte[]) object; - dest.writeInt(bytes.length); - dest.writeByteArray(bytes); - - } else if (object instanceof ParseFieldOperation) { - dest.writeString(TYPE_OP); - ((ParseFieldOperation) object).encode(dest, this); - - } else if (object instanceof ParseFile) { - dest.writeString(TYPE_FILE); - ((ParseFile) object).writeToParcel(dest, this); - - } else if (object instanceof ParseGeoPoint) { - dest.writeString(TYPE_GEOPOINT); - ((ParseGeoPoint) object).writeToParcel(dest, this); - - } else if (object instanceof ParsePolygon) { - dest.writeString(TYPE_POLYGON); - ((ParsePolygon) object).writeToParcel(dest, this); - - } else if (object instanceof ParseACL) { - dest.writeString(TYPE_ACL); - ((ParseACL) object).writeToParcel(dest, this); - - } else if (object instanceof ParseRelation) { - dest.writeString(TYPE_RELATION); - ((ParseRelation) object).writeToParcel(dest, this); - - } else if (object instanceof Map) { - dest.writeString(TYPE_MAP); - @SuppressWarnings("unchecked") - Map map = (Map) object; - dest.writeInt(map.size()); - for (Map.Entry pair : map.entrySet()) { - dest.writeString(pair.getKey()); - encode(pair.getValue(), dest); - } - - } else if (object instanceof Collection) { - dest.writeString(TYPE_COLLECTION); - Collection collection = (Collection) object; - dest.writeInt(collection.size()); - for (Object item : collection) { - encode(item, dest); - } - - } else if (object == JSONObject.NULL) { - dest.writeString(TYPE_JSON_NULL); - - } else if (object == null) { - dest.writeString(TYPE_NULL); - - // String, Number, Boolean. Simply use writeValue - } else if (isValidType(object)) { - dest.writeString(TYPE_NATIVE); - dest.writeValue(object); - - } else { - throw new IllegalArgumentException("Could not encode this object into Parcel. " - + object.getClass().toString()); - } - - } catch (Exception e) { - throw new IllegalArgumentException("Could not encode this object into Parcel. " - + object.getClass().toString()); - } - } - - protected void encodeParseObject(ParseObject object, Parcel dest) { - dest.writeString(TYPE_OBJECT); - object.writeToParcel(dest, this); - } - - protected void encodePointer(String className, String objectId, Parcel dest) { - dest.writeString(TYPE_POINTER); - dest.writeString(className); - dest.writeString(objectId); - } -} diff --git a/parse/src/main/java/com/parse/ParseParcelEncoder.kt b/parse/src/main/java/com/parse/ParseParcelEncoder.kt new file mode 100644 index 000000000..ebb563935 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseParcelEncoder.kt @@ -0,0 +1,139 @@ +/* + * 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 android.os.Parcel +import org.json.JSONObject +import java.util.* + +/** + * A `ParseParcelableEncoder` can be used to parcel objects into a [android.os.Parcel]. + * + * + * This is capable of parceling [ParseObject]s, but the result can likely be a + * [StackOverflowError] due to circular references in the objects tree. + * When needing to parcel [ParseObject], use the stateful [ParseObjectParcelEncoder]. + * + * @see ParseParcelDecoder + * + * @see ParseObjectParcelEncoder + */ +/* package */ +open class ParseParcelEncoder { + fun encode(`object`: Any?, dest: Parcel) { + try { + if (`object` is ParseObject) { + // By default, encode as a full ParseObject. Overriden in sublasses. + encodeParseObject(`object`, dest) + } else if (`object` is Date) { + dest.writeString(TYPE_DATE) + dest.writeString(ParseDateFormat.getInstance().format(`object` as Date?)) + } else if (`object` is ByteArray) { + dest.writeString(TYPE_BYTES) + val bytes = `object` + dest.writeInt(bytes.size) + dest.writeByteArray(bytes) + } else if (`object` is ParseFieldOperation) { + dest.writeString(TYPE_OP) + `object`.encode(dest, this) + } else if (`object` is ParseFile) { + dest.writeString(TYPE_FILE) + `object`.writeToParcel(dest, this) + } else if (`object` is ParseGeoPoint) { + dest.writeString(TYPE_GEOPOINT) + `object`.writeToParcel(dest, this) + } else if (`object` is ParsePolygon) { + dest.writeString(TYPE_POLYGON) + `object`.writeToParcel(dest, this) + } else if (`object` is ParseACL) { + dest.writeString(TYPE_ACL) + `object`.writeToParcel(dest, this) + } else if (`object` is ParseRelation<*>) { + dest.writeString(TYPE_RELATION) + `object`.writeToParcel(dest, this) + } else if (`object` is Map<*, *>) { + dest.writeString(TYPE_MAP) + val map = `object` as Map + dest.writeInt(map.size) + for ((key, value) in map) { + dest.writeString(key) + encode(value, dest) + } + } else if (`object` is Collection<*>) { + dest.writeString(TYPE_COLLECTION) + dest.writeInt(`object`.size) + for (item in `object`) { + encode(item, dest) + } + } else if (`object` === JSONObject.NULL) { + dest.writeString(TYPE_JSON_NULL) + } else if (`object` == null) { + dest.writeString(TYPE_NULL) + + // String, Number, Boolean. Simply use writeValue + } else if (isValidType(`object`)) { + dest.writeString(TYPE_NATIVE) + dest.writeValue(`object`) + } else { + throw IllegalArgumentException( + "Could not encode this object into Parcel. " + + `object`.javaClass.toString() + ) + } + } catch (e: Exception) { + throw IllegalArgumentException( + "Could not encode this object into Parcel. " + + `object`!!.javaClass.toString() + ) + } + } + + protected open fun encodeParseObject(`object`: ParseObject, dest: Parcel) { + dest.writeString(TYPE_OBJECT) + `object`.writeToParcel(dest, this) + } + + protected fun encodePointer(className: String?, objectId: String?, dest: Parcel) { + dest.writeString(TYPE_POINTER) + dest.writeString(className) + dest.writeString(objectId) + } + + companion object { + + const val TYPE_OBJECT = "Object" + const val TYPE_POINTER = "Pointer" + const val TYPE_DATE = "Date" + const val TYPE_BYTES = "Bytes" + const val TYPE_ACL = "Acl" + const val TYPE_RELATION = "Relation" + const val TYPE_MAP = "Map" + const val TYPE_COLLECTION = "Collection" + const val TYPE_JSON_NULL = "JsonNull" + const val TYPE_NULL = "Null" + const val TYPE_NATIVE = "Native" + const val TYPE_OP = "Operation" + const val TYPE_FILE = "File" + const val TYPE_GEOPOINT = "GeoPoint" + const val TYPE_POLYGON = "Polygon" + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = ParseParcelEncoder() + @JvmStatic + fun get(): ParseParcelEncoder { + return INSTANCE + } + + private fun isValidType(value: Any): Boolean { + // This encodes to parcel what ParseEncoder does for JSON + return ParseEncoder.isValidType(value) + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java b/parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java index 0e1cd9570..7493fe74d 100644 --- a/parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java +++ b/parse/src/main/java/com/parse/ParsePinningEventuallyQueue.java @@ -11,9 +11,12 @@ import android.Manifest; import android.content.Context; -import android.content.Intent; import android.net.ConnectivityManager; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.json.JSONObject; import java.util.ArrayList; @@ -22,10 +25,6 @@ import java.util.HashMap; import java.util.List; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - /** * Manages all *Eventually calls when the local datastore is enabled. *

@@ -46,56 +45,53 @@ class ParsePinningEventuallyQueue extends ParseEventuallyQueue { /** * TCS that is held until a {@link ParseOperationSet} is completed. */ - private HashMap> pendingOperationSetUUIDTasks = + private final HashMap> pendingOperationSetUUIDTasks = new HashMap<>(); /** * Queue for reading/writing eventually operations. Makes all reads/writes atomic operations. */ - private TaskQueue taskQueue = new TaskQueue(); + private final TaskQueue taskQueue = new TaskQueue(); /** * Queue for running *Eventually operations. It uses waitForOperationSetAndEventuallyPin to * synchronize {@link ParseObject#taskQueue} until they are both ready to process the same * ParseOperationSet. */ - private TaskQueue operationSetTaskQueue = new TaskQueue(); + private final TaskQueue operationSetTaskQueue = new TaskQueue(); /** * List of {@link ParseOperationSet#uuid} that are currently queued in * {@link ParsePinningEventuallyQueue#operationSetTaskQueue}. */ - private ArrayList eventuallyPinUUIDQueue = new ArrayList<>(); - /** - * TCS that is created when there is no internet connection and isn't resolved until connectivity - * is achieved. - *

- * If an error is set, it means that we are trying to clear out the taskQueues. - */ - private TaskCompletionSource connectionTaskCompletionSource = new TaskCompletionSource<>(); - private ConnectivityNotifier notifier; - private ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() { - @Override - public void networkConnectivityStatusChanged(Context context, Intent intent) { - boolean connectionLost = - intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); - if (connectionLost) { - setConnected(false); - } else { - setConnected(ConnectivityNotifier.isConnected(context)); - } - } - }; + private final ArrayList eventuallyPinUUIDQueue = new ArrayList<>(); + private final ConnectivityNotifier notifier; /** * Map of eventually operation UUID to TCS that is resolved when the operation is complete. */ - private HashMap> pendingEventuallyTasks = + private final HashMap> pendingEventuallyTasks = new HashMap<>(); /** * Map of eventually operation UUID to matching ParseOperationSet. */ - private HashMap uuidToOperationSet = new HashMap<>(); + private final HashMap uuidToOperationSet = new HashMap<>(); /** * Map of eventually operation UUID to matching EventuallyPin. */ - private HashMap uuidToEventuallyPin = new HashMap<>(); + private final HashMap uuidToEventuallyPin = new HashMap<>(); + /** + * TCS that is created when there is no internet connection and isn't resolved until connectivity + * is achieved. + *

+ * If an error is set, it means that we are trying to clear out the taskQueues. + */ + private TaskCompletionSource connectionTaskCompletionSource = new TaskCompletionSource<>(); + private final ConnectivityNotifier.ConnectivityListener listener = (context, intent) -> { + boolean connectionLost = + intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + if (connectionLost) { + setConnected(false); + } else { + setConnected(ConnectivityNotifier.isConnected(context)); + } + }; public ParsePinningEventuallyQueue(Context context, ParseHttpClient client) { setConnected(ConnectivityNotifier.isConnected(context)); @@ -143,36 +139,20 @@ public int pendingCount() { public Task pendingCountAsync() { final TaskCompletionSource tcs = new TaskCompletionSource<>(); - taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return pendingCountAsync(toAwait).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - int count = task.getResult(); - tcs.setResult(count); - return Task.forResult(null); - } - }); - } - }); + taskQueue.enqueue((Continuation>) toAwait -> pendingCountAsync(toAwait).continueWithTask(task -> { + int count = task.getResult(); + tcs.setResult(count); + return Task.forResult(null); + })); return tcs.getTask(); } public Task pendingCountAsync(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return EventuallyPin.findAllPinned().continueWithTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - List pins = task.getResult(); - return Task.forResult(pins.size()); - } - }); - } - }); + return toAwait.continueWithTask(task -> EventuallyPin.findAllPinned().continueWithTask(task1 -> { + List pins = task1.getResult(); + return Task.forResult(pins.size()); + })); } @Override @@ -232,55 +212,41 @@ public Task enqueueEventuallyAsync(final ParseRESTCommand command, Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE); final TaskCompletionSource tcs = new TaskCompletionSource<>(); - taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return enqueueEventuallyAsync(command, object, toAwait, tcs); - } - }); + taskQueue.enqueue(toAwait -> enqueueEventuallyAsync(command, object, toAwait, tcs)); return tcs.getTask(); } private Task enqueueEventuallyAsync(final ParseRESTCommand command, final ParseObject object, Task toAwait, final TaskCompletionSource tcs) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task toAwait) { - Task pinTask = EventuallyPin.pinEventuallyCommand(object, command); - - return pinTask.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - EventuallyPin pin = task.getResult(); - Exception error = task.getError(); - if (error != null) { - if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { - PLog.w(TAG, "Unable to save command for later.", error); - } - notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); - return Task.forResult(null); - } + return toAwait.continueWithTask(toAwait1 -> { + Task pinTask = EventuallyPin.pinEventuallyCommand(object, command); - pendingOperationSetUUIDTasks.put(pin.getUUID(), tcs); - - // We don't need to wait for this. - populateQueueAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - /* - * We need to wait until after we populated the operationSetTaskQueue to notify - * that we've enqueued this command. - */ - notifyTestHelper(TestHelper.COMMAND_ENQUEUED); - return task; - } - }); - - return task.makeVoid(); + return pinTask.continueWithTask(task -> { + EventuallyPin pin = task.getResult(); + Exception error = task.getError(); + if (error != null) { + if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { + PLog.w(TAG, "Unable to save command for later.", error); } + notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); + return Task.forResult(null); + } + + pendingOperationSetUUIDTasks.put(pin.getUUID(), tcs); + + // We don't need to wait for this. + populateQueueAsync().continueWithTask(task1 -> { + /* + * We need to wait until after we populated the operationSetTaskQueue to notify + * that we've enqueued this command. + */ + notifyTestHelper(TestHelper.COMMAND_ENQUEUED); + return task1; }); - } + + return task.makeVoid(); + }); }); } @@ -291,33 +257,22 @@ public Task then(Task task) { * operationSetTaskQueue. */ private Task populateQueueAsync() { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return populateQueueAsync(toAwait); - } - }); + return taskQueue.enqueue(this::populateQueueAsync); } private Task populateQueueAsync(Task toAwait) { - return toAwait.continueWithTask(new Continuation>>() { - @Override - public Task> then(Task task) { - // We don't want to enqueue any EventuallyPins that are already queued. - return EventuallyPin.findAllPinned(eventuallyPinUUIDQueue); + return toAwait.continueWithTask(task -> { + // We don't want to enqueue any EventuallyPins that are already queued. + return EventuallyPin.findAllPinned(eventuallyPinUUIDQueue); + }).onSuccessTask(task -> { + List pins = task.getResult(); + + for (final EventuallyPin pin : pins) { + // We don't need to wait for this. + runEventuallyAsync(pin); } - }).onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - List pins = task.getResult(); - - for (final EventuallyPin pin : pins) { - // We don't need to wait for this. - runEventuallyAsync(pin); - } - return task.makeVoid(); - } + return task.makeVoid(); }); } @@ -335,18 +290,10 @@ private Task runEventuallyAsync(final EventuallyPin eventuallyPin) { } eventuallyPinUUIDQueue.add(uuid); - operationSetTaskQueue.enqueue(new Continuation>() { - @Override - public Task then(final Task toAwait) { - return runEventuallyAsync(eventuallyPin, toAwait).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - eventuallyPinUUIDQueue.remove(uuid); - return task; - } - }); - } - }); + operationSetTaskQueue.enqueue(toAwait -> runEventuallyAsync(eventuallyPin, toAwait).continueWithTask(task -> { + eventuallyPinUUIDQueue.remove(uuid); + return task; + })); return Task.forResult(null); } @@ -358,47 +305,34 @@ public Task then(Task task) { * @return A task that is resolved when the eventually operation completes. */ private Task runEventuallyAsync(final EventuallyPin eventuallyPin, final Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return waitForConnectionAsync(); + return toAwait.continueWithTask(task -> waitForConnectionAsync()).onSuccessTask(task -> waitForOperationSetAndEventuallyPin(null, eventuallyPin).continueWithTask(task1 -> { + Exception error = task1.getError(); + if (error != null) { + if (error instanceof PauseException) { + // Bubble up the PauseException. + return task1.makeVoid(); + } + + if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { + PLog.e(TAG, "Failed to run command.", error); + } + + notifyTestHelper(TestHelper.COMMAND_FAILED, error); + } else { + notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return waitForOperationSetAndEventuallyPin(null, eventuallyPin).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - Exception error = task.getError(); - if (error != null) { - if (error instanceof PauseException) { - // Bubble up the PauseException. - return task.makeVoid(); - } - - if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { - PLog.e(TAG, "Failed to run command.", error); - } - - notifyTestHelper(TestHelper.COMMAND_FAILED, error); - } else { - notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); - } - TaskCompletionSource tcs = - pendingOperationSetUUIDTasks.remove(eventuallyPin.getUUID()); - if (tcs != null) { - if (error != null) { - tcs.setError(error); - } else { - tcs.setResult(task.getResult()); - } - } - return task.makeVoid(); - } - }); + TaskCompletionSource tcs = + pendingOperationSetUUIDTasks.remove(eventuallyPin.getUUID()); + if (tcs != null) { + if (error != null) { + tcs.setError(error); + } else { + tcs.setResult(task1.getResult()); + } } - }); + return task1.makeVoid(); + })); } /** @@ -422,7 +356,7 @@ public Task then(Task task) { synchronized (taskQueueSyncLock) { if (operationSet != null && eventuallyPin == null) { - uuid = operationSet.getUUID(); + uuid = operationSet.getUuid(); uuidToOperationSet.put(uuid, operationSet); } else if (operationSet == null && eventuallyPin != null) { uuid = eventuallyPin.getOperationSetUUID(); @@ -447,25 +381,22 @@ public Task then(Task task) { } } - return process(eventuallyPin, operationSet).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (taskQueueSyncLock) { - pendingEventuallyTasks.remove(uuid); - uuidToOperationSet.remove(uuid); - uuidToEventuallyPin.remove(uuid); - } + return process(eventuallyPin, operationSet).continueWithTask(task -> { + synchronized (taskQueueSyncLock) { + pendingEventuallyTasks.remove(uuid); + uuidToOperationSet.remove(uuid); + uuidToEventuallyPin.remove(uuid); + } - Exception error = task.getError(); - if (error != null) { - tcs.trySetError(error); - } else if (task.isCancelled()) { - tcs.trySetCancelled(); - } else { - tcs.trySetResult(task.getResult()); - } - return tcs.getTask(); + Exception error = task.getError(); + if (error != null) { + tcs.trySetError(error); + } else if (task.isCancelled()) { + tcs.trySetCancelled(); + } else { + tcs.trySetResult(task.getResult()); } + return tcs.getTask(); }); } @@ -475,74 +406,60 @@ public Task then(Task task) { private Task process(final EventuallyPin eventuallyPin, final ParseOperationSet operationSet) { - return waitForConnectionAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - final int type = eventuallyPin.getType(); - final ParseObject object = eventuallyPin.getObject(); - String sessionToken = eventuallyPin.getSessionToken(); - - Task executeTask; - if (type == EventuallyPin.TYPE_SAVE) { - executeTask = object.saveAsync(httpClient, operationSet, sessionToken); - } else if (type == EventuallyPin.TYPE_DELETE) { - executeTask = object.deleteAsync(sessionToken).cast(); - } else { // else if (type == EventuallyPin.TYPE_COMMAND) { - ParseRESTCommand command = eventuallyPin.getCommand(); - if (command == null) { - executeTask = Task.forResult(null); - notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED); - } else { - executeTask = command.executeAsync(httpClient); + return waitForConnectionAsync().onSuccessTask(task -> { + final int type = eventuallyPin.getType(); + final ParseObject object = eventuallyPin.getObject(); + String sessionToken = eventuallyPin.getSessionToken(); + + Task executeTask; + if (type == EventuallyPin.TYPE_SAVE) { + executeTask = object.saveAsync(httpClient, operationSet, sessionToken); + } else if (type == EventuallyPin.TYPE_DELETE) { + executeTask = object.deleteAsync(sessionToken).cast(); + } else { // else if (type == EventuallyPin.TYPE_COMMAND) { + ParseRESTCommand command = eventuallyPin.getCommand(); + if (command == null) { + executeTask = Task.forResult(null); + notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED); + } else { + executeTask = command.executeAsync(httpClient); + } + } + + return executeTask.continueWithTask(executeTask1 -> { + Exception error = executeTask1.getError(); + if (error != null) { + if (error instanceof ParseException + && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { + // We did our retry logic in ParseRequest, so just mark as not connected + // and move on. + setConnected(false); + + notifyTestHelper(TestHelper.NETWORK_DOWN); + + return process(eventuallyPin, operationSet); } } - return executeTask.continueWithTask(new Continuation>() { - @Override - public Task then(final Task executeTask) { - Exception error = executeTask.getError(); - if (error != null) { - if (error instanceof ParseException - && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { - // We did our retry logic in ParseRequest, so just mark as not connected - // and move on. - setConnected(false); - - notifyTestHelper(TestHelper.NETWORK_DOWN); - - return process(eventuallyPin, operationSet); - } + // Delete the command regardless, even if it failed. Otherwise, we'll just keep + // trying it forever. + // We don't have to wait for taskQueue since it will not be enqueued again + // since this EventuallyPin is still in eventuallyPinUUIDQueue. + return eventuallyPin.unpinInBackground(EventuallyPin.PIN_NAME).continueWithTask(task12 -> { + JSONObject result = executeTask1.getResult(); + if (type == EventuallyPin.TYPE_SAVE) { + return object.handleSaveEventuallyResultAsync(result, operationSet); + } else if (type == EventuallyPin.TYPE_DELETE) { + if (executeTask1.isFaulted()) { + return task12; + } else { + return object.handleDeleteEventuallyResultAsync(); } - - // Delete the command regardless, even if it failed. Otherwise, we'll just keep - // trying it forever. - // We don't have to wait for taskQueue since it will not be enqueued again - // since this EventuallyPin is still in eventuallyPinUUIDQueue. - return eventuallyPin.unpinInBackground(EventuallyPin.PIN_NAME).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - JSONObject result = executeTask.getResult(); - if (type == EventuallyPin.TYPE_SAVE) { - return object.handleSaveEventuallyResultAsync(result, operationSet); - } else if (type == EventuallyPin.TYPE_DELETE) { - if (executeTask.isFaulted()) { - return task; - } else { - return object.handleDeleteEventuallyResultAsync(); - } - } else { // else if (type == EventuallyPin.TYPE_COMMAND) { - return task; - } - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return executeTask; - } - }); + } else { // else if (type == EventuallyPin.TYPE_COMMAND) { + return task12; } - }); - } + }).continueWithTask(task1 -> executeTask1); + }); }); } @@ -562,28 +479,15 @@ public Task then(Task task) { public void clear() { pause(); - Task task = taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return EventuallyPin.findAllPinned().onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - List pins = task.getResult(); - - List> tasks = new ArrayList<>(); - for (EventuallyPin pin : pins) { - tasks.add(pin.unpinInBackground(EventuallyPin.PIN_NAME)); - } - return Task.whenAll(tasks); - } - }); - } - }); + Task task = taskQueue.enqueue(toAwait -> toAwait.continueWithTask(task12 -> EventuallyPin.findAllPinned().onSuccessTask(task1 -> { + List pins = task1.getResult(); + + List> tasks = new ArrayList<>(); + for (EventuallyPin pin : pins) { + tasks.add(pin.unpinInBackground(EventuallyPin.PIN_NAME)); } - }); + return Task.whenAll(tasks); + }))); try { ParseTaskUtils.wait(task); @@ -607,12 +511,7 @@ private Task whenAll(Collection taskQueues) { List> tasks = new ArrayList<>(); for (TaskQueue taskQueue : taskQueues) { - Task task = taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return toAwait; - } - }); + Task task = taskQueue.enqueue(toAwait -> toAwait); tasks.add(task); } diff --git a/parse/src/main/java/com/parse/ParsePlugins.java b/parse/src/main/java/com/parse/ParsePlugins.java index 1a02703e9..74aa44e24 100644 --- a/parse/src/main/java/com/parse/ParsePlugins.java +++ b/parse/src/main/java/com/parse/ParsePlugins.java @@ -12,13 +12,10 @@ import android.os.Build; import java.io.File; -import java.io.IOException; import okhttp3.Headers; -import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.Response; /** * Public for LiveQuery. You probably don't need access @@ -38,6 +35,7 @@ public class ParsePlugins { ParseHttpClient fileClient; private Context applicationContext; private InstallationId installationId; + private ParsePlugins(Context context, Parse.Configuration configuration) { if (context != null) { applicationContext = context.getApplicationContext(); @@ -116,31 +114,28 @@ ParseHttpClient restClient() { clientBuilder = new OkHttpClient.Builder(); } //add it as the first interceptor - clientBuilder.interceptors().add(0, new Interceptor() { - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - Headers.Builder headersBuilder = request.headers().newBuilder() - .set(ParseRESTCommand.HEADER_APPLICATION_ID, configuration.applicationId) - .set(ParseRESTCommand.HEADER_APP_BUILD_VERSION, - String.valueOf(ManifestInfo.getVersionCode())) - .set(ParseRESTCommand.HEADER_APP_DISPLAY_VERSION, - ManifestInfo.getVersionName()) - .set(ParseRESTCommand.HEADER_OS_VERSION, Build.VERSION.RELEASE) - .set(ParseRESTCommand.USER_AGENT, userAgent()); - if (request.header(ParseRESTCommand.HEADER_INSTALLATION_ID) == null) { - // We can do this synchronously since the caller is already on a background thread - headersBuilder.set(ParseRESTCommand.HEADER_INSTALLATION_ID, installationId().get()); - } - // client key can be null with self-hosted Parse Server - if (configuration.clientKey != null) { - headersBuilder.set(ParseRESTCommand.HEADER_CLIENT_KEY, configuration.clientKey); - } - request = request.newBuilder() - .headers(headersBuilder.build()) - .build(); - return chain.proceed(request); + clientBuilder.interceptors().add(0, chain -> { + Request request = chain.request(); + Headers.Builder headersBuilder = request.headers().newBuilder() + .set(ParseRESTCommand.HEADER_APPLICATION_ID, configuration.applicationId) + .set(ParseRESTCommand.HEADER_APP_BUILD_VERSION, + String.valueOf(ManifestInfo.getVersionCode())) + .set(ParseRESTCommand.HEADER_APP_DISPLAY_VERSION, + ManifestInfo.getVersionName()) + .set(ParseRESTCommand.HEADER_OS_VERSION, Build.VERSION.RELEASE) + .set(ParseRESTCommand.USER_AGENT, userAgent()); + if (request.header(ParseRESTCommand.HEADER_INSTALLATION_ID) == null) { + // We can do this synchronously since the caller is already on a background thread + headersBuilder.set(ParseRESTCommand.HEADER_INSTALLATION_ID, installationId().get()); + } + // client key can be null with self-hosted Parse Server + if (configuration.clientKey != null) { + headersBuilder.set(ParseRESTCommand.HEADER_CLIENT_KEY, configuration.clientKey); } + request = request.newBuilder() + .headers(headersBuilder.build()) + .build(); + return chain.proceed(request); }); restClient = ParseHttpClient.createClient(clientBuilder); } diff --git a/parse/src/main/java/com/parse/ParsePush.java b/parse/src/main/java/com/parse/ParsePush.java index b91ca00cc..021fc7134 100644 --- a/parse/src/main/java/com/parse/ParsePush.java +++ b/parse/src/main/java/com/parse/ParsePush.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONException; import org.json.JSONObject; @@ -16,9 +18,6 @@ import java.util.HashSet; import java.util.Set; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The {@code ParsePush} is a local representation of data that can be sent as a push notification. *

@@ -28,8 +27,8 @@ */ public class ParsePush { + /* package for test */ static final String KEY_DATA_MESSAGE = "alert"; private static final String TAG = "com.parse.ParsePush"; - /* package for test */ static String KEY_DATA_MESSAGE = "alert"; /* package for test */ final State.Builder builder; /** @@ -282,12 +281,9 @@ public void setMessage(String message) { public Task sendInBackground() { // Since getCurrentSessionTokenAsync takes time, we build the state before it. final State state = builder.build(); - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - return getPushController().sendInBackground(state, sessionToken); - } + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + String sessionToken = task.getResult(); + return getPushController().sendInBackground(state, sessionToken); }); } @@ -320,6 +316,7 @@ public void sendInBackground(SendCallback callback) { private final Long expirationTimeInterval; private final Long pushTime; private final JSONObject data; + private State(Builder builder) { this.channelSet = builder.channelSet == null ? null : Collections.unmodifiableSet(new HashSet<>(builder.channelSet)); @@ -387,7 +384,7 @@ public Builder(State state) { : Collections.unmodifiableSet(new HashSet<>(state.channelSet())); this.query = state.queryState() == null ? null - : new ParseQuery<>(new ParseQuery.State.Builder(state.queryState())); + : new ParseQuery<>(new ParseQuery.State.Builder<>(state.queryState())); this.expirationTime = state.expirationTime(); this.expirationTimeInterval = state.expirationTimeInterval(); this.pushTime = state.pushTime(); diff --git a/parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java b/parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java index 19b8a1b59..99d28fb05 100644 --- a/parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java +++ b/parse/src/main/java/com/parse/ParsePushBroadcastReceiver.java @@ -21,6 +21,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; + import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; diff --git a/parse/src/main/java/com/parse/ParsePushChannelsController.java b/parse/src/main/java/com/parse/ParsePushChannelsController.java index f2468f550..9794f9608 100644 --- a/parse/src/main/java/com/parse/ParsePushChannelsController.java +++ b/parse/src/main/java/com/parse/ParsePushChannelsController.java @@ -8,12 +8,11 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import java.util.Collections; import java.util.List; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - class ParsePushChannelsController { private static ParseCurrentInstallationController getCurrentInstallationController() { @@ -24,19 +23,16 @@ public Task subscribeInBackground(final String channel) { if (channel == null) { throw new IllegalArgumentException("Can't subscribe to null channel."); } - return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseInstallation installation = task.getResult(); - List channels = installation.getList(ParseInstallation.KEY_CHANNELS); - if (channels == null - || installation.isDirty(ParseInstallation.KEY_CHANNELS) - || !channels.contains(channel)) { - installation.addUnique(ParseInstallation.KEY_CHANNELS, channel); - return installation.saveInBackground(); - } else { - return Task.forResult(null); - } + return getCurrentInstallationController().getAsync().onSuccessTask(task -> { + ParseInstallation installation = task.getResult(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + if (channels == null + || installation.isDirty(ParseInstallation.KEY_CHANNELS) + || !channels.contains(channel)) { + installation.addUnique(ParseInstallation.KEY_CHANNELS, channel); + return installation.saveInBackground(); + } else { + return Task.forResult(null); } }); } @@ -45,18 +41,15 @@ public Task unsubscribeInBackground(final String channel) { if (channel == null) { throw new IllegalArgumentException("Can't unsubscribe from null channel."); } - return getCurrentInstallationController().getAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseInstallation installation = task.getResult(); - List channels = installation.getList(ParseInstallation.KEY_CHANNELS); - if (channels != null && channels.contains(channel)) { - installation.removeAll( - ParseInstallation.KEY_CHANNELS, Collections.singletonList(channel)); - return installation.saveInBackground(); - } else { - return Task.forResult(null); - } + return getCurrentInstallationController().getAsync().onSuccessTask(task -> { + ParseInstallation installation = task.getResult(); + List channels = installation.getList(ParseInstallation.KEY_CHANNELS); + if (channels != null && channels.contains(channel)) { + installation.removeAll( + ParseInstallation.KEY_CHANNELS, Collections.singletonList(channel)); + return installation.saveInBackground(); + } else { + return Task.forResult(null); } }); } diff --git a/parse/src/main/java/com/parse/ParsePushController.java b/parse/src/main/java/com/parse/ParsePushController.java index 2991e115a..f4494a634 100644 --- a/parse/src/main/java/com/parse/ParsePushController.java +++ b/parse/src/main/java/com/parse/ParsePushController.java @@ -23,7 +23,7 @@ public Task sendInBackground(ParsePush.State state, String sessionToken) { } ParseRESTCommand buildRESTSendPushCommand(ParsePush.State state, - String sessionToken) { + String sessionToken) { return ParseRESTPushCommand.sendPushCommand(state.queryState(), state.channelSet(), state.expirationTime(), state.expirationTimeInterval(), state.pushTime(), state.data(), sessionToken); diff --git a/parse/src/main/java/com/parse/ParseQuery.java b/parse/src/main/java/com/parse/ParseQuery.java index 22d1178b9..3b03ad4ea 100644 --- a/parse/src/main/java/com/parse/ParseQuery.java +++ b/parse/src/main/java/com/parse/ParseQuery.java @@ -10,6 +10,9 @@ import androidx.annotation.NonNull; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.json.JSONException; import org.json.JSONObject; @@ -25,10 +28,6 @@ import java.util.concurrent.Callable; import java.util.regex.Pattern; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - /** * The {@code ParseQuery} class defines a query that is used to fetch {@link ParseObject}s. The most * common use case is finding all objects that match a query through the {@link #findInBackground()} @@ -95,10 +94,10 @@ public class ParseQuery { public static final int MAX_LIMIT = 1000; private final State.Builder builder; - private ParseUser user; // Just like ParseFile - private Set> currentTasks = Collections.synchronizedSet( - new HashSet>()); + private final Set> currentTasks = Collections.synchronizedSet( + new HashSet<>()); + private ParseUser user; /** * Constructs a query for a {@link ParseObject} subclass type. A default query with no further @@ -117,7 +116,7 @@ public ParseQuery(Class subclass) { * @param theClassName The name of the class to retrieve {@link ParseObject}s for. */ public ParseQuery(String theClassName) { - this(new State.Builder(theClassName)); + this(new State.Builder<>(theClassName)); } /** @@ -404,13 +403,10 @@ private Task perform(Callable> runnable, final } catch (Exception e) { task = Task.forError(e); } - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - tcs.trySetResult(null); // release - currentTasks.remove(tcs); - return task; - } + return task.continueWithTask(task1 -> { + tcs.trySetResult(null); // release + currentTasks.remove(tcs); + return task1; }); } @@ -444,30 +440,17 @@ public void findInBackground(final FindCallback callback) { state.isFromLocalDatastore()) { task = findAsync(state); } else { - task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>>() { - @Override - public Task> call(State state, ParseUser user, Task cancellationToken) { - return findAsync(state, user, cancellationToken); - } - }); + task = doCacheThenNetwork(state, callback, this::findAsync); } ParseTaskUtils.callbackOnMainThreadAsync(task, callback); } private Task> findAsync(final State state) { final TaskCompletionSource tcs = new TaskCompletionSource<>(); - return perform(new Callable>>() { - @Override - public Task> call() { - return getUserAsync(state).onSuccessTask(new Continuation>>() { - @Override - public Task> then(Task task) { - final ParseUser user = task.getResult(); - return findAsync(state, user, tcs.getTask()); - } - }); - } - }, tcs); + return perform(() -> getUserAsync(state).onSuccessTask(task -> { + final ParseUser user = task.getResult(); + return findAsync(state, user, tcs.getTask()); + }), tcs); } /* package */ Task> findAsync(State state, ParseUser user, Task cancellationToken) { @@ -511,30 +494,17 @@ public void getFirstInBackground(final GetCallback callback) { state.isFromLocalDatastore()) { task = getFirstAsync(state); } else { - task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>() { - @Override - public Task call(State state, ParseUser user, Task cancellationToken) { - return getFirstAsync(state, user, cancellationToken); - } - }); + task = doCacheThenNetwork(state, callback, this::getFirstAsync); } ParseTaskUtils.callbackOnMainThreadAsync(task, callback); } private Task getFirstAsync(final State state) { final TaskCompletionSource tcs = new TaskCompletionSource<>(); - return perform(new Callable>() { - @Override - public Task call() { - return getUserAsync(state).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser user = task.getResult(); - return getFirstAsync(state, user, tcs.getTask()); - } - }); - } - }, tcs); + return perform(() -> getUserAsync(state).onSuccessTask(task -> { + final ParseUser user = task.getResult(); + return getFirstAsync(state, user, tcs.getTask()); + }), tcs); } private Task getFirstAsync(State state, ParseUser user, Task cancellationToken) { @@ -574,12 +544,7 @@ public void countInBackground(final CountCallback callback) { // Hack to workaround CountCallback's non-uniform signature. final ParseCallback2 c = callback != null - ? new ParseCallback2() { - @Override - public void done(Integer integer, ParseException e) { - callback.done(e == null ? integer : -1, e); - } - } + ? (integer, e) -> callback.done(e == null ? integer : -1, e) : null; final Task task; @@ -587,30 +552,17 @@ public void done(Integer integer, ParseException e) { state.isFromLocalDatastore()) { task = countAsync(state); } else { - task = doCacheThenNetwork(state, c, new CacheThenNetworkCallable>() { - @Override - public Task call(State state, ParseUser user, Task cancellationToken) { - return countAsync(state, user, cancellationToken); - } - }); + task = doCacheThenNetwork(state, c, this::countAsync); } ParseTaskUtils.callbackOnMainThreadAsync(task, c); } private Task countAsync(final State state) { final TaskCompletionSource tcs = new TaskCompletionSource<>(); - return perform(new Callable>() { - @Override - public Task call() { - return getUserAsync(state).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser user = task.getResult(); - return countAsync(state, user, tcs.getTask()); - } - }); - } - }, tcs); + return perform(() -> getUserAsync(state).onSuccessTask(task -> { + final ParseUser user = task.getResult(); + return countAsync(state, user, tcs.getTask()); + }), tcs); } private Task countAsync(State state, ParseUser user, Task cancellationToken) { @@ -729,12 +681,7 @@ public void getInBackground(final String objectId, final GetCallback callback state.isFromLocalDatastore()) { task = getFirstAsync(state); } else { - task = doCacheThenNetwork(state, callback, new CacheThenNetworkCallable>() { - @Override - public Task call(State state, ParseUser user, Task cancellationToken) { - return getFirstAsync(state, user, cancellationToken); - } - }); + task = doCacheThenNetwork(state, callback, this::getFirstAsync); } ParseTaskUtils.callbackOnMainThreadAsync(task, callback); } @@ -752,35 +699,24 @@ private Task doCacheThenNetwork( final CacheThenNetworkCallable> delegate) { final TaskCompletionSource tcs = new TaskCompletionSource<>(); - return perform(new Callable>() { - @Override - public Task call() { - return getUserAsync(state).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser user = task.getResult(); - final State cacheState = new State.Builder(state) - .setCachePolicy(CachePolicy.CACHE_ONLY) - .build(); - final State networkState = new State.Builder(state) - .setCachePolicy(CachePolicy.NETWORK_ONLY) - .build(); - - Task executionTask = delegate.call(cacheState, user, tcs.getTask()); - executionTask = ParseTaskUtils.callbackOnMainThreadAsync(executionTask, callback); - return executionTask.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isCancelled()) { - return task; - } - return delegate.call(networkState, user, tcs.getTask()); - } - }); - } - }); - } - }, tcs); + return perform(() -> getUserAsync(state).onSuccessTask(task -> { + final ParseUser user = task.getResult(); + final State cacheState = new State.Builder(state) + .setCachePolicy(CachePolicy.CACHE_ONLY) + .build(); + final State networkState = new State.Builder(state) + .setCachePolicy(CachePolicy.NETWORK_ONLY) + .build(); + + Task executionTask = delegate.call(cacheState, user, tcs.getTask()); + executionTask = ParseTaskUtils.callbackOnMainThreadAsync(executionTask, callback); + return executionTask.continueWithTask(task1 -> { + if (task1.isCancelled()) { + return task1; + } + return delegate.call(networkState, user, tcs.getTask()); + }); + }), tcs); } /** @@ -1463,8 +1399,8 @@ public QueryConstraints(Map map) { * Constraint for a $relatedTo query. */ /* package */ static class RelationConstraint { - private String key; - private ParseObject object; + private final String key; + private final ParseObject object; public RelationConstraint(String key, ParseObject object) { if (key == null || object == null) { @@ -1524,6 +1460,7 @@ public static class State { private final boolean isFromLocalDatastore; private final String pinName; private final boolean ignoreACLs; + private State(Builder builder) { className = builder.className; where = new QueryConstraints(builder.where); @@ -1665,11 +1602,11 @@ public static class Builder { private final QueryConstraints where = new QueryConstraints(); private final Set includes = new HashSet<>(); private final Map extraOptions = new HashMap<>(); + private final List order = new ArrayList<>(); // This is nullable since we allow unset selectedKeys as well as no selectedKeys private Set selectedKeys; private int limit = -1; // negative limits mean, do not send a limit private int skip = 0; // negative skip means do not send a skip - private List order = new ArrayList<>(); // TODO(grantland): Move out of State private boolean trace; // Query Caching @@ -1679,6 +1616,7 @@ public static class Builder { private boolean isFromLocalDatastore = false; private String pinName; private boolean ignoreACLs; + public Builder(String className) { this.className = className; } diff --git a/parse/src/main/java/com/parse/ParseQueryController.java b/parse/src/main/java/com/parse/ParseQueryController.java deleted file mode 100644 index 8917558fd..000000000 --- a/parse/src/main/java/com/parse/ParseQueryController.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 java.util.List; - -import com.parse.boltsinternal.Task; - -/** - * A {@code ParseQueryController} defines how a {@link ParseQuery} is executed. - */ -interface ParseQueryController { - - /** - * Executor for {@code find} queries. - * - * @param state Immutable query state to execute. - * @param user The user executing the query that can be used to match ACLs. - * @param cancellationToken Cancellation token. - * @return A {@link Task} that resolves to the results of the find. - */ - Task> findAsync(ParseQuery.State state, ParseUser user, - Task cancellationToken); - - /** - * Executor for {@code count} queries. - * - * @param state Immutable query state to execute. - * @param user The user executing the query that can be used to match ACLs. - * @param cancellationToken Cancellation token. - * @return A {@link Task} that resolves to the results of the count. - */ - Task countAsync(ParseQuery.State state, ParseUser user, - Task cancellationToken); - - /** - * Executor for {@code getFirst} queries. - * - * @param state Immutable query state to execute. - * @param user The user executing the query that can be used to match ACLs. - * @param cancellationToken Cancellation token. - * @return A {@link Task} that resolves to the the first result of the query if successful and - * there is at least one result or {@link ParseException#OBJECT_NOT_FOUND} if there are no - * results. - */ - Task getFirstAsync(ParseQuery.State state, ParseUser user, - Task cancellationToken); -} diff --git a/parse/src/main/java/com/parse/ParseQueryController.kt b/parse/src/main/java/com/parse/ParseQueryController.kt new file mode 100644 index 000000000..5fbaec378 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseQueryController.kt @@ -0,0 +1,57 @@ +/* + * 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 com.parse.boltsinternal.Task + +/** + * A `ParseQueryController` defines how a [ParseQuery] is executed. + */ +internal interface ParseQueryController { + /** + * Executor for `find` queries. + * + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A [Task] that resolves to the results of the find. + */ + fun findAsync( + state: ParseQuery.State, user: ParseUser?, + cancellationToken: Task? + ): Task> + + /** + * Executor for `count` queries. + * + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A [Task] that resolves to the results of the count. + */ + fun countAsync( + state: ParseQuery.State, user: ParseUser?, + cancellationToken: Task? + ): Task + + /** + * Executor for `getFirst` queries. + * + * @param state Immutable query state to execute. + * @param user The user executing the query that can be used to match ACLs. + * @param cancellationToken Cancellation token. + * @return A [Task] that resolves to the the first result of the query if successful and + * there is at least one result or [ParseException.OBJECT_NOT_FOUND] if there are no + * results. + */ + fun getFirstAsync( + state: ParseQuery.State, user: ParseUser?, + cancellationToken: Task? + ): Task +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseRESTCommand.java b/parse/src/main/java/com/parse/ParseRESTCommand.java deleted file mode 100644 index 250b5a783..000000000 --- a/parse/src/main/java/com/parse/ParseRESTCommand.java +++ /dev/null @@ -1,534 +0,0 @@ -/* - * 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 com.parse.http.ParseHttpBody; -import com.parse.http.ParseHttpRequest; -import com.parse.http.ParseHttpResponse; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.json.JSONStringer; - -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; - -import com.parse.boltsinternal.Task; - -/** - * A helper object to send requests to the server. - */ -class ParseRESTCommand extends ParseRequest { - - /* package */ static final String HEADER_APPLICATION_ID = "X-Parse-Application-Id"; - /* package */ static final String HEADER_CLIENT_KEY = "X-Parse-Client-Key"; - /* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version"; - /* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version"; - /* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version"; - - /* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id"; - /* package */ static final String USER_AGENT = "User-Agent"; - private static final String HEADER_SESSION_TOKEN = "X-Parse-Session-Token"; - private static final String HEADER_MASTER_KEY = "X-Parse-Master-Key"; - private static final String PARAMETER_METHOD_OVERRIDE = "_method"; - - // Set via Parse.initialize(Configuration) - /* package */ static URL server = null; - /* package */ final JSONObject jsonParameters; - // Headers - private final String sessionToken; - public String masterKey; - /* package */ String httpPath; - private String installationId; - private String operationSetUUID; - private String localId; - public ParseRESTCommand( - String httpPath, - ParseHttpRequest.Method httpMethod, - Map parameters, - String sessionToken) { - this( - httpPath, - httpMethod, - parameters != null ? (JSONObject) NoObjectsEncoder.get().encode(parameters) : null, - sessionToken); - } - - public ParseRESTCommand( - String httpPath, - ParseHttpRequest.Method httpMethod, - JSONObject jsonParameters, - String sessionToken) { - this(httpPath, httpMethod, jsonParameters, null, sessionToken); - } - private ParseRESTCommand( - String httpPath, - ParseHttpRequest.Method httpMethod, - JSONObject jsonParameters, - String localId, String sessionToken) { - super(httpMethod, createUrl(httpPath)); - - this.httpPath = httpPath; - this.jsonParameters = jsonParameters; - this.localId = localId; - this.sessionToken = sessionToken; - } - - /* package */ ParseRESTCommand(Init builder) { - super(builder.method, createUrl(builder.httpPath)); - sessionToken = builder.sessionToken; - installationId = builder.installationId; - masterKey = builder.masterKey; - - httpPath = builder.httpPath; - jsonParameters = builder.jsonParameters; - operationSetUUID = builder.operationSetUUID; - localId = builder.localId; - } - - private static LocalIdManager getLocalIdManager() { - return ParseCorePlugins.getInstance().getLocalIdManager(); - } - - public static ParseRESTCommand fromJSONObject(JSONObject jsonObject) { - String httpPath = jsonObject.optString("httpPath"); - ParseHttpRequest.Method httpMethod = - ParseHttpRequest.Method.fromString(jsonObject.optString("httpMethod")); - String sessionToken = jsonObject.optString("sessionToken", null); - String localId = jsonObject.optString("localId", null); - JSONObject jsonParameters = jsonObject.optJSONObject("parameters"); - - return new ParseRESTCommand(httpPath, httpMethod, jsonParameters, localId, sessionToken); - } - - private static String createUrl(String httpPath) { - // We send all parameters for GET/HEAD/DELETE requests in a post body, - // so no need to worry about query parameters here. - if (httpPath == null) { - return server.toString(); - } - - try { - return new URL(server, httpPath).toString(); - } catch (MalformedURLException ex) { - throw new RuntimeException(ex); - } - } - - // Encodes the object to JSON, but ensures that JSONObjects - // and nested JSONObjects are encoded with keys in alphabetical order. - static String toDeterministicString(Object o) throws JSONException { - JSONStringer stringer = new JSONStringer(); - addToStringer(stringer, o); - return stringer.toString(); - } - - // Uses the provided JSONStringer to encode this object to JSON, but ensures that JSONObjects and - // nested JSONObjects are encoded with keys in alphabetical order. - private static void addToStringer(JSONStringer stringer, Object o) throws JSONException { - if (o instanceof JSONObject) { - stringer.object(); - JSONObject object = (JSONObject) o; - Iterator keyIterator = object.keys(); - ArrayList keys = new ArrayList<>(); - while (keyIterator.hasNext()) { - keys.add(keyIterator.next()); - } - Collections.sort(keys); - - for (String key : keys) { - stringer.key(key); - addToStringer(stringer, object.opt(key)); - } - - stringer.endObject(); - return; - } - - if (o instanceof JSONArray) { - JSONArray array = (JSONArray) o; - stringer.array(); - for (int i = 0; i < array.length(); ++i) { - addToStringer(stringer, array.get(i)); - } - stringer.endArray(); - return; - } - - stringer.value(o); - } - - /* package */ - static boolean isValidCommandJSONObject(JSONObject jsonObject) { - return jsonObject.has("httpPath"); - } - - // This function checks whether a json object is a valid /2 ParseCommand json. - /* package */ - static boolean isValidOldFormatCommandJSONObject(JSONObject jsonObject) { - return jsonObject.has("op"); - } - - protected static void getLocalPointersIn(Object container, ArrayList localPointers) - throws JSONException { - if (container instanceof JSONObject) { - JSONObject object = (JSONObject) container; - if ("Pointer".equals(object.opt("__type")) && object.has("localId")) { - localPointers.add((JSONObject) container); - return; - } - - Iterator keyIterator = object.keys(); - while (keyIterator.hasNext()) { - String key = keyIterator.next(); - getLocalPointersIn(object.get(key), localPointers); - } - } - - if (container instanceof JSONArray) { - JSONArray array = (JSONArray) container; - for (int i = 0; i < array.length(); ++i) { - getLocalPointersIn(array.get(i), localPointers); - } - } - } - - protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) { - if (installationId != null) { - requestBuilder.addHeader(HEADER_INSTALLATION_ID, installationId); - } - if (sessionToken != null) { - requestBuilder.addHeader(HEADER_SESSION_TOKEN, sessionToken); - } - if (masterKey != null) { - requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey); - } - } - - @Override - protected ParseHttpRequest newRequest( - ParseHttpRequest.Method method, - String url, - ProgressCallback uploadProgressCallback) { - ParseHttpRequest request; - if (jsonParameters != null && - method != ParseHttpRequest.Method.POST && - method != ParseHttpRequest.Method.PUT) { - // The request URI may be too long to include parameters in the URI. - // To avoid this problem we send the parameters in a POST request json-encoded body - // and add a http method override parameter in newBody. - request = super.newRequest(ParseHttpRequest.Method.POST, url, uploadProgressCallback); - } else { - request = super.newRequest(method, url, uploadProgressCallback); - } - ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder(request); - addAdditionalHeaders(requestBuilder); - return requestBuilder.build(); - } - - @Override - protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) { - if (jsonParameters == null) { - String message = String.format("Trying to execute a %s command without body parameters.", - method.toString()); - throw new IllegalArgumentException(message); - } - - try { - JSONObject parameters = jsonParameters; - if (method == ParseHttpRequest.Method.GET || - method == ParseHttpRequest.Method.DELETE) { - // The request URI may be too long to include parameters in the URI. - // To avoid this problem we send the parameters in a POST request json-encoded body - // and add a http method override parameter. - parameters = new JSONObject(jsonParameters.toString()); - parameters.put(PARAMETER_METHOD_OVERRIDE, method.toString()); - } - return new ParseByteArrayHttpBody(parameters.toString(), "application/json"); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - public Task executeAsync( - final ParseHttpClient client, - final ProgressCallback uploadProgressCallback, - final ProgressCallback downloadProgressCallback, - final Task cancellationToken) { - resolveLocalIds(); - return super.executeAsync( - client, uploadProgressCallback, downloadProgressCallback, cancellationToken); - } - - @Override - protected Task onResponseAsync(ParseHttpResponse response, - ProgressCallback downloadProgressCallback) { - String content; - InputStream responseStream = null; - try { - responseStream = response.getContent(); - content = new String(ParseIOUtils.toByteArray(responseStream)); - } catch (IOException e) { - return Task.forError(e); - } finally { - ParseIOUtils.closeQuietly(responseStream); - } - - // We need to check for errors differently in /1/ than /2/ since object data in /2/ was - // encapsulated in "data" and everything was 200, but /2/ everything is in the root JSON, - // but errors are status 4XX. - // See https://quip.com/4pbbA9HbOPjQ - int statusCode = response.getStatusCode(); - if (statusCode >= 200 && statusCode < 600) { // Assume 3XX is handled by http library - JSONObject json; - try { - json = new JSONObject(content); - - if (statusCode >= 400 && statusCode < 500) { // 4XX - return Task.forError(newPermanentException(json.optInt("code"), json.optString("error"))); - } else if (statusCode >= 500) { // 5XX - return Task.forError(newTemporaryException(json.optInt("code"), json.optString("error"))); - } - - return Task.forResult(json); - } catch (JSONException e) { - return Task.forError(newTemporaryException("bad json response", e)); - } - } - - return Task.forError(newPermanentException(ParseException.OTHER_CAUSE, content)); - } - - // Creates a somewhat-readable string that uniquely identifies this command. - public String getCacheKey() { - String json; - if (jsonParameters != null) { - try { - json = toDeterministicString(jsonParameters); - } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); - } - } else { - json = ""; - } - - // Include the session token in the cache in order to avoid mixing permissions. - if (sessionToken != null) { - json += sessionToken; - } - - return String.format( - "ParseRESTCommand.%s.%s.%s", - method.toString(), - ParseDigestUtils.md5(httpPath), - ParseDigestUtils.md5(json) - ); - } - - public JSONObject toJSONObject() { - JSONObject jsonObject = new JSONObject(); - try { - if (httpPath != null) { - jsonObject.put("httpPath", httpPath); - } - jsonObject.put("httpMethod", method.toString()); - if (jsonParameters != null) { - jsonObject.put("parameters", jsonParameters); - } - if (sessionToken != null) { - jsonObject.put("sessionToken", sessionToken); - } - if (localId != null) { - jsonObject.put("localId", localId); - } - } catch (JSONException e) { - throw new RuntimeException(e.getMessage()); - } - return jsonObject; - } - - public String getSessionToken() { - return sessionToken; - } - - public String getOperationSetUUID() { - return operationSetUUID; - } - - /* package */ void setOperationSetUUID(String operationSetUUID) { - this.operationSetUUID = operationSetUUID; - } - - public String getLocalId() { - return localId; - } - - public void setLocalId(String localId) { - this.localId = localId; - } - - /** - * If this was the second save on a new object while offline, then its objectId wasn't yet set - * when the command was created, so it would have been considered a "create". But if the first - * save succeeded, then there is an objectId now, and it will be mapped to the localId for this - * command's result. If so, change the "create" operation to an "update", and add the objectId to - * the command. - */ - private void maybeChangeServerOperation() { - if (localId != null) { - String objectId = getLocalIdManager().getObjectId(localId); - if (objectId != null) { - localId = null; - httpPath += String.format("/%s", objectId); - url = createUrl(httpPath); - - if (httpPath.startsWith("classes") && method == ParseHttpRequest.Method.POST) { - method = ParseHttpRequest.Method.PUT; - } - } - } - } - - public void resolveLocalIds() { - try { - ArrayList localPointers = new ArrayList<>(); - getLocalPointersIn(jsonParameters, localPointers); - for (JSONObject pointer : localPointers) { - String localId = (String) pointer.get("localId"); - String objectId = getLocalIdManager().getObjectId(localId); - if (objectId == null) { - throw new IllegalStateException( - "Tried to serialize a command referencing a new, unsaved object."); - } - pointer.put("objectId", objectId); - pointer.remove("localId"); - } - maybeChangeServerOperation(); - } catch (JSONException e) { - // Well, nothing to do here... - } - } - - /** - * Finds all of the local ids in this command and increments their retain counts in the on-disk - * store. This should be called immediately before serializing the command to disk, so that we - * know we might need to resolve these local ids at some point in the future. - */ - public void retainLocalIds() { - if (localId != null) { - getLocalIdManager().retainLocalIdOnDisk(localId); - } - - try { - ArrayList localPointers = new ArrayList<>(); - getLocalPointersIn(jsonParameters, localPointers); - for (JSONObject pointer : localPointers) { - String localId = (String) pointer.get("localId"); - getLocalIdManager().retainLocalIdOnDisk(localId); - } - } catch (JSONException e) { - // Well, nothing to do here... - } - } - - /** - * Finds all of the local ids in this command and decrements their retain counts in the on-disk - * store. This should be called when removing a serialized command from the disk, when we know - * that we will never need to resolve these local ids for this command again in the future. - */ - public void releaseLocalIds() { - if (localId != null) { - getLocalIdManager().releaseLocalIdOnDisk(localId); - } - try { - ArrayList localPointers = new ArrayList<>(); - getLocalPointersIn(jsonParameters, localPointers); - for (JSONObject pointer : localPointers) { - String localId = (String) pointer.get("localId"); - getLocalIdManager().releaseLocalIdOnDisk(localId); - } - } catch (JSONException e) { - // Well, nothing to do here... - } - } - - /* package */ static abstract class Init> { - public String masterKey; - private String sessionToken; - private String installationId; - private ParseHttpRequest.Method method = ParseHttpRequest.Method.GET; - private String httpPath; - private JSONObject jsonParameters; - - private String operationSetUUID; - private String localId; - - /* package */ - abstract T self(); - - public T sessionToken(String sessionToken) { - this.sessionToken = sessionToken; - return self(); - } - - public T installationId(String installationId) { - this.installationId = installationId; - return self(); - } - - public T masterKey(String masterKey) { - this.masterKey = masterKey; - return self(); - } - - public T method(ParseHttpRequest.Method method) { - this.method = method; - return self(); - } - - public T httpPath(String httpPath) { - this.httpPath = httpPath; - return self(); - } - - public T jsonParameters(JSONObject jsonParameters) { - this.jsonParameters = jsonParameters; - return self(); - } - - public T operationSetUUID(String operationSetUUID) { - this.operationSetUUID = operationSetUUID; - return self(); - } - - public T localId(String localId) { - this.localId = localId; - return self(); - } - } - - public static class Builder extends Init { - @Override - /* package */ Builder self() { - return this; - } - - public ParseRESTCommand build() { - return new ParseRESTCommand(this); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseRESTCommand.kt b/parse/src/main/java/com/parse/ParseRESTCommand.kt new file mode 100644 index 000000000..57ac0954c --- /dev/null +++ b/parse/src/main/java/com/parse/ParseRESTCommand.kt @@ -0,0 +1,522 @@ +/* + * 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 com.parse.NoObjectsEncoder.Companion.get +import com.parse.boltsinternal.Task +import com.parse.http.ParseHttpBody +import com.parse.http.ParseHttpRequest +import com.parse.http.ParseHttpResponse +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONStringer +import java.io.IOException +import java.io.InputStream +import java.net.MalformedURLException +import java.net.URL +import java.util.* + +/** + * A helper object to send requests to the server. + */ +internal open class ParseRESTCommand : ParseRequest { + @JvmField + val jsonParameters: JSONObject? + + // Headers + val sessionToken: String? + var masterKey: String? = null + + /* package */ + @JvmField + var httpPath: String? + var installationId: String? = null + var operationSetUUID: String? = null + var localId: String? + + constructor( + httpPath: String?, + httpMethod: ParseHttpRequest.Method?, + parameters: Map?, + sessionToken: String? + ) : this( + httpPath, + httpMethod, + if (parameters != null) get().encode(parameters) as JSONObject else null, + sessionToken + ) + + constructor( + httpPath: String?, + httpMethod: ParseHttpRequest.Method?, + jsonParameters: JSONObject?, + sessionToken: String? + ) : this(httpPath, httpMethod, jsonParameters, null, sessionToken) { + } + + private constructor( + httpPath: String?, + httpMethod: ParseHttpRequest.Method?, + jsonParameters: JSONObject?, + localId: String?, sessionToken: String? + ) : super(httpMethod!!, createUrl(httpPath)) { + this.httpPath = httpPath + this.jsonParameters = jsonParameters + this.localId = localId + this.sessionToken = sessionToken + } + + constructor(builder: Init<*>) : super(builder.method, createUrl(builder.httpPath)) { + sessionToken = builder.sessionToken + installationId = builder.installationId + masterKey = builder.masterKey + httpPath = builder.httpPath + jsonParameters = builder.jsonParameters + operationSetUUID = builder.operationSetUUID + localId = builder.localId + } + + protected open fun addAdditionalHeaders(requestBuilder: ParseHttpRequest.Builder) { + if (installationId != null) { + requestBuilder.addHeader(HEADER_INSTALLATION_ID, installationId) + } + if (sessionToken != null) { + requestBuilder.addHeader(HEADER_SESSION_TOKEN, sessionToken) + } + if (masterKey != null) { + requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey) + } + } + + override fun newRequest( + method: ParseHttpRequest.Method, + url: String?, + uploadProgressCallback: ProgressCallback? + ): ParseHttpRequest { + val request: ParseHttpRequest = if (jsonParameters != null && method != ParseHttpRequest.Method.POST && method != ParseHttpRequest.Method.PUT) { + // The request URI may be too long to include parameters in the URI. + // To avoid this problem we send the parameters in a POST request json-encoded body + // and add a http method override parameter in newBody. + super.newRequest(ParseHttpRequest.Method.POST, url, uploadProgressCallback) + } else { + super.newRequest(method, url, uploadProgressCallback) + } + val requestBuilder = ParseHttpRequest.Builder(request) + addAdditionalHeaders(requestBuilder) + return requestBuilder.build() + } + + public override fun newBody(uploadProgressCallback: ProgressCallback?): ParseHttpBody? { + if (jsonParameters == null) { + val message = String.format( + "Trying to execute a %s command without body parameters.", + method.toString() + ) + throw IllegalArgumentException(message) + } + return try { + var parameters: JSONObject = jsonParameters + if (method == ParseHttpRequest.Method.GET || + method == ParseHttpRequest.Method.DELETE + ) { + // The request URI may be too long to include parameters in the URI. + // To avoid this problem we send the parameters in a POST request json-encoded body + // and add a http method override parameter. + parameters = JSONObject(jsonParameters.toString()) + parameters.put(PARAMETER_METHOD_OVERRIDE, method.toString()) + } + ParseByteArrayHttpBody(parameters.toString(), "application/json") + } catch (e: Exception) { + throw RuntimeException(e.message) + } + } + + override fun executeAsync( + client: ParseHttpClient, + uploadProgressCallback: ProgressCallback?, + downloadProgressCallback: ProgressCallback?, + cancellationToken: Task? + ): Task { + resolveLocalIds() + return super.executeAsync( + client, uploadProgressCallback, downloadProgressCallback, cancellationToken + ) + } + + public override fun onResponseAsync( + response: ParseHttpResponse?, + downloadProgressCallback: ProgressCallback? + ): Task? { + val content: String + var responseStream: InputStream? = null + try { + responseStream = response!!.content + content = String(ParseIOUtils.toByteArray(responseStream)) + } catch (e: IOException) { + return Task.forError(e) + } finally { + ParseIOUtils.closeQuietly(responseStream) + } + + // We need to check for errors differently in /1/ than /2/ since object data in /2/ was + // encapsulated in "data" and everything was 200, but /2/ everything is in the root JSON, + // but errors are status 4XX. + // See https://quip.com/4pbbA9HbOPjQ + val statusCode = response!!.statusCode + if (statusCode in 200..599) { // Assume 3XX is handled by http library + val json: JSONObject + return try { + json = JSONObject(content) + if (statusCode in 400..499) { // 4XX + return Task.forError( + newPermanentException( + json.optInt("code"), + json.optString("error") + ) + ) + } else if (statusCode >= 500) { // 5XX + return Task.forError( + newTemporaryException( + json.optInt("code"), + json.optString("error") + ) + ) + } + Task.forResult(json) + } catch (e: JSONException) { + Task.forError(newTemporaryException("bad json response", e)) + } + } + return Task.forError(newPermanentException(ParseException.OTHER_CAUSE, content)) + }// Include the session token in the cache in order to avoid mixing permissions. + + // Creates a somewhat-readable string that uniquely identifies this command. + val cacheKey: String + get() { + var json: String? = if (jsonParameters != null) { + try { + toDeterministicString(jsonParameters) + } catch (e: JSONException) { + throw RuntimeException(e.message) + } + } else { + "" + } + + // Include the session token in the cache in order to avoid mixing permissions. + if (sessionToken != null) { + json += sessionToken + } + return String.format( + "ParseRESTCommand.%s.%s.%s", + method.toString(), + ParseDigestUtils.md5(httpPath), + ParseDigestUtils.md5(json) + ) + } + + fun toJSONObject(): JSONObject { + val jsonObject = JSONObject() + try { + if (httpPath != null) { + jsonObject.put("httpPath", httpPath) + } + jsonObject.put("httpMethod", method.toString()) + if (jsonParameters != null) { + jsonObject.put("parameters", jsonParameters) + } + if (sessionToken != null) { + jsonObject.put("sessionToken", sessionToken) + } + if (localId != null) { + jsonObject.put("localId", localId) + } + } catch (e: JSONException) { + throw RuntimeException(e.message) + } + return jsonObject + } + + /** + * If this was the second save on a new object while offline, then its objectId wasn't yet set + * when the command was created, so it would have been considered a "create". But if the first + * save succeeded, then there is an objectId now, and it will be mapped to the localId for this + * command's result. If so, change the "create" operation to an "update", and add the objectId to + * the command. + */ + private fun maybeChangeServerOperation() { + if (localId != null) { + val objectId = localIdManager.getObjectId(localId!!) + if (objectId != null) { + localId = null + httpPath += String.format("/%s", objectId) + url = createUrl(httpPath) + if (httpPath!!.startsWith("classes") && method == ParseHttpRequest.Method.POST) { + method = ParseHttpRequest.Method.PUT + } + } + } + } + + fun resolveLocalIds() { + try { + val localPointers = ArrayList() + getLocalPointersIn(jsonParameters, localPointers) + for (pointer in localPointers) { + val localId = pointer["localId"] as String + val objectId = localIdManager.getObjectId(localId) + ?: throw IllegalStateException( + "Tried to serialize a command referencing a new, unsaved object." + ) + pointer.put("objectId", objectId) + pointer.remove("localId") + } + maybeChangeServerOperation() + } catch (e: JSONException) { + // Well, nothing to do here... + } + } + + /** + * Finds all of the local ids in this command and increments their retain counts in the on-disk + * store. This should be called immediately before serializing the command to disk, so that we + * know we might need to resolve these local ids at some point in the future. + */ + fun retainLocalIds() { + if (localId != null) { + localIdManager.retainLocalIdOnDisk(localId!!) + } + try { + val localPointers = ArrayList() + getLocalPointersIn(jsonParameters, localPointers) + for (pointer in localPointers) { + val localId = pointer["localId"] as String + localIdManager.retainLocalIdOnDisk(localId) + } + } catch (e: JSONException) { + // Well, nothing to do here... + } + } + + /** + * Finds all of the local ids in this command and decrements their retain counts in the on-disk + * store. This should be called when removing a serialized command from the disk, when we know + * that we will never need to resolve these local ids for this command again in the future. + */ + fun releaseLocalIds() { + if (localId != null) { + localIdManager.releaseLocalIdOnDisk(localId!!) + } + try { + val localPointers = ArrayList() + getLocalPointersIn(jsonParameters, localPointers) + for (pointer in localPointers) { + val localId = pointer["localId"] as String + localIdManager.releaseLocalIdOnDisk(localId) + } + } catch (e: JSONException) { + // Well, nothing to do here... + } + } + + /* package */ + internal abstract class Init?> { + var masterKey: String? = null + var sessionToken: String? = null + var installationId: String? = null + var method = ParseHttpRequest.Method.GET + var httpPath: String? = null + var jsonParameters: JSONObject? = null + var operationSetUUID: String? = null + var localId: String? = null + + /* package */ + abstract fun self(): T + fun sessionToken(sessionToken: String?): T { + this.sessionToken = sessionToken + return self() + } + + fun installationId(installationId: String?): T { + this.installationId = installationId + return self() + } + + fun masterKey(masterKey: String?): T { + this.masterKey = masterKey + return self() + } + + fun method(method: ParseHttpRequest.Method): T { + this.method = method + return self() + } + + fun httpPath(httpPath: String?): T { + this.httpPath = httpPath + return self() + } + + fun jsonParameters(jsonParameters: JSONObject?): T { + this.jsonParameters = jsonParameters + return self() + } + + fun operationSetUUID(operationSetUUID: String?): T { + this.operationSetUUID = operationSetUUID + return self() + } + + fun localId(localId: String?): T { + this.localId = localId + return self() + } + } + + class Builder : Init() { + override fun /* package */self(): Builder { + return this + } + + fun build(): ParseRESTCommand { + return ParseRESTCommand(this) + } + } + + companion object { + /* package */ + const val HEADER_APPLICATION_ID = "X-Parse-Application-Id" + + /* package */ + const val HEADER_CLIENT_KEY = "X-Parse-Client-Key" + + /* package */ + const val HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version" + + /* package */ + const val HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version" + + /* package */ + const val HEADER_OS_VERSION = "X-Parse-OS-Version" + + /* package */ + const val HEADER_INSTALLATION_ID = "X-Parse-Installation-Id" + + /* package */ + const val USER_AGENT = "User-Agent" + private const val HEADER_SESSION_TOKEN = "X-Parse-Session-Token" + private const val HEADER_MASTER_KEY = "X-Parse-Master-Key" + private const val PARAMETER_METHOD_OVERRIDE = "_method" + + // Set via Parse.initialize(Configuration) + /* package */ + @JvmField + var server: URL? = null + private val localIdManager: LocalIdManager + get() = ParseCorePlugins.getInstance().localIdManager + + @JvmStatic + fun fromJSONObject(jsonObject: JSONObject): ParseRESTCommand { + val httpPath = jsonObject.optString("httpPath") + val httpMethod = ParseHttpRequest.Method.fromString(jsonObject.optString("httpMethod")) + val sessionToken = jsonObject.optString("sessionToken", null) + val localId = jsonObject.optString("localId", null) + val jsonParameters = jsonObject.optJSONObject("parameters") + return ParseRESTCommand(httpPath, httpMethod, jsonParameters, localId, sessionToken) + } + + private fun createUrl(httpPath: String?): String { + // We send all parameters for GET/HEAD/DELETE requests in a post body, + // so no need to worry about query parameters here. + return if (httpPath == null) { + server.toString() + } else try { + URL(server, httpPath).toString() + } catch (ex: MalformedURLException) { + throw RuntimeException(ex) + } + } + + // Encodes the object to JSON, but ensures that JSONObjects + // and nested JSONObjects are encoded with keys in alphabetical order. + @JvmStatic + @Throws(JSONException::class) + fun toDeterministicString(o: Any): String { + val stringer = JSONStringer() + addToStringer(stringer, o) + return stringer.toString() + } + + // Uses the provided JSONStringer to encode this object to JSON, but ensures that JSONObjects and + // nested JSONObjects are encoded with keys in alphabetical order. + @Throws(JSONException::class) + private fun addToStringer(stringer: JSONStringer, o: Any) { + if (o is JSONObject) { + stringer.`object`() + val `object` = o + val keyIterator = `object`.keys() + val keys = ArrayList() + while (keyIterator.hasNext()) { + keys.add(keyIterator.next()) + } + keys.sort() + for (key in keys) { + stringer.key(key) + addToStringer(stringer, `object`.opt(key)) + } + stringer.endObject() + return + } + if (o is JSONArray) { + val array = o + stringer.array() + for (i in 0 until array.length()) { + addToStringer(stringer, array[i]) + } + stringer.endArray() + return + } + stringer.value(o) + } + + /* package */ + @JvmStatic + fun isValidCommandJSONObject(jsonObject: JSONObject): Boolean { + return jsonObject.has("httpPath") + } + + // This function checks whether a json object is a valid /2 ParseCommand json. + /* package */ + @JvmStatic + fun isValidOldFormatCommandJSONObject(jsonObject: JSONObject): Boolean { + return jsonObject.has("op") + } + + @Throws(JSONException::class) + protected fun getLocalPointersIn(container: Any?, localPointers: ArrayList) { + if (container is JSONObject) { + if ("Pointer" == container.opt("__type") && container.has("localId")) { + localPointers.add(container) + return + } + val keyIterator = container.keys() + while (keyIterator.hasNext()) { + val key = keyIterator.next() + getLocalPointersIn(container[key], localPointers) + } + } + if (container is JSONArray) { + for (i in 0 until container.length()) { + getLocalPointersIn(container[i], localPointers) + } + } + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseRESTFileCommand.java b/parse/src/main/java/com/parse/ParseRESTFileCommand.java index 02e37ebcd..f190f78e6 100644 --- a/parse/src/main/java/com/parse/ParseRESTFileCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTFileCommand.java @@ -21,6 +21,7 @@ class ParseRESTFileCommand extends ParseRESTCommand { private final byte[] data; private final String contentType; private final File file; + public ParseRESTFileCommand(Builder builder) { super(builder); if (builder.file != null && builder.data != null) { diff --git a/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java b/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java deleted file mode 100644 index 75fa1bd09..000000000 --- a/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 com.parse.http.ParseHttpRequest; -import com.parse.http.ParseHttpResponse; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -class ParseRESTObjectBatchCommand extends ParseRESTCommand { - public final static int COMMAND_OBJECT_BATCH_MAX_SIZE = 50; - - private static final String KEY_RESULTS = "results"; - - private ParseRESTObjectBatchCommand( - String httpPath, - ParseHttpRequest.Method httpMethod, - JSONObject parameters, - String sessionToken) { - super(httpPath, httpMethod, parameters, sessionToken); - } - - public static List> executeBatch( - ParseHttpClient client, List commands, String sessionToken) { - final int batchSize = commands.size(); - List> tasks = new ArrayList<>(batchSize); - - if (batchSize == 1) { - // There's only one, just execute it - tasks.add(commands.get(0).executeAsync(client)); - return tasks; - } - - if (batchSize > COMMAND_OBJECT_BATCH_MAX_SIZE) { - // There's more than the max, split it up into batches - List> batches = Lists.partition(commands, - COMMAND_OBJECT_BATCH_MAX_SIZE); - for (int i = 0, size = batches.size(); i < size; i++) { - List batch = batches.get(i); - tasks.addAll(executeBatch(client, batch, sessionToken)); - } - return tasks; - } - - final List> tcss = new ArrayList<>(batchSize); - for (int i = 0; i < batchSize; i++) { - TaskCompletionSource tcs = new TaskCompletionSource<>(); - tcss.add(tcs); - tasks.add(tcs.getTask()); - } - - JSONObject parameters = new JSONObject(); - JSONArray requests = new JSONArray(); - try { - for (ParseRESTObjectCommand command : commands) { - JSONObject requestParameters = new JSONObject(); - requestParameters.put("method", command.method.toString()); - requestParameters.put("path", new URL(server, command.httpPath).getPath()); - JSONObject body = command.jsonParameters; - if (body != null) { - requestParameters.put("body", body); - } - requests.put(requestParameters); - } - parameters.put("requests", requests); - } catch (JSONException | MalformedURLException e) { - throw new RuntimeException(e); - } - - ParseRESTCommand command = new ParseRESTObjectBatchCommand( - "batch", ParseHttpRequest.Method.POST, parameters, sessionToken); - - command.executeAsync(client).continueWith(new Continuation() { - @Override - public Void then(Task task) throws Exception { - TaskCompletionSource tcs; - - if (task.isFaulted() || task.isCancelled()) { - // REST command failed or canceled, fail or cancel all tasks - for (int i = 0; i < batchSize; i++) { - tcs = tcss.get(i); - if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setCancelled(); - } - } - } - - JSONObject json = task.getResult(); - JSONArray results = json.getJSONArray(KEY_RESULTS); - - int resultLength = results.length(); - if (resultLength != batchSize) { - // Invalid response, fail all tasks - for (int i = 0; i < batchSize; i++) { - tcs = tcss.get(i); - tcs.setError(new IllegalStateException( - "Batch command result count expected: " + batchSize + " but was: " + resultLength)); - } - } - - for (int i = 0; i < batchSize; i++) { - JSONObject result = results.getJSONObject(i); - tcs = tcss.get(i); - - if (result.has("success")) { - JSONObject success = result.getJSONObject("success"); - tcs.setResult(success); - } else if (result.has("error")) { - JSONObject error = result.getJSONObject("error"); - tcs.setError(new ParseException(error.getInt("code"), error.getString("error"))); - } - } - return null; - } - }); - - return tasks; - } - - /** - * /batch is the only endpoint that doesn't return a JSONObject... It returns a JSONArray, but - * let's wrap that with a JSONObject {@code { "results": <original response%gt; }}. - */ - @Override - protected Task onResponseAsync(ParseHttpResponse response, - ProgressCallback downloadProgressCallback) { - InputStream responseStream = null; - String content; - try { - responseStream = response.getContent(); - content = new String(ParseIOUtils.toByteArray(responseStream)); - } catch (IOException e) { - return Task.forError(e); - } finally { - ParseIOUtils.closeQuietly(responseStream); - } - - JSONObject json; - try { - JSONArray results = new JSONArray(content); - json = new JSONObject(); - json.put(KEY_RESULTS, results); - } catch (JSONException e) { - return Task.forError(newTemporaryException("bad json response", e)); - } - - return Task.forResult(json); - } -} diff --git a/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.kt b/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.kt new file mode 100644 index 000000000..e1584e4b7 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseRESTObjectBatchCommand.kt @@ -0,0 +1,163 @@ +/* + * 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 com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import com.parse.http.ParseHttpRequest +import com.parse.http.ParseHttpResponse +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.InputStream +import java.net.MalformedURLException +import java.net.URL +import java.util.* + +internal class ParseRESTObjectBatchCommand private constructor( + httpPath: String, + httpMethod: ParseHttpRequest.Method, + parameters: JSONObject, + sessionToken: String? +) : ParseRESTCommand(httpPath, httpMethod, parameters, sessionToken) { + /** + * /batch is the only endpoint that doesn't return a JSONObject... It returns a JSONArray, but + * let's wrap that with a JSONObject `{ "results": <original response%gt; }`. + */ + override fun onResponseAsync( + response: ParseHttpResponse?, + downloadProgressCallback: ProgressCallback? + ): Task? { + var responseStream: InputStream? = null + val content: String + try { + responseStream = response!!.content + content = String(ParseIOUtils.toByteArray(responseStream)) + } catch (e: IOException) { + return Task.forError(e) + } finally { + ParseIOUtils.closeQuietly(responseStream) + } + val json: JSONObject + try { + val results = JSONArray(content) + json = JSONObject() + json.put(KEY_RESULTS, results) + } catch (e: JSONException) { + return Task.forError(newTemporaryException("bad json response", e)) + } + return Task.forResult(json) + } + + companion object { + private const val COMMAND_OBJECT_BATCH_MAX_SIZE = 50 + private const val KEY_RESULTS = "results" + @JvmStatic + fun executeBatch( + client: ParseHttpClient?, commands: List, sessionToken: String? + ): List> { + val batchSize = commands.size + val tasks: MutableList> = ArrayList(batchSize) + if (batchSize == 1) { + // There's only one, just execute it + tasks.add(commands[0].executeAsync(client!!)) + return tasks + } + if (batchSize > COMMAND_OBJECT_BATCH_MAX_SIZE) { + // There's more than the max, split it up into batches + val batches = commands.chunked(COMMAND_OBJECT_BATCH_MAX_SIZE) + var i = 0 + val size = batches.size + while (i < size) { + val batch = batches[i] + tasks.addAll(executeBatch(client, batch, sessionToken)) + i++ + } + return tasks + } + val tcss: MutableList> = ArrayList(batchSize) + for (i in 0 until batchSize) { + val tcs = TaskCompletionSource() + tcss.add(tcs) + tasks.add(tcs.task) + } + val parameters = JSONObject() + val requests = JSONArray() + try { + for (command in commands) { + val requestParameters = JSONObject() + requestParameters.put("method", command.method.toString()) + requestParameters.put("path", URL(server, command.httpPath).path) + val body = command.jsonParameters + if (body != null) { + requestParameters.put("body", body) + } + requests.put(requestParameters) + } + parameters.put("requests", requests) + } catch (e: JSONException) { + throw RuntimeException(e) + } catch (e: MalformedURLException) { + throw RuntimeException(e) + } + val command: ParseRESTCommand = ParseRESTObjectBatchCommand( + "batch", ParseHttpRequest.Method.POST, parameters, sessionToken + ) + command.executeAsync(client!!) + .continueWith(Continuation { task: Task -> + var tcs: TaskCompletionSource + if (task.isFaulted || task.isCancelled) { + // REST command failed or canceled, fail or cancel all tasks + for (i in 0 until batchSize) { + tcs = tcss[i] + if (task.isFaulted) { + tcs.setError(task.error) + } else { + tcs.setCancelled() + } + } + } + val json = task.result + val results = json.getJSONArray(KEY_RESULTS) + val resultLength = results.length() + if (resultLength != batchSize) { + // Invalid response, fail all tasks + for (i in 0 until batchSize) { + tcs = tcss[i] + tcs.setError( + IllegalStateException( + "Batch command result count expected: $batchSize but was: $resultLength" + ) + ) + } + } + for (i in 0 until batchSize) { + val result = results.getJSONObject(i) + tcs = tcss[i] + if (result.has("success")) { + val success = result.getJSONObject("success") + tcs.setResult(success) + } else if (result.has("error")) { + val error = result.getJSONObject("error") + tcs.setError( + ParseException( + error.getInt("code"), + error.getString("error") + ) + ) + } + } + null + }) + return tasks + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseRESTUserCommand.java b/parse/src/main/java/com/parse/ParseRESTUserCommand.java index a3df33438..48def5b87 100644 --- a/parse/src/main/java/com/parse/ParseRESTUserCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTUserCommand.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -17,13 +18,11 @@ import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - class ParseRESTUserCommand extends ParseRESTCommand { private static final String HEADER_REVOCABLE_SESSION = "X-Parse-Revocable-Session"; private static final String HEADER_TRUE = "1"; - private boolean isRevocableSessionEnabled; + private final boolean isRevocableSessionEnabled; //region Authentication private int statusCode; diff --git a/parse/src/main/java/com/parse/ParseRelation.java b/parse/src/main/java/com/parse/ParseRelation.java index 257105bc9..09c35580b 100644 --- a/parse/src/main/java/com/parse/ParseRelation.java +++ b/parse/src/main/java/com/parse/ParseRelation.java @@ -37,6 +37,8 @@ public ParseRelation[] newArray(int size) { } }; private final Object mutex = new Object(); + // For offline caching, we keep track of every object we've known to be in the relation. + private final Set knownObjects = new HashSet<>(); // The owning object of this ParseRelation. private WeakReference parent; // The object Id of the parent. @@ -47,8 +49,6 @@ public ParseRelation[] newArray(int size) { private String key; // The className of the target objects. private String targetClass; - // For offline caching, we keep track of every object we've known to be in the relation. - private Set knownObjects = new HashSet<>(); /* package */ ParseRelation(ParseObject parent, String key) { this.parent = new WeakReference<>(parent); diff --git a/parse/src/main/java/com/parse/ParseRequest.java b/parse/src/main/java/com/parse/ParseRequest.java deleted file mode 100644 index efd5443bf..000000000 --- a/parse/src/main/java/com/parse/ParseRequest.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * 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 androidx.annotation.NonNull; - -import com.parse.http.ParseHttpBody; -import com.parse.http.ParseHttpRequest; -import com.parse.http.ParseHttpResponse; - -import java.io.IOException; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - -/** - * ParseRequest takes an arbitrary HttpUriRequest and retries it a number of times with - * exponential backoff. - */ -abstract class ParseRequest { - - protected static final int DEFAULT_MAX_RETRIES = 4; - /* package */ static final long DEFAULT_INITIAL_RETRY_DELAY = 1000L; - private static final ThreadFactory sThreadFactory = new ThreadFactory() { - private final AtomicInteger mCount = new AtomicInteger(1); - - public Thread newThread(@NonNull Runnable r) { - return new Thread(r, "ParseRequest.NETWORK_EXECUTOR-thread-" + mCount.getAndIncrement()); - } - }; - /** - * We want to use more threads than default in {@code bolts.Executors} since most of the time - * the threads will be asleep waiting for data. - */ - private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); - private static final int CORE_POOL_SIZE = CPU_COUNT * 2 + 1; - private static final int MAX_POOL_SIZE = CPU_COUNT * 2 * 2 + 1; - private static final long KEEP_ALIVE_TIME = 1L; - private static final int MAX_QUEUE_SIZE = 128; - protected static final ExecutorService NETWORK_EXECUTOR = newThreadPoolExecutor( - CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, - new LinkedBlockingQueue(MAX_QUEUE_SIZE), sThreadFactory); - private static long defaultInitialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY; - /* package */ ParseHttpRequest.Method method; - /* package */ String url; - - public ParseRequest(String url) { - this(ParseHttpRequest.Method.GET, url); - } - - public ParseRequest(ParseHttpRequest.Method method, String url) { - this.method = method; - this.url = url; - } - - private static ThreadPoolExecutor newThreadPoolExecutor(int corePoolSize, int maxPoolSize, - long keepAliveTime, TimeUnit timeUnit, BlockingQueue workQueue, - ThreadFactory threadFactory) { - ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, - keepAliveTime, timeUnit, workQueue, threadFactory); - executor.allowCoreThreadTimeOut(true); - return executor; - } - - public static long getDefaultInitialRetryDelay() { - return defaultInitialRetryDelay; - } - - public static void setDefaultInitialRetryDelay(long delay) { - defaultInitialRetryDelay = delay; - } - - private static int maxRetries() { - //typically happens just within tests - if (ParsePlugins.get() == null) { - return DEFAULT_MAX_RETRIES; - } else { - return ParsePlugins.get().configuration().maxRetries; - } - } - - protected ParseHttpBody newBody(ProgressCallback uploadProgressCallback) { - // do nothing - return null; - } - - protected ParseHttpRequest newRequest( - ParseHttpRequest.Method method, - String url, - ProgressCallback uploadProgressCallback) { - ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder() - .setMethod(method) - .setUrl(url); - - switch (method) { - case GET: - case DELETE: - break; - case POST: - case PUT: - requestBuilder.setBody(newBody(uploadProgressCallback)); - break; - default: - throw new IllegalStateException("Invalid method " + method); - } - return requestBuilder.build(); - } - - /* - * Runs one iteration of the request. - */ - private Task sendOneRequestAsync( - final ParseHttpClient client, - final ParseHttpRequest request, - final ProgressCallback downloadProgressCallback) { - return Task.forResult(null).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - ParseHttpResponse response = client.execute(request); - return onResponseAsync(response, downloadProgressCallback); - } - }, NETWORK_EXECUTOR).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - Exception error = task.getError(); - if (error instanceof IOException) { - return Task.forError(newTemporaryException("i/o failure", error)); - } - } - return task; - // Jump off the network executor so this task continuations won't steal network threads - } - }, Task.BACKGROUND_EXECUTOR); - } - - protected abstract Task onResponseAsync(ParseHttpResponse response, - ProgressCallback downloadProgressCallback); - - /* - * Starts retrying the block. - */ - public Task executeAsync(final ParseHttpClient client) { - return executeAsync(client, (ProgressCallback) null, null, null); - } - - public Task executeAsync(final ParseHttpClient client, Task cancellationToken) { - return executeAsync(client, (ProgressCallback) null, null, cancellationToken); - } - - public Task executeAsync( - final ParseHttpClient client, - final ProgressCallback uploadProgressCallback, - final ProgressCallback downloadProgressCallback) { - return executeAsync(client, uploadProgressCallback, downloadProgressCallback, null); - } - - public Task executeAsync( - final ParseHttpClient client, - final ProgressCallback uploadProgressCallback, - final ProgressCallback downloadProgressCallback, - Task cancellationToken) { - ParseHttpRequest request = newRequest(method, url, uploadProgressCallback); - return executeAsync( - client, - request, - downloadProgressCallback, - cancellationToken); - } - - // Although we can not cancel a single request, but we allow cancel between retries so we need a - // cancellationToken here. - private Task executeAsync( - final ParseHttpClient client, - final ParseHttpRequest request, - final ProgressCallback downloadProgressCallback, - final Task cancellationToken) { - long delay = defaultInitialRetryDelay + (long) (defaultInitialRetryDelay * Math.random()); - - return executeAsync( - client, - request, - 0, - delay, - downloadProgressCallback, - cancellationToken); - } - - private Task executeAsync( - final ParseHttpClient client, - final ParseHttpRequest request, - final int attemptsMade, - final long delay, - final ProgressCallback downloadProgressCallback, - final Task cancellationToken) { - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } - return sendOneRequestAsync(client, request, downloadProgressCallback).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - Exception e = task.getError(); - if (task.isFaulted() && e instanceof ParseException) { - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } - - if (e instanceof ParseRequestException && - ((ParseRequestException) e).isPermanentFailure) { - return task; - } - - if (attemptsMade < maxRetries()) { - PLog.i("com.parse.ParseRequest", "Request failed. Waiting " + delay - + " milliseconds before attempt #" + (attemptsMade + 1)); - - final TaskCompletionSource retryTask = new TaskCompletionSource<>(); - ParseExecutors.scheduled().schedule(new Runnable() { - @Override - public void run() { - executeAsync( - client, - request, - attemptsMade + 1, - delay * 2, - downloadProgressCallback, - cancellationToken).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isCancelled()) { - retryTask.setCancelled(); - } else if (task.isFaulted()) { - retryTask.setError(task.getError()); - } else { - retryTask.setResult(task.getResult()); - } - return null; - } - }); - } - }, delay, TimeUnit.MILLISECONDS); - return retryTask.getTask(); - } - } - return task; - } - }); - } - - /** - * Constructs a permanent exception that won't be retried. - */ - protected ParseException newPermanentException(int code, String message) { - ParseRequestException e = new ParseRequestException(code, message); - e.isPermanentFailure = true; - return e; - } - - /** - * Constructs a temporary exception that will be retried. - */ - protected ParseException newTemporaryException(int code, String message) { - ParseRequestException e = new ParseRequestException(code, message); - e.isPermanentFailure = false; - return e; - } - - /** - * Constructs a temporary exception that will be retried with json error code 100. - * - * @see ParseException#CONNECTION_FAILED - */ - protected ParseException newTemporaryException(String message, Throwable t) { - ParseRequestException e = new ParseRequestException( - ParseException.CONNECTION_FAILED, message, t); - e.isPermanentFailure = false; - return e; - } - - private static class ParseRequestException extends ParseException { - boolean isPermanentFailure = false; - - public ParseRequestException(int theCode, String theMessage) { - super(theCode, theMessage); - } - - public ParseRequestException(int theCode, String message, Throwable cause) { - super(theCode, message, cause); - } - } -} diff --git a/parse/src/main/java/com/parse/ParseRequest.kt b/parse/src/main/java/com/parse/ParseRequest.kt new file mode 100644 index 000000000..ac2648aee --- /dev/null +++ b/parse/src/main/java/com/parse/ParseRequest.kt @@ -0,0 +1,292 @@ +/* + * 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 com.parse.PLog.i +import com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import com.parse.boltsinternal.TaskCompletionSource +import com.parse.http.ParseHttpBody +import com.parse.http.ParseHttpRequest +import com.parse.http.ParseHttpResponse +import java.io.IOException +import java.util.concurrent.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * ParseRequest takes an arbitrary HttpUriRequest and retries it a number of times with + * exponential backoff. + */ +internal abstract class ParseRequest(/* package */ + var method: ParseHttpRequest.Method, /* package */ + var url: String? +) { + constructor(url: String?) : this(ParseHttpRequest.Method.GET, url) + + protected open fun newBody(uploadProgressCallback: ProgressCallback?): ParseHttpBody? { + // do nothing + return null + } + + protected open fun newRequest( + method: ParseHttpRequest.Method, + url: String?, + uploadProgressCallback: ProgressCallback? + ): ParseHttpRequest { + val requestBuilder = ParseHttpRequest.Builder() + .setMethod(method) + .setUrl(url) + when (method) { + ParseHttpRequest.Method.GET, ParseHttpRequest.Method.DELETE -> { + } + ParseHttpRequest.Method.POST, ParseHttpRequest.Method.PUT -> requestBuilder.setBody( + newBody(uploadProgressCallback) + ) + } + return requestBuilder.build() + } + + /* + * Runs one iteration of the request. + */ + private fun sendOneRequestAsync( + client: ParseHttpClient, + request: ParseHttpRequest, + downloadProgressCallback: ProgressCallback? + ): Task { + return Task.forResult(null).onSuccessTask({ + val response = client.execute(request) + onResponseAsync(response, downloadProgressCallback) + }, NETWORK_EXECUTOR).continueWithTask(Continuation continueWithTask@{ task: Task -> + if (task.isFaulted) { + val error = task.error + if (error is IOException) { + return@continueWithTask Task.forError( + newTemporaryException( + "i/o failure", + error + ) + ) + } + } + task + }, Task.BACKGROUND_EXECUTOR) + } + + protected abstract fun onResponseAsync( + response: ParseHttpResponse?, + downloadProgressCallback: ProgressCallback? + ): Task? + + /* + * Starts retrying the block. + */ + fun executeAsync(client: ParseHttpClient): Task { + return executeAsync(client, null as ProgressCallback?, null, null) + } + + fun executeAsync(client: ParseHttpClient, cancellationToken: Task?): Task { + return executeAsync(client, null as ProgressCallback?, null, cancellationToken) + } + + fun executeAsync( + client: ParseHttpClient, + uploadProgressCallback: ProgressCallback?, + downloadProgressCallback: ProgressCallback? + ): Task { + return executeAsync(client, uploadProgressCallback, downloadProgressCallback, null) + } + + open fun executeAsync( + client: ParseHttpClient, + uploadProgressCallback: ProgressCallback?, + downloadProgressCallback: ProgressCallback?, + cancellationToken: Task? + ): Task { + val request = newRequest(method, url, uploadProgressCallback) + return executeAsync( + client, + request, + downloadProgressCallback, + cancellationToken + ) + } + + // Although we can not cancel a single request, but we allow cancel between retries so we need a + // cancellationToken here. + private fun executeAsync( + client: ParseHttpClient, + request: ParseHttpRequest, + downloadProgressCallback: ProgressCallback?, + cancellationToken: Task? + ): Task { + val delay = defaultInitialRetryDelay + (defaultInitialRetryDelay * Math.random()).toLong() + return executeAsync( + client, + request, + 0, + delay, + downloadProgressCallback, + cancellationToken + ) + } + + private fun executeAsync( + client: ParseHttpClient, + request: ParseHttpRequest, + attemptsMade: Int, + delay: Long, + downloadProgressCallback: ProgressCallback?, + cancellationToken: Task? + ): Task { + return if (cancellationToken != null && cancellationToken.isCancelled) { + Task.cancelled() + } else sendOneRequestAsync( + client, + request, + downloadProgressCallback + ).continueWithTask { task: Task -> + val e = task.error + if (task.isFaulted && e is ParseException) { + if (cancellationToken != null && cancellationToken.isCancelled) { + return@continueWithTask Task.cancelled() + } + if (e is ParseRequestException && + e.isPermanentFailure + ) { + return@continueWithTask task + } + if (attemptsMade < maxRetries()) { + i( + "com.parse.ParseRequest", "Request failed. Waiting " + delay + + " milliseconds before attempt #" + (attemptsMade + 1) + ) + val retryTask = TaskCompletionSource() + ParseExecutors.scheduled().schedule({ + executeAsync( + client, + request, + attemptsMade + 1, + delay * 2, + downloadProgressCallback, + cancellationToken + ).continueWithTask(Continuation?> { task1: Task -> + when { + task1.isCancelled -> { + retryTask.setCancelled() + } + task1.isFaulted -> { + retryTask.setError(task1.error) + } + else -> { + retryTask.setResult(task1.result) + } + } + null + }) + }, delay, TimeUnit.MILLISECONDS) + return@continueWithTask retryTask.task + } + } + task + } + } + + /** + * Constructs a permanent exception that won't be retried. + */ + protected fun newPermanentException(code: Int, message: String?): ParseException { + val e = ParseRequestException(code, message) + e.isPermanentFailure = true + return e + } + + /** + * Constructs a temporary exception that will be retried. + */ + protected fun newTemporaryException(code: Int, message: String?): ParseException { + val e = ParseRequestException(code, message) + e.isPermanentFailure = false + return e + } + + /** + * Constructs a temporary exception that will be retried with json error code 100. + * + * @see ParseException.CONNECTION_FAILED + */ + protected fun newTemporaryException(message: String?, t: Throwable?): ParseException { + val e = ParseRequestException( + ParseException.CONNECTION_FAILED, message, t + ) + e.isPermanentFailure = false + return e + } + + private class ParseRequestException : ParseException { + var isPermanentFailure = false + + constructor(theCode: Int, theMessage: String?) : super(theCode, theMessage) + constructor(theCode: Int, message: String?, cause: Throwable?) : super( + theCode, + message, + cause + ) + } + + companion object { + const val DEFAULT_MAX_RETRIES = 4 + + /* package */ + const val DEFAULT_INITIAL_RETRY_DELAY = 1000L + private val sThreadFactory: ThreadFactory = object : ThreadFactory { + private val mCount = AtomicInteger(1) + override fun newThread(r: Runnable): Thread { + return Thread(r, "ParseRequest.NETWORK_EXECUTOR-thread-" + mCount.getAndIncrement()) + } + } + + /** + * We want to use more threads than default in `bolts.Executors` since most of the time + * the threads will be asleep waiting for data. + */ + private val CPU_COUNT = Runtime.getRuntime().availableProcessors() + private val CORE_POOL_SIZE = CPU_COUNT * 2 + 1 + private val MAX_POOL_SIZE = CPU_COUNT * 2 * 2 + 1 + private const val KEEP_ALIVE_TIME = 1L + private const val MAX_QUEUE_SIZE = 128 + protected val NETWORK_EXECUTOR: ExecutorService = newThreadPoolExecutor( + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, + LinkedBlockingQueue(MAX_QUEUE_SIZE), sThreadFactory + ) + @JvmStatic + var defaultInitialRetryDelay = DEFAULT_INITIAL_RETRY_DELAY + private fun newThreadPoolExecutor( + corePoolSize: Int, maxPoolSize: Int, + keepAliveTime: Long, timeUnit: TimeUnit, workQueue: BlockingQueue, + threadFactory: ThreadFactory + ): ThreadPoolExecutor { + val executor = ThreadPoolExecutor( + corePoolSize, maxPoolSize, + keepAliveTime, timeUnit, workQueue, threadFactory + ) + executor.allowCoreThreadTimeOut(true) + return executor + } + + private fun maxRetries(): Int { + //typically happens just within tests + return if (ParsePlugins.get() == null) { + DEFAULT_MAX_RETRIES + } else { + ParsePlugins.get().configuration().maxRetries + } + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseSQLiteCursor.java b/parse/src/main/java/com/parse/ParseSQLiteCursor.java index 092c9db27..13f1d762a 100644 --- a/parse/src/main/java/com/parse/ParseSQLiteCursor.java +++ b/parse/src/main/java/com/parse/ParseSQLiteCursor.java @@ -18,11 +18,11 @@ import android.os.Build; import android.os.Bundle; +import com.parse.boltsinternal.Task; + import java.util.concurrent.Callable; import java.util.concurrent.Executor; -import com.parse.boltsinternal.Task; - /** * Wrapper class to invoke {@link Cursor#close()} on a specific thread on Android versions below * android-14 as they require {@link Cursor#close()} to be called on the same thread the cursor @@ -32,8 +32,9 @@ */ class ParseSQLiteCursor implements Cursor { - private Cursor cursor; - private Executor executor; + private final Cursor cursor; + private final Executor executor; + private ParseSQLiteCursor(Cursor cursor, Executor executor) { this.cursor = cursor; this.executor = executor; @@ -197,12 +198,9 @@ public boolean requery() { @Override public void close() { // Basically close _eventually_. - Task.call(new Callable() { - @Override - public Void call() { - cursor.close(); - return null; - } + Task.call((Callable) () -> { + cursor.close(); + return null; }, executor); } diff --git a/parse/src/main/java/com/parse/ParseSQLiteDatabase.java b/parse/src/main/java/com/parse/ParseSQLiteDatabase.java index c1e72dd4d..689a5a0d7 100644 --- a/parse/src/main/java/com/parse/ParseSQLiteDatabase.java +++ b/parse/src/main/java/com/parse/ParseSQLiteDatabase.java @@ -13,13 +13,12 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; import com.parse.boltsinternal.TaskCompletionSource; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + class ParseSQLiteDatabase { /** @@ -39,9 +38,9 @@ class ParseSQLiteDatabase { private static final TaskQueue taskQueue = new TaskQueue(); private final Object currentLock = new Object(); private final TaskCompletionSource tcs = new TaskCompletionSource<>(); + private final int openFlags; private SQLiteDatabase db; private Task current = null; - private int openFlags; /** * Creates a Session which opens a database connection and begins a transaction @@ -51,36 +50,23 @@ private ParseSQLiteDatabase(int flags) { //TODO (grantland): if (!writable) -- do we have to serialize everything? openFlags = flags; - taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - synchronized (currentLock) { - current = toAwait; - } - return tcs.getTask(); + taskQueue.enqueue(toAwait -> { + synchronized (currentLock) { + current = toAwait; } + return tcs.getTask(); }); } /* protected */ static Task openDatabaseAsync(final SQLiteOpenHelper helper, int flags) { final ParseSQLiteDatabase db = new ParseSQLiteDatabase(flags); - return db.open(helper).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return Task.forResult(db); - } - }); + return db.open(helper).continueWithTask(task -> Task.forResult(db)); } public Task isReadOnlyAsync() { synchronized (currentLock) { - Task task = current.continueWith(new Continuation() { - @Override - public Boolean then(Task task) { - return db.isReadOnly(); - } - }); + Task task = current.continueWith(task1 -> db.isReadOnly()); current = task.makeVoid(); return task; } @@ -88,12 +74,7 @@ public Boolean then(Task task) { public Task isOpenAsync() { synchronized (currentLock) { - Task task = current.continueWith(new Continuation() { - @Override - public Boolean then(Task task) { - return db.isOpen(); - } - }); + Task task = current.continueWith(task1 -> db.isOpen()); current = task.makeVoid(); return task; } @@ -105,21 +86,15 @@ public boolean inTransaction() { /* package */ Task open(final SQLiteOpenHelper helper) { synchronized (currentLock) { - current = current.continueWith(new Continuation() { - @Override - public SQLiteDatabase then(Task task) { - // get*Database() is synchronous and calls through SQLiteOpenHelper#onCreate, onUpdate, - // etc. - return (openFlags & SQLiteDatabase.OPEN_READONLY) == SQLiteDatabase.OPEN_READONLY - ? helper.getReadableDatabase() - : helper.getWritableDatabase(); - } - }, dbExecutor).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - db = task.getResult(); - return task.makeVoid(); - } + current = current.continueWith(task -> { + // get*Database() is synchronous and calls through SQLiteOpenHelper#onCreate, onUpdate, + // etc. + return (openFlags & SQLiteDatabase.OPEN_READONLY) == SQLiteDatabase.OPEN_READONLY + ? helper.getReadableDatabase() + : helper.getWritableDatabase(); + }, dbExecutor).continueWithTask(task -> { + db = task.getResult(); + return task.makeVoid(); }, Task.BACKGROUND_EXECUTOR); // We want to jump off the dbExecutor return current; } @@ -132,19 +107,13 @@ public Task then(Task task) { */ public Task beginTransactionAsync() { synchronized (currentLock) { - current = current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - db.beginTransaction(); - return task; - } + current = current.continueWithTask(task -> { + db.beginTransaction(); + return task; }, dbExecutor); - return current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return current.continueWithTask(task -> { + // We want to jump off the dbExecutor + return task; }, Task.BACKGROUND_EXECUTOR); } } @@ -156,19 +125,13 @@ public Task then(Task task) { */ public Task setTransactionSuccessfulAsync() { synchronized (currentLock) { - current = current.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - db.setTransactionSuccessful(); - return task; - } + current = current.onSuccessTask(task -> { + db.setTransactionSuccessful(); + return task; }, dbExecutor); - return current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return current.continueWithTask(task -> { + // We want to jump off the dbExecutor + return task; }, Task.BACKGROUND_EXECUTOR); } } @@ -180,20 +143,14 @@ public Task then(Task task) { */ public Task endTransactionAsync() { synchronized (currentLock) { - current = current.continueWith(new Continuation() { - @Override - public Void then(Task task) { - db.endTransaction(); - // We want to swallow any exceptions from our Session task - return null; - } + current = current.continueWith(task -> { + db.endTransaction(); + // We want to swallow any exceptions from our Session task + return null; }, dbExecutor); - return current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return current.continueWithTask(task -> { + // We want to jump off the dbExecutor + return task; }, Task.BACKGROUND_EXECUTOR); } } @@ -204,23 +161,17 @@ public Task then(Task task) { */ public Task closeAsync() { synchronized (currentLock) { - current = current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - try { - db.close(); - } finally { - tcs.setResult(null); - } - return tcs.getTask(); + current = current.continueWithTask(task -> { + try { + db.close(); + } finally { + tcs.setResult(null); } + return tcs.getTask(); }, dbExecutor); - return current.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return current.continueWithTask(task -> { + // We want to jump off the dbExecutor + return task; }, Task.BACKGROUND_EXECUTOR); } } @@ -233,29 +184,18 @@ public Task then(Task task) { public Task queryAsync(final String table, final String[] select, final String where, final String[] args) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Cursor then(Task task) { - return db.query(table, select, where, args, null, null, null); - } - }, dbExecutor).onSuccess(new Continuation() { - @Override - public Cursor then(Task task) { - Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor); - /* Ensure the cursor window is filled on the dbExecutor thread. We need to do this because - * the cursor cannot be filled from a different thread than it was created on. - */ - cursor.getCount(); - return cursor; - } + Task task = current.onSuccess(task13 -> db.query(table, select, where, args, null, null, null), dbExecutor).onSuccess(task12 -> { + Cursor cursor = ParseSQLiteCursor.create(task12.getResult(), dbExecutor); + /* Ensure the cursor window is filled on the dbExecutor thread. We need to do this because + * the cursor cannot be filled from a different thread than it was created on. + */ + cursor.getCount(); + return cursor; }, dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR); } } @@ -268,19 +208,11 @@ public Task then(Task task) { public Task insertWithOnConflict(final String table, final ContentValues values, final int conflictAlgorithm) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Long then(Task task) { - return db.insertWithOnConflict(table, null, values, conflictAlgorithm); - } - }, dbExecutor); + Task task = current.onSuccess(task12 -> db.insertWithOnConflict(table, null, values, conflictAlgorithm), dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR).makeVoid(); } } @@ -292,19 +224,11 @@ public Task then(Task task) { */ public Task insertOrThrowAsync(final String table, final ContentValues values) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Long then(Task task) { - return db.insertOrThrow(table, null, values); - } - }, dbExecutor); + Task task = current.onSuccess(task12 -> db.insertOrThrow(table, null, values), dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR).makeVoid(); } } @@ -317,19 +241,11 @@ public Task then(Task task) { public Task updateAsync(final String table, final ContentValues values, final String where, final String[] args) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Integer then(Task task) { - return db.update(table, values, where, args); - } - }, dbExecutor); + Task task = current.onSuccess(task12 -> db.update(table, values, where, args), dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR); } } @@ -341,19 +257,11 @@ public Task then(Task task) { */ public Task deleteAsync(final String table, final String where, final String[] args) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Integer then(Task task) { - return db.delete(table, where, args); - } - }, dbExecutor); + Task task = current.onSuccess(task12 -> db.delete(table, where, args), dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR).makeVoid(); } } @@ -365,28 +273,17 @@ public Task then(Task task) { */ public Task rawQueryAsync(final String sql, final String[] args) { synchronized (currentLock) { - Task task = current.onSuccess(new Continuation() { - @Override - public Cursor then(Task task) { - return db.rawQuery(sql, args); - } - }, dbExecutor).onSuccess(new Continuation() { - @Override - public Cursor then(Task task) { - Cursor cursor = ParseSQLiteCursor.create(task.getResult(), dbExecutor); - // Ensure the cursor window is filled on the dbExecutor thread. We need to do this because - // the cursor cannot be filled from a different thread than it was created on. - cursor.getCount(); - return cursor; - } + Task task = current.onSuccess(task13 -> db.rawQuery(sql, args), dbExecutor).onSuccess(task12 -> { + Cursor cursor = ParseSQLiteCursor.create(task12.getResult(), dbExecutor); + // Ensure the cursor window is filled on the dbExecutor thread. We need to do this because + // the cursor cannot be filled from a different thread than it was created on. + cursor.getCount(); + return cursor; }, dbExecutor); current = task.makeVoid(); - return task.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - // We want to jump off the dbExecutor - return task; - } + return task.continueWithTask(task1 -> { + // We want to jump off the dbExecutor + return task1; }, Task.BACKGROUND_EXECUTOR); } } diff --git a/parse/src/main/java/com/parse/ParseSession.java b/parse/src/main/java/com/parse/ParseSession.java index 7824a379d..ca3b1d5d4 100644 --- a/parse/src/main/java/com/parse/ParseSession.java +++ b/parse/src/main/java/com/parse/ParseSession.java @@ -8,13 +8,12 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import java.util.Arrays; import java.util.Collections; import java.util.List; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The {@code ParseSession} is a local representation of session data that can be saved * and retrieved from the Parse cloud. @@ -44,21 +43,15 @@ private static ParseSessionController getSessionController() { * logged in. */ public static Task getCurrentSessionInBackground() { - return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String sessionToken = task.getResult(); - if (sessionToken == null) { - return Task.forResult(null); - } - return getSessionController().getSessionAsync(sessionToken).onSuccess(new Continuation() { - @Override - public ParseSession then(Task task) { - ParseObject.State result = task.getResult(); - return ParseObject.from(result); - } - }); + return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(task -> { + String sessionToken = task.getResult(); + if (sessionToken == null) { + return Task.forResult(null); } + return getSessionController().getSessionAsync(sessionToken).onSuccess(task1 -> { + State result = task1.getResult(); + return ParseObject.from(result); + }); }); } @@ -86,12 +79,9 @@ static Task upgradeToRevocableSessionAsync(String sessionToken) { return Task.forResult(sessionToken); } - return getSessionController().upgradeToRevocable(sessionToken).onSuccess(new Continuation() { - @Override - public String then(Task task) { - ParseObject.State result = task.getResult(); - return ParseObject.from(result).getSessionToken(); - } + return getSessionController().upgradeToRevocable(sessionToken).onSuccess(task -> { + State result = task.getResult(); + return ParseObject.from(result).getSessionToken(); }); } diff --git a/parse/src/main/java/com/parse/ParseSessionController.java b/parse/src/main/java/com/parse/ParseSessionController.kt similarity index 50% rename from parse/src/main/java/com/parse/ParseSessionController.java rename to parse/src/main/java/com/parse/ParseSessionController.kt index 79ebf6c49..936649245 100644 --- a/parse/src/main/java/com/parse/ParseSessionController.java +++ b/parse/src/main/java/com/parse/ParseSessionController.kt @@ -6,15 +6,12 @@ * 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; +package com.parse -import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.Task -interface ParseSessionController { - - Task getSessionAsync(String sessionToken); - - Task revokeAsync(String sessionToken); - - Task upgradeToRevocable(String sessionToken); -} +internal interface ParseSessionController { + fun getSessionAsync(sessionToken: String): Task + fun revokeAsync(sessionToken: String): Task + fun upgradeToRevocable(sessionToken: String): Task +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseTaskUtils.java b/parse/src/main/java/com/parse/ParseTaskUtils.java index 82e1bdb27..8f9b43621 100644 --- a/parse/src/main/java/com/parse/ParseTaskUtils.java +++ b/parse/src/main/java/com/parse/ParseTaskUtils.java @@ -8,13 +8,13 @@ */ package com.parse; -import java.util.concurrent.CancellationException; - import com.parse.boltsinternal.AggregateException; import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; import com.parse.boltsinternal.TaskCompletionSource; +import java.util.concurrent.CancellationException; + class ParseTaskUtils { /** @@ -72,12 +72,7 @@ static Task callbackOnMainThreadAsync(Task task, if (callback == null) { return task; } - return callbackOnMainThreadAsync(task, new ParseCallback2() { - @Override - public void done(Void aVoid, ParseException e) { - callback.done(e); - } - }, reportCancellation); + return callbackOnMainThreadAsync(task, (aVoid, e) -> callback.done(e), reportCancellation); } @@ -103,35 +98,29 @@ static Task callbackOnMainThreadAsync(Task task, return task; } final TaskCompletionSource tcs = new TaskCompletionSource(); - task.continueWith(new Continuation() { - @Override - public Void then(final Task task) { - if (task.isCancelled() && !reportCancellation) { - tcs.setCancelled(); - return null; - } - ParseExecutors.main().execute(new Runnable() { - @Override - public void run() { - try { - Exception error = task.getError(); - if (error != null && !(error instanceof ParseException)) { - error = new ParseException(error); - } - callback.done(task.getResult(), (ParseException) error); - } finally { - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(task.getResult()); - } - } - } - }); + task.continueWith((Continuation) task1 -> { + if (task1.isCancelled() && !reportCancellation) { + tcs.setCancelled(); return null; } + ParseExecutors.main().execute(() -> { + try { + Exception error = task1.getError(); + if (error != null && !(error instanceof ParseException)) { + error = new ParseException(error); + } + callback.done(task1.getResult(), (ParseException) error); + } finally { + if (task1.isCancelled()) { + tcs.setCancelled(); + } else if (task1.isFaulted()) { + tcs.setError(task1.getError()); + } else { + tcs.setResult(task1.getResult()); + } + } + }); + return null; }); return tcs.getTask(); } diff --git a/parse/src/main/java/com/parse/ParseTextUtils.java b/parse/src/main/java/com/parse/ParseTextUtils.java index 152d93e4a..cc7221a04 100644 --- a/parse/src/main/java/com/parse/ParseTextUtils.java +++ b/parse/src/main/java/com/parse/ParseTextUtils.java @@ -16,6 +16,8 @@ package com.parse; +import java.util.Objects; + /* package */ class ParseTextUtils { private ParseTextUtils() { @@ -63,6 +65,6 @@ public static boolean isEmpty(CharSequence text) { * @return true if a and b are equal */ public static boolean equals(CharSequence a, CharSequence b) { - return (a == b) || (a != null && a.equals(b)); + return Objects.equals(a, b); } } diff --git a/parse/src/main/java/com/parse/ParseUser.java b/parse/src/main/java/com/parse/ParseUser.java index b93dbb2db..935584d88 100644 --- a/parse/src/main/java/com/parse/ParseUser.java +++ b/parse/src/main/java/com/parse/ParseUser.java @@ -10,8 +10,12 @@ import android.os.Bundle; import android.os.Parcel; + import androidx.annotation.NonNull; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import java.util.ArrayList; @@ -23,9 +27,6 @@ import java.util.List; import java.util.Map; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - /** * The {@code ParseUser} is a local representation of user data that can be saved and retrieved from * the Parse cloud. @@ -100,18 +101,10 @@ public static Task logInInBackground(String username, String password throw new IllegalArgumentException("Must specify a password for the user to log in with"); } - return getUserController().logInAsync(username, password).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - State result = task.getResult(); - final ParseUser newCurrent = ParseObject.from(result); - return saveCurrentUserAsync(newCurrent).onSuccess(new Continuation() { - @Override - public ParseUser then(Task task) { - return newCurrent; - } - }); - } + return getUserController().logInAsync(username, password).onSuccessTask(task -> { + State result = task.getResult(); + final ParseUser newCurrent = ParseObject.from(result); + return saveCurrentUserAsync(newCurrent).onSuccess(task1 -> newCurrent); }); } @@ -162,19 +155,11 @@ public static Task becomeInBackground(String sessionToken) { throw new IllegalArgumentException("Must specify a sessionToken for the user to log in with"); } - return getUserController().getUserAsync(sessionToken).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - State result = task.getResult(); - - final ParseUser user = ParseObject.from(result); - return saveCurrentUserAsync(user).onSuccess(new Continuation() { - @Override - public ParseUser then(Task task) { - return user; - } - }); - } + return getUserController().getUserAsync(sessionToken).onSuccessTask(task -> { + State result = task.getResult(); + + final ParseUser user = ParseObject.from(result); + return saveCurrentUserAsync(user).onSuccess(task1 -> user); }); } @@ -390,97 +375,67 @@ public static Task logInWithInBackground( throw new IllegalArgumentException("Invalid authType: " + null); } - final Continuation> logInWithTask = new Continuation>() { - @Override - public Task then(Task task) { - return getUserController().logInAsync(authType, authData).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser.State result = task.getResult(); - final ParseUser user = ParseObject.from(result); - return saveCurrentUserAsync(user).onSuccess(new Continuation() { - @Override - public ParseUser then(Task task) { - return user; - } - }); - } - }); - } - }; + final Continuation> logInWithTask = task -> getUserController().logInAsync(authType, authData).onSuccessTask(task15 -> { + State result = task15.getResult(); + final ParseUser user = ParseObject.from(result); + return saveCurrentUserAsync(user).onSuccess(task14 -> user); + }); // Handle claiming of user. - return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser user = task.getResult(); - if (user != null) { - synchronized (user.mutex) { - if (ParseAnonymousUtils.isLinked(user)) { - if (user.isLazy()) { - final Map oldAnonymousData = - user.getAuthData(ParseAnonymousUtils.AUTH_TYPE); - return user.taskQueue.enqueue(new Continuation>() { - @Override - public Task then(final Task toAwait) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (user.mutex) { - // Replace any anonymity with the new linked authData. - user.stripAnonymity(); - user.putAuthData(authType, authData); - - return user.resolveLazinessAsync(task); - } - } - }).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (user.mutex) { - if (task.isFaulted()) { - user.removeAuthData(authType); - user.restoreAnonymity(oldAnonymousData); - return Task.forError(task.getError()); - } - if (task.isCancelled()) { - return Task.cancelled(); - } - return Task.forResult(user); - } - } - }); + return getCurrentUserController().getAsync(false).onSuccessTask(task -> { + final ParseUser user = task.getResult(); + if (user != null) { + synchronized (user.mutex) { + if (ParseAnonymousUtils.isLinked(user)) { + if (user.isLazy()) { + final Map oldAnonymousData = + user.getAuthData(ParseAnonymousUtils.AUTH_TYPE); + return user.taskQueue.enqueue(toAwait -> toAwait.continueWithTask(task13 -> { + synchronized (user.mutex) { + // Replace any anonymity with the new linked authData. + user.stripAnonymity(); + user.putAuthData(authType, authData); + + return user.resolveLazinessAsync(task13); + } + }).continueWithTask(task12 -> { + synchronized (user.mutex) { + if (task12.isFaulted()) { + user.removeAuthData(authType); + user.restoreAnonymity(oldAnonymousData); + return Task.forError(task12.getError()); } - }); - } else { - // Try to link the current user with third party user, unless a user is already linked - // to that third party user, then we'll just create a new user and link it with the - // third party user. New users will not be linked to the previous user's data. - return user.linkWithInBackground(authType, authData) - .continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isFaulted()) { - Exception error = task.getError(); - if (error instanceof ParseException - && ((ParseException) error).getCode() == ParseException.ACCOUNT_ALREADY_LINKED) { - // An account that's linked to the given authData already exists, so log in - // instead of trying to claim. - return Task.forResult(null).continueWithTask(logInWithTask); - } - } - if (task.isCancelled()) { - return Task.cancelled(); - } - return Task.forResult(user); + if (task12.isCancelled()) { + return Task.cancelled(); + } + return Task.forResult(user); + } + })); + } else { + // Try to link the current user with third party user, unless a user is already linked + // to that third party user, then we'll just create a new user and link it with the + // third party user. New users will not be linked to the previous user's data. + return user.linkWithInBackground(authType, authData) + .continueWithTask(task1 -> { + if (task1.isFaulted()) { + Exception error = task1.getError(); + if (error instanceof ParseException + && ((ParseException) error).getCode() == ParseException.ACCOUNT_ALREADY_LINKED) { + // An account that's linked to the given authData already exists, so log in + // instead of trying to claim. + return Task.forResult(null).continueWithTask(logInWithTask); } - }); - } + } + if (task1.isCancelled()) { + return Task.cancelled(); + } + return Task.forResult(user); + }); } } } - return Task.forResult(null).continueWithTask(logInWithTask); } + return Task.forResult(null).continueWithTask(logInWithTask); }); } @@ -529,15 +484,12 @@ public static Task enableRevocableSessionInBackground() { ParseCorePlugins.getInstance().registerUserController( new NetworkUserController(ParsePlugins.get().restClient(), true)); - return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser user = task.getResult(); - if (user == null) { - return Task.forResult(null); - } - return user.upgradeToRevocableSessionAsync(); + return getCurrentUserController().getAsync(false).onSuccessTask(task -> { + ParseUser user = task.getResult(); + if (user == null) { + return Task.forResult(null); } + return user.upgradeToRevocableSessionAsync(); }); } @@ -875,17 +827,7 @@ private void restoreAnonymity(Map anonymousData) { if (isCurrentUser()) { // If the user is the currently logged in user, we persist all data to disk - return task.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return cleanUpAuthDataAsync(); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return saveCurrentUserAsync(ParseUser.this); - } - }); + return task.onSuccessTask(task12 -> cleanUpAuthDataAsync()).onSuccessTask(task1 -> saveCurrentUserAsync(ParseUser.this)); } return task; @@ -920,22 +862,7 @@ public ParseUser fetch() throws ParseException { Task task = super.fetchAsync(sessionToken, toAwait); if (isCurrentUser()) { - return task.onSuccessTask(new Continuation>() { - @Override - public Task then(final Task fetchAsyncTask) { - return cleanUpAuthDataAsync(); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return saveCurrentUserAsync(ParseUser.this); - } - }).onSuccess(new Continuation() { - @Override - public T then(Task task) { - return (T) ParseUser.this; - } - }); + return task.onSuccessTask(fetchAsyncTask -> cleanUpAuthDataAsync()).onSuccessTask(task12 -> saveCurrentUserAsync(ParseUser.this)).onSuccess(task1 -> (T) ParseUser.this); } return task; @@ -956,12 +883,7 @@ public T then(Task task) { * @return A Task that is resolved when sign up completes. */ public Task signUpInBackground() { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task task) { - return signUpAsync(task); - } - }); + return taskQueue.enqueue(this::signUpAsync); } /* package for tests */ Task signUpAsync(Task toAwait) { @@ -1020,60 +942,46 @@ public Task then(Task task) { user.setPassword(getPassword()); revert(); - return user.saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (task.isCancelled() || task.isFaulted()) { // Error - synchronized (user.mutex) { - if (oldUsername != null) { - user.setUsername(oldUsername); - } else { - user.revert(KEY_USERNAME); - } - if (oldPassword != null) { - user.setPassword(oldPassword); - } else { - user.revert(KEY_PASSWORD); - } - user.restoreAnonymity(anonymousData); + return user.saveAsync(sessionToken, isLazy, toAwait).continueWithTask(task -> { + if (task.isCancelled() || task.isFaulted()) { // Error + synchronized (user.mutex) { + if (oldUsername != null) { + user.setUsername(oldUsername); + } else { + user.revert(KEY_USERNAME); } - return task; - } else { // Success - user.revert(KEY_PASSWORD); - revert(KEY_PASSWORD); + if (oldPassword != null) { + user.setPassword(oldPassword); + } else { + user.revert(KEY_PASSWORD); + } + user.restoreAnonymity(anonymousData); } - - mergeFromObject(user); - return saveCurrentUserAsync(ParseUser.this); + return task; + } else { // Success + user.revert(KEY_PASSWORD); + revert(KEY_PASSWORD); } + + mergeFromObject(user); + return saveCurrentUserAsync(ParseUser.this); }); } final ParseOperationSet operations = startSave(); - return toAwait.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getUserController().signUpAsync( - getState(), operations, sessionToken - ).continueWithTask(new Continuation>() { - @Override - public Task then(final Task signUpTask) { - ParseUser.State result = signUpTask.getResult(); - return handleSaveResultAsync(result, - operations).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (!signUpTask.isCancelled() && !signUpTask.isFaulted()) { - return saveCurrentUserAsync(ParseUser.this); - } - return signUpTask.makeVoid(); - } - }); - } - }); - } - }); + return toAwait.onSuccessTask(task -> getUserController().signUpAsync( + getState(), operations, sessionToken + ).continueWithTask(signUpTask -> { + State result = signUpTask.getResult(); + return handleSaveResultAsync(result, + operations).continueWithTask(task1 -> { + if (!signUpTask.isCancelled() && !signUpTask.isFaulted()) { + return saveCurrentUserAsync(ParseUser.this); + } + return signUpTask.makeVoid(); + }); + })); } } @@ -1197,15 +1105,12 @@ public boolean isLinked(String authType) { private Task synchronizeAuthDataAsync( ParseAuthenticationManager manager, final String authType, Map authData) { - return manager.restoreAuthenticationAsync(authType, authData).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - boolean success = !task.isFaulted() && task.getResult(); - if (!success) { - return unlinkFromInBackground(authType); - } - return task.makeVoid(); + return manager.restoreAuthenticationAsync(authType, authData).continueWithTask(task -> { + boolean success = !task.isFaulted() && task.getResult(); + if (!success) { + return unlinkFromInBackground(authType); } + return task.makeVoid(); }); } @@ -1221,17 +1126,14 @@ private Task linkWithAsync( stripAnonymity(); putAuthData(authType, authData); - return saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - synchronized (mutex) { - if (task.isFaulted() || task.isCancelled()) { - removeAuthData(authType); - restoreAnonymity(oldAnonymousData); - return task; - } - return synchronizeAuthDataAsync(authType); + return saveAsync(sessionToken, isLazy, toAwait).continueWithTask(task -> { + synchronized (mutex) { + if (task.isFaulted() || task.isCancelled()) { + removeAuthData(authType); + restoreAnonymity(oldAnonymousData); + return task; } + return synchronizeAuthDataAsync(authType); } }); } @@ -1243,12 +1145,7 @@ private Task linkWithAsync( final String authType, final Map authData, final String sessionToken) { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task task) { - return linkWithAsync(authType, authData, task, sessionToken); - } - }); + return taskQueue.enqueue(task -> linkWithAsync(authType, authData, task, sessionToken)); } /** @@ -1316,45 +1213,29 @@ public Task unlinkFromInBackground(final String authType) { final ParseOperationSet operations = startSave(); // Otherwise, treat this as a SignUpOrLogIn - return toAwait.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - return getUserController().logInAsync(getState(), operations).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - final ParseUser.State result = task.getResult(); - - Task resultTask; - // We can't merge this user with the server if this is a LogIn because LDS might - // already be keeping track of the servers objectId. - if (Parse.isLocalDatastoreEnabled() && !result.isNew()) { - resultTask = Task.forResult(result); - } else { - resultTask = handleSaveResultAsync(result, - operations).onSuccess(new Continuation() { - @Override - public ParseUser.State then(Task task) { - return result; - } - }); - } - return resultTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - ParseUser.State result = task.getResult(); - if (!result.isNew()) { - // If the result is not a new user, treat this as a fresh logIn with complete - // serverData, and switch the current user to the new user. - final ParseUser newUser = ParseObject.from(result); - return saveCurrentUserAsync(newUser); - } - return task.makeVoid(); - } - }); - } - }); + return toAwait.onSuccessTask(task -> getUserController().logInAsync(getState(), operations).onSuccessTask(task13 -> { + final State result = task13.getResult(); + + Task resultTask; + // We can't merge this user with the server if this is a LogIn because LDS might + // already be keeping track of the servers objectId. + if (Parse.isLocalDatastoreEnabled() && !result.isNew()) { + resultTask = Task.forResult(result); + } else { + resultTask = handleSaveResultAsync(result, + operations).onSuccess(task12 -> result); } - }); + return resultTask.onSuccessTask(task1 -> { + State result1 = task1.getResult(); + if (!result1.isNew()) { + // If the result is not a new user, treat this as a fresh logIn with complete + // serverData, and switch the current user to the new user. + final ParseUser newUser = ParseObject.from(result1); + return saveCurrentUserAsync(newUser); + } + return task1.makeVoid(); + }); + })); } } @@ -1391,27 +1272,14 @@ protected void onRestoreInstanceState(Bundle savedState) { //region Legacy/Revocable Session Tokens /* package */ Task upgradeToRevocableSessionAsync() { - return taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task toAwait) { - return upgradeToRevocableSessionAsync(toAwait); - } - }); + return taskQueue.enqueue(this::upgradeToRevocableSessionAsync); } private Task upgradeToRevocableSessionAsync(Task toAwait) { final String sessionToken = getSessionToken(); - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - return ParseSession.upgradeToRevocableSessionAsync(sessionToken); - } - }).onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) { - String result = task.getResult(); - return setSessionTokenInBackground(result); - } + return toAwait.continueWithTask(task -> ParseSession.upgradeToRevocableSessionAsync(sessionToken)).onSuccessTask(task -> { + String result = task.getResult(); + return setSessionTokenInBackground(result); }); } diff --git a/parse/src/main/java/com/parse/ParseUserController.java b/parse/src/main/java/com/parse/ParseUserController.java deleted file mode 100644 index 0ee548231..000000000 --- a/parse/src/main/java/com/parse/ParseUserController.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 java.util.Map; - -import com.parse.boltsinternal.Task; - -interface ParseUserController { - - Task signUpAsync( - ParseObject.State state, - ParseOperationSet operations, - String sessionToken); - - //region logInAsync - - Task logInAsync( - String username, String password); - - Task logInAsync( - ParseUser.State state, ParseOperationSet operations); - - Task logInAsync( - String authType, Map authData); - - //endregion - - Task getUserAsync(String sessionToken); - - Task requestPasswordResetAsync(String email); -} diff --git a/parse/src/main/java/com/parse/ParseUserController.kt b/parse/src/main/java/com/parse/ParseUserController.kt new file mode 100644 index 000000000..c868eb008 --- /dev/null +++ b/parse/src/main/java/com/parse/ParseUserController.kt @@ -0,0 +1,37 @@ +/* + * 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 com.parse.boltsinternal.Task + +internal interface ParseUserController { + fun signUpAsync( + state: ParseObject.State, + operations: ParseOperationSet, + sessionToken: String + ): Task + + //region logInAsync + + fun logInAsync( + username: String, password: String + ): Task + + fun logInAsync( + state: ParseUser.State, operations: ParseOperationSet + ): Task + + fun logInAsync( + authType: String, authData: Map = emptyMap() + ): Task + + //endregion + fun getUserAsync(sessionToken: String): Task + fun requestPasswordResetAsync(email: String): Task +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ParseUserCurrentCoder.java b/parse/src/main/java/com/parse/ParseUserCurrentCoder.java deleted file mode 100644 index dedd9eae9..000000000 --- a/parse/src/main/java/com/parse/ParseUserCurrentCoder.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.util.Iterator; -import java.util.Map; - -import static com.parse.ParseUser.State; - -/** - * Handles encoding/decoding ParseUser to/from /2 format JSON. /2 format json is only used for - * persisting current ParseUser and ParseInstallation to disk when LDS is not enabled. - */ -class ParseUserCurrentCoder extends ParseObjectCurrentCoder { - - private static final String KEY_AUTH_DATA = "auth_data"; - private static final String KEY_SESSION_TOKEN = "session_token"; - - private static final ParseUserCurrentCoder INSTANCE = new ParseUserCurrentCoder(); - - /* package */ ParseUserCurrentCoder() { - // do nothing - } - - public static ParseUserCurrentCoder get() { - return INSTANCE; - } - - /** - * Converts a ParseUser state to /2/ JSON representation suitable for saving to disk. - *

- *

-     * {
-     *   data: {
-     *     // data fields, including objectId, createdAt, updatedAt
-     *   },
-     *   classname: class name for the object,
-     *   operations: { } // operations per field
-     * }
-     * 
- *

- * All keys are included, regardless of whether they are dirty. - * We also add sessionToken and authData to the json. - * - * @see #decode(ParseObject.State.Init, JSONObject, ParseDecoder) - */ - @Override - public JSONObject encode( - T state, ParseOperationSet operations, ParseEncoder encoder) { - - // FYI we'll be double writing sessionToken and authData for now... - // This is important. super.encode() has no notion of sessionToken and authData, so it treats them - // like objects (simply passed to the encoder). This means that a null sessionToken will become - // JSONObject.NULL. This must be accounted in #decode(). - JSONObject objectJSON = super.encode(state, operations, encoder); - - String sessionToken = ((State) state).sessionToken(); - if (sessionToken != null) { - try { - objectJSON.put(KEY_SESSION_TOKEN, sessionToken); - } catch (JSONException e) { - throw new RuntimeException("could not encode value for key: session_token"); - } - } - - Map> authData = ((State) state).authData(); - if (authData.size() > 0) { - try { - objectJSON.put(KEY_AUTH_DATA, encoder.encode(authData)); - } catch (JSONException e) { - throw new RuntimeException("could not attach key: auth_data"); - } - } - - return objectJSON; - } - - /** - * Merges from JSON in /2/ format. - *

- * This is only used to read ParseUser state stored on disk in JSON. - * Since in encode we add sessionToken and authData to the json, we need remove them from json - * to generate state. - * - * @see #encode(ParseObject.State, ParseOperationSet, ParseEncoder) - */ - @Override - public > T decode( - T builder, JSONObject json, ParseDecoder decoder) { - ParseUser.State.Builder userBuilder = (State.Builder) super.decode(builder, json, decoder); - - // super.decode will read its own values and add them to the builder using put(). - // This means the state for session token and auth data might be illegal, returning - // unexpected types. For instance if sessionToken was null, now it's JSONObject.NULL. - // We must overwrite these possibly wrong values. - String newSessionToken = json.optString(KEY_SESSION_TOKEN, null); - userBuilder.sessionToken(newSessionToken); - - JSONObject newAuthData = json.optJSONObject(KEY_AUTH_DATA); - if (newAuthData == null) { - userBuilder.authData(null); - } else { - try { - @SuppressWarnings("rawtypes") - Iterator i = newAuthData.keys(); - while (i.hasNext()) { - String key = (String) i.next(); - if (!newAuthData.isNull(key)) { - userBuilder.putAuthData(key, - (Map) ParseDecoder.get().decode(newAuthData.getJSONObject(key))); - } - } - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - return (T) userBuilder; - } -} diff --git a/parse/src/main/java/com/parse/ParseUserCurrentCoder.kt b/parse/src/main/java/com/parse/ParseUserCurrentCoder.kt new file mode 100644 index 000000000..0255165be --- /dev/null +++ b/parse/src/main/java/com/parse/ParseUserCurrentCoder.kt @@ -0,0 +1,121 @@ +/* + * 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 + +/** + * Handles encoding/decoding ParseUser to/from /2 format JSON. /2 format json is only used for + * persisting current ParseUser and ParseInstallation to disk when LDS is not enabled. + */ +internal class ParseUserCurrentCoder /* package */ + : ParseObjectCurrentCoder() { + /** + * Converts a ParseUser state to /2/ JSON representation suitable for saving to disk. + * + * + *

+     * {
+     * data: {
+     * // data fields, including objectId, createdAt, updatedAt
+     * },
+     * classname: class name for the object,
+     * operations: { } // operations per field
+     * }
+    
* + * + * + * All keys are included, regardless of whether they are dirty. + * We also add sessionToken and authData to the json. + * + * @see .decode + */ + override fun encode( + state: T, operations: ParseOperationSet?, encoder: ParseEncoder + ): JSONObject? { + + // FYI we'll be double writing sessionToken and authData for now... + // This is important. super.encode() has no notion of sessionToken and authData, so it treats them + // like objects (simply passed to the encoder). This means that a null sessionToken will become + // JSONObject.NULL. This must be accounted in #decode(). + val objectJSON = super.encode(state, operations, encoder) + val sessionToken = (state as ParseUser.State).sessionToken() + if (sessionToken != null) { + try { + objectJSON!!.put(KEY_SESSION_TOKEN, sessionToken) + } catch (e: JSONException) { + throw RuntimeException("could not encode value for key: session_token") + } + } + val authData = (state as ParseUser.State).authData() + if (authData.isNotEmpty()) { + try { + objectJSON!!.put(KEY_AUTH_DATA, encoder.encode(authData)) + } catch (e: JSONException) { + throw RuntimeException("could not attach key: auth_data") + } + } + return objectJSON + } + + /** + * Merges from JSON in /2/ format. + * + * + * This is only used to read ParseUser state stored on disk in JSON. + * Since in encode we add sessionToken and authData to the json, we need remove them from json + * to generate state. + * + * @see .encode + */ + override fun > decode( + builder: T, json: JSONObject, decoder: ParseDecoder + ): T { + val userBuilder = super.decode(builder, json, decoder) as ParseUser.State.Builder + + // super.decode will read its own values and add them to the builder using put(). + // This means the state for session token and auth data might be illegal, returning + // unexpected types. For instance if sessionToken was null, now it's JSONObject.NULL. + // We must overwrite these possibly wrong values. + val newSessionToken = json.optString(KEY_SESSION_TOKEN, null) + userBuilder.sessionToken(newSessionToken) + val newAuthData = json.optJSONObject(KEY_AUTH_DATA) + if (newAuthData == null) { + userBuilder.authData(null) + } else { + try { + val i: Iterator<*> = newAuthData.keys() + while (i.hasNext()) { + val key = i.next() as String + if (!newAuthData.isNull(key)) { + userBuilder.putAuthData( + key, + ParseDecoder.get() + .decode(newAuthData.getJSONObject(key)) as Map + ) + } + } + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + return userBuilder as T + } + + companion object { + private const val KEY_AUTH_DATA = "auth_data" + private const val KEY_SESSION_TOKEN = "session_token" + private val INSTANCE = ParseUserCurrentCoder() + @JvmStatic + fun get(): ParseUserCurrentCoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/PointerEncoder.java b/parse/src/main/java/com/parse/PointerEncoder.java deleted file mode 100644 index 2f8db51b2..000000000 --- a/parse/src/main/java/com/parse/PointerEncoder.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.JSONObject; - -/** - * Encodes {@link ParseObject}s as pointers. If the object does not have an objectId, throws an - * exception. - */ -public class PointerEncoder extends PointerOrLocalIdEncoder { - - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final PointerEncoder INSTANCE = new PointerEncoder(); - - public static PointerEncoder get() { - return INSTANCE; - } - - @Override - public JSONObject encodeRelatedObject(ParseObject object) { - // Ensure the ParseObject has an id so it can be encoded as a pointer. - if (object.getObjectId() == null) { - // object that hasn't been saved. - throw new IllegalStateException("unable to encode an association with an unsaved ParseObject"); - } - return super.encodeRelatedObject(object); - } -} diff --git a/parse/src/main/java/com/parse/PointerEncoder.kt b/parse/src/main/java/com/parse/PointerEncoder.kt new file mode 100644 index 000000000..58972feca --- /dev/null +++ b/parse/src/main/java/com/parse/PointerEncoder.kt @@ -0,0 +1,36 @@ +/* + * 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.JSONObject + +/** + * Encodes [ParseObject]s as pointers. If the object does not have an objectId, throws an + * exception. + */ +class PointerEncoder : PointerOrLocalIdEncoder() { + override fun encodeRelatedObject(`object`: ParseObject): JSONObject { + // Ensure the ParseObject has an id so it can be encoded as a pointer. + checkNotNull(`object`.objectId) { + // object that hasn't been saved. + "unable to encode an association with an unsaved ParseObject" + } + return super.encodeRelatedObject(`object`) + } + + companion object { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = PointerEncoder() + @JvmStatic + fun get(): PointerEncoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java b/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java deleted file mode 100644 index 4c406c4e3..000000000 --- a/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; - -/** - * Encodes {@link ParseObject}s as pointers. If the object does not have an objectId, uses a - * local id. - */ -public class PointerOrLocalIdEncoder extends ParseEncoder { - - // This class isn't really a Singleton, but since it has no state, it's more efficient to get the - // default instance. - private static final PointerOrLocalIdEncoder INSTANCE = new PointerOrLocalIdEncoder(); - - public static PointerOrLocalIdEncoder get() { - return INSTANCE; - } - - @Override - public JSONObject encodeRelatedObject(ParseObject object) { - JSONObject json = new JSONObject(); - try { - if (object.getObjectId() != null) { - json.put("__type", "Pointer"); - json.put("className", object.getClassName()); - json.put("objectId", object.getObjectId()); - } else { - json.put("__type", "Pointer"); - json.put("className", object.getClassName()); - json.put("localId", object.getOrCreateLocalId()); - } - } catch (JSONException e) { - // This should not happen - throw new RuntimeException(e); - } - return json; - } -} diff --git a/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.kt b/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.kt new file mode 100644 index 000000000..b130e75a6 --- /dev/null +++ b/parse/src/main/java/com/parse/PointerOrLocalIdEncoder.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * Encodes [ParseObject]s as pointers. If the object does not have an objectId, uses a + * local id. + */ +open class PointerOrLocalIdEncoder : ParseEncoder() { + override fun encodeRelatedObject(`object`: ParseObject): JSONObject { + val json = JSONObject() + try { + if (`object`.objectId != null) { + json.put("__type", "Pointer") + json.put("className", `object`.className) + json.put("objectId", `object`.objectId) + } else { + json.put("__type", "Pointer") + json.put("className", `object`.className) + json.put("localId", `object`.getOrCreateLocalId()) + } + } catch (e: JSONException) { + // This should not happen + throw RuntimeException(e) + } + return json + } + + companion object { + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private val INSTANCE = PointerOrLocalIdEncoder() + @JvmStatic + fun get(): PointerOrLocalIdEncoder { + return INSTANCE + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/ProgressCallback.java b/parse/src/main/java/com/parse/ProgressCallback.kt similarity index 64% rename from parse/src/main/java/com/parse/ProgressCallback.java rename to parse/src/main/java/com/parse/ProgressCallback.kt index 5a209fa9b..a3e98b5b6 100644 --- a/parse/src/main/java/com/parse/ProgressCallback.java +++ b/parse/src/main/java/com/parse/ProgressCallback.kt @@ -6,19 +6,20 @@ * 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; +package com.parse /** - * A {@code ProgressCallback} is used to get upload or download progress of a {@link ParseFile} + * A `ProgressCallback` is used to get upload or download progress of a [ParseFile] * action. - *

- * The easiest way to use a {@code ProgressCallback} is through an anonymous inner class. + * + * + * The easiest way to use a `ProgressCallback` is through an anonymous inner class. */ // FYI, this does not extend ParseCallback2 since it does not match the usual signature // done(T, ParseException), but is done(T). -public interface ProgressCallback { +interface ProgressCallback { /** * Override this function with your desired callback. */ - void done(Integer percentDone); -} + fun done(percentDone: Int) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/PushHistory.java b/parse/src/main/java/com/parse/PushHistory.java deleted file mode 100644 index b952e9cf2..000000000 --- a/parse/src/main/java/com/parse/PushHistory.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 androidx.annotation.NonNull; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.PriorityQueue; - -/** - * PushHistory manages a fixed-length history of pushes received. It is used by to dedup recently - * received messages, as well as keep track of a last received timestamp that is included in PPNS - * handshakes. - */ -class PushHistory { - private static final String TAG = "com.parse.PushHistory"; - private final int maxHistoryLength; - private final PriorityQueue entries; - private final HashSet pushIds; - private String lastTime; - /** - * Creates a push history object from a JSON object that looks like this: - *

- * { - * "seen": { - * "push_id_1": "2013-11-01T22:01:00.000Z", - * "push_id_2": "2013-11-01T22:01:01.000Z", - * "push_id_3": "2013-11-01T22:01:02.000Z" - * }, - * "lastTime": "2013-11-01T22:01:02.000Z" - * } - *

- * The "history" entries correspond to entries in the "entries" queue. - * The "lastTime" entry corresponds to the "lastTime" field. - */ - public PushHistory(int maxHistoryLength, JSONObject json) { - this.maxHistoryLength = maxHistoryLength; - this.entries = new PriorityQueue<>(maxHistoryLength + 1); - this.pushIds = new HashSet<>(maxHistoryLength + 1); - this.lastTime = null; - - if (json != null) { - JSONObject jsonHistory = json.optJSONObject("seen"); - if (jsonHistory != null) { - Iterator it = jsonHistory.keys(); - while (it.hasNext()) { - String pushId = it.next(); - String timestamp = jsonHistory.optString(pushId, null); - - if (pushId != null && timestamp != null) { - tryInsertPush(pushId, timestamp); - } - } - } - setLastReceivedTimestamp(json.optString("lastTime", null)); - } - } - - /** - * Serializes the history state to a JSON object using the format described in loadJSON(). - */ - public JSONObject toJSON() throws JSONException { - JSONObject json = new JSONObject(); - - if (entries.size() > 0) { - JSONObject history = new JSONObject(); - for (Entry e : entries) { - history.put(e.pushId, e.timestamp); - } - json.put("seen", history); - } - - json.putOpt("lastTime", lastTime); - - return json; - } - - /** - * Returns the last received timestamp, which is always updated whether or not a push was - * successfully inserted into history. - */ - public String getLastReceivedTimestamp() { - return lastTime; - } - - public void setLastReceivedTimestamp(String lastTime) { - this.lastTime = lastTime; - } - - /** - * Attempts to insert a push into history. The push is ignored if we have already seen it - * recently. Otherwise, the push is inserted into history. If the length of the history exceeds - * the maximum length, then the history is trimmed by removing the oldest pushes until it no - * longer exceeds the maximum length. - * - * @return Returns whether or not the push was inserted into history. - */ - public boolean tryInsertPush(String pushId, String timestamp) { - if (timestamp == null) { - throw new IllegalArgumentException("Can't insert null pushId or timestamp into history"); - } - - if (lastTime == null || timestamp.compareTo(lastTime) > 0) { - lastTime = timestamp; - } - - if (pushIds.contains(pushId)) { - PLog.e(TAG, "Ignored duplicate push " + pushId); - return false; - } - - entries.add(new Entry(pushId, timestamp)); - pushIds.add(pushId); - - while (entries.size() > maxHistoryLength) { - Entry head = entries.remove(); - pushIds.remove(head.pushId); - } - - return true; - } - - private static class Entry implements Comparable { - public String pushId; - public String timestamp; - - public Entry(String pushId, String timestamp) { - this.pushId = pushId; - this.timestamp = timestamp; - } - - @Override - public int compareTo(@NonNull Entry other) { - return timestamp.compareTo(other.timestamp); - } - } -} diff --git a/parse/src/main/java/com/parse/PushHistory.kt b/parse/src/main/java/com/parse/PushHistory.kt new file mode 100644 index 000000000..cd7992960 --- /dev/null +++ b/parse/src/main/java/com/parse/PushHistory.kt @@ -0,0 +1,117 @@ +/* + * 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.util.* + +/** + * PushHistory manages a fixed-length history of pushes received. It is used by to dedup recently + * received messages, as well as keep track of a last received timestamp that is included in PPNS + * handshakes. + */ +internal class PushHistory(private val maxHistoryLength: Int, json: JSONObject?) { + private val entries: PriorityQueue = PriorityQueue(maxHistoryLength + 1) + private val pushIds: HashSet = HashSet(maxHistoryLength + 1) + + /** + * Returns the last received timestamp, which is always updated whether or not a push was + * successfully inserted into history. + */ + var lastReceivedTimestamp: String? + + /** + * Serializes the history state to a JSON object using the format described in loadJSON(). + */ + @Throws(JSONException::class) + fun toJSON(): JSONObject { + val json = JSONObject() + if (entries.size > 0) { + val history = JSONObject() + for (e in entries) { + history.put(e.pushId, e.timestamp) + } + json.put("seen", history) + } + json.putOpt("lastTime", lastReceivedTimestamp) + return json + } + + /** + * Attempts to insert a push into history. The push is ignored if we have already seen it + * recently. Otherwise, the push is inserted into history. If the length of the history exceeds + * the maximum length, then the history is trimmed by removing the oldest pushes until it no + * longer exceeds the maximum length. + * + * @return Returns whether or not the push was inserted into history. + */ + fun tryInsertPush(pushId: String, timestamp: String?): Boolean { + requireNotNull(timestamp) { "Can't insert null pushId or timestamp into history" } + if (lastReceivedTimestamp == null || timestamp > lastReceivedTimestamp!!) { + lastReceivedTimestamp = timestamp + } + if (pushIds.contains(pushId)) { + PLog.e(TAG, "Ignored duplicate push $pushId") + return false + } + entries.add(Entry(pushId, timestamp)) + pushIds.add(pushId) + while (entries.size > maxHistoryLength) { + val head = entries.remove() + pushIds.remove(head.pushId) + } + return true + } + + private class Entry(val pushId: String, val timestamp: String) : Comparable { + override fun compareTo(other: Entry): Int { + return timestamp.compareTo(other.timestamp) + } + } + + companion object { + private const val TAG = "com.parse.PushHistory" + } + + /** + * Creates a push history object from a JSON object that looks like this: + * + * + * { + * "seen": { + * "push_id_1": "2013-11-01T22:01:00.000Z", + * "push_id_2": "2013-11-01T22:01:01.000Z", + * "push_id_3": "2013-11-01T22:01:02.000Z" + * }, + * "lastTime": "2013-11-01T22:01:02.000Z" + * } + * + * + * The "history" entries correspond to entries in the "entries" queue. + * The "lastTime" entry corresponds to the "lastTime" field. + */ + init { + lastReceivedTimestamp = null + if (json != null) { + val jsonHistory = json.optJSONObject("seen") + if (jsonHistory != null) { + val it = jsonHistory.keys() + while (it.hasNext()) { + val pushId = it.next() + val timestamp = jsonHistory.optString(pushId) + if (pushId != null && timestamp != null) { + tryInsertPush(pushId, timestamp) + } + } + } + lastReceivedTimestamp = json.optString("lastTime", null) + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/PushRouter.java b/parse/src/main/java/com/parse/PushRouter.java deleted file mode 100644 index da450a077..000000000 --- a/parse/src/main/java/com/parse/PushRouter.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; - -/** - * PushRouter handles distribution of push payloads through a broadcast intent with the - * "com.parse.push.intent.RECEIVE" action. It also serializes a history of the last several pushes - * seen by this app. This history is necessary for two reasons: - *

- * - For PPNS, we provide the last-seen timestamp to the server as part of the handshake. This is - * used as a cursor into the server-side inbox of recent pushes for this client. - * - For GCM, we use the history to deduplicate pushes when GCM decides to change the canonical - * registration id for a client (which can result in duplicate pushes while both the old and - * new registration id are still valid). - */ -public class PushRouter { - private static final String TAG = "com.parse.ParsePushRouter"; - private static final String LEGACY_STATE_LOCATION = "pushState"; - private static final String STATE_LOCATION = "push"; - private static final int MAX_HISTORY_LENGTH = 10; - - private static PushRouter instance; - private final File diskState; - private final PushHistory history; - - private PushRouter(File diskState, PushHistory history) { - this.diskState = diskState; - this.history = history; - } - - public static synchronized PushRouter getInstance() { - if (instance == null) { - File diskState = new File(ParsePlugins.get().getFilesDir(), STATE_LOCATION); - File oldDiskState = new File(ParsePlugins.get().getParseDir(), LEGACY_STATE_LOCATION); - instance = pushRouterFromState(diskState, oldDiskState, MAX_HISTORY_LENGTH); - } - - return instance; - } - - /* package for tests */ - static synchronized void resetInstance() { - ParseFileUtils.deleteQuietly(new File(ParsePlugins.get().getFilesDir(), STATE_LOCATION)); - instance = null; - } - - /* package for tests */ - static PushRouter pushRouterFromState( - File diskState, File oldDiskState, int maxHistoryLength) { - JSONObject state = readJSONFileQuietly(diskState); - JSONObject historyJSON = (state != null) ? state.optJSONObject("history") : null; - PushHistory history = new PushHistory(maxHistoryLength, historyJSON); - - // If the deserialized push history object doesn't have a last timestamp, we might have to - // migrate the last timestamp from the legacy pushState file instead. - boolean didMigrate = false; - if (history.getLastReceivedTimestamp() == null) { - JSONObject oldState = readJSONFileQuietly(oldDiskState); - if (oldState != null) { - String lastTime = oldState.optString("lastTime", null); - if (lastTime != null) { - history.setLastReceivedTimestamp(lastTime); - } - didMigrate = true; - } - } - - PushRouter router = new PushRouter(diskState, history); - - if (didMigrate) { - router.saveStateToDisk(); - ParseFileUtils.deleteQuietly(oldDiskState); - } - - return router; - } - - private static JSONObject readJSONFileQuietly(File file) { - JSONObject json = null; - if (file != null) { - try { - json = ParseFileUtils.readFileToJSONObject(file); - } catch (IOException | JSONException e) { - // do nothing - } - } - return json; - } - - /** - * Returns the state in this object as a persistable JSONObject. The persisted state looks like - * this: - *

- * { - * "history": { - * "seen": { - * "": "", - * ... - * } - * "lastTime": "" - * } - * } - */ - /* package */ - synchronized JSONObject toJSON() throws JSONException { - JSONObject json = new JSONObject(); - json.put("history", history.toJSON()); - return json; - } - - private synchronized void saveStateToDisk() { - try { - ParseFileUtils.writeJSONObjectToFile(diskState, toJSON()); - } catch (IOException | JSONException e) { - PLog.e(TAG, "Unexpected error when serializing push state to " + diskState, e); - } - } - - public synchronized String getLastReceivedTimestamp() { - return history.getLastReceivedTimestamp(); - } - - public synchronized boolean handlePush( - String pushId, String timestamp, String channel, JSONObject data) { - if (ParseTextUtils.isEmpty(pushId) || ParseTextUtils.isEmpty(timestamp)) { - return false; - } - - if (!history.tryInsertPush(pushId, timestamp)) { - return false; - } - - // Persist the fact that we've seen this push. - saveStateToDisk(); - - Bundle extras = new Bundle(); - extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_CHANNEL, channel); - if (data == null) { - extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, "{}"); - } else { - extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, data.toString()); - } - - Intent intent = new Intent(ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE); - intent.putExtras(extras); - - // Set the package name to keep this intent within the given package. - Context context = Parse.getApplicationContext(); - intent.setPackage(context.getPackageName()); - context.sendBroadcast(intent); - - return true; - } -} diff --git a/parse/src/main/java/com/parse/PushRouter.kt b/parse/src/main/java/com/parse/PushRouter.kt new file mode 100644 index 000000000..b4899d06f --- /dev/null +++ b/parse/src/main/java/com/parse/PushRouter.kt @@ -0,0 +1,165 @@ +/* + * 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 android.content.Intent +import android.os.Bundle +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.IOException + +/** + * PushRouter handles distribution of push payloads through a broadcast intent with the + * "com.parse.push.intent.RECEIVE" action. It also serializes a history of the last several pushes + * seen by this app. This history is necessary for two reasons: + * + * + * - For PPNS, we provide the last-seen timestamp to the server as part of the handshake. This is + * used as a cursor into the server-side inbox of recent pushes for this client. + * - For GCM, we use the history to deduplicate pushes when GCM decides to change the canonical + * registration id for a client (which can result in duplicate pushes while both the old and + * new registration id are still valid). + */ +class PushRouter private constructor( + private val diskState: File, + private val history: PushHistory +) { + /** + * Returns the state in this object as a persistable JSONObject. The persisted state looks like + * this: + *

+ * { + * "history": { + * "seen": { + * "": "", + * ... + * } + * "lastTime": "" + * } + * } + */ + /* package */ + @Synchronized + @Throws(JSONException::class) + fun toJSON(): JSONObject { + val json = JSONObject() + json.put("history", history.toJSON()) + return json + } + + @Synchronized + private fun saveStateToDisk() { + try { + ParseFileUtils.writeJSONObjectToFile(diskState, toJSON()) + } catch (e: IOException) { + PLog.e(TAG, "Unexpected error when serializing push state to $diskState", e) + } catch (e: JSONException) { + PLog.e(TAG, "Unexpected error when serializing push state to $diskState", e) + } + } + + @get:Synchronized + val lastReceivedTimestamp: String? + get() = history.lastReceivedTimestamp + + @Synchronized + fun handlePush( + pushId: String, timestamp: String, channel: String?, data: JSONObject? + ): Boolean { + if (ParseTextUtils.isEmpty(pushId) || ParseTextUtils.isEmpty(timestamp)) { + return false + } + if (!history.tryInsertPush(pushId, timestamp)) { + return false + } + + // Persist the fact that we've seen this push. + saveStateToDisk() + val extras = Bundle() + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_CHANNEL, channel) + if (data == null) { + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, "{}") + } else { + extras.putString(ParsePushBroadcastReceiver.KEY_PUSH_DATA, data.toString()) + } + val intent = Intent(ParsePushBroadcastReceiver.ACTION_PUSH_RECEIVE) + intent.putExtras(extras) + + // Set the package name to keep this intent within the given package. + val context = Parse.getApplicationContext() + intent.setPackage(context.packageName) + context.sendBroadcast(intent) + return true + } + + companion object { + private const val TAG = "com.parse.ParsePushRouter" + private const val LEGACY_STATE_LOCATION = "pushState" + private const val STATE_LOCATION = "push" + private const val MAX_HISTORY_LENGTH = 10 + private var instance: PushRouter? = null + @JvmStatic + @Synchronized + fun getInstance(): PushRouter? { + if (instance == null) { + val diskState = File(ParsePlugins.get().getFilesDir(), STATE_LOCATION) + val oldDiskState = File(ParsePlugins.get().getParseDir(), LEGACY_STATE_LOCATION) + instance = pushRouterFromState(diskState, oldDiskState, MAX_HISTORY_LENGTH) + } + return instance + } + + /* package for tests */ + @Synchronized + fun resetInstance() { + ParseFileUtils.deleteQuietly(File(ParsePlugins.get().getFilesDir(), STATE_LOCATION)) + instance = null + } + + /* package for tests */ + fun pushRouterFromState( + diskState: File, oldDiskState: File?, maxHistoryLength: Int + ): PushRouter { + val state = readJSONFileQuietly(diskState) + val historyJSON = state?.optJSONObject("history") + val history = PushHistory(maxHistoryLength, historyJSON) + + // If the deserialized push history object doesn't have a last timestamp, we might have to + // migrate the last timestamp from the legacy pushState file instead. + var didMigrate = false + if (history.lastReceivedTimestamp == null) { + val oldState = readJSONFileQuietly(oldDiskState) + if (oldState != null) { + history.lastReceivedTimestamp = oldState.optString("lastTime", history.lastReceivedTimestamp) + didMigrate = true + } + } + val router = PushRouter(diskState, history) + if (didMigrate) { + router.saveStateToDisk() + ParseFileUtils.deleteQuietly(oldDiskState) + } + return router + } + + private fun readJSONFileQuietly(file: File?): JSONObject? { + var json: JSONObject? = null + if (file != null) { + try { + json = ParseFileUtils.readFileToJSONObject(file) + } catch (e: IOException) { + // do nothing + } catch (e: JSONException) { + } + } + return json + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/RequestPasswordResetCallback.java b/parse/src/main/java/com/parse/RequestPasswordResetCallback.java deleted file mode 100644 index 86e896384..000000000 --- a/parse/src/main/java/com/parse/RequestPasswordResetCallback.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -/** - * A {@code RequestPasswordResetCallback} is used to run code requesting a password reset for a - * user. - *

- * The easiest way to use a {@code RequestPasswordResetCallback} is through an anonymous inner - * class. Override the {@code done} function to specify what the callback should do after the - * request is complete. The {@code done} function will be run in the UI thread, while the request - * happens in a background thread. This ensures that the UI does not freeze while the request - * happens. - *

- * For example, this sample code requests a password reset for a user and calls a different function - * depending on whether the request succeeded or not. - *

- *

- * ParseUser.requestPasswordResetInBackground("forgetful@example.com",
- *     new RequestPasswordResetCallback() {
- *       public void done(ParseException e) {
- *         if (e == null) {
- *           requestedSuccessfully();
- *         } else {
- *           requestDidNotSucceed();
- *         }
- *       }
- *     });
- * 
- */ -public interface RequestPasswordResetCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the request is complete. - * - * @param e The exception raised by the save, or {@code null} if no account is associated with the - * email address. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/RequestPasswordResetCallback.kt b/parse/src/main/java/com/parse/RequestPasswordResetCallback.kt new file mode 100644 index 000000000..0e5f3d53d --- /dev/null +++ b/parse/src/main/java/com/parse/RequestPasswordResetCallback.kt @@ -0,0 +1,48 @@ +/* + * 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 + +/** + * A `RequestPasswordResetCallback` is used to run code requesting a password reset for a + * user. + * + * + * The easiest way to use a `RequestPasswordResetCallback` is through an anonymous inner + * class. Override the `done` function to specify what the callback should do after the + * request is complete. The `done` function will be run in the UI thread, while the request + * happens in a background thread. This ensures that the UI does not freeze while the request + * happens. + * + * + * For example, this sample code requests a password reset for a user and calls a different function + * depending on whether the request succeeded or not. + * + * + *
+ * ParseUser.requestPasswordResetInBackground("forgetful@example.com",
+ * new RequestPasswordResetCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * requestedSuccessfully();
+ * } else {
+ * requestDidNotSucceed();
+ * }
+ * }
+ * });
+
* + */ +internal interface RequestPasswordResetCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the request is complete. + * + * @param e The exception raised by the save, or `null` if no account is associated with the + * email address. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/SaveCallback.java b/parse/src/main/java/com/parse/SaveCallback.java deleted file mode 100644 index 212596b6a..000000000 --- a/parse/src/main/java/com/parse/SaveCallback.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -/** - * A {@code SaveCallback} is used to run code after saving a {@link ParseObject} in a background - * thread. - *

- * The easiest way to use a {@code SaveCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the save is complete. The - * {@code done} function will be run in the UI thread, while the save happens in a background - * thread. This ensures that the UI does not freeze while the save happens. - *

- * For example, this sample code saves the object {@code myObject} and calls a different - * function depending on whether the save succeeded or not. - *

- *

- * myObject.saveInBackground(new SaveCallback() {
- *   public void done(ParseException e) {
- *     if (e == null) {
- *       myObjectSavedSuccessfully();
- *     } else {
- *       myObjectSaveDidNotSucceed();
- *     }
- *   }
- * });
- * 
- */ -public interface SaveCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the save is complete. - * - * @param e The exception raised by the save, or {@code null} if it succeeded. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/SaveCallback.kt b/parse/src/main/java/com/parse/SaveCallback.kt new file mode 100644 index 000000000..a2c110b45 --- /dev/null +++ b/parse/src/main/java/com/parse/SaveCallback.kt @@ -0,0 +1,45 @@ +/* + * 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 + +/** + * A `SaveCallback` is used to run code after saving a [ParseObject] in a background + * thread. + * + * + * The easiest way to use a `SaveCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the save is complete. The + * `done` function will be run in the UI thread, while the save happens in a background + * thread. This ensures that the UI does not freeze while the save happens. + * + * + * For example, this sample code saves the object `myObject` and calls a different + * function depending on whether the save succeeded or not. + * + * + *
+ * myObject.saveInBackground(new SaveCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * myObjectSavedSuccessfully();
+ * } else {
+ * myObjectSaveDidNotSucceed();
+ * }
+ * }
+ * });
+
* + */ +interface SaveCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the save is complete. + * + * @param e The exception raised by the save, or `null` if it succeeded. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/SendCallback.java b/parse/src/main/java/com/parse/SendCallback.java deleted file mode 100644 index a4483aa1f..000000000 --- a/parse/src/main/java/com/parse/SendCallback.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -/** - * A {@code SendCallback} is used to run code after sending a {@link ParsePush} in a background - * thread. - *

- * The easiest way to use a {@code SendCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the send is complete. The - * {@code done} function will be run in the UI thread, while the send happens in a background - * thread. This ensures that the UI does not freeze while the send happens. - *

- * For example, this sample code sends the message {@code "Hello world"} on the - * {@code "hello"} channel and logs whether the send succeeded. - *

- *

- * ParsePush push = new ParsePush();
- * push.setChannel("hello");
- * push.setMessage("Hello world!");
- * push.sendInBackground(new SendCallback() {
- *   public void done(ParseException e) {
- *     if (e == null) {
- *       Log.d("push", "success!");
- *     } else {
- *       Log.d("push", "failure");
- *     }
- *   }
- * });
- * 
- */ -public interface SendCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the send is complete. - * - * @param e The exception raised by the send, or {@code null} if it succeeded. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/SendCallback.kt b/parse/src/main/java/com/parse/SendCallback.kt new file mode 100644 index 000000000..7304c267e --- /dev/null +++ b/parse/src/main/java/com/parse/SendCallback.kt @@ -0,0 +1,48 @@ +/* + * 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 + +/** + * A `SendCallback` is used to run code after sending a [ParsePush] in a background + * thread. + * + * + * The easiest way to use a `SendCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the send is complete. The + * `done` function will be run in the UI thread, while the send happens in a background + * thread. This ensures that the UI does not freeze while the send happens. + * + * + * For example, this sample code sends the message `"Hello world"` on the + * `"hello"` channel and logs whether the send succeeded. + * + * + *
+ * ParsePush push = new ParsePush();
+ * push.setChannel("hello");
+ * push.setMessage("Hello world!");
+ * push.sendInBackground(new SendCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * Log.d("push", "success!");
+ * } else {
+ * Log.d("push", "failure");
+ * }
+ * }
+ * });
+
* + */ +internal interface SendCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the send is complete. + * + * @param e The exception raised by the send, or `null` if it succeeded. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/SignUpCallback.java b/parse/src/main/java/com/parse/SignUpCallback.java deleted file mode 100644 index 53bc38c64..000000000 --- a/parse/src/main/java/com/parse/SignUpCallback.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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; - -/** - * A {@code SignUpCallback} is used to run code after signing up a {@link ParseUser} in a background - * thread. - *

- * The easiest way to use a {@code SignUpCallback} is through an anonymous inner class. Override the - * {@code done} function to specify what the callback should do after the save is complete. The - * {@code done} function will be run in the UI thread, while the signup happens in a background - * thread. This ensures that the UI does not freeze while the signup happens. - *

- * For example, this sample code signs up the object {@code myUser} and calls a different - * function depending on whether the signup succeeded or not. - *

- *

- *

- * myUser.signUpInBackground(new SignUpCallback() {
- *   public void done(ParseException e) {
- *     if (e == null) {
- *       myUserSignedUpSuccessfully();
- *     } else {
- *       myUserSignUpDidNotSucceed();
- *     }
- *   }
- * });
- * 
- */ -public interface SignUpCallback extends ParseCallback1 { - /** - * Override this function with the code you want to run after the signUp is complete. - * - * @param e The exception raised by the signUp, or {@code null} if it succeeded. - */ - @Override - void done(ParseException e); -} diff --git a/parse/src/main/java/com/parse/SignUpCallback.kt b/parse/src/main/java/com/parse/SignUpCallback.kt new file mode 100644 index 000000000..df362b9b7 --- /dev/null +++ b/parse/src/main/java/com/parse/SignUpCallback.kt @@ -0,0 +1,47 @@ +/* + * 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 + +/** + * A `SignUpCallback` is used to run code after signing up a [ParseUser] in a background + * thread. + * + * + * The easiest way to use a `SignUpCallback` is through an anonymous inner class. Override the + * `done` function to specify what the callback should do after the save is complete. The + * `done` function will be run in the UI thread, while the signup happens in a background + * thread. This ensures that the UI does not freeze while the signup happens. + * + * + * For example, this sample code signs up the object `myUser` and calls a different + * function depending on whether the signup succeeded or not. + * + * + * + * + *
+ * myUser.signUpInBackground(new SignUpCallback() {
+ * public void done(ParseException e) {
+ * if (e == null) {
+ * myUserSignedUpSuccessfully();
+ * } else {
+ * myUserSignUpDidNotSucceed();
+ * }
+ * }
+ * });
+
* + */ +internal interface SignUpCallback : ParseCallback1 { + /** + * Override this function with the code you want to run after the signUp is complete. + * + * @param e The exception raised by the signUp, or `null` if it succeeded. + */ + override fun done(e: ParseException?) +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/TaskQueue.java b/parse/src/main/java/com/parse/TaskQueue.java deleted file mode 100644 index f8ba25181..000000000 --- a/parse/src/main/java/com/parse/TaskQueue.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 java.util.Arrays; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - -/** - * A helper class for enqueueing tasks - */ -class TaskQueue { - private final Lock lock = new ReentrantLock(); - /** - * We only need to keep the tail of the queue. Cancelled tasks will just complete - * normally/immediately when their turn arrives. - */ - private Task tail; - - /** - * Creates a continuation that will wait for the given task to complete before running the next - * continuations. - */ - static Continuation> waitFor(final Task toAwait) { - return new Continuation>() { - @Override - public Task then(final Task task) { - return toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task ignored) { - return task; - } - }); - } - }; - } - - /** - * Gets a task that can be safely awaited and is dependent on the current tail of the queue. This - * essentially gives us a proxy for the tail end of the queue that can be safely cancelled. - * - * @return A new task that should be awaited by enqueued tasks. - */ - private Task getTaskToAwait() { - lock.lock(); - try { - Task toAwait = tail != null ? tail : Task.forResult(null); - return toAwait.continueWith(new Continuation() { - @Override - public Void then(Task task) { - return null; - } - }); - } finally { - lock.unlock(); - } - } - - /** - * Enqueues a task created by taskStart. - * - * @param taskStart A function given a task to await once state is snapshotted (e.g. after capturing - * session tokens at the time of the save call). Awaiting this task will wait for the - * created task's turn in the queue. - * @return The task created by the taskStart function. - */ - Task enqueue(Continuation> taskStart) { - lock.lock(); - try { - Task task; - Task oldTail = tail != null ? tail : Task.forResult(null); - // The task created by taskStart is responsible for waiting for the task passed into it before - // doing its work (this gives it an opportunity to do startup work or save state before - // waiting for its turn in the queue) - try { - Task toAwait = getTaskToAwait(); - task = taskStart.then(toAwait); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - - // The tail task should be dependent on the old tail as well as the newly-created task. This - // prevents cancellation of the new task from causing the queue to run out of order. - tail = Task.whenAll(Arrays.asList(oldTail, task)); - return task; - } finally { - lock.unlock(); - } - } - - Lock getLock() { - return lock; - } - - void waitUntilFinished() throws InterruptedException { - lock.lock(); - try { - if (tail == null) { - return; - } - tail.waitForCompletion(); - } finally { - lock.unlock(); - } - } -} diff --git a/parse/src/main/java/com/parse/TaskQueue.kt b/parse/src/main/java/com/parse/TaskQueue.kt new file mode 100644 index 000000000..0fdbb3f29 --- /dev/null +++ b/parse/src/main/java/com/parse/TaskQueue.kt @@ -0,0 +1,101 @@ +/* + * 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 com.parse.boltsinternal.Continuation +import com.parse.boltsinternal.Task +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +/** + * A helper class for enqueueing tasks + */ +class TaskQueue { + val lock: Lock = ReentrantLock() + + /** + * We only need to keep the tail of the queue. Cancelled tasks will just complete + * normally/immediately when their turn arrives. + */ + private var tail: Task? = null + + /** + * Gets a task that can be safely awaited and is dependent on the current tail of the queue. This + * essentially gives us a proxy for the tail end of the queue that can be safely cancelled. + * + * @return A new task that should be awaited by enqueued tasks. + */ + private val taskToAwait: Task + get() { + lock.lock() + return try { + val toAwait = if (tail != null) tail!! else Task.forResult(null) + toAwait.continueWith { null } + } finally { + lock.unlock() + } + } + + /** + * Enqueues a task created by taskStart. + * + * @param taskStart A function given a task to await once state is snapshotted (e.g. after capturing + * session tokens at the time of the save call). Awaiting this task will wait for the + * created task's turn in the queue. + * @return The task created by the taskStart function. + */ + fun enqueue(taskStart: Continuation>): Task { + lock.lock() + return try { + val oldTail = if (tail != null) tail!! else Task.forResult(null) + // The task created by taskStart is responsible for waiting for the task passed into it before + // doing its work (this gives it an opportunity to do startup work or save state before + // waiting for its turn in the queue) + val task: Task = try { + val toAwait = taskToAwait + taskStart.then(toAwait) + } catch (e: RuntimeException) { + throw e + } catch (e: Exception) { + throw RuntimeException(e) + } + + // The tail task should be dependent on the old tail as well as the newly-created task. This + // prevents cancellation of the new task from causing the queue to run out of order. + tail = Task.whenAll(listOf(oldTail, task)) + task + } finally { + lock.unlock() + } + } + + @Throws(InterruptedException::class) + fun waitUntilFinished() { + lock.lock() + try { + if (tail == null) { + return + } + tail!!.waitForCompletion() + } finally { + lock.unlock() + } + } + + companion object { + /** + * Creates a continuation that will wait for the given task to complete before running the next + * continuations. + */ + @JvmStatic + fun waitFor(toAwait: Task): Continuation> { + return Continuation { task: Task? -> toAwait.continueWithTask { task } } + } + } +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/TaskStackBuilderHelper.java b/parse/src/main/java/com/parse/TaskStackBuilderHelper.kt similarity index 52% rename from parse/src/main/java/com/parse/TaskStackBuilderHelper.java rename to parse/src/main/java/com/parse/TaskStackBuilderHelper.kt index a87fa9c6b..6399b6711 100644 --- a/parse/src/main/java/com/parse/TaskStackBuilderHelper.java +++ b/parse/src/main/java/com/parse/TaskStackBuilderHelper.kt @@ -6,14 +6,14 @@ * 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; +package com.parse -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.TaskStackBuilder; -import android.content.Context; -import android.content.Intent; -import android.os.Build; +import android.annotation.TargetApi +import android.app.Activity +import android.app.TaskStackBuilder +import android.content.Context +import android.content.Intent +import android.os.Build /** * This is here to avoid the dependency on the android support library. @@ -22,11 +22,12 @@ * TODO (pdjones): make more similar to support-v4 api */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - /* package */ class TaskStackBuilderHelper { - public static void startActivities(Context context, Class cls, Intent activityIntent) { - TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); - stackBuilder.addParentStack(cls); - stackBuilder.addNextIntent(activityIntent); - stackBuilder.startActivities(); +internal object TaskStackBuilderHelper { + @JvmStatic + fun startActivities(context: Context?, cls: Class?, activityIntent: Intent?) { + val stackBuilder = TaskStackBuilder.create(context) + stackBuilder.addParentStack(cls) + stackBuilder.addNextIntent(activityIntent) + stackBuilder.startActivities() } -} +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/WeakValueHashMap.java b/parse/src/main/java/com/parse/WeakValueHashMap.java deleted file mode 100644 index fab6a62af..000000000 --- a/parse/src/main/java/com/parse/WeakValueHashMap.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 java.lang.ref.WeakReference; -import java.util.HashMap; - -/** - * A HashMap where all the values are weak. - */ -class WeakValueHashMap { - private HashMap> map; - - public WeakValueHashMap() { - map = new HashMap<>(); - } - - public void put(K key, V value) { - map.put(key, new WeakReference<>(value)); - } - - /** - * Returns null if the key isn't in the map, or if it is an expired reference. If it is, then the - * reference is removed from the map. - */ - public V get(K key) { - WeakReference reference = map.get(key); - if (reference == null) { - return null; - } - - V value = reference.get(); - if (value == null) { - map.remove(key); - } - - return value; - } - - public void remove(K key) { - map.remove(key); - } - - public void clear() { - map.clear(); - } -} diff --git a/parse/src/main/java/com/parse/WeakValueHashMap.kt b/parse/src/main/java/com/parse/WeakValueHashMap.kt new file mode 100644 index 000000000..10c39d5a1 --- /dev/null +++ b/parse/src/main/java/com/parse/WeakValueHashMap.kt @@ -0,0 +1,45 @@ +/* + * 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 java.lang.ref.WeakReference +import java.util.* + +/** + * A HashMap where all the values are weak. + */ +internal class WeakValueHashMap { + private val map: HashMap> = HashMap() + + fun put(key: K, value: V) { + map[key] = WeakReference(value) + } + + /** + * Returns null if the key isn't in the map, or if it is an expired reference. If it is, then the + * reference is removed from the map. + */ + operator fun get(key: K): V? { + val reference = map[key] ?: return null + val value = reference.get() + if (value == null) { + map.remove(key) + } + return value + } + + fun remove(key: K) { + map.remove(key) + } + + fun clear() { + map.clear() + } + +} \ No newline at end of file diff --git a/parse/src/main/java/com/parse/http/ParseHttpRequest.java b/parse/src/main/java/com/parse/http/ParseHttpRequest.java index 2e91f3597..80e5d576b 100644 --- a/parse/src/main/java/com/parse/http/ParseHttpRequest.java +++ b/parse/src/main/java/com/parse/http/ParseHttpRequest.java @@ -22,6 +22,7 @@ public final class ParseHttpRequest { private final Method method; private final Map headers; private final ParseHttpBody body; + private ParseHttpRequest(Builder builder) { this.url = builder.url; this.method = builder.method; diff --git a/parse/src/main/java/com/parse/http/ParseHttpResponse.java b/parse/src/main/java/com/parse/http/ParseHttpResponse.java index 367985019..8cb9c96b1 100644 --- a/parse/src/main/java/com/parse/http/ParseHttpResponse.java +++ b/parse/src/main/java/com/parse/http/ParseHttpResponse.java @@ -25,6 +25,7 @@ public final class ParseHttpResponse { private final String reasonPhrase; private final Map headers; private final String contentType; + private ParseHttpResponse(Builder builder) { this.statusCode = builder.statusCode; this.content = builder.content; diff --git a/parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java b/parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java index 421ecf3c1..42aa38bef 100644 --- a/parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java +++ b/parse/src/test/java/com/parse/CachedCurrentInstallationControllerTest.java @@ -8,13 +8,13 @@ */ package com.parse; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.junit.After; import org.junit.Before; import org.junit.Test; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; @@ -105,7 +105,7 @@ public void testGetAsyncFromStore() throws Exception { ParseObjectStore store = mock(ParseObjectStore.class); ParseInstallation installation = mock(ParseInstallation.class); when(installation.getInstallationId()).thenReturn("testInstallationId"); - when(store.getAsync()).thenReturn(Task.forResult(installation)); + when(store.getGetAsync()).thenReturn(Task.forResult(installation)); // Create test controller CachedCurrentInstallationController controller = @@ -113,7 +113,7 @@ public void testGetAsyncFromStore() throws Exception { ParseInstallation currentInstallation = ParseTaskUtils.wait(controller.getAsync()); - verify(store, times(1)).getAsync(); + verify(store, times(1)).getGetAsync(); // Make sure installationId is updated verify(installationId, times(1)).set("testInstallationId"); // Make sure controller state is update to date @@ -129,7 +129,7 @@ public void testGetAsyncWithNoInstallation() throws Exception { when(installationId.get()).thenReturn("testInstallationId"); //noinspection unchecked ParseObjectStore store = mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.getGetAsync()).thenReturn(Task.forResult(null)); // Create test controller CachedCurrentInstallationController controller = @@ -137,7 +137,7 @@ public void testGetAsyncWithNoInstallation() throws Exception { ParseInstallation currentInstallation = ParseTaskUtils.wait(controller.getAsync()); - verify(store, times(1)).getAsync(); + verify(store, times(1)).getGetAsync(); // Make sure controller state is update to date assertSame(controller.currentInstallation, currentInstallation); // Make sure device info is updated @@ -153,7 +153,7 @@ public void testGetAsyncWithNoInstallationRaceCondition() throws ParseException //noinspection unchecked ParseObjectStore store = mock(ParseObjectStore.class); TaskCompletionSource tcs = new TaskCompletionSource(); - when(store.getAsync()).thenReturn(tcs.getTask()); + when(store.getGetAsync()).thenReturn(tcs.getTask()); // Create test controller CachedCurrentInstallationController controller = @@ -166,7 +166,7 @@ public void testGetAsyncWithNoInstallationRaceCondition() throws ParseException ParseInstallation installationA = ParseTaskUtils.wait(taskA); ParseInstallation installationB = ParseTaskUtils.wait(taskB); - verify(store, times(1)).getAsync(); + verify(store, times(1)).getGetAsync(); assertSame(controller.currentInstallation, installationA); assertSame(controller.currentInstallation, installationB); // Make sure device info is updated diff --git a/parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java b/parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java index 56d05a444..f40d2b493 100644 --- a/parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java +++ b/parse/src/test/java/com/parse/CachedCurrentUserControllerTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -15,8 +17,6 @@ import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -156,7 +156,7 @@ public void testGetAsyncWithInMemoryCurrentUserSet() throws Exception { public void testGetAsyncWithNoInMemoryCurrentUserAndLazyLogin() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.getGetAsync()).thenReturn(Task.forResult(null)); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -179,7 +179,7 @@ public void testGetAsyncWithNoInMemoryAndInDiskCurrentUserAndNoLazyLogin() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.getGetAsync()).thenReturn(Task.forResult(null)); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -199,7 +199,7 @@ public void testGetAsyncWithCurrentUserReadFromDiskSuccess() throws Exception { ParseUser currentUserInDisk = ParseObject.from(state); ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(currentUserInDisk)); + when(store.getGetAsync()).thenReturn(Task.forResult(currentUserInDisk)); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -217,11 +217,11 @@ public void testGetAsyncWithCurrentUserReadFromDiskSuccess() throws Exception { public void testGetAsyncAnonymousUser() throws Exception { ParseUser.State state = new ParseUser.State.Builder() .objectId("fake") - .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()) .build(); ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(ParseObject.from(state))); + when(store.getGetAsync()).thenReturn(Task.forResult(ParseObject.from(state))); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -233,11 +233,11 @@ public void testGetAsyncAnonymousUser() throws Exception { @Test public void testGetAsyncLazyAnonymousUser() throws Exception { ParseUser.State state = new ParseUser.State.Builder() - .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()) .build(); ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(ParseObject.from(state))); + when(store.getGetAsync()).thenReturn(Task.forResult(ParseObject.from(state))); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -250,7 +250,7 @@ public void testGetAsyncLazyAnonymousUser() throws Exception { public void testGetAsyncWithCurrentUserReadFromDiskFailure() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); + when(store.getGetAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -264,7 +264,7 @@ public void testGetAsyncWithCurrentUserReadFromDiskFailure() throws Exception { public void testGetAsyncWithCurrentUserReadFromDiskFailureAndLazyLogin() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); + when(store.getGetAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); CachedCurrentUserController controller = new CachedCurrentUserController(store); @@ -311,7 +311,7 @@ public void testLogOutAsyncWithDeleteInDiskCurrentUserSuccess() throws Exception public void testLogOutAsyncWithDeleteInDiskCurrentUserFailure() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.getGetAsync()).thenReturn(Task.forResult(null)); when(store.deleteAsync()).thenReturn(Task.forError(new RuntimeException("failure"))); CachedCurrentUserController controller = @@ -375,7 +375,7 @@ public void testGetCurrentSessionTokenAsyncWithCurrentUserSet() throws Exception public void testGetCurrentSessionTokenAsyncWithNoCurrentUserSet() throws Exception { ParseObjectStore store = (ParseObjectStore) mock(ParseObjectStore.class); - when(store.getAsync()).thenReturn(Task.forResult(null)); + when(store.getGetAsync()).thenReturn(Task.forResult(null)); CachedCurrentUserController controller = new CachedCurrentUserController(store); diff --git a/parse/src/test/java/com/parse/EventuallyPinTest.java b/parse/src/test/java/com/parse/EventuallyPinTest.java index d8d6d05cb..a362bb7e5 100644 --- a/parse/src/test/java/com/parse/EventuallyPinTest.java +++ b/parse/src/test/java/com/parse/EventuallyPinTest.java @@ -2,6 +2,8 @@ import android.database.sqlite.SQLiteException; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -9,21 +11,18 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import com.parse.boltsinternal.Task; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class EventuallyPinTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { @@ -45,7 +44,7 @@ public void testFailingFindAllPinned() throws Exception { Parse.setLocalDatastore(offlineStore); when(offlineStore.findFromPinAsync(eq(EventuallyPin.PIN_NAME), any(ParseQuery.State.class), - any(ParseUser.class))) + nullable(ParseUser.class))) .thenReturn(Task.forError(new SQLiteException())); ParsePlugins plugins = mock(ParsePlugins.class); diff --git a/parse/src/test/java/com/parse/FileObjectStoreTest.java b/parse/src/test/java/com/parse/FileObjectStoreTest.java index 0581db5c7..56bd44ef0 100644 --- a/parse/src/test/java/com/parse/FileObjectStoreTest.java +++ b/parse/src/test/java/com/parse/FileObjectStoreTest.java @@ -31,7 +31,7 @@ public class FileObjectStoreTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setUp() { @@ -77,7 +77,7 @@ public void testGetAsync() throws Exception { .thenReturn(builder); FileObjectStore store = new FileObjectStore<>(ParseUser.class, file, coder); - ParseUser user = ParseTaskUtils.wait(store.getAsync()); + ParseUser user = ParseTaskUtils.wait(store.getGetAsync()); assertEquals("bar", user.getState().get("foo")); } diff --git a/parse/src/test/java/com/parse/InstallationIdTest.java b/parse/src/test/java/com/parse/InstallationIdTest.java index fdf6dea9f..23da3cb6f 100644 --- a/parse/src/test/java/com/parse/InstallationIdTest.java +++ b/parse/src/test/java/com/parse/InstallationIdTest.java @@ -16,12 +16,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; public class InstallationIdTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testGetGeneratesInstallationIdAndFile() throws Exception { @@ -78,7 +79,7 @@ public void testInstallationIdIsRandom() { String installationIdString = new InstallationId(installationIdFile).get(); ParseFileUtils.deleteQuietly(installationIdFile); - assertFalse(installationIdString.equals(new InstallationId(installationIdFile).get())); + assertNotEquals(installationIdString, new InstallationId(installationIdFile).get()); } @Test diff --git a/parse/src/test/java/com/parse/ListsTest.java b/parse/src/test/java/com/parse/ListsTest.java deleted file mode 100644 index b32bd301e..000000000 --- a/parse/src/test/java/com/parse/ListsTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -public class ListsTest { - - @Test - public void testPartition() { - List list = new ArrayList<>(); - for (int i = 0; i < 99; i++) { - list.add(i); - } - List> partitions = Lists.partition(list, 5); - assertEquals(20, partitions.size()); - - int count = 0; - for (int i = 0; i < 19; i++) { - List partition = partitions.get(i); - assertEquals(5, partition.size()); - for (int j : partition) { - assertEquals(count, j); - count += 1; - } - } - assertEquals(4, partitions.get(19).size()); - for (int i = 0; i < 4; i++) { - assertEquals(95 + i, partitions.get(19).get(i).intValue()); - } - } -} diff --git a/parse/src/test/java/com/parse/ListsTest.kt b/parse/src/test/java/com/parse/ListsTest.kt new file mode 100644 index 000000000..a9f20a4e0 --- /dev/null +++ b/parse/src/test/java/com/parse/ListsTest.kt @@ -0,0 +1,38 @@ +/* + * 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.junit.Assert +import org.junit.Test +import java.util.* + +class ListsTest { + @Test + fun testPartition() { + val list: MutableList = ArrayList() + for (i in 0..98) { + list.add(i) + } + val partitions = list.chunked(5) + Assert.assertEquals(20, partitions.size.toLong()) + var count = 0 + for (i in 0..18) { + val partition = partitions[i] + Assert.assertEquals(5, partition.size.toLong()) + for (j in partition) { + Assert.assertEquals(count.toLong(), j.toLong()) + count += 1 + } + } + Assert.assertEquals(4, partitions[19].size.toLong()) + for (i in 0..3) { + Assert.assertEquals((95 + i).toLong(), partitions[19][i].toLong()) + } + } +} \ No newline at end of file diff --git a/parse/src/test/java/com/parse/LocalIdManagerTest.java b/parse/src/test/java/com/parse/LocalIdManagerTest.java index acd0dd7d3..37a193f28 100644 --- a/parse/src/test/java/com/parse/LocalIdManagerTest.java +++ b/parse/src/test/java/com/parse/LocalIdManagerTest.java @@ -14,7 +14,6 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -22,11 +21,10 @@ import static org.junit.Assert.assertNull; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class LocalIdManagerTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testLocalIdManager() throws Exception { diff --git a/parse/src/test/java/com/parse/NetworkObjectControllerTest.java b/parse/src/test/java/com/parse/NetworkObjectControllerTest.java index 866af9f30..ef44acfab 100644 --- a/parse/src/test/java/com/parse/NetworkObjectControllerTest.java +++ b/parse/src/test/java/com/parse/NetworkObjectControllerTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -20,7 +21,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.ByteArrayInputStream; import java.net.MalformedURLException; @@ -28,8 +28,6 @@ import java.util.ArrayList; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -39,11 +37,10 @@ // For Uri.encode @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class NetworkObjectControllerTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); @Before public void setUp() throws MalformedURLException { diff --git a/parse/src/test/java/com/parse/NetworkQueryControllerTest.java b/parse/src/test/java/com/parse/NetworkQueryControllerTest.java index 74b9ff82a..3c700b5af 100644 --- a/parse/src/test/java/com/parse/NetworkQueryControllerTest.java +++ b/parse/src/test/java/com/parse/NetworkQueryControllerTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -19,8 +21,6 @@ import java.net.URL; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/parse/src/test/java/com/parse/NetworkSessionControllerTest.java b/parse/src/test/java/com/parse/NetworkSessionControllerTest.java index b2c2a4b1a..587b2da81 100644 --- a/parse/src/test/java/com/parse/NetworkSessionControllerTest.java +++ b/parse/src/test/java/com/parse/NetworkSessionControllerTest.java @@ -15,7 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.net.MalformedURLException; import java.net.URL; @@ -26,7 +25,6 @@ // For Uri.encode @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class NetworkSessionControllerTest { private static JSONObject generateBasicMockResponse() throws JSONException { diff --git a/parse/src/test/java/com/parse/NetworkUserControllerTest.java b/parse/src/test/java/com/parse/NetworkUserControllerTest.java index 9c87aafbf..1cf36219b 100644 --- a/parse/src/test/java/com/parse/NetworkUserControllerTest.java +++ b/parse/src/test/java/com/parse/NetworkUserControllerTest.java @@ -15,7 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.net.MalformedURLException; import java.net.URL; @@ -28,7 +27,6 @@ // For Uri.encode @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class NetworkUserControllerTest { @Before diff --git a/parse/src/test/java/com/parse/OfflineObjectStoreTest.java b/parse/src/test/java/com/parse/OfflineObjectStoreTest.java index 6affeb896..bfda42e83 100644 --- a/parse/src/test/java/com/parse/OfflineObjectStoreTest.java +++ b/parse/src/test/java/com/parse/OfflineObjectStoreTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -18,12 +20,11 @@ import java.util.Arrays; import java.util.Collections; -import com.parse.boltsinternal.Task; - import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyList; @@ -36,11 +37,10 @@ public class OfflineObjectStoreTest { private static final String PIN_NAME = "test"; - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final ExpectedException thrown = ExpectedException.none(); @Rule - public ExpectedException thrown = ExpectedException.none(); + public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setUp() { @@ -82,7 +82,7 @@ public void testGetAsyncFromLDS() throws Exception { //noinspection unchecked when(queryController.findAsync( any(ParseQuery.State.class), - any(ParseUser.class), + nullable(ParseUser.class), any(Task.class)) ).thenReturn(Task.forResult(Collections.singletonList(user))); ParseCorePlugins.getInstance().registerQueryController(queryController); @@ -90,10 +90,10 @@ public void testGetAsyncFromLDS() throws Exception { OfflineObjectStore store = new OfflineObjectStore<>(ParseUser.class, PIN_NAME, null); - ParseUser userAgain = ParseTaskUtils.wait(store.getAsync()); + ParseUser userAgain = ParseTaskUtils.wait(store.getGetAsync()); //noinspection unchecked verify(queryController, times(1)) - .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + .findAsync(any(ParseQuery.State.class), nullable(ParseUser.class), any(Task.class)); assertSame(user, userAgain); } @@ -104,7 +104,7 @@ public void testGetAsyncFromLDSWithTooManyObjects() throws Exception { //noinspection unchecked when(queryController.findAsync( any(ParseQuery.State.class), - any(ParseUser.class), + nullable(ParseUser.class), any(Task.class)) ).thenReturn(Task.forResult(Arrays.asList(mock(ParseUser.class), mock(ParseUser.class)))); ParseCorePlugins.getInstance().registerQueryController(queryController); @@ -114,15 +114,15 @@ public void testGetAsyncFromLDSWithTooManyObjects() throws Exception { @SuppressWarnings("unchecked") ParseObjectStore legacy = mock(ParseObjectStore.class); - when(legacy.getAsync()).thenReturn(Task.forResult(null)); + when(legacy.getGetAsync()).thenReturn(Task.forResult(null)); OfflineObjectStore store = new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); - ParseUser user = ParseTaskUtils.wait(store.getAsync()); + ParseUser user = ParseTaskUtils.wait(store.getGetAsync()); //noinspection unchecked verify(queryController, times(1)) - .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + .findAsync(any(ParseQuery.State.class), nullable(ParseUser.class), any(Task.class)); verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); assertNull(user); } @@ -134,7 +134,7 @@ public void testGetAsyncMigrate() throws Exception { //noinspection unchecked when(queryController.findAsync( any(ParseQuery.State.class), - any(ParseUser.class), + nullable(ParseUser.class), any(Task.class)) ).thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerQueryController(queryController); @@ -150,17 +150,17 @@ public void testGetAsyncMigrate() throws Exception { when(user.pinInBackground(anyString(), anyBoolean())).thenReturn(Task.forResult(null)); @SuppressWarnings("unchecked") ParseObjectStore legacy = mock(ParseObjectStore.class); - when(legacy.getAsync()).thenReturn(Task.forResult(user)); + when(legacy.getGetAsync()).thenReturn(Task.forResult(user)); when(legacy.deleteAsync()).thenReturn(Task.forResult(null)); OfflineObjectStore store = new OfflineObjectStore<>(ParseUser.class, PIN_NAME, legacy); - ParseUser userAgain = ParseTaskUtils.wait(store.getAsync()); + ParseUser userAgain = ParseTaskUtils.wait(store.getGetAsync()); //noinspection unchecked verify(queryController, times(1)) - .findAsync(any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); - verify(legacy, times(1)).getAsync(); + .findAsync(any(ParseQuery.State.class), nullable(ParseUser.class), any(Task.class)); + verify(legacy, times(1)).getGetAsync(); verify(legacy, times(1)).deleteAsync(); verify(lds, times(1)).unpinAllObjectsAsync(PIN_NAME); verify(user, times(1)).pinInBackground(PIN_NAME, false); @@ -179,7 +179,7 @@ public void testExistsAsyncLDS() throws Exception { //noinspection unchecked when(queryController.countAsync( any(ParseQuery.State.class), - any(ParseUser.class), + nullable(ParseUser.class), any(Task.class)) ).thenReturn(Task.forResult(1)); ParseCorePlugins.getInstance().registerQueryController(queryController); @@ -189,7 +189,7 @@ public void testExistsAsyncLDS() throws Exception { assertTrue(ParseTaskUtils.wait(store.existsAsync())); //noinspection unchecked verify(queryController, times(1)).countAsync( - any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + any(ParseQuery.State.class), nullable(ParseUser.class), any(Task.class)); } @Test @@ -199,7 +199,7 @@ public void testExistsAsyncLegacy() throws Exception { //noinspection unchecked when(queryController.countAsync( any(ParseQuery.State.class), - any(ParseUser.class), + nullable(ParseUser.class), any(Task.class)) ).thenReturn(Task.forResult(0)); ParseCorePlugins.getInstance().registerQueryController(queryController); @@ -214,7 +214,7 @@ public void testExistsAsyncLegacy() throws Exception { assertTrue(ParseTaskUtils.wait(store.existsAsync())); //noinspection unchecked verify(queryController, times(1)).countAsync( - any(ParseQuery.State.class), any(ParseUser.class), any(Task.class)); + any(ParseQuery.State.class), nullable(ParseUser.class), any(Task.class)); } //endregion diff --git a/parse/src/test/java/com/parse/OfflineQueryControllerTest.java b/parse/src/test/java/com/parse/OfflineQueryControllerTest.java index e3172f075..284c69765 100644 --- a/parse/src/test/java/com/parse/OfflineQueryControllerTest.java +++ b/parse/src/test/java/com/parse/OfflineQueryControllerTest.java @@ -8,14 +8,14 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Test; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertTrue; public class OfflineQueryControllerTest { @@ -99,8 +99,8 @@ public void testGetFromLDS() { private static class TestOfflineStore extends OfflineStore { - private AtomicBoolean findCalled = new AtomicBoolean(); - private AtomicBoolean countCalled = new AtomicBoolean(); + private final AtomicBoolean findCalled = new AtomicBoolean(); + private final AtomicBoolean countCalled = new AtomicBoolean(); TestOfflineStore() { super((OfflineSQLiteOpenHelper) null); @@ -131,8 +131,8 @@ public void verifyCount() { private static class TestNetworkQueryController implements ParseQueryController { - private AtomicBoolean findCalled = new AtomicBoolean(); - private AtomicBoolean countCalled = new AtomicBoolean(); + private final AtomicBoolean findCalled = new AtomicBoolean(); + private final AtomicBoolean countCalled = new AtomicBoolean(); @Override public Task> findAsync( diff --git a/parse/src/test/java/com/parse/OfflineQueryLogicTest.java b/parse/src/test/java/com/parse/OfflineQueryLogicTest.java index 94f775866..4cdfcf7f5 100644 --- a/parse/src/test/java/com/parse/OfflineQueryLogicTest.java +++ b/parse/src/test/java/com/parse/OfflineQueryLogicTest.java @@ -10,6 +10,8 @@ import androidx.annotation.NonNull; +import com.parse.boltsinternal.Task; + import org.json.JSONArray; import org.json.JSONObject; import org.junit.After; @@ -27,12 +29,11 @@ import java.util.Map; import java.util.regex.Pattern; -import com.parse.boltsinternal.Task; - import static com.parse.ParseMatchers.hasParseErrorCode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -47,7 +48,7 @@ public class OfflineQueryLogicTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private static boolean matches( OfflineQueryLogic logic, ParseQuery.State query, T object) throws ParseException { @@ -1189,7 +1190,7 @@ public void testFetchIncludesJSONObject() throws Exception { @Test public void testFetchIncludesNull() throws ParseException { OfflineStore store = mock(OfflineStore.class); - when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + when(store.fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class))) .thenReturn(Task.forResult(null)); ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") @@ -1202,13 +1203,13 @@ public void testFetchIncludesNull() throws ParseException { ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); // only itself verify(store, times(1)) - .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + .fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class)); } @Test public void testFetchIncludesNonParseObject() throws ParseException { OfflineStore store = mock(OfflineStore.class); - when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + when(store.fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class))) .thenReturn(Task.forResult(null)); ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") @@ -1222,7 +1223,7 @@ public void testFetchIncludesNonParseObject() throws ParseException { ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); // only itself verify(store, times(1)) - .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + .fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class)); } //endregion @@ -1230,7 +1231,7 @@ public void testFetchIncludesNonParseObject() throws ParseException { @Test public void testFetchIncludesDoesNotExist() throws ParseException { OfflineStore store = mock(OfflineStore.class); - when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + when(store.fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class))) .thenReturn(Task.forResult(null)); ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") @@ -1242,13 +1243,13 @@ public void testFetchIncludesDoesNotExist() throws ParseException { ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); // only itself verify(store, times(1)) - .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + .fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class)); } @Test public void testFetchIncludesNestedNull() throws Exception { OfflineStore store = mock(OfflineStore.class); - when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + when(store.fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class))) .thenReturn(Task.forResult(null)); ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") @@ -1261,13 +1262,13 @@ public void testFetchIncludesNestedNull() throws Exception { ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); // only itself verify(store, times(1)) - .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + .fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class)); } @Test public void testFetchIncludesNestedNonParseObject() throws Exception { OfflineStore store = mock(OfflineStore.class); - when(store.fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class))) + when(store.fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class))) .thenReturn(Task.forResult(null)); ParseQuery.State query = new ParseQuery.State.Builder<>("TestObject") @@ -1281,6 +1282,6 @@ public void testFetchIncludesNestedNonParseObject() throws Exception { ParseTaskUtils.wait(OfflineQueryLogic.fetchIncludesAsync(store, object, query, null)); // only itself verify(store, times(1)) - .fetchLocallyAsync(any(ParseObject.class), any(ParseSQLiteDatabase.class)); + .fetchLocallyAsync(any(ParseObject.class), nullable(ParseSQLiteDatabase.class)); } } diff --git a/parse/src/test/java/com/parse/ParseACLTest.java b/parse/src/test/java/com/parse/ParseACLTest.java index 8e000c1ee..183211baf 100644 --- a/parse/src/test/java/com/parse/ParseACLTest.java +++ b/parse/src/test/java/com/parse/ParseACLTest.java @@ -17,7 +17,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.HashMap; import java.util.Map; @@ -36,7 +35,6 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseACLTest { private final static String UNRESOLVED_KEY = "*unresolved"; @@ -165,10 +163,10 @@ public void testToJson() throws Exception { JSONObject aclJson = acl.toJSONObject(mockEncoder); assertEquals("unresolvedUserJson", aclJson.getString("unresolvedUser")); - assertEquals(aclJson.getJSONObject("userId").getBoolean("read"), true); - assertEquals(aclJson.getJSONObject("userId").has("write"), false); - assertEquals(aclJson.getJSONObject("*unresolved").getBoolean("read"), true); - assertEquals(aclJson.getJSONObject("*unresolved").has("write"), false); + assertTrue(aclJson.getJSONObject("userId").getBoolean("read")); + assertFalse(aclJson.getJSONObject("userId").has("write")); + assertTrue(aclJson.getJSONObject("*unresolved").getBoolean("read")); + assertFalse(aclJson.getJSONObject("*unresolved").has("write")); assertEquals(aclJson.length(), 3); } diff --git a/parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java b/parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java index 428525597..b37f4e23e 100644 --- a/parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java +++ b/parse/src/test/java/com/parse/ParseAnalyticsControllerTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -15,19 +17,17 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -36,7 +36,6 @@ // For android.net.Uri @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseAnalyticsControllerTest { @Before @@ -66,7 +65,7 @@ public void testConstructor() { public void testTrackEvent() throws Exception { // Mock eventually queue ParseEventuallyQueue queue = mock(ParseEventuallyQueue.class); - when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), any(ParseObject.class))) + when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), nullable(ParseObject.class))) .thenReturn(Task.forResult(new JSONObject())); // Execute @@ -100,7 +99,7 @@ public void testTrackEvent() throws Exception { public void testTrackAppOpened() throws Exception { // Mock eventually queue ParseEventuallyQueue queue = mock(ParseEventuallyQueue.class); - when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), any(ParseObject.class))) + when(queue.enqueueEventuallyAsync(any(ParseRESTCommand.class), nullable(ParseObject.class))) .thenReturn(Task.forResult(new JSONObject())); // Execute diff --git a/parse/src/test/java/com/parse/ParseAnalyticsTest.java b/parse/src/test/java/com/parse/ParseAnalyticsTest.java index bbebcb999..6f4c44ac5 100644 --- a/parse/src/test/java/com/parse/ParseAnalyticsTest.java +++ b/parse/src/test/java/com/parse/ParseAnalyticsTest.java @@ -11,6 +11,8 @@ import android.content.Intent; import android.os.Bundle; +import com.parse.boltsinternal.Task; + import org.json.JSONException; import org.json.JSONObject; import org.junit.After; @@ -19,15 +21,13 @@ import org.junit.runner.RunWith; import org.mockito.Matchers; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -40,10 +40,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; // For android.os.BaseBundle @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@LooperMode(PAUSED) public class ParseAnalyticsTest { ParseAnalyticsController controller; @@ -143,28 +145,26 @@ public void testTrackEventInBackgroundNormalCallback() throws Exception { dimensions.put("key", "value"); final Semaphore done = new Semaphore(0); ParseAnalytics.trackEventInBackground("test", dimensions, - new SaveCallback() { - @Override - public void done(ParseException e) { - assertNull(e); - done.release(); - } + e -> { + assertNull(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).trackEventInBackground( eq("test"), eq(dimensions), isNull(String.class)); final Semaphore doneAgain = new Semaphore(0); - ParseAnalytics.trackEventInBackground("test", new SaveCallback() { - @Override - public void done(ParseException e) { - assertNull(e); - doneAgain.release(); - } + ParseAnalytics.trackEventInBackground("test", e -> { + assertNull(e); + doneAgain.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(doneAgain.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).trackEventInBackground @@ -224,14 +224,13 @@ public void testTrackAppOpenedInBackgroundNullCallback() throws Exception { public void testTrackAppOpenedInBackgroundNormalCallback() throws Exception { Intent intent = makeIntentWithParseData("test"); final Semaphore done = new Semaphore(0); - ParseAnalytics.trackAppOpenedInBackground(intent, new SaveCallback() { - @Override - public void done(ParseException e) { - assertNull(e); - done.release(); - } + ParseAnalytics.trackAppOpenedInBackground(intent, e -> { + assertNull(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).trackAppOpenedInBackground(eq("test"), isNull(String.class)); @@ -245,7 +244,7 @@ public void done(ParseException e) { public void testGetPushHashFromIntentNullIntent() { String pushHash = ParseAnalytics.getPushHashFromIntent(null); - assertEquals(null, pushHash); + assertNull(pushHash); } @Test @@ -259,7 +258,7 @@ public void testGetPushHashFromIntentEmptyIntent() throws Exception { String pushHash = ParseAnalytics.getPushHashFromIntent(intent); - assertEquals(null, pushHash); + assertNull(pushHash); } @Test @@ -285,7 +284,7 @@ public void testGetPushHashFromIntentWrongPushHashIntent() { String pushHash = ParseAnalytics.getPushHashFromIntent(intent); - assertEquals(null, pushHash); + assertNull(pushHash); } @Test diff --git a/parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java b/parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java index b5daa700b..f1100df4d 100644 --- a/parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java +++ b/parse/src/test/java/com/parse/ParseAuthenticationManagerTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -17,8 +19,6 @@ import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -27,7 +27,7 @@ public class ParseAuthenticationManagerTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private ParseAuthenticationManager manager; private ParseCurrentUserController controller; diff --git a/parse/src/test/java/com/parse/ParseClientConfigurationTest.java b/parse/src/test/java/com/parse/ParseClientConfigurationTest.java index 6d8d96fbd..ce501a2b1 100644 --- a/parse/src/test/java/com/parse/ParseClientConfigurationTest.java +++ b/parse/src/test/java/com/parse/ParseClientConfigurationTest.java @@ -11,13 +11,12 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseClientConfigurationTest { @Test @@ -31,7 +30,7 @@ public void testBuilder() { assertNull(configuration.context); assertEquals(configuration.applicationId, "foo"); assertEquals(configuration.clientKey, "bar"); - assertEquals(configuration.localDataStoreEnabled, true); + assertTrue(configuration.localDataStoreEnabled); } @Test diff --git a/parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java b/parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java index 177ab4ae9..15d04f586 100644 --- a/parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java +++ b/parse/src/test/java/com/parse/ParseCloudCodeControllerTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -24,8 +25,6 @@ import java.util.HashMap; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -131,7 +130,7 @@ public void testCallFunctionInBackgroundSuccessWithResult() throws Exception { ParseCloudCodeController controller = new ParseCloudCodeController(restClient); Task cloudCodeTask = controller.callFunctionInBackground( - "test", new HashMap(), "sessionToken"); + "test", new HashMap<>(), "sessionToken"); ParseTaskUtils.wait(cloudCodeTask); verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); @@ -153,7 +152,7 @@ public void testCallFunctionInBackgroundSuccessWithoutResult() throws Exception ParseCloudCodeController controller = new ParseCloudCodeController(restClient); Task cloudCodeTask = controller.callFunctionInBackground( - "test", new HashMap(), "sessionToken"); + "test", new HashMap<>(), "sessionToken"); ParseTaskUtils.wait(cloudCodeTask); verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); @@ -171,7 +170,7 @@ public void testCallFunctionInBackgroundFailure() throws Exception { ParseCloudCodeController controller = new ParseCloudCodeController(restClient); Task cloudCodeTask = - controller.callFunctionInBackground("test", new HashMap(), "sessionToken"); + controller.callFunctionInBackground("test", new HashMap<>(), "sessionToken"); // Do not use ParseTaskUtils.wait() since we do not want to throw the exception cloudCodeTask.waitForCompletion(); @@ -199,12 +198,12 @@ public void testCallFunctionWithNullResult() throws Exception { ParseCloudCodeController controller = new ParseCloudCodeController(restClient); Task cloudCodeTask = controller.callFunctionInBackground( - "test", new HashMap(), "sessionToken"); + "test", new HashMap<>(), "sessionToken"); ParseTaskUtils.wait(cloudCodeTask); verify(restClient, times(1)).execute(any(ParseHttpRequest.class)); String result = cloudCodeTask.getResult(); - assertEquals(null, result); + assertNull(result); } private ParseHttpClient mockParseHttpClientWithReponse(ParseHttpResponse response) diff --git a/parse/src/test/java/com/parse/ParseCloudTest.java b/parse/src/test/java/com/parse/ParseCloudTest.java index e5e70700a..e4bec4931 100644 --- a/parse/src/test/java/com/parse/ParseCloudTest.java +++ b/parse/src/test/java/com/parse/ParseCloudTest.java @@ -8,11 +8,13 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import java.util.Arrays; import java.util.HashMap; @@ -20,14 +22,13 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import com.parse.boltsinternal.Task; - +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -36,10 +37,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; // For android.os.Looper @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@LooperMode(PAUSED) public class ParseCloudTest extends ResetPluginsParseTest { @Before @@ -74,7 +77,7 @@ public void testCallFunctionAsync() throws Exception { ParseTaskUtils.wait(cloudCodeTask); verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), - isNull(String.class)); + isNull()); assertTrue(cloudCodeTask.isCompleted()); assertNull(cloudCodeTask.getError()); assertThat(cloudCodeTask.getResult(), instanceOf(String.class)); @@ -92,7 +95,7 @@ public void testCallFunctionSync() throws Exception { Object result = ParseCloud.callFunction("name", parameters); verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), - isNull(String.class)); + isNull()); assertThat(result, instanceOf(String.class)); assertEquals("result", result); } @@ -108,7 +111,7 @@ public void testCallFunctionNullCallback() { ParseCloud.callFunctionInBackground("name", parameters, null); verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), - isNull(String.class)); + isNull()); } @Test @@ -120,27 +123,26 @@ public void testCallFunctionNormalCallback() throws Exception { parameters.put("key2", "value1"); final Semaphore done = new Semaphore(0); - ParseCloud.callFunctionInBackground("name", parameters, new FunctionCallback() { - @Override - public void done(Object result, ParseException e) { - assertNull(e); - assertThat(result, instanceOf(String.class)); - assertEquals("result", result); - done.release(); - } + ParseCloud.callFunctionInBackground("name", parameters, (result, e) -> { + assertNull(e); + assertThat(result, instanceOf(String.class)); + assertEquals("result", result); + done.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).callFunctionInBackground(eq("name"), eq(parameters), - isNull(String.class)); + isNull()); } //endregion private ParseCloudCodeController mockParseCloudCodeControllerWithResponse(final T result) { ParseCloudCodeController controller = mock(ParseCloudCodeController.class); - when(controller.callFunctionInBackground(anyString(), anyMap(), anyString())) + when(controller.callFunctionInBackground(anyString(), anyMap(), nullable(String.class))) .thenReturn(Task.forResult(result)); return controller; } diff --git a/parse/src/test/java/com/parse/ParseCoderTest.java b/parse/src/test/java/com/parse/ParseCoderTest.java index 967e55aa7..0b220a762 100644 --- a/parse/src/test/java/com/parse/ParseCoderTest.java +++ b/parse/src/test/java/com/parse/ParseCoderTest.java @@ -12,13 +12,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; // For android.util.Base64 @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseCoderTest { @Test diff --git a/parse/src/test/java/com/parse/ParseConfigControllerTest.java b/parse/src/test/java/com/parse/ParseConfigControllerTest.java index 4525d897b..6d10bd21e 100644 --- a/parse/src/test/java/com/parse/ParseConfigControllerTest.java +++ b/parse/src/test/java/com/parse/ParseConfigControllerTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -28,8 +29,6 @@ import java.util.List; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; diff --git a/parse/src/test/java/com/parse/ParseConfigTest.java b/parse/src/test/java/com/parse/ParseConfigTest.java index a6dbc4920..ca14ead18 100644 --- a/parse/src/test/java/com/parse/ParseConfigTest.java +++ b/parse/src/test/java/com/parse/ParseConfigTest.java @@ -8,13 +8,15 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONArray; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import org.skyscreamer.jsonassert.JSONCompareMode; import java.util.ArrayList; @@ -25,8 +27,6 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import com.parse.boltsinternal.Task; - import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -36,17 +36,19 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; // For android.os.Looper @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@LooperMode(PAUSED) public class ParseConfigTest extends ResetPluginsParseTest { @Before @@ -103,7 +105,7 @@ public void testGetInBackgroundSuccess() throws Exception { ParseTaskUtils.wait(configTask); ParseConfig configAgain = configTask.getResult(); - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); assertEquals(1, configAgain.params.size()); assertEquals("value", configAgain.params.get("string")); } @@ -118,7 +120,7 @@ public void testGetInBackgroundFail() throws Exception { Task configTask = ParseConfig.getInBackground(); configTask.waitForCompletion(); - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); assertThat(configTask.getError(), instanceOf(ParseException.class)); assertEquals(ParseException.CONNECTION_FAILED, ((ParseException) configTask.getError()).getCode()); @@ -135,18 +137,17 @@ public void testGetInBackgroundWithCallbackSuccess() throws Exception { ParseCorePlugins.getInstance().registerConfigController(controller); final Semaphore done = new Semaphore(0); - ParseConfig.getInBackground(new ConfigCallback() { - @Override - public void done(ParseConfig config, ParseException e) { - assertEquals(1, config.params.size()); - assertEquals("value", config.params.get("string")); - done.release(); - } + ParseConfig.getInBackground((config1, e) -> { + assertEquals(1, config1.params.size()); + assertEquals("value", config1.params.get("string")); + done.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); } @Test @@ -156,18 +157,17 @@ public void testGetInBackgroundWithCallbackFail() throws Exception { ParseCorePlugins.getInstance().registerConfigController(controller); final Semaphore done = new Semaphore(0); - ParseConfig.getInBackground(new ConfigCallback() { - @Override - public void done(ParseConfig config, ParseException e) { - assertEquals(ParseException.CONNECTION_FAILED, e.getCode()); - assertEquals("error", e.getMessage()); - done.release(); - } + ParseConfig.getInBackground((config, e) -> { + assertEquals(ParseException.CONNECTION_FAILED, e.getCode()); + assertEquals("error", e.getMessage()); + done.release(); }); + shadowMainLooper().idle(); + // Make sure the callback is called assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); } @Test @@ -181,7 +181,7 @@ public void testGetSyncSuccess() throws Exception { ParseConfig configAgain = ParseConfig.get(); - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); assertEquals(1, configAgain.params.size()); assertEquals("value", configAgain.params.get("string")); } @@ -196,7 +196,7 @@ public void testGetSyncFail() { ParseConfig.get(); fail("Should throw an exception"); } catch (ParseException e) { - verify(controller, times(1)).getAsync(anyString()); + verify(controller, times(1)).getAsync(nullable(String.class)); assertEquals(ParseException.CONNECTION_FAILED, e.getCode()); assertEquals("error", e.getMessage()); } @@ -1007,14 +1007,14 @@ public void testToStringParseGeoPoint() { private ParseConfigController mockParseConfigControllerWithResponse(final ParseConfig result) { ParseConfigController controller = mock(ParseConfigController.class); - when(controller.getAsync(anyString())) + when(controller.getAsync(nullable(String.class))) .thenReturn(Task.forResult(result)); return controller; } private ParseConfigController mockParseConfigControllerWithException(Exception exception) { ParseConfigController controller = mock(ParseConfigController.class); - when(controller.getAsync(anyString())) + when(controller.getAsync(nullable(String.class))) .thenReturn(Task.forError(exception)); return controller; } diff --git a/parse/src/test/java/com/parse/ParseCorePluginsTest.java b/parse/src/test/java/com/parse/ParseCorePluginsTest.java index 4694dfa82..93fc68aa3 100644 --- a/parse/src/test/java/com/parse/ParseCorePluginsTest.java +++ b/parse/src/test/java/com/parse/ParseCorePluginsTest.java @@ -8,22 +8,20 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseCorePluginsTest extends ResetPluginsParseTest { @Before diff --git a/parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java b/parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java index 008d05407..9137d5c5c 100644 --- a/parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java +++ b/parse/src/test/java/com/parse/ParseCountingByteArrayHttpBodyTest.java @@ -16,6 +16,7 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -62,7 +63,7 @@ public void done(Integer percentDone) { body.writeTo(output); // Check content - assertTrue(Arrays.equals(content, output.toByteArray())); + assertArrayEquals(content, output.toByteArray()); // Check progress callback assertTrue(didReportIntermediateProgress.tryAcquire(5, TimeUnit.SECONDS)); diff --git a/parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java b/parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java index 3e9bb6bc9..7733d1c05 100644 --- a/parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java +++ b/parse/src/test/java/com/parse/ParseCountingFileHttpBodyTest.java @@ -27,7 +27,7 @@ public class ParseCountingFileHttpBodyTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private static String getData() { char[] chars = new char[64 << 14]; // 1MB diff --git a/parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java b/parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java index c66b75c46..9036cc27f 100644 --- a/parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java +++ b/parse/src/test/java/com/parse/ParseCurrentConfigControllerTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; @@ -20,8 +22,6 @@ import java.util.List; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -33,7 +33,7 @@ public class ParseCurrentConfigControllerTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); //region testConstructor diff --git a/parse/src/test/java/com/parse/ParseDecoderTest.java b/parse/src/test/java/com/parse/ParseDecoderTest.java index 056186083..9316cafd0 100644 --- a/parse/src/test/java/com/parse/ParseDecoderTest.java +++ b/parse/src/test/java/com/parse/ParseDecoderTest.java @@ -16,7 +16,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Date; @@ -32,11 +31,10 @@ // For android.util.Base64 @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseDecoderTest extends ResetPluginsParseTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); @Test public void testJSONArray() { diff --git a/parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java b/parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java index 8289968e0..afebcc0eb 100644 --- a/parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java +++ b/parse/src/test/java/com/parse/ParseDefaultACLControllerTest.java @@ -8,14 +8,14 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Test; import java.lang.ref.WeakReference; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; diff --git a/parse/src/test/java/com/parse/ParseEncoderTest.java b/parse/src/test/java/com/parse/ParseEncoderTest.java index 5d09f4667..f9da56165 100644 --- a/parse/src/test/java/com/parse/ParseEncoderTest.java +++ b/parse/src/test/java/com/parse/ParseEncoderTest.java @@ -17,7 +17,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Date; @@ -30,11 +29,10 @@ // For android.util.Base64 @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseEncoderTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); ParseEncoderTestClass testClassObject = null; @Before diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index b31f7e45a..601febfb4 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -19,7 +20,6 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.ByteArrayInputStream; import java.io.File; @@ -27,8 +27,6 @@ import java.net.MalformedURLException; import java.net.URL; -import com.parse.boltsinternal.Task; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -43,11 +41,10 @@ // For org.json @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseFileControllerTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setUp() throws MalformedURLException { diff --git a/parse/src/test/java/com/parse/ParseFileHttpBodyTest.java b/parse/src/test/java/com/parse/ParseFileHttpBodyTest.java index a69066858..2e6add15a 100644 --- a/parse/src/test/java/com/parse/ParseFileHttpBodyTest.java +++ b/parse/src/test/java/com/parse/ParseFileHttpBodyTest.java @@ -25,7 +25,7 @@ public class ParseFileHttpBodyTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); // Generate a test file used for create ParseFileHttpBody, if you change file's content, make sure // you also change the test file content in verifyTestFileContent(). diff --git a/parse/src/test/java/com/parse/ParseFileRequestTest.java b/parse/src/test/java/com/parse/ParseFileRequestTest.java index 621a71149..624bf7bb3 100644 --- a/parse/src/test/java/com/parse/ParseFileRequestTest.java +++ b/parse/src/test/java/com/parse/ParseFileRequestTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -16,8 +17,6 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import com.parse.boltsinternal.Task; - import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/parse/src/test/java/com/parse/ParseFileStateTest.java b/parse/src/test/java/com/parse/ParseFileStateTest.java index fd1c56ba8..d077124c1 100644 --- a/parse/src/test/java/com/parse/ParseFileStateTest.java +++ b/parse/src/test/java/com/parse/ParseFileStateTest.java @@ -15,7 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; @@ -24,7 +23,6 @@ // For android.webkit.MimeTypeMap @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseFileStateTest { @Before @@ -41,7 +39,7 @@ public void tearDown() { public void testDefaults() { ParseFile.State state = new ParseFile.State.Builder().build(); assertEquals("file", state.name()); - assertEquals(null, state.mimeType()); + assertNull(state.mimeType()); assertNull(state.url()); } @@ -84,6 +82,6 @@ public void testMimeTypeNotSetFromExtension() { ParseFile.State state = new ParseFile.State.Builder() .name("test.txt") .build(); - assertEquals(null, state.mimeType()); + assertNull(state.mimeType()); } } diff --git a/parse/src/test/java/com/parse/ParseFileTest.java b/parse/src/test/java/com/parse/ParseFileTest.java index 61ef9bc21..546f459a8 100644 --- a/parse/src/test/java/com/parse/ParseFileTest.java +++ b/parse/src/test/java/com/parse/ParseFileTest.java @@ -10,6 +10,8 @@ import android.os.Parcel; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -19,22 +21,19 @@ import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.File; import java.io.InputStream; import java.util.Arrays; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -42,11 +41,10 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseFileTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setup() { @@ -76,12 +74,12 @@ public void testConstructor() throws Exception { parseFile = new ParseFile(data); assertEquals("file", parseFile.getName()); // Default - assertEquals(null, parseFile.getState().mimeType()); + assertNull(parseFile.getState().mimeType()); assertTrue(parseFile.isDirty()); parseFile = new ParseFile(name, data); assertEquals("name", parseFile.getName()); - assertEquals(null, parseFile.getState().mimeType()); + assertNull(parseFile.getState().mimeType()); assertTrue(parseFile.isDirty()); parseFile = new ParseFile(data, contentType); @@ -91,7 +89,7 @@ public void testConstructor() throws Exception { parseFile = new ParseFile(file); assertEquals(name, parseFile.getName()); // Default - assertEquals(null, parseFile.getState().mimeType()); + assertNull(parseFile.getState().mimeType()); assertTrue(parseFile.isDirty()); parseFile = new ParseFile(file, contentType); @@ -187,9 +185,9 @@ public void testSaveAsyncSuccessWithData() throws Exception { when(controller.saveAsync( any(ParseFile.State.class), any(byte[].class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(state)); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))).thenReturn(Task.forResult(state)); ParseCorePlugins.getInstance().registerFileController(controller); ParseFile parseFile = new ParseFile(name, data, contentType); @@ -201,9 +199,9 @@ public void testSaveAsyncSuccessWithData() throws Exception { verify(controller, times(1)).saveAsync( stateCaptor.capture(), dataCaptor.capture(), - any(String.class), - any(ProgressCallback.class), - Matchers.>any()); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class)); assertNull(stateCaptor.getValue().url()); assertEquals(name, stateCaptor.getValue().name()); assertEquals(contentType, stateCaptor.getValue().mimeType()); @@ -225,9 +223,9 @@ public void testSaveAsyncSuccessWithFile() throws Exception { when(controller.saveAsync( any(ParseFile.State.class), any(File.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(state)); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))).thenReturn(Task.forResult(state)); ParseCorePlugins.getInstance().registerFileController(controller); ParseFile parseFile = new ParseFile(file, contentType); @@ -239,9 +237,9 @@ public void testSaveAsyncSuccessWithFile() throws Exception { verify(controller, times(1)).saveAsync( stateCaptor.capture(), fileCaptor.capture(), - any(String.class), - any(ProgressCallback.class), - Matchers.>any()); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class)); assertNull(stateCaptor.getValue().url()); assertEquals(name, stateCaptor.getValue().name()); assertEquals(contentType, stateCaptor.getValue().mimeType()); @@ -267,9 +265,9 @@ public void testGetDataAsyncSuccess() throws Exception { ParseFileController controller = mock(ParseFileController.class); when(controller.fetchAsync( any(ParseFile.State.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(file)); + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class))).thenReturn(Task.forResult(file)); ParseCorePlugins.getInstance().registerFileController(controller); String url = "url"; @@ -284,9 +282,9 @@ public void testGetDataAsyncSuccess() throws Exception { ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(1)).fetchAsync( stateCaptor.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptor.getValue().url()); // Verify the data we get is correct @@ -300,9 +298,9 @@ public void testGetDataAsyncSuccess() throws Exception { ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(2)).fetchAsync( stateCaptorAgain.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptorAgain.getValue().url()); // Verify the data we get is correct @@ -317,9 +315,9 @@ public void testGetDataStreamAsyncSuccess() throws Exception { ParseFileController controller = mock(ParseFileController.class); when(controller.fetchAsync( any(ParseFile.State.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(file)); + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class))).thenReturn(Task.forResult(file)); ParseCorePlugins.getInstance().registerFileController(controller); String url = "url"; @@ -334,9 +332,9 @@ public void testGetDataStreamAsyncSuccess() throws Exception { ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(1)).fetchAsync( stateCaptor.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptor.getValue().url()); // Verify the data we get is correct @@ -350,9 +348,9 @@ public void testGetDataStreamAsyncSuccess() throws Exception { ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(2)).fetchAsync( stateCaptorAgain.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptorAgain.getValue().url()); // Verify the data we get is correct @@ -367,9 +365,9 @@ public void testGetFileAsyncSuccess() throws Exception { ParseFileController controller = mock(ParseFileController.class); when(controller.fetchAsync( any(ParseFile.State.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(file)); + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class))).thenReturn(Task.forResult(file)); ParseCorePlugins.getInstance().registerFileController(controller); String url = "url"; @@ -384,9 +382,9 @@ public void testGetFileAsyncSuccess() throws Exception { ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(1)).fetchAsync( stateCaptor.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptor.getValue().url()); // Verify the data we get is correct @@ -400,9 +398,9 @@ public void testGetFileAsyncSuccess() throws Exception { ArgumentCaptor.forClass(ParseFile.State.class); verify(controller, times(2)).fetchAsync( stateCaptorAgain.capture(), - anyString(), - any(ProgressCallback.class), - Matchers.>any() + nullable(String.class), + nullable(ProgressCallback.class), + any(Task.class) ); assertEquals(url, stateCaptorAgain.getValue().url()); // Verify the data we get is correct @@ -420,20 +418,20 @@ public void testTaskQueuedMethods() throws Exception { when(controller.saveAsync( any(ParseFile.State.class), any(byte[].class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(state)); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))).thenReturn(Task.forResult(state)); when(controller.saveAsync( any(ParseFile.State.class), - any(File.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(state)); + nullable(File.class), + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))).thenReturn(Task.forResult(state)); when(controller.fetchAsync( any(ParseFile.State.class), - any(String.class), - any(ProgressCallback.class), - Matchers.>any())).thenReturn(Task.forResult(cachedFile)); + nullable(String.class), + nullable(ProgressCallback.class), + nullable(Task.class))).thenReturn(Task.forResult(cachedFile)); ParseCorePlugins.getInstance().registerFileController(controller); diff --git a/parse/src/test/java/com/parse/ParseFileUtilsTest.java b/parse/src/test/java/com/parse/ParseFileUtilsTest.java index 684f6c3fe..77a254026 100644 --- a/parse/src/test/java/com/parse/ParseFileUtilsTest.java +++ b/parse/src/test/java/com/parse/ParseFileUtilsTest.java @@ -14,7 +14,6 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; @@ -22,19 +21,19 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseFileUtilsTest { private static final String TEST_STRING = "this is a test string"; private static final String TEST_JSON = "{ \"foo\": \"bar\" }"; @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testReadFileToString() throws Exception { @@ -42,7 +41,7 @@ public void testReadFileToString() throws Exception { BufferedOutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(file)); - out.write(TEST_STRING.getBytes("UTF-8")); + out.write(TEST_STRING.getBytes(StandardCharsets.UTF_8)); } finally { ParseIOUtils.closeQuietly(out); } @@ -61,7 +60,7 @@ public void testWriteStringToFile() throws Exception { in = new FileInputStream(file); ByteArrayOutputStream out = new ByteArrayOutputStream(); ParseIOUtils.copy(in, out); - content = new String(out.toByteArray(), "UTF-8"); + content = new String(out.toByteArray(), StandardCharsets.UTF_8); } finally { ParseIOUtils.closeQuietly(in); } @@ -75,7 +74,7 @@ public void testReadFileToJSONObject() throws Exception { BufferedOutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(file)); - out.write(TEST_JSON.getBytes("UTF-8")); + out.write(TEST_JSON.getBytes(StandardCharsets.UTF_8)); } finally { ParseIOUtils.closeQuietly(out); } diff --git a/parse/src/test/java/com/parse/ParseGeoPointTest.java b/parse/src/test/java/com/parse/ParseGeoPointTest.java index 3aacb7c44..5962474ce 100644 --- a/parse/src/test/java/com/parse/ParseGeoPointTest.java +++ b/parse/src/test/java/com/parse/ParseGeoPointTest.java @@ -13,14 +13,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotEquals; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseGeoPointTest { @Test @@ -46,13 +43,13 @@ public void testEquals() { ParseGeoPoint pointB = new ParseGeoPoint(30d, 50d); ParseGeoPoint pointC = new ParseGeoPoint(45d, 45d); - assertTrue(pointA.equals(pointB)); - assertTrue(pointA.equals(pointA)); - assertTrue(pointB.equals(pointA)); + assertEquals(pointA, pointB); + assertEquals(pointA, pointA); + assertEquals(pointB, pointA); - assertFalse(pointA.equals(null)); - assertFalse(pointA.equals(true)); - assertFalse(pointA.equals(pointC)); + assertNotEquals(null, pointA); + assertNotEquals(true, pointA); + assertNotEquals(pointA, pointC); } @Test diff --git a/parse/src/test/java/com/parse/ParseHttpClientTest.java b/parse/src/test/java/com/parse/ParseHttpClientTest.java index 94982a2ed..e6ae25b1a 100644 --- a/parse/src/test/java/com/parse/ParseHttpClientTest.java +++ b/parse/src/test/java/com/parse/ParseHttpClientTest.java @@ -15,7 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.ByteArrayOutputStream; import java.util.HashMap; @@ -37,7 +36,6 @@ import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseHttpClientTest { // We can not use ParameterizedRobolectricTestRunner right now since Robolectric use diff --git a/parse/src/test/java/com/parse/ParseInstallationTest.java b/parse/src/test/java/com/parse/ParseInstallationTest.java index 311232744..8b070c32b 100644 --- a/parse/src/test/java/com/parse/ParseInstallationTest.java +++ b/parse/src/test/java/com/parse/ParseInstallationTest.java @@ -12,22 +12,22 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; import java.util.Collections; import java.util.Locale; import java.util.TimeZone; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -39,7 +39,6 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseInstallationTest extends ResetPluginsParseTest { private static final String KEY_INSTALLATION_ID = "installationId"; private static final String KEY_DEVICE_TYPE = "deviceType"; @@ -280,7 +279,7 @@ public void testUpdateBeforeSave() throws Exception { assertEquals(zone, deviceZone); } else { // If it's not updated it's because it was not acceptable. - assertFalse(deviceZone.equals("GMT")); + assertNotEquals("GMT", deviceZone); assertFalse(deviceZone.indexOf("/") > 0); } diff --git a/parse/src/test/java/com/parse/ParseKeyValueCacheTest.java b/parse/src/test/java/com/parse/ParseKeyValueCacheTest.java index 22e026885..5b5ad5de9 100644 --- a/parse/src/test/java/com/parse/ParseKeyValueCacheTest.java +++ b/parse/src/test/java/com/parse/ParseKeyValueCacheTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import com.parse.boltsinternal.Task; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -17,9 +19,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.Task; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -29,7 +28,7 @@ public class ParseKeyValueCacheTest { @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private File keyValueCacheDir; @Before @@ -57,12 +56,9 @@ public void testMultipleAsynchronousWrites() throws ParseException { List> tasks = new ArrayList<>(); for (int i = 0; i < 1000; i++) { - tasks.add(Task.call(new Callable() { - @Override - public Void call() { - ParseKeyValueCache.saveToKeyValueCache("foo", "test"); - return null; - } + tasks.add(Task.call(() -> { + ParseKeyValueCache.saveToKeyValueCache("foo", "test"); + return null; }, Task.BACKGROUND_EXECUTOR)); } ParseTaskUtils.wait(Task.whenAll(tasks)); diff --git a/parse/src/test/java/com/parse/ParseObjectStateTest.java b/parse/src/test/java/com/parse/ParseObjectStateTest.java index 03f55e5b6..71f232141 100644 --- a/parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -13,7 +13,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.Date; @@ -24,7 +23,6 @@ import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectStateTest { @Test diff --git a/parse/src/test/java/com/parse/ParseObjectTest.java b/parse/src/test/java/com/parse/ParseObjectTest.java index ae9b096c4..c549ce80f 100644 --- a/parse/src/test/java/com/parse/ParseObjectTest.java +++ b/parse/src/test/java/com/parse/ParseObjectTest.java @@ -10,6 +10,9 @@ import android.os.Parcel; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -21,7 +24,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; import java.net.URL; import java.util.ArrayList; @@ -34,14 +36,12 @@ import java.util.Map; import java.util.Set; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -49,11 +49,10 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private static void mockCurrentUserController() { ParseCurrentUserController userController = mock(ParseCurrentUserController.class); @@ -68,7 +67,7 @@ private static TaskCompletionSource mockObjectControllerForSa ParseObjectController objectController = mock(ParseObjectController.class); when(objectController.saveAsync( any(ParseObject.State.class), any(ParseOperationSet.class), - anyString(), any(ParseDecoder.class)) + nullable(String.class), any(ParseDecoder.class)) ).thenReturn(tcs.getTask()); ParseCorePlugins.getInstance().registerObjectController(objectController); return tcs; @@ -117,8 +116,8 @@ public void testFromJSONPayload() throws JSONException { assertEquals("GameScore", parseObject.getClassName()); assertEquals("TT1ZskATqS", parseObject.getObjectId()); ParseDateFormat format = ParseDateFormat.getInstance(); - assertTrue(parseObject.getCreatedAt().equals(format.parse("2015-06-22T21:23:41.733Z"))); - assertTrue(parseObject.getUpdatedAt().equals(format.parse("2015-06-22T22:06:18.104Z"))); + assertEquals(parseObject.getCreatedAt(), format.parse("2015-06-22T21:23:41.733Z")); + assertEquals(parseObject.getUpdatedAt(), format.parse("2015-06-22T22:06:18.104Z")); Set keys = parseObject.getState().keySet(); assertEquals(0, keys.size()); diff --git a/parse/src/test/java/com/parse/ParseOkHttpClientTest.java b/parse/src/test/java/com/parse/ParseOkHttpClientTest.java index 7d57a62bc..003ebfa19 100644 --- a/parse/src/test/java/com/parse/ParseOkHttpClientTest.java +++ b/parse/src/test/java/com/parse/ParseOkHttpClientTest.java @@ -14,7 +14,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -39,10 +38,9 @@ import static org.junit.Assert.assertNull; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseOkHttpClientTest { - private MockWebServer server = new MockWebServer(); + private final MockWebServer server = new MockWebServer(); //region testTransferRequest/Response @@ -75,7 +73,7 @@ public void testGetOkHttpRequestType() throws IOException { .build(); okHttpRequest = parseClient.getRequest(parseRequest); assertEquals(ParseHttpRequest.Method.DELETE.toString(), okHttpRequest.method()); - assertEquals(null, okHttpRequest.body()); + assertNull(okHttpRequest.body()); // Put parseRequest = builder diff --git a/parse/src/test/java/com/parse/ParsePolygonTest.java b/parse/src/test/java/com/parse/ParsePolygonTest.java index 412b891e0..d44724bd8 100644 --- a/parse/src/test/java/com/parse/ParsePolygonTest.java +++ b/parse/src/test/java/com/parse/ParsePolygonTest.java @@ -13,7 +13,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Arrays; @@ -21,10 +20,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParsePolygonTest { @Test @@ -82,13 +81,13 @@ public void testEquality() { ParsePolygon polygonB = new ParsePolygon(points); ParsePolygon polygonC = new ParsePolygon(diff); - assertTrue(polygonA.equals(polygonB)); - assertTrue(polygonA.equals(polygonA)); - assertTrue(polygonB.equals(polygonA)); + assertEquals(polygonA, polygonB); + assertEquals(polygonA, polygonA); + assertEquals(polygonB, polygonA); - assertFalse(polygonA.equals(null)); - assertFalse(polygonA.equals(true)); - assertFalse(polygonA.equals(polygonC)); + assertNotEquals(null, polygonA); + assertNotEquals(true, polygonA); + assertNotEquals(polygonA, polygonC); } @Test diff --git a/parse/src/test/java/com/parse/ParsePushControllerTest.java b/parse/src/test/java/com/parse/ParsePushControllerTest.java index d716d4a48..80f09ed25 100644 --- a/parse/src/test/java/com/parse/ParsePushControllerTest.java +++ b/parse/src/test/java/com/parse/ParsePushControllerTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -19,7 +20,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import java.io.ByteArrayInputStream; @@ -29,8 +29,6 @@ import java.util.ArrayList; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -45,7 +43,6 @@ // For SSLSessionCache @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParsePushControllerTest { private static boolean containsString(JSONArray array, String value) throws JSONException { diff --git a/parse/src/test/java/com/parse/ParsePushStateTest.java b/parse/src/test/java/com/parse/ParsePushStateTest.java index d21690bf6..f997cbe8c 100644 --- a/parse/src/test/java/com/parse/ParsePushStateTest.java +++ b/parse/src/test/java/com/parse/ParsePushStateTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -51,12 +52,12 @@ public void testDefaultsWithData() throws Exception { .data(data) .build(); - assertEquals(null, state.expirationTime()); - assertEquals(null, state.expirationTimeInterval()); - assertEquals(null, state.pushTime()); - assertEquals(null, state.channelSet()); + assertNull(state.expirationTime()); + assertNull(state.expirationTimeInterval()); + assertNull(state.pushTime()); + assertNull(state.channelSet()); JSONAssert.assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); - assertEquals(null, state.queryState()); + assertNull(state.queryState()); } //endregion @@ -102,7 +103,7 @@ public void testExpirationTimeNullTime() { .data(new JSONObject()) .build(); - assertEquals(null, state.expirationTime()); + assertNull(state.expirationTime()); } @Test @@ -130,7 +131,7 @@ public void testExpirationTimeIntervalNullInterval() { .data(new JSONObject()) .build(); - assertEquals(null, state.expirationTimeInterval()); + assertNull(state.expirationTimeInterval()); } @Test @@ -158,7 +159,7 @@ public void testPushTimeNullTime() { .data(new JSONObject()) .build(); - assertEquals(null, state.pushTime()); + assertNull(state.pushTime()); } @Test @@ -315,7 +316,7 @@ public void testQueryNotInstallationQuery() { ParsePush.State.Builder builder = new ParsePush.State.Builder(); ParsePush.State state = builder - .query(new ParseQuery("test")) + .query(new ParseQuery<>("test")) .data(new JSONObject()) .build(); } diff --git a/parse/src/test/java/com/parse/ParsePushTest.java b/parse/src/test/java/com/parse/ParsePushTest.java index e818c2e60..726067ac3 100644 --- a/parse/src/test/java/com/parse/ParsePushTest.java +++ b/parse/src/test/java/com/parse/ParsePushTest.java @@ -8,6 +8,9 @@ */ package com.parse; +import com.parse.boltsinternal.Capture; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -15,7 +18,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import org.skyscreamer.jsonassert.JSONCompareMode; import java.util.ArrayList; @@ -23,23 +26,23 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@LooperMode(PAUSED) public class ParsePushTest { @Before @@ -271,13 +274,13 @@ public void testSubscribeInBackgroundWithCallbackSuccess() throws Exception { ParsePush push = new ParsePush(); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.subscribeInBackground("test", new SaveCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.subscribeInBackground("test", e -> { + exceptionCapture.set(e); + done.release(); }); + + shadowMainLooper().idle(); + assertNull(exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).subscribeInBackground("test"); @@ -307,13 +310,13 @@ public void testSubscribeInBackgroundWithCallbackFail() throws Exception { ParsePush push = new ParsePush(); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.subscribeInBackground("test", new SaveCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.subscribeInBackground("test", e -> { + exceptionCapture.set(e); + done.release(); }); + + shadowMainLooper().idle(); + assertSame(exception, exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).subscribeInBackground("test"); @@ -341,13 +344,13 @@ public void testUnsubscribeInBackgroundWithCallbackSuccess() throws Exception { final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.unsubscribeInBackground("test", new SaveCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.unsubscribeInBackground("test", e -> { + exceptionCapture.set(e); + done.release(); }); + + shadowMainLooper().idle(); + assertNull(exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).unsubscribeInBackground("test"); @@ -379,13 +382,13 @@ public void testUnsubscribeInBackgroundWithCallbackFail() throws Exception { ParsePush push = new ParsePush(); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.unsubscribeInBackground("test", new SaveCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.unsubscribeInBackground("test", e -> { + exceptionCapture.set(e); + done.release(); }); + + shadowMainLooper().idle(); + assertSame(exception, exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); verify(controller, times(1)).unsubscribeInBackground("test"); @@ -423,7 +426,7 @@ public void testGetPushController() { public void testSendInBackgroundSuccess() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -441,7 +444,7 @@ public void testSendInBackgroundSuccess() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -453,7 +456,7 @@ public void testSendInBackgroundSuccess() throws Exception { public void testSendInBackgroundWithCallbackSuccess() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -469,19 +472,18 @@ public void testSendInBackgroundWithCallbackSuccess() throws Exception { .channelSet(channels); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - push.sendInBackground(new SendCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + push.sendInBackground(e -> { + exceptionCapture.set(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure controller is executed and state parameter is correct assertNull(exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -494,7 +496,7 @@ public void testSendInBackgroundFail() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forError(exception)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -513,7 +515,7 @@ public void testSendInBackgroundFail() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -529,7 +531,7 @@ public void testSendInBackgroundWithCallbackFail() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forError(exception)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -545,19 +547,18 @@ public void testSendInBackgroundWithCallbackFail() throws Exception { .channelSet(channels); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - push.sendInBackground(new SendCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + push.sendInBackground(e -> { + exceptionCapture.set(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure controller is executed and state parameter is correct assertSame(exception, exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -569,7 +570,7 @@ public void done(ParseException e) { public void testSendSuccess() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -587,7 +588,7 @@ public void testSendSuccess() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -600,7 +601,7 @@ public void testSendFail() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); final ParseException exception = new ParseException(ParseException.OTHER_CAUSE, "error"); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forError(exception)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -622,7 +623,7 @@ public void testSendFail() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); assertEquals(data, state.data(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(2, state.channelSet().size()); @@ -637,7 +638,7 @@ public void testSendFail() throws Exception { public void testSendMessageInBackground() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -649,7 +650,7 @@ public void testSendMessageInBackground() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); // Verify query state ParseQuery.State queryState = state.queryState(); @@ -663,7 +664,7 @@ public void testSendMessageInBackground() throws Exception { public void testSendMessageInBackgroundWithCallback() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -673,19 +674,18 @@ public void testSendMessageInBackgroundWithCallback() throws Exception { .whereEqualTo("foo", "bar"); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.sendMessageInBackground("test", query, new SendCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.sendMessageInBackground("test", query, e -> { + exceptionCapture.set(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure controller is executed and state parameter is correct assertNull(exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); // Verify query state ParseQuery.State queryState = state.queryState(); @@ -703,7 +703,7 @@ public void done(ParseException e) { public void testSendDataInBackground() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -718,7 +718,7 @@ public void testSendDataInBackground() throws Exception { // Make sure controller is executed and state parameter is correct ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); // Verify query state ParseQuery.State queryState = state.queryState(); @@ -732,7 +732,7 @@ public void testSendDataInBackground() throws Exception { public void testSendDataInBackgroundWithCallback() throws Exception { // Mock controller ParsePushController controller = mock(ParsePushController.class); - when(controller.sendInBackground(any(ParsePush.State.class), anyString())) + when(controller.sendInBackground(any(ParsePush.State.class), nullable(String.class))) .thenReturn(Task.forResult(null)); ParseCorePlugins.getInstance().registerPushController(controller); @@ -745,19 +745,18 @@ public void testSendDataInBackgroundWithCallback() throws Exception { .whereEqualTo("foo", "bar"); final Semaphore done = new Semaphore(0); final Capture exceptionCapture = new Capture<>(); - ParsePush.sendDataInBackground(data, query, new SendCallback() { - @Override - public void done(ParseException e) { - exceptionCapture.set(e); - done.release(); - } + ParsePush.sendDataInBackground(data, query, e -> { + exceptionCapture.set(e); + done.release(); }); + shadowMainLooper().idle(); + // Make sure controller is executed and state parameter is correct assertNull(exceptionCapture.get()); assertTrue(done.tryAcquire(1, 10, TimeUnit.SECONDS)); ArgumentCaptor stateCaptor = ArgumentCaptor.forClass(ParsePush.State.class); - verify(controller, times(1)).sendInBackground(stateCaptor.capture(), anyString()); + verify(controller, times(1)).sendInBackground(stateCaptor.capture(), nullable(String.class)); ParsePush.State state = stateCaptor.getValue(); // Verify query state ParseQuery.State queryState = state.queryState(); diff --git a/parse/src/test/java/com/parse/ParseQueryStateTest.java b/parse/src/test/java/com/parse/ParseQueryStateTest.java index 313f0d9b5..dc919c02e 100644 --- a/parse/src/test/java/com/parse/ParseQueryStateTest.java +++ b/parse/src/test/java/com/parse/ParseQueryStateTest.java @@ -13,7 +13,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -30,7 +29,6 @@ import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseQueryStateTest extends ResetPluginsParseTest { @Test @@ -216,7 +214,7 @@ public void testOrIsMutable() { @Test(expected = IllegalArgumentException.class) public void testOrThrowsWithEmptyList() { - ParseQuery.State.Builder.or(new ArrayList>()).build(); + ParseQuery.State.Builder.or(new ArrayList<>()).build(); } @Test(expected = IllegalArgumentException.class) diff --git a/parse/src/test/java/com/parse/ParseQueryTest.java b/parse/src/test/java/com/parse/ParseQueryTest.java index 5ba86a89b..f21773512 100644 --- a/parse/src/test/java/com/parse/ParseQueryTest.java +++ b/parse/src/test/java/com/parse/ParseQueryTest.java @@ -10,6 +10,10 @@ import androidx.annotation.NonNull; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; +import com.parse.boltsinternal.TaskCompletionSource; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -24,10 +28,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; -import com.parse.boltsinternal.TaskCompletionSource; - import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -854,12 +854,7 @@ private static class TestQueryController implements ParseQueryController { private Task toAwait = Task.forResult(null); public Task await(final Task task) { - toAwait = toAwait.continueWithTask(new Continuation>() { - @Override - public Task then(Task ignored) { - return task; - } - }); + toAwait = toAwait.continueWithTask(ignored -> task); return toAwait; } @@ -867,21 +862,15 @@ public Task then(Task ignored) { public Task> findAsync(ParseQuery.State state, ParseUser user, Task cancellationToken) { final AtomicBoolean cancelled = new AtomicBoolean(false); - cancellationToken.continueWith(new Continuation() { - @Override - public Void then(Task task) { - cancelled.set(true); - return null; - } + cancellationToken.continueWith((Continuation) task -> { + cancelled.set(true); + return null; }); - return await(Task.forResult(null).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (cancelled.get()) { - return Task.cancelled(); - } - return task; + return await(Task.forResult(null).continueWithTask(task -> { + if (cancelled.get()) { + return Task.cancelled(); } + return task; })).cast(); } @@ -889,21 +878,15 @@ public Task then(Task task) { public Task countAsync(ParseQuery.State state, ParseUser user, Task cancellationToken) { final AtomicBoolean cancelled = new AtomicBoolean(false); - cancellationToken.continueWith(new Continuation() { - @Override - public Void then(Task task) { - cancelled.set(true); - return null; - } + cancellationToken.continueWith((Continuation) task -> { + cancelled.set(true); + return null; }); - return await(Task.forResult(null).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (cancelled.get()) { - return Task.cancelled(); - } - return task; + return await(Task.forResult(null).continueWithTask(task -> { + if (cancelled.get()) { + return Task.cancelled(); } + return task; })).cast(); } @@ -911,21 +894,15 @@ public Task then(Task task) { public Task getFirstAsync(ParseQuery.State state, ParseUser user, Task cancellationToken) { final AtomicBoolean cancelled = new AtomicBoolean(false); - cancellationToken.continueWith(new Continuation() { - @Override - public Void then(Task task) { - cancelled.set(true); - return null; - } + cancellationToken.continueWith((Continuation) task -> { + cancelled.set(true); + return null; }); - return await(Task.forResult(null).continueWithTask(new Continuation>() { - @Override - public Task then(Task task) { - if (cancelled.get()) { - return Task.cancelled(); - } - return task; + return await(Task.forResult(null).continueWithTask(task -> { + if (cancelled.get()) { + return Task.cancelled(); } + return task; })).cast(); } } diff --git a/parse/src/test/java/com/parse/ParseRESTCommandTest.java b/parse/src/test/java/com/parse/ParseRESTCommandTest.java index 7f8747cfb..d4cb2117a 100644 --- a/parse/src/test/java/com/parse/ParseRESTCommandTest.java +++ b/parse/src/test/java/com/parse/ParseRESTCommandTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -20,7 +21,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import java.io.ByteArrayInputStream; @@ -28,8 +28,6 @@ import java.io.InputStream; import java.net.URL; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -46,12 +44,11 @@ // For org.json @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public class ParseRESTCommandTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private static ParseHttpResponse newMockParseHttpResponse(int statusCode, JSONObject body) { return newMockParseHttpResponse(statusCode, body.toString()); diff --git a/parse/src/test/java/com/parse/ParseRelationTest.java b/parse/src/test/java/com/parse/ParseRelationTest.java index edc89c6f4..3419c9beb 100644 --- a/parse/src/test/java/com/parse/ParseRelationTest.java +++ b/parse/src/test/java/com/parse/ParseRelationTest.java @@ -17,7 +17,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import static org.junit.Assert.assertEquals; @@ -30,11 +29,10 @@ import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseRelationTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); //region testConstructor diff --git a/parse/src/test/java/com/parse/ParseRequestTest.java b/parse/src/test/java/com/parse/ParseRequestTest.java index 39f723062..515839bf8 100644 --- a/parse/src/test/java/com/parse/ParseRequestTest.java +++ b/parse/src/test/java/com/parse/ParseRequestTest.java @@ -8,6 +8,7 @@ */ package com.parse; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpBody; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -26,8 +27,6 @@ import java.util.LinkedList; import java.util.List; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -42,7 +41,7 @@ public class ParseRequestTest { private static byte[] data; @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @BeforeClass public static void setUpClass() { @@ -121,7 +120,7 @@ public void testDownloadProgress() throws Exception { } private static class TestProgressCallback implements ProgressCallback { - List history = new LinkedList<>(); + final List history = new LinkedList<>(); @Override public void done(Integer percentDone) { diff --git a/parse/src/test/java/com/parse/ParseTaskUtilsTest.java b/parse/src/test/java/com/parse/ParseTaskUtilsTest.java index 173415618..6d531f51a 100644 --- a/parse/src/test/java/com/parse/ParseTaskUtilsTest.java +++ b/parse/src/test/java/com/parse/ParseTaskUtilsTest.java @@ -8,13 +8,12 @@ */ package com.parse; +import com.parse.boltsinternal.AggregateException; +import com.parse.boltsinternal.Task; + import org.junit.Test; import java.util.ArrayList; -import java.util.concurrent.Callable; - -import com.parse.boltsinternal.AggregateException; -import com.parse.boltsinternal.Task; import static org.junit.Assert.assertTrue; @@ -30,15 +29,12 @@ public void testWaitForTaskWrapsAggregateExceptionAsParseException() { final ArrayList> tasks = new ArrayList<>(); for (int i = 0; i < 20; i++) { final int number = i; - Task task = Task.callInBackground(new Callable() { - @Override - public Void call() throws Exception { - Thread.sleep((long) (Math.random() * 100)); - if (number == 10 || number == 11) { - throw error; - } - return null; + Task task = Task.callInBackground(() -> { + Thread.sleep((long) (Math.random() * 100)); + if (number == 10 || number == 11) { + throw error; } + return null; }); tasks.add(task); } diff --git a/parse/src/test/java/com/parse/ParseTestUtils.java b/parse/src/test/java/com/parse/ParseTestUtils.java index 501e65c21..0c0d3d92a 100644 --- a/parse/src/test/java/com/parse/ParseTestUtils.java +++ b/parse/src/test/java/com/parse/ParseTestUtils.java @@ -10,6 +10,7 @@ import android.content.Context; +import com.parse.boltsinternal.Task; import com.parse.http.ParseHttpRequest; import com.parse.http.ParseHttpResponse; @@ -19,8 +20,6 @@ import java.io.File; import java.io.IOException; -import com.parse.boltsinternal.Task; - import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/parse/src/test/java/com/parse/ParseUserTest.java b/parse/src/test/java/com/parse/ParseUserTest.java index 3c65c2b8b..d23ab357c 100644 --- a/parse/src/test/java/com/parse/ParseUserTest.java +++ b/parse/src/test/java/com/parse/ParseUserTest.java @@ -11,6 +11,10 @@ import android.Manifest; import android.os.Parcel; +import com.parse.boltsinternal.Capture; +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -23,7 +27,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; -import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; import java.util.Collections; import java.util.Date; @@ -33,16 +37,13 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import com.parse.boltsinternal.Capture; -import com.parse.boltsinternal.Continuation; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyMapOf; @@ -56,14 +57,16 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.robolectric.annotation.LooperMode.Mode.PAUSED; +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; // For ParseExecutors.main() @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) +@LooperMode(PAUSED) public class ParseUserTest extends ResetPluginsParseTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); private static void setLazy(ParseUser user) { Map anonymousAuthData = new HashMap<>(); @@ -281,7 +284,7 @@ public void testSignUpAsyncWithMergeInDiskAnonymousUser() throws Exception { when(currentUser.isLazy()).thenReturn(false); when(currentUser.isLinked(ParseAnonymousUtils.AUTH_TYPE)).thenReturn(true); when(currentUser.getSessionToken()).thenReturn("oldSessionToken"); - when(currentUser.getAuthData()).thenReturn(new HashMap>()); + when(currentUser.getAuthData()).thenReturn(new HashMap<>()); when(currentUser.saveAsync(anyString(), eq(false), Matchers.>any())) .thenReturn(Task.forResult(null)); ParseUser.State state = new ParseUser.State.Builder() @@ -380,7 +383,7 @@ public void testSignUpAsyncWithNoCurrentUserAndSignUpSuccess() throws Exception .sessionToken("newSessionToken") .build(); when(userController.signUpAsync( - any(ParseUser.State.class), any(ParseOperationSet.class), anyString())) + any(ParseUser.State.class), any(ParseOperationSet.class), nullable(String.class))) .thenReturn(Task.forResult(newUserState)); ParseCorePlugins.getInstance().registerUserController(userController); @@ -392,7 +395,7 @@ public void testSignUpAsyncWithNoCurrentUserAndSignUpSuccess() throws Exception // Make sure we sign up the user verify(userController, times(1)).signUpAsync( - any(ParseUser.State.class), any(ParseOperationSet.class), anyString()); + any(ParseUser.State.class), any(ParseOperationSet.class), nullable(String.class)); // Make sure user's data is correct assertEquals("newSessionToken", user.getSessionToken()); assertEquals("newValue", user.getString("newKey")); @@ -416,7 +419,7 @@ public void testSignUpAsyncWithNoCurrentUserAndSignUpFailure() { ParseUserController userController = mock(ParseUserController.class); ParseException signUpException = new ParseException(ParseException.OTHER_CAUSE, "test"); when(userController.signUpAsync( - any(ParseUser.State.class), any(ParseOperationSet.class), anyString())) + any(ParseUser.State.class), any(ParseOperationSet.class), nullable(String.class))) .thenReturn(Task.forError(signUpException)); ParseCorePlugins.getInstance().registerUserController(userController); @@ -429,7 +432,7 @@ public void testSignUpAsyncWithNoCurrentUserAndSignUpFailure() { // Make sure we sign up the user verify(userController, times(1)).signUpAsync( - any(ParseUser.State.class), any(ParseOperationSet.class), anyString()); + any(ParseUser.State.class), any(ParseOperationSet.class), nullable(String.class)); // Make sure user's data is correct assertEquals("value", user.getString("key")); // Make sure we never set the current user @@ -476,7 +479,7 @@ public void testLoginWithAsyncWithoutExistingLazyUser() throws ParseException { public void testLoginWithAsyncWithLinkedLazyUser() throws Exception { // Register a mock currentUserController to make getCurrentUser work ParseUser currentUser = new ParseUser(); - currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()); setLazy(currentUser); ParseUser partialMockCurrentUser = spy(currentUser); when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken"); @@ -540,7 +543,7 @@ public void testLoginWithAsyncWithLinkedNotLazyUser() throws Exception { // Register a mock currentUserController to make getCurrentUser work ParseUser.State state = new ParseUser.State.Builder() .objectId("objectId") // Make it not lazy - .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()) + .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()) .build(); ParseUser currentUser = ParseUser.from(state); ParseUser partialMockCurrentUser = spy(currentUser); // ParseUser.mutex @@ -578,7 +581,7 @@ public void testLoginWithAsyncWithLinkedNotLazyUserLinkFailure() throws Exceptio ParseCorePlugins.getInstance().registerUserController(userController); // Register a mock currentUserController to make getCurrentUser work ParseUser currentUser = new ParseUser(); - currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()); currentUser.setObjectId("objectId"); // Make it not lazy. ParseUser partialMockCurrentUser = spy(currentUser); when(partialMockCurrentUser.getSessionToken()).thenReturn("sessionToken"); @@ -668,7 +671,7 @@ public void testlinkWithInBackgroundWithSaveAsyncSuccess() throws Exception { user.setIsCurrentUser(true); // To verify stripAnonymity user.setObjectId("objectId"); - user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap()); + user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>()); ParseUser partialMockUser = spy(user); doReturn(Task.forResult(null)) .when(partialMockUser) @@ -741,7 +744,7 @@ public void testlinkWithInBackgroundWithSaveAsyncFailure() throws Exception { public void testResolveLazinessAsyncWithAuthDataAndNotNewUser() throws Exception { ParseUser user = new ParseUser(); setLazy(user); - user.putAuthData("facebook", new HashMap()); + user.putAuthData("facebook", new HashMap<>()); // Register a mock userController to make logIn work ParseUserController userController = mock(ParseUserController.class); ParseUser.State newUserState = new ParseUser.State.Builder() @@ -781,7 +784,7 @@ public void testResolveLazinessAsyncWithAuthDataAndNotNewUser() throws Exception public void testResolveLazinessAsyncWithAuthDataAndNewUser() throws Exception { ParseUser user = new ParseUser(); setLazy(user); - user.putAuthData("facebook", new HashMap()); + user.putAuthData("facebook", new HashMap<>()); // Register a mock userController to make logIn work ParseUserController userController = mock(ParseUserController.class); ParseUser.State newUserState = new ParseUser.State.Builder() @@ -819,7 +822,7 @@ public void testResolveLazinessAsyncWithAuthDataAndNewUser() throws Exception { public void testResolveLazinessAsyncWithAuthDataAndNotNewUserAndLDSEnabled() throws Exception { ParseUser user = new ParseUser(); setLazy(user); - user.putAuthData("facebook", new HashMap()); + user.putAuthData("facebook", new HashMap<>()); // To verify handleSaveResultAsync is not called user.setPassword("password"); // Register a mock userController to make logIn work @@ -1219,17 +1222,16 @@ public void testLogInWithCallback() throws Exception { ParseCorePlugins.getInstance().registerUserController(userController); final Semaphore done = new Semaphore(0); - ParseUser.logInInBackground("userName", "password", new LogInCallback() { - @Override - public void done(ParseUser user, ParseException e) { - done.release(); - assertNull(e); - // Make sure user's data is correct - assertEquals("newSessionToken", user.getSessionToken()); - assertEquals("newValue", user.get("newKey")); - } + ParseUser.logInInBackground("userName", "password", (user, e) -> { + done.release(); + assertNull(e); + // Make sure user's data is correct + assertEquals("newSessionToken", user.getSessionToken()); + assertEquals("newValue", user.get("newKey")); }); + shadowMainLooper().idle(); + assertTrue(done.tryAcquire(5, TimeUnit.SECONDS)); // Make sure user is login verify(userController, times(1)).logInAsync("userName", "password"); @@ -1295,15 +1297,12 @@ public void testBecomeWithCallback() { ParseCorePlugins.getInstance().registerUserController(userController); final Semaphore done = new Semaphore(0); - ParseUser.becomeInBackground("sessionToken", new LogInCallback() { - @Override - public void done(ParseUser user, ParseException e) { - done.release(); - assertNull(e); - // Make sure user's data is correct - assertEquals("sessionToken", user.getSessionToken()); - assertEquals("value", user.get("key")); - } + ParseUser.becomeInBackground("sessionToken", (user, e) -> { + done.release(); + assertNull(e); + // Make sure user's data is correct + assertEquals("sessionToken", user.getSessionToken()); + assertEquals("value", user.get("key")); }); // Make sure we call getUserAsync @@ -1567,7 +1566,7 @@ public void testSaveEventuallyWhenServerError() throws Exception { .grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE); Parse.Configuration configuration = new Parse.Configuration.Builder(RuntimeEnvironment.application) - .applicationId(BuildConfig.APPLICATION_ID) + .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME) .server("https://api.parse.com/1") .enableLocalDataStore() .build(); @@ -1598,13 +1597,10 @@ public void testSaveEventuallyWhenServerError() throws Exception { final CountDownLatch saveCountDown1 = new CountDownLatch(1); final Capture exceptionCapture = new Capture<>(); - user.saveInBackground().continueWith(new Continuation() { - @Override - public Void then(Task task) { - exceptionCapture.set(task.getError()); - saveCountDown1.countDown(); - return null; - } + user.saveInBackground().continueWith((Continuation) task -> { + exceptionCapture.set(task.getError()); + saveCountDown1.countDown(); + return null; }); assertTrue(saveCountDown1.await(5, TimeUnit.SECONDS)); assertNull(exceptionCapture.get()); @@ -1620,13 +1616,10 @@ public Void then(Task task) { restClient, mockResponse, 400, "Bad Request"); final CountDownLatch saveEventuallyCountDown = new CountDownLatch(1); - user.saveEventually().continueWith(new Continuation() { - @Override - public Void then(Task task) { - exceptionCapture.set(task.getError()); - saveEventuallyCountDown.countDown(); - return null; - } + user.saveEventually().continueWith((Continuation) task -> { + exceptionCapture.set(task.getError()); + saveEventuallyCountDown.countDown(); + return null; }); assertTrue(saveEventuallyCountDown.await(5, TimeUnit.SECONDS)); assertTrue(exceptionCapture.get() instanceof ParseException); @@ -1650,13 +1643,10 @@ public Void then(Task task) { restClient, mockResponse, 200, "OK"); final CountDownLatch saveCountDown2 = new CountDownLatch(1); - user.saveInBackground().continueWith(new Continuation() { - @Override - public Void then(Task task) { - exceptionCapture.set(task.getError()); - saveCountDown2.countDown(); - return null; - } + user.saveInBackground().continueWith((Continuation) task -> { + exceptionCapture.set(task.getError()); + saveCountDown2.countDown(); + return null; }); assertTrue(saveCountDown2.await(5, TimeUnit.SECONDS)); diff --git a/parse/src/test/java/com/parse/PointerEncoderTest.java b/parse/src/test/java/com/parse/PointerEncoderTest.java index 21005dca3..d1554826b 100644 --- a/parse/src/test/java/com/parse/PointerEncoderTest.java +++ b/parse/src/test/java/com/parse/PointerEncoderTest.java @@ -18,7 +18,7 @@ public class PointerEncoderTest { @Rule - public ExpectedException thrown = ExpectedException.none(); + public final ExpectedException thrown = ExpectedException.none(); @Test public void testEncodeRelatedObjectWithoutObjectId() { diff --git a/parse/src/test/java/com/parse/TaskQueueTestHelper.java b/parse/src/test/java/com/parse/TaskQueueTestHelper.java index 5fa8e0296..72b3f8c10 100644 --- a/parse/src/test/java/com/parse/TaskQueueTestHelper.java +++ b/parse/src/test/java/com/parse/TaskQueueTestHelper.java @@ -8,13 +8,12 @@ */ package com.parse; -import java.util.ArrayList; -import java.util.List; - -import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; import com.parse.boltsinternal.TaskCompletionSource; +import java.util.ArrayList; +import java.util.List; + /** * Helper class to step through a {@link TaskQueue}. *

@@ -37,12 +36,7 @@ public TaskQueueTestHelper(TaskQueue taskQueue) { public void enqueue() { synchronized (lock) { final TaskCompletionSource tcs = new TaskCompletionSource(); - taskQueue.enqueue(new Continuation>() { - @Override - public Task then(Task task) { - return tcs.getTask(); - } - }); + taskQueue.enqueue(task -> tcs.getTask()); pendingTasks.add(tcs); } } diff --git a/rxjava/build.gradle b/rxjava/build.gradle index e1c160fc3..65bb19a1b 100644 --- a/rxjava/build.gradle +++ b/rxjava/build.gradle @@ -29,9 +29,9 @@ android { } dependencies { - api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" api "io.reactivex.rxjava3:rxjava:3.0.4" implementation project(":parse") } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/twitter/build.gradle b/twitter/build.gradle index 76e3a3404..6b2c6e9fb 100644 --- a/twitter/build.gradle +++ b/twitter/build.gradle @@ -20,13 +20,13 @@ android { } dependencies { - api "androidx.appcompat:appcompat:1.1.0" + api "androidx.appcompat:appcompat:1.3.0" api "oauth.signpost:signpost-core:1.2.1.2" api "se.akerfeldt:okhttp-signpost:1.1.0" implementation project(":parse") - testImplementation "junit:junit:4.13" - testImplementation "org.mockito:mockito-core:1.10.19" + testImplementation "junit:junit:4.13.2" + testImplementation "org.mockito:mockito-core:3.6.28" } -apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/1.1.0/gradle-android-javadocs.gradle" +apply from: "https://raw.githubusercontent.com/Commit451/gradle-android-javadocs/2.0.0/gradle-android-javadocs.gradle" diff --git a/twitter/src/main/AndroidManifest.xml b/twitter/src/main/AndroidManifest.xml index c5ae2f108..8d42f0441 100644 --- a/twitter/src/main/AndroidManifest.xml +++ b/twitter/src/main/AndroidManifest.xml @@ -6,4 +6,4 @@ ~ 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. --> - + diff --git a/twitter/src/main/java/com/parse/twitter/OAuth1FlowDialog.java b/twitter/src/main/java/com/parse/twitter/OAuth1FlowDialog.java index 59befab9b..92f8b2d6a 100644 --- a/twitter/src/main/java/com/parse/twitter/OAuth1FlowDialog.java +++ b/twitter/src/main/java/com/parse/twitter/OAuth1FlowDialog.java @@ -10,18 +10,18 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; -import androidx.appcompat.app.AppCompatDialog; import android.view.View; import android.view.Window; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; +import androidx.appcompat.app.AppCompatDialog; + /** * For internal use. */ @@ -41,12 +41,7 @@ class OAuth1FlowDialog extends AppCompatDialog { this.callbackUrl = callbackUrl; this.serviceUrlIdentifier = serviceUrlIdentifier; this.handler = resultHandler; - this.setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - handler.onCancel(); - } - }); + this.setOnCancelListener(dialog -> handler.onCancel()); } @SuppressLint("SetJavaScriptEnabled") diff --git a/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java b/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java index fcc9f051d..84ccc44f9 100644 --- a/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java +++ b/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java @@ -15,14 +15,13 @@ import com.parse.ParseException; import com.parse.ParseUser; import com.parse.SaveCallback; - -import java.util.Map; -import java.util.concurrent.CancellationException; - import com.parse.boltsinternal.AggregateException; import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; +import java.util.Map; +import java.util.concurrent.CancellationException; + /** * Provides a set of utilities for using Parse with Twitter. */ @@ -36,6 +35,10 @@ public final class ParseTwitterUtils { static TwitterController controller; static ParseUserDelegate userDelegate = new ParseUserDelegateImpl(); + private ParseTwitterUtils() { + // do nothing + } + private static TwitterController getTwitterController() { synchronized (lock) { if (controller == null) { @@ -86,15 +89,12 @@ public static void initialize(String consumerKey, String consumerSecret, String controller.initialize(consumerKey, consumerSecret); } - userDelegate.registerAuthenticationCallback(AUTH_TYPE, new AuthenticationCallback() { - @Override - public boolean onRestore(Map authData) { - try { - getTwitterController().setAuthData(authData); - return true; - } catch (Exception e) { - return false; - } + userDelegate.registerAuthenticationCallback(AUTH_TYPE, authData -> { + try { + getTwitterController().setAuthData(authData); + return true; + } catch (Exception e) { + return false; } }); @@ -127,12 +127,7 @@ public static boolean isLinked(ParseUser user) { */ public static Task linkInBackground(Context context, final ParseUser user) { checkInitialization(); - return getTwitterController().authenticateAsync(context).onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - return user.linkWithInBackground(AUTH_TYPE, task.getResult()); - } - }); + return getTwitterController().authenticateAsync(context).onSuccessTask(task -> user.linkWithInBackground(AUTH_TYPE, task.getResult())); } /** @@ -268,12 +263,7 @@ public static void logIn(String twitterId, String screenName, String authToken, */ public static Task logInInBackground(Context context) { checkInitialization(); - return getTwitterController().authenticateAsync(context).onSuccessTask(new Continuation, Task>() { - @Override - public Task then(Task> task) { - return userDelegate.logInWithInBackground(AUTH_TYPE, task.getResult()); - } - }); + return getTwitterController().authenticateAsync(context).onSuccessTask(task -> userDelegate.logInWithInBackground(AUTH_TYPE, task.getResult())); } /** @@ -311,6 +301,8 @@ public static Task unlinkInBackground(ParseUser user) { return user.unlinkFromInBackground(AUTH_TYPE); } + //region TaskUtils + /** * Unlinks a user from a Twitter account in the background. Unlinking a user will save the user's * data. @@ -323,8 +315,6 @@ public static void unlinkInBackground(ParseUser user, SaveCallback callback) { callbackOnMainThreadAsync(unlinkInBackground(user), callback, false); } - //region TaskUtils - /** * Converts a task execution into a synchronous action. */ @@ -375,6 +365,8 @@ private static Task callbackOnMainThreadAsync( return callbackOnMainThreadInternalAsync(task, callback, reportCancellation); } + //endregion + /** * Calls the callback after a task completes on the main thread, returning a Task that completes * with the same result as the input task after the callback has been run. If reportCancellation @@ -386,50 +378,38 @@ private static Task callbackOnMainThreadInternalAsync( return task; } final Task.TaskCompletionSource tcs = Task.create(); - task.continueWith(new Continuation() { - @Override - public Void then(final Task task) throws Exception { - if (task.isCancelled() && !reportCancellation) { - tcs.setCancelled(); - return null; - } - Task.UI_THREAD_EXECUTOR.execute(new Runnable() { - @Override - public void run() { - try { - Exception error = task.getError(); - if (error != null && !(error instanceof ParseException)) { - error = new ParseException(error); - } - if (callback instanceof SaveCallback) { - ((SaveCallback) callback).done((ParseException) error); - } else if (callback instanceof LogInCallback) { - ((LogInCallback) callback).done( - (ParseUser) task.getResult(), (ParseException) error); - } - } finally { - if (task.isCancelled()) { - tcs.setCancelled(); - } else if (task.isFaulted()) { - tcs.setError(task.getError()); - } else { - tcs.setResult(task.getResult()); - } - } - } - }); + task.continueWith((Continuation) task1 -> { + if (task1.isCancelled() && !reportCancellation) { + tcs.setCancelled(); return null; } + Task.UI_THREAD_EXECUTOR.execute(() -> { + try { + Exception error = task1.getError(); + if (error != null && !(error instanceof ParseException)) { + error = new ParseException(error); + } + if (callback instanceof SaveCallback) { + ((SaveCallback) callback).done((ParseException) error); + } else if (callback instanceof LogInCallback) { + ((LogInCallback) callback).done( + (ParseUser) task1.getResult(), (ParseException) error); + } + } finally { + if (task1.isCancelled()) { + tcs.setCancelled(); + } else if (task1.isFaulted()) { + tcs.setError(task1.getError()); + } else { + tcs.setResult(task1.getResult()); + } + } + }); + return null; }); return tcs.getTask(); } - //endregion - - private ParseTwitterUtils() { - // do nothing - } - interface ParseUserDelegate { void registerAuthenticationCallback(String authType, AuthenticationCallback callback); diff --git a/twitter/src/main/java/com/parse/twitter/Twitter.java b/twitter/src/main/java/com/parse/twitter/Twitter.java index 2af3f4fe8..b90e430d9 100644 --- a/twitter/src/main/java/com/parse/twitter/Twitter.java +++ b/twitter/src/main/java/com/parse/twitter/Twitter.java @@ -31,12 +31,10 @@ public class Twitter { private static final String VERIFIER_PARAM = "oauth_verifier"; private static final String USER_ID_PARAM = "user_id"; private static final String SCREEN_NAME_PARAM = "screen_name"; - + private final String callbackUrl; // App configuration for enabling authentication. private String consumerKey; private String consumerSecret; - private String callbackUrl; - // User information. private String authToken; private String authTokenSecret; diff --git a/twitter/src/main/java/com/parse/twitter/TwitterController.java b/twitter/src/main/java/com/parse/twitter/TwitterController.java index 0e0665d94..ba9c7e31c 100644 --- a/twitter/src/main/java/com/parse/twitter/TwitterController.java +++ b/twitter/src/main/java/com/parse/twitter/TwitterController.java @@ -11,12 +11,11 @@ import android.content.Context; import com.parse.ParseException; +import com.parse.boltsinternal.Task; import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - class TwitterController { private static final String CONSUMER_KEY_KEY = "consumer_key"; diff --git a/twitter/src/test/java/com/parse/twitter/ParseTwitterUtilsTest.java b/twitter/src/test/java/com/parse/twitter/ParseTwitterUtilsTest.java index 2ee17ff98..424a90660 100644 --- a/twitter/src/test/java/com/parse/twitter/ParseTwitterUtilsTest.java +++ b/twitter/src/test/java/com/parse/twitter/ParseTwitterUtilsTest.java @@ -12,6 +12,7 @@ import com.parse.AuthenticationCallback; import com.parse.ParseUser; +import com.parse.boltsinternal.Task; import org.junit.After; import org.junit.Before; @@ -24,8 +25,6 @@ import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; diff --git a/twitter/src/test/java/com/parse/twitter/TwitterControllerTest.java b/twitter/src/test/java/com/parse/twitter/TwitterControllerTest.java index 27879654d..ff6b2b0f5 100644 --- a/twitter/src/test/java/com/parse/twitter/TwitterControllerTest.java +++ b/twitter/src/test/java/com/parse/twitter/TwitterControllerTest.java @@ -10,6 +10,8 @@ import android.content.Context; +import com.parse.boltsinternal.Task; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,8 +22,6 @@ import java.util.HashMap; import java.util.Map; -import com.parse.boltsinternal.Task; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue;