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