/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS.
 */
package org.opends.server.tools;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.DecodeException;
import org.opends.server.backends.task.TaskState;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.JDKLogging;
import org.opends.server.tools.tasks.TaskClient;
import org.opends.server.tools.tasks.TaskEntry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.LDAPException;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.args.LDAPConnectionArgumentParser;
import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.CommonArguments;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.Menu;
import com.forgerock.opendj.cli.MenuBuilder;
import com.forgerock.opendj.cli.MenuCallback;
import com.forgerock.opendj.cli.MenuResult;
import com.forgerock.opendj.cli.StringArgument;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TextTablePrinter;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import static org.opends.messages.ToolMessages.*;

import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.Utils.*;

/** Tool for getting information and managing tasks in the Directory Server. */
public class ManageTasks extends ConsoleApplication {
  /** This CLI is always using the administration connector with SSL. */
  private static final boolean alwaysSSL = true;

  /**
   * The main method for TaskInfo tool.
   *
   * @param args The command-line arguments provided to this program.
   */
  public static void main(String[] args) {
    int retCode = mainTaskInfo(args, System.in, System.out, System.err);

    if (retCode != 0) {
      System.exit(filterExitCode(retCode));
    }
  }

  /**
   * Processes the command-line arguments and invokes the process for
   * displaying task information.
   *
   * @param args The command-line arguments provided to this program.
   * @return int return code
   */
  public static int mainTaskInfo(String[] args) {
    return mainTaskInfo(args, System.in, System.out, System.err);
  }

  /**
   * Processes the command-line arguments and invokes the export process.
   *
   * @param args             The command-line arguments provided to this
   * @param in               Input stream from which to solicit user input.
   * @param out              The output stream to use for standard output, or
   *                         {@code null} if standard output is not needed.
   * @param err              The output stream to use for standard error, or
   *                         {@code null} if standard error is not needed.
   * @param initializeServer Indicates whether to initialize the server.
   * @return int return code
   */
  public static int mainTaskInfo(String[] args,
                                 InputStream in,
                                 OutputStream out,
                                 OutputStream err,
                                 boolean initializeServer) {
    ManageTasks tool = new ManageTasks(in, out, err);
    return tool.process(args, initializeServer);
  }

  /**
   * Processes the command-line arguments and invokes the export process.
   *
   * @param args             The command-line arguments provided to this
   * @param in               Input stream from which to solicit user input.
   * @param out              The output stream to use for standard output, or
   *                         {@code null} if standard output is not needed.
   * @param err              The output stream to use for standard error, or
   *                         {@code null} if standard error is not needed.
   * @return int return code
   */
  public static int mainTaskInfo(String[] args,
                                 InputStream in,
                                 OutputStream out,
                                 OutputStream err) {
    return mainTaskInfo(args, in, out, err, true);
  }

  private static final int INDENT = 2;

  /** ID of task for which to display details and exit. */
  private StringArgument task;
  /** Indicates print summary and exit. */
  private BooleanArgument summary;
  /** ID of task to cancel. */
  private StringArgument cancel;
  /** Argument used to request non-interactive behavior. */
  private BooleanArgument noPrompt;

  /** Accesses the directory's task backend. */
  private TaskClient taskClient;

  /**
   * Constructs a parameterized instance.
   *
   * @param in               Input stream from which to solicit user input.
   * @param out              The output stream to use for standard output, or
   *                         {@code null} if standard output is not needed.
   * @param err              The output stream to use for standard error, or
   *                         {@code null} if standard error is not needed.
   */
  public ManageTasks(InputStream in, OutputStream out, OutputStream err)
  {
    super(new PrintStream(out), new PrintStream(err));
  }

  /**
   * Processes the command-line arguments and invokes the export process.
   *
   * @param args       The command-line arguments provided to this
   *                   program.
   * @return The error code.
   */
  public int process(String[] args)
  {
    return process(args, true);
  }

