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}