com.adeptj.runtime.server.Server.java Source code

Java tutorial

Introduction

Here is the source code for com.adeptj.runtime.server.Server.java

Source

/*
###############################################################################
#                                                                             #
#    Copyright 2016, AdeptJ (http://www.adeptj.com)                           #
#                                                                             #
#    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 com.adeptj.runtime.server;

import com.adeptj.runtime.common.Constants;
import com.adeptj.runtime.common.DefaultExecutorService;
import com.adeptj.runtime.common.Environment;
import com.adeptj.runtime.common.IOUtils;
import com.adeptj.runtime.common.Lifecycle;
import com.adeptj.runtime.common.SslContextFactory;
import com.adeptj.runtime.common.Times;
import com.adeptj.runtime.common.Verb;
import com.adeptj.runtime.config.Configs;
import com.adeptj.runtime.core.RuntimeInitializer;
import com.adeptj.runtime.exception.InitializationException;
import com.adeptj.runtime.osgi.FrameworkLauncher;
import com.adeptj.runtime.servlet.AuthServlet;
import com.adeptj.runtime.servlet.CryptoServlet;
import com.adeptj.runtime.servlet.ErrorPageServlet;
import com.adeptj.runtime.servlet.ToolsServlet;
import com.adeptj.runtime.tools.logging.LogbackManager;
import com.adeptj.runtime.websocket.ServerLogsWebSocket;
import com.typesafe.config.Config;
import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.Version;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.AllowedMethodsHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.RequestBufferingHandler;
import io.undertow.server.handlers.RequestLimitingHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.CrawlerSessionManagerConfig;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ErrorPage;
import io.undertow.servlet.api.SecurityConstraint;
import io.undertow.servlet.api.ServletContainerInitializerInfo;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.ServletSessionConfig;
import io.undertow.util.HttpString;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;

import javax.net.ssl.SSLContext;
import javax.servlet.MultipartConfigElement;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.channels.ServerSocketChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static io.undertow.websockets.jsr.WebSocketDeploymentInfo.ATTRIBUTE_NAME;
import static javax.servlet.http.HttpServletRequest.FORM_AUTH;
import static org.apache.commons.lang3.SystemUtils.USER_DIR;

/**
 * Provisions the Undertow Web Server, start OSGi framework and much more.
 *
 * @author Rakesh.Kumar, AdeptJ
 */
public final class Server implements Lifecycle {

    private static final Logger LOGGER = LoggerFactory.getLogger(Server.class);

    private Map<String, String> runtimeArgs;

    private Undertow undertow;

    private DeploymentManager deploymentManager;

    private GracefulShutdownHandler rootHandler;

    private WeakReference<Config> cfgReference;

    public Server(Map<String, String> runtimeArgs) {
        this.runtimeArgs = runtimeArgs;
    }

    /**
     * Bootstrap Undertow Server and OSGi Framework.
     */
    @Override
    public void start() {
        LOGGER.debug("AdeptJ Runtime jvm args: {}", this.runtimeArgs);
        this.cfgReference = new WeakReference<>(Configs.of().undertow());
        Config undertowConf = Objects.requireNonNull(this.cfgReference.get());
        Config httpConf = undertowConf.getConfig(Constants.KEY_HTTP);
        int httpPort = this.handlePortAvailability(httpConf);
        LOGGER.info("Starting AdeptJ Runtime @port: [{}]", httpPort);
        this.printBanner();
        this.deploymentManager = Servlets.newContainer().addDeployment(this.deploymentInfo());
        this.deploymentManager.deploy();
        try {
            this.rootHandler = this.rootHandler(this.deploymentManager.start());
            this.undertow = this
                    .enableHttp2(ServerOptions.build(this.workerOptions(Undertow.builder()), undertowConf))
                    .addHttpListener(httpPort, httpConf.getString(Constants.KEY_HOST)).setHandler(this.rootHandler)
                    .build();
            this.undertow.start();
        } catch (Exception ex) { // NOSONAR
            throw new InitializationException(ex.getMessage(), ex);
        }
        this.createServerConfFile();
    }

