This is an automated email from the ASF dual-hosted git repository.

tison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new c5333ea3e feat(binding/java): add list & remove_all support (#3333)
c5333ea3e is described below

commit c5333ea3e76e94201b5110b99093ff08c87fc076
Author: G-XD <[email protected]>
AuthorDate: Thu Oct 26 00:34:51 2023 +0800

    feat(binding/java): add list & remove_all support (#3333)
    
    * feat(binding/java): add Entry for list
    * feat(binding/java): support list for blocking
    * feat(binding/java): support list & remove_all
    * test(binding/java): add list test
    * feat(binding/java): add OpList args
    * feat(binding/java): support metakey args
    * chore(binding/java): use Set for Metakey args
    * chore(binding/java): use Data for OpList
    * test(binding/java): add test for list
    * refactor(binding/java): update list api
    * ci(binding/java): update root path of fs test
    * refactor(binding/java): refactor Metakey name
    * refactor(binding/java): remove list with args support
    * refactor(binding/java): align the pattern of convert list
    * refactor(binding/java): use Metadata::metakey() api for make entry
    * ci(binding/java): remove behavior tests
    * refactor(binding/java): update code style
---
 .github/workflows/bindings_java.yml                |  12 --
 bindings/java/src/blocking_operator.rs             |  58 ++++++
 bindings/java/src/lib.rs                           |  84 ++++++--
 .../java/org/apache/opendal/BlockingOperator.java  |  14 ++
 .../src/main/java/org/apache/opendal/Entry.java    |  33 ++++
 .../src/main/java/org/apache/opendal/Metadata.java |  58 +++++-
 .../src/main/java/org/apache/opendal/Operator.java |  18 ++
 bindings/java/src/operator.rs                      |  84 ++++++++
 .../opendal/test/behavior/AsyncListTest.java       | 212 +++++++++++++++++++++
 .../opendal/test/behavior/BlockingCopyTest.java    |   8 +-
 .../opendal/test/behavior/BlockingListTest.java    | 106 +++++++++++
 .../opendal/test/behavior/BlockingRenameTest.java  |  14 +-
 12 files changed, 658 insertions(+), 43 deletions(-)

diff --git a/.github/workflows/bindings_java.yml 
b/.github/workflows/bindings_java.yml
index fef27ca45..077f28627 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 8de632dc5..4713e8894 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<jobjectArray> {
+    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 4a3097013..a70642ae8 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<JObject
         EntryMode::Unknown => 2,
     };
 
-    let last_modified = metadata.last_modified().map_or_else(
-        || Ok::<JObject<'_>, 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::<JObject<'_>, 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<JObject
             
"(IJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;)V",
             &[
                 JValue::Int(mode as jint),
-                JValue::Long(metadata.content_length() as jlong),
+                JValue::Long(content_length),
                 JValue::Object(&content_disposition),
                 JValue::Object(&content_md5),
                 JValue::Object(&content_type),
@@ -270,6 +309,17 @@ fn make_metadata<'a>(env: &mut JNIEnv<'a>, metadata: 
Metadata) -> Result<JObject
     Ok(result)
 }
 
+fn make_entry<'a>(env: &mut JNIEnv<'a>, entry: Entry) -> Result<JObject<'a>> {
+    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<JObject<'a>> {
     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 8a3dab8ed..276043a28 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 class BlockingOperator extends NativeObject {
         rename(nativeHandle, sourcePath, targetPath);
     }
 
+    public void removeAll(String path) {
+        removeAll(nativeHandle, path);
+    }
+
+    public List<Entry> list(String path) {
+        return Arrays.asList(list(nativeHandle, path));
+    }
+
     @Override
     protected native void disposeInternal(long handle);
 
@@ -110,4 +120,8 @@ public class BlockingOperator extends NativeObject {
     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 000000000..d8518776c
--- /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 6d886a780..0a35735d1 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 @@ import lombok.Data;
  */
 @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 class Metadata {
     }
 
     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 6aa957a5d..79f9799e5 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 @@ package org.apache.opendal;
 
 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 class Operator extends NativeObject {
         return AsyncRegistry.take(requestId);
     }
 
+    public CompletableFuture<Void> removeAll(String path) {
+        final long requestId = removeAll(nativeHandle, path);
+        return AsyncRegistry.take(requestId);
+    }
+
+    public CompletableFuture<List<Entry>> list(String path) {
+        final long requestid = list(nativeHandle, path);
+        final CompletableFuture<Entry[]> result = 
AsyncRegistry.take(requestid);
+        return Objects.requireNonNull(result).thenApplyAsync(Arrays::asList);
+    }
+
     @Override
     protected native void disposeInternal(long handle);
 
@@ -237,4 +251,8 @@ public class Operator extends NativeObject {
     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 9f5437e57..9b9b79982 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<jlong> {
+    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<jlong> {
+    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<JObject<'local>> {
+    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 000000000..bf6e5b0f4
--- /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<Entry> 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<String> 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<Entry> entries = op().list(parent + "/").join();
+        final List<String> 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<Entry> 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<Entry> 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<Entry> 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<Entry> 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 bba304e7a..9d60b1947 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 @@ class BlockingCopyTest extends BehaviorTestBase {
         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 @@ class BlockingCopyTest extends BehaviorTestBase {
 
         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 @@ class BlockingCopyTest extends BehaviorTestBase {
         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 000000000..feb291329
--- /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<Entry> 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<Entry> 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 0993f15ff..925c95532 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 @@ class BlockingRenameTest extends BehaviorTestBase {
         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 @@ class BlockingRenameTest extends BehaviorTestBase {
         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 @@ class BlockingRenameTest extends BehaviorTestBase {
         blockingOp().createDir(sourcePath);
 
         assertThatThrownBy(() -> blockingOp().rename(sourcePath, targetPath))
-                
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.IsADirectory));
+                
.is(OpenDALExceptionCondition.ofSync(OpenDALException.Code.IsADirectory));
     }
 
     /**
@@ -103,7 +103,7 @@ class BlockingRenameTest extends BehaviorTestBase {
         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 @@ class BlockingRenameTest extends BehaviorTestBase {
         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 @@ class BlockingRenameTest extends BehaviorTestBase {
         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 @@ class BlockingRenameTest extends BehaviorTestBase {
         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);
 


Reply via email to