001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 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.NotMutable;
040import com.unboundid.util.ThreadSafety;
041import com.unboundid.util.ThreadSafetyLevel;
042
043import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
044import static com.unboundid.util.Debug.*;
045import static com.unboundid.util.StaticUtils.*;
046import static com.unboundid.util.Validator.*;
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      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      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            debugException(ae);
261            throw new LDAPException(ResultCode.DECODING_ERROR,
262                                    ERR_ECN_CANNOT_DECODE_CHANGE_NUMBER.get(ae),
263                                    ae);
264          }
265          break;
266
267        default:
268          throw new LDAPException(ResultCode.DECODING_ERROR,
269                                  ERR_ECN_INVALID_ELEMENT_TYPE.get(
270                                       toHex(ecnElements[i].getType())));
271      }
272    }
273
274    previousDN   = prevDN;
275    changeNumber = chgNum;
276  }
277
278
279
280  /**
281   * {@inheritDoc}
282   */
283  @Override()
284  public EntryChangeNotificationControl
285              decodeControl(final String oid, final boolean isCritical,
286                            final ASN1OctetString value)
287         throws LDAPException
288  {
289    return new EntryChangeNotificationControl(oid, isCritical, value);
290  }
291
292
293
294  /**
295   * Extracts an entry change notification control from the provided search
296   * result entry.
297   *
298   * @param  entry  The search result entry from which to retrieve the entry
299   *                change notification control.
300   *
301   * @return  The entry change notification control contained in the provided
302   *          search result entry, or {@code null} if the entry did not contain
303   *          an entry change notification control.
304   *
305   * @throws  LDAPException  If a problem is encountered while attempting to
306   *                         decode the entry change notification control
307   *                         contained in the provided entry.
308   */
309  public static EntryChangeNotificationControl
310                     get(final SearchResultEntry entry)
311         throws LDAPException
312  {
313    final Control c = entry.getControl(ENTRY_CHANGE_NOTIFICATION_OID);
314    if (c == null)
315    {
316      return null;
317    }
318
319    if (c instanceof EntryChangeNotificationControl)
320    {
321      return (EntryChangeNotificationControl) c;
322    }
323    else
324    {
325      return new EntryChangeNotificationControl(c.getOID(), c.isCritical(),
326           c.getValue());
327    }
328  }
329
330
331
332  /**
333   * Encodes the provided information into an octet string that can be used as
334   * the value for this control.
335   *
336   * @param  changeType    The change type for the change.  It must not be
337   *                       {@code null}.
338   * @param  previousDN    The previous DN of the entry, if applicable.
339   * @param  changeNumber  The change number to include in this control, or
340   *                       -1 if there should not be a change number.
341   *
342   * @return  An ASN.1 octet string that can be used as the value for this
343   *          control.
344   */
345  private static ASN1OctetString encodeValue(
346               final PersistentSearchChangeType changeType,
347               final String previousDN, final long changeNumber)
348  {
349    ensureNotNull(changeType);
350
351    final ArrayList<ASN1Element> elementList = new ArrayList<ASN1Element>(3);
352    elementList.add(new ASN1Enumerated(changeType.intValue()));
353
354    if (previousDN != null)
355    {
356      elementList.add(new ASN1OctetString(previousDN));
357    }
358
359    if (changeNumber > 0)
360    {
361      elementList.add(new ASN1Long(changeNumber));
362    }
363
364    return new ASN1OctetString(new ASN1Sequence(elementList).encode());
365  }
366
367
368
369  /**
370   * Retrieves the change type for this entry change notification control.
371   *
372   * @return  The change type for this entry change notification control.
373   */
374  public PersistentSearchChangeType getChangeType()
375  {
376    return changeType;
377  }
378
379
380
381  /**
382   * Retrieves the previous DN for the entry, if applicable.
383   *
384   * @return  The previous DN for the entry, or {@code null} if there is none.
385   */
386  public String getPreviousDN()
387  {
388    return previousDN;
389  }
390
391
392
393  /**
394   * Retrieves the change number for the associated change, if available.
395   *
396   * @return  The change number for the associated change, or -1 if none was
397   *          provided.
398   */
399  public long getChangeNumber()
400  {
401    return changeNumber;
402  }
403
404
405
406  /**
407   * {@inheritDoc}
408   */
409  @Override()
410  public String getControlName()
411  {
412    return INFO_CONTROL_NAME_ENTRY_CHANGE_NOTIFICATION.get();
413  }
414
415
416
417  /**
418   * {@inheritDoc}
419   */
420  @Override()
421  public void toString(final StringBuilder buffer)
422  {
423    buffer.append("EntryChangeNotificationControl(changeType=");
424    buffer.append(changeType.getName());
425
426    if (previousDN != null)
427    {
428      buffer.append(", previousDN='");
429      buffer.append(previousDN);
430      buffer.append('\'');
431    }
432
433    if (changeNumber > 0)
434    {
435      buffer.append(", changeNumber=");
436      buffer.append(changeNumber);
437    }
438
439    buffer.append(", isCritical=");
440    buffer.append(isCritical());
441    buffer.append(')');
442  }
443}