com.sun.enterprise.admin.cli.CLICommand Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2018-2021] Payara Foundation and/or affiliates
package com.sun.enterprise.admin.cli;
import com.sun.appserv.server.util.Version;
import com.sun.enterprise.admin.cli.remote.RemoteCLICommand;
import com.sun.enterprise.admin.cli.remote.RemoteCommand;
import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
import com.sun.enterprise.admin.util.LineTokenReplacer;
import com.sun.enterprise.admin.util.TokenValue;
import com.sun.enterprise.admin.util.TokenValueSet;
import com.sun.enterprise.universal.glassfish.ASenvPropertyReader;
import com.sun.enterprise.universal.i18n.LocalStringsImpl;
import fish.payara.api.admin.config.NameGenerator;
import java.io.*;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.inject.Inject;
import jakarta.inject.Scope;
import jakarta.inject.Singleton;
import org.glassfish.api.Param;
import org.glassfish.api.admin.CommandException;
import org.glassfish.api.admin.CommandModel;
import org.glassfish.api.admin.CommandModel.ParamModel;
import org.glassfish.api.admin.CommandValidationException;
import org.glassfish.api.admin.ParameterMap;
import org.glassfish.common.util.admin.CommandModelImpl;
import org.glassfish.common.util.admin.ManPageFinder;
import org.glassfish.common.util.admin.MapInjectionResolver;
import org.glassfish.hk2.api.PerLookup;
import org.glassfish.hk2.api.PostConstruct;
import org.glassfish.hk2.api.ServiceLocator;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jvnet.hk2.annotations.Contract;
import org.jvnet.hk2.annotations.Service;
import org.jvnet.hk2.config.InjectionManager;
import org.jvnet.hk2.config.InjectionResolver;
import org.jvnet.hk2.config.UnsatisfiedDependencyException;
/**
* Base class for a CLI command. An instance of a subclass of this
* class is created using the getCommand method with the name of the
* command and the information about its environment.
*
* A command is executed with a list of arguments using the execute
* method. The implementation of the execute method in this class
* saves the arguments in the protected argv field, then calls the
* following protected methods in order: prepare, parse, validate,
* and executeCommand. A subclass must implement the prepare method
* to initialize the metadata that specified the valid options for
* the command, and the executeCommand method to actually perform the
* command. The parse and validate method may also be overridden if
* needed. Or, the subclass may override the execute method and
* provide the complete implementation for the command, including
* option parsing.
*
* @author Bill Shannon
*/
@Contract
@PerLookup
public abstract class CLICommand implements PostConstruct {
public static final int ERROR = CLIConstants.ERROR;
public static final int CONNECTION_ERROR = 2;
public static final int INVALID_COMMAND_ERROR = 3;
public static final int SUCCESS = CLIConstants.SUCCESS;
public static final int WARNING = CLIConstants.WARNING;
protected Terminal terminal;
protected LineReader lineReader;
protected static final String ASADMIN = "asadmin";
private static final Set unsupported;
private static final String UNSUPPORTED_CMD_FILE_NAME = "unsupported-legacy-command-names";
private static final String PACKAGE_NAME = "com.sun.enterprise.admin.cli";
private static final LocalStringsImpl strings = new LocalStringsImpl(CLICommand.class);
private static final Map systemProps = Collections.unmodifiableMap(new ASenvPropertyReader().getProps());
protected static final Logger logger = Logger.getLogger(CLICommand.class.getPackage().getName());
// InjectionManager is completely stateless with only one method that
// operates on its arguments, so we can share a single instance.
private static final InjectionManager injectionMgr = new InjectionManager();
private static String commandScope = null;
// tokens that are substituted in manual pages
// the tokens are delimited with {}
// the tokens and tokenValues arrays must be kept in sync. See the
// expandManPage method for where the tokenValues are assigned.
private static String manpageTokens[] = {
"cname", // the command name
"cprefix", // the environment variable prefix
"product---name", // the product name
};
private String manpageTokenValues[] = new String[manpageTokens.length];
/**
* The name of the command.
* Initialized in the constructor.
*/
protected String name;
/**
* The program options for the command.
* Initialized in the constructor.
*/
@Inject
protected ProgramOptions programOpts;
/**
* The environment for the command.
* Initialized in the constructor.
*/
@Inject
protected Environment env;
/**
* The command line arguments for this execution.
* Initialized in the execute method.
*/
protected String[] argv;
/**
* The metadata describing the command's options and operands.
*/
protected CommandModel commandModel;
protected StringBuilder metadataErrors;
/**
* The options parsed from the command line.
* Initialized by the parse method. The keys
* are the parameter names from the command model,
* not the "forced to all lower case" names that
* are presented to the user.
*/
protected ParameterMap options;
/**
* The operands parsed from the command line.
* Initialized by the parse method.
*/
protected List operands;
/**
* The passwords read from the password file.
* Initialized by the initializeCommandPassword method.
*/
protected Map passwords;
static {
Set unsup = new HashSet();
file2Set(UNSUPPORTED_CMD_FILE_NAME, unsup);
unsupported = Collections.unmodifiableSet(unsup);
}
private static boolean useRest() {
//return environment != null && environment.getBooleanOption("USE_REST");
return true;
}
/**
* Get a CLICommand object representing the named command.
* @param serviceLocator
* @param name The name of the command
* @return
* @throws CommandException
*/
public static CLICommand getCommand(ServiceLocator serviceLocator, String name) throws CommandException {
// first, check if it's a known unsupported command
checkUnsupportedLegacyCommand(name);
// next, try to load our own implementation of the command
ProgramOptions po = serviceLocator.getService(ProgramOptions.class);
CLICommand cmd = serviceLocator.getService(CLICommand.class, name);
if (cmd != null) {
po.removeDetach();
return cmd;
}
// nope, must be a remote command
logger.log(Level.FINER, "Assuming it''s a remote command: {0}", name);
return getRemoteCommand(name, po, serviceLocator.getService(Environment.class));
}
public static CLICommand getCommand(CLIContainer cLIContainer, String name)
throws CommandException {
// first, check if it's a known unsupported command
checkUnsupportedLegacyCommand(name);
// next, try to load our own implementation of the command
ProgramOptions po = cLIContainer.getProgramOptions();
CLICommand cmd = cLIContainer.getLocalCommand(name);
if (cmd != null) {
po.removeDetach();
return cmd;
}
// nope, must be a remote command
logger.log(Level.FINER, "Assuming it''s a remote command: {0}", name);
return getRemoteCommand(name, po, cLIContainer.getEnvironment());
}
private static CLICommand getRemoteCommand(String name, ProgramOptions po, Environment env) throws CommandException {
if (useRest()) {
return new RemoteCLICommand(name, po, env);
} else {
return new RemoteCommand(name, po, env);
}
}
/**
* Constructor used by subclasses when instantiated by HK2.
* ProgramOptions and Environment are injected. name is set here.
*/
protected CLICommand() {
Service service = this.getClass().getAnnotation(Service.class);
if (service == null)
name = "unknown-command"; // should never happen
else
name = service.name();
}
/**
* Initialise the logger after being instantiated by HK2.
*/
@Override
public void postConstruct() {
initializeLogger();
}
/**
* Constructor used by subclasses to save the name, program options,
* and environment information into corresponding protected fields.
* Finally, this constructor calls the initializeLogger method.
* @param name
* @param programOpts
* @param env
*/
protected CLICommand(String name, ProgramOptions programOpts, Environment env) {
this.name = name;
this.programOpts = programOpts;
this.env = env;
initializeLogger();
}
public int execute(Terminal terminal, String... argv) throws CommandException {
if (terminal != null) {
this.terminal = terminal;
}
return execute(argv);
}
/**
* Execute this command with the given arguemnts.
* The implementation in this class saves the passed arguments in
* the argv field and calls the initializePasswords method.
* Then it calls the prepare, parse, and validate methods, finally
* returning the result of calling the executeCommand method.
* Note that argv[0] is the command name.
*
* @param argv Arguments to execute command with
* @return exit code of the command
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
public int execute(String... argv) throws CommandException {
this.argv = argv;
initializePasswords();
logger.finer("Prepare");
prepare();
logger.finer("Process program options");
processProgramOptions();
logger.finer("Parse command options");
parse();
if (checkHelp())
return 0;
logger.finer("Prevalidate command options");
prevalidate();
logger.finer("Inject command options");
inject();
logger.finer("Validate command options");
validate();
if (programOpts.isEcho()) {
logger.info(echoCommand());
// In order to avoid echoing commands used intenally to the
// implementation of *this* command, we turn off echo after
// having echoed this command.
programOpts.setEcho(false);
} else {
logger.log(Level.FINER, echoCommand());
}
logger.finer("Execute command");
return executeCommand();
}
/**
* Return the name of this command.
* @return
*/
public String getName() {
return name;
}
/*
* Return the command scope for this command. The command scope is
* a name space in which commands are defined. Command clients can specify a scope
* to use in looking up a command. Currently this is only used for remote
* commands. By default, the context is null.
*/
public static String getCommandScope() {
return commandScope;
}
/*
* Set the command scope for this command.
*/
public static void setCommandScope(String ctx) {
commandScope = ctx;
}
/**
* Returns the program options associated with this command.
*
* @return the command's program options
*/
public ProgramOptions getProgramOptions() {
return programOpts;
}
/**
* Return a BufferedReader for the man page for this command,
* or null if not found.
* @return
*/
public BufferedReader getManPage() {
String commandName = getName();
if (commandName.length() == 0){
throw new IllegalArgumentException("Command name cannot be empty");
}
// special case "help" --> help for the command
if (commandName.equals("help"))
commandName = programOpts.getCommandName();
return ManPageFinder.getCommandManPage(
commandName,
getClass().getName(),
Locale.getDefault(),
getClass().getClassLoader(),
logger);
}
/**
* Return a man page for this command that has the tokens substituted
* @param r
* @return
*/
public BufferedReader expandManPage(Reader r) {
manpageTokenValues[0] = programOpts.getCommandName();
manpageTokenValues[1] = Environment.getPrefix();
manpageTokenValues[2] = Version.getBriefProductName();
TokenValueSet tvs = new TokenValueSet();
for (int i = 0; i < manpageTokens.length; i++) {
tvs.add(new TokenValue(manpageTokens[i], manpageTokenValues[i], "{", "}"));
}
return new BufferedReader(new LineTokenReplacer(tvs).getReader(r));
}
/**
* Get the usage text for the subcommand. This method shows the details for
* the subcommand options but does not provide details about the command
* options.
*
* @return usage text
*/
public String getUsage() {
String usage;
if (commandModel != null && ok(usage = commandModel.getUsageText())) {
StringBuilder usageText = new StringBuilder();
usageText.append(strings.get("Usage", strings.get("Usage.brief", programOpts.getCommandName())));
usageText.append(" ");
usageText.append(usage);
return usageText.toString();
} else {
return generateUsageText();
}
}
private String generateUsageText() {
StringBuilder usageText = new StringBuilder();
usageText.append(strings.get("Usage", strings.get("Usage.brief", programOpts.getCommandName())));
usageText.append(" ");
usageText.append(getName());
int len = usageText.length();
StringBuilder optText = new StringBuilder();
String lsep = System.getProperty("line.separator");
for (ParamModel opt : usageOptions()) {
optText.setLength(0);
final String optName = lc(opt.getName());
// "--terse" is part of asadmin utility options
if (optName.equals("terse"))
continue;
// skip "hidden" options
if (optName.startsWith("_"))
continue;
// do not want to display password as an option
if (opt.getParam().password())
continue;
// also do not want to display obsolete options
if (opt.getParam().obsolete())
continue;
// primary parameter is the operand, not an option
if (opt.getParam().primary())
continue;
boolean optional = opt.getParam().optional();
String defValue = opt.getParam().defaultValue();
if (optional){
optText.append("[");
}
String sn = opt.getParam().shortName();
if (ok(sn)){
optText.append('-').append(sn).append('|');
}
optText.append("--").append(optName);
if (opt.getType() == Boolean.class || opt.getType() == boolean.class) {
// canonicalize default value
if (ok(defValue) && Boolean.parseBoolean(defValue)) {
defValue = "true";
} else {
defValue = "false";
}
optText.append("[=<").append(optName);
optText.append(strings.get("Usage.default", defValue));
optText.append(">]");
} else { // STRING or FILE
if (ok(defValue)) {
optText.append(" <").append(optName);
optText.append(strings.get("Usage.default", defValue));
optText.append('>');
} else
optText.append(" <").append(optName).append('>');
}
if (optional){
optText.append("]");
}
if (len + 1 + optText.length() > 80) {
usageText.append(lsep).append('\t');
len = 8;
} else {
usageText.append(' ');
len++;
}
usageText.append(optText);
len += optText.length();
}
// add --help text
String helpText = "[-?|--help[=]]";
if (len + 1 + helpText.length() > 80) {
usageText.append(lsep).append('\t');
len = 8;
} else {
usageText.append(' ');
len++;
}
usageText.append(helpText);
len += helpText.length();
optText.setLength(0);
ParamModel operandParam = getOperandModel();
String opname = operandParam != null ?
lc(operandParam.getName()) : null;
if (!ok(opname))
opname = "operand";
int operandMin = 0;
int operandMax = 0;
if (operandParam != null) {
operandMin = operandParam.getParam().optional() ? 0 : 1;
operandMax = operandParam.getParam().multiple() ? Integer.MAX_VALUE : 1;
}
if (operandMax > 0) {
if (operandMin == 0) {
optText.append("[").append(opname);
if (operandMax > 1){
optText.append(" ...");
}
optText.append("]");
} else {
optText.append(opname);
if (operandMax > 1){
optText.append(" ...");
}
}
}
if (len + 1 + optText.length() > 80) {
usageText.append(lsep).append('\t');
len = 8;
} else {
usageText.append(' ');
len++;
}
usageText.append(optText);
return usageText.toString();
}
/**
* Subclasses can override this method to supply additional
* or different options that should be part of the usage text.
* Most commands will never need to do this, but the create-domain
* command uses it to include the --user option as a required option.
* @return
*/
protected Collection usageOptions() {
return commandModel.getParameters();
}
/**
* Get the usage text for the command. This usage text shows the details
* of the command options but does not show the details for the subcommand
* options. The subcommand argument is used to fill in the subcommand name
* in the usage text.
* @return usage text for the command
*/
public String getCommandUsage() {
return strings.get("Usage.full", programOpts.getCommandName());
}
public String getBriefCommandUsage() {
return strings.get("Usage.brief", programOpts.getCommandName());
}
@Override
public String toString() {
return echoCommand();
}
/**
* Return a string representing the command line used with this command.
*/
private String echoCommand() {
StringBuilder sb = new StringBuilder();
// first, the program options
sb.append(programOpts.getCommandName());
sb.append(' ');
sb.append(programOpts.toString()).append(' ');
// now the subcommand options and operands
sb.append(name).append(' ');
// have we parsed any options yet?
if (options != null && operands != null) {
for (ParamModel opt : commandModel.getParameters()) {
if (opt.getParam().password())
continue; // don't print passwords
if (opt.getParam().primary())
continue;
// include every option that was specified on the command line
// and every option that has a default value
if (opt.getParam().multiple()) {
List paramValues = getOptions(opt.getName());
for (String v : paramValues) {
appendEchoOption(sb, opt, v);
}
} else {
String value = getOption(opt.getName());
if (value != null) {
appendEchoOption(sb, opt, value);
}
}
}
for (String o : operands) {
sb.append(quote(o)).append(' ');
}
} else if (argv != null) {
// haven't parsed any options, include raw arguments, if any
for (String arg : argv) {
sb.append(quote(arg)).append(' ');
}
}
sb.setLength(sb.length() - 1); // strip trailing space
return sb.toString();
}
private void appendEchoOption(StringBuilder sb, ParamModel opt, String value) {
sb.append("--").append(lc(opt.getName()));
if (opt.getType() == Boolean.class || opt.getType() == boolean.class) {
sb.append("=").append(Boolean.toString(Boolean.parseBoolean(value)));
} else { // STRING or FILE
sb.append(" ").append(quote(value));
}
sb.append(' ');
}
/**
* Quote a value, if the value contains any special characters.
*
* @param value value to be quoted
* @return the possibly quoted value
*/
public static String quote(String value) {
int len = value.length();
if (len == 0){
return "\"\""; // an empty string is handled specially
}
/*
* Look for any special characters. Escape and
* quote the entire string if necessary.
*/
boolean needQuoting = false;
for (int i = 0; i < len; i++) {
char c = value.charAt(i);
if (c == '"' || c == '\\' || c == '\r' || c == '\n') {
// need to escape them and then quote the whole string
StringBuilder sb = new StringBuilder(len + 3);
sb.append('"');
sb.append(value.substring(0, i));
int lastc = 0;
for (int j = i; j < len; j++) {
char cc = value.charAt(j);
if ((cc == '"') || (cc == '\\') ||
(cc == '\r') || (cc == '\n'))
if (cc == '\n' && lastc == '\r')
; // do nothing, CR was already escaped
else
sb.append('\\'); // Escape the character
sb.append(cc);
lastc = cc;
}
sb.append('"');
return sb.toString();
} else if (c <= 040 || c >= 0177)
// These characters cause the string to be quoted
needQuoting = true;
}
if (needQuoting) {
StringBuilder sb = new StringBuilder(len + 2);
sb.append('"').append(value).append('"');
return sb.toString();
} else
return value;
}
/**
* If the program options haven't already been set, parse them
* on the command line and remove them from the command line.
* Subclasses should call this method in their prepare method
* after initializing commandOpts (so usage is available on failure)
* if they want to allow program options after the command name.
* Currently RemoteCommand does this, as well as the local commands
* that also need to talk to the server.
* @throws CommandException
*/
protected void processProgramOptions() throws CommandException {
/*
* asadmin options and command options are intermixed.
* Parse the entire command line for asadmin options,
* removing them from the command line, and ignoring
* unknown options.
*/
Collection model = ProgramOptions.getValidOptions();
if (programOpts.isOptionsSet()) {
model = ProgramOptions.getHelpOption();
}
Parser rcp = new Parser(argv, 0, model, true);
ParameterMap params = rcp.getOptions();
List oprds = rcp.getOperands();
argv = oprds.toArray(new String[oprds.size()]);
if (params.size() > 0) {
// at least one program option specified after command name
logger.finer("Update program options");
programOpts.updateOptions(params);
initializeLogger();
initializePasswords();
if (!programOpts.isTerse() && !(params.size() == 1 && params.get("help") != null)) {
// warn about deprecated use of program options
// (except --help)
// XXX - a lot of work for a nice message...
Collection programOptions = ProgramOptions.getValidOptions();
StringBuilder sb = new StringBuilder();
sb.append(programOpts.getCommandName());
for (Map.Entry> p : params.entrySet()) {
// find the corresponding ParamModel
ParamModel opt = null;
for (ParamModel vo : programOptions) {
if (vo.getName().equalsIgnoreCase(p.getKey())) {
opt = vo;
break;
}
}
if (opt == null) {
continue;
}
// format the option appropriately
sb.append(" --").append(p.getKey());
List pl = p.getValue();
// XXX - won't handle multi-values
if (opt.getType() == Boolean.class || opt.getType() == boolean.class) {
if (!pl.get(0).equalsIgnoreCase("true")) {
sb.append("=false");
}
} else if (pl != null && pl.size() > 0) {
sb.append(" ").append(pl.get(0));
}
}
sb.append(" ").append(name).append(" [options] ...");
logger.info(strings.get("DeprecatedSyntax"));
logger.info(sb.toString());
}
}
}
/**
* Initialize the state of the logger based on any program options.
*/
protected void initializeLogger() {
if (!logger.isLoggable(Level.FINER)) {
if (programOpts.isTerse()) {
logger.setLevel(Level.INFO);
} else {
logger.setLevel(Level.FINE);
}
}
}
/**
* Initialise the passwords field based on the password
* file specified in the program options, and initialise the
* program option's password if available in the password file.
* @throws CommandException
*/
protected void initializePasswords() throws CommandException {
passwords = new HashMap();
String pwfile = programOpts.getPasswordFile();
if (ok(pwfile)) {
passwords = CLIUtil.readPasswordFileOptions(pwfile, true);
logger.log(Level.FINER, "Passwords were read from password file: {0}", pwfile);
if (passwords.get(Environment.getPrefix() + "PASSWORD") != null){
char[] password = passwords.get(Environment.getPrefix() + "PASSWORD").toCharArray();
if (programOpts.getPassword() == null){
programOpts.setPassword(password, ProgramOptions.PasswordLocation.PASSWORD_FILE);
}
}
}
}
/**
* The prepare method must ensure that the commandModel field is set.
* @throws CommandException
*/
protected void prepare() throws CommandException {
commandModel = new CommandModelImpl(this.getClass());
}
/**
* The parse method sets the options and operands fields
* based on the content of the command line arguments.
* If the program options say this is a help request,
* we set options and operands as if "--help" had been specified.
*
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
protected void parse() throws CommandException {
/*
* If this is a help request, we don't need the command
* metadata and we throw away all the other options and
* fake everything else.
*/
if (programOpts.isHelp()) {
options = new ParameterMap();
options.set("help", "true");
operands = Collections.emptyList();
} else {
Parser rcp = new Parser(argv, 1, commandModel.getParameters(), commandModel.unknownOptionsAreOperands());
options = rcp.getOptions();
operands = rcp.getOperands();
/*
* In the case where we're accepting unknown options as
* operands, the special "--" delimiter will also be
* accepted as an operand. We eliminate it here.
*/
if (commandModel.unknownOptionsAreOperands() &&
operands.size() > 0 && operands.get(0).equals("--"))
operands.remove(0);
}
if (logger.isLoggable(Level.FINER)) {
logger.finer("params: " + options);
logger.finer("operands: " + operands);
}
}
/**
* Check if the current request is a help request, either because
* --help was specified as a program option or a command option.
* If so, get the man page using the getManPage method, copy the
* content to System.out, and return true. Otherwise return false.
* Subclasses may override this method to perform a different check
* or to use a different method to display the man page.
* If this method returns true, the validate and executeCommand methods
* won't be called.
* @return
* @throws CommandException
*/
protected boolean checkHelp() throws CommandException {
if (programOpts.isHelp()) {
BufferedReader br = getManPage();
if (br == null) {
throw new CommandException(strings.get("ManpageMissing", name));
}
br = expandManPage(br);
String line;
try {
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException ioex) {
throw new CommandException(strings.get("ManpageMissing", name), ioex);
} finally {
try {
br.close();
} catch (IOException ex) {
}
}
return true;
}
return false;
}
private Class extends Annotation> getScope(Class> onMe) {
if (onMe == null) return null;
for (Annotation anno : onMe.getAnnotations()) {
if (anno.annotationType().isAnnotationPresent(Scope.class)) {
return anno.annotationType();
}
}
return null;
}
/**
* The prevalidate method supplies missing options from
* the environment. It also supplies passwords from the password
* file or prompts for them if interactive.
*
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
protected void prevalidate() throws CommandException {
/*
* First, check that the command has the proper scope.
* (Could check this in getCommand(), but at that point we
* don't have the CommandModel yet.)
* Remote commands are checked on the server.
*/
if (!(this instanceof RemoteCommand) && !(this instanceof RemoteCLICommand)) {
Class extends Annotation> myScope = getScope(this.getClass());
if (myScope == null) {
throw new CommandException(strings.get("NoScope", name));
} else if (Singleton.class.equals(myScope)) {
// check that there are no parameters for this command
if (commandModel.getParameters().size() > 0) {
throw new CommandException(strings.get("HasParams", name));
}
}
}
/*
* Check for missing options and operands.
*/
int operandMin = 0;
int operandMax = 0;
ParamModel operandParam = getOperandModel();
if (operandParam != null) {
operandMin = operandParam.getParam().optional() ? 0 : 1;
operandMax = operandParam.getParam().multiple() ? Integer.MAX_VALUE : 1;
}
if (programOpts.isInteractive()) {
try {
buildTerminal();
buildLineReader();
boolean missingOption = false;
for (ParamModel opt : commandModel.getParameters()) {
if (opt.getParam().password()) {
continue; // passwords are handled later
}
if (opt.getParam().obsolete() && getOption(opt.getName()) != null) {
logger.info(strings.get("ObsoleteOption", opt.getName()));
}
if (opt.getParam().optional()) {
continue;
}
if (opt.getParam().primary()) {
continue;
}
// if option isn't set, prompt for it (if interactive)
if (getOption(opt.getName()) == null && lineReader != null && !missingOption) {
String val = lineReader.readLine(strings.get("optionPrompt", lc(opt.getName())));
if (ok(val)) {
options.set(opt.getName(), val);
}
}
// if it's still not set, that's an error
if (getOption(opt.getName()) == null) {
missingOption = true;
logger.log(Level.INFO, strings.get("missingOption", "--" + opt.getName()));
}
if (opt.getParam().obsolete()) { // a required obsolete option?
logger.log(Level.INFO, strings.get("ObsoleteOption", opt.getName()));
}
}
if (missingOption) {
throw new CommandValidationException(strings.get("missingOptions", name));
}
if (operands.size() < operandMin && lineReader != null) {
String val = null;
if (programOpts.isAutoName()) {
val = NameGenerator.generateName();
}
if (!ok(val)) {
val = lineReader.readLine(strings.get("operandPrompt", operandParam.getName()));
}
if (ok(val)) {
operands = new ArrayList<>();
operands.add(val);
}
}
} catch (UserInterruptException | EndOfFileException e) {
// Ignore
} finally {
closeTerminal();
}
} else {
// Check if we're missing an operand even if not interactive in case we want to generate it.
if (operands.size() < operandMin) {
String val = null;
if (programOpts.isAutoName()) {
val = NameGenerator.generateName();
}
if (ok(val)) {
operands = new ArrayList<>();
operands.add(val);
}
}
}
// Validate that we have the required operands
if (operands.size() < operandMin) {
throw new CommandValidationException(strings.get("notEnoughOperands", name,
operandParam.getType()));
}
if (operands.size() > operandMax) {
switch (operandMax) {
case 0:
throw new CommandValidationException(
strings.get("noOperandsAllowed", name));
case 1:
throw new CommandValidationException(
strings.get("tooManyOperands1", name));
default:
throw new CommandValidationException(
strings.get("tooManyOperands", name, operandMax));
}
}
initializeCommandPassword();
}
/**
* Inject this instance with the final values of all the command
* parameters.
*
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
protected void inject() throws CommandException {
// injector expects operands to be in the ParameterMap with the key
// "DEFAULT"
options.set("DEFAULT", operands);
// if command has a "terse" or "extraterse" option, set it from ProgramOptions
if (commandModel.getModelFor("terse") != null){
options.set("terse", Boolean.toString(programOpts.isTerse()));
}
if (commandModel.getModelFor("extraterse") != null){
options.set("extraterse", Boolean.toString(programOpts.isExtraTerse()));
}
if (commandModel.getModelFor("autoname") != null) {
options.set("autoname", Boolean.toString(programOpts.isAutoName()));
}
// initialize the injector.
InjectionResolver injector = new MapInjectionResolver(commandModel, options);
// inject
try {
injectionMgr.inject(this, injector);
} catch (UnsatisfiedDependencyException e) {
throw new CommandValidationException(e.getMessage(), e);
}
}
/**
* The validate method can be used by a subclass to validate
* that the type and quantity of parameters and operands matches
* the requirements for this command.
*
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
protected void validate() throws CommandException {
}
/**
* Execute the command using the options in options and the
* operands in operands.
*
* @return the exit code
* @throws CommandException if execution of the command fails
* @throws CommandValidationException if there's something wrong
* with the options or arguments
*/
protected abstract int executeCommand() throws CommandException;
/**
* Initialize all the passwords required by the command.
*
* @throws CommandException
*/
private void initializeCommandPassword() throws CommandException {
/*
* Go through all the valid options and check for required password
* options that weren't specified in the password file. If option
* is missing and we're interactive, prompt for it. Store the
* password as if it was a parameter.
*/
for (ParamModel opt : commandModel.getParameters()) {
if (!opt.getParam().password())
continue;
String pwdname = opt.getName();
char[] pwd = getPassword(opt, null, true);
if (pwd == null) {
if (opt.getParam().optional())
continue; // not required, skip it
// if not terse, provide more advice about what to do
String msg;
if (programOpts.isTerse()){
msg = strings.get("missingPassword", name, passwordName(opt));
} else {
msg = strings.get("missingPasswordAdvice", name, passwordName(opt));
}
throw new CommandValidationException(msg);
}
options.set(pwdname, new String(pwd));
}
}
protected char[] getPassword(String paramname, String localizedPrompt,
String localizedPromptConfirm, boolean create) throws CommandValidationException {
ParamModelData po = new ParamModelData(paramname, String.class, false, null);
po.prompt = localizedPrompt;
po.promptAgain = localizedPromptConfirm;
po.param._password = true;
return getPassword(po, null, create);
}
/**
* Get a password for the given option.
* First, look in the passwords map. If found, return it.
* If not found, and not required, return null;
* If not interactive, return null. Otherwise, prompt for the
* password. If create is true, prompt twice and compare the two values
* to make sure they're the same. If the password meets other validity
* criteria (i.e., length) returns the password. If defaultPassword is
* not null, "Enter" selects this default password, which is returned.
* @param opt
* @param defaultPassword
* @param create
* @return
* @throws CommandValidationException
*/
protected char[] getPassword(ParamModel opt, String defaultPassword, boolean create) throws CommandValidationException {
String passwordName = passwordName(opt);
char[] password = passwords.get(passwordName) != null ? passwords.get(passwordName).toCharArray() : null;
if (password != null){
return password; //Password is in the password file
}
if (opt.getParam().optional())
return null; // not required
if (!programOpts.isInteractive())
return null; // can't prompt for it
String prompt = null;
String promptAgain = null;
if (opt instanceof ParamModelData) {
prompt = ((ParamModelData)opt).getPrompt();
promptAgain = ((ParamModelData)opt).getPromptAgain();
}
String newprompt;
if (ok(prompt)) {
if (defaultPassword != null) {
if (defaultPassword.length() == 0) {
newprompt = strings.get("NewPasswordDescriptionDefaultEmptyPrompt", prompt);
} else {
newprompt = strings.get("NewPasswordDescriptionDefaultPrompt", prompt, defaultPassword);
}
} else {
newprompt = strings.get("NewPasswordDescriptionPrompt", prompt);
}
} else {
if (defaultPassword != null) {
if (defaultPassword.length() == 0){
newprompt = strings.get("NewPasswordDefaultEmptyPrompt", passwordName);
} else {
newprompt = strings.get("NewPasswordDefaultPrompt", passwordName, defaultPassword);
}
} else {
newprompt = strings.get("NewPasswordPrompt", passwordName);
}
}
char[] newpassword = readPassword(newprompt);
/*
* If we allow for a default password, and the user just hit "Enter",
* return the default password. No need to prompt twice or check
* for validity.
*/
if (defaultPassword != null) {
if (newpassword == null) {
newpassword = "".toCharArray();
}
if (newpassword.length == 0) {
newpassword = defaultPassword.toCharArray();
passwords.put(passwordName, new String(newpassword));
return newpassword;
}
}
/*
* If not creating a new password, don't need to verify that
* the user typed it correctly by making them type it twice,
* and don't need to check it for validity. Just return what
* we have.
*/
if (!create) {
passwords.put(passwordName, newpassword != null ? new String(newpassword) : null);
return newpassword;
}
String confirmationPrompt;
if (ok(promptAgain)) {
confirmationPrompt = strings.get("NewPasswordDescriptionPrompt", promptAgain);
} else {
confirmationPrompt = strings.get("NewPasswordConfirmationPrompt", passwordName);
}
char[] newpasswordAgain = readPassword(confirmationPrompt);
if (!Arrays.equals(newpassword, newpasswordAgain)) {
throw new CommandValidationException(strings.get("OptionsDoNotMatch", ok(prompt) ? prompt : passwordName));
}
passwords.put(passwordName, newpassword != null ? new String(newpassword) : null);
return newpassword;
}
private String passwordName(ParamModel opt) {
return Environment.getPrefix() + opt.getName().toUpperCase(Locale.ENGLISH);
}
/**
* Display the given prompt and read a password without echoing it.
* Returns null if no console available.
* @param prompt
* @return
*/
protected char[] readPassword(String prompt) {
if (!programOpts.isInteractive()) {
return null;
}
try {
buildTerminal();
buildLineReader();
char echoCharacter = 0;
String line = lineReader.readLine(prompt, echoCharacter);
return line.toCharArray();
} catch (UserInterruptException | EndOfFileException e) {
// Ignore
} finally {
closeTerminal();
}
return null;
}
/**
* Get the ParamModel that corresponds to the operand
* (primary parameter). Return null if none.
* @return
*/
protected ParamModel getOperandModel() {
for (ParamModel pm : commandModel.getParameters()) {
if (pm.getParam().primary())
return pm;
}
return null;
}
/**
* Get an option value, that might come from the command line
* or from the environment. Return the default value for the
* option if not otherwise specified.
* @param name
* @return
*/
protected String getOption(String name) {
String val = options.getOne(name);
if (val == null) {
val = env.getStringOption(name);
}
if (val == null) {
// no value, find the default
ParamModel opt = commandModel.getModelFor(name);
// if no value was specified and there's a default value, return it
if (opt != null) {
String def = opt.getParam().defaultValue();
if (ok(def)) {
val = def;
}
}
}
return val;
}
/**
* Get option values, that might come from the command line
* or from the environment. Return the default value for the
* option if not otherwise specified. This method works with options
* for with multiple() is true.
* @param name
* @return
*/
protected List getOptions(String name) {
List val = options.get(name);
if (val.isEmpty()) {
String v = env.getStringOption(name);
if (v != null) {
val.add(v);
}
}
if (val.isEmpty()) {
// no value, find the default
ParamModel opt = commandModel.getModelFor(name);
// if no value was specified and there's a default value, return it
if (opt != null) {
String def = opt.getParam().defaultValue();
if (ok(def)) {
val.add(def);
}
}
}
return val;
}
/**
* Get a boolean option value, that might come from the command line
* or from the environment.
* @param name
* @return
*/
protected boolean getBooleanOption(String name) {
String val = getOption(name);
return val != null && Boolean.parseBoolean(val);
}
/**
* Return the named system property, or property
* set in asenv.conf.
* @param name
* @return
*/
protected String getSystemProperty(String name) {
return systemProps.get(name);
}
/**
* Return all the system properties and properties set
* in asenv.conf. The returned Map may not be modified.
* @return
*/
protected Map getSystemProperties() {
return systemProps;
}
/**
* If this is an unsupported command, throw an exception.
*/
private static void checkUnsupportedLegacyCommand(String cmd)
throws CommandException {
for (String c : unsupported) {
if (c.equals(cmd)) {
throw new CommandException(strings.get("UnsupportedLegacyCommand", cmd));
}
}
// it is a supported command; do nothing
}
/**
* Prints the exception message with level as FINER.
*
* @param e the exception object to print
*/
protected void printExceptionStackTrace(java.lang.Throwable e) {
if (logger.isLoggable(Level.FINER)) {
ByteArrayOutputStream output = new ByteArrayOutputStream(512);
e.printStackTrace(new java.io.PrintStream(output));
try {
output.close();
} catch (IOException ex) {
// ignore
}
logger.finer(output.toString());
}
}
protected static boolean ok(String s) {
return s != null && s.length() > 0;
}
// shorthand for this too-verbose operation
private static String lc(String s) {
return s.toLowerCase(Locale.ENGLISH);
}
/**
* Read the named resource file and add the first token on each line
* to the set. Skip comment lines.
*/
private static void file2Set(String file, Set set) {
//BufferedReader reader = null;
try {
InputStream is = CLICommand.class.getClassLoader().getResourceAsStream(file);
if (is == null) {
return; // in case the resource doesn't exist
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) {
continue; // # indicates comment
}
StringTokenizer tok = new StringTokenizer(line, " ");
// handles with or without space, rudimendary as of now
String cmd = tok.nextToken();
set.add(cmd);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
protected void buildTerminal() {
try {
if (terminal == null) {
terminal = TerminalBuilder.builder()
.system(true)
.build();
}
} catch (IOException ioe) {
logger.log(Level.WARNING, "Error building a Terminal", ioe);
}
}
protected void buildLineReader() {
if (lineReader == null) {
lineReader = newLineReaderBuilder()
.terminal(terminal)
.build();
}
}
protected LineReaderBuilder newLineReaderBuilder() {
// In community this should be disabled by default
boolean disabled = true;
Environment environment = this.env;
if(environment.hasOption("DISABLE_EVENT_EXPANSION")) {
disabled = environment.getBooleanOption("DISABLE_EVENT_EXPANSION");
}
return LineReaderBuilder.builder()
.appName(ASADMIN)
// disable event expansion because it swallows backslashes and we don't need to support events
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, disabled);
}
protected void closeTerminal() {
try {
if (terminal != null) {
if (!terminal.getName().equals(ASADMIN)) {
terminal.close();
terminal = null;
}
}
lineReader = null;
} catch (IOException ioe) {
logger.log(Level.WARNING, "Error closing terminal", ioe);
}
}
}