/**
* 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.connectors.nuxeo;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;

import org.apache.commons.lang.StringUtils;
import org.apache.manifoldcf.agents.interfaces.RepositoryDocument;
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.connectors.BaseRepositoryConnector;
import org.apache.manifoldcf.crawler.connectors.nuxeo.model.Attachment;
import org.apache.manifoldcf.crawler.connectors.nuxeo.model.DocumentManifold;
import org.apache.manifoldcf.crawler.interfaces.IExistingVersions;
import org.apache.manifoldcf.crawler.interfaces.IProcessActivity;
import org.apache.manifoldcf.crawler.interfaces.IRepositoryConnector;
import org.apache.manifoldcf.crawler.interfaces.ISeedingActivity;
import org.nuxeo.client.api.NuxeoClient;
import org.nuxeo.client.api.objects.Document;
import org.nuxeo.client.api.objects.Documents;
import org.nuxeo.client.internals.spi.NuxeoClientException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * 
 * Nuxeo Repository Connector class
 * 
 * @author David Arroyo Escobar <arroyoescobardavid@gmail.com>
 *
 */
public class NuxeoRepositoryConnector extends BaseRepositoryConnector {

  private static final String URI_DOCUMENT = "SELECT * FROM Document";

  protected final static String ACTIVITY_READ = "read document";

  // Configuration tabs
  private static final String NUXEO_SERVER_TAB_PROPERTY = "NuxeoRepositoryConnector.Server";

  // Specification tabs
  private static final String CONF_DOMAINS_TAB_PROPERTY = "NuxeoRepositoryConnector.Domains";
  private static final String CONF_DOCUMENTS_TYPE_TAB_PROPERTY = "NuxeoRepositoryConnector.DocumentsType";
  private static final String CONF_DOCUMENT_PROPERTY = "NuxeoRepositoryConnector.Documents";

  // Prefix for nuxeo configuration and specification parameters
  private static final String PARAMETER_PREFIX = "nuxeo_";

  // Templates for Nuxeo configuration
  /**
   * Javascript to check the configuration parameters
   */
  private static final String EDIT_CONFIG_HEADER_FORWARD = "editConfiguration_conf.js";

  /**
   * Server edit tab template
   */
  private static final String EDIT_CONFIG_FORWARD_SERVER = "editConfiguration_conf_server.html";

  /**
   * Server view tab template
   */
  private static final String VIEW_CONFIG_FORWARD = "viewConfiguration_conf.html";

  // Templates for Nuxeo specification
  /**
   * Forward to the javascript to check the specification parameters for the
   * job
   */
  private static final String EDIT_SPEC_HEADER_FORWARD = "editSpecification_conf.js";

  /**
   * Forward to the template to edit domains for the job
   */
  private static final String EDIT_SPEC_FORWARD_CONF_DOMAINS = "editSpecification_confDomains.html";

  /**
   * Forward to the template to edit documents type for the job
   */
  private static final String EDIT_SPEC_FORWARD_CONF_DOCUMENTS_TYPE = "editSpecification_confDocumentsType.html";

  /**
   * Forward to the template to edit document properties for the job
   */
  private static final String EDIT_SPEC_FORWARD_CONF_DOCUMENTS = "editSpecification_confDocuments.html";

  /**
   * Forward to the template to view the specification parameters for the job
   */
  private static final String VIEW_SPEC_FORWARD = "viewSpecification_conf.html";

  protected long lastSessionFetch = -1L;
  protected static final long timeToRelease = 300000L;

  private Logger logger = LoggerFactory.getLogger(NuxeoRepositoryConnector.class);

  /* Nuxeo instance parameters */
  protected String protocol = null;
  protected String host = null;
  protected String port = null;
  protected String path = null;
  protected String username = null;
  protected String password = null;

  protected NuxeoClient nuxeoClient = null;

  // Constructor
  public NuxeoRepositoryConnector() {
    super();
  }

  void setNuxeoClient(NuxeoClient nuxeoClient) {
    this.nuxeoClient = nuxeoClient;
  }

  @Override
  public String[] getActivitiesList() {
    return new String[] { ACTIVITY_READ };
  }

