/*
 * 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 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS
 */
package org.opends.server.protocols.ldap;

import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.testng.Assert.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.Backend;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.tools.LDAPModify;
import org.opends.server.tools.LDAPSearch;
import org.opends.server.types.*;
import org.opends.server.util.Base64;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

/**
 * This class defines a set of test cases for testing the binary transfer option
 * functionality across different protocol versions of LDAP.
 */
public class LDAPBinaryOptionTestCase extends LdapTestCase {
  /** Exported LDIF file. */
  private File ldif;
  /** LDIFExportConfig used for exporting entries. */
  private LDIFExportConfig exportConfig;
  /** LDIFImportConfig used for importing entries. */
  private LDIFImportConfig importConfig;
  /** Test Backend. */
  private Backend<?> backend;

  /** Constant value of userCertificate attribute. */
  private static final String CERT=
      "MIIB5TCCAU6gAwIBAgIERloIajANBgkqhkiG9" +
      "w0BAQUFADA3MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRXhhbXBs" +
      "ZSBDb3JwMREwDwYDVQQDEwhKb2huIERvZTAeFw0wNzA1MjcyMjM4" +
      "MzRaFw0wNzA4MjUyMjM4MzRaMDcxCzAJBgNVBAYTAlVTMRUwEwYD" +
      "VQQKEwxFeGFtcGxlIENvcnAxETAPBgNVBAMTCEpvaG4gRG9lMIGfMA" +
      "0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWNZB4qs1UvjYgvGvB" +
      "9udmiUi4X4DeaSm3o0p8PSwpOFxSqgWdSwKgUugZ1EJVyYoakljDFs" +
      "J0GVown+dIB24V4ozNs6wa0YotIKTV2AcySQkmzzP3e+OnE9Aa1wlB/" +
      "PVnh1CFLgk1UOoruLE10bac5HA8QiAmfNMorU26AwFTcwIDAQABMA" +
      "0GCSqGSIb3DQEBBQUAA4GBAGrzMKNbBRWn+LIfYTfqKYUc258XVbhFri" +
      "1OV0oF82vyvciYWZzyxLc52EPDsymLmcDh+CdWxy3bVkjdMg1WEtMGr" +
      "1GsxOVi/vWe+kT4tPhinnB4Fowf8zgqiUKo9/FJN26y7Fpvy1IODiBInDrKZ" +
      "RvNfqemCf7o3+Cp00OmF5ey";



