/*
|
* 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-2010 Sun Microsystems, Inc.
|
* Portions Copyright 2012-2014 ForgeRock AS
|
*/
|
package org.opends.server.util.cli;
|
|
import static org.opends.messages.AdminToolMessages.*;
|
import static org.opends.messages.DSConfigMessages.*;
|
import static org.opends.messages.QuickSetupMessages.*;
|
import static org.opends.messages.UtilityMessages.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.io.BufferedReader;
|
import java.io.EOFException;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.InputStreamReader;
|
import java.io.OutputStream;
|
import java.io.PrintStream;
|
import java.io.Reader;
|
import java.text.SimpleDateFormat;
|
import java.util.Date;
|
import java.util.TimeZone;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
|
import javax.naming.NamingException;
|
import javax.naming.NoPermissionException;
|
import javax.naming.ldap.InitialLdapContext;
|
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLHandshakeException;
|
import javax.net.ssl.TrustManager;
|
|
import org.opends.admin.ads.ServerDescriptor;
|
import org.opends.admin.ads.util.ApplicationTrustManager;
|
import org.opends.admin.ads.util.ConnectionUtils;
|
import org.opends.admin.ads.util.OpendsCertificateException;
|
import org.opends.quicksetup.util.Utils;
|
import org.opends.server.protocols.ldap.LDAPResultCode;
|
import org.opends.server.tools.ClientException;
|
import org.opends.server.types.NullOutputStream;
|
import org.opends.server.util.PasswordReader;
|
import org.opends.server.util.SetupUtils;
|
import org.opends.server.util.StaticUtils;
|
|
|
/**
|
* This class provides an abstract base class which can be used as the basis of
|
* a console-based application.
|
*/
|
public abstract class ConsoleApplication
|
{
|
|
/**
|
* A null reader.
|
*/
|
private static final class NullReader extends Reader
|
{
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close() throws IOException
|
{
|
// Do nothing.
|
}
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public int read(char[] cbuf, int off, int len) throws IOException
|
{
|
return -1;
|
}
|
}
|
|
/**
|
* 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
|
}
|
|
// The error stream which this application should use.
|
private final PrintStream err;
|
|
// The input stream reader which this application should use.
|
private final BufferedReader in;
|
|
// The output stream which this application should use.
|
private final PrintStream out;
|
|
/**
|
* The maximum number of times we try to confirm.
|
*/
|
protected final static int CONFIRMATION_MAX_TRIES = 5;
|
|
private static final String COMMENT_SHELL_UNIX = "# ";
|
private static final String COMMENT_BATCH_WINDOWS = "rem ";
|
|
/**
|
* The String used to write comments in a shell (or batch) script.
|
*/
|
protected static final String SHELL_COMMENT_SEPARATOR = SetupUtils
|
.isWindows() ? COMMENT_BATCH_WINDOWS : COMMENT_SHELL_UNIX;
|
|
/**
|
* Creates a new console application instance.
|
*
|
* @param in
|
* The application input stream.
|
* @param out
|
* The application output stream.
|
* @param err
|
* The application error stream.
|
*/
|
protected ConsoleApplication(BufferedReader in, PrintStream out,
|
PrintStream err)
|
{
|
if (in != null)
|
{
|
this.in = in;
|
}
|
else
|
{
|
this.in = new BufferedReader(new NullReader());
|
}
|
|
this.out = NullOutputStream.wrapOrNullStream(out);
|
this.err = NullOutputStream.wrapOrNullStream(err);
|
}
|
|
/**
|
* Creates a new console application instance.
|
*
|
* @param in
|
* The application input stream.
|
* @param out
|
* The application output stream.
|
* @param err
|
* The application error stream.
|
*/
|
protected ConsoleApplication(InputStream in, OutputStream out,
|
OutputStream err)
|
{
|
if (in != null)
|
{
|
this.in = new BufferedReader(new InputStreamReader(in));
|
}
|
else
|
{
|
this.in = new BufferedReader(new NullReader());
|
}
|
|
this.out = NullOutputStream.wrapOrNullStream(out);
|
this.err = NullOutputStream.wrapOrNullStream(err);
|
}
|
|
/**
|
* 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 <code>true</code> if the user wishes the action to be
|
* performed, or <code>false</code> if they refused, or if an
|
* exception occurred.
|
* @throws CLIException
|
* 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 CLIException
|
{
|
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<Boolean> validator = new ValidationCallback<Boolean>()
|
{
|
|
@Override
|
public Boolean validate(ConsoleApplication app, String input)
|
{
|
String ninput = input.toLowerCase().trim();
|
if (ninput.length() == 0)
|
{
|
return defaultValue;
|
}
|
else if (no.toString().toLowerCase().startsWith(ninput))
|
{
|
return false;
|
}
|
else if (yes.toString().toLowerCase().startsWith(ninput))
|
{
|
return true;
|
}
|
else
|
{
|
// Try again...
|
app.println();
|
app.println(errMsg);
|
app.println();
|
}
|
|
return null;
|
}
|
};
|
|
return readValidatedInput(prompt, validator, CONFIRMATION_MAX_TRIES);
|
}
|
|
/**
|
* Gets the application error stream.
|
*
|
* @return Returns the application error stream.
|
*/
|
public final PrintStream getErrorStream()
|
{
|
return err;
|
}
|
|
/**
|
* Gets the application input stream.
|
*
|
* @return Returns the application input stream.
|
*/
|
public final BufferedReader getInputStream()
|
{
|
return in;
|
}
|
|
/**
|
* Gets the application output stream.
|
*
|
* @return Returns the application output stream.
|
*/
|
public final PrintStream getOutputStream()
|
{
|
return out;
|
}
|
|
/**
|
* Indicates whether or not the user has requested advanced mode.
|
*
|
* @return Returns <code>true</code> if the user has requested advanced mode.
|
*/
|
public abstract boolean isAdvancedMode();
|
|
/**
|
* Indicates whether or not the user has requested interactive behavior.
|
*
|
* @return Returns <code>true</code> if the user has requested interactive
|
* behavior.
|
*/
|
public abstract boolean isInteractive();
|
|
/**
|
* 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 <code>true</code> if this console application is running in
|
* its menu-driven mode.
|
*/
|
public abstract boolean isMenuDrivenMode();
|
|
/**
|
* Indicates whether or not the user has requested quiet output.
|
*
|
* @return Returns <code>true</code> if the user has requested quiet output.
|
*/
|
public abstract boolean isQuiet();
|
|
/**
|
* Indicates whether or not the user has requested script-friendly output.
|
*
|
* @return Returns <code>true</code> if the user has requested script-friendly
|
* output.
|
*/
|
public abstract boolean isScriptFriendly();
|
|
/**
|
* Indicates whether or not the user has requested verbose output.
|
*
|
* @return Returns <code>true</code> if the user has requested verbose output.
|
*/
|
public abstract boolean isVerbose();
|
|
/**
|
* 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()
|
{
|
LocalizableMessage msg = INFO_MENU_PROMPT_RETURN_TO_CONTINUE.get();
|
try
|
{
|
readLineOfInput(msg);
|
}
|
catch (CLIException e)
|
{
|
// Ignore the exception - applications don't care.
|
}
|
}
|
|
/**
|
* Displays a blank line to the error stream.
|
*/
|
public final void println()
|
{
|
err.println();
|
}
|
|
/**
|
* Displays a message to the error stream.
|
*
|
* @param msg
|
* The message.
|
*/
|
public final void println(LocalizableMessage msg)
|
{
|
err.println(wrapText(msg, MAX_LINE_WIDTH));
|
}
|
|
/**
|
* Displays a message to the error stream.
|
*
|
* @param msg
|
* The message.
|
*/
|
public final void print(LocalizableMessage msg)
|
{
|
err.print(wrapText(msg, MAX_LINE_WIDTH));
|
}
|
|
/**
|
* Print a line with EOL in the output stream.
|
*
|
* @param msg
|
* The message to display in normal mode.
|
* @param indent
|
* The indentation.
|
*/
|
public final void println(final LocalizableMessage msg, final int indent)
|
{
|
println(Style.NORMAL, msg, indent);
|
}
|
|
/**
|
* 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 blank line to the output stream if we are not in quiet mode.
|
*/
|
public final void printlnProgress()
|
{
|
if (!isQuiet())
|
{
|
out.println();
|
}
|
}
|
|
/**
|
* Displays a message to the output stream if we are not in quiet mode.
|
* LocalizableMessage is wrap to max line width.
|
*
|
* @param msg
|
* The message.
|
*/
|
public final void printlnProgress(LocalizableMessage msg)
|
{
|
if (!isQuiet())
|
{
|
out.println(wrapText(msg, MAX_LINE_WIDTH));
|
}
|
}
|
|
/**
|
* Displays a message to the output stream if we are not in quiet mode.
|
*
|
* @param msg
|
* The message.
|
*/
|
public final void printProgress(final LocalizableMessage msg)
|
{
|
if (!isQuiet())
|
{
|
out.print(msg);
|
}
|
}
|
|
/**
|
* Prints a progress bar on the same output stream line if not in quiet mode.
|
*
|
* <pre>
|
* Like
|
* msg...... 50%
|
* if progress is up to 100 :
|
* msg..................... 100%
|
* if progress is < 0 :
|
* msg.... FAIL
|
* msg..................... FAIL
|
* </pre>
|
*
|
* @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());
|
}
|
}
|
|
/**
|
* Display the batch progress string to the error stream, if we are not in
|
* quiet mode.
|
*
|
* @param s
|
* The string to display
|
*/
|
public final void printlnBatchProgress(String s)
|
{
|
if (!isQuiet())
|
{
|
err.println(s);
|
}
|
}
|
|
/**
|
* 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 printErrln(LocalizableMessage msg, int indent)
|
{
|
err.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 printVerboseMessage(LocalizableMessage msg)
|
{
|
if (isVerbose() || isInteractive())
|
{
|
err.println(wrapText(msg, MAX_LINE_WIDTH));
|
}
|
}
|
|
/**
|
* Interactively retrieves a line of input from the console.
|
*
|
* @param prompt
|
* The prompt.
|
* @return Returns the line of input, or <code>null</code> if the end of input
|
* has been reached.
|
* @throws CLIException
|
* If the line of input could not be retrieved for some reason.
|
*/
|
public final String readLineOfInput(LocalizableMessage prompt) throws CLIException
|
{
|
if (prompt != null)
|
{
|
err.print(wrapText(prompt, MAX_LINE_WIDTH));
|
err.print(" ");
|
}
|
try
|
{
|
String s = in.readLine();
|
if (s == null)
|
{
|
throw CLIException
|
.adaptInputException(new EOFException("End of input"));
|
}
|
else
|
{
|
return s;
|
}
|
}
|
catch (IOException e)
|
{
|
throw CLIException.adaptInputException(e);
|
}
|
}
|
|
/**
|
* Displays a message and read the user's input from output.
|
*
|
* @param prompt
|
* The message to display.
|
* @param defaultValue
|
* The default answer by default.
|
* @param msgStyle
|
* The formatted style chosen.
|
* @return The user's input as a string.
|
* @throws CLIException
|
* If an Exception occurs during the process.
|
*/
|
public final String readInput(final LocalizableMessage prompt,
|
final String defaultValue, final Style msgStyle)
|
throws CLIException
|
{
|
String answer = null;
|
if (msgStyle == Style.TITLE)
|
{
|
println();
|
}
|
print(INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue));
|
out.print(" ");
|
|
try
|
{
|
// Reads the user input.
|
answer = in.readLine();
|
}
|
catch (IOException e)
|
{
|
throw CLIException.adaptInputException(e);
|
}
|
|
if (msgStyle == Style.TITLE
|
|| msgStyle == Style.SUBTITLE)
|
{
|
println();
|
}
|
|
if ("".equals(answer))
|
{
|
if (defaultValue == null)
|
{
|
println(INFO_ERROR_EMPTY_RESPONSE.get());
|
}
|
else
|
{
|
return defaultValue;
|
}
|
}
|
return answer;
|
}
|
|
/**
|
* Commodity method that 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</CODE> if there should not be a
|
* default and the user must explicitly provide a value.
|
* @throws CLIException
|
* If the line of input could not be retrieved for some reason.
|
* @return The string value read from the user.
|
*/
|
public String readInput(LocalizableMessage prompt, String defaultValue)
|
throws CLIException
|
{
|
while (true)
|
{
|
if (defaultValue != null)
|
{
|
prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
|
}
|
String response = readLineOfInput(prompt);
|
|
if ("".equals(response))
|
{
|
if (defaultValue == null)
|
{
|
println(INFO_ERROR_EMPTY_RESPONSE.get());
|
}
|
else
|
{
|
return defaultValue;
|
}
|
}
|
else
|
{
|
return response;
|
}
|
}
|
}
|
|
/**
|
* Commodity method that 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). If
|
* an error occurs a message will be logged to the provided logger.
|
*
|
* @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</CODE> if there should not be a
|
* default and the user must explicitly provide a value.
|
* @param logger
|
* the Logger to be used to log the error message.
|
* @return The string value read from the user.
|
*/
|
public String readInput(LocalizableMessage prompt, String defaultValue, LocalizedLogger logger)
|
{
|
String s = defaultValue;
|
try
|
{
|
s = readInput(prompt, defaultValue);
|
}
|
catch (CLIException ce)
|
{
|
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
|
}
|
return s;
|
}
|
|
/**
|
* Interactively retrieves a password from the console.
|
*
|
* @param prompt
|
* The password prompt.
|
* @return Returns the password.
|
* @throws CLIException
|
* If the password could not be retrieved for some reason.
|
*/
|
public final String readPassword(LocalizableMessage prompt) throws CLIException
|
{
|
err.print(wrapText(prompt + " ", MAX_LINE_WIDTH));
|
char[] pwChars;
|
try
|
{
|
pwChars = PasswordReader.readPassword();
|
}
|
catch (Exception e)
|
{
|
throw CLIException.adaptInputException(e);
|
}
|
return new String(pwChars);
|
}
|
|
/**
|
* Commodity method that interactively retrieves a password from the console.
|
* If there is an error an error message is logged to the provided Logger and
|
* <CODE>null</CODE> is returned.
|
*
|
* @param prompt
|
* The password prompt.
|
* @param logger
|
* the Logger to be used to log the error message.
|
* @return Returns the password.
|
*/
|
protected final String readPassword(LocalizableMessage prompt, LocalizedLogger logger)
|
{
|
String pwd = null;
|
try
|
{
|
pwd = readPassword(prompt);
|
}
|
catch (CLIException ce)
|
{
|
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
|
}
|
return pwd;
|
}
|
|
/**
|
* Interactively retrieves a port value from the console.
|
*
|
* @param prompt
|
* The port prompt.
|
* @param defaultValue
|
* The port default value.
|
* @return Returns the port.
|
* @throws CLIException
|
* If the port could not be retrieved for some reason.
|
*/
|
public final int readPort(LocalizableMessage prompt, final int defaultValue)
|
throws CLIException
|
{
|
ValidationCallback<Integer> callback = new ValidationCallback<Integer>()
|
{
|
@Override
|
public Integer validate(ConsoleApplication app, String input)
|
throws CLIException
|
{
|
String ninput = input.trim();
|
if (ninput.length() == 0)
|
{
|
return defaultValue;
|
}
|
else
|
{
|
try
|
{
|
int i = Integer.parseInt(ninput);
|
if (i < 1 || i > 65535)
|
{
|
throw new NumberFormatException();
|
}
|
return i;
|
}
|
catch (NumberFormatException e)
|
{
|
// Try again...
|
app.println();
|
app.println(ERR_LDAP_CONN_BAD_PORT_NUMBER.get(ninput));
|
app.println();
|
return null;
|
}
|
}
|
}
|
|
};
|
|
if (defaultValue != -1)
|
{
|
prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
|
}
|
|
return readValidatedInput(prompt, callback, CONFIRMATION_MAX_TRIES);
|
}
|
|
/**
|
* Returns a message object for the given NamingException.
|
*
|
* @param ne
|
* the NamingException.
|
* @param hostPort
|
* the hostPort representation of the server we were contacting when
|
* the NamingException occurred.
|
* @return a message object for the given NamingException.
|
*/
|
protected LocalizableMessage getMessageForException(NamingException ne, String hostPort)
|
{
|
return Utils.getMessageForException(ne, hostPort);
|
}
|
|
/**
|
* Commodity method used to repeatidly ask the user to provide a port value.
|
*
|
* @param prompt
|
* the prompt message.
|
* @param defaultValue
|
* the default value of the port to be proposed to the user.
|
* @param logger
|
* the logger where the errors will be written.
|
* @return the port value provided by the user.
|
*/
|
protected int askPort(LocalizableMessage prompt, int defaultValue, LocalizedLogger logger)
|
{
|
int port = -1;
|
while (port == -1)
|
{
|
try
|
{
|
port = readPort(prompt, defaultValue);
|
}
|
catch (CLIException ce)
|
{
|
port = -1;
|
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
|
}
|
}
|
return port;
|
}
|
|
/**
|
* Interactively prompts for user input and continues until valid input is
|
* provided.
|
*
|
* @param <T>
|
* The type of decoded user input.
|
* @param prompt
|
* The interactive prompt which should be displayed on each input
|
* attempt.
|
* @param validator
|
* An input validator responsible for validating and decoding the
|
* user's response.
|
* @return Returns the decoded user's response.
|
* @throws CLIException
|
* If an unexpected error occurred which prevented validation.
|
*/
|
public final <T> T readValidatedInput(LocalizableMessage prompt,
|
ValidationCallback<T> validator) throws CLIException
|
{
|
while (true)
|
{
|
String response = readLineOfInput(prompt);
|
T value = validator.validate(this, response);
|
if (value != null)
|
{
|
return value;
|
}
|
}
|
}
|
|
/**
|
* Interactively prompts for user input and continues until valid input is
|
* provided.
|
*
|
* @param <T>
|
* The type of decoded user input.
|
* @param prompt
|
* The interactive prompt which should be displayed on each input
|
* attempt.
|
* @param validator
|
* An input validator responsible for validating and decoding the
|
* user's response.
|
* @param maxTries
|
* The maximum number of tries that we can make.
|
* @return Returns the decoded user's response.
|
* @throws CLIException
|
* If an unexpected error occurred which prevented validation or if
|
* the maximum number of tries was reached.
|
*/
|
public final <T> T readValidatedInput(LocalizableMessage prompt,
|
ValidationCallback<T> validator, int maxTries) throws CLIException
|
{
|
int nTries = 0;
|
while (nTries < maxTries)
|
{
|
String response = readLineOfInput(prompt);
|
T value = validator.validate(this, response);
|
if (value != null)
|
{
|
return value;
|
}
|
nTries++;
|
}
|
throw new CLIException(ERR_TRIES_LIMIT_REACHED.get(maxTries));
|
}
|
|
/**
|
* Commodity method that interactively confirms whether a user wishes to
|
* perform an action. If the application is non-interactive, then the provided
|
* default is returned automatically. If there is an error an error message is
|
* logged to the provided Logger and the defaul value is returned.
|
*
|
* @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.
|
* @param logger
|
* the Logger to be used to log the error message.
|
* @return Returns <code>true</code> if the user wishes the action to be
|
* performed, or <code>false</code> if they refused.
|
* @throws CLIException
|
* 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 CLIException
|
{
|
boolean v = defaultValue;
|
|
boolean done = false;
|
int nTries = 0;
|
|
while (!done && (nTries < CONFIRMATION_MAX_TRIES))
|
{
|
nTries++;
|
try
|
{
|
v = confirmAction(prompt, defaultValue);
|
done = true;
|
}
|
catch (CLIException ce)
|
{
|
if (StaticUtils.hasDescriptor(ce.getMessageObject(),
|
ERR_CONFIRMATION_TRIES_LIMIT_REACHED)
|
|| StaticUtils.hasDescriptor(ce.getMessageObject(),
|
ERR_TRIES_LIMIT_REACHED))
|
{
|
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 CLIException(ERR_CONFIRMATION_TRIES_LIMIT_REACHED
|
.get(CONFIRMATION_MAX_TRIES));
|
}
|
return v;
|
}
|
|
/**
|
* Returns an InitialLdapContext using the provided parameters. We try to
|
* guarantee that the connection is able to read the configuration.
|
*
|
* @param host
|
* the host name.
|
* @param port
|
* the port to connect.
|
* @param useSSL
|
* whether to use SSL or not.
|
* @param useStartTLS
|
* whether to use StartTLS or not.
|
* @param bindDn
|
* the bind dn to be used.
|
* @param pwd
|
* the password.
|
* @param connectTimeout
|
* the timeout in milliseconds to connect to the server.
|
* @param trustManager
|
* the trust manager.
|
* @return an InitialLdapContext connected.
|
* @throws NamingException
|
* if there was an error establishing the connection.
|
*/
|
protected InitialLdapContext createAdministrativeContext(String host,
|
int port, boolean useSSL, boolean useStartTLS, String bindDn, String pwd,
|
int connectTimeout, ApplicationTrustManager trustManager)
|
throws NamingException
|
{
|
InitialLdapContext ctx;
|
String ldapUrl = ConnectionUtils.getLDAPUrl(host, port, useSSL);
|
if (useSSL)
|
{
|
ctx =
|
Utils.createLdapsContext(ldapUrl, bindDn, pwd, connectTimeout, null,
|
trustManager);
|
}
|
else if (useStartTLS)
|
{
|
ctx =
|
Utils.createStartTLSContext(ldapUrl, bindDn, pwd, connectTimeout,
|
null, trustManager, null);
|
}
|
else
|
{
|
ctx = Utils.createLdapContext(ldapUrl, bindDn, pwd, connectTimeout, null);
|
}
|
if (!ConnectionUtils.connectedAsAdministrativeUser(ctx))
|
{
|
throw new NoPermissionException(ERR_NOT_ADMINISTRATIVE_USER.get()
|
.toString());
|
}
|
return ctx;
|
}
|
|
/**
|
* Creates an Initial LDAP Context interacting with the user if the
|
* application is interactive.
|
*
|
* @param ci
|
* the LDAPConnectionConsoleInteraction object that is assumed to
|
* have been already run.
|
* @return the initial LDAP context or <CODE>null</CODE> if the user did not
|
* accept to trust the certificates.
|
* @throws ClientException
|
* if there was an error establishing the connection.
|
*/
|
protected InitialLdapContext createInitialLdapContextInteracting(
|
LDAPConnectionConsoleInteraction ci) throws ClientException
|
{
|
return createInitialLdapContextInteracting(ci, isInteractive()
|
&& ci.isTrustStoreInMemory());
|
}
|
|
/**
|
* Creates an Initial LDAP Context interacting with the user if the
|
* application is interactive.
|
*
|
* @param ci
|
* the LDAPConnectionConsoleInteraction object that is assumed to
|
* have been already run.
|
* @param promptForCertificate
|
* whether we should prompt for the certificate or not.
|
* @return the initial LDAP context or <CODE>null</CODE> if the user did not
|
* accept to trust the certificates.
|
* @throws ClientException
|
* if there was an error establishing the connection.
|
*/
|
protected InitialLdapContext createInitialLdapContextInteracting(
|
LDAPConnectionConsoleInteraction ci, boolean promptForCertificate)
|
throws ClientException
|
{
|
// Interact with the user though the console to get
|
// LDAP connection information
|
String hostName = ConnectionUtils.getHostNameForLdapUrl(ci.getHostName());
|
Integer portNumber = ci.getPortNumber();
|
String bindDN = ci.getBindDN();
|
String bindPassword = ci.getBindPassword();
|
TrustManager trustManager = ci.getTrustManager();
|
KeyManager keyManager = ci.getKeyManager();
|
|
InitialLdapContext ctx;
|
|
if (ci.useSSL())
|
{
|
String ldapsUrl = "ldaps://" + hostName + ":" + portNumber;
|
while (true)
|
{
|
try
|
{
|
ctx =
|
ConnectionUtils.createLdapsContext(ldapsUrl, bindDN,
|
bindPassword, ci.getConnectTimeout(), null, trustManager,
|
keyManager);
|
ctx.reconnect(null);
|
break;
|
}
|
catch (NamingException e)
|
{
|
if (promptForCertificate)
|
{
|
OpendsCertificateException oce = getCertificateRootException(e);
|
if (oce != null)
|
{
|
String authType = null;
|
if (trustManager instanceof ApplicationTrustManager)
|
{
|
ApplicationTrustManager appTrustManager =
|
(ApplicationTrustManager) trustManager;
|
authType = appTrustManager.getLastRefusedAuthType();
|
}
|
if (ci.checkServerCertificate(oce.getChain(), authType, hostName))
|
{
|
// If the certificate is trusted, update the trust manager.
|
trustManager = ci.getTrustManager();
|
|
// Try to connect again.
|
continue;
|
}
|
else
|
{
|
// Assume user canceled.
|
return null;
|
}
|
}
|
}
|
if (e.getCause() != null)
|
{
|
if (!isInteractive() && !ci.isTrustAll())
|
{
|
if (getCertificateRootException(e) != null
|
|| (e.getCause() instanceof SSLHandshakeException))
|
{
|
LocalizableMessage message =
|
ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT_NOT_TRUSTED.get(
|
hostName, String.valueOf(portNumber));
|
throw new ClientException(
|
LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
|
}
|
}
|
if (e.getCause() instanceof SSLException)
|
{
|
LocalizableMessage message =
|
ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT_WRONG_PORT.get(
|
hostName, String.valueOf(portNumber));
|
throw new ClientException(
|
LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
|
}
|
}
|
String hostPort =
|
ServerDescriptor.getServerRepresentation(hostName, portNumber);
|
LocalizableMessage message = Utils.getMessageForException(e, hostPort);
|
throw new ClientException(LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
message);
|
}
|
}
|
}
|
else if (ci.useStartTLS())
|
{
|
String ldapUrl = "ldap://" + hostName + ":" + portNumber;
|
while (true)
|
{
|
try
|
{
|
ctx =
|
ConnectionUtils.createStartTLSContext(ldapUrl, bindDN,
|
bindPassword, ConnectionUtils.getDefaultLDAPTimeout(), null,
|
trustManager, keyManager, null);
|
ctx.reconnect(null);
|
break;
|
}
|
catch (NamingException e)
|
{
|
if (promptForCertificate)
|
{
|
OpendsCertificateException oce = getCertificateRootException(e);
|
if (oce != null)
|
{
|
String authType = null;
|
if (trustManager instanceof ApplicationTrustManager)
|
{
|
ApplicationTrustManager appTrustManager =
|
(ApplicationTrustManager) trustManager;
|
authType = appTrustManager.getLastRefusedAuthType();
|
}
|
|
if (ci.checkServerCertificate(oce.getChain(), authType, hostName))
|
{
|
// If the certificate is trusted, update the trust manager.
|
trustManager = ci.getTrustManager();
|
|
// Try to connect again.
|
continue;
|
}
|
else
|
{
|
// Assume user cancelled.
|
return null;
|
}
|
}
|
else
|
{
|
LocalizableMessage message =
|
ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(hostName, String
|
.valueOf(portNumber));
|
throw new ClientException(
|
LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
|
}
|
}
|
LocalizableMessage message =
|
ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(hostName, String
|
.valueOf(portNumber));
|
throw new ClientException(LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
message);
|
}
|
}
|
}
|
else
|
{
|
String ldapUrl = "ldap://" + hostName + ":" + portNumber;
|
while (true)
|
{
|
try
|
{
|
ctx =
|
ConnectionUtils.createLdapContext(ldapUrl, bindDN, bindPassword,
|
ConnectionUtils.getDefaultLDAPTimeout(), null);
|
ctx.reconnect(null);
|
break;
|
}
|
catch (NamingException e)
|
{
|
LocalizableMessage message =
|
ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(hostName, String
|
.valueOf(portNumber));
|
throw new ClientException(LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
message);
|
}
|
}
|
}
|
return ctx;
|
}
|
|
/**
|
* Returns the message to be displayed in the file with the equivalent
|
* command-line with information about the current time.
|
*
|
* @return the message to be displayed in the file with the equivalent
|
* command-line with information about the current time.
|
*/
|
protected String getCurrentOperationDateMessage()
|
{
|
String date = formatDateTimeStringForEquivalentCommand(new Date());
|
return INFO_OPERATION_START_TIME_MESSAGE.get(date).toString();
|
}
|
|
/**
|
* Formats a Date to String representation in "dd/MMM/yyyy:HH:mm:ss Z".
|
*
|
* @param date
|
* to format; null if <code>date</code> is null
|
* @return string representation of the date
|
*/
|
protected String formatDateTimeStringForEquivalentCommand(Date date)
|
{
|
String timeStr = null;
|
if (date != null)
|
{
|
SimpleDateFormat dateFormat =
|
new SimpleDateFormat(DATE_FORMAT_LOCAL_TIME);
|
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
timeStr = dateFormat.format(date);
|
}
|
return timeStr;
|
}
|
|
/**
|
* Prompts the user to give the Global Administrator UID.
|
*
|
* @param defaultValue
|
* the default value that will be proposed in the prompt message.
|
* @param logger
|
* the Logger to be used to log the error message.
|
* @return the Global Administrator UID as provided by the user.
|
*/
|
protected String askForAdministratorUID(String defaultValue, LocalizedLogger logger)
|
{
|
String s = defaultValue;
|
try
|
{
|
s = readInput(INFO_ADMINISTRATOR_UID_PROMPT.get(), defaultValue);
|
}
|
catch (CLIException ce)
|
{
|
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
|
}
|
return s;
|
}
|
|
/**
|
* Prompts the user to give the Global Administrator password.
|
*
|
* @param logger
|
* the Logger to be used to log the error message.
|
* @return the Global Administrator password as provided by the user.
|
*/
|
protected String askForAdministratorPwd(LocalizedLogger logger)
|
{
|
String pwd = readPassword(INFO_ADMINISTRATOR_PWD_PROMPT.get(), logger);
|
return pwd;
|
}
|
|
private OpendsCertificateException getCertificateRootException(Throwable t)
|
{
|
OpendsCertificateException oce = null;
|
while (t != null && oce == null)
|
{
|
t = t.getCause();
|
if (t instanceof OpendsCertificateException)
|
{
|
oce = (OpendsCertificateException) t;
|
}
|
}
|
return oce;
|
}
|
|
/**
|
* Commodity method used to repeatidly ask the user to provide an integer
|
* value.
|
*
|
* @param prompt
|
* the prompt message.
|
* @param defaultValue
|
* the default value to be proposed to the user.
|
* @param logger
|
* the logger where the errors will be written.
|
* @return the value provided by the user.
|
*/
|
protected int askInteger(LocalizableMessage prompt, int defaultValue, LocalizedLogger logger)
|
{
|
int newInt = -1;
|
while (newInt == -1)
|
{
|
try
|
{
|
newInt = readInteger(prompt, defaultValue);
|
}
|
catch (CLIException ce)
|
{
|
newInt = -1;
|
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
|
}
|
}
|
return newInt;
|
}
|
|
/**
|
* Interactively retrieves an integer value from the console.
|
*
|
* @param prompt
|
* The message prompt.
|
* @param defaultValue
|
* The default value.
|
* @return Returns the value.
|
* @throws CLIException
|
* If the value could not be retrieved for some reason.
|
*/
|
public final int readInteger(LocalizableMessage prompt, final int defaultValue)
|
throws CLIException
|
{
|
ValidationCallback<Integer> callback = new ValidationCallback<Integer>()
|
{
|
@Override
|
public Integer validate(ConsoleApplication app, String input)
|
throws CLIException
|
{
|
String ninput = input.trim();
|
if (ninput.length() == 0)
|
{
|
return defaultValue;
|
}
|
else
|
{
|
try
|
{
|
int i = Integer.parseInt(ninput);
|
if (i < 1)
|
{
|
throw new NumberFormatException();
|
}
|
return i;
|
}
|
catch (NumberFormatException e)
|
{
|
// Try again...
|
app.println();
|
app.println(ERR_LDAP_CONN_BAD_INTEGER.get(ninput));
|
app.println();
|
return null;
|
}
|
}
|
}
|
|
};
|
|
if (defaultValue != -1)
|
{
|
prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
|
}
|
|
return readValidatedInput(prompt, callback, CONFIRMATION_MAX_TRIES);
|
}
|
}
|