Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions site/docs/oci/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,49 @@ 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;
```

Comment thread
anders-swanson marked this conversation as resolved.
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

| Name | Description | Required | Default |
| --- | --- | --- | --- |
| `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).
10 changes: 8 additions & 2 deletions site/docs/releases/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
- Upgrade third-party dependencies and migrated to Spring Boot 4
Original file line number Diff line number Diff line change
@@ -1 +1 @@
org.springframework.boot.env.EnvironmentPostProcessor=com.oracle.cloud.spring.vault.VaultEnvironmentPostProcessor
org.springframework.boot.EnvironmentPostProcessor=com.oracle.cloud.spring.vault.VaultEnvironmentPostProcessor
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <COMPARTMENT_OCID>
OCI_NAMESPACE = <NAMESPACE>
OCI_BUCKET = <BUCKET_NAME>
OCI_OBJECT = <OBJECT_NAME>
```
1. Start the application using the following command from sample root directory.
```
Expand All @@ -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
Expand All @@ -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.
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.
Original file line number Diff line number Diff line change
@@ -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/
*/

Expand All @@ -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;
Expand All @@ -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")
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
/*
** 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 {

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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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";
}
Expand All @@ -65,5 +152,3 @@ public String getMessage() {
}
}
}


Loading
Loading