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}