com.planet57.gshell.MainSupport.java Source code

Java tutorial

Introduction

Here is the source code for com.planet57.gshell.MainSupport.java

Source

/*
 * Copyright (c) 2009-present the original author or authors.
 *
 * 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.planet57.gshell;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.planet57.gossip.Level;
import com.planet57.gossip.Log;
import com.planet57.gshell.branding.Branding;
import com.planet57.gshell.branding.BrandingSupport;
import com.planet57.gshell.guice.BeanContainer;
import com.planet57.gshell.internal.ShellBuilderImpl;
import com.planet57.gshell.shell.ShellBuilder;
import com.planet57.gshell.util.io.IO;
import com.planet57.gshell.internal.ExitCodeDecoder;
import com.planet57.gshell.shell.Shell;
import com.planet57.gshell.util.NameValue;
import com.planet57.gshell.util.cli2.Argument;
import com.planet57.gshell.util.cli2.CliProcessor;
import com.planet57.gshell.util.cli2.HelpPrinter;
import com.planet57.gshell.util.cli2.Option;
import com.planet57.gshell.util.io.StreamSet;
import com.planet57.gshell.util.pref.Preference;
import com.planet57.gshell.util.pref.PreferenceProcessor;
import com.planet57.gshell.util.pref.Preferences;
import com.planet57.gshell.util.io.StyledIO;
import com.planet57.gshell.variables.VariableNames;
import com.planet57.gshell.variables.Variables;
import com.planet57.gshell.variables.VariablesSupport;
import org.apache.commons.cli.ParseException;
import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
import org.apache.felix.service.threadio.ThreadIO;
import org.eclipse.sisu.space.BeanScanning;
import org.eclipse.sisu.space.SpaceModule;
import org.eclipse.sisu.space.URLClassSpace;
import org.eclipse.sisu.wire.WireModule;
import org.jline.style.MemoryStyleSource;
import org.jline.style.Styler;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Support for booting shell applications.
 *
 * @since 2.0
 */
@Preferences(path = "cli")
public abstract class MainSupport {
    private final Logger log = Log.getLogger(getClass());

    private final ThreadIOImpl threadIO = new ThreadIOImpl();

    @Option(name = "h", longName = "help", description = "Display usage", override = true)
    private boolean help;

    @Option(name = "V", longName = "version", description = "Display program version", override = true)
    private boolean version;

    @Preference
    @Option(name = "e", longName = "errors", description = "Produce detailed exceptions")
    private boolean showErrorTraces;

    @Nullable
    private Level loggingLevel;

    @Preference(name = "debug")
    @Option(name = "d", longName = "debug", description = "Enable debug output")
    private void setDebug(final boolean flag) {
        log.debug("Debug: {}", flag);
        if (flag) {
            loggingLevel = Level.DEBUG;
            // imply --errors
            showErrorTraces = true;
        }
    }

    @Preference(name = "trace")
    @Option(name = "X", longName = "trace", description = "Enable trace output")
    private void setTrace(final boolean flag) {
        log.debug("Trace: {}", flag);
        if (flag) {
            loggingLevel = Level.TRACE;
            // imply --errors
            showErrorTraces = true;
        }
    }

    @Nullable
    @Option(name = "c", longName = "command", description = "Execute COMMAND", token = "COMMAND")
    private String command;

    private final Variables variables = new VariablesSupport();

    @Option(name = "D", longName = "define", description = "Define a variable", token = "NAME=VALUE")
    private void setVariable(final String input) {
        log.debug("Set variable: {}", input);
        NameValue nv = NameValue.parse(input);
        variables.set(nv.name, nv.value);
    }

    @Option(name = "P", longName = "property", description = "Define a system-property", token = "NAME=VALUE")
    private void setSystemProperty(final String input) {
        log.debug("Set system-property: {}", input);
        NameValue nv = NameValue.parse(input);
        System.setProperty(nv.name, nv.value);
    }

    @Argument(description = "Command expression to execute", token = "EXPR")
    @Nullable
    private List<String> appArgs;

    //
    // Boot
    //

