io.github.felsenhower.stine_calendar_bot.main.CallLevelWrapper.java Source code

Java tutorial

Introduction

Here is the source code for io.github.felsenhower.stine_calendar_bot.main.CallLevelWrapper.java

Source

package io.github.felsenhower.stine_calendar_bot.main;

import java.io.IOException;
import java.net.URLDecoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.stream.Collectors;

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.MissingArgumentException;
import org.apache.commons.cli.MissingOptionException;
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.apache.commons.cli.UnrecognizedOptionException;

import io.github.felsenhower.stine_calendar_bot.util.StringProvider;

/**
 * Creates a new instance
 * 
 * @param args
 *            the submitted args from the main method.
 */
public class CallLevelWrapper {

    private final String username;
    private final String password;
    private final boolean echoPages;
    private final Path calendarCache;
    private final Path outputFile;
    private final boolean echoCalendar;

    private final StringProvider strings;
    private final StringProvider cliStrings;
    private final StringProvider messages;
    private final StringProvider appInfo;

    private boolean isLangInitialised = false;

    final private Options options;

    public CallLevelWrapper(String[] args) throws IOException {
        String username = null;
        String password = null;
        boolean echoPages = false;
        Path calendarCache = null;
        Path outputFile = null;
        boolean echoCalendar = false;

        // These temporary options don't have descriptions and have their
        // required-value all set to false
        final Options tempOptions = getOptions();

        final CommandLineParser parser = new DefaultParser();
        CommandLine cmd = null;
        StringProvider strings = null;

        // Get the StringProvider
        try {
            cmd = parser.parse(tempOptions, args, true);
            if (cmd.hasOption("language")) {
                String lang = cmd.getOptionValue("language").toLowerCase();
                if (lang.equals("de")) {
                    strings = new StringProvider(Locale.GERMAN);
                } else {
                    strings = new StringProvider(Locale.ENGLISH);
                    if (!lang.equals("en")) {
                        System.err.println(strings.get("HumanReadable.CallLevel.LangNotRecognised", lang));
                    }
                }
            } else {
                strings = new StringProvider(new Locale(Locale.getDefault().getLanguage()));
            }
        } catch (Exception e) {
            strings = new StringProvider(Locale.ENGLISH);
        }

        this.strings = strings;
        this.cliStrings = strings.from("HumanReadable.CallLevel");
        this.messages = strings.from("HumanReadable.Messages");
        this.appInfo = strings.from("MachineReadable.App");

        // Get the localised options, with all required fields enabled as well.
        // Note that we are not yet applying the options to any command line,
        // because we still want to exit if only the help screen shall be
        // displayed first, but of course, we do need the localised options
        // here.
        this.isLangInitialised = true;
        this.options = getOptions();

        try {
            // If no arguments are supplied, or --help is used, we will exit
            // after printing the help screen
            if (cmd.hasOption("help") || (cmd.hasOption("language") && cmd.getOptions().length == 1)) {
                printHelp();
            }

            cmd = parser.parse(this.options, args, false);

            username = cmd.getOptionValue("user");

            // URL-decode the password (STiNE doesn't actually allow special
            // chars in passwords, but meh...)
            password = URLDecoder.decode(cmd.getOptionValue("pass"), "UTF-8");
            // Double-dash signals that the password shall be read from stdin
            if (password.equals("--")) {
                password = readPassword(messages.get("PasswordQuery"), messages.get("PasswordFallbackMsg"));
            }

            echoPages = cmd.hasOption("echo");

            // the cache-dir argument is optional, so we read it with a default
            // value
            calendarCache = Paths
                    .get(cmd.getOptionValue("cache-dir", strings.get("MachineReadable.Paths.CalendarCache")))
                    .toAbsolutePath();

            // output-argument is optional as well, but this time we check if
            // double-dash is specified (for echo to stdout)
            String outputStr = cmd.getOptionValue("output", strings.get("MachineReadable.Paths.OutputFile"));
            if (outputStr.equals("--")) {
                echoCalendar = true;
                outputFile = null;
            } else {
                echoCalendar = false;
                outputFile = Paths.get(outputStr).toAbsolutePath();
            }

        } catch (UnrecognizedOptionException e) {
            System.err.println(messages.get("UnrecognisedOption", e.getOption().toString()));
            this.printHelp();
        } catch (MissingOptionException e) {
            // e.getMissingOptions() is just extremely horribly designed and
            // here is why:
            //
            // It returns an unparametrised list, to make your job especially
            // hard, whose elements may be:
            // - String-instances, if there are single independent options
            // missing (NOT the stupid Option itself, just why????)
            // - OptionGroup-instances, if there are whole OptionGroups missing
            // (This time the actual OptionGroup and not an unparametrised Set
            // that may or may not contain an Option, how inconsequential).
            // - Basically anything because the programmer who wrote that
            // function was clearly high and is most probably not to be trusted.
            //
            // This makes the job of actually displaying all the options as a
            // comma-separated list unnecessarily hard and hence leads to this
            // ugly contraption of Java-8-statements. But hey, at least it's not
            // as ugly as the Java 7 version (for me at least).
            // Sorry!
            // TODO: Write better code.
            // TODO: Write my own command line interpreter, with blackjack and
            // hookers.
            try {
                System.err.println(messages.get("MissingRequiredOption", ((List<?>) (e.getMissingOptions()))
                        .stream().filter(Object.class::isInstance).map(Object.class::cast).map(o -> {
                            if (o instanceof String) {
                                return Collections.singletonList(options.getOption((String) o));
                            } else {
                                return ((OptionGroup) o).getOptions();
                            }
                        }).flatMap(o -> o.stream()).filter(Option.class::isInstance).map(Option.class::cast)
                        .map(o -> o.getLongOpt()).collect(Collectors.joining(", "))));
                this.printHelp();
            } catch (Exception totallyMoronicException) {
                throw new RuntimeException("I hate 3rd party libraries!", totallyMoronicException);
            }
        } catch (MissingArgumentException e) {
            System.err.println(messages.get("MissingRequiredArgument", e.getOption().getLongOpt()));
            this.printHelp();
        } catch (ParseException e) {
            System.err.println(messages.get("CallLevelParsingException", e.getMessage()));
        }

        this.username = username;
        this.password = password;
        this.echoPages = echoPages;
        this.calendarCache = calendarCache;
        this.outputFile = outputFile;
        this.echoCalendar = echoCalendar;
    }

