001/* 002 * Copyright 2007-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2022 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2007-2022 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.controls; 037 038 039 040import com.unboundid.asn1.ASN1Element; 041import com.unboundid.asn1.ASN1Exception; 042import com.unboundid.asn1.ASN1Integer; 043import com.unboundid.asn1.ASN1OctetString; 044import com.unboundid.asn1.ASN1Sequence; 045import com.unboundid.ldap.sdk.Control; 046import com.unboundid.ldap.sdk.DecodeableControl; 047import com.unboundid.ldap.sdk.LDAPException; 048import com.unboundid.ldap.sdk.ResultCode; 049import com.unboundid.ldap.sdk.SearchResult; 050import com.unboundid.util.Debug; 051import com.unboundid.util.NotMutable; 052import com.unboundid.util.NotNull; 053import com.unboundid.util.Nullable; 054import com.unboundid.util.ThreadSafety; 055import com.unboundid.util.ThreadSafetyLevel; 056 057import static com.unboundid.ldap.sdk.controls.ControlMessages.*; 058 059 060 061/** 062 * This class provides an implementation of the simple paged results control as 063 * defined in <A HREF="http://www.ietf.org/rfc/rfc2696.txt">RFC 2696</A>. It 064 * allows the client to iterate through a potentially large set of search 065 * results in subsets of a specified number of entries (i.e., "pages"). 066 * <BR><BR> 067 * The same control encoding is used for both the request control sent by 068 * clients and the response control returned by the server. It may contain 069 * two elements: 070 * <UL> 071 * <LI>Size -- In a request control, this provides the requested page size, 072 * which is the maximum number of entries that the server should return 073 * in the next iteration of the search. In a response control, it is an 074 * estimate of the total number of entries that match the search 075 * criteria.</LI> 076 * <LI>Cookie -- A token which is used by the server to keep track of its 077 * position in the set of search results. The first request sent by the 078 * client should not include a cookie, and the last response sent by the 079 * server should not include a cookie. For all other intermediate search 080 * requests and responses, the server will include a cookie value in its 081 * response that the client should include in its next request.</LI> 082 * </UL> 083 * When the client wishes to use the paged results control, the first search 084 * request should include a version of the paged results request control that 085 * was created with a requested page size but no cookie. The corresponding 086 * response from the server will include a version of the paged results control 087 * that may include an estimate of the total number of matching entries, and 088 * may also include a cookie. The client should include this cookie in the 089 * next request (with the same set of search criteria) to retrieve the next page 090 * of results. This process should continue until the response control returned 091 * by the server does not include a cookie, which indicates that the end of the 092 * result set has been reached. 093 * <BR><BR> 094 * Note that the simple paged results control is similar to the 095 * {@link VirtualListViewRequestControl} in that both allow the client to 096 * request that only a portion of the result set be returned at any one time. 097 * However, there are significant differences between them, including: 098 * <UL> 099 * <LI>In order to use the virtual list view request control, it is also 100 * necessary to use the {@link ServerSideSortRequestControl} to ensure 101 * that the entries are sorted. This is not a requirement for the 102 * simple paged results control.</LI> 103 * <LI>The simple paged results control may only be used to iterate 104 * sequentially through the set of search results. The virtual list view 105 * control can retrieve pages out of order, can retrieve overlapping 106 * pages, and can re-request pages that it had already retrieved.</LI> 107 * </UL> 108 * <H2>Example</H2> 109 * The following example demonstrates the use of the simple paged results 110 * control. It will iterate through all users, retrieving up to 10 entries at a 111 * time: 112 * <PRE> 113 * // Perform a search to retrieve all users in the server, but only retrieving 114 * // ten at a time. 115 * int numSearches = 0; 116 * int totalEntriesReturned = 0; 117 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com", 118 * SearchScope.SUB, Filter.createEqualityFilter("objectClass", "person")); 119 * ASN1OctetString resumeCookie = null; 120 * while (true) 121 * { 122 * searchRequest.setControls( 123 * new SimplePagedResultsControl(10, resumeCookie)); 124 * SearchResult searchResult = connection.search(searchRequest); 125 * numSearches++; 126 * totalEntriesReturned += searchResult.getEntryCount(); 127 * for (SearchResultEntry e : searchResult.getSearchEntries()) 128 * { 129 * // Do something with each entry... 130 * } 131 * 132 * LDAPTestUtils.assertHasControl(searchResult, 133 * SimplePagedResultsControl.PAGED_RESULTS_OID); 134 * SimplePagedResultsControl responseControl = 135 * SimplePagedResultsControl.get(searchResult); 136 * if (responseControl.moreResultsToReturn()) 137 * { 138 * // The resume cookie can be included in the simple paged results 139 * // control included in the next search to get the next page of results. 140 * resumeCookie = responseControl.getCookie(); 141 * } 142 * else 143 * { 144 * break; 145 * } 146 * } 147 * </PRE> 148 */ 149@NotMutable() 150@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 151public final class SimplePagedResultsControl 152 extends Control 153 implements DecodeableControl 154{ 155 /** 156 * The OID (1.2.840.113556.1.4.319) for the paged results control. 157 */ 158 @NotNull public static final String PAGED_RESULTS_OID = 159 "1.2.840.113556.1.4.319"; 160 161 162 163 /** 164 * The serial version UID for this serializable class. 165 */ 166 private static final long serialVersionUID = 2186787148024999291L; 167 168 169 170 // The encoded cookie returned from the server (for a response control) or 171 // that should be included in the next request to the server (for a request 172 // control). 173 @NotNull private final ASN1OctetString cookie; 174 175 // The maximum requested page size (for a request control), or the estimated 176 // total result set size (for a response control). 177 private final int size; 178 179 180 181 /** 182 * Creates a new empty control instance that is intended to be used only for 183 * decoding controls via the {@code DecodeableControl} interface. 184 */ 185 SimplePagedResultsControl() 186 { 187 size = 0; 188 cookie = new ASN1OctetString(); 189 } 190 191 192 193 /** 194 * Creates a new paged results control with the specified page size. This 195 * version of the constructor should only be used when creating the first 196 * search as part of the set of paged results. Subsequent searches to 197 * retrieve additional pages should use the response control returned by the 198 * server in their next request, until the response control returned by the 199 * server does not include a cookie. 200 * 201 * @param pageSize The maximum number of entries that the server should 202 * return in the first page. 203 */ 204 public SimplePagedResultsControl(final int pageSize) 205 { 206 super(PAGED_RESULTS_OID, false, encodeValue(pageSize, null)); 207 208 size = pageSize; 209 cookie = new ASN1OctetString(); 210 } 211 212 213 214 /** 215 * Creates a new paged results control with the specified page size. This 216 * version of the constructor should only be used when creating the first 217 * search as part of the set of paged results. Subsequent searches to 218 * retrieve additional pages should use the response control returned by the 219 * server in their next request, until the response control returned by the 220 * server does not include a cookie. 221 * 222 * @param pageSize The maximum number of entries that the server should 223 * return in the first page. 224 * @param isCritical Indicates whether this control should be marked 225 * critical. 226 */ 227 public SimplePagedResultsControl(final int pageSize, final boolean isCritical) 228 { 229 super(PAGED_RESULTS_OID, isCritical, encodeValue(pageSize, null)); 230 231 size = pageSize; 232 cookie = new ASN1OctetString(); 233 } 234 235 236 237 /** 238 * Creates a new paged results control with the specified page size and the 239 * provided cookie. This version of the constructor should be used to 240 * continue iterating through an existing set of results, but potentially 241 * using a different page size. 242 * 243 * @param pageSize The maximum number of entries that the server should 244 * return in the next page of the results. 245 * @param cookie The cookie provided by the server after returning the 246 * previous page of results, or {@code null} if this request 247 * will retrieve the first page of results. 248 */ 249 public SimplePagedResultsControl(final int pageSize, 250 @Nullable final ASN1OctetString cookie) 251 { 252 super(PAGED_RESULTS_OID, false, encodeValue(pageSize, cookie)); 253 254 size = pageSize; 255 256 if (cookie == null) 257 { 258 this.cookie = new ASN1OctetString(); 259 } 260 else 261 { 262 this.cookie = cookie; 263 } 264 } 265 266 267 268 /** 269 * Creates a new paged results control with the specified page size and the 270 * provided cookie. This version of the constructor should be used to 271 * continue iterating through an existing set of results, but potentially 272 * using a different page size. 273 * 274 * @param pageSize The maximum number of entries that the server should 275 * return in the first page. 276 * @param cookie The cookie provided by the server after returning the 277 * previous page of results, or {@code null} if this 278 * request will retrieve the first page of results. 279 * @param isCritical Indicates whether this control should be marked 280 * critical. 281 */ 282 public SimplePagedResultsControl(final int pageSize, 283 @Nullable final ASN1OctetString cookie, 284 final boolean isCritical) 285 { 286 super(PAGED_RESULTS_OID, isCritical, encodeValue(pageSize, cookie)); 287 288 size = pageSize; 289 290 if (cookie == null) 291 { 292 this.cookie = new ASN1OctetString(); 293 } 294 else 295 { 296 this.cookie = cookie; 297 } 298 } 299 300 301 302 /** 303 * Creates a new paged results control from the control with the provided set 304 * of information. This should be used to decode the paged results response 305 * control returned by the server with a page of results. 306 * 307 * @param oid The OID for the control. 308 * @param isCritical Indicates whether the control should be marked 309 * critical. 310 * @param value The encoded value for the control. This may be 311 * {@code null} if no value was provided. 312 * 313 * @throws LDAPException If the provided control cannot be decoded as a 314 * simple paged results control. 315 */ 316 public SimplePagedResultsControl(@NotNull final String oid, 317 final boolean isCritical, 318 @Nullable final ASN1OctetString value) 319 throws LDAPException 320 { 321 super(oid, isCritical, value); 322 323 if (value == null) 324 { 325 throw new LDAPException(ResultCode.DECODING_ERROR, 326 ERR_PAGED_RESULTS_NO_VALUE.get()); 327 } 328 329 final ASN1Sequence valueSequence; 330 try 331 { 332 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 333 valueSequence = ASN1Sequence.decodeAsSequence(valueElement); 334 } 335 catch (final ASN1Exception ae) 336 { 337 Debug.debugException(ae); 338 throw new LDAPException(ResultCode.DECODING_ERROR, 339 ERR_PAGED_RESULTS_VALUE_NOT_SEQUENCE.get(ae), ae); 340 } 341 342 final ASN1Element[] valueElements = valueSequence.elements(); 343 if (valueElements.length != 2) 344 { 345 throw new LDAPException(ResultCode.DECODING_ERROR, 346 ERR_PAGED_RESULTS_INVALID_ELEMENT_COUNT.get( 347 valueElements.length)); 348 } 349 350 try 351 { 352 size = ASN1Integer.decodeAsInteger(valueElements[0]).intValue(); 353 } 354 catch (final ASN1Exception ae) 355 { 356 Debug.debugException(ae); 357 throw new LDAPException(ResultCode.DECODING_ERROR, 358 ERR_PAGED_RESULTS_FIRST_NOT_INTEGER.get(ae), ae); 359 } 360 361 cookie = ASN1OctetString.decodeAsOctetString(valueElements[1]); 362 } 363 364 365 366 /** 367 * {@inheritDoc} 368 */ 369 @Override() 370 @NotNull() 371 public SimplePagedResultsControl decodeControl(@NotNull final String oid, 372 final boolean isCritical, 373 @Nullable final ASN1OctetString value) 374 throws LDAPException 375 { 376 return new SimplePagedResultsControl(oid, isCritical, value); 377 } 378 379 380 381 /** 382 * Extracts a simple paged results response control from the provided result. 383 * 384 * @param result The result from which to retrieve the simple paged results 385 * response control. 386 * 387 * @return The simple paged results response control contained in the 388 * provided result, or {@code null} if the result did not contain a 389 * simple paged results response control. 390 * 391 * @throws LDAPException If a problem is encountered while attempting to 392 * decode the simple paged results response control 393 * contained in the provided result. 394 */ 395 @Nullable() 396 public static SimplePagedResultsControl get( 397 @NotNull final SearchResult result) 398 throws LDAPException 399 { 400 final Control c = result.getResponseControl(PAGED_RESULTS_OID); 401 if (c == null) 402 { 403 return null; 404 } 405 406 if (c instanceof SimplePagedResultsControl) 407 { 408 return (SimplePagedResultsControl) c; 409 } 410 else 411 { 412 return new SimplePagedResultsControl(c.getOID(), c.isCritical(), 413 c.getValue()); 414 } 415 } 416 417 418 419 /** 420 * Encodes the provided information into an octet string that can be used as 421 * the value for this control. 422 * 423 * @param pageSize The maximum number of entries that the server should 424 * return in the next page of the results. 425 * @param cookie The cookie provided by the server after returning the 426 * previous page of results, or {@code null} if this request 427 * will retrieve the first page of results. 428 * 429 * @return An ASN.1 octet string that can be used as the value for this 430 * control. 431 */ 432 @NotNull() 433 private static ASN1OctetString encodeValue(final int pageSize, 434 @Nullable final ASN1OctetString cookie) 435 { 436 final ASN1Element[] valueElements; 437 if (cookie == null) 438 { 439 valueElements = new ASN1Element[] 440 { 441 new ASN1Integer(pageSize), 442 new ASN1OctetString() 443 }; 444 } 445 else 446 { 447 valueElements = new ASN1Element[] 448 { 449 new ASN1Integer(pageSize), 450 cookie 451 }; 452 } 453 454 return new ASN1OctetString(new ASN1Sequence(valueElements).encode()); 455 } 456 457 458 459 /** 460 * Retrieves the size for this paged results control. For a request control, 461 * it may be used to specify the number of entries that should be included in 462 * the next page of results. For a response control, it may be used to 463 * specify the estimated number of entries in the complete result set. 464 * 465 * @return The size for this paged results control. 466 */ 467 public int getSize() 468 { 469 return size; 470 } 471 472 473 474 /** 475 * Retrieves the cookie for this control, which may be used in a subsequent 476 * request to resume reading entries from the next page of results. The 477 * value should have a length of zero when used to retrieve the first page of 478 * results for a given search, and also in the response from the server when 479 * there are no more entries to send. It should be non-empty for all other 480 * conditions. 481 * 482 * @return The cookie for this control, or an empty cookie (with a value 483 * length of zero) if there is none. 484 */ 485 @NotNull() 486 public ASN1OctetString getCookie() 487 { 488 return cookie; 489 } 490 491 492 493 /** 494 * Indicates whether there are more results to return as part of this search. 495 * 496 * @return {@code true} if there are more results to return, or 497 * {@code false} if not. 498 */ 499 public boolean moreResultsToReturn() 500 { 501 return (cookie.getValue().length > 0); 502 } 503 504 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override() 510 @NotNull() 511 public String getControlName() 512 { 513 return INFO_CONTROL_NAME_PAGED_RESULTS.get(); 514 } 515 516 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override() 522 public void toString(@NotNull final StringBuilder buffer) 523 { 524 buffer.append("SimplePagedResultsControl(pageSize="); 525 buffer.append(size); 526 buffer.append(", isCritical="); 527 buffer.append(isCritical()); 528 buffer.append(')'); 529 } 530}