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