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);