/* * 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 2014-2015 ForgeRock AS */ package org.opends.server.core; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.controls.EntryChangeNotificationControl; import org.opends.server.controls.PersistentSearchChangeType; import org.opends.server.types.CancelResult; 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 static org.opends.server.controls.PersistentSearchChangeType.*; /** * 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); } private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** 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); DirectoryServer.deregisterPersistentSearch(); // Notify any cancellation callbacks. for (CancellationCallback callback : psearch.cancellationCallbacks) { try { callback.persistentSearchCancelled(psearch); } catch (Exception e) { logger.traceException(e); } } } } /** Cancellation callbacks which should be run when this persistent search is cancelled. */ private final List<CancellationCallback> cancellationCallbacks = new CopyOnWriteArrayList<>(); /** The set of change types to send to the client. */ private final Set<PersistentSearchChangeType> changeTypes; /** Indicates whether or not this persistent search has already been aborted. */ private boolean isCancelled; /** * Indicates whether entries returned should include the entry change * notification control. */ private final boolean returnECs; /** The reference to the associated search operation. */ private final SearchOperation searchOperation; /** * Indicates whether to only return entries that have been updated since the * beginning of the search. */ private final boolean changesOnly; /** * 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 changesOnly * whether to only return entries that have been updated since the * beginning of the search * @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 changesOnly, boolean returnECs) { this.searchOperation = searchOperation; this.changeTypes = changeTypes; this.changesOnly = changesOnly; this.returnECs = returnECs; } /** * 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.CANCELLED, 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; } /** * Returns whether only entries updated after the beginning of this persistent * search should be returned. * * @return true if only entries updated after the beginning of this search * should be returned, false otherwise */ public boolean isChangesOnly() { return changesOnly; } /** * Notifies the persistent searches that an entry has been added. * * @param entry * The entry that was added. */ public void processAdd(Entry entry) { if (changeTypes.contains(ADD) && isInScope(entry.getName()) && matchesFilter(entry)) { sendEntry(entry, createControls(ADD, null)); } } private boolean isInScope(final DN dn) { final DN baseDN = searchOperation.getBaseDN(); switch (searchOperation.getScope().asEnum()) { case BASE_OBJECT: return baseDN.equals(dn); case SINGLE_LEVEL: return baseDN.equals(dn.getParentDNInSuffix()); case WHOLE_SUBTREE: return baseDN.isAncestorOf(dn); case SUBORDINATES: return !baseDN.equals(dn) && baseDN.isAncestorOf(dn); default: return false; } } private boolean matchesFilter(Entry entry) { try { final boolean filterMatchesEntry = searchOperation.getFilter().matchesEntry(entry); if (logger.isTraceEnabled()) { logger.trace(this + " " + entry + " filter=" + filterMatchesEntry); } return filterMatchesEntry; } catch (DirectoryException de) { logger.traceException(de); // FIXME -- Do we need to do anything here? return false; } } /** * Notifies the persistent searches that an entry has been deleted. * * @param entry * The entry that was deleted. */ public void processDelete(Entry entry) { if (changeTypes.contains(DELETE) && isInScope(entry.getName()) && matchesFilter(entry)) { sendEntry(entry, createControls(DELETE, null)); } } /** * Notifies the persistent searches that an entry has been modified. * * @param entry * The entry after it was modified. */ public void processModify(Entry entry) { processModify(entry, entry); } /** * Notifies persistent searches that an entry has been modified. * * @param entry * The entry after it was modified. * @param oldEntry * The entry before it was modified. */ public void processModify(Entry entry, Entry oldEntry) { if (changeTypes.contains(MODIFY) && isInScopeForModify(oldEntry.getName()) && anyMatchesFilter(entry, oldEntry)) { sendEntry(entry, createControls(MODIFY, null)); } } private boolean isInScopeForModify(final DN dn) { final DN baseDN = searchOperation.getBaseDN(); switch (searchOperation.getScope().asEnum()) { case BASE_OBJECT: return baseDN.equals(dn); case SINGLE_LEVEL: return baseDN.equals(dn.parent()); case WHOLE_SUBTREE: return baseDN.isAncestorOf(dn); case SUBORDINATES: return !baseDN.equals(dn) && baseDN.isAncestorOf(dn); default: return false; } } private boolean anyMatchesFilter(Entry entry, Entry oldEntry) { return matchesFilter(oldEntry) || matchesFilter(entry); } /** * Notifies the persistent searches that an entry has been renamed. * * @param entry * The entry after it was modified. * @param oldDN * The DN of the entry before it was renamed. */ public void processModifyDN(Entry entry, DN oldDN) { if (changeTypes.contains(MODIFY_DN) && isAnyInScopeForModify(entry, oldDN) && matchesFilter(entry)) { sendEntry(entry, createControls(MODIFY_DN, oldDN)); } } private boolean isAnyInScopeForModify(Entry entry, DN oldDN) { return isInScopeForModify(oldDN) || isInScopeForModify(entry.getName()); } /** * The entry is one that should be sent to the client. See if we also need to * construct an entry change notification control. */ private List<Control> createControls(PersistentSearchChangeType changeType, DN previousDN) { if (returnECs) { final Control c = previousDN != null ? new EntryChangeNotificationControl(changeType, previousDN, -1) : new EntryChangeNotificationControl(changeType, -1); return Collections.singletonList(c); } return Collections.emptyList(); } private void sendEntry(Entry entry, List<Control> entryControls) { try { if (!searchOperation.returnEntry(entry, entryControls)) { cancel(); searchOperation.sendSearchResultDone(); } } catch (Exception e) { logger.traceException(e); cancel(); try { searchOperation.sendSearchResultDone(); } catch (Exception e2) { logger.traceException(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(searchOperation.getScope()); buffer.append(",filter=\""); searchOperation.getFilter().toString(buffer); buffer.append("\")"); } }