    /**
     * Does graceful server shutdown, this first cleans up the deployment and then stops Undertow server.
     */
    @Override
    public void stop() {
        long startTime = System.nanoTime();
        LOGGER.info("Stopping AdeptJ Runtime!!");
        try {
            this.gracefulShutdown();
            // Calls stop on LifecycleObjects - io.undertow.servlet.core.Lifecycle
            this.deploymentManager.stop();
            // Calls contextDestroyed on all registered ServletContextListener and performs other cleanup tasks.
            this.deploymentManager.undeploy();
            this.undertow.stop();
            DefaultExecutorService.getInstance().shutdown();
            LOGGER.info("AdeptJ Runtime stopped in [{}] ms!!", Times.elapsedMillis(startTime));
        } catch (Throwable ex) { // NOSONAR
            LOGGER.error("Exception while stopping AdeptJ Runtime!!", ex);
        } finally {
            // Let the Logback cleans up it's state.
            LogbackManager.getInstance().getLoggerContext().stop();
        }
    }

    private void gracefulShutdown() {
        try {
            this.rootHandler.shutdown();
            if (this.rootHandler.awaitShutdown(
                    Long.getLong(ServerConstants.SYS_PROP_SHUTDOWN_WAIT_TIME, ServerConstants.DEFAULT_WAIT_TIME))) {
                LOGGER.debug("Completed remaining requests successfully!!");
            }
        } catch (InterruptedException ie) {
            LOGGER.error("Error while waiting for pending request to complete!!", ie);
            // SONAR - "InterruptedException" should not be ignored
            // Can't really rethrow it as we are yet to stop the server and anyway it's a shutdown hook
            // and JVM itself will be shutting down shortly.
            Thread.currentThread().interrupt();
        }
    }

    private void printBanner() {
        try (InputStream stream = this.getClass().getResourceAsStream(Constants.BANNER_TXT)) {
            LOGGER.info(IOUtils.toString(stream)); // NOSONAR
        } catch (IOException ex) {
            // Just log it, its not critical.
            LOGGER.error("Exception while printing server banner!!", ex);
        }
    }

    private void createServerConfFile() {
        if (!Environment.isServerConfFileExists()) {
            try (InputStream stream = this.getClass().getResourceAsStream("/reference.conf")) {
                Files.write(Paths.get(USER_DIR, Constants.DIR_ADEPTJ_RUNTIME, Constants.DIR_DEPLOYMENT,
                        Constants.SERVER_CONF_FILE), IOUtils.toBytes(stream), StandardOpenOption.CREATE);
            } catch (IOException ex) {
                LOGGER.error("Exception while creating server conf file!!", ex);
            }
        }
    }

    private Builder workerOptions(Builder builder) {
        if (Environment.isProd()) {
            // Note : For a 16 core system, number of worker task core and max threads will be.
            // 1. core task thread: 128 (16[cores] * 8)
            // 2. max task thread: 128 * 2 = 256
            // Default settings would have set the following.
            // 1. core task thread: 128 (16[cores] * 8)
            // 2. max task thread: 128 (Same as core task thread)
            Config workerOptions = Objects.requireNonNull(this.cfgReference.get())
                    .getConfig(ServerConstants.KEY_WORKER_OPTIONS);
            // defaults to 64
            int cfgCoreTaskThreads = workerOptions.getInt(ServerConstants.KEY_WORKER_TASK_CORE_THREADS);
            LOGGER.info("Configured worker task core threads: [{}]", cfgCoreTaskThreads);
            int availableProcessors = Runtime.getRuntime().availableProcessors();
            LOGGER.info("No. of CPU available: [{}]", availableProcessors);
            int calcCoreTaskThreads = availableProcessors
                    * Integer.getInteger(ServerConstants.SYS_PROP_WORKER_TASK_THREAD_MULTIPLIER,
                            ServerConstants.WORKER_TASK_THREAD_MULTIPLIER);
            LOGGER.info("Calculated worker task core threads: [{}]", calcCoreTaskThreads);
            builder.setWorkerOption(Options.WORKER_TASK_CORE_THREADS,
                    calcCoreTaskThreads > cfgCoreTaskThreads ? calcCoreTaskThreads : cfgCoreTaskThreads);
            // defaults to double of [worker-task-core-threads] i.e 128
            int cfgMaxTaskThreads = workerOptions.getInt(ServerConstants.KEY_WORKER_TASK_MAX_THREADS);
            LOGGER.info("Configured worker task max threads: [{}]", cfgCoreTaskThreads);
            int calcMaxTaskThreads = calcCoreTaskThreads
                    * Integer.getInteger(ServerConstants.SYS_PROP_SYS_TASK_THREAD_MULTIPLIER,
                            ServerConstants.SYS_TASK_THREAD_MULTIPLIER);
            LOGGER.info("Calculated worker task max threads: [{}]", cfgCoreTaskThreads);
            builder.setWorkerOption(Options.WORKER_TASK_MAX_THREADS,
                    calcMaxTaskThreads > cfgMaxTaskThreads ? calcMaxTaskThreads : cfgMaxTaskThreads);
            LOGGER.info("Undertow Worker Options optimized for AdeptJ Runtime [PROD] mode.");
        }
        return builder;
    }

