/* $Id$ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.crawler.notifications.slack;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.manifoldcf.agents.interfaces.ServiceInterruption;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
import org.apache.manifoldcf.core.interfaces.IPasswordMapperActivity;
import org.apache.manifoldcf.core.interfaces.IPostParameters;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.interfaces.Specification;
import org.apache.manifoldcf.core.interfaces.SpecificationNode;
import org.apache.manifoldcf.crawler.system.Logging;

/**
*/
public class SlackConnector extends org.apache.manifoldcf.crawler.notifications.BaseNotificationConnector {

  protected final static long SESSION_EXPIRATION_MILLISECONDS = 300000L;

  // Local variables.
  protected long sessionExpiration = -1L;

  // Parameters for establishing a session

  protected String webHookUrl = null;

  // Parameters for proxy connection
  protected SlackSession.ProxySettings proxySettings = null;

  // Local session handle
  protected SlackSession session = null;

  //////////////////////////////////Start of Basic Connector Methods/////////////////////////

  /**
  * Connect.
  *
  * @param configParameters is the set of configuration parameters, which
  * in this case describe the root directory.
  */
  @Override
  public void connect(ConfigParams configParameters) {
    super.connect(configParameters);
    this.webHookUrl = configParameters.getObfuscatedParameter(SlackConfig.WEBHOOK_URL_PARAM);

    String proxyHost = configParameters.getParameter(SlackConfig.PROXY_HOST_PARAM);
    String proxyPortString = configParameters.getParameter(SlackConfig.PROXY_PORT_PARAM);
    if(StringUtils.isNotEmpty(proxyHost) && StringUtils.isNotEmpty(proxyPortString)) {
      String proxyUsername = configParameters.getParameter(SlackConfig.PROXY_USERNAME_PARAM);
      String proxyPassword = configParameters.getObfuscatedParameter(SlackConfig.PROXY_PASSWORD_PARAM);
      String proxyDomain = configParameters.getParameter(SlackConfig.PROXY_DOMAIN_PARAM);
      this.proxySettings = new SlackSession.ProxySettings(proxyHost, proxyPortString, proxyUsername, proxyPassword, proxyDomain);
    } else {
      Logging.connectors.info("Using no proxy settings - no proxyHost and no proxyPort found.");
    }
  }

  /**
  * Close the connection. Call this before discarding this instance of the
  * repository connector.
  */
  @Override
  public void disconnect()
    throws ManifoldCFException {
    this.webHookUrl = null;
    this.proxySettings = null;
    finalizeConnection();
    super.disconnect();
  }

  /**
  * This method is periodically called for all connectors that are connected but not
  * in active use.
  */
  @Override
  public void poll() throws ManifoldCFException {
    if (session != null)
    {
      if (System.currentTimeMillis() >= sessionExpiration)
        finalizeConnection();
    }
  }

  /**
  * Test the connection. Returns a string describing the connection integrity.
  *
  * @return the connection's status as a displayable string.
  */
  @Override
  public String check()
      throws ManifoldCFException {
    try {
      checkConnection();
      return super.check();
    } catch (ServiceInterruption e) {
      return "Connection temporarily failed: " + e.getMessage();
    } catch (ManifoldCFException e) {
      return "Connection failed: " + e.getMessage();
    }
  }

  protected void checkConnection() throws ManifoldCFException, ServiceInterruption {
    // Force a re-connection
    finalizeConnection();
    getSession();
    try {
      CheckConnectionThread cct = new CheckConnectionThread(session);
      cct.start();
      cct.finishUp();
    } catch (InterruptedException e) {
      throw new ManifoldCFException(e.getMessage(),ManifoldCFException.INTERRUPTED);
    } catch (IOException e) {
      handleIOException(e,"checking the connection");
    }
  }

