FullSystem.java

package com.renomad.minum.web;

import com.renomad.minum.queue.ActionQueueKiller;
import com.renomad.minum.state.Constants;
import com.renomad.minum.logging.ILogger;
import com.renomad.minum.logging.Logger;
import com.renomad.minum.security.ITheBrig;
import com.renomad.minum.security.TheBrig;
import com.renomad.minum.state.Context;
import com.renomad.minum.utils.*;

import java.io.File;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * This class is responsible for instantiating necessary classes
 * for a valid system, in the proper order.
 * @see #initialize()
 * @see #start()
 */
public final class FullSystem {

    final ILogger logger;
    private final Constants constants;
    private final FileUtils fileUtils;
    private IServer server;
    private WebFramework webFramework;
    private IServer sslServer;
    Thread shutdownHook;
    private ITheBrig theBrig;
    final ExecutorService es;
    private WebEngine webEngine;

    /**
     * This flag gives us some control if we need
     * to call {@link #shutdown()} manually, so close()
     * doesn't get run again when the shutdownHook
     * tries calling it.  This is primarily an issue just during
     * testing.
     */
    private boolean hasShutdown;

    private final Context context;

    /**
     * This constructor requires a {@link Context} object,
     * but it is easier and recommended to use {@link #initialize()}
     * instead.
     */
    public FullSystem(Context context) {
        this.logger = context.getLogger();
        this.constants = context.getConstants();
        this.fileUtils = new FileUtils(logger, constants);
        this.es = context.getExecutorService();
        this.context = context;
        context.setFullSystem(this);
    }

    /**
     * Builds a context object that is appropriate as a
     * parameter to constructing a {@link FullSystem}
     */
    public static Context buildContext() {
        var constants = new Constants();
        var executorService = Executors.newVirtualThreadPerTaskExecutor();
        var logger = new Logger(constants, executorService, "primary logger");

        var context = new Context(executorService, constants);
        context.setLogger(logger);

        return context;
    }

    /**
     * This is the typical entry point for system instantiation.  It will build
     * a {@link Context} object for you, and then properly instantiates the {@link FullSystem}.
     * <p>
     *     <em>Here is an example of a simple Main file using this method:</em>
     * </p>
     * <pre>{@code
     *   package org.example;
     *
     *   import com.renomad.minum.web.FullSystem;
     *   import com.renomad.minum.web.Response;
     *
     *   import static com.renomad.minum.web.RequestLine.Method.GET;
     *
     *   public class Main {
     *
     *       public static void main(String[] args) {
     *           FullSystem fs = FullSystem.initialize();
     *           fs.getWebFramework().registerPath(GET, "", request -> Response.htmlOk("<p>Hi there world!</p>"));
     *           fs.block();
     *       }
     *   }
     * }</pre>
     */
    public static FullSystem initialize() {
        var context = buildContext();
        var fullSystem = new FullSystem(context);
        return fullSystem.start();
    }

    /**
     * This method runs the necessary methods for starting the Minum
     * web server.  It is unlikely you will want to use this, unless you
     * require it for more control in testing.
     * @see #initialize()
     */
    public FullSystem start() {
        // create a file in our current working directory to indicate we are running
        createSystemRunningMarker();

        // set up an action to take place if the user shuts us down
        addShutdownHook();
        
        // Add useful startup info to the logs
        String serverComment = "at http://" + constants.hostName + ":" + constants.serverPort + " and https://" + constants.hostName + ":" + constants.secureServerPort;
        logger.logDebug(() -> " *** Minum is starting "+serverComment+" ***");
        
        // instantiate our security code
        theBrig = new TheBrig(context).initialize();
        
        // the web framework handles the HTTP communications
        webFramework = new WebFramework(context);

        // kick off the servers - low level internet handlers
        webEngine = new WebEngine(context, webFramework);
        server = webEngine.startServer();
        sslServer = webEngine.startSslServer();

        // document how long it took to start up the system
        var now = ZonedDateTime.now(ZoneId.of("UTC"));
        var nowMillis = now.toInstant().toEpochMilli();
        var startupTime = nowMillis - constants.startTime;
        logger.logDebug(() -> " *** Minum has finished primary startup after " + startupTime + " milliseconds ***");

        return this;
    }