  /**
   * Ensures that the Directory Server is running before executing the
   * testcases.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
    TestCaseUtils.initializeTestBackend(true);
  }



  /**
   * Test to verify an ADD of the binary attributes  using a V3
   * protocol.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void binaryAttributeAddV3() throws Exception
  {
    //Add a binary attribute both with or without the binary option.
    String filePath = TestCaseUtils.createTempFile(
      "dn: uid=user.1,o=test",
      "objectClass: inetOrgPerson",
      "uid: user.1",
      "sn: 1",
      "cn: user 1",
      "userCertificate:: "+CERT
      );
    String[] args = new String []
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D","cn=directory manager",
      "-w","password",
      "-a",
      "-f", filePath
    };
    int err = LDAPModify.mainModify(args, false, null,null);
    assertEquals(err,0);

    //ADD with ;binary option.
    filePath = TestCaseUtils.createTempFile(
      "dn: uid=user.2,o=test",
      "objectClass: inetOrgPerson",
      "uid: user.2",
      "sn: 2",
      "cn: user 2",
      "userCertificate;binary:: "+CERT
      );
    args = new String []
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D","cn=directory manager",
      "-w","password",
      "-a",
      "-f", filePath
    };
    err = LDAPModify.mainModify(args, false, null,null);
    assertEquals(err,0);
  }



  /**
   * Test to verify an ADD of a non-binary attribute with the ;binary
   * transfer option using a V3 protocol.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void nonBinaryAttributeAddV3() throws Exception
  {
    String filePath = TestCaseUtils.createTempFile(
      "dn: uid=user.3,o=test",
      "objectClass: inetOrgPerson",
      "uid: user.3",
      "sn: 3",
      "cn: user 3",
      "cn;binary: common name"
      );
    String[] args = new String []
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D","cn=directory manager",
      "-w","password",
      "-a",
      "-f", filePath,
    };
    int err = LDAPModify.mainModify(args, false, null,null);
    assertFalse(err==0);
  }




  /**
   * Test to verify a SEARCH using the ;binary transfer option using a V3
   * protocol.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dependsOnMethods = {"org.opends.server.protocols.ldap."+
  "LDAPBinaryOptionTestCase.binaryAttributeAddV3"})
  public void binaryAttributeSearchV3() throws Exception
  {
    SearchRequest request = newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, "(uid=user.1)");
    InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
    List<SearchResultEntry> entries = searchOperation.getSearchEntries();
    SearchResultEntry e = entries.get(0);
    assertNotNull(e);
    List<Attribute> attrs = e.getAttribute("usercertificate");
    Attribute a = attrs.get(0);
    assertNotNull(a);
    assertTrue(a.getOptions().contains("binary"));
  }

  /**
   * Test to verify a SEARCH using the ;binary transfer option using a V3
   * protocol for a non-binary attribute.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dependsOnMethods = {"org.opends.server.protocols.ldap."+
  "LDAPBinaryOptionTestCase.binaryAttributeAddV3"})
  public void invalidBinaryAttributeSearchV3() throws Exception
  {
    SearchRequest request = newSearchRequest("o=test", SearchScope.WHOLE_SUBTREE, "(uid=user.1)")
        .addAttribute("cn;binary");
    InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
    assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
    List<SearchResultEntry> entries = searchOperation.getSearchEntries();
    SearchResultEntry e = entries.get(0);
    assertNotNull(e);
    List<Attribute> list = e.getAttributes();
    assertEquals(list.size(), 0);
  }



  /**
   * Test to verify an ADD and SEARCH using the ;binary transfer option using a
   * V2 protocol.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void binaryOptionUsingV2() throws Exception
  {
    //Construct a V2 connection.
    Socket     s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest =
           new BindRequestProtocolOp(
                    ByteString.valueOfUtf8("cn=Directory Manager"), 2,
                    ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), 0);
      ArrayList<RawAttribute> addAttrs = new ArrayList<>();
      addAttrs.add(RawAttribute.create("objectClass", "inetOrgPerson"));
      addAttrs.add(RawAttribute.create("uid", "user.7"));
      addAttrs.add(RawAttribute.create("cn", "user 7"));
      addAttrs.add(RawAttribute.create("sn", "sn#1"));
      addAttrs.add(RawAttribute.create("sn;x-foo", "sn#2"));
      addAttrs.add(RawAttribute.create("sn;lang-fr", "sn#3"));
      addAttrs.add(RawAttribute.create("userCertificate;binary",
                                       ByteString.wrap(Base64.decode(CERT))));

      AddRequestProtocolOp addRequest =
           new AddRequestProtocolOp(ByteString.valueOfUtf8("uid=user.7,o=test"),
                                    addAttrs);
      message = new LDAPMessage(2, addRequest);
      w.writeMessage(message);

      message = r.readMessage();
      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
      assertEquals(addResponse.getResultCode(),0);

      //Create a SEARCH request to search for this added entry.
      LinkedHashSet<String> attrs = new LinkedHashSet<>();
      //Request only the interesting attributes.
      attrs.add("sn");
      attrs.add("userCertificate;binary");
      SearchRequestProtocolOp searchRequest =
         new SearchRequestProtocolOp(ByteString.valueOfUtf8("o=test"),
                                     SearchScope.WHOLE_SUBTREE,
                                     DereferenceAliasesPolicy.NEVER, 0,
                                     0, false,
                                     LDAPFilter.decode("(uid=user.7)"),
                                     attrs);
      message = new LDAPMessage(2, searchRequest);
      w.writeMessage(message);

      SearchResultEntryProtocolOp searchResultEntry = null;
      SearchResultDoneProtocolOp searchResultDone = null;
      while (searchResultDone == null)
      {
        message = r.readMessage();
        switch (message.getProtocolOpType())
        {
          case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY:
            searchResultEntry = message.getSearchResultEntryProtocolOp();
            break;
          case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE:
            searchResultDone = message.getSearchResultDoneProtocolOp();
            assertEquals(searchResultDone.getResultCode(),
                         LDAPResultCode.SUCCESS);
            break;
        }
      }
      assertNotNull(searchResultEntry);
      boolean certWithNoOption = false;
      boolean snWithMultiVal = false;
      for(LDAPAttribute a:searchResultEntry.getAttributes())
      {
        //Shouldn't be userCertificate;binary.
        if ("userCertificate".equalsIgnoreCase(a.getAttributeType()))
        {
          certWithNoOption=true;
        }
        else if ("sn".equalsIgnoreCase(a.getAttributeType()))
        {
          for (ByteString v : a.getValues())
          {
            String val = v.toString();
            snWithMultiVal = val.equals("sn#1") || val.equals("sn#2") || val.equals("sn#3");
          }
        }
      }
      assertTrue(snWithMultiVal && certWithNoOption);
    }
    finally
    {
      StaticUtils.close(r, w);
      StaticUtils.close(s);
    }
  }



  /**
   * Test to verify that the DB stores the binary option by
   * exporting the LDIF and searching for the binary keyword.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dependsOnMethods = {"org.opends.server.protocols.ldap."+
  "LDAPBinaryOptionTestCase.binaryAttributeAddV3"})
  public void exportAndValidateBinaryAttribute() throws Exception
  {
    //Ensure that the entry exists.
    String args[] = new String []
    {
        "--noPropertiesFile",
        "-h", "127.0.0.1",
        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
        "-b", "o=test",
        "(uid=user.1)"
    };
    assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
    exportBackend();
    assertTrue(ldif.exists());
    assertTrue(containsBinary());
  }



  /**
   * Test to verify that the server adds the binary option by
   * importing an LDIF both with or without the binary option.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dependsOnMethods = {"org.opends.server.protocols.ldap."+
  "LDAPBinaryOptionTestCase.exportAndValidateBinaryAttribute"})
  public void ImportAndValidateBinaryAttribute() throws Exception
  {
    //This testcase uses the exported ldif in exportAndValidBinaryAttribute
    //testcase. It imports the ldif after cleaning up the database and verifies
    //that the binary option is present in the binary attributes. After that, it
    //modifies the binary option from the ldif and re-imports it.A re-export of
    //the last import should have the binary option even though it wasn't
    //there in the imported ldif.
    assertTrue(ldif.exists());
    importLDIF();
    assertTrue(containsBinary());
    //Remove the binary option and re-import it.
    FileReader reader = new FileReader(ldif);
    BufferedReader buf = new BufferedReader(reader);
    StringBuilder builder = new StringBuilder();
    String userCert = "userCertificate;binary";
    String line = null;
    while((line=buf.readLine())!=null)
    {
      if(line.startsWith(userCert))
      {
        builder.append("userCertificate:");
        builder.append(line, userCert.length()+1, line.length());
      }
      else
      {
        builder.append(line);
      }
      builder.append("\n");
    }
    buf.close();
    ldif.delete();
    ldif = new File(TestCaseUtils.createTempFile(builder.toString()));
    importLDIF();
    //Re-export it.
    exportBackend();
    assertTrue(containsBinary());
    ldif.delete();
  }



  /**
   * Test to verify a MODIFY using the ;binary transfer option using V3
   * protocol.
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void binaryModifyV3() throws Exception
  {
    String filePath = TestCaseUtils.createTempFile(
      "dn: uid=user.4,o=test",
      "objectClass: inetOrgPerson",
      "uid: user.4",
      "sn: 4",
      "cn: user 4"
      );
    String[] args = new String []
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D","cn=directory manager",
      "-w","password",
      "-a",
      "-f", filePath,
    };
    int err = LDAPModify.mainModify(args, false, null,null);
    assertEquals(err,0);

    filePath = TestCaseUtils.createTempFile(
     "dn: uid=user.4,o=test",
     "changetype: modify",
     "add: usercertificate;binary",
     "userCertificate;binary:: " + CERT);
    args = new String[]
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D","cn=directory manager",
      "-w","password",
      "-f", filePath,
    };
    err = LDAPModify.mainModify(args, false, null,null);
    assertEquals(err,0);

  }



  /**
   * Utility method to verify if the LDIF file contains binary option.
   * @return  {@code true} if binary option is found in the LDIF
   *           , or {@code false} if not.
   * @throws  Exception  If an unexpected problem occurs.
   */
  private boolean containsBinary() throws Exception
  {
    FileReader reader = new FileReader(ldif);
    BufferedReader buf = new BufferedReader(reader);
    String line = null;
    boolean found=false;
    while((line=buf.readLine())!=null)
    {
      if(line.startsWith("userCertificate;binary"))
      {
        found = true;
      }
    }
    buf.close();
    return found;
  }



  /**
   * Utility method to export the database into an LDIF file.
   * @throws  Exception  If an unexpected problem occurs.
   */
  private void exportBackend() throws Exception
  {
     //Initialize necessary instance variables.
    if(ldif==null)
    {
      ldif = File.createTempFile("LDAPBinaryOptionTestCase", ".ldif");
    }
    exportConfig = new LDIFExportConfig(ldif.getAbsolutePath(),
                              ExistingFileBehavior.OVERWRITE);
    backend = DirectoryServer.getBackend("test");
    backend.exportLDIF(exportConfig);
  }



  /**
   * Utility method to import the LDIF into  the database.
   * @throws  Exception  If an unexpected problem occurs.
   */
  private void importLDIF() throws Exception
  {
    importConfig = new LDIFImportConfig(ldif.getAbsolutePath());
    TestCaseUtils.initializeTestBackend(false);
    backend = DirectoryServer.getBackend("test");
    backend.importLDIF(importConfig, DirectoryServer.getInstance().getServerContext());
  }
}
