I recently applied for a job as a C# developer. Based on the first phone call, they wanted to check my skills with a code interview in Java. Since I haven't programmed Java for years, I tried my best, and I would like to submit my solution for review (for personal improvement) because I believe a programming language is just a tool a programmer uses to solve the problem. Please also tell me what you think about the documentation, since it is my first time using one of these documentation tools.
The problem was defined as the following (translated from German):
Goal
Implement a Web service Checker.
Functional Requirements
The application is required to be capable of checking the availability of an URL. For that, the user starts the program and inserts a URL of a WWW resource followed by the enter key. After that the program checks every 30 seconds whether the URL is accessible and logs the result continuously to a local log file.
Example Output
15-SEP-2011 14:00:30 Uhr : http://mynode/login.html -> available! 15-SEP-2011 14:01:00 Uhr : http://mynode/login.html -> not available! 15-SEP-2011 14:01:30 Uhr : http://mynode/login.html -> available!
After the insertion of the URL the program shows the following message:
http://mynode/login.html is checked, press any key to terminate the program...
Technical Hints
- Console Application
- Comments if meaningful
- Choice of classes: feel free.
Code:
public class Main {
public static void main(String[] args) {
try (InputReader reader = new InputReader()) {
URL url = reader.readUrl();
WebserviceObserver observer = new WebserviceObserver(url);
observer.observe();
System.out.println(String.format("%s is checked, press any key to terminate the program...", url.toString()));
reader.waitForKeypress();
observer.stopObserving();
} catch (IOException exception) {
System.out.println("It was not possible to start the program.");
}
}
}
public class InputReader implements Closeable {
/**
* The raw terminal for single user keypass which do not require the <enter>-key.
*/
final private Terminal terminal;
InputReader() throws IOException {
this.terminal = getRawTerminal();
}
/**
* Creates a raw terminal which enables non-buffered key inputs.
* This enables the detection of single character inputs without pressing the <code>enter</code>-key.
* <p>
* Hint: For the IntelliJ console, this method returns a {@link org.jline.terminal.impl.DumbTerminal}, which means
* that the terminal has no advanced functionality.
* Therefore, the {@link org.jline.terminal.impl.DumbTerminal} is still buffered and requires the <code>enter</code>-key after single character inputs.
* <p>
* Jansi is required for system compatibility, especially for windows.
*
* @return a {@link Terminal} object with raw mode enables, if terminal creation is successful, otherwise <code>null</code>
*/
private static Terminal getRawTerminal() throws IOException {
Terminal terminal = TerminalBuilder.builder()
.jansi(true)
.system(true)
.build();
terminal.enterRawMode();
return terminal;
}
/**
* Requests the user to input an URL until a valid URL is obtained.
*
* @return a valid URL.
*/
public URL readUrl() {
URL url = null;
// Create a line reader for the raw terminal
LineReader reader = LineReaderBuilder.builder().terminal(terminal).build();
while (url == null) {
System.out.println("Bitte geben sie eine gültige URL ein:");
String urlInput = reader.readLine();
url = validateUrl(urlInput);
}
return url;
}
/**
* Converts the string-based URL into a URL object if the given URL is valid. Otherwise, returns null.
*
* @param urlString the URL to validate.
* @return A URL if the input is a valid URL, otherwise null.
*/
private URL validateUrl(String urlString) {
URL url = null;
try {
url = new URL(urlString);
// if the user types an url like:
// http:example.com, then this will cause a connection error because the host
// is an empty string, but will not cause a MalformedURLException
String host = url.getHost();
if (host.isEmpty()) {
url = null;
}
}
catch (MalformedURLException ignored) {
// MalformedURLException is also throws if http:// or https:// is missing
// in case of an exception, url stays null
}
if(url == null) {
String errorMessage = String.format("The URL %s is invalid.", urlString);
System.out.println(errorMessage);
}
return url;
}
/**
* Blocks the program until the user presses any key.
*/
public void waitForKeypress() {
try {
int notUsed = terminal.reader().read();
} catch (IOException exception) {
System.out.println("Because of an error it was not possible to wait for the user input.");
}
}
@Override
public void close() throws IOException {
this.terminal.close();
}
}
public enum WebserviceStatus {
AVAILABLE("available"),
NOT_AVAILABLE("not available");
/**
* Representation of the enum value as a string
*/
private final String textRepresentation;
WebserviceStatus(String textRepresentation) {
this.textRepresentation = textRepresentation;
}
public String getTextRepresentation() {
return textRepresentation;
}
}
public class WebserviceStatusChecker {
/**
* This is the user agent string for the chrome web browser, required to handle HTTP requests with URLs containing "www".
*
* @see <a href="http://www.useragentstring.com/pages/useragentstring.php">User agents</a>
*/
private static final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36";
/**
* The HTTP request method.
* <p>
* HEAD was chosen over GET because of efficiency.
*
* @see <a href="https://www.w3schools.com/tags/ref_httpmethods.asp">HTTP request methods</a>
*/
private static final String requestMethod = "HEAD";
/**
* The URL to check.
*/
private final URL url;
/**
* The timeout duration for the HTTP request.
*/
private final Duration timeoutDuration;
/**
* Creates a new instance of {@link WebserviceStatusChecker}
*
* @param url the URL which is checked.
*/
WebserviceStatusChecker(URL url) {
this(url, Duration.ofMillis(2000));
}
/**
* Creates a new instance of {@link WebserviceStatusChecker}
*
* @param url the URL to check.
* @param timeoutDuration the timeout duration for the HTTP request.
*/
WebserviceStatusChecker(URL url, Duration timeoutDuration) {
this.url = url;
this.timeoutDuration = timeoutDuration;
}
/**
* Sends a HTTP request to check whether the webservice is available or not.
*
* @return {@link WebserviceStatus#AVAILABLE} if the HTTP request is successful, otherwise {@link WebserviceStatus#NOT_AVAILABLE}
* @see <a href="https://stackoverflow.com/questions/3584210/preferred-java-way-to-ping-an-http-url-for-availability">StackOverflow: Ping a HTTP URL for availability</a>
*/
public WebserviceStatus checkWebserviceStatus() {
WebserviceStatus status = WebserviceStatus.NOT_AVAILABLE;
try {
HttpURLConnection connection = (HttpURLConnection) this.url.openConnection();
int timeoutInMilliseconds = (int) this.timeoutDuration.toMillis();
// Required, otherwise some HTTP requests from URLs containing "www",
// e.g., https://www.google.com/search?q=java are rejected.
// see https://stackoverflow.com/questions/2793150/how-to-use-java-net-urlconnection-to-fire-and-handle-http-requests
// for more information about user agents
connection.setRequestProperty("User-Agent", userAgent);
connection.setConnectTimeout(timeoutInMilliseconds);
connection.setReadTimeout(timeoutInMilliseconds);
connection.setRequestMethod(requestMethod);
int responseCode = connection.getResponseCode();
if (isCorrectHttpStatusCode(responseCode)) {
status = WebserviceStatus.AVAILABLE;
}
} catch (IOException ignored) {
// In the case of any exception the method returns WebserviceStatus.NOT_AVAILABLE
}
return status;
}
/**
* Indicates whether the response code is correct.
*
* @param httResponseCode
* @return <code>true</code> if the code is in the interval [200, 399], otherwise <code>false</code>
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">HTTP response status codes</a>
*/
private boolean isCorrectHttpStatusCode(int httResponseCode) {
return httResponseCode <= 399 && httResponseCode >= HttpURLConnection.HTTP_OK;
}
}
public class WebserviceStatusLogger {
/**
* The path to the file which is written by the {@link WebserviceStatusLogger}.
*/
final String filePath;
/**
* The URL which is logged to the file.
*/
final URL url;
/**
* Creates a new instance of {@link WebserviceStatusLogger}, which writes the {@link WebserviceStatus} of the url.
*
* @param url the url which was checked.
* @param path the path to the log file. If the file exists, the logger appends to the file, otherwise a new file is created.
*/
WebserviceStatusLogger(URL url, String path) {
this.filePath = path;
this.url = url;
}
WebserviceStatusLogger(URL url) {
this(url, "Webservice Availability.log");
}
/**
* Logs the status of the webservice with a timestamp.
*
* @param status Availability of the webservice.
*/
public void writeWebserviceStatus(WebserviceStatus status) {
Date now = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("dd-MMM-yyyy hh:mm:ss");
String dateText = dateFormat.format(now).toUpperCase();
String urlText = url.toString();
String statusText = status.getTextRepresentation();
String content = String.format("%s Uhr : %s -> %s!", dateText, urlText, statusText);
this.write(content);
}
/**
* Writes the content into the log file.
*
* @param content The content which is written to the file.
* @see <a href=" https://stackoverflow.com/questions/1625234/how-to-append-text-to-an-existing-file-in-java">StackOverflow: Append to an existing file</a>
*/
private void write(String content) {
// This implementation closes the fileWriter with every write.
// Therefore, this approach trades performance for the ability to log from multiple processes
// to the same log file, since the file lock is released after every write.
try (FileWriter fileWriter = new FileWriter(this.filePath, true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
PrintWriter printWriter = new PrintWriter(bufferedWriter)) {
printWriter.println(content);
} catch (IOException ioException) {
System.out.println(String.format("An error occurred: %s", ioException.toString()));
}
}
}
/**
* Hint: First this was implemented with a {@link ScheduledExecutorService};
* however, the solution was highly unreliable regarding the timespan between thread spawns.
* This problem was only detected on Windows.
*/
public class PeriodicRunner implements Runnable {
/**
* Specifies the interval in which the {@link WebserviceStatus} is checked an logged.
*/
final private Duration interval;
/**
* The method which is run periodically
*/
final private Consumer<Void> runnerFunction;
PeriodicRunner(Consumer<Void> runnerFunction, Duration interval) {
this.runnerFunction = runnerFunction;
this.interval = interval;
}
PeriodicRunner(Consumer<Void> runnerFunction) {
this(runnerFunction, Duration.ofSeconds(30));
}
/**
* Runs the function in the interval by measuring the time consumed to execute the function.
* Thread execution stops when the thread is interrupted by the main thread.
*/
@Override
public void run() {
boolean isInterrupted = false;
while (!isInterrupted) {
long startTime = System.nanoTime();
this.runnerFunction.accept(null);
long endTime = System.nanoTime();
Duration executionDuration = Duration.ofNanos(endTime - startTime);
Duration duration = this.interval.minus(executionDuration);
isInterrupted = this.sleepUntilEndOfDurationOrInterrupted(duration);
}
}
/**
* Sets the thread sleeping until it is woken up by the scheduler or by an interrupt.
*
* @param duration the sleep duration.
* @return <code>true</code> if the thread was interrupted while sleeping, otherwise <code>false</code>.
*/
boolean sleepUntilEndOfDurationOrInterrupted(Duration duration) {
boolean isInterrupted = false;
try {
Thread.sleep(duration.toMillis());
} catch (InterruptedException interrupt) {
// Thread was interrupted for termination.
isInterrupted = true;
}
return isInterrupted;
}
}
public class WebserviceObserver {
/**
* The logger which writes the {@link WebserviceStatus}.
*/
final WebserviceStatusLogger logger;
/**
* The {@link WebserviceStatusChecker} which obtains the {@link WebserviceStatus} based on the URL.
*/
final WebserviceStatusChecker checker;
/**
* The thread which is responsive for executing the periodic {@link WebserviceStatus} check and logging.
*/
final Thread thread;
WebserviceObserver(URL url) {
this.logger = new WebserviceStatusLogger(url);
this.checker = new WebserviceStatusChecker(url);
Runnable periodicRunner = new PeriodicRunner((Void notUsed) -> this.logWebserviceStatus());
this.thread = new Thread(periodicRunner);
}
/**
* Starts the thread.
*/
public void observe() {
this.thread.start();
}
/**
* Stops the thread.
*/
public void stopObserving() {
this.thread.interrupt();
}
/**
* Obtains the webservice status and writes the measured state to the log file.
*/
private void logWebserviceStatus() {
WebserviceStatus status = this.checker.checkWebserviceStatus();
// It is unlikely that the output in the log file will be the exact specified interval, because the execution of
// the webservice status takes additional time.
// Only after the check is finished, the result is logged.
// The expectation is that the additional time cannot exceed the timeout duration of the Checker.
this.logger.writeWebserviceStatus(status);
}
}