/* * Copyright (c) 2015 Spotify AB. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.spotify.heroic.http; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.spotify.heroic.HeroicConfigurationContext; import com.spotify.heroic.HeroicCoreInstance; import com.spotify.heroic.dagger.CoreComponent; import com.spotify.heroic.jetty.JettyJSONErrorHandler; import com.spotify.heroic.jetty.JettyServerConnector; import com.spotify.heroic.lifecycle.LifeCycleRegistry; import com.spotify.heroic.lifecycle.LifeCycles; import com.spotify.heroic.servlet.ShutdownFilter; import eu.toolchain.async.AsyncFramework; import eu.toolchain.async.AsyncFuture; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.rewrite.handler.RewritePatternRule; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.Slf4jRequestLog; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import javax.inject.Inject; import javax.inject.Named; import javax.ws.rs.core.MediaType; import java.net.InetSocketAddress; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import java.util.function.Function; import java.util.function.Supplier; @HttpServerScope @Slf4j @ToString(of = {"address"}) public class HttpServer implements LifeCycles { public static final String DEFAULT_CORS_ALLOW_ORIGIN = "*"; private final InetSocketAddress address; private final HeroicCoreInstance instance; private final HeroicConfigurationContext config; private final ObjectMapper mapper; private final AsyncFramework async; private final boolean enableCors; private final Optional<String> corsAllowOrigin; private final List<JettyServerConnector> connectors; private final Supplier<Boolean> stopping; private volatile Server server = null; private final Object lock = new Object(); @Inject public HttpServer( @Named("bind") final InetSocketAddress address, final HeroicCoreInstance instance, final HeroicConfigurationContext config, @Named(MediaType.APPLICATION_JSON) final ObjectMapper mapper, final AsyncFramework async, @Named("enableCors") final boolean enableCors, @Named("corsAllowOrigin") final Optional<String> corsAllowOrigin, final List<JettyServerConnector> connectors, @Named("stopping") final Supplier<Boolean> stopping ) { this.address = address; this.instance = instance; this.config = config; this.mapper = mapper; this.async = async; this.enableCors = enableCors; this.corsAllowOrigin = corsAllowOrigin; this.connectors = connectors; this.stopping = stopping; } @Override public void register(final LifeCycleRegistry registry) { registry.start(this::start); registry.stop(this::stop); } public int getPort() { if (server == null) { throw new IllegalStateException("Server is not running"); } return findServerConnector(server).getLocalPort(); } private List<Connector> setupConnectors(final Server server) { return ImmutableList.copyOf( connectors.stream().map(c -> c.setup(server, address)).iterator()); } private ServerConnector findServerConnector(Server server) { final Connector[] connectors = server.getConnectors(); if (connectors.length == 0) { throw new IllegalStateException("server has no connectors"); } for (final Connector c : connectors) { if (!(c instanceof ServerConnector)) { continue; } return (ServerConnector) c; } throw new IllegalStateException("Server has no associated ServerConnector"); } private AsyncFuture<Void> start() { final Server newServer = new Server(); setupConnectors(newServer).forEach(newServer::addConnector); try { newServer.setHandler(setupHandler()); } catch (final Exception e) { return async.failed(e); } return async.call(() -> { synchronized (lock) { if (server != null) { throw new RuntimeException("Server already started"); } newServer.start(); server = newServer; } log.info("Started HTTP Server on {}", address); return null; }); } private AsyncFuture<Void> stop() { return async.call((Callable<Void>) () -> { final Server s; synchronized (lock) { if (server == null) { throw new IllegalStateException("Server has not been started"); } s = server; server = null; } log.info("Stopping http server"); s.stop(); s.join(); return null; }); } private HandlerCollection setupHandler() throws Exception { final ResourceConfig resourceConfig = setupResourceConfig(); final ServletContainer servlet = new ServletContainer(resourceConfig); final ServletHolder jerseyServlet = new ServletHolder(servlet); // Initialize and register Jersey ServletContainer jerseyServlet.setInitOrder(1); // statically provide injector to jersey application. final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); context.setContextPath("/"); context.addServlet(jerseyServlet, "/*"); context.addFilter(new FilterHolder(new ShutdownFilter(stopping, mapper)), "/*", null); context.setErrorHandler(new JettyJSONErrorHandler(mapper)); final RequestLogHandler requestLogHandler = new RequestLogHandler(); requestLogHandler.setRequestLog(new Slf4jRequestLog()); final RewriteHandler rewrite = new RewriteHandler(); makeRewriteRules(rewrite); final HandlerCollection handlers = new HandlerCollection(); handlers.setHandlers(new Handler[]{rewrite, context, requestLogHandler}); return handlers; } private void makeRewriteRules(RewriteHandler rewrite) { { final RewritePatternRule rule = new RewritePatternRule(); rule.setPattern("/metrics"); rule.setReplacement("/query/metrics"); rewrite.addRule(rule); } { final RewritePatternRule rule = new RewritePatternRule(); rule.setPattern("/metrics-stream/*"); rule.setReplacement("/query/metrics-stream"); rewrite.addRule(rule); } { final RewritePatternRule rule = new RewritePatternRule(); rule.setPattern("/tags"); rule.setReplacement("/metadata/tags"); rewrite.addRule(rule); } { final RewritePatternRule rule = new RewritePatternRule(); rule.setPattern("/keys"); rule.setReplacement("/metadata/keys"); rewrite.addRule(rule); } } private ResourceConfig setupResourceConfig() throws Exception { final ResourceConfig c = new ResourceConfig(); int count = 0; for (final Function<CoreComponent, List<Object>> resource : config.getResources()) { if (log.isTraceEnabled()) { log.trace("Loading resource: {}", resource); } final List<Object> resources = instance.inject(resource::apply); for (final Object r : resources) { c.register(r); } count += resources.size(); } // Resources. if (enableCors) { c.register(new CorsResponseFilter(corsAllowOrigin.orElse(DEFAULT_CORS_ALLOW_ORIGIN))); } log.info("Loaded {} resource(s)", count); return c; } }