/*
 * 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 2007-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */

package org.opends.server.admin.client.ldap;



import javax.naming.NameAlreadyBoundException;
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.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.admin.AggregationPropertyDefinition;
import org.opends.server.admin.Configuration;
import org.opends.server.admin.ConfigurationClient;
import org.opends.server.admin.InstantiableRelationDefinition;
import org.opends.server.admin.ManagedObjectAlreadyExistsException;
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.PropertyOption;
import org.opends.server.admin.PropertyValueVisitor;
import org.opends.server.admin.Reference;
import org.opends.server.admin.RelationDefinition;
import org.opends.server.admin.SetRelationDefinition;
import org.opends.server.admin.PropertyException;
import org.opends.server.admin.client.AuthorizationException;
import org.opends.server.admin.client.CommunicationException;
import org.opends.server.admin.client.ConcurrentModificationException;
import org.opends.server.admin.client.ManagedObject;
import org.opends.server.admin.client.OperationRejectedException;
import org.opends.server.admin.client.OperationRejectedException.OperationType;
import org.opends.server.admin.client.spi.AbstractManagedObject;
import org.opends.server.admin.client.spi.Driver;
import org.opends.server.admin.client.spi.Property;
import org.opends.server.admin.client.spi.PropertySet;



/**
 * A managed object bound to an LDAP connection.
 *
 * @param <T>
 *          The type of client configuration represented by the client
 *          managed object.
 */