  protected void getSession()
    throws ManifoldCFException, ServiceInterruption {
    if (session == null) {

      // Check that all the required parameters are there.
      if (webHookUrl == null)
        throw new ManifoldCFException("Missing webHookUrl parameter");

      // Create a session.
      try {
        ConnectThread connectThread = new ConnectThread(webHookUrl, proxySettings);
        connectThread.start();
        session = connectThread.finishUp();
      } catch (InterruptedException e) {
        throw new ManifoldCFException(e.getMessage(),ManifoldCFException.INTERRUPTED);
      } catch (IOException e) {
        handleIOException(e, "connecting");
      }
    }
    sessionExpiration = System.currentTimeMillis() + SESSION_EXPIRATION_MILLISECONDS;
  }

  protected void finalizeConnection() {
    if (session != null) {
      try {
        CloseSessionThread closeSessionThread = new CloseSessionThread(session);
        closeSessionThread.start();
        closeSessionThread.finishUp();
      } catch (InterruptedException e) {
      } catch (IOException e) {
        Logging.connectors.warn("Error while closing connection to server: " + e.getMessage(),e);
      } finally {
        session = null;
      }
    }
  }

  ///////////////////////////////End of Basic Connector Methods////////////////////////////////////////

  //////////////////////////////Start of Notification Connector Method///////////////////////////////////

  /** Notify of job stop due to error abort.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobStopErrorAbort(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_ERRORABORTED);
  }

  /** Notify of job stop due to manual abort.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobStopManualAbort(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_MANUALABORTED);
  }

  /** Notify of job stop due to manual pause.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobStopManualPause(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_MANUALPAUSED);
  }

  /** Notify of job stop due to schedule pause.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobStopSchedulePause(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_SCHEDULEPAUSED);
  }

  /** Notify of job stop due to restart.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobStopRestart(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_RESTARTED);
  }

  /** Notify of job end.
  *@param spec is the notification specification.
  */
  @Override
  public void notifyOfJobEnd(final Specification spec)
    throws ManifoldCFException, ServiceInterruption {
    sendSlackMessage(spec, SlackConfig.NODE_FINISHED);
  }

  protected void sendSlackMessage(final Specification spec, final String nodeType)
    throws ManifoldCFException, ServiceInterruption
  {
    String channel = "";
    String message = "";
    for (int i = 0; i < spec.getChildCount(); i++) {
      SpecificationNode sn = spec.getChild(i);
      if (sn.getType().equals(SlackConfig.NODE_CHANNEL))
        channel = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
      else if (sn.getType().equals(SlackConfig.NODE_MESSAGE))
        message = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
    }
    // Look for node of the specified type
    if (nodeType != null)
    {
      for (int i = 0; i < spec.getChildCount(); i++) {
        SpecificationNode childNode = spec.getChild(i);
        if (childNode.getType().equals(nodeType))
        {
          for (int j = 0; j < childNode.getChildCount(); j++) {
            SpecificationNode sn = childNode.getChild(j);
            if (sn.getType().equals(SlackConfig.NODE_CHANNEL))
              channel = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
            else if (sn.getType().equals(SlackConfig.NODE_MESSAGE))
              message = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
          }
        }
      }
    }

    if (StringUtils.isBlank(message)) {
      return;
    }

    // Construct and send a slack message
    getSession();

    SendThread st = new SendThread(session, channel, message);
    st.start();
    try {
      st.finishUp();
    } catch (InterruptedException e) {
      throw new ManifoldCFException(e.getMessage(),ManifoldCFException.INTERRUPTED);
    } catch (IOException e) {
      handleIOException(e,"sending slack message");
    }
  }


  //////////////////////////////End of Notification Connector Methods///////////////////////////////////


  ///////////////////////////////////////Start of Configuration UI/////////////////////////////////////