    private Builder enableHttp2(Builder builder) throws GeneralSecurityException, IOException {
        if (Boolean.getBoolean(ServerConstants.SYS_PROP_ENABLE_HTTP2)) {
            Config httpsConf = Objects.requireNonNull(this.cfgReference.get()).getConfig(ServerConstants.KEY_HTTPS);
            int httpsPort = httpsConf.getInt(Constants.KEY_PORT);
            if (!Environment.useProvidedKeyStore()) {
                System.setProperty("adeptj.rt.keyStore", httpsConf.getString(ServerConstants.KEY_KEYSTORE));
                System.setProperty("adeptj.rt.keyStorePassword", httpsConf.getString("keyStorePwd"));
                System.setProperty("adeptj.rt.keyPassword", httpsConf.getString("keyPwd"));
                LOGGER.info("HTTP2 enabled @ port: [{}] using bundled KeyStore.", httpsPort);
            }
            SSLContext sslContext = SslContextFactory.newSslContext(httpsConf.getString("tlsVersion"));
            builder.addHttpsListener(httpsPort, httpsConf.getString(Constants.KEY_HOST), sslContext);
        }
        return builder;
    }

    private int handlePortAvailability(Config httpConf) {
        Integer port = Integer.getInteger(Constants.SYS_PROP_SERVER_PORT);
        if (port == null) {
            LOGGER.warn("No port specified via system property: [{}], using default port: [{}]",
                    Constants.SYS_PROP_SERVER_PORT, httpConf.getInt(Constants.KEY_PORT));
            port = httpConf.getInt(Constants.KEY_PORT);
        }
        // Note: Shall we do it ourselves or let server do it later? Problem may arise in OSGi Framework provisioning
        // as it is being started already and another server start(from same location) will again start new OSGi
        // Framework which may interfere with already started OSGi Framework as the bundle deployment, heap dump,
        // OSGi configurations directory is common, this is unknown at this moment but just to be on safer side doing this.
        if (Boolean.getBoolean(ServerConstants.SYS_PROP_CHECK_PORT) && !isPortAvailable(port)) {
            LOGGER.error("Port: [{}] already used, shutting down JVM!!", port);
            // Let the LOGBACK cleans up it's state.
            LogbackManager.getInstance().getLoggerContext().stop();
            System.exit(-1); // NOSONAR
        }
        return port;
    }

