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