001/* 002 * Copyright 2007-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2024 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-2024 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 java.util.LinkedHashMap; 041import java.util.List; 042import java.util.Map; 043 044import com.unboundid.asn1.ASN1Element; 045import com.unboundid.asn1.ASN1Enumerated; 046import com.unboundid.asn1.ASN1Exception; 047import com.unboundid.asn1.ASN1Integer; 048import com.unboundid.asn1.ASN1OctetString; 049import com.unboundid.asn1.ASN1Sequence; 050import com.unboundid.ldap.sdk.Control; 051import com.unboundid.ldap.sdk.DecodeableControl; 052import com.unboundid.ldap.sdk.JSONControlDecodeHelper; 053import com.unboundid.ldap.sdk.LDAPException; 054import com.unboundid.ldap.sdk.ResultCode; 055import com.unboundid.ldap.sdk.SearchResult; 056import com.unboundid.util.Base64; 057import com.unboundid.util.Debug; 058import com.unboundid.util.NotMutable; 059import com.unboundid.util.NotNull; 060import com.unboundid.util.Nullable; 061import com.unboundid.util.ThreadSafety; 062import com.unboundid.util.ThreadSafetyLevel; 063import com.unboundid.util.json.JSONField; 064import com.unboundid.util.json.JSONNumber; 065import com.unboundid.util.json.JSONObject; 066import com.unboundid.util.json.JSONString; 067import com.unboundid.util.json.JSONValue; 068 069import static com.unboundid.ldap.sdk.controls.ControlMessages.*; 070 071 072 073/** 074 * This class provides an implementation of the virtual list view (VLV) response 075 * control, as defined in draft-ietf-ldapext-ldapv3-vlv. It may be used to 076 * provide information about the result of virtual list view processing for a 077 * search containing the {@link VirtualListViewRequestControl}. 078 * <BR><BR> 079 * The virtual list view response control may include the following elements: 080 * <UL> 081 * <LI>{@code resultCode} -- A result code that indicates the result of the 082 * virtual list view processing. It may be the same as or different from 083 * the result code contained in the search result done message.</LI> 084 * <LI>{@code targetPosition} -- The offset of the target entry specified by 085 * the client in the result set.</LI> 086 * <LI>{@code contentCount} -- The estimated total number of entries in the 087 * entire result set.</LI> 088 * <LI>{@code contextID} -- An optional cookie that the client should include 089 * in the next request as part of the virtual list view sequence.</LI> 090 * </UL> 091 */ 092@NotMutable() 093@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 094public final class VirtualListViewResponseControl 095 extends Control 096 implements DecodeableControl 097{ 098 /** 099 * The OID (2.16.840.1.113730.3.4.10) for the virtual list view response 100 * control. 101 */ 102 @NotNull public static final String VIRTUAL_LIST_VIEW_RESPONSE_OID = 103 "2.16.840.1.113730.3.4.10"; 104 105 106 107 /** 108 * The name of the field used to hold the content count in the JSON 109 * representation of this control. 110 */ 111 @NotNull private static final String JSON_FIELD_CONTENT_COUNT = 112 "content-count"; 113 114 115 116 /** 117 * The name of the field used to hold the context ID in the JSON 118 * representation of this control. 119 */ 120 @NotNull private static final String JSON_FIELD_CONTEXT_ID = "context-id"; 121 122 123 124 /** 125 * The name of the field used to hold the result code in the JSON 126 * representation of this control. 127 */ 128 @NotNull private static final String JSON_FIELD_RESULT_CODE = "result-code"; 129 130 131 132 /** 133 * The name of the field used to hold the target position in the JSON 134 * representation of this control. 135 */ 136 @NotNull private static final String JSON_FIELD_TARGET_POSITION = 137 "target-position"; 138 139 140 141 /** 142 * The serial version UID for this serializable class. 143 */ 144 private static final long serialVersionUID = -534656674756287217L; 145 146 147 148 // The context ID for this VLV response control, if available. 149 @Nullable private final ASN1OctetString contextID; 150 151 // The estimated total number of entries in the result set. 152 private final int contentCount; 153 154 // The result code for this VLV response control. 155 @NotNull private final ResultCode resultCode; 156 157 // The offset of the target entry for this VLV response control. 158 private final int targetPosition; 159 160 161 162 /** 163 * Creates a new empty control instance that is intended to be used only for 164 * decoding controls via the {@code DecodeableControl} interface. 165 */ 166 VirtualListViewResponseControl() 167 { 168 targetPosition = -1; 169 contentCount = -1; 170 resultCode = null; 171 contextID = null; 172 } 173 174 175 176 /** 177 * Creates a new virtual list view response control with the provided 178 * information. It will not be marked critical. 179 * 180 * @param targetPosition The offset of the target entry for this VLV 181 * response control. 182 * @param contentCount The estimated total number of entries in the 183 * result set. 184 * @param resultCode The result code for this VLV response control. 185 * @param contextID The context ID for this VLV response control. It 186 * may be {@code null} if no context ID is available. 187 */ 188 public VirtualListViewResponseControl(final int targetPosition, 189 final int contentCount, @NotNull final ResultCode resultCode, 190 @Nullable final ASN1OctetString contextID) 191 { 192 super(VIRTUAL_LIST_VIEW_RESPONSE_OID, false, 193 encodeValue(targetPosition, contentCount, resultCode, contextID)); 194 195 this.targetPosition = targetPosition; 196 this.contentCount = contentCount; 197 this.resultCode = resultCode; 198 this.contextID = contextID; 199 } 200 201 202 203 /** 204 * Creates a new virtual list view response control from the information 205 * contained in the provided control. 206 * 207 * @param oid The OID for the control. 208 * @param isCritical Indicates whether the control should be marked 209 * critical. 210 * @param value The encoded value for the control. This may be 211 * {@code null} if no value was provided. 212 * 213 * @throws LDAPException If a problem occurs while attempting to decode the 214 * provided control as a virtual list view response 215 * control. 216 */ 217 public VirtualListViewResponseControl(@NotNull final String oid, 218 final boolean isCritical, 219 @Nullable final ASN1OctetString value) 220 throws LDAPException 221 { 222 super(oid, isCritical, value); 223 224 if (value == null) 225 { 226 throw new LDAPException(ResultCode.DECODING_ERROR, 227 ERR_VLV_RESPONSE_NO_VALUE.get()); 228 } 229 230 final ASN1Sequence valueSequence; 231 try 232 { 233 final ASN1Element valueElement = 234 ASN1Element.decode(value.getValue()); 235 valueSequence = ASN1Sequence.decodeAsSequence(valueElement); 236 } 237 catch (final ASN1Exception ae) 238 { 239 Debug.debugException(ae); 240 throw new LDAPException(ResultCode.DECODING_ERROR, 241 ERR_VLV_RESPONSE_VALUE_NOT_SEQUENCE.get(ae), ae); 242 } 243 244 final ASN1Element[] valueElements = valueSequence.elements(); 245 if ((valueElements.length < 3) || (valueElements.length > 4)) 246 { 247 throw new LDAPException(ResultCode.DECODING_ERROR, 248 ERR_VLV_RESPONSE_INVALID_ELEMENT_COUNT.get( 249 valueElements.length)); 250 } 251 252 try 253 { 254 targetPosition = ASN1Integer.decodeAsInteger(valueElements[0]).intValue(); 255 } 256 catch (final ASN1Exception ae) 257 { 258 Debug.debugException(ae); 259 throw new LDAPException(ResultCode.DECODING_ERROR, 260 ERR_VLV_RESPONSE_FIRST_NOT_INTEGER.get(ae), ae); 261 } 262 263 try 264 { 265 contentCount = ASN1Integer.decodeAsInteger(valueElements[1]).intValue(); 266 } 267 catch (final ASN1Exception ae) 268 { 269 Debug.debugException(ae); 270 throw new LDAPException(ResultCode.DECODING_ERROR, 271 ERR_VLV_RESPONSE_SECOND_NOT_INTEGER.get(ae), ae); 272 } 273 274 try 275 { 276 final int rc = 277 ASN1Enumerated.decodeAsEnumerated(valueElements[2]).intValue(); 278 resultCode = ResultCode.valueOf(rc); 279 } 280 catch (final ASN1Exception ae) 281 { 282 Debug.debugException(ae); 283 throw new LDAPException(ResultCode.DECODING_ERROR, 284 ERR_VLV_RESPONSE_THIRD_NOT_ENUM.get(ae), ae); 285 } 286 287 if (valueElements.length == 4) 288 { 289 contextID = ASN1OctetString.decodeAsOctetString(valueElements[3]); 290 } 291 else 292 { 293 contextID = null; 294 } 295 } 296 297 298 299 /** 300 * {@inheritDoc} 301 */ 302 @Override() 303 @NotNull() 304 public VirtualListViewResponseControl decodeControl(@NotNull final String oid, 305 final boolean isCritical, 306 @Nullable final ASN1OctetString value) 307 throws LDAPException 308 { 309 return new VirtualListViewResponseControl(oid, isCritical, value); 310 } 311 312 313 314 /** 315 * Extracts a virtual list view response control from the provided result. 316 * 317 * @param result The result from which to retrieve the virtual list view 318 * response control. 319 * 320 * @return The virtual list view response control contained in the provided 321 * result, or {@code null} if the result did not contain a virtual 322 * list view response control. 323 * 324 * @throws LDAPException If a problem is encountered while attempting to 325 * decode the virtual list view response control 326 * contained in the provided result. 327 */ 328 @Nullable() 329 public static VirtualListViewResponseControl get( 330 @NotNull final SearchResult result) 331 throws LDAPException 332 { 333 final Control c = result.getResponseControl(VIRTUAL_LIST_VIEW_RESPONSE_OID); 334 if (c == null) 335 { 336 return null; 337 } 338 339 if (c instanceof VirtualListViewResponseControl) 340 { 341 return (VirtualListViewResponseControl) c; 342 } 343 else 344 { 345 return new VirtualListViewResponseControl(c.getOID(), c.isCritical(), 346 c.getValue()); 347 } 348 } 349 350 351 352 /** 353 * Encodes the provided information into an octet string that can be used as 354 * the value for this control. 355 * 356 * @param targetPosition The offset of the target entry for this VLV 357 * response control. 358 * @param contentCount The estimated total number of entries in the 359 * result set. 360 * @param resultCode The result code for this VLV response control. 361 * @param contextID The context ID for this VLV response control. It 362 * may be {@code null} if no context ID is available. 363 * 364 * @return An ASN.1 octet string that can be used as the value for this 365 * control. 366 */ 367 @NotNull() 368 private static ASN1OctetString encodeValue(final int targetPosition, 369 final int contentCount, 370 @NotNull final ResultCode resultCode, 371 @Nullable final ASN1OctetString contextID) 372 { 373 final ASN1Element[] vlvElements; 374 if (contextID == null) 375 { 376 vlvElements = new ASN1Element[] 377 { 378 new ASN1Integer(targetPosition), 379 new ASN1Integer(contentCount), 380 new ASN1Enumerated(resultCode.intValue()) 381 }; 382 } 383 else 384 { 385 vlvElements = new ASN1Element[] 386 { 387 new ASN1Integer(targetPosition), 388 new ASN1Integer(contentCount), 389 new ASN1Enumerated(resultCode.intValue()), 390 contextID 391 }; 392 } 393 394 return new ASN1OctetString(new ASN1Sequence(vlvElements).encode()); 395 } 396 397 398 399 /** 400 * Retrieves the offset of the target entry for this virtual list view 401 * response control. 402 * 403 * @return The offset of the target entry for this virtual list view response 404 * control. 405 */ 406 public int getTargetPosition() 407 { 408 return targetPosition; 409 } 410 411 412 413 /** 414 * Retrieves the estimated total number of entries in the result set. 415 * 416 * @return The estimated total number of entries in the result set. 417 */ 418 public int getContentCount() 419 { 420 return contentCount; 421 } 422 423 424 425 /** 426 * Retrieves the result code for this virtual list view response control. 427 * 428 * @return The result code for this virtual list view response control. 429 */ 430 @NotNull() 431 public ResultCode getResultCode() 432 { 433 return resultCode; 434 } 435 436 437 438 /** 439 * Retrieves the context ID for this virtual list view response control, if 440 * available. 441 * 442 * @return The context ID for this virtual list view response control, or 443 * {@code null} if none was provided. 444 */ 445 @Nullable() 446 public ASN1OctetString getContextID() 447 { 448 return contextID; 449 } 450 451 452 453 /** 454 * {@inheritDoc} 455 */ 456 @Override() 457 @NotNull() 458 public String getControlName() 459 { 460 return INFO_CONTROL_NAME_VLV_RESPONSE.get(); 461 } 462 463 464 465 /** 466 * Retrieves a representation of this virtual list view response control as a 467 * JSON object. The JSON object uses the following fields: 468 * <UL> 469 * <LI> 470 * {@code oid} -- A mandatory string field whose value is the object 471 * identifier for this control. For the virtual list view response 472 * control, the OID is "2.16.840.1.113730.3.4.10". 473 * </LI> 474 * <LI> 475 * {@code control-name} -- An optional string field whose value is a 476 * human-readable name for this control. This field is only intended for 477 * descriptive purposes, and when decoding a control, the {@code oid} 478 * field should be used to identify the type of control. 479 * </LI> 480 * <LI> 481 * {@code criticality} -- A mandatory Boolean field used to indicate 482 * whether this control is considered critical. 483 * </LI> 484 * <LI> 485 * {@code value-base64} -- An optional string field whose value is a 486 * base64-encoded representation of the raw value for this virtual list 487 * view response control. Exactly one of the {@code value-base64} and 488 * {@code value-json} fields must be present. 489 * </LI> 490 * <LI> 491 * {@code value-json} -- An optional JSON object field whose value is a 492 * user-friendly representation of the value for this virtual list view 493 * response control. Exactly one of the {@code value-base64} and 494 * {@code value-json} fields must be present, and if the 495 * {@code value-json} field is used, then it will use the following 496 * fields: 497 * <UL> 498 * <LI> 499 * {@code result-code} -- An integer field whose value is the numeric 500 * representation of the result code for the virtual list view 501 * processing. 502 * </LI> 503 * <LI> 504 * {@code target-position} -- An integer field whose value is the 505 * offset for the targeted entry in the entire result set. 506 * processing. 507 * </LI> 508 * <LI> 509 * {@code content-count} -- An integer field whose value is the 510 * estimated total number of entries in the entire result set. 511 * </LI> 512 * <LI> 513 * {@code context-id} -- An optional string field whose value 514 * represents an opaque cookie that may be used to help the server 515 * continue returning results in the series of VLV searches. The 516 * context ID value used in the JSON representation of the control 517 * will be a base64-encoded representation of the raw cookie value 518 * that would be used in the LDAP representation of the control, and 519 * it must be treated as an opaque blob by the client. 520 * </LI> 521 * </UL> 522 * </LI> 523 * </UL> 524 * 525 * @return A JSON object that contains a representation of this control. 526 */ 527 @Override() 528 @NotNull() 529 public JSONObject toJSONControl() 530 { 531 final Map<String,JSONValue> valueFields = new LinkedHashMap<>(); 532 533 valueFields.put(JSON_FIELD_RESULT_CODE, 534 new JSONNumber(resultCode.intValue())); 535 valueFields.put(JSON_FIELD_TARGET_POSITION, new JSONNumber(targetPosition)); 536 valueFields.put(JSON_FIELD_CONTENT_COUNT, new JSONNumber(contentCount)); 537 538 if (contextID != null) 539 { 540 valueFields.put(JSON_FIELD_CONTEXT_ID, 541 new JSONString(Base64.encode(contextID.getValue()))); 542 } 543 544 return new JSONObject( 545 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, 546 VIRTUAL_LIST_VIEW_RESPONSE_OID), 547 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 548 INFO_CONTROL_NAME_VLV_RESPONSE.get()), 549 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 550 isCritical()), 551 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 552 new JSONObject(valueFields))); 553 } 554 555 556 557 /** 558 * Attempts to decode the provided object as a JSON representation of a 559 * virtual list view response control. 560 * 561 * @param controlObject The JSON object to be decoded. It must not be 562 * {@code null}. 563 * @param strict Indicates whether to use strict mode when decoding 564 * the provided JSON object. If this is {@code true}, 565 * then this method will throw an exception if the 566 * provided JSON object contains any unrecognized 567 * fields. If this is {@code false}, then unrecognized 568 * fields will be ignored. 569 * 570 * @return The virtual list view response control that was decoded from 571 * the provided JSON object. 572 * 573 * @throws LDAPException If the provided JSON object cannot be parsed as a 574 * valid virtual list view response control. 575 */ 576 @NotNull() 577 public static VirtualListViewResponseControl decodeJSONControl( 578 @NotNull final JSONObject controlObject, 579 final boolean strict) 580 throws LDAPException 581 { 582 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 583 controlObject, strict, true, true); 584 585 final ASN1OctetString rawValue = jsonControl.getRawValue(); 586 if (rawValue != null) 587 { 588 return new VirtualListViewResponseControl(jsonControl.getOID(), 589 jsonControl.getCriticality(), rawValue); 590 } 591 592 593 final JSONObject valueObject = jsonControl.getValueObject(); 594 595 final Integer resultCodeInt = 596 valueObject.getFieldAsInteger(JSON_FIELD_RESULT_CODE); 597 if (resultCodeInt == null) 598 { 599 throw new LDAPException(ResultCode.DECODING_ERROR, 600 ERR_VLV_RESPONSE_JSON_MISSING_FIELD.get( 601 controlObject.toSingleLineString(), JSON_FIELD_RESULT_CODE)); 602 } 603 604 final ResultCode resultCode = ResultCode.valueOf(resultCodeInt); 605 606 607 final Integer targetPosition = 608 valueObject.getFieldAsInteger(JSON_FIELD_TARGET_POSITION); 609 if (targetPosition == null) 610 { 611 throw new LDAPException(ResultCode.DECODING_ERROR, 612 ERR_VLV_RESPONSE_JSON_MISSING_FIELD.get( 613 controlObject.toSingleLineString(), 614 JSON_FIELD_TARGET_POSITION)); 615 } 616 617 618 final Integer contentCount = 619 valueObject.getFieldAsInteger(JSON_FIELD_CONTENT_COUNT); 620 if (contentCount == null) 621 { 622 throw new LDAPException(ResultCode.DECODING_ERROR, 623 ERR_VLV_RESPONSE_JSON_MISSING_FIELD.get( 624 controlObject.toSingleLineString(), 625 JSON_FIELD_CONTENT_COUNT)); 626 } 627 628 629 final ASN1OctetString contextID; 630 final String contextIDBase64 = 631 valueObject.getFieldAsString(JSON_FIELD_CONTEXT_ID); 632 if (contextIDBase64 == null) 633 { 634 contextID = null; 635 } 636 else 637 { 638 try 639 { 640 contextID = new ASN1OctetString(Base64.decode(contextIDBase64)); 641 } 642 catch (final Exception e) 643 { 644 Debug.debugException(e); 645 throw new LDAPException(ResultCode.DECODING_ERROR, 646 ERR_VLV_RESPONSE_JSON_CONTEXT_ID_NOT_BASE64.get( 647 controlObject.toSingleLineString(), JSON_FIELD_CONTEXT_ID), 648 e); 649 } 650 } 651 652 653 if (strict) 654 { 655 final List<String> unrecognizedFields = 656 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 657 valueObject, JSON_FIELD_RESULT_CODE, JSON_FIELD_TARGET_POSITION, 658 JSON_FIELD_CONTENT_COUNT, JSON_FIELD_CONTEXT_ID); 659 if (! unrecognizedFields.isEmpty()) 660 { 661 throw new LDAPException(ResultCode.DECODING_ERROR, 662 ERR_VLV_RESPONSE_JSON_UNRECOGNIZED_FIELD.get( 663 controlObject.toSingleLineString(), 664 unrecognizedFields.get(0))); 665 } 666 } 667 668 669 return new VirtualListViewResponseControl(targetPosition, contentCount, 670 resultCode, contextID); 671 } 672 673 674 675 /** 676 * {@inheritDoc} 677 */ 678 @Override() 679 public void toString(@NotNull final StringBuilder buffer) 680 { 681 buffer.append("VirtualListViewResponseControl(targetPosition="); 682 buffer.append(targetPosition); 683 buffer.append(", contentCount="); 684 buffer.append(contentCount); 685 buffer.append(", resultCode="); 686 buffer.append(resultCode); 687 buffer.append(')'); 688 } 689}