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