User input with a timeout in Java

Viewed 1779

I'm trying to build a Command Line Interface with this functionality: if the user takes more than 15 seconds to insert an input (an Integer in this case), the function makes a default choice (0). The code below is what I wrote so far and it works properly.

The problem is that I wanna add a new functionality: if the user writes a wrong number (<0 or >range) the console should print something like ("Wrong choice, you have to pick an integer between 0 - "+ range);

However, while the console prints the message, the timer should still be running and end this loop after 15 seconds, in case the user keeps inserting wrong numbers. In case the user gets eventually a correct number, it should break the loop immediately.

This is my code, but I don't have clear ideas on how to add the functionality because I'm relatively new to Future,Callable and Executor functionalities. If anyone has more experience on it I would be glad to learn!

private int getChoiceWithTimeout(int range){
       Callable<Integer> k = () -> new Scanner(System.in).nextInt();
       Long start= System.currentTimeMillis();
       int choice=0;
       ExecutorService l = Executors.newFixedThreadPool(1);  ;
       Future<Integer> g;
       System.out.println("Enter your choice in 15 seconds :");
       g= l.submit(k);
       while(System.currentTimeMillis()-start<15*1000 && !g.isDone()){
           // Wait for future
       }
       if(g.isDone()){
           try {
               choice=g.get();
           } catch (InterruptedException | ExecutionException e) {
               e.printStackTrace();
           }
       }
       g.cancel(true);
       return choice;
    }
3 Answers

You can do it by using labelled break (done: in the code given below) and a boolean variable (valid in the code given below) to track if the input is valid or not.

import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) {
        // Test
        System.out.println(getChoiceWithTimeout(10));
    }

    static int getChoiceWithTimeout(int range) {
        Callable<Integer> k = () -> new Scanner(System.in).nextInt();
        Long start = System.currentTimeMillis();
        int choice = 0;
        boolean valid;
        ExecutorService l = Executors.newFixedThreadPool(1);
        Future<Integer> g;
        System.out.println("Enter your choice in 15 seconds :");
        g = l.submit(k);
        done: while (System.currentTimeMillis() - start < 15 * 1000) {
            do {
                valid = true;
                if (g.isDone()) {
                    try {
                        choice = g.get();
                        if (choice >= 0 && choice <= range) {
                            break done;
                        } else {
                            throw new IllegalArgumentException();
                        }
                    } catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
                        System.out.println("Wrong choice, you have to pick an integer between 0 - " + range);
                        g = l.submit(k);
                        valid = false;
                    }
                }
            } while (!valid);
        }

        g.cancel(true);
        return choice;
    }
}

A sample run: Do not enter anything and the method will return with 0 after 15 seconds, the way it is doing currently with your code

Enter your choice in 15 seconds :
0

Another sample run: As soon the user enters a valid number, the method will return with the value of the input; otherwise, it will keep asking for the valid input or return 0 after 15 seconds.

Enter your choice in 15 seconds :
a
Wrong choice, you have to pick an integer between 0 - 10
12
Wrong choice, you have to pick an integer between 0 - 10
5
5

Note: Using the labelled break is not mandatory and you can replace it with the traditional way of breaking but that will require you adding a few more lines of code.

Yeah so what you want to do is submit the future and call Future#get() and use the TimeUnit and Long parameters to indicate a threshold to block before giving up on the operation/execution.

It is worth noting that a ThreadPoolExecutor shouldn't be used like this. It should be defined outside of the method and re-used then shutdown on application termination or when no longer needed.

    private int getChoiceWithTimeout(int range){
       Callable<Integer> callable = () -> new Scanner(System.in).nextInt();

       ExecutorService service = Executors.newFixedThreadPool(1);

       System.out.println("Enter your choice in 15 seconds :");

       Future<Integer> inputFuture = service.submit(callable);

       try {
           return inputFuture.get(15, TimeUnit.SECONDS);
       } catch (InterruptedException e) {
           throw new IllegalStateException("Thread was interrupted", e);
       } catch (ExecutionException e) {
           throw new IllegalStateException("Something went wrong", e);
       } catch (TimeoutException e) {
           // tell user they timed out or do something
           throw new IllegalStateException("Timed out! Do something here OP!");
       } finally {
           service.shutdown();
       }
    }

here is a simplified shorter version which times out after n seconds if the user doesn't type something in:

private static final ExecutorService l = Executors.newFixedThreadPool(1);

private static String getUserInputWithTimeout(int timeout) {
    Callable<String> k = () -> new Scanner(System.in).nextLine();
    LocalDateTime start = LocalDateTime.now();
    Future<String> g = l.submit(k);
    while (ChronoUnit.SECONDS.between(start, LocalDateTime.now()) < timeout) {
        if (g.isDone()) {
            try {
                String choice = g.get();
                return choice;
            } catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
                logger.error("ERROR", e);
                g = l.submit(k);
            }
        }
    }
    logger.info("Timeout...");
    g.cancel(true);
    return null;
}

public static void main(String[] args) {
    logger.info("Input the data in 10 seconds or less...");
    String res = getUserInputWithTimeout(10); // 10s until timeout
    if(res != null) {
        logger.info("user response: [" + res + "]");
    }
    System.exit(0);
}
Related