All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mastfrog.acteur.server.ServerBuilder Maven / Gradle / Ivy

There is a newer version: 2.9.7
Show newest version
/*
 * The MIT License
 *
 * Copyright 2014 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.acteur.server;

import com.google.inject.Binder;
import com.google.inject.Module;
import com.mastfrog.acteur.Application;
import com.mastfrog.acteur.annotations.GenericApplication;
import com.mastfrog.acteur.annotations.GenericApplication.GenericApplicationSettings;
import com.mastfrog.acteur.annotations.GenericApplicationModule;
import com.mastfrog.acteur.util.Server;
import com.mastfrog.giulius.DependenciesBuilder;
import com.mastfrog.giulius.InjectionInfo;
import com.mastfrog.giulius.SettingsBindings;
import com.mastfrog.giulius.scope.ReentrantScope;
import com.mastfrog.settings.Settings;
import com.mastfrog.settings.SettingsBuilder;
import static com.mastfrog.settings.SettingsBuilder.DEFAULT_NAMESPACE;
import com.mastfrog.util.preconditions.Checks;
import com.mastfrog.util.preconditions.Exceptions;
import io.netty.handler.ssl.SslContext;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.net.ssl.SSLException;

/**
 * Hides the complexity of initializing Guice and Settings to start a server
 * simply.
 *
 * @author Tim Boudreau
 */
public final class ServerBuilder {

    private final String namespace;
    private Class appType;
    private final List settingsList = new LinkedList<>();
    private final List modules = new LinkedList<>();
    private final List> moduleClasses = new LinkedList<>();
    private final Set> types = new HashSet<>();
    private final ReentrantScope scope;
    private boolean enableCors = true;
    private boolean enableHelp = false;
    private boolean mergeNamespaces;

    public ServerBuilder(ReentrantScope scope) {
        this(DEFAULT_NAMESPACE, scope);
    }

    public ServerBuilder(String namespace, ReentrantScope scope) {
        Checks.notNull("namespace", namespace);
        Checks.notNull("scope", scope);
        this.namespace = namespace;
        this.scope = scope;
    }

    /**
     * Create a ServerBuilder with Namespace.DEFAULT as its namespace. That
     * means settings will be read from /etc/defaults.properties,
     * ~/defaults.properties and ./defaults.properties
     */
    public ServerBuilder() {
        this(DEFAULT_NAMESPACE);
    }

    /**
     * Create a ServerBuilder with the passed namespace as its namespace. That
     * means settings will be read from /etc/$NAMESPACE.properties,
     * ~/$NAMESPACE.properties and ./$NAMESPACE.properties
     *
     * @param namespace A namespace - must be a legal file name, no path
     * separators
     */
    public ServerBuilder(String namespace) {
        Checks.notNull("namespace", namespace);
        this.namespace = namespace;
        this.scope = new ReentrantScope(new InjectionInfo());
    }

    /**
     * If true, do not actually use the namespace feature of Dependencies -
     * there is only one namespace - it is just not named "default". This is the
     * common case, but for compatibility reasons is not currently the default.
     * This reduces memory footprint overhead when looking up bound settings,
     * since reflection is not needed and there is only one place to look.
     *
     * @return
     */
    public ServerBuilder mergeNamespaces() {
        this.mergeNamespaces = true;
        return this;
    }

    /**
     * Set the class of the application. If not set, assumes GenericApplication
     *
     * @param type The application class
     * @return this
     */
    public ServerBuilder applicationClass(Class type) {
        if (this.appType != null) {
            throw new IllegalStateException("App type was already set to " + this.appType);
        }
        this.appType = type;
        return this;
    }

    private SslContext sslContext;

    public ServerBuilder sslConfig(SslContext context) {
        ssl();
        this.sslContext = context;
        return this;
    }

    public ServerBuilder ssl() {
        try {
            settingsList.add(new SettingsBuilder().add(ServerModule.SETTINGS_KEY_SSL_ENABLED, true).build());
        } catch (IOException ex) {
            return Exceptions.chuck(ex);
        }
        return this;
    }

