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