001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 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     */
021    package com.unboundid.ldap.sdk.controls;
022    
023    
024    
025    import java.util.ArrayList;
026    
027    import com.unboundid.asn1.ASN1Constants;
028    import com.unboundid.asn1.ASN1Element;
029    import com.unboundid.asn1.ASN1Enumerated;
030    import com.unboundid.asn1.ASN1Exception;
031    import com.unboundid.asn1.ASN1Long;
032    import com.unboundid.asn1.ASN1OctetString;
033    import com.unboundid.asn1.ASN1Sequence;
034    import com.unboundid.ldap.sdk.Control;
035    import com.unboundid.ldap.sdk.DecodeableControl;
036    import com.unboundid.ldap.sdk.LDAPException;
037    import com.unboundid.ldap.sdk.ResultCode;
038    import com.unboundid.ldap.sdk.SearchResultEntry;
039    import com.unboundid.util.NotMutable;
040    import com.unboundid.util.ThreadSafety;
041    import com.unboundid.util.ThreadSafetyLevel;
042    
043    import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
044    import static com.unboundid.util.Debug.*;
045    import static com.unboundid.util.StaticUtils.*;
046    import 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)
083    public 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    }