    /**
     * Gets the password from stdin
     * 
     * @param query
     *            the query to the user
     * @param errorMsg
     *            the message to display when the input gets somehow redirected
     *            and System.console() becomes null.
     * @return the entered password
     */
    private static String readPassword(String query, String errorMsg) {
        final String result;
        if (System.console() != null) {
            System.err.print(query);
            result = new String(System.console().readPassword());
        } else {
            System.err.println(errorMsg);
            System.err.print(query);
            Scanner scanner = new Scanner(System.in);
            result = scanner.nextLine();
            scanner.close();
        }
        System.err.println();
        return result;
    }

    /**
     * Prints the help screen for the current Options instance and exits the
     * application
     */
    private void printHelp() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setOptionComparator(null);
        formatter.setDescPadding(4);
        formatter.setLeftPadding(2);
        formatter.setLongOptSeparator("=");
        formatter.setLongOptPrefix(" --");
        formatter.setSyntaxPrefix(cliStrings.get("Usage"));
        formatter.printHelp(cliStrings.get("UsageTemplate", appInfo.get("Name")),
                cliStrings.get("HelpHeader", appInfo.get("Name"), appInfo.get("Version")), this.options,
                cliStrings.get("HelpFooter", cliStrings.get("Author"), cliStrings.get("License"),
                        appInfo.get("ProjectPage")),
                true);
        System.exit(0);
    }

    /**
     * Acquires the Options with the given localisation from the StringProvider.
     * We already need a complete Options instance before we know which language
     * we are using, so passing null will simply not add any descriptions or
     * required options. These initial options should not be printed with a
     * HelpFormatter.
     */
    private Options getOptions() {

        final Options options = new Options();

        if (this.isLangInitialised) {
            // @formatter:off
            options.addOption(Option.builder("h").longOpt("help").desc(cliStrings.get("HelpDescription")).build());

            options.addOption(Option.builder("l").longOpt("language").hasArg().argName("en|de")
                    .desc(cliStrings.get("LangDescription")).build());

            options.addOption(Option.builder("u").longOpt("user").required().hasArg().argName("user")
                    .desc(cliStrings.get("UserDescription")).build());

            options.addOption(Option.builder("p").longOpt("pass").required().hasArg().argName("pass")
                    .desc(cliStrings.get("PassDescription")).build());

            options.addOption(Option.builder("e").longOpt("echo").desc(cliStrings.get("EchoDescription")).build());

            options.addOption(Option.builder("c").longOpt("cache-dir").hasArg().argName("dir")
                    .desc(cliStrings.get("CacheDirDescription", "MachineReadable.Paths.CalendarCache")).build());

            options.addOption(Option.builder("o").longOpt("output").hasArg().argName("file")
                    .desc(cliStrings.get("OutputDescription", "MachineReadable.Paths.OutputFile")).build());
            // @formatter:on
        } else {
            // @formatter:off
            options.addOption(Option.builder("l").longOpt("language").hasArg().build());
            options.addOption(Option.builder("h").longOpt("help").build());
            // @formatter:on
        }
        return options;
    }

    /**
     * @return the username (STiNE)
     */
    public String getUsername() {
        return username;
    }

    /**
     * @return the password (STiNE)
     */
    public String getPassword() {
        return password;
    }

    /**
     * @return Determines whether the pages shall be echo'ed during browsing
     */
    public boolean isEchoPages() {
        return echoPages;
    }

    /**
     * @return the calendar cache directory
     */
    public Path getCalendarCache() {
        return calendarCache;
    }

    /**
     * @return the output filename
     */
    public Path getOutputFile() {
        return outputFile;
    }

    /**
     * @return Determines whether the calender shall be echo'ed to stdout at the
     *         end (true) or saved to file (false) according to
     *         getOutputFilename().
     */
    public boolean isEchoCalendar() {
        return echoCalendar;
    }

    /**
     * @return the {@link StringProvider} according to the specified --language
     *         argument
     */
    public StringProvider getStringProvider() {
        return strings;
    }
}