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



import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NoPermissionException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.admin.AbstractManagedObjectDefinition;
import org.opends.server.admin.AggregationPropertyDefinition;
import org.opends.server.admin.Configuration;
import org.opends.server.admin.ConfigurationClient;
import org.opends.server.admin.PropertyException;
import org.opends.server.admin.DefinitionDecodingException;
import org.opends.server.admin.DefinitionResolver;
import org.opends.server.admin.InstantiableRelationDefinition;
import org.opends.server.admin.LDAPProfile;
import org.opends.server.admin.ManagedObjectDefinition;
import org.opends.server.admin.ManagedObjectNotFoundException;
import org.opends.server.admin.ManagedObjectPath;
import org.opends.server.admin.PropertyDefinition;
import org.opends.server.admin.PropertyDefinitionVisitor;
import org.opends.server.admin.PropertyOption;
import org.opends.server.admin.Reference;
import org.opends.server.admin.RelationDefinition;
import org.opends.server.admin.SetRelationDefinition;
import org.opends.server.admin.DefinitionDecodingException.Reason;
import org.opends.server.admin.client.AuthorizationException;
import org.opends.server.admin.client.CommunicationException;
import org.opends.server.admin.client.ManagedObject;
import org.opends.server.admin.client.ManagedObjectDecodingException;
import org.opends.server.admin.client.OperationRejectedException;
import org.opends.server.admin.client.OperationRejectedException.OperationType;
import org.opends.server.admin.client.spi.Driver;
import org.opends.server.admin.client.spi.PropertySet;
import org.opends.server.admin.std.client.RootCfgClient;
import org.opends.server.admin.std.meta.RootCfgDefn;



/**
 * The LDAP management context driver implementation.
 */
final class LDAPDriver extends Driver {

  /**
   * A visitor which is used to decode property LDAP values.
   */
  private static final class ValueDecoder extends
      PropertyDefinitionVisitor<Object, String> {

    /**
     * Decodes the provided property LDAP value.
     *
     * @param <PD>
     *          The type of the property.
     * @param pd
     *          The property definition.
     * @param value
     *          The LDAP string representation.
     * @return Returns the decoded LDAP value.
     * @throws PropertyException
     *           If the property value could not be decoded because it
     *           was invalid.
     */
    public static <PD> PD decode(PropertyDefinition<PD> pd, Object value)
        throws PropertyException {
      String s = String.valueOf(value);
      return pd.castValue(pd.accept(new ValueDecoder(), s));
    }



    /** Prevent instantiation. */
    private ValueDecoder() {
      // No implementation required.
    }



    /** {@inheritDoc} */
    @Override
    public <C extends ConfigurationClient, S extends Configuration>
    Object visitAggregation(AggregationPropertyDefinition<C, S> d, String p) {
      // Aggregations values are stored as full DNs in LDAP, but
      // just their common name is exposed in the admin framework.
      try {
        Reference<C, S> reference = Reference.parseDN(d.getParentPath(), d
            .getRelationDefinition(), p);
        return reference.getName();
      } catch (IllegalArgumentException e) {
        throw PropertyException.illegalPropertyValueException(d, p);
      }
    }



    /** {@inheritDoc} */
    @Override
    public <T> Object visitUnknown(PropertyDefinition<T> d, String p)
        throws PropertyException {
      // By default the property definition's decoder will do.
      return d.decodeValue(p);
    }
  }



  /** The LDAP connection. */
  private final LDAPConnection connection;

  /** The LDAP management context. */
  private final LDAPManagementContext context;

  /**
   * The LDAP profile which should be used to construct LDAP
   * requests and decode LDAP responses.
   */
  private final LDAPProfile profile;



  /**
   * Creates a new LDAP driver using the specified LDAP connection and
   * profile.
   *
   * @param context
   *          The LDAP management context.
   * @param connection
   *          The LDAP connection.
   * @param profile
   *          The LDAP profile.
   */
  public LDAPDriver(LDAPManagementContext context, LDAPConnection connection,
      LDAPProfile profile) {
    this.context = context;
    this.connection = connection;
    this.profile = profile;
  }



