/*
 * 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 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import org.forgerock.i18n.slf4j.LocalizedLogger;
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.api.AccessControlHandler;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.controls.AccountUsableResponseControl;
import org.opends.server.controls.MatchedValuesControl;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.types.AbstractOperation;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.OperationType;
import org.opends.server.types.RawFilter;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.types.operation.PostResponseSearchOperation;
import org.opends.server.types.operation.PreParseSearchOperation;
import org.opends.server.types.operation.SearchEntrySearchOperation;
import org.opends.server.types.operation.SearchReferenceSearchOperation;
import org.opends.server.util.TimeThread;

import static org.opends.messages.CoreMessages.*;
import static org.opends.server.core.DirectoryServer.*;
import static org.opends.server.loggers.AccessLogger.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import static org.opends.server.workflowelement.localbackend.LocalBackendWorkflowElement.*;

/**
 * This class defines an operation that may be used to locate entries in the
 * Directory Server based on a given set of criteria.
 */
public class SearchOperationBasis
       extends AbstractOperation
       implements PreParseSearchOperation,
                  PostResponseSearchOperation,
                  SearchEntrySearchOperation,
                  SearchReferenceSearchOperation,
                  SearchOperation
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * Indicates whether a search result done response has been sent to the
   * client.
   */
  private final AtomicBoolean responseSent = new AtomicBoolean(false);

  /** Indicates whether the client is able to handle referrals. */
  private boolean clientAcceptsReferrals = true;

  /**
   * Indicates whether to include the account usable control with search result
   * entries.
   */
  private boolean includeUsableControl;

  /** Indicates whether to only real attributes should be returned. */
  private boolean realAttributesOnly;

  /** Indicates whether only LDAP subentries should be returned. */
  private boolean returnSubentriesOnly;

  /**
   * Indicates whether the filter references subentry or ldapSubentry object
   * class.
   */
  private boolean filterIncludesSubentries;
  private boolean filterNeedsCheckingForSubentries = true;

  /**
   * Indicates whether to include attribute types only or both types and values.
   */
  private boolean typesOnly;

  /** Indicates whether to only virtual attributes should be returned. */
  private boolean virtualAttributesOnly;

  /**
   * The raw, unprocessed base DN as included in the request from the client.
   */
  private ByteString rawBaseDN;

  /** The dereferencing policy for the search operation. */
  private DereferenceAliasesPolicy derefPolicy;

  /** The base DN for the search operation. */
  private DN baseDN;

  /** The proxied authorization target DN for this operation. */
  private DN proxiedAuthorizationDN;

  /** The number of entries that have been sent to the client. */
  private int entriesSent;

  /**
   * The number of search result references that have been sent to the client.
   */
  private int referencesSent;

  /** The size limit for the search operation. */
  private int sizeLimit;

  /** The time limit for the search operation. */
  private int timeLimit;

  /** The raw, unprocessed filter as included in the request from the client. */
  private RawFilter rawFilter;

  /** The set of attributes that should be returned in matching entries. */
  private Set<String> attributes;

  /** The set of response controls for this search operation. */
  private final List<Control> responseControls = new ArrayList<>();

  /** The time that the search time limit has expired. */
  private long timeLimitExpiration;

  /** The matched values control associated with this search operation. */
  private MatchedValuesControl matchedValuesControl;

  /** The search filter for the search operation. */
  private SearchFilter filter;

  /** The search scope for the search operation. */
  private SearchScope scope;

  /** Indicates whether to send the search result done to the client or not. */
  private boolean sendResponse = true;

  /**
   * Creates a new search operation with the provided information.
   *
   * @param  clientConnection  The client connection with which this operation
   *                           is associated.
   * @param  operationID       The operation ID for this operation.
   * @param  messageID         The message ID of the request with which this
   *                           operation is associated.
   * @param  requestControls   The set of controls included in the request.
   * @param  rawBaseDN         The raw, unprocessed base DN as included in the
   *                           request from the client.
   * @param  scope             The scope for this search operation.
   * @param  derefPolicy       The alias dereferencing policy for this search
   *                           operation.
   * @param  sizeLimit         The size limit for this search operation.
   * @param  timeLimit         The time limit for this search operation.
   * @param  typesOnly         The typesOnly flag for this search operation.
   * @param  rawFilter         the raw, unprocessed filter as included in the
   *                           request from the client.
   * @param  attributes        The requested attributes for this search
   *                           operation.
   */
  public SearchOperationBasis(ClientConnection clientConnection,
                         long operationID,
                         int messageID, List<Control> requestControls,
                         ByteString rawBaseDN, SearchScope scope,
                         DereferenceAliasesPolicy derefPolicy, int sizeLimit,
                         int timeLimit, boolean typesOnly, RawFilter rawFilter,
                         Set<String> attributes)
  {
    super(clientConnection, operationID, messageID, requestControls);

    this.rawBaseDN   = rawBaseDN;
    this.scope       = scope;
    this.derefPolicy = derefPolicy;
    this.sizeLimit   = sizeLimit;
    this.timeLimit   = timeLimit;
    this.typesOnly   = typesOnly;
    this.rawFilter   = rawFilter;
    this.attributes  = attributes != null ? attributes : new LinkedHashSet<String>(0);

    this.sizeLimit = getSizeLimit(sizeLimit, clientConnection);
    this.timeLimit = getTimeLimit(timeLimit, clientConnection);
  }

  /**
   * Creates a new search operation with the provided information.
   *
   * @param  clientConnection  The client connection with which this operation
   *                           is associated.
   * @param  operationID       The operation ID for this operation.
   * @param  messageID         The message ID of the request with which this
   *                           operation is associated.
   * @param  requestControls   The set of controls included in the request.
   * @param  baseDN            The base DN for this search operation.
   * @param  scope             The scope for this search operation.
   * @param  derefPolicy       The alias dereferencing policy for this search
   *                           operation.
   * @param  sizeLimit         The size limit for this search operation.
   * @param  timeLimit         The time limit for this search operation.
   * @param  typesOnly         The typesOnly flag for this search operation.
   * @param  filter            The filter for this search operation.
   * @param  attributes        The attributes for this search operation.
   */
  public SearchOperationBasis(ClientConnection clientConnection,
                         long operationID,
                         int messageID, List<Control> requestControls,
                         DN baseDN, SearchScope scope,
                         DereferenceAliasesPolicy derefPolicy, int sizeLimit,
                         int timeLimit, boolean typesOnly, SearchFilter filter,
                         Set<String> attributes)
  {
    super(clientConnection, operationID, messageID, requestControls);

    this.baseDN      = baseDN;
    this.scope       = scope;
    this.derefPolicy = derefPolicy;
    this.sizeLimit   = sizeLimit;
    this.timeLimit   = timeLimit;
    this.typesOnly   = typesOnly;
    this.filter      = filter;
    this.attributes  = attributes != null ? attributes : new LinkedHashSet<String>(0);

    rawBaseDN = ByteString.valueOfUtf8(baseDN.toString());
    rawFilter = new LDAPFilter(filter);

    this.sizeLimit = getSizeLimit(sizeLimit, clientConnection);
    this.timeLimit = getTimeLimit(timeLimit, clientConnection);
  }


  private int getSizeLimit(int sizeLimit, ClientConnection clientConnection)
  {
    if (clientConnection.getSizeLimit() <= 0)
    {
      return sizeLimit;
    }
    else if (sizeLimit <= 0)
    {
      return clientConnection.getSizeLimit();
    }
    return Math.min(sizeLimit, clientConnection.getSizeLimit());
  }

  private int getTimeLimit(int timeLimit, ClientConnection clientConnection)
  {
    if (clientConnection.getTimeLimit() <= 0)
    {
      return timeLimit;
    }
    else if (timeLimit <= 0)
    {
      return clientConnection.getTimeLimit();
    }
    return Math.min(timeLimit, clientConnection.getTimeLimit());
  }

  @Override
  public final ByteString getRawBaseDN()
  {
    return rawBaseDN;
  }

  @Override
  public final void setRawBaseDN(ByteString rawBaseDN)
  {
    this.rawBaseDN = rawBaseDN;

    baseDN = null;
  }

  @Override
  public final DN getBaseDN()
  {
    try
    {
      if (baseDN == null)
      {
        baseDN = DN.decode(rawBaseDN);
      }
    }
    catch (DirectoryException de)
    {
      logger.traceException(de);
      setResponseData(de);
    }
    return baseDN;
  }

  @Override
  public final void setBaseDN(DN baseDN)
  {
    this.baseDN = baseDN;
  }

  @Override
  public final SearchScope getScope()
  {
    return scope;
  }

  @Override
  public final void setScope(SearchScope scope)
  {
    this.scope = scope;
  }

  @Override
  public final DereferenceAliasesPolicy getDerefPolicy()
  {
    return derefPolicy;
  }

  @Override
  public final void setDerefPolicy(DereferenceAliasesPolicy derefPolicy)
  {
    this.derefPolicy = derefPolicy;
  }

  @Override
  public final int getSizeLimit()
  {
    return sizeLimit;
  }

  @Override
  public final void setSizeLimit(int sizeLimit)
  {
    this.sizeLimit = sizeLimit;
  }

  @Override
  public final int getTimeLimit()
  {
    return timeLimit;
  }

  @Override
  public final void setTimeLimit(int timeLimit)
  {
    this.timeLimit = timeLimit;
  }

  @Override
  public final boolean getTypesOnly()
  {
    return typesOnly;
  }

  @Override
  public final void setTypesOnly(boolean typesOnly)
  {
    this.typesOnly = typesOnly;
  }

  @Override
  public final RawFilter getRawFilter()
  {
    return rawFilter;
  }

  @Override
  public final void setRawFilter(RawFilter rawFilter)
  {
    this.rawFilter = rawFilter;

    filter = null;
  }

  @Override
  public final SearchFilter getFilter()
  {
    try
    {
      if (filter == null)
      {
        filter = rawFilter.toSearchFilter();
      }
    }
    catch (DirectoryException de)
    {
      logger.traceException(de);
      setResponseData(de);
    }
    return filter;
  }

  @Override
  public final Set<String> getAttributes()
  {
    return attributes;
  }

  @Override
  public final void setAttributes(Set<String> attributes)
  {
    if (attributes == null)
    {
      this.attributes.clear();
    }
    else
    {
      this.attributes = attributes;
    }
  }

  @Override
  public final int getEntriesSent()
  {
    return entriesSent;
  }

  @Override
  public final int getReferencesSent()
  {
    return referencesSent;
  }

  @Override
  public final boolean returnEntry(Entry entry, List<Control> controls)
  {
    return returnEntry(entry, controls, true);
  }

  @Override
  public final boolean returnEntry(Entry entry, List<Control> controls,
                                   boolean evaluateAci)
  {
    boolean typesOnly = getTypesOnly();

    // See if the size limit has been exceeded.  If so, then don't send the
    // entry and indicate that the search should end.
    if (getSizeLimit() > 0 && getEntriesSent() >= getSizeLimit())
    {
      setResultCode(ResultCode.SIZE_LIMIT_EXCEEDED);
      appendErrorMessage(ERR_SEARCH_SIZE_LIMIT_EXCEEDED.get(getSizeLimit()));
      return false;
    }

    // See if the time limit has expired.  If so, then don't send the entry and
    // indicate that the search should end.
    if (getTimeLimit() > 0
        && TimeThread.getTime() >= getTimeLimitExpiration())
    {
      setResultCode(ResultCode.TIME_LIMIT_EXCEEDED);
      appendErrorMessage(ERR_SEARCH_TIME_LIMIT_EXCEEDED.get(getTimeLimit()));
      return false;
    }

    // Determine whether the provided entry is a subentry and if so whether it
    // should be returned.
    if (entry.isSubentry() || entry.isLDAPSubentry())
    {
      if (filterNeedsCheckingForSubentries)
      {
        filterIncludesSubentries = checkFilterForLDAPSubEntry(filter, 0);
        filterNeedsCheckingForSubentries = false;
      }

      if (getScope() != SearchScope.BASE_OBJECT
          && !filterIncludesSubentries
          && !isReturnSubentriesOnly())
      {
        return true;
      }
    }
    else if (isReturnSubentriesOnly())
    {
      // Subentries are visible and normal entries are not.
      return true;
    }

    // Determine whether to include the account usable control. If so, then
    // create it now.
    if (isIncludeUsableControl())
    {
      if (controls == null)
      {
        controls = new ArrayList<>(1);
      }

      try
      {
        // FIXME -- Need a way to enable PWP debugging.
        AuthenticationPolicyState state = AuthenticationPolicyState.forUser(
            entry, false);
        if (state.isPasswordPolicy())
        {
          PasswordPolicyState pwpState = (PasswordPolicyState) state;

          boolean isInactive = pwpState.isDisabled()
              || pwpState.isAccountExpired();
          boolean isLocked = pwpState.isLocked();
          boolean isReset = pwpState.mustChangePassword();
          boolean isExpired = pwpState.isPasswordExpired();

          if (isInactive || isLocked || isReset || isExpired)
          {
            int secondsBeforeUnlock = pwpState.getSecondsUntilUnlock();
            int remainingGraceLogins = pwpState.getGraceLoginsRemaining();
            controls
                .add(new AccountUsableResponseControl(isInactive, isReset,
                    isExpired, remainingGraceLogins, isLocked,
                    secondsBeforeUnlock));
          }
          else
          {
            int secondsBeforeExpiration = pwpState.getSecondsUntilExpiration();
            controls.add(new AccountUsableResponseControl(
                secondsBeforeExpiration));
          }
        }
        // Another type of authentication policy (e.g. PTA).
        else if (state.isDisabled())
        {
          controls.add(new AccountUsableResponseControl(false, false, false,
              -1, true, -1));
        }
        else
        {
          controls.add(new AccountUsableResponseControl(-1));
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }

    // Check to see if the entry can be read by the client.
    SearchResultEntry unfilteredSearchEntry = new SearchResultEntry(entry, controls);
    if (evaluateAci && !getACIHandler().maySend(this, unfilteredSearchEntry))
    {
      return true;
    }

    // Make a copy of the entry and pare it down to only include the set
    // of requested attributes.

    // NOTE: that this copy will include the objectClass attribute.
    Entry filteredEntry =
        entry.filterEntry(getAttributes(), typesOnly,
            isVirtualAttributesOnly(), isRealAttributesOnly());


    // If there is a matched values control, then further pare down the entry
    // based on the filters that it contains.
    MatchedValuesControl matchedValuesControl = getMatchedValuesControl();
    if (matchedValuesControl != null && !typesOnly)
    {
      // First, look at the set of objectclasses.

      // NOTE: the objectClass attribute is also present and must be
      // dealt with later.
      AttributeType attrType = DirectoryServer.getObjectClassAttributeType();
      Iterator<String> ocIterator =
           filteredEntry.getObjectClasses().values().iterator();
      while (ocIterator.hasNext())
      {
        ByteString ocName = ByteString.valueOfUtf8(ocIterator.next());
        if (! matchedValuesControl.valueMatches(attrType, ocName))
        {
          ocIterator.remove();
        }
      }


      // Next, the set of user attributes (incl. objectClass attribute).
      for (Map.Entry<AttributeType, List<Attribute>> e : filteredEntry
          .getUserAttributes().entrySet())
      {
        AttributeType t = e.getKey();
        List<Attribute> oldAttributes = e.getValue();
        List<Attribute> newAttributes = new ArrayList<>(oldAttributes.size());

        for (Attribute a : oldAttributes)
        {
          // Assume that the attribute will be either empty or contain
          // very few values.
          AttributeBuilder builder = new AttributeBuilder(a, true);
          for (ByteString v : a)
          {
            if (matchedValuesControl.valueMatches(t, v))
            {
              builder.add(v);
            }
          }
          newAttributes.add(builder.toAttribute());
        }
        e.setValue(newAttributes);
      }


      // Then the set of operational attributes.
      for (Map.Entry<AttributeType, List<Attribute>> e : filteredEntry
          .getOperationalAttributes().entrySet())
      {
        AttributeType t = e.getKey();
        List<Attribute> oldAttributes = e.getValue();
        List<Attribute> newAttributes = new ArrayList<>(oldAttributes.size());

        for (Attribute a : oldAttributes)
        {
          // Assume that the attribute will be either empty or contain
          // very few values.
          AttributeBuilder builder = new AttributeBuilder(a, true);
          for (ByteString v : a)
          {
            if (matchedValuesControl.valueMatches(t, v))
            {
              builder.add(v);
            }
          }
          newAttributes.add(builder.toAttribute());
        }
        e.setValue(newAttributes);
      }
    }


    // Convert the provided entry to a search result entry.
    SearchResultEntry filteredSearchEntry = new SearchResultEntry(
        filteredEntry, controls);

    // Strip out any attributes that the client does not have access to.

    // FIXME: need some way to prevent plugins from adding attributes or
    // values that the client is not permitted to see.
    if (evaluateAci)
    {
      getACIHandler().filterEntry(this, unfilteredSearchEntry, filteredSearchEntry);
    }

    // Invoke any search entry plugins that may be registered with the server.
    PluginResult.IntermediateResponse pluginResult =
         DirectoryServer.getPluginConfigManager().
              invokeSearchResultEntryPlugins(this, filteredSearchEntry);

    // Send the entry to the client.
    if (pluginResult.sendResponse())
    {
      // Log the entry sent to the client.
      logSearchResultEntry(this, filteredSearchEntry);

      try
      {
        sendSearchEntry(filteredSearchEntry);

        entriesSent++;
      }
      catch (DirectoryException de)
      {
        logger.traceException(de);

        setResponseData(de);
        return false;
      }
    }

    return pluginResult.continueProcessing();
  }

  private AccessControlHandler<?> getACIHandler()
  {
    return AccessControlConfigManager.getInstance().getAccessControlHandler();
  }

  @Override
  public final boolean returnReference(DN dn, SearchResultReference reference)
  {
    return returnReference(dn, reference, true);
  }

  @Override
  public final boolean returnReference(DN dn, SearchResultReference reference,
                                       boolean evaluateAci)
  {
    // See if the time limit has expired.  If so, then don't send the entry and
    // indicate that the search should end.
    if (getTimeLimit() > 0
        && TimeThread.getTime() >= getTimeLimitExpiration())
    {
      setResultCode(ResultCode.TIME_LIMIT_EXCEEDED);
      appendErrorMessage(ERR_SEARCH_TIME_LIMIT_EXCEEDED.get(getTimeLimit()));
      return false;
    }


    // See if we know that this client can't handle referrals.  If so, then
    // don't even try to send it.
    if (!isClientAcceptsReferrals()
        // See if the client has permission to read this reference.
        || (evaluateAci && !getACIHandler().maySend(dn, this, reference)))
    {
      return true;
    }


    // Invoke any search reference plugins that may be registered with the
    // server.
    PluginResult.IntermediateResponse pluginResult =
         DirectoryServer.getPluginConfigManager().
              invokeSearchResultReferencePlugins(this, reference);

    // Send the reference to the client.  Note that this could throw an
    // exception, which would indicate that the associated client can't handle
    // referrals.  If that't the case, then set a flag so we'll know not to try
    // to send any more.
    if (pluginResult.sendResponse())
    {
      // Log the entry sent to the client.
      logSearchResultReference(this, reference);

      try
      {
        if (sendSearchReference(reference))
        {
          referencesSent++;

          // FIXME -- Should the size limit apply here?
        }
        else
        {
          // We know that the client can't handle referrals, so we won't try to
          // send it any more.
          setClientAcceptsReferrals(false);
        }
      }
      catch (DirectoryException de)
      {
        logger.traceException(de);

        setResponseData(de);
        return false;
      }
    }

    return pluginResult.continueProcessing();
  }

  @Override
  public final void sendSearchResultDone()
  {
    // Send the search result done message to the client.  We want to make sure
    // that this only gets sent once, and it's possible that this could be
    // multithreaded in the event of a persistent search, so do it safely.
    if (responseSent.compareAndSet(false, true))
    {
      logSearchResultDone(this);

      clientConnection.sendResponse(this);

      invokePostResponsePlugins();
    }
  }

  @Override
  public final OperationType getOperationType()
  {
    // Note that no debugging will be done in this method because it is a likely
    // candidate for being called by the logging subsystem.
    return OperationType.SEARCH;
  }

  @Override
  public DN getProxiedAuthorizationDN()
  {
    return proxiedAuthorizationDN;
  }

  @Override
  public final List<Control> getResponseControls()
  {
    return responseControls;
  }

  @Override
  public final void addResponseControl(Control control)
  {
    responseControls.add(control);
  }

  @Override
  public final void removeResponseControl(Control control)
  {
    responseControls.remove(control);
  }

  @Override
  public void abort(CancelRequest cancelRequest)
  {
    if(cancelResult == null && this.cancelRequest == null)
    {
      this.cancelRequest = cancelRequest;
    }
  }

  @Override
  public final void toString(StringBuilder buffer)
  {
    buffer.append("SearchOperation(connID=");
    buffer.append(clientConnection.getConnectionID());
    buffer.append(", opID=");
    buffer.append(operationID);
    buffer.append(", baseDN=");
    buffer.append(rawBaseDN);
    buffer.append(", scope=");
    buffer.append(scope);
    buffer.append(", filter=");
    buffer.append(rawFilter);
    buffer.append(")");
  }

  @Override
  public void setTimeLimitExpiration(long timeLimitExpiration)
  {
    this.timeLimitExpiration = timeLimitExpiration;
  }

  @Override
  public boolean isReturnSubentriesOnly()
  {
    return returnSubentriesOnly;
  }

  @Override
  public void setReturnSubentriesOnly(boolean returnLDAPSubentries)
  {
    this.returnSubentriesOnly = returnLDAPSubentries;
  }

  @Override
  public MatchedValuesControl getMatchedValuesControl()
  {
    return matchedValuesControl;
  }

  @Override
  public void setMatchedValuesControl(MatchedValuesControl controls)
  {
    this.matchedValuesControl = controls;
  }

  @Override
  public boolean isIncludeUsableControl()
  {
    return includeUsableControl;
  }

  @Override
  public void setIncludeUsableControl(boolean includeUsableControl)
  {
    this.includeUsableControl = includeUsableControl;
  }

  @Override
  public long getTimeLimitExpiration()
  {
    return timeLimitExpiration;
  }

  @Override
  public boolean isClientAcceptsReferrals()
  {
    return clientAcceptsReferrals;
  }

  @Override
  public void setClientAcceptsReferrals(boolean clientAcceptReferrals)
  {
    this.clientAcceptsReferrals = clientAcceptReferrals;
  }

  @Override
  public boolean isSendResponse()
  {
    return sendResponse;
  }

  @Override
  public void setSendResponse(boolean sendResponse)
  {
    this.sendResponse = sendResponse;
  }

  @Override
  public boolean isRealAttributesOnly()
  {
    return this.realAttributesOnly;
  }

  @Override
  public boolean isVirtualAttributesOnly()
  {
    return this.virtualAttributesOnly;
  }

  @Override
  public void setRealAttributesOnly(boolean realAttributesOnly)
  {
    this.realAttributesOnly = realAttributesOnly;
  }

  @Override
  public void setVirtualAttributesOnly(boolean virtualAttributesOnly)
  {
    this.virtualAttributesOnly = virtualAttributesOnly;
  }

  @Override
  public void sendSearchEntry(SearchResultEntry searchEntry)
      throws DirectoryException
  {
    getClientConnection().sendSearchEntry(this, searchEntry);
  }

  @Override
  public boolean sendSearchReference(SearchResultReference searchReference)
      throws DirectoryException
  {
    return getClientConnection().sendSearchReference(this, searchReference);
  }

  @Override
  public void setProxiedAuthorizationDN(DN proxiedAuthorizationDN)
  {
    this.proxiedAuthorizationDN = proxiedAuthorizationDN;
  }

  @Override
  public final void run()
  {
    setResultCode(ResultCode.UNDEFINED);

    // Start the processing timer.
    setProcessingStartTime();

    logSearchRequest(this);

    setSendResponse(true);

    int timeLimit = getTimeLimit();
    long timeLimitExpiration;
    if (timeLimit <= 0)
    {
      timeLimitExpiration = Long.MAX_VALUE;
    }
    else
    {
      // FIXME -- Factor in the user's effective time limit.
      timeLimitExpiration = getProcessingStartTime() + (1000L * timeLimit);
    }
    setTimeLimitExpiration(timeLimitExpiration);

    try
    {
      // Check for and handle a request to cancel this operation.
      checkIfCanceled(false);

      if (!processOperationResult(getPluginConfigManager().invokePreParseSearchPlugins(this)))
      {
        return;
      }

      // Check for and handle a request to cancel this operation.
      checkIfCanceled(false);

      // Process the search base and filter to convert them from their raw forms
      // as provided by the client to the forms required for the rest of the
      // search processing.
      DN baseDN = getBaseDN();
      if (baseDN == null){
        return;
      }

      execute(this, baseDN);
    }
    catch(CanceledOperationException coe)
    {
      logger.traceException(coe);

      setResultCode(ResultCode.CANCELLED);
      cancelResult = new CancelResult(ResultCode.CANCELLED, null);

      appendErrorMessage(coe.getCancelRequest().getCancelReason());
    }
    finally
    {
      // Stop the processing timer.
      setProcessingStopTime();

      if(cancelRequest == null || cancelResult == null ||
          cancelResult.getResultCode() != ResultCode.CANCELLED)
      {
        // If everything is successful to this point and it is not a persistent
        // search, then send the search result done message to the client.
        // Otherwise, we'll want to make the size and time limit values
        // unlimited to ensure that the remainder of the persistent search
        // isn't subject to those restrictions.
        if (isSendResponse())
        {
          sendSearchResultDone();
        }
        else
        {
          setSizeLimit(0);
          setTimeLimit(0);
        }
      }
      else if(cancelRequest.notifyOriginalRequestor() ||
          DirectoryServer.notifyAbandonedOperations())
      {
        sendSearchResultDone();
      }

      // If no cancel result, set it
      if(cancelResult == null)
      {
        cancelResult = new CancelResult(ResultCode.TOO_LATE, null);
      }
    }
  }


  /** Invokes the post response plugins. */
  private void invokePostResponsePlugins()
  {
    // Invoke the post response plugins that have been registered with
    // the current operation
    getPluginConfigManager().invokePostResponseSearchPlugins(this);
  }

  @Override
  public void updateOperationErrMsgAndResCode()
  {
    setResultCode(ResultCode.NO_SUCH_OBJECT);
    appendErrorMessage(ERR_SEARCH_BASE_DOESNT_EXIST.get(getBaseDN()));
  }

  /**
   * Checks if the filter contains an equality element with the objectclass
   * attribute type and a value of "ldapSubentry" and if so sets
   * returnSubentriesOnly to <code>true</code>.
   *
   * @param filter
   *          The complete filter being checked, of which this filter may be a
   *          subset.
   * @param depth
   *          The current depth of the evaluation, which is used to prevent
   *          infinite recursion due to highly nested filters and eventually
   *          running out of stack space.
   * @return {@code true} if the filter references the sub-entry object class.
   */
  private boolean checkFilterForLDAPSubEntry(SearchFilter filter, int depth)
  {
    // Paranoid check to avoid recursion deep enough to provoke
    // the stack overflow. This should never happen because if
    // a given filter is too nested SearchFilter exception gets
    // raised long before this method is invoked.
    if (depth >= MAX_NESTED_FILTER_DEPTH)
    {
      if (logger.isTraceEnabled())
      {
        logger.trace("Exceeded maximum filter depth");
      }
      return false;
    }

    switch (filter.getFilterType())
    {
    case EQUALITY:
      if (filter.getAttributeType().isObjectClass())
      {
        ByteString v = filter.getAssertionValue();
        // FIXME : technically this is not correct since the presence
        // of draft oc would trigger rfc oc visibility and visa versa.
        String stringValueLC = toLowerCase(v.toString());
        if (OC_LDAP_SUBENTRY_LC.equals(stringValueLC) ||
            OC_SUBENTRY.equals(stringValueLC))
        {
          return true;
        }
      }
      break;
    case AND:
    case OR:
      for (SearchFilter f : filter.getFilterComponents())
      {
        if (checkFilterForLDAPSubEntry(f, depth + 1))
        {
          return true;
        }
      }
      break;
    }

    return false;
  }
}
