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