BaseCommand.java

package emissary.command;

import emissary.command.converter.PathExistsConverter;
import emissary.command.converter.ProjectBaseConverter;
import emissary.config.ConfigUtil;
import emissary.core.EmissaryException;

import ch.qos.logback.classic.ClassicConstants;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import javax.annotation.Nullable;

@Command(description = "Base Command")
public abstract class BaseCommand implements EmissaryCommand {
    static final Logger LOG = LoggerFactory.getLogger(BaseCommand.class);

    public static final String COMMAND_NAME = "BaseCommand";

    @Option(names = {"-c", "--config"}, description = "config dir, comma separated if multiple, defaults to <projectBase>/config",
            converter = PathExistsConverter.class)
    @Nullable
    private Path config;

    @Option(names = {"-b", "--projectBase"}, description = "defaults to PROJECT_BASE, errors if different\nDefault: ${DEFAULT-VALUE}",
            converter = ProjectBaseConverter.class)
    private Path projectBase = Paths.get(System.getenv("PROJECT_BASE"));

    @Option(names = "--logbackConfig", description = "logback configuration file, defaults to <configDir>/logback.xml")
    @Nullable
    private String logbackConfig;

    @Option(names = {"--flavor"}, description = "emissary config flavor, comma separated for multiple")
    private String flavor;

    @Option(names = {"--binDir"}, description = "emissary bin dir, defaults to <projectBase>/bin")
    @Nullable
    private Path binDir;

    @Option(names = {"--outputRoot"}, description = "root output directory, defaults to <projectBase>/localoutput")
    @Nullable
    private Path outputDir;

    @Option(names = {"--errorRoot"}, description = "root error directory, defaults to <projectBase>/localerrors")
    @Nullable
    private Path errorDir;

    @Option(names = {"-q", "--quiet"}, description = "hide banner and non essential messages\nDefault: ${DEFAULT-VALUE}")
    private boolean quiet = false;

    public Path getConfig() {
        if (config == null) {
            config = getProjectBase().toAbsolutePath().resolve("config");
            if (!Files.exists(config)) {
                throw new IllegalArgumentException("Config dir not configured and " + config.toAbsolutePath() + " does not exist");
            }
        }

        return config;
    }

    public Path getProjectBase() {
        return projectBase;
    }

    public String getLogbackConfig() {
        if (logbackConfig == null) {
            return getConfig() + "/logback.xml";
        }
        return logbackConfig;
    }

    public String getFlavor() {
        return flavor;
    }

    protected void overrideFlavor(String flavor) {
        logInfo("Overriding current {} {} to {} ", ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor(), flavor);
        this.flavor = flavor;
        System.setProperty(ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor());
    }

    public Path getBinDir() {
        if (binDir == null) {
            return getProjectBase().toAbsolutePath().resolve("bin");
        }
        return binDir;
    }

    public Path getOutputDir() {
        if (outputDir == null) {
            return getProjectBase().toAbsolutePath().resolve("localoutput");
        }
        return outputDir;
    }

    public Path getErrorDir() {
        if (errorDir == null) {
            return getProjectBase().toAbsolutePath().resolve("localerror");
        }
        return errorDir;
    }

    public boolean getQuiet() {
        return quiet;
    }

    public boolean isVerbose() {
        return !getQuiet();
    }

    @Override
    public void setupCommand() {
        setupConfig();
    }

    public void setupConfig() {
        logInfo("{} is set to {} ", ConfigUtil.PROJECT_BASE_ENV, getProjectBase().toAbsolutePath().toString());
        setSystemProperty(ConfigUtil.CONFIG_DIR_PROPERTY, getConfig().toAbsolutePath().toString());
        setSystemProperty(ConfigUtil.CONFIG_BIN_PROPERTY, getBinDir().toAbsolutePath().toString());
        setSystemProperty(ConfigUtil.CONFIG_OUTPUT_ROOT_PROPERTY, getOutputDir().toAbsolutePath().toString());
        logInfo("Emissary error dir set to {} ", getErrorDir().toAbsolutePath().toString());
        if (getFlavor() != null) {
            setSystemProperty(ConfigUtil.CONFIG_FLAVOR_PROPERTY, getFlavor());
        }
    }

    protected void setSystemProperty(String key, String value) {
        logInfo("Setting {} to {} ", key, value);
        System.setProperty(key, value);
    }

    /**
     * Create a new command and parse the args.
     * <p>
     * Useful for testings. Also calls setup so properties are set
     * 
     * @param clazz the Class of return type class
     * @param args vararg of Strings
     */
    @SuppressWarnings("SystemOut")
    public static <T extends EmissaryCommand> T parse(Class<T> clazz, String... args) throws EmissaryException {
        T cmd;
        try {
            cmd = clazz.cast(Class.forName(clazz.getName()).getDeclaredConstructor().newInstance());
        } catch (ReflectiveOperationException e) {
            throw new EmissaryException("Cannot construct command", e);
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos);
        PrintStream old = System.out;
        System.setOut(ps);

        CommandLine cl;
        int code;
        try {
            cl = new CommandLine(cmd);
            code = cl.execute(args);
        } finally {
            System.out.flush();
            System.setOut(old);
        }
        if (cl != null && code == 2) {
            throw new ParameterException(cl, baos.toString());
        }
        cmd.setup();
        return cmd;
    }

    /**
     * Create a new command and parse the args
     * <p>
     * Useful for testings
     * 
     * @param clazz the Class of return type class
     * @param args vararg of Strings
     */
    public static <T extends EmissaryCommand> T parse(Class<T> clazz, List<String> args) throws EmissaryException {
        return parse(clazz, args.toArray(new String[0]));
    }

    /*
     * Try to reinitialize the logback context with the configured file you may have 2 log files if anything logged before
     * we do this. Useful when you are running a server For troubleshooting, looking at the http://localhost:8001/lbConfig
     * when this works, you will see the initial logger and then new one
     *
     * Need to reinit because logback config uses ${emissary.node.name}-${emissary.node.port} which are now set by the
     * commands after logback is initialized
     */
    public void reinitLogback() {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        URL logCfg = ConfigurationWatchListUtil.getMainWatchURL(loggerContext);

        if (logCfg != null && (logCfg.toString().endsWith("logback-test.xml") || logCfg.toString().endsWith("logback-test.groovy"))) {
            // logCfg can be null if Emissary.setupLogbackForConsole is called
            LOG.warn("Not using {}, staying with test config {}", getLogbackConfig(), logCfg);
            doLogbackReinit(loggerContext, logCfg.getPath());
        } else if (Files.exists(Paths.get(getLogbackConfig()))) {
            doLogbackReinit(loggerContext, getLogbackConfig());
        } else {
            LOG.warn("logback configuration not found {}, not reconfiguring logging", getLogbackConfig());
        }

    }

    private void doLogbackReinit(LoggerContext loggerContext, String configFilePath) {
        System.setProperty(ClassicConstants.CONFIG_FILE_PROPERTY, configFilePath);
        loggerContext.reset();
        ContextInitializer newContext = new ContextInitializer(loggerContext);
        try {
            newContext.autoConfig();
        } catch (JoranException e) {
            LOG.error("Problem reconfiguring logback with {}", getLogbackConfig(), e);
        }
    }

    public void logInfo(String format, Object... args) {
        if (isVerbose()) {
            LOG.info(format, args);
        }
    }

    @Override
    public void outputBanner() {
        if (isVerbose()) {
            new Banner().dump();
        }
    }

    @Override
    public void run() {}
}