final class LDAPManagedObject<T extends ConfigurationClient> extends
    AbstractManagedObject<T> {

  /**
   * A visitor which is used to encode property LDAP values.
   */
  private static final class ValueEncoder extends
      PropertyValueVisitor<Object, Void> {

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



    /** {@inheritDoc} */
    @Override
    public <C extends ConfigurationClient, S extends Configuration>
    Object visitAggregation(
        AggregationPropertyDefinition<C, S> pd, String v, Void p) {
      // Aggregations values are stored as full DNs in LDAP, but
      // just their common name is exposed in the admin framework.
      Reference<C, S> reference = Reference.parseName(pd.getParentPath(), pd
          .getRelationDefinition(), v);
      return reference.toDN().toString();
    }



    /** {@inheritDoc} */
    @Override
    public <PD> Object visitUnknown(PropertyDefinition<PD> pd, PD v, Void p)
        throws PropertyException {
      return pd.encodeValue(v);
    }
  }



  /** The LDAP management driver associated with this managed object. */
  private final LDAPDriver driver;



  /**
   * Creates a new LDAP managed object instance.
   *
   * @param driver
   *          The underlying LDAP management driver.
   * @param d
   *          The managed object's definition.
   * @param path
   *          The managed object's path.
   * @param properties
   *          The managed object's properties.
   * @param existsOnServer
   *          Indicates whether or not the managed object already
   *          exists.
   * @param namingPropertyDefinition
   *          The managed object's naming property definition if there
   *          is one.
   */
  LDAPManagedObject(LDAPDriver driver,
      ManagedObjectDefinition<T, ? extends Configuration> d,
      ManagedObjectPath<T, ? extends Configuration> path,
      PropertySet properties, boolean existsOnServer,
      PropertyDefinition<?> namingPropertyDefinition) {
    super(d, path, properties, existsOnServer, namingPropertyDefinition);
    this.driver = driver;
  }



  /** {@inheritDoc} */
  @Override
  protected void addNewManagedObject() throws AuthorizationException,
      CommunicationException, OperationRejectedException,
      ConcurrentModificationException, ManagedObjectAlreadyExistsException {
    // First make sure that the parent managed object still exists.
    ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
    ManagedObjectPath<?, ?> path = getManagedObjectPath();
    ManagedObjectPath<?, ?> parent = path.parent();

    try {
      if (!driver.managedObjectExists(parent)) {
        throw new ConcurrentModificationException();
      }
    } catch (ManagedObjectNotFoundException e) {
      throw new ConcurrentModificationException();
    }

    // We may need to create the parent "relation" entry if this is a
    // child of an instantiable or set relation.
    RelationDefinition<?, ?> r = path.getRelationDefinition();
    if (r instanceof InstantiableRelationDefinition
        || r instanceof SetRelationDefinition) {

      // TODO: this implementation does not handle relations which
      // comprise of more than one RDN arc (this will probably never
      // be required anyway).
      LdapName dn;
      if (r instanceof InstantiableRelationDefinition) {
        dn = LDAPNameBuilder.create(parent,
            (InstantiableRelationDefinition) r, driver.getLDAPProfile());
      } else {
        dn = LDAPNameBuilder.create(parent,
            (SetRelationDefinition) r, driver.getLDAPProfile());
      }

      if (!driver.entryExists(dn)) {
        // We need to create the entry.
        Attributes attributes = new BasicAttributes();

        // Create the branch's object class attribute.
        Attribute oc = new BasicAttribute("objectClass");
        for (String objectClass : driver.getLDAPProfile()
            .getRelationObjectClasses(r)) {
          oc.add(objectClass);
        }
        attributes.put(oc);

        // Create the branch's naming attribute.
        Rdn rdn = dn.getRdn(dn.size() - 1);
        attributes.put(rdn.getType(), rdn.getValue().toString());

        // Create the entry.
        try {
          driver.getLDAPConnection().createEntry(dn, attributes);
        } catch (OperationNotSupportedException e) {
          // Unwilling to perform.
          if (e.getMessage() == null) {
            throw new OperationRejectedException(OperationType.CREATE, d
                .getUserFriendlyName());
          } else {
            LocalizableMessage m = LocalizableMessage.raw("%s", e.getMessage());
            throw new OperationRejectedException(OperationType.CREATE, d
                .getUserFriendlyName(), m);
          }
        } catch (NamingException e) {
          driver.adaptNamingException(e);
        }
      }
    }

    // Now add the entry representing this new managed object.
    LdapName dn = LDAPNameBuilder.create(path, driver.getLDAPProfile());
    Attributes attributes = new BasicAttributes(true);

    // Create the object class attribute.
    Attribute oc = new BasicAttribute("objectclass");
    ManagedObjectDefinition<?, ?> definition = getManagedObjectDefinition();
    for (String objectClass : driver.getLDAPProfile().getObjectClasses(
        definition)) {
      oc.add(objectClass);
    }
    attributes.put(oc);

    // Create the naming attribute if there is not naming property.
    PropertyDefinition<?> npd = getNamingPropertyDefinition();
    if (npd == null) {
      Rdn rdn = dn.getRdn(dn.size() - 1);
      attributes.put(rdn.getType(), rdn.getValue().toString());
    }

    // Create the remaining attributes.
    for (PropertyDefinition<?> pd : definition.getAllPropertyDefinitions()) {
      String attrID = driver.getLDAPProfile().getAttributeName(definition, pd);
      Attribute attribute = new BasicAttribute(attrID);
      encodeProperty(attribute, pd);
      if (attribute.size() != 0) {
        attributes.put(attribute);
      }
    }

    try {
      // Create the entry.
      driver.getLDAPConnection().createEntry(dn, attributes);
    } catch (NameAlreadyBoundException e) {
      throw new ManagedObjectAlreadyExistsException();
    } catch (OperationNotSupportedException e) {
      // Unwilling to perform.
      if (e.getMessage() == null) {
        throw new OperationRejectedException(OperationType.CREATE, d
            .getUserFriendlyName());
      } else {
        LocalizableMessage m = LocalizableMessage.raw("%s", e.getMessage());
        throw new OperationRejectedException(OperationType.CREATE, d
            .getUserFriendlyName(), m);
      }
    } catch (NamingException e) {
      driver.adaptNamingException(e);
    }
  }



  /** {@inheritDoc} */
  @Override
  protected Driver getDriver() {
    return driver;
  }



  /** {@inheritDoc} */
  @Override
  protected void modifyExistingManagedObject()
      throws ConcurrentModificationException, OperationRejectedException,
      AuthorizationException, CommunicationException {
    // Build the list of modified attributes.
    Attributes mods = new BasicAttributes();
    ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
    for (PropertyDefinition<?> pd : d.getAllPropertyDefinitions()) {
      Property<?> p = getProperty(pd);
      if (p.isModified()) {
        String attrID = driver.getLDAPProfile().getAttributeName(d, pd);
        Attribute attribute = new BasicAttribute(attrID);
        encodeProperty(attribute, pd);
        mods.put(attribute);
      }
    }

    // Perform the LDAP modification if something has changed.
    if (mods.size() > 0) {
      try {
        ManagedObjectPath<?, ?> path = getManagedObjectPath();
        LdapName dn = LDAPNameBuilder.create(path, driver.getLDAPProfile());
        driver.getLDAPConnection().modifyEntry(dn, mods);
      } catch (NoPermissionException e) {
        throw new AuthorizationException(e);
      } catch (OperationNotSupportedException e) {
        // Unwilling to perform.
        if (e.getMessage() == null) {
          throw new OperationRejectedException(OperationType.MODIFY, d
              .getUserFriendlyName());
        } else {
          LocalizableMessage m = LocalizableMessage.raw("%s", e.getMessage());
          throw new OperationRejectedException(OperationType.MODIFY, d
              .getUserFriendlyName(), m);
        }
      } catch (NamingException e) {
        // Just treat it as a communication problem.
        throw new CommunicationException(e);
      }
    }
  }



  /** {@inheritDoc} */
  @Override
  protected <M extends ConfigurationClient> ManagedObject<M> newInstance(
      ManagedObjectDefinition<M, ?> d, ManagedObjectPath<M, ?> path,
      PropertySet properties, boolean existsOnServer,
      PropertyDefinition<?> namingPropertyDefinition) {
    return new LDAPManagedObject<>(driver, d, path, properties,
        existsOnServer, namingPropertyDefinition);
  }



  /** Encode a property into LDAP string values. */
  private <PD> void encodeProperty(Attribute attribute,
      PropertyDefinition<PD> pd) {
    PropertyValueVisitor<Object, Void> visitor = new ValueEncoder();
    Property<PD> p = getProperty(pd);
    if (pd.hasOption(PropertyOption.MANDATORY)) {
      // For mandatory properties we fall-back to the default values
      // if defined which can sometimes be the case e.g when a
      // mandatory property is overridden.
      for (PD value : p.getEffectiveValues()) {
        attribute.add(pd.accept(visitor, value, null));
      }
    } else {
      for (PD value : p.getPendingValues()) {
        attribute.add(pd.accept(visitor, value, null));
      }
    }
  }

  /** {@inheritDoc} */
  public boolean isModified() {
    ManagedObjectDefinition<?, ?> d = getManagedObjectDefinition();
    for (PropertyDefinition<?> pd : d.getAllPropertyDefinitions()) {
      Property<?> p = getProperty(pd);
      if (p.isModified()) {
        return true;
      }
    }
    return false;
  }

}
