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.ArrayList; 041 042import com.unboundid.asn1.ASN1Constants; 043import com.unboundid.asn1.ASN1Element; 044import com.unboundid.asn1.ASN1Enumerated; 045import com.unboundid.asn1.ASN1Exception; 046import com.unboundid.asn1.ASN1Long; 047import com.unboundid.asn1.ASN1OctetString; 048import com.unboundid.asn1.ASN1Sequence; 049import com.unboundid.ldap.sdk.Control; 050import com.unboundid.ldap.sdk.DecodeableControl; 051import com.unboundid.ldap.sdk.LDAPException; 052import com.unboundid.ldap.sdk.ResultCode; 053import com.unboundid.ldap.sdk.SearchResultEntry; 054import com.unboundid.util.Debug; 055import com.unboundid.util.NotMutable; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.Nullable; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061import com.unboundid.util.Validator; 062 063import static com.unboundid.ldap.sdk.controls.ControlMessages.*; 064 065 066 067/** 068 * This class provides an implementation of the entry change notification 069 * control as defined in draft-ietf-ldapext-psearch. It will be returned in 070 * search result entries that match the criteria associated with a persistent 071 * search (see the {@link PersistentSearchRequestControl} class) and have been 072 * changed in a way associated with the registered change types for that search. 073 * <BR><BR> 074 * The information that can be included in an entry change notification control 075 * includes: 076 * <UL> 077 * <LI>A change type, which indicates the type of operation that was performed 078 * to trigger this entry change notification control. It will be one of 079 * the values of the {@link PersistentSearchChangeType} enum.</LI> 080 * <LI>An optional previous DN, which indicates the DN that the entry had 081 * before the associated operation was processed. It will only be present 082 * if the associated operation was a modify DN operation.</LI> 083 * <LI>An optional change number, which may be used to retrieve additional 084 * information about the associated operation from the server. This may 085 * not be available in all directory server implementations.</LI> 086 * </UL> 087 * Note that the entry change notification control should only be included in 088 * search result entries that are associated with a search request that included 089 * the persistent search request control, and only if that persistent search 090 * request control had the {@code returnECs} flag set to {@code true} to 091 * indicate that entry change notification controls should be included in 092 * resulting entries. Further, the entry change notification control will only 093 * be included in entries that are returned as the result of a change in the 094 * server and not any of the preliminary entries that may be returned if the 095 * corresponding persistent search request had the {@code changesOnly} flag set 096 * to {@code false}. 097 */ 098@NotMutable() 099@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 100public final class EntryChangeNotificationControl 101 extends Control 102 implements DecodeableControl 103{ 104 /** 105 * The OID (2.16.840.1.113730.3.4.7) for the entry change notification 106 * control. 107 */ 108 @NotNull public static final String ENTRY_CHANGE_NOTIFICATION_OID = 109 "2.16.840.1.113730.3.4.7"; 110 111 112 113 /** 114 * The serial version UID for this serializable class. 115 */ 116 private static final long serialVersionUID = -1305357948140939303L; 117 118 119 120 // The change number for the change, if available. 121 private final long changeNumber; 122 123 // The change type for the change. 124 @NotNull private final PersistentSearchChangeType changeType; 125 126 // The previous DN of the entry, if applicable. 127 @Nullable private final String previousDN; 128 129 130 131 /** 132 * Creates a new empty control instance that is intended to be used only for 133 * decoding controls via the {@code DecodeableControl} interface. 134 */ 135 EntryChangeNotificationControl() 136 { 137 changeNumber = -1; 138 changeType = null; 139 previousDN = null; 140 } 141 142 143 144 /** 145 * Creates a new entry change notification control with the provided 146 * information. It will not be critical. 147 * 148 * @param changeType The change type for the change. It must not be 149 * {@code null}. 150 * @param previousDN The previous DN of the entry, if applicable. 151 * @param changeNumber The change number to include in this control, or 152 * -1 if there should not be a change number. 153 */ 154 public EntryChangeNotificationControl( 155 @NotNull final PersistentSearchChangeType changeType, 156 @Nullable final String previousDN, final long changeNumber) 157 { 158 this(changeType, previousDN, changeNumber, false); 159 } 160 161 162 163 /** 164 * Creates a new entry change notification control with the provided 165 * information. 166 * 167 * @param changeType The change type for the change. It must not be 168 * {@code null}. 169 * @param previousDN The previous DN of the entry, if applicable. 170 * @param changeNumber The change number to include in this control, or 171 * -1 if there should not be a change number. 172 * @param isCritical Indicates whether this control should be marked 173 * critical. Response controls should generally not be 174 * critical. 175 */ 176 public EntryChangeNotificationControl( 177 @NotNull final PersistentSearchChangeType changeType, 178 @Nullable final String previousDN, final long changeNumber, 179 final boolean isCritical) 180 { 181 super(ENTRY_CHANGE_NOTIFICATION_OID, isCritical, 182 encodeValue(changeType, previousDN, changeNumber)); 183 184 this.changeType = changeType; 185 this.previousDN = previousDN; 186 this.changeNumber = changeNumber; 187 } 188 189 190 191 /** 192 * Creates a new entry change notification control with the provided 193 * information. 194 * 195 * @param oid The OID for the control. 196 * @param isCritical Indicates whether the control should be marked 197 * critical. 198 * @param value The encoded value for the control. This may be 199 * {@code null} if no value was provided. 200 * 201 * @throws LDAPException If the provided control cannot be decoded as an 202 * entry change notification control. 203 */ 204 public EntryChangeNotificationControl(@NotNull final String oid, 205 final boolean isCritical, 206 @Nullable final ASN1OctetString value) 207 throws LDAPException 208 { 209 super(oid, isCritical, value); 210 211 if (value == null) 212 { 213 throw new LDAPException(ResultCode.DECODING_ERROR, 214 ERR_ECN_NO_VALUE.get()); 215 } 216 217 final ASN1Sequence ecnSequence; 218 try 219 { 220 final ASN1Element element = ASN1Element.decode(value.getValue()); 221 ecnSequence = ASN1Sequence.decodeAsSequence(element); 222 } 223 catch (final ASN1Exception ae) 224 { 225 Debug.debugException(ae); 226 throw new LDAPException(ResultCode.DECODING_ERROR, 227 ERR_ECN_VALUE_NOT_SEQUENCE.get(ae), ae); 228 } 229 230 final ASN1Element[] ecnElements = ecnSequence.elements(); 231 if ((ecnElements.length < 1) || (ecnElements.length > 3)) 232 { 233 throw new LDAPException(ResultCode.DECODING_ERROR, 234 ERR_ECN_INVALID_ELEMENT_COUNT.get( 235 ecnElements.length)); 236 } 237 238 final ASN1Enumerated ecnEnumerated; 239 try 240 { 241 ecnEnumerated = ASN1Enumerated.decodeAsEnumerated(ecnElements[0]); 242 } 243 catch (final ASN1Exception ae) 244 { 245 Debug.debugException(ae); 246 throw new LDAPException(ResultCode.DECODING_ERROR, 247 ERR_ECN_FIRST_NOT_ENUMERATED.get(ae), ae); 248 } 249 250 changeType = PersistentSearchChangeType.valueOf(ecnEnumerated.intValue()); 251 if (changeType == null) 252 { 253 throw new LDAPException(ResultCode.DECODING_ERROR, 254 ERR_ECN_INVALID_CHANGE_TYPE.get( 255 ecnEnumerated.intValue())); 256 } 257 258 259 String prevDN = null; 260 long chgNum = -1; 261 for (int i=1; i < ecnElements.length; i++) 262 { 263 switch (ecnElements[i].getType()) 264 { 265 case ASN1Constants.UNIVERSAL_OCTET_STRING_TYPE: 266 prevDN = ASN1OctetString.decodeAsOctetString( 267 ecnElements[i]).stringValue(); 268 break; 269 270 case ASN1Constants.UNIVERSAL_INTEGER_TYPE: 271 try 272 { 273 chgNum = ASN1Long.decodeAsLong(ecnElements[i]).longValue(); 274 } 275 catch (final ASN1Exception ae) 276 { 277 Debug.debugException(ae); 278 throw new LDAPException(ResultCode.DECODING_ERROR, 279 ERR_ECN_CANNOT_DECODE_CHANGE_NUMBER.get(ae), ae); 280 } 281 break; 282 283 default: 284 throw new LDAPException(ResultCode.DECODING_ERROR, 285 ERR_ECN_INVALID_ELEMENT_TYPE.get( 286 StaticUtils.toHex(ecnElements[i].getType()))); 287 } 288 } 289 290 previousDN = prevDN; 291 changeNumber = chgNum; 292 } 293 294 295 296 /** 297 * {@inheritDoc} 298 */ 299 @Override() 300 @NotNull() 301 public EntryChangeNotificationControl 302 decodeControl(@NotNull final String oid, final boolean isCritical, 303 @Nullable final ASN1OctetString value) 304 throws LDAPException 305 { 306 return new EntryChangeNotificationControl(oid, isCritical, value); 307 } 308 309 310 311 /** 312 * Extracts an entry change notification control from the provided search 313 * result entry. 314 * 315 * @param entry The search result entry from which to retrieve the entry 316 * change notification control. 317 * 318 * @return The entry change notification control contained in the provided 319 * search result entry, or {@code null} if the entry did not contain 320 * an entry change notification control. 321 * 322 * @throws LDAPException If a problem is encountered while attempting to 323 * decode the entry change notification control 324 * contained in the provided entry. 325 */ 326 @Nullable() 327 public static EntryChangeNotificationControl get( 328 @NotNull final SearchResultEntry entry) 329 throws LDAPException 330 { 331 final Control c = entry.getControl(ENTRY_CHANGE_NOTIFICATION_OID); 332 if (c == null) 333 { 334 return null; 335 } 336 337 if (c instanceof EntryChangeNotificationControl) 338 { 339 return (EntryChangeNotificationControl) c; 340 } 341 else 342 { 343 return new EntryChangeNotificationControl(c.getOID(), c.isCritical(), 344 c.getValue()); 345 } 346 } 347 348 349 350 /** 351 * Encodes the provided information into an octet string that can be used as 352 * the value for this control. 353 * 354 * @param changeType The change type for the change. It must not be 355 * {@code null}. 356 * @param previousDN The previous DN of the entry, if applicable. 357 * @param changeNumber The change number to include in this control, or 358 * -1 if there should not be a change number. 359 * 360 * @return An ASN.1 octet string that can be used as the value for this 361 * control. 362 */ 363 @NotNull() 364 private static ASN1OctetString encodeValue( 365 @NotNull final PersistentSearchChangeType changeType, 366 @Nullable final String previousDN, final long changeNumber) 367 { 368 Validator.ensureNotNull(changeType); 369 370 final ArrayList<ASN1Element> elementList = new ArrayList<>(3); 371 elementList.add(new ASN1Enumerated(changeType.intValue())); 372 373 if (previousDN != null) 374 { 375 elementList.add(new ASN1OctetString(previousDN)); 376 } 377 378 if (changeNumber > 0) 379 { 380 elementList.add(new ASN1Long(changeNumber)); 381 } 382 383 return new ASN1OctetString(new ASN1Sequence(elementList).encode()); 384 } 385 386 387 388 /** 389 * Retrieves the change type for this entry change notification control. 390 * 391 * @return The change type for this entry change notification control. 392 */ 393 @NotNull() 394 public PersistentSearchChangeType getChangeType() 395 { 396 return changeType; 397 } 398 399 400 401 /** 402 * Retrieves the previous DN for the entry, if applicable. 403 * 404 * @return The previous DN for the entry, or {@code null} if there is none. 405 */ 406 @Nullable() 407 public String getPreviousDN() 408 { 409 return previousDN; 410 } 411 412 413 414 /** 415 * Retrieves the change number for the associated change, if available. 416 * 417 * @return The change number for the associated change, or -1 if none was 418 * provided. 419 */ 420 public long getChangeNumber() 421 { 422 return changeNumber; 423 } 424 425 426 427 /** 428 * {@inheritDoc} 429 */ 430 @Override() 431 @NotNull() 432 public String getControlName() 433 { 434 return INFO_CONTROL_NAME_ENTRY_CHANGE_NOTIFICATION.get(); 435 } 436 437 438 439 /** 440 * {@inheritDoc} 441 */ 442 @Override() 443 public void toString(@NotNull final StringBuilder buffer) 444 { 445 buffer.append("EntryChangeNotificationControl(changeType="); 446 buffer.append(changeType.getName()); 447 448 if (previousDN != null) 449 { 450 buffer.append(", previousDN='"); 451 buffer.append(previousDN); 452 buffer.append('\''); 453 } 454 455 if (changeNumber > 0) 456 { 457 buffer.append(", changeNumber="); 458 buffer.append(changeNumber); 459 } 460 461 buffer.append(", isCritical="); 462 buffer.append(isCritical()); 463 buffer.append(')'); 464 } 465}