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