• ConsoleScanner.java

  • §
    package MusicLandscape.util;
    
    import java.util.NoSuchElementException;
    import java.util.Scanner;
    import java.util.function.Function;
    import java.util.function.Predicate;
  • §

    This is the same as in ES06, with a little refactoring sprinkled on top.

    • The property skippable is actually used now.
    • The Scanner object in use is now checked with the getter every time we do scan(), so the initialisation time for the default scanner which uses System.in is later. I think there was some buggy behaviour in the tests. It is not the best design to create the scanner ourselves here, I should have made the code that uses this class pass in a Scanner object.
    /**
     * Generic console function for getting a new value for an entity field from the user.
     *
     * <p>
     * Example:
     *
     * <pre>
     * Integer num = new ConsoleScanner(Integer::parseInt, (Integer i) -&gt; i &gt; 0, null)
     *     .scan("Give me a number")
     * </pre>
     *
     * Produces this output:
     *
     * <pre>
     * Give me a number: &lt;user inputs 123 and presses Enter&gt;
     * </pre>
     *
     * Leading to the variable <kbd>num</kbd> having the value <kbd>123</kbd>.
     *
     * @param <T> The type of the value being read
     * @author Jonas Altrock (ew20b126@technikum-wien.at)
     * @version 1
     * @since ExerciseSheet04
     */
    public class ConsoleScanner<T> {
        /**
         * constant to allow readable indication of a skippable input
         */
        public static final boolean SKIPPABLE = true;
        /**
         * constant to allow readable indication of a non-skippable input
         */
        public static final boolean NOT_SKIPPABLE = false;
    
        /**
         * The default input scanner. Uses System.in if not set from outside.
         */
        public static Scanner defaultScanner;
    
        /**
         * Whether the input can be skipped (by pressing Enter for example).
         */
        public boolean skippable = SKIPPABLE;
    
        /**
         * The input to value transformer function.
         */
        public Function<String, T> transformer;
    
        /**
         * The input value validator function.
         */
        public Predicate<T> validator;
    
        /**
         * The scanner object in use.
         */
        public Scanner scanner;
    
        /**
         * A scanner for reading non-empty strings.
         */
        public static final ConsoleScanner<String> nonEmptyString = new ConsoleScanner<>((String s) -> s, (String s) -> !s.isBlank(), null);
    
        /**
         * A scanner for reading positive integers.
         */
        public static final ConsoleScanner<Integer> positiveInteger = new ConsoleScanner<>(Integer::parseInt, (Integer i) -> i >= 0, null);
    
        /**
         * Create a scanner for a single value. Skippable by default.
         *
         * @param transformer a function that transforms the input string to a value
         * @param validator a function that validates the given value
         * @param scanner optional Scanner to use, pass null to use the default scanner (System.in)
         */
        public ConsoleScanner(
                Function<String, T> transformer,
                Predicate<T> validator,
                Scanner scanner
        ) {
            this.transformer = transformer;
            this.validator = validator;
    
            if (scanner != null) {
                this.scanner = scanner;
            }
        }
    
        /**
         * Create a scanner for a single value.
         *
         * @param transformer a function that transforms the input string to a value
         * @param validator a function that validates the given value
         * @param scanner optional Scanner to use, pass null to use the default scanner (System.in)
         * @param skippable whether the user is allowed to skip entry by just pressing Enter
         */
        public ConsoleScanner(
                Function<String, T> transformer,
                Predicate<T> validator,
                Scanner scanner,
                boolean skippable
        ) {
            this(transformer, validator, scanner);
            this.skippable = skippable;
        }
    
        /**
         * Duplicate this object and use a different Scanner.
         *
         * @param s the Scanner to use
         * @return a new ConsoleScanner using the provided Scanner
         */
        public ConsoleScanner<T> withScanner(Scanner s) {
            return new ConsoleScanner<>(transformer, validator, s, skippable);
        }
    
        /**
         * Duplicate this object and make it unskippable.
         *
         * @return a new ConsoleScanner that is not skippable.
         */
        public ConsoleScanner<T> unskippable() {
            return new ConsoleScanner<>(transformer, validator, scanner, NOT_SKIPPABLE);
        }
    
        /**
         * Get the default scanner object. System.in by if not set otherwise.
         *
         * @return the default scanner
         */
        public Scanner getDefaultScanner() {
            if (defaultScanner != null) {
                return defaultScanner;
            }
            return new Scanner(System.in);
        }
    
        /**
         * Get Scanner in use
         * @return Scanner
         */
        public Scanner getScanner() {
            if (scanner != null) {
                return scanner;
            }
    
            return getDefaultScanner();
        }
    
        /**
         * Prompt the user for an input value.
         *
         * @param prompt string to print before prompt input
         * @return a value or null, if no value was given
         */
        public T scan(String prompt) {
            T value;
    
            do {
                System.out.print(prompt + ": ");
    
                try {
                    String in = getScanner().nextLine();
    
                    if (skippable && in.isEmpty()) {
                        return null;
                    }
    
                    value = transformer.apply(in);
                } catch (NoSuchElementException e) {
                    value = null;
    
                    if (skippable) {
                        return null;
                    }
                } catch (Exception e) {
                    value = null;
                }
    
                if (value == null) {
                    System.out.println("Invalid input, try again?");
                    continue;
                }
    
                if (!validator.test(value)) {
                    System.out.println("Value not allowed, try again?");
                    value = null;
                }
            } while (value == null);
    
            return value;
        }
    }