001 /*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright (c) 1997-2011 Oracle and/or its affiliates. All rights reserved.
005 *
006 * The contents of this file are subject to the terms of either the GNU
007 * General Public License Version 2 only ("GPL") or the Common Development
008 * and Distribution License("CDDL") (collectively, the "License"). You
009 * may not use this file except in compliance with the License. You can
010 * obtain a copy of the License at
011 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
012 * or packager/legal/LICENSE.txt. See the License for the specific
013 * language governing permissions and limitations under the License.
014 *
015 * When distributing the software, include this License Header Notice in each
016 * file and include the License file at packager/legal/LICENSE.txt.
017 *
018 * GPL Classpath Exception:
019 * Oracle designates this particular file as subject to the "Classpath"
020 * exception as provided by Oracle in the GPL Version 2 section of the License
021 * file that accompanied this code.
022 *
023 * Modifications:
024 * If applicable, add the following below the License Header, with the fields
025 * enclosed by brackets [] replaced by your own identifying information:
026 * "Portions Copyright [year] [name of copyright owner]"
027 *
028 * Contributor(s):
029 * If you wish your version of this file to be governed by only the CDDL or
030 * only the GPL Version 2, indicate your decision by adding "[Contributor]
031 * elects to include this software in this distribution under the [CDDL or GPL
032 * Version 2] license." If you don't indicate a single choice of license, a
033 * recipient has the option to distribute your version of this file under
034 * either the CDDL, the GPL Version 2 or to extend the choice of license to
035 * its licensees as provided above. However, if you add GPL Version 2 code
036 * and therefore, elected the GPL Version 2 license, then the option applies
037 * only if the new code is made subject to such option by the copyright
038 * holder.
039 */
040
041 package com.sun.enterprise.admin.cli;
042
043 import java.io.*;
044 import java.util.*;
045 import java.lang.reflect.*;
046 import java.util.logging.*;
047
048 import org.jvnet.hk2.annotations.*;
049 import org.jvnet.hk2.component.*;
050 import com.sun.hk2.component.InjectionResolver;
051
052 import org.glassfish.api.Param;
053 import org.glassfish.api.admin.*;
054 import org.glassfish.api.admin.CommandModel.ParamModel;
055 import org.glassfish.common.util.admin.CommandModelImpl;
056 import org.glassfish.common.util.admin.MapInjectionResolver;
057 import org.glassfish.common.util.admin.ManPageFinder;
058
059 import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
060 import com.sun.enterprise.admin.cli.remote.RemoteCommand;
061 import com.sun.enterprise.universal.i18n.LocalStringsImpl;
062 import com.sun.enterprise.universal.glassfish.ASenvPropertyReader;
063 import com.sun.logging.LogDomains;
064
065
066 /**
067 * Base class for a CLI command. An instance of a subclass of this
068 * class is created using the getCommand method with the name of the
069 * command and the information about its environment.
070 * <p>
071 * A command is executed with a list of arguments using the execute
072 * method. The implementation of the execute method in this class
073 * saves the arguments in the protected argv field, then calls the
074 * following protected methods in order: prepare, parse, validate,
075 * and executeCommand. A subclass must implement the prepare method
076 * to initialize the metadata that specified the valid options for
077 * the command, and the executeCommand method to actually perform the
078 * command. The parse and validate method may also be overridden if
079 * needed. Or, the subclass may override the execute method and
080 * provide the complete implementation for the command, including
081 * option parsing.
082 *
083 * @author Bill Shannon
084 */
085 @Contract
086 @Scoped(PerLookup.class)
087 public abstract class CLICommand implements PostConstruct {
088 public static final int ERROR = CLIConstants.ERROR;
089 public static final int CONNECTION_ERROR = 2;
090 public static final int INVALID_COMMAND_ERROR = 3;
091 public static final int SUCCESS = CLIConstants.SUCCESS;
092 public static final int WARNING = CLIConstants.WARNING;
093
094 private static final Set<String> unsupported;
095 private static final String UNSUPPORTED_CMD_FILE_NAME =
096 "unsupported-legacy-command-names";
097 private static final String PACKAGE_NAME = "com.sun.enterprise.admin.cli";
098
099 private static final LocalStringsImpl strings =
100 new LocalStringsImpl(CLICommand.class);
101
102 private static final Map<String,String> systemProps =
103 Collections.unmodifiableMap(new ASenvPropertyReader().getProps());
104
105 protected static final Logger logger =
106 Logger.getLogger(CLICommand.class.getPackage().getName());
107
108 // InjectionManager is completely stateless with only one method that
109 // operates on its arguments, so we can share a single instance.
110 private static final InjectionManager injectionMgr = new InjectionManager();
111
112 /**
113 * The name of the command.
114 * Initialized in the constructor.
115 */
116 protected String name;
117
118 /**
119 * The program options for the command.
120 * Initialized in the constructor.
121 */
122 @Inject
123 protected ProgramOptions programOpts;
124
125 /**
126 * The environment for the command.
127 * Initialized in the constructor.
128 */
129 @Inject
130 protected Environment env;
131
132 /**
133 * The command line arguments for this execution.
134 * Initialized in the execute method.
135 */
136 protected String[] argv;
137
138 /**
139 * The metadata describing the command's options and operands.
140 */
141 protected CommandModel commandModel;
142
143 protected StringBuilder metadataErrors;
144
145 /**
146 * The options parsed from the command line.
147 * Initialized by the parse method. The keys
148 * are the parameter names from the command model,
149 * not the "forced to all lower case" names that
150 * are presented to the user.
151 */
152 protected ParameterMap options;
153
154 /**
155 * The operands parsed from the command line.
156 * Initialized by the parse method.
157 */
158 protected List<String> operands;
159
160 /**
161 * The passwords read from the password file.
162 * Initialized by the initializeCommandPassword method.
163 */
164 protected Map<String, String> passwords;
165
166 static {
167 Set<String> unsup = new HashSet<String>();
168 file2Set(UNSUPPORTED_CMD_FILE_NAME, unsup);
169 unsupported = Collections.unmodifiableSet(unsup);
170 }
171
172 /**
173 * Get a CLICommand object representing the named command.
174 */
175 public static CLICommand getCommand(Habitat habitat, String name)
176 throws CommandException {
177
178 // first, check if it's a known unsupported command
179 checkUnsupportedLegacyCommand(name);
180
181 // next, try to load our own implementation of the command
182 CLICommand cmd = habitat.getComponent(CLICommand.class, name);
183 if (cmd != null)
184 return cmd;
185
186 // nope, must be a remote command
187 logger.finer("Assuming it's a remote command: " + name);
188 return new RemoteCommand(name,
189 habitat.getComponent(ProgramOptions.class),
190 habitat.getComponent(Environment.class));
191 }
192
193 /**
194 * Constructor used by subclasses when instantiated by HK2.
195 * ProgramOptions and Environment are injected. name is set here.
196 */
197 protected CLICommand() {
198 Service service = this.getClass().getAnnotation(Service.class);
199
200 if (service == null)
201 name = "unknown-command"; // should never happen
202 else
203 name = service.name();
204 }
205
206 /**
207 * Initialize the logger after being instantiated by HK2.
208 */
209 public void postConstruct() {
210 initializeLogger();
211 }
212
213 /**
214 * Constructor used by subclasses to save the name, program options,
215 * and environment information into corresponding protected fields.
216 * Finally, this constructor calls the initializeLogger method.
217 */
218 protected CLICommand(String name, ProgramOptions programOpts,
219 Environment env) {
220 this.name = name;
221 this.programOpts = programOpts;
222 this.env = env;
223 initializeLogger();
224 }
225
226 /**
227 * Execute this command with the given arguemnts.
228 * The implementation in this class saves the passed arguments in
229 * the argv field and calls the initializePasswords method.
230 * Then it calls the prepare, parse, and validate methods, finally
231 * returning the result of calling the executeCommand method.
232 * Note that argv[0] is the command name.
233 *
234 * @throws CommandException if execution of the command fails
235 * @throws CommandValidationException if there's something wrong
236 * with the options or arguments
237 */
238 public int execute(String... argv) throws CommandException {
239 this.argv = argv;
240 initializePasswords();
241 logger.finer("Prepare");
242 prepare();
243 logger.finer("Process program options");
244 processProgramOptions();
245 logger.finer("Parse command options");
246 parse();
247 if (checkHelp())
248 return 0;
249 logger.finer("Prevalidate command options");
250 prevalidate();
251 logger.finer("Inject command options");
252 inject();
253 logger.finer("Validate command options");
254 validate();
255 if (programOpts.isEcho()) {
256 logger.info(echoCommand());
257 // In order to avoid echoing commands used intenally to the
258 // implementation of *this* command, we turn off echo after
259 // having echoed this command.
260 programOpts.setEcho(false);
261 } else if (logger.isLoggable(Level.FINER))
262 logger.finer(echoCommand());
263 logger.finer("Execute command");
264 return executeCommand();
265 }
266
267 /**
268 * Return the name of this command.
269 */
270 public String getName() {
271 return name;
272 }
273
274 /**
275 * Returns the program options associated with this command.
276 *
277 * @return the command's program options
278 */
279 public ProgramOptions getProgramOptions() {
280 return programOpts;
281 }
282
283 /**
284 * Return a BufferedReader for the man page for this command,
285 * or null if not found.
286 */
287 public BufferedReader getManPage() {
288 String commandName = getName();
289 if (commandName.length() == 0)
290 throw new IllegalArgumentException("Command name cannot be empty");
291
292 // special case "help" --> "asadmin"
293 if (commandName.equals("help"))
294 commandName = "asadmin";
295
296 return ManPageFinder.getCommandManPage(
297 commandName,
298 getClass().getName(),
299 Locale.getDefault(),
300 getClass().getClassLoader(),
301 logger);
302 }
303
304 /**
305 * Get the usage text.
306 *
307 * @return usage text
308 */
309 public String getUsage() {
310 String usage;
311 if (commandModel != null && ok(usage = commandModel.getUsageText())) {
312 StringBuffer usageText = new StringBuffer();
313 usageText.append(
314 strings.get("Usage", strings.get("Usage.asadmin")));
315 usageText.append(" ");
316 usageText.append(usage);
317 return usageText.toString();
318 } else {
319 return generateUsageText();
320 }
321 }
322
323 private String generateUsageText() {
324 StringBuilder usageText = new StringBuilder();
325 usageText.append(strings.get("Usage", strings.get("Usage.asadmin")));
326 usageText.append(" ");
327 usageText.append(getName());
328 int len = usageText.length();
329 StringBuilder optText = new StringBuilder();
330 String lsep = System.getProperty("line.separator");
331 for (ParamModel opt : usageOptions()) {
332 optText.setLength(0);
333 final String optName = lc(opt.getName());
334 // "--terse" is part of asadmin utility options
335 if (optName.equals("terse"))
336 continue;
337 // skip "hidden" options
338 if (optName.startsWith("_"))
339 continue;
340 // do not want to display password as an option
341 if (opt.getParam().password())
342 continue;
343 // also do not want to display obsolete options
344 if (opt.getParam().obsolete())
345 continue;
346 // primary parameter is the operand, not an option
347 if (opt.getParam().primary())
348 continue;
349 boolean optional = opt.getParam().optional();
350 String defValue = opt.getParam().defaultValue();
351 if (optional)
352 optText.append("[");
353 String sn = opt.getParam().shortName();
354 if (ok(sn))
355 optText.append('-').append(sn).append('|');
356 optText.append("--").append(optName);
357
358 if (opt.getType() == Boolean.class ||
359 opt.getType() == boolean.class) {
360 // canonicalize default value
361 if (ok(defValue) && Boolean.parseBoolean(defValue))
362 defValue = "true";
363 else
364 defValue = "false";
365 optText.append("[=<").append(optName);
366 optText.append(strings.get("Usage.default", defValue));
367 optText.append(">]");
368 } else { // STRING or FILE
369 if (ok(defValue)) {
370 optText.append(" <").append(optName);
371 optText.append(strings.get("Usage.default", defValue));
372 optText.append('>');
373 } else
374 optText.append(" <").append(optName).append('>');
375 }
376 if (optional)
377 optText.append("]");
378
379 if (len + 1 + optText.length() > 80) {
380 usageText.append(lsep).append('\t');
381 len = 8;
382 } else {
383 usageText.append(' ');
384 len++;
385 }
386 usageText.append(optText);
387 len += optText.length();
388 }
389
390 // add --help text
391 String helpText = "[-?|--help[=<help(default:false)>]]";
392 if (len + 1 + helpText.length() > 80) {
393 usageText.append(lsep).append('\t');
394 len = 8;
395 } else {
396 usageText.append(' ');
397 len++;
398 }
399 usageText.append(helpText);
400 len += helpText.length();
401
402 optText.setLength(0);
403 ParamModel operandParam = getOperandModel();
404 String opname = operandParam != null ?
405 lc(operandParam.getName()) : null;
406 if (!ok(opname))
407 opname = "operand";
408
409 int operandMin = 0;
410 int operandMax = 0;
411 if (operandParam != null) {
412 operandMin = operandParam.getParam().optional() ? 0 : 1;
413 operandMax = operandParam.getParam().multiple() ?
414 Integer.MAX_VALUE : 1;
415 }
416 if (operandMax > 0) {
417 if (operandMin == 0) {
418 optText.append("[").append(opname);
419 if (operandMax > 1)
420 optText.append(" ...");
421 optText.append("]");
422 } else {
423 optText.append(opname);
424 if (operandMax > 1)
425 optText.append(" ...");
426 }
427 }
428 if (len + 1 + optText.length() > 80) {
429 usageText.append(lsep).append('\t');
430 len = 8;
431 } else {
432 usageText.append(' ');
433 len++;
434 }
435 usageText.append(optText);
436 return usageText.toString();
437 }
438
439 /**
440 * Subclasses can override this method to supply additional
441 * or different options that should be part of the usage text.
442 * Most commands will never need to do this, but the create-domain
443 * command uses it to include the --user option as a required option.
444 */
445 protected Collection<ParamModel> usageOptions() {
446 return commandModel.getParameters();
447 }
448
449 @Override
450 public String toString() {
451 return echoCommand();
452 }
453
454
455 /**
456 * Return a string representing the command line used with this command.
457 */
458 private String echoCommand() {
459 StringBuilder sb = new StringBuilder();
460
461 // first, the program options
462 sb.append("asadmin ");
463 sb.append(programOpts.toString()).append(' ');
464
465 // now the subcommand options and operands
466 sb.append(name).append(' ');
467
468 // have we parsed any options yet?
469 if (options != null && operands != null) {
470 for (ParamModel opt : commandModel.getParameters()) {
471 if (opt.getParam().password())
472 continue; // don't print passwords
473 if (opt.getParam().primary())
474 continue;
475 // include every option that was specified on the command line
476 // and every option that has a default value
477 String value = getOption(opt.getName());
478 if (value == null) {
479 value = opt.getParam().defaultValue();
480 if (value != null && value.length() == 0)
481 value = null;
482 }
483 if (value != null) {
484 sb.append("--").append(lc(opt.getName()));
485 if (opt.getType() == Boolean.class ||
486 opt.getType() == boolean.class) {
487 if (Boolean.parseBoolean(value))
488 sb.append("=").append("true");
489 else
490 sb.append("=").append("false");
491 } else { // STRING or FILE
492 sb.append(" ").append(quote(value));
493 }
494 sb.append(' ');
495 }
496 }
497 for (String o : operands)
498 sb.append(quote(o)).append(' ');
499 } else if (argv != null) {
500 // haven't parsed any options, include raw arguments, if any
501 for (String arg : argv)
502 sb.append(quote(arg)).append(' ');
503 }
504
505 sb.setLength(sb.length() - 1); // strip trailing space
506 return sb.toString();
507 }
508
509 /**
510 * Quote a value, if the value contains any special characters.
511 *
512 * @param value value to be quoted
513 * @return the possibly quoted value
514 */
515 public static String quote(String value) {
516 int len = value.length();
517 if (len == 0)
518 return "\"\""; // an empty string is handled specially
519
520 /*
521 * Look for any special characters. Escape and
522 * quote the entire string if necessary.
523 */
524 boolean needQuoting = false;
525 for (int i = 0; i < len; i++) {
526 char c = value.charAt(i);
527 if (c == '"' || c == '\\' || c == '\r' || c == '\n') {
528 // need to escape them and then quote the whole string
529 StringBuffer sb = new StringBuffer(len + 3);
530 sb.append('"');
531 sb.append(value.substring(0, i));
532 int lastc = 0;
533 for (int j = i; j < len; j++) {
534 char cc = value.charAt(j);
535 if ((cc == '"') || (cc == '\\') ||
536 (cc == '\r') || (cc == '\n'))
537 if (cc == '\n' && lastc == '\r')
538 ; // do nothing, CR was already escaped
539 else
540 sb.append('\\'); // Escape the character
541 sb.append(cc);
542 lastc = cc;
543 }
544 sb.append('"');
545 return sb.toString();
546 } else if (c <= 040 || c >= 0177)
547 // These characters cause the string to be quoted
548 needQuoting = true;
549 }
550
551 if (needQuoting) {
552 StringBuffer sb = new StringBuffer(len + 2);
553 sb.append('"').append(value).append('"');
554 return sb.toString();
555 } else
556 return value;
557 }
558
559 /**
560 * If the program options haven't already been set, parse them
561 * on the command line and remove them from the command line.
562 * Subclasses should call this method in their prepare method
563 * after initializing commandOpts (so usage is available on failure)
564 * if they want to allow program options after the command name.
565 * Currently RemoteCommand does this, as well as the local commands
566 * that also need to talk to the server.
567 */
568 protected void processProgramOptions() throws CommandException {
569 if (!programOpts.isOptionsSet()) {
570 logger.finer("Parsing program options");
571 /*
572 * asadmin options and command options are intermixed.
573 * Parse the entire command line for asadmin options,
574 * removing them from the command line, and ignoring
575 * unknown options.
576 */
577 Parser rcp = new Parser(argv, 0,
578 ProgramOptions.getValidOptions(), true);
579 ParameterMap params = rcp.getOptions();
580 List<String> operands = rcp.getOperands();
581 argv = operands.toArray(new String[operands.size()]);
582 if (params.size() > 0) {
583 // at least one program option specified after command name
584 logger.finer("Update program options");
585 programOpts.updateOptions(params);
586 initializeLogger();
587 initializePasswords();
588 if (!programOpts.isTerse() &&
589 !(params.size() == 1 && params.get("help") != null)) {
590 // warn about deprecated use of program options
591 // (except --help)
592 // XXX - a lot of work for a nice message...
593 Collection<ParamModel> programOptions =
594 ProgramOptions.getValidOptions();
595 StringBuilder sb = new StringBuilder();
596 sb.append("asadmin");
597 for (Map.Entry<String,List<String>> p : params.entrySet()) {
598 // find the corresponding ParamModel
599 ParamModel opt = null;
600 for (ParamModel vo : programOptions) {
601 if (vo.getName().equalsIgnoreCase(p.getKey())) {
602 opt = vo;
603 break;
604 }
605 }
606 if (opt == null) // should never happen
607 continue;
608
609 // format the option appropriately
610 sb.append(" --").append(p.getKey());
611 List<String> pl = p.getValue();
612 // XXX - won't handle multi-values
613 if (opt.getType() == Boolean.class ||
614 opt.getType() == boolean.class) {
615 if (!pl.get(0).equalsIgnoreCase("true"))
616 sb.append("=false");
617 } else {
618 if (pl != null && pl.size() > 0)
619 sb.append(" ").append(pl.get(0));
620 }
621 }
622 sb.append(" ").append(name).append(" [options] ...");
623 logger.info(strings.get("DeprecatedSyntax"));
624 logger.info(sb.toString());
625 }
626 }
627 }
628 }
629
630 /**
631 * Initialize the state of the logger based on any program options.
632 */
633 protected void initializeLogger() {
634 if (!logger.isLoggable(Level.FINER)) {
635 if (programOpts.isTerse())
636 logger.setLevel(Level.INFO);
637 else
638 logger.setLevel(Level.FINE);
639 }
640 }
641
642 /**
643 * Initialize the passwords field based on the password
644 * file specified in the program options, and initialize the
645 * program option's password if available in the password file.
646 */
647 protected void initializePasswords() throws CommandException {
648 passwords = new HashMap<String, String>();
649 String pwfile = programOpts.getPasswordFile();
650
651 if (ok(pwfile)) {
652 passwords = CLIUtil.readPasswordFileOptions(pwfile, true);
653 logger.finer("Passwords were read from password file: " +
654 pwfile);
655 String password = passwords.get(
656 Environment.AS_ADMIN_ENV_PREFIX + "PASSWORD");
657 if (password != null && programOpts.getPassword() == null)
658 programOpts.setPassword(password,
659 ProgramOptions.PasswordLocation.PASSWORD_FILE);
660 }
661 }
662
663 /**
664 * The prepare method must ensure that the commandModel field is set.
665 */
666 protected void prepare() throws CommandException {
667 commandModel = new CommandModelImpl(this.getClass());
668 }
669
670 /**
671 * The parse method sets the options and operands fields
672 * based on the content of the command line arguments.
673 * If the program options say this is a help request,
674 * we set options and operands as if "--help" had been specified.
675 *
676 * @throws CommandException if execution of the command fails
677 * @throws CommandValidationException if there's something wrong
678 * with the options or arguments
679 */
680 protected void parse() throws CommandException {
681 /*
682 * If this is a help request, we don't need the command
683 * metadata and we throw away all the other options and
684 * fake everything else.
685 */
686 if (programOpts.isHelp()) {
687 options = new ParameterMap();
688 options.set("help", "true");
689 operands = Collections.emptyList();
690 } else {
691 Parser rcp =
692 new Parser(argv, 1, commandModel.getParameters(),
693 commandModel.unknownOptionsAreOperands());
694 options = rcp.getOptions();
695 operands = rcp.getOperands();
696
697 /*
698 * In the case where we're accepting unknown options as
699 * operands, the special "--" delimiter will also be
700 * accepted as an operand. We eliminate it here.
701 */
702 if (commandModel.unknownOptionsAreOperands() &&
703 operands.size() > 0 && operands.get(0).equals("--"))
704 operands.remove(0);
705 }
706 logger.finer("params: " + options);
707 logger.finer("operands: " + operands);
708 }
709
710 /**
711 * Check if the current request is a help request, either because
712 * --help was specified as a programoption or a command option.
713 * If so, get the man page using the getManPage method, copy the
714 * content to System.out, and return true. Otherwise return false.
715 * Subclasses may override this method to perform a different check
716 * or to use a different method to display the man page.
717 * If this method returns true, the validate and executeCommand methods
718 * won't be called.
719 */
720 protected boolean checkHelp() throws CommandException {
721 if (programOpts.isHelp()) {
722 BufferedReader br = getManPage();
723 if (br == null)
724 throw new CommandException(strings.get("ManpageMissing", name));
725 String line;
726 try {
727 while ((line = br.readLine()) != null)
728 System.out.println(line);
729 } catch (IOException ioex) {
730 throw new CommandException(
731 strings.get("ManpageMissing", name), ioex);
732 } finally {
733 try {
734 br.close();
735 } catch (IOException ex) {
736 }
737 }
738 return true;
739 } else
740 return false;
741 }
742
743 /**
744 * The prevalidate method supplies missing options from
745 * the environment. It also supplies passwords from the password
746 * file or prompts for them if interactive.
747 *
748 * @throws CommandException if execution of the command fails
749 * @throws CommandValidationException if there's something wrong
750 * with the options or arguments
751 */
752 protected void prevalidate() throws CommandException {
753 /*
754 * First, check that the command has the proper scope.
755 * (Could check this in getCommand(), but at that point we
756 * don't have the CommandModel yet.)
757 * Remote commands are checked on the server.
758 */
759 if (!(this instanceof RemoteCommand)) {
760 Scoped scoped = this.getClass().getAnnotation(Scoped.class);
761 if (scoped == null) {
762 throw new CommandException(strings.get("NoScope", name));
763 } else if (scoped.value() == Singleton.class) {
764 // check that there are no parameters for this command
765 if (commandModel.getParameters().size() > 0) {
766 throw new CommandException(strings.get("HasParams", name));
767 }
768 }
769 }
770
771 /*
772 * Check for missing options and operands.
773 */
774 Console cons = programOpts.isInteractive() ? System.console() : null;
775
776 boolean missingOption = false;
777 for (ParamModel opt : commandModel.getParameters()) {
778 if (opt.getParam().password())
779 continue; // passwords are handled later
780 if (opt.getParam().obsolete() && getOption(opt.getName()) != null)
781 logger.info(
782 strings.get("ObsoleteOption", opt.getName()));
783 if (opt.getParam().optional())
784 continue;
785 if (opt.getParam().primary())
786 continue;
787 // if option isn't set, prompt for it (if interactive)
788 if (getOption(opt.getName()) == null && cons != null &&
789 !missingOption) {
790 cons.printf("%s",
791 strings.get("optionPrompt", lc(opt.getName())));
792 String val = cons.readLine();
793 if (ok(val))
794 options.set(opt.getName(), val);
795 }
796 // if it's still not set, that's an error
797 if (getOption(opt.getName()) == null) {
798 missingOption = true;
799 logger.info(
800 strings.get("missingOption", "--" + opt.getName()));
801 }
802 if (opt.getParam().obsolete()) // a required obsolete option?
803 logger.info(
804 strings.get("ObsoleteOption", opt.getName()));
805 }
806 if (missingOption)
807 throw new CommandValidationException(
808 strings.get("missingOptions", name));
809
810 int operandMin = 0;
811 int operandMax = 0;
812 ParamModel operandParam = getOperandModel();
813 if (operandParam != null) {
814 operandMin = operandParam.getParam().optional() ? 0 : 1;
815 operandMax = operandParam.getParam().multiple() ?
816 Integer.MAX_VALUE : 1;
817 }
818
819 if (operands.size() < operandMin && cons != null) {
820 cons.printf("%s",
821 strings.get("operandPrompt", operandParam.getName()));
822 String val = cons.readLine();
823 if (ok(val)) {
824 operands = new ArrayList<String>();
825 operands.add(val);
826 }
827 }
828 if (operands.size() < operandMin)
829 throw new CommandValidationException(
830 strings.get("notEnoughOperands", name,
831 operandParam.getType()));
832 if (operands.size() > operandMax) {
833 if (operandMax == 0)
834 throw new CommandValidationException(
835 strings.get("noOperandsAllowed", name));
836 else if (operandMax == 1)
837 throw new CommandValidationException(
838 strings.get("tooManyOperands1", name));
839 else
840 throw new CommandValidationException(
841 strings.get("tooManyOperands", name, operandMax));
842 }
843
844 initializeCommandPassword();
845 }
846
847 /**
848 * Inject this instance with the final values of all the command
849 * parameters.
850 *
851 * @throws CommandException if execution of the command fails
852 * @throws CommandValidationException if there's something wrong
853 * with the options or arguments
854 */
855 protected void inject() throws CommandException {
856 // injector expects operands to be in the ParameterMap with the key
857 // "DEFAULT"
858 options.set("DEFAULT", operands);
859
860 // if command has a "terse" option, set it from ProgramOptions
861 if (commandModel.getModelFor("terse") != null)
862 options.set("terse", Boolean.toString(programOpts.isTerse()));
863
864 // initialize the injector.
865 InjectionResolver<Param> injector =
866 new MapInjectionResolver(commandModel, options);
867
868 // inject
869 try {
870 injectionMgr.inject(this, injector);
871 } catch (UnsatisfiedDependencyException e) {
872 throw new CommandValidationException(e.getMessage(), e);
873 }
874 }
875
876 /**
877 * The validate method can be used by a subclass to validate
878 * that the type and quantity of parameters and operands matches
879 * the requirements for this command.
880 *
881 * @throws CommandException if execution of the command fails
882 * @throws CommandValidationException if there's something wrong
883 * with the options or arguments
884 */
885 protected void validate() throws CommandException {
886 }
887
888 /**
889 * Execute the command using the options in options and the
890 * operands in operands.
891 *
892 * @return the exit code
893 * @throws CommandException if execution of the command fails
894 * @throws CommandValidationException if there's something wrong
895 * with the options or arguments
896 */
897 protected abstract int executeCommand() throws CommandException;
898
899 /**
900 * Initialize all the passwords required by the command.
901 *
902 * @throws CommandException
903 */
904 private void initializeCommandPassword() throws CommandException {
905 /*
906 * Go through all the valid options and check for required password
907 * options that weren't specified in the password file. If option
908 * is missing and we're interactive, prompt for it. Store the
909 * password as if it was a parameter.
910 */
911 for (ParamModel opt : commandModel.getParameters()) {
912 if (!opt.getParam().password())
913 continue;
914 String pwdname = opt.getName();
915 String pwd = getPassword(opt, null, true);
916 if (pwd == null) {
917 if (opt.getParam().optional())
918 continue; // not required, skip it
919 // if not terse, provide more advice about what to do
920 String msg;
921 if (programOpts.isTerse())
922 msg = strings.get("missingPassword", name, pwdname);
923 else
924 msg = strings.get("missingPasswordAdvice", name, pwdname);
925 throw new CommandValidationException(msg);
926 }
927 options.set(pwdname, pwd);
928 }
929 }
930
931 /**
932 * Get a password for the given option.
933 * First, look in the passwords map. If found, return it.
934 * If not found, and not required, return null;
935 * If not interactive, return null. Otherwise, prompt for the
936 * password. If create is true, prompt twice and compare the two values
937 * to make sure they're the same. If the password meets other validity
938 * criteria (i.e., length) returns the password. If defaultPassword is
939 * not null, "Enter" selects this default password, which is returned.
940 */
941 protected String getPassword(ParamModel opt, String defaultPassword,
942 boolean create) throws CommandValidationException {
943
944 String passwordName = opt.getName();
945 String password = passwords.get(passwordName);
946 if (password != null)
947 return password;
948
949 if (opt.getParam().optional())
950 return null; // not required
951
952 if (!programOpts.isInteractive())
953 return null; // can't prompt for it
954
955 String description = null;
956 if (opt instanceof ParamModelData)
957 description = ((ParamModelData)opt).getDescription();
958 String newprompt;
959 if (ok(description)) {
960 if (defaultPassword != null) {
961 if (defaultPassword.length() == 0)
962 newprompt =
963 strings.get("NewPasswordDescriptionDefaultEmptyPrompt",
964 description);
965 else
966 newprompt =
967 strings.get("NewPasswordDescriptionDefaultPrompt",
968 description, defaultPassword);
969 } else
970 newprompt =
971 strings.get("NewPasswordDescriptionPrompt", description);
972 } else {
973 if (defaultPassword != null) {
974 if (defaultPassword.length() == 0)
975 newprompt =
976 strings.get("NewPasswordDefaultEmptyPrompt",
977 passwordName);
978 else
979 newprompt =
980 strings.get("NewPasswordDefaultPrompt",
981 passwordName, defaultPassword);
982 } else
983 newprompt = strings.get("NewPasswordPrompt", passwordName);
984 }
985
986 String newpassword = readPassword(newprompt);
987
988 /*
989 * If we allow for a default password, and the user just hit "Enter",
990 * return the default password. No need to prompt twice or check
991 * for validity.
992 */
993 if (defaultPassword != null) {
994 if (newpassword == null)
995 newpassword = "";
996 if (newpassword.length() == 0) {
997 newpassword = defaultPassword;
998 passwords.put(passwordName, newpassword);
999 return newpassword;
1000 }
1001 }
1002
1003 /*
1004 * If not creating a new password, don't need to verify that
1005 * the user typed it correctly by making them type it twice,
1006 * and don't need to check it for validity. Just return what
1007 * we have.
1008 */
1009 if (!create) {
1010 passwords.put(passwordName, newpassword);
1011 return newpassword;
1012 }
1013
1014 String confirmationPrompt;
1015 if (ok(description)) {
1016 confirmationPrompt =
1017 strings.get("NewPasswordDescriptionConfirmationPrompt",
1018 description);
1019 } else {
1020 confirmationPrompt =
1021 strings.get("NewPasswordConfirmationPrompt", passwordName);
1022 }
1023 String newpasswordAgain = readPassword(confirmationPrompt);
1024 if (!newpassword.equals(newpasswordAgain)) {
1025 throw new CommandValidationException(
1026 strings.get("OptionsDoNotMatch",
1027 ok(description) ? description : passwordName));
1028 }
1029 passwords.put(passwordName, newpassword);
1030 return newpassword;
1031 }
1032
1033 /**
1034 * Display the given prompt and read a password without echoing it.
1035 * Returns null if no console available.
1036 */
1037 protected String readPassword(String prompt) {
1038 String password = null;
1039 Console cons = System.console();
1040 if (cons != null) {
1041 char[] pc = cons.readPassword("%s", prompt);
1042 // yes, yes, yes, it would be safer to not keep it in a String
1043 password = new String(pc);
1044 }
1045 return password;
1046 }
1047
1048 /**
1049 * Get the ParamModel that corresponds to the operand
1050 * (primary parameter). Return null if none.
1051 */
1052 protected ParamModel getOperandModel() {
1053 for (ParamModel pm : commandModel.getParameters()) {
1054 if (pm.getParam().primary())
1055 return pm;
1056 }
1057 return null;
1058 }
1059
1060 /**
1061 * Get an option value, that might come from the command line
1062 * or from the environment. Return the default value for the
1063 * option if not otherwise specified.
1064 */
1065 protected String getOption(String name) {
1066 String val = options.getOne(name);
1067 if (val == null)
1068 val = env.getStringOption(name);
1069 if (val == null) {
1070 // no value, find the default
1071 ParamModel opt = commandModel.getModelFor(name);
1072 // if no value was specified and there's a default value, return it
1073 if (opt != null) {
1074 String def = opt.getParam().defaultValue();
1075 if (ok(def))
1076 val = def;
1077 }
1078 }
1079 return val;
1080 }
1081
1082 /**
1083 * Get a boolean option value, that might come from the command line
1084 * or from the environment.
1085 */
1086 protected boolean getBooleanOption(String name) {
1087 String val = getOption(name);
1088 return val != null && Boolean.parseBoolean(val);
1089 }
1090
1091 /**
1092 * Return the named system property, or property
1093 * set in asenv.conf.
1094 */
1095 protected String getSystemProperty(String name) {
1096 return systemProps.get(name);
1097 }
1098
1099 /**
1100 * Return all the system properties and properties set
1101 * in asenv.conf. The returned Map may not be modified.
1102 */
1103 protected Map<String,String> getSystemProperties() {
1104 return systemProps;
1105 }
1106
1107 /**
1108 * If this is an unsupported command, throw an exception.
1109 */
1110 private static void checkUnsupportedLegacyCommand(String cmd)
1111 throws CommandException {
1112 for (String c : unsupported) {
1113 if (c.equals(cmd)) {
1114 throw new CommandException(
1115 strings.get("UnsupportedLegacyCommand", cmd));
1116 }
1117 }
1118 // it is a supported command; do nothing
1119 }
1120
1121 /**
1122 * Prints the exception message with level as FINER.
1123 *
1124 * @param e the exception object to print
1125 */
1126 protected void printExceptionStackTrace(java.lang.Throwable e) {
1127 if (!logger.isLoggable(Level.FINER))
1128 return;
1129 /*
1130 java.lang.StackTraceElement[] ste = e.getStackTrace();
1131 for (int ii = 0; ii < ste.length; ii++)
1132 printDebugMessage(ste[ii].toString());
1133 */
1134 final ByteArrayOutputStream output = new ByteArrayOutputStream(512);
1135 e.printStackTrace(new java.io.PrintStream(output));
1136 try {
1137 output.close();
1138 } catch (IOException ex) {
1139 // ignore
1140 }
1141 logger.finer(output.toString());
1142 }
1143
1144 protected static boolean ok(String s) {
1145 return s != null && s.length() > 0;
1146 }
1147
1148 // shorthand for this too-verbose operation
1149 private static String lc(String s) {
1150 return s.toLowerCase(Locale.ENGLISH);
1151 }
1152
1153 /**
1154 * Read the named resource file and add the first token on each line
1155 * to the set. Skip comment lines.
1156 */
1157 private static void file2Set(String file, Set<String> set) {
1158 BufferedReader reader = null;
1159 try {
1160 InputStream is = CLICommand.class.getClassLoader().
1161 getResourceAsStream(file);
1162 if (is == null)
1163 return; // in case the resource doesn't exist
1164 reader = new BufferedReader(new InputStreamReader(is));
1165 String line;
1166 while ((line = reader.readLine()) != null) {
1167 if (line.startsWith("#"))
1168 continue; // # indicates comment
1169 StringTokenizer tok = new StringTokenizer(line, " ");
1170 // handles with or without space, rudimendary as of now
1171 String cmd = tok.nextToken();
1172 set.add(cmd);
1173 }
1174 } catch (IOException e) {
1175 e.printStackTrace();
1176 } finally {
1177 if (reader != null) {
1178 try {
1179 reader.close();
1180 } catch (IOException ee) {
1181 // ignore
1182 }
1183
1184 }
1185 }
1186 }
1187 }