io.confluent.rest.Application.java Source code

Java tutorial

Introduction

Here is the source code for io.confluent.rest.Application.java

Source

/**
 * Copyright 2014 Confluent Inc.
 *
 * Licensed 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 io.confluent.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper;

import io.confluent.common.config.ConfigException;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.NetworkTrafficServerConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
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.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.validation.ValidationFeature;
import org.glassfish.jersey.servlet.ServletContainer;

import java.util.EnumSet;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.servlet.DispatcherType;
import javax.ws.rs.core.Configurable;

import io.confluent.common.metrics.JmxReporter;
import io.confluent.common.metrics.MetricConfig;
import io.confluent.common.metrics.Metrics;
import io.confluent.common.metrics.MetricsReporter;
import io.confluent.rest.exceptions.ConstraintViolationExceptionMapper;
import io.confluent.rest.exceptions.GenericExceptionMapper;
import io.confluent.rest.exceptions.WebApplicationExceptionMapper;
import io.confluent.rest.logging.Slf4jRequestLog;
import io.confluent.rest.metrics.MetricsResourceMethodApplicationListener;
import io.confluent.rest.validation.JacksonMessageBodyProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A REST application. Extend this class and implement setupResources() to register REST
 * resources with the JAX-RS server. Use createServer() to get a fully-configured, ready to run
 * Jetty server.
 */
public abstract class Application<T extends RestConfig> {
    protected T config;
    protected Server server = null;
    protected CountDownLatch shutdownLatch = new CountDownLatch(1);
    protected Metrics metrics;

    private static final Logger log = LoggerFactory.getLogger(Application.class);

