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