From efef08d48ec9a0b2cce580a6b9bff1cb082e044f Mon Sep 17 00:00:00 2001 From: geruh Date: Mon, 2 Jun 2025 22:46:12 -0700 Subject: [PATCH 1/2] Add StatOptions support for new options API --- bindings/java/Cargo.toml | 1 + bindings/java/src/async_operator.rs | 9 +- bindings/java/src/convert.rs | 29 +++ bindings/java/src/lib.rs | 30 ++- .../org/apache/opendal/AsyncOperator.java | 8 +- .../java/org/apache/opendal/Capability.java | 21 +++ .../java/org/apache/opendal/Operator.java | 8 +- .../java/org/apache/opendal/StatOptions.java | 58 ++++++ bindings/java/src/operator.rs | 15 +- .../test/behavior/AsyncStatOptionsTest.java | 175 ++++++++++++++++++ .../behavior/BlockingStatOptionsTest.java | 175 ++++++++++++++++++ 11 files changed, 517 insertions(+), 12 deletions(-) create mode 100644 bindings/java/src/main/java/org/apache/opendal/StatOptions.java create mode 100644 bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java create mode 100644 bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index 75f4e4ad4d65..c3bae56fc7e4 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -156,6 +156,7 @@ opendal = { version = ">=0", path = "../../core", features = [ "blocking", ] } tokio = { version = "1.28.1", features = ["full"] } +chrono = "0.4" # This is not optimal. See also the Cargo issue: # https://github.com/rust-lang/cargo/issues/1197#issuecomment-1641086954 diff --git a/bindings/java/src/async_operator.rs b/bindings/java/src/async_operator.rs index 7835b2256684..0340a127c26a 100644 --- a/bindings/java/src/async_operator.rs +++ b/bindings/java/src/async_operator.rs @@ -44,7 +44,7 @@ use crate::make_metadata; use crate::make_operator_info; use crate::make_presigned_request; use crate::Result; -use crate::{make_entry, make_list_options, make_write_options}; +use crate::{make_entry, make_list_options, make_stat_options, make_write_options}; #[no_mangle] pub extern "system" fn Java_org_apache_opendal_AsyncOperator_constructor( @@ -147,8 +147,9 @@ pub unsafe extern "system" fn Java_org_apache_opendal_AsyncOperator_stat( op: *mut Operator, executor: *const Executor, path: JString, + stat_options: JObject, ) -> jlong { - intern_stat(&mut env, op, executor, path).unwrap_or_else(|e| { + intern_stat(&mut env, op, executor, path, stat_options).unwrap_or_else(|e| { e.throw(&mut env); 0 }) @@ -159,14 +160,16 @@ fn intern_stat( op: *mut Operator, executor: *const Executor, path: JString, + options: JObject, ) -> Result { let op = unsafe { &mut *op }; let id = request_id(env)?; let path = jstring_to_string(env, &path)?; + let stat_opts = make_stat_options(env, &options)?; executor_or_default(env, executor)?.spawn(async move { - let metadata = op.stat(&path).await.map_err(Into::into); + let metadata = op.stat_options(&path, stat_opts).await.map_err(Into::into); let mut env = unsafe { get_current_env() }; let result = metadata.and_then(|metadata| make_metadata(&mut env, metadata)); complete_future(id, result.map(JValueOwned::Object)) diff --git a/bindings/java/src/convert.rs b/bindings/java/src/convert.rs index 78e2497622fc..ade4d490c71b 100644 --- a/bindings/java/src/convert.rs +++ b/bindings/java/src/convert.rs @@ -16,6 +16,7 @@ // under the License. use crate::Result; +use chrono::{DateTime, Utc}; use jni::objects::JObject; use jni::objects::JString; use jni::objects::{JByteArray, JMap}; @@ -122,6 +123,34 @@ pub(crate) fn read_jlong_field_to_usize( } } +pub(crate) fn read_instant_field_to_date_time( + env: &mut JNIEnv<'_>, + obj: &JObject, + field: &str, +) -> Result>> { + let result = env.get_field(obj, field, "Ljava/time/Instant;")?.l()?; + if result.is_null() { + return Ok(None); + } + + let epoch_second = env + .call_method(&result, "getEpochSecond", "()J", &[])? + .j()?; + let nano = env.call_method(&result, "getNano", "()I", &[])?.i()?; + DateTime::from_timestamp(epoch_second, nano as u32) + .map(Some) + .ok_or_else(|| { + Error::new( + ErrorKind::Unexpected, + format!( + "Invalid timestamp: seconds={}, nanos={}", + epoch_second, nano + ), + ) + .into() + }) +} + pub(crate) fn offset_length_to_range(offset: i64, length: i64) -> Result<(Bound, Bound)> { let offset = u64::try_from(offset) .map_err(|_| Error::new(ErrorKind::RangeNotSatisfied, "offset must be non-negative"))?; diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 21c3359b8b0a..c927a14b26a8 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -94,11 +94,14 @@ fn make_operator_info<'a>(env: &mut JNIEnv<'a>, info: OperatorInfo) -> Result(env: &mut JNIEnv<'a>, cap: Capability) -> Result> { let capability = env.new_object( "org/apache/opendal/Capability", - "(ZZZZZZZZZZZZZZZZZZZJJZZZZZZZZZZZZZZZ)V", + "(ZZZZZZZZZZZZZZZZZZZZZZJJZZZZZZZZZZZZZZZ)V", &[ JValue::Bool(cap.stat as jboolean), JValue::Bool(cap.stat_with_if_match as jboolean), JValue::Bool(cap.stat_with_if_none_match as jboolean), + JValue::Bool(cap.stat_with_if_modified_since as jboolean), + JValue::Bool(cap.stat_with_if_unmodified_since as jboolean), + JValue::Bool(cap.stat_with_version as jboolean), JValue::Bool(cap.read as jboolean), JValue::Bool(cap.read_with_if_match as jboolean), JValue::Bool(cap.read_with_if_none_match as jboolean), @@ -246,3 +249,28 @@ fn make_list_options<'a>( deleted: convert::read_bool_field(env, options, "deleted").unwrap_or_default(), }) } + +fn make_stat_options(env: &mut JNIEnv, options: &JObject) -> Result { + Ok(opendal::options::StatOptions { + if_match: convert::read_string_field(env, options, "ifMatch")?, + if_none_match: convert::read_string_field(env, options, "ifNoneMatch")?, + if_modified_since: convert::read_instant_field_to_date_time( + env, + options, + "ifModifiedSince", + )?, + if_unmodified_since: convert::read_instant_field_to_date_time( + env, + options, + "ifUnmodifiedSince", + )?, + version: convert::read_string_field(env, options, "version")?, + override_content_type: convert::read_string_field(env, options, "overrideContentType")?, + override_cache_control: convert::read_string_field(env, options, "overrideCacheControl")?, + override_content_disposition: convert::read_string_field( + env, + options, + "overrideContentDisposition", + )?, + }) +} diff --git a/bindings/java/src/main/java/org/apache/opendal/AsyncOperator.java b/bindings/java/src/main/java/org/apache/opendal/AsyncOperator.java index c0e0d3c0092a..ebc9c34badaf 100644 --- a/bindings/java/src/main/java/org/apache/opendal/AsyncOperator.java +++ b/bindings/java/src/main/java/org/apache/opendal/AsyncOperator.java @@ -228,7 +228,11 @@ public CompletableFuture write(String path, byte[] content, WriteOptions o } public CompletableFuture stat(String path) { - final long requestId = stat(nativeHandle, executorHandle, path); + return stat(path, StatOptions.builder().build()); + } + + public CompletableFuture stat(String path, StatOptions options) { + final long requestId = stat(nativeHandle, executorHandle, path, options); return AsyncRegistry.take(requestId); } @@ -311,7 +315,7 @@ private static native long write( private static native long delete(long nativeHandle, long executorHandle, String path); - private static native long stat(long nativeHandle, long executorHandle, String path); + private static native long stat(long nativeHandle, long executorHandle, String path, StatOptions options); private static native long presignRead(long nativeHandle, long executorHandle, String path, long duration); diff --git a/bindings/java/src/main/java/org/apache/opendal/Capability.java b/bindings/java/src/main/java/org/apache/opendal/Capability.java index d108990c20ab..80491749dff6 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Capability.java +++ b/bindings/java/src/main/java/org/apache/opendal/Capability.java @@ -39,6 +39,21 @@ public class Capability { */ public final boolean statWithIfNoneMatch; + /** + * If operator supports stat with if modified since. + */ + public final boolean statWithIfModifiedSince; + + /** + * If operator supports stat with if unmodified since. + */ + public final boolean statWithIfUnmodifiedSince; + + /** + * If operator supports stat with versions. + */ + public final boolean statWithVersion; + /** * If operator supports read. */ @@ -211,6 +226,9 @@ public Capability( boolean stat, boolean statWithIfMatch, boolean statWithIfNoneMatch, + boolean statWithIfModifiedSince, + boolean statWithIfUnmodifiedSince, + boolean statWithVersion, boolean read, boolean readWithIfMatch, boolean readWithIfNoneMatch, @@ -247,6 +265,9 @@ public Capability( this.stat = stat; this.statWithIfMatch = statWithIfMatch; this.statWithIfNoneMatch = statWithIfNoneMatch; + this.statWithIfModifiedSince = statWithIfModifiedSince; + this.statWithIfUnmodifiedSince = statWithIfUnmodifiedSince; + this.statWithVersion = statWithVersion; this.read = read; this.readWithIfMatch = readWithIfMatch; this.readWithIfNoneMatch = readWithIfNoneMatch; 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 8f109c612835..b25bb7824a1e 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Operator.java +++ b/bindings/java/src/main/java/org/apache/opendal/Operator.java @@ -116,7 +116,11 @@ public void delete(String path) { } public Metadata stat(String path) { - return stat(nativeHandle, path); + return stat(nativeHandle, path, StatOptions.builder().build()); + } + + public Metadata stat(String path, StatOptions options) { + return stat(nativeHandle, path, options); } public void createDir(String path) { @@ -154,7 +158,7 @@ public List list(String path, ListOptions options) { private static native void delete(long op, String path); - private static native Metadata stat(long op, String path); + private static native Metadata stat(long op, String path, StatOptions options); private static native long createDir(long op, String path); diff --git a/bindings/java/src/main/java/org/apache/opendal/StatOptions.java b/bindings/java/src/main/java/org/apache/opendal/StatOptions.java new file mode 100644 index 000000000000..6bb4c319dd73 --- /dev/null +++ b/bindings/java/src/main/java/org/apache/opendal/StatOptions.java @@ -0,0 +1,58 @@ +package org.apache.opendal; + +import java.time.Instant; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class StatOptions { + + /** + * Sets if-match condition for this operation. + * If file exists and its etag doesn't match, an error will be returned. + */ + private String ifMatch; + + /** + * Sets if-none-match condition for this operation. + * If file exists and its etag matches, an error will be returned. + */ + private String ifNoneMatch; + + /** + * Sets if-modified-since condition for this operation. + * If file exists and hasn't been modified since the specified time, an error will be returned. + */ + private Instant ifModifiedSince; + + /** + * Sets if-unmodified-since condition for this operation. + * If file exists and has been modified since the specified time, an error will be returned. + */ + private Instant ifUnmodifiedSince; + + /** + * Sets version for this operation. + * Retrieves data of a specified version of the given path. + */ + private String version; + + /** + * Specifies the content-type header for presigned operations. + * Only meaningful when used along with presign. + */ + private String overrideContentType; + + /** + * Specifies the cache-control header for presigned operations. + * Only meaningful when used along with presign. + */ + private String overrideCacheControl; + + /** + * Specifies the content-disposition header for presigned operations. + * Only meaningful when used along with presign. + */ + private String overrideContentDisposition; +} diff --git a/bindings/java/src/operator.rs b/bindings/java/src/operator.rs index 160ac15ce8de..3a9e7088dea9 100644 --- a/bindings/java/src/operator.rs +++ b/bindings/java/src/operator.rs @@ -33,7 +33,7 @@ use crate::convert::{ }; use crate::make_metadata; use crate::Result; -use crate::{make_entry, make_list_options, make_write_options}; +use crate::{make_entry, make_list_options, make_stat_options, make_write_options}; /// # Safety /// @@ -141,16 +141,23 @@ pub unsafe extern "system" fn Java_org_apache_opendal_Operator_stat( _: JClass, op: *mut blocking::Operator, path: JString, + stat_options: JObject, ) -> jobject { - intern_stat(&mut env, &mut *op, path).unwrap_or_else(|e| { + intern_stat(&mut env, &mut *op, path, stat_options).unwrap_or_else(|e| { e.throw(&mut env); JObject::default().into_raw() }) } -fn intern_stat(env: &mut JNIEnv, op: &mut blocking::Operator, path: JString) -> Result { +fn intern_stat( + env: &mut JNIEnv, + op: &mut blocking::Operator, + path: JString, + options: JObject, +) -> Result { let path = jstring_to_string(env, &path)?; - let metadata = op.stat(&path)?; + let stat_opts = make_stat_options(env, &options)?; + let metadata = op.stat_options(&path, stat_opts)?; Ok(make_metadata(env, metadata)?.into_raw()) } diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java new file mode 100644 index 000000000000..c7b92f3f4247 --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java @@ -0,0 +1,175 @@ +package org.apache.opendal.test.behavior; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import org.apache.opendal.Capability; +import org.apache.opendal.Metadata; +import org.apache.opendal.OpenDALException; +import org.apache.opendal.StatOptions; +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 AsyncStatOptionsTest extends BehaviorTestBase { + + @BeforeAll + public void precondition() { + Capability capability = asyncOp().info.fullCapability; + assumeTrue(capability.read && capability.write && capability.stat); + } + + @Test + void testStatWithIfMatch() { + assumeTrue(asyncOp().info.fullCapability.statWithIfMatch); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + Metadata meta = asyncOp().stat(path).join(); + StatOptions invalidOptions = + StatOptions.builder().ifMatch("\"invalid\"").build(); + + assertThatThrownBy(() -> asyncOp().stat(path, invalidOptions).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + StatOptions validOptions = StatOptions.builder().ifMatch(meta.getEtag()).build(); + Metadata result = asyncOp().stat(path, validOptions).join(); + assertThat(result).isNotNull(); + + asyncOp().delete(path).join(); + } + + @Test + void testStatWithIfNoneMatch() { + assumeTrue(asyncOp().info.fullCapability.statWithIfNoneMatch); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + Metadata meta = asyncOp().stat(path).join(); + StatOptions matchingOptions = + StatOptions.builder().ifNoneMatch(meta.getEtag()).build(); + + assertThatThrownBy(() -> asyncOp().stat(path, matchingOptions).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + StatOptions nonMatchingOptions = + StatOptions.builder().ifNoneMatch("\"invalid\"").build(); + Metadata result = asyncOp().stat(path, nonMatchingOptions).join(); + assertThat(result.getContentLength()).isEqualTo(meta.getContentLength()); + + asyncOp().delete(path).join(); + } + + @Test + void testStatWithIfModifiedSince() { + assumeTrue(asyncOp().info.fullCapability.statWithIfModifiedSince); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + Metadata meta = asyncOp().stat(path).join(); + Instant since = meta.getLastModified().minus(1, ChronoUnit.SECONDS); + StatOptions options1 = StatOptions.builder().ifModifiedSince(since).build(); + Metadata result1 = asyncOp().stat(path, options1).join(); + assertThat(result1.getLastModified()).isEqualTo(meta.getLastModified()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Instant futureTime = meta.getLastModified().plus(1, ChronoUnit.SECONDS); + StatOptions options2 = StatOptions.builder().ifModifiedSince(futureTime).build(); + + assertThatThrownBy(() -> asyncOp().stat(path, options2).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + asyncOp().delete(path).join(); + } + + @Test + void testStatWithIfUnmodifiedSince() { + assumeTrue(asyncOp().info.fullCapability.statWithIfUnmodifiedSince); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + Metadata meta = asyncOp().stat(path).join(); + Instant beforeTime = meta.getLastModified().minus(1, ChronoUnit.SECONDS); + StatOptions options1 = + StatOptions.builder().ifUnmodifiedSince(beforeTime).build(); + + assertThatThrownBy(() -> asyncOp().stat(path, options1).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Instant afterTime = meta.getLastModified().plus(1, ChronoUnit.SECONDS); + StatOptions options2 = + StatOptions.builder().ifUnmodifiedSince(afterTime).build(); + Metadata result2 = asyncOp().stat(path, options2).join(); + + assertThat(result2.getLastModified()).isEqualTo(meta.getLastModified()); + + asyncOp().delete(path).join(); + } + + @Test + void testStatWithVersion() { + assumeTrue(asyncOp().info.fullCapability.statWithVersion); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + Metadata metadata = asyncOp().stat(path).join(); + String version = metadata.getVersion(); + + assertThat(version).isNotNull(); + + StatOptions versionOptions = StatOptions.builder().version(version).build(); + Metadata versionedMeta = asyncOp().stat(path, versionOptions).join(); + + assertThat(versionedMeta.getVersion()).isEqualTo(version); + asyncOp().write(path, content).join(); + Metadata metadata2 = asyncOp().stat(path).join(); + + assertThat(metadata2.getVersion()).isNotEqualTo(version); + Metadata oldVersionMetadata = asyncOp().stat(path, versionOptions).join(); + assertThat(oldVersionMetadata.getVersion()).isEqualTo(version); + + asyncOp().delete(path).join(); + } + + @Test + void testStatWithNotExistingVersion() { + assumeTrue(asyncOp().info.fullCapability.statWithVersion); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + asyncOp().write(path, content).join(); + String version = asyncOp().stat(path).join().getVersion(); + + String path2 = UUID.randomUUID().toString(); + asyncOp().write(path2, content).join(); + + StatOptions options = StatOptions.builder().version(version).build(); + + assertThatThrownBy(() -> asyncOp().stat(path2, options).join()) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + asyncOp().delete(path).join(); + asyncOp().delete(path2).join(); + } +} diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java new file mode 100644 index 000000000000..ef8e1e35fead --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java @@ -0,0 +1,175 @@ +package org.apache.opendal.test.behavior; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import org.apache.opendal.Capability; +import org.apache.opendal.Metadata; +import org.apache.opendal.OpenDALException; +import org.apache.opendal.StatOptions; +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 BlockingStatOptionsTest extends BehaviorTestBase { + + @BeforeAll + public void precondition() { + Capability capability = op().info.fullCapability; + assumeTrue(capability.read && capability.write && capability.stat); + } + + @Test + void testStatWithIfMatch() { + assumeTrue(op().info.fullCapability.statWithIfMatch); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + Metadata meta = op().stat(path); + StatOptions invalidOptions = + StatOptions.builder().ifMatch("\"invalid\"").build(); + + assertThatThrownBy(() -> op().stat(path, invalidOptions)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + StatOptions validOptions = StatOptions.builder().ifMatch(meta.getEtag()).build(); + Metadata result = op().stat(path, validOptions); + assertThat(result).isNotNull(); + + op().delete(path); + } + + @Test + void testStatWithIfNoneMatch() { + assumeTrue(op().info.fullCapability.statWithIfNoneMatch); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + Metadata meta = op().stat(path); + StatOptions matchingOptions = + StatOptions.builder().ifNoneMatch(meta.getEtag()).build(); + + assertThatThrownBy(() -> op().stat(path, matchingOptions)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + StatOptions nonMatchingOptions = + StatOptions.builder().ifNoneMatch("\"invalid\"").build(); + Metadata result = op().stat(path, nonMatchingOptions); + assertThat(result.getContentLength()).isEqualTo(meta.getContentLength()); + + op().delete(path); + } + + @Test + void testStatWithIfModifiedSince() { + assumeTrue(op().info.fullCapability.statWithIfModifiedSince); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + Metadata meta = op().stat(path); + Instant since = meta.getLastModified().minus(1, ChronoUnit.SECONDS); + StatOptions options1 = StatOptions.builder().ifModifiedSince(since).build(); + Metadata result1 = op().stat(path, options1); + + assertThat(result1.getLastModified()).isEqualTo(meta.getLastModified()); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Instant futureTime = meta.getLastModified().plus(1, ChronoUnit.SECONDS); + StatOptions options2 = StatOptions.builder().ifModifiedSince(futureTime).build(); + + assertThatThrownBy(() -> op().stat(path, options2)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + op().delete(path); + } + + @Test + void testStatWithIfUnmodifiedSince() { + assumeTrue(op().info.fullCapability.statWithIfUnmodifiedSince); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + Metadata meta = op().stat(path); + Instant beforeTime = meta.getLastModified().minus(1, ChronoUnit.SECONDS); + StatOptions options1 = + StatOptions.builder().ifUnmodifiedSince(beforeTime).build(); + + assertThatThrownBy(() -> op().stat(path, options1)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.ConditionNotMatch)); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Instant afterTime = meta.getLastModified().plus(1, ChronoUnit.SECONDS); + StatOptions options2 = + StatOptions.builder().ifUnmodifiedSince(afterTime).build(); + Metadata result2 = op().stat(path, options2); + + assertThat(result2.getLastModified()).isEqualTo(meta.getLastModified()); + + op().delete(path); + } + + @Test + void testStatWithVersion() { + assumeTrue(op().info.fullCapability.statWithVersion); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + Metadata metadata = op().stat(path); + String version = metadata.getVersion(); + + assertThat(version).isNotNull(); + + StatOptions versionOptions = StatOptions.builder().version(version).build(); + Metadata versionedMeta = op().stat(path, versionOptions); + + assertThat(versionedMeta.getVersion()).isEqualTo(version); + op().write(path, content); + Metadata metadata2 = op().stat(path); + + assertThat(metadata2.getVersion()).isNotEqualTo(version); + Metadata oldVersionMetadata = op().stat(path, versionOptions); + assertThat(oldVersionMetadata.getVersion()).isEqualTo(version); + + op().delete(path); + } + + @Test + void testStatWithNotExistingVersion() { + assumeTrue(op().info.fullCapability.statWithVersion); + + String path = UUID.randomUUID().toString(); + byte[] content = generateBytes(); + op().write(path, content); + String version = op().stat(path).getVersion(); + + String path2 = UUID.randomUUID().toString(); + op().write(path2, content); + StatOptions options = StatOptions.builder().version(version).build(); + + assertThatThrownBy(() -> op().stat(path2, options)) + .is(OpenDALExceptionCondition.ofAsync(OpenDALException.Code.NotFound)); + + op().delete(path); + op().delete(path2); + } +} From 52a3e839b180938fe07382efae66d902cce38a6f Mon Sep 17 00:00:00 2001 From: geruh Date: Mon, 2 Jun 2025 23:04:16 -0700 Subject: [PATCH 2/2] add licence --- .../java/org/apache/opendal/StatOptions.java | 19 +++++++++++++++++++ .../test/behavior/AsyncStatOptionsTest.java | 19 +++++++++++++++++++ .../behavior/BlockingStatOptionsTest.java | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/bindings/java/src/main/java/org/apache/opendal/StatOptions.java b/bindings/java/src/main/java/org/apache/opendal/StatOptions.java index 6bb4c319dd73..29a001b5432b 100644 --- a/bindings/java/src/main/java/org/apache/opendal/StatOptions.java +++ b/bindings/java/src/main/java/org/apache/opendal/StatOptions.java @@ -1,3 +1,22 @@ +/* + * 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.time.Instant; diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java index c7b92f3f4247..0368deed6ac7 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/AsyncStatOptionsTest.java @@ -1,3 +1,22 @@ +/* + * 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.assertThatThrownBy; diff --git a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java index ef8e1e35fead..968ad2e91f20 100644 --- a/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java +++ b/bindings/java/src/test/java/org/apache/opendal/test/behavior/BlockingStatOptionsTest.java @@ -1,3 +1,22 @@ +/* + * 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.assertThatThrownBy;