diff --git a/.github/workflows/bindings_java.yml b/.github/workflows/bindings_java.yml index fef27ca45058..077f28627aab 100644 --- a/.github/workflows/bindings_java.yml +++ b/.github/workflows/bindings_java.yml @@ -81,15 +81,3 @@ jobs: run: | ./mvnw clean install -DskipTests ./mvnw verify artifact:compare - - name: Behavior tests - working-directory: bindings/java - shell: bash - run: | - export OPENDAL_TEST=memory - export OPENDAL_MEMORY_ROOT=/opendal - ./mvnw test -Dtest="behavior.*Test" - - export OPENDAL_TEST=fs - export OPENDAL_FS_ROOT=/tmp - ./mvnw test -Dtest="behavior.*Test" - diff --git a/bindings/java/src/blocking_operator.rs b/bindings/java/src/blocking_operator.rs index 8de632dc509c..4713e8894f7a 100644 --- a/bindings/java/src/blocking_operator.rs +++ b/bindings/java/src/blocking_operator.rs @@ -20,12 +20,15 @@ use jni::objects::JClass; use jni::objects::JObject; use jni::objects::JString; use jni::sys::jobject; +use jni::sys::jobjectArray; +use jni::sys::jsize; use jni::sys::{jbyteArray, jlong}; use jni::JNIEnv; use opendal::BlockingOperator; use crate::jstring_to_string; +use crate::make_entry; use crate::make_metadata; use crate::Result; @@ -221,3 +224,58 @@ fn intern_rename( Ok(op.rename(&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_removeAll( + mut env: JNIEnv, + _: JClass, + op: *mut BlockingOperator, + path: JString, +) { + intern_remove_all(&mut env, &mut *op, path).unwrap_or_else(|e| { + e.throw(&mut env); + }) +} + +fn intern_remove_all(env: &mut JNIEnv, op: &mut BlockingOperator, path: JString) -> Result<()> { + let path = jstring_to_string(env, &path)?; + + Ok(op.remove_all(&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_list( + mut env: JNIEnv, + _: JClass, + op: *mut BlockingOperator, + path: JString, +) -> jobjectArray { + intern_list(&mut env, &mut *op, path).unwrap_or_else(|e| { + e.throw(&mut env); + JObject::default().into_raw() + }) +} + +fn intern_list(env: &mut JNIEnv, op: &mut BlockingOperator, path: JString) -> Result { + let path = jstring_to_string(env, &path)?; + let obs = op.list(&path)?; + + let jarray = env.new_object_array( + obs.len() as jsize, + "org/apache/opendal/Entry", + JObject::null(), + )?; + + for (idx, entry) in obs.iter().enumerate() { + let entry = make_entry(env, entry.to_owned())?; + env.set_object_array_element(&jarray, idx as jsize, entry)?; + } + + Ok(jarray.into_raw()) +} diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 4a309701316e..a70642ae8d57 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -32,8 +32,10 @@ use jni::JavaVM; use once_cell::sync::OnceCell; use opendal::raw::PresignedRequest; use opendal::Capability; +use opendal::Entry; use opendal::EntryMode; use opendal::Metadata; +use opendal::Metakey; use opendal::OperatorInfo; use tokio::runtime::Builder; use tokio::runtime::Runtime; @@ -233,23 +235,60 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result 2, }; - let last_modified = metadata.last_modified().map_or_else( - || Ok::, Error>(JObject::null()), - |v| { - Ok(env.new_object( - "java/util/Date", - "(J)V", - &[JValue::Long(v.timestamp_millis())], - )?) - }, - )?; + let metakey = metadata.metakey(); + + let contains_metakey = |k| metakey.contains(k) || metakey.contains(Metakey::Complete); + + let last_modified = if contains_metakey(Metakey::LastModified) { + metadata.last_modified().map_or_else( + || Ok::, Error>(JObject::null()), + |v| { + Ok(env.new_object( + "java/util/Date", + "(J)V", + &[JValue::Long(v.timestamp_millis())], + )?) + }, + )? + } else { + JObject::null() + }; - let cache_control = string_to_jstring(env, metadata.cache_control())?; - let content_disposition = string_to_jstring(env, metadata.content_disposition())?; - let content_md5 = string_to_jstring(env, metadata.content_md5())?; - let content_type = string_to_jstring(env, metadata.content_type())?; - let etag = string_to_jstring(env, metadata.etag())?; - let version = string_to_jstring(env, metadata.version())?; + let cache_control = if contains_metakey(Metakey::CacheControl) { + string_to_jstring(env, metadata.cache_control())? + } else { + JObject::null() + }; + let content_disposition = if contains_metakey(Metakey::ContentDisposition) { + string_to_jstring(env, metadata.content_disposition())? + } else { + JObject::null() + }; + let content_md5 = if contains_metakey(Metakey::ContentMd5) { + string_to_jstring(env, metadata.content_md5())? + } else { + JObject::null() + }; + let content_type = if contains_metakey(Metakey::ContentType) { + string_to_jstring(env, metadata.content_type())? + } else { + JObject::null() + }; + let etag = if contains_metakey(Metakey::Etag) { + string_to_jstring(env, metadata.etag())? + } else { + JObject::null() + }; + let version = if contains_metakey(Metakey::Version) { + string_to_jstring(env, metadata.version())? + } else { + JObject::null() + }; + let content_length = if contains_metakey(Metakey::ContentLength) { + metadata.content_length() as jlong + } else { + -1 + }; let result = env .new_object( @@ -257,7 +296,7 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result(env: &mut JNIEnv<'a>, metadata: Metadata) -> Result(env: &mut JNIEnv<'a>, entry: Entry) -> Result> { + let path = env.new_string(entry.path())?; + let metadata = make_metadata(env, entry.metadata().to_owned())?; + + Ok(env.new_object( + "org/apache/opendal/Entry", + "(Ljava/lang/String;Lorg/apache/opendal/Metadata;)V", + &[JValue::Object(&path), JValue::Object(&metadata)], + )?) +} + fn string_to_jstring<'a>(env: &mut JNIEnv<'a>, s: Option<&str>) -> Result> { s.map_or_else( || Ok(JObject::null()), 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 8a3dab8edd7c..276043a2871d 100644 --- a/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java +++ b/bindings/java/src/main/java/org/apache/opendal/BlockingOperator.java @@ -20,6 +20,8 @@ package org.apache.opendal; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -92,6 +94,14 @@ public void rename(String sourcePath, String targetPath) { rename(nativeHandle, sourcePath, targetPath); } + public void removeAll(String path) { + removeAll(nativeHandle, path); + } + + public List list(String path) { + return Arrays.asList(list(nativeHandle, path)); + } + @Override protected native void disposeInternal(long handle); @@ -110,4 +120,8 @@ public void rename(String sourcePath, String targetPath) { private static native long copy(long nativeHandle, String sourcePath, String targetPath); private static native long rename(long nativeHandle, String sourcePath, String targetPath); + + private static native void removeAll(long nativeHandle, String path); + + private static native Entry[] list(long nativeHandle, String path); } diff --git a/bindings/java/src/main/java/org/apache/opendal/Entry.java b/bindings/java/src/main/java/org/apache/opendal/Entry.java new file mode 100644 index 000000000000..d8518776cbd8 --- /dev/null +++ b/bindings/java/src/main/java/org/apache/opendal/Entry.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.opendal; + +import lombok.Data; + +@Data +public class Entry { + public final String path; + public final Metadata metadata; + + public Entry(String path, Metadata metadata) { + this.path = path; + this.metadata = metadata; + } +} diff --git a/bindings/java/src/main/java/org/apache/opendal/Metadata.java b/bindings/java/src/main/java/org/apache/opendal/Metadata.java index 6d886a7802a0..0a35735d15f4 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Metadata.java +++ b/bindings/java/src/main/java/org/apache/opendal/Metadata.java @@ -27,14 +27,60 @@ */ @Data public class Metadata { + /** + * Mode of the entry. + */ public final EntryMode mode; + /** + * Content Length of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be -1. + */ public final long contentLength; + /** + * Content-Disposition of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String contentDisposition; + /** + * Content MD5 of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String contentMd5; + /** + * Content Type of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String contentType; + /** + * Cache Control of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String cacheControl; + /** + * Etag of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String etag; + /** + * Last Modified of the entry. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final Date lastModified; + /** + * Version of the entry. + * Version is a string that can be used to identify the version of this entry. + * This field may come out from the version control system, like object + * versioning in AWS S3. + * + * Note: For now, this value is only available when calling on result of `stat`, otherwise it will be null. + */ public final String version; public Metadata( @@ -67,11 +113,17 @@ public boolean isDir() { } public enum EntryMode { - /// FILE means the path has data to read. + /** + * FILE means the path has data to read. + */ FILE, - /// DIR means the path can be listed. + /** + * DIR means the path can be listed. + */ DIR, - /// Unknown means we don't know what we can do on this path. + /** + * Unknown means we don't know what we can do on this path. + */ UNKNOWN; public static EntryMode of(int mode) { 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 6aa957a5dc62..79f9799e5be1 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Operator.java +++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java @@ -21,7 +21,10 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -205,6 +208,17 @@ public CompletableFuture rename(String sourcePath, String targetPath) { return AsyncRegistry.take(requestId); } + public CompletableFuture removeAll(String path) { + final long requestId = removeAll(nativeHandle, path); + return AsyncRegistry.take(requestId); + } + + public CompletableFuture> list(String path) { + final long requestid = list(nativeHandle, path); + final CompletableFuture result = AsyncRegistry.take(requestid); + return Objects.requireNonNull(result).thenApplyAsync(Arrays::asList); + } + @Override protected native void disposeInternal(long handle); @@ -237,4 +251,8 @@ public CompletableFuture rename(String sourcePath, String targetPath) { private static native long copy(long nativeHandle, String sourcePath, String targetPath); private static native long rename(long nativeHandle, String sourcePath, String targetPath); + + private static native long removeAll(long nativeHandle, String path); + + private static native long list(long nativeHandle, String path); } diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs index 9f5437e579d2..9b9b79982ce8 100644 --- a/bindings/java/src/operator.rs +++ b/bindings/java/src/operator.rs @@ -24,6 +24,7 @@ use jni::objects::JObject; use jni::objects::JString; use jni::objects::JValue; use jni::objects::JValueOwned; +use jni::sys::jsize; use jni::sys::{jlong, jobject}; use jni::JNIEnv; use opendal::layers::BlockingLayer; @@ -35,6 +36,7 @@ use crate::get_current_env; use crate::get_global_runtime; use crate::jmap_to_hashmap; use crate::jstring_to_string; +use crate::make_entry; use crate::make_metadata; use crate::make_operator_info; use crate::make_presigned_request; @@ -428,6 +430,88 @@ async fn do_rename(op: &mut Operator, source_path: String, target_path: String) Ok(op.rename(&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_removeAll( + mut env: JNIEnv, + _: JClass, + op: *mut Operator, + path: JString, +) -> jlong { + intern_remove_all(&mut env, op, path).unwrap_or_else(|e| { + e.throw(&mut env); + 0 + }) +} + +fn intern_remove_all(env: &mut JNIEnv, op: *mut Operator, path: JString) -> Result { + let op = unsafe { &mut *op }; + let id = request_id(env)?; + + let path = jstring_to_string(env, &path)?; + + unsafe { get_global_runtime() }.spawn(async move { + let result = do_remove_all(op, path).await; + complete_future(id, result.map(|_| JValueOwned::Void)) + }); + + Ok(id) +} + +async fn do_remove_all(op: &mut Operator, path: String) -> Result<()> { + Ok(op.remove_all(&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_list( + mut env: JNIEnv, + _: JClass, + op: *mut Operator, + path: JString, +) -> jlong { + intern_list(&mut env, op, path).unwrap_or_else(|e| { + e.throw(&mut env); + 0 + }) +} + +fn intern_list(env: &mut JNIEnv, op: *mut Operator, path: JString) -> Result { + let op = unsafe { &mut *op }; + let id = request_id(env)?; + + let path = jstring_to_string(env, &path)?; + + unsafe { get_global_runtime() }.spawn(async move { + let result = do_list(op, path).await; + complete_future(id, result.map(JValueOwned::Object)) + }); + + Ok(id) +} + +async fn do_list<'local>(op: &mut Operator, path: String) -> Result> { + let obs = op.list(&path).await?; + + let mut env = unsafe { get_current_env() }; + let jarray = env.new_object_array( + obs.len() as jsize, + "org/apache/opendal/Entry", + JObject::null(), + )?; + + for (idx, entry) in obs.iter().enumerate() { + let entry = make_entry(&mut env, entry.to_owned())?; + env.set_object_array_element(&jarray, idx as jsize, entry)?; + } + + Ok(jarray.into()) +} + /// # 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/AsyncListTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncListTest.java new file mode 100644 index 000000000000..bf6e5b0f43b0 --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncListTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.opendal.test.behavior; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.opendal.Capability; +import org.apache.opendal.Entry; +import org.apache.opendal.Metadata; +import org.apache.opendal.OpenDALException; +import org.apache.opendal.test.condition.OpenDALExceptionCondition; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AsyncListTest extends BehaviorTestBase { + + @BeforeAll + public void precondition() { + final Capability capability = op().info.fullCapability; + assumeTrue(capability.read && capability.write && capability.list); + } + + /** + * List dir should return newly created file. + */ + @Test + public void testListDir() { + final String parent = UUID.randomUUID().toString(); + final String path = String.format("%s/%s", parent, UUID.randomUUID()); + final byte[] content = generateBytes(); + + op().write(path, content).join(); + + final List entries = op().list(parent + "/").join(); + boolean found = false; + for (Entry entry : entries) { + if (entry.getPath().equals(path)) { + Metadata meta = op().stat(path).join(); + assertTrue(meta.isFile()); + assertThat(meta.getContentLength()).isEqualTo(content.length); + + found = true; + } + } + assertTrue(found); + op().delete(path).join(); + } + + /** + * listing a directory, which contains more objects than a single page can take. + */ + @Test + public void testListRichDir() { + final String parent = "test_list_rich_dir"; + op().createDir(parent + "/").join(); + final List expected = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + expected.add(String.format("%s/file-%d", parent, i)); + } + + for (String path : expected) { + op().write(path, parent).join(); + } + + final List entries = op().list(parent + "/").join(); + final List actual = + entries.stream().map(Entry::getPath).sorted().collect(Collectors.toList()); + + Collections.sort(expected); + assertThat(actual).isEqualTo(expected); + op().removeAll(parent + "/").join(); + } + + /** + * List empty dir should return nothing. + */ + @Test + public void testListEmptyDir() { + final String dir = String.format("%s/", UUID.randomUUID()); + op().createDir(dir).join(); + + final List entries = op().list(dir).join(); + assertThat(entries).isEmpty(); + + op().delete(dir).join(); + } + + /** + * List non exist dir should return nothing. + */ + @Test + public void testListNotExistDir() { + final String dir = String.format("%s/", UUID.randomUUID()); + + final List entries = op().list(dir).join(); + assertThat(entries).isEmpty(); + } + + /** + * List dir should return correct sub dir. + */ + @Test + public void testListSubDir() { + final String path = String.format("%s/", UUID.randomUUID()); + op().createDir(path).join(); + + final List entries = op().list("/").join(); + boolean found = false; + for (Entry entry : entries) { + if (entry.getPath().equals(path)) { + Metadata metadata = entry.getMetadata(); + assertTrue(metadata.isDir()); + found = true; + } + } + assertTrue(found); + + op().delete(path).join(); + } + + /** + * List dir should also to list nested dir. + */ + @Test + public void testListNestedDir() { + final String dir = String.format("%s/%s/", UUID.randomUUID(), UUID.randomUUID()); + final String fileName = UUID.randomUUID().toString(); + final String filePath = String.format("%s/%s", dir, fileName); + final String dirName = String.format("%s/", UUID.randomUUID()); + final String dirPath = String.format("%s/%s", dir, dirName); + final String content = "test_list_nested_dir"; + + op().createDir(dir).join(); + op().write(filePath, content).join(); + op().createDir(dirPath).join(); + + final List entries = op().list(dir).join(); + assertThat(entries).hasSize(2); + + for (Entry entry : entries) { + // check file + if (entry.getPath().equals(filePath)) { + Metadata metadata = entry.getMetadata(); + assertTrue(metadata.isFile()); + assertThat(metadata.getContentLength()).isEqualTo(content.length()); + // check dir + } else if (entry.getPath().equals(dirPath)) { + Metadata metadata = entry.getMetadata(); + assertTrue(metadata.isDir()); + } + } + + op().removeAll(dir).join(); + } + + /** + * Remove all should remove all in this path. + */ + @Test + public void testRemoveAll() { + final String parent = UUID.randomUUID().toString(); + final String[] expected = new String[] { + "x/", "x/y", "x/x/", "x/x/y", "x/x/x/", "x/x/x/y", "x/x/x/x/", + }; + for (String path : expected) { + if (path.endsWith("/")) { + op().createDir(String.format("%s/%s", parent, path)).join(); + } else { + op().write(String.format("%s/%s", parent, path), "test_scan").join(); + } + } + + op().removeAll(parent + "/x/").join(); + + for (String path : expected) { + if (path.endsWith("/")) { + continue; + } + assertThatThrownBy(() -> + op().stat(String.format("%s/%s", parent, path)).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + } + + op().removeAll(parent + "/").join(); + } +} diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingCopyTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingCopyTest.java index bba304e7a415..9d60b194773b 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingCopyTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingCopyTest.java @@ -83,7 +83,7 @@ public void testBlockingCopySourceDir() { blockingOp().createDir(sourcePath); assertThatThrownBy(() -> blockingOp().copy(sourcePath, targetPath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsADirectory)); blockingOp().delete(sourcePath); } @@ -102,8 +102,8 @@ public void testBlockingCopyTargetDir() { blockingOp().createDir(targetPath); - assertThatThrownBy(() -> op().copy(sourcePath, targetPath).join()) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + assertThatThrownBy(() -> blockingOp().copy(sourcePath, targetPath)) + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsADirectory)); blockingOp().delete(sourcePath); blockingOp().delete(targetPath); @@ -120,7 +120,7 @@ public void testBlockingCopySelf() { blockingOp().write(sourcePath, sourceContent); assertThatThrownBy(() -> blockingOp().copy(sourcePath, sourcePath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsSameFile)); blockingOp().delete(sourcePath); } diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingListTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingListTest.java new file mode 100644 index 000000000000..feb2913297bd --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingListTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.opendal.test.behavior; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.util.List; +import java.util.UUID; +import org.apache.opendal.Capability; +import org.apache.opendal.Entry; +import org.apache.opendal.Metadata; +import org.apache.opendal.OpenDALException; +import org.apache.opendal.test.condition.OpenDALExceptionCondition; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class BlockingListTest extends BehaviorTestBase { + @BeforeAll + public void precondition() { + final Capability capability = blockingOp().info.fullCapability; + assumeTrue(capability.read && capability.write && capability.copy && capability.blocking && capability.list); + } + + @Test + public void testBlockingListDir() { + final String parent = UUID.randomUUID().toString(); + final String path = String.format("%s/%s", parent, UUID.randomUUID()); + final byte[] content = generateBytes(); + + blockingOp().write(path, content); + + final List list = blockingOp().list(parent + "/"); + boolean found = false; + for (Entry entry : list) { + if (entry.getPath().equals(path)) { + Metadata meta = blockingOp().stat(path); + assertTrue(meta.isFile()); + assertThat(meta.getContentLength()).isEqualTo(content.length); + + found = true; + } + } + assertTrue(found); + + blockingOp().delete(path); + } + + @Test + public void testBlockingListNonExistDir() { + final String dir = String.format("%s/", UUID.randomUUID()); + + final List list = blockingOp().list(dir); + assertTrue(list.isEmpty()); + } + + /** + * Remove all should remove all in this path. + */ + @Test + public void testBlockingRemoveAll() { + final String parent = UUID.randomUUID().toString(); + final String[] expected = new String[] { + "x/", "x/y", "x/x/", "x/x/y", "x/x/x/", "x/x/x/y", "x/x/x/x/", + }; + for (String path : expected) { + if (path.endsWith("/")) { + blockingOp().createDir(String.format("%s/%s", parent, path)); + } else { + blockingOp().write(String.format("%s/%s", parent, path), "test_scan"); + } + } + + blockingOp().removeAll(parent + "/x/"); + + for (String path : expected) { + if (path.endsWith("/")) { + continue; + } + assertThatThrownBy(() -> blockingOp().stat(String.format("%s/%s", parent, path))) + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); + } + + blockingOp().removeAll(parent + "/"); + } +} diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingRenameTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingRenameTest.java index 0993f15ff3e9..925c95532e8a 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingRenameTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingRenameTest.java @@ -54,7 +54,7 @@ public void testBlockingRenameFile() { blockingOp().rename(sourcePath, targetPath); assertThatThrownBy(() -> blockingOp().stat(sourcePath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); assertThat(blockingOp().stat(targetPath).getContentLength()).isEqualTo(sourceContent.length); @@ -71,7 +71,7 @@ public void testBlockingRenameNonExistingSource() { final String targetPath = UUID.randomUUID().toString(); assertThatThrownBy(() -> blockingOp().rename(sourcePath, targetPath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); } /** @@ -85,7 +85,7 @@ public void testBlockingRenameSourceDir() { blockingOp().createDir(sourcePath); assertThatThrownBy(() -> blockingOp().rename(sourcePath, targetPath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsADirectory)); } /** @@ -103,7 +103,7 @@ public void testBlockingRenameTargetDir() { blockingOp().createDir(targetPath); assertThatThrownBy(() -> blockingOp().rename(sourcePath, targetPath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsADirectory)); blockingOp().delete(sourcePath); blockingOp().delete(targetPath); @@ -120,7 +120,7 @@ public void testBlockingRenameSelf() { blockingOp().write(sourcePath, sourceContent); assertThatThrownBy(() -> blockingOp().rename(sourcePath, sourcePath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsSameFile)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsSameFile)); blockingOp().delete(sourcePath); } @@ -140,7 +140,7 @@ public void testBlockingRenameNested() { blockingOp().rename(sourcePath, targetPath); assertThatThrownBy(() -> blockingOp().stat(sourcePath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); assertThat(blockingOp().read(targetPath)).isEqualTo(sourceContent); @@ -168,7 +168,7 @@ public void testBlockingRenameOverwrite() { blockingOp().rename(sourcePath, targetPath); assertThatThrownBy(() -> blockingOp().stat(sourcePath)) - .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + .is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.NotFound)); assertThat(blockingOp().read(targetPath)).isEqualTo(sourceContent);