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}