001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 UnboundID Corp.
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.
157   */
158  public EntryChangeNotificationControl(
159              final PersistentSearchChangeType changeType,
160              final String previousDN, final long changeNumber,
161              final boolean isCritical)
162  {
163    super(ENTRY_CHANGE_NOTIFICATION_OID, isCritical,
164          encodeValue(changeType, previousDN, changeNumber));
165
166    this.changeType   = changeType;
167    this.previousDN   = previousDN;
168    this.changeNumber = changeNumber;
169  }
170
171
172
173  /**
174   * Creates a new entry change notification control with the provided
175   * information.
176   *
177   * @param  oid         The OID for the control.
178   * @param  isCritical  Indicates whether the control should be marked
179   *                     critical.
180   * @param  value       The encoded value for the control.  This may be
181   *                     {@code null} if no value was provided.
182   *
183   * @throws  LDAPException  If the provided control cannot be decoded as an
184   *                         entry change notification control.
185   */
186  public EntryChangeNotificationControl(final String oid,
187                                        final boolean isCritical,
188                                        final ASN1OctetString value)
189         throws LDAPException
190  {
191    super(oid, isCritical, value);
192
193    if (value == null)
194    {
195      throw new LDAPException(ResultCode.DECODING_ERROR,
196                              ERR_ECN_NO_VALUE.get());
197    }
198
199    final ASN1Sequence ecnSequence;
200    try
201    {
202      final ASN1Element element = ASN1Element.decode(value.getValue());
203      ecnSequence = ASN1Sequence.decodeAsSequence(element);
204    }
205    catch (final ASN1Exception ae)
206    {
207      debugException(ae);
208      throw new LDAPException(ResultCode.DECODING_ERROR,
209                              ERR_ECN_VALUE_NOT_SEQUENCE.get(ae), ae);
210    }
211
212    final ASN1Element[] ecnElements = ecnSequence.elements();
213    if ((ecnElements.length < 1) || (ecnElements.length > 3))
214    {
215      throw new LDAPException(ResultCode.DECODING_ERROR,
216                              ERR_ECN_INVALID_ELEMENT_COUNT.get(
217                                   ecnElements.length));
218    }
219
220    final ASN1Enumerated ecnEnumerated;
221    try
222    {
223      ecnEnumerated = ASN1Enumerated.decodeAsEnumerated(ecnElements[0]);
224    }
225    catch (final ASN1Exception ae)
226    {
227      debugException(ae);
228      throw new LDAPException(ResultCode.DECODING_ERROR,
229                              ERR_ECN_FIRST_NOT_ENUMERATED.get(ae), ae);
230    }
231
232    changeType = PersistentSearchChangeType.valueOf(ecnEnumerated.intValue());
233    if (changeType == null)
234    {
235      throw new LDAPException(ResultCode.DECODING_ERROR,
236                              ERR_ECN_INVALID_CHANGE_TYPE.get(
237                                   ecnEnumerated.intValue()));
238    }
239
240
241    String prevDN = null;
242    long   chgNum = -1;
243    for (int i=1; i < ecnElements.length; i++)
244    {
245      switch (ecnElements[i].getType())
246      {
247        case ASN1Constants.UNIVERSAL_OCTET_STRING_TYPE:
248          prevDN = ASN1OctetString.decodeAsOctetString(
249                        ecnElements[i]).stringValue();
250          break;
251
252        case ASN1Constants.UNIVERSAL_INTEGER_TYPE:
253          try
254          {
255            chgNum = ASN1Long.decodeAsLong(ecnElements[i]).longValue();
256          }
257          catch (final ASN1Exception ae)
258          {
259            debugException(ae);
260            throw new LDAPException(ResultCode.DECODING_ERROR,
261                                    ERR_ECN_CANNOT_DECODE_CHANGE_NUMBER.get(ae),
262                                    ae);
263          }
264          break;
265
266        default:
267          throw new LDAPException(ResultCode.DECODING_ERROR,
268                                  ERR_ECN_INVALID_ELEMENT_TYPE.get(
269                                       toHex(ecnElements[i].getType())));
270      }
271    }
272
273    previousDN   = prevDN;
274    changeNumber = chgNum;
275  }
276
277
278
279  /**
280   * {@inheritDoc}
281   */
282  public EntryChangeNotificationControl
283              decodeControl(final String oid, final boolean isCritical,
284                            final ASN1OctetString value)
285         throws LDAPException
286  {
287    return new EntryChangeNotificationControl(oid, isCritical, value);
288  }
289
290
291
292  /**
293   * Extracts an entry change notification control from the provided search
294   * result entry.
295   *
296   * @param  entry  The search result entry from which to retrieve the entry
297   *                change notification control.
298   *
299   * @return  The entry change notification control contained in the provided
300   *          search result entry, or {@code null} if the entry did not contain
301   *          an entry change notification control.
302   *
303   * @throws  LDAPException  If a problem is encountered while attempting to
304   *                         decode the entry change notification control
305   *                         contained in the provided entry.
306   */
307  public static EntryChangeNotificationControl
308                     get(final SearchResultEntry entry)
309         throws LDAPException
310  {
311    final Control c = entry.getControl(ENTRY_CHANGE_NOTIFICATION_OID);
312    if (c == null)
313    {
314      return null;
315    }
316
317    if (c instanceof EntryChangeNotificationControl)
318    {
319      return (EntryChangeNotificationControl) c;
320    }
321    else
322    {
323      return new EntryChangeNotificationControl(c.getOID(), c.isCritical(),
324           c.getValue());
325    }
326  }
327
328
329
330  /**
331   * Encodes the provided information into an octet string that can be used as
332   * the value for this control.
333   *
334   * @param  changeType    The change type for the change.  It must not be
335   *                       {@code null}.
336   * @param  previousDN    The previous DN of the entry, if applicable.
337   * @param  changeNumber  The change number to include in this control, or
338   *                       -1 if there should not be a change number.
339   *
340   * @return  An ASN.1 octet string that can be used as the value for this
341   *          control.
342   */
343  private static ASN1OctetString encodeValue(
344               final PersistentSearchChangeType changeType,
345               final String previousDN, final long changeNumber)
346  {
347    ensureNotNull(changeType);
348
349    final ArrayList<ASN1Element> elementList = new ArrayList<ASN1Element>(3);
350    elementList.add(new ASN1Enumerated(changeType.intValue()));
351
352    if (previousDN != null)
353    {
354      elementList.add(new ASN1OctetString(previousDN));
355    }
356
357    if (changeNumber > 0)
358    {
359      elementList.add(new ASN1Long(changeNumber));
360    }
361
362    return new ASN1OctetString(new ASN1Sequence(elementList).encode());
363  }
364
365
366
367  /**
368   * Retrieves the change type for this entry change notification control.
369   *
370   * @return  The change type for this entry change notification control.
371   */
372  public PersistentSearchChangeType getChangeType()
373  {
374    return changeType;
375  }
376
377
378
379  /**
380   * Retrieves the previous DN for the entry, if applicable.
381   *
382   * @return  The previous DN for the entry, or {@code null} if there is none.
383   */
384  public String getPreviousDN()
385  {
386    return previousDN;
387  }
388
389
390
391  /**
392   * Retrieves the change number for the associated change, if available.
393   *
394   * @return  The change number for the associated change, or -1 if none was
395   *          provided.
396   */
397  public long getChangeNumber()
398  {
399    return changeNumber;
400  }
401
402
403
404  /**
405   * {@inheritDoc}
406   */
407  @Override()
408  public String getControlName()
409  {
410    return INFO_CONTROL_NAME_ENTRY_CHANGE_NOTIFICATION.get();
411  }
412
413
414
415  /**
416   * {@inheritDoc}
417   */
418  @Override()
419  public void toString(final StringBuilder buffer)
420  {
421    buffer.append("EntryChangeNotificationControl(changeType=");
422    buffer.append(changeType.getName());
423
424    if (previousDN != null)
425    {
426      buffer.append(", previousDN='");
427      buffer.append(previousDN);
428      buffer.append('\'');
429    }
430
431    if (changeNumber > 0)
432    {
433      buffer.append(", changeNumber=");
434      buffer.append(changeNumber);
435    }
436
437    buffer.append(", isCritical=");
438    buffer.append(isCritical());
439    buffer.append(')');
440  }
441}