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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
* 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 {

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);
}
}
}
80 changes: 39 additions & 41 deletions server/opensharing-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,8 @@
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria</artifactId>
</dependency>

<dependency>
Expand All @@ -45,48 +32,59 @@
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>io.opensharing.OpenSharingServer</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Builds the runnable opensharing-server-*-exec.jar with every dependency inside. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<mainClass>io.opensharing.OpenSharingServer</mainClass>
<classifier>exec</classifier>
<skip>true</skip>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>exec</shadedClassifierName>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>repackage</goal>
<goal>shade</goal>
</goals>
</execution>
</executions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.opensharing;

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.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. It builds {@link OpenSharing} from {@link
* ServerProperties} and listens for HTTP with Armeria.
*/
public final class OpenSharingServer implements AutoCloseable {

private final Server server;
private final OpenSharing openSharing;

private OpenSharingServer(Server server, OpenSharing openSharing) {
this.server = server;
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. 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();
}

/**
* 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 =
Server.builder()
.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();
try {
server.start().join();
} catch (RuntimeException e) {
server.close();
throw e;
}
return new OpenSharingServer(server, openSharing);
}

/** The OpenSharing instance built from the server's settings. */
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)) {
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 + "'");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
/** Reads a {@link LocalCatalogFile} from YAML or JSON, matching keys and enums ignoring case. */
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)
Expand All @@ -18,14 +24,49 @@ public final class LocalCatalogLoader {

private LocalCatalogLoader() {}

/**
* 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);
} catch (IOException e) {
throw new CatalogException("failed to read local catalog file " + location, e);
}
}

/** 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);
// 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);
} catch (IllegalArgumentException e) {
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)) {
// 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);
} catch (NoSuchFileException e) {
in = null;
}
}
if (in == null) {
throw new IllegalStateException("local catalog file '" + location + "' does not exist");
}
return in;
}
}
Loading
Loading