From 0995d4014c3f1fa016fce22f03126a86e781938c Mon Sep 17 00:00:00 2001 From: G-XD Date: Mon, 9 Oct 2023 02:08:55 +0800 Subject: [PATCH] feat(binding/java): add `rename` support (#3238) * test(binding/java): add rename test * test(binding/java): remove copy capability dependency in rename test --- bindings/java/src/blocking_operator.rs | 28 ++ .../org/apache/opendal/BlockingOperator.java | 6 + .../java/org/apache/opendal/Operator.java | 7 + bindings/java/src/operator.rs | 41 +++ .../test/behavior/AbstractBehaviorTest.java | 303 ++++++++++++++++++ 5 files changed, 385 insertions(+) diff --git a/bindings/java/src/blocking_operator.rs b/bindings/java/src/blocking_operator.rs index 1f4e9240d894..fd7fffa23550 100644 --- a/bindings/java/src/blocking_operator.rs +++ b/bindings/java/src/blocking_operator.rs @@ -178,3 +178,31 @@ fn intern_copy( Ok(op.copy(&source_path, &target_path)?) } + +/// # Safety +/// +/// This function should not be called before the Operator are ready. +#[no_mangle] +pub unsafe extern "system" fn Java_org_apache_opendal_BlockingOperator_rename( + mut env: JNIEnv, + _: JClass, + op: *mut BlockingOperator, + source_path: JString, + target_path: JString, +) { + intern_rename(&mut env, &mut *op, source_path, target_path).unwrap_or_else(|e| { + e.throw(&mut env); + }) +} + +fn intern_rename( + env: &mut JNIEnv, + op: &mut BlockingOperator, + source_path: JString, + target_path: JString, +) -> Result<()> { + let source_path = jstring_to_string(env, &source_path)?; + let target_path = jstring_to_string(env, &target_path)?; + + Ok(op.rename(&source_path, &target_path)?) +} diff --git a/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java b/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java index 811f3d6b9bc2..03e8ba8b7458 100644 --- a/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java +++ b/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java @@ -78,6 +78,10 @@ public void copy(String sourcePath, String targetPath) { copy(nativeHandle, sourcePath, targetPath); } + public void rename(String sourcePath, String targetPath) { + rename(nativeHandle, sourcePath, targetPath); + } + @Override protected native void disposeInternal(long handle); @@ -92,4 +96,6 @@ public void copy(String sourcePath, String targetPath) { private static native long createDir(long nativeHandle, String path); private static native long copy(long nativeHandle, String sourcePath, String targetPath); + + private static native long rename(long nativeHandle, String sourcePath, String targetPath); } diff --git a/bindings/java/src/main/java/org/apache/opendal/Operator.java b/bindings/java/src/main/java/org/apache/opendal/Operator.java index 8c791aeddadf..7ca7bccd204d 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Operator.java +++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java @@ -187,6 +187,11 @@ public CompletableFuture copy(String sourcePath, String targetPath) { return AsyncRegistry.take(requestId); } + public CompletableFuture rename(String sourcePath, String targetPath) { + final long requestId = rename(nativeHandle, sourcePath, targetPath); + return AsyncRegistry.take(requestId); + } + @Override protected native void disposeInternal(long handle); @@ -215,4 +220,6 @@ public CompletableFuture copy(String sourcePath, String targetPath) { private static native long createDir(long nativeHandle, String path); private static native long copy(long nativeHandle, String sourcePath, String targetPath); + + private static native long rename(long nativeHandle, String sourcePath, String targetPath); } diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs index 74e0b9c301bb..97fbaaa9a1e8 100644 --- a/bindings/java/src/operator.rs +++ b/bindings/java/src/operator.rs @@ -372,6 +372,47 @@ async fn do_copy(op: &mut Operator, source_path: String, target_path: String) -> Ok(op.copy(&source_path, &target_path).await?) } +/// # Safety +/// +/// This function should not be called before the Operator are ready. +#[no_mangle] +pub unsafe extern "system" fn Java_org_apache_opendal_Operator_rename( + mut env: JNIEnv, + _: JClass, + op: *mut Operator, + source_path: JString, + target_path: JString, +) -> jlong { + intern_rename(&mut env, op, source_path, target_path).unwrap_or_else(|e| { + e.throw(&mut env); + 0 + }) +} + +fn intern_rename( + env: &mut JNIEnv, + op: *mut Operator, + source_path: JString, + target_path: JString, +) -> Result { + let op = unsafe { &mut *op }; + let id = request_id(env)?; + + let source_path = jstring_to_string(env, &source_path)?; + let target_path = jstring_to_string(env, &target_path)?; + + unsafe { get_global_runtime() }.spawn(async move { + let result = do_rename(op, source_path, target_path).await; + complete_future(id, result.map(|_| JValueOwned::Void)) + }); + + Ok(id) +} + +async fn do_rename(op: &mut Operator, source_path: String, target_path: String) -> Result<()> { + Ok(op.rename(&source_path, &target_path).await?) +} + /// # Safety /// /// This function should not be called before the Operator are ready. diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java index ee7c5721b2a5..b88290f65ec3 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AbstractBehaviorTest.java @@ -375,6 +375,158 @@ public void testCopyOverwrite() { } } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + class AsyncRenameTest { + @BeforeAll + public void precondition() { + final Capability capability = operator.info.fullCapability; + assumeTrue(capability.read && capability.write && capability.rename); + } + + /** + * Rename a file and test with stat. + */ + @Test + public void testRenameFile() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + final String targetPath = UUID.randomUUID().toString(); + + operator.rename(sourcePath, targetPath).join(); + + assertThatThrownBy(() -> operator.stat(sourcePath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(operator.read(targetPath).join()).isEqualTo(content); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Rename a nonexistent source should return an error. + */ + @Test + public void testRenameNonExistingSource() { + final String sourcePath = UUID.randomUUID().toString(); + final String targetPath = UUID.randomUUID().toString(); + + assertThatThrownBy(() -> operator.rename(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + } + + /** + * Rename a dir as source should return an error. + */ + @Test + public void testRenameSourceDir() { + final String sourcePath = String.format("%s/", UUID.randomUUID().toString()); + final String targetPath = UUID.randomUUID().toString(); + + operator.createDir(sourcePath).join(); + + assertThatThrownBy(() -> operator.rename(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + operator.delete(sourcePath).join(); + } + + /** + * Rename to a dir should return an error. + */ + @Test + public void testRenameTargetDir() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + final String targetPath = String.format("%s/", UUID.randomUUID().toString()); + + operator.createDir(targetPath).join(); + + assertThatThrownBy(() -> operator.rename(sourcePath, targetPath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Rename a file to self should return an error. + */ + @Test + public void testRenameSelf() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + assertThatThrownBy(() -> operator.rename(sourcePath, sourcePath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + + operator.delete(sourcePath).join(); + } + + /** + * Rename to a nested path, parent path should be created successfully. + */ + @Test + public void testRenameNested() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] content = generateBytes(); + + operator.write(sourcePath, content).join(); + + final String targetPath = String.format( + "%s/%s/%s", + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + operator.rename(sourcePath, targetPath).join(); + + assertThatThrownBy(() -> operator.stat(sourcePath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(operator.read(targetPath).join()).isEqualTo(content); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + + /** + * Rename to a exist path should overwrite successfully. + */ + @Test + public void testRenameOverwrite() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + operator.write(sourcePath, sourceContent).join(); + + final String targetPath = UUID.randomUUID().toString(); + final byte[] targetContent = generateBytes(); + assertNotEquals(sourceContent, targetContent); + + operator.write(targetPath, targetContent).join(); + + operator.rename(sourcePath, targetPath).join(); + + assertThatThrownBy(() -> operator.stat(sourcePath).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(operator.read(targetPath).join()).isEqualTo(sourceContent); + + operator.delete(sourcePath).join(); + operator.delete(targetPath).join(); + } + } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Nested class BlockingWriteTest { @@ -605,6 +757,157 @@ public void testBlockingCopyOverwrite() { } } + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + class BlockingRenameTest { + @BeforeAll + public void precondition() { + final Capability capability = blockingOperator.info.fullCapability; + assumeTrue(capability.read && capability.write && capability.blocking && capability.rename); + } + + /** + * Rename a file and test with stat. + */ + @Test + public void testBlockingRenameFile() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = UUID.randomUUID().toString(); + + blockingOperator.rename(sourcePath, targetPath); + + assertThatThrownBy(() -> blockingOperator.stat(sourcePath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(blockingOperator.stat(targetPath).getContentLength()).isEqualTo(sourceContent.length); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Rename a nonexistent source should return an error. + */ + @Test + public void testBlockingRenameNonExistingSource() { + final String sourcePath = UUID.randomUUID().toString(); + final String targetPath = UUID.randomUUID().toString(); + + assertThatThrownBy(() -> blockingOperator.rename(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + } + + /** + * Rename a dir as source should return an error. + */ + @Test + public void testBlockingRenameSourceDir() { + final String sourcePath = String.format("%s/", UUID.randomUUID().toString()); + final String targetPath = UUID.randomUUID().toString(); + + blockingOperator.createDir(sourcePath); + + assertThatThrownBy(() -> blockingOperator.rename(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + } + + /** + * Rename to a dir should return an error. + */ + @Test + public void testBlockingRenameTargetDir() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = String.format("%s/", UUID.randomUUID().toString()); + + blockingOperator.createDir(targetPath); + + assertThatThrownBy(() -> blockingOperator.rename(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Rename a file to self should return an error. + */ + @Test + public void testBlockingRenameSelf() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + assertThatThrownBy(() -> blockingOperator.rename(sourcePath, sourcePath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + + blockingOperator.delete(sourcePath); + } + + /** + * Rename to a nested path, parent path should be created successfully. + */ + @Test + public void testBlockingRenameNested() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = String.format( + "%s/%s/%s", + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + blockingOperator.rename(sourcePath, targetPath); + + assertThatThrownBy(() -> blockingOperator.stat(sourcePath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(blockingOperator.read(targetPath)).isEqualTo(sourceContent); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + + /** + * Rename to a exist path should overwrite successfully. + */ + @Test + public void testBlockingRenameOverwrite() { + final String sourcePath = UUID.randomUUID().toString(); + final byte[] sourceContent = generateBytes(); + + blockingOperator.write(sourcePath, sourceContent); + + final String targetPath = UUID.randomUUID().toString(); + final byte[] targetContent = generateBytes(); + + assertNotEquals(sourceContent, targetContent); + + blockingOperator.write(targetPath, targetContent); + + blockingOperator.rename(sourcePath, targetPath); + + assertThatThrownBy(() -> blockingOperator.stat(sourcePath)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + assertThat(blockingOperator.read(targetPath)).isEqualTo(sourceContent); + + blockingOperator.delete(sourcePath); + blockingOperator.delete(targetPath); + } + } + /** * Generates a byte array of random content. */