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.util.StaticUtils.byteToHex; 030import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage; 031import static com.forgerock.opendj.ldap.CoreMessages.ERR_VLVREQ_CONTROL_BAD_OID; 032import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE; 033import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE; 034import static com.forgerock.opendj.ldap.CoreMessages.INFO_VLVREQ_CONTROL_NO_VALUE; 035 036import java.io.IOException; 037 038import org.forgerock.i18n.LocalizableMessage; 039import org.forgerock.opendj.io.ASN1; 040import org.forgerock.opendj.io.ASN1Reader; 041import org.forgerock.opendj.io.ASN1Writer; 042import org.forgerock.opendj.ldap.ByteString; 043import org.forgerock.opendj.ldap.ByteStringBuilder; 044import org.forgerock.opendj.ldap.DecodeException; 045import org.forgerock.opendj.ldap.DecodeOptions; 046import org.forgerock.util.Reject; 047 048/** 049 * The virtual list view request control as defined in 050 * draft-ietf-ldapext-ldapv3-vlv. This control allows a client to specify that 051 * the server return, for a given search request with associated sort keys, a 052 * contiguous subset of the search result set. This subset is specified in terms 053 * of offsets into the ordered list, or in terms of a greater than or equal 054 * assertion value. 055 * <p> 056 * This control must be used in conjunction with the server-side sort request 057 * control in order to ensure that results are returned in a consistent order. 058 * <p> 059 * This control is similar to the simple paged results request control, except 060 * that it allows the client to move backwards and forwards in the result set. 061 * <p> 062 * The following example demonstrates use of the virtual list view controls. 063 * 064 * <pre> 065 * ByteString contextID = ByteString.empty(); 066 * 067 * // Add a window of 2 entries on either side of the first sn=Jensen entry. 068 * SearchRequest request = Requests.newSearchRequest("ou=People,dc=example,dc=com", 069 * SearchScope.WHOLE_SUBTREE, "(sn=*)", "sn", "givenName") 070 * .addControl(ServerSideSortRequestControl.newControl(true, new SortKey("sn"))) 071 * .addControl(VirtualListViewRequestControl.newAssertionControl( 072 * true, ByteString.valueOf("Jensen"), 2, 2, contextID)); 073 * 074 * SearchResultHandler resultHandler = new MySearchResultHandler(); 075 * Result result = connection.search(request, resultHandler); 076 * 077 * ServerSideSortResponseControl sssControl = 078 * result.getControl(ServerSideSortResponseControl.DECODER, new DecodeOptions()); 079 * if (sssControl != null && sssControl.getResult() == ResultCode.SUCCESS) { 080 * // Entries are sorted. 081 * } else { 082 * // Entries not necessarily sorted 083 * } 084 * 085 * VirtualListViewResponseControl vlvControl = 086 * result.getControl(VirtualListViewResponseControl.DECODER, new DecodeOptions()); 087 * // Position in list: vlvControl.getTargetPosition()/vlvControl.getContentCount() 088 * </pre> 089 * 090 * The search result handler in this case displays pages of results as LDIF on 091 * standard out. 092 * 093 * <pre> 094 * private static class MySearchResultHandler implements SearchResultHandler { 095 * 096 * {@literal @}Override 097 * public void handleExceptionResult(LdapException error) { 098 * // Ignore. 099 * } 100 * 101 * {@literal @}Override 102 * public void handleResult(Result result) { 103 * // Ignore. 104 * } 105 * 106 * {@literal @}Override 107 * public boolean handleEntry(SearchResultEntry entry) { 108 * final LDIFEntryWriter writer = new LDIFEntryWriter(System.out); 109 * try { 110 * writer.writeEntry(entry); 111 * writer.flush(); 112 * } catch (final IOException e) { 113 * // The writer could not write to System.out. 114 * } 115 * return true; 116 * } 117 * 118 * {@literal @}Override 119 * public boolean handleReference(SearchResultReference reference) { 120 * System.out.println("Got a reference: " + reference.toString()); 121 * return false; 122 * } 123 * } 124 * </pre> 125 * 126 * @see VirtualListViewResponseControl 127 * @see ServerSideSortRequestControl 128 * @see <a href="http://tools.ietf.org/html/draft-ietf-ldapext-ldapv3-vlv"> 129 * draft-ietf-ldapext-ldapv3-vlv - LDAP Extensions for Scrolling View 130 * Browsing of Search Results </a> 131 */ 132public final class VirtualListViewRequestControl implements Control { 133 /** 134 * The OID for the virtual list view request control. 135 */ 136 public static final String OID = "2.16.840.1.113730.3.4.9"; 137 138 /** 139 * A decoder which can be used for decoding the virtual list view request 140 * control. 141 */ 142 public static final ControlDecoder<VirtualListViewRequestControl> DECODER = 143 new ControlDecoder<VirtualListViewRequestControl>() { 144 145 public VirtualListViewRequestControl decodeControl(final Control control, 146 final DecodeOptions options) throws DecodeException { 147 Reject.ifNull(control); 148 149 if (control instanceof VirtualListViewRequestControl) { 150 return (VirtualListViewRequestControl) control; 151 } 152 153 if (!control.getOID().equals(OID)) { 154 final LocalizableMessage message = 155 ERR_VLVREQ_CONTROL_BAD_OID.get(control.getOID(), OID); 156 throw DecodeException.error(message); 157 } 158 159 if (!control.hasValue()) { 160 // The request control must always have a value. 161 final LocalizableMessage message = INFO_VLVREQ_CONTROL_NO_VALUE.get(); 162 throw DecodeException.error(message); 163 } 164 165 final ASN1Reader reader = ASN1.getReader(control.getValue()); 166 try { 167 reader.readStartSequence(); 168 169 final int beforeCount = (int) reader.readInteger(); 170 final int afterCount = (int) reader.readInteger(); 171 172 int offset = -1; 173 int contentCount = -1; 174 ByteString assertionValue = null; 175 final byte targetType = reader.peekType(); 176 switch (targetType) { 177 case TYPE_TARGET_BYOFFSET: 178 reader.readStartSequence(); 179 offset = (int) reader.readInteger(); 180 contentCount = (int) reader.readInteger(); 181 reader.readEndSequence(); 182 break; 183 case TYPE_TARGET_GREATERTHANOREQUAL: 184 assertionValue = reader.readOctetString(); 185 break; 186 default: 187 final LocalizableMessage message = 188 INFO_VLVREQ_CONTROL_INVALID_TARGET_TYPE 189 .get(byteToHex(targetType)); 190 throw DecodeException.error(message); 191 } 192 193 ByteString contextID = null; 194 if (reader.hasNextElement()) { 195 contextID = reader.readOctetString(); 196 } 197 198 return new VirtualListViewRequestControl(control.isCritical(), beforeCount, 199 afterCount, contentCount, offset, assertionValue, contextID); 200 } catch (final IOException e) { 201 final LocalizableMessage message = 202 INFO_VLVREQ_CONTROL_CANNOT_DECODE_VALUE.get(getExceptionMessage(e)); 203 throw DecodeException.error(message, e); 204 } 205 } 206 207 public String getOID() { 208 return OID; 209 } 210 }; 211 212 /** 213 * The BER type to use when encoding the byOffset target element. 214 */ 215 private static final byte TYPE_TARGET_BYOFFSET = (byte) 0xA0; 216 217 /** 218 * The BER type to use when encoding the greaterThanOrEqual target element. 219 */ 220 private static final byte TYPE_TARGET_GREATERTHANOREQUAL = (byte) 0x81; 221 222 /** 223 * Creates a new virtual list view request control that will identify the 224 * target entry by an assertion value. The assertion value is encoded 225 * according to the ORDERING matching rule for the attribute description in 226 * the sort control. The assertion value is used to determine the target 227 * entry by comparison with the values of the attribute specified as the 228 * primary sort key. The first list entry who's value is no less than (less 229 * than or equal to when the sort order is reversed) the supplied value is 230 * the target entry. 231 * 232 * @param isCritical 233 * {@code true} if it is unacceptable to perform the operation 234 * without applying the semantics of this control, or 235 * {@code false} if it can be ignored. 236 * @param assertionValue 237 * The assertion value that will be used to locate the target 238 * entry. 239 * @param beforeCount 240 * The number of entries before the target entry to be included 241 * in the search results. 242 * @param afterCount 243 * The number of entries after the target entry to be included in 244 * the search results. 245 * @param contextID 246 * The context ID provided by the server in the last virtual list 247 * view response for the same set of criteria, or {@code null} if 248 * there was no previous virtual list view response or the server 249 * did not include a context ID in the last response. 250 * @return The new control. 251 * @throws IllegalArgumentException 252 * If {@code beforeCount} or {@code afterCount} were less than 253 * {@code 0}. 254 * @throws NullPointerException 255 * If {@code assertionValue} was {@code null}. 256 */ 257 public static VirtualListViewRequestControl newAssertionControl(final boolean isCritical, 258 final ByteString assertionValue, final int beforeCount, final int afterCount, 259 final ByteString contextID) { 260 Reject.ifNull(assertionValue); 261 Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0"); 262 Reject.ifFalse(afterCount >= 0, "afterCount is less than 0"); 263 264 return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, -1, -1, 265 assertionValue, contextID); 266 } 267 268 /** 269 * Creates a new virtual list view request control that will identify the 270 * target entry by a positional offset within the complete result set. 271 * 272 * @param isCritical 273 * {@code true} if it is unacceptable to perform the operation 274 * without applying the semantics of this control, or 275 * {@code false} if it can be ignored. 276 * @param offset 277 * The positional offset of the target entry in the result set, 278 * where {@code 1} is the first entry. 279 * @param contentCount 280 * The content count returned by the server in the last virtual 281 * list view response, or {@code 0} if this is the first virtual 282 * list view request. 283 * @param beforeCount 284 * The number of entries before the target entry to be included 285 * in the search results. 286 * @param afterCount 287 * The number of entries after the target entry to be included in 288 * the search results. 289 * @param contextID 290 * The context ID provided by the server in the last virtual list 291 * view response for the same set of criteria, or {@code null} if 292 * there was no previous virtual list view response or the server 293 * did not include a context ID in the last response. 294 * @return The new control. 295 * @throws IllegalArgumentException 296 * If {@code beforeCount}, {@code afterCount}, or 297 * {@code contentCount} were less than {@code 0}, or if 298 * {@code offset} was less than {@code 1}. 299 */ 300 public static VirtualListViewRequestControl newOffsetControl(final boolean isCritical, 301 final int offset, final int contentCount, final int beforeCount, final int afterCount, 302 final ByteString contextID) { 303 Reject.ifFalse(beforeCount >= 0, "beforeCount is less than 0"); 304 Reject.ifFalse(afterCount >= 0, "afterCount is less than 0"); 305 Reject.ifFalse(offset > 0, "offset is less than 1"); 306 Reject.ifFalse(contentCount >= 0, "contentCount is less than 0"); 307 308 return new VirtualListViewRequestControl(isCritical, beforeCount, afterCount, contentCount, 309 offset, null, contextID); 310 } 311 312 private final int beforeCount; 313 314 private final int afterCount; 315 316 private final ByteString contextID; 317 318 private final boolean isCritical; 319 320 private final int contentCount; 321 322 private final int offset; 323 324 private final ByteString assertionValue; 325 326 private VirtualListViewRequestControl(final boolean isCritical, final int beforeCount, 327 final int afterCount, final int contentCount, final int offset, 328 final ByteString assertionValue, final ByteString contextID) { 329 this.isCritical = isCritical; 330 this.beforeCount = beforeCount; 331 this.afterCount = afterCount; 332 this.contentCount = contentCount; 333 this.offset = offset; 334 this.assertionValue = assertionValue; 335 this.contextID = contextID; 336 } 337 338 /** 339 * Returns the number of entries after the target entry to be included in 340 * the search results. 341 * 342 * @return The number of entries after the target entry to be included in 343 * the search results. 344 */ 345 public int getAfterCount() { 346 return afterCount; 347 } 348 349 /** 350 * Returns the assertion value that will be used to locate the target entry, 351 * if applicable. 352 * 353 * @return The assertion value that will be used to locate the target entry, 354 * or {@code null} if this control is using a target offset. 355 */ 356 public ByteString getAssertionValue() { 357 return assertionValue; 358 } 359 360 /** 361 * Returns the assertion value that will be used to locate the target entry, 362 * if applicable, decoded as a UTF-8 string. 363 * 364 * @return The assertion value that will be used to locate the target entry 365 * decoded as a UTF-8 string, or {@code null} if this control is 366 * using a target offset. 367 */ 368 public String getAssertionValueAsString() { 369 return assertionValue != null ? assertionValue.toString() : null; 370 } 371 372 /** 373 * Returns the number of entries before the target entry to be included in 374 * the search results. 375 * 376 * @return The number of entries before the target entry to be included in 377 * the search results. 378 */ 379 public int getBeforeCount() { 380 return beforeCount; 381 } 382 383 /** 384 * Returns the content count returned by the server in the last virtual list 385 * view response, if applicable. 386 * 387 * @return The content count returned by the server in the last virtual list 388 * view response, which may be {@code 0} if this is the first 389 * virtual list view request, or {@code -1} if this control is using 390 * a target assertion. 391 */ 392 public int getContentCount() { 393 return contentCount; 394 } 395 396 /** 397 * Returns the context ID provided by the server in the last virtual list 398 * view response for the same set of criteria, or {@code null} if there was 399 * no previous virtual list view response or the server did not include a 400 * context ID in the last response. 401 * 402 * @return The context ID provided by the server in the last virtual list 403 * view response, or {@code null} if unavailable. 404 */ 405 public ByteString getContextID() { 406 return contextID; 407 } 408 409 /** 410 * Returns the positional offset of the target entry in the result set, if 411 * applicable, where {@code 1} is the first entry. 412 * 413 * @return The positional offset of the target entry in the result set, or 414 * {@code -1} if this control is using a target assertion. 415 */ 416 public int getOffset() { 417 return offset; 418 } 419 420 /** {@inheritDoc} */ 421 public String getOID() { 422 return OID; 423 } 424 425 /** {@inheritDoc} */ 426 public ByteString getValue() { 427 final ByteStringBuilder buffer = new ByteStringBuilder(); 428 final ASN1Writer writer = ASN1.getWriter(buffer); 429 try { 430 writer.writeStartSequence(); 431 writer.writeInteger(beforeCount); 432 writer.writeInteger(afterCount); 433 if (hasTargetOffset()) { 434 writer.writeStartSequence(TYPE_TARGET_BYOFFSET); 435 writer.writeInteger(offset); 436 writer.writeInteger(contentCount); 437 writer.writeEndSequence(); 438 } else { 439 writer.writeOctetString(TYPE_TARGET_GREATERTHANOREQUAL, assertionValue); 440 } 441 if (contextID != null) { 442 writer.writeOctetString(contextID); 443 } 444 writer.writeEndSequence(); 445 return buffer.toByteString(); 446 } catch (final IOException ioe) { 447 // This should never happen unless there is a bug somewhere. 448 throw new RuntimeException(ioe); 449 } 450 } 451 452 /** 453 * Returns {@code true} if this control is using a target offset, or 454 * {@code false} if this control is using a target assertion. 455 * 456 * @return {@code true} if this control is using a target offset, or 457 * {@code false} if this control is using a target assertion. 458 */ 459 public boolean hasTargetOffset() { 460 return assertionValue == null; 461 } 462 463 /** {@inheritDoc} */ 464 public boolean hasValue() { 465 return true; 466 } 467 468 /** {@inheritDoc} */ 469 public boolean isCritical() { 470 return isCritical; 471 } 472 473 /** {@inheritDoc} */ 474 @Override 475 public String toString() { 476 final StringBuilder builder = new StringBuilder(); 477 builder.append("VirtualListViewRequestControl(oid="); 478 builder.append(getOID()); 479 builder.append(", criticality="); 480 builder.append(isCritical()); 481 builder.append(", beforeCount="); 482 builder.append(beforeCount); 483 builder.append(", afterCount="); 484 builder.append(afterCount); 485 if (hasTargetOffset()) { 486 builder.append(", offset="); 487 builder.append(offset); 488 builder.append(", contentCount="); 489 builder.append(contentCount); 490 } else { 491 builder.append(", greaterThanOrEqual="); 492 builder.append(assertionValue); 493 } 494 if (contextID != null) { 495 builder.append(", contextID="); 496 builder.append(contextID); 497 } 498 builder.append(")"); 499 return builder.toString(); 500 } 501}