From b44fe77029771c4ad6d56bc5ef47c6909f7c20ef Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Fri, 11 Sep 2026 15:28:02 -0700 Subject: [PATCH 01/18] Wire the local catalog into the server --- .../opensharing/OpenSharingApplication.java | 9 ++++ .../config/OpenSharingProperties.java | 45 ++++++++++++++++++ server/opensharing-server/pom.xml | 1 - .../io/opensharing/OpenSharingServer.java | 13 ++++++ .../config/CatalogConfiguration.java | 46 +++++++++++++++++++ .../config/CatalogConfigurationTest.java | 35 ++++++++++++++ 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java create mode 100644 server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java create mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java create mode 100644 server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java b/server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java new file mode 100644 index 000000000..1a4d46c46 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java @@ -0,0 +1,9 @@ +package io.opensharing; + +import io.opensharing.config.OpenSharingProperties; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication +@EnableConfigurationProperties(OpenSharingProperties.class) +public class OpenSharingApplication {} 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 new file mode 100644 index 000000000..6022be153 --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java @@ -0,0 +1,45 @@ +package io.opensharing.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** OpenSharing server configuration. */ +@ConfigurationProperties(prefix = "opensharing") +public class OpenSharingProperties { + + private final Catalog catalog = new Catalog(); + + public Catalog getCatalog() { + return catalog; + } + + public static class Catalog { + + private String type = "local"; + private final Local local = new Local(); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Local getLocal() { + return local; + } + + public static class Local { + + private String file = "classpath:local-catalog.yml"; + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + } + } +} diff --git a/server/opensharing-server/pom.xml b/server/opensharing-server/pom.xml index 99c5ad1f7..5f9639420 100644 --- a/server/opensharing-server/pom.xml +++ b/server/opensharing-server/pom.xml @@ -81,7 +81,6 @@ io.opensharing.OpenSharingServer exec - true diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java new file mode 100644 index 000000000..bb8c29aac --- /dev/null +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -0,0 +1,13 @@ +package io.opensharing; + +import org.springframework.boot.SpringApplication; + +/** Runnable standalone OpenSharing server. */ +public final class OpenSharingServer { + + private OpenSharingServer() {} + + public static void main(String[] args) { + SpringApplication.run(OpenSharingApplication.class, args); + } +} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java new file mode 100644 index 000000000..230050a2c --- /dev/null +++ b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java @@ -0,0 +1,46 @@ +package io.opensharing.config; + +import io.opensharing.catalog.CatalogConnector; +import io.opensharing.catalog.local.LocalCatalogConnector; +import io.opensharing.catalog.local.LocalCatalogFile; +import io.opensharing.catalog.local.LocalCatalogLoader; +import io.opensharing.exception.CatalogException; +import java.io.IOException; +import java.io.InputStream; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** Configures the catalog used by the standalone server. */ +@Configuration +public class CatalogConfiguration { + + @Bean + @ConditionalOnMissingBean(CatalogConnector.class) + public CatalogConnector catalogConnector( + OpenSharingProperties properties, ResourceLoader resourceLoader) { + OpenSharingProperties.Catalog catalog = properties.getCatalog(); + if (!LocalCatalogConnector.NAME.equalsIgnoreCase(catalog.getType())) { + throw new IllegalStateException( + "unknown opensharing.catalog.type '" + + catalog.getType() + + "'; this build ships '" + + LocalCatalogConnector.NAME + + "'"); + } + + String location = catalog.getLocal().getFile(); + Resource resource = resourceLoader.getResource(location); + if (!resource.exists()) { + throw new IllegalStateException("local catalog file '" + location + "' does not exist"); + } + try (InputStream in = resource.getInputStream()) { + LocalCatalogFile file = LocalCatalogLoader.load(in, location); + return new LocalCatalogConnector(file); + } catch (IOException e) { + throw new CatalogException("failed to read local catalog file " + location, e); + } + } +} diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java new file mode 100644 index 000000000..65ee55c68 --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -0,0 +1,35 @@ +package io.opensharing.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opensharing.catalog.AssetLookup; +import io.opensharing.catalog.AssetType; +import io.opensharing.catalog.CatalogCaller; +import io.opensharing.catalog.CatalogConnector; +import io.opensharing.catalog.TableFormat; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest( + properties = { + "spring.datasource.url=jdbc:h2:mem:catalog-configuration;DB_CLOSE_DELAY=-1", + "opensharing.catalog.type=local", + "opensharing.catalog.local.file=classpath:local-catalog.yml" + }) +class CatalogConfigurationTest { + + @Autowired private CatalogConnector catalog; + + @Test + void loadsTheConfiguredLocalCatalog() { + CatalogCaller caller = CatalogCaller.withBearerToken("alice@example.com", "token"); + + assertEquals("local", catalog.name()); + assertEquals( + TableFormat.DELTA, + catalog + .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), caller) + .format()); + } +} From eb7c42c9b4fa132beb08a1692de6e6c98f2a2e78 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 14 Sep 2026 10:51:10 -0700 Subject: [PATCH 02/18] Select the catalog connector with a switch. --- .../config/CatalogConfiguration.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java index 230050a2c..c79fd56e9 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java @@ -7,6 +7,7 @@ import io.opensharing.exception.CatalogException; import java.io.IOException; import java.io.InputStream; +import java.util.Locale; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,16 +23,22 @@ public class CatalogConfiguration { public CatalogConnector catalogConnector( OpenSharingProperties properties, ResourceLoader resourceLoader) { OpenSharingProperties.Catalog catalog = properties.getCatalog(); - if (!LocalCatalogConnector.NAME.equalsIgnoreCase(catalog.getType())) { - throw new IllegalStateException( - "unknown opensharing.catalog.type '" - + catalog.getType() - + "'; this build ships '" - + LocalCatalogConnector.NAME - + "'"); - } + String type = catalog.getType() == null ? "" : catalog.getType().trim().toLowerCase(Locale.ROOT); + return switch (type) { + case LocalCatalogConnector.NAME -> localConnector(catalog.getLocal(), resourceLoader); + default -> + throw new IllegalStateException( + "unknown opensharing.catalog.type '" + + catalog.getType() + + "'; this build ships '" + + LocalCatalogConnector.NAME + + "'"); + }; + } - String location = catalog.getLocal().getFile(); + private CatalogConnector localConnector( + OpenSharingProperties.Catalog.Local config, ResourceLoader resourceLoader) { + String location = config.getFile(); Resource resource = resourceLoader.getResource(location); if (!resource.exists()) { throw new IllegalStateException("local catalog file '" + location + "' does not exist"); From cdcd5ee2f314685fc31b61cdd96f3a8830aeb677 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 14 Sep 2026 11:03:26 -0700 Subject: [PATCH 03/18] Take the local catalog file path from application.yml. --- .../main/java/io/opensharing/config/OpenSharingProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6022be153..72e5a3f91 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 @@ -31,7 +31,7 @@ public Local getLocal() { public static class Local { - private String file = "classpath:local-catalog.yml"; + private String file; public String getFile() { return file; From 1679934f225059c281c9f8e0b7cd721089750ffe Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 14 Sep 2026 11:04:17 -0700 Subject: [PATCH 04/18] Take the catalog type from application.yml. --- .../main/java/io/opensharing/config/OpenSharingProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 72e5a3f91..ed888de62 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 @@ -14,7 +14,7 @@ public Catalog getCatalog() { public static class Catalog { - private String type = "local"; + private String type; private final Local local = new Local(); public String getType() { From ad192ad81115d4298161f10c1fb39d2dc76edd6e Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Wed, 16 Sep 2026 14:51:23 -0700 Subject: [PATCH 05/18] Use UserContext in catalog configuration test --- .../io/opensharing/config/CatalogConfigurationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 65ee55c68..3e85b9e15 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -2,9 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import io.opensharing.auth.UserContext; import io.opensharing.catalog.AssetLookup; import io.opensharing.catalog.AssetType; -import io.opensharing.catalog.CatalogCaller; import io.opensharing.catalog.CatalogConnector; import io.opensharing.catalog.TableFormat; import org.junit.jupiter.api.Test; @@ -23,13 +23,13 @@ class CatalogConfigurationTest { @Test void loadsTheConfiguredLocalCatalog() { - CatalogCaller caller = CatalogCaller.withBearerToken("alice@example.com", "token"); + UserContext user = new UserContext("alice@example.com", "alice@example.com"); assertEquals("local", catalog.name()); assertEquals( TableFormat.DELTA, catalog - .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), caller) + .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), user) .format()); } } From 0e7831e9393a185cb5b34744f428eb56d79d1a7c Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 22 Sep 2026 09:35:37 -0700 Subject: [PATCH 06/18] Pass AuthContext from the catalog configuration test. --- .../java/io/opensharing/config/CatalogConfigurationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 3e85b9e15..d0b5c5a0b 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import io.opensharing.auth.AuthContext; import io.opensharing.auth.UserContext; import io.opensharing.catalog.AssetLookup; import io.opensharing.catalog.AssetType; @@ -29,7 +30,7 @@ void loadsTheConfiguredLocalCatalog() { assertEquals( TableFormat.DELTA, catalog - .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), user) + .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) .format()); } } From b132593d4135ab0b273a88eb56c2d3f5a18ec803 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Fri, 2 Oct 2026 09:07:53 -0700 Subject: [PATCH 07/18] Adapt the catalog configuration test to the revised catalog SPI. --- .../opensharing/config/CatalogConfigurationTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index d0b5c5a0b..64f54b9a9 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -4,10 +4,10 @@ import io.opensharing.auth.AuthContext; import io.opensharing.auth.UserContext; -import io.opensharing.catalog.AssetLookup; +import io.opensharing.catalog.Asset; import io.opensharing.catalog.AssetType; import io.opensharing.catalog.CatalogConnector; -import io.opensharing.catalog.TableFormat; +import io.opensharing.catalog.DataSourceFormat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -24,13 +24,13 @@ class CatalogConfigurationTest { @Test void loadsTheConfiguredLocalCatalog() { - UserContext user = new UserContext("alice@example.com", "alice@example.com"); + UserContext user = UserContext.fromUserIdAndName("alice@example.com", "alice@example.com"); assertEquals("local", catalog.name()); assertEquals( - TableFormat.DELTA, + DataSourceFormat.DELTA, catalog - .resolveAsset(AssetLookup.of(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) - .format()); + .resolveAsset(Asset.of(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) + .dataSourceFormat()); } } From 7c9689d0fca010ee6b27639319853612555b2df0 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 5 Oct 2026 15:57:02 -0700 Subject: [PATCH 08/18] Construct Asset directly now that Asset.of is gone. --- .../java/io/opensharing/config/CatalogConfigurationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 64f54b9a9..72ad9f6fe 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -30,7 +30,7 @@ void loadsTheConfiguredLocalCatalog() { assertEquals( DataSourceFormat.DELTA, catalog - .resolveAsset(Asset.of(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) + .resolveAsset(new Asset(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) .dataSourceFormat()); } } From 2732853bf3d53e2bad71182a386f28292a39ce75 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 5 Oct 2026 16:25:09 -0700 Subject: [PATCH 09/18] Read the configured table format from TableProperties. --- .../opensharing/config/CatalogConfigurationTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 72ad9f6fe..6e3298faa 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -8,6 +8,7 @@ import io.opensharing.catalog.AssetType; import io.opensharing.catalog.CatalogConnector; import io.opensharing.catalog.DataSourceFormat; +import io.opensharing.catalog.TableProperties; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -27,10 +28,11 @@ void loadsTheConfiguredLocalCatalog() { UserContext user = UserContext.fromUserIdAndName("alice@example.com", "alice@example.com"); assertEquals("local", catalog.name()); - assertEquals( - DataSourceFormat.DELTA, - catalog - .resolveAsset(new Asset(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) - .dataSourceFormat()); + TableProperties table = + (TableProperties) + catalog + .resolveAsset(new Asset(AssetType.TABLE, "main.sales.table1"), AuthContext.of(user)) + .additionalProperties(); + assertEquals(DataSourceFormat.DELTA, table.dataSourceFormat()); } } From 63b3205173b36567cd02aed8c59ae67da3f2fddd Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 6 Oct 2026 11:47:30 -0700 Subject: [PATCH 10/18] Boot the standalone server from opensharing-server, not server-core. --- .../src/main/java/io/opensharing/OpenSharingApplication.java | 0 .../main/java/io/opensharing/config/OpenSharingProperties.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename server/{opensharing-server-core => opensharing-server}/src/main/java/io/opensharing/OpenSharingApplication.java (100%) rename server/{opensharing-server-core => opensharing-server}/src/main/java/io/opensharing/config/OpenSharingProperties.java (100%) diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java similarity index 100% rename from server/opensharing-server-core/src/main/java/io/opensharing/OpenSharingApplication.java rename to server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java similarity index 100% rename from server/opensharing-server-core/src/main/java/io/opensharing/config/OpenSharingProperties.java rename to server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java From de1f264b6cf815727dd0c6567138df98587c4fa9 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 6 Oct 2026 12:12:53 -0700 Subject: [PATCH 11/18] Build OpenSharing from host-supplied parts so hosts can embed it. --- .../io/opensharing/runtime/OpenSharing.java | 45 +++++++++++++++++++ .../opensharing/runtime/OpenSharingTest.java | 30 +++++++++++++ .../config/OpenSharingConfiguration.java | 16 +++++++ .../config/CatalogConfigurationTest.java | 8 ++++ 4 files changed, 99 insertions(+) create mode 100644 server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java create mode 100644 server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java create mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java new file mode 100644 index 000000000..ebd01e44e --- /dev/null +++ b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java @@ -0,0 +1,45 @@ +package io.opensharing.runtime; + +import io.opensharing.catalog.CatalogConnector; + +/** + * OpenSharing assembled from what its host supplies. The standalone server builds one from its + * Spring beans. An embedding host, such as Unity Catalog, builds one directly, serves it through + * its own HTTP stack, and authenticates callers itself. + */ +public final class OpenSharing { + + private final CatalogConnector catalog; + + private OpenSharing(CatalogConnector catalog) { + this.catalog = catalog; + } + + public static Builder builder() { + return new Builder(); + } + + public CatalogConnector catalog() { + return catalog; + } + + public static final class Builder { + + private CatalogConnector catalog; + + private Builder() {} + + /** The host's catalog integration (required). */ + public Builder catalog(CatalogConnector catalog) { + this.catalog = catalog; + return this; + } + + public OpenSharing build() { + if (catalog == null) { + throw new IllegalStateException("OpenSharing requires a CatalogConnector from the host"); + } + return new OpenSharing(catalog); + } + } +} diff --git a/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java b/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java new file mode 100644 index 000000000..e1c4140fe --- /dev/null +++ b/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java @@ -0,0 +1,30 @@ +package io.opensharing.runtime; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.opensharing.catalog.CatalogConnector; +import java.lang.reflect.Proxy; +import org.junit.jupiter.api.Test; + +class OpenSharingTest { + + @Test + void refusesToBuildWithoutACatalogConnector() { + assertThrows(IllegalStateException.class, () -> OpenSharing.builder().build()); + } + + @Test + void buildsWithTheHostCatalog() { + CatalogConnector catalog = + (CatalogConnector) + Proxy.newProxyInstance( + CatalogConnector.class.getClassLoader(), + new Class[] {CatalogConnector.class}, + (proxy, method, args) -> { + throw new UnsupportedOperationException(); + }); + + assertSame(catalog, OpenSharing.builder().catalog(catalog).build().catalog()); + } +} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java new file mode 100644 index 000000000..72138065d --- /dev/null +++ b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java @@ -0,0 +1,16 @@ +package io.opensharing.config; + +import io.opensharing.catalog.CatalogConnector; +import io.opensharing.runtime.OpenSharing; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** Builds the standalone server's {@link OpenSharing} from its Spring beans. */ +@Configuration +public class OpenSharingConfiguration { + + @Bean + public OpenSharing openSharing(CatalogConnector catalog) { + return OpenSharing.builder().catalog(catalog).build(); + } +} diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 6e3298faa..6f03adcde 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -1,6 +1,7 @@ package io.opensharing.config; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import io.opensharing.auth.AuthContext; import io.opensharing.auth.UserContext; @@ -9,6 +10,7 @@ import io.opensharing.catalog.CatalogConnector; import io.opensharing.catalog.DataSourceFormat; import io.opensharing.catalog.TableProperties; +import io.opensharing.runtime.OpenSharing; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -22,6 +24,7 @@ class CatalogConfigurationTest { @Autowired private CatalogConnector catalog; + @Autowired private OpenSharing openSharing; @Test void loadsTheConfiguredLocalCatalog() { @@ -35,4 +38,9 @@ void loadsTheConfiguredLocalCatalog() { .additionalProperties(); assertEquals(DataSourceFormat.DELTA, table.dataSourceFormat()); } + + @Test + void buildsOpenSharingWithTheConfiguredCatalog() { + assertSame(catalog, openSharing.catalog()); + } } From 4f318f5fefdb4f4959d488b2b4349fc920e35ad3 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 14:59:47 -0700 Subject: [PATCH 12/18] Serve the standalone server with Armeria instead of Spring Boot. --- .../io/opensharing/runtime/OpenSharing.java | 2 +- server/opensharing-server/pom.xml | 79 ++++++------ .../opensharing/OpenSharingApplication.java | 9 -- .../io/opensharing/OpenSharingServer.java | 53 +++++++- .../config/CatalogConfiguration.java | 57 ++++++--- .../config/OpenSharingConfiguration.java | 12 +- .../config/OpenSharingProperties.java | 5 +- .../io/opensharing/config/ServerSettings.java | 119 ++++++++++++++++++ .../src/main/resources/application.yml | 41 +----- .../src/main/resources/logback.xml | 15 +++ .../config/CatalogConfigurationTest.java | 42 ++++--- .../config/ServerSettingsTest.java | 56 +++++++++ server/pom.xml | 50 ++++++-- 13 files changed, 394 insertions(+), 146 deletions(-) delete mode 100644 server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java create mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java create mode 100644 server/opensharing-server/src/main/resources/logback.xml create mode 100644 server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java diff --git a/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java index ebd01e44e..d699b200e 100644 --- a/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java +++ b/server/opensharing-server-core/src/main/java/io/opensharing/runtime/OpenSharing.java @@ -4,7 +4,7 @@ /** * OpenSharing assembled from what its host supplies. The standalone server builds one from its - * Spring beans. An embedding host, such as Unity Catalog, builds one directly, serves it through + * settings. An embedding host, such as Unity Catalog, builds one directly, serves it through * its own HTTP stack, and authenticates callers itself. */ public final class OpenSharing { diff --git a/server/opensharing-server/pom.xml b/server/opensharing-server/pom.xml index 5f9639420..f5f18da67 100644 --- a/server/opensharing-server/pom.xml +++ b/server/opensharing-server/pom.xml @@ -22,21 +22,8 @@ - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-configuration-processor - true + com.linecorp.armeria + armeria @@ -45,30 +32,14 @@ - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc.version} - - - - com.h2database - h2 - runtime - - - org.postgresql - postgresql - runtime - - - com.mysql - mysql-connector-j + ch.qos.logback + logback-classic runtime - org.springframework.boot - spring-boot-starter-test + org.junit.jupiter + junit-jupiter test @@ -76,16 +47,44 @@ - org.springframework.boot - spring-boot-maven-plugin + org.apache.maven.plugins + maven-jar-plugin + + + + io.opensharing.OpenSharingServer + + + + + + + org.apache.maven.plugins + maven-shade-plugin - io.opensharing.OpenSharingServer - exec + true + exec + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package - repackage + shade diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java deleted file mode 100644 index 1a4d46c46..000000000 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingApplication.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.opensharing; - -import io.opensharing.config.OpenSharingProperties; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; - -@SpringBootApplication -@EnableConfigurationProperties(OpenSharingProperties.class) -public class OpenSharingApplication {} diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java index bb8c29aac..d13d5a4e0 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -1,13 +1,56 @@ package io.opensharing; -import org.springframework.boot.SpringApplication; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.server.Server; +import io.opensharing.catalog.CatalogConnector; +import io.opensharing.config.CatalogConfiguration; +import io.opensharing.config.OpenSharingConfiguration; +import io.opensharing.config.ServerSettings; +import io.opensharing.runtime.OpenSharing; /** Runnable standalone OpenSharing server. */ -public final class OpenSharingServer { +public final class OpenSharingServer implements AutoCloseable { - private OpenSharingServer() {} + private final Server server; + private final OpenSharing openSharing; - public static void main(String[] args) { - SpringApplication.run(OpenSharingApplication.class, args); + private OpenSharingServer(Server server, OpenSharing openSharing) { + this.server = server; + this.openSharing = openSharing; + } + + public static void main(String[] args) throws InterruptedException { + OpenSharingServer server = start(ServerSettings.load(args, System.getenv())); + server.server.closeOnJvmShutdown(); + server.server.blockUntilShutdown(); + } + + /** Builds the server from {@code settings} and returns once it is listening. */ + public static OpenSharingServer start(ServerSettings settings) { + CatalogConnector catalog = CatalogConfiguration.catalogConnector(settings.getOpensharing()); + OpenSharing openSharing = OpenSharingConfiguration.openSharing(catalog); + Server server = + Server.builder() + .http(settings.getServer().getPort()) + // Armeria refuses to start without a service; routes nothing serves are not found. + .serviceUnder("/", (ctx, req) -> HttpResponse.of(HttpStatus.NOT_FOUND)) + .build(); + server.start().join(); + return new OpenSharingServer(server, openSharing); + } + + /** The port the server listens on, which it picks itself when configured with 0. */ + public int port() { + return server.activeLocalPort(); + } + + public OpenSharing openSharing() { + return openSharing; + } + + @Override + public void close() { + server.stop().join(); } } diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java index c79fd56e9..62e7f2c19 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java @@ -7,25 +7,24 @@ import io.opensharing.exception.CatalogException; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.Locale; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -/** Configures the catalog used by the standalone server. */ -@Configuration -public class CatalogConfiguration { +/** Builds the catalog used by the standalone server. */ +public final class CatalogConfiguration { - @Bean - @ConditionalOnMissingBean(CatalogConnector.class) - public CatalogConnector catalogConnector( - OpenSharingProperties properties, ResourceLoader resourceLoader) { + private static final String CLASSPATH = "classpath:"; + private static final String FILE = "file:"; + + private CatalogConfiguration() {} + + public static CatalogConnector catalogConnector(OpenSharingProperties properties) { OpenSharingProperties.Catalog catalog = properties.getCatalog(); String type = catalog.getType() == null ? "" : catalog.getType().trim().toLowerCase(Locale.ROOT); return switch (type) { - case LocalCatalogConnector.NAME -> localConnector(catalog.getLocal(), resourceLoader); + case LocalCatalogConnector.NAME -> localConnector(catalog.getLocal()); default -> throw new IllegalStateException( "unknown opensharing.catalog.type '" @@ -36,18 +35,38 @@ public CatalogConnector catalogConnector( }; } - private CatalogConnector localConnector( - OpenSharingProperties.Catalog.Local config, ResourceLoader resourceLoader) { + private static CatalogConnector localConnector(OpenSharingProperties.Catalog.Local config) { String location = config.getFile(); - Resource resource = resourceLoader.getResource(location); - if (!resource.exists()) { - throw new IllegalStateException("local catalog file '" + location + "' does not exist"); + if (location == null || location.isBlank()) { + throw new IllegalStateException("opensharing.catalog.local.file is required"); } - try (InputStream in = resource.getInputStream()) { + try (InputStream in = open(location)) { LocalCatalogFile file = LocalCatalogLoader.load(in, location); return new LocalCatalogConnector(file); } catch (IOException e) { throw new CatalogException("failed to read local catalog file " + location, e); } } + + // A classpath: resource, or a file path with or without file:. + private static InputStream open(String location) throws IOException { + InputStream in; + if (location.startsWith(CLASSPATH)) { + in = + CatalogConfiguration.class + .getClassLoader() + .getResourceAsStream(location.substring(CLASSPATH.length()).replaceFirst("^/", "")); + } else { + Path path = Path.of(location.startsWith(FILE) ? location.substring(FILE.length()) : location); + try { + in = Files.newInputStream(path); + } catch (NoSuchFileException e) { + in = null; + } + } + if (in == null) { + throw new IllegalStateException("local catalog file '" + location + "' does not exist"); + } + return in; + } } diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java index 72138065d..7679d6164 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java @@ -2,15 +2,13 @@ import io.opensharing.catalog.CatalogConnector; import io.opensharing.runtime.OpenSharing; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -/** Builds the standalone server's {@link OpenSharing} from its Spring beans. */ -@Configuration -public class OpenSharingConfiguration { +/** Builds the standalone server's {@link OpenSharing} from its settings. */ +public final class OpenSharingConfiguration { - @Bean - public OpenSharing openSharing(CatalogConnector catalog) { + private OpenSharingConfiguration() {} + + public static OpenSharing openSharing(CatalogConnector catalog) { return OpenSharing.builder().catalog(catalog).build(); } } diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java index ed888de62..65d557798 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java @@ -1,9 +1,6 @@ package io.opensharing.config; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** OpenSharing server configuration. */ -@ConfigurationProperties(prefix = "opensharing") +/** OpenSharing server configuration, the {@code opensharing} key of {@link ServerSettings}. */ public class OpenSharingProperties { private final Catalog catalog = new Catalog(); diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java new file mode 100644 index 000000000..fb6c8306c --- /dev/null +++ b/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java @@ -0,0 +1,119 @@ +package io.opensharing.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +/** + * The standalone server's settings. Defaults come from {@code application.yml}; an environment + * variable overrides a key in it, and a {@code --key=value} argument overrides any key. + */ +public final class ServerSettings { + + private static final String DEFAULTS = "application.yml"; + + private static final ObjectMapper MAPPER = + new ObjectMapper(new YAMLFactory()) + .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + private final Server server = new Server(); + private final OpenSharingProperties opensharing = new OpenSharingProperties(); + + public Server getServer() { + return server; + } + + public OpenSharingProperties getOpensharing() { + return opensharing; + } + + /** + * Reads {@code application.yml}, then applies {@code env} and {@code args}. + * + *

An environment variable names a key in {@code application.yml} in upper case, with dots as + * underscores and dashes dropped: {@code OPENSHARING_CATALOG_TYPE} sets {@code + * opensharing.catalog.type}. + */ + public static ServerSettings load(String[] args, Map env) { + ObjectNode tree = defaults(); + applyEnvironment(tree, "", env); + for (String arg : args) { + int equals = arg.indexOf('='); + if (!arg.startsWith("--") || equals < 3) { + throw new IllegalArgumentException("expected --key=value, got '" + arg + "'"); + } + set(tree, arg.substring(2, equals), arg.substring(equals + 1)); + } + try { + return MAPPER.treeToValue(tree, ServerSettings.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("invalid server settings: " + e.getOriginalMessage(), e); + } + } + + private static ObjectNode defaults() { + try (InputStream in = ServerSettings.class.getClassLoader().getResourceAsStream(DEFAULTS)) { + if (in == null) { + throw new IllegalStateException(DEFAULTS + " is missing from the classpath"); + } + return (ObjectNode) MAPPER.readTree(in); + } catch (IOException e) { + throw new UncheckedIOException("failed to read " + DEFAULTS, e); + } + } + + private static void applyEnvironment(ObjectNode node, String path, Map env) { + Iterator> fields = node.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + String key = path.isEmpty() ? field.getKey() : path + "." + field.getKey(); + if (field.getValue() instanceof ObjectNode child) { + applyEnvironment(child, key, env); + } else { + String value = env.get(key.toUpperCase(Locale.ROOT).replace('.', '_').replace("-", "")); + if (value != null) { + field.setValue(JsonNodeFactory.instance.textNode(value)); + } + } + } + } + + private static void set(ObjectNode tree, String key, String value) { + String[] parts = key.split("\\."); + ObjectNode node = tree; + for (int i = 0; i < parts.length - 1; i++) { + node = node.get(parts[i]) instanceof ObjectNode child ? child : node.putObject(parts[i]); + } + node.put(parts[parts.length - 1], value); + } + + /** The HTTP listener. */ + public static class Server { + + /** Port to listen on; 0 picks a free one. */ + private int port = 8080; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + } +} diff --git a/server/opensharing-server/src/main/resources/application.yml b/server/opensharing-server/src/main/resources/application.yml index 61d9f670f..b4d83f13a 100644 --- a/server/opensharing-server/src/main/resources/application.yml +++ b/server/opensharing-server/src/main/resources/application.yml @@ -2,39 +2,10 @@ # java -jar opensharing-server.jar --opensharing.delta.url-ttl=15m # or as an environment variable: # OPENSHARING_DELTA_URLTTL=15m java -jar opensharing-server.jar -spring: - application: - name: opensharing-server - datasource: - # The driver is derived from the URL, so pointing this at Postgres or MySQL is the only change - # needed to move the metadata store there. - url: jdbc:h2:file:./data/opensharing;AUTO_SERVER=TRUE - username: sa - password: "" - jpa: - hibernate: - # Creates missing tables and columns to match the entities on startup. - # Production should use 'validate' and manage the schema with a migration tool. - ddl-auto: update - # Closes the database session when each transaction ends, so nothing loads lazily outside the store. - open-in-view: false - jackson: - # Leaves null fields out of JSON responses, as the protocol expects for absent optional fields. - default-property-inclusion: non_null - mapper: - # Accepts enum values in requests in any case, such as "table" for TABLE. - accept-case-insensitive-enums: true - serialization: - # Writes dates and times as ISO-8601 strings, not epoch numbers. - write-dates-as-timestamps: false - +# Log levels are set in logback.xml. server: - # HTTP port the server listens on. + # HTTP port the server listens on; 0 picks a free one. port: 8080 - error: - whitelabel: - # Errors use the protocol's JSON error body, not Spring's HTML error page. - enabled: false opensharing: # Prefix the protocol-serving endpoints are mounted under; this is what goes in a profile file. @@ -59,11 +30,5 @@ opensharing: # Which CatalogConnector the standalone server uses; local reads assets from a YAML file. type: local local: - # The YAML file the local catalog reads, as a Spring resource path. + # The YAML file the local catalog reads: a classpath: resource or a file path. file: classpath:local-catalog.yml - -logging: - level: - io.opensharing: INFO - # Delta Kernel narrates every log segment it loads, which is a line per request. - io.delta: WARN diff --git a/server/opensharing-server/src/main/resources/logback.xml b/server/opensharing-server/src/main/resources/logback.xml new file mode 100644 index 000000000..7efeaf452 --- /dev/null +++ b/server/opensharing-server/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level [%thread] %logger{36} : %msg%n + + + + + + + + + + + diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java index 6f03adcde..25c1ed110 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java @@ -1,8 +1,9 @@ package io.opensharing.config; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import io.opensharing.OpenSharingServer; import io.opensharing.auth.AuthContext; import io.opensharing.auth.UserContext; import io.opensharing.catalog.Asset; @@ -10,24 +11,15 @@ import io.opensharing.catalog.CatalogConnector; import io.opensharing.catalog.DataSourceFormat; import io.opensharing.catalog.TableProperties; -import io.opensharing.runtime.OpenSharing; +import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest( - properties = { - "spring.datasource.url=jdbc:h2:mem:catalog-configuration;DB_CLOSE_DELAY=-1", - "opensharing.catalog.type=local", - "opensharing.catalog.local.file=classpath:local-catalog.yml" - }) -class CatalogConfigurationTest { - @Autowired private CatalogConnector catalog; - @Autowired private OpenSharing openSharing; +class CatalogConfigurationTest { @Test void loadsTheConfiguredLocalCatalog() { + CatalogConnector catalog = + CatalogConfiguration.catalogConnector(settings().getOpensharing()); UserContext user = UserContext.fromUserIdAndName("alice@example.com", "alice@example.com"); assertEquals("local", catalog.name()); @@ -39,8 +31,28 @@ void loadsTheConfiguredLocalCatalog() { assertEquals(DataSourceFormat.DELTA, table.dataSourceFormat()); } + @Test + void rejectsAMissingLocalCatalogFile() { + ServerSettings settings = settings("--opensharing.catalog.local.file=classpath:missing.yml"); + + IllegalStateException missing = + assertThrows( + IllegalStateException.class, + () -> CatalogConfiguration.catalogConnector(settings.getOpensharing())); + assertEquals("local catalog file 'classpath:missing.yml' does not exist", missing.getMessage()); + } + @Test void buildsOpenSharingWithTheConfiguredCatalog() { - assertSame(catalog, openSharing.catalog()); + try (OpenSharingServer server = OpenSharingServer.start(settings())) { + assertEquals("local", server.openSharing().catalog().name()); + } + } + + private static ServerSettings settings(String... overrides) { + String[] args = new String[overrides.length + 1]; + args[0] = "--server.port=0"; + System.arraycopy(overrides, 0, args, 1, overrides.length); + return ServerSettings.load(args, Map.of()); } } diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java new file mode 100644 index 000000000..48ae783a4 --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java @@ -0,0 +1,56 @@ +package io.opensharing.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ServerSettingsTest { + + @Test + void readsDefaultsFromApplicationYml() { + ServerSettings settings = ServerSettings.load(new String[0], Map.of()); + + assertEquals(8080, settings.getServer().getPort()); + assertEquals("local", settings.getOpensharing().getCatalog().getType()); + assertEquals( + "classpath:local-catalog.yml", settings.getOpensharing().getCatalog().getLocal().getFile()); + } + + @Test + void overridesKeysFromTheEnvironment() { + ServerSettings settings = + ServerSettings.load( + new String[0], + Map.of("SERVER_PORT", "9090", "OPENSHARING_CATALOG_LOCAL_FILE", "/tmp/catalog.yml")); + + assertEquals(9090, settings.getServer().getPort()); + assertEquals("/tmp/catalog.yml", settings.getOpensharing().getCatalog().getLocal().getFile()); + } + + @Test + void letsArgumentsOverrideTheEnvironment() { + ServerSettings settings = + ServerSettings.load(new String[] {"--server.port=7070"}, Map.of("SERVER_PORT", "9090")); + + assertEquals(7070, settings.getServer().getPort()); + } + + @Test + void rejectsArgumentsThatAreNotKeyValuePairs() { + assertThrows( + IllegalArgumentException.class, + () -> ServerSettings.load(new String[] {"server.port=7070"}, Map.of())); + assertThrows( + IllegalArgumentException.class, + () -> ServerSettings.load(new String[] {"--server.port"}, Map.of())); + } + + @Test + void rejectsValuesOfTheWrongType() { + assertThrows( + IllegalArgumentException.class, + () -> ServerSettings.load(new String[] {"--server.port=eighty"}, Map.of())); + } +} diff --git a/server/pom.xml b/server/pom.xml index 1dca47e68..7822e8ac0 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -27,12 +27,17 @@ UTF-8 21 - 3.5.3 + + 1.28.4 + 2.19.1 + 3.1.0 + 2.0.17 + 1.5.18 + 5.12.2 4.0.0 3.4.3 2.35.4 hadoop3-2.2.30 - 2.9.0 @@ -43,12 +48,41 @@ ${project.version} - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} + com.linecorp.armeria + armeria-bom + ${armeria.version} pom import + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + org.junit + junit-bom + ${junit.version} + pom + import + + + jakarta.persistence + jakarta.persistence-api + ${jakarta-persistence.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + @@ -71,9 +105,9 @@ 3.2.5 - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 From 054c6f499110e7345b4aa6b12ef0f26341afaa33 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 15:11:57 -0700 Subject: [PATCH 13/18] Read standalone server settings from etc/conf/server.properties as Unity Catalog does. --- server/etc/conf/server.properties | 11 ++ .../io/opensharing/OpenSharingServer.java | 32 +++-- .../catalog/local/LocalCatalogLoader.java | 34 +++++ .../config/CatalogConfiguration.java | 72 ----------- .../config/OpenSharingConfiguration.java | 14 --- .../config/OpenSharingProperties.java | 42 ------- .../opensharing/config/ServerProperties.java | 87 +++++++++++++ .../io/opensharing/config/ServerSettings.java | 119 ------------------ .../src/main/resources/application.yml | 34 ----- .../src/main/resources/local-catalog.yml | 2 +- ...onTest.java => OpenSharingServerTest.java} | 40 +++--- .../config/ServerPropertiesTest.java | 64 ++++++++++ .../config/ServerSettingsTest.java | 56 --------- 13 files changed, 244 insertions(+), 363 deletions(-) create mode 100644 server/etc/conf/server.properties delete mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java delete mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java delete mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java create mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java delete mode 100644 server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java delete mode 100644 server/opensharing-server/src/main/resources/application.yml rename server/opensharing-server/src/test/java/io/opensharing/{config/CatalogConfigurationTest.java => OpenSharingServerTest.java} (53%) create mode 100644 server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java delete mode 100644 server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java diff --git a/server/etc/conf/server.properties b/server/etc/conf/server.properties new file mode 100644 index 000000000..96061246b --- /dev/null +++ b/server/etc/conf/server.properties @@ -0,0 +1,11 @@ +## OpenSharing standalone server settings, read from the directory the server is started in. +## A JVM system property (-Dserver.port=9090) or an environment variable with the same name as a +## key overrides it. A key left blank keeps its default. + +# HTTP port the server listens on; 0 picks a free one. +server.port=8080 + +# Which CatalogConnector the server uses; local reads assets from a YAML file. +catalog.type=local +# The YAML file the local catalog reads: a classpath: resource or a file path. +catalog.local.file=classpath:local-catalog.yml diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java index d13d5a4e0..36346f260 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -4,10 +4,12 @@ import com.linecorp.armeria.common.HttpStatus; import com.linecorp.armeria.server.Server; import io.opensharing.catalog.CatalogConnector; -import io.opensharing.config.CatalogConfiguration; -import io.opensharing.config.OpenSharingConfiguration; -import io.opensharing.config.ServerSettings; +import io.opensharing.catalog.local.LocalCatalogConnector; +import io.opensharing.catalog.local.LocalCatalogLoader; +import io.opensharing.config.ServerProperties; +import io.opensharing.config.ServerProperties.Property; import io.opensharing.runtime.OpenSharing; +import java.util.Locale; /** Runnable standalone OpenSharing server. */ public final class OpenSharingServer implements AutoCloseable { @@ -21,18 +23,17 @@ private OpenSharingServer(Server server, OpenSharing openSharing) { } public static void main(String[] args) throws InterruptedException { - OpenSharingServer server = start(ServerSettings.load(args, System.getenv())); + OpenSharingServer server = start(new ServerProperties(ServerProperties.DEFAULT_FILE)); server.server.closeOnJvmShutdown(); server.server.blockUntilShutdown(); } - /** Builds the server from {@code settings} and returns once it is listening. */ - public static OpenSharingServer start(ServerSettings settings) { - CatalogConnector catalog = CatalogConfiguration.catalogConnector(settings.getOpensharing()); - OpenSharing openSharing = OpenSharingConfiguration.openSharing(catalog); + /** Builds the server from {@code properties} and returns once it is listening. */ + public static OpenSharingServer start(ServerProperties properties) { + OpenSharing openSharing = OpenSharing.builder().catalog(catalog(properties)).build(); Server server = Server.builder() - .http(settings.getServer().getPort()) + .http(properties.getPort()) // Armeria refuses to start without a service; routes nothing serves are not found. .serviceUnder("/", (ctx, req) -> HttpResponse.of(HttpStatus.NOT_FOUND)) .build(); @@ -53,4 +54,17 @@ public OpenSharing openSharing() { public void close() { server.stop().join(); } + + static CatalogConnector catalog(ServerProperties properties) { + String type = properties.get(Property.CATALOG_TYPE); + return switch (type.trim().toLowerCase(Locale.ROOT)) { + case LocalCatalogConnector.NAME -> + new LocalCatalogConnector( + LocalCatalogLoader.load(properties.get(Property.LOCAL_CATALOG_FILE))); + default -> + throw new IllegalStateException( + "unknown catalog.type '" + type + "'; this build ships '" + + LocalCatalogConnector.NAME + "'"); + }; + } } diff --git a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java index 352a5a3d2..3a954b7da 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java +++ b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java @@ -6,10 +6,16 @@ import io.opensharing.exception.CatalogException; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; /** Reads a {@link LocalCatalogFile} from YAML or JSON. */ public final class LocalCatalogLoader { + private static final String CLASSPATH = "classpath:"; + private static final String FILE = "file:"; + private static final ObjectMapper MAPPER = YAMLMapper.builder() .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) @@ -18,6 +24,15 @@ public final class LocalCatalogLoader { private LocalCatalogLoader() {} + /** Reads {@code location}: a {@code classpath:} resource, or a file path with or without file:. */ + public static LocalCatalogFile load(String location) { + try (InputStream in = open(location)) { + return load(in, location); + } catch (IOException e) { + throw new CatalogException("failed to read local catalog file " + location, e); + } + } + public static LocalCatalogFile load(InputStream in, String origin) { try { LocalCatalogFile file = MAPPER.readValue(in, LocalCatalogFile.class); @@ -28,4 +43,23 @@ public static LocalCatalogFile load(InputStream in, String origin) { throw new CatalogException("invalid local catalog file " + origin + ": " + e.getMessage(), e); } } + + private static InputStream open(String location) throws IOException { + InputStream in; + if (location.startsWith(CLASSPATH)) { + String resource = location.substring(CLASSPATH.length()).replaceFirst("^/", ""); + in = LocalCatalogLoader.class.getClassLoader().getResourceAsStream(resource); + } else { + Path path = Path.of(location.startsWith(FILE) ? location.substring(FILE.length()) : location); + try { + in = Files.newInputStream(path); + } catch (NoSuchFileException e) { + in = null; + } + } + if (in == null) { + throw new IllegalStateException("local catalog file '" + location + "' does not exist"); + } + return in; + } } diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java deleted file mode 100644 index 62e7f2c19..000000000 --- a/server/opensharing-server/src/main/java/io/opensharing/config/CatalogConfiguration.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.opensharing.config; - -import io.opensharing.catalog.CatalogConnector; -import io.opensharing.catalog.local.LocalCatalogConnector; -import io.opensharing.catalog.local.LocalCatalogFile; -import io.opensharing.catalog.local.LocalCatalogLoader; -import io.opensharing.exception.CatalogException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.Locale; - -/** Builds the catalog used by the standalone server. */ -public final class CatalogConfiguration { - - private static final String CLASSPATH = "classpath:"; - private static final String FILE = "file:"; - - private CatalogConfiguration() {} - - public static CatalogConnector catalogConnector(OpenSharingProperties properties) { - OpenSharingProperties.Catalog catalog = properties.getCatalog(); - String type = catalog.getType() == null ? "" : catalog.getType().trim().toLowerCase(Locale.ROOT); - return switch (type) { - case LocalCatalogConnector.NAME -> localConnector(catalog.getLocal()); - default -> - throw new IllegalStateException( - "unknown opensharing.catalog.type '" - + catalog.getType() - + "'; this build ships '" - + LocalCatalogConnector.NAME - + "'"); - }; - } - - private static CatalogConnector localConnector(OpenSharingProperties.Catalog.Local config) { - String location = config.getFile(); - if (location == null || location.isBlank()) { - throw new IllegalStateException("opensharing.catalog.local.file is required"); - } - try (InputStream in = open(location)) { - LocalCatalogFile file = LocalCatalogLoader.load(in, location); - return new LocalCatalogConnector(file); - } catch (IOException e) { - throw new CatalogException("failed to read local catalog file " + location, e); - } - } - - // A classpath: resource, or a file path with or without file:. - private static InputStream open(String location) throws IOException { - InputStream in; - if (location.startsWith(CLASSPATH)) { - in = - CatalogConfiguration.class - .getClassLoader() - .getResourceAsStream(location.substring(CLASSPATH.length()).replaceFirst("^/", "")); - } else { - Path path = Path.of(location.startsWith(FILE) ? location.substring(FILE.length()) : location); - try { - in = Files.newInputStream(path); - } catch (NoSuchFileException e) { - in = null; - } - } - if (in == null) { - throw new IllegalStateException("local catalog file '" + location + "' does not exist"); - } - return in; - } -} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java deleted file mode 100644 index 7679d6164..000000000 --- a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingConfiguration.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.opensharing.config; - -import io.opensharing.catalog.CatalogConnector; -import io.opensharing.runtime.OpenSharing; - -/** Builds the standalone server's {@link OpenSharing} from its settings. */ -public final class OpenSharingConfiguration { - - private OpenSharingConfiguration() {} - - public static OpenSharing openSharing(CatalogConnector catalog) { - return OpenSharing.builder().catalog(catalog).build(); - } -} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java deleted file mode 100644 index 65d557798..000000000 --- a/server/opensharing-server/src/main/java/io/opensharing/config/OpenSharingProperties.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.opensharing.config; - -/** OpenSharing server configuration, the {@code opensharing} key of {@link ServerSettings}. */ -public class OpenSharingProperties { - - private final Catalog catalog = new Catalog(); - - public Catalog getCatalog() { - return catalog; - } - - public static class Catalog { - - private String type; - private final Local local = new Local(); - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Local getLocal() { - return local; - } - - public static class Local { - - private String file; - - public String getFile() { - return file; - } - - public void setFile(String file) { - this.file = file; - } - } - } -} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java new file mode 100644 index 000000000..940c9e805 --- /dev/null +++ b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java @@ -0,0 +1,87 @@ +package io.opensharing.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +/** + * The standalone server's settings, read from {@code etc/conf/server.properties} as Unity Catalog + * reads its own. A JVM system property or an environment variable with the same name as a key + * overrides it. + */ +public class ServerProperties { + + public static final String DEFAULT_FILE = "etc/conf/server.properties"; + + private final Properties properties = new Properties(defaults()); + + public enum Property { + PORT("server.port", "8080"), + CATALOG_TYPE("catalog.type", "local"), + LOCAL_CATALOG_FILE("catalog.local.file", "classpath:local-catalog.yml"); + + private final String key; + private final String defaultValue; + + Property(String key, String defaultValue) { + this.key = key; + this.defaultValue = defaultValue; + } + + public String key() { + return key; + } + } + + /** Reads {@code file}, keeping the defaults when it does not exist. */ + public ServerProperties(String file) { + Path path = Path.of(file); + if (Files.exists(path)) { + try (InputStream in = Files.newInputStream(path)) { + properties.load(in); + } catch (IOException e) { + throw new UncheckedIOException("failed to read " + file, e); + } + } + dropBlankValues(); + } + + public ServerProperties(Properties overrides) { + properties.putAll(overrides); + dropBlankValues(); + } + + public String get(Property property) { + String value = System.getProperty(property.key); + if (value == null) { + value = System.getenv(property.key); + } + return value != null ? value : properties.getProperty(property.key); + } + + public int getPort() { + String port = get(Property.PORT); + try { + return Integer.parseInt(port.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "invalid value '" + port + "' for " + Property.PORT.key + "; expected a port number"); + } + } + + private static Properties defaults() { + Properties defaults = new Properties(); + for (Property property : Property.values()) { + defaults.setProperty(property.key, property.defaultValue); + } + return defaults; + } + + // A blank value in server.properties means unset, so the default applies. + private void dropBlankValues() { + properties.entrySet().removeIf(entry -> entry.getValue().toString().isBlank()); + } +} diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java deleted file mode 100644 index fb6c8306c..000000000 --- a/server/opensharing-server/src/main/java/io/opensharing/config/ServerSettings.java +++ /dev/null @@ -1,119 +0,0 @@ -package io.opensharing.config; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; - -/** - * The standalone server's settings. Defaults come from {@code application.yml}; an environment - * variable overrides a key in it, and a {@code --key=value} argument overrides any key. - */ -public final class ServerSettings { - - private static final String DEFAULTS = "application.yml"; - - private static final ObjectMapper MAPPER = - new ObjectMapper(new YAMLFactory()) - .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) - .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - private final Server server = new Server(); - private final OpenSharingProperties opensharing = new OpenSharingProperties(); - - public Server getServer() { - return server; - } - - public OpenSharingProperties getOpensharing() { - return opensharing; - } - - /** - * Reads {@code application.yml}, then applies {@code env} and {@code args}. - * - *

An environment variable names a key in {@code application.yml} in upper case, with dots as - * underscores and dashes dropped: {@code OPENSHARING_CATALOG_TYPE} sets {@code - * opensharing.catalog.type}. - */ - public static ServerSettings load(String[] args, Map env) { - ObjectNode tree = defaults(); - applyEnvironment(tree, "", env); - for (String arg : args) { - int equals = arg.indexOf('='); - if (!arg.startsWith("--") || equals < 3) { - throw new IllegalArgumentException("expected --key=value, got '" + arg + "'"); - } - set(tree, arg.substring(2, equals), arg.substring(equals + 1)); - } - try { - return MAPPER.treeToValue(tree, ServerSettings.class); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("invalid server settings: " + e.getOriginalMessage(), e); - } - } - - private static ObjectNode defaults() { - try (InputStream in = ServerSettings.class.getClassLoader().getResourceAsStream(DEFAULTS)) { - if (in == null) { - throw new IllegalStateException(DEFAULTS + " is missing from the classpath"); - } - return (ObjectNode) MAPPER.readTree(in); - } catch (IOException e) { - throw new UncheckedIOException("failed to read " + DEFAULTS, e); - } - } - - private static void applyEnvironment(ObjectNode node, String path, Map env) { - Iterator> fields = node.fields(); - while (fields.hasNext()) { - Map.Entry field = fields.next(); - String key = path.isEmpty() ? field.getKey() : path + "." + field.getKey(); - if (field.getValue() instanceof ObjectNode child) { - applyEnvironment(child, key, env); - } else { - String value = env.get(key.toUpperCase(Locale.ROOT).replace('.', '_').replace("-", "")); - if (value != null) { - field.setValue(JsonNodeFactory.instance.textNode(value)); - } - } - } - } - - private static void set(ObjectNode tree, String key, String value) { - String[] parts = key.split("\\."); - ObjectNode node = tree; - for (int i = 0; i < parts.length - 1; i++) { - node = node.get(parts[i]) instanceof ObjectNode child ? child : node.putObject(parts[i]); - } - node.put(parts[parts.length - 1], value); - } - - /** The HTTP listener. */ - public static class Server { - - /** Port to listen on; 0 picks a free one. */ - private int port = 8080; - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - } -} diff --git a/server/opensharing-server/src/main/resources/application.yml b/server/opensharing-server/src/main/resources/application.yml deleted file mode 100644 index b4d83f13a..000000000 --- a/server/opensharing-server/src/main/resources/application.yml +++ /dev/null @@ -1,34 +0,0 @@ -# Defaults for a local run. Override any key without editing this file, either as an argument: -# java -jar opensharing-server.jar --opensharing.delta.url-ttl=15m -# or as an environment variable: -# OPENSHARING_DELTA_URLTTL=15m java -jar opensharing-server.jar -# Log levels are set in logback.xml. -server: - # HTTP port the server listens on; 0 picks a free one. - port: 8080 - -opensharing: - # Prefix the protocol-serving endpoints are mounted under; this is what goes in a profile file. - protocol-prefix: /api/1.0/opensharing - asset-credentials: - # Lifetime of the cloud credentials the catalog vends for reading an asset's storage. - ttl: 1h - pagination: - # Page size for list endpoints when the client sends no maxResults. - default-max-results: 500 - # Largest page size a client may request; larger values are capped to this. - max-max-results: 1000 - storage: - # Service-account key used to sign GCS URLs. - gcs-service-account-key-file: "" - delta: - # Whether Query Table may return pre-signed file URLs to recipients. - url-access-enabled: true - # Lifetime of each pre-signed file URL. - url-ttl: 1h - catalog: - # Which CatalogConnector the standalone server uses; local reads assets from a YAML file. - type: local - local: - # The YAML file the local catalog reads: a classpath: resource or a file path. - file: classpath:local-catalog.yml diff --git a/server/opensharing-server/src/main/resources/local-catalog.yml b/server/opensharing-server/src/main/resources/local-catalog.yml index 1d615f7a9..920e65480 100644 --- a/server/opensharing-server/src/main/resources/local-catalog.yml +++ b/server/opensharing-server/src/main/resources/local-catalog.yml @@ -1,4 +1,4 @@ -# Sample catalog for opensharing.catalog.type=local. +# Sample catalog for catalog.type=local. # FAKE credentials are placeholders. Use STATIC with credentials.values for real keys. credentials: provider: AWS diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java b/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java similarity index 53% rename from server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java rename to server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java index 25c1ed110..80802a55a 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/CatalogConfigurationTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java @@ -1,9 +1,8 @@ -package io.opensharing.config; +package io.opensharing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import io.opensharing.OpenSharingServer; import io.opensharing.auth.AuthContext; import io.opensharing.auth.UserContext; import io.opensharing.catalog.Asset; @@ -11,15 +10,15 @@ import io.opensharing.catalog.CatalogConnector; import io.opensharing.catalog.DataSourceFormat; import io.opensharing.catalog.TableProperties; -import java.util.Map; +import io.opensharing.config.ServerProperties; +import java.util.Properties; import org.junit.jupiter.api.Test; -class CatalogConfigurationTest { +class OpenSharingServerTest { @Test void loadsTheConfiguredLocalCatalog() { - CatalogConnector catalog = - CatalogConfiguration.catalogConnector(settings().getOpensharing()); + CatalogConnector catalog = OpenSharingServer.catalog(properties()); UserContext user = UserContext.fromUserIdAndName("alice@example.com", "alice@example.com"); assertEquals("local", catalog.name()); @@ -33,26 +32,35 @@ void loadsTheConfiguredLocalCatalog() { @Test void rejectsAMissingLocalCatalogFile() { - ServerSettings settings = settings("--opensharing.catalog.local.file=classpath:missing.yml"); + ServerProperties properties = properties("catalog.local.file", "classpath:missing.yml"); IllegalStateException missing = - assertThrows( - IllegalStateException.class, - () -> CatalogConfiguration.catalogConnector(settings.getOpensharing())); + assertThrows(IllegalStateException.class, () -> OpenSharingServer.catalog(properties)); assertEquals("local catalog file 'classpath:missing.yml' does not exist", missing.getMessage()); } + @Test + void rejectsAnUnknownCatalogType() { + ServerProperties properties = properties("catalog.type", "glue"); + + IllegalStateException unknown = + assertThrows(IllegalStateException.class, () -> OpenSharingServer.catalog(properties)); + assertEquals("unknown catalog.type 'glue'; this build ships 'local'", unknown.getMessage()); + } + @Test void buildsOpenSharingWithTheConfiguredCatalog() { - try (OpenSharingServer server = OpenSharingServer.start(settings())) { + try (OpenSharingServer server = OpenSharingServer.start(properties())) { assertEquals("local", server.openSharing().catalog().name()); } } - private static ServerSettings settings(String... overrides) { - String[] args = new String[overrides.length + 1]; - args[0] = "--server.port=0"; - System.arraycopy(overrides, 0, args, 1, overrides.length); - return ServerSettings.load(args, Map.of()); + private static ServerProperties properties(String... overrides) { + Properties props = new Properties(); + props.setProperty("server.port", "0"); + for (int i = 0; i < overrides.length; i += 2) { + props.setProperty(overrides[i], overrides[i + 1]); + } + return new ServerProperties(props); } } diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java new file mode 100644 index 000000000..529023a16 --- /dev/null +++ b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java @@ -0,0 +1,64 @@ +package io.opensharing.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.opensharing.config.ServerProperties.Property; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ServerPropertiesTest { + + @TempDir Path dir; + + @Test + void keepsTheDefaultsWithoutAFile() { + ServerProperties properties = new ServerProperties(dir.resolve("missing.properties").toString()); + + assertEquals(8080, properties.getPort()); + assertEquals("local", properties.get(Property.CATALOG_TYPE)); + assertEquals("classpath:local-catalog.yml", properties.get(Property.LOCAL_CATALOG_FILE)); + } + + @Test + void readsTheFileAndKeepsTheDefaultOfBlankKeys() throws IOException { + Path file = dir.resolve("server.properties"); + Files.writeString(file, "server.port=9090\ncatalog.local.file=\n"); + + ServerProperties properties = new ServerProperties(file.toString()); + + assertEquals(9090, properties.getPort()); + assertEquals("classpath:local-catalog.yml", properties.get(Property.LOCAL_CATALOG_FILE)); + } + + @Test + void letsSystemPropertiesOverrideTheFile() { + ServerProperties properties = new ServerProperties(props("server.port", "9090")); + System.setProperty("server.port", "7070"); + try { + assertEquals(7070, properties.getPort()); + } finally { + System.clearProperty("server.port"); + } + } + + @Test + void rejectsAPortThatIsNotANumber() { + ServerProperties properties = new ServerProperties(props("server.port", "eighty")); + + IllegalArgumentException invalid = + assertThrows(IllegalArgumentException.class, properties::getPort); + assertEquals( + "invalid value 'eighty' for server.port; expected a port number", invalid.getMessage()); + } + + private static Properties props(String key, String value) { + Properties props = new Properties(); + props.setProperty(key, value); + return props; + } +} diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java deleted file mode 100644 index 48ae783a4..000000000 --- a/server/opensharing-server/src/test/java/io/opensharing/config/ServerSettingsTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.opensharing.config; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Map; -import org.junit.jupiter.api.Test; - -class ServerSettingsTest { - - @Test - void readsDefaultsFromApplicationYml() { - ServerSettings settings = ServerSettings.load(new String[0], Map.of()); - - assertEquals(8080, settings.getServer().getPort()); - assertEquals("local", settings.getOpensharing().getCatalog().getType()); - assertEquals( - "classpath:local-catalog.yml", settings.getOpensharing().getCatalog().getLocal().getFile()); - } - - @Test - void overridesKeysFromTheEnvironment() { - ServerSettings settings = - ServerSettings.load( - new String[0], - Map.of("SERVER_PORT", "9090", "OPENSHARING_CATALOG_LOCAL_FILE", "/tmp/catalog.yml")); - - assertEquals(9090, settings.getServer().getPort()); - assertEquals("/tmp/catalog.yml", settings.getOpensharing().getCatalog().getLocal().getFile()); - } - - @Test - void letsArgumentsOverrideTheEnvironment() { - ServerSettings settings = - ServerSettings.load(new String[] {"--server.port=7070"}, Map.of("SERVER_PORT", "9090")); - - assertEquals(7070, settings.getServer().getPort()); - } - - @Test - void rejectsArgumentsThatAreNotKeyValuePairs() { - assertThrows( - IllegalArgumentException.class, - () -> ServerSettings.load(new String[] {"server.port=7070"}, Map.of())); - assertThrows( - IllegalArgumentException.class, - () -> ServerSettings.load(new String[] {"--server.port"}, Map.of())); - } - - @Test - void rejectsValuesOfTheWrongType() { - assertThrows( - IllegalArgumentException.class, - () -> ServerSettings.load(new String[] {"--server.port=eighty"}, Map.of())); - } -} From bba23978b405835693273767704a62c5aa7515db Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 15:16:59 -0700 Subject: [PATCH 14/18] Bundle server.properties on the standalone server's classpath. --- .../io/opensharing/OpenSharingServer.java | 2 +- .../opensharing/config/ServerProperties.java | 21 +++++++------------ .../src/main/resources}/server.properties | 2 +- .../config/ServerPropertiesTest.java | 18 ++++++---------- 4 files changed, 16 insertions(+), 27 deletions(-) rename server/{etc/conf => opensharing-server/src/main/resources}/server.properties (82%) diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java index 36346f260..f270cdd81 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -23,7 +23,7 @@ private OpenSharingServer(Server server, OpenSharing openSharing) { } public static void main(String[] args) throws InterruptedException { - OpenSharingServer server = start(new ServerProperties(ServerProperties.DEFAULT_FILE)); + OpenSharingServer server = start(new ServerProperties()); server.server.closeOnJvmShutdown(); server.server.blockUntilShutdown(); } diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java index 940c9e805..27ed0b1af 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java @@ -3,18 +3,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Properties; /** - * The standalone server's settings, read from {@code etc/conf/server.properties} as Unity Catalog - * reads its own. A JVM system property or an environment variable with the same name as a key - * overrides it. + * The standalone server's settings, read from {@code server.properties} on the classpath. A JVM + * system property or an environment variable with the same name as a key overrides it. */ public class ServerProperties { - public static final String DEFAULT_FILE = "etc/conf/server.properties"; + private static final String RESOURCE = "server.properties"; private final Properties properties = new Properties(defaults()); @@ -36,15 +33,13 @@ public String key() { } } - /** Reads {@code file}, keeping the defaults when it does not exist. */ - public ServerProperties(String file) { - Path path = Path.of(file); - if (Files.exists(path)) { - try (InputStream in = Files.newInputStream(path)) { + public ServerProperties() { + try (InputStream in = ServerProperties.class.getClassLoader().getResourceAsStream(RESOURCE)) { + if (in != null) { properties.load(in); - } catch (IOException e) { - throw new UncheckedIOException("failed to read " + file, e); } + } catch (IOException e) { + throw new UncheckedIOException("failed to read " + RESOURCE, e); } dropBlankValues(); } diff --git a/server/etc/conf/server.properties b/server/opensharing-server/src/main/resources/server.properties similarity index 82% rename from server/etc/conf/server.properties rename to server/opensharing-server/src/main/resources/server.properties index 96061246b..0ee023e59 100644 --- a/server/etc/conf/server.properties +++ b/server/opensharing-server/src/main/resources/server.properties @@ -1,4 +1,4 @@ -## OpenSharing standalone server settings, read from the directory the server is started in. +## OpenSharing standalone server settings, read from the classpath. ## A JVM system property (-Dserver.port=9090) or an environment variable with the same name as a ## key overrides it. A key left blank keeps its default. diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java index 529023a16..0ca4836e4 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java @@ -4,20 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import io.opensharing.config.ServerProperties.Property; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Properties; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; class ServerPropertiesTest { - @TempDir Path dir; - @Test - void keepsTheDefaultsWithoutAFile() { - ServerProperties properties = new ServerProperties(dir.resolve("missing.properties").toString()); + void readsServerPropertiesFromTheClasspath() { + ServerProperties properties = new ServerProperties(); assertEquals(8080, properties.getPort()); assertEquals("local", properties.get(Property.CATALOG_TYPE)); @@ -25,11 +19,11 @@ void keepsTheDefaultsWithoutAFile() { } @Test - void readsTheFileAndKeepsTheDefaultOfBlankKeys() throws IOException { - Path file = dir.resolve("server.properties"); - Files.writeString(file, "server.port=9090\ncatalog.local.file=\n"); + void keepsTheDefaultOfBlankKeys() { + Properties props = props("server.port", "9090"); + props.setProperty("catalog.local.file", " "); - ServerProperties properties = new ServerProperties(file.toString()); + ServerProperties properties = new ServerProperties(props); assertEquals(9090, properties.getPort()); assertEquals("classpath:local-catalog.yml", properties.get(Property.LOCAL_CATALOG_FILE)); From 4b4e58d5999e6ebe6ac5b755c781b039d0ce3019 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 15:18:04 -0700 Subject: [PATCH 15/18] Drop OpenSharingTest. --- .../opensharing/runtime/OpenSharingTest.java | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java diff --git a/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java b/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java deleted file mode 100644 index e1c4140fe..000000000 --- a/server/opensharing-server-core/src/test/java/io/opensharing/runtime/OpenSharingTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.opensharing.runtime; - -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import io.opensharing.catalog.CatalogConnector; -import java.lang.reflect.Proxy; -import org.junit.jupiter.api.Test; - -class OpenSharingTest { - - @Test - void refusesToBuildWithoutACatalogConnector() { - assertThrows(IllegalStateException.class, () -> OpenSharing.builder().build()); - } - - @Test - void buildsWithTheHostCatalog() { - CatalogConnector catalog = - (CatalogConnector) - Proxy.newProxyInstance( - CatalogConnector.class.getClassLoader(), - new Class[] {CatalogConnector.class}, - (proxy, method, args) -> { - throw new UnsupportedOperationException(); - }); - - assertSame(catalog, OpenSharing.builder().catalog(catalog).build().catalog()); - } -} From ebf2499578a2ee3541b083372671abb5e815d4a9 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Tue, 13 Oct 2026 15:22:40 -0700 Subject: [PATCH 16/18] Drop the Unity Catalog note from server/pom.xml. --- server/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/server/pom.xml b/server/pom.xml index 7822e8ac0..e33f4ccd1 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -27,7 +27,6 @@ UTF-8 21 - 1.28.4 2.19.1 3.1.0 From 06f9727ed8d89a8ba1418ea7e2c184066c97e1a5 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Wed, 14 Oct 2026 11:57:12 -0700 Subject: [PATCH 17/18] Document the standalone server and local catalog loader, and drop unused accessors. --- .../io/opensharing/OpenSharingServer.java | 27 ++++++++++++++----- .../catalog/local/LocalCatalogLoader.java | 13 +++++++-- .../opensharing/config/ServerProperties.java | 4 --- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java index f270cdd81..be776d866 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -11,7 +11,11 @@ import io.opensharing.runtime.OpenSharing; import java.util.Locale; -/** Runnable standalone OpenSharing server. */ +/** + * Runnable standalone OpenSharing server. It assembles {@link OpenSharing} from {@link + * ServerProperties} and serves it over HTTP with Armeria. An embedding host skips this class and + * builds {@link OpenSharing} itself. + */ public final class OpenSharingServer implements AutoCloseable { private final Server server; @@ -22,13 +26,21 @@ private OpenSharingServer(Server server, OpenSharing openSharing) { this.openSharing = openSharing; } + /** + * Starts the server with the settings in {@code server.properties}, overridden by JVM system + * properties and environment variables, and blocks until the JVM shuts down. Program arguments + * are ignored, so settings are passed as {@code -D} JVM options instead. + */ public static void main(String[] args) throws InterruptedException { OpenSharingServer server = start(new ServerProperties()); server.server.closeOnJvmShutdown(); server.server.blockUntilShutdown(); } - /** Builds the server from {@code properties} and returns once it is listening. */ + /** + * Builds the server from {@code properties} and returns once it is listening. The catalog is + * loaded first, so an invalid catalog fails startup before the port is bound. + */ public static OpenSharingServer start(ServerProperties properties) { OpenSharing openSharing = OpenSharing.builder().catalog(catalog(properties)).build(); Server server = @@ -41,20 +53,21 @@ public static OpenSharingServer start(ServerProperties properties) { return new OpenSharingServer(server, openSharing); } - /** The port the server listens on, which it picks itself when configured with 0. */ - public int port() { - return server.activeLocalPort(); - } - + /** The OpenSharing instance this server serves. */ public OpenSharing openSharing() { return openSharing; } + /** Stops the server and blocks until it has stopped. */ @Override public void close() { server.stop().join(); } + /** + * The catalog named by {@code catalog.type}, matched ignoring case and surrounding spaces. Only + * the local catalog ships with this build; it reads {@code catalog.local.file}. + */ static CatalogConnector catalog(ServerProperties properties) { String type = properties.get(Property.CATALOG_TYPE); return switch (type.trim().toLowerCase(Locale.ROOT)) { diff --git a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java index 3a954b7da..4c4e7837b 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java +++ b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java @@ -10,7 +10,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; -/** Reads a {@link LocalCatalogFile} from YAML or JSON. */ +/** Reads a {@link LocalCatalogFile} from YAML or JSON, matching keys and enums ignoring case. */ public final class LocalCatalogLoader { private static final String CLASSPATH = "classpath:"; @@ -24,7 +24,10 @@ public final class LocalCatalogLoader { private LocalCatalogLoader() {} - /** Reads {@code location}: a {@code classpath:} resource, or a file path with or without file:. */ + /** + * Reads {@code location}: a {@code classpath:} resource, or a file path with or without {@code + * file:}. + */ public static LocalCatalogFile load(String location) { try (InputStream in = open(location)) { return load(in, location); @@ -33,9 +36,13 @@ public static LocalCatalogFile load(String location) { } } + /** + * Parses {@code in} without closing it. {@code origin} only names the source in error messages. + */ public static LocalCatalogFile load(InputStream in, String origin) { try { LocalCatalogFile file = MAPPER.readValue(in, LocalCatalogFile.class); + // A null YAML document, such as "~" or a lone "---", is an empty catalog. return file == null ? new LocalCatalogFile(null, null) : file; } catch (IOException e) { throw new CatalogException("failed to parse local catalog file " + origin, e); @@ -47,9 +54,11 @@ public static LocalCatalogFile load(InputStream in, String origin) { private static InputStream open(String location) throws IOException { InputStream in; if (location.startsWith(CLASSPATH)) { + // ClassLoader resources never start with "/", so "classpath:/x.yml" means "classpath:x.yml". String resource = location.substring(CLASSPATH.length()).replaceFirst("^/", ""); in = LocalCatalogLoader.class.getClassLoader().getResourceAsStream(resource); } else { + // "file:" is stripped, not parsed as a URI, so relative paths and %-escapes stay as written. Path path = Path.of(location.startsWith(FILE) ? location.substring(FILE.length()) : location); try { in = Files.newInputStream(path); diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java index 27ed0b1af..d8a98bfc9 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java @@ -27,10 +27,6 @@ public enum Property { this.key = key; this.defaultValue = defaultValue; } - - public String key() { - return key; - } } public ServerProperties() { From 548574278964f04e30710e2dbdac5fffe6c49938 Mon Sep 17 00:00:00 2001 From: Sun Cao Date: Mon, 19 Oct 2026 09:43:13 -0700 Subject: [PATCH 18/18] Pin Netty for its HTTP/2 fixes, ignore blank overrides, accept upper snake case environment variables, and reject program arguments. --- .../io/opensharing/OpenSharingServer.java | 23 +++++++++---- .../catalog/local/LocalCatalogLoader.java | 4 +-- .../opensharing/config/ServerProperties.java | 32 +++++++++++++++---- .../src/main/resources/server.properties | 3 +- .../io/opensharing/OpenSharingServerTest.java | 12 +++++++ .../config/ServerPropertiesTest.java | 28 ++++++++++++++++ server/pom.xml | 9 ++++++ 7 files changed, 93 insertions(+), 18 deletions(-) diff --git a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java index be776d866..a6a413a9b 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java +++ b/server/opensharing-server/src/main/java/io/opensharing/OpenSharingServer.java @@ -12,9 +12,8 @@ import java.util.Locale; /** - * Runnable standalone OpenSharing server. It assembles {@link OpenSharing} from {@link - * ServerProperties} and serves it over HTTP with Armeria. An embedding host skips this class and - * builds {@link OpenSharing} itself. + * Runnable standalone OpenSharing server. It builds {@link OpenSharing} from {@link + * ServerProperties} and listens for HTTP with Armeria. */ public final class OpenSharingServer implements AutoCloseable { @@ -28,10 +27,15 @@ private OpenSharingServer(Server server, OpenSharing openSharing) { /** * Starts the server with the settings in {@code server.properties}, overridden by JVM system - * properties and environment variables, and blocks until the JVM shuts down. Program arguments - * are ignored, so settings are passed as {@code -D} JVM options instead. + * properties and environment variables, and blocks until the JVM shuts down. It takes no program + * arguments. */ public static void main(String[] args) throws InterruptedException { + if (args.length > 0) { + throw new IllegalArgumentException( + "program arguments are not supported; set server.properties keys as -D JVM options or" + + " environment variables"); + } OpenSharingServer server = start(new ServerProperties()); server.server.closeOnJvmShutdown(); server.server.blockUntilShutdown(); @@ -49,11 +53,16 @@ public static OpenSharingServer start(ServerProperties properties) { // Armeria refuses to start without a service; routes nothing serves are not found. .serviceUnder("/", (ctx, req) -> HttpResponse.of(HttpStatus.NOT_FOUND)) .build(); - server.start().join(); + try { + server.start().join(); + } catch (RuntimeException e) { + server.close(); + throw e; + } return new OpenSharingServer(server, openSharing); } - /** The OpenSharing instance this server serves. */ + /** The OpenSharing instance built from the server's settings. */ public OpenSharing openSharing() { return openSharing; } diff --git a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java index 4c4e7837b..d3a62827a 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java +++ b/server/opensharing-server/src/main/java/io/opensharing/catalog/local/LocalCatalogLoader.java @@ -36,9 +36,7 @@ public static LocalCatalogFile load(String location) { } } - /** - * Parses {@code in} without closing it. {@code origin} only names the source in error messages. - */ + /** Parses {@code in}. {@code origin} only names the source in error messages. */ public static LocalCatalogFile load(InputStream in, String origin) { try { LocalCatalogFile file = MAPPER.readValue(in, LocalCatalogFile.class); diff --git a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java index d8a98bfc9..c374fbe99 100644 --- a/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java +++ b/server/opensharing-server/src/main/java/io/opensharing/config/ServerProperties.java @@ -3,17 +3,22 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.Locale; +import java.util.Map; import java.util.Properties; /** * The standalone server's settings, read from {@code server.properties} on the classpath. A JVM - * system property or an environment variable with the same name as a key overrides it. + * system property with the same name as a key overrides it, then an environment variable with that + * name or its upper snake case form, such as {@code SERVER_PORT} for {@code server.port}. A blank + * value anywhere means unset. */ public class ServerProperties { private static final String RESOURCE = "server.properties"; private final Properties properties = new Properties(defaults()); + private final Map env; public enum Property { PORT("server.port", "8080"), @@ -38,19 +43,29 @@ public ServerProperties() { throw new UncheckedIOException("failed to read " + RESOURCE, e); } dropBlankValues(); + env = System.getenv(); } - public ServerProperties(Properties overrides) { - properties.putAll(overrides); + /** Settings read from {@code values} instead of {@code server.properties}. */ + public ServerProperties(Properties values) { + this(values, System.getenv()); + } + + ServerProperties(Properties values, Map env) { + properties.putAll(values); dropBlankValues(); + this.env = env; } public String get(Property property) { String value = System.getProperty(property.key); - if (value == null) { - value = System.getenv(property.key); + if (isBlank(value)) { + value = env.get(property.key); + } + if (isBlank(value)) { + value = env.get(property.key.toUpperCase(Locale.ROOT).replaceAll("[.-]", "_")); } - return value != null ? value : properties.getProperty(property.key); + return isBlank(value) ? properties.getProperty(property.key) : value; } public int getPort() { @@ -71,7 +86,10 @@ private static Properties defaults() { return defaults; } - // A blank value in server.properties means unset, so the default applies. + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + private void dropBlankValues() { properties.entrySet().removeIf(entry -> entry.getValue().toString().isBlank()); } diff --git a/server/opensharing-server/src/main/resources/server.properties b/server/opensharing-server/src/main/resources/server.properties index 0ee023e59..8882e4c57 100644 --- a/server/opensharing-server/src/main/resources/server.properties +++ b/server/opensharing-server/src/main/resources/server.properties @@ -1,6 +1,7 @@ ## OpenSharing standalone server settings, read from the classpath. ## A JVM system property (-Dserver.port=9090) or an environment variable with the same name as a -## key overrides it. A key left blank keeps its default. +## key, or its upper snake case form (SERVER_PORT=9090), overrides it. A blank value keeps the +## default. # HTTP port the server listens on; 0 picks a free one. server.port=8080 diff --git a/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java b/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java index 80802a55a..9a839d20c 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/OpenSharingServerTest.java @@ -55,6 +55,18 @@ void buildsOpenSharingWithTheConfiguredCatalog() { } } + @Test + void rejectsProgramArguments() { + IllegalArgumentException rejected = + assertThrows( + IllegalArgumentException.class, + () -> OpenSharingServer.main(new String[] {"--server.port=9090"})); + assertEquals( + "program arguments are not supported; set server.properties keys as -D JVM options or" + + " environment variables", + rejected.getMessage()); + } + private static ServerProperties properties(String... overrides) { Properties props = new Properties(); props.setProperty("server.port", "0"); diff --git a/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java index 0ca4836e4..657a6c913 100644 --- a/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java +++ b/server/opensharing-server/src/test/java/io/opensharing/config/ServerPropertiesTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import io.opensharing.config.ServerProperties.Property; +import java.util.Map; import java.util.Properties; import org.junit.jupiter.api.Test; @@ -40,6 +41,33 @@ void letsSystemPropertiesOverrideTheFile() { } } + @Test + void ignoresBlankOverrides() { + ServerProperties properties = + new ServerProperties(props("server.port", "9090"), Map.of("CATALOG_TYPE", " ")); + System.setProperty("server.port", ""); + try { + assertEquals(9090, properties.getPort()); + assertEquals("local", properties.get(Property.CATALOG_TYPE)); + } finally { + System.clearProperty("server.port"); + } + } + + @Test + void readsEnvironmentVariablesByKeyOrUpperSnakeCase() { + ServerProperties properties = + new ServerProperties( + new Properties(), + Map.of( + "SERVER_PORT", "7070", + "catalog.type", "dotted", + "CATALOG_TYPE", "snake")); + + assertEquals(7070, properties.getPort()); + assertEquals("dotted", properties.get(Property.CATALOG_TYPE)); + } + @Test void rejectsAPortThatIsNotANumber() { ServerProperties properties = new ServerProperties(props("server.port", "eighty")); diff --git a/server/pom.xml b/server/pom.xml index e33f4ccd1..287199aa4 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -28,6 +28,8 @@ UTF-8 21 1.28.4 + + 4.1.126.Final 2.19.1 3.1.0 2.0.17 @@ -46,6 +48,13 @@ opensharing-server-core ${project.version} + + io.netty + netty-bom + ${netty.version} + pom + import + com.linecorp.armeria armeria-bom