From 4ff555e6d5dee7bccf6b4ef5ec64b160ea106158 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 15 Sep 2026 15:01:53 -0700 Subject: [PATCH] Serve GET /shares under the configured protocol prefix. Mount the first recipient protocol route so opensharing.protocol-prefix is used; the list is empty until share storage exists. --- .../config/OpenSharingProperties.java | 22 +++++++++++++ .../io/opensharing/http/ListResponse.java | 13 ++++++++ .../opensharing/http/ProtocolMediaType.java | 12 +++++++ .../java/io/opensharing/protocol/Share.java | 9 +++++ .../opensharing/serving/SharesController.java | 22 +++++++++++++ .../serving/SharesControllerTest.java | 33 +++++++++++++++++++ 6 files changed, 111 insertions(+) create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/http/ListResponse.java create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/http/ProtocolMediaType.java create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/protocol/Share.java create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/serving/SharesController.java create mode 100644 server/opensharing-server/src/test/java/io/opensharing/serving/SharesControllerTest.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java b/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java index e80b50c35..e3b6ed00b 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java @@ -6,9 +6,20 @@ @ConfigurationProperties(prefix = "opensharing") public class OpenSharingProperties { + /** URL prefix the recipient-facing protocol endpoints are mounted under. */ + private String protocolPrefix = "/api/1.0/opensharing"; + private final Hosting hosting = new Hosting(); private final Catalog catalog = new Catalog(); + public String getProtocolPrefix() { + return protocolPrefix; + } + + public void setProtocolPrefix(String protocolPrefix) { + this.protocolPrefix = prefix(protocolPrefix); + } + public Hosting getHosting() { return hosting; } @@ -17,6 +28,17 @@ public Catalog getCatalog() { return catalog; } + /** + * A url prefix without its trailing slash, kept that way here so that everything appending to + * one — a filter's url pattern, an OpenAPI path match, a route the server builds — appends to a + * known shape instead of each trimming first. + */ + private static String prefix(String value) { + return value != null && value.length() > 1 && value.endsWith("/") + ? value.substring(0, value.length() - 1) + : value; + } + public static class Hosting { public enum Mode { diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/http/ListResponse.java b/server/opensharing-server-core/src/main/java/io/opensharing/http/ListResponse.java new file mode 100644 index 000000000..8092a6ef5 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/http/ListResponse.java @@ -0,0 +1,13 @@ +package io.opensharing.http; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; + +/** Paginated list envelope shared by every {@code list} endpoint. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ListResponse(List items, String nextPageToken) { + + public static ListResponse of(List items) { + return new ListResponse<>(items, null); + } +} diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/http/ProtocolMediaType.java b/server/opensharing-server-core/src/main/java/io/opensharing/http/ProtocolMediaType.java new file mode 100644 index 000000000..ccdd19825 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/http/ProtocolMediaType.java @@ -0,0 +1,12 @@ +package io.opensharing.http; + +/** + * Content types the spec requires on successful protocol responses. Both are spelled the way a + * parsed media type renders, so the published API lists each of them once. + */ +public final class ProtocolMediaType { + + public static final String JSON_UTF8 = "application/json;charset=utf-8"; + + private ProtocolMediaType() {} +} diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/protocol/Share.java b/server/opensharing-server-core/src/main/java/io/opensharing/protocol/Share.java new file mode 100644 index 000000000..f402e5767 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/protocol/Share.java @@ -0,0 +1,9 @@ +package io.opensharing.protocol; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Map; + +/** A share as returned by {@code GET /shares}. */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Share( + String name, String id, String displayName, String comment, Map properties) {} diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/serving/SharesController.java b/server/opensharing-server-core/src/main/java/io/opensharing/serving/SharesController.java new file mode 100644 index 000000000..b183455d1 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/serving/SharesController.java @@ -0,0 +1,22 @@ +package io.opensharing.serving; + +import io.opensharing.http.ListResponse; +import io.opensharing.http.ProtocolMediaType; +import io.opensharing.protocol.Share; +import java.util.List; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** Recipient-facing {@code GET /shares}. Persistence and recipient auth come in later slices. */ +@RestController +@RequestMapping( + value = "${opensharing.protocol-prefix}/shares", + produces = ProtocolMediaType.JSON_UTF8) +public class SharesController { + + @GetMapping + public ListResponse listShares() { + return ListResponse.of(List.of()); + } +} diff --git a/server/opensharing-server/src/test/java/io/opensharing/serving/SharesControllerTest.java b/server/opensharing-server/src/test/java/io/opensharing/serving/SharesControllerTest.java new file mode 100644 index 000000000..fdecc6b49 --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/serving/SharesControllerTest.java @@ -0,0 +1,33 @@ +package io.opensharing.serving; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import io.opensharing.http.ProtocolMediaType; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest( + properties = { + "spring.datasource.url=jdbc:h2:mem:list-shares;DB_CLOSE_DELAY=-1", + "opensharing.catalog.type=local", + "opensharing.catalog.local.file=classpath:local-catalog.yml" + }) +@AutoConfigureMockMvc +class SharesControllerTest { + + @Autowired private MockMvc mvc; + + @Test + void listsNoSharesUntilShareStorageExists() throws Exception { + mvc.perform(get("/api/1.0/opensharing/shares")) + .andExpect(status().isOk()) + .andExpect(content().contentType(ProtocolMediaType.JSON_UTF8)) + .andExpect(jsonPath("$.items").isEmpty()); + } +}