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;
033import java.util.Arrays;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.EnumSet;
037import java.util.Set;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.slf4j.LocalizedLogger;
041import org.forgerock.opendj.io.ASN1;
042import org.forgerock.opendj.io.ASN1Reader;
043import org.forgerock.opendj.io.ASN1Writer;
044import org.forgerock.opendj.ldap.ByteString;
045import org.forgerock.opendj.ldap.ByteStringBuilder;
046import org.forgerock.opendj.ldap.DecodeException;
047import org.forgerock.opendj.ldap.DecodeOptions;
048import org.forgerock.util.Reject;
049
050/**
051 * The persistent search request control as defined in
052 * draft-ietf-ldapext-psearch. This control allows a client to receive
053 * notification of changes that occur in an LDAP server.
054 * <p>
055 * You can examine the entry change notification response control to get more
056 * information about a change returned by the persistent search.
057 *
058 * <pre>
059 * Connection connection = ...;
060 *
061 * SearchRequest request =
062 *         Requests.newSearchRequest(
063 *                 "dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
064 *                 "(objectclass=inetOrgPerson)", "cn")
065 *                 .addControl(PersistentSearchRequestControl.newControl(
066 *                             true, true, true, // critical,changesOnly,returnECs
067 *                             PersistentSearchChangeType.ADD,
068 *                             PersistentSearchChangeType.DELETE,
069 *                             PersistentSearchChangeType.MODIFY,
070 *                             PersistentSearchChangeType.MODIFY_DN));
071 *
072 * ConnectionEntryReader reader = connection.search(request);
073 *
074 * while (reader.hasNext()) {
075 *     if (!reader.isReference()) {
076 *         SearchResultEntry entry = reader.readEntry(); // Entry that changed
077 *
078 *         EntryChangeNotificationResponseControl control = entry.getControl(
079 *                 EntryChangeNotificationResponseControl.DECODER,
080 *                 new DecodeOptions());
081 *
082 *         PersistentSearchChangeType type = control.getChangeType();
083 *         if (type.equals(PersistentSearchChangeType.MODIFY_DN)) {
084 *             // Previous DN: control.getPreviousName()
085 *         }
086 *         // Change number: control.getChangeNumber());
087 *     }
088 * }
089 *
090 * </pre>
091 *
092 * @see EntryChangeNotificationResponseControl
093 * @see PersistentSearchChangeType
094 * @see <a
095 *      href="http://tools.ietf.org/html/draft-ietf-ldapext-psearch">draft-ietf-ldapext-psearch
096 *      - Persistent Search: A Simple LDAP Change Notification Mechanism </a>
097 */
098public final class PersistentSearchRequestControl implements Control {
099
100    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
101    /**
102     * The OID for the persistent search request control.
103     */
104    public static final String OID = "2.16.840.1.113730.3.4.3";
105
106    /**
107     * A decoder which can be used for decoding the persistent search request
108     * control.
109     */
110    public static final ControlDecoder<PersistentSearchRequestControl> DECODER =
111            new ControlDecoder<PersistentSearchRequestControl>() {
112
113                public PersistentSearchRequestControl decodeControl(final Control control,
114                        final DecodeOptions options) throws DecodeException {
115                    Reject.ifNull(control);
116
117                    if (control instanceof PersistentSearchRequestControl) {
118                        return (PersistentSearchRequestControl) control;
119                    }
120
121                    if (!control.getOID().equals(OID)) {
122                        final LocalizableMessage message =
123                                ERR_PSEARCH_CONTROL_BAD_OID.get(control.getOID(), OID);
124                        throw DecodeException.error(message);
125                    }
126
127                    if (!control.hasValue()) {
128                        // The control must always have a value.
129                        final LocalizableMessage message = ERR_PSEARCH_NO_CONTROL_VALUE.get();
130                        throw DecodeException.error(message);
131                    }
132
133                    final ASN1Reader reader = ASN1.getReader(control.getValue());
134                    boolean changesOnly;
135                    boolean returnECs;
136                    int changeTypes;
137
138                    try {
139                        reader.readStartSequence();
140
141                        changeTypes = (int) reader.readInteger();
142                        changesOnly = reader.readBoolean();
143                        returnECs = reader.readBoolean();
144
145                        reader.readEndSequence();
146                    } catch (final IOException e) {
147                        logger.debug(LocalizableMessage.raw("Unable to read sequence", e));
148
149                        final LocalizableMessage message =
150                                ERR_PSEARCH_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
151                        throw DecodeException.error(message, e);
152                    }
153
154                    final Set<PersistentSearchChangeType> changeTypeSet =
155                            EnumSet.noneOf(PersistentSearchChangeType.class);
156
157                    if ((changeTypes & 15) != 0) {
158                        final LocalizableMessage message =
159                                ERR_PSEARCH_BAD_CHANGE_TYPES.get(changeTypes);
160                        throw DecodeException.error(message);
161                    }
162
163                    if ((changeTypes & 1) != 0) {
164                        changeTypeSet.add(PersistentSearchChangeType.ADD);
165                    }
166
167                    if ((changeTypes & 2) != 0) {
168                        changeTypeSet.add(PersistentSearchChangeType.DELETE);
169                    }
170
171                    if ((changeTypes & 4) != 0) {
172                        changeTypeSet.add(PersistentSearchChangeType.MODIFY);
173                    }
174
175                    if ((changeTypes & 8) != 0) {
176                        changeTypeSet.add(PersistentSearchChangeType.MODIFY_DN);
177                    }
178
179                    return new PersistentSearchRequestControl(control.isCritical(), changesOnly,
180                            returnECs, Collections.unmodifiableSet(changeTypeSet));
181                }
182
183                public String getOID() {
184                    return OID;
185                }
186            };
187
188    /**
189     * Creates a new persistent search request control.
190     *
191     * @param isCritical
192     *            {@code true} if it is unacceptable to perform the operation
193     *            without applying the semantics of this control, or
194     *            {@code false} if it can be ignored
195     * @param changesOnly
196     *            Indicates whether or not only updated entries should be
197     *            returned (added, modified, deleted, or subject to a modifyDN
198     *            operation). If this parameter is {@code false} then the search
199     *            will initially return all the existing entries which match the
200     *            filter.
201     * @param returnECs
202     *            Indicates whether or not the entry change notification control
203     *            should be included in updated entries that match the
204     *            associated search criteria.
205     * @param changeTypes
206     *            The types of update operation for which change notifications
207     *            should be returned.
208     * @return The new control.
209     * @throws NullPointerException
210     *             If {@code changeTypes} was {@code null}.
211     */
212    public static PersistentSearchRequestControl newControl(final boolean isCritical,
213            final boolean changesOnly, final boolean returnECs,
214            final Collection<PersistentSearchChangeType> changeTypes) {
215        Reject.ifNull(changeTypes);
216
217        final Set<PersistentSearchChangeType> copyOfChangeTypes =
218                EnumSet.noneOf(PersistentSearchChangeType.class);
219        copyOfChangeTypes.addAll(changeTypes);
220        return new PersistentSearchRequestControl(isCritical, changesOnly, returnECs, Collections
221                .unmodifiableSet(copyOfChangeTypes));
222    }
223
224    /**
225     * Creates a new persistent search request control.
226     *
227     * @param isCritical
228     *            {@code true} if it is unacceptable to perform the operation
229     *            without applying the semantics of this control, or
230     *            {@code false} if it can be ignored
231     * @param changesOnly
232     *            Indicates whether or not only updated entries should be
233     *            returned (added, modified, deleted, or subject to a modifyDN
234     *            operation). If this parameter is {@code false} then the search
235     *            will initially return all the existing entries which match the
236     *            filter.
237     * @param returnECs
238     *            Indicates whether or not the entry change notification control
239     *            should be included in updated entries that match the
240     *            associated search criteria.
241     * @param changeTypes
242     *            The types of update operation for which change notifications
243     *            should be returned.
244     * @return The new control.
245     * @throws NullPointerException
246     *             If {@code changeTypes} was {@code null}.
247     */
248    public static PersistentSearchRequestControl newControl(final boolean isCritical,
249            final boolean changesOnly, final boolean returnECs,
250            final PersistentSearchChangeType... changeTypes) {
251        Reject.ifNull((Object) changeTypes);
252
253        return newControl(isCritical, changesOnly, returnECs, Arrays.asList(changeTypes));
254    }
255
256    /**
257     * Indicates whether to only return entries that have been updated
258     * since the beginning of the search.
259     */
260    private final boolean changesOnly;
261
262    /**
263     * Indicates whether entries returned as a result of changes to
264     * directory data should include the entry change notification control.
265     */
266    private final boolean returnECs;
267
268    /** The logical OR of change types associated with this control. */
269    private final Set<PersistentSearchChangeType> changeTypes;
270
271    private final boolean isCritical;
272
273    private PersistentSearchRequestControl(final boolean isCritical, final boolean changesOnly,
274            final boolean returnECs, final Set<PersistentSearchChangeType> changeTypes) {
275        this.isCritical = isCritical;
276        this.changesOnly = changesOnly;
277        this.returnECs = returnECs;
278        this.changeTypes = changeTypes;
279    }
280
281    /**
282     * Returns an unmodifiable set containing the types of update operation for
283     * which change notifications should be returned.
284     *
285     * @return An unmodifiable set containing the types of update operation for
286     *         which change notifications should be returned.
287     */
288    public Set<PersistentSearchChangeType> getChangeTypes() {
289        return changeTypes;
290    }
291
292    /** {@inheritDoc} */
293    public String getOID() {
294        return OID;
295    }
296
297    /** {@inheritDoc} */
298    public ByteString getValue() {
299        final ByteStringBuilder buffer = new ByteStringBuilder();
300        final ASN1Writer writer = ASN1.getWriter(buffer);
301        try {
302            writer.writeStartSequence();
303
304            int changeTypesInt = 0;
305            for (final PersistentSearchChangeType changeType : changeTypes) {
306                changeTypesInt |= changeType.intValue();
307            }
308            writer.writeInteger(changeTypesInt);
309
310            writer.writeBoolean(changesOnly);
311            writer.writeBoolean(returnECs);
312            writer.writeEndSequence();
313            return buffer.toByteString();
314        } catch (final IOException ioe) {
315            // This should never happen unless there is a bug somewhere.
316            throw new RuntimeException(ioe);
317        }
318    }
319
320    /** {@inheritDoc} */
321    public boolean hasValue() {
322        return true;
323    }
324
325    /**
326     * Returns {@code true} if only updated entries should be returned (added,
327     * modified, deleted, or subject to a modifyDN operation), otherwise
328     * {@code false} if the search will initially return all the existing
329     * entries which match the filter.
330     *
331     * @return {@code true} if only updated entries should be returned (added,
332     *         modified, deleted, or subject to a modifyDN operation), otherwise
333     *         {@code false} if the search will initially return all the
334     *         existing entries which match the filter.
335     */
336    public boolean isChangesOnly() {
337        return changesOnly;
338    }
339
340    /** {@inheritDoc} */
341    public boolean isCritical() {
342        return isCritical;
343    }
344
345    /**
346     * Returns {@code true} if the entry change notification control should be
347     * included in updated entries that match the associated search criteria.
348     *
349     * @return {@code true} if the entry change notification control should be
350     *         included in updated entries that match the associated search
351     *         criteria.
352     */
353    public boolean isReturnECs() {
354        return returnECs;
355    }
356
357    /** {@inheritDoc} */
358    @Override
359    public String toString() {
360        final StringBuilder builder = new StringBuilder();
361        builder.append("PersistentSearchRequestControl(oid=");
362        builder.append(getOID());
363        builder.append(", criticality=");
364        builder.append(isCritical());
365        builder.append(", changeTypes=[");
366
367        boolean comma = false;
368        for (final PersistentSearchChangeType type : changeTypes) {
369            if (comma) {
370                builder.append(", ");
371            }
372            builder.append(type);
373            comma = true;
374        }
375
376        builder.append("](");
377        builder.append(changeTypes);
378        builder.append("), changesOnly=");
379        builder.append(changesOnly);
380        builder.append(", returnECs=");
381        builder.append(returnECs);
382        builder.append(")");
383        return builder.toString();
384    }
385}