/*
 * 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 2011-2015 ForgeRock AS
 */
package org.opends.server.types;

import static org.forgerock.util.Reject.*;

import java.util.Collection;
import java.util.Set;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.util.Utils;
import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn;
import org.opends.server.admin.std.server.VirtualAttributeCfg;
import org.opends.server.api.Group;
import org.opends.server.api.VirtualAttributeProvider;
import org.opends.server.core.DirectoryServer;

/**
 * This class defines a virtual attribute rule, which associates a
 * virtual attribute provider with its associated configuration,
 * including the attribute type for which the values should be
 * generated; the base DN(s), group DN(s), and search filter(s) that
 * should be used to identify which entries should have the virtual
 * attribute, and how conflicts between real and virtual values should
 * be handled.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.VOLATILE,
     mayInstantiate=false,
     mayExtend=false,
     mayInvoke=true)
public final class VirtualAttributeRule
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The attribute type for which the values should be generated. */
  private final AttributeType attributeType;
  /** The set of base DNs for branches that are eligible to have this virtual attribute. */
  private final Set<DN> baseDNs;
  /** The scope of entries eligible to have this virtual attribute, under the base DNs. */
  private final SearchScope scope;
  /** The set of DNs for groups whose members are eligible to have this virtual attribute. */
  private final Set<DN> groupDNs;
  /** The set of search filters for entries that are eligible to have this virtual attribute. */
  private final Set<SearchFilter> filters;
  /** The virtual attribute provider used to generate the values. */
  private final VirtualAttributeProvider<? extends VirtualAttributeCfg> provider;
  /**
   * The behavior that should be exhibited for entries that already have real
   * values for the target attribute.
   */
  private final VirtualAttributeCfgDefn.ConflictBehavior conflictBehavior;

  /**
   * Creates a new virtual attribute rule with the provided information.
   *
   * @param  attributeType     The attribute type for which the values
   *                           should be generated.
   * @param  provider          The virtual attribute provider to use
   *                           to generate the values.
   * @param  baseDNs           The set of base DNs for branches that
   *                           are eligible to have this virtual attribute.
   * @param  scope             The scope of entries, related to the
   *                           base DNs, that are eligible to have
   *                           this virtual attribute.
   * @param  groupDNs          The set of DNs for groups whose members
   *                           are eligible to have this virtual attribute.
   * @param  filters           The set of search filters for entries
   *                           that are eligible to have this virtual attribute.
   * @param  conflictBehavior  The behavior that the server should
   *                           exhibit for entries that already have
   *                           one or more real values for the target
   *                           attribute.
   */
  public VirtualAttributeRule(AttributeType attributeType,
              VirtualAttributeProvider<? extends VirtualAttributeCfg>
                   provider,
              Set<DN> baseDNs, SearchScope scope, Set<DN> groupDNs,
              Set<SearchFilter> filters,
              VirtualAttributeCfgDefn.ConflictBehavior
                   conflictBehavior)
  {
    ifNull(attributeType, provider, baseDNs, groupDNs);
    ifNull(filters, conflictBehavior);

    this.attributeType    = attributeType;
    this.provider         = provider;
    this.baseDNs          = baseDNs;
    this.scope            = scope;
    this.groupDNs         = groupDNs;
    this.filters          = filters;
    this.conflictBehavior = conflictBehavior;
  }

  /**
   * Retrieves the attribute type for which the values should be generated.
   *
   * @return  The attribute type for which the values should be generated.
   */
  public AttributeType getAttributeType()
  {
    return attributeType;
  }

  /**
   * Retrieves the virtual attribute provider used to generate the values.
   *
   * @return  The virtual attribute provider to use to generate the values.
   */
  public VirtualAttributeProvider<? extends VirtualAttributeCfg>
              getProvider()
  {
    return provider;
  }

  /**
   * Retrieves the set of base DNs for branches that are eligible to
   * have this virtual attribute.
   *
   * @return  The set of base DNs for branches that are eligible to
   *          have this virtual attribute.
   */
  public Set<DN> getBaseDNs()
  {
    return baseDNs;
  }

  /**
   * Retrieves the scope of entries in the base DNs that are eligible
   * to have this virtual attribute.
   *
   * @return  The scope of entries that are eligible to
   *          have this virtual attribute.
   */
  public SearchScope getScope()
  {
    return scope;
  }

  /**
   * Retrieves the set of DNs for groups whose members are eligible to
   * have this virtual attribute.
   *
   * @return  The set of DNs for groups whose members are eligible to
   *          have this virtual attribute.
   */
  public Set<DN> getGroupDNs()
  {
    return groupDNs;
  }

  /**
   * Retrieves the set of search filters for entries that are eligible
   * to have this virtual attribute.
   *
   * @return  The set of search filters for entries that are eligible
   *          to have this virtual attribute.
   */
  public Set<SearchFilter> getFilters()
  {
    return filters;
  }

  /**
   * Retrieves the behavior that the server should exhibit for entries
   * that already have one or more real values for the target attribute.
   *
   * @return  The behavior that the server should exhibit for entries
   *          that already have one or more real values for the target
   *          attribute.
   */
  public VirtualAttributeCfgDefn.ConflictBehavior
              getConflictBehavior()
  {
    return conflictBehavior;
  }

  /**
   * Indicates whether this virtual attribute rule applies to the
   * provided entry, taking into account the eligibility requirements
   * defined in the rule.
   *
   * @param  entry  The entry for which to make the determination.
   *
   * @return  {@code true} if this virtual attribute rule may be used
   *          to generate values for the entry, or {@code false} if not.
   */
  public boolean appliesToEntry(Entry entry)
  {
    // We'll do this in order of expense so that the checks which are
    // potentially most expensive are done last.  First, check to see
    // if real values should override virtual ones and if so whether
    // the entry already has virtual values.
    if (conflictBehavior == VirtualAttributeCfgDefn.ConflictBehavior.
                                REAL_OVERRIDES_VIRTUAL
        && entry.hasAttribute(attributeType))
    {
      return false;
    }

    // If there are any base DNs defined, then the entry must be below one of them.
    if (!baseDNs.isEmpty() && !matchesAnyBaseDN(entry.getName()))
    {
      return false;
    }

    // If there are any search filters defined, then the entry must match one of them.
    if (!filters.isEmpty() && !matchesAnyFilter(entry))
    {
      return false;
    }

    // If there are any group memberships defined, then the entry must
    // be a member of one of them.
    if (!groupDNs.isEmpty() && !isMemberOfAnyGroup(entry))
    {
      return false;
    }

    // If we've gotten here, then the rule is applicable.
    return true;
  }

  private boolean matchesAnyBaseDN(DN entryDN)
  {
    for (DN dn : baseDNs)
    {
      if (entryDN.matchesBaseAndScope(dn, scope))
      {
        return true;
      }
    }
    return false;
  }

  private boolean matchesAnyFilter(Entry entry)
  {
    for (SearchFilter filter : filters)
    {
      try
      {
        if (filter.matchesEntry(entry))
        {
          return true;
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }
    return false;
  }

  private boolean isMemberOfAnyGroup(Entry entry)
  {
    for (DN dn : groupDNs)
    {
      try
      {
        Group<?> group = DirectoryServer.getGroupManager().getGroupInstance(dn);
        if (group != null && group.isMember(entry))
        {
          return true;
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }
    return false;
  }

  @Override
  public String toString()
  {
    StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }

  /**
   * Appends a string representation of this virtual attribute rule to
   * the provided buffer.
   *
   * @param  buffer  The buffer to which the information should be written.
   */
  public void toString(StringBuilder buffer)
  {
    buffer.append("VirtualAttributeRule(attrType=");
    buffer.append(attributeType.getNameOrOID());
    buffer.append(", providerDN=\"").append(provider.getClass().getName());

    buffer.append("\", baseDNs={");
    append(buffer, baseDNs);

    buffer.append("}, scope=").append(scope);

    buffer.append(", groupDNs={");
    append(buffer, groupDNs);
    buffer.append("}, filters={");
    append(buffer, filters);

    buffer.append("}, conflictBehavior=").append(conflictBehavior);
    buffer.append(")");
  }

  private void append(StringBuilder buffer, Collection<?> col)
  {
    if (!col.isEmpty())
    {
      buffer.append("\"");
      Utils.joinAsString(buffer, "\", \"", col);
      buffer.append("\"");
    }
  }
}