    public void boot(final String... args) throws Exception {
        checkNotNull(args);

        if (log.isDebugEnabled()) {
            log.debug("Booting w/args: {}", Arrays.toString(args));
        }

        // Register default handler for uncaught exceptions
        Thread.setDefaultUncaughtExceptionHandler(
                (thread, cause) -> log.warn("Unhandled exception occurred on thread: {}", thread, cause));

        // Prepare branding
        Branding branding = createBranding();

        // Process preferences
        PreferenceProcessor pp = new PreferenceProcessor();
        pp.setBasePath(branding.getPreferencesBasePath());
        pp.addBean(this);
        pp.process();

        // Process command line options & arguments
        CliProcessor clp = new CliProcessor();
        clp.addBean(this);
        clp.setStopAtNonOption(true);

        // cope with cli exceptions; which are expected
        try {
            clp.process(args);
        } catch (ParseException e) {
            e.printStackTrace(System.err);
            exit(2);
        }

        // once options are processed setup logging environment
        setupLogging(loggingLevel);

        // setup styling
        Styler.setSource(new MemoryStyleSource());

        // prepare terminal and I/O
        Terminal terminal = createTerminal(branding);
        IO io = StyledIO.create("shell", createStreamSet(terminal), terminal);

        if (help) {
            HelpPrinter printer = new HelpPrinter(clp, terminal.getWidth());
            printer.printUsage(io.out, branding.getProgramName());
            io.flush();
            exit(0);
        }

        if (version) {
            io.format("%s %s%n", branding.getDisplayName(), branding.getVersion());
            io.flush();
            exit(0);
        }

        // install thread-IO handler and attach streams
        threadIO.start();
        threadIO.setStreams(io.streams.in, io.streams.out, io.streams.err);

        Object result = null;
        try {
            variables.set(VariableNames.SHELL_ERRORS, showErrorTraces);

            Shell shell = createShell(io, variables, branding);
            shell.start();
            try {
                if (command != null) {
                    result = shell.execute(command);
                } else if (appArgs != null) {
                    result = shell.execute(String.join(" ", appArgs));
                } else {
                    shell.run();
                }
            } finally {
                shell.stop();
            }
        } finally {
            io.flush();
            threadIO.stop();
            terminal.close();
        }

        if (result == null) {
            result = variables.get(VariableNames.LAST_RESULT);
        }

        exit(ExitCodeDecoder.decode(result));
    }

    //
    // Shell creation
    //

    /**
     * Create a the {@link Branding} instance.
     *
     * Branding is needed very early to allow customization of command-line processing.
     */
    protected Branding createBranding() {
        return new BrandingSupport();
    }

    /**
     * Setup logging environment.
     */
    protected void setupLogging(@Nullable final Level level) {
        // install JUL adapter
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();

        // conifgure gossip bootstrap loggers with target factory
        Log.configure(LoggerFactory.getILoggerFactory());
        log.debug("Logging setup; level: {}", level);
    }

    /**
     * Create a new {@link Shell}.
     */
    @VisibleForTesting
    protected Shell createShell(final IO io, final Variables variables, final Branding branding) throws Exception {
        log.debug("Creating shell instance");

        List<Module> modules = new ArrayList<>();

        URLClassSpace space = new URLClassSpace(getClass().getClassLoader());
        modules.add(new SpaceModule(space, BeanScanning.INDEX));

        final BeanContainer container = new BeanContainer();
        modules.add(BeanContainer.module(container));
        modules.add(binder -> {
            binder.bind(ThreadIO.class).toInstance(threadIO);
            binder.bind(Branding.class).toInstance(branding);
            binder.bind(ShellBuilder.class).to(ShellBuilderImpl.class);
        });

        configure(modules);

        Injector injector = Guice.createInjector(new WireModule(modules));
        // injector is automatically bound to BeanLocator by sisu

        return injector.getInstance(ShellBuilder.class).branding(branding).io(io).variables(variables).build();
    }

    /**
     * Allow sub-class to customize container.
     */
    protected void configure(@Nonnull final List<Module> modules) {
        // empty
    }

    //
    // Helpers
    //

    /**
     * Create the {@link Terminal}.
     */
    @VisibleForTesting
    protected Terminal createTerminal(final Branding branding) throws Exception {
        return TerminalBuilder.builder().name(branding.getProgramName()).system(true).nativeSignals(true)
                .signalHandler(Terminal.SignalHandler.SIG_IGN) // ignore signals by default
                .build();
    }

    /**
     * Create the {@link StreamSet} used to register.
     */
    @VisibleForTesting
    protected StreamSet createStreamSet(final Terminal terminal) {
        InputStream in = new FilterInputStream(terminal.input()) {
            @Override
            public void close() throws IOException {
                // ignore
            }
        };
        PrintStream out = new PrintStream(terminal.output(), true) {
            @Override
            public void close() {
                // ignore
            }
        };
        return new StreamSet(in, out);
    }

    /**
     * Allow control of exit behavior.
     */
    @VisibleForTesting
    protected void exit(final int code) {
        log.debug("Existing with code: {}", code);
        System.exit(code);
    }
}