    /**
     * this adds a hook to the Java runtime, so that if the app is running
     * and a user stops it - by pressing ctrl+c or a unix "kill" command - the
     * server socket will be shutdown and some messages about closing the server
     * will log
     */
    private void addShutdownHook() {
        shutdownHook = new Thread(ThrowingRunnable.throwingRunnableWrapper(this::shutdown, logger));
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    /**
     * this saves a file to the home directory, SYSTEM_RUNNING,
     * that will indicate the system is active
     */
    private void createSystemRunningMarker() {
        fileUtils.writeString(Path.of("SYSTEM_RUNNING"), "This file serves as a marker to indicate the system is running.\n");
        new File("SYSTEM_RUNNING").deleteOnExit();
    }

    IServer getServer() {
        return server;
    }

    IServer getSslServer() {
        return sslServer;
    }

    public WebFramework getWebFramework() {
        return webFramework;
    }

    public ITheBrig getTheBrig() {
        return theBrig;
    }

    public Context getContext() {
        return context;
    }

    WebEngine getWebEngine() {
        return webEngine;
    }

    public void shutdown() {

        if (!hasShutdown) {
            logger.logTrace(() -> "close called on " + this);
            closeCore(logger, context, server, sslServer, this.toString());
            hasShutdown = true;
        }
    }

    /**
     * The core code for closing resources
     * @param fullSystemName the name of this FullSystem, in cases where several are running concurrently
     */
    static void closeCore(ILogger logger, Context context, IServer server, IServer sslServer, String fullSystemName) {
        try {
            logger.logDebug(() -> "Received shutdown command");
            logger.logDebug(() -> " Stopping the server: " + server);
            server.close();
            logger.logDebug(() -> " Stopping the SSL server: " + server);
            sslServer.close();
            logger.logDebug(() -> "Killing all the action queues: " + context.getActionQueueState().aqQueueAsString());
            new ActionQueueKiller(context).killAllQueues();
            logger.logDebug(() -> String.format(
                    "%s %s says: Goodbye world!%n", TimeUtils.getTimestampIsoInstant(), fullSystemName));
        } catch (Exception e) {
            throw new WebServerException(e);
        }
    }

    /**
     * A blocking call for our multi-threaded application.
     * <p>
     * This method is needed because the entire application is
     * multi-threaded.  Let me help contextualize the problem
     * for you:
     * </p>
     * <p>
     *     For this application, multi-threaded means that we
     *     are wrapping our code in {@link Thread} classes and
     *     having them run using a {@link ExecutorService}.  It's
     *     sort of like giving instructions to someone else to carry
     *     out the work and sending them away, trusting the work will
     *     get done, rather than doing it yourself.
     * </p>
     * <p>
     *     But, since our entire system is done this way, once we
     *     have sent all our threads on their way, there's nothing
     *     left for us to do! Continuing the analogy, it is like
     *     our whole job is to give other people instructions, and
     *     then just wait for them to return.
     * </p>
     * <p>
     *     That's the purpose of this method.  It's to wait for
     *     the return.
     * </p>
     * <p>
     *     It's probably best to call this method as one of the
     *     last statements in the main method, so it is clear where
     *     execution is blocking.
     * </p>
     */
    public void block() {
        blockCore(this.server, this.sslServer);
    }

    static void blockCore(IServer server, IServer sslServer) {
        try {
            server.getCentralLoopFuture().get();
            sslServer.getCentralLoopFuture().get();
        } catch (InterruptedException | ExecutionException | CancellationException ex) {
            Thread.currentThread().interrupt();
            throw new WebServerException(ex);
        }
    }

    /**
     * Intentionally return just the default object toString, this is only used
     * to differentiate between multiple instances in memory.
     */
    @Override
    public String toString() {
        return super.toString();
    }
}