  /**
  * Output the configuration header section.
  * This method is called in the head section of the connector's configuration page. Its purpose is to
  * add the required tabs to the list, and to output any javascript methods that might be needed by
  * the configuration editing HTML.
  * The connector does not need to be connected for this method to be called.
  *
  * @param threadContext is the local thread context.
  * @param out is the output to which any HTML should be sent.
  * @param locale is the desired locale.
  * @param parameters are the configuration parameters, as they currently exist, for this connection being configured.
  * @param tabsArray is an array of tab names. Add to this array any tab names that are specific to the connector.
  */
  @Override
  public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters, List<String> tabsArray)
    throws ManifoldCFException, IOException {
    tabsArray.add(Messages.getString(locale, "SlackConnector.WebHook"));
    // Map the parameters
    Map<String, Object> paramMap = new HashMap<String, Object>();

    // Fill in the parameters from each tab
    fillInServerConfigurationMap(paramMap, out, parameters);

    // Output the Javascript - only one Velocity template for all tabs
    Messages.outputResourceWithVelocity(out, locale, "ConfigurationHeader.js", paramMap);
  }

  @Override
  public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters, String tabName)
    throws ManifoldCFException, IOException {
    // Output the Server tab
    Map<String, Object> paramMap = new HashMap<String, Object>();
    // Set the tab name
    paramMap.put("TabName", tabName);
    // Fill in the parameters
    fillInServerConfigurationMap(paramMap, out, parameters);
    Messages.outputResourceWithVelocity(out, locale, "Configuration_Server.html", paramMap);
  }

  private static void fillInServerConfigurationMap(Map<String, Object> paramMap, IPasswordMapperActivity mapper, ConfigParams parameters) {
    String webHookUrl = parameters.getObfuscatedParameter(SlackConfig.WEBHOOK_URL_PARAM);
    if (webHookUrl == null) {
      webHookUrl = StringUtils.EMPTY;
    } else {
      webHookUrl = mapper.mapPasswordToKey(webHookUrl);
    }

    String proxyHost = getEmptyOnNull(parameters, SlackConfig.PROXY_HOST_PARAM);
    String proxyPort = getEmptyOnNull(parameters, SlackConfig.PROXY_PORT_PARAM);
    String proxyUsername = getEmptyOnNull(parameters, SlackConfig.PROXY_USERNAME_PARAM);

    String proxyPassword = parameters.getObfuscatedParameter(SlackConfig.PROXY_PASSWORD_PARAM);
    if(proxyPassword == null) {
      proxyPassword = StringUtils.EMPTY;
    } else {
      proxyPassword = mapper.mapPasswordToKey(proxyPassword);
    }

    String proxyDomain = getEmptyOnNull(parameters, SlackConfig.PROXY_DOMAIN_PARAM);

    paramMap.put("WEBHOOK_URL", webHookUrl);
    paramMap.put("PROXY_HOST", proxyHost);
    paramMap.put("PROXY_PORT", proxyPort);
    paramMap.put("PROXY_USERNAME", proxyUsername);
    paramMap.put("PROXY_PASSWORD", proxyPassword);
    paramMap.put("PROXY_DOMAIN", proxyDomain);
  }

  private static String getEmptyOnNull(ConfigParams parameters, String key) {
    String value = parameters.getParameter(key);
    if (value == null) {
      value = StringUtils.EMPTY;
    }
    return value;
  }

  /**
  * Process a configuration post.
  * This method is called at the start of the connector's configuration page, whenever there is a possibility
  * that form data for a connection has been posted. Its purpose is to gather form information and modify
  * the configuration parameters accordingly.
  * The name of the posted form is always "editconnection".
  * The connector does not need to be connected for this method to be called.
  *
  * @param threadContext is the local thread context.
  * @param variableContext is the set of variables available from the post, including binary file post information.
  * @param parameters are the configuration parameters, as they currently exist, for this connection being configured.
  * @return null if all is well, or a string error message if there is an error that should prevent saving of the
  * connection (and cause a redirection to an error page).
  */
  @Override
  public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext,
    ConfigParams parameters) throws ManifoldCFException {

    final String webHookUrl = variableContext.getParameter("webHookUrl");
    if (webHookUrl != null) {
      parameters.setObfuscatedParameter(SlackConfig.WEBHOOK_URL_PARAM, variableContext.mapKeyToPassword(webHookUrl));
    }

    final String proxyHost = variableContext.getParameter("proxyHost");
    if (proxyHost != null) {
      parameters.setParameter(SlackConfig.PROXY_HOST_PARAM, proxyHost);
    }

    final String proxyPort = variableContext.getParameter("proxyPort");
    if (StringUtils.isNotEmpty(proxyPort)) {
      try {
        Integer.parseInt(proxyPort);
      } catch (NumberFormatException e) {
        Logging.connectors.warn("Proxy port must be a number. Found " + proxyPort);
        throw new ManifoldCFException("Proxy Port must be a number: " + e.getMessage(), e);
      }
      parameters.setParameter(SlackConfig.PROXY_PORT_PARAM, proxyPort);
    } else if(proxyPort != null){
      parameters.setParameter(SlackConfig.PROXY_PORT_PARAM, proxyPort);
    }

    final String proxyUsername = variableContext.getParameter("proxyUsername");
    if (proxyUsername != null) {
      parameters.setParameter(SlackConfig.PROXY_USERNAME_PARAM, proxyUsername);
    }

    final String proxyPassword = variableContext.getParameter("proxyPassword");
    if (proxyPassword != null) {
      parameters.setObfuscatedParameter(SlackConfig.PROXY_PASSWORD_PARAM, variableContext.mapKeyToPassword(proxyPassword));
    }

    final String proxyDomain = variableContext.getParameter("proxyDomain");
    if (proxyDomain != null) {
      parameters.setParameter(SlackConfig.PROXY_DOMAIN_PARAM, proxyDomain);
    }

    return null;
  }

  /**
  * View configuration. This method is called in the body section of the
  * connector's view configuration page. Its purpose is to present the
  * connection information to the user. The coder can presume that the HTML that
  * is output from this configuration will be within appropriate &lt;html&gt; and
  * &lt;body&gt; tags.
  *
  * @param threadContext is the local thread context.
  * @param out is the output to which any HTML should be sent.
  * @param parameters are the configuration parameters, as they currently exist, for
  * this connection being configured.
  */
  @Override
  public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters) throws ManifoldCFException, IOException {
    Map<String, Object> paramMap = new HashMap<String, Object>();

    // Fill in map from each tab
    fillInServerConfigurationMap(paramMap, out, parameters);

    Messages.outputResourceWithVelocity(out, locale, "ConfigurationView.html", paramMap);
  }


  /////////////////////////////////End of configuration UI////////////////////////////////////////////////////


  /////////////////////////////////Start of Specification UI//////////////////////////////////////////////////

  /** Output the specification header section.
  * This method is called in the head section of a job page which has selected a repository connection of the
  * current type.  Its purpose is to add the required tabs to the list, and to output any javascript methods
  * that might be needed by the job editing HTML.
  * The connector will be connected before this method can be called.
  *@param out is the output to which any HTML should be sent.
  *@param locale is the locale the output is preferred to be in.
  *@param ds is the current document specification for this job.
  *@param connectionSequenceNumber is the unique number of this connection within the job.
  *@param tabsArray is an array of tab names.  Add to this array any tab names that are specific to the connector.
  */
  @Override
  public void outputSpecificationHeader(IHTTPOutput out, Locale locale, Specification ds,
    int connectionSequenceNumber, List<String> tabsArray)
    throws ManifoldCFException, IOException {
    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("SeqNum", Integer.toString(connectionSequenceNumber));
    // Add the tabs
    tabsArray.add(Messages.getString(locale, "SlackConnector.Message"));
    Messages.outputResourceWithVelocity(out, locale, "SpecificationHeader.js", paramMap);
  }

  /** Output the specification body section.
  * This method is called in the body section of a job page which has selected a repository connection of the
  * current type.  Its purpose is to present the required form elements for editing.
  * The coder can presume that the HTML that is output from this configuration will be within appropriate
  *  &lt;html&gt;, &lt;body&gt;, and &lt;form&gt; tags.  The name of the form is always "editjob".
  * The connector will be connected before this method can be called.
  *@param out is the output to which any HTML should be sent.
  *@param locale is the locale the output is preferred to be in.
  *@param ds is the current document specification for this job.
  *@param connectionSequenceNumber is the unique number of this connection within the job.
  *@param actualSequenceNumber is the connection within the job that has currently been selected.
  *@param tabName is the current tab name.  (actualSequenceNumber, tabName) form a unique tuple within
  *  the job.
  */
  @Override
  public void outputSpecificationBody(IHTTPOutput out, Locale locale, Specification ds,
    int connectionSequenceNumber, int actualSequenceNumber, String tabName)
    throws ManifoldCFException, IOException {
    outputMessageTab(out, locale, ds, tabName, connectionSequenceNumber, actualSequenceNumber);
  }

  /**
* Take care of "Message" tab.
*/
  protected void outputMessageTab(IHTTPOutput out, Locale locale,
    Specification ds, String tabName, int connectionSequenceNumber, int actualSequenceNumber)
    throws ManifoldCFException, IOException {
    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("TabName", tabName);
    paramMap.put("SeqNum", Integer.toString(connectionSequenceNumber));
    paramMap.put("SelectedNum", Integer.toString(actualSequenceNumber));
    fillInMessageTab(paramMap, ds);
    Messages.outputResourceWithVelocity(out, locale, "Specification_Message.html", paramMap);
  }

  /**
  * Fill in Velocity context for Metadata tab.
  */
  protected static void fillInMessageTab(Map<String, Object> paramMap,
    Specification ds) {

    // Preload default values, for backwards compatibility
    String channel = "";
    String message = "";
    for (int i = 0; i < ds.getChildCount(); i++) {
      SpecificationNode sn = ds.getChild(i);
      if (sn.getType().equals(SlackConfig.NODE_CHANNEL)) {
        channel = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
      } else if (sn.getType().equals(SlackConfig.NODE_MESSAGE)) {
        message = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
      }
    }
    // If ANY of the above are non-empty, we create a new dummy record
    if (channel.length() > 0) {
      // Add the dummy records
      addRecord(paramMap, SlackConfig.NODE_FINISHED, channel, message);
      addRecord(paramMap, SlackConfig.NODE_ERRORABORTED, channel, message);
      addRecord(paramMap, SlackConfig.NODE_MANUALABORTED, channel, message);
      addRecord(paramMap, SlackConfig.NODE_MANUALPAUSED, channel, message);
      addRecord(paramMap, SlackConfig.NODE_SCHEDULEPAUSED, channel, message);
      addRecord(paramMap, SlackConfig.NODE_RESTARTED, channel, message);

    }
    else
    {
      // Initialize all records with blanks
      addRecord(paramMap, SlackConfig.NODE_FINISHED, "", "");
      addRecord(paramMap, SlackConfig.NODE_ERRORABORTED, "", "");
      addRecord(paramMap, SlackConfig.NODE_MANUALABORTED, "", "");
      addRecord(paramMap, SlackConfig.NODE_MANUALPAUSED, "", "");
      addRecord(paramMap, SlackConfig.NODE_SCHEDULEPAUSED, "", "");
      addRecord(paramMap, SlackConfig.NODE_RESTARTED, "" ,"");

      // Loop through nodes and pick them out that way
      for (int i = 0; i < ds.getChildCount(); i++) {
        SpecificationNode childNode = ds.getChild(i);
        if (childNode.getType().equals(SlackConfig.NODE_FINISHED) ||
          childNode.getType().equals(SlackConfig.NODE_ERRORABORTED) ||
          childNode.getType().equals(SlackConfig.NODE_MANUALABORTED) ||
          childNode.getType().equals(SlackConfig.NODE_MANUALPAUSED) ||
          childNode.getType().equals(SlackConfig.NODE_SCHEDULEPAUSED) ||
          childNode.getType().equals(SlackConfig.NODE_RESTARTED)) {
          channel = "";
          message = "";
          for (int j = 0; j < childNode.getChildCount(); j++) {
            SpecificationNode sn = childNode.getChild(j);
            if (sn.getType().equals(SlackConfig.NODE_CHANNEL)) {
              channel = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
            } else if (sn.getType().equals(SlackConfig.NODE_MESSAGE)) {
              message = sn.getAttributeValue(SlackConfig.ATTRIBUTE_VALUE);
            }
          }
          addRecord(paramMap, childNode.getType(), channel, message);
        }
      }
    }
  }

  protected static void addRecord(Map<String,Object> paramMap, String nodeType, String channel, String message) {
    paramMap.put(nodeType+"_CHANNEL", channel);
    paramMap.put(nodeType+"_MESSAGE", message);
  }

  /** Process a specification post.
  * This method is called at the start of job's edit or view page, whenever there is a possibility that form
  * data for a connection has been posted.  Its purpose is to gather form information and modify the
  * document specification accordingly.  The name of the posted form is always "editjob".
  * The connector will be connected before this method can be called.
  *@param variableContext contains the post data, including binary file-upload information.
  *@param locale is the locale the output is preferred to be in.
  *@param ds is the current document specification for this job.
  *@param connectionSequenceNumber is the unique number of this connection within the job.
  *@return null if all is well, or a string error message if there is an error that should prevent saving of
  * the job (and cause a redirection to an error page).
  */
  @Override
  public String processSpecificationPost(IPostParameters variableContext, Locale locale, Specification ds,
    int connectionSequenceNumber)
    throws ManifoldCFException {

    return processMessageTab(variableContext, ds, connectionSequenceNumber);
  }

  protected String processMessageTab(IPostParameters variableContext, Specification ds,
    int connectionSequenceNumber)
    throws ManifoldCFException {

    String seqPrefix = "s"+connectionSequenceNumber+"_";

    // Remove legacy nodes always
    removeNodes(ds, SlackConfig.NODE_CHANNEL);
    removeNodes(ds, SlackConfig.NODE_MESSAGE);

    // Gather all different kinds.
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_FINISHED);
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_ERRORABORTED);
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_MANUALABORTED);
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_MANUALPAUSED);
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_SCHEDULEPAUSED);
    gatherRecord(ds, seqPrefix, variableContext, SlackConfig.NODE_RESTARTED);

    return null;
  }

  protected static void gatherRecord(Specification ds, String seqPrefix, IPostParameters variableContext, String nodeType) {
    removeNodes(ds, nodeType);
    SpecificationNode sn = new SpecificationNode(nodeType);
    String channel = variableContext.getParameter(seqPrefix + nodeType + "_channel");
    if (channel != null)
    {
      addNodeValue(sn, SlackConfig.NODE_CHANNEL, channel);
    }
    String message = variableContext.getParameter(seqPrefix + nodeType + "_message");
    if (message != null)
    {
      addNodeValue(sn, SlackConfig.NODE_MESSAGE, message);
    }
    ds.addChild(ds.getChildCount(),sn);
  }

  /** View specification.
  * This method is called in the body section of a job's view page.  Its purpose is to present the document
  * specification information to the user.  The coder can presume that the HTML that is output from
  * this configuration will be within appropriate &lt;html&gt; and &lt;body&gt;tags.
  * The connector will be connected before this method can be called.
  *@param out is the output to which any HTML should be sent.
  *@param locale is the locale the output is preferred to be in.
  *@param ds is the current document specification for this job.
  *@param connectionSequenceNumber is the unique number of this connection within the job.
  */
  @Override
  public void viewSpecification(IHTTPOutput out, Locale locale, Specification ds,
    int connectionSequenceNumber)
    throws ManifoldCFException, IOException {
    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("SeqNum", Integer.toString(connectionSequenceNumber));
    fillInMessageTab(paramMap, ds);
    Messages.outputResourceWithVelocity(out, locale, "SpecificationView.html", paramMap);
  }

  ///////////////////////////////////////End of specification UI///////////////////////////////////////////////

  protected static void removeNodes(Specification ds, String nodeTypeName) {
    int i = 0;
    while (i < ds.getChildCount()) {
      SpecificationNode sn = ds.getChild(i);
      if (sn.getType().equals(nodeTypeName))
        ds.removeChild(i);
      else
        i++;
    }
  }

  protected static void addNodeValue(SpecificationNode ds, String nodeType, String value)
  {
    SpecificationNode sn = new SpecificationNode(nodeType);
    sn.setAttribute(SlackConfig.ATTRIBUTE_VALUE,value);
    ds.addChild(ds.getChildCount(),sn);
  }


  /** Handle Messaging exceptions in a consistent global manner */
  protected static void handleIOException(IOException e, String context)
    throws ManifoldCFException, ServiceInterruption
  {
    Logging.connectors.error("Slack: Error "+context+": "+e.getMessage(),e);
    throw new ManifoldCFException("Error "+context+": "+e.getMessage(),e);
  }

  /** Class to set up connection.
  */
  protected static class ConnectThread extends Thread
  {
    protected final String webHookUrl;
    protected final SlackSession.ProxySettings proxySettings;

    // Local session handle
    protected SlackSession session = null;
    protected Throwable exception = null;

    public ConnectThread(String webHookUrl, SlackSession.ProxySettings proxySettings)
    {
      this.webHookUrl = webHookUrl;
      this.proxySettings = proxySettings;
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        session = new SlackSession(webHookUrl, proxySettings);
      }
      catch (Throwable e)
      {
        exception = e;
      }
    }

    public SlackSession finishUp()
      throws IOException, InterruptedException
    {
      try
      {
        join();
        if (exception != null)
        {
          if (exception instanceof RuntimeException)
            throw (RuntimeException)exception;
          else if (exception instanceof Error)
            throw (Error)exception;
          else if (exception instanceof IOException)
            throw (IOException)exception;
          else
            throw new RuntimeException("Unknown exception type: "+exception.getClass().getName()+": "+exception.getMessage(),exception);
        }
        return session;
      } catch (InterruptedException e) {
        this.interrupt();
        throw e;
      }
    }
  }

  /** Class to close the session.
  */
  protected static class CloseSessionThread extends Thread
  {
    protected final SlackSession session;

    protected Throwable exception = null;

    public CloseSessionThread(SlackSession session)
    {
      this.session = session;
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        session.close();
      }
      catch (Throwable e)
      {
        exception = e;
      }
    }

    public void finishUp()
      throws IOException, InterruptedException
    {
      try
      {
        join();
        if (exception != null)
        {
          if (exception instanceof RuntimeException)
            throw (RuntimeException)exception;
          else if (exception instanceof Error)
            throw (Error)exception;
          else if (exception instanceof IOException)
            throw (IOException)exception;
          else
            throw new RuntimeException("Unknown exception type: "+exception.getClass().getName()+": "+exception.getMessage(),exception);
        }
      } catch (InterruptedException e) {
        this.interrupt();
        throw e;
      }
    }
  }

  /** Class to check the connection.
  */
  protected static class CheckConnectionThread extends Thread
  {
    protected final SlackSession session;

    protected Throwable exception = null;

    public CheckConnectionThread(SlackSession session)
    {
      this.session = session;
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        session.checkConnection();
      }
      catch (Throwable e)
      {
        exception = e;
      }
    }

    public void finishUp()
      throws IOException, InterruptedException
    {
      try
      {
        join();
        if (exception != null)
        {
          if (exception instanceof RuntimeException)
            throw (RuntimeException)exception;
          else if (exception instanceof Error)
            throw (Error)exception;
          else if (exception instanceof IOException)
            throw (IOException)exception;
          else
            throw new RuntimeException("Unknown exception type: "+exception.getClass().getName()+": "+exception.getMessage(),exception);
        }
      } catch (InterruptedException e) {
        this.interrupt();
        throw e;
      }
    }
  }

  /** Class to send slack messages.
  */
  protected static class SendThread extends Thread
  {
    protected final SlackSession session;
    protected final String channel;
    protected final String message;

    protected Throwable exception = null;

    public SendThread(SlackSession session, String channel, String message)
    {
      this.session = session;
      this.channel = channel;
      this.message = message;
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        session.send(channel, message);
      }
      catch (Throwable e)
      {
        exception = e;
      }
    }

    public void finishUp()
      throws IOException, InterruptedException
    {
      try
      {
        join();
        if (exception != null)
        {
          if (exception instanceof RuntimeException)
            throw (RuntimeException)exception;
          else if (exception instanceof Error)
            throw (Error)exception;
          else if (exception instanceof IOException)
            throw (IOException)exception;
          else
            throw new RuntimeException("Unknown exception type: "+exception.getClass().getName()+": "+exception.getMessage(),exception);
        }
      } catch (InterruptedException e) {
        this.interrupt();
        throw e;
      }
    }
  }
}