    /**
     * Explicitly add a Settings which should be used for resolving @Named
     * bindings, in addition to any bound to the namespace (i.e.
     * /etc/$NAMESPACE.properties)
     *
     * @param settings A settings
     * @return this
     */
    public ServerBuilder add(Settings settings) {
        this.settingsList.add(settings);
        return this;
    }

    /**
     * Add a module which should be used for Guice bindings
     *
     * @param module A module
     * @return this
     */
    public ServerBuilder add(Module module) {
        this.modules.add(module);
        return this;
    }

    /**
     * Add a module type which will be instantiated. The constructor may be a
     * default constructor, or may take a Settings object, or a Settings and a
     * ReentrantScope.
     *
     * @param module The module type
     * @return this
     */
    public ServerBuilder add(Class module) {
        this.moduleClasses.add(module);
        return this;
    }

    /**
     * Add a type which will be bound. Use this to add your classes to the Guice
     * injector if they will be passed between Acteurs. The rule is: If you
     * accept it in an Acteur constructor and it is not part of the framework,
     * you need to pass it here.
     *
     * @param types Some classes
     * @return this
     */
    public ServerBuilder withType(Class... types) {
        this.types.addAll(Arrays.asList(types));
        return this;
    }

    public ReentrantScope scope() {
        return scope;
    }

    public ServerBuilder enableHelp() {
        this.enableHelp = true;
        return this;
    }

    public ServerBuilder disableHelp() {
        this.enableHelp = false;
        return this;
    }

    public ServerBuilder enableCORS() {
        this.enableCors = true;
        return this;
    }

    public ServerBuilder disableCORS() {
        this.enableCors = false;
        return this;
    }

    private final Set settingsBindings = EnumSet.allOf(SettingsBindings.class);

    /**
     * Disable binding of settings to some types if you know they will not be
     * used, to save (minimal) memory. This is only significant if you are
     * running in < 20Mb heap.
     *
     * @param bindings The bindings to remove
     * @return this
     */
    public ServerBuilder disableBindings(SettingsBindings... bindings) {
        EnumSet toRemove = EnumSet.noneOf(SettingsBindings.class);
        for (SettingsBindings b : bindings) {
            toRemove.add(b);
        }
        settingsBindings.removeAll(toRemove);
        return this;
    }

    /**
     * Explicitly set the list of types that are bound to settings to save
     * (minimal) memory.
     *
     * @param bindings The types of bindings to set up
     * @return this
     */
    public ServerBuilder enableOnlyBindingsFor(SettingsBindings... bindings) {
        EnumSet newSet = EnumSet.noneOf(SettingsBindings.class);
        for (SettingsBindings b : bindings) {
            newSet.add(b);
        }
        this.settingsBindings.clear();
        this.settingsBindings.addAll(newSet);
        return this;
    }

    /**
     * Build a Server object which can be started with its start() method (call
     * await() on the resulting ServerControl object to keep it running).
     *
     * @return A server, not yet started
     * @throws IOException if something goes wrong
     */
    public Server build() throws IOException {
        return toDependenciesBuilder().build().getInstance(Server.class);
    }

    private Duration shutdownHookWaitMillis;

    public ServerBuilder withShutdownHookWaitMillis(Duration dur) {
        return this;
    }

    public DependenciesBuilder toDependenciesBuilder() throws IOException {
        SettingsBuilder sb = new SettingsBuilder(namespace);
        sb.addDefaultLocations();
        for (Settings s : settingsList) {
            sb.add(s);
        }
        Settings settings = sb.build();
        ScopeProvider appModule = appModule(settings);
        DependenciesBuilder db = new DependenciesBuilder().add(appModule);
        if (shutdownHookWaitMillis != null) {
            db.withShutdownHookExecutorAwaitDuration(shutdownHookWaitMillis);
        } else if (!Boolean.getBoolean("unit.test")) {
            db.withShutdownHookExecutorAwaitDuration(Duration.ofMinutes(1));
        }

        db.enableOnlyBindingsFor(settingsBindings.toArray(new SettingsBindings[settingsBindings.size()]));
        db.add(settings, namespace);
        db.add(settings, DEFAULT_NAMESPACE);
        for (Module m : modules) {
            db.add(m);
        }
        for (Class m : moduleClasses) {
            db.add(instantiateModule(m, appModule, settings));
        }
        db.add(new CorsAndHelpModule(enableCors, enableHelp));

        if (mergeNamespaces) {
            db.mergeNamespaces();
        }
        return db;
    }

