2

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;
    }
anonymflaco
  • 123
  • 5

3 Answers3

2

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.

Arvind Kumar Avinash
  • 50,121
  • 5
  • 26
  • 72
  • THANK YOU so much Sir! I didn't consider the labelled break but it's a really efficient way to solve the problem! I see you're expert so I'll bother you with one more question: Let's say that a timeout occurred (so the user didn't input anything and the Scanner didn't read anything); when the same function is called later on in the program, the first input from command line is not read by the Scanner (because Scanner didn't perform the nextInt previously, so I guess it still waits for that), so it takes 2 inputs to work properly. Is there a way to solve this? – anonymflaco May 17 '20 at 14:43
  • Edit: I noticed while debugging that the problem is related to the Thread Pool: apparently when a timeout occurs, that thread is not interrupted so his Scanner keeps waiting for a nextInt – anonymflaco May 17 '20 at 15:25
  • @anonymflaco - A [Scanner](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Scanner.html) is not safe for multithreaded use without external synchronization. Therefore, `Scanner` is not the right tool for this job. I will have to do some research to write a conclusive answer. I suggest you post a new question to attract more attention. I also recommend you check https://stackoverflow.com/questions/9536555/utility-of-future-cancelboolean-method and https://stackoverflow.com/questions/4983065/how-to-interrupt-java-util-scanner-nextline-call before posting the question. – Arvind Kumar Avinash May 17 '20 at 15:32
  • As you suggested me, I changed it to a Buffered Reader and now it works perfectly! can't thank you enough Sir, really appreciate! – anonymflaco May 17 '20 at 21:58
0

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();
       }
    }
Jason
  • 4,590
  • 2
  • 9
  • 18
  • thanks for your answer, but it's not really clear to me how this can solve my problem: in the meanwhile of the 15 seconds timeout, if a wrong input is inserted the user should be able to keep inserting an input until either he gets it right or the 15 seconds pass. Do you know how something like this is achievable? – anonymflaco May 14 '20 at 23:31
0

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);
}
user3892260
  • 851
  • 2
  • 9
  • 17