com.qwazr.server.GenericServer.java Source code

Java tutorial

Introduction

Here is the source code for com.qwazr.server.GenericServer.java

Source

/*
 * Copyright 2015-2018 Emmanuel Keller / QWAZR
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.qwazr.server;

import com.qwazr.server.configuration.ServerConfiguration;
import com.qwazr.server.logs.AccessLogger;
import com.qwazr.server.logs.LogMetricsHandler;
import com.qwazr.utils.CollectionsUtils;
import com.qwazr.utils.LoggerUtils;
import com.qwazr.utils.StringUtils;
import com.qwazr.utils.reflection.ConstructorParameters;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.security.idm.IdentityManager;
import io.undertow.server.HttpHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.ServletContainer;
import org.apache.commons.lang3.SystemUtils;

import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.MBeanException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.OperationsException;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GenericServer implements AutoCloseable {

    final private ExecutorService executorService;
    final private ServletContainer servletContainer;
    final private ServletContextBuilder webAppContext;
    final private ServletContextBuilder webServiceContext;

    final private Map<String, Object> contextAttributes;
    final private IdentityManagerProvider identityManagerProvider;
    final private HostnameAuthenticationMechanism.PrincipalResolver hostnamePrincipalResolver;
    final private Collection<ConnectorStatisticsMXBean> connectorsStatistics;

    final private Collection<Listener> startedListeners;
    final private Collection<Listener> shutdownListeners;

    final private Collection<Undertow> undertows;
    final private Collection<DeploymentManager> deploymentManagers;

    final private ServerConfiguration configuration;

    final private AccessLogger webAppAccessLogger;
    final private AccessLogger webServiceAccessLogger;

    final private Set<String> webAppEndPoints;
    final private Set<String> webServiceEndPoints;

    final private UdpServerThread udpServer;

    final private Collection<ObjectName> registeredObjectNames;

    static final private Logger LOGGER = LoggerUtils.getLogger(GenericServer.class);

    GenericServer(final GenericServerBuilder builder) throws IOException {

        this.configuration = builder.configuration;
        this.executorService = builder.executorService == null ? Executors.newCachedThreadPool()
                : builder.executorService;
        this.servletContainer = Servlets.newContainer();
        this.webAppContext = builder.webAppContext;
        this.webServiceContext = builder.webServiceContext;
        this.webAppEndPoints = webAppContext == null ? null : Collections.unmodifiableSet(webAppContext.endPoints);
        this.webServiceEndPoints = webServiceContext == null ? null
                : Collections.unmodifiableSet(webServiceContext.endPoints);
        builder.contextAttribute(this);
        this.contextAttributes = new LinkedHashMap<>(builder.contextAttributes);
        this.undertows = new ArrayList<>();
        this.deploymentManagers = new ArrayList<>();
        this.identityManagerProvider = builder.identityManagerProvider;
        this.hostnamePrincipalResolver = builder.hostnamePrincipalResolver;
        this.webAppAccessLogger = builder.webAppAccessLogger;
        this.webServiceAccessLogger = builder.webServiceAccessLogger;
        this.udpServer = buildUdpServer(builder, configuration);
        this.startedListeners = CollectionsUtils.copyIfNotEmpty(builder.startedListeners, ArrayList::new);
        this.shutdownListeners = CollectionsUtils.copyIfNotEmpty(builder.shutdownListeners, ArrayList::new);
        this.connectorsStatistics = new ArrayList<>();
        this.registeredObjectNames = new LinkedHashSet<>();
    }

    /**
     * Returns the named attribute. The method checks the type of the object.
     *
     * @param context the context to request
     * @param name    the name of the attribute
     * @param type    the expected type
     * @param <T>     the expected object
     * @return the expected object
     */
    public static <T> T getContextAttribute(final ServletContext context, final String name, final Class<T> type) {
        final Object object = context.getAttribute(name);
        if (object == null)
            return null;
        if (!object.getClass().isAssignableFrom(type))
            throw new RuntimeException(
                    "Wrong returned type: " + object.getClass().getName() + " - Expected: " + type.getName());
        return type.cast(object);
    }

    /**
     * Returns an attribute where the name of the attribute in the name of the class
     *
     * @param context the context to request
     * @param cls     the type of the object
     * @param <T>     the expected object
     * @return the expected object
     */
    public static <T> T getContextAttribute(final ServletContext context, final Class<T> cls) {
        return getContextAttribute(context, cls.getName(), cls);
    }

    Set<String> getWebServiceEndPoints() {
        return webServiceEndPoints;
    }

    Set<String> getWebAppEndPoints() {
        return webAppEndPoints;
    }

    private static UdpServerThread buildUdpServer(final GenericServerBuilder builder,
            final ServerConfiguration configuration) throws IOException {

        if (builder.packetListeners == null || builder.packetListeners.isEmpty())
            return null;

        if (configuration.multicastConnector.address != null && configuration.multicastConnector.port != -1)
            return new UdpServerThread(configuration.multicastConnector.address,
                    configuration.multicastConnector.port, builder.packetListeners);
        else
            return new UdpServerThread(
                    new InetSocketAddress(configuration.listenAddress, configuration.webServiceConnector.port),
                    builder.packetListeners);
    }

    private synchronized void start(final Undertow undertow) {
        // start the server
        undertow.start();
        undertows.add(undertow);
    }

    /**
     * Uses close()
     */
    @Deprecated
    public void stopAll() {
        close();
    }

    @Override
    public synchronized void close() {

        LOGGER.info("The server is stopping...");

        executeListener(shutdownListeners, LOGGER);

        if (udpServer != null) {
            try {
                udpServer.shutdown();
            } catch (InterruptedException e) {
                LOGGER.log(Level.WARNING, e.getMessage(), e);
            }
        }

        for (final DeploymentManager manager : deploymentManagers) {
            try {
                if (manager.getState() == DeploymentManager.State.STARTED)
                    manager.stop();
                if (manager.getState() == DeploymentManager.State.DEPLOYED)
                    manager.undeploy();
            } catch (Exception e) {
                LOGGER.log(Level.WARNING, e, () -> "Cannot stop the manager: " + e.getMessage());
            }
        }

        for (final Undertow undertow : undertows) {
            try {
                undertow.stop();
            } catch (Exception e) {
                LOGGER.log(Level.WARNING, e, () -> "Cannot stop Undertow: " + e.getMessage());
            }
        }

        if (!executorService.isTerminated()) {
            if (!executorService.isShutdown())
                executorService.shutdown();
            try {
                executorService.awaitTermination(2, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                LOGGER.log(Level.WARNING, e, () -> "Executor shutdown failed: " + e.getMessage());
            }
        }

        // Unregister MBeans
        if (registeredObjectNames != null && !registeredObjectNames.isEmpty()) {
            final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            for (ObjectName objectName : registeredObjectNames) {
                try {
                    mbs.unregisterMBean(objectName);
                } catch (InstanceNotFoundException | MBeanRegistrationException e) {
                    LOGGER.log(Level.WARNING, e, e::getMessage);
                }
            }
            registeredObjectNames.clear();
        }

        LOGGER.info("The server is stopped.");
    }

    private IdentityManager getIdentityManager(final ServerConfiguration.WebConnector connector)
            throws IOException {
        if (identityManagerProvider == null)
            return null;
        return identityManagerProvider
                .getIdentityManager(connector == null || connector.realm == null ? null : connector.realm);
    }

    private final static AtomicInteger serverCounter = new AtomicInteger();

    private void startHttpServer(final ServerConfiguration.WebConnector connector,
            final ServletContextBuilder context, final AccessLogger accessLogger)
            throws IOException, ServletException, OperationsException, MBeanException {

        if (context == null || context.getServlets().isEmpty())
            return;

        context.setIdentityManager(getIdentityManager(connector));

        contextAttributes.forEach(context::addServletContextAttribute);

        if (context.getIdentityManager() != null && !StringUtils.isEmpty(connector.authentication)) {
            if (hostnamePrincipalResolver != null)
                HostnameAuthenticationMechanism.register(context, hostnamePrincipalResolver);
            final LoginConfig loginConfig = Servlets.loginConfig(connector.realm);
            for (String authmethod : StringUtils.split(connector.authentication, ','))
                loginConfig.addLastAuthMethod(authmethod);
            context.setLoginConfig(loginConfig);
        }

        final DeploymentManager manager = servletContainer.addDeployment(context);
        manager.deploy();

        LOGGER.info(() -> "Start the connector " + configuration.listenAddress + ":" + connector.port);

        HttpHandler httpHandler = manager.start();
        final LogMetricsHandler logMetricsHandler = new LogMetricsHandler(httpHandler, configuration.listenAddress,
                connector.port, context.jmxName, accessLogger);
        deploymentManagers.add(manager);
        httpHandler = logMetricsHandler;

        final Undertow.Builder servletBuilder = Undertow.builder()
                .addHttpListener(connector.port, configuration.listenAddress)
                .setServerOption(UndertowOptions.NO_REQUEST_TIMEOUT, 10000)
                .setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, true)
                .setServerOption(UndertowOptions.ENABLE_STATISTICS, true)
                .setServerOption(UndertowOptions.ENABLE_HTTP2, true).setHandler(httpHandler);
        start(servletBuilder.build());

        // Register MBeans
        final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        final Hashtable<String, String> props = new Hashtable<>();
        props.put("type", "connector");
        props.put("name", context.jmxName);
        final ObjectName name = new ObjectName(
                "com.qwazr.server." + serverCounter.incrementAndGet() + "." + context.jmxName, props);
        mbs.registerMBean(logMetricsHandler, name);
        registeredObjectNames.add(name);
        connectorsStatistics.add(logMetricsHandler);
    }

    /**
     * Call this method to start the server
     *
     * @param shutdownHook pass true to install the StopAll method as Runtime shutdown hook
     * @throws IOException      if any IO error occurs
     * @throws ServletException if the servlet configuration failed
     * @throws JMException      if any JMX error occurs
     */
    final public void start(boolean shutdownHook) throws IOException, ServletException, JMException {

        LOGGER.info("The server is starting...");
        LOGGER.info(() -> "Data directory sets to: " + configuration.dataDirectory);

        if (!Files.exists(configuration.dataDirectory))
            throw new IOException(
                    "The data directory does not exists: " + configuration.dataDirectory.toAbsolutePath());
        if (!Files.isDirectory(configuration.dataDirectory))
            throw new IOException(
                    "The data directory path is not a directory: " + configuration.dataDirectory.toAbsolutePath());

        if (udpServer != null)
            udpServer.checkStarted();

        // Launch the applications/connector
        startHttpServer(configuration.webAppConnector, webAppContext, webAppAccessLogger);
        startHttpServer(configuration.webServiceConnector, webServiceContext, webServiceAccessLogger);

        if (shutdownHook)
            Runtime.getRuntime().addShutdownHook(new Thread(this::close));

        executeListener(startedListeners, null);

        LOGGER.info("The server started successfully.");
    }

    public Collection<ConnectorStatisticsMXBean> getConnectorsStatistics() {
        return connectorsStatistics;
    }

    @FunctionalInterface
    public interface Listener {
        void accept(GenericServer server) throws Exception;
    }

    private void executeListener(final Collection<Listener> listeners, final Logger logger) {
        if (listeners == null)
            return;
        listeners.forEach(listener -> {
            try {
                listener.accept(this);
            } catch (Exception e) {
                if (logger == null)
                    throw ServerException.of("Listeners failure", e);
                else
                    LOGGER.log(Level.SEVERE, e, e::getMessage);
            }
        });
    }

    final static MultipartConfigElement DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(
            SystemUtils.getJavaIoTmpDir().getAbsolutePath());

    public interface IdentityManagerProvider {

        IdentityManager getIdentityManager(String realm) throws IOException;

    }

    public static GenericServerBuilder of(ServerConfiguration config, ExecutorService executorService,
            ClassLoader classLoader, ConstructorParameters constructorParameters) {
        return new GenericServerBuilder(config, executorService, classLoader, constructorParameters);
    }

    public static GenericServerBuilder of(ServerConfiguration config, ExecutorService executorService,
            ClassLoader classLoader) {
        return of(config, executorService, classLoader, null);
    }

    public static GenericServerBuilder of(ServerConfiguration config, ExecutorService executorService) {
        return of(config, executorService, null);
    }

    public static GenericServerBuilder of(ServerConfiguration config) {
        return of(config, null);
    }

}