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  /**
071   * A cancellation call-back which can be used by work-flow element
072   * implementations in order to register for resource cleanup when a
073   * persistent search is cancelled.
074   */
075  public static interface CancellationCallback
076  {
077
078    /**
079     * The provided persistent search has been cancelled. Any
080     * resources associated with the persistent search should be
081     * released.
082     *
083     * @param psearch
084     *          The persistent search which has just been cancelled.
085     */
086    void persistentSearchCancelled(PersistentSearch psearch);
087  }
088  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
089
090
091
092  /** Cancel a persistent search. */
093  private static synchronized void cancel(PersistentSearch psearch)
094  {
095    if (!psearch.isCancelled)
096    {
097      psearch.isCancelled = true;
098
099      // The persistent search can no longer be cancelled.
100      psearch.searchOperation.getClientConnection().deregisterPersistentSearch(psearch);
101
102      DirectoryServer.deregisterPersistentSearch();
103
104      // Notify any cancellation callbacks.
105      for (CancellationCallback callback : psearch.cancellationCallbacks)
106      {
107        try
108        {
109          callback.persistentSearchCancelled(psearch);
110        }
111        catch (Exception e)
112        {
113          logger.traceException(e);
114        }
115      }
116    }
117  }
118
119  /** Cancellation callbacks which should be run when this persistent search is cancelled. */
120  private final List<CancellationCallback> cancellationCallbacks = new CopyOnWriteArrayList<>();
121
122  /** The set of change types to send to the client. */
123  private final Set<PersistentSearchChangeType> changeTypes;
124
125  /** Indicates whether or not this persistent search has already been aborted. */
126  private boolean isCancelled;
127
128  /**
129   * Indicates whether entries returned should include the entry change
130   * notification control.
131   */
132  private final boolean returnECs;
133
134  /** The reference to the associated search operation. */
135  private final SearchOperation searchOperation;
136
137  /**
138   * Indicates whether to only return entries that have been updated since the
139   * beginning of the search.
140   */
141  private final boolean changesOnly;
142
143  /**
144   * Creates a new persistent search object with the provided information.
145   *
146   * @param searchOperation
147   *          The search operation for this persistent search.
148   * @param changeTypes
149   *          The change types for which changes should be examined.
150   * @param changesOnly
151   *          whether to only return entries that have been updated since the
152   *          beginning of the search
153   * @param returnECs
154   *          Indicates whether to include entry change notification controls in
155   *          search result entries sent to the client.
156   */
157  public PersistentSearch(SearchOperation searchOperation,
158      Set<PersistentSearchChangeType> changeTypes, boolean changesOnly,
159      boolean returnECs)
160  {
161    this.searchOperation = searchOperation;
162    this.changeTypes = changeTypes;
163    this.changesOnly = changesOnly;
164    this.returnECs = returnECs;
165  }
166
167
168
169  /**
170   * Cancels this persistent search operation. On exit this persistent
171   * search will no longer be valid and any resources associated with
172   * it will have been released. In addition, any other persistent
173   * searches that are associated with this persistent search will
174   * also be canceled.
175   *
176   * @return The result of the cancellation.
177   */
178  public synchronized CancelResult cancel()
179  {
180    if (!isCancelled)
181    {
182      // Cancel this persistent search.
183      cancel(this);
184
185      // Cancel any other persistent searches which are associated
186      // with this one. For example, a persistent search may be
187      // distributed across multiple proxies.
188      for (PersistentSearch psearch : searchOperation.getClientConnection()
189          .getPersistentSearches())
190      {
191        if (psearch.getMessageID() == getMessageID())
192        {
193          cancel(psearch);
194        }
195      }
196    }
197
198    return new CancelResult(ResultCode.CANCELLED, null);
199  }
200
201
202
203  /**
204   * Gets the message ID associated with this persistent search.
205   *
206   * @return The message ID associated with this persistent search.
207   */
208  public int getMessageID()
209  {
210    return searchOperation.getMessageID();
211  }
212
213
214  /**
215   * Get the search operation associated with this persistent search.
216   *
217   * @return The search operation associated with this persistent search.
218   */
219  public SearchOperation getSearchOperation()
220  {
221    return searchOperation;
222  }
223
224  /**
225   * Returns whether only entries updated after the beginning of this persistent
226   * search should be returned.
227   *
228   * @return true if only entries updated after the beginning of this search
229   *         should be returned, false otherwise
230   */
231  public boolean isChangesOnly()
232  {
233    return changesOnly;
234  }
235
236  /**
237   * Notifies the persistent searches that an entry has been added.
238   *
239   * @param entry
240   *          The entry that was added.
241   */
242  public void processAdd(Entry entry)
243  {
244    if (changeTypes.contains(ADD)
245        && isInScope(entry.getName())
246        && matchesFilter(entry))
247    {
248      sendEntry(entry, createControls(ADD, null));
249    }
250  }
251
252  private boolean isInScope(final DN dn)
253  {
254    final DN baseDN = searchOperation.getBaseDN();
255    switch (searchOperation.getScope().asEnum())
256    {
257    case BASE_OBJECT:
258      return baseDN.equals(dn);
259    case SINGLE_LEVEL:
260      return baseDN.equals(DirectoryServer.getParentDNInSuffix(dn));
261    case WHOLE_SUBTREE:
262      return baseDN.isSuperiorOrEqualTo(dn);
263    case SUBORDINATES:
264      return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn);
265    default:
266      return false;
267    }
268  }
269
270  private boolean matchesFilter(Entry entry)
271  {
272    try
273    {
274      final boolean filterMatchesEntry = searchOperation.getFilter().matchesEntry(entry);
275      if (logger.isTraceEnabled())
276      {
277        logger.trace(this + " " + entry + " filter=" + filterMatchesEntry);
278      }
279      return filterMatchesEntry;
280    }
281    catch (DirectoryException de)
282    {
283      logger.traceException(de);
284
285      // FIXME -- Do we need to do anything here?
286      return false;
287    }
288  }
289
290  /**
291   * Notifies the persistent searches that an entry has been deleted.
292   *
293   * @param entry
294   *          The entry that was deleted.
295   */
296  public void processDelete(Entry entry)
297  {
298    if (changeTypes.contains(DELETE)
299        && isInScope(entry.getName())
300        && matchesFilter(entry))
301    {
302      sendEntry(entry, createControls(DELETE, null));
303    }
304  }
305
306
307
308  /**
309   * Notifies the persistent searches that an entry has been modified.
310   *
311   * @param entry
312   *          The entry after it was modified.
313   */
314  public void processModify(Entry entry)
315  {
316    processModify(entry, entry);
317  }
318
319
320
321  /**
322   * Notifies persistent searches that an entry has been modified.
323   *
324   * @param entry
325   *          The entry after it was modified.
326   * @param oldEntry
327   *          The entry before it was modified.
328   */
329  public void processModify(Entry entry, Entry oldEntry)
330  {
331    if (changeTypes.contains(MODIFY)
332        && isInScopeForModify(oldEntry.getName())
333        && anyMatchesFilter(entry, oldEntry))
334    {
335      sendEntry(entry, createControls(MODIFY, null));
336    }
337  }
338
339  private boolean isInScopeForModify(final DN dn)
340  {
341    final DN baseDN = searchOperation.getBaseDN();
342    switch (searchOperation.getScope().asEnum())
343    {
344    case BASE_OBJECT:
345      return baseDN.equals(dn);
346    case SINGLE_LEVEL:
347      return baseDN.equals(dn.parent());
348    case WHOLE_SUBTREE:
349      return baseDN.isSuperiorOrEqualTo(dn);
350    case SUBORDINATES:
351      return !baseDN.equals(dn) && baseDN.isSuperiorOrEqualTo(dn);
352    default:
353      return false;
354    }
355  }
356
357  private boolean anyMatchesFilter(Entry entry, Entry oldEntry)
358  {
359    return matchesFilter(oldEntry) || matchesFilter(entry);
360  }
361
362  /**
363   * Notifies the persistent searches that an entry has been renamed.
364   *
365   * @param entry
366   *          The entry after it was modified.
367   * @param oldDN
368   *          The DN of the entry before it was renamed.
369   */
370  public void processModifyDN(Entry entry, DN oldDN)
371  {
372    if (changeTypes.contains(MODIFY_DN)
373        && isAnyInScopeForModify(entry, oldDN)
374        && matchesFilter(entry))
375    {
376      sendEntry(entry, createControls(MODIFY_DN, oldDN));
377    }
378  }
379
380  private boolean isAnyInScopeForModify(Entry entry, DN oldDN)
381  {
382    return isInScopeForModify(oldDN) || isInScopeForModify(entry.getName());
383  }
384
385  /**
386   * The entry is one that should be sent to the client. See if we also need to
387   * construct an entry change notification control.
388   */
389  private List<Control> createControls(PersistentSearchChangeType changeType,
390      DN previousDN)
391  {
392    if (returnECs)
393    {
394      final Control c = previousDN != null
395          ? new EntryChangeNotificationControl(changeType, previousDN, -1)
396          : new EntryChangeNotificationControl(changeType, -1);
397      return Collections.singletonList(c);
398    }
399    return Collections.emptyList();
400  }
401
402  private void sendEntry(Entry entry, List<Control> entryControls)
403  {
404    try
405    {
406      if (!searchOperation.returnEntry(entry, entryControls))
407      {
408        cancel();
409        searchOperation.sendSearchResultDone();
410      }
411    }
412    catch (Exception e)
413    {
414      logger.traceException(e);
415
416      cancel();
417
418      try
419      {
420        searchOperation.sendSearchResultDone();
421      }
422      catch (Exception e2)
423      {
424        logger.traceException(e2);
425      }
426    }
427  }
428
429
430
431  /**
432   * Registers a cancellation callback with this persistent search.
433   * The cancellation callback will be notified when this persistent
434   * search has been cancelled.
435   *
436   * @param callback
437   *          The cancellation callback.
438   */
439  public void registerCancellationCallback(CancellationCallback callback)
440  {
441    cancellationCallbacks.add(callback);
442  }
443
444
445
446  /**
447   * Enable this persistent search. The persistent search will be
448   * registered with the client connection and will be prevented from
449   * sending responses to the client.
450   */
451  public void enable()
452  {
453    searchOperation.getClientConnection().registerPersistentSearch(this);
454    searchOperation.setSendResponse(false);
455    //Register itself with the Core.
456    DirectoryServer.registerPersistentSearch();
457  }
458
459
460
461  /**
462   * Retrieves a string representation of this persistent search.
463   *
464   * @return A string representation of this persistent search.
465   */
466  @Override
467  public String toString()
468  {
469    StringBuilder buffer = new StringBuilder();
470    toString(buffer);
471    return buffer.toString();
472  }
473
474
475
476  /**
477   * Appends a string representation of this persistent search to the
478   * provided buffer.
479   *
480   * @param buffer
481   *          The buffer to which the information should be appended.
482   */
483  public void toString(StringBuilder buffer)
484  {
485    buffer.append("PersistentSearch(connID=");
486    buffer.append(searchOperation.getConnectionID());
487    buffer.append(",opID=");
488    buffer.append(searchOperation.getOperationID());
489    buffer.append(",baseDN=\"");
490    buffer.append(searchOperation.getBaseDN());
491    buffer.append("\",scope=");
492    buffer.append(searchOperation.getScope());
493    buffer.append(",filter=\"");
494    searchOperation.getFilter().toString(buffer);
495    buffer.append("\")");
496  }
497}