001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2010 Sun Microsystems, Inc.
025 *      Portions copyright 2012-2014 ForgeRock AS.
026 */
027package org.forgerock.opendj.ldap.controls;
028
029import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
030import static com.forgerock.opendj.ldap.CoreMessages.*;
031
032import java.io.IOException;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.LocalizedIllegalArgumentException;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.forgerock.opendj.io.ASN1;
038import org.forgerock.opendj.io.ASN1Reader;
039import org.forgerock.opendj.io.ASN1Writer;
040import org.forgerock.opendj.ldap.ByteString;
041import org.forgerock.opendj.ldap.ByteStringBuilder;
042import org.forgerock.opendj.ldap.DN;
043import org.forgerock.opendj.ldap.DecodeException;
044import org.forgerock.opendj.ldap.DecodeOptions;
045import org.forgerock.opendj.ldap.schema.Schema;
046import org.forgerock.util.Reject;
047
048/**
049 * The entry change notification response control as defined in
050 * draft-ietf-ldapext-psearch. This control provides additional information
051 * about the change that caused a particular entry to be returned as the result
052 * of a persistent search.
053 *
054 * <pre>
055 * Connection connection = ...;
056 *
057 * SearchRequest request =
058 *         Requests.newSearchRequest(
059 *                 "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
060 *                 "(objectclass=inetOrgPerson)", "cn")
061 *                 .addControl(PersistentSearchRequestControl.newControl(
062 *                             true, true, true, // critical,changesOnly,returnECs
063 *                             PersistentSearchChangeType.ADD,
064 *                             PersistentSearchChangeType.DELETE,
065 *                             PersistentSearchChangeType.MODIFY,
066 *                             PersistentSearchChangeType.MODIFY_DN));
067 *
068 * ConnectionEntryReader reader = connection.search(request);
069 *
070 * while (reader.hasNext()) {
071 *     if (!reader.isReference()) {
072 *         SearchResultEntry entry = reader.readEntry(); // Entry that changed
073 *
074 *         EntryChangeNotificationResponseControl control = entry.getControl(
075 *                 EntryChangeNotificationResponseControl.DECODER,
076 *                 new DecodeOptions());
077 *
078 *         PersistentSearchChangeType type = control.getChangeType();
079 *         if (type.equals(PersistentSearchChangeType.MODIFY_DN)) {
080 *             // Previous DN: control.getPreviousName()
081 *         }
082 *         // Change number: control.getChangeNumber());
083 *     }
084 * }
085 *
086 * </pre>
087 *
088 * @see PersistentSearchRequestControl
089 * @see PersistentSearchChangeType
090 * @see <a
091 *      href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch
092 *      - Persistent Search: A Simple LDAP Change Notification Mechanism </a>
093 */
094public final class EntryChangeNotificationResponseControl implements Control {
095
096    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
097    /**
098     * The OID for the entry change notification response control.
099     */
100    public static final String OID = "2.16.840.1.113730.3.4.7";
101
102    /**
103     * A decoder which can be used for decoding the entry change notification
104     * response control.
105     */
106    public static final ControlDecoder<EntryChangeNotificationResponseControl> DECODER =
107            new ControlDecoder<EntryChangeNotificationResponseControl>() {
108
109                public EntryChangeNotificationResponseControl decodeControl(final Control control,
110                        final DecodeOptions options) throws DecodeException {
111                    Reject.ifNull(control, options);
112
113                    if (control instanceof EntryChangeNotificationResponseControl) {
114                        return (EntryChangeNotificationResponseControl) control;
115                    }
116
117                    if (!control.getOID().equals(OID)) {
118                        final LocalizableMessage message =
119                                ERR_ECN_CONTROL_BAD_OID.get(control.getOID(), OID);
120                        throw DecodeException.error(message);
121                    }
122
123                    if (!control.hasValue()) {
124                        // The response control must always have a value.
125                        final LocalizableMessage message = ERR_ECN_NO_CONTROL_VALUE.get();
126                        throw DecodeException.error(message);
127                    }
128
129                    String previousDNString = null;
130                    long changeNumber = -1;
131                    PersistentSearchChangeType changeType;
132                    final ASN1Reader reader = ASN1.getReader(control.getValue());
133                    try {
134                        reader.readStartSequence();
135
136                        final int changeTypeInt = reader.readEnumerated();
137                        switch (changeTypeInt) {
138                        case 1:
139                            changeType = PersistentSearchChangeType.ADD;
140                            break;
141                        case 2:
142                            changeType = PersistentSearchChangeType.DELETE;
143                            break;
144                        case 4:
145                            changeType = PersistentSearchChangeType.MODIFY;
146                            break;
147                        case 8:
148                            changeType = PersistentSearchChangeType.MODIFY_DN;
149                            break;
150                        default:
151                            final LocalizableMessage message =
152                                    ERR_ECN_BAD_CHANGE_TYPE.get(changeTypeInt);
153                            throw DecodeException.error(message);
154                        }
155
156                        if (reader.hasNextElement()
157                                && (reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE)) {
158                            if (changeType != PersistentSearchChangeType.MODIFY_DN) {
159                                final LocalizableMessage message =
160                                        ERR_ECN_ILLEGAL_PREVIOUS_DN.get(String.valueOf(changeType));
161                                throw DecodeException.error(message);
162                            }
163
164                            previousDNString = reader.readOctetStringAsString();
165                        }
166                        if (reader.hasNextElement()
167                                && (reader.peekType() == ASN1.UNIVERSAL_INTEGER_TYPE)) {
168                            changeNumber = reader.readInteger();
169                        }
170                    } catch (final IOException e) {
171                        logger.debug(LocalizableMessage.raw("%s", e));
172
173                        final LocalizableMessage message =
174                                ERR_ECN_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
175                        throw DecodeException.error(message, e);
176                    }
177
178                    final Schema schema =
179                            options.getSchemaResolver().resolveSchema(previousDNString);
180                    DN previousDN = null;
181                    if (previousDNString != null) {
182                        try {
183                            previousDN = DN.valueOf(previousDNString, schema);
184                        } catch (final LocalizedIllegalArgumentException e) {
185                            final LocalizableMessage message =
186                                    ERR_ECN_INVALID_PREVIOUS_DN.get(getExceptionMessage(e));
187                            throw DecodeException.error(message, e);
188                        }
189                    }
190
191                    return new EntryChangeNotificationResponseControl(control.isCritical(),
192                            changeType, previousDN, changeNumber);
193                }
194
195                public String getOID() {
196                    return OID;
197                }
198            };
199
200    /**
201     * Creates a new entry change notification response control with the
202     * provided change type and optional previous distinguished name and change
203     * number.
204     *
205     * @param type
206     *            The change type for this change notification control.
207     * @param previousName
208     *            The distinguished name that the entry had prior to a modify DN
209     *            operation, or <CODE>null</CODE> if the operation was not a
210     *            modify DN.
211     * @param changeNumber
212     *            The change number for the associated change, or a negative
213     *            value if no change number is available.
214     * @return The new control.
215     * @throws NullPointerException
216     *             If {@code type} was {@code null}.
217     */
218    public static EntryChangeNotificationResponseControl newControl(
219            final PersistentSearchChangeType type, final DN previousName, final long changeNumber) {
220        return new EntryChangeNotificationResponseControl(false, type, previousName, changeNumber);
221    }
222
223    /**
224     * Creates a new entry change notification response control with the
225     * provided change type and optional previous distinguished name and change
226     * number. The previous distinguished name, if provided, will be decoded
227     * using the default schema.
228     *
229     * @param type
230     *            The change type for this change notification control.
231     * @param previousName
232     *            The distinguished name that the entry had prior to a modify DN
233     *            operation, or <CODE>null</CODE> if the operation was not a
234     *            modify DN.
235     * @param changeNumber
236     *            The change number for the associated change, or a negative
237     *            value if no change number is available.
238     * @return The new control.
239     * @throws LocalizedIllegalArgumentException
240     *             If {@code previousName} is not a valid LDAP string
241     *             representation of a DN.
242     * @throws NullPointerException
243     *             If {@code type} was {@code null}.
244     */
245    public static EntryChangeNotificationResponseControl newControl(
246            final PersistentSearchChangeType type, final String previousName,
247            final long changeNumber) {
248        return new EntryChangeNotificationResponseControl(false, type, DN.valueOf(previousName),
249                changeNumber);
250    }
251
252    /** The previous DN for this change notification control. */
253    private final DN previousName;
254
255    /** The change number for this change notification control. */
256    private final long changeNumber;
257
258    /** The change type for this change notification control. */
259    private final PersistentSearchChangeType changeType;
260
261    private final boolean isCritical;
262
263    private EntryChangeNotificationResponseControl(final boolean isCritical,
264            final PersistentSearchChangeType changeType, final DN previousName,
265            final long changeNumber) {
266        Reject.ifNull(changeType);
267        this.isCritical = isCritical;
268        this.changeType = changeType;
269        this.previousName = previousName;
270        this.changeNumber = changeNumber;
271    }
272
273    /**
274     * Returns the change number for this entry change notification control.
275     *
276     * @return The change number for this entry change notification control, or
277     *         a negative value if no change number is available.
278     */
279    public long getChangeNumber() {
280        return changeNumber;
281    }
282
283    /**
284     * Returns the change type for this entry change notification control.
285     *
286     * @return The change type for this entry change notification control.
287     */
288    public PersistentSearchChangeType getChangeType() {
289        return changeType;
290    }
291
292    /** {@inheritDoc} */
293    public String getOID() {
294        return OID;
295    }
296
297    /**
298     * Returns the distinguished name that the entry had prior to a modify DN
299     * operation, or <CODE>null</CODE> if the operation was not a modify DN.
300     *
301     * @return The distinguished name that the entry had prior to a modify DN
302     *         operation.
303     */
304    public DN getPreviousName() {
305        return previousName;
306    }
307
308    /** {@inheritDoc} */
309    public ByteString getValue() {
310        final ByteStringBuilder buffer = new ByteStringBuilder();
311        final ASN1Writer writer = ASN1.getWriter(buffer);
312        try {
313            writer.writeStartSequence();
314            writer.writeInteger(changeType.intValue());
315
316            if (previousName != null) {
317                writer.writeOctetString(previousName.toString());
318            }
319
320            if (changeNumber > 0) {
321                writer.writeInteger(changeNumber);
322            }
323            writer.writeEndSequence();
324            return buffer.toByteString();
325        } catch (final IOException ioe) {
326            // This should never happen unless there is a bug somewhere.
327            throw new RuntimeException(ioe);
328        }
329    }
330
331    /** {@inheritDoc} */
332    public boolean hasValue() {
333        return true;
334    }
335
336    /** {@inheritDoc} */
337    public boolean isCritical() {
338        return isCritical;
339    }
340
341    /** {@inheritDoc} */
342    @Override
343    public String toString() {
344        final StringBuilder builder = new StringBuilder();
345        builder.append("EntryChangeNotificationResponseControl(oid=");
346        builder.append(getOID());
347        builder.append(", criticality=");
348        builder.append(isCritical());
349        builder.append(", changeType=");
350        builder.append(changeType);
351        builder.append(", previousDN=\"");
352        builder.append(previousName);
353        builder.append("\"");
354        builder.append(", changeNumber=");
355        builder.append(changeNumber);
356        builder.append(")");
357        return builder.toString();
358    }
359
360}