diff --git a/bindings/java/pom.xml b/bindings/java/pom.xml index 5b542b5ce737..abe86ad936b0 100644 --- a/bindings/java/pom.xml +++ b/bindings/java/pom.xml @@ -132,25 +132,6 @@ maven-surefire-plugin 3.0.0 - - com.coderplus.maven.plugins - copy-rename-maven-plugin - 1.0 - - - copy-file - generate-sources - - copy - - - ../tests/features/binding.feature - target/test-classes/features/binding.feature - - - - - - \ No newline at end of file + diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 1edd5b261ad5..fc8167ba969e 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -21,10 +21,10 @@ use std::ffi::c_void; use std::str::FromStr; use std::sync::Arc; -use jni::objects::JClass; use jni::objects::JMap; use jni::objects::JObject; use jni::objects::JString; +use jni::objects::{JClass, JThrowable, JValue}; use jni::sys::{jboolean, jint}; use jni::sys::{jlong, JNI_VERSION_1_8}; use jni::{JNIEnv, JavaVM}; @@ -120,7 +120,7 @@ pub extern "system" fn Java_org_apache_opendal_Operator_getOperator( /// /// This function should not be called before the Operator are ready. #[no_mangle] -pub unsafe extern "system" fn Java_org_apache_opendal_Operator_asyncWrite( +pub unsafe extern "system" fn Java_org_apache_opendal_Operator_writeAsync( mut env: JNIEnv, _class: JClass, ptr: *mut Operator, @@ -221,6 +221,31 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Operator_read<'local>( output } +fn convert_error_into_java_exception<'local>( + env: &mut JNIEnv<'local>, + error: opendal::Error, +) -> Result, jni::errors::Error> { + let error_code_class = env.find_class("org/apache/opendal/exception/OpenDALErrorCode")?; + let error_code_string = env.new_string(error.kind().into_static())?; + let error_code = env.call_static_method( + error_code_class, + "parse", + "(Ljava/lang/String;)Lorg/apache/opendal/exception/OpenDALErrorCode;", + &[JValue::Object(error_code_string.as_ref())], + )?; + + let exception_class = env.find_class("org/apache/opendal/exception/OpenDALException")?; + let exception = env.new_object( + exception_class, + "(Lorg/apache/opendal/exception/OpenDALErrorCode;Ljava/lang/String;)V", + &[ + JValue::Object(error_code.l()?.as_ref()), + JValue::Object(env.new_string(error.to_string())?.as_ref()), + ], + )?; + Ok(JThrowable::from(exception)) +} + /// # Safety /// /// This function should not be called before the Operator are ready. @@ -236,8 +261,13 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Operator_stat( .get_string(&file) .expect("Couldn't get java string!") .into(); - let metadata = op.stat(&file).unwrap(); - Box::into_raw(Box::new(metadata)) as jlong + let result = op.stat(&file); + if let Err(error) = result { + let exception = convert_error_into_java_exception(&mut env, error).unwrap(); + env.throw(exception).unwrap(); + return 0 as jlong; + } + Box::into_raw(Box::new(result.unwrap())) as jlong } /// # Safety @@ -270,7 +300,7 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Metadata_getContentLength( /// /// This function should not be called before the Stat are ready. #[no_mangle] -pub unsafe extern "system" fn Java_org_apache_opendal_Metadata_freeStat( +pub unsafe extern "system" fn Java_org_apache_opendal_Metadata_freeMetadata( mut _env: JNIEnv, _class: JClass, ptr: *mut opendal::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 281215036295..809ba7e73948 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Metadata.java +++ b/bindings/java/src/main/java/org/apache/opendal/Metadata.java @@ -19,17 +19,7 @@ package org.apache.opendal; -public class Metadata { - - long ptr; - - private native void freeStat(long statPtr); - - private native boolean isFile(long statPtr); - - private native long getContentLength(long statPtr); - - +public class Metadata extends OpenDALObject { public Metadata(long ptr) { this.ptr = ptr; } @@ -38,12 +28,18 @@ public boolean isFile() { return isFile(this.ptr); } - @Override - protected void finalize() { - freeStat(this.ptr); - } - public long getContentLength() { return getContentLength(this.ptr); } + + @Override + public void close() { + freeMetadata(this.ptr); + } + + private native void freeMetadata(long statPtr); + + private native boolean isFile(long statPtr); + + private native long getContentLength(long statPtr); } diff --git a/bindings/java/src/main/java/org/apache/opendal/OpenDALObject.java b/bindings/java/src/main/java/org/apache/opendal/OpenDALObject.java new file mode 100644 index 000000000000..8a60cf9a9f0a --- /dev/null +++ b/bindings/java/src/main/java/org/apache/opendal/OpenDALObject.java @@ -0,0 +1,37 @@ +/* + * 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 io.questdb.jar.jni.JarJniLoader; + +public abstract class OpenDALObject implements AutoCloseable { + private static final String ORG_APACHE_OPENDAL_RUST_LIBS = "/org/apache/opendal/rust/libs"; + + private static final String OPENDAL_JAVA = "opendal_java"; + + static { + JarJniLoader.loadLib( + Operator.class, + ORG_APACHE_OPENDAL_RUST_LIBS, + OPENDAL_JAVA); + } + + long ptr; +} 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 cbd7f4a2e34e..e436ed753871 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Operator.java +++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java @@ -20,50 +20,21 @@ package org.apache.opendal; -import io.questdb.jar.jni.JarJniLoader; import java.util.Map; import java.util.concurrent.CompletableFuture; -public class Operator { - - long ptr; - +public class Operator extends OpenDALObject { public Operator(String schema, Map params) { this.ptr = getOperator(schema, params); } - public static final String ORG_APACHE_OPENDAL_RUST_LIBS = "/org/apache/opendal/rust/libs"; - - public static final String OPENDAL_JAVA = "opendal_java"; - - static { - JarJniLoader.loadLib( - Operator.class, - ORG_APACHE_OPENDAL_RUST_LIBS, - OPENDAL_JAVA); - } - - private native long getOperator(String type, Map params); - - protected native void freeOperator(long ptr); - - private native void write(long ptr, String fileName, String content); - - private native void asyncWrite(long ptr, String fileName, String content, CompletableFuture future); - - private native String read(long ptr, String fileName); - - private native void delete(long ptr, String fileName); - - private native long stat(long ptr, String file); - public void write(String fileName, String content) { write(this.ptr, fileName, content); } - public CompletableFuture asyncWrite(String fileName, String content) { + public CompletableFuture writeAsync(String fileName, String content) { CompletableFuture future = new CompletableFuture<>(); - asyncWrite(this.ptr, fileName, content, future); + writeAsync(this.ptr, fileName, content, future); return future; } @@ -81,8 +52,21 @@ public Metadata stat(String fileName) { } @Override - protected void finalize() throws Throwable { - super.finalize(); + public void close() { this.freeOperator(ptr); } + + private native long getOperator(String type, Map params); + + protected native void freeOperator(long ptr); + + private native void write(long ptr, String fileName, String content); + + private native void writeAsync(long ptr, String fileName, String content, CompletableFuture future); + + private native String read(long ptr, String fileName); + + private native void delete(long ptr, String fileName); + + private native long stat(long ptr, String file); } diff --git a/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALErrorCode.java b/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALErrorCode.java new file mode 100644 index 000000000000..1806b908e4ee --- /dev/null +++ b/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALErrorCode.java @@ -0,0 +1,67 @@ +/* + * 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.exception; + +public enum OpenDALErrorCode { + UNEXPECTED, + UNSUPPORTED, + CONFIG_INVALID, + NOT_FOUND, + PERMISSION_DENIED, + IS_A_DIRECTORY, + NOT_A_DIRECTORY, + ALREADY_EXISTS, + RATE_LIMITED, + IS_SAME_FILE, + CONDITION_NOT_MATCH, + CONTENT_TRUNCATED, + CONTENT_INCOMPLETE; + + public static OpenDALErrorCode parse(String errorCode) { + switch (errorCode) { + case "Unsupported": + return UNSUPPORTED; + case "ConfigInvalid": + return CONFIG_INVALID; + case "NotFound": + return NOT_FOUND; + case "PermissionDenied": + return PERMISSION_DENIED; + case "IsADirectory": + return IS_A_DIRECTORY; + case "NotADirectory": + return NOT_A_DIRECTORY; + case "AlreadyExists": + return ALREADY_EXISTS; + case "RateLimited": + return RATE_LIMITED; + case "IsSameFile": + return IS_SAME_FILE; + case "ConditionNotMatch": + return CONDITION_NOT_MATCH; + case "ContentTruncated": + return CONTENT_TRUNCATED; + case "ContentIncomplete": + return CONTENT_INCOMPLETE; + default: + return UNEXPECTED; + } + } +} diff --git a/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALException.java b/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALException.java new file mode 100644 index 000000000000..104423ac90b8 --- /dev/null +++ b/bindings/java/src/main/java/org/apache/opendal/exception/OpenDALException.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.exception; + +public class OpenDALException extends RuntimeException { + private final OpenDALErrorCode errorCode; + + public OpenDALException(OpenDALErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public OpenDALErrorCode getErrorCode() { + return errorCode; + } +} 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 76633ae51988..d34fead8e06e 100644 --- a/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/AsyncStepsTest.java @@ -26,10 +26,9 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; public class AsyncStepsTest { - Operator operator; @Given("A new OpenDAL Async Operator") @@ -41,7 +40,7 @@ public void a_new_open_dal_async_operator() { @When("Async write path {string} with content {string}") public void async_write_path_test_with_content_hello_world(String fileName, String content) { - CompletableFuture future = operator.asyncWrite(fileName, content); + CompletableFuture future = operator.writeAsync(fileName, content); Boolean result = future.join(); assertTrue(result); } diff --git a/bindings/java/src/test/java/org/apache/opendal/ExceptionTest.java b/bindings/java/src/test/java/org/apache/opendal/ExceptionTest.java new file mode 100644 index 000000000000..ce2cf3a1a7eb --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/ExceptionTest.java @@ -0,0 +1,53 @@ +/* + * 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.HashMap; +import java.util.Map; +import org.apache.opendal.exception.OpenDALErrorCode; +import org.apache.opendal.exception.OpenDALException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ExceptionTest { + Operator operator; + + @BeforeEach + public void init() { + Map params = new HashMap<>(); + params.put("root", "/tmp"); + this.operator = new Operator("Memory", params); + } + + @AfterEach + public void clean() { + this.operator.close(); + } + + @Test + public void testStatNotExistFile() { + OpenDALException exception = assertThrows(OpenDALException.class, () -> this.operator.stat("not_exist_file")); + assertEquals(exception.getErrorCode(), OpenDALErrorCode.NOT_FOUND); + } +} diff --git a/bindings/java/src/test/java/org/apache/opendal/StepsTest.java b/bindings/java/src/test/java/org/apache/opendal/StepsTest.java index a4f411f0ee39..71a4a82c6af9 100644 --- a/bindings/java/src/test/java/org/apache/opendal/StepsTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/StepsTest.java @@ -22,14 +22,14 @@ import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; - import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class StepsTest { - Operator operator; @Given("A new OpenDAL Blocking Operator") @@ -44,7 +44,6 @@ public void blocking_write_path_test_with_content_hello_world(String fileName, S this.operator.write(fileName, content); } - @Then("The blocking file {string} should exist") public void the_blocking_file_test_should_exist(String fileName) { Metadata metadata = this.operator.stat(fileName); @@ -56,22 +55,17 @@ public void the_blocking_file_test_should_exist(String fileName) { public void the_blocking_file_test_entry_mode_must_be_file(String fileName) { Metadata metadata = this.operator.stat(fileName); assertTrue(metadata.isFile()); - } @Then("The blocking file {string} content length must be {int}") public void the_blocking_file_test_content_length_must_be_13(String fileName, int length) { Metadata metadata = this.operator.stat(fileName); - assertEquals(metadata.getContentLength(), length); } @Then("The blocking file {string} must have content {string}") public void the_blocking_file_test_must_have_content_hello_world(String fileName, String content) { String readContent = this.operator.read(fileName); - assertEquals(content, readContent); } - - } diff --git a/bindings/java/src/test/resources/features/binding.feature b/bindings/java/src/test/resources/features/binding.feature new file mode 120000 index 000000000000..ad3840b3e359 --- /dev/null +++ b/bindings/java/src/test/resources/features/binding.feature @@ -0,0 +1 @@ +../../../../../tests/features/binding.feature \ No newline at end of file