001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.controls;
037
038
039
040import java.util.EnumSet;
041import java.util.Iterator;
042import java.util.Set;
043
044import com.unboundid.asn1.ASN1Boolean;
045import com.unboundid.asn1.ASN1Element;
046import com.unboundid.asn1.ASN1Integer;
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.asn1.ASN1Sequence;
049import com.unboundid.ldap.sdk.Control;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.util.Debug;
053import com.unboundid.util.NotMutable;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.ThreadSafety;
056import com.unboundid.util.ThreadSafetyLevel;
057import com.unboundid.util.Validator;
058
059import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
060
061
062
063/**
064 * This class provides an implementation of the persistent search request
065 * control as defined in draft-ietf-ldapext-psearch.  It may be included in a
066 * search request to request notification for changes to entries that match the
067 * associated set of search criteria.  It can provide a basic mechanism for
068 * clients to request to be notified whenever entries matching the associated
069 * search criteria are altered.
070 * <BR><BR>
071 * A persistent search request control may include the following elements:
072 * <UL>
073 *   <LI>{@code changeTypes} -- Specifies the set of change types for which to
074 *       receive notification.  This may be any combination of one or more of
075 *       the {@link PersistentSearchChangeType} values.</LI>
076 *   <LI>{@code changesOnly} -- Indicates whether to only return updated entries
077 *       that match the associated search criteria.  If this is {@code false},
078 *       then the server will first return all existing entries in the server
079 *       that match the search criteria, and will then begin returning entries
080 *       that are updated in an operation associated with one of the
081 *       registered {@code changeTypes}.  If this is {@code true}, then the
082 *       server will not return all matching entries that already exist in the
083 *       server but will only return entries in response to changes that
084 *       occur.</LI>
085 *   <LI>{@code returnECs} -- Indicates whether search result entries returned
086 *       as a result of a change to the directory data should include the
087 *       {@link EntryChangeNotificationControl} to provide information about
088 *       the type of operation that occurred.  If {@code changesOnly} is
089 *       {@code false}, then entry change notification controls will not be
090 *       included in existing entries that match the search criteria, but only
091 *       in entries that are updated by an operation with one of the registered
092 *       {@code changeTypes}.</LI>
093 * </UL>
094 * Note that when an entry is returned in response to a persistent search
095 * request, the content of the entry that is returned will reflect the updated
096 * entry in the server (except in the case of a delete operation, in which case
097 * it will be the entry as it appeared before it was removed).  Other than the
098 * information included in the entry change notification control, the search
099 * result entry will not contain any information about what actually changed in
100 * the entry.
101 * <BR><BR>
102 * Many servers do not enforce time limit or size limit restrictions on the
103 * persistent search control, and because there is no defined "end" to the
104 * search, it may remain active until the client abandons or cancels the search
105 * or until the connection is closed.  Because of this, it is strongly
106 * recommended that clients only use the persistent search request control in
107 * conjunction with asynchronous search operations invoked using the
108 * {@link com.unboundid.ldap.sdk.LDAPConnection#asyncSearch} method.
109 * <BR><BR>
110 * <H2>Example</H2>
111 * The following example demonstrates the process for beginning an asynchronous
112 * search that includes the persistent search control in order to notify the
113 * client of all changes to entries within the "dc=example,dc=com" subtree.
114 * <PRE>
115 * SearchRequest persistentSearchRequest = new SearchRequest(
116 *      asyncSearchListener, "dc=example,dc=com", SearchScope.SUB,
117 *      Filter.createPresenceFilter("objectClass"));
118 * persistentSearchRequest.addControl(new PersistentSearchRequestControl(
119 *      PersistentSearchChangeType.allChangeTypes(), // Notify change types.
120 *      true, // Only return new changes, don't match existing entries.
121 *      true)); // Include change notification controls in search entries.
122 *
123 * // Launch the persistent search as an asynchronous operation.
124 * AsyncRequestID persistentSearchRequestID =
125 *      connection.asyncSearch(persistentSearchRequest);
126 *
127 * // Modify an entry that matches the persistent search criteria.  This
128 * // should cause the persistent search listener to be notified.
129 * LDAPResult modifyResult = connection.modify(
130 *      "uid=test.user,ou=People,dc=example,dc=com",
131 *      new Modification(ModificationType.REPLACE, "description", "test"));
132 *
133 * // Verify that the persistent search listener was notified....
134 *
135 * // Since persistent search operations don't end on their own, we need to
136 * // abandon the search when we don't need it anymore.
137 * connection.abandon(persistentSearchRequestID);
138 * </PRE>
139 */
140@NotMutable()
141@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
142public final class PersistentSearchRequestControl
143       extends Control
144{
145  /**
146   * The OID (2.16.840.1.113730.3.4.3) for the persistent search request
147   * control.
148   */
149  @NotNull public static final String PERSISTENT_SEARCH_REQUEST_OID =
150       "2.16.840.1.113730.3.4.3";
151
152
153
154  /**
155   * The serial version UID for this serializable class.
156   */
157  private static final long serialVersionUID = 3532762682521779027L;
158
159
160
161  // Indicates whether the search should only return search result entries for
162  // changes made to entries matching the search criteria, or if existing
163  // entries already in the server should be returned as well.
164  private final boolean changesOnly;
165
166  // Indicates whether search result entries returned as part of this persistent
167  // search should include the entry change notification control.
168  private final boolean returnECs;
169
170  // The set of change types for which this persistent search control is
171  // registered.
172  @NotNull private final EnumSet<PersistentSearchChangeType> changeTypes;
173
174
175
176  /**
177   * Creates a new persistent search control with the provided information.  It
178   * will be marked critical.
179   *
180   * @param  changeType   The change type for which to register.  It must not be
181   *                      {@code null}.
182   * @param  changesOnly  Indicates whether the search should only return search
183   *                      result entries for changes made to entries matching
184   *                      the search criteria, or if existing matching entries
185   *                      in the server should be returned as well.
186   * @param  returnECs    Indicates whether the search result entries returned
187   *                      as part of this persistent search should include the
188   *                      entry change notification control.
189   */
190  public PersistentSearchRequestControl(
191              @NotNull final PersistentSearchChangeType changeType,
192              final boolean changesOnly, final boolean returnECs)
193  {
194    super(PERSISTENT_SEARCH_REQUEST_OID, true,
195          encodeValue(changeType, changesOnly, returnECs));
196
197    changeTypes = EnumSet.of(changeType);
198
199    this.changesOnly = changesOnly;
200    this.returnECs   = returnECs;
201  }
202
203
204
205  /**
206   * Creates a new persistent search control with the provided information.  It
207   * will be marked critical.
208   *
209   * @param  changeTypes  The set of change types for which to register.  It
210   *                      must not be {@code null} or empty.
211   * @param  changesOnly  Indicates whether the search should only return search
212   *                      result entries for changes made to entries matching
213   *                      the search criteria, or if existing matching entries
214   *                      in the server should be returned as well.
215   * @param  returnECs    Indicates whether the search result entries returned
216   *                      as part of this persistent search should include the
217   *                      entry change notification control.
218   */
219  public PersistentSearchRequestControl(
220              @NotNull final Set<PersistentSearchChangeType> changeTypes,
221              final boolean changesOnly, final boolean returnECs)
222  {
223    super(PERSISTENT_SEARCH_REQUEST_OID, true,
224          encodeValue(changeTypes, changesOnly, returnECs));
225
226    this.changeTypes = EnumSet.copyOf(changeTypes);
227    this.changesOnly = changesOnly;
228    this.returnECs   = returnECs;
229  }
230
231
232
233  /**
234   * Creates a new persistent search control with the provided information.
235   *
236   * @param  changeType   The change type for which to register.  It must not be
237   *                      {@code null}.
238   * @param  changesOnly  Indicates whether the search should only return search
239   *                      result entries for changes made to entries matching
240   *                      the search criteria, or if existing matching entries
241   *                      in the server should be returned as well.
242   * @param  returnECs    Indicates whether the search result entries returned
243   *                      as part of this persistent search should include the
244   *                      entry change notification control.
245   * @param  isCritical   Indicates whether the control should be marked
246   *                      critical.
247   */
248  public PersistentSearchRequestControl(
249              @NotNull final PersistentSearchChangeType changeType,
250              final boolean changesOnly, final boolean returnECs,
251              final boolean isCritical)
252  {
253    super(PERSISTENT_SEARCH_REQUEST_OID, isCritical,
254          encodeValue(changeType, changesOnly, returnECs));
255
256    changeTypes = EnumSet.of(changeType);
257
258    this.changesOnly = changesOnly;
259    this.returnECs   = returnECs;
260  }
261
262
263
264  /**
265   * Creates a new persistent search control with the provided information.
266   *
267   * @param  changeTypes  The set of change types for which to register.  It
268   *                      must not be {@code null} or empty.
269   * @param  changesOnly  Indicates whether the search should only return search
270   *                      result entries for changes made to entries matching
271   *                      the search criteria, or if existing matching entries
272   *                      in the server should be returned as well.
273   * @param  returnECs    Indicates whether the search result entries returned
274   *                      as part of this persistent search should include the
275   *                      entry change notification control.
276   * @param  isCritical   Indicates whether the control should be marked
277   *                      critical.
278   */
279  public PersistentSearchRequestControl(
280              @NotNull final Set<PersistentSearchChangeType> changeTypes,
281              final boolean changesOnly, final boolean returnECs,
282              final boolean isCritical)
283  {
284    super(PERSISTENT_SEARCH_REQUEST_OID, isCritical,
285          encodeValue(changeTypes, changesOnly, returnECs));
286
287    this.changeTypes = EnumSet.copyOf(changeTypes);
288    this.changesOnly = changesOnly;
289    this.returnECs   = returnECs;
290  }
291
292
293
294  /**
295   * Creates a new persistent search request control which is decoded from the
296   * provided generic control.
297   *
298   * @param  control  The generic control to be decoded as a persistent search
299   *                  request control.
300   *
301   * @throws  LDAPException  If the provided control cannot be decoded as a
302   *                         persistent search request control.
303   */
304  public PersistentSearchRequestControl(@NotNull final Control control)
305         throws LDAPException
306  {
307    super(control);
308
309    final ASN1OctetString value = control.getValue();
310    if (value == null)
311    {
312      throw new LDAPException(ResultCode.DECODING_ERROR,
313                              ERR_PSEARCH_NO_VALUE.get());
314    }
315
316    try
317    {
318      final ASN1Element valueElement = ASN1Element.decode(value.getValue());
319      final ASN1Element[] elements =
320           ASN1Sequence.decodeAsSequence(valueElement).elements();
321
322      changeTypes =
323           EnumSet.copyOf(PersistentSearchChangeType.decodeChangeTypes(
324                          ASN1Integer.decodeAsInteger(elements[0]).intValue()));
325      changesOnly = ASN1Boolean.decodeAsBoolean(elements[1]).booleanValue();
326      returnECs   = ASN1Boolean.decodeAsBoolean(elements[2]).booleanValue();
327    }
328    catch (final Exception e)
329    {
330      Debug.debugException(e);
331      throw new LDAPException(ResultCode.DECODING_ERROR,
332                              ERR_PSEARCH_CANNOT_DECODE.get(e), e);
333    }
334  }
335
336
337
338  /**
339   * Encodes the provided information into an octet string that can be used as
340   * the value for this control.
341   *
342   * @param  changeType   The change type for which to register.  It must not be
343   *                      {@code null}.
344   * @param  changesOnly  Indicates whether the search should only return search
345   *                      result entries for changes made to entries matching
346   *                      the search criteria, or if existing matching entries
347   *                      in the server should be returned as well.
348   * @param  returnECs    Indicates whether the search result entries returned
349   *                      as part of this persistent search should include the
350   *                      entry change notification control.
351   *
352   * @return  An ASN.1 octet string that can be used as the value for this
353   *          control.
354   */
355  @NotNull()
356  private static ASN1OctetString encodeValue(
357               @NotNull final PersistentSearchChangeType changeType,
358               final boolean changesOnly, final boolean returnECs)
359  {
360    Validator.ensureNotNull(changeType);
361
362    final ASN1Element[] elements =
363    {
364      new ASN1Integer(changeType.intValue()),
365      new ASN1Boolean(changesOnly),
366      new ASN1Boolean(returnECs)
367    };
368
369    return new ASN1OctetString(new ASN1Sequence(elements).encode());
370  }
371
372
373
374  /**
375   * Encodes the provided information into an octet string that can be used as
376   * the value for this control.
377   *
378   * @param  changeTypes  The set of change types for which to register.  It
379   *                      must not be {@code null} or empty.
380   * @param  changesOnly  Indicates whether the search should only return search
381   *                      result entries for changes made to entries matching
382   *                      the search criteria, or if existing matching entries
383   *                      in the server should be returned as well.
384   * @param  returnECs    Indicates whether the search result entries returned
385   *                      as part of this persistent search should include the
386   *                      entry change notification control.
387   *
388   * @return  An ASN.1 octet string that can be used as the value for this
389   *          control.
390   */
391  @NotNull()
392  private static ASN1OctetString encodeValue(
393               @NotNull final Set<PersistentSearchChangeType> changeTypes,
394               final boolean changesOnly, final boolean returnECs)
395  {
396    Validator.ensureNotNull(changeTypes);
397    Validator.ensureFalse(changeTypes.isEmpty(),
398         "PersistentSearchRequestControl.changeTypes must not be empty.");
399
400    final ASN1Element[] elements =
401    {
402      new ASN1Integer(
403               PersistentSearchChangeType.encodeChangeTypes(changeTypes)),
404      new ASN1Boolean(changesOnly),
405      new ASN1Boolean(returnECs)
406    };
407
408    return new ASN1OctetString(new ASN1Sequence(elements).encode());
409  }
410
411
412
413  /**
414   * Retrieves the set of change types for this persistent search request
415   * control.
416   *
417   * @return  The set of change types for this persistent search request
418   *          control.
419   */
420  @NotNull()
421  public Set<PersistentSearchChangeType> getChangeTypes()
422  {
423    return changeTypes;
424  }
425
426
427
428  /**
429   * Indicates whether the search should only return search result entries for
430   * changes made to entries matching the search criteria, or if existing
431   * matching entries should be returned as well.
432   *
433   * @return  {@code true} if the search should only return search result
434   *          entries for changes matching the search criteria, or {@code false}
435   *          if it should also return existing entries that match the search
436   *          criteria.
437   */
438  public boolean changesOnly()
439  {
440    return changesOnly;
441  }
442
443
444
445  /**
446   * Indicates whether the search result entries returned as part of this
447   * persistent search should include the entry change notification control.
448   *
449   * @return  {@code true} if search result entries returned as part of this
450   *          persistent search should include the entry change notification
451   *          control, or {@code false} if not.
452   */
453  public boolean returnECs()
454  {
455    return returnECs;
456  }
457
458
459
460  /**
461   * {@inheritDoc}
462   */
463  @Override()
464  @NotNull()
465  public String getControlName()
466  {
467    return INFO_CONTROL_NAME_PSEARCH_REQUEST.get();
468  }
469
470
471
472  /**
473   * {@inheritDoc}
474   */
475  @Override()
476  public void toString(@NotNull final StringBuilder buffer)
477  {
478    buffer.append("PersistentSearchRequestControl(changeTypes={");
479
480    final Iterator<PersistentSearchChangeType> iterator =
481         changeTypes.iterator();
482    while (iterator.hasNext())
483    {
484      buffer.append(iterator.next().getName());
485      if (iterator.hasNext())
486      {
487        buffer.append(", ");
488      }
489    }
490
491    buffer.append("}, changesOnly=");
492    buffer.append(changesOnly);
493    buffer.append(", returnECs=");
494    buffer.append(returnECs);
495    buffer.append(", isCritical=");
496    buffer.append(isCritical());
497    buffer.append(')');
498  }
499}