  /**
   * Processes the command-line arguments and invokes the export process.
   *
   * @param args       The command-line arguments provided to this
   *                   program.
   * @param initializeServer  Indicates whether to initialize the server.
   * @return The error code.
   */
  public int process(String[] args, boolean initializeServer)
  {
    if (initializeServer)
    {
      DirectoryServer.bootstrapClient();
    }
    JDKLogging.disableLogging();

    // Create the command-line argument parser for use with this program.
    LDAPConnectionArgumentParser argParser = new LDAPConnectionArgumentParser(
            "org.opends.server.tools.TaskInfo",
            INFO_TASKINFO_TOOL_DESCRIPTION.get(),
            false, null, alwaysSSL);
    argParser.setShortToolDescription(REF_SHORT_DESC_MANAGE_TASKS.get());

    // Initialize all the command-line argument types and register them with the parser
    try {
       StringArgument propertiesFileArgument = new StringArgument(
          "propertiesFilePath", null, OPTION_LONG_PROP_FILE_PATH, false, false,
          true, INFO_PROP_FILE_PATH_PLACEHOLDER.get(), null, null,
          INFO_DESCRIPTION_PROP_FILE_PATH.get());
      argParser.addArgument(propertiesFileArgument);
      argParser.setFilePropertiesArgument(propertiesFileArgument);

      BooleanArgument noPropertiesFileArgument = new BooleanArgument(
          "noPropertiesFileArgument", null, OPTION_LONG_NO_PROP_FILE,
          INFO_DESCRIPTION_NO_PROP_FILE.get());
      argParser.addArgument(noPropertiesFileArgument);
      argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);

      task = new StringArgument(
              "info", 'i', "info",
              false, true, INFO_TASK_ID_PLACEHOLDER.get(),
              INFO_TASKINFO_TASK_ARG_DESCRIPTION.get());
      argParser.addArgument(task);

      cancel = new StringArgument(
              "cancel", 'c', "cancel",
              false, true, INFO_TASK_ID_PLACEHOLDER.get(),
              INFO_TASKINFO_TASK_ARG_CANCEL.get());
      argParser.addArgument(cancel);

      summary = new BooleanArgument(
              "summary", 's', "summary",
              INFO_TASKINFO_SUMMARY_ARG_DESCRIPTION.get());
      argParser.addArgument(summary);

      noPrompt = CommonArguments.getNoPrompt();
      argParser.addArgument(noPrompt);

      BooleanArgument displayUsage = CommonArguments.getShowUsage();
      argParser.addArgument(displayUsage);
      argParser.setUsageArgument(displayUsage);
    }
    catch (ArgumentException ae) {
      LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage());
      println(message);
      return 1;
    }

    try
    {
      argParser.getArguments().initArgumentsWithConfiguration();
    }
    catch (ConfigException ignored) {}

    // Parse the command-line arguments provided to this program.
    try {
      argParser.parseArguments(args);
      StaticUtils.checkOnlyOneArgPresent(task, summary, cancel);
    }
    catch (ArgumentException ae) {
      argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      return 1;
    }

    if (!argParser.usageOrVersionDisplayed()) {
      // Checks the version - if upgrade required, the tool is unusable
      try
      {
        BuildVersion.checkVersionMismatch();
      }
      catch (InitializationException e)
      {
        println(e.getMessageObject());
        return 1;
      }

      try {
        LDAPConnectionConsoleInteraction ui =
                new LDAPConnectionConsoleInteraction(
                        this, argParser.getArguments());

        taskClient = new TaskClient(argParser.connect(ui,
                getOutputStream(), getErrorStream()));

        if (isMenuDrivenMode()) {
          // Keep prompting the user until they specify quit of there is a fatal exception
          while (true) {
            getOutputStream().println();
            Menu<Void> menu = getSummaryMenu();
            MenuResult<Void> result = menu.run();
            if (result.isQuit()) {
              return 0;
            }
          }
        } else if (task.isPresent()) {
          getOutputStream().println();
          MenuResult<TaskEntry> r = new PrintTaskInfo(task.getValue()).invoke(this);
          if (r.isAgain())
          {
            return 1;
          }
        } else if (summary.isPresent()) {
          getOutputStream().println();
          printSummaryTable();
        } else if (cancel.isPresent()) {
          MenuResult<TaskEntry> r = new CancelTask(cancel.getValue()).invoke(this);
          if (r.isAgain())
          {
            return 1;
          }
        } else if (!isInteractive()) {
           // no-prompt option
           getOutputStream().println();
           printSummaryTable();
           return 0;
        }
      } catch (LDAPConnectionException lce) {
        println(INFO_TASKINFO_LDAP_EXCEPTION.get(lce.getMessageObject()));
        return 1;
      } catch (Exception e) {
        println(LocalizableMessage.raw(StaticUtils.getExceptionMessage(e)));
        return 1;
      }
    }
    return 0;
  }

  @Override
  public boolean isAdvancedMode() {
    return false;
  }

  @Override
  public boolean isInteractive() {
    return !noPrompt.isPresent();
  }

  @Override
  public boolean isMenuDrivenMode() {
    return !task.isPresent() && !cancel.isPresent() && !summary.isPresent() && !noPrompt.isPresent();
  }

  @Override
  public boolean isQuiet() {
    return false;
  }

  @Override
  public boolean isScriptFriendly() {
    return false;
  }

  @Override
  public boolean isVerbose() {
    return false;
  }

  /**
   * Creates the summary table.
   *
   * @throws IOException if there is a problem with screen I/O
   * @throws LDAPException if there is a problem getting information
   *         out to the directory
   * @throws DecodeException if there is a problem with the encoding
   */
  private void printSummaryTable()
          throws LDAPException, IOException, DecodeException {
    List<TaskEntry> entries = taskClient.getTaskEntries();
    if (!entries.isEmpty()) {
      TableBuilder table = new TableBuilder();
      Map<String, TaskEntry> mapIdToEntry = new TreeMap<>();
      for (TaskEntry entry : entries) {
        String taskId = entry.getId();
        if (taskId != null) {
          mapIdToEntry.put(taskId, entry);
        }
      }

      table.appendHeading(INFO_TASKINFO_FIELD_ID.get());
      table.appendHeading(INFO_TASKINFO_FIELD_TYPE.get());
      table.appendHeading(INFO_TASKINFO_FIELD_STATUS.get());
      for (String taskId : mapIdToEntry.keySet()) {
        TaskEntry entryWrapper = mapIdToEntry.get(taskId);
        table.startRow();
        table.appendCell(taskId);
        table.appendCell(entryWrapper.getType());
        table.appendCell(entryWrapper.getState());
      }
      StringWriter sw = new StringWriter();
      TextTablePrinter tablePrinter = new TextTablePrinter(sw);
      tablePrinter.setIndentWidth(INDENT);
      tablePrinter.setTotalWidth(80);
      table.print(tablePrinter);
      getOutputStream().println(LocalizableMessage.raw(sw.getBuffer()));
    } else {
      getOutputStream().println(INFO_TASKINFO_NO_TASKS.get());
      getOutputStream().println();
    }
  }

  /**
   * Creates the summary table.
   *
   * @return list of strings of IDs of all the tasks in the table in order
   *         of the indexes printed in the table
   * @throws IOException if there is a problem with screen I/O
   * @throws LDAPException if there is a problem getting information
   *         out to the directory
   * @throws DecodeException if there is a problem with the encoding
   */
  private Menu<Void> getSummaryMenu()
          throws LDAPException, IOException, DecodeException {
    List<String> taskIds = new ArrayList<>();
    List<Integer> cancelableIndices = new ArrayList<>();
    List<TaskEntry> entries = taskClient.getTaskEntries();
    MenuBuilder<Void> menuBuilder = new MenuBuilder<>(this);
    if (!entries.isEmpty()) {
      Map<String, TaskEntry> mapIdToEntry = new TreeMap<>();
      for (TaskEntry entry : entries) {
        String taskId = entry.getId();
        if (taskId != null) {
          mapIdToEntry.put(taskId, entry);
        }
      }

      menuBuilder.setColumnHeadings(
              INFO_TASKINFO_FIELD_ID.get(),
              INFO_TASKINFO_FIELD_TYPE.get(),
              INFO_TASKINFO_FIELD_STATUS.get());
      menuBuilder.setColumnWidths(null, null, 0);
      int index = 0;
      for (final String taskId : mapIdToEntry.keySet()) {
        taskIds.add(taskId);
        final TaskEntry taskEntry = mapIdToEntry.get(taskId);
        menuBuilder.addNumberedOption(
                LocalizableMessage.raw(taskEntry.getId()),
                new TaskDrilldownMenu(taskId),
                taskEntry.getType(), taskEntry.getState());
        index++;
        if (taskEntry.isCancelable()) {
          cancelableIndices.add(index);
        }
      }
    } else {
      getOutputStream().println(INFO_TASKINFO_NO_TASKS.get());
      getOutputStream().println();
    }

    menuBuilder.addCharOption(
            INFO_TASKINFO_CMD_REFRESH_CHAR.get(),
            INFO_TASKINFO_CMD_REFRESH.get(),
            new PrintSummaryTop());

    if (!cancelableIndices.isEmpty()) {
      menuBuilder.addCharOption(
              INFO_TASKINFO_CMD_CANCEL_CHAR.get(),
              INFO_TASKINFO_CMD_CANCEL.get(),
              new CancelTaskTop(taskIds, cancelableIndices));
    }
    menuBuilder.addQuitOption();

    return menuBuilder.toMenu();
  }

  /**
   * Gets the client that can be used to interact with the task backend.
   *
   * @return TaskClient for interacting with the task backend.
   */
  public TaskClient getTaskClient() {
    return this.taskClient;
  }

  private static void printTable(TableBuilder table, int column, int width, StringWriter sw)
  {
    TextTablePrinter tablePrinter = new TextTablePrinter(sw);
    tablePrinter.setTotalWidth(80);
    tablePrinter.setIndentWidth(INDENT);
    tablePrinter.setColumnWidth(column, width);
    table.print(tablePrinter);
  }

  /** Base for callbacks that implement top level menu items. */
  private static abstract class TopMenuCallback
          implements MenuCallback<Void> {
    @Override
    public MenuResult<Void> invoke(ConsoleApplication app) throws ClientException {
      return invoke((ManageTasks)app);
    }

    /**
     * Called upon task invocation.
     *
     * @param app this console application
     * @return MessageResult result of task
     * @throws ClientException if there is a problem
     */
    protected abstract MenuResult<Void> invoke(ManageTasks app) throws ClientException;
  }

  /** Base for callbacks that manage task entries. */
  private static abstract class TaskOperationCallback
          implements MenuCallback<TaskEntry> {
    /** ID of the task to manage. */
    protected String taskId;

    /**
     * Constructs a parameterized instance.
     *
     * @param taskId if the task to examine
     */
    public TaskOperationCallback(String taskId) {
      this.taskId = taskId;
    }

    @Override
    public MenuResult<TaskEntry> invoke(ConsoleApplication app) throws ClientException
    {
      return invoke((ManageTasks)app);
    }

    /**
     * Invokes the task.
     *
     * @param app
     *          the current application running
     * @return how the application should proceed next
     * @throws ClientException
     *           if any problem occurred
     */
    protected abstract MenuResult<TaskEntry> invoke(ManageTasks app) throws ClientException;
  }

  /** Executable for printing a task summary table. */
  private static class PrintSummaryTop extends TopMenuCallback {
    @Override
    public MenuResult<Void> invoke(ManageTasks app) throws ClientException
    {
      // Since the summary table is reprinted every time,
      // the user enters the top level this task just returns 'success'
      return MenuResult.success();
    }
  }

  /** Executable for printing a particular task's details. */
  private static class TaskDrilldownMenu extends TopMenuCallback {
    private String taskId;

    /**
     * Constructs a parameterized instance.
     *
     * @param taskId of the task for which information will be displayed
     */
    public TaskDrilldownMenu(String taskId) {
      this.taskId = taskId;
    }

    @Override
    public MenuResult<Void> invoke(ManageTasks app) throws ClientException {
      MenuResult<TaskEntry> res = new PrintTaskInfo(taskId).invoke(app);
      TaskEntry taskEntry = res.getValue();
      if (taskEntry != null) {
        while (true) {
          try {
            taskEntry = app.getTaskClient().getTaskEntry(taskId);

            // Show the menu
            MenuBuilder<TaskEntry> menuBuilder = new MenuBuilder<>(app);
            menuBuilder.addBackOption(true);
            menuBuilder.addCharOption(
                    INFO_TASKINFO_CMD_REFRESH_CHAR.get(),
                    INFO_TASKINFO_CMD_REFRESH.get(),
                    new PrintTaskInfo(taskId));
            List<LocalizableMessage> logs = taskEntry.getLogMessages();
            if (logs != null && !logs.isEmpty()) {
              menuBuilder.addCharOption(
                      INFO_TASKINFO_CMD_VIEW_LOGS_CHAR.get(),
                      INFO_TASKINFO_CMD_VIEW_LOGS.get(),
                      new ViewTaskLogs(taskId));
            }
            if (taskEntry.isCancelable() && !taskEntry.isDone()) {
              menuBuilder.addCharOption(
                      INFO_TASKINFO_CMD_CANCEL_CHAR.get(),
                      INFO_TASKINFO_CMD_CANCEL.get(),
                      new CancelTask(taskId));
            }
            menuBuilder.addQuitOption();
            Menu<TaskEntry> menu = menuBuilder.toMenu();
            MenuResult<TaskEntry> result = menu.run();
            if (result.isCancel()) {
              break;
            } else if (result.isQuit()) {
              System.exit(0);
            }
          } catch (Exception e) {
            app.println(LocalizableMessage.raw(StaticUtils.getExceptionMessage(e)));
          }
        }
      } else {
        app.println(ERR_TASKINFO_UNKNOWN_TASK_ENTRY.get(taskId));
      }
      return MenuResult.success();
    }
  }

  /** Executable for printing a particular task's details. */
  private static class PrintTaskInfo extends TaskOperationCallback {
    /**
     * Constructs a parameterized instance.
     *
     * @param taskId of the task for which information will be printed
     */
    public PrintTaskInfo(String taskId) {
      super(taskId);
    }

    @Override
    public MenuResult<TaskEntry> invoke(ManageTasks app) throws ClientException
    {
      try {
        TaskEntry taskEntry = app.getTaskClient().getTaskEntry(taskId);

        TableBuilder table = new TableBuilder();
        table.appendHeading(INFO_TASKINFO_DETAILS.get());

        table.startRow();
        table.appendCell(INFO_TASKINFO_FIELD_ID.get());
        table.appendCell(taskEntry.getId());

        table.startRow();
        table.appendCell(INFO_TASKINFO_FIELD_TYPE.get());
        table.appendCell(taskEntry.getType());

        table.startRow();
        table.appendCell(INFO_TASKINFO_FIELD_STATUS.get());
        table.appendCell(taskEntry.getState());

        table.startRow();
        table.appendCell(INFO_TASKINFO_FIELD_SCHEDULED_START.get());

        if (TaskState.isRecurring(taskEntry.getTaskState())) {
          LocalizableMessage m = taskEntry.getScheduleTab();
          table.appendCell(m);
        } else {
          LocalizableMessage m = taskEntry.getScheduledStartTime();
          if (m == null || m.equals(LocalizableMessage.EMPTY)) {
            table.appendCell(INFO_TASKINFO_IMMEDIATE_EXECUTION.get());
          } else {
            table.appendCell(m);
          }

          table.startRow();
          table.appendCell(INFO_TASKINFO_FIELD_ACTUAL_START.get());
          table.appendCell(taskEntry.getActualStartTime());

          table.startRow();
          table.appendCell(INFO_TASKINFO_FIELD_COMPLETION_TIME.get());
          table.appendCell(taskEntry.getCompletionTime());
        }

        writeMultiValueCells(
                table,
                INFO_TASKINFO_FIELD_DEPENDENCY.get(),
                taskEntry.getDependencyIds());

        table.startRow();
        table.appendCell(INFO_TASKINFO_FIELD_FAILED_DEPENDENCY_ACTION.get());
        LocalizableMessage m = taskEntry.getFailedDependencyAction();
        table.appendCell(m != null ? m : INFO_TASKINFO_NONE.get());

        writeMultiValueCells(
                table,
                INFO_TASKINFO_FIELD_NOTIFY_ON_COMPLETION.get(),
                taskEntry.getCompletionNotificationEmailAddresses(),
                INFO_TASKINFO_NONE_SPECIFIED.get());

        writeMultiValueCells(
                table,
                INFO_TASKINFO_FIELD_NOTIFY_ON_ERROR.get(),
                taskEntry.getErrorNotificationEmailAddresses(),
                INFO_TASKINFO_NONE_SPECIFIED.get());

        StringWriter sw = new StringWriter();
        printTable(table, 1, 0, sw);
        app.getOutputStream().println();
        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));

        // Create a table for the task options
        table = new TableBuilder();
        table.appendHeading(INFO_TASKINFO_OPTIONS.get(taskEntry.getType()));
        Map<LocalizableMessage,List<String>> taskSpecificAttrs =
                taskEntry.getTaskSpecificAttributeValuePairs();
        for (LocalizableMessage attrName : taskSpecificAttrs.keySet()) {
          table.startRow();
          table.appendCell(attrName);
          List<String> values = taskSpecificAttrs.get(attrName);
          if (!values.isEmpty()) {
            table.appendCell(values.get(0));
          }
          if (values.size() > 1) {
            for (int i = 1; i < values.size(); i++) {
              table.startRow();
              table.appendCell();
              table.appendCell(values.get(i));
            }
          }
        }
        sw = new StringWriter();
        printTable(table, 1, 0, sw);
        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));

        // Print the last log message if any
        List<LocalizableMessage> logs = taskEntry.getLogMessages();
        if (logs != null && !logs.isEmpty()) {
          // Create a table for the last log entry
          table = new TableBuilder();
          table.appendHeading(INFO_TASKINFO_FIELD_LAST_LOG.get());
          table.startRow();
          table.appendCell(logs.get(logs.size() - 1));

          sw = new StringWriter();
          printTable(table, 0, 0, sw);
          app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
        }

        app.getOutputStream().println();
        return MenuResult.success(taskEntry);
      } catch (Exception e) {
        app.errPrintln(ERR_TASKINFO_RETRIEVING_TASK_ENTRY.get(taskId, e.getMessage()));
        return MenuResult.again();
      }
    }

    /**
     * Writes an attribute and associated values to the table.
     * @param table of task details
     * @param fieldLabel of attribute
     * @param values of the attribute
     */
    private void writeMultiValueCells(TableBuilder table,
                                      LocalizableMessage fieldLabel,
                                      List<?> values) {
      writeMultiValueCells(table, fieldLabel, values, INFO_TASKINFO_NONE.get());
    }

    /**
     * Writes an attribute and associated values to the table.
     *
     * @param table of task details
     * @param fieldLabel of attribute
     * @param values of the attribute
     * @param noneLabel label for the value column when there are no values
     */
    private void writeMultiValueCells(TableBuilder table,
                                      LocalizableMessage fieldLabel,
                                      List<?> values,
                                      LocalizableMessage noneLabel) {
      table.startRow();
      table.appendCell(fieldLabel);
      if (values.isEmpty()) {
        table.appendCell(noneLabel);
      } else {
        table.appendCell(values.get(0));
      }
      if (values.size() > 1) {
        for (int i = 1; i < values.size(); i++) {
          table.startRow();
          table.appendCell();
          table.appendCell(values.get(i));
        }
      }
    }
  }

  /** Executable for printing a particular task's details. */
  private static class ViewTaskLogs extends TaskOperationCallback {
    /**
     * Constructs a parameterized instance.
     *
     * @param taskId of the task for which log records will be printed
     */
    public ViewTaskLogs(String taskId) {
      super(taskId);
    }

    @Override
    protected MenuResult<TaskEntry> invoke(ManageTasks app) throws ClientException
    {
      TaskEntry taskEntry = null;
      try {
        taskEntry = app.getTaskClient().getTaskEntry(taskId);
        List<LocalizableMessage> logs = taskEntry.getLogMessages();
        app.getOutputStream().println();

        // Create a table for the last log entry
        TableBuilder table = new TableBuilder();
        table.appendHeading(INFO_TASKINFO_FIELD_LOG.get());
        if (logs != null && !logs.isEmpty()) {
          for (LocalizableMessage log : logs) {
            table.startRow();
            table.appendCell(log);
          }
        } else {
          table.startRow();
          table.appendCell(INFO_TASKINFO_NONE.get());
        }
        StringWriter sw = new StringWriter();
        printTable(table, 0, 0, sw);
        app.getOutputStream().println(LocalizableMessage.raw(sw.getBuffer().toString()));
        app.getOutputStream().println();
      } catch (Exception e) {
        app.println(ERR_TASKINFO_ACCESSING_LOGS.get(taskId, e.getMessage()));
      }
      return MenuResult.success(taskEntry);
    }
  }

  /** Executable for canceling a particular task. */
  private static class CancelTaskTop extends TopMenuCallback {
    private List<String> taskIds;
    private List<Integer> cancelableIndices;

    /**
     * Constructs a parameterized instance.
     *
     * @param taskIds of all known tasks
     * @param cancelableIndices list of integers whose elements represent
     *        the indices of <code>taskIds</code> that are cancelable
     */
    public CancelTaskTop(List<String> taskIds, List<Integer> cancelableIndices)
    {
      this.taskIds = taskIds;
      this.cancelableIndices = cancelableIndices;
    }

    @Override
    public MenuResult<Void> invoke(ManageTasks app) throws ClientException
    {
      if (taskIds == null || taskIds.isEmpty()) {
        app.println(INFO_TASKINFO_NO_TASKS.get());
        return MenuResult.cancel();
      }
      if (cancelableIndices == null || cancelableIndices.isEmpty()) {
        app.println(INFO_TASKINFO_NO_CANCELABLE_TASKS.get());
        return MenuResult.cancel();
      }

      // Prompt for the task number
      Integer index = null;
      String line = app.readLineOfInput(INFO_TASKINFO_CMD_CANCEL_NUMBER_PROMPT.get(cancelableIndices.get(0)));
      if (line.length() == 0) {
        line = String.valueOf(cancelableIndices.get(0));
      }

      try {
        int i = Integer.parseInt(line);
        if (!cancelableIndices.contains(i)) {
          app.println(ERR_TASKINFO_NOT_CANCELABLE_TASK_INDEX.get(i));
        } else {
          index = i - 1;
        }
      } catch (NumberFormatException ignored) {}

      if (index == null) {
        app.errPrintln(ERR_TASKINFO_INVALID_MENU_KEY.get(line));
        return MenuResult.again();
      }

      String taskId = taskIds.get(index);
      try {
        CancelTask ct = new CancelTask(taskId);
        MenuResult<TaskEntry> result = ct.invoke(app);
        return result.isSuccess() ? MenuResult.<Void> success() : MenuResult.<Void> again();
      } catch (Exception e) {
        app.errPrintln(ERR_TASKINFO_CANCELING_TASK.get(taskId, e.getMessage()));
        return MenuResult.again();
      }
    }
  }

  /** Executable for canceling a particular task. */
  private static class CancelTask extends TaskOperationCallback {
    /**
     * Constructs a parameterized instance.
     *
     * @param taskId of the task to cancel
     */
    public CancelTask(String taskId) {
      super(taskId);
    }

    @Override
    public MenuResult<TaskEntry> invoke(ManageTasks app) throws ClientException
    {
      try {
        TaskEntry entry = app.getTaskClient().getTaskEntry(taskId);
        if (!entry.isCancelable()) {
          app.errPrintln(ERR_TASKINFO_TASK_NOT_CANCELABLE_TASK.get(taskId));
          return MenuResult.again();
        }

        app.getTaskClient().cancelTask(taskId);
        app.println(INFO_TASKINFO_CMD_CANCEL_SUCCESS.get(taskId));
        return MenuResult.success(entry);
      } catch (Exception e) {
        app.errPrintln(ERR_TASKINFO_CANCELING_TASK.get(taskId, e.getMessage()));
        return MenuResult.again();
      }
    }
  }
}