    @SuppressWarnings("unchecked")
    private ScopeProvider appModule(Settings settings) {
        if (appType == null || GenericApplication.class.isAssignableFrom(appType)) {
            return createGenericApplicationModule(settings, types, sslContext);
        } else {
            return createModule(scope, appType, types, sslContext);
        }
    }

    private static  TS createModule(ReentrantScope scope, Class appType, Set> toBind, SslContext sslContext) {
        return new TS<>(scope, appType, toBind, sslContext);
    }

    static class CorsAndHelpModule extends GenericApplicationSettings implements Module {

        public CorsAndHelpModule(boolean corsEnabled, boolean helpEnabled) {
            super(corsEnabled, helpEnabled);
        }

        @Override
        public void configure(Binder binder) {
            binder.bind(GenericApplicationSettings.class).toInstance(this);
        }
    }

    static final class SslConfigImpl extends ActeurSslConfig {

        private final SslContext ctx;

        public SslConfigImpl(SslContext ctx) {
            this.ctx = ctx;
        }

        @Override
        protected SslContext createSslContext() throws CertificateException, SSLException {
            return ctx;
        }
    }

    private static final class TS extends ServerModule implements ScopeProvider {

        private final Set> toBind;
        private final SslContext ctx;

        TS(ReentrantScope scope, Class appType, Set> toBind, SslContext ctx) {
            super(scope, appType, -1, -1, -1);
            this.toBind = toBind;
            this.ctx = ctx;
        }

        @Override
        protected void configure() {
            super.configure();
            Class[] types = toBind.toArray(new Class[toBind.size()]);
            scope.bindTypes(binder(), types);
            if (ctx != null) {
                bind(ActeurSslConfig.class).toInstance(new SslConfigImpl(ctx));
            }
        }

        public ReentrantScope scope() {
            return scope;
        }
    }

    @SuppressWarnings("unchecked")
    private GS createGenericApplicationModule(Settings settings, Set> toBind, SslContext ctx) {
        Class c = appType == null ? GenericApplication.class : (Class) appType;
        return createGenericApplicationModule(scope, c, settings, toBind, ctx);
    }

    private static  GS createGenericApplicationModule(ReentrantScope scope, Class type, Settings settings, Set> toBind, SslContext ctx) {
        return new GS<>(scope, type, settings, toBind, ctx);
    }

    private static final class GS extends GenericApplicationModule implements ScopeProvider {

        private final Set> toBind;
        private SslContext ctx;

        public GS(ReentrantScope scope, Class appType, Settings settings, Set> toBind, SslContext ctx) {
            super(scope, settings, appType, new Class[0]);
            this.toBind = toBind;
            this.ctx = ctx;
        }

        public GS(Settings settings, Set> toBind) {
            super(settings);
            this.toBind = toBind;
        }

        @Override
        protected void configure() {
            super.configure();
            Class[] types = toBind.toArray(new Class[toBind.size()]);
            scope.bindTypes(binder(), types);
            if (ctx != null) {
                bind(ActeurSslConfig.class).toInstance(new SslConfigImpl(ctx));
            }
        }

        public ReentrantScope scope() {
            return scope;
        }
    }

    private interface ScopeProvider extends Module {

        ReentrantScope scope();
    }

    private Module instantiateModule(Class module, ScopeProvider s, Settings settings) {
        try {
            return GenericApplicationModule.instantiateModule(module, settings, s.scope());
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            return Exceptions.chuck(ex);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy