This is an automated email from the ASF dual-hosted git repository.
xuanwo 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 066a80300 feat(bindings/java): support presign ops (#3069)
066a80300 is described below
commit 066a803009c0dc854f7eac885d8b6a7bb2d60f78
Author: tison <[email protected]>
AuthorDate: Thu Sep 14 23:02:01 2023 +0800
feat(bindings/java): support presign ops (#3069)
* feat: hashmap_to_jmap
Signed-off-by: tison <[email protected]>
* feat: make_presigned_request
Signed-off-by: tison <[email protected]>
* feat: bind presign methods
Signed-off-by: tison <[email protected]>
* implement behavior tests
Signed-off-by: tison <[email protected]>
* tidy tests
Signed-off-by: tison <[email protected]>
* better utilities
Signed-off-by: tison <[email protected]>
---------
Signed-off-by: tison <[email protected]>
---
bindings/java/README.md | 13 +-
bindings/java/src/error.rs | 18 +--
bindings/java/src/lib.rs | 40 +++++-
.../src/main/java/org/apache/opendal/Operator.java | 22 ++++
.../java/org/apache/opendal/PresignedRequest.java | 41 ++++++
bindings/java/src/operator.rs | 145 ++++++++++++++++++++-
.../java/org/apache/opendal/AsyncStepsTest.java | 13 +-
.../test/java/org/apache/opendal/OperatorTest.java | 14 +-
.../java/org/apache/opendal/RedisServiceTest.java | 14 +-
.../condition/OpenDALExceptionCondition.java | 70 ++++++++++
10 files changed, 355 insertions(+), 35 deletions(-)
diff --git a/bindings/java/README.md b/bindings/java/README.md
index 3a2edacfe..6523b00d4 100644
--- a/bindings/java/README.md
+++ b/bindings/java/README.md
@@ -59,7 +59,7 @@ This project provides OpenDAL Java bindings with artifact
name `opendal-java`. I
You can use Maven to build both Rust dynamic lib and JAR files with one
command now:
```shell
-mvn clean package -DskipTests=true
+./mvnw clean package -DskipTests=true
```
## Run tests
@@ -69,13 +69,20 @@ Currently, all tests are written in Java. It contains the
Cucumber feature tests
You can run tests with the following command:
```shell
-mvn clean verify
+./mvnw clean verify -Dcargo-build.features=services-redis
```
+> **Note:**
+>
+> The `-Dcargo-build.features=services-redis` argument is a temporary
workaround. See also:
+>
+> * https://github.com/apache/incubator-opendal/pull/3060
+> * https://github.com/apache/incubator-opendal/issues/3066
+
Additionally, this project uses
[spotless](https://github.com/diffplug/spotless) for code formatting so that
all developers share a consistent code style without bikeshedding on it.
You can apply the code style with the following command::
```shell
-mvn spotless:apply
+./mvnw spotless:apply
```
diff --git a/bindings/java/src/error.rs b/bindings/java/src/error.rs
index 8a99c4f49..2d13d6754 100644
--- a/bindings/java/src/error.rs
+++ b/bindings/java/src/error.rs
@@ -29,6 +29,12 @@ pub(crate) struct Error {
}
impl Error {
+ pub(crate) fn unexpected(err: impl Into<anyhow::Error> + Display) -> Error
{
+ Error {
+ inner: opendal::Error::new(ErrorKind::Unexpected,
&err.to_string()).set_source(err),
+ }
+ }
+
pub(crate) fn throw(&self, env: &mut JNIEnv) {
if let Err(err) = self.do_throw(env) {
match err {
@@ -84,25 +90,19 @@ impl From<opendal::Error> for Error {
impl From<jni::errors::Error> for Error {
fn from(error: jni::errors::Error) -> Self {
- Self {
- inner: opendal::Error::new(ErrorKind::Unexpected,
&error.to_string()).set_source(error),
- }
+ Error::unexpected(error)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(error: std::str::Utf8Error) -> Self {
- Self {
- inner: opendal::Error::new(ErrorKind::Unexpected,
&error.to_string()).set_source(error),
- }
+ Error::unexpected(error)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(error: std::string::FromUtf8Error) -> Self {
- Self {
- inner: opendal::Error::new(ErrorKind::Unexpected,
&error.to_string()).set_source(error),
- }
+ Error::unexpected(error)
}
}
diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs
index f838cff56..e1703b527 100644
--- a/bindings/java/src/lib.rs
+++ b/bindings/java/src/lib.rs
@@ -19,14 +19,16 @@ use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::c_void;
-use jni::objects::JMap;
+use crate::error::Error;
use jni::objects::JObject;
use jni::objects::JString;
+use jni::objects::{JMap, JValue};
use jni::sys::jint;
use jni::sys::JNI_VERSION_1_8;
use jni::JNIEnv;
use jni::JavaVM;
use once_cell::sync::OnceCell;
+use opendal::raw::PresignedRequest;
use tokio::runtime::Builder;
use tokio::runtime::Runtime;
@@ -104,3 +106,39 @@ fn jmap_to_hashmap(env: &mut JNIEnv, params: &JObject) ->
Result<HashMap<String,
Ok(result)
}
+
+fn hashmap_to_jmap<'a>(env: &mut JNIEnv<'a>, map: &HashMap<String, String>) ->
Result<JObject<'a>> {
+ let map_object = env.new_object("java/util/HashMap", "()V", &[])?;
+ let jmap = env.get_map(&map_object)?;
+ for (k, v) in map {
+ let key = env.new_string(k)?;
+ let value = env.new_string(v)?;
+ jmap.put(env, &key, &value)?;
+ }
+ Ok(map_object)
+}
+
+fn make_presigned_request<'a>(env: &mut JNIEnv<'a>, req: PresignedRequest) ->
Result<JObject<'a>> {
+ let method = env.new_string(req.method().as_str())?;
+ let uri = env.new_string(req.uri().to_string())?;
+ let headers = {
+ let mut map = HashMap::new();
+ for (k, v) in req.header().iter() {
+ let key = k.to_string();
+ let value = v.to_str().map_err(Error::unexpected)?;
+ map.insert(key, value.to_owned());
+ }
+ map
+ };
+ let headers = hashmap_to_jmap(env, &headers)?;
+ let result = env.new_object(
+ "org/apache/opendal/PresignedRequest",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V",
+ &[
+ JValue::Object(&method),
+ JValue::Object(&uri),
+ JValue::Object(&headers),
+ ],
+ )?;
+ Ok(result)
+}
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 650c6ab2a..4cd6c2c15 100644
--- a/bindings/java/src/main/java/org/apache/opendal/Operator.java
+++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java
@@ -20,6 +20,7 @@
package org.apache.opendal;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -141,6 +142,21 @@ public class Operator extends NativeObject {
return AsyncRegistry.take(requestId);
}
+ public CompletableFuture<Void> presignRead(String path, Duration duration)
{
+ final long requestId = presignRead(nativeHandle, path,
duration.toNanos());
+ return AsyncRegistry.take(requestId);
+ }
+
+ public CompletableFuture<Void> presignWrite(String path, Duration
duration) {
+ final long requestId = presignWrite(nativeHandle, path,
duration.toNanos());
+ return AsyncRegistry.take(requestId);
+ }
+
+ public CompletableFuture<Void> presignStat(String path, Duration duration)
{
+ final long requestId = presignStat(nativeHandle, path,
duration.toNanos());
+ return AsyncRegistry.take(requestId);
+ }
+
public CompletableFuture<Void> delete(String path) {
final long requestId = delete(nativeHandle, path);
return AsyncRegistry.take(requestId);
@@ -160,4 +176,10 @@ public class Operator extends NativeObject {
private static native long delete(long nativeHandle, String path);
private static native long stat(long nativeHandle, String path);
+
+ private static native long presignRead(long nativeHandle, String path,
long duration);
+
+ private static native long presignWrite(long nativeHandle, String path,
long duration);
+
+ private static native long presignStat(long nativeHandle, String path,
long duration);
}
diff --git
a/bindings/java/src/main/java/org/apache/opendal/PresignedRequest.java
b/bindings/java/src/main/java/org/apache/opendal/PresignedRequest.java
new file mode 100644
index 000000000..c14f9faf8
--- /dev/null
+++ b/bindings/java/src/main/java/org/apache/opendal/PresignedRequest.java
@@ -0,0 +1,41 @@
+/*
+ * 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 java.util.Map;
+import lombok.Data;
+
+@Data
+public class PresignedRequest {
+ /**
+ * HTTP method of this request.
+ */
+ private final String method;
+
+ /**
+ * URI of this request.
+ */
+ private final String uri;
+
+ /**
+ * HTTP headers of this request.
+ */
+ private final Map<String, String> headers;
+}
diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs
index b06b09067..f18a3cb0a 100644
--- a/bindings/java/src/operator.rs
+++ b/bindings/java/src/operator.rs
@@ -16,6 +16,7 @@
// under the License.
use std::str::FromStr;
+use std::time::Duration;
use jni::objects::JByteArray;
use jni::objects::JClass;
@@ -26,13 +27,14 @@ use jni::objects::JValueOwned;
use jni::sys::jlong;
use jni::JNIEnv;
use opendal::layers::BlockingLayer;
+use opendal::raw::PresignedRequest;
use opendal::Operator;
use opendal::Scheme;
-use crate::get_current_env;
use crate::get_global_runtime;
use crate::jmap_to_hashmap;
use crate::Result;
+use crate::{get_current_env, make_presigned_request};
#[no_mangle]
pub extern "system" fn Java_org_apache_opendal_Operator_constructor(
@@ -260,6 +262,147 @@ async fn do_delete(op: &mut Operator, path: String) ->
Result<()> {
Ok(op.delete(&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_presignRead(
+ mut env: JNIEnv,
+ _: JClass,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> jlong {
+ intern_presign_read(&mut env, op, path, expire).unwrap_or_else(|e| {
+ e.throw(&mut env);
+ 0
+ })
+}
+
+fn intern_presign_read(
+ env: &mut JNIEnv,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> Result<jlong> {
+ let op = unsafe { &mut *op };
+ let id = request_id(env)?;
+
+ let path = env.get_string(&path)?.to_str()?.to_string();
+ let expire = Duration::from_nanos(expire as u64);
+
+ unsafe { get_global_runtime() }.spawn(async move {
+ let result = do_presign_read(op, path, expire).await;
+ let mut env = unsafe { get_current_env() };
+ let result = result.and_then(|req| make_presigned_request(&mut env,
req));
+ complete_future(id, result.map(JValueOwned::Object))
+ });
+
+ Ok(id)
+}
+
+async fn do_presign_read(
+ op: &mut Operator,
+ path: String,
+ expire: Duration,
+) -> Result<PresignedRequest> {
+ Ok(op.presign_read(&path, expire).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_presignWrite(
+ mut env: JNIEnv,
+ _: JClass,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> jlong {
+ intern_presign_write(&mut env, op, path, expire).unwrap_or_else(|e| {
+ e.throw(&mut env);
+ 0
+ })
+}
+
+fn intern_presign_write(
+ env: &mut JNIEnv,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> Result<jlong> {
+ let op = unsafe { &mut *op };
+ let id = request_id(env)?;
+
+ let path = env.get_string(&path)?.to_str()?.to_string();
+ let expire = Duration::from_nanos(expire as u64);
+
+ unsafe { get_global_runtime() }.spawn(async move {
+ let result = do_presign_write(op, path, expire).await;
+ let mut env = unsafe { get_current_env() };
+ let result = result.and_then(|req| make_presigned_request(&mut env,
req));
+ complete_future(id, result.map(JValueOwned::Object))
+ });
+
+ Ok(id)
+}
+
+async fn do_presign_write(
+ op: &mut Operator,
+ path: String,
+ expire: Duration,
+) -> Result<PresignedRequest> {
+ Ok(op.presign_write(&path, expire).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_presignStat(
+ mut env: JNIEnv,
+ _: JClass,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> jlong {
+ intern_presign_stat(&mut env, op, path, expire).unwrap_or_else(|e| {
+ e.throw(&mut env);
+ 0
+ })
+}
+
+fn intern_presign_stat(
+ env: &mut JNIEnv,
+ op: *mut Operator,
+ path: JString,
+ expire: jlong,
+) -> Result<jlong> {
+ let op = unsafe { &mut *op };
+ let id = request_id(env)?;
+
+ let path = env.get_string(&path)?.to_str()?.to_string();
+ let expire = Duration::from_nanos(expire as u64);
+
+ unsafe { get_global_runtime() }.spawn(async move {
+ let result = do_presign_stat(op, path, expire).await;
+ let mut env = unsafe { get_current_env() };
+ let result = result.and_then(|req| make_presigned_request(&mut env,
req));
+ complete_future(id, result.map(JValueOwned::Object))
+ });
+
+ Ok(id)
+}
+
+async fn do_presign_stat(
+ op: &mut Operator,
+ path: String,
+ expire: Duration,
+) -> Result<PresignedRequest> {
+ Ok(op.presign_stat(&path, expire).await?)
+}
+
fn make_object<'local>(
env: &mut JNIEnv<'local>,
value: JValueOwned<'local>,
diff --git a/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java
b/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java
index 996c79080..5955d6c32 100644
--- a/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java
+++ b/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java
@@ -19,15 +19,18 @@
package org.apache.opendal;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
+import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import lombok.Cleanup;
+import org.apache.opendal.condition.OpenDALExceptionCondition;
public class AsyncStepsTest {
Operator op;
@@ -70,6 +73,14 @@ public class AsyncStepsTest {
@Then("The presign operation should success or raise exception
Unsupported")
public void
the_presign_operation_should_success_or_raise_exception_unsupported() {
- // TODO: please implement me
+ assertThatThrownBy(
+ () -> op.presignStat("test.txt",
Duration.ofSeconds(10)).join())
+
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.Unsupported));
+ assertThatThrownBy(
+ () -> op.presignRead("test.txt",
Duration.ofSeconds(10)).join())
+
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.Unsupported));
+ assertThatThrownBy(() ->
+ op.presignWrite("test.txt",
Duration.ofSeconds(10)).join())
+
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.Unsupported));
}
}
diff --git a/bindings/java/src/test/java/org/apache/opendal/OperatorTest.java
b/bindings/java/src/test/java/org/apache/opendal/OperatorTest.java
index 9e81e535e..bcde1c181 100644
--- a/bindings/java/src/test/java/org/apache/opendal/OperatorTest.java
+++ b/bindings/java/src/test/java/org/apache/opendal/OperatorTest.java
@@ -20,13 +20,14 @@
package org.apache.opendal;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import lombok.Cleanup;
+import org.apache.opendal.condition.OpenDALExceptionCondition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -43,15 +44,8 @@ public class OperatorTest {
op.write("testCreateAndDelete", "Odin").join();
assertThat(op.read("testCreateAndDelete").join()).isEqualTo("Odin");
op.delete("testCreateAndDelete").join();
- op.stat("testCreateAndDelete")
- .handle((r, e) -> {
- assertThat(r).isNull();
-
assertThat(e).isInstanceOf(CompletionException.class).hasCauseInstanceOf(OpenDALException.class);
- OpenDALException.Code code = ((OpenDALException)
e.getCause()).getCode();
- assertThat(code).isEqualTo(OpenDALException.Code.NotFound);
- return null;
- })
- .join();
+ assertThatThrownBy(() -> op.stat("testCreateAndDelete").join())
+
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound));
}
@Test
diff --git
a/bindings/java/src/test/java/org/apache/opendal/RedisServiceTest.java
b/bindings/java/src/test/java/org/apache/opendal/RedisServiceTest.java
index b93a40115..a344f2c93 100644
--- a/bindings/java/src/test/java/org/apache/opendal/RedisServiceTest.java
+++ b/bindings/java/src/test/java/org/apache/opendal/RedisServiceTest.java
@@ -21,10 +21,11 @@ package org.apache.opendal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.CompletionException;
import lombok.Cleanup;
+import org.apache.opendal.condition.OpenDALExceptionCondition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.testcontainers.containers.GenericContainer;
@@ -49,15 +50,8 @@ public class RedisServiceTest {
op.write("testAccessRedisService", "Odin").join();
assertThat(op.read("testAccessRedisService").join()).isEqualTo("Odin");
op.delete("testAccessRedisService").join();
- op.stat("testAccessRedisService")
- .handle((r, e) -> {
- assertThat(r).isNull();
-
assertThat(e).isInstanceOf(CompletionException.class).hasCauseInstanceOf(OpenDALException.class);
- OpenDALException.Code code = ((OpenDALException)
e.getCause()).getCode();
- assertThat(code).isEqualTo(OpenDALException.Code.NotFound);
- return null;
- })
- .join();
+ assertThatThrownBy(() -> op.stat("testAccessRedisService").join())
+
.is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound));
}
@Test
diff --git
a/bindings/java/src/test/java/org/apache/opendal/condition/OpenDALExceptionCondition.java
b/bindings/java/src/test/java/org/apache/opendal/condition/OpenDALExceptionCondition.java
new file mode 100644
index 000000000..4c3afddb8
--- /dev/null
+++
b/bindings/java/src/test/java/org/apache/opendal/condition/OpenDALExceptionCondition.java
@@ -0,0 +1,70 @@
+/*
+ * 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.condition;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
+import org.apache.opendal.OpenDALException;
+import org.assertj.core.api.Condition;
+
+public class OpenDALExceptionCondition extends Condition<Throwable> {
+ private final List<Class<? extends Throwable>> canBeStripped;
+ private final OpenDALException.Code code;
+
+ public static OpenDALExceptionCondition ofAsync(OpenDALException.Code
code) {
+ final List<Class<? extends Throwable>> canBeStripped = new
ArrayList<>();
+ canBeStripped.add(CompletionException.class);
+ canBeStripped.add(ExecutionException.class);
+ return new OpenDALExceptionCondition(code, canBeStripped);
+ }
+
+ public OpenDALExceptionCondition(OpenDALException.Code code, List<Class<?
extends Throwable>> canBeStripped) {
+ as("OpenDALException with code " + code + ", stripping " +
canBeStripped);
+ this.code = code;
+ this.canBeStripped = canBeStripped;
+ }
+
+ private Throwable stripException(Throwable throwable) {
+ while (throwable.getCause() != null) {
+ final Class<?> thisClass = throwable.getClass();
+ final Predicate<Class<? extends Throwable>> predicate = clazz ->
clazz.isAssignableFrom(thisClass);
+ if (this.canBeStripped.stream().noneMatch(predicate)) {
+ break;
+ }
+ throwable = throwable.getCause();
+ }
+ return throwable;
+ }
+
+ @Override
+ public boolean matches(Throwable throwable) {
+ throwable = stripException(throwable);
+
+ if (throwable instanceof OpenDALException) {
+ final OpenDALException exception = (OpenDALException) throwable;
+ return exception.getCode() == code;
+ }
+
+ return false;
+ }
+}