  @Override
  public String[] getBinNames(String documentIdenfitier) {
    return new String[] { host };
  }

  /** CONFIGURATION CONNECTOR **/
  @Override
  public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out, Locale locale,
      ConfigParams parameters, List<String> tabsArray) throws ManifoldCFException, IOException {

    // Server tab
    tabsArray.add(Messages.getString(locale, NUXEO_SERVER_TAB_PROPERTY));

    Map<String, String> paramMap = new HashMap<String, String>();

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

    Messages.outputResourceWithVelocity(out, locale, EDIT_CONFIG_HEADER_FORWARD, paramMap, true);
  }

  @Override
  public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out, Locale locale,
      ConfigParams parameters, String tabName) throws ManifoldCFException, IOException {

    // Call the Velocity tempaltes for each tab
    Map<String, String> paramMap = new HashMap<String, String>();

    // Set the tab name
    paramMap.put("TabName", tabName);

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

    // Server tab
    Messages.outputResourceWithVelocity(out, locale, EDIT_CONFIG_FORWARD_SERVER, paramMap, true);

  }

  private static void fillInServerConfigurationMap(Map<String, String> serverMap, IPasswordMapperActivity mapper,
      ConfigParams parameters) {

    String nuxeoProtocol = parameters.getParameter(NuxeoConfiguration.Server.PROTOCOL);
    String nuxeoHost = parameters.getParameter(NuxeoConfiguration.Server.HOST);
    String nuxeoPort = parameters.getParameter(NuxeoConfiguration.Server.PORT);
    String nuxeoPath = parameters.getParameter(NuxeoConfiguration.Server.PATH);
    String nuxeoUsername = parameters.getParameter(NuxeoConfiguration.Server.USERNAME);
    String nuxeoPassword = parameters.getObfuscatedParameter(NuxeoConfiguration.Server.PASSWORD);

    if (nuxeoProtocol == null)
      nuxeoProtocol = NuxeoConfiguration.Server.PROTOCOL_DEFAULT_VALUE;
    if (nuxeoHost == null)
      nuxeoHost = NuxeoConfiguration.Server.HOST_DEFAULT_VALUE;
    if (nuxeoPort == null)
      nuxeoPort = NuxeoConfiguration.Server.PORT_DEFAULT_VALUE;
    if (nuxeoPath == null)
      nuxeoPath = NuxeoConfiguration.Server.PATH_DEFAULT_VALUE;
    if (nuxeoUsername == null)
      nuxeoUsername = NuxeoConfiguration.Server.USERNAME_DEFAULT_VALUE;
    if (nuxeoPassword == null)
      nuxeoPassword = NuxeoConfiguration.Server.PASSWORD_DEFAULT_VALUE;
    else
      nuxeoPassword = mapper.mapPasswordToKey(nuxeoPassword);

    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.PROTOCOL, nuxeoProtocol);
    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.HOST, nuxeoHost);
    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.PORT, nuxeoPort);
    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.PATH, nuxeoPath);
    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.USERNAME, nuxeoUsername);
    serverMap.put(PARAMETER_PREFIX + NuxeoConfiguration.Server.PASSWORD, nuxeoPassword);

  }

  @Override
  public String processConfigurationPost(IThreadContext thredContext, IPostParameters variableContext,
      ConfigParams parameters) {

    String nuxeoProtocol = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.PROTOCOL);
    if (nuxeoProtocol != null)
      parameters.setParameter(NuxeoConfiguration.Server.PROTOCOL, nuxeoProtocol);

    String nuxeoHost = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.HOST);
    if (nuxeoHost != null)
      parameters.setParameter(NuxeoConfiguration.Server.HOST, nuxeoHost);

    String nuxeoPort = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.PORT);
    if (nuxeoPort != null)
      parameters.setParameter(NuxeoConfiguration.Server.PORT, nuxeoPort);

    String nuxeoPath = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.PATH);
    if (nuxeoPath != null)
      parameters.setParameter(NuxeoConfiguration.Server.PATH, nuxeoPath);

    String nuxeoUsername = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.USERNAME);
    if (nuxeoUsername != null)
      parameters.setParameter(NuxeoConfiguration.Server.USERNAME, nuxeoUsername);

    String nuxeoPassword = variableContext.getParameter(PARAMETER_PREFIX + NuxeoConfiguration.Server.PASSWORD);
    if (nuxeoPassword != null)
      parameters.setObfuscatedParameter(NuxeoConfiguration.Server.PASSWORD,
          variableContext.mapKeyToPassword(nuxeoPassword));

    return null;
  }

  @Override
  public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters)
      throws ManifoldCFException, IOException {

    Map<String, String> paramMap = new HashMap<String, String>();

    fillInServerConfigurationMap(paramMap, out, parameters);

    Messages.outputResourceWithVelocity(out, locale, VIEW_CONFIG_FORWARD, paramMap, true);
  }

  /** CONNECTION **/
  @Override
  public void connect(ConfigParams configParams) {
    super.connect(configParams);

    protocol = params.getParameter(NuxeoConfiguration.Server.PROTOCOL);
    host = params.getParameter(NuxeoConfiguration.Server.HOST);
    port = params.getParameter(NuxeoConfiguration.Server.PORT);
    path = params.getParameter(NuxeoConfiguration.Server.PATH);
    username = params.getParameter(NuxeoConfiguration.Server.USERNAME);
    password = params.getObfuscatedParameter(NuxeoConfiguration.Server.PASSWORD);

  }

  @Override
  public void disconnect() throws ManifoldCFException {
    shutdownNuxeoClient();
    protocol = null;
    host = null;
    port = null;
    path = null;
    username = null;
    password = null;
    super.disconnect();
  }
  
  // Check the connection
  @Override
  public String check() throws ManifoldCFException {
    shutdownNuxeoClient();
    initNuxeoClient();
    try {
      nuxeoClient.repository().getDocumentRoot();
    } catch (NuxeoClientException ex) {
      return "Connection failed: "+ex.getMessage();
    }

    return super.check();
  }

  /**
   * Initialize Nuxeo client using the configured parameters.
   * 
   * @throws ManifoldCFException
   */
  private void initNuxeoClient() throws ManifoldCFException {
    if (nuxeoClient == null) {

      if (StringUtils.isEmpty(protocol)) {
        throw new ManifoldCFException(
            "Parameter " + NuxeoConfiguration.Server.PROTOCOL + " required but not set");
      }

      if (StringUtils.isEmpty(host)) {
        throw new ManifoldCFException("Parameter " + NuxeoConfiguration.Server.HOST + " required but not set");
      }

      String url = getUrl();
      nuxeoClient = new NuxeoClient(url, username, password);

      lastSessionFetch = System.currentTimeMillis();

    }

  }

  /**
   * Shut down Nuxeo client
   */
  private void shutdownNuxeoClient() {
    if (nuxeoClient != null) {
      nuxeoClient.shutdown();
      nuxeoClient = null;
      lastSessionFetch = -1L;
    }
  }
  
  /**
   * Formatter URL
   * 
   * @throws ManifoldCFException
   */
  String getUrl() throws ManifoldCFException {
    int portInt;
    if (port != null && port.length() > 0) {
      try {
        portInt = Integer.parseInt(port);
      } catch (NumberFormatException formatException) {
        throw new ManifoldCFException("Bad number: " + formatException.getMessage(), formatException);
      }
    } else {
      if (protocol.toLowerCase(Locale.ROOT).equals("http")) {
        portInt = 80;
      } else {
        portInt = 443;
      }
    }

    String url = protocol + "://" + host + ":" + portInt + "/" + path;

    return url;
  }

  @Override
  public boolean isConnected() {
    return nuxeoClient != null;
  }

  @Override
  public void poll() throws ManifoldCFException {
    if (lastSessionFetch == -1L) {
      return;
    }

    long currentTime = System.currentTimeMillis();

    if (currentTime > lastSessionFetch + timeToRelease) {
      shutdownNuxeoClient();
    }
  }

  /** SEEDING **/
  @Override
  public String addSeedDocuments(ISeedingActivity activities, Specification spec, String lastSeedVersion,
      long seedTime, int jobMode) throws ManifoldCFException, ServiceInterruption {

    initNuxeoClient();
    try {

      int lastStart = 0;
      int defaultSize = 50;
      Boolean isLast = true;
      NuxeoSpecification ns = NuxeoSpecification.from(spec);
      List<String> domains = ns.getDomains();
      List<String> documentsType = ns.getDocumentsType();

      do {

        Documents docs = getDocsByDate(nuxeoClient, lastSeedVersion, domains, documentsType,
            defaultSize, lastStart);

        for (Document doc : docs.getDocuments()) {
          activities.addSeedDocument(doc.getUid());
        }

        lastStart++;
        isLast = docs.getIsNextPageAvailable();

      } while (isLast);

      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
      sdf.setTimeZone(TimeZone.getTimeZone("GMT"));

      lastSeedVersion = sdf.format(new Date());

      return lastSeedVersion;
    } catch (NuxeoClientException exception) {
      throw new ManifoldCFException("Failure during seeding: "+exception.getMessage(), exception);
      /*
      exception.printStackTrace();
      long interruptionRetryTime = 5L * 60L * 1000L;
      String message = "Server appears down during seeding: " + exception.getMessage();
      throw new ServiceInterruption(message, exception, System.currentTimeMillis() + interruptionRetryTime, -1L,
          3, true);
      */
    }
  }
  
  /**
   * 
   * @param nuxeoClient
   * @param date
   * @param domains
   * @param documentsType
   * @param limit
   * @param start
   * @return Documents
   */
  Documents getDocsByDate(NuxeoClient nuxeoClient, String date, List<String> domains,
      List<String> documentsType, int limit, int start) {

    String query = "";

    if (date == null || date.isEmpty()) {
      SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
      date = DATE_FORMAT.format(new Date(0));
    }
    query = "dc:modified > '" + date + "'";

    if (!domains.isEmpty()) {
      Iterator<String> itdom = domains.iterator();

      query = String.format(" %s AND ( ecm:path STARTSWITH '/%s'", query, itdom.next());

      while (itdom.hasNext()) {
        query = String.format("%s OR ecm:path STARTSWITH '/%s'", query, itdom.next());
      }

      query = String.format("%s )", query);
    }

    if (!documentsType.isEmpty()) {
      Iterator<String> itDocTy = documentsType.iterator();

      query = String.format(" %s AND ( ecm:primaryType = '%s'", query, itDocTy.next());

      while (itDocTy.hasNext()) {
        query = String.format("%s OR ecm:primaryType = '%s'", query, itDocTy.next());
      }

      query = String.format("%s )", query);
    }

    query = String.format(URI_DOCUMENT + " where %s ", query);

    nuxeoClient.header("X-NXDocumentProperties", "*");
    Documents docs = nuxeoClient.repository().query(query, String.valueOf(limit), String.valueOf(start), null, null,
        null, null);

    return docs;
  }

  /** PROCESS DOCUMENTS **/

  @Override
  public void processDocuments(String[] documentsIdentifieres, IExistingVersions statuses, Specification spec,
      IProcessActivity activities, int jobMode, boolean usesDefaultAuthority)
      throws ManifoldCFException, ServiceInterruption {

    initNuxeoClient();
        
    for (int i = 0; i < documentsIdentifieres.length; i++) {

      String documentId = documentsIdentifieres[i];
      String version = statuses.getIndexedVersionString(documentId);

      long startTime = System.currentTimeMillis();
      ProcessResult pResult = null;
      boolean doLog = true;

      try {

        pResult = processDocument(documentId, spec, version, activities, doLog,
            Maps.<String, String> newHashMap());
      } catch (NuxeoClientException exception) {
        throw new ManifoldCFException("Client exception: "+exception.getMessage(), exception);
      } catch (IOException exception) {
        long interruptionRetryTime = 5L * 60L * 1000L;
        String message = "Server appears down during seeding: " + exception.getMessage();
        throw new ServiceInterruption(message, exception, System.currentTimeMillis() + interruptionRetryTime,
            -1L, 3, true);
      } finally {
        if (doLog)
          if (pResult != null && pResult.errorCode != null && !pResult.errorCode.isEmpty())
            activities.recordActivity(new Long(startTime), ACTIVITY_READ, pResult.fileSize, documentId,
                pResult.errorCode, pResult.errorDecription, null);
      }

    }
  }

  /**
   * @param documentId
   * @param version
   * @param activities
   * @param doLog
   * @param newHashMap
   * @return
   */

  private ProcessResult processDocument(String documentId, Specification spec, String version,
      IProcessActivity activities, boolean doLog, HashMap<String, String> extraProperties)
      throws ManifoldCFException, ServiceInterruption, IOException {

    DocumentManifold doc = new DocumentManifold(nuxeoClient.repository().fetchDocumentById(documentId));

    return processDocumentInternal(doc, documentId, spec, version, activities, doLog, extraProperties);
  }

  /**
   * @param doc
   * @param documentId
   * @param version
   * @param activities
   * @param doLog
   * @param extraProperties
   * @return
   */

  private ProcessResult processDocumentInternal(DocumentManifold doc, String manifoldDocumentIdentifier,
      Specification spec, String version, IProcessActivity activities, boolean doLog,
      HashMap<String, String> extraProperties) throws ManifoldCFException, ServiceInterruption, IOException {

    RepositoryDocument rd = new RepositoryDocument();
    NuxeoSpecification ns = NuxeoSpecification.from(spec);

    String lastModified = doc.getDocument().getLastModified();
    Date lastModifiedDate = null;

    if (lastModified != null) {
      DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
      try {
        lastModifiedDate = formatter.parse(lastModified);
      } catch (Exception ex) {
        lastModifiedDate = new Date(0);
      }
    }

    int lenght = doc.getLenght();
    if (doc.getDocument().getState() != null
        && doc.getDocument().getState().equalsIgnoreCase(DocumentManifold.DELETED)) {
      activities.deleteDocument(manifoldDocumentIdentifier);
      return new ProcessResult(lenght, "DELETED", "");
    }

    if (!activities.checkDocumentNeedsReindexing(manifoldDocumentIdentifier, lastModified)) {
      return new ProcessResult(lenght, "RETAINED", "");
    }

    if (doc.getDocument().getUid() == null) {
      activities.deleteDocument(manifoldDocumentIdentifier);
      return new ProcessResult(lenght, "DELETED", "");
    }

    // Add respository document information
    rd.setMimeType(doc.getMimeType());
    rd.setModifiedDate(lastModifiedDate);
    rd.setIndexingDate(new Date());

    // Adding Document metadata
    Map<String, Object> docMetadata = doc.getMetadata();

    for (Entry<String, Object> entry : docMetadata.entrySet()) {
      if (entry.getValue() instanceof List) {
        List<?> list = (List<?>) entry.getValue();
        rd.addField(entry.getKey(), list.toArray(new String[list.size()]));
      } else {
        rd.addField(entry.getKey(), entry.getValue().toString());
      }
    }

    if (ns.isProcessTags())
      rd.addField("Tags", doc.getTags(nuxeoClient));

    String documentUri = null;
    try {
      documentUri = getUrl() + "/nxpath/" + doc.getDocument().getRepositoryName() + doc.getDocument().getPath()
          + "@view_documents";
    } catch (NuxeoClientException ex) {
      documentUri = doc.getDocument().getUid();
    }

    // Set repository ACLs
    String[] permissions = doc.getPermissions(nuxeoClient);
    rd.setSecurityACL(RepositoryDocument.SECURITY_TYPE_DOCUMENT, permissions);
    rd.setSecurityDenyACL(RepositoryDocument.SECURITY_TYPE_DOCUMENT, new String[] { GLOBAL_DENY_TOKEN });
    rd.setBinary(doc.getContent(), lenght);

    // Action
    activities.ingestDocumentWithException(manifoldDocumentIdentifier, lastModified, documentUri, rd);

    if (ns.isProcessAttachments()) {
      for (Attachment att : doc.getAttachments(nuxeoClient)) {
        RepositoryDocument att_rd = new RepositoryDocument();
        String attDocumentUri = att.getUrl();

        att_rd.setMimeType(att.getMime_type());
        att_rd.setBinary(att.getData(), att.getLength());

        if (lastModified != null)
          att_rd.setModifiedDate(lastModifiedDate);
        att_rd.setIndexingDate(new Date());

        att_rd.addField(Attachment.ATT_KEY_NAME, att.getName());
        att_rd.addField(Attachment.ATT_KEY_LENGTH, String.valueOf(att.getLength()));
        att_rd.addField(Attachment.ATT_KEY_DIGEST, att.getDigest());
        att_rd.addField(Attachment.ATT_KEY_DIGEST_ALGORITHM, att.getDigestAlgorithm());
        att_rd.addField(Attachment.ATT_KEY_ENCODING, att.getEncoding());
        // Set repository ACLs
        att_rd.setSecurityACL(RepositoryDocument.SECURITY_TYPE_DOCUMENT, permissions);
        att_rd.setSecurityDenyACL(RepositoryDocument.SECURITY_TYPE_DOCUMENT,
            new String[] { GLOBAL_DENY_TOKEN });

        activities.ingestDocumentWithException(manifoldDocumentIdentifier, attDocumentUri, lastModified,
            attDocumentUri, att_rd);
      }
    }

    return new ProcessResult(lenght, "OK", StringUtils.EMPTY);
  }

  private class ProcessResult {
    private long fileSize;
    private String errorCode;
    private String errorDecription;

    private ProcessResult(long fileSize, String errorCode, String errorDescription) {
      this.fileSize = fileSize;
      this.errorCode = errorCode;
      this.errorDecription = errorDescription;
    }

  }

  @Override
  public int getConnectorModel() {
    return IRepositoryConnector.MODEL_ADD_CHANGE_DELETE;
  }

  /** Specifications **/

  @Override
  public void viewSpecification(IHTTPOutput out, Locale locale, Specification spec, int connectionSequenceNumber)
      throws ManifoldCFException, IOException {

    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("SeqNum", Integer.toString(connectionSequenceNumber));

    NuxeoSpecification ns = NuxeoSpecification.from(spec);

    paramMap.put(NuxeoConfiguration.Specification.DOMAINS.toUpperCase(), ns.getDomains());
    paramMap.put(NuxeoConfiguration.Specification.DOCUMENTS_TYPE.toUpperCase(), ns.documentsType);
    paramMap.put(NuxeoConfiguration.Specification.PROCESS_TAGS.toUpperCase(), ns.isProcessTags().toString());
    paramMap.put(NuxeoConfiguration.Specification.PROCESS_ATTACHMENTS.toUpperCase(),
        ns.isProcessAttachments().toString());

    Messages.outputResourceWithVelocity(out, locale, VIEW_SPEC_FORWARD, paramMap);
  }

  @Override
  public String processSpecificationPost(IPostParameters variableContext, Locale locale, Specification ds,
      int connectionSequenceNumber) throws ManifoldCFException {

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

    // DOMAINS
    String xc = variableContext.getParameter(seqPrefix + "domainscount");

    if (xc != null) {
      // Delete all preconfigured domains
      int i = 0;
      while (i < ds.getChildCount()) {
        SpecificationNode sn = ds.getChild(i);
        if (sn.getType().equals(NuxeoConfiguration.Specification.DOMAINS)) {
          ds.removeChild(i);
        } else {
          i++;
        }
      }

      SpecificationNode domains = new SpecificationNode(NuxeoConfiguration.Specification.DOMAINS);
      ds.addChild(ds.getChildCount(), domains);
      int domainsCount = Integer.parseInt(xc);
      i = 0;
      while (i < domainsCount) {
        String domainDescription = "_" + Integer.toString(i);
        String domainOpName = seqPrefix + "domainop" + domainDescription;
        xc = variableContext.getParameter(domainOpName);
        if (xc != null && xc.equals("Delete")) {
          i++;
          continue;
        }

        String domainKey = variableContext.getParameter(seqPrefix + "domain" + domainDescription);
        SpecificationNode node = new SpecificationNode(NuxeoConfiguration.Specification.DOMAIN);
        node.setAttribute(NuxeoConfiguration.Specification.DOMAIN_KEY, domainKey);
        domains.addChild(domains.getChildCount(), node);
        i++;
      }

      String op = variableContext.getParameter(seqPrefix + "domainop");
      if (op != null && op.equals("Add")) {
        String domainSpec = variableContext.getParameter(seqPrefix + "domain");
        SpecificationNode node = new SpecificationNode(NuxeoConfiguration.Specification.DOMAIN);
        node.setAttribute(NuxeoConfiguration.Specification.DOMAIN_KEY, domainSpec);
        domains.addChild(domains.getChildCount(), node);
      }
    }

    // TYPE OF DOCUMENTS
    String xt = variableContext.getParameter(seqPrefix + "documentsTypecount");

    if (xt != null) {
      // Delete all preconfigured type of documents
      int i = 0;
      while (i < ds.getChildCount()) {
        SpecificationNode sn = ds.getChild(i);
        if (sn.getType().equals(NuxeoConfiguration.Specification.DOCUMENTS_TYPE)) {
          ds.removeChild(i);
        } else {
          i++;
        }
      }

      SpecificationNode documentsType = new SpecificationNode(NuxeoConfiguration.Specification.DOCUMENTS_TYPE);
      ds.addChild(ds.getChildCount(), documentsType);
      int documentsTypeCount = Integer.parseInt(xt);
      i = 0;
      while (i < documentsTypeCount) {
        String documentTypeDescription = "_" + Integer.toString(i);
        String documentTypeOpName = seqPrefix + "documentTypeop" + documentTypeDescription;
        xt = variableContext.getParameter(documentTypeOpName);
        if (xt != null && xt.equals("Delete")) {
          i++;
          continue;
        }

        String documentTypeKey = variableContext
            .getParameter(seqPrefix + "documentType" + documentTypeDescription);
        SpecificationNode node = new SpecificationNode(NuxeoConfiguration.Specification.DOCUMENT_TYPE);
        node.setAttribute(NuxeoConfiguration.Specification.DOCUMENT_TYPE_KEY, documentTypeKey);
        documentsType.addChild(documentsType.getChildCount(), node);
        i++;
      }

      String op = variableContext.getParameter(seqPrefix + "documentTypeop");
      if (op != null && op.equals("Add")) {
        String documentTypeSpec = variableContext.getParameter(seqPrefix + "documentType");
        SpecificationNode node = new SpecificationNode(NuxeoConfiguration.Specification.DOCUMENT_TYPE);
        node.setAttribute(NuxeoConfiguration.Specification.DOCUMENT_TYPE_KEY, documentTypeSpec);
        documentsType.addChild(documentsType.getChildCount(), node);
      }

    }

    // TAGS
    SpecificationNode documents = new SpecificationNode(NuxeoConfiguration.Specification.DOCUMENTS);
    ds.addChild(ds.getChildCount(), documents);

    String processTags = variableContext.getParameter(seqPrefix + NuxeoConfiguration.Specification.PROCESS_TAGS);
    String processAttachments = variableContext
        .getParameter(seqPrefix + NuxeoConfiguration.Specification.PROCESS_ATTACHMENTS);

    if (processTags != null && !processTags.isEmpty()) {
      documents.setAttribute(NuxeoConfiguration.Specification.PROCESS_TAGS, String.valueOf(processTags));
    }
    if (processAttachments != null && !processAttachments.isEmpty()) {
      documents.setAttribute(NuxeoConfiguration.Specification.PROCESS_ATTACHMENTS,
          String.valueOf(processAttachments));
    }

    return null;
  }

  @Override
  public void outputSpecificationBody(IHTTPOutput out, Locale locale, Specification spec,
      int connectionSequenceNumber, int actualSequenceNumber, String tabName)
      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));

    NuxeoSpecification ns = NuxeoSpecification.from(spec);

    paramMap.put(NuxeoConfiguration.Specification.DOMAINS.toUpperCase(), ns.getDomains());
    paramMap.put(NuxeoConfiguration.Specification.DOCUMENTS_TYPE.toUpperCase(), ns.getDocumentsType());
    paramMap.put(NuxeoConfiguration.Specification.PROCESS_TAGS.toUpperCase(), ns.isProcessTags());
    paramMap.put(NuxeoConfiguration.Specification.PROCESS_ATTACHMENTS.toUpperCase(), ns.isProcessAttachments());

    Messages.outputResourceWithVelocity(out, locale, EDIT_SPEC_FORWARD_CONF_DOMAINS, paramMap);
    Messages.outputResourceWithVelocity(out, locale, EDIT_SPEC_FORWARD_CONF_DOCUMENTS_TYPE, paramMap);
    Messages.outputResourceWithVelocity(out, locale, EDIT_SPEC_FORWARD_CONF_DOCUMENTS, paramMap);

  }

  @Override
  public void outputSpecificationHeader(IHTTPOutput out, Locale locale, Specification spec,
      int connectionSequenceNumber, List<String> tabsArray) throws ManifoldCFException, IOException {

    tabsArray.add(Messages.getString(locale, CONF_DOMAINS_TAB_PROPERTY));
    tabsArray.add(Messages.getString(locale, CONF_DOCUMENTS_TYPE_TAB_PROPERTY));
    tabsArray.add(Messages.getString(locale, CONF_DOCUMENT_PROPERTY));

    Map<String, Object> paramMap = new HashMap<String, Object>();
    paramMap.put("SeqNum", Integer.toString(connectionSequenceNumber));

    Messages.outputResourceWithVelocity(out, locale, EDIT_SPEC_HEADER_FORWARD, paramMap);
  }

  static class NuxeoSpecification {

    private List<String> domains;
    private List<String> documentsType;
    private Boolean processTags = false;
    private Boolean processAttahcments = false;

    public List<String> getDomains() {
      return this.domains;
    }

    public List<String> getDocumentsType() {
      return this.documentsType;
    }

    public Boolean isProcessTags() {
      return this.processTags;
    }

    public Boolean isProcessAttachments() {
      return this.processAttahcments;
    }

    /**
     * @param spec
     * @return
     */
    public static NuxeoSpecification from(Specification spec) {
      NuxeoSpecification ns = new NuxeoSpecification();

      ns.domains = Lists.newArrayList();
      ns.documentsType = Lists.newArrayList();

      for (int i = 0, len = spec.getChildCount(); i < len; i++) {
        SpecificationNode sn = spec.getChild(i);

        if (sn.getType().equals(NuxeoConfiguration.Specification.DOMAINS)) {
          for (int j = 0, sLen = sn.getChildCount(); j < sLen; j++) {
            SpecificationNode spectNode = sn.getChild(j);
            if (spectNode.getType().equals(NuxeoConfiguration.Specification.DOMAIN)) {
              ns.domains.add(spectNode.getAttributeValue(NuxeoConfiguration.Specification.DOMAIN_KEY));
            }
          }
        } else if (sn.getType().equals(NuxeoConfiguration.Specification.DOCUMENTS_TYPE)) {
          for (int j = 0, sLen = sn.getChildCount(); j < sLen; j++) {
            SpecificationNode spectNode = sn.getChild(j);
            if (spectNode.getType().equals(NuxeoConfiguration.Specification.DOCUMENT_TYPE)) {
              ns.documentsType.add(
                  spectNode.getAttributeValue(NuxeoConfiguration.Specification.DOCUMENT_TYPE_KEY));
            }
          }
        } else if (sn.getType().equals(NuxeoConfiguration.Specification.DOCUMENTS)) {
          String procTags = sn.getAttributeValue(NuxeoConfiguration.Specification.PROCESS_TAGS);
          ns.processTags = Boolean.valueOf(procTags);
          String procAtt = sn.getAttributeValue(NuxeoConfiguration.Specification.PROCESS_ATTACHMENTS);
          ns.processAttahcments = Boolean.valueOf(procAtt);
        }
      }

      return ns;
    }

  }
}
