/*
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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.
*/
package org.opends.server.core;
import static org.opends.server.loggers.debug.DebugLogger.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.opends.server.controls.EntryChangeNotificationControl;
import org.opends.server.controls.PersistentSearchChangeType;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.CancelResult;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
/**
* This class defines a data structure that will be used to hold the
* information necessary for processing a persistent search.
* <p>
* Work flow element implementations are responsible for managing the
* persistent searches that they are currently handling.
* <p>
* Typically, a work flow element search operation will first decode
* the persistent search control and construct a new {@code
* PersistentSearch}.
* <p>
* Once the initial search result set has been returned and no errors
* encountered, the work flow element implementation should register a
* cancellation callback which will be invoked when the persistent
* search is cancelled. This is achieved using
* {@link #registerCancellationCallback(CancellationCallback)}. The
* callback should make sure that any resources associated with the
* {@code PersistentSearch} are released. This may included removing
* the {@code PersistentSearch} from a list, or abandoning a
* persistent search operation that has been sent to a remote server.
* <p>
* Finally, the {@code PersistentSearch} should be enabled using
* {@link #enable()}. This method will register the {@code
* PersistentSearch} with the client connection and notify the
* underlying search operation that no result should be sent to the
* client.
* <p>
* Work flow element implementations should {@link #cancel()} active
* persistent searches when the work flow element fails or is shut
* down.
*/
public final class PersistentSearch
{
/**
* A cancellation call-back which can be used by work-flow element
* implementations in order to register for resource cleanup when a
* persistent search is cancelled.
*/
public static interface CancellationCallback
{
/**
* The provided persistent search has been cancelled. Any
* resources associated with the persistent search should be
* released.
*
* @param psearch
* The persistent search which has just been cancelled.
*/
void persistentSearchCancelled(PersistentSearch psearch);
}
/**
* The tracer object for the debug logger.
*/
private static final DebugTracer TRACER = getTracer();
// Cancel a persistent search.
private static synchronized void cancel(PersistentSearch psearch)
{
if (!psearch.isCancelled)
{
psearch.isCancelled = true;
// The persistent search can no longer be cancelled.
psearch.searchOperation.getClientConnection().deregisterPersistentSearch(
psearch);
//Decrement of psearch count maintained by the server.
DirectoryServer.deregisterPersistentSearch();
// Notify any cancellation callbacks.
for (CancellationCallback callback : psearch.cancellationCallbacks)
{
try
{
callback.persistentSearchCancelled(psearch);
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
}
}
}
// The base DN for the search operation.
private final DN baseDN;
// Cancellation callbacks which should be run when this persistent
// search is cancelled.
private final List<CancellationCallback> cancellationCallbacks =
new CopyOnWriteArrayList<CancellationCallback>();
// The set of change types we want to see.
private final Set<PersistentSearchChangeType> changeTypes;
// The filter for the search operation.
private final SearchFilter filter;
// Indicates whether or not this persistent search has already been
// aborted.
private boolean isCancelled = false;
// Indicates whether entries returned should include the entry
// change notification control.
private final boolean returnECs;
// The scope for the search operation.
private final SearchScope scope;
// The reference to the associated search operation.
private final SearchOperation searchOperation;
/**
* Creates a new persistent search object with the provided
* information.
*
* @param searchOperation
* The search operation for this persistent search.
* @param changeTypes
* The change types for which changes should be examined.
* @param returnECs
* Indicates whether to include entry change notification
* controls in search result entries sent to the client.
*/
public PersistentSearch(SearchOperation searchOperation,
Set<PersistentSearchChangeType> changeTypes, boolean returnECs)
{
this.searchOperation = searchOperation;
this.changeTypes = changeTypes;
this.returnECs = returnECs;
this.baseDN = searchOperation.getBaseDN();
this.scope = searchOperation.getScope();
this.filter = searchOperation.getFilter();
}
/**
* Cancels this persistent search operation. On exit this persistent
* search will no longer be valid and any resources associated with
* it will have been released. In addition, any other persistent
* searches that are associated with this persistent search will
* also be canceled.
*
* @return The result of the cancellation.
*/
public synchronized CancelResult cancel()
{
if (!isCancelled)
{
// Cancel this persistent search.
cancel(this);
// Cancel any other persistent searches which are associated
// with this one. For example, a persistent search may be
// distributed across multiple proxies.
for (PersistentSearch psearch : searchOperation.getClientConnection()
.getPersistentSearches())
{
if (psearch.getMessageID() == getMessageID())
{
cancel(psearch);
}
}
}
return new CancelResult(ResultCode.CANCELED, null);
}
/**
* Gets the message ID associated with this persistent search.
*
* @return The message ID associated with this persistent search.
*/
public int getMessageID()
{
return searchOperation.getMessageID();
}
/**
* Get the search operation associated with this persistent search.
*
* @return The search operation associated with this persistent search.
*/
public SearchOperation getSearchOperation()
{
return searchOperation;
}
/**
* Notifies the persistent searches that an entry has been added.
*
* @param entry
* The entry that was added.
* @param changeNumber
* The change number associated with the operation that
* added the entry, or {@code -1} if there is no change
* number.
*/
public void processAdd(Entry entry, long changeNumber)
{
// See if we care about add operations.
if (!changeTypes.contains(PersistentSearchChangeType.ADD))
{
return;
}
// Make sure that the entry is within our target scope.
switch (scope)
{
case BASE_OBJECT:
if (!baseDN.equals(entry.getDN()))
{
return;
}
break;
case SINGLE_LEVEL:
if (!baseDN.equals(entry.getDN().getParentDNInSuffix()))
{
return;
}
break;
case WHOLE_SUBTREE:
if (!baseDN.isAncestorOf(entry.getDN()))
{
return;
}
break;
case SUBORDINATE_SUBTREE:
if (baseDN.equals(entry.getDN()) || (!baseDN.isAncestorOf(entry.getDN())))
{
return;
}
break;
default:
return;
}
// Make sure that the entry matches the target filter.
try
{
TRACER.debugInfo(this + " " + entry + " +filter="
+ filter.matchesEntry(entry));
if (!filter.matchesEntry(entry))
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
// FIXME -- Do we need to do anything here?
return;
}
// The entry is one that should be sent to the client. See if we
// also need to construct an entry change notification control.
ArrayList<Control> entryControls = new ArrayList<Control>(1);
if (returnECs)
{
entryControls.add(new EntryChangeNotificationControl(
PersistentSearchChangeType.ADD, changeNumber));
}
// Send the entry and see if we should continue processing. If
// not, then deregister this persistent search.
try
{
if (!searchOperation.returnEntry(entry, entryControls))
{
cancel();
searchOperation.sendSearchResultDone();
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
cancel();
try
{
searchOperation.sendSearchResultDone();
}
catch (Exception e2)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e2);
}
}
}
}
/**
* Notifies the persistent searches that an entry has been deleted.
*
* @param entry
* The entry that was deleted.
* @param changeNumber
* The change number associated with the operation that
* deleted the entry, or {@code -1} if there is no change
* number.
*/
public void processDelete(Entry entry, long changeNumber)
{
// See if we care about delete operations.
if (!changeTypes.contains(PersistentSearchChangeType.DELETE))
{
return;
}
// Make sure that the entry is within our target scope.
switch (scope)
{
case BASE_OBJECT:
if (!baseDN.equals(entry.getDN()))
{
return;
}
break;
case SINGLE_LEVEL:
if (!baseDN.equals(entry.getDN().getParentDNInSuffix()))
{
return;
}
break;
case WHOLE_SUBTREE:
if (!baseDN.isAncestorOf(entry.getDN()))
{
return;
}
break;
case SUBORDINATE_SUBTREE:
if (baseDN.equals(entry.getDN()) || (!baseDN.isAncestorOf(entry.getDN())))
{
return;
}
break;
default:
return;
}
// Make sure that the entry matches the target filter.
try
{
if (!filter.matchesEntry(entry))
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
// FIXME -- Do we need to do anything here?
return;
}
// The entry is one that should be sent to the client. See if we
// also need to construct an entry change notification control.
ArrayList<Control> entryControls = new ArrayList<Control>(1);
if (returnECs)
{
entryControls.add(new EntryChangeNotificationControl(
PersistentSearchChangeType.DELETE, changeNumber));
}
// Send the entry and see if we should continue processing. If
// not, then deregister this persistent search.
try
{
if (!searchOperation.returnEntry(entry, entryControls))
{
cancel();
searchOperation.sendSearchResultDone();
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
cancel();
try
{
searchOperation.sendSearchResultDone();
}
catch (Exception e2)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e2);
}
}
}
}
/**
* Notifies the persistent searches that an entry has been modified.
*
* @param entry
* The entry after it was modified.
* @param changeNumber
* The change number associated with the operation that
* modified the entry, or {@code -1} if there is no change
* number.
*/
public void processModify(Entry entry, long changeNumber)
{
processModify(entry, changeNumber, entry);
}
/**
* Notifies persistent searches that an entry has been modified.
*
* @param entry
* The entry after it was modified.
* @param changeNumber
* The change number associated with the operation that
* modified the entry, or {@code -1} if there is no change
* number.
* @param oldEntry
* The entry before it was modified.
*/
public void processModify(Entry entry, long changeNumber, Entry oldEntry)
{
// See if we care about modify operations.
if (!changeTypes.contains(PersistentSearchChangeType.MODIFY))
{
return;
}
// Make sure that the entry is within our target scope.
switch (scope)
{
case BASE_OBJECT:
if (!baseDN.equals(oldEntry.getDN()))
{
return;
}
break;
case SINGLE_LEVEL:
if (!baseDN.equals(oldEntry.getDN().getParent()))
{
return;
}
break;
case WHOLE_SUBTREE:
if (!baseDN.isAncestorOf(oldEntry.getDN()))
{
return;
}
break;
case SUBORDINATE_SUBTREE:
if (baseDN.equals(oldEntry.getDN())
|| (!baseDN.isAncestorOf(oldEntry.getDN())))
{
return;
}
break;
default:
return;
}
// Make sure that the entry matches the target filter.
try
{
if ((!filter.matchesEntry(oldEntry)) && (!filter.matchesEntry(entry)))
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
// FIXME -- Do we need to do anything here?
return;
}
// The entry is one that should be sent to the client. See if we
// also need to construct an entry change notification control.
ArrayList<Control> entryControls = new ArrayList<Control>(1);
if (returnECs)
{
entryControls.add(new EntryChangeNotificationControl(
PersistentSearchChangeType.MODIFY, changeNumber));
}
// Send the entry and see if we should continue processing. If
// not, then deregister this persistent search.
try
{
if (!searchOperation.returnEntry(entry, entryControls))
{
cancel();
searchOperation.sendSearchResultDone();
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
cancel();
try
{
searchOperation.sendSearchResultDone();
}
catch (Exception e2)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e2);
}
}
}
}
/**
* Notifies the persistent searches that an entry has been renamed.
*
* @param entry
* The entry after it was modified.
* @param changeNumber
* The change number associated with the operation that
* modified the entry, or {@code -1} if there is no change
* number.
* @param oldDN
* The DN of the entry before it was renamed.
*/
public void processModifyDN(Entry entry, long changeNumber, DN oldDN)
{
// See if we care about modify DN operations.
if (!changeTypes.contains(PersistentSearchChangeType.MODIFY_DN))
{
return;
}
// Make sure that the old or new entry is within our target scope.
// In this case, we need to check the DNs of both the old and new
// entry so we know which one(s) should be compared against the
// filter.
boolean oldMatches = false;
boolean newMatches = false;
switch (scope)
{
case BASE_OBJECT:
oldMatches = baseDN.equals(oldDN);
newMatches = baseDN.equals(entry.getDN());
if (!(oldMatches || newMatches))
{
return;
}
break;
case SINGLE_LEVEL:
oldMatches = baseDN.equals(oldDN.getParent());
newMatches = baseDN.equals(entry.getDN().getParent());
if (!(oldMatches || newMatches))
{
return;
}
break;
case WHOLE_SUBTREE:
oldMatches = baseDN.isAncestorOf(oldDN);
newMatches = baseDN.isAncestorOf(entry.getDN());
if (!(oldMatches || newMatches))
{
return;
}
break;
case SUBORDINATE_SUBTREE:
oldMatches = ((!baseDN.equals(oldDN)) && baseDN.isAncestorOf(oldDN));
newMatches = ((!baseDN.equals(entry.getDN())) && baseDN
.isAncestorOf(entry.getDN()));
if (!(oldMatches || newMatches))
{
return;
}
break;
default:
return;
}
// Make sure that the entry matches the target filter.
try
{
if (!oldMatches && !newMatches && !filter.matchesEntry(entry))
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, de);
}
// FIXME -- Do we need to do anything here?
return;
}
// The entry is one that should be sent to the client. See if we
// also need to construct an entry change notification control.
ArrayList<Control> entryControls = new ArrayList<Control>(1);
if (returnECs)
{
entryControls.add(new EntryChangeNotificationControl(
PersistentSearchChangeType.MODIFY_DN, oldDN, changeNumber));
}
// Send the entry and see if we should continue processing. If
// not, then deregister this persistent search.
try
{
if (!searchOperation.returnEntry(entry, entryControls))
{
cancel();
searchOperation.sendSearchResultDone();
}
}
catch (Exception e)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
cancel();
try
{
searchOperation.sendSearchResultDone();
}
catch (Exception e2)
{
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e2);
}
}
}
}
/**
* Registers a cancellation callback with this persistent search.
* The cancellation callback will be notified when this persistent
* search has been cancelled.
*
* @param callback
* The cancellation callback.
*/
public void registerCancellationCallback(CancellationCallback callback)
{
cancellationCallbacks.add(callback);
}
/**
* Enable this persistent search. The persistent search will be
* registered with the client connection and will be prevented from
* sending responses to the client.
*/
public void enable()
{
searchOperation.getClientConnection().registerPersistentSearch(this);
searchOperation.setSendResponse(false);
//Register itself with the Core.
DirectoryServer.registerPersistentSearch();
}
/**
* Retrieves a string representation of this persistent search.
*
* @return A string representation of this persistent search.
*/
@Override
public String toString()
{
StringBuilder buffer = new StringBuilder();
toString(buffer);
return buffer.toString();
}
/**
* Appends a string representation of this persistent search to the
* provided buffer.
*
* @param buffer
* The buffer to which the information should be appended.
*/
public void toString(StringBuilder buffer)
{
buffer.append("PersistentSearch(connID=");
buffer.append(searchOperation.getConnectionID());
buffer.append(",opID=");
buffer.append(searchOperation.getOperationID());
buffer.append(",baseDN=\"");
searchOperation.getBaseDN().toString(buffer);
buffer.append("\",scope=");
buffer.append(scope.toString());
buffer.append(",filter=\"");
filter.toString(buffer);
buffer.append("\")");
}
}