    private boolean isPortAvailable(int port) {
        boolean portAvailable = false;
        ServerSocket socket = null;
        try (ServerSocketChannel socketChannel = ServerSocketChannel.open()) {
            socket = socketChannel.socket();
            socket.setReuseAddress(true);
            socket.bind(new InetSocketAddress(port));
            portAvailable = true;
        } catch (BindException ex) {
            LOGGER.error("BindException while acquiring port: [{}], cause:", port, ex);
        } catch (IOException ex) {
            LOGGER.error("IOException while acquiring port: [{}], cause:", port, ex);
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ex) {
                    LOGGER.error("IOException while closing socket!!", ex);
                }
            }
        }
        return portAvailable;
    }

    /**
     * Chaining of Undertow {@link HttpHandler} instances as follows.
     * <p>
     * 1. GracefulShutdownHandler
     * 2. RequestLimitingHandler
     * 3. AllowedMethodsHandler
     * 4. PredicateHandler which resolves to either RedirectHandler or SetHeadersHandler
     * 5. RequestBufferingHandler if request buffering is enabled, wrapped in SetHeadersHandler
     * 5. And Finally ServletInitialHandler
     *
     * @param servletInitialHandler the {@link io.undertow.servlet.handlers.ServletInitialHandler}
     * @return GracefulShutdownHandler as the root handler
     */
    private GracefulShutdownHandler rootHandler(HttpHandler servletInitialHandler) {
        Config cfg = Objects.requireNonNull(this.cfgReference.get());
        Map<HttpString, String> headers = new HashMap<>();
        headers.put(HttpString.tryFromString(Constants.HEADER_SERVER), cfg.getString(Constants.KEY_HEADER_SERVER));
        if (Environment.isDev()) {
            headers.put(HttpString.tryFromString(Constants.HEADER_X_POWERED_BY), Version.getFullVersionString());
        }
        HttpHandler headersHandler = Boolean.getBoolean(ServerConstants.SYS_PROP_ENABLE_REQ_BUFF)
                ? new SetHeadersHandler(new RequestBufferingHandler(servletInitialHandler,
                        Integer.getInteger(ServerConstants.SYS_PROP_REQ_BUFF_MAX_BUFFERS,
                                cfg.getInt(Constants.KEY_REQ_BUFF_MAX_BUFFERS))),
                        headers)
                : new SetHeadersHandler(servletInitialHandler, headers);
        return Handlers.gracefulShutdown(new RequestLimitingHandler(
                Integer.getInteger(ServerConstants.SYS_PROP_MAX_CONCUR_REQ,
                        cfg.getInt(Constants.KEY_MAX_CONCURRENT_REQS)),
                new AllowedMethodsHandler(
                        Handlers.predicate(exchange -> Constants.CONTEXT_PATH.equals(exchange.getRequestURI()),
                                Handlers.redirect(Constants.TOOLS_DASHBOARD_URI), headersHandler),
                        this.allowedMethods(cfg))));
    }

    private Set<HttpString> allowedMethods(Config cfg) {
        return cfg.getStringList(Constants.KEY_ALLOWED_METHODS).stream().map(Verb::from)
                .collect(Collectors.toSet());
    }

    private List<ErrorPage> errorPages(Config cfg) {
        return cfg.getObject(ServerConstants.KEY_ERROR_PAGES).unwrapped().entrySet().stream()
                .map(e -> Servlets.errorPage(String.valueOf(e.getValue()), Integer.parseInt(e.getKey())))
                .collect(Collectors.toList());
    }

    private ServletContainerInitializerInfo sciInfo() {
        // Since the execution order of StartupAware instances matter that's why a LinkedHashSet.
        // FrameworkLauncher must always be executed first.
        Set<Class<?>> handlesTypes = new LinkedHashSet<>();
        handlesTypes.add(FrameworkLauncher.class);
        handlesTypes.add(DefaultStartupAware.class);
        return new ServletContainerInitializerInfo(RuntimeInitializer.class, handlesTypes);
    }

    private SecurityConstraint securityConstraint(Config cfg) {
        return Servlets.securityConstraint().addRolesAllowed(cfg.getStringList(ServerConstants.KEY_AUTH_ROLES))
                .addWebResourceCollection(Servlets.webResourceCollection()
                        .addHttpMethods(cfg.getStringList(ServerConstants.KEY_SECURED_URLS_ALLOWED_METHODS))
                        .addUrlPatterns(cfg.getStringList(ServerConstants.KEY_SECURED_URLS)));
    }

    private List<ServletInfo> servlets() {
        List<ServletInfo> servlets = new ArrayList<>();
        servlets.add(Servlets.servlet(ServerConstants.ERROR_PAGE_SERVLET, ErrorPageServlet.class)
                .addMapping(ServerConstants.TOOLS_ERROR_URL).setAsyncSupported(true));
        servlets.add(Servlets.servlet(ServerConstants.TOOLS_SERVLET, ToolsServlet.class)
                .addMapping(ServerConstants.TOOLS_DASHBOARD_URL).setAsyncSupported(true));
        servlets.add(Servlets.servlet(ServerConstants.AUTH_SERVLET, AuthServlet.class)
                .addMappings(Constants.TOOLS_LOGIN_URI, Constants.TOOLS_LOGOUT_URI).setAsyncSupported(true));
        servlets.add(Servlets.servlet(ServerConstants.CRYPTO_SERVLET, CryptoServlet.class)
                .addMappings(Constants.TOOLS_CRYPTO_URI).setAsyncSupported(true));
        return servlets;
    }

    private MultipartConfigElement defaultMultipartConfig(Config cfg) {
        return Servlets.multipartConfig(cfg.getString(ServerConstants.KEY_MULTIPART_FILE_LOCATION),
                cfg.getLong(ServerConstants.KEY_MULTIPART_MAX_FILE_SIZE),
                cfg.getLong(ServerConstants.KEY_MULTIPART_MAX_REQUEST_SIZE),
                cfg.getInt(ServerConstants.KEY_MULTIPART_FILE_SIZE_THRESHOLD));
    }

    private WebSocketDeploymentInfo webSocketDeploymentInfo(Config cfg) {
        Config wsOptions = cfg.getConfig(ServerConstants.KEY_WS_WEB_SOCKET_OPTIONS);
        return new WebSocketDeploymentInfo().setWorker(this.webSocketWorker(wsOptions))
                .setBuffers(
                        new DefaultByteBufferPool(wsOptions.getBoolean(ServerConstants.KEY_WS_USE_DIRECT_BUFFER),
                                wsOptions.getInt(ServerConstants.KEY_WS_BUFFER_SIZE)))
                .addEndpoint(ServerLogsWebSocket.class);
    }

    private XnioWorker webSocketWorker(Config wsOptions) {
        XnioWorker worker = null;
        try {
            worker = Xnio.getInstance().createWorker(OptionMap.builder()
                    .set(Options.WORKER_IO_THREADS, wsOptions.getInt(ServerConstants.KEY_WS_IO_THREADS))
                    .set(Options.WORKER_TASK_CORE_THREADS,
                            wsOptions.getInt(ServerConstants.KEY_WS_TASK_CORE_THREADS))
                    .set(Options.WORKER_TASK_MAX_THREADS, wsOptions.getInt(ServerConstants.KEY_WS_TASK_MAX_THREADS))
                    .set(Options.TCP_NODELAY, wsOptions.getBoolean(ServerConstants.KEY_WS_TCP_NO_DELAY)).getMap());
        } catch (IOException ex) {
            LOGGER.error("Can't create XnioWorker!!", ex);
        }
        return worker;
    }

    private int sessionTimeout(Config cfg) {
        return Integer.getInteger(ServerConstants.SYS_PROP_SESSION_TIMEOUT,
                cfg.getInt(ServerConstants.KEY_SESSION_TIMEOUT));
    }

    private ServletSessionConfig sessionConfig(Config cfg) {
        return new ServletSessionConfig().setHttpOnly(cfg.getBoolean(ServerConstants.KEY_HTTP_ONLY));
    }

    private DeploymentInfo deploymentInfo() {
        Config cfg = Objects.requireNonNull(this.cfgReference.get());
        return Servlets.deployment().setDeploymentName(Constants.DEPLOYMENT_NAME)
                .setContextPath(Constants.CONTEXT_PATH).setClassLoader(Server.class.getClassLoader())
                .addServletContainerInitializer(this.sciInfo())
                .setIgnoreFlush(cfg.getBoolean(ServerConstants.KEY_IGNORE_FLUSH))
                .setDefaultEncoding(cfg.getString(ServerConstants.KEY_DEFAULT_ENCODING))
                .setDefaultSessionTimeout(this.sessionTimeout(cfg))
                .setChangeSessionIdOnLogin(cfg.getBoolean(ServerConstants.KEY_CHANGE_SESSIONID_ON_LOGIN))
                .setInvalidateSessionOnLogout(cfg.getBoolean(ServerConstants.KEY_INVALIDATE_SESSION_ON_LOGOUT))
                .setIdentityManager(new SimpleIdentityManager(cfg))
                .setUseCachedAuthenticationMechanism(cfg.getBoolean(ServerConstants.KEY_USE_CACHED_AUTH_MECHANISM))
                .setLoginConfig(Servlets.loginConfig(FORM_AUTH, ServerConstants.REALM, Constants.TOOLS_LOGIN_URI,
                        Constants.TOOLS_LOGIN_URI))
                .addSecurityConstraint(this.securityConstraint(cfg)).addServlets(this.servlets())
                .addErrorPages(this.errorPages(cfg)).setDefaultMultipartConfig(this.defaultMultipartConfig(cfg))
                .addInitialHandlerChainWrapper(new ServletInitialHandlerWrapper())
                .addServletContextAttribute(ATTRIBUTE_NAME, this.webSocketDeploymentInfo(cfg))
                .setServletSessionConfig(this.sessionConfig(cfg))
                .setCrawlerSessionManagerConfig(new CrawlerSessionManagerConfig());
    }
}