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}