001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.Set;
022import java.util.concurrent.CopyOnWriteArrayList;
023
024import org.forgerock.i18n.slf4j.LocalizedLogger;
025import org.forgerock.opendj.ldap.ResultCode;
026import org.opends.server.controls.EntryChangeNotificationControl;
027import org.opends.server.controls.PersistentSearchChangeType;
028import org.opends.server.types.CancelResult;
029import org.opends.server.types.Control;
030import org.forgerock.opendj.ldap.DN;
031import org.opends.server.types.DirectoryException;
032import org.opends.server.types.Entry;
033
034import static org.opends.server.controls.PersistentSearchChangeType.*;
035
036/**
037 * This class defines a data structure that will be used to hold the
038 * information necessary for processing a persistent search.
039 * <p>
040 * Work flow element implementations are responsible for managing the
041 * persistent searches that they are currently handling.
042 * <p>
043 * Typically, a work flow element search operation will first decode
044 * the persistent search control and construct a new {@code
045 * PersistentSearch}.
046 * <p>
047 * Once the initial search result set has been returned and no errors
048 * encountered, the work flow element implementation should register a
049 * cancellation callback which will be invoked when the persistent
050 * search is cancelled. This is achieved using
051 * {@link #registerCancellationCallback(CancellationCallback)}. The
052 * callback should make sure that any resources associated with the
053 * {@code PersistentSearch} are released. This may included removing
054 * the {@code PersistentSearch} from a list, or abandoning a
055 * persistent search operation that has been sent to a remote server.
056 * <p>
057 * Finally, the {@code PersistentSearch} should be enabled using
058 * {@link #enable()}. This method will register the {@code
059 * PersistentSearch} with the client connection and notify the
060 * underlying search operation that no result should be sent to the
061 * client.
062 * <p>
063 * Work flow element implementations should {@link #cancel()} active
064 * persistent searches when the work flow element fails or is shut
065 * down.
066 */
067public final class PersistentSearch
068{
069  /**
070   * A cancellation call-back which can be used by work-flow element
071   * implementations in order to register for resource cleanup when a
072   * persistent search is cancelled.
073   */
074  public static interface CancellationCallback
075  {
076    /**
077     * The provided persistent search has been cancelled. Any
078     * resources associated with the persistent search should be
079     * released.
080     *
081     * @param psearch
082     *          The persistent search which has just been cancelled.
083     */
084    void persistentSearchCancelled(PersistentSearch psearch);
085  }
086  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
087
088  /** Cancel a persistent search. */
089  private static synchronized void cancel(PersistentSearch psearch)
090  {
091    if (!psearch.isCancelled)
092    {
093      psearch.isCancelled = true;
094
095      // The persistent search can no longer be cancelled.
096      psearch.searchOperation.getClientConnection().deregisterPersistentSearch(psearch);
097
098      DirectoryServer.deregisterPersistentSearch();
099
100      // Notify any cancellation callbacks.
101      for (CancellationCallback callback : psearch.cancellationCallbacks)
102      {
103        try
104        {
105          callback.persistentSearchCancelled(psearch);
106        }
107        catch (Exception e)
108        {
109          logger.traceException(e);
110        }
111      }
112    }
113  }
114
115  /** Cancellation callbacks which should be run when this persistent search is cancelled. */
116  private final List<CancellationCallback> cancellationCallbacks = new CopyOnWriteArrayList<>();
117
118  /** The set of change types to send to the client. */
119  private final Set<PersistentSearchChangeType> changeTypes;
120
121  /** Indicates whether this persistent search has already been aborted. */
122  private boolean isCancelled;
123
124  /** Indicates whether entries returned should include the entry change notification control. */
125  private final boolean returnECs;
126
127  /** The reference to the associated search operation. */
128  private final SearchOperation searchOperation;
129
130  /**
131   * Indicates whether to only return entries that have been updated since the
132   * beginning of the search.
133   */
134  private final boolean changesOnly;
135
136  /**
137   * Creates a new persistent search object with the provided information.
138   *
139   * @param searchOperation
140   *          The search operation for this persistent search.
141   * @param changeTypes
142   *          The change types for which changes should be examined.
143   * @param changesOnly
144   *          whether to only return entries that have been updated since the
145   *          beginning of the search
146   * @param returnECs
147   *          Indicates whether to include entry change notification controls in
148   *          search result entries sent to the client.
149   */
150  public PersistentSearch(SearchOperation searchOperation,
151      Set<PersistentSearchChangeType> changeTypes, boolean changesOnly,
152      boolean returnECs)
153  {
154    this.searchOperation = searchOperation;
155    this.changeTypes = changeTypes;
156    this.changesOnly = changesOnly;
157    this.returnECs = returnECs;
158  }
159
160  /**
161   * Cancels this persistent search operation. On exit this persistent
162   * search will no longer be valid and any resources associated with
163   * it will have been released. In addition, any other persistent
164   * searches that are associated with this persistent search will
165   * also be canceled.
166   *
167   * @return The result of the cancellation.
168   */
169  public synchronized CancelResult cancel()
170  {
171    if (!isCancelled)
172    {
173      // Cancel this persistent search.
174      cancel(this);
175
176      // Cancel any other persistent searches which are associated
177      // with this one. For example, a persistent search may be
178      // distributed across multiple proxies.
179      for (PersistentSearch psearch : searchOperation.getClientConnection()
180          .getPersistentSearches())
181      {
182        if (psearch.getMessageID() == getMessageID())
183        {
184          cancel(psearch);
185        }
186      }
187    }
188
189    return new CancelResult(ResultCode.CANCELLED, null);
190  }
191
192  /**
193   * Gets the message ID associated with this persistent search.
194   *
195   * @return The message ID associated with this persistent search.
196   */
197  public int getMessageID()
198  {
199    return searchOperation.getMessageID();
200  }
201
202  /**
203   * Get the search operation associated with this persistent search.
204   *
205   * @return The search operation associated with this persistent search.
206   */
207  public SearchOperation getSearchOperation()
208  {
209    return searchOperation;
210  }
211
212  /**
213   * Returns whether only entries updated after the beginning of this persistent
214   * search should be returned.
215   *
216   * @return true if only entries updated after the beginning of this search
217   *         should be returned, false otherwise
218   */
219  public boolean isChangesOnly()
220  {
221    return changesOnly;
222  }
223
224  /**
225   * Notifies the persistent searches that an entry has been added.
226   *
227   * @param entry
228   *          The entry that was added.
229   */
230  public void processAdd(Entry entry)
231  {
232    if (changeTypes.contains(ADD)
233        && isInScope(entry.getName())
234        && matchesFilter(entry))
235    {
236      sendEntry(entry, createControls(ADD, null));
237    }
238  }
239
240  private boolean isInScope(final DN dn)
241  {
242    final DN baseDN = searchOperation.getBaseDN();
243    switch (searchOperation.getScope().asEnum())
244    {
245    case BASE_OBJECT:
246      return baseDN.equals(dn);
247    case SINGLE_LEVEL:
248      return baseDN.equals(DirectoryServer.getParentDNInSuffix(dn));
249    case WHOLE_SUBTREE:
250      return baseDN.isSuperiorOrEqualTo(dn);
251    case SUBORDINATES:
252      return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn);
253    default:
254      return false;
255    }
256  }
257
258  private boolean matchesFilter(Entry entry)
259  {
260    try
261    {
262      final boolean filterMatchesEntry = searchOperation.getFilter().matchesEntry(entry);
263      if (logger.isTraceEnabled())
264      {
265        logger.trace(this + " " + entry + " filter=" + filterMatchesEntry);
266      }
267      return filterMatchesEntry;
268    }
269    catch (DirectoryException de)
270    {
271      logger.traceException(de);
272
273      // FIXME -- Do we need to do anything here?
274      return false;
275    }
276  }
277
278  /**
279   * Notifies the persistent searches that an entry has been deleted.
280   *
281   * @param entry
282   *          The entry that was deleted.
283   */
284  public void processDelete(Entry entry)
285  {
286    if (changeTypes.contains(DELETE)
287        && isInScope(entry.getName())
288        && matchesFilter(entry))
289    {
290      sendEntry(entry, createControls(DELETE, null));
291    }
292  }
293
294  /**
295   * Notifies the persistent searches that an entry has been modified.
296   *
297   * @param entry
298   *          The entry after it was modified.
299   */
300  public void processModify(Entry entry)
301  {
302    processModify(entry, entry);
303  }
304
305  /**
306   * Notifies persistent searches that an entry has been modified.
307   *
308   * @param entry
309   *          The entry after it was modified.
310   * @param oldEntry
311   *          The entry before it was modified.
312   */
313  public void processModify(Entry entry, Entry oldEntry)
314  {
315    if (changeTypes.contains(MODIFY)
316        && isInScopeForModify(oldEntry.getName())
317        && anyMatchesFilter(entry, oldEntry))
318    {
319      sendEntry(entry, createControls(MODIFY, null));
320    }
321  }
322
323  private boolean isInScopeForModify(final DN dn)
324  {
325    final DN baseDN = searchOperation.getBaseDN();
326    switch (searchOperation.getScope().asEnum())
327    {
328    case BASE_OBJECT:
329      return baseDN.equals(dn);
330    case SINGLE_LEVEL:
331      return baseDN.equals(dn.parent());
332    case WHOLE_SUBTREE:
333      return baseDN.isSuperiorOrEqualTo(dn);
334    case SUBORDINATES:
335      return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn);
336    default:
337      return false;
338    }
339  }
340
341  private boolean anyMatchesFilter(Entry entry, Entry oldEntry)
342  {
343    return matchesFilter(oldEntry) || matchesFilter(entry);
344  }
345
346  /**
347   * Notifies the persistent searches that an entry has been renamed.
348   *
349   * @param entry
350   *          The entry after it was modified.
351   * @param oldDN
352   *          The DN of the entry before it was renamed.
353   */
354  public void processModifyDN(Entry entry, DN oldDN)
355  {
356    if (changeTypes.contains(MODIFY_DN)
357        && isAnyInScopeForModify(entry, oldDN)
358        && matchesFilter(entry))
359    {
360      sendEntry(entry, createControls(MODIFY_DN, oldDN));
361    }
362  }
363
364  private boolean isAnyInScopeForModify(Entry entry, DN oldDN)
365  {
366    return isInScopeForModify(oldDN) || isInScopeForModify(entry.getName());
367  }
368
369  /**
370   * The entry is one that should be sent to the client. See if we also need to
371   * construct an entry change notification control.
372   */
373  private List<Control> createControls(PersistentSearchChangeType changeType,
374      DN previousDN)
375  {
376    if (returnECs)
377    {
378      final Control c = previousDN != null
379          ? new EntryChangeNotificationControl(changeType, previousDN, -1)
380          : new EntryChangeNotificationControl(changeType, -1);
381      return Collections.singletonList(c);
382    }
383    return Collections.emptyList();
384  }
385
386  private void sendEntry(Entry entry, List<Control> entryControls)
387  {
388    try
389    {
390      if (!searchOperation.returnEntry(entry, entryControls))
391      {
392        cancel();
393        searchOperation.sendSearchResultDone();
394      }
395    }
396    catch (Exception e)
397    {
398      logger.traceException(e);
399
400      cancel();
401
402      try
403      {
404        searchOperation.sendSearchResultDone();
405      }
406      catch (Exception e2)
407      {
408        logger.traceException(e2);
409      }
410    }
411  }
412
413  /**
414   * Registers a cancellation callback with this persistent search.
415   * The cancellation callback will be notified when this persistent
416   * search has been cancelled.
417   *
418   * @param callback
419   *          The cancellation callback.
420   */
421  public void registerCancellationCallback(CancellationCallback callback)
422  {
423    cancellationCallbacks.add(callback);
424  }
425
426  /**
427   * Enable this persistent search. The persistent search will be
428   * registered with the client connection and will be prevented from
429   * sending responses to the client.
430   */
431  public void enable()
432  {
433    searchOperation.getClientConnection().registerPersistentSearch(this);
434    searchOperation.setSendResponse(false);
435    //Register itself with the Core.
436    DirectoryServer.registerPersistentSearch();
437  }
438
439  /**
440   * Retrieves a string representation of this persistent search.
441   *
442   * @return A string representation of this persistent search.
443   */
444  @Override
445  public String toString()
446  {
447    StringBuilder buffer = new StringBuilder();
448    toString(buffer);
449    return buffer.toString();
450  }
451
452  /**
453   * Appends a string representation of this persistent search to the
454   * provided buffer.
455   *
456   * @param buffer
457   *          The buffer to which the information should be appended.
458   */
459  public void toString(StringBuilder buffer)
460  {
461    buffer.append("PersistentSearch(connID=");
462    buffer.append(searchOperation.getConnectionID());
463    buffer.append(",opID=");
464    buffer.append(searchOperation.getOperationID());
465    buffer.append(",baseDN=\"");
466    buffer.append(searchOperation.getBaseDN());
467    buffer.append("\",scope=");
468    buffer.append(searchOperation.getScope());
469    buffer.append(",filter=\"");
470    searchOperation.getFilter().toString(buffer);
471    buffer.append("\")");
472  }
473}