    public Application(T config) {
        this.config = config;
        MetricConfig metricConfig = new MetricConfig().samples(config.getInt(RestConfig.METRICS_NUM_SAMPLES_CONFIG))
                .timeWindow(config.getLong(RestConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS);
        List<MetricsReporter> reporters = config.getConfiguredInstances(RestConfig.METRICS_REPORTER_CLASSES_CONFIG,
                MetricsReporter.class);
        reporters.add(new JmxReporter(config.getString(RestConfig.METRICS_JMX_PREFIX_CONFIG)));
        this.metrics = new Metrics(metricConfig, reporters, config.getTime());
    }

    /**
     * Register resources or additional Providers, ExceptionMappers, and other JAX-RS components with
     * the Jersey application. This, combined with your Configuration class, is where you can
     * customize the behavior of the application.
     */
    public abstract void setupResources(Configurable<?> config, T appConfig);

    /**
     * Returns a map of tag names to tag values to apply to metrics for this application.
     *
     * @return a Map of tags and values
     */
    public Map<String, String> getMetricsTags() {
        return new LinkedHashMap<String, String>();
    }

    /**
     * Configure and create the server.
     */
    public Server createServer() throws RestConfigException {
        // The configuration for the JAX-RS REST service
        ResourceConfig resourceConfig = new ResourceConfig();

        Map<String, String> metricTags = getMetricsTags();

        configureBaseApplication(resourceConfig, metricTags);
        setupResources(resourceConfig, getConfiguration());

        // Configure the servlet container
        ServletContainer servletContainer = new ServletContainer(resourceConfig);
        ServletHolder servletHolder = new ServletHolder(servletContainer);
        server = new Server() {
            @Override
            protected void doStop() throws Exception {
                super.doStop();
                Application.this.metrics.close();
                Application.this.onShutdown();
                Application.this.shutdownLatch.countDown();
            }
        };

        MetricsListener metricsListener = new MetricsListener(metrics, "jetty", metricTags);

        List<URI> listeners = parseListeners(config.getList(RestConfig.LISTENERS_CONFIG),
                config.getInt(RestConfig.PORT_CONFIG));
        for (URI listener : listeners) {
            log.info("Adding listener: " + listener.toString());
            NetworkTrafficServerConnector connector;
            if (listener.getScheme().equals("http")) {
                connector = new NetworkTrafficServerConnector(server);
            } else {
                SslContextFactory sslContextFactory = new SslContextFactory();
                // IMPORTANT: the key's CN, stored in the keystore, must match the FQDN. This is a Jetty requirement.
                // TODO: investigate this further. Would be better to use SubjectAltNames.
                if (!config.getString(RestConfig.SSL_KEYSTORE_LOCATION_CONFIG).isEmpty()) {
                    sslContextFactory.setKeyStorePath(config.getString(RestConfig.SSL_KEYSTORE_LOCATION_CONFIG));
                    sslContextFactory
                            .setKeyStorePassword(config.getString(RestConfig.SSL_KEYSTORE_PASSWORD_CONFIG));
                    sslContextFactory.setKeyManagerPassword(config.getString(RestConfig.SSL_KEY_PASSWORD_CONFIG));
                    sslContextFactory.setKeyStoreType(config.getString(RestConfig.SSL_KEYSTORE_TYPE_CONFIG));

                    if (!config.getString(RestConfig.SSL_KEYMANAGER_ALGORITHM_CONFIG).isEmpty()) {
                        sslContextFactory.setSslKeyManagerFactoryAlgorithm(
                                config.getString(RestConfig.SSL_KEYMANAGER_ALGORITHM_CONFIG));
                    }
                }

                sslContextFactory.setNeedClientAuth(config.getBoolean(RestConfig.SSL_CLIENT_AUTH_CONFIG));

                List<String> enabledProtocols = config.getList(RestConfig.SSL_ENABLED_PROTOCOLS_CONFIG);
                if (!enabledProtocols.isEmpty()) {
                    sslContextFactory.setIncludeProtocols((String[]) enabledProtocols.toArray());
                }

                List<String> cipherSuites = config.getList(RestConfig.SSL_CIPHER_SUITES_CONFIG);
                if (!cipherSuites.isEmpty()) {
                    sslContextFactory.setIncludeCipherSuites((String[]) cipherSuites.toArray());
                }

                if (!config.getString(RestConfig.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG).isEmpty()) {
                    sslContextFactory.setEndpointIdentificationAlgorithm(
                            config.getString(RestConfig.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG));
                }

                if (!config.getString(RestConfig.SSL_TRUSTSTORE_LOCATION_CONFIG).isEmpty()) {
                    sslContextFactory
                            .setTrustStorePath(config.getString(RestConfig.SSL_TRUSTSTORE_LOCATION_CONFIG));
                    sslContextFactory
                            .setTrustStorePassword(config.getString(RestConfig.SSL_TRUSTSTORE_PASSWORD_CONFIG));
                    sslContextFactory.setTrustStoreType(config.getString(RestConfig.SSL_TRUSTSTORE_TYPE_CONFIG));

                    if (!config.getString(RestConfig.SSL_TRUSTMANAGER_ALGORITHM_CONFIG).isEmpty()) {
                        sslContextFactory.setTrustManagerFactoryAlgorithm(
                                config.getString(RestConfig.SSL_TRUSTMANAGER_ALGORITHM_CONFIG));
                    }
                }

                sslContextFactory.setProtocol(config.getString(RestConfig.SSL_PROTOCOL_CONFIG));
                if (!config.getString(RestConfig.SSL_PROVIDER_CONFIG).isEmpty()) {
                    sslContextFactory.setProtocol(config.getString(RestConfig.SSL_PROVIDER_CONFIG));
                }

                connector = new NetworkTrafficServerConnector(server, sslContextFactory);
            }

            connector.addNetworkTrafficListener(metricsListener);
            connector.setPort(listener.getPort());
            connector.setHost(listener.getHost());
            server.addConnector(connector);
        }

        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        context.addServlet(servletHolder, "/*");

        String allowedOrigins = getConfiguration().getString(RestConfig.ACCESS_CONTROL_ALLOW_ORIGIN_CONFIG);
        if (allowedOrigins != null && !allowedOrigins.trim().isEmpty()) {
            FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class);
            filterHolder.setName("cross-origin");
            filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, allowedOrigins);
            String allowedMethods = getConfiguration().getString(RestConfig.ACCESS_CONTROL_ALLOW_METHODS);
            if (allowedMethods != null && !allowedOrigins.trim().isEmpty()) {
                filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, allowedMethods);
            }
            context.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
        }

        RequestLogHandler requestLogHandler = new RequestLogHandler();
        Slf4jRequestLog requestLog = new Slf4jRequestLog();
        requestLog.setLoggerName(config.getString(RestConfig.REQUEST_LOGGER_NAME_CONFIG));
        requestLog.setLogLatency(true);
        requestLogHandler.setRequestLog(requestLog);

        HandlerCollection handlers = new HandlerCollection();
        handlers.setHandlers(new Handler[] { context, new DefaultHandler(), requestLogHandler });

        /* Needed for graceful shutdown as per `setStopTimeout` documentation */
        StatisticsHandler statsHandler = new StatisticsHandler();
        statsHandler.setHandler(handlers);

        server.setHandler(statsHandler);

        int gracefulShutdownMs = getConfiguration().getInt(RestConfig.SHUTDOWN_GRACEFUL_MS_CONFIG);
        if (gracefulShutdownMs > 0) {
            server.setStopTimeout(gracefulShutdownMs);
        }
        server.setStopAtShutdown(true);

        return server;
    }

    // TODO: delete deprecatedPort parameter when `PORT_CONFIG` is deprecated. It's only used to support the deprecated
    //       configuration.
    static List<URI> parseListeners(List<String> listenersConfig, int deprecatedPort) {
        // handle deprecated case, using PORT_CONFIG.
        // TODO: remove this when `PORT_CONFIG` is deprecated, because LISTENER_CONFIG will have a default value which
        //       includes the default port.
        if (listenersConfig.isEmpty() || listenersConfig.get(0).isEmpty()) {
            log.warn(
                    "DEPRECATION warning: `listeners` configuration is not configured. Falling back to the deprecated "
                            + "`port` configuration.");
            listenersConfig = new ArrayList<String>(1);
            listenersConfig.add("http://0.0.0.0:" + deprecatedPort);
        }

        List<URI> listeners = new ArrayList<URI>(listenersConfig.size());
        for (String listenerStr : listenersConfig) {
            URI uri;
            try {
                uri = new URI(listenerStr);
            } catch (URISyntaxException use) {
                throw new ConfigException(
                        "Could not parse a listener URI from the `listener` configuration option.");
            }
            String scheme = uri.getScheme();
            if (scheme != null && (scheme.equals("http") || scheme.equals("https"))) {
                listeners.add(uri);
            } else {
                log.warn("Found a listener with an unsupported scheme (ony http and https are supported). Ignoring "
                        + "listener '" + listenerStr + "'");
            }
        }

        if (listeners.isEmpty()) {
            throw new ConfigException("No listeners are configured. Must have at least one listener.");
        }

        return listeners;
    }

    public void configureBaseApplication(Configurable<?> config) {
        configureBaseApplication(config, null);
    }

    /**
     * Register standard components for a JSON REST application on the given JAX-RS configurable,
     * which can be either an ResourceConfig for a server or a ClientConfig for a Jersey-based REST
     * client.
     */
    public void configureBaseApplication(Configurable<?> config, Map<String, String> metricTags) {
        RestConfig restConfig = getConfiguration();

        ObjectMapper jsonMapper = getJsonMapper();
        JacksonMessageBodyProvider jsonProvider = new JacksonMessageBodyProvider(jsonMapper);
        config.register(jsonProvider);
        config.register(JsonParseExceptionMapper.class);

        config.register(ValidationFeature.class);
        config.register(ConstraintViolationExceptionMapper.class);
        config.register(new WebApplicationExceptionMapper(restConfig));
        config.register(new GenericExceptionMapper(restConfig));

        config.register(
                new MetricsResourceMethodApplicationListener(metrics, "jersey", metricTags, restConfig.getTime()));

        config.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
    }

    public T getConfiguration() {
        return this.config;
    }

    /**
     * Gets a JSON ObjectMapper to use for (de)serialization of request/response entities. Override
     * this to configure the behavior of the serializer. One simple example of customization is to
     * set the INDENT_OUTPUT flag to make the output more readable. The default is a default
     * Jackson ObjectMapper.
     */
    protected ObjectMapper getJsonMapper() {
        return new ObjectMapper();
    }

    /**
     * Start the server (creating it if necessary).
     * @throws Exception
     */
    public void start() throws Exception {
        if (server == null) {
            createServer();
        }
        server.start();
    }

    /**
     * Wait for the server to exit, allowing existing requests to complete if graceful shutdown is
     * enabled and invoking the shutdown hook before returning.
     * @throws InterruptedException
     */
    public void join() throws InterruptedException {
        server.join();
        shutdownLatch.await();
    }

    /**
     * Request that the server shutdown.
     * @throws Exception
     */
    public void stop() throws Exception {
        server.stop();
    }

    /**
     * Shutdown hook that is invoked after the Jetty server has processed the shutdown request,
     * stopped accepting new connections, and tried to gracefully finish existing requests. At this
     * point it should be safe to clean up any resources used while processing requests.
     */
    public void onShutdown() {
    }
}