/*
* The MIT License
*
* Copyright 2015 Ahseya.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.horrorho.liquiddonkey.util;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import net.jcip.annotations.NotThreadSafe;
/**
* User selector.
*
* @author Ahseya
* @param <T> item type
*/
@NotThreadSafe
public class Selector<T> {
/**
* Returns a new Builder instance.
*
* @param <T> the item type
* @param options the available options, not null and no null elements
* @return a new Builder instance, not null
*/
public static <T> Selector.Builder<T> builder(List<T> options) {
return new Builder(options);
}
private final String prompt;
private final String quit;
private final String header;
private final List<T> options;
private final Function<T, String> formatter;
private final String footer;
private final Supplier<List<T>> onLineIsEmpty;
private final Supplier<List<T>> onQuit;
private final String delimiter;
private final Printer out;
private final InputStream in;
Selector(
String prompt,
String quit,
String header,
List<T> options,
Function<T, String> formatter,
String footer,
Supplier<List<T>> onLineIsEmpty,
Supplier<List<T>> onQuit,
String delimiter,
Printer out,
InputStream in) {
this.prompt = prompt;
this.quit = quit;
this.header = header;
this.options = Objects.requireNonNull(options);
this.formatter = Objects.requireNonNull(formatter);
this.footer = footer;
this.onLineIsEmpty = onLineIsEmpty;
this.onQuit = onQuit;
this.delimiter = Objects.requireNonNull(delimiter);
this.out = Objects.requireNonNull(out);
this.in = Objects.requireNonNull(in);
}
/**
* Prints options. First option is 1.
*
* @return this Selector, not null
*/
public Selector printOptions() {
if (header != null) {
out.println(header);
}
for (int i = 0; i < options.size(); i++) {
out.println((i + 1) + ":" + formatter.apply(options.get(i)));
}
if (footer != null) {
out.println(footer);
}
return this;
}
/**
* Returns the user's selection.
* <p>
* Prompts, acquires, validates and returns the user's selection/s with the ordering preserved. Duplicates are
* ignored.
* <p>
* Streams are not closed after use.
*
* @return the user's selection, not null
*/
public List<T> selection() {
Scanner console = new Scanner(in, StandardCharsets.UTF_8.name());
while (true) {
out.print(prompt);
String line = console.nextLine();
if (line == null || line.toLowerCase(Locale.getDefault()).equals(quit)) {
return onQuit.get();
}
if (line.isEmpty()) {
return onLineIsEmpty.get();
}
Set<Integer> numbers = parseLineToNumbers(line);
if (numbers != null && validate(numbers, options)) {
return numbers.stream()
.map(i -> i - 1)
.map(options::get)
.collect(Collectors.toList());
}
}
}
boolean validate(Set<Integer> selected, List<T> options) {
for (Integer i : selected) {
if (i < 1 || i > options.size()) {
out.println("Invalid selection: " + i);
return false;
}
}
return true;
}
Set<Integer> parseLineToNumbers(String line) {
Scanner tokens = new Scanner(line).useDelimiter(delimiter);
// LinkedHashSet to preserve the input order and avoid duplicates
Set<Integer> numbers = new LinkedHashSet<>();
while (tokens.hasNext()) {
if (tokens.hasNextInt()) {
numbers.add(tokens.nextInt());
} else {
out.println("Bad number: " + tokens.next());
return null;
}
}
return numbers;
}
public static class Builder<T> {
private final List<T> options;
private String quit = "q";
private String prompt = ": ";
private String header = null;
private String delimiter = "[,\\s]+";
private Function<T, String> formatter = Object::toString;
private String footer = null;
private Supplier<List<T>> onLineIsEmpty = ArrayList::new;
private Supplier<List<T>> onQuit = ArrayList::new;
private Printer out = System.out::print;
private InputStream in = System.in;
/**
* Selector Builder.
*
* @param options the available options, not null and no null elements
*/
public Builder(List<T> options) {
this.options = options;
}
public Builder<T> prompt(String prompt) {
this.prompt = prompt;
return this;
}
public Builder<T> quit(String quit) {
this.quit = quit;
return this;
}
public Builder<T> header(String header) {
this.header = header;
return this;
}
public Builder<T> delimiter(String delimiter) {
this.delimiter = delimiter;
return this;
}
public Builder<T> formatter(Function<T, String> formatter) {
this.formatter = formatter;
return this;
}
public Builder<T> footer(String footer) {
this.footer = footer;
return this;
}
public Builder<T> onLineIsEmpty(Supplier<List<T>> onLineIsEmpty) {
this.onLineIsEmpty = onLineIsEmpty;
return this;
}
public Builder<T> onQuit(Supplier<List<T>> onQuit) {
this.onQuit = onQuit;
return this;
}
public Builder<T> input(InputStream in) {
this.in = in;
return this;
}
public Builder<T> output(PrintStream out) {
this.out = out::print;
return this;
}
public Builder<T> output(Printer out) {
this.out = out;
return this;
}
public Selector<T> build() {
return new Selector<>(
prompt,
quit,
header,
options,
formatter,
footer,
onLineIsEmpty,
onQuit,
delimiter,
out,
in);
}
}
}