diff --git a/site/docs/oci/storage.md b/site/docs/oci/storage.md index 7279ab14..90495dc7 100644 --- a/site/docs/oci/storage.md +++ b/site/docs/oci/storage.md @@ -50,15 +50,42 @@ public void createBucketAndUploadFile() { Object Storage objects can be accessed using Spring's resource abstraction. ```java -@Value("https://objectstorage.us-chicago-1.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}") +@Value("https://objectstorage.${OCI_REGION}.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}") private Resource myObjectResource; ``` ```java -SpringApplication.run(...).getResource("Object Storage URL"); +Resource objectResource = applicationContext.getResource( + "https://objectstorage.${OCI_REGION}.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}"); ``` -The resulting `Resource` can be read like other Spring resources. This resource support is currently read-only. +The resulting `Resource` can be read like other Spring resources. + +```java +try (InputStream inputStream = myObjectResource.getInputStream()) { + String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); +} +``` + +Object Storage resources also implement Spring's `WritableResource`, so they can be used for write operations as well. + +```java +@Value("https://objectstorage.${OCI_REGION}.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}") +private WritableResource myWritableObjectResource; +``` + +You can also resolve a writable object resource programmatically from the application context. + +```java +WritableResource writableResource = (WritableResource) applicationContext.getResource( + "https://objectstorage.${OCI_REGION}.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}"); + +try (OutputStream outputStream = writableResource.getOutputStream()) { + outputStream.write("Hello Object Storage".getBytes(StandardCharsets.UTF_8)); +} +``` + +Data written through `WritableResource#getOutputStream()` is uploaded to OCI Object Storage when the stream is closed. ## Configuration @@ -66,6 +93,6 @@ The resulting `Resource` can be read like other Spring resources. This resource | --- | --- | --- | --- | | `spring.cloud.oci.storage.enabled` | Enables the OCI Object Storage APIs | No | `true` | -## Sample +## Learn By Example See [`spring-cloud-oci-storage-sample`](https://github.com/oracle/spring-cloud-oracle/tree/main/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample). diff --git a/site/docs/releases/changelog.md b/site/docs/releases/changelog.md index c2c1e627..e91845fd 100644 --- a/site/docs/releases/changelog.md +++ b/site/docs/releases/changelog.md @@ -9,7 +9,13 @@ List of upcoming and historic changes to Spring Cloud Oracle. ### Next, TBD -TBD +#### Spring Cloud OCI + +- OCI Object Storage `WritableResource` support: + - The OCI Object Storage Spring `Resource` now also implements `WritableResource`, allowing object uploads through `getOutputStream()` + - The Object Storage sample now demonstrates Spring `Resource`, `WritableResource`, and direct `Storage` API round trips for objects and bucket lifecycle operations + - The Object Storage documentation now includes `WritableResource` usage in addition to the existing `Resource` examples + ### 2.0.0, March TBD @@ -27,4 +33,4 @@ Notably, the documentation has been updated and moved to a new site. For histori #### Spring Cloud OCI -- Upgrade third-party dependencies and migrated to Spring Boot 4 \ No newline at end of file +- Upgrade third-party dependencies and migrated to Spring Boot 4 diff --git a/spring-cloud-oci/spring-cloud-oci-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-oci/spring-cloud-oci-autoconfigure/src/main/resources/META-INF/spring.factories index 19496f33..11ebadbe 100644 --- a/spring-cloud-oci/spring-cloud-oci-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-oci/spring-cloud-oci-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.env.EnvironmentPostProcessor=com.oracle.cloud.spring.vault.VaultEnvironmentPostProcessor \ No newline at end of file +org.springframework.boot.EnvironmentPostProcessor=com.oracle.cloud.spring.vault.VaultEnvironmentPostProcessor \ No newline at end of file diff --git a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/README.md b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/README.md index fb1884ec..8255648c 100644 --- a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/README.md +++ b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/README.md @@ -49,6 +49,9 @@ git clone https://github.com/oracle/spring-cloud-oci.git spring-cloud-oci ``` spring.cloud.oci.region.static = US_ASHBURN_1 spring.cloud.oci.compartment.static = +OCI_NAMESPACE = +OCI_BUCKET = +OCI_OBJECT = ``` 1. Start the application using the following command from sample root directory. ``` @@ -61,6 +64,11 @@ Note: Default service port is `8080`. You can change this with the `server.port Launch the Swagger UI (http://localhost:8080/swagger-ui/index.html) to view all available APIs and their payload samples. +The sample now demonstrates both Spring `Resource` and `WritableResource` usage for OCI Object Storage: + +* `GET /demoapp/api/object/resource` reads the configured object through a `Resource` +* `POST /demoapp/api/object/resource` writes plain text to the configured object through a `WritableResource` + ![Swagger UI](./images/swagger-ui.png) ## References @@ -78,4 +86,4 @@ Licensed under the Universal Permissive License (UPL), Version 1.0. See [LICENSE](../../LICENSE.txt) for more details. -ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. \ No newline at end of file +ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. diff --git a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/ObjectController.java b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/ObjectController.java index dcb302c4..07a8015d 100644 --- a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/ObjectController.java +++ b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/ObjectController.java @@ -1,5 +1,5 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ @@ -10,9 +10,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.WritableResource; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RestController; import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; @RestController @RequestMapping("demoapp/api/object") @@ -32,16 +34,32 @@ public class ObjectController { Storage storage; @Autowired - ResourceLoader loader; + @Qualifier("sampleObjectResource") + Resource sampleObjectResource; - @Value("https://objectstorage.us-chicago-1.oraclecloud.com/n/${OCI_NAMESPACE}/b/${OCI_BUCKET}/o/${OCI_OBJECT}") - Resource myObject; + @Autowired + @Qualifier("sampleWritableObjectResource") + WritableResource sampleWritableObjectResource; @GetMapping("/") String hello() { return "Hello World "; } + @GetMapping("/resource") + String readWithResource() throws IOException { + try (var inputStream = sampleObjectResource.getInputStream()) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + @PostMapping("/resource") + void writeWithWritableResource(@RequestBody String content) throws IOException { + try (OutputStream outputStream = sampleWritableObjectResource.getOutputStream()) { + outputStream.write(content.getBytes(StandardCharsets.UTF_8)); + } + } + @PostMapping("/{bucketName}") void storeObject(@Parameter(required = true) @RequestBody Person p, @Parameter(required = true, example = "new-bucket") @PathVariable String bucketName) throws IOException { diff --git a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplication.java b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplication.java index 0d92dc69..1afcb39b 100644 --- a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplication.java +++ b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/main/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplication.java @@ -1,12 +1,18 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ package com.oracle.cloud.spring.sample.storage.springcloudocistoragesample; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.WritableResource; @SpringBootApplication public class SpringCloudOciStorageSampleApplication { @@ -14,4 +20,29 @@ public class SpringCloudOciStorageSampleApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudOciStorageSampleApplication.class, args); } + + @Bean + @Qualifier("sampleObjectResource") + Resource sampleObjectResource(ResourceLoader resourceLoader, + @Value("${OCI_REGION:${spring.cloud.oci.region.static}}") String region, + @Value("${OCI_NAMESPACE}") String namespace, + @Value("${OCI_BUCKET}") String bucket, + @Value("${OCI_OBJECT}") String objectName) { + return resourceLoader.getResource(storageLocation(region, namespace, bucket, objectName)); + } + + @Bean + @Qualifier("sampleWritableObjectResource") + WritableResource sampleWritableObjectResource(@Qualifier("sampleObjectResource") Resource resource) { + if (resource instanceof WritableResource writableResource) { + return writableResource; + } + + throw new IllegalStateException("OCI storage resource does not implement WritableResource"); + } + + private static String storageLocation(String region, String namespace, String bucket, String objectName) { + return "https://objectstorage.%s.oraclecloud.com/n/%s/b/%s/o/%s" + .formatted(region, namespace, bucket, objectName); + } } diff --git a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/test/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplicationTests.java b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/test/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplicationTests.java index 6b540ceb..e247aa2c 100644 --- a/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/test/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplicationTests.java +++ b/spring-cloud-oci/spring-cloud-oci-samples/spring-cloud-oci-storage-sample/src/test/java/com/oracle/cloud/spring/sample/storage/springcloudocistoragesample/SpringCloudOciStorageSampleApplicationTests.java @@ -1,19 +1,24 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ package com.oracle.cloud.spring.sample.storage.springcloudocistoragesample; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.oracle.cloud.spring.storage.Storage; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; @@ -22,36 +27,118 @@ @EnabledIfEnvironmentVariable(named = "OCI_BUCKET", matches = ".+") @EnabledIfEnvironmentVariable(named = "OCI_OBJECT", matches = ".+") @EnabledIfEnvironmentVariable(named = "OCI_COMPARTMENT", matches = ".+") +@EnabledIfEnvironmentVariable(named = "OCI_REGION", matches = ".+") class SpringCloudOciStorageSampleApplicationTests { static final String testBucket = System.getenv("OCI_BUCKET"); + static final String testCompartment = System.getenv("OCI_COMPARTMENT"); @Autowired Storage storage; @Autowired - ObjectController objectController; + @Qualifier("sampleObjectResource") + Resource sampleObjectResource; + + @Autowired + @Qualifier("sampleWritableObjectResource") + WritableResource sampleWritableObjectResource; @Test void resourceIsLoaded() throws IOException { - Resource myObject = objectController.myObject; - assertThat(myObject).isNotNull(); - assertThat(myObject.getContentAsByteArray()).hasSizeGreaterThan(1); + assertThat(sampleObjectResource).isNotNull(); + assertThat(sampleObjectResource.getContentAsByteArray()).hasSizeGreaterThan(1); + } + + @Test + void writableResourceIsLoaded() { + assertThat(sampleWritableObjectResource).isNotNull(); + assertThat(sampleWritableObjectResource.isWritable()).isTrue(); + } + + @Test + void writableResourceRoundTrip() throws IOException { + String content = Instant.now().toString(); + + try (var outputStream = sampleWritableObjectResource.getOutputStream()) { + outputStream.write(content.getBytes(StandardCharsets.UTF_8)); + } + + try (var inputStream = sampleObjectResource.getInputStream()) { + String actual = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + assertThat(actual).isEqualTo(content); + } + } + + @Test + void storageUploadAndDownloadRoundTrip() throws IOException { + String objectName = "storage-upload-" + Instant.now().toEpochMilli() + ".txt"; + String content = "upload-round-trip-" + Instant.now(); + + try { + assertThat(storage.upload(testBucket, objectName, + new java.io.ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))).isNotNull(); + + try (InputStream inputStream = storage.download(testBucket, objectName).getInputStream()) { + assertThat(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo(content); + } + } finally { + deleteQuietly(objectName); + } + } + + @Test + void storageStoreAndReadRoundTrip() throws IOException { + ActivityInfo activityInfo = new ActivityInfo("Hello from Storage integration test"); + + try { + assertThat(storage.store(testBucket, activityInfo.getFileName(), activityInfo)).isNotNull(); + + ActivityInfo actual = storage.read(testBucket, activityInfo.getFileName(), ActivityInfo.class); + assertThat(actual.getMessage()).isEqualTo(activityInfo.getMessage()); + assertThat(actual.getTime()).isEqualTo(activityInfo.getTime()); + } finally { + deleteQuietly(activityInfo.getFileName()); + } } @Test - @Disabled - void storePOJO() throws IOException { - ActivityInfo ainfo = new ActivityInfo("Hello from Storage integration test"); - storage.store(testBucket, ainfo.getFileName(), ainfo); + void bucketCreateAndDeleteRoundTrip() { + String bucketName = "storage-sample-" + Instant.now().toEpochMilli(); + + try { + assertThat(storage.createBucket(bucketName, testCompartment)).isNotNull(); + } finally { + deleteBucketQuietly(bucketName); + } } - private class ActivityInfo { + private void deleteQuietly(String objectName) { + try { + storage.deleteObject(testBucket, objectName); + } catch (Exception ex) { + throw new IllegalStateException("Failed to delete object " + objectName, ex); + } + } + + private void deleteBucketQuietly(String bucketName) { + try { + storage.deleteBucket(bucketName); + } catch (Exception ex) { + throw new IllegalStateException("Failed to delete bucket " + bucketName, ex); + } + } + + private static class ActivityInfo { long time = System.currentTimeMillis(); String message; + public ActivityInfo() { + } + public ActivityInfo(String message) { this.message = message; } + @JsonIgnore public String getFileName() { return "activity_" + time + ".json"; } @@ -65,5 +152,3 @@ public String getMessage() { } } } - - diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/OracleStorageResource.java b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/OracleStorageResource.java index 2c24c29d..d2616cf2 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/OracleStorageResource.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/OracleStorageResource.java @@ -1,5 +1,5 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ @@ -8,21 +8,32 @@ import com.oracle.bmc.objectstorage.ObjectStorageClient; import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; import com.oracle.bmc.objectstorage.requests.GetObjectRequest; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; import com.oracle.bmc.objectstorage.responses.GetObjectResponse; import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.WritableResource; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; /** * Default OCI Storage resource implementation of Spring Resource. */ -public class OracleStorageResource extends AbstractResource { +public class OracleStorageResource extends AbstractResource implements WritableResource { private final ObjectStorageClient osClient; private final StorageLocation location; + @Nullable + private final StorageContentTypeResolver contentTypeResolver; + private final StorageObjectUploader objectUploader; /** * Creates new OracleStorageResource identified by location URI and other information. @@ -42,12 +53,39 @@ public static OracleStorageResource create(String location, ObjectStorageClient public OracleStorageResource(String bucketName, String objectName, ObjectStorageClient osClient) { - this(new StorageLocation(bucketName, objectName), osClient); + this(new StorageLocation(bucketName, objectName), osClient, null, new UploadManagerStorageObjectUploader()); + } + + OracleStorageResource(String bucketName, String objectName, + ObjectStorageClient osClient, + @Nullable StorageContentTypeResolver contentTypeResolver) { + this(new StorageLocation(bucketName, objectName), osClient, contentTypeResolver, + new UploadManagerStorageObjectUploader()); + } + + OracleStorageResource(String bucketName, String objectName, + ObjectStorageClient osClient, + @Nullable StorageContentTypeResolver contentTypeResolver, + StorageObjectUploader objectUploader) { + this(new StorageLocation(bucketName, objectName), osClient, contentTypeResolver, objectUploader); } public OracleStorageResource(StorageLocation location, ObjectStorageClient osClient) { + this(location, osClient, null, new UploadManagerStorageObjectUploader()); + } + + OracleStorageResource(StorageLocation location, ObjectStorageClient osClient, + @Nullable StorageContentTypeResolver contentTypeResolver) { + this(location, osClient, contentTypeResolver, new UploadManagerStorageObjectUploader()); + } + + OracleStorageResource(StorageLocation location, ObjectStorageClient osClient, + @Nullable StorageContentTypeResolver contentTypeResolver, + StorageObjectUploader objectUploader) { this.location = location; this.osClient = osClient; + this.contentTypeResolver = contentTypeResolver; + this.objectUploader = objectUploader; } /** @@ -79,4 +117,92 @@ public InputStream getInputStream() throws IOException { .build()); return getResponse.getInputStream(); } + + @Override + public boolean isWritable() { + return true; + } + + @Override + public OutputStream getOutputStream() { + return new UploadOnCloseOutputStream(); + } + + OracleStorageResource upload(InputStream inputStream, @Nullable StorageObjectMetadata objectMetadata) throws IOException { + Assert.notNull(inputStream, "inputStream is required"); + + Path temporaryFile = Files.createTempFile("oci-storage-resource-", ".tmp"); + + try { + long contentLength; + try (InputStream source = inputStream; + OutputStream fileOutputStream = Files.newOutputStream(temporaryFile)) { + contentLength = source.transferTo(fileOutputStream); + } + + PutObjectRequest.Builder builder = PutObjectRequest.builder() + .bucketName(location.getBucket()) + .namespaceName(getNamespaceName()) + .objectName(location.getObject()) + .contentLength(resolveContentLength(contentLength, objectMetadata)); + + if (objectMetadata != null) { + objectMetadata.apply(builder); + } + + String contentType = resolveContentType(objectMetadata); + if (contentType != null) { + builder.contentType(contentType); + } + + PutObjectRequest putObjectRequest = builder.build(); + objectUploader.upload(osClient, putObjectRequest, temporaryFile); + return this; + } finally { + Files.deleteIfExists(temporaryFile); + } + } + + private String getNamespaceName() { + GetNamespaceResponse namespaceResponse = + osClient.getNamespace(GetNamespaceRequest.builder().build()); + return namespaceResponse.getValue(); + } + + private long resolveContentLength(long contentLength, @Nullable StorageObjectMetadata objectMetadata) { + if (objectMetadata != null && objectMetadata.getContentLength() != null) { + return objectMetadata.getContentLength(); + } + return contentLength; + } + + @Nullable + private String resolveContentType(@Nullable StorageObjectMetadata objectMetadata) { + if (objectMetadata != null && objectMetadata.getContentType() != null) { + return objectMetadata.getContentType(); + } + if (contentTypeResolver != null) { + return contentTypeResolver.resolveContentType(location.getObject()); + } + return null; + } + + private final class UploadOnCloseOutputStream extends ByteArrayOutputStream { + private boolean closed; + + @Override + public void close() throws IOException { + if (closed) { + return; + } + + closed = true; + super.close(); + + StorageObjectMetadata metadata = StorageObjectMetadata.builder() + .contentLength((long) size()) + .build(); + upload(new ByteArrayInputStream(toByteArray()), metadata); + } + } } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageImpl.java b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageImpl.java index 95ef0758..7d29467d 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageImpl.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageImpl.java @@ -1,5 +1,5 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ @@ -11,11 +11,8 @@ import com.oracle.bmc.objectstorage.requests.DeleteBucketRequest; import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; -import com.oracle.bmc.objectstorage.requests.PutObjectRequest; import com.oracle.bmc.objectstorage.responses.CreateBucketResponse; import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; -import com.oracle.bmc.objectstorage.transfer.UploadConfiguration; -import com.oracle.bmc.objectstorage.transfer.UploadManager; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -31,6 +28,7 @@ public class StorageImpl implements Storage { private final StorageObjectConverter storageObjectConverter; private final StorageContentTypeResolver contentTypeResolver; private final String defaultCompartmentOCID; + private final StorageObjectUploader objectUploader; static final String ERROR_OSCLIENT_REQUIRED = "ObjectStorageClient is required"; static final String ERROR_STORAGE_OBJECT_CONVERTER_REQUIRED = "storageObjectConverter is required"; static final String ERROR_CONTENT_TYPE_RESOLVER_REQUIRED = "contentTypeResolver is required"; @@ -43,6 +41,16 @@ public StorageImpl( StorageObjectConverter storageObjectConverter, StorageContentTypeResolver contentTypeResolver, String defaultCompartmentOCID) { + this(osClient, storageObjectConverter, contentTypeResolver, defaultCompartmentOCID, + new UploadManagerStorageObjectUploader()); + } + + StorageImpl( + ObjectStorageClient osClient, + StorageObjectConverter storageObjectConverter, + StorageContentTypeResolver contentTypeResolver, + String defaultCompartmentOCID, + StorageObjectUploader objectUploader) { Assert.notNull(osClient, ERROR_OSCLIENT_REQUIRED); Assert.notNull(storageObjectConverter, "storageObjectConverter is required"); Assert.notNull(contentTypeResolver, "contentTypeResolver is required"); @@ -51,6 +59,7 @@ public StorageImpl( this.storageObjectConverter = storageObjectConverter; this.contentTypeResolver = contentTypeResolver; this.defaultCompartmentOCID = defaultCompartmentOCID; + this.objectUploader = objectUploader; } /** @@ -65,7 +74,7 @@ public OracleStorageResource download(String bucketName, String key, String vers Assert.notNull(bucketName, ERROR_BUCKET_NAME_REQUIRED); Assert.notNull(key, ERROR_KEY_REQUIRED); - return new OracleStorageResource(bucketName, key, osClient); + return new OracleStorageResource(bucketName, key, osClient, contentTypeResolver, objectUploader); } /** @@ -94,30 +103,9 @@ public OracleStorageResource upload(String bucketName, String key, InputStream i Assert.notNull(key, ERROR_KEY_REQUIRED); Assert.notNull(inputStream, "inputStream is required"); - UploadConfiguration uploadConfiguration = - UploadConfiguration.builder() - .allowMultipartUploads(true) - .allowParallelUploads(true) - .build(); - UploadManager uploadManager = new UploadManager(osClient, uploadConfiguration); - - PutObjectRequest.Builder builder = PutObjectRequest.builder() - .bucketName(bucketName) - .namespaceName(getNamespaceName()) - .objectName(key); - - if (objectMetadata != null) { - objectMetadata.apply(builder); - } - - builder.contentType(resolveContentType(key, objectMetadata)); - PutObjectRequest putObjectRequest = builder.build(); - - UploadManager.UploadRequest uploadRequest = UploadManager.UploadRequest.builder(inputStream, inputStream.available()).build(putObjectRequest); - UploadManager.UploadResponse uploadResponse = uploadManager.upload(uploadRequest); - System.out.println(uploadResponse); - - return null; + OracleStorageResource resource = + new OracleStorageResource(bucketName, key, osClient, contentTypeResolver, objectUploader); + return resource.upload(inputStream, withResolvedContentType(key, objectMetadata)); } /** @@ -254,9 +242,20 @@ public String resolveContentType(String objectName, StorageObjectMetadata metada } if (contentTypeResolver != null && (metadata == null || metadata.getContentType() == null)) { - contentTypeResolver.resolveContentType(objectName); + return contentTypeResolver.resolveContentType(objectName); } return null; } + + private StorageObjectMetadata withResolvedContentType(String objectName, @Nullable StorageObjectMetadata metadata) { + String contentType = resolveContentType(objectName, metadata); + if (contentType == null || (metadata != null && contentType.equals(metadata.getContentType()))) { + return metadata; + } + + StorageObjectMetadata resolvedMetadata = metadata != null ? metadata : StorageObjectMetadata.builder().build(); + resolvedMetadata.setContentType(contentType); + return resolvedMetadata; + } } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageObjectUploader.java b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageObjectUploader.java new file mode 100644 index 00000000..1719c328 --- /dev/null +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/main/java/com/oracle/cloud/spring/storage/StorageObjectUploader.java @@ -0,0 +1,41 @@ +/* + ** Copyright (c) 2026, Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.cloud.spring.storage; + +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import com.oracle.bmc.objectstorage.transfer.UploadConfiguration; +import com.oracle.bmc.objectstorage.transfer.UploadManager; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +interface StorageObjectUploader { + + void upload(ObjectStorageClient osClient, PutObjectRequest putObjectRequest, Path sourceFile) throws IOException; +} + +final class UploadManagerStorageObjectUploader implements StorageObjectUploader { + + @Override + public void upload(ObjectStorageClient osClient, PutObjectRequest putObjectRequest, Path sourceFile) throws IOException { + UploadConfiguration uploadConfiguration = + UploadConfiguration.builder() + .allowMultipartUploads(true) + .allowParallelUploads(true) + .build(); + UploadManager uploadManager = new UploadManager(osClient, uploadConfiguration); + + try (InputStream uploadStream = Files.newInputStream(sourceFile)) { + UploadManager.UploadRequest uploadRequest = + UploadManager.UploadRequest.builder(uploadStream, putObjectRequest.getContentLength()) + .build(putObjectRequest); + uploadManager.upload(uploadRequest); + } + } +} diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/JacksonJSONStorageObjectConverterTests.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/JacksonJSONStorageObjectConverterTests.java index 2217abcb..5e181559 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/JacksonJSONStorageObjectConverterTests.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/JacksonJSONStorageObjectConverterTests.java @@ -1,5 +1,5 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ @@ -12,8 +12,6 @@ import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; - class JacksonJSONStorageObjectConverterTests { String expectedValue = "{\"name\":\"testName\"}"; @@ -35,7 +33,7 @@ void testWriteStorageObject() { assertEquals(expectedValue, actualValue); assertThrows(StorageException.class, () -> { - new JacksonJSONStorageObjectConverter(new ObjectMapper()).write(mock(Object.class)); + new JacksonJSONStorageObjectConverter(new ObjectMapper()).write(new SelfReferencingPerson()); }); } @@ -62,3 +60,10 @@ public void setName(String name) { this.name = name; } } + +class SelfReferencingPerson { + + public SelfReferencingPerson getSelf() { + return this; + } +} diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageProtocolResolverTests.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageProtocolResolverTests.java index 30802fc5..6f7a52bf 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageProtocolResolverTests.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageProtocolResolverTests.java @@ -1,42 +1,49 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ package com.oracle.cloud.spring.storage; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class OracleStorageProtocolResolverTests { -public class OracleStorageProtocolResolverTests { - final BeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class); final OracleStorageProtocolResolver oracleStorageProtocolResolver = new OracleStorageProtocolResolver(); @Test - public void testPostProcessBeanFactory() { - oracleStorageProtocolResolver.postProcessBeanFactory((ConfigurableListableBeanFactory) beanFactory); + void testPostProcessBeanFactory() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("objectStorageClient", StorageTestSupport.newObjectStorageClient()); + oracleStorageProtocolResolver.postProcessBeanFactory(beanFactory); + assertNotNull(oracleStorageProtocolResolver.getStorageClient()); } @Test - public void testSetResourceLoader() { + void testSetResourceLoader() { oracleStorageProtocolResolver.setResourceLoader(new DefaultResourceLoader()); } @Test - public void testGetStorageClient() { - oracleStorageProtocolResolver.getStorageClient(); + void testGetStorageClient() { + assertNull(oracleStorageProtocolResolver.getStorageClient()); } @Test - public void testResolve() { - try (MockedStatic mock = mockStatic(OracleStorageResource.class)) { - oracleStorageProtocolResolver.resolve("https://objectstorage.us-chicago-1.oraclecloud.com/n/namespace/b/mybucket/o/myobject", new DefaultResourceLoader()); - } + void testResolve() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerSingleton("objectStorageClient", StorageTestSupport.newObjectStorageClient()); + oracleStorageProtocolResolver.postProcessBeanFactory(beanFactory); + + Resource resource = oracleStorageProtocolResolver.resolve( + "https://objectstorage.us-chicago-1.oraclecloud.com/n/namespace/b/mybucket/o/myobject", + new DefaultResourceLoader()); + assertNotNull(resource); } } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageResourceTests.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageResourceTests.java index 44fb272e..6e262bc7 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageResourceTests.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/OracleStorageResourceTests.java @@ -1,44 +1,73 @@ /* - ** Copyright (c) 2023, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ package com.oracle.cloud.spring.storage; -import com.oracle.bmc.objectstorage.ObjectStorageClient; -import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; -import com.oracle.bmc.objectstorage.responses.GetObjectResponse; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; +import org.springframework.core.io.WritableResource; import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.*; +class OracleStorageResourceTests { -public class OracleStorageResourceTests { - final ObjectStorageClient osClient = mock(ObjectStorageClient.class); - final OracleStorageResource oracleStorageResource = new OracleStorageResource("testBucket", "testObject", osClient); + @Test + void testCreate() { + StorageTestSupport.FakeObjectStorageClient osClient = StorageTestSupport.newObjectStorageClient(); + assertNotNull(OracleStorageResource.create( + "https://objectstorage.us-chicago-1.oraclecloud.com/n/namespace/b/mybucket/o/myobject", osClient)); + assertNull(OracleStorageResource.create("classpath:test.txt", osClient)); + } @Test - public void testCreate() { - try (MockedStatic mock = mockStatic(StorageLocation.class)) { - OracleStorageResource.create("https://objectstorage.us-chicago-1.oraclecloud.com/n/namespace/b/mybucket/o/myobject", osClient); + void testGetInputStream() throws Exception { + StorageTestSupport.FakeObjectStorageClient osClient = StorageTestSupport.newObjectStorageClient(); + osClient.objectContent = "test-content".getBytes(StandardCharsets.UTF_8); + + OracleStorageResource oracleStorageResource = new OracleStorageResource("testBucket", "testObject", osClient); + try (InputStream inputStream = oracleStorageResource.getInputStream()) { + assertEquals("test-content", new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); } + assertEquals("testBucket", osClient.lastGetObjectRequest.getBucketName()); + assertEquals("testObject", osClient.lastGetObjectRequest.getObjectName()); } @Test - public void testGetInputStream() throws Exception { - when(osClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - GetObjectResponse mockResponse = mock(GetObjectResponse.class); - when(osClient.getObject(any())).thenReturn(mockResponse); - when(mockResponse.getInputStream()).thenReturn(mock(InputStream.class)); - assertNotNull(oracleStorageResource.getInputStream()); + void testGetDescription() { + OracleStorageResource oracleStorageResource = + new OracleStorageResource("testBucket", "testObject", StorageTestSupport.newObjectStorageClient()); + assertNotNull(oracleStorageResource.getDescription()); } @Test - public void testGetDescription() { - assertNotNull(oracleStorageResource.getDescription()); + void testWritableResourceUploadsOnClose() throws Exception { + StorageTestSupport.FakeObjectStorageClient osClient = StorageTestSupport.newObjectStorageClient(); + StorageTestSupport.RecordingContentTypeResolver contentTypeResolver = + new StorageTestSupport.RecordingContentTypeResolver("text/plain"); + StorageTestSupport.RecordingStorageObjectUploader objectUploader = + new StorageTestSupport.RecordingStorageObjectUploader(); + OracleStorageResource oracleStorageResource = + new OracleStorageResource("testBucket", "testObject", osClient, contentTypeResolver, objectUploader); + + assertInstanceOf(WritableResource.class, oracleStorageResource); + assertTrue(oracleStorageResource.isWritable()); + + try (OutputStream outputStream = oracleStorageResource.getOutputStream()) { + outputStream.write("sample".getBytes(StandardCharsets.UTF_8)); + } + + assertEquals(1, objectUploader.uploadCount); + assertEquals("sample", new String(objectUploader.lastContent, StandardCharsets.UTF_8)); + assertEquals("text/plain", objectUploader.lastRequest.getContentType()); + assertEquals("testObject", contentTypeResolver.lastObjectName); } } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageImplTests.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageImplTests.java index 7d8d0a48..90de4c2f 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageImplTests.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageImplTests.java @@ -1,164 +1,137 @@ /* - ** Copyright (c) 2023, 2024, Oracle and/or its affiliates. + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ */ package com.oracle.cloud.spring.storage; -import com.oracle.bmc.objectstorage.ObjectStorage; -import com.oracle.bmc.objectstorage.ObjectStorageClient; -import com.oracle.bmc.objectstorage.responses.CreateBucketResponse; -import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; -import com.oracle.bmc.objectstorage.responses.PutObjectResponse; -import com.oracle.bmc.objectstorage.transfer.UploadManager; - -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -// TODO: Needs fixing -- all test doesn't pass class StorageImplTests { - final ObjectStorageClient objectStorageClient = mock(ObjectStorageClient.class); - final StorageObjectConverter storageObjectConverter = mock(StorageObjectConverter.class); - final StorageContentTypeResolver storageContentTypeResolver = mock(StorageContentTypeResolver.class); - final ObjectStorage objectStorage = mock(ObjectStorage.class); + final StorageTestSupport.FakeObjectStorageClient objectStorageClient = StorageTestSupport.newObjectStorageClient(); + final StorageTestSupport.RecordingStorageObjectConverter storageObjectConverter = + new StorageTestSupport.RecordingStorageObjectConverter(); + final StorageTestSupport.RecordingContentTypeResolver storageContentTypeResolver = + new StorageTestSupport.RecordingContentTypeResolver("text/plain"); + final StorageTestSupport.RecordingStorageObjectUploader objectUploader = + new StorageTestSupport.RecordingStorageObjectUploader(); final Storage storage = new StorageImpl(objectStorageClient, storageObjectConverter, - storageContentTypeResolver, "defaultCompartmentId"); + storageContentTypeResolver, "defaultCompartmentId", objectUploader); @Test void testStorageImplWithNullValues() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - new StorageImpl(null, storageObjectConverter, storageContentTypeResolver, "compartmentId"); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> + new StorageImpl(null, storageObjectConverter, storageContentTypeResolver, "compartmentId")); assertEquals(StorageImpl.ERROR_OSCLIENT_REQUIRED, exception.getMessage()); - exception = assertThrows(IllegalArgumentException.class, () -> { - new StorageImpl(objectStorageClient, null, storageContentTypeResolver, "compartmentId"); - }); + exception = assertThrows(IllegalArgumentException.class, () -> + new StorageImpl(objectStorageClient, null, storageContentTypeResolver, "compartmentId")); assertEquals(StorageImpl.ERROR_STORAGE_OBJECT_CONVERTER_REQUIRED, exception.getMessage()); - exception = assertThrows(IllegalArgumentException.class, () -> { - new StorageImpl(objectStorageClient, storageObjectConverter, null, "compartmentId"); - }); + exception = assertThrows(IllegalArgumentException.class, () -> + new StorageImpl(objectStorageClient, storageObjectConverter, null, "compartmentId")); assertEquals(StorageImpl.ERROR_CONTENT_TYPE_RESOLVER_REQUIRED, exception.getMessage()); } @Test void testCreateBucket() { - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - when(objectStorageClient.createBucket(any())).thenReturn(mock(CreateBucketResponse.class)); - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - storage.createBucket(null, "compartmentId"); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> storage.createBucket(null, "compartmentId")); assertEquals(StorageImpl.ERROR_BUCKET_NAME_REQUIRED, exception.getMessage()); - exception = assertThrows(IllegalArgumentException.class, () -> { - storage.createBucket("testBucket", null); - }); + + exception = assertThrows(IllegalArgumentException.class, () -> storage.createBucket("testBucket", null)); assertEquals(StorageImpl.ERROR_COMPARTMENT_REQUIRED, exception.getMessage()); + assertNotNull(storage.createBucket("testBucket")); + assertEquals("testBucket", objectStorageClient.lastCreateBucketRequest.getCreateBucketDetails().getName()); } @Test void testDownloadObject() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - storage.download(null, "testKey"); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> storage.download(null, "testKey")); assertEquals(StorageImpl.ERROR_BUCKET_NAME_REQUIRED, exception.getMessage()); - exception = assertThrows(IllegalArgumentException.class, () -> { - storage.deleteObject("testObject", null); - }); + + exception = assertThrows(IllegalArgumentException.class, () -> storage.deleteObject("testObject", null)); assertEquals(StorageImpl.ERROR_KEY_REQUIRED, exception.getMessage()); + assertNotNull(storage.download("testObject", "testKey")); } @Test void testDeleteObject() { - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - storage.deleteObject(null, "testKey"); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> storage.deleteObject(null, "testKey")); assertEquals(StorageImpl.ERROR_BUCKET_NAME_REQUIRED, exception.getMessage()); - exception = assertThrows(IllegalArgumentException.class, () -> { - storage.deleteObject("testObject", null); - }); + + exception = assertThrows(IllegalArgumentException.class, () -> storage.deleteObject("testObject", null)); assertEquals(StorageImpl.ERROR_KEY_REQUIRED, exception.getMessage()); - assertDoesNotThrow(() -> { - storage.deleteObject("testObject", "testKey"); - }); + + assertDoesNotThrow(() -> storage.deleteObject("testObject", "testKey")); + assertEquals("testObject", objectStorageClient.lastDeleteObjectRequest.getBucketName()); + assertEquals("testKey", objectStorageClient.lastDeleteObjectRequest.getObjectName()); } @Test void testDeleteBucket() { - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - storage.deleteBucket(null); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> storage.deleteBucket(null)); assertEquals(StorageImpl.ERROR_BUCKET_NAME_REQUIRED, exception.getMessage()); - assertDoesNotThrow(() -> { - storage.deleteBucket("testBucket"); - }); + + assertDoesNotThrow(() -> storage.deleteBucket("testBucket")); + assertEquals("testBucket", objectStorageClient.lastDeleteBucketRequest.getBucketName()); } @Test - @Disabled void testStore() throws IOException { - when(storageObjectConverter.write(any())).thenReturn("sample".getBytes()); - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - when(mock(UploadManager.class).upload(any())).thenReturn(mock(UploadManager.UploadResponse.class)); - when(objectStorage.putObject(any())).thenReturn(mock(PutObjectResponse.class)); - assertNull(storage.store("bucketName", "key", "sample")); + assertNotNull(storage.store("bucketName", "key", "sample")); + assertEquals("sample", new String(objectUploader.lastContent, StandardCharsets.UTF_8)); + assertEquals("text/plain", objectUploader.lastRequest.getContentType()); + assertEquals("sample", storageObjectConverter.lastWrittenObject); } @Test - @Disabled void testUpload() throws IOException { - when(storageObjectConverter.write(any())).thenReturn("sample".getBytes()); - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - when(mock(UploadManager.class).upload(any())).thenReturn(mock(UploadManager.UploadResponse.class)); - when(objectStorage.putObject(any())).thenReturn(mock(PutObjectResponse.class)); - assertNull(storage.upload("bucketName", "key", new ByteArrayInputStream("sample".getBytes()), StorageObjectMetadata.builder().build())); - assertNull(storage.upload("bucketName", "key", new ByteArrayInputStream("sample".getBytes()), StorageObjectMetadata.builder().contentType("application/json").build())); + assertNotNull(storage.upload("bucketName", "key", + new ByteArrayInputStream("sample".getBytes(StandardCharsets.UTF_8)), + StorageObjectMetadata.builder().build())); + assertEquals("sample", new String(objectUploader.lastContent, StandardCharsets.UTF_8)); + assertEquals("text/plain", objectUploader.lastRequest.getContentType()); + assertEquals("key", storageContentTypeResolver.lastObjectName); + + assertNotNull(storage.upload("bucketName", "key", + new ByteArrayInputStream("sample".getBytes(StandardCharsets.UTF_8)), + StorageObjectMetadata.builder().contentType("application/json").build())); + assertEquals("application/json", objectUploader.lastRequest.getContentType()); } @Test - public void testDownload() { + void testDownload() { assertNotNull(storage.download("testBucket", "testKey")); } @Test - @Disabled - public void testRead() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - storage.read(null, "testKey", String.class); - }); + void testRead() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> storage.read(null, "testKey", String.class)); assertEquals(StorageImpl.ERROR_BUCKET_NAME_REQUIRED, exception.getMessage()); - when(storageObjectConverter.read(any(), any())).thenThrow(mock(StorageException.class)); - assertThrows(StorageException.class, () -> { - storage.read("testBucket", "testKey", String.class); - }); + storageObjectConverter.readException = new StorageException("boom", new RuntimeException("cause")); + assertThrows(StorageException.class, () -> storage.read("testBucket", "testKey", String.class)); - when(storageObjectConverter.read(any(), any())).thenReturn(mock(String.class)); - assertNotNull(storage.read("testBucket", "testKey", String.class)); + storageObjectConverter.readException = null; + storageObjectConverter.readValue = "read-value"; + assertEquals("read-value", storage.read("testBucket", "testKey", String.class)); } @Test - @Disabled - public void testGetNamespaceName() { - when(objectStorageClient.getNamespace(any())).thenReturn(mock(GetNamespaceResponse.class)); - assertNotNull(storage.getNamespaceName()); + void testGetNamespaceName() { + assertEquals("testNamespace", storage.getNamespaceName()); assertNotNull(storage.getClient()); } } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageLocationTests.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageLocationTests.java index 7e2c9b77..1108c6f6 100644 --- a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageLocationTests.java +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageLocationTests.java @@ -50,18 +50,18 @@ void testSimpleStorageResource() { @Test void testResolveBucketName() { Exception exception = assertThrows(IllegalArgumentException.class, () -> { - StorageLocation.resolveBucketName("https://maacloud.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace"); + StorageLocation.resolveBucketName("https://example.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace"); }); assertTrue(exception.getMessage().contains(StorageLocation.ERROR_INVALID_BUCKET)); - String bucketName = StorageLocation.resolveBucketName("https://maacloud.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket/o/myobject"); + String bucketName = StorageLocation.resolveBucketName("https://example.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket/o/myobject"); assertThat(bucketName).isEqualTo("mybucket"); } @Test void testResolveObjectName() { - Exception exception = assertThrows(IllegalArgumentException.class, () -> StorageLocation.resolveObjectName("https://maacloud.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket")); + Exception exception = assertThrows(IllegalArgumentException.class, () -> StorageLocation.resolveObjectName("https://example.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket")); assertTrue(exception.getMessage().contains(StorageLocation.ERROR_OBJECT_REQUIRED)); - String objectName = StorageLocation.resolveObjectName("https://maacloud.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket/o/myobject"); + String objectName = StorageLocation.resolveObjectName("https://example.objectstorage.us-chicago-1.oci.customer-oci.com/n/namespace/b/mybucket/o/myobject"); assertThat(objectName).isEqualTo("myobject"); } diff --git a/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageTestSupport.java b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageTestSupport.java new file mode 100644 index 00000000..7b75571a --- /dev/null +++ b/spring-cloud-oci/spring-cloud-oci-storage/src/test/java/com/oracle/cloud/spring/storage/StorageTestSupport.java @@ -0,0 +1,188 @@ +/* + ** Copyright (c) 2023, 2026, Oracle and/or its affiliates. + ** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ + */ + +package com.oracle.cloud.spring.storage; + +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.CreateBucketRequest; +import com.oracle.bmc.objectstorage.requests.DeleteBucketRequest; +import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.requests.GetObjectRequest; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import com.oracle.bmc.objectstorage.responses.CreateBucketResponse; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import com.oracle.bmc.objectstorage.responses.GetObjectResponse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +final class StorageTestSupport { + + private StorageTestSupport() { + } + + static FakeObjectStorageClient newObjectStorageClient() { + return new FakeObjectStorageClient(); + } + + static final class FakeObjectStorageClient extends ObjectStorageClient { + private static final String PRIVATE_KEY = """ + -----BEGIN PRIVATE KEY----- + MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMg7GKI/0sbjirpT + 2UPcabmjDbWfO1ZsvLbIq6PgPVaW443yFFM9fYxUBS6a3MFx1rsOD0sSzWl1LsZx + Wc0bxi7KdQ+bjdZ5UBPwzrtxXDyp+ufWnv7yecV/TwmvHTqeaL+l5VbBMYXuiQnv + yhaLL10zMlEEvMk3GbQZVJ9DMs5nAgMBAAECgYEAglL0laY06n7vrJcmsqSjq9AU + /EHHvVjI+69hCCjLw7AyLBGEaSl8rfmB5fOl+8K8oMNl8NcsG5fJ+h+M85NASczi + 2GStDOZ4OI90f+B3/YPgCWfL26g/xe6cW2/oURHC1btDY04x7vw1W++6CB+XXfUD + GBbm/pHRv3qRxpBxxgECQQDxqHlg1F8v1d4DzqVTkIFH3oWbJVb2MVz/MoXuCdBA + yvyzVGu2JotNvAgPV66zW+6/bWk07GiRj6eeHOJuDKDvAkEA1B03HthHu+hCjgzc + kXUhYrOjSiKjQXKS21IJJ2o3H9lxh9Qkvnr1vfdOFttQEUBWXza2G3yEBs9GNr69 + lmU6CQJAGIkgecJWP8cZGY3bn1ZmqeNf8VajM6/jX03D5107tbhmW9bQcNgNAMF8 + mAIxDKji3rC/I85094J8ZENOghnqJQJAIibyEQ1Rv3eN/8EiYmkxjurNh8o77vW7 + n4R95NK9PWuNVAlcQS8bEhMXh6aYJa7uOTZd698IgvAspfPgIq75wQJBAOkx78wE + vOOmAEsvjjR5PfKoIA4wYDYocUcrC9Fz51pGc8dftoNR/6ml7/PNbuyS3oA4ZjPU + 2a62fTbqYKvhyoc= + -----END PRIVATE KEY----- + """; + + String namespace = "testNamespace"; + byte[] objectContent = "sample".getBytes(StandardCharsets.UTF_8); + CreateBucketRequest lastCreateBucketRequest; + DeleteBucketRequest lastDeleteBucketRequest; + DeleteObjectRequest lastDeleteObjectRequest; + GetObjectRequest lastGetObjectRequest; + + FakeObjectStorageClient() { + super(new TestAuthenticationDetailsProvider()); + } + + @Override + public GetNamespaceResponse getNamespace(GetNamespaceRequest request) { + return GetNamespaceResponse.builder() + .__httpStatusCode__(200) + .headers(Map.of()) + .value(namespace) + .build(); + } + + @Override + public GetObjectResponse getObject(GetObjectRequest request) { + lastGetObjectRequest = request; + return GetObjectResponse.builder() + .__httpStatusCode__(200) + .headers(Map.of()) + .contentLength((long) objectContent.length) + .inputStream(new ByteArrayInputStream(objectContent)) + .build(); + } + + @Override + public CreateBucketResponse createBucket(CreateBucketRequest request) { + lastCreateBucketRequest = request; + return CreateBucketResponse.builder() + .__httpStatusCode__(200) + .headers(Map.of()) + .location("/b/" + request.getCreateBucketDetails().getName()) + .build(); + } + + @Override + public com.oracle.bmc.objectstorage.responses.DeleteBucketResponse deleteBucket(DeleteBucketRequest request) { + lastDeleteBucketRequest = request; + return null; + } + + @Override + public com.oracle.bmc.objectstorage.responses.DeleteObjectResponse deleteObject(DeleteObjectRequest request) { + lastDeleteObjectRequest = request; + return null; + } + + private static final class TestAuthenticationDetailsProvider implements BasicAuthenticationDetailsProvider { + + @Override + public String getKeyId() { + return "ocid1.tenancy.oc1..test/ocid1.user.oc1..test/fingerprint"; + } + + @Override + public InputStream getPrivateKey() { + return new ByteArrayInputStream(PRIVATE_KEY.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getPassPhrase() { + return null; + } + + @Override + public char[] getPassphraseCharacters() { + return null; + } + } + } + + static final class RecordingStorageObjectUploader implements StorageObjectUploader { + int uploadCount; + PutObjectRequest lastRequest; + byte[] lastContent = new byte[0]; + + @Override + public void upload(ObjectStorageClient osClient, PutObjectRequest putObjectRequest, Path sourceFile) throws IOException { + uploadCount++; + lastRequest = putObjectRequest; + lastContent = Files.readAllBytes(sourceFile); + } + } + + static final class RecordingContentTypeResolver implements StorageContentTypeResolver { + private final String contentType; + String lastObjectName; + + RecordingContentTypeResolver(String contentType) { + this.contentType = contentType; + } + + @Override + public String resolveContentType(String fileName) { + lastObjectName = fileName; + return contentType; + } + } + + static final class RecordingStorageObjectConverter implements StorageObjectConverter { + byte[] writeBytes = "sample".getBytes(StandardCharsets.UTF_8); + RuntimeException readException; + Object readValue = "value"; + Object lastWrittenObject; + + @Override + public byte[] write(T object) { + lastWrittenObject = object; + return writeBytes; + } + + @SuppressWarnings("unchecked") + @Override + public T read(InputStream is, Class clazz) { + if (readException != null) { + throw readException; + } + return (T) readValue; + } + + @Override + public String contentType() { + return "application/json"; + } + } +}