/* $Id: CmisAuthorityConnector.java 1791824 2017-04-18 19:08:40Z kamaci $ */

/**
 * 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.authorities.authorities.cmis;

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

import org.apache.commons.lang.StringUtils;
import org.apache.manifoldcf.authorities.authorities.BaseAuthorityConnector;
import org.apache.manifoldcf.authorities.interfaces.AuthorizationResponse;
import org.apache.manifoldcf.core.interfaces.CacheManagerFactory;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.ICacheCreateHandle;
import org.apache.manifoldcf.core.interfaces.ICacheDescription;
import org.apache.manifoldcf.core.interfaces.ICacheHandle;
import org.apache.manifoldcf.core.interfaces.ICacheManager;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
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.StringSet;
import org.apache.manifoldcf.crawler.system.Logging;
import org.apache.manifoldcf.ui.util.Encoder;

/**
 * 
 * The CMIS Authority Connector is based only on a regular expression checker, because the CMIS specification 
 * (and the Apache Chemistry implementation) doesn't have any exposed method to get info about users.
 * 
 * For the configuration we can assume that we could have different users for any CMIS repository that 
 * could have the same endpoint. That's why for this connector is required the Repository ID.
 * 
 * @author Piergiorgio Lucidi
 * 
 */
public class CmisAuthorityConnector extends BaseAuthorityConnector {

  public static final String CONFIG_PARAM_USERNAME = "username";
  public static final String CONFIG_PARAM_PASSWORD = "password";
  public static final String CONFIG_PARAM_ENDPOINT = "endpoint";
  public static final String CONFIG_PARAM_REPOSITORY_ID = "repositoryId";
  
  protected static final String CONFIG_PARAM_USERNAME_REGEXP = "usernameregexp";
  protected static final String CONFIG_PARAM_USER_TRANSLATION = "usertranslation";
  
  private static final String DEFAULT_VALUE_ENDPOINT = "http://localhost:8080/cmis/";
  private static final String DEFAULT_VALUE_REPOSITORY_ID = "uuid";

  protected String endpoint = null;
  protected String repositoryId = null;

  protected Map<String, String> parameters = new HashMap<String, String>();
  
  /** The cache manager. */
  protected ICacheManager cacheManager = null;
  
  protected static long responseLifetime = 60000L;
  protected static int LRUsize = 1000;
  protected static StringSet emptyStringSet = new StringSet();
  
  /** This is the active directory global deny token.  This should be ingested with all documents. */
  public static final String GLOBAL_DENY_TOKEN = "DEAD_AUTHORITY";
  
  /** Unreachable CMIS */
  private static final AuthorizationResponse unreachableResponse = new AuthorizationResponse(
    new String[]{GLOBAL_DENY_TOKEN},AuthorizationResponse.RESPONSE_UNREACHABLE);
  
  /** User not found */
  private static final AuthorizationResponse userNotFoundResponse = new AuthorizationResponse(
    new String[]{GLOBAL_DENY_TOKEN},AuthorizationResponse.RESPONSE_USERNOTFOUND);

  /** Set thread context.
   */
   @Override
   public void setThreadContext(IThreadContext tc)
     throws ManifoldCFException {
     super.setThreadContext(tc);
     cacheManager = CacheManagerFactory.make(tc);
   }
   
   /** Clear thread context.
   */
   @Override
   public void clearThreadContext() {
     super.clearThreadContext();
     cacheManager = null;
   }
  
