001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS.
026 */
027package org.opends.server.tools;
028
029import org.forgerock.i18n.LocalizableMessage;
030import org.forgerock.opendj.ldap.DecodeException;
031import org.forgerock.opendj.config.server.ConfigException;
032import org.opends.server.core.DirectoryServer;
033import org.opends.server.loggers.JDKLogging;
034import org.opends.server.tools.tasks.TaskClient;
035import org.opends.server.tools.tasks.TaskEntry;
036import org.opends.server.types.InitializationException;
037import org.opends.server.types.LDAPException;
038import org.opends.server.util.BuildVersion;
039import org.opends.server.util.StaticUtils;
040
041import com.forgerock.opendj.cli.ArgumentException;
042import com.forgerock.opendj.cli.BooleanArgument;
043import com.forgerock.opendj.cli.ClientException;
044import com.forgerock.opendj.cli.CommonArguments;
045import com.forgerock.opendj.cli.StringArgument;
046
047import org.opends.server.backends.task.TaskState;
048import org.opends.server.util.args.LDAPConnectionArgumentParser;
049import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;
050
051import com.forgerock.opendj.cli.ConsoleApplication;
052import com.forgerock.opendj.cli.Menu;
053import com.forgerock.opendj.cli.MenuBuilder;
054import com.forgerock.opendj.cli.MenuCallback;
055import com.forgerock.opendj.cli.MenuResult;
056import com.forgerock.opendj.cli.TableBuilder;
057import com.forgerock.opendj.cli.TextTablePrinter;
058
059import java.io.IOException;
060import java.io.InputStream;
061import java.io.OutputStream;
062import java.io.PrintStream;
063import java.io.StringWriter;
064import java.util.ArrayList;
065import java.util.List;
066import java.util.Map;
067import java.util.TreeMap;
068
069import static org.opends.messages.ToolMessages.*;
070
071import static com.forgerock.opendj.cli.ArgumentConstants.*;
072import static com.forgerock.opendj.cli.Utils.filterExitCode;
073
074/**
075 * Tool for getting information and managing tasks in the Directory Server.
076 */
077public class ManageTasks extends ConsoleApplication {
078
079  /** This CLI is always using the administration connector with SSL. */
080  private static final boolean alwaysSSL = true;
081
082  /**
083   * The main method for TaskInfo tool.
084   *
085   * @param args The command-line arguments provided to this program.
086   */
087  public static void main(String[] args) {
088    int retCode = mainTaskInfo(args, System.in, System.out, System.err);
089
090    if (retCode != 0) {
091      System.exit(filterExitCode(retCode));
092    }
093  }
094
095  /**
096   * Processes the command-line arguments and invokes the process for
097   * displaying task information.
098   *
099   * @param args The command-line arguments provided to this program.
100   * @return int return code
101   */
102  public static int mainTaskInfo(String[] args) {
103    return mainTaskInfo(args, System.in, System.out, System.err);
104  }
105
106  /**
107   * Processes the command-line arguments and invokes the export process.
108   *
109   * @param args             The command-line arguments provided to this
110   * @param in               Input stream from which to solicit user input.
111   * @param out              The output stream to use for standard output, or
112   *                         {@code null} if standard output is not needed.
113   * @param err              The output stream to use for standard error, or
114   *                         {@code null} if standard error is not needed.
115   * @param initializeServer Indicates whether to initialize the server.
116   * @return int return code
117   */
118  public static int mainTaskInfo(String[] args,
119                                 InputStream in,
120                                 OutputStream out,
121                                 OutputStream err,
122                                 boolean initializeServer) {
123    ManageTasks tool = new ManageTasks(in, out, err);
124    return tool.process(args, initializeServer);
125  }
126
127  /**
128   * Processes the command-line arguments and invokes the export process.
129   *
130   * @param args             The command-line arguments provided to this
131   * @param in               Input stream from which to solicit user input.
132   * @param out              The output stream to use for standard output, or
133   *                         {@code null} if standard output is not needed.
134   * @param err              The output stream to use for standard error, or
135   *                         {@code null} if standard error is not needed.
136   * @return int return code
137   */
138  public static int mainTaskInfo(String[] args,
139                                 InputStream in,
140                                 OutputStream out,
141                                 OutputStream err) {
142    return mainTaskInfo(args, in, out, err, true);
143  }
144
145  private static final int INDENT = 2;
146
147  /**
148   * ID of task for which to display details and exit.
149   */
150  private StringArgument task;
151
152  /**
153   * Indicates print summary and exit.
154   */
155  private BooleanArgument summary;
156
157  /**
158   * ID of task to cancel.
159   */
160  private StringArgument cancel;
161
162  /**
163   * Argument used to request non-interactive behavior.
164   */
165  private BooleanArgument noPrompt;
166
167  /**
168   * Accesses the directory's task backend.
169   */
170  private TaskClient taskClient;
171
172  /**
173   * Constructs a parameterized instance.
174   *
175   * @param in               Input stream from which to solicit user input.
176   * @param out              The output stream to use for standard output, or
177   *                         {@code null} if standard output is not needed.
178   * @param err              The output stream to use for standard error, or
179   *                         {@code null} if standard error is not needed.
180   */
181  public ManageTasks(InputStream in, OutputStream out, OutputStream err)
182  {
183    super(new PrintStream(out), new PrintStream(err));
184  }
185
186  /**
187   * Processes the command-line arguments and invokes the export process.
188   *
189   * @param args       The command-line arguments provided to this
190   *                   program.
191   * @return The error code.
192   */
193  public int process(String[] args)
194  {
195    return process(args, true);
196  }
197
198  /**
199   * Processes the command-line arguments and invokes the export process.
200   *
201   * @param args       The command-line arguments provided to this
202   *                   program.
203   * @param initializeServer  Indicates whether to initialize the server.
204   * @return The error code.
205   */
206  public int process(String[] args, boolean initializeServer)
207  {
208    if (initializeServer)
209    {
210      DirectoryServer.bootstrapClient();
211    }
212    JDKLogging.disableLogging();
213
214
215    // Create the command-line argument parser for use with this program.
216    LDAPConnectionArgumentParser argParser = new LDAPConnectionArgumentParser(
217            "org.opends.server.tools.TaskInfo",
218            INFO_TASKINFO_TOOL_DESCRIPTION.get(),
219            false, null, alwaysSSL);
220    argParser.setShortToolDescription(REF_SHORT_DESC_MANAGE_TASKS.get());
221
222    // Initialize all the command-line argument types and register them with the
223    // parser.
224    try {
225
226       StringArgument propertiesFileArgument = new StringArgument(
227          "propertiesFilePath", null, OPTION_LONG_PROP_FILE_PATH, false, false,
228          true, INFO_PROP_FILE_PATH_PLACEHOLDER.get(), null, null,
229          INFO_DESCRIPTION_PROP_FILE_PATH.get());
230      argParser.addArgument(propertiesFileArgument);
231      argParser.setFilePropertiesArgument(propertiesFileArgument);
232
233      BooleanArgument noPropertiesFileArgument = new BooleanArgument(
234          "noPropertiesFileArgument", null, OPTION_LONG_NO_PROP_FILE,
235          INFO_DESCRIPTION_NO_PROP_FILE.get());
236      argParser.addArgument(noPropertiesFileArgument);
237      argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
238
239      task = new StringArgument(
240              "info", 'i', "info",
241              false, true, INFO_TASK_ID_PLACEHOLDER.get(),
242              INFO_TASKINFO_TASK_ARG_DESCRIPTION.get());
243      argParser.addArgument(task);
244
245      cancel = new StringArgument(
246              "cancel", 'c', "cancel",
247              false, true, INFO_TASK_ID_PLACEHOLDER.get(),
248              INFO_TASKINFO_TASK_ARG_CANCEL.get());
249      argParser.addArgument(cancel);
250
251      summary = new BooleanArgument(
252              "summary", 's', "summary",
253              INFO_TASKINFO_SUMMARY_ARG_DESCRIPTION.get());
254      argParser.addArgument(summary);
255
256      noPrompt = CommonArguments.getNoPrompt();
257      argParser.addArgument(noPrompt);
258
259      BooleanArgument displayUsage = CommonArguments.getShowUsage();
260      argParser.addArgument(displayUsage);
261      argParser.setUsageArgument(displayUsage);
262    }
263    catch (ArgumentException ae) {
264      LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
265      println(message);
266      return 1;
267    }
268
269    try
270    {
271      argParser.getArguments().initArgumentsWithConfiguration();
272    }
273    catch (ConfigException ce)
274    {
275      // Ignore.
276    }
277
278    // Parse the command-line arguments provided to this program.
279    try {
280      argParser.parseArguments(args);
281      StaticUtils.checkOnlyOneArgPresent(task, summary, cancel);
282    }
283    catch (ArgumentException ae) {
284      argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
285      return 1;
286    }
287
288    if (!argParser.usageOrVersionDisplayed()) {
289      // Checks the version - if upgrade required, the tool is unusable
290      try
291      {
292        BuildVersion.checkVersionMismatch();
293      }
294      catch (InitializationException e)
295      {
296        println(e.getMessageObject());
297        return 1;
298      }
299
300      try {
301        LDAPConnectionConsoleInteraction ui =
302                new LDAPConnectionConsoleInteraction(
303                        this, argParser.getArguments());
304
305        taskClient = new TaskClient(argParser.connect(ui,
306                getOutputStream(), getErrorStream()));
307
308        if (isMenuDrivenMode()) {
309
310          // Keep prompting the user until they specify quit of
311          // there is a fatal exception
312          while (true) {
313            getOutputStream().println();
314            Menu<Void> menu = getSummaryMenu();
315            MenuResult<Void> result = menu.run();
316            if (result.isQuit()) {
317              return 0;
318            }
319          }
320
321        } else if (task.isPresent()) {
322          getOutputStream().println();
323          MenuResult<TaskEntry> r =
324                  new PrintTaskInfo(task.getValue()).invoke(this);
325          if (r.isAgain())
326          {
327            return 1;
328          }
329        } else if (summary.isPresent()) {
330          getOutputStream().println();
331          printSummaryTable();
332        } else if (cancel.isPresent()) {
333          MenuResult<TaskEntry> r =
334                  new CancelTask(cancel.getValue()).invoke(this);
335          if (r.isAgain())
336          {
337            return 1;
338          }
339        } else if (!isInteractive()) {
340           // no-prompt option
341           getOutputStream().println();
342           printSummaryTable();
343           return 0;
344        }
345
346      } catch (LDAPConnectionException lce) {
347        println(INFO_TASKINFO_LDAP_EXCEPTION.get(lce.getMessageObject()));
348        return 1;
349      } catch (Exception e) {
350        println(LocalizableMessage.raw(StaticUtils.getExceptionMessage(e)));
351        return 1;
352      }
353    }
354    return 0;
355  }
356
357  /** {@inheritDoc} */
358  @Override
359  public boolean isAdvancedMode() {
360    return false;
361  }
362
363  /** {@inheritDoc} */
364  @Override
365  public boolean isInteractive() {
366    return !noPrompt.isPresent();
367  }
368
369  /** {@inheritDoc} */
370  @Override
371  public boolean isMenuDrivenMode() {
372    return !task.isPresent() && !cancel.isPresent() && !summary.isPresent() &&
373           !noPrompt.isPresent();
374  }
375
376  /** {@inheritDoc} */
377  @Override
378  public boolean isQuiet() {
379    return false;
380  }
381
382  /** {@inheritDoc} */
383  @Override
384  public boolean isScriptFriendly() {
385    return false;
386  }
387
388  /** {@inheritDoc} */
389  @Override
390  public boolean isVerbose() {
391    return false;
392  }
393
394  /**
395   * Creates the summary table.
396   *
397   * @throws IOException if there is a problem with screen I/O
398   * @throws LDAPException if there is a problem getting information
399   *         out to the directory
400   * @throws DecodeException if there is a problem with the encoding
401   */
402  private void printSummaryTable()
403          throws LDAPException, IOException, DecodeException {
404    List<TaskEntry> entries = taskClient.getTaskEntries();
405    if (!entries.isEmpty()) {
406      TableBuilder table = new TableBuilder();
407      Map<String, TaskEntry> mapIdToEntry = new TreeMap<>();
408      for (TaskEntry entry : entries) {
409        String taskId = entry.getId();
410        if (taskId != null) {
411          mapIdToEntry.put(taskId, entry);
412        }
413      }
414
415      table.appendHeading(INFO_TASKINFO_FIELD_ID.get());
416      table.appendHeading(INFO_TASKINFO_FIELD_TYPE.get());
417      table.appendHeading(INFO_TASKINFO_FIELD_STATUS.get());
418      for (String taskId : mapIdToEntry.keySet()) {
419        TaskEntry entryWrapper = mapIdToEntry.get(taskId);
420        table.startRow();
421        table.appendCell(taskId);
422        table.appendCell(entryWrapper.getType());
423        table.appendCell(entryWrapper.getState());
424      }
425      StringWriter sw = new StringWriter();
426      TextTablePrinter tablePrinter = new TextTablePrinter(sw);
427      tablePrinter.setIndentWidth(INDENT);
428      tablePrinter.setTotalWidth(80);
429      table.print(tablePrinter);
430      getOutputStream().println(LocalizableMessage.raw(sw.getBuffer()));
431    } else {
432      getOutputStream().println(INFO_TASKINFO_NO_TASKS.get());
433      getOutputStream().println();
434    }
435  }
436
437  /**
438   * Creates the summary table.
439   *
440   * @return list of strings of IDs of all the tasks in the table in order
441   *         of the indexes printed in the table
442   * @throws IOException if there is a problem with screen I/O
443   * @throws LDAPException if there is a problem getting information
444   *         out to the directory
445   * @throws DecodeException if there is a problem with the encoding
446   */
447  private Menu<Void> getSummaryMenu()
448          throws LDAPException, IOException, DecodeException {
449    List<String> taskIds = new ArrayList<>();
450    List<Integer> cancelableIndices = new ArrayList<>();
451    List<TaskEntry> entries = taskClient.getTaskEntries();
452    MenuBuilder<Void> menuBuilder = new MenuBuilder<>(this);
453    if (!entries.isEmpty()) {
454      Map<String, TaskEntry> mapIdToEntry = new TreeMap<>();
455      for (TaskEntry entry : entries) {
456        String taskId = entry.getId();
457        if (taskId != null) {
458          mapIdToEntry.put(taskId, entry);
459        }
460      }
461
462      menuBuilder.setColumnHeadings(
463              INFO_TASKINFO_FIELD_ID.get(),
464              INFO_TASKINFO_FIELD_TYPE.get(),
465              INFO_TASKINFO_FIELD_STATUS.get());
466      menuBuilder.setColumnWidths(null, null, 0);
467      int index = 0;
468      for (final String taskId : mapIdToEntry.keySet()) {
469        taskIds.add(taskId);
470        final TaskEntry taskEntry = mapIdToEntry.get(taskId);
471        menuBuilder.addNumberedOption(
472                LocalizableMessage.raw(taskEntry.getId()),
473                new TaskDrilldownMenu(taskId),
474                taskEntry.getType(), taskEntry.getState());
475        index++;
476        if (taskEntry.isCancelable()) {
477          cancelableIndices.add(index);
478        }
479      }
480    } else {
481      getOutputStream().println(INFO_TASKINFO_NO_TASKS.get());
482      getOutputStream().println();
483    }
484
485    menuBuilder.addCharOption(
486            INFO_TASKINFO_CMD_REFRESH_CHAR.get(),
487            INFO_TASKINFO_CMD_REFRESH.get(),
488            new PrintSummaryTop());
489
490    if (!cancelableIndices.isEmpty()) {
491      menuBuilder.addCharOption(
492              INFO_TASKINFO_CMD_CANCEL_CHAR.get(),
493              INFO_TASKINFO_CMD_CANCEL.get(),
494              new CancelTaskTop(taskIds, cancelableIndices));
495    }
496    menuBuilder.addQuitOption();
497
498    return menuBuilder.toMenu();
499  }
500
501  /**
502   * Gets the client that can be used to interact with the task backend.
503   *
504   * @return TaskClient for interacting with the task backend.
505   */
506  public TaskClient getTaskClient() {
507    return this.taskClient;
508  }
509
510  /**
511   * Base for callbacks that implement top level menu items.
512   */
513  private static abstract class TopMenuCallback
514          implements MenuCallback<Void> {
515
516    /** {@inheritDoc} */
517    @Override
518    public MenuResult<Void> invoke(ConsoleApplication app) throws ClientException {
519      return invoke((ManageTasks)app);
520    }
521
522    /**
523     * Called upon task invocation.
524     *
525     * @param app this console application
526     * @return MessageResult result of task
527     * @throws ClientException if there is a problem
528     */
529    protected abstract MenuResult<Void> invoke(ManageTasks app)
530            throws ClientException;
531
532  }
533
534  /**
535   * Base for callbacks that manage task entries.
536   */
537  private static abstract class TaskOperationCallback
538          implements MenuCallback<TaskEntry> {
539
540    /** ID of the task to manage. */
541    protected String taskId;
542
543    /**
544     * Constructs a parameterized instance.
545     *
546     * @param taskId if the task to examine
547     */
548    public TaskOperationCallback(String taskId) {
549      this.taskId = taskId;
550    }
551
552    /** {@inheritDoc} */
553    @Override
554    public MenuResult<TaskEntry> invoke(ConsoleApplication app)
555            throws ClientException
556    {
557      return invoke((ManageTasks)app);
558    }
559
560    /**
561     * Invokes the task.
562     *
563     * @param app
564     *          the current application running
565     * @return how the application should proceed next
566     * @throws ClientException
567     *           if any problem occurred
568     */
569    protected abstract MenuResult<TaskEntry> invoke(ManageTasks app)
570            throws ClientException;
571
572  }
573
574  /**
575   * Executable for printing a task summary table.
576   */
577  private static class PrintSummaryTop extends TopMenuCallback {
578
579    @Override
580    public MenuResult<Void> invoke(ManageTasks app)
581            throws ClientException
582    {
583      // Since the summary table is reprinted every time the
584      // user enters the top level this task just returns
585      // 'success'
586      return MenuResult.success();
587    }
588  }
589
590  /**
591   * Executable for printing a particular task's details.
592   */
593  private static class TaskDrilldownMenu extends TopMenuCallback {
594
595    private String taskId;
596
597    /**
598     * Constructs a parameterized instance.
599     *
600     * @param taskId of the task for which information will be displayed
601     */
602    public TaskDrilldownMenu(String taskId) {
603      this.taskId = taskId;
604    }
605
606    /** {@inheritDoc} */
607    @Override
608    public MenuResult<Void> invoke(ManageTasks app) throws ClientException {
609      MenuResult<TaskEntry> res = new PrintTaskInfo(taskId).invoke(app);
610      TaskEntry taskEntry = res.getValue();
611      if (taskEntry != null) {
612        while (true) {
613          try {
614            taskEntry = app.getTaskClient().getTaskEntry(taskId);
615
616            // Show the menu
617            MenuBuilder<TaskEntry> menuBuilder = new MenuBuilder<>(app);
618            menuBuilder.addBackOption(true);
619            menuBuilder.addCharOption(
620                    INFO_TASKINFO_CMD_REFRESH_CHAR.get(),
621                    INFO_TASKINFO_CMD_REFRESH.get(),
622                    new PrintTaskInfo(taskId));
623            List<LocalizableMessage> logs = taskEntry.getLogMessages();
624            if (logs != null && !logs.isEmpty()) {
625              menuBuilder.addCharOption(
626                      INFO_TASKINFO_CMD_VIEW_LOGS_CHAR.get(),
627                      INFO_TASKINFO_CMD_VIEW_LOGS.get(),
628                      new ViewTaskLogs(taskId));
629            }
630            if (taskEntry.isCancelable() && !taskEntry.isDone()) {
631              menuBuilder.addCharOption(
632                      INFO_TASKINFO_CMD_CANCEL_CHAR.get(),
633                      INFO_TASKINFO_CMD_CANCEL.get(),
634                      new CancelTask(taskId));
635            }
636            menuBuilder.addQuitOption();
637            Menu<TaskEntry> menu = menuBuilder.toMenu();
638            MenuResult<TaskEntry> result = menu.run();
639            if (result.isCancel()) {
640              break;
641            } else if (result.isQuit()) {
642              System.exit(0);
643            }
644          } catch (Exception e) {
645            app.println(LocalizableMessage.raw(StaticUtils.getExceptionMessage(e)));
646          }
647        }
648      } else {
649        app.println(ERR_TASKINFO_UNKNOWN_TASK_ENTRY.get(taskId));
650      }
651      return MenuResult.success();
652    }
653
654  }
655
656  /**
657   * Executable for printing a particular task's details.
658   */
659  private static class PrintTaskInfo extends TaskOperationCallback {
660
661    /**
662     * Constructs a parameterized instance.
663     *
664     * @param taskId of the task for which information will be printed
665     */
666    public PrintTaskInfo(String taskId) {
667      super(taskId);
668    }
669
670    /** {@inheritDoc} */
671    @Override
672    public MenuResult<TaskEntry> invoke(ManageTasks app)
673            throws ClientException
674    {
675      LocalizableMessage m;
676      TaskEntry taskEntry;
677      try {
678        taskEntry = app.getTaskClient().getTaskEntry(taskId);
679
680        TableBuilder table = new TableBuilder();
681        table.appendHeading(INFO_TASKINFO_DETAILS.get());
682
683        table.startRow();
684        table.appendCell(INFO_TASKINFO_FIELD_ID.get());
685        table.appendCell(taskEntry.getId());
686
687        table.startRow();
688        table.appendCell(INFO_TASKINFO_FIELD_TYPE.get());
689        table.appendCell(taskEntry.getType());
690
691        table.startRow();
692        table.appendCell(INFO_TASKINFO_FIELD_STATUS.get());
693        table.appendCell(taskEntry.getState());
694
695        table.startRow();
696        table.appendCell(INFO_TASKINFO_FIELD_SCHEDULED_START.get());
697
698        if (TaskState.isRecurring(taskEntry.getTaskState())) {
699          m = taskEntry.getScheduleTab();
700          table.appendCell(m);
701        } else {
702          m = taskEntry.getScheduledStartTime();
703          if (m == null || m.equals(LocalizableMessage.EMPTY)) {
704            table.appendCell(INFO_TASKINFO_IMMEDIATE_EXECUTION.get());
705          } else {
706            table.appendCell(m);
707          }
708
709          table.startRow();
710          table.appendCell(INFO_TASKINFO_FIELD_ACTUAL_START.get());
711          table.appendCell(taskEntry.getActualStartTime());
712
713          table.startRow();
714          table.appendCell(INFO_TASKINFO_FIELD_COMPLETION_TIME.get());
715          table.appendCell(taskEntry.getCompletionTime());
716        }
717
718        writeMultiValueCells(
719                table,
720                INFO_TASKINFO_FIELD_DEPENDENCY.get(),
721                taskEntry.getDependencyIds());
722
723        table.startRow();
724        table.appendCell(INFO_TASKINFO_FIELD_FAILED_DEPENDENCY_ACTION.get());
725        m = taskEntry.getFailedDependencyAction();
726        table.appendCell(m != null ? m : INFO_TASKINFO_NONE.get());
727
728        writeMultiValueCells(
729                table,
730                INFO_TASKINFO_FIELD_NOTIFY_ON_COMPLETION.get(),
731                taskEntry.getCompletionNotificationEmailAddresses(),
732                INFO_TASKINFO_NONE_SPECIFIED.get());
733
734        writeMultiValueCells(
735                table,
736                INFO_TASKINFO_FIELD_NOTIFY_ON_ERROR.get(),
737                taskEntry.getErrorNotificationEmailAddresses(),
738                INFO_TASKINFO_NONE_SPECIFIED.get());
739
740        StringWriter sw = new StringWriter();
741        TextTablePrinter tablePrinter = new TextTablePrinter(sw);
742        tablePrinter.setTotalWidth(80);
743        tablePrinter.setIndentWidth(INDENT);
744        tablePrinter.setColumnWidth(1, 0);
745        table.print(tablePrinter);
746        app.getOutputStream().println();
747        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
748
749        // Create a table for the task options
750        table = new TableBuilder();
751        table.appendHeading(INFO_TASKINFO_OPTIONS.get(taskEntry.getType()));
752        Map<LocalizableMessage,List<String>> taskSpecificAttrs =
753                taskEntry.getTaskSpecificAttributeValuePairs();
754        for (LocalizableMessage attrName : taskSpecificAttrs.keySet()) {
755          table.startRow();
756          table.appendCell(attrName);
757          List<String> values = taskSpecificAttrs.get(attrName);
758          if (!values.isEmpty()) {
759            table.appendCell(values.get(0));
760          }
761          if (values.size() > 1) {
762            for (int i = 1; i < values.size(); i++) {
763              table.startRow();
764              table.appendCell();
765              table.appendCell(values.get(i));
766            }
767          }
768        }
769        sw = new StringWriter();
770        tablePrinter = new TextTablePrinter(sw);
771        tablePrinter.setTotalWidth(80);
772        tablePrinter.setIndentWidth(INDENT);
773        tablePrinter.setColumnWidth(1, 0);
774        table.print(tablePrinter);
775        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
776
777        // Print the last log message if any
778        List<LocalizableMessage> logs = taskEntry.getLogMessages();
779        if (logs != null && !logs.isEmpty()) {
780          // Create a table for the last log entry
781          table = new TableBuilder();
782          table.appendHeading(INFO_TASKINFO_FIELD_LAST_LOG.get());
783          table.startRow();
784          table.appendCell(logs.get(logs.size() - 1));
785
786          sw = new StringWriter();
787          tablePrinter = new TextTablePrinter(sw);
788          tablePrinter.setTotalWidth(80);
789          tablePrinter.setIndentWidth(INDENT);
790          tablePrinter.setColumnWidth(0, 0);
791          table.print(tablePrinter);
792          app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
793        }
794
795        app.getOutputStream().println();
796      } catch (Exception e) {
797        app.println(ERR_TASKINFO_RETRIEVING_TASK_ENTRY.get(taskId, e.getMessage()));
798        return MenuResult.again();
799      }
800      return MenuResult.success(taskEntry);
801    }
802
803    /**
804     * Writes an attribute and associated values to the table.
805     * @param table of task details
806     * @param fieldLabel of attribute
807     * @param values of the attribute
808     */
809    private void writeMultiValueCells(TableBuilder table,
810                                      LocalizableMessage fieldLabel,
811                                      List<?> values) {
812      writeMultiValueCells(table, fieldLabel, values, INFO_TASKINFO_NONE.get());
813    }
814
815    /**
816     * Writes an attribute and associated values to the table.
817     *
818     * @param table of task details
819     * @param fieldLabel of attribute
820     * @param values of the attribute
821     * @param noneLabel label for the value column when there are no values
822     */
823    private void writeMultiValueCells(TableBuilder table,
824                                      LocalizableMessage fieldLabel,
825                                      List<?> values,
826                                      LocalizableMessage noneLabel) {
827      table.startRow();
828      table.appendCell(fieldLabel);
829      if (values.isEmpty()) {
830        table.appendCell(noneLabel);
831      } else {
832        table.appendCell(values.get(0));
833      }
834      if (values.size() > 1) {
835        for (int i = 1; i < values.size(); i++) {
836          table.startRow();
837          table.appendCell();
838          table.appendCell(values.get(i));
839        }
840      }
841    }
842  }
843
844  /**
845   * Executable for printing a particular task's details.
846   */
847  private static class ViewTaskLogs extends TaskOperationCallback {
848
849    /**
850     * Constructs a parameterized instance.
851     *
852     * @param taskId of the task for which log records will be printed
853     */
854    public ViewTaskLogs(String taskId) {
855      super(taskId);
856    }
857
858    /** {@inheritDoc} */
859    @Override
860    protected MenuResult<TaskEntry> invoke(ManageTasks app)
861            throws ClientException
862    {
863      TaskEntry taskEntry = null;
864      try {
865        taskEntry = app.getTaskClient().getTaskEntry(taskId);
866        List<LocalizableMessage> logs = taskEntry.getLogMessages();
867        app.getOutputStream().println();
868
869        // Create a table for the last log entry
870        TableBuilder table = new TableBuilder();
871        table.appendHeading(INFO_TASKINFO_FIELD_LOG.get());
872        if (logs != null && !logs.isEmpty()) {
873          for (LocalizableMessage log : logs) {
874            table.startRow();
875            table.appendCell(log);
876          }
877        } else {
878          table.startRow();
879          table.appendCell(INFO_TASKINFO_NONE.get());
880        }
881        StringWriter sw = new StringWriter();
882        TextTablePrinter tablePrinter = new TextTablePrinter(sw);
883        tablePrinter.setTotalWidth(80);
884        tablePrinter.setIndentWidth(INDENT);
885        tablePrinter.setColumnWidth(0, 0);
886        table.print(tablePrinter);
887        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
888        app.getOutputStream().println();
889      } catch (Exception e) {
890        app.println(ERR_TASKINFO_ACCESSING_LOGS.get(taskId, e.getMessage()));
891      }
892      return MenuResult.success(taskEntry);
893    }
894  }
895
896  /**
897   * Executable for canceling a particular task.
898   */
899  private static class CancelTaskTop extends TopMenuCallback {
900
901    private List<String> taskIds;
902    private List<Integer> cancelableIndices;
903
904    /**
905     * Constructs a parameterized instance.
906     *
907     * @param taskIds of all known tasks
908     * @param cancelableIndices list of integers whose elements represent
909     *        the indices of <code>taskIds</code> that are cancelable
910     */
911    public CancelTaskTop(List<String> taskIds,
912                         List<Integer> cancelableIndices) {
913      this.taskIds = taskIds;
914      this.cancelableIndices = cancelableIndices;
915    }
916
917    /** {@inheritDoc} */
918    @Override
919    public MenuResult<Void> invoke(ManageTasks app)
920            throws ClientException
921    {
922      if (taskIds != null && !taskIds.isEmpty()) {
923        if (cancelableIndices != null && !cancelableIndices.isEmpty()) {
924
925          // Prompt for the task number
926          Integer index = null;
927          String line = app.readLineOfInput(
928                  INFO_TASKINFO_CMD_CANCEL_NUMBER_PROMPT.get(
929                          cancelableIndices.get(0)));
930          if (line.length() == 0) {
931            line = String.valueOf(cancelableIndices.get(0));
932          }
933          try {
934            int i = Integer.parseInt(line);
935            if (!cancelableIndices.contains(i)) {
936              app.println(ERR_TASKINFO_NOT_CANCELABLE_TASK_INDEX.get(i));
937            } else {
938              index = i - 1;
939            }
940          } catch (NumberFormatException nfe) {
941            // ignore;
942          }
943          if (index != null) {
944            String taskId = taskIds.get(index);
945            try {
946              CancelTask ct = new CancelTask(taskId);
947              MenuResult<TaskEntry> result = ct.invoke(app);
948              if (result.isSuccess()) {
949                return MenuResult.success();
950              } else {
951                return MenuResult.again();
952              }
953            } catch (Exception e) {
954              app.println(ERR_TASKINFO_CANCELING_TASK.get(
955                          taskId, e.getMessage()));
956              return MenuResult.again();
957            }
958          } else {
959            app.println(ERR_TASKINFO_INVALID_MENU_KEY.get(line));
960            return MenuResult.again();
961          }
962        } else {
963          app.println(INFO_TASKINFO_NO_CANCELABLE_TASKS.get());
964          return MenuResult.cancel();
965        }
966      } else {
967        app.println(INFO_TASKINFO_NO_TASKS.get());
968        return MenuResult.cancel();
969      }
970    }
971
972  }
973
974  /**
975   * Executable for canceling a particular task.
976   */
977  private static class CancelTask extends TaskOperationCallback {
978
979    /**
980     * Constructs a parameterized instance.
981     *
982     * @param taskId of the task to cancel
983     */
984    public CancelTask(String taskId) {
985      super(taskId);
986    }
987
988    /** {@inheritDoc} */
989    @Override
990    public MenuResult<TaskEntry> invoke(ManageTasks app)
991            throws ClientException
992    {
993      try {
994        TaskEntry entry = app.getTaskClient().getTaskEntry(taskId);
995        if (entry.isCancelable()) {
996          app.getTaskClient().cancelTask(taskId);
997          app.println(INFO_TASKINFO_CMD_CANCEL_SUCCESS.get(taskId));
998          return MenuResult.success(entry);
999        } else {
1000          app.println(ERR_TASKINFO_TASK_NOT_CANCELABLE_TASK.get(taskId));
1001          return MenuResult.again();
1002        }
1003      } catch (Exception e) {
1004        app.println(ERR_TASKINFO_CANCELING_TASK.get(
1005                taskId, e.getMessage()));
1006        return MenuResult.again();
1007      }
1008    }
1009
1010  }
1011
1012}