  /** {@inheritDoc} */
  @Override
  public void close() {
    connection.unbind();
  }



  /** {@inheritDoc} */
  @Override
  public <C extends ConfigurationClient, S extends Configuration>
  ManagedObject<? extends C> getManagedObject(
      ManagedObjectPath<C, S> path) throws DefinitionDecodingException,
      ManagedObjectDecodingException, ManagedObjectNotFoundException,
      AuthorizationException, CommunicationException {
    if (!managedObjectExists(path)) {
      throw new ManagedObjectNotFoundException();
    }

    try {
      // Read the entry associated with the managed object.
      LdapName dn = LDAPNameBuilder.create(path, profile);
      AbstractManagedObjectDefinition<C, S> d = path
          .getManagedObjectDefinition();
      ManagedObjectDefinition<? extends C, ? extends S> mod =
        getEntryDefinition(d, dn);

      ArrayList<String> attrIds = new ArrayList<>();
      for (PropertyDefinition<?> pd : mod.getAllPropertyDefinitions()) {
        String attrId = profile.getAttributeName(mod, pd);
        attrIds.add(attrId);
      }

      Attributes attributes = connection.readEntry(dn, attrIds);

      // Build the managed object's properties.
      List<PropertyException> exceptions = new LinkedList<>();
      PropertySet newProperties = new PropertySet();
      for (PropertyDefinition<?> pd : mod.getAllPropertyDefinitions()) {
        String attrID = profile.getAttributeName(mod, pd);
        Attribute attribute = attributes.get(attrID);
        try {
          decodeProperty(newProperties, path, pd, attribute);
        } catch (PropertyException e) {
          exceptions.add(e);
        }
      }

      // If there were no decoding problems then return the object,
      // otherwise throw an operations exception.
      ManagedObject<? extends C> mo = createExistingManagedObject(mod, path,
          newProperties);
      if (exceptions.isEmpty()) {
        return mo;
      } else {
        throw new ManagedObjectDecodingException(mo, exceptions);
      }
    } catch (NameNotFoundException e) {
      throw new ManagedObjectNotFoundException();
    } catch (NoPermissionException e) {
      throw new AuthorizationException(e);
    } catch (NamingException e) {
      throw new CommunicationException(e);
    }
  }



  /** {@inheritDoc} */
  @SuppressWarnings("unchecked")
  @Override
  public <C extends ConfigurationClient, S extends Configuration, PD>
  SortedSet<PD> getPropertyValues(ManagedObjectPath<C, S> path,
      PropertyDefinition<PD> pd) throws IllegalArgumentException,
      DefinitionDecodingException, AuthorizationException,
      ManagedObjectNotFoundException, CommunicationException,
      PropertyException {
    // Check that the requested property is from the definition
    // associated with the path.
    AbstractManagedObjectDefinition<C, S> d = path.getManagedObjectDefinition();
    PropertyDefinition<?> tmp = d.getPropertyDefinition(pd.getName());
    if (tmp != pd) {
      throw new IllegalArgumentException("The property " + pd.getName()
          + " is not associated with a " + d.getName());
    }

    if (!managedObjectExists(path)) {
      throw new ManagedObjectNotFoundException();
    }

    try {
      // Read the entry associated with the managed object.
      LdapName dn = LDAPNameBuilder.create(path, profile);
      ManagedObjectDefinition<? extends C, ? extends S> mod;
      mod = getEntryDefinition(d, dn);

      // Make sure we use the correct property definition, the
      // provided one might have been overridden in the resolved
      // definition.
      pd = (PropertyDefinition<PD>) mod.getPropertyDefinition(pd.getName());

      String attrID = profile.getAttributeName(mod, pd);
      Attributes attributes = connection.readEntry(dn, Collections
          .singleton(attrID));
      Attribute attribute = attributes.get(attrID);

      // Decode the values.
      SortedSet<PD> values = new TreeSet<>(pd);
      if (attribute != null) {
        NamingEnumeration<?> ldapValues = attribute.getAll();
        while (ldapValues.hasMore()) {
          Object obj = ldapValues.next();
          if (obj != null) {
            PD value = ValueDecoder.decode(pd, obj);
            values.add(value);
          }
        }
      }

      // Sanity check the returned values.
      if (values.size() > 1 && !pd.hasOption(PropertyOption.MULTI_VALUED)) {
        throw PropertyException.propertyIsSingleValuedException(pd);
      }

      if (values.isEmpty() && pd.hasOption(PropertyOption.MANDATORY)) {
        throw PropertyException.propertyIsMandatoryException(pd);
      }

      if (values.isEmpty()) {
        // Use the property's default values.
        values.addAll(findDefaultValues(path.asSubType(mod), pd, false));
      }

      return values;
    } catch (NameNotFoundException e) {
      throw new ManagedObjectNotFoundException();
    } catch (NoPermissionException e) {
      throw new AuthorizationException(e);
    } catch (NamingException e) {
      throw new CommunicationException(e);
    }
  }



  /** {@inheritDoc} */
  @Override
  public ManagedObject<RootCfgClient> getRootConfigurationManagedObject() {
    return new LDAPManagedObject<>(this,
        RootCfgDefn.getInstance(), ManagedObjectPath.emptyPath(),
        new PropertySet(), true, null);
  }



  /** {@inheritDoc} */
  @Override
  public <C extends ConfigurationClient, S extends Configuration>
  String[] listManagedObjects(
      ManagedObjectPath<?, ?> parent, InstantiableRelationDefinition<C, S> rd,
      AbstractManagedObjectDefinition<? extends C, ? extends S> d)
      throws IllegalArgumentException, ManagedObjectNotFoundException,
      AuthorizationException, CommunicationException {
    validateRelationDefinition(parent, rd);

    if (!managedObjectExists(parent)) {
      throw new ManagedObjectNotFoundException();
    }

    // Get the search base DN.
    LdapName dn = LDAPNameBuilder.create(parent, rd, profile);

    // Retrieve only those entries which are sub-types of the
    // specified definition.
    StringBuilder builder = new StringBuilder();
    builder.append("(objectclass=");
    builder.append(profile.getObjectClass(d));
    builder.append(')');
    String filter = builder.toString();

    List<String> children = new ArrayList<>();
    try {
      for (LdapName child : connection.listEntries(dn, filter)) {
        children.add(child.getRdn(child.size() - 1).getValue().toString());
      }
    } catch (NameNotFoundException e) {
      // Ignore this - it means that the base entry does not exist
      // (which it might not if this managed object has just been
      // created.
    } catch (NamingException e) {
      adaptNamingException(e);
    }

    return children.toArray(new String[children.size()]);
  }



  /** {@inheritDoc} */
  @Override
  public <C extends ConfigurationClient, S extends Configuration>
  String[] listManagedObjects(
      ManagedObjectPath<?, ?> parent, SetRelationDefinition<C, S> rd,
      AbstractManagedObjectDefinition<? extends C, ? extends S> d)
      throws IllegalArgumentException, ManagedObjectNotFoundException,
      AuthorizationException, CommunicationException {
    validateRelationDefinition(parent, rd);

    if (!managedObjectExists(parent)) {
      throw new ManagedObjectNotFoundException();
    }

    // Get the search base DN.
    LdapName dn = LDAPNameBuilder.create(parent, rd, profile);

    // Retrieve only those entries which are sub-types of the
    // specified definition.
    StringBuilder builder = new StringBuilder();
    builder.append("(objectclass=");
    builder.append(profile.getObjectClass(d));
    builder.append(')');
    String filter = builder.toString();

    List<String> children = new ArrayList<>();
    try {
      for (LdapName child : connection.listEntries(dn, filter)) {
        children.add(child.getRdn(child.size() - 1).getValue().toString());
      }
    } catch (NameNotFoundException e) {
      // Ignore this - it means that the base entry does not exist
      // (which it might not if this managed object has just been
      // created.
    } catch (NamingException e) {
      adaptNamingException(e);
    }

    return children.toArray(new String[children.size()]);
  }



  /** {@inheritDoc} */
  @Override
  public boolean managedObjectExists(ManagedObjectPath<?, ?> path)
      throws ManagedObjectNotFoundException, AuthorizationException,
      CommunicationException {
    if (path.isEmpty()) {
      return true;
    }

    ManagedObjectPath<?, ?> parent = path.parent();
    LdapName dn = LDAPNameBuilder.create(parent, profile);
    if (!entryExists(dn)) {
      throw new ManagedObjectNotFoundException();
    }

    dn = LDAPNameBuilder.create(path, profile);
    return entryExists(dn);
  }



  /** {@inheritDoc} */
  @Override
  protected <C extends ConfigurationClient, S extends Configuration>
  void deleteManagedObject(
      ManagedObjectPath<C, S> path) throws OperationRejectedException,
      AuthorizationException, CommunicationException {
    // Delete the entry and any subordinate entries.
    LdapName dn = LDAPNameBuilder.create(path, profile);
    try {
      connection.deleteSubtree(dn);
    } catch (OperationNotSupportedException e) {
      // Unwilling to perform.
      AbstractManagedObjectDefinition<?, ?> d =
        path.getManagedObjectDefinition();
      if (e.getMessage() == null) {
        throw new OperationRejectedException(OperationType.DELETE, d
            .getUserFriendlyName());
      } else {
        LocalizableMessage m = LocalizableMessage.raw("%s", e.getMessage());
        throw new OperationRejectedException(OperationType.DELETE, d
            .getUserFriendlyName(), m);
      }
    } catch (NamingException e) {
      adaptNamingException(e);
    }
  }



  /** {@inheritDoc} */
  @Override
  protected LDAPManagementContext getManagementContext() {
    return context;
  }



  /**
   * Adapts a naming exception to an appropriate admin client
   * exception.
   *
   * @param ne
   *          The naming exception.
   * @throws CommunicationException
   *           If the naming exception mapped to a communication
   *           exception.
   * @throws AuthorizationException
   *           If the naming exception mapped to an authorization
   *           exception.
   */
  void adaptNamingException(NamingException ne) throws CommunicationException,
      AuthorizationException {
    try {
      throw ne;
    } catch (javax.naming.NoPermissionException e) {
      throw new AuthorizationException(e);
    } catch (NamingException e) {
      // Just treat it as a communication problem.
      throw new CommunicationException(e);
    }
  }



  /**
   * Determines whether the named LDAP entry exists.
   *
   * @param dn
   *          The LDAP entry name.
   * @return Returns <code>true</code> if the named LDAP entry
   *         exists.
   * @throws AuthorizationException
   *           If the server refuses to make the determination because
   *           the client does not have the correct privileges.
   * @throws CommunicationException
   *           If the client cannot contact the server due to an
   *           underlying communication problem.
   */
  boolean entryExists(LdapName dn) throws CommunicationException,
      AuthorizationException {
    try {
      return connection.entryExists(dn);
    } catch (NamingException e) {
      adaptNamingException(e);
    }
    return false;
  }



  /**
   * Gets the LDAP connection used for interacting with the server.
   *
   * @return Returns the LDAP connection used for interacting with the
   *         server.
   */
  LDAPConnection getLDAPConnection() {
    return connection;
  }



  /**
   * Gets the LDAP profile which should be used to construct LDAP
   * requests and decode LDAP responses.
   *
   * @return Returns the LDAP profile which should be used to
   *         construct LDAP requests and decode LDAP responses.
   */
  LDAPProfile getLDAPProfile() {
    return profile;
  }



  /** Create a managed object which already exists on the server. */
  private <M extends ConfigurationClient, N extends Configuration>
  ManagedObject<M> createExistingManagedObject(
      ManagedObjectDefinition<M, N> d,
      ManagedObjectPath<? super M, ? super N> p, PropertySet properties) {
    RelationDefinition<?, ?> rd = p.getRelationDefinition();
    PropertyDefinition<?> pd = null;
    if (rd instanceof InstantiableRelationDefinition) {
      InstantiableRelationDefinition<?, ?> ird =
        (InstantiableRelationDefinition<?, ?>) rd;
      pd = ird.getNamingPropertyDefinition();
    }
    return new LDAPManagedObject<>(this, d, p.asSubType(d), properties, true, pd);
  }



  /** Create a property using the provided string values. */
  private <PD> void decodeProperty(PropertySet newProperties,
      ManagedObjectPath<?, ?> p, PropertyDefinition<PD> pd,
      Attribute attribute) throws PropertyException,
      NamingException {
    PropertyException exception = null;

    // Get the property's active values.
    SortedSet<PD> activeValues = new TreeSet<>(pd);
    if (attribute != null) {
      NamingEnumeration<?> ldapValues = attribute.getAll();
      while (ldapValues.hasMore()) {
        Object obj = ldapValues.next();
        if (obj != null) {
          PD value = ValueDecoder.decode(pd, obj);
          activeValues.add(value);
        }
      }
    }

    if (activeValues.size() > 1 && !pd.hasOption(PropertyOption.MULTI_VALUED)) {
      // This exception takes precedence over previous exceptions.
      exception = PropertyException.propertyIsSingleValuedException(pd);
      PD value = activeValues.first();
      activeValues.clear();
      activeValues.add(value);
    }

    // Get the property's default values.
    Collection<PD> defaultValues;
    try {
      defaultValues = findDefaultValues(p, pd, false);
    } catch (PropertyException e) {
      defaultValues = Collections.emptySet();
      exception = e;
    }

    newProperties.addProperty(pd, defaultValues, activeValues);

    if (activeValues.isEmpty()
        && defaultValues.isEmpty()
        && pd.hasOption(PropertyOption.MANDATORY)
        // The active values maybe empty because of a previous exception.
        && exception == null) {
      exception = PropertyException.propertyIsMandatoryException(pd);
    }

    if (exception != null) {
      throw exception;
    }
  }



  /** Determine the type of managed object associated with the named entry. */
  private <C extends ConfigurationClient, S extends Configuration>
  ManagedObjectDefinition<? extends C, ? extends S> getEntryDefinition(
      AbstractManagedObjectDefinition<C, S> d, LdapName dn)
      throws NamingException, DefinitionDecodingException {
    Attributes attributes = connection.readEntry(dn, Collections
        .singleton("objectclass"));
    Attribute oc = attributes.get("objectclass");

    if (oc == null) {
      // No object classes.
      throw new DefinitionDecodingException(d, Reason.NO_TYPE_INFORMATION);
    }

    final Set<String> objectClasses = new HashSet<>();
    NamingEnumeration<?> values = oc.getAll();
    while (values.hasMore()) {
      Object value = values.next();
      if (value != null) {
        objectClasses.add(value.toString().toLowerCase().trim());
      }
    }

    if (objectClasses.isEmpty()) {
      // No object classes.
      throw new DefinitionDecodingException(d, Reason.NO_TYPE_INFORMATION);
    }

    // Resolve the appropriate sub-type based on the object classes.
    DefinitionResolver resolver = new DefinitionResolver() {

      public boolean matches(AbstractManagedObjectDefinition<?, ?> d) {
        String objectClass = profile.getObjectClass(d);
        return objectClasses.contains(objectClass);
      }

    };

    return d.resolveManagedObjectDefinition(resolver);
  }
}