  public CmisAuthorityConnector() {
    super();
  }
  
  
  /**
   * Output the configuration body section. This method is called in the body
   * section of the authority connector's configuration page. 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
   * <html>, <body>, and <form> tags. The name of the form is "editconnection".
   * 
   * @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.
   * @param tabName
   *          is the current tab name.
   */
  @Override
  public void outputConfigurationBody(IThreadContext threadContext,
      IHTTPOutput out, Locale locale, ConfigParams parameters, String tabName)
      throws ManifoldCFException, IOException {

    String endpoint = parameters.getParameter(CONFIG_PARAM_ENDPOINT);
    String repositoryId = parameters.getParameter(CONFIG_PARAM_REPOSITORY_ID);
    
    if(StringUtils.isEmpty(endpoint))
      endpoint = DEFAULT_VALUE_ENDPOINT;
    if(StringUtils.isEmpty(repositoryId))
      repositoryId = DEFAULT_VALUE_REPOSITORY_ID;
    
    if (tabName.equals(Messages.getString(locale,"CmisAuthorityConnector.Repository")))
    {
    out.print("<table class=\"displaytable\">\n"
        + "  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n");
    out.print("<tr><td class=\"description\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.Endpoint") + "</nobr></td>" +
        "<td class=\"value\"><input type=\"text\" name=\""
        + CONFIG_PARAM_ENDPOINT + "\" value=\""+Encoder.attributeEscape(endpoint)+"\" size=\"50\"/></td></tr>\n");
    out.print("<tr><td class=\"description\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.RepositoryID") + "</nobr></td>" +
        "<td class=\"value\"><input type=\"text\" name=\""
        + CONFIG_PARAM_REPOSITORY_ID + "\" value=\""+Encoder.attributeEscape(repositoryId)+"\"/></td></tr>\n");
    out.print("</table>\n");
    }
    else
    {
      out.print("<input type=\"hidden\" name=\""+CONFIG_PARAM_ENDPOINT+"\" value=\""+Encoder.attributeEscape(endpoint)+"\"/>\n");
      out.print("<input type=\"hidden\" name=\""+CONFIG_PARAM_REPOSITORY_ID+"\" value=\""+Encoder.attributeEscape(repositoryId)+"\"/>\n");
    }
    
  }

  /**
   * 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.
   * 
   * @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.
   * @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,"CmisAuthorityConnector.Repository"));
    
    out.print("<script type=\"text/javascript\">\n" + "<!--\n"
        + "function checkConfig()\n" + "{\n"
        + "  if (editconnection.endpoint.value == \"\")\n" + "  {\n"
        + "    alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheEndpointMustBeNotNull") + "\");\n"
        + "    editconnection.endpoint.focus();\n" + "    return false;\n"
        + "  }\n" + "\n" + "  return true;\n" + "}\n" + " \n"
        + "function checkConfigForSave()\n" + "{\n"
        + "  if (editconnection.endpoint.value == \"\")\n" + "  {\n"
        + "    alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheEndpointMustBeNotNull") + "\");\n"
        + "    editconnection.endpoint.focus();\n" + "    return false;\n"
        + "  }\n" + "  if (editconnection.repositoryId.value == \"\")\n" + "  {\n"
        + "    alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheRepositoryIDMustBeNotNull") + "\");\n"
        + "    editconnection.repositoryId.focus();\n" + "    return false;\n"
        + "  }\n" + "  return true;\n" + "}\n" + "\n" + "//-->\n"
        + "</script>\n");
  }

  /**
   * 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 "editconnection".
   * 
   * @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, Locale locale, ConfigParams parameters)
      throws ManifoldCFException {

    //Repository
    String endpoint = variableContext.getParameter(CONFIG_PARAM_ENDPOINT);
    if (StringUtils.isNotEmpty(endpoint) && endpoint.length() > 0)
      parameters.setParameter(CONFIG_PARAM_ENDPOINT, endpoint);

    String repositoryId = variableContext
        .getParameter(CONFIG_PARAM_REPOSITORY_ID);
    if (StringUtils.isNotEmpty(repositoryId))
      parameters.setParameter(CONFIG_PARAM_REPOSITORY_ID, repositoryId);
    
    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 <html> and
   * <body> 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 {
    out.print("<table class=\"displaytable\">\n"
        + "  <tr>\n"
        + "    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.Parameters") + "</nobr></td>\n"
        + "    <td class=\"value\" colspan=\"3\">\n");
    Iterator iter = parameters.listParameters();
    while (iter.hasNext()) {
      String param = (String) iter.next();
      String value = parameters.getParameter(param);
        out.print("      <nobr>"
            + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "="
            + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value)
            + "</nobr><br/>\n");
    }
    out.print("</td>\n" + "  </tr>\n" + "</table>\n");
  }
  
  /** Obtain the default access tokens for a given user name.
   *@param userName is the user name or identifier.
   *@return the default response tokens, presuming that the connect method fails.
   */
   @Override
   public AuthorizationResponse getDefaultAuthorizationResponse(String userName)
   {
     return unreachableResponse;
   }
   
   /** Uncached version of the getAuthorizationResponse method.
    *@param userName is the user name or identifier.
    *@return the response tokens (according to the current authority).
    * (Should throws an exception only when a condition cannot be properly described within the authorization response object.)
    */
    protected AuthorizationResponse getAuthorizationResponseUncached(String userName)
      throws ManifoldCFException
    {
      if (Logging.connectors.isDebugEnabled())
        Logging.connectors.debug("CMIS: Calculating response access tokens for user '"+userName+"'");

      // Map the user to the final value
      String verifiedUserName = userName;

      if (Logging.connectors.isDebugEnabled())
        Logging.connectors.debug("CMIS: Mapped user name is '"+verifiedUserName+"'");
      
      String[] tokens = new String[1];
      tokens[0] = verifiedUserName;
      return new AuthorizationResponse(tokens,AuthorizationResponse.RESPONSE_OK);

    }
  
  /** Obtain the access tokens for a given user name.
   *@param userName is the user name or identifier.
   *@return the response tokens (according to the current authority).
   * (Should throws an exception only when a condition cannot be properly described within the authorization response object.)
   */
   @Override
   public AuthorizationResponse getAuthorizationResponse(String userName)
     throws ManifoldCFException
   {
     if (Logging.connectors.isDebugEnabled())
       Logging.connectors.debug("CMIS: Received request for user '"+userName+"'");
     
     ICacheDescription objectDescription = new AuthorizationResponseDescription(userName,endpoint,repositoryId);
     
     // Enter the cache
     ICacheHandle ch = cacheManager.enterCache(new ICacheDescription[]{objectDescription},null,null);
     try
     {
       ICacheCreateHandle createHandle = cacheManager.enterCreateSection(ch);
       try
       {
         // Lookup the object
         AuthorizationResponse response = (AuthorizationResponse)cacheManager.lookupObject(createHandle,objectDescription);
         if (response != null)
           return response;
         // Create the object.
         response = getAuthorizationResponseUncached(userName);
         // Save it in the cache
         cacheManager.saveObject(createHandle,objectDescription,response);
         // And return it...
         return response;
       }
       finally
       {
         cacheManager.leaveCreateSection(createHandle);
       }
     }
     finally
     {
       cacheManager.leaveCache(ch);
     }
   }
   
   /** This is the cache object descriptor for cached access tokens from
    * this connector.
    */
    protected static class AuthorizationResponseDescription extends org.apache.manifoldcf.core.cachemanager.BaseDescription
    {
      /** The user name associated with the access tokens */
      protected final String userName;
      /** The repository endpoint */
      protected final String endpoint;
      /** The repository id */
      protected final String repositoryId;
      /** The expiration time */
      protected long expirationTime = -1;
      
      /** Constructor. */
      public AuthorizationResponseDescription(String userName, String endpoint, String repositoryId)
      {
        super("CMISAuthority",LRUsize);
        this.userName = userName;
        this.endpoint = endpoint;
        this.repositoryId = repositoryId;
      }

      /** Return the invalidation keys for this object. */
      public StringSet getObjectKeys()
      {
        return emptyStringSet;
      }

      /** Get the critical section name, used for synchronizing the creation of the object */
      public String getCriticalSectionName()
      {
        return getClass().getName() + "-" + userName + "-" + endpoint +
          "-" + repositoryId;
      }

      /** Return the object expiration interval */
      public long getObjectExpirationTime(long currentTime)
      {
        if (expirationTime == -1)
          expirationTime = currentTime + responseLifetime;
        return expirationTime;
      }

      public int hashCode()
      {
        return userName.hashCode() + endpoint.hashCode() + repositoryId.hashCode();
      }
      
      public boolean equals(Object o)
      {
        if (!(o instanceof AuthorizationResponseDescription))
          return false;
        AuthorizationResponseDescription ard = (AuthorizationResponseDescription)o;
        return ard.userName.equals(userName) && ard.endpoint.equals(endpoint) &&
          ard.repositoryId.equals(repositoryId);
      }
      
    }
    
}
