diff --git a/platform-http-service-framework/build.gradle.kts b/platform-http-service-framework/build.gradle.kts new file mode 100644 index 0000000..5a77c48 --- /dev/null +++ b/platform-http-service-framework/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + `java-library` + id("org.hypertrace.publish-plugin") +} + +dependencies { + api(project(":platform-service-framework")) + api("org.hypertrace.core.grpcutils:grpc-client-utils:0.7.6") + api("com.typesafe:config:1.4.2") + api("javax.servlet:javax.servlet-api:4.0.1") + api("com.google.inject:guice:5.1.0") + api(project(":service-framework-spi")) + + implementation("org.slf4j:slf4j-api:1.7.36") + implementation("com.google.inject.extensions:guice-servlet:5.1.0") + implementation("com.google.guava:guava:31.1-jre") + implementation("org.eclipse.jetty:jetty-servlet:9.4.48.v20220622") + implementation("org.eclipse.jetty:jetty-server:9.4.48.v20220622") + implementation("org.eclipse.jetty:jetty-servlets:9.4.48.v20220622") + + annotationProcessor("org.projectlombok:lombok:1.18.24") + compileOnly("org.projectlombok:lombok:1.18.24") + +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainer.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainer.java new file mode 100644 index 0000000..ba5f358 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainer.java @@ -0,0 +1,11 @@ +package org.hypertrace.core.serviceframework.http; + +public interface HttpContainer { + void start(); + + void blockUntilStopped(); + + void stop(); + + boolean isStopped(); +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainerEnvironment.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainerEnvironment.java new file mode 100644 index 0000000..f4ec985 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpContainerEnvironment.java @@ -0,0 +1,13 @@ +package org.hypertrace.core.serviceframework.http; + +import com.typesafe.config.Config; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; +import org.hypertrace.core.serviceframework.spi.PlatformServiceLifecycle; + +public interface HttpContainerEnvironment { + GrpcChannelRegistry getChannelRegistry(); + + Config getConfig(String serviceName); + + PlatformServiceLifecycle getLifecycle(); +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerDefinition.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerDefinition.java new file mode 100644 index 0000000..331770a --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerDefinition.java @@ -0,0 +1,32 @@ +package org.hypertrace.core.serviceframework.http; + +import com.google.inject.Injector; +import java.util.List; +import javax.servlet.MultipartConfigElement; +import javax.servlet.Servlet; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; + +@Value +@Builder +public class HttpHandlerDefinition { + String name; + int port; + String contextPath; + Servlet servlet; + int maxHeaderSizeBytes; + CorsConfig corsConfig; + Injector injector; + MultipartConfigElement multipartConfig; + + @Accessors(fluent = true) + boolean useSessions; + + @Value + @Builder + public static class CorsConfig { + List allowedHeaders; + List allowedOrigins; + } +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerFactory.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerFactory.java new file mode 100644 index 0000000..47f5d50 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/HttpHandlerFactory.java @@ -0,0 +1,8 @@ +package org.hypertrace.core.serviceframework.http; + +import java.util.List; + +@FunctionalInterface +public interface HttpHandlerFactory { + List buildHandlers(HttpContainerEnvironment containerEnvironment); +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/ServerBuilder.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/ServerBuilder.java new file mode 100644 index 0000000..8cd4f1d --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/ServerBuilder.java @@ -0,0 +1,14 @@ +package org.hypertrace.core.serviceframework.http; + +import java.util.List; +import java.util.concurrent.ExecutorService; + +public interface ServerBuilder { + T addHandler(HttpHandlerDefinition handlerDefinition); + + T addHandlers(List handlerDefinitions); + + T setExecutor(ExecutorService executorService); + + HttpContainer build(); +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpContainerEnvironment.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpContainerEnvironment.java new file mode 100644 index 0000000..b0cb9da --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpContainerEnvironment.java @@ -0,0 +1,20 @@ +package org.hypertrace.core.serviceframework.http; + +import com.typesafe.config.Config; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; +import org.hypertrace.core.serviceframework.config.ConfigClient; +import org.hypertrace.core.serviceframework.spi.PlatformServiceLifecycle; + +@AllArgsConstructor +public class StandAloneHttpContainerEnvironment implements HttpContainerEnvironment { + @Getter private final GrpcChannelRegistry channelRegistry; + @Getter private final PlatformServiceLifecycle lifecycle; + private final ConfigClient configClient; + + @Override + public Config getConfig(String serviceName) { + return this.configClient.getConfig(serviceName, null, null, null); + } +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpPlatformServiceContainer.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpPlatformServiceContainer.java new file mode 100644 index 0000000..a9d5502 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/StandAloneHttpPlatformServiceContainer.java @@ -0,0 +1,58 @@ +package org.hypertrace.core.serviceframework.http; + +import static io.grpc.Deadline.after; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.hypertrace.core.grpcutils.client.GrpcChannelRegistry; +import org.hypertrace.core.serviceframework.PlatformService; +import org.hypertrace.core.serviceframework.config.ConfigClient; +import org.hypertrace.core.serviceframework.http.jetty.JettyHttpServerBuilder; + +@Slf4j +public abstract class StandAloneHttpPlatformServiceContainer extends PlatformService { + private HttpContainer container; + private final GrpcChannelRegistry grpcChannelRegistry = new GrpcChannelRegistry(); + + public StandAloneHttpPlatformServiceContainer(ConfigClient config) { + super(config); + } + + protected abstract List getHandlerFactories(); + + @Override + protected void doInit() { + this.container = + new JettyHttpServerBuilder().addHandlers(this.buildHandlerDefinitions()).build(); + } + + @Override + protected void doStart() { + log.info("Starting service {}", this.getServiceName()); + this.container.start(); + this.container.blockUntilStopped(); + } + + @Override + protected void doStop() { + log.info("Stopping service {}", this.getServiceName()); + grpcChannelRegistry.shutdown(after(10, SECONDS)); + this.container.stop(); + } + + @Override + public boolean healthCheck() { + return true; + } + + private List buildHandlerDefinitions() { + HttpContainerEnvironment environment = + new StandAloneHttpContainerEnvironment( + this.grpcChannelRegistry, this.getLifecycle(), this.configClient); + return this.getHandlerFactories().stream() + .flatMap(handlerFactory -> handlerFactory.buildHandlers(environment).stream()) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/guice/SimpleGuiceServletContextListener.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/guice/SimpleGuiceServletContextListener.java new file mode 100644 index 0000000..4d19d9a --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/guice/SimpleGuiceServletContextListener.java @@ -0,0 +1,15 @@ +package org.hypertrace.core.serviceframework.http.guice; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceServletContextListener; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class SimpleGuiceServletContextListener extends GuiceServletContextListener { + private final Injector injector; + + @Override + protected Injector getInjector() { + return injector; + } +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpContainer.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpContainer.java new file mode 100644 index 0000000..ff1d044 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpContainer.java @@ -0,0 +1,46 @@ +package org.hypertrace.core.serviceframework.http.jetty; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.eclipse.jetty.server.Server; +import org.hypertrace.core.serviceframework.http.HttpContainer; + +@RequiredArgsConstructor +class JettyHttpContainer implements HttpContainer { + private final Server server; + private final ExecutorService executorService; + private Future future; + + @Override + public void start() { + this.future = this.executorService.submit(this::startAndWaitUnchecked); + } + + @SneakyThrows + @Override + public void stop() { + this.executorService.shutdown(); + this.executorService.awaitTermination(30, SECONDS); + } + + @SneakyThrows + @Override + public void blockUntilStopped() { + this.future.get(); + } + + @Override + public boolean isStopped() { + return this.server.isStopped(); + } + + @SneakyThrows + private void startAndWaitUnchecked() { + this.server.start(); + this.server.join(); + } +} diff --git a/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpServerBuilder.java b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpServerBuilder.java new file mode 100644 index 0000000..04cbd73 --- /dev/null +++ b/platform-http-service-framework/src/main/java/org/hypertrace/core/serviceframework/http/jetty/JettyHttpServerBuilder.java @@ -0,0 +1,176 @@ +package org.hypertrace.core.serviceframework.http.jetty; + +import static com.google.common.base.Joiner.on; +import static java.util.Objects.isNull; +import static java.util.Optional.ofNullable; + +import com.google.inject.Injector; +import com.google.inject.servlet.GuiceFilter; +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.annotation.Nullable; +import javax.servlet.DispatcherType; +import javax.servlet.ServletContextListener; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.hypertrace.core.serviceframework.http.HttpContainer; +import org.hypertrace.core.serviceframework.http.HttpHandlerDefinition; +import org.hypertrace.core.serviceframework.http.HttpHandlerDefinition.CorsConfig; +import org.hypertrace.core.serviceframework.http.ServerBuilder; +import org.hypertrace.core.serviceframework.http.guice.SimpleGuiceServletContextListener; + +public class JettyHttpServerBuilder implements ServerBuilder { + private final List handlers = new LinkedList<>(); + @Nullable private ExecutorService executorService; + + @Override + public JettyHttpServerBuilder addHandler(HttpHandlerDefinition handlerDefinition) { + this.handlers.add(handlerDefinition); + return this; + } + + @Override + public JettyHttpServerBuilder addHandlers(List handlerDefinitions) { + handlerDefinitions.forEach(this::addHandler); + return this; + } + + @Override + public JettyHttpServerBuilder setExecutor(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + @Override + public HttpContainer build() { + Server server = new Server(); + this.handlers.stream() + .map( + (HttpHandlerDefinition definition) -> this.buildConnectorForHandler(server, definition)) + .forEach(server::addConnector); + + server.setHandler(this.buildCompositeHandler(this.handlers)); + server.setStopAtShutdown(true); + return new JettyHttpContainer( + server, + Optional.ofNullable(this.executorService).orElseGet(Executors::newSingleThreadExecutor)); + } + + private Connector buildConnectorForHandler( + Server server, HttpHandlerDefinition handlerDefinition) { + ServerConnector connector = + new ServerConnector(server, this.buildConnectionFactory(handlerDefinition)); + connector.setPort(handlerDefinition.getPort()); + connector.setName(handlerDefinition.getName()); + return connector; + } + + private HttpConnectionFactory buildConnectionFactory(HttpHandlerDefinition handlerDefinition) { + HttpConfiguration httpConfig = new HttpConfiguration(); + if (handlerDefinition.getMaxHeaderSizeBytes() > 0) { + httpConfig.setRequestHeaderSize(handlerDefinition.getMaxHeaderSizeBytes()); + } + return new HttpConnectionFactory(httpConfig); + } + + private Handler buildCompositeHandler(List handlerDefinitions) { + ContextHandlerCollection compositeHandler = new ContextHandlerCollection(); + + handlerDefinitions.stream().map(this::buildHandler).forEach(compositeHandler::addHandler); + + return compositeHandler; + } + + private Handler buildHandler(HttpHandlerDefinition handlerDefinition) { + int options = + handlerDefinition.useSessions() + ? ServletContextHandler.SESSIONS + : ServletContextHandler.NO_SESSIONS; + ServletContextHandler context = new ServletContextHandler(options); + this.buildCorsFilterIfRequired(handlerDefinition.getCorsConfig()) + .ifPresent( + corsFilter -> + context.addFilter( + corsFilter, + this.wildcardSubpath(handlerDefinition.getContextPath()), + EnumSet.of(DispatcherType.REQUEST))); + this.buildGuiceFilterIfRequired(handlerDefinition.getInjector()) + .ifPresent( + guiceFilter -> + context.addFilter( + guiceFilter, + this.wildcardSubpath(handlerDefinition.getContextPath()), + EnumSet.of(DispatcherType.REQUEST))); + this.buildGuiceContextListenerIfRequired(handlerDefinition.getInjector()) + .ifPresent(context::addEventListener); + this.buildServletHolderIfRequired(handlerDefinition) + .ifPresent( + servletHolder -> context.addServlet(servletHolder, handlerDefinition.getContextPath())); + context.setVirtualHosts(new String[] {"@" + handlerDefinition.getName()}); + return context; + } + + private Optional buildServletHolderIfRequired( + HttpHandlerDefinition handlerDefinition) { + if (isNull(handlerDefinition.getServlet())) { + return Optional.empty(); + } + ServletHolder servletHolder = new ServletHolder(handlerDefinition.getServlet()); + Optional.ofNullable(handlerDefinition.getMultipartConfig()) + .ifPresent(servletHolder.getRegistration()::setMultipartConfig); + return Optional.of(servletHolder); + } + + private Optional buildCorsFilterIfRequired(@Nullable CorsConfig config) { + if (isNull(config)) { + return Optional.empty(); + } + FilterHolder crossOriginFilterHolder = new FilterHolder(CrossOriginFilter.class); + ofNullable(config.getAllowedOrigins()) + .map(on(",")::join) + .ifPresent( + origins -> + crossOriginFilterHolder.setInitParameter( + CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origins)); + ofNullable(config.getAllowedHeaders()) + .map(on(",")::join) + .ifPresent( + headers -> + crossOriginFilterHolder.setInitParameter( + CrossOriginFilter.ALLOWED_HEADERS_PARAM, headers)); + return Optional.of(crossOriginFilterHolder); + } + + private Optional buildGuiceFilterIfRequired(@Nullable Injector injector) { + if (isNull(injector)) { + return Optional.empty(); + } + return Optional.of(new FilterHolder(GuiceFilter.class)); + } + + private Optional buildGuiceContextListenerIfRequired( + @Nullable Injector injector) { + if (isNull(injector)) { + return Optional.empty(); + } + return Optional.of(new SimpleGuiceServletContextListener(injector)); + } + + private String wildcardSubpath(String path) { + return Path.of(path, "*").toString(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 140202e..8bd7475 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ plugins { } include(":platform-grpc-service-framework") +include(":platform-http-service-framework") include(":platform-service-framework") include(":platform-metrics") include(":integrationtest-service-framework")