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