diff --git a/conf/application.conf b/conf/application.conf index 576dc9f..d4d7dd3 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -23,7 +23,9 @@ application { defaultResponseListener = "io.sinistral.proteus.server.handlers.ServerDefaultResponseListener" tmpdir = ${java.io.tmpdir}/${application.name} - + + favicon = "/io/sinistral/proteus/favicon.ico" + } api.version="v1" @@ -105,7 +107,7 @@ undertow truststorePassword="password" } - + enableHttp2=true # x AvailableProcessors ioThreads = 16 workerThreads = 200 diff --git a/src/main/java/io/sinistral/proteus/ProteusApplication.java b/src/main/java/io/sinistral/proteus/ProteusApplication.java index ae61e9c..74b28bd 100644 --- a/src/main/java/io/sinistral/proteus/ProteusApplication.java +++ b/src/main/java/io/sinistral/proteus/ProteusApplication.java @@ -2,7 +2,14 @@ * */ package io.sinistral.proteus; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; @@ -15,6 +22,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import javax.ws.rs.core.MediaType; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,15 +55,19 @@ import io.undertow.Undertow; import io.undertow.UndertowOptions; import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; import io.undertow.server.RoutingHandler; +import io.undertow.util.Headers; +import io.undertow.util.Methods; + /** * @author jbauer */ public class ProteusApplication { - + private static Logger log = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ProteusApplication.class.getCanonicalName()); - + @Inject @Named("registeredControllers") protected Set> registeredControllers; @@ -62,82 +75,79 @@ public class ProteusApplication @Inject @Named("registeredEndpoints") protected Set registeredEndpoints; - + @Inject @Named("registeredServices") protected Set> registeredServices; - + @Inject protected RoutingHandler router; - + @Inject protected Config config; - + protected List registeredModules = new ArrayList<>(); protected Injector injector = null; protected ServiceManager serviceManager = null; - protected Undertow undertow = null; + protected Undertow undertow = null; protected Class rootHandlerClass; protected HttpHandler rootHandler; protected AtomicBoolean running = new AtomicBoolean(false); protected List ports = new ArrayList<>(); - protected Function serverConfigurationFunction = null; + protected Function serverConfigurationFunction = null; - public ProteusApplication() { - - injector = Guice.createInjector(new ConfigModule()); - injector.injectMembers(this); - + + injector = Guice.createInjector(new ConfigModule()); + injector.injectMembers(this); + } - + public ProteusApplication(String configFile) { - - injector = Guice.createInjector(new ConfigModule(configFile)); - injector.injectMembers(this); - + + injector = Guice.createInjector(new ConfigModule(configFile)); + injector.injectMembers(this); + } - + public ProteusApplication(URL configURL) { - - injector = Guice.createInjector(new ConfigModule(configURL)); - injector.injectMembers(this); - + + injector = Guice.createInjector(new ConfigModule(configURL)); + injector.injectMembers(this); + } - + public void start() { - if(this.isRunning()) + if (this.isRunning()) { log.warn("Server has already started..."); return; } - + injector = injector.createChildInjector(registeredModules); - - if( rootHandlerClass == null && rootHandler == null ) + + if (rootHandlerClass == null && rootHandler == null) { log.warn("No root handler class or root HttpHandler was specified, using default ServerDefaultHttpHandler."); rootHandlerClass = ServerDefaultHttpHandler.class; } - + log.info("Starting services..."); - - Set services = registeredServices.stream() - .map( sc -> injector.getInstance(sc) ) - .collect(Collectors.toSet()); - + + Set services = registeredServices.stream().map(sc -> injector.getInstance(sc)).collect(Collectors.toSet()); + serviceManager = new ServiceManager(services); serviceManager.addListener(new Listener() { public void stopped() { - undertow.stop(); + undertow.stop(); running.set(false); } @@ -146,11 +156,11 @@ public void healthy() log.info("Services are healthy..."); buildServer(); - + undertow.start(); - - printStatus(); - + + printStatus(); + running.set(true); } @@ -158,7 +168,7 @@ public void failure(Service service) { log.error("Service failure: " + service); } - + }, MoreExecutors.directExecutor()); Runtime.getRuntime().addShutdownHook(new Thread() @@ -176,56 +186,58 @@ public void run() } }); - serviceManager.startAsync(); - - } - + serviceManager.startAsync(); + + } + public void shutdown() throws TimeoutException { - if(!this.isRunning()) + if (!this.isRunning()) { - log.warn("Server is not running..."); - + log.warn("Server is not running..."); + return; } - + log.info("Shutting down..."); - serviceManager.stopAsync().awaitStopped(8, TimeUnit.SECONDS); + serviceManager.stopAsync().awaitStopped(8, TimeUnit.SECONDS); log.info("Shutdown complete."); } - + public boolean isRunning() { return this.running.get(); } - + public void buildServer() { - - for(Class controllerClass : registeredControllers) + + for (Class controllerClass : registeredControllers) { - HandlerGenerator generator = new HandlerGenerator("io.sinistral.proteus.controllers.handlers",controllerClass); - + HandlerGenerator generator = new HandlerGenerator("io.sinistral.proteus.controllers.handlers", controllerClass); + injector.injectMembers(generator); - + try { Supplier generatedRouteSupplier = injector.getInstance(generator.compileClass()); - + router.addAll(generatedRouteSupplier.get()); - + } catch (Exception e) { - log.error("Exception creating handlers for " + controllerClass.getName() + "!!!\n" + e.getMessage(), e); + log.error("Exception creating handlers for " + controllerClass.getName() + "!!!\n" + e.getMessage(), e); } - + } - - final HttpHandler handler; - - if( rootHandlerClass != null ) + + this.addDefaultRoutes(router); + + final HttpHandler handler; + + if (rootHandlerClass != null) { handler = injector.getInstance(rootHandlerClass); } @@ -233,93 +245,78 @@ public void buildServer() { handler = rootHandler; } - - Undertow.Builder undertowBuilder = Undertow.builder() - .addHttpListener(config.getInt("application.ports.http"),config.getString("application.host")) - .setBufferSize(16 * 1024) - .setIoThreads( config.getInt("undertow.ioThreads") ) - .setServerOption(UndertowOptions.ENABLE_HTTP2, config.getBoolean("undertow.enableHttp2")) - .setServerOption(UndertowOptions.ALWAYS_SET_DATE, true) - .setSocketOption(org.xnio.Options.BACKLOG, config.getInt("undertow.socket.backlog") ) - .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) - .setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false) - .setServerOption(UndertowOptions.MAX_ENTITY_SIZE, config.getBytes("undertow.server.maxEntitySize") ) - .setWorkerThreads( config.getInt("undertow.workerThreads") ) - .setHandler( handler ); - - + + Undertow.Builder undertowBuilder = Undertow.builder().addHttpListener(config.getInt("application.ports.http"), config.getString("application.host")).setBufferSize(16 * 1024).setIoThreads(config.getInt("undertow.ioThreads")) + .setServerOption(UndertowOptions.ENABLE_HTTP2, config.getBoolean("undertow.enableHttp2")).setServerOption(UndertowOptions.ALWAYS_SET_DATE, true).setSocketOption(org.xnio.Options.BACKLOG, config.getInt("undertow.socket.backlog")) + .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false).setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false).setServerOption(UndertowOptions.MAX_ENTITY_SIZE, config.getBytes("undertow.server.maxEntitySize")) + .setWorkerThreads(config.getInt("undertow.workerThreads")).setHandler(handler); + ports.add(config.getInt("application.ports.http")); - - if( config.getBoolean("undertow.ssl.enabled") ) + + if (config.getBoolean("undertow.ssl.enabled")) { try { - KeyStore keyStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.keystorePath"), config.getString("undertow.ssl.keystorePassword") ); - KeyStore trustStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.truststorePath"), config.getString("undertow.ssl.truststorePassword") ); - - - undertowBuilder.addHttpsListener(config.getInt("application.ports.https"), config.getString("application.host"), - SecurityOps.createSSLContext( - keyStore, - trustStore, - config.getString("undertow.ssl.keystorePassword") - )); + KeyStore keyStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.keystorePath"), config.getString("undertow.ssl.keystorePassword")); + KeyStore trustStore = SecurityOps.loadKeyStore(config.getString("undertow.ssl.truststorePath"), config.getString("undertow.ssl.truststorePassword")); + + undertowBuilder.addHttpsListener(config.getInt("application.ports.https"), config.getString("application.host"), SecurityOps.createSSLContext(keyStore, trustStore, config.getString("undertow.ssl.keystorePassword"))); ports.add(config.getInt("application.ports.https")); } catch (Exception e) { - log.error(e.getMessage(),e); + log.error(e.getMessage(), e); } - } - - if( serverConfigurationFunction != null ) + } + + if (serverConfigurationFunction != null) { undertowBuilder = serverConfigurationFunction.apply(undertowBuilder); } - + this.undertow = undertowBuilder.build(); - + } - + public ProteusApplication addService(Class serviceClass) { registeredServices.add(serviceClass); return this; } - + public ProteusApplication addController(Class controllerClass) { registeredControllers.add(controllerClass); return this; } - + public ProteusApplication addModule(Module module) { registeredModules.add(module); return this; } - - public void setRootHandlerClass( Class rootHandlerClass ) + + public void setRootHandlerClass(Class rootHandlerClass) { this.rootHandlerClass = rootHandlerClass; } - - public void setRootHandler( HttpHandler rootHandler ) + + public void setRootHandler(HttpHandler rootHandler) { this.rootHandler = rootHandler; } - - + public Undertow getUndertow() { return undertow; } - /** * Allows direct access to the Undertow.Builder for custom configuration - * @param serverConfigurationFunction the serverConfigurationFunction + * + * @param serverConfigurationFunction + * the serverConfigurationFunction */ public void setServerConfigurationFunction(Function serverConfigurationFunction) { @@ -341,74 +338,145 @@ public Config getConfig() { return config; } - + public void printStatus() { Config globalHeaders = config.getConfig("globalHeaders"); - - Map globalHeadersParameters = globalHeaders.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().render())); - - - StringBuilder sb = new StringBuilder(); + + Map globalHeadersParameters = globalHeaders.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().render())); + + StringBuilder sb = new StringBuilder(); sb.append("\n\nUsing global headers: \n\n"); - sb.append(globalHeadersParameters.entrySet().stream().map( e -> "\t" + e.getKey() + " = " + e.getValue() ).collect(Collectors.joining("\n"))); - sb.append("\n\nRegistered endpoints: \n\n"); - sb.append(this.registeredEndpoints.stream().sorted().map(EndpointInfo::toString).collect(Collectors.joining("\n"))); - sb.append("\n\nRegistered services: \n\n"); - - ImmutableMultimap serviceStateMap = this.serviceManager.servicesByState(); - - String serviceStrings = serviceStateMap.asMap().entrySet().stream().sorted().flatMap( e -> { - - - return e.getValue().stream().map( s -> { - return "\t" + s.getClass().getSimpleName() + "\t" + e.getKey(); - }); - - - }).collect(Collectors.joining("\n")); - - sb.append(serviceStrings); - - sb.append("\n"); - - sb.append("\nListening on: " + this.ports); - - sb.append("\n"); - - log.info(sb.toString()); + sb.append(globalHeadersParameters.entrySet().stream().map(e -> "\t" + e.getKey() + " = " + e.getValue()).collect(Collectors.joining("\n"))); + sb.append("\n\nRegistered endpoints: \n\n"); + sb.append(this.registeredEndpoints.stream().sorted().map(EndpointInfo::toString).collect(Collectors.joining("\n"))); + sb.append("\n\nRegistered services: \n\n"); + + ImmutableMultimap serviceStateMap = this.serviceManager.servicesByState(); + + String serviceStrings = serviceStateMap.asMap().entrySet().stream().sorted().flatMap(e -> { + + return e.getValue().stream().map(s -> { + return "\t" + s.getClass().getSimpleName() + "\t" + e.getKey(); + }); + + }).collect(Collectors.joining("\n")); + + sb.append(serviceStrings); + + sb.append("\n"); + + sb.append("\nListening on: " + this.ports); + + sb.append("\n"); + + log.info(sb.toString()); } - public static void main(String[] args) + public void addDefaultRoutes(RoutingHandler router) { - try - { - - JsonIterator.setMode(DecodingMode.DYNAMIC_MODE_AND_MATCH_FIELD_WITH_HASH); - JsonStream.setMode(EncodingMode.DYNAMIC_MODE); - JsoniterAnnotationSupport.enable(); - - ProteusApplication app = new ProteusApplication(); - - app.addService(SwaggerService.class); - - app.addService(AssetsService.class); - - app.setRootHandlerClass(ServerDefaultHttpHandler.class); - - app.start(); - - - - } catch (Exception e) + if (config.hasPath("health.statusPath")) { - log.error(e.getMessage(),e); + try + { + final String statusPath = config.getString("health.statusPath"); + + router.add(Methods.GET, statusPath, new HttpHandler() + { + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception + { + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, MediaType.TEXT_PLAIN); + exchange.getResponseSender().send("OK"); + } + + }); + + this.registeredEndpoints.add(EndpointInfo.builder().withConsumes("*/*").withProduces("text/plain").withPathTemplate(statusPath).withControllerName("Internal").withMethod(Methods.GET).build()); + + } catch (Exception e) + { + log.error("Error adding health status route.", e.getMessage()); + } } + if (config.hasPath("application.favicon")) + { + try + { + + final ByteBuffer faviconImageBuffer; + + final File faviconFile = new File(config.getString("application.favicon")); + + if (!faviconFile.exists()) + { + try (final InputStream stream = this.getClass().getResourceAsStream(config.getString("application.favicon"))) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + byte[] buffer = new byte[4096]; + int read = 0; + while (read != -1) + { + read = stream.read(buffer); + if (read > 0) + { + baos.write(buffer, 0, read); + } + } + + faviconImageBuffer = ByteBuffer.wrap(baos.toByteArray()); + } + + } + else + { + try (final InputStream stream = Files.newInputStream(Paths.get(config.getString("application.favicon")))) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + byte[] buffer = new byte[4096]; + int read = 0; + while (read != -1) + { + read = stream.read(buffer); + if (read > 0) + { + baos.write(buffer, 0, read); + } + } + faviconImageBuffer = ByteBuffer.wrap(baos.toByteArray()); + } + } + + if (faviconImageBuffer != null) + { + + router.add(Methods.GET, "favicon.ico", new HttpHandler() + { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception + { + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, io.sinistral.proteus.server.MediaType.IMAGE_X_ICON.toString()); + exchange.getResponseSender().send(faviconImageBuffer); + } + + }); + + } + + } catch (Exception e) + { + log.error("Error adding favicon route.", e.getMessage()); + } + } } - + + } diff --git a/src/main/java/io/sinistral/proteus/modules/ApplicationModule.java b/src/main/java/io/sinistral/proteus/modules/ApplicationModule.java index d3c19e3..58fc3aa 100644 --- a/src/main/java/io/sinistral/proteus/modules/ApplicationModule.java +++ b/src/main/java/io/sinistral/proteus/modules/ApplicationModule.java @@ -7,8 +7,6 @@ import java.util.Set; import java.util.TreeSet; -import javax.ws.rs.core.MediaType; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,10 +26,7 @@ import io.sinistral.proteus.server.endpoints.EndpointInfo; import io.undertow.server.DefaultResponseListener; import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; import io.undertow.server.RoutingHandler; -import io.undertow.util.Headers; -import io.undertow.util.Methods; /** * @author jbauer @@ -60,25 +55,6 @@ protected void configure() this.binder().requestInjection(this); RoutingHandler router = new RoutingHandler(); - - if(config.hasPath("health.statusPath")) - { - final String statusPath = config.getString("health.statusPath"); - - router.add(Methods.GET, statusPath, new HttpHandler() - { - - @Override - public void handleRequest(HttpServerExchange exchange) throws Exception - { - exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, MediaType.TEXT_PLAIN); - exchange.getResponseSender().send("OK"); - } - - }); - - this.registeredEndpoints.add(EndpointInfo.builder().withConsumes("*/*").withProduces("text/plain").withPathTemplate(statusPath).withControllerName("Internal").withMethod(Methods.GET).build()); - } try { diff --git a/src/main/resources/io/sinistral/proteus/favicon.ico b/src/main/resources/io/sinistral/proteus/favicon.ico new file mode 100644 index 0000000..6f383f0 Binary files /dev/null and b/src/main/resources/io/sinistral/proteus/favicon.ico differ diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 576dc9f..b68c5bb 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -23,6 +23,9 @@ application { defaultResponseListener = "io.sinistral.proteus.server.handlers.ServerDefaultResponseListener" tmpdir = ${java.io.tmpdir}/${application.name} + + # path to default favicon file + favicon = "/io/sinistral/proteus/favicon.ico" } @@ -51,6 +54,8 @@ assets { # cache timeout for the assets time = 500 } + + } @@ -105,7 +110,7 @@ undertow truststorePassword="password" } - + enableHttp2=true # x AvailableProcessors ioThreads = 16 workerThreads = 200