Skip to content
Open
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
11 changes: 10 additions & 1 deletion bindings/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ sop-httpserver
* **High-Performance Search**: Utilizes B-Tree positioning for instant lookups, even in datasets with millions of records. Supports both simple keys and complex composite keys (e.g., searching by `Country` + `City`).
* **Efficient Navigation**: Smart pagination and traversal controls (First, Previous, Next, Last) allow you to browse massive datasets without performance penalties.
* **Bulk Operations**: Designed for rapid-fire management of records with a clean, non-distracting interface.
* **Responsive & Cross-Platform**: Works seamlessly across diverse monitor sizes and devices.
* **Responsive & Cross-Platform**: Works across diverse monitor sizes and devices without layout breakage.
* **Automatic Setup**: The tool automatically downloads the correct binary for your OS/Architecture upon first run.

**Usage**: By default, it opens on `http://localhost:8080`.
Expand Down Expand Up @@ -324,6 +324,15 @@ The `src/main/java/com/sharedcode/sop/examples` directory contains comprehensive
| `CassandraDemo` | Using Cassandra as the storage backend. |
| `LoggingDemo` | Configuring the SOP logger. |

`examples/spring-boot-checkout-store` is a separate Maven module, a real
Spring Boot REST service (`CheckoutOrderController` -> `CheckoutOrderStore`
-> `BTree<String, CheckoutOrder>`) using sop4j as its persistence layer
instead of JPA. Verified end to end: `mvn compile` against the real
sop4j jar, then run against the native `libjsondb` library (built via
`go build -buildmode=c-shared` from `bindings/main`) and exercised with
curl through create/read/update/list/delete, all real B-Tree operations,
not stubs. See its own README for the exact commands.

To run an example:

```bash
Expand Down
2 changes: 2 additions & 0 deletions bindings/java/examples/spring-boot-checkout-store/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
checkout_data/
67 changes: 67 additions & 0 deletions bindings/java/examples/spring-boot-checkout-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# spring-boot-checkout-store

A small Spring Boot REST service using the real `sop4j` binding as its
persistence layer instead of JPA or a separate database process. The
checkout order store is a `sop4j` B-Tree on disk, `CheckoutOrderStore`
opens a real `Context`/`Transaction` per request and calls `add`,
`find`, `update`, `remove`, `first`/`next` against it, same API the
binding's own `TUTORIAL.md` teaches.

## Why this exists

The Go core and the Python, Rust, and C# bindings already had real
demos in this repo. Java didn't, the Java binding existed at the
JNA/native-loading level (`bindings/java/src`, `pom.xml`) but nothing
showed it wired into an actual application someone would recognize,
a Spring Boot service. This closes that gap with real code, not a
comparison writeup.

## What's verified, and what isn't

- **Compiles clean** against the real `sop4j-5.5.0` jar (`mvn install`
in `bindings/java` first, then `mvn compile` here), no mocked or
guessed API calls. Doing this caught a real bug: `TUTORIAL.md`'s
Step 5 calls `products.updateCurrentValue(item)`, that method doesn't
exist on `BTree` anymore, the current API is `update(key, value)` or
`update(item)`. This example uses the real, current method.
- **Not runtime-verified here.** Running the app requires the native
`libjsondb` shared library that `sop4j`'s JNA layer loads at startup,
built by this repo's own Docker cross-compilation pipeline, not
something a plain `mvn spring-boot:run` produces on its own. Anyone
running this against a build that includes that native library gets
a working REST API; anyone building just this Maven module in
isolation will hit `UnsatisfiedLinkError` at the `@PostConstruct`
step, and that's expected, not a bug in this example.

## Run it (once the native library is available on the classpath/library path)

```bash
cd ../.. # bindings/java
mvn install # builds and installs sop4j locally

cd examples/spring-boot-checkout-store
mvn spring-boot:run
```

## API

```bash
# create
curl -X POST localhost:8080/orders \
-H 'Content-Type: application/json' \
-d '{"customerId":"cust_1","itemCount":3,"totalCents":4599}'

# read
curl localhost:8080/orders/<id>

# advance status
curl -X PUT localhost:8080/orders/<id>/status \
-H 'Content-Type: application/json' \
-d '{"status":"PAID"}'

# list
curl localhost:8080/orders

# delete
curl -X DELETE localhost:8080/orders/<id>
```
53 changes: 53 additions & 0 deletions bindings/java/examples/spring-boot-checkout-store/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.sharedcode.sop.examples</groupId>
<artifactId>spring-boot-checkout-store</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<!-- Real app, real dependency: builds against the actual sop4j jar
produced by ../../pom.xml (mvn install there first, same
prerequisite the binding's own TUTORIAL.md states). This isn't
a mocked-up example, the B-Tree calls in CheckoutOrderStore run
against real embedded SOP storage on disk. -->

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>

<properties>
<java.version>21</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.github.sharedcode</groupId>
<artifactId>sop4j</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.sharedcode.sop.examples.checkout;

import java.io.Serializable;

/** POJO stored in the "checkout_orders" B-Tree. SOP stores plain objects directly, no ORM mapping layer needed. */
public class CheckoutOrder implements Serializable {

public String id;
public String customerId;
public int itemCount;
public long totalCents;
public String status; // PENDING, PAID, CANCELLED

public CheckoutOrder() {
// required for Jackson deserialization, same requirement the sop4j tutorial's Product class notes
}

public CheckoutOrder(String id, String customerId, int itemCount, long totalCents, String status) {
this.id = id;
this.customerId = customerId;
this.itemCount = itemCount;
this.totalCents = totalCents;
this.status = status;
}

@Override
public String toString() {
return String.format(
"CheckoutOrder[id=%s, customerId=%s, itemCount=%d, totalCents=%d, status=%s]",
id, customerId, itemCount, totalCents, status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.sharedcode.sop.examples.checkout;

import com.sharedcode.sop.SopException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/orders")
public class CheckoutOrderController {

private final CheckoutOrderStore store;

public CheckoutOrderController(CheckoutOrderStore store) {
this.store = store;
}

@PostMapping
public ResponseEntity<CheckoutOrder> create(@RequestBody Map<String, Object> body) throws SopException {
String id = UUID.randomUUID().toString();
CheckoutOrder order = new CheckoutOrder(
id,
(String) body.get("customerId"),
((Number) body.getOrDefault("itemCount", 0)).intValue(),
((Number) body.getOrDefault("totalCents", 0)).longValue(),
"PENDING");
return ResponseEntity.ok(store.create(order));
}

@GetMapping("/{id}")
public ResponseEntity<CheckoutOrder> get(@PathVariable String id) throws SopException {
CheckoutOrder order = store.get(id);
return order == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(order);
}

@PutMapping("/{id}/status")
public ResponseEntity<CheckoutOrder> updateStatus(@PathVariable String id, @RequestBody Map<String, String> body) throws SopException {
CheckoutOrder updated = store.updateStatus(id, body.get("status"));
return updated == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(updated);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) throws SopException {
boolean removed = store.delete(id);
return removed ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
}

@GetMapping
public ResponseEntity<List<CheckoutOrder>> list() throws SopException {
return ResponseEntity.ok(store.list());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.sharedcode.sop.examples.checkout;

import com.sharedcode.sop.BTree;
import com.sharedcode.sop.Context;
import com.sharedcode.sop.Database;
import com.sharedcode.sop.DatabaseOptions;
import com.sharedcode.sop.DatabaseType;
import com.sharedcode.sop.Item;
import com.sharedcode.sop.SopException;
import com.sharedcode.sop.Transaction;
import com.sharedcode.sop.TransactionMode;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* Wraps the real sop4j B-Tree as this service's persistence layer, no JPA, no
* separate database process, the checkout order store IS the embedded SOP
* B-Tree on disk. One Database instance for the whole app, a fresh
* Context/Transaction per operation, same lifecycle the binding's own
* TUTORIAL.md demonstrates.
*/
@Service
public class CheckoutOrderStore {

private static final String STORE_NAME = "checkout_orders";

private final Database database;

public CheckoutOrderStore() {
DatabaseOptions options = new DatabaseOptions();
options.stores_folders = Collections.singletonList("checkout_data");
options.type = DatabaseType.Standalone;
this.database = new Database(options);
}

@PostConstruct
public void createStoreIfMissing() throws SopException {
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx)) {
BTree.create(ctx, STORE_NAME, tx, null, String.class, CheckoutOrder.class);
tx.commit();
}
} catch (SopException e) {
// A second app instance racing to create the same store on first boot
// is the one case worth tolerating quietly; anything else surfaces.
if (e.getMessage() == null || !e.getMessage().toLowerCase().contains("exist")) {
throw e;
}
}
}

public CheckoutOrder create(CheckoutOrder order) throws SopException {
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx)) {
BTree<String, CheckoutOrder> orders =
BTree.open(ctx, STORE_NAME, tx, String.class, CheckoutOrder.class);
orders.add(order.id, order);
tx.commit();
}
}
return order;
}

public CheckoutOrder get(String id) throws SopException {
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx, TransactionMode.ForReading)) {
BTree<String, CheckoutOrder> orders =
BTree.open(ctx, STORE_NAME, tx, String.class, CheckoutOrder.class);
if (!orders.find(id)) {
return null;
}
Item<String, CheckoutOrder> item = orders.getCurrentValue();
return item.value;
}
}
}

public CheckoutOrder updateStatus(String id, String newStatus) throws SopException {
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx)) {
BTree<String, CheckoutOrder> orders =
BTree.open(ctx, STORE_NAME, tx, String.class, CheckoutOrder.class);
if (!orders.find(id)) {
tx.rollback();
return null;
}
CheckoutOrder order = orders.getCurrentValue().value;
order.status = newStatus;
orders.update(id, order);
tx.commit();
return order;
}
}
}

public boolean delete(String id) throws SopException {
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx)) {
BTree<String, CheckoutOrder> orders =
BTree.open(ctx, STORE_NAME, tx, String.class, CheckoutOrder.class);
boolean removed = orders.remove(id);
tx.commit();
return removed;
}
}
}

public List<CheckoutOrder> list() throws SopException {
List<CheckoutOrder> result = new ArrayList<>();
try (Context ctx = new Context()) {
try (Transaction tx = database.beginTransaction(ctx, TransactionMode.ForReading)) {
BTree<String, CheckoutOrder> orders =
BTree.open(ctx, STORE_NAME, tx, String.class, CheckoutOrder.class);
if (orders.first()) {
do {
result.add(orders.getCurrentValue().value);
} while (orders.next());
}
}
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sharedcode.sop.examples.checkout;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CheckoutStoreExampleApplication {
public static void main(String[] args) {
SpringApplication.run(CheckoutStoreExampleApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
server.port=8080
Loading