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.ldap.CoreMessages.*;
030
031import java.io.IOException;
032
033import org.forgerock.i18n.LocalizableMessage;
034import org.forgerock.i18n.slf4j.LocalizedLogger;
035import org.forgerock.opendj.io.ASN1;
036import org.forgerock.opendj.io.ASN1Reader;
037import org.forgerock.opendj.io.ASN1Writer;
038import org.forgerock.opendj.ldap.ByteString;
039import org.forgerock.opendj.ldap.ByteStringBuilder;
040import org.forgerock.opendj.ldap.DecodeException;
041import org.forgerock.opendj.ldap.DecodeOptions;
042
043import org.forgerock.util.Reject;
044
045/**
046 * The simple paged results request and response control as defined in RFC 2696.
047 * This control allows a client to control the rate at which an LDAP server
048 * returns the results of an LDAP search operation. This control may be useful
049 * when the LDAP client has limited resources and may not be able to process the
050 * entire result set from a given LDAP query, or when the LDAP client is
051 * connected over a low-bandwidth connection.
052 * <p>
053 * This control is included in the searchRequest and searchResultDone messages
054 * and has the following structure:
055 *
056 * <pre>
057 * realSearchControlValue ::= SEQUENCE {
058 *         size            INTEGER (0..maxInt),
059 *                                 -- requested page size from client
060 *                                 -- result set size estimate from server
061 *         cookie          OCTET STRING
062 * }
063 * </pre>
064 * <p>
065 * The following example demonstrates use of simple paged results to handle
066 * three entries at a time.
067 *
068 * <pre>
069 * ByteString cookie = ByteString.empty();
070 * SearchRequest request;
071 * SearchResultHandler resultHandler = new MySearchResultHandler();
072 * Result result;
073 *
074 * int page = 1;
075 * do {
076        System.out.println("# Simple paged results: Page " + page);
077 *
078 *      request = Requests.newSearchRequest(
079                "dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(sn=Jensen)", "cn")
080                .addControl(SimplePagedResultsControl.newControl(true, 3, cookie));
081 *
082 *      result = connection.search(request, resultHandler);
083 *      try {
084 *      SimplePagedResultsControl control = result.getControl(
085                SimplePagedResultsControl.DECODER, new DecodeOptions());
086        cookie = control.getCookie();
087        } catch (final DecodeException e) {
088            // Failed to decode the response control.
089        }
090 *
091 *      ++page;
092 * } while (cookie.length() != 0);
093 * </pre>
094 *
095 * The search result handler in this case displays pages of results as LDIF on
096 * standard out.
097 *
098 * <pre>
099 * private static class MySearchResultHandler implements SearchResultHandler {
100 *
101 *     {@literal @}Override
102 *     public void handleExceptionResult(LdapException error) {
103 *         // Ignore.
104 *     }
105 *
106 *     {@literal @}Override
107 *     public void handleResult(Result result) {
108 *         // Ignore.
109 *     }
110 *
111 *     {@literal @}Override
112 *     public boolean handleEntry(SearchResultEntry entry) {
113 *         final LDIFEntryWriter writer = new LDIFEntryWriter(System.out);
114 *         try {
115 *             writer.writeEntry(entry);
116 *             writer.flush();
117 *         } catch (final IOException e) {
118 *             // The writer could not write to System.out.
119 *         }
120 *         return true;
121 *     }
122 *
123 *     {@literal @}Override
124 *     public boolean handleReference(SearchResultReference reference) {
125 *         System.out.println("Got a reference: " + reference.toString());
126 *         return false;
127 *     }
128 * }
129 * </pre>
130 *
131 * @see <a href="http://tools.ietf.org/html/rfc2696">RFC 2696 - LDAP Control
132 *      Extension for Simple Paged Results Manipulation </a>
133 */
134public final class SimplePagedResultsControl implements Control {
135
136    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
137    /**
138     * The OID for the paged results request/response control defined in RFC
139     * 2696.
140     */
141    public static final String OID = "1.2.840.113556.1.4.319";
142
143    /**
144     * A decoder which can be used for decoding the simple paged results
145     * control.
146     */
147    public static final ControlDecoder<SimplePagedResultsControl> DECODER =
148            new ControlDecoder<SimplePagedResultsControl>() {
149
150                public SimplePagedResultsControl decodeControl(final Control control,
151                        final DecodeOptions options) throws DecodeException {
152                    Reject.ifNull(control);
153
154                    if (control instanceof SimplePagedResultsControl) {
155                        return (SimplePagedResultsControl) control;
156                    }
157
158                    if (!control.getOID().equals(OID)) {
159                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_CONTROL_BAD_OID.get(control.getOID(), OID));
160                    }
161
162                    if (!control.hasValue()) {
163                        // The control must always have a value.
164                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_NULL.get());
165                    }
166
167                    final ASN1Reader reader = ASN1.getReader(control.getValue());
168                    try {
169                        reader.readStartSequence();
170                    } catch (final Exception e) {
171                        logger.debug(LocalizableMessage.raw("Unable to read start sequence", e));
172                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SEQUENCE.get(e), e);
173                    }
174
175                    int size;
176                    try {
177                        size = (int) reader.readInteger();
178                    } catch (final Exception e) {
179                        logger.debug(LocalizableMessage.raw("Unable to read size", e));
180                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SIZE.get(e), e);
181                    }
182
183                    ByteString cookie;
184                    try {
185                        cookie = reader.readOctetString();
186                    } catch (final Exception e) {
187                        logger.debug(LocalizableMessage.raw("Unable to read cookie", e));
188                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_COOKIE.get(e), e);
189                    }
190
191                    try {
192                        reader.readEndSequence();
193                    } catch (final Exception e) {
194                        logger.debug(LocalizableMessage.raw("Unable to read end sequence", e));
195                        throw DecodeException.error(ERR_LDAP_PAGED_RESULTS_DECODE_SEQUENCE.get(e), e);
196                    }
197
198                    return new SimplePagedResultsControl(control.isCritical(), size, cookie);
199                }
200
201                public String getOID() {
202                    return OID;
203                }
204            };
205
206    /**
207     * Creates a new simple paged results control with the provided criticality,
208     * size, and cookie.
209     *
210     * @param isCritical
211     *            {@code true} if it is unacceptable to perform the operation
212     *            without applying the semantics of this control, or
213     *            {@code false} if it can be ignored.
214     * @param size
215     *            The requested page size when used in a request control from
216     *            the client, or an estimate of the result set size when used in
217     *            a response control from the server (may be 0, indicating that
218     *            the server does not know).
219     * @param cookie
220     *            An opaque cookie which is used by the server to track its
221     *            position in the set of search results. The cookie must be
222     *            empty in the initial search request sent by the client. For
223     *            subsequent search requests the client must include the cookie
224     *            returned with the previous search result, until the server
225     *            returns an empty cookie indicating that the final page of
226     *            results has been returned.
227     * @return The new control.
228     * @throws NullPointerException
229     *             If {@code cookie} was {@code null}.
230     */
231    public static SimplePagedResultsControl newControl(final boolean isCritical, final int size,
232            final ByteString cookie) {
233        Reject.ifNull(cookie);
234        return new SimplePagedResultsControl(isCritical, size, cookie);
235    }
236
237    /**
238     * The control value size element, which is either the requested page size
239     * from the client, or the result set size estimate from the server.
240     */
241    private final int size;
242
243    /**
244     * The control value cookie element.
245     */
246    private final ByteString cookie;
247
248    private final boolean isCritical;
249
250    private SimplePagedResultsControl(final boolean isCritical, final int size,
251            final ByteString cookie) {
252        this.isCritical = isCritical;
253        this.size = size;
254        this.cookie = cookie;
255    }
256
257    /**
258     * Returns the opaque cookie which is used by the server to track its
259     * position in the set of search results. The cookie must be empty in the
260     * initial search request sent by the client. For subsequent search requests
261     * the client must include the cookie returned with the previous search
262     * result, until the server returns an empty cookie indicating that the
263     * final page of results has been returned.
264     *
265     * @return The opaque cookie which is used by the server to track its
266     *         position in the set of search results.
267     */
268    public ByteString getCookie() {
269        return cookie;
270    }
271
272    /** {@inheritDoc} */
273    public String getOID() {
274        return OID;
275    }
276
277    /**
278     * Returns the requested page size when used in a request control from the
279     * client, or an estimate of the result set size when used in a response
280     * control from the server (may be 0, indicating that the server does not
281     * know).
282     *
283     * @return The requested page size when used in a request control from the
284     *         client, or an estimate of the result set size when used in a
285     *         response control from the server.
286     */
287    public int getSize() {
288        return size;
289    }
290
291    /** {@inheritDoc} */
292    public ByteString getValue() {
293        final ByteStringBuilder buffer = new ByteStringBuilder();
294        final ASN1Writer writer = ASN1.getWriter(buffer);
295        try {
296            writer.writeStartSequence();
297            writer.writeInteger(size);
298            writer.writeOctetString(cookie);
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    /** {@inheritDoc} */
308    public boolean hasValue() {
309        return true;
310    }
311
312    /** {@inheritDoc} */
313    public boolean isCritical() {
314        return isCritical;
315    }
316
317    /** {@inheritDoc} */
318    @Override
319    public String toString() {
320        final StringBuilder builder = new StringBuilder();
321        builder.append("SimplePagedResultsControl(oid=");
322        builder.append(getOID());
323        builder.append(", criticality=");
324        builder.append(isCritical());
325        builder.append(", size=");
326        builder.append(size);
327        builder.append(", cookie=");
328        builder.append(cookie);
329        builder.append(")");
330        return builder.toString();
331    }
332}