From 19a252a341bfd4abc14334ff2ebb5bae434fec8d Mon Sep 17 00:00:00 2001 From: Vasiliy Zukanov Date: Sat, 20 Aug 2022 14:45:17 +0300 Subject: [PATCH 1/9] Bugfix: fixes #1155 - supporting concurrent download of the same ParseFile from multiple threads --- .../java/com/parse/ParseFileController.java | 135 ++++++++++-------- .../com/parse/ParseFileControllerTest.java | 63 ++++++++ 2 files changed, 140 insertions(+), 58 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index df8ea2e69..849422d87 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -12,6 +12,8 @@ import com.parse.http.ParseHttpRequest; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CancellationException; import org.json.JSONObject; @@ -21,6 +23,8 @@ class ParseFileController { private final Object lock = new Object(); private final ParseHttpClient restClient; private final File cachePath; + private final List currentlyDownloadedFilesNames = new ArrayList<>(); + private ParseHttpClient fileClient; @@ -168,64 +172,79 @@ public Task fetchAsync( if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } - final File cacheFile = getCacheFile(state); - return Task.call(cacheFile::exists, ParseExecutors.io()) - .continueWithTask( - task -> { - boolean result = task.getResult(); - if (result) { - return Task.forResult(cacheFile); - } - if (cancellationToken != null && cancellationToken.isCancelled()) { - return Task.cancelled(); - } + return Task.call(() -> { + final File cacheFile = getCacheFile(state); + + synchronized (lock) { + if (currentlyDownloadedFilesNames.contains(state.name())) { + while (currentlyDownloadedFilesNames.contains(state.name())) { + lock.wait(); + } + } + + if (cacheFile.exists()) { + return cacheFile; + } else { + currentlyDownloadedFilesNames.add(state.name()); + } + } + + try { + if (cancellationToken != null && cancellationToken.isCancelled()) { + throw new CancellationException(); + } + + // 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 + Task downloadTask = request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken + ); + ParseTaskUtils.wait(downloadTask); + + // 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 (downloadTask.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + throw new RuntimeException(downloadTask.getError()); + } + + // 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 cacheFile; + } finally { + synchronized (lock) { + currentlyDownloadedFilesNames.remove(state.name()); + lock.notifyAll(); + } + } - // 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()) { - throw new CancellationException(); - } - if (task1.isFaulted()) { - ParseFileUtils.deleteQuietly(tempFile); - return task1.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()); - }); + }, ParseExecutors.io()); } } diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index 39a0d2222..f15191b7d 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -28,6 +28,9 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -341,5 +344,65 @@ public void testFetchAsyncFailure() throws Exception { assertFalse(controller.getTempFile(state).exists()); } + + @Test + public void testFetchAsyncConcurrentCallsSuccess() throws Exception { + byte[] data = "hello".getBytes(); + ParseHttpResponse mockResponse = + new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); + + ParseHttpClient fileClient = mock(ParseHttpClient.class); + when(fileClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); + // Make sure cache dir does not exist + File root = new File(temporaryFolder.getRoot(), "cache"); + assertFalse(root.exists()); + ParseFileController controller = new ParseFileController(null, root).fileClient(fileClient); + + ParseFile.State state = new ParseFile.State.Builder().name("file_name").url("url").build(); + + CountDownLatch countDownLatch = new CountDownLatch(2); + AtomicReference file1Ref = new AtomicReference<>(); + AtomicReference file2Ref = new AtomicReference<>(); + + new Thread(() -> { + try { + file1Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }).start(); + + new Thread(() -> { + try { + file2Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }).start(); + + countDownLatch.await(); + + File result1 = file1Ref.get(); + File result2 = file2Ref.get(); + + + assertTrue(result1.exists()); + assertEquals("hello", ParseFileUtils.readFileToString(result1, "UTF-8")); + + assertTrue(result2.exists()); + assertEquals("hello", ParseFileUtils.readFileToString(result2, "UTF-8")); + + verify(fileClient, times(1)).execute(any(ParseHttpRequest.class)); + assertFalse(controller.getTempFile(state).exists()); + } + // endregion } From d2135c2e316fc75df332e3140be80ba0c21701d5 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:44:38 +0200 Subject: [PATCH 2/9] Update parse/src/test/java/com/parse/ParseFileControllerTest.java --- parse/src/test/java/com/parse/ParseFileControllerTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index f15191b7d..a2488c89d 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -392,14 +392,11 @@ public void testFetchAsyncConcurrentCallsSuccess() throws Exception { File result1 = file1Ref.get(); File result2 = file2Ref.get(); - - + assertTrue(result1.exists()); assertEquals("hello", ParseFileUtils.readFileToString(result1, "UTF-8")); - assertTrue(result2.exists()); assertEquals("hello", ParseFileUtils.readFileToString(result2, "UTF-8")); - verify(fileClient, times(1)).execute(any(ParseHttpRequest.class)); assertFalse(controller.getTempFile(state).exists()); } From d5c50f4ca6eff76248e451cac57c36463058c526 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 22 Aug 2022 19:44:43 +0200 Subject: [PATCH 3/9] Update parse/src/test/java/com/parse/ParseFileControllerTest.java --- parse/src/test/java/com/parse/ParseFileControllerTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index a2488c89d..a9eb55d3c 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -361,9 +361,7 @@ public void testFetchAsyncConcurrentCallsSuccess() throws Exception { File root = new File(temporaryFolder.getRoot(), "cache"); assertFalse(root.exists()); ParseFileController controller = new ParseFileController(null, root).fileClient(fileClient); - ParseFile.State state = new ParseFile.State.Builder().name("file_name").url("url").build(); - CountDownLatch countDownLatch = new CountDownLatch(2); AtomicReference file1Ref = new AtomicReference<>(); AtomicReference file2Ref = new AtomicReference<>(); From 8d8a00d3d73c17f5c69f04ef60dd9339febc803a Mon Sep 17 00:00:00 2001 From: Romman Sabbir Date: Tue, 23 Aug 2022 19:15:24 +0600 Subject: [PATCH 4/9] ci failing : fixed spotless violations --- .../java/com/parse/ParseFileController.java | 144 +++++++++--------- .../com/parse/ParseFileControllerTest.java | 60 ++++---- 2 files changed, 105 insertions(+), 99 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index 849422d87..084ac7e40 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -25,7 +25,6 @@ class ParseFileController { private final File cachePath; private final List currentlyDownloadedFilesNames = new ArrayList<>(); - private ParseHttpClient fileClient; public ParseFileController(ParseHttpClient restClient, File cachePath) { @@ -172,79 +171,80 @@ public Task fetchAsync( if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } - return Task.call(() -> { - final File cacheFile = getCacheFile(state); + return Task.call( + () -> { + final File cacheFile = getCacheFile(state); + + synchronized (lock) { + if (currentlyDownloadedFilesNames.contains(state.name())) { + while (currentlyDownloadedFilesNames.contains(state.name())) { + lock.wait(); + } + } - synchronized (lock) { - if (currentlyDownloadedFilesNames.contains(state.name())) { - while (currentlyDownloadedFilesNames.contains(state.name())) { - lock.wait(); + if (cacheFile.exists()) { + return cacheFile; + } else { + currentlyDownloadedFilesNames.add(state.name()); + } } - } - - if (cacheFile.exists()) { - return cacheFile; - } else { - currentlyDownloadedFilesNames.add(state.name()); - } - } - try { - if (cancellationToken != null && cancellationToken.isCancelled()) { - throw new CancellationException(); - } - - // 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 - Task downloadTask = request.executeAsync( - fileClient(), - null, - downloadProgressCallback, - cancellationToken - ); - ParseTaskUtils.wait(downloadTask); - - // 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 (downloadTask.isFaulted()) { - ParseFileUtils.deleteQuietly(tempFile); - throw new RuntimeException(downloadTask.getError()); - } - - // 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 cacheFile; - } finally { - synchronized (lock) { - currentlyDownloadedFilesNames.remove(state.name()); - lock.notifyAll(); - } - } - - }, ParseExecutors.io()); + try { + if (cancellationToken != null && cancellationToken.isCancelled()) { + throw new CancellationException(); + } + + // 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 + Task downloadTask = + request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken); + ParseTaskUtils.wait(downloadTask); + + // 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 (downloadTask.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + throw new RuntimeException(downloadTask.getError()); + } + + // 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 cacheFile; + } finally { + synchronized (lock) { + currentlyDownloadedFilesNames.remove(state.name()); + lock.notifyAll(); + } + } + }, + ParseExecutors.io()); } } diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index a9eb55d3c..1dd65e151 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -30,7 +30,6 @@ import java.net.URL; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; - import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -344,16 +343,15 @@ public void testFetchAsyncFailure() throws Exception { assertFalse(controller.getTempFile(state).exists()); } - @Test public void testFetchAsyncConcurrentCallsSuccess() throws Exception { byte[] data = "hello".getBytes(); ParseHttpResponse mockResponse = - new ParseHttpResponse.Builder() - .setStatusCode(200) - .setTotalSize((long) data.length) - .setContent(new ByteArrayInputStream(data)) - .build(); + new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); ParseHttpClient fileClient = mock(ParseHttpClient.class); when(fileClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); @@ -366,31 +364,39 @@ public void testFetchAsyncConcurrentCallsSuccess() throws Exception { AtomicReference file1Ref = new AtomicReference<>(); AtomicReference file2Ref = new AtomicReference<>(); - new Thread(() -> { - try { - file1Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }).start(); - - new Thread(() -> { - try { - file2Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }).start(); + new Thread( + () -> { + try { + file1Ref.set( + ParseTaskUtils.wait( + controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }) + .start(); + + new Thread( + () -> { + try { + file2Ref.set( + ParseTaskUtils.wait( + controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }) + .start(); countDownLatch.await(); File result1 = file1Ref.get(); File result2 = file2Ref.get(); - + assertTrue(result1.exists()); assertEquals("hello", ParseFileUtils.readFileToString(result1, "UTF-8")); assertTrue(result2.exists()); From c19946b5068f507955a4d2e381a1bbe7de708e62 Mon Sep 17 00:00:00 2001 From: Romman Sabbir Date: Tue, 23 Aug 2022 19:34:51 +0600 Subject: [PATCH 5/9] Revert "ci failing : fixed spotless violations" This reverts commit 8d8a00d3 --- .../java/com/parse/ParseFileController.java | 144 +++++++++--------- .../com/parse/ParseFileControllerTest.java | 60 ++++---- 2 files changed, 99 insertions(+), 105 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index 084ac7e40..849422d87 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -25,6 +25,7 @@ class ParseFileController { private final File cachePath; private final List currentlyDownloadedFilesNames = new ArrayList<>(); + private ParseHttpClient fileClient; public ParseFileController(ParseHttpClient restClient, File cachePath) { @@ -171,80 +172,79 @@ public Task fetchAsync( if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } - return Task.call( - () -> { - final File cacheFile = getCacheFile(state); - - synchronized (lock) { - if (currentlyDownloadedFilesNames.contains(state.name())) { - while (currentlyDownloadedFilesNames.contains(state.name())) { - lock.wait(); - } - } + return Task.call(() -> { + final File cacheFile = getCacheFile(state); - if (cacheFile.exists()) { - return cacheFile; - } else { - currentlyDownloadedFilesNames.add(state.name()); - } + synchronized (lock) { + if (currentlyDownloadedFilesNames.contains(state.name())) { + while (currentlyDownloadedFilesNames.contains(state.name())) { + lock.wait(); } + } - try { - if (cancellationToken != null && cancellationToken.isCancelled()) { - throw new CancellationException(); - } - - // 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 - Task downloadTask = - request.executeAsync( - fileClient(), - null, - downloadProgressCallback, - cancellationToken); - ParseTaskUtils.wait(downloadTask); - - // 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 (downloadTask.isFaulted()) { - ParseFileUtils.deleteQuietly(tempFile); - throw new RuntimeException(downloadTask.getError()); - } - - // 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 cacheFile; - } finally { - synchronized (lock) { - currentlyDownloadedFilesNames.remove(state.name()); - lock.notifyAll(); - } - } - }, - ParseExecutors.io()); + if (cacheFile.exists()) { + return cacheFile; + } else { + currentlyDownloadedFilesNames.add(state.name()); + } + } + + try { + if (cancellationToken != null && cancellationToken.isCancelled()) { + throw new CancellationException(); + } + + // 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 + Task downloadTask = request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken + ); + ParseTaskUtils.wait(downloadTask); + + // 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 (downloadTask.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + throw new RuntimeException(downloadTask.getError()); + } + + // 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 cacheFile; + } finally { + synchronized (lock) { + currentlyDownloadedFilesNames.remove(state.name()); + lock.notifyAll(); + } + } + + }, ParseExecutors.io()); } } diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index 1dd65e151..a9eb55d3c 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -30,6 +30,7 @@ import java.net.URL; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -343,15 +344,16 @@ public void testFetchAsyncFailure() throws Exception { assertFalse(controller.getTempFile(state).exists()); } + @Test public void testFetchAsyncConcurrentCallsSuccess() throws Exception { byte[] data = "hello".getBytes(); ParseHttpResponse mockResponse = - new ParseHttpResponse.Builder() - .setStatusCode(200) - .setTotalSize((long) data.length) - .setContent(new ByteArrayInputStream(data)) - .build(); + new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); ParseHttpClient fileClient = mock(ParseHttpClient.class); when(fileClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); @@ -364,39 +366,31 @@ public void testFetchAsyncConcurrentCallsSuccess() throws Exception { AtomicReference file1Ref = new AtomicReference<>(); AtomicReference file2Ref = new AtomicReference<>(); - new Thread( - () -> { - try { - file1Ref.set( - ParseTaskUtils.wait( - controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }) - .start(); - - new Thread( - () -> { - try { - file2Ref.set( - ParseTaskUtils.wait( - controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }) - .start(); + new Thread(() -> { + try { + file1Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }).start(); + + new Thread(() -> { + try { + file2Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }).start(); countDownLatch.await(); File result1 = file1Ref.get(); File result2 = file2Ref.get(); - + assertTrue(result1.exists()); assertEquals("hello", ParseFileUtils.readFileToString(result1, "UTF-8")); assertTrue(result2.exists()); From ec87160a0836137b1392996141d46cc195395e3b Mon Sep 17 00:00:00 2001 From: Romman Sabbir Date: Tue, 23 Aug 2022 19:36:03 +0600 Subject: [PATCH 6/9] fix: ci failing on PR #1179 --- .../java/com/parse/ParseFileController.java | 144 +++++++++--------- .../com/parse/ParseFileControllerTest.java | 60 ++++---- 2 files changed, 105 insertions(+), 99 deletions(-) diff --git a/parse/src/main/java/com/parse/ParseFileController.java b/parse/src/main/java/com/parse/ParseFileController.java index 849422d87..084ac7e40 100644 --- a/parse/src/main/java/com/parse/ParseFileController.java +++ b/parse/src/main/java/com/parse/ParseFileController.java @@ -25,7 +25,6 @@ class ParseFileController { private final File cachePath; private final List currentlyDownloadedFilesNames = new ArrayList<>(); - private ParseHttpClient fileClient; public ParseFileController(ParseHttpClient restClient, File cachePath) { @@ -172,79 +171,80 @@ public Task fetchAsync( if (cancellationToken != null && cancellationToken.isCancelled()) { return Task.cancelled(); } - return Task.call(() -> { - final File cacheFile = getCacheFile(state); + return Task.call( + () -> { + final File cacheFile = getCacheFile(state); + + synchronized (lock) { + if (currentlyDownloadedFilesNames.contains(state.name())) { + while (currentlyDownloadedFilesNames.contains(state.name())) { + lock.wait(); + } + } - synchronized (lock) { - if (currentlyDownloadedFilesNames.contains(state.name())) { - while (currentlyDownloadedFilesNames.contains(state.name())) { - lock.wait(); + if (cacheFile.exists()) { + return cacheFile; + } else { + currentlyDownloadedFilesNames.add(state.name()); + } } - } - - if (cacheFile.exists()) { - return cacheFile; - } else { - currentlyDownloadedFilesNames.add(state.name()); - } - } - try { - if (cancellationToken != null && cancellationToken.isCancelled()) { - throw new CancellationException(); - } - - // 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 - Task downloadTask = request.executeAsync( - fileClient(), - null, - downloadProgressCallback, - cancellationToken - ); - ParseTaskUtils.wait(downloadTask); - - // 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 (downloadTask.isFaulted()) { - ParseFileUtils.deleteQuietly(tempFile); - throw new RuntimeException(downloadTask.getError()); - } - - // 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 cacheFile; - } finally { - synchronized (lock) { - currentlyDownloadedFilesNames.remove(state.name()); - lock.notifyAll(); - } - } - - }, ParseExecutors.io()); + try { + if (cancellationToken != null && cancellationToken.isCancelled()) { + throw new CancellationException(); + } + + // 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 + Task downloadTask = + request.executeAsync( + fileClient(), + null, + downloadProgressCallback, + cancellationToken); + ParseTaskUtils.wait(downloadTask); + + // 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 (downloadTask.isFaulted()) { + ParseFileUtils.deleteQuietly(tempFile); + throw new RuntimeException(downloadTask.getError()); + } + + // 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 cacheFile; + } finally { + synchronized (lock) { + currentlyDownloadedFilesNames.remove(state.name()); + lock.notifyAll(); + } + } + }, + ParseExecutors.io()); } } diff --git a/parse/src/test/java/com/parse/ParseFileControllerTest.java b/parse/src/test/java/com/parse/ParseFileControllerTest.java index a9eb55d3c..1dd65e151 100644 --- a/parse/src/test/java/com/parse/ParseFileControllerTest.java +++ b/parse/src/test/java/com/parse/ParseFileControllerTest.java @@ -30,7 +30,6 @@ import java.net.URL; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; - import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -344,16 +343,15 @@ public void testFetchAsyncFailure() throws Exception { assertFalse(controller.getTempFile(state).exists()); } - @Test public void testFetchAsyncConcurrentCallsSuccess() throws Exception { byte[] data = "hello".getBytes(); ParseHttpResponse mockResponse = - new ParseHttpResponse.Builder() - .setStatusCode(200) - .setTotalSize((long) data.length) - .setContent(new ByteArrayInputStream(data)) - .build(); + new ParseHttpResponse.Builder() + .setStatusCode(200) + .setTotalSize((long) data.length) + .setContent(new ByteArrayInputStream(data)) + .build(); ParseHttpClient fileClient = mock(ParseHttpClient.class); when(fileClient.execute(any(ParseHttpRequest.class))).thenReturn(mockResponse); @@ -366,31 +364,39 @@ public void testFetchAsyncConcurrentCallsSuccess() throws Exception { AtomicReference file1Ref = new AtomicReference<>(); AtomicReference file2Ref = new AtomicReference<>(); - new Thread(() -> { - try { - file1Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }).start(); - - new Thread(() -> { - try { - file2Ref.set(ParseTaskUtils.wait(controller.fetchAsync(state, null, null, null))); - } catch (ParseException e) { - throw new RuntimeException(e); - } finally { - countDownLatch.countDown(); - } - }).start(); + new Thread( + () -> { + try { + file1Ref.set( + ParseTaskUtils.wait( + controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }) + .start(); + + new Thread( + () -> { + try { + file2Ref.set( + ParseTaskUtils.wait( + controller.fetchAsync(state, null, null, null))); + } catch (ParseException e) { + throw new RuntimeException(e); + } finally { + countDownLatch.countDown(); + } + }) + .start(); countDownLatch.await(); File result1 = file1Ref.get(); File result2 = file2Ref.get(); - + assertTrue(result1.exists()); assertEquals("hello", ParseFileUtils.readFileToString(result1, "UTF-8")); assertTrue(result2.exists()); From 4d0494a355f12cc72526efa69d6d5f2b9df3bcfa Mon Sep 17 00:00:00 2001 From: rommansabbir Date: Tue, 6 Aug 2024 22:31:13 +0600 Subject: [PATCH 7/9] feat: Implementing encrypted local storage for user sessions with tests --- parse/build.gradle | 2 + .../com/parse/EncryptedFileObjectStore.java | 110 +++++++++++ .../main/java/com/parse/ParseCorePlugins.java | 5 +- .../main/java/com/parse/ParseFileUtils.java | 107 +++++++++++ .../com/parse/ParseObjectStoreMigrator.java | 69 +++++++ .../java/com/parse/AlgorithmParameterSpec.kt | 6 + .../java/com/parse/AndroidKeyStoreProvider.kt | 177 ++++++++++++++++++ .../java/com/parse/AndroidOpenSSLProvider.kt | 64 +++++++ .../parse/EncryptedFileObjectStoreTest.java | 119 ++++++++++++ .../com/parse/ParseObjectStoreMigratorTest.kt | 101 ++++++++++ .../java/com/parse/RobolectricKeyStore.kt | 17 ++ 11 files changed, 776 insertions(+), 1 deletion(-) create mode 100644 parse/src/main/java/com/parse/EncryptedFileObjectStore.java create mode 100644 parse/src/main/java/com/parse/ParseObjectStoreMigrator.java create mode 100644 parse/src/test/java/com/parse/AlgorithmParameterSpec.kt create mode 100644 parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt create mode 100644 parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt create mode 100644 parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java create mode 100644 parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt create mode 100644 parse/src/test/java/com/parse/RobolectricKeyStore.kt diff --git a/parse/build.gradle b/parse/build.gradle index 32b8129c3..1630380ed 100644 --- a/parse/build.gradle +++ b/parse/build.gradle @@ -1,4 +1,5 @@ apply plugin: "com.android.library" +apply plugin: "kotlin-android" apply plugin: "maven-publish" apply plugin: "io.freefair.android-javadoc-jar" apply plugin: "io.freefair.android-sources-jar" @@ -50,6 +51,7 @@ dependencies { api "androidx.core:core:1.8.0" api "com.squareup.okhttp3:okhttp:$okhttpVersion" api project(':bolts-tasks') + implementation "androidx.security:security-crypto:1.1.0-alpha03" testImplementation "org.junit.jupiter:junit-jupiter:$rootProject.ext.jupiterVersion" testImplementation "org.skyscreamer:jsonassert:1.5.0" diff --git a/parse/src/main/java/com/parse/EncryptedFileObjectStore.java b/parse/src/main/java/com/parse/EncryptedFileObjectStore.java new file mode 100644 index 000000000..b31ca647b --- /dev/null +++ b/parse/src/main/java/com/parse/EncryptedFileObjectStore.java @@ -0,0 +1,110 @@ +package com.parse; + +import android.content.Context; + +import androidx.security.crypto.EncryptedFile; +import androidx.security.crypto.MasterKey; +import com.parse.boltsinternal.Task; +import org.json.JSONException; +import org.json.JSONObject; +import java.io.File; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.concurrent.Callable; + +/** + * a file based {@link ParseObjectStore} using Jetpack's {@link EncryptedFile} class to protect files from a malicious copy. + */ +class EncryptedFileObjectStore implements ParseObjectStore { + + private final String className; + private final File file; + private final EncryptedFile encryptedFile; + private final ParseObjectCurrentCoder coder; + + public EncryptedFileObjectStore(Class clazz, File file, ParseObjectCurrentCoder coder) { + this(getSubclassingController().getClassName(clazz), file, coder); + } + + public EncryptedFileObjectStore(String className, File file, ParseObjectCurrentCoder coder) { + this.className = className; + this.file = file; + this.coder = coder; + Context context = ParsePlugins.get().applicationContext(); + try { + encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + private static ParseObjectSubclassingController getSubclassingController() { + return ParseCorePlugins.getInstance().getSubclassingController(); + } + + /** + * Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. + * + * @param current ParseObject which needs to be saved to disk. + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + */ + private void saveToDisk(ParseObject current) throws IOException, GeneralSecurityException { + JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get()); + ParseFileUtils.writeJSONObjectToFile(encryptedFile, json); + } + + /** + * Retrieves a {@code ParseObject} from a file on disk in /2/ format. + * + * @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}. + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws JSONException thrown if an error occurred during the decoding process of the ParseObject to a JSONObject + * @throws IOException thrown if an error occurred during writing of the file + */ + private T getFromDisk() throws GeneralSecurityException, JSONException, IOException { + return ParseObject.from(coder.decode(ParseObject.State.newBuilder(className), ParseFileUtils.readFileToJSONObject(encryptedFile), ParseDecoder.get()).isComplete(true).build()); + } + + @Override + public Task getAsync() { + return Task.call(new Callable() { + @Override + public T call() throws Exception { + if (!file.exists()) return null; + try { + return getFromDisk(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e.getMessage()); + } + } + }, ParseExecutors.io()); + } + + @Override + public Task setAsync(T object) { + return Task.call(() -> { + if (file.exists() && !ParseFileUtils.deleteQuietly(file)) throw new RuntimeException("Unable to delete"); + try { + saveToDisk(object); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e.getMessage()); + } + return null; + }, ParseExecutors.io()); + } + + @Override + public Task existsAsync() { + return Task.call(file::exists, ParseExecutors.io()); + } + + @Override + public Task deleteAsync() { + return Task.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/ParseCorePlugins.java b/parse/src/main/java/com/parse/ParseCorePlugins.java index 01d5ed54b..ed18f230d 100644 --- a/parse/src/main/java/com/parse/ParseCorePlugins.java +++ b/parse/src/main/java/com/parse/ParseCorePlugins.java @@ -135,7 +135,10 @@ public ParseCurrentUserController getCurrentUserController() { Parse.isLocalDatastoreEnabled() ? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore) : fileStore; - ParseCurrentUserController controller = new CachedCurrentUserController(store); + EncryptedFileObjectStore encryptedFileObjectStore = new EncryptedFileObjectStore<>(ParseUser.class, file, ParseUserCurrentCoder.get()); + ParseObjectStoreMigrator storeMigrator = new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store); + ParseCurrentUserController controller = new CachedCurrentUserController(storeMigrator); + currentUserController.compareAndSet(null, controller); currentUserController.compareAndSet(null, controller); } return currentUserController.get(); diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java index c48f7b517..804f8c140 100644 --- a/parse/src/main/java/com/parse/ParseFileUtils.java +++ b/parse/src/main/java/com/parse/ParseFileUtils.java @@ -18,6 +18,8 @@ import android.net.Uri; import androidx.annotation.NonNull; +import androidx.security.crypto.EncryptedFile; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -27,6 +29,7 @@ import java.io.OutputStream; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.security.GeneralSecurityException; import java.util.List; import org.json.JSONException; import org.json.JSONObject; @@ -63,6 +66,27 @@ public static byte[] readFileToByteArray(File file) throws IOException { // ----------------------------------------------------------------------- + /** + * + * Reads the contents of an encrypted file into a byte array. The file is always closed. + * + * @param file the encrypted file to read, must not be null + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @throws GeneralSecurityException in case of an encryption related error + */ + public static byte[] readFileToByteArray(EncryptedFile file) throws IOException, GeneralSecurityException { + InputStream in = null; + try { + in = file.openFileInput(); + return ParseIOUtils.toByteArray(in); + } finally { + ParseIOUtils.closeQuietly(in); + } + } + + + /** * Opens a {@link FileInputStream} for the specified file, providing better error messages than * simply calling new FileInputStream(file). @@ -116,6 +140,26 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti } } + /** + * Writes a byte array to an encrypted file, will not create the file if it does not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @throws GeneralSecurityException in case of an encryption related error + */ + public static void writeByteArrayToFile(EncryptedFile file, byte[] data) throws IOException, GeneralSecurityException { + OutputStream out = null; + try { + out = file.openFileOutput(); + out.write(data); + } finally { + ParseIOUtils.closeQuietly(out); + } + } + + + /** * Writes a content uri to a file creating the file if it does not exist. * @@ -549,6 +593,30 @@ public static boolean isSymlink(final File file) throws IOException { return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile()); } + /** + * @param file the encrypted file to read + * @param encoding the file encoding used when written to disk + * @return Reads the contents of an encrypted file into a {@link String}. The file is always closed. + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + */ + public static String readFileToString(EncryptedFile file, Charset encoding) throws IOException, GeneralSecurityException { + return new String(readFileToByteArray(file), encoding); + } + + /** + * @param file the encrypted file to read + * @param encoding the file encoding used when written to disk + * @return Reads the contents of an encrypted file into a {@link String}. The file is always closed. + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + */ + public static String readFileToString(EncryptedFile file, String encoding) throws IOException, GeneralSecurityException { + return readFileToString(file, Charset.forName(encoding)); + } + + + // region String public static String readFileToString(File file, Charset encoding) throws IOException { @@ -569,6 +637,32 @@ public static void writeStringToFile(File file, String string, String encoding) writeStringToFile(file, string, Charset.forName(encoding)); } + /** + * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. + * @param file the encrypted file to use for writing. + * @param string the text to write. + * @param encoding the encoding used for the text written. + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + */ + public static void writeStringToFile(EncryptedFile file, String string, Charset encoding) + throws IOException, GeneralSecurityException { + writeByteArrayToFile(file, string.getBytes(encoding)); + } + + /** + * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. + * @param file the encrypted file to use for writing. + * @param string the text to write. + * @param encoding the encoding used for the text written. + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + */ + public static void writeStringToFile(EncryptedFile file, String string, String encoding) + throws IOException, GeneralSecurityException { + writeStringToFile(file, string, Charset.forName(encoding)); + } + // endregion // region JSONObject @@ -584,5 +678,18 @@ public static void writeJSONObjectToFile(File file, JSONObject json) throws IOEx ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes("UTF-8")); } + /** Reads the contents of an encrypted file into a {@link JSONObject}. The file is always closed. */ + public static JSONObject readFileToJSONObject(EncryptedFile file) throws IOException, JSONException, GeneralSecurityException { + String content = readFileToString(file, "UTF-8"); + return new JSONObject(content); + } + + /** Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. */ + public static void writeJSONObjectToFile(EncryptedFile file, JSONObject json) throws IOException, GeneralSecurityException { + ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes("UTF-8")); + } + + + // endregion } diff --git a/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java b/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java new file mode 100644 index 000000000..47da1ecde --- /dev/null +++ b/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java @@ -0,0 +1,69 @@ +package com.parse; + +import com.parse.boltsinternal.Continuation; +import com.parse.boltsinternal.Task; + +import java.util.Arrays; + +/** + * Use this utility class to migrate from one {@link ParseObjectStore} to another + */ +class ParseObjectStoreMigrator implements ParseObjectStore { + + private final ParseObjectStore store; + private final ParseObjectStore legacy; + + /** + * @param store the new {@link ParseObjectStore} to migrate to + * @param legacy the old {@link ParseObjectStore} to migrate from + */ + public ParseObjectStoreMigrator(ParseObjectStore store, ParseObjectStore legacy) { + this.store = store; + this.legacy = legacy; + } + + @Override + public Task getAsync() { + return store.getAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.getResult() != null) return task; + return legacy.getAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + T object = task.getResult(); + if (object == null) return task; + return legacy.deleteAsync().continueWith(task1 -> ParseTaskUtils.wait(store.setAsync(object))).onSuccess(task1 -> object); + } + }); + } + }); + } + + @Override + public Task setAsync(T object) { + return store.setAsync(object); + } + + @Override + public Task existsAsync() { + return store.existsAsync().continueWithTask(new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.getResult()) return Task.forResult(true); + return legacy.existsAsync(); + } + }); + } + + @Override + public Task deleteAsync() { + Task storeTask = store.deleteAsync(); + return Task.whenAll(Arrays.asList(legacy.deleteAsync(), storeTask)).continueWithTask(new Continuation>() { + @Override + public Task then(Task task1) throws Exception { + return storeTask; + } + }); + } +} diff --git a/parse/src/test/java/com/parse/AlgorithmParameterSpec.kt b/parse/src/test/java/com/parse/AlgorithmParameterSpec.kt new file mode 100644 index 000000000..7e97ee6fa --- /dev/null +++ b/parse/src/test/java/com/parse/AlgorithmParameterSpec.kt @@ -0,0 +1,6 @@ +package com.parse + +import java.security.spec.AlgorithmParameterSpec + +internal val AlgorithmParameterSpec.keystoreAlias: String + get() = this::class.java.getDeclaredMethod("getKeystoreAlias").invoke(this) as String diff --git a/parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt b/parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt new file mode 100644 index 000000000..8087b2117 --- /dev/null +++ b/parse/src/test/java/com/parse/AndroidKeyStoreProvider.kt @@ -0,0 +1,177 @@ +package com.parse + +/* + * Copyright 2020 Appmattus Limited + * + * 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. + */ + +import java.io.InputStream +import java.io.OutputStream +import java.security.Key +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.KeyPairGeneratorSpi +import java.security.KeyStore +import java.security.KeyStoreSpi +import java.security.Provider +import java.security.SecureRandom +import java.security.cert.Certificate +import java.security.spec.AlgorithmParameterSpec +import java.util.Collections +import java.util.Date +import java.util.Enumeration +import javax.crypto.KeyGenerator +import javax.crypto.KeyGeneratorSpi +import javax.crypto.SecretKey + +class AndroidKeyStoreProvider : Provider("AndroidKeyStore", 1.0, "") { + init { + put("KeyStore.AndroidKeyStore", AndroidKeyStore::class.java.name) + put("KeyGenerator.AES", AesKeyGenerator::class.java.name) + put("KeyGenerator.HmacSHA256", HmacSHA256KeyGenerator::class.java.name) + put("KeyPairGenerator.RSA", RsaKeyPairGenerator::class.java.name) + } + + @Suppress("TooManyFunctions") + class AndroidKeyStore : KeyStoreSpi() { + override fun engineIsKeyEntry(alias: String?): Boolean = wrapped.isKeyEntry(alias) + + override fun engineIsCertificateEntry(alias: String?): Boolean = + wrapped.isCertificateEntry(alias) + + override fun engineGetCertificate(alias: String?): Certificate = + wrapped.getCertificate(alias) + + override fun engineGetCreationDate(alias: String?): Date = wrapped.getCreationDate(alias) + + override fun engineDeleteEntry(alias: String?) { + storedKeys.remove(alias) + } + + override fun engineSetKeyEntry( + alias: String?, + key: Key?, + password: CharArray?, + chain: Array?, + ) = + wrapped.setKeyEntry(alias, key, password, chain) + + override fun engineSetKeyEntry( + alias: String?, + key: ByteArray?, + chain: Array?, + ) = wrapped.setKeyEntry(alias, key, chain) + + override fun engineStore(stream: OutputStream?, password: CharArray?) = + wrapped.store(stream, password) + + override fun engineSize(): Int = wrapped.size() + + override fun engineAliases(): Enumeration = Collections.enumeration(storedKeys.keys) + + override fun engineContainsAlias(alias: String?): Boolean = storedKeys.containsKey(alias) + + override fun engineLoad(stream: InputStream?, password: CharArray?) = + wrapped.load(stream, password) + + override fun engineGetCertificateChain(alias: String?): Array? = + wrapped.getCertificateChain(alias) + + override fun engineSetCertificateEntry(alias: String?, cert: Certificate?) = + wrapped.setCertificateEntry(alias, cert) + + override fun engineGetCertificateAlias(cert: Certificate?): String? = + wrapped.getCertificateAlias(cert) + + override fun engineGetKey(alias: String?, password: CharArray?): Key? = + (storedKeys[alias] as? KeyStore.SecretKeyEntry)?.secretKey + + override fun engineGetEntry( + p0: String, + p1: KeyStore.ProtectionParameter?, + ): KeyStore.Entry? = storedKeys[p0] + + override fun engineSetEntry( + p0: String, + p1: KeyStore.Entry, + p2: KeyStore.ProtectionParameter?, + ) { + storedKeys[p0] = p1 + } + + override fun engineLoad(p0: KeyStore.LoadStoreParameter?) = wrapped.load(p0) + + override fun engineStore(p0: KeyStore.LoadStoreParameter?) = wrapped.store(p0) + + override fun engineEntryInstanceOf(p0: String?, p1: Class?) = + wrapped.entryInstanceOf(p0, p1) + + companion object { + private val wrapped = KeyStore.getInstance("BKS", "BC") + internal val storedKeys = mutableMapOf() + } + } + + class AesKeyGenerator : KeyGeneratorSpi() { + private val wrapped = KeyGenerator.getInstance("AES", "BC") + private var lastSpec: AlgorithmParameterSpec? = null + + override fun engineInit(random: SecureRandom?) = wrapped.init(random) + + override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?) = + wrapped.init(random).also { + lastSpec = params + } + + override fun engineInit(keysize: Int, random: SecureRandom?) = wrapped.init(keysize, random) + + override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also { + AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it) + } + } + + class HmacSHA256KeyGenerator : KeyGeneratorSpi() { + private val wrapped = KeyGenerator.getInstance("HmacSHA256", "BC") + private var lastSpec: AlgorithmParameterSpec? = null + + override fun engineInit(random: SecureRandom?) = wrapped.init(random) + override fun engineInit(params: AlgorithmParameterSpec?, random: SecureRandom?) = + wrapped.init(random).also { + lastSpec = params + } + + override fun engineInit(keysize: Int, random: SecureRandom?) = Unit + override fun engineGenerateKey(): SecretKey = wrapped.generateKey().also { + AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.SecretKeyEntry(it) + } + } + + class RsaKeyPairGenerator : KeyPairGeneratorSpi() { + private val wrapped = KeyPairGenerator.getInstance("RSA", "BC") + + private var lastSpec: AlgorithmParameterSpec? = null + + // {@link KeyPair#toCertificate()} is used for generating JcaX509 certificates using org.bouncycastle library which might not be required now, but can be implemented when needed. + override fun generateKeyPair(): KeyPair = wrapped.generateKeyPair().also { keyPair -> + null +// AndroidKeyStore.storedKeys[lastSpec!!.keystoreAlias] = KeyStore.PrivateKeyEntry(keyPair.private, arrayOf(keyPair.toCertificate())) + } + + override fun initialize(p0: Int, p1: SecureRandom?) = Unit + + override fun initialize(p0: AlgorithmParameterSpec?, p1: SecureRandom?) { + lastSpec = p0 + } + } +} diff --git a/parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt b/parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt new file mode 100644 index 000000000..2f98e8742 --- /dev/null +++ b/parse/src/test/java/com/parse/AndroidOpenSSLProvider.kt @@ -0,0 +1,64 @@ +package com.parse + +/* + * Copyright 2020 Appmattus Limited + * + * 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. + */ + +import android.annotation.SuppressLint +import java.security.AlgorithmParameters +import java.security.Key +import java.security.Provider +import java.security.SecureRandom +import java.security.spec.AlgorithmParameterSpec +import javax.crypto.Cipher +import javax.crypto.CipherSpi + +class AndroidOpenSSLProvider : Provider("AndroidOpenSSL", 1.0, "") { + init { + put("Cipher.RSA/ECB/PKCS1Padding", RsaCipher::class.java.name) + } + + @Suppress("TooManyFunctions") + class RsaCipher : CipherSpi() { + @SuppressLint("GetInstance") + private val wrapped = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC") + + override fun engineSetMode(p0: String?) = Unit + + override fun engineInit(p0: Int, p1: Key?, p2: SecureRandom?) = wrapped.init(p0, p1, p2) + + override fun engineInit(p0: Int, p1: Key?, p2: AlgorithmParameterSpec?, p3: SecureRandom?) = wrapped.init(p0, p1, p2, p3) + + override fun engineInit(p0: Int, p1: Key?, p2: AlgorithmParameters?, p3: SecureRandom?) = wrapped.init(p0, p1, p2, p3) + + override fun engineGetIV(): ByteArray = wrapped.iv + + override fun engineDoFinal(p0: ByteArray?, p1: Int, p2: Int): ByteArray = wrapped.doFinal(p0, p1, p2) + + override fun engineDoFinal(p0: ByteArray?, p1: Int, p2: Int, p3: ByteArray?, p4: Int) = wrapped.doFinal(p0, p1, p2, p3, p4) + + override fun engineSetPadding(p0: String?) = Unit + + override fun engineGetParameters(): AlgorithmParameters = wrapped.parameters + + override fun engineUpdate(p0: ByteArray?, p1: Int, p2: Int): ByteArray = wrapped.update(p0, p1, p2) + + override fun engineUpdate(p0: ByteArray?, p1: Int, p2: Int, p3: ByteArray?, p4: Int): Int = wrapped.update(p0, p1, p2, p3, p4) + + override fun engineGetBlockSize(): Int = wrapped.blockSize + + override fun engineGetOutputSize(p0: Int): Int = wrapped.getOutputSize(p0) + } +} diff --git a/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java b/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java new file mode 100644 index 000000000..78458853c --- /dev/null +++ b/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java @@ -0,0 +1,119 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +import android.content.Context; +import androidx.security.crypto.EncryptedFile; +import androidx.security.crypto.MasterKey; +import androidx.test.platform.app.InstrumentationRegistry; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.skyscreamer.jsonassert.JSONCompareMode; +import java.io.File; +import kotlin.jvm.JvmStatic; + +@RunWith(RobolectricTestRunner.class) +public class EncryptedFileObjectStoreTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setUp() { + RobolectricKeyStore.INSTANCE.getSetup(); + ParseObject.registerSubclass(ParseUser.class); + Parse.initialize(new Parse.Configuration.Builder(InstrumentationRegistry.getInstrumentation().getTargetContext()).server("http://parse.com").build()); + } + + @After + public void tearDown() { + ParseObject.unregisterSubclass(ParseUser.class); + } + + @Test + public void testSetAsync() throws Exception { + File file = new File(temporaryFolder.getRoot(), "test"); + + ParseUser.State state = mock(ParseUser.State.class); + JSONObject json = new JSONObject(); + json.put("foo", "bar"); + ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); + when(coder.encode(eq(state), isNull(), any(PointerEncoder.class))) + .thenReturn(json); + EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, coder); + + ParseUser user = mock(ParseUser.class); + when(user.getState()).thenReturn(state); + ParseTaskUtils.wait(store.setAsync(user)); + + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + EncryptedFile encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + JSONObject jsonAgain = ParseFileUtils.readFileToJSONObject(encryptedFile); + assertEquals(json, jsonAgain, JSONCompareMode.STRICT); + } + + @Test + public void testGetAsync() throws Exception { + File file = new File(temporaryFolder.getRoot(), "test"); + + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + EncryptedFile encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + + JSONObject json = new JSONObject(); + ParseFileUtils.writeJSONObjectToFile(encryptedFile, json); + + ParseUser.State.Builder builder = new ParseUser.State.Builder(); + builder.put("foo", "bar"); + ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); + when(coder.decode( + any(ParseUser.State.Builder.class), + any(JSONObject.class), + any(ParseDecoder.class))) + .thenReturn(builder); + EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, coder); + + ParseUser user = ParseTaskUtils.wait(store.getAsync()); + assertEquals("bar", user.getState().get("foo")); + } + + @Test + public void testExistsAsync() throws Exception { + File file = temporaryFolder.newFile("test"); + EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, null); + assertTrue(ParseTaskUtils.wait(store.existsAsync())); + + temporaryFolder.delete(); + assertFalse(ParseTaskUtils.wait(store.existsAsync())); + } + + @Test + public void testDeleteAsync() throws Exception { + File file = temporaryFolder.newFile("test"); + EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, null); + assertTrue(file.exists()); + + ParseTaskUtils.wait(store.deleteAsync()); + assertFalse(file.exists()); + } +} diff --git a/parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt b/parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt new file mode 100644 index 000000000..5335dcc10 --- /dev/null +++ b/parse/src/test/java/com/parse/ParseObjectStoreMigratorTest.kt @@ -0,0 +1,101 @@ +package com.parse + +import com.parse.boltsinternal.Task +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) +class ParseObjectStoreMigratorTest { + + private lateinit var store: ParseObjectStore + private lateinit var legacy: ParseObjectStore + private lateinit var migrator: ParseObjectStoreMigrator + + @BeforeEach + fun setUp() { + store = mock(ParseObjectStore::class.java) as ParseObjectStore + legacy = mock(ParseObjectStore::class.java) as ParseObjectStore + migrator = ParseObjectStoreMigrator(store, legacy) + } + + @Test + fun testGetAsyncWhenStoreHasData() { + val parseObject = mock(ParseObject::class.java) + `when`(store.getAsync()).thenReturn(Task.forResult(parseObject)) + + val result = migrator.getAsync().result + + assertEquals(parseObject, result) + verify(store, times(1)).getAsync() + verify(legacy, never()).getAsync() + } + + @Test + fun testGetAsyncWhenStoreIsEmptyAndLegacyHasData() { + val parseObject = mock(ParseObject::class.java) + `when`(store.getAsync()).thenReturn(Task.forResult(null)) + `when`(legacy.getAsync()).thenReturn(Task.forResult(parseObject)) + `when`(legacy.deleteAsync()).thenReturn(Task.forResult(null)) + `when`(store.setAsync(parseObject)).thenReturn(Task.forResult(null)) + + val result = migrator.getAsync().result + + assertEquals(parseObject, result) + verify(store, times(1)).getAsync() + verify(legacy, times(1)).getAsync() + verify(legacy, times(1)).deleteAsync() + verify(store, times(1)).setAsync(parseObject) + } + + @Test + fun testSetAsync() { + val parseObject = mock(ParseObject::class.java) + `when`(store.setAsync(parseObject)).thenReturn(Task.forResult(null)) + + migrator.setAsync(parseObject).waitForCompletion() + + verify(store, times(1)).setAsync(parseObject) + } + + @Test + fun testExistsAsyncWhenStoreHasData() { + `when`(store.existsAsync()).thenReturn(Task.forResult(true)) + + val result = migrator.existsAsync().result + + assertTrue(result) + verify(store, times(1)).existsAsync() + verify(legacy, never()).existsAsync() + } + + @Test + fun testExistsAsyncWhenStoreIsEmptyAndLegacyHasData() { + `when`(store.existsAsync()).thenReturn(Task.forResult(false)) + `when`(legacy.existsAsync()).thenReturn(Task.forResult(true)) + + val result = migrator.existsAsync().result + + assertTrue(result) + verify(store, times(1)).existsAsync() + verify(legacy, times(1)).existsAsync() + } + + @Test + fun testDeleteAsync() { + `when`(store.deleteAsync()).thenReturn(Task.forResult(null)) + `when`(legacy.deleteAsync()).thenReturn(Task.forResult(null)) + + migrator.deleteAsync().waitForCompletion() + + verify(store, times(1)).deleteAsync() + verify(legacy, times(1)).deleteAsync() + } +} diff --git a/parse/src/test/java/com/parse/RobolectricKeyStore.kt b/parse/src/test/java/com/parse/RobolectricKeyStore.kt new file mode 100644 index 000000000..7162ff1c5 --- /dev/null +++ b/parse/src/test/java/com/parse/RobolectricKeyStore.kt @@ -0,0 +1,17 @@ +package com.parse + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security + +object RobolectricKeyStore { + + val setup by lazy { + Security.removeProvider("AndroidKeyStore") + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.removeProvider("AndroidOpenSSL") + + Security.addProvider(AndroidKeyStoreProvider()) + Security.addProvider(BouncyCastleProvider()) + Security.addProvider(AndroidOpenSSLProvider()) + } +} From 6d208d780810d048002da10ba9d6e7e7dfa261aa Mon Sep 17 00:00:00 2001 From: rommansabbir Date: Fri, 16 Aug 2024 00:34:44 +0600 Subject: [PATCH 8/9] fix : spotless apply issue fixed, using a new version. --- .../CancellationTokenSource.java | 4 +- .../java/com/parse/boltsinternal/Task.java | 16 ++- .../boltsinternal/TaskCompletionSource.java | 4 +- build.gradle | 2 +- .../com/parse/EncryptedFileObjectStore.java | 97 ++++++++++++------- .../src/main/java/com/parse/ManifestInfo.java | 4 +- parse/src/main/java/com/parse/Parse.java | 4 +- .../main/java/com/parse/ParseClassName.java | 4 +- .../main/java/com/parse/ParseCorePlugins.java | 7 +- .../main/java/com/parse/ParseFileUtils.java | 67 +++++++------ .../com/parse/ParseObjectStoreMigrator.java | 77 +++++++++------ parse/src/main/java/com/parse/ParseQuery.java | 4 +- .../src/main/java/com/parse/ParseSession.java | 4 +- parse/src/main/java/com/parse/ParseUser.java | 4 +- .../parse/EncryptedFileObjectStoreTest.java | 56 +++++++---- .../com/parse/twitter/ParseTwitterUtils.java | 4 +- 16 files changed, 229 insertions(+), 129 deletions(-) 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 2ca4a19f9..210eae9cf 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/CancellationTokenSource.java @@ -46,7 +46,9 @@ public boolean isCancellationRequested() { } } - /** @return the token that can be passed to asynchronous method to control cancellation. */ + /** + * @return the token that can be passed to asynchronous method to control cancellation. + */ public CancellationToken getToken() { synchronized (lock) { throwIfClosed(); 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 8e73588b2..34da3dcb3 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/Task.java @@ -541,28 +541,36 @@ public boolean isCompleted() { } } - /** @return {@code true} if the task was cancelled, {@code false} otherwise. */ + /** + * @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. */ + /** + * @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. */ + /** + * @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. */ + /** + * @return The error for the task, if set. {@code null} otherwise. + */ public Exception getError() { synchronized (lock) { if (error != null) { diff --git a/bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java b/bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java index 4406cefbd..4513b76d6 100644 --- a/bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java +++ b/bolts-tasks/src/main/java/com/parse/boltsinternal/TaskCompletionSource.java @@ -24,7 +24,9 @@ public TaskCompletionSource() { task = new Task<>(); } - /** @return the Task associated with this TaskCompletionSource. */ + /** + * @return the Task associated with this TaskCompletionSource. + */ public Task getTask() { return task; } diff --git a/build.gradle b/build.gradle index 7f0b633e7..8d392d916 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { classpath "org.jacoco:org.jacoco.core:$jacocoVersion" classpath "com.dicedmelon.gradle:jacoco-android:0.1.5" classpath "io.freefair.gradle:android-gradle-plugins:4.2.0-m1" - classpath "com.diffplug.spotless:spotless-plugin-gradle:5.17.1" + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.7.1" } } diff --git a/parse/src/main/java/com/parse/EncryptedFileObjectStore.java b/parse/src/main/java/com/parse/EncryptedFileObjectStore.java index b31ca647b..59caa1a3e 100644 --- a/parse/src/main/java/com/parse/EncryptedFileObjectStore.java +++ b/parse/src/main/java/com/parse/EncryptedFileObjectStore.java @@ -1,19 +1,19 @@ package com.parse; import android.content.Context; - import androidx.security.crypto.EncryptedFile; import androidx.security.crypto.MasterKey; import com.parse.boltsinternal.Task; -import org.json.JSONException; -import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.concurrent.Callable; +import org.json.JSONException; +import org.json.JSONObject; /** - * a file based {@link ParseObjectStore} using Jetpack's {@link EncryptedFile} class to protect files from a malicious copy. + * a file based {@link ParseObjectStore} using Jetpack's {@link EncryptedFile} class to protect + * files from a malicious copy. */ class EncryptedFileObjectStore implements ParseObjectStore { @@ -32,7 +32,15 @@ public EncryptedFileObjectStore(String className, File file, ParseObjectCurrentC this.coder = coder; Context context = ParsePlugins.get().applicationContext(); try { - encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + encryptedFile = + new EncryptedFile.Builder( + context, + file, + new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(), + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB) + .build(); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e.getMessage()); } @@ -46,8 +54,9 @@ private static ParseObjectSubclassingController getSubclassingController() { * Saves the {@code ParseObject} to the a file on disk as JSON in /2/ format. * * @param current ParseObject which needs to be saved to disk. - * @throws IOException thrown if an error occurred during writing of the file - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws IOException thrown if an error occurred during writing of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file */ private void saveToDisk(ParseObject current) throws IOException, GeneralSecurityException { JSONObject json = coder.encode(current.getState(), null, PointerEncoder.get()); @@ -58,41 +67,54 @@ private void saveToDisk(ParseObject current) throws IOException, GeneralSecurity * Retrieves a {@code ParseObject} from a file on disk in /2/ format. * * @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}. - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file - * @throws JSONException thrown if an error occurred during the decoding process of the ParseObject to a JSONObject - * @throws IOException thrown if an error occurred during writing of the file + * of the file is an invalid {@code ParseObject}, returns {@code null}. + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file + * @throws JSONException thrown if an error occurred during the decoding process of the + * ParseObject to a JSONObject + * @throws IOException thrown if an error occurred during writing of the file */ private T getFromDisk() throws GeneralSecurityException, JSONException, IOException { - return ParseObject.from(coder.decode(ParseObject.State.newBuilder(className), ParseFileUtils.readFileToJSONObject(encryptedFile), ParseDecoder.get()).isComplete(true).build()); + return ParseObject.from( + coder.decode( + ParseObject.State.newBuilder(className), + ParseFileUtils.readFileToJSONObject(encryptedFile), + ParseDecoder.get()) + .isComplete(true) + .build()); } @Override public Task getAsync() { - return Task.call(new Callable() { - @Override - public T call() throws Exception { - if (!file.exists()) return null; - try { - return getFromDisk(); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e.getMessage()); - } - } - }, ParseExecutors.io()); + return Task.call( + new Callable() { + @Override + public T call() throws Exception { + if (!file.exists()) return null; + try { + return getFromDisk(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e.getMessage()); + } + } + }, + ParseExecutors.io()); } @Override public Task setAsync(T object) { - return Task.call(() -> { - if (file.exists() && !ParseFileUtils.deleteQuietly(file)) throw new RuntimeException("Unable to delete"); - try { - saveToDisk(object); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e.getMessage()); - } - return null; - }, ParseExecutors.io()); + return Task.call( + () -> { + if (file.exists() && !ParseFileUtils.deleteQuietly(file)) + throw new RuntimeException("Unable to delete"); + try { + saveToDisk(object); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e.getMessage()); + } + return null; + }, + ParseExecutors.io()); } @Override @@ -102,9 +124,12 @@ public Task existsAsync() { @Override public Task deleteAsync() { - return Task.call(() -> { - if (file.exists() && !ParseFileUtils.deleteQuietly(file)) throw new RuntimeException("Unable to delete"); - return null; - }, ParseExecutors.io()); + return Task.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/ManifestInfo.java b/parse/src/main/java/com/parse/ManifestInfo.java index 31b221976..276fcd14b 100644 --- a/parse/src/main/java/com/parse/ManifestInfo.java +++ b/parse/src/main/java/com/parse/ManifestInfo.java @@ -150,7 +150,9 @@ private static ApplicationInfo getApplicationInfo(Context context, int flags) { } } - /** @return A {@link Bundle} if meta-data is specified in AndroidManifest, otherwise 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) { diff --git a/parse/src/main/java/com/parse/Parse.java b/parse/src/main/java/com/parse/Parse.java index c3ad5c1ee..66fa2502a 100644 --- a/parse/src/main/java/com/parse/Parse.java +++ b/parse/src/main/java/com/parse/Parse.java @@ -288,7 +288,9 @@ public static void destroy() { allowCustomObjectId = false; } - /** @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. */ + /** + * @return {@code True} if {@link #initialize} has been called, otherwise {@code false}. + */ static boolean isInitialized() { return ParsePlugins.get() != null; } diff --git a/parse/src/main/java/com/parse/ParseClassName.java b/parse/src/main/java/com/parse/ParseClassName.java index d2f4a3562..ab5b436a2 100644 --- a/parse/src/main/java/com/parse/ParseClassName.java +++ b/parse/src/main/java/com/parse/ParseClassName.java @@ -21,6 +21,8 @@ @Inherited @Documented public @interface ParseClassName { - /** @return The Parse class name associated with the ParseObject subclass. */ + /** + * @return The Parse class name associated with the ParseObject subclass. + */ String value(); } diff --git a/parse/src/main/java/com/parse/ParseCorePlugins.java b/parse/src/main/java/com/parse/ParseCorePlugins.java index ed18f230d..420ba9233 100644 --- a/parse/src/main/java/com/parse/ParseCorePlugins.java +++ b/parse/src/main/java/com/parse/ParseCorePlugins.java @@ -135,8 +135,11 @@ public ParseCurrentUserController getCurrentUserController() { Parse.isLocalDatastoreEnabled() ? new OfflineObjectStore<>(ParseUser.class, PIN_CURRENT_USER, fileStore) : fileStore; - EncryptedFileObjectStore encryptedFileObjectStore = new EncryptedFileObjectStore<>(ParseUser.class, file, ParseUserCurrentCoder.get()); - ParseObjectStoreMigrator storeMigrator = new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store); + EncryptedFileObjectStore encryptedFileObjectStore = + new EncryptedFileObjectStore<>( + ParseUser.class, file, ParseUserCurrentCoder.get()); + ParseObjectStoreMigrator storeMigrator = + new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store); ParseCurrentUserController controller = new CachedCurrentUserController(storeMigrator); currentUserController.compareAndSet(null, controller); currentUserController.compareAndSet(null, controller); diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java index 804f8c140..d0c0d1732 100644 --- a/parse/src/main/java/com/parse/ParseFileUtils.java +++ b/parse/src/main/java/com/parse/ParseFileUtils.java @@ -19,7 +19,6 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.security.crypto.EncryptedFile; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -67,7 +66,6 @@ public static byte[] readFileToByteArray(File file) throws IOException { // ----------------------------------------------------------------------- /** - * * Reads the contents of an encrypted file into a byte array. The file is always closed. * * @param file the encrypted file to read, must not be null @@ -75,7 +73,8 @@ public static byte[] readFileToByteArray(File file) throws IOException { * @throws IOException in case of an I/O error * @throws GeneralSecurityException in case of an encryption related error */ - public static byte[] readFileToByteArray(EncryptedFile file) throws IOException, GeneralSecurityException { + public static byte[] readFileToByteArray(EncryptedFile file) + throws IOException, GeneralSecurityException { InputStream in = null; try { in = file.openFileInput(); @@ -85,8 +84,6 @@ public static byte[] readFileToByteArray(EncryptedFile file) throws IOException, } } - - /** * Opens a {@link FileInputStream} for the specified file, providing better error messages than * simply calling new FileInputStream(file). @@ -148,7 +145,8 @@ public static void writeByteArrayToFile(File file, byte[] data) throws IOExcepti * @throws IOException in case of an I/O error * @throws GeneralSecurityException in case of an encryption related error */ - public static void writeByteArrayToFile(EncryptedFile file, byte[] data) throws IOException, GeneralSecurityException { + public static void writeByteArrayToFile(EncryptedFile file, byte[] data) + throws IOException, GeneralSecurityException { OutputStream out = null; try { out = file.openFileOutput(); @@ -158,8 +156,6 @@ public static void writeByteArrayToFile(EncryptedFile file, byte[] data) throws } } - - /** * Writes a content uri to a file creating the file if it does not exist. * @@ -596,27 +592,31 @@ public static boolean isSymlink(final File file) throws IOException { /** * @param file the encrypted file to read * @param encoding the file encoding used when written to disk - * @return Reads the contents of an encrypted file into a {@link String}. The file is always closed. + * @return Reads the contents of an encrypted file into a {@link String}. The file is always + * closed. * @throws IOException thrown if an error occurred during writing of the file - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file */ - public static String readFileToString(EncryptedFile file, Charset encoding) throws IOException, GeneralSecurityException { + public static String readFileToString(EncryptedFile file, Charset encoding) + throws IOException, GeneralSecurityException { return new String(readFileToByteArray(file), encoding); } /** * @param file the encrypted file to read * @param encoding the file encoding used when written to disk - * @return Reads the contents of an encrypted file into a {@link String}. The file is always closed. + * @return Reads the contents of an encrypted file into a {@link String}. The file is always + * closed. * @throws IOException thrown if an error occurred during writing of the file - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file */ - public static String readFileToString(EncryptedFile file, String encoding) throws IOException, GeneralSecurityException { + public static String readFileToString(EncryptedFile file, String encoding) + throws IOException, GeneralSecurityException { return readFileToString(file, Charset.forName(encoding)); } - - // region String public static String readFileToString(File file, Charset encoding) throws IOException { @@ -638,28 +638,34 @@ public static void writeStringToFile(File file, String string, String encoding) } /** - * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. + * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already + * exists. + * * @param file the encrypted file to use for writing. * @param string the text to write. * @param encoding the encoding used for the text written. * @throws IOException thrown if an error occurred during writing of the file - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file */ public static void writeStringToFile(EncryptedFile file, String string, Charset encoding) - throws IOException, GeneralSecurityException { + throws IOException, GeneralSecurityException { writeByteArrayToFile(file, string.getBytes(encoding)); } /** - * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. + * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already + * exists. + * * @param file the encrypted file to use for writing. * @param string the text to write. * @param encoding the encoding used for the text written. * @throws IOException thrown if an error occurred during writing of the file - * @throws GeneralSecurityException thrown if there is an error with encryption keys or during the encryption of the file + * @throws GeneralSecurityException thrown if there is an error with encryption keys or during + * the encryption of the file */ public static void writeStringToFile(EncryptedFile file, String string, String encoding) - throws IOException, GeneralSecurityException { + throws IOException, GeneralSecurityException { writeStringToFile(file, string, Charset.forName(encoding)); } @@ -678,18 +684,23 @@ public static void writeJSONObjectToFile(File file, JSONObject json) throws IOEx ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes("UTF-8")); } - /** Reads the contents of an encrypted file into a {@link JSONObject}. The file is always closed. */ - public static JSONObject readFileToJSONObject(EncryptedFile file) throws IOException, JSONException, GeneralSecurityException { + /** + * Reads the contents of an encrypted file into a {@link JSONObject}. The file is always closed. + */ + public static JSONObject readFileToJSONObject(EncryptedFile file) + throws IOException, JSONException, GeneralSecurityException { String content = readFileToString(file, "UTF-8"); return new JSONObject(content); } - /** Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already exists. */ - public static void writeJSONObjectToFile(EncryptedFile file, JSONObject json) throws IOException, GeneralSecurityException { + /** + * Writes a {@link JSONObject} to an encrypted file, will throw an error if the file already + * exists. + */ + public static void writeJSONObjectToFile(EncryptedFile file, JSONObject json) + throws IOException, GeneralSecurityException { ParseFileUtils.writeByteArrayToFile(file, json.toString().getBytes("UTF-8")); } - - // endregion } diff --git a/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java b/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java index 47da1ecde..13218d100 100644 --- a/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java +++ b/parse/src/main/java/com/parse/ParseObjectStoreMigrator.java @@ -2,19 +2,16 @@ import com.parse.boltsinternal.Continuation; import com.parse.boltsinternal.Task; - import java.util.Arrays; -/** - * Use this utility class to migrate from one {@link ParseObjectStore} to another - */ +/** Use this utility class to migrate from one {@link ParseObjectStore} to another */ class ParseObjectStoreMigrator implements ParseObjectStore { private final ParseObjectStore store; private final ParseObjectStore legacy; /** - * @param store the new {@link ParseObjectStore} to migrate to + * @param store the new {@link ParseObjectStore} to migrate to * @param legacy the old {@link ParseObjectStore} to migrate from */ public ParseObjectStoreMigrator(ParseObjectStore store, ParseObjectStore legacy) { @@ -24,20 +21,32 @@ public ParseObjectStoreMigrator(ParseObjectStore store, ParseObjectStore l @Override public Task getAsync() { - return store.getAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - if (task.getResult() != null) return task; - return legacy.getAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - T object = task.getResult(); - if (object == null) return task; - return legacy.deleteAsync().continueWith(task1 -> ParseTaskUtils.wait(store.setAsync(object))).onSuccess(task1 -> object); - } - }); - } - }); + return store.getAsync() + .continueWithTask( + new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.getResult() != null) return task; + return legacy.getAsync() + .continueWithTask( + new Continuation>() { + @Override + public Task then(Task task) + throws Exception { + T object = task.getResult(); + if (object == null) return task; + return legacy.deleteAsync() + .continueWith( + task1 -> + ParseTaskUtils.wait( + store + .setAsync( + object))) + .onSuccess(task1 -> object); + } + }); + } + }); } @Override @@ -47,23 +56,27 @@ public Task setAsync(T object) { @Override public Task existsAsync() { - return store.existsAsync().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - if (task.getResult()) return Task.forResult(true); - return legacy.existsAsync(); - } - }); + return store.existsAsync() + .continueWithTask( + new Continuation>() { + @Override + public Task then(Task task) throws Exception { + if (task.getResult()) return Task.forResult(true); + return legacy.existsAsync(); + } + }); } @Override public Task deleteAsync() { Task storeTask = store.deleteAsync(); - return Task.whenAll(Arrays.asList(legacy.deleteAsync(), storeTask)).continueWithTask(new Continuation>() { - @Override - public Task then(Task task1) throws Exception { - return storeTask; - } - }); + return Task.whenAll(Arrays.asList(legacy.deleteAsync(), storeTask)) + .continueWithTask( + new Continuation>() { + @Override + public Task then(Task task1) throws Exception { + return storeTask; + } + }); } } diff --git a/parse/src/main/java/com/parse/ParseQuery.java b/parse/src/main/java/com/parse/ParseQuery.java index 132ad4e39..67c44b3ef 100644 --- a/parse/src/main/java/com/parse/ParseQuery.java +++ b/parse/src/main/java/com/parse/ParseQuery.java @@ -282,7 +282,9 @@ public T getFirst() throws ParseException { return ParseTaskUtils.wait(getFirstInBackground()); } - /** @return the caching policy. */ + /** + * @return the caching policy. + */ public CachePolicy getCachePolicy() { return builder.getCachePolicy(); } diff --git a/parse/src/main/java/com/parse/ParseSession.java b/parse/src/main/java/com/parse/ParseSession.java index a2d5b2e4d..a656966d0 100644 --- a/parse/src/main/java/com/parse/ParseSession.java +++ b/parse/src/main/java/com/parse/ParseSession.java @@ -122,7 +122,9 @@ public static ParseQuery getQuery() { return !READ_ONLY_KEYS.contains(key); } - /** @return the session token for a user, if they are logged in. */ + /** + * @return the session token for a user, if they are logged in. + */ public String getSessionToken() { return getString(KEY_SESSION_TOKEN); } diff --git a/parse/src/main/java/com/parse/ParseUser.java b/parse/src/main/java/com/parse/ParseUser.java index 6fdf91845..c52c34862 100644 --- a/parse/src/main/java/com/parse/ParseUser.java +++ b/parse/src/main/java/com/parse/ParseUser.java @@ -717,7 +717,9 @@ public void remove(@NonNull String key) { } } - /** @return the session token for a user, if they are logged in. */ + /** + * @return the session token for a user, if they are logged in. + */ public String getSessionToken() { return getState().sessionToken(); } diff --git a/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java b/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java index 78458853c..e48afc39c 100644 --- a/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java +++ b/parse/src/test/java/com/parse/EncryptedFileObjectStoreTest.java @@ -17,10 +17,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; + import android.content.Context; import androidx.security.crypto.EncryptedFile; import androidx.security.crypto.MasterKey; import androidx.test.platform.app.InstrumentationRegistry; +import java.io.File; import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -30,20 +32,21 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.skyscreamer.jsonassert.JSONCompareMode; -import java.io.File; -import kotlin.jvm.JvmStatic; @RunWith(RobolectricTestRunner.class) public class EncryptedFileObjectStoreTest { - @Rule - public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before public void setUp() { RobolectricKeyStore.INSTANCE.getSetup(); ParseObject.registerSubclass(ParseUser.class); - Parse.initialize(new Parse.Configuration.Builder(InstrumentationRegistry.getInstrumentation().getTargetContext()).server("http://parse.com").build()); + Parse.initialize( + new Parse.Configuration.Builder( + InstrumentationRegistry.getInstrumentation().getTargetContext()) + .server("http://parse.com") + .build()); } @After @@ -59,16 +62,24 @@ public void testSetAsync() throws Exception { JSONObject json = new JSONObject(); json.put("foo", "bar"); ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); - when(coder.encode(eq(state), isNull(), any(PointerEncoder.class))) - .thenReturn(json); - EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, coder); + when(coder.encode(eq(state), isNull(), any(PointerEncoder.class))).thenReturn(json); + EncryptedFileObjectStore store = + new EncryptedFileObjectStore<>(ParseUser.class, file, coder); ParseUser user = mock(ParseUser.class); when(user.getState()).thenReturn(state); ParseTaskUtils.wait(store.setAsync(user)); Context context = InstrumentationRegistry.getInstrumentation().getContext(); - EncryptedFile encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + EncryptedFile encryptedFile = + new EncryptedFile.Builder( + context, + file, + new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(), + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB) + .build(); JSONObject jsonAgain = ParseFileUtils.readFileToJSONObject(encryptedFile); assertEquals(json, jsonAgain, JSONCompareMode.STRICT); } @@ -78,7 +89,15 @@ public void testGetAsync() throws Exception { File file = new File(temporaryFolder.getRoot(), "test"); Context context = InstrumentationRegistry.getInstrumentation().getContext(); - EncryptedFile encryptedFile = new EncryptedFile.Builder(context, file, new MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB).build(); + EncryptedFile encryptedFile = + new EncryptedFile.Builder( + context, + file, + new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(), + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB) + .build(); JSONObject json = new JSONObject(); ParseFileUtils.writeJSONObjectToFile(encryptedFile, json); @@ -87,11 +106,12 @@ public void testGetAsync() throws Exception { builder.put("foo", "bar"); ParseUserCurrentCoder coder = mock(ParseUserCurrentCoder.class); when(coder.decode( - any(ParseUser.State.Builder.class), - any(JSONObject.class), - any(ParseDecoder.class))) - .thenReturn(builder); - EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, coder); + any(ParseUser.State.Builder.class), + any(JSONObject.class), + any(ParseDecoder.class))) + .thenReturn(builder); + EncryptedFileObjectStore store = + new EncryptedFileObjectStore<>(ParseUser.class, file, coder); ParseUser user = ParseTaskUtils.wait(store.getAsync()); assertEquals("bar", user.getState().get("foo")); @@ -100,7 +120,8 @@ public void testGetAsync() throws Exception { @Test public void testExistsAsync() throws Exception { File file = temporaryFolder.newFile("test"); - EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, null); + EncryptedFileObjectStore store = + new EncryptedFileObjectStore<>(ParseUser.class, file, null); assertTrue(ParseTaskUtils.wait(store.existsAsync())); temporaryFolder.delete(); @@ -110,7 +131,8 @@ public void testExistsAsync() throws Exception { @Test public void testDeleteAsync() throws Exception { File file = temporaryFolder.newFile("test"); - EncryptedFileObjectStore store = new EncryptedFileObjectStore<>(ParseUser.class, file, null); + EncryptedFileObjectStore store = + new EncryptedFileObjectStore<>(ParseUser.class, file, null); assertTrue(file.exists()); ParseTaskUtils.wait(store.deleteAsync()); diff --git a/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java b/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java index 76707cb6f..2966df279 100644 --- a/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java +++ b/twitter/src/main/java/com/parse/twitter/ParseTwitterUtils.java @@ -108,7 +108,9 @@ private static void checkInitialization() { } } - /** @return {@code true} if the user is linked to a Twitter account. */ + /** + * @return {@code true} if the user is linked to a Twitter account. + */ public static boolean isLinked(ParseUser user) { return user.isLinked(AUTH_TYPE); } From 6887cfededf9e431498d7b7b163124b7e25bfd47 Mon Sep 17 00:00:00 2001 From: rommansabbir Date: Fri, 16 Aug 2024 00:39:29 +0600 Subject: [PATCH 9/9] refactoring : removed duplicate code --- parse/src/main/java/com/parse/ParseCorePlugins.java | 1 - 1 file changed, 1 deletion(-) diff --git a/parse/src/main/java/com/parse/ParseCorePlugins.java b/parse/src/main/java/com/parse/ParseCorePlugins.java index 420ba9233..197b3c2af 100644 --- a/parse/src/main/java/com/parse/ParseCorePlugins.java +++ b/parse/src/main/java/com/parse/ParseCorePlugins.java @@ -142,7 +142,6 @@ public ParseCurrentUserController getCurrentUserController() { new ParseObjectStoreMigrator<>(encryptedFileObjectStore, store); ParseCurrentUserController controller = new CachedCurrentUserController(storeMigrator); currentUserController.compareAndSet(null, controller); - currentUserController.compareAndSet(null, controller); } return currentUserController.get(); }