/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2009 Sun Microsystems, Inc.
* Portions copyright 2011-2014 ForgeRock AS
* Portions copyright 2011 Nemanja Lukić
*/
package com.forgerock.opendj.cli;
import static com.forgerock.opendj.cli.CliMessages.*;
import static com.forgerock.opendj.cli.Utils.MAX_LINE_WIDTH;
import static com.forgerock.opendj.cli.Utils.CONFIRMATION_MAX_TRIES;
import static com.forgerock.opendj.cli.Utils.wrapText;
import static com.forgerock.opendj.util.StaticUtils.EOL;
import java.io.BufferedReader;
import java.io.Console;
import java.io.EOFException;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
/**
* This class provides an abstract base class which can be used as the basis of a console-based application.
*/
public abstract class ConsoleApplication {
private final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private final InputStream in = System.in;
private final PrintStream out;
private final PrintStream err;
private final Console console = System.console();
/**
* Defines the different line styles for output.
*/
public enum Style {
/**
* Defines a title.
*/
TITLE,
/**
* Defines a subtitle.
*/
SUBTITLE,
/**
* Defines a notice.
*/
NOTICE,
/**
* Defines a normal line.
*/
NORMAL,
/**
* Defines an error.
*/
ERROR,
/**
* Defines a warning.
*/
WARNING
}
/**
* Creates a new console application instance.
*/
public ConsoleApplication() {
this(System.out, System.err);
}
/**
* Creates a new console application instance with provided standard and error out streams.
*
* @param out
* The output stream.
* @param err
* The error stream.
*/
public ConsoleApplication(PrintStream out, PrintStream err) {
this.out = out;
this.err = err;
}
/**
* Returns the application error stream.
*
* @return The application error stream.
*/
public final PrintStream getErrorStream() {
return err;
}
/**
* Returns the application input stream.
*
* @return The application input stream.
*/
public final InputStream getInputStream() {
return in;
}
/**
* Returns the application output stream.
*
* @return The application output stream.
*/
public final PrintStream getOutputStream() {
return out;
}
/**
* Indicates whether or not the user has requested interactive behavior. The default implementation returns
* {@code true}.
*
* @return {@code true} if the user has requested interactive behavior.
*/
public boolean isInteractive() {
return true;
}
/**
* Indicates whether or not the user has requested quiet output. The default implementation returns {@code false}.
*
* @return {@code true} if the user has requested quiet output.
*/
public boolean isQuiet() {
return false;
}
/**
* Indicates whether or not the user has requested script-friendly output. The default implementation returns
* {@code false}.
*
* @return {@code true} if the user has requested script-friendly output.
*/
public boolean isScriptFriendly() {
return false;
}
/**
* Indicates whether or not the user has requested verbose output. The default implementation returns {@code false}.
*
* @return {@code true} if the user has requested verbose output.
*/
public boolean isVerbose() {
return false;
}
/**
* Indicates whether or not the user has requested advanced mode.
*
* @return Returns true if the user has requested advanced mode.
*/
public boolean isAdvancedMode() {
return false;
}
/**
* Indicates whether or not this console application is running in its menu-driven mode. This can be used to dictate
* whether output should go to the error stream or not. In addition, it may also dictate whether or not sub-menus
* should display a cancel option as well as a quit option.
*
* @return Returns true if this console application is running in its menu-driven mode.
*/
public boolean isMenuDrivenMode() {
return false;
}
/**
* Interactively prompts the user to press return to continue. This method should be called in situations where a
* user needs to be given a chance to read some documentation before continuing (continuing may cause the
* documentation to be scrolled out of view).
*/
public final void pressReturnToContinue() {
final LocalizableMessage msg = INFO_MENU_PROMPT_RETURN_TO_CONTINUE.get();
try {
readLineOfInput(msg);
} catch (final ClientException e) {
// Ignore the exception - applications don't care.
}
}
/**
* Displays a message to the error stream.
*
* @param msg
* The message.
*/
public final void errPrint(final LocalizableMessage msg) {
getErrStream().print(wrap(msg));
}
/**
* Displays a blank line to the error stream.
*/
public final void errPrintln() {
getErrStream().println();
}
/**
* Displays a message to the error stream.
*
* @param msg
* The message.
*/
public final void errPrintln(final LocalizableMessage msg) {
getErrStream().println(wrap(msg));
}
/**
* Displays a message to the error stream indented by the specified number of columns.
*
* @param msg
* The message.
* @param indent
* The number of columns to indent.
*/
public final void errPrintln(final LocalizableMessage msg, final int indent) {
getErrStream().println(wrapText(msg, MAX_LINE_WIDTH, indent));
}
/**
* Displays a message to the error stream if verbose mode is enabled.
*
* @param msg
* The verbose message.
*/
public final void errPrintVerboseMessage(final LocalizableMessage msg) {
if (isVerbose()) {
getErrStream().println(wrap(msg));
}
}
/**
* Displays a message to the output stream.
*
* @param msg
* The message.
*/
public final void print(final LocalizableMessage msg) {
if (!isQuiet()) {
out.print(wrap(msg));
}
}
/**
* Displays a blank line to the output stream.
*/
public final void println() {
if (!isQuiet()) {
out.println();
}
}
/**
* Displays a message to the output stream.
*
* @param msg
* The message.
*/
public final void println(final LocalizableMessage msg) {
if (!isQuiet()) {
out.println(wrap(msg));
}
}
/**
* Displays a message to the output stream indented by the specified number of columns.
*
* @param msg
* The message.
* @param indent
* The number of columns to indent.
*/
public final void println(final LocalizableMessage msg, final int indent) {
if (!isQuiet()) {
out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
}
}
/**
* Prints a progress bar on the same output stream line if not in quiet mode.
*
*
* Like
* msg...... 50%
* if progress is up to 100 :
* msg..................... 100%
* if progress is < 0 :
* msg.... FAIL
* msg..................... FAIL
*
*
* @param linePos
* The progress bar starts at this position on the line.
* @param progress
* The current percentage progress to print.
*/
public final void printProgressBar(final int linePos, final int progress) {
if (!isQuiet()) {
final int spacesLeft = MAX_LINE_WIDTH - linePos - 10;
StringBuilder bar = new StringBuilder();
if (progress != 0) {
for (int i = 0; i < 50; i++) {
if ((i < (Math.abs(progress) / 2)) && (bar.length() < spacesLeft)) {
bar.append(".");
}
}
}
bar.append(". ");
if (progress >= 0) {
bar.append(progress).append("% ");
} else {
bar.append("FAIL");
}
final int endBuilder = linePos + bar.length();
for (int i = 0; i < endBuilder; i++) {
bar.append("\b");
}
if (progress >= 100 || progress < 0) {
bar.append(EOL);
}
out.print(bar.toString());
}
}
/**
* Print a line with EOL in the output stream.
*
* @param msgStyle
* The type of formatted output desired.
* @param msg
* The message to display in normal mode.
* @param indent
* The indentation.
*/
public final void println(final Style msgStyle, final LocalizableMessage msg, final int indent) {
if (!isQuiet()) {
switch (msgStyle) {
case TITLE:
out.println();
out.println(">>>> " + wrapText(msg, MAX_LINE_WIDTH, indent));
out.println();
break;
case SUBTITLE:
out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
out.println();
break;
case NOTICE:
out.println(wrapText("* " + msg, MAX_LINE_WIDTH, indent));
break;
case ERROR:
out.println();
out.println(wrapText("** " + msg, MAX_LINE_WIDTH, indent));
out.println();
break;
case WARNING:
out.println(wrapText("[!] " + msg, MAX_LINE_WIDTH, indent));
break;
default:
out.println(wrapText(msg, MAX_LINE_WIDTH, indent));
break;
}
}
}
/**
* Displays a message to the output stream if verbose mode is enabled.
*
* @param msg
* The verbose message.
*/
public final void printVerboseMessage(final LocalizableMessage msg) {
if (isVerbose()) {
out.println(wrap(msg));
}
}
/**
* Interactively prompts (on error output) the user to provide a string value. Any non-empty string will be allowed
* (the empty string will indicate that the default should be used, if there is one).
*
* @param prompt
* The prompt to present to the user.
* @param defaultValue
* The default value to assume if the user presses ENTER without typing anything, or {@code null} if
* there should not be a default and the user must explicitly provide a value.
* @throws ClientException
* If the line of input could not be retrieved for some reason.
* @return The string value read from the user.
*/
public final String readInput(LocalizableMessage prompt, final String defaultValue) throws ClientException {
return readInput(prompt, defaultValue, null);
}
/**
* Interactively prompts (on error output) the user to provide a string value. Any non-empty string will be allowed
* (the empty string will indicate that the default should be used, if there is one).
*
* @param prompt
* The prompt to present to the user.
* @param defaultValue
* The default value to assume if the user presses ENTER without typing anything, or {@code null} if
* there should not be a default and the user must explicitly provide a value.
* @param msgStyle
* The formatted style chosen.
* @throws ClientException
* If the line of input could not be retrieved for some reason.
* @return The string value read from the user.
*/
public final String readInput(LocalizableMessage prompt, final String defaultValue, final Style msgStyle)
throws ClientException {
if (msgStyle != null && msgStyle == Style.TITLE) {
println();
}
while (true) {
if (defaultValue != null) {
prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt.toString(), defaultValue);
}
final String response = readLineOfInput(prompt);
if (msgStyle != null && (msgStyle == Style.TITLE || msgStyle == Style.SUBTITLE)) {
println();
}
if ("".equals(response)) {
if (defaultValue == null) {
print(INFO_ERROR_EMPTY_RESPONSE.get());
} else {
return defaultValue;
}
} else {
return response;
}
}
}
/**
* Interactively reads a password from the console.
*
* @param prompt
* The password prompt.
* @return The password.
* @throws ClientException
* If the password could not be retrieved for some reason.
*/
public final char[] readPassword(final LocalizableMessage prompt) throws ClientException {
if (console != null) {
if (prompt != null) {
out.print(wrap(prompt));
out.print(" ");
}
try {
final char[] password = console.readPassword();
if (password == null) {
throw new EOFException("End of input");
}
return password;
} catch (final Throwable e) {
throw ClientException.adaptInputException(e);
}
} else {
// FIXME: should go direct to char[] and avoid the String.
return readLineOfInput(prompt).toCharArray();
}
}
/**
* Reads a password from the console without echoing it to the client.
* FIXME This method should disappear when all
* the tools will extend to ConsoleApplication.
*
* @return The password as an array of characters.
* @throws ClientException
* If an error occurs when reading the password.
*/
public static final char[] readPassword() throws ClientException {
try {
char[] password = System.console().readPassword();
if (password != null) {
return password;
}
} catch (IOError e) {
throw ClientException.adaptInputException(e);
}
return null;
}
/**
* Interactively retrieves a line of input from the console.
*
* @param prompt
* The prompt.
* @return The line of input.
* @throws ClientException
* If the line of input could not be retrieved for some reason.
*/
public final String readLineOfInput(final LocalizableMessage prompt) throws ClientException {
if (prompt != null) {
out.print(wrap(prompt));
out.print(" ");
}
try {
final String s = reader.readLine();
if (s == null) {
throw ClientException.adaptInputException(new EOFException("End of input"));
} else {
return s;
}
} catch (final IOException e) {
throw ClientException.adaptInputException(e);
}
}
/**
* Interactively retrieves a port value from the console.
*
* @param prompt
* The port prompt.
* @param defaultValue
* The port default value.
* @return Returns the port.
* @throws ClientException
* If the port could not be retrieved for some reason.
*/
public final int readPort(LocalizableMessage prompt, final int defaultValue) throws ClientException {
final ValidationCallbacktrue if the user wishes the action to be performed, or false if they
* refused.
* @throws ClientException
* if the user did not provide valid answer after a certain number of tries
* (ConsoleApplication.CONFIRMATION_MAX_TRIES)
*/
protected final boolean askConfirmation(LocalizableMessage prompt, boolean defaultValue, LocalizedLogger logger)
throws ClientException {
boolean v = defaultValue;
boolean done = false;
int nTries = 0;
while (!done && (nTries < CONFIRMATION_MAX_TRIES)) {
nTries++;
try {
v = confirmAction(prompt, defaultValue);
done = true;
} catch (ClientException ce) {
if (ce.getMessageObject().toString().contains(ERR_CONFIRMATION_TRIES_LIMIT_REACHED.get(nTries))) {
throw ce;
}
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
// Try again...
println();
}
}
if (!done) {
// This means we reached the maximum number of tries
throw new ClientException(ReturnCode.ERROR_USER_DATA,
ERR_CONFIRMATION_TRIES_LIMIT_REACHED.get(CONFIRMATION_MAX_TRIES));
}
return v;
}
/**
* Interactively confirms whether a user wishes to perform an action.
* If the application is non-interactive, then the provided default is returned automatically.
*
* @param prompt
* The prompt describing the action.
* @param defaultValue
* The default value for the confirmation message. This will be returned if the application is
* non-interactive or if the user just presses return.
* @return Returns true if the user wishes the action to be performed, or false if they
* refused, or if an exception occurred.
* @throws ClientException
* If the user's response could not be read from the console for some reason.
*/
public final boolean confirmAction(LocalizableMessage prompt, final boolean defaultValue) throws ClientException {
if (!isInteractive()) {
return defaultValue;
}
final LocalizableMessage yes = INFO_GENERAL_YES.get();
final LocalizableMessage no = INFO_GENERAL_NO.get();
final LocalizableMessage errMsg = ERR_CONSOLE_APP_CONFIRM.get(yes, no);
prompt = INFO_MENU_PROMPT_CONFIRM.get(prompt, yes, no, defaultValue ? yes : no);
ValidationCallback