com.github.triceo.robozonky.app.CommandLineInterface.java Source code

Java tutorial

Introduction

Here is the source code for com.github.triceo.robozonky.app.CommandLineInterface.java

Source

/*
 * Copyright 2016 Luk Petrovick
 *
 * 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.github.triceo.robozonky.app;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Processes command line arguments and provides access to their values.
 */
class CommandLineInterface {

    private static final Logger LOGGER = LoggerFactory.getLogger(CommandLineInterface.class);
    private static final int DEFAULT_CAPTCHA_DELAY_SECONDS = 2 * 60;
    private static final int DEFAULT_SLEEP_PERIOD_MINUTES = 60;

    static final Option OPTION_STRATEGY = Option.builder("s").hasArg().longOpt("strategy")
            .argName("Investment strategy")
            .desc("Points to a file that holds the investment strategy configuration.").build();
    static final Option OPTION_INVESTMENT = Option.builder("l").hasArg().longOpt("loan").argName("Single loan ID")
            .desc("Ignore strategy, invest to one specific loan and exit.").build();
    static final Option OPTION_AMOUNT = Option.builder("a").hasArg().longOpt("amount").argName("Amount to invest")
            .desc("Amount to invest to a single loan when ignoring strategy.").build();
    static final Option OPTION_USERNAME = Option.builder("u").hasArg().longOpt("username").argName("Zonky username")
            .desc("Used to connect to the Zonky server.").build();
    static final Option OPTION_KEYSTORE = Option.builder("g").hasArg().longOpt("guarded").argName("Guarded file")
            .desc("Path to secure file that contains username, password etc.").build();
    static final Option OPTION_PASSWORD = Option.builder("p").required().hasArg().longOpt("password")
            .argName("Guarded password").desc("Used to connect to the Zonky server, or to read the secure storage.")
            .build();
    static final Option OPTION_USE_TOKEN = Option.builder("r").hasArg().optionalArg(true)
            .argName("Seconds before expiration").longOpt("refresh")
            .desc("Once logged in, RoboZonky will never log out unless login expires. Use with caution.").build();
    static final Option OPTION_FAULT_TOLERANT = Option.builder("t").longOpt("fault-tolerant")
            .desc("RoboZonky will not report errors if it thinks Zonky is down. Use with caution.").build();
    static final Option OPTION_DRY_RUN = Option.builder("d").hasArg().optionalArg(true).argName("Dry run balance")
            .longOpt("dry").desc("Simulate the investments, but never actually spend money.").build();
    static final Option OPTION_CLOSED_SEASON = Option.builder("c").hasArg()
            .argName("Delay in seconds before new loans are made available for investing.").longOpt("closed-season")
            .desc("Allows to override the default CAPTCHA loan delay.").build();
    static final Option OPTION_ZONK = Option.builder("z").hasArg()
            .argName("The longest amount of time in minutes for which Zonky is allowed to sleep.").longOpt("zonk")
            .desc("Allows to override the default length of sleep period.").build();

    /**
     * Convert the command line arguments into a string and log it.
     * @param cli Parsed command line.
     */
    private static void logOptionValues(final CommandLine cli) {
        final List<String> optionsString = Arrays.stream(cli.getOptions()).filter(o -> cli.hasOption(o.getOpt()))
                .filter(o -> !o.equals(CommandLineInterface.OPTION_PASSWORD)).map(o -> {
                    String result = "-" + o.getOpt();
                    final String value = cli.getOptionValue(o.getOpt());
                    if (value != null) {
                        result += " " + value;
                    }
                    return result;
                }).collect(Collectors.toList());
        CommandLineInterface.LOGGER.debug("Processing command line: {}.", optionsString);
    }

    /**
     * Parse the command line.
     *
     * @param args Command line arguments as received by {@link App#main(String...)}.
     * @return Empty if parsing failed, at which point it will write standard help message to sysout.
     */
    public static Optional<CommandLineInterface> parse(final String... args) {
        // create the mode of operation
        final OptionGroup operatingModes = new OptionGroup();
        operatingModes.setRequired(true);
        Stream.of(OperatingMode.values()).forEach(mode -> operatingModes.addOption(mode.getSelectingOption()));
        // find all options from all modes of operation
        final Collection<Option> ops = Stream.of(OperatingMode.values()).map(OperatingMode::getOtherOptions)
                .collect(LinkedHashSet::new, LinkedHashSet::addAll, LinkedHashSet::addAll);
        // include authentication options
        final OptionGroup authenticationModes = new OptionGroup();
        authenticationModes.setRequired(true);
        authenticationModes.addOption(CommandLineInterface.OPTION_USERNAME);
        authenticationModes.addOption(CommandLineInterface.OPTION_KEYSTORE);
        ops.add(CommandLineInterface.OPTION_PASSWORD);
        ops.add(CommandLineInterface.OPTION_USE_TOKEN);
        ops.add(CommandLineInterface.OPTION_FAULT_TOLERANT);
        ops.add(CommandLineInterface.OPTION_CLOSED_SEASON);
        ops.add(CommandLineInterface.OPTION_ZONK);
        // join all in a single config
        final Options options = new Options();
        options.addOptionGroup(operatingModes);
        options.addOptionGroup(authenticationModes);
        ops.forEach(options::addOption);
        final CommandLineParser parser = new DefaultParser();
        // and parse
        try {
            final CommandLine cli = parser.parse(options, args);
            CommandLineInterface.logOptionValues(cli);
            return Optional.of(new CommandLineInterface(options, cli));
        } catch (final ParseException ex) {
            CommandLineInterface.printHelp(options, ex.getMessage(), true);
            return Optional.empty();
        }
    }

    private final CommandLine cli;
    private final Options options;

    private CommandLineInterface(final Options options, final CommandLine cli) {
        this.options = options;
        this.cli = cli;
    }

    OperatingMode getCliOperatingMode() {
        for (final OperatingMode mode : OperatingMode.values()) {
            if (this.hasOption(mode.getSelectingOption())) {
                return mode;
            }
        }
        // a choice of the operating mode is mandatory; parsing the command line will never get here
        throw new IllegalStateException("This situation is impossible.");
    }

    private boolean hasOption(final Option option) {
        return this.cli.hasOption(option.getOpt());
    }

    private Optional<String> getOptionValue(final Option option) {
        if (this.hasOption(option)) {
            final String val = this.cli.getOptionValue(option.getOpt());
            if (val == null || val.isEmpty()) {
                return Optional.empty();
            } else {
                return Optional.of(val);
            }
        } else {
            return Optional.empty();
        }
    }

    private Optional<Integer> getIntegerOptionValue(final Option option) {
        final Optional<String> result = this.getOptionValue(option);
        if (result.isPresent()) {
            try {
                return Optional.of(Integer.valueOf(result.get()));
            } catch (final NumberFormatException ex) {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    Optional<String> getStrategyConfigurationFilePath() {
        return this.getOptionValue(OperatingMode.STRATEGY_DRIVEN.getSelectingOption());
    }

    public Optional<String> getUsername() {
        return this.getOptionValue(CommandLineInterface.OPTION_USERNAME);
    }

    public String getPassword() {
        return this.getOptionValue(CommandLineInterface.OPTION_PASSWORD).get();
    }

    Optional<Integer> getLoanId() {
        return this.getIntegerOptionValue(CommandLineInterface.OPTION_INVESTMENT);
    }

    Optional<Integer> getLoanAmount() {
        return this.getIntegerOptionValue(CommandLineInterface.OPTION_AMOUNT);
    }

    boolean isDryRun() {
        return this.hasOption(CommandLineInterface.OPTION_DRY_RUN);
    }

    public boolean isTokenEnabled() {
        return this.hasOption(CommandLineInterface.OPTION_USE_TOKEN);
    }

    public boolean isFaultTolerant() {
        return this.hasOption(CommandLineInterface.OPTION_FAULT_TOLERANT);
    }

    public int getCaptchaPreventingInvestingDelayInSeconds() { // FIXME do not allow negaive values
        return this.hasOption(CommandLineInterface.OPTION_CLOSED_SEASON)
                ? this.getIntegerOptionValue(CommandLineInterface.OPTION_CLOSED_SEASON)
                        .orElseThrow(() -> new IllegalStateException("Missing mandatory argument value."))
                : CommandLineInterface.DEFAULT_CAPTCHA_DELAY_SECONDS;
    }

    public int getMaximumSleepPeriodInMinutes() { // FIXME do not allow negaive values
        return this.hasOption(CommandLineInterface.OPTION_ZONK)
                ? this.getIntegerOptionValue(CommandLineInterface.OPTION_ZONK)
                        .orElseThrow(() -> new IllegalStateException("Missing mandatory argument value."))
                : CommandLineInterface.DEFAULT_SLEEP_PERIOD_MINUTES;
    }

    public Optional<Integer> getTokenRefreshBeforeExpirationInSeconds() {
        return this.getIntegerOptionValue(CommandLineInterface.OPTION_USE_TOKEN);
    }

    public Optional<File> getKeyStoreLocation() {
        final Optional<String> value = this.getOptionValue(CommandLineInterface.OPTION_KEYSTORE);
        if (value.isPresent()) {
            return Optional.of(new File(value.get()));
        } else {
            return Optional.empty();
        }
    }

    Optional<Integer> getDryRunBalance() {
        return this.getIntegerOptionValue(CommandLineInterface.OPTION_DRY_RUN);
    }

    static String getScriptIdentifier() {
        return System.getProperty("os.name").contains("Windows") ? "robozonky.bat" : "robozonky.sh";
    }

    private static void printHelp(final Options options, final String message, final boolean isError) {
        new HelpFormatter().printHelp(CommandLineInterface.getScriptIdentifier(), null, options,
                isError ? "Error: " + message : message, true);
    }

    public void printHelp(final String message, final boolean isError) {
        CommandLineInterface.printHelp(this.options, message, isError);
    }

}