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.ArrayList; 041import java.util.LinkedHashMap; 042import java.util.List; 043import java.util.Map; 044 045import com.unboundid.asn1.ASN1Element; 046import com.unboundid.asn1.ASN1OctetString; 047import com.unboundid.asn1.ASN1Sequence; 048import com.unboundid.ldap.sdk.Control; 049import com.unboundid.ldap.sdk.JSONControlDecodeHelper; 050import com.unboundid.ldap.sdk.LDAPException; 051import com.unboundid.ldap.sdk.ResultCode; 052import com.unboundid.util.Debug; 053import com.unboundid.util.NotMutable; 054import com.unboundid.util.NotNull; 055import com.unboundid.util.ThreadSafety; 056import com.unboundid.util.ThreadSafetyLevel; 057import com.unboundid.util.Validator; 058import com.unboundid.util.json.JSONArray; 059import com.unboundid.util.json.JSONBoolean; 060import com.unboundid.util.json.JSONField; 061import com.unboundid.util.json.JSONObject; 062import com.unboundid.util.json.JSONString; 063import com.unboundid.util.json.JSONValue; 064 065import static com.unboundid.ldap.sdk.controls.ControlMessages.*; 066 067 068 069/** 070 * This class provides an implementation of the server-side sort request 071 * control, as defined in 072 * <A HREF="http://www.ietf.org/rfc/rfc2891.txt">RFC 2891</A>. It may be 073 * included in a search request to indicate that the server should sort the 074 * results before returning them to the client. 075 * <BR><BR> 076 * The order in which the entries are to be sorted is specified by one or more 077 * {@link SortKey} values. Each sort key includes an attribute name and a flag 078 * that indicates whether to sort in ascending or descending order. It may also 079 * specify a custom matching rule that should be used to specify which logic 080 * should be used to perform the sorting. 081 * <BR><BR> 082 * If the search is successful, then the search result done message may include 083 * the {@link ServerSideSortResponseControl} to provide information about the 084 * status of the sort processing. 085 * <BR><BR> 086 * <H2>Example</H2> 087 * The following example demonstrates the use of the server-side sort controls 088 * to retrieve users in different sort orders. 089 * <PRE> 090 * // Perform a search to get all user entries sorted by last name, then by 091 * // first name, both in ascending order. 092 * SearchRequest searchRequest = new SearchRequest( 093 * "ou=People,dc=example,dc=com", SearchScope.SUB, 094 * Filter.createEqualityFilter("objectClass", "person")); 095 * searchRequest.addControl(new ServerSideSortRequestControl( 096 * new SortKey("sn"), new SortKey("givenName"))); 097 * SearchResult lastNameAscendingResult; 098 * try 099 * { 100 * lastNameAscendingResult = connection.search(searchRequest); 101 * // If we got here, then the search was successful. 102 * } 103 * catch (LDAPSearchException lse) 104 * { 105 * // The search failed for some reason. 106 * lastNameAscendingResult = lse.getSearchResult(); 107 * ResultCode resultCode = lse.getResultCode(); 108 * String errorMessageFromServer = lse.getDiagnosticMessage(); 109 * } 110 * 111 * // Get the response control and retrieve the result code for the sort 112 * // processing. 113 * LDAPTestUtils.assertHasControl(lastNameAscendingResult, 114 * ServerSideSortResponseControl.SERVER_SIDE_SORT_RESPONSE_OID); 115 * ServerSideSortResponseControl lastNameAscendingResponseControl = 116 * ServerSideSortResponseControl.get(lastNameAscendingResult); 117 * ResultCode lastNameSortResult = 118 * lastNameAscendingResponseControl.getResultCode(); 119 * 120 * 121 * // Perform the same search, but this time request the results to be sorted 122 * // in descending order by first name, then last name. 123 * searchRequest.setControls(new ServerSideSortRequestControl( 124 * new SortKey("givenName", true), new SortKey("sn", true))); 125 * SearchResult firstNameDescendingResult; 126 * try 127 * { 128 * firstNameDescendingResult = connection.search(searchRequest); 129 * // If we got here, then the search was successful. 130 * } 131 * catch (LDAPSearchException lse) 132 * { 133 * // The search failed for some reason. 134 * firstNameDescendingResult = lse.getSearchResult(); 135 * ResultCode resultCode = lse.getResultCode(); 136 * String errorMessageFromServer = lse.getDiagnosticMessage(); 137 * } 138 * 139 * // Get the response control and retrieve the result code for the sort 140 * // processing. 141 * LDAPTestUtils.assertHasControl(firstNameDescendingResult, 142 * ServerSideSortResponseControl.SERVER_SIDE_SORT_RESPONSE_OID); 143 * ServerSideSortResponseControl firstNameDescendingResponseControl = 144 * ServerSideSortResponseControl.get(firstNameDescendingResult); 145 * ResultCode firstNameSortResult = 146 * firstNameDescendingResponseControl.getResultCode(); 147 * </PRE> 148 * <BR><BR> 149 * <H2>Client-Side Sorting</H2> 150 * The UnboundID LDAP SDK for Java provides support for client-side sorting as 151 * an alternative to server-side sorting. Client-side sorting may be useful in 152 * cases in which the target server does not support the use of the server-side 153 * sort control, or when it is desirable to perform the sort processing on the 154 * client systems rather than on the directory server systems. See the 155 * {@link com.unboundid.ldap.sdk.EntrySorter} class for details on performing 156 * client-side sorting in the LDAP SDK. 157 */ 158@NotMutable() 159@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 160public final class ServerSideSortRequestControl 161 extends Control 162{ 163 /** 164 * The OID (1.2.840.113556.1.4.473) for the server-side sort request control. 165 */ 166 @NotNull public static final String SERVER_SIDE_SORT_REQUEST_OID = 167 "1.2.840.113556.1.4.473"; 168 169 170 171 /** 172 * The name of the field used to hold the attribute name in the JSON 173 * representation of this control. 174 */ 175 @NotNull private static final String JSON_FIELD_ATTRIBUTE_NAME = 176 "attribute-name"; 177 178 179 180 /** 181 * The name of the field used to hold the matching rule ID in the JSON 182 * representation of this control. 183 */ 184 @NotNull private static final String JSON_FIELD_MATCHING_RULE_ID = 185 "matching-rule-id"; 186 187 188 189 /** 190 * The name of the field used to hold the reverse-order flag in the JSON 191 * representation of this control. 192 */ 193 @NotNull private static final String JSON_FIELD_REVERSE_ORDER = 194 "reverse-order"; 195 196 197 198 /** 199 * The name of the field used to hold the sort keys in the JSON representation 200 * of this control. 201 */ 202 @NotNull private static final String JSON_FIELD_SORT_KEYS = "sort-keys"; 203 204 205 206 /** 207 * The serial version UID for this serializable class. 208 */ 209 private static final long serialVersionUID = -3021901578330574772L; 210 211 212 213 // The set of sort keys to use with this control. 214 @NotNull private final SortKey[] sortKeys; 215 216 217 218 /** 219 * Creates a new server-side sort control that will sort the results based on 220 * the provided set of sort keys. 221 * 222 * @param sortKeys The set of sort keys to define the desired order in which 223 * the results should be returned. It must not be 224 * {@code null} or empty. 225 */ 226 public ServerSideSortRequestControl(@NotNull final SortKey... sortKeys) 227 { 228 this(false, sortKeys); 229 } 230 231 232 233 /** 234 * Creates a new server-side sort control that will sort the results based on 235 * the provided set of sort keys. 236 * 237 * @param sortKeys The set of sort keys to define the desired order in which 238 * the results should be returned. It must not be 239 * {@code null} or empty. 240 */ 241 public ServerSideSortRequestControl(@NotNull final List<SortKey> sortKeys) 242 { 243 this(false, sortKeys); 244 } 245 246 247 248 /** 249 * Creates a new server-side sort control that will sort the results based on 250 * the provided set of sort keys. 251 * 252 * @param isCritical Indicates whether this control should be marked 253 * critical. 254 * @param sortKeys The set of sort keys to define the desired order in 255 * which the results should be returned. It must not be 256 * {@code null} or empty. 257 */ 258 public ServerSideSortRequestControl(final boolean isCritical, 259 @NotNull final SortKey... sortKeys) 260 { 261 super(SERVER_SIDE_SORT_REQUEST_OID, isCritical, encodeValue(sortKeys)); 262 263 this.sortKeys = sortKeys; 264 } 265 266 267 268 /** 269 * Creates a new server-side sort control that will sort the results based on 270 * the provided set of sort keys. 271 * 272 * @param isCritical Indicates whether this control should be marked 273 * critical. 274 * @param sortKeys The set of sort keys to define the desired order in 275 * which the results should be returned. It must not be 276 * {@code null} or empty. 277 */ 278 public ServerSideSortRequestControl(final boolean isCritical, 279 @NotNull final List<SortKey> sortKeys) 280 { 281 this(isCritical, sortKeys.toArray(new SortKey[sortKeys.size()])); 282 } 283 284 285 286 /** 287 * Creates a new server-side sort request control which is decoded from the 288 * provided generic control. 289 * 290 * @param control The generic control to be decoded as a server-side sort 291 * request control. 292 * 293 * @throws LDAPException If the provided control cannot be decoded as a 294 * server-side sort request control. 295 */ 296 public ServerSideSortRequestControl(@NotNull final Control control) 297 throws LDAPException 298 { 299 super(control); 300 301 final ASN1OctetString value = control.getValue(); 302 if (value == null) 303 { 304 throw new LDAPException(ResultCode.DECODING_ERROR, 305 ERR_SORT_REQUEST_NO_VALUE.get()); 306 } 307 308 try 309 { 310 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 311 final ASN1Element[] elements = 312 ASN1Sequence.decodeAsSequence(valueElement).elements(); 313 sortKeys = new SortKey[elements.length]; 314 for (int i=0; i < elements.length; i++) 315 { 316 sortKeys[i] = SortKey.decode(elements[i]); 317 } 318 } 319 catch (final Exception e) 320 { 321 Debug.debugException(e); 322 throw new LDAPException(ResultCode.DECODING_ERROR, 323 ERR_SORT_REQUEST_CANNOT_DECODE.get(e), e); 324 } 325 } 326 327 328 329 /** 330 * Encodes the provided information into an octet string that can be used as 331 * the value for this control. 332 * 333 * @param sortKeys The set of sort keys to define the desired order in which 334 * the results should be returned. It must not be 335 * {@code null} or empty. 336 * 337 * @return An ASN.1 octet string that can be used as the value for this 338 * control. 339 */ 340 @NotNull() 341 private static ASN1OctetString encodeValue(@NotNull final SortKey[] sortKeys) 342 { 343 Validator.ensureNotNull(sortKeys); 344 Validator.ensureTrue(sortKeys.length > 0, 345 "ServerSideSortRequestControl.sortKeys must not be empty."); 346 347 final ASN1Element[] valueElements = new ASN1Element[sortKeys.length]; 348 for (int i=0; i < sortKeys.length; i++) 349 { 350 valueElements[i] = sortKeys[i].encode(); 351 } 352 353 return new ASN1OctetString(new ASN1Sequence(valueElements).encode()); 354 } 355 356 357 358 /** 359 * Retrieves the set of sort keys that define the desired order in which the 360 * results should be returned. 361 * 362 * @return The set of sort keys that define the desired order in which the 363 * results should be returned. 364 */ 365 @NotNull() 366 public SortKey[] getSortKeys() 367 { 368 return sortKeys; 369 } 370 371 372 373 /** 374 * {@inheritDoc} 375 */ 376 @Override() 377 @NotNull() 378 public String getControlName() 379 { 380 return INFO_CONTROL_NAME_SORT_REQUEST.get(); 381 } 382 383 384 385 /** 386 * Retrieves a representation of this server-side sort request control as a 387 * JSON object. The JSON object uses the following fields: 388 * <UL> 389 * <LI> 390 * {@code oid} -- A mandatory string field whose value is the object 391 * identifier for this control. For the server-side sort request control, 392 * the OID is "1.2.840.113556.1.4.473". 393 * </LI> 394 * <LI> 395 * {@code control-name} -- An optional string field whose value is a 396 * human-readable name for this control. This field is only intended for 397 * descriptive purposes, and when decoding a control, the {@code oid} 398 * field should be used to identify the type of control. 399 * </LI> 400 * <LI> 401 * {@code criticality} -- A mandatory Boolean field used to indicate 402 * whether this control is considered critical. 403 * </LI> 404 * <LI> 405 * {@code value-base64} -- An optional string field whose value is a 406 * base64-encoded representation of the raw value for this server-side 407 * sort request control. Exactly one of the {@code value-base64} and 408 * {@code value-json} fields must be present. 409 * </LI> 410 * <LI> 411 * {@code value-json} -- An optional JSON object field whose value is a 412 * user-friendly representation of the value for this server-side sort 413 * request control. Exactly one of the {@code value-base64} and 414 * {@code value-json} fields must be present, and if the 415 * {@code value-json} field is used, then it will use the following 416 * fields: 417 * <UL> 418 * <LI> 419 * {@code sort-keys} -- A mandatory array field whose values are JSON 420 * objects used to specify the requested sort order. Each of the JSON 421 * objects with the following fields: 422 * <UL> 423 * <LI> 424 * {@code attribute-name} -- A mandatory string field whose value 425 * is the name of the attribute to use for sorting. 426 * </LI> 427 * <LI> 428 * {@code reverse-order} -- A mandatory Boolean field that 429 * indicates whether the results should be sorted in descending 430 * order rather than ascending. 431 * </LI> 432 * <LI> 433 * {@code matching-rule-id} -- An optional string field whose 434 * value is the name or OID of the ordering matching rule to use 435 * to perform the sorting. 436 * </LI> 437 * </UL> 438 * </LI> 439 * </UL> 440 * </LI> 441 * </UL> 442 * 443 * @return A JSON object that contains a representation of this control. 444 */ 445 @Override() 446 @NotNull() 447 public JSONObject toJSONControl() 448 { 449 final List<JSONValue> sortKeyValues = new ArrayList<>(sortKeys.length); 450 for (final SortKey sortKey : sortKeys) 451 { 452 final Map<String,JSONValue> fields = new LinkedHashMap<>(); 453 fields.put(JSON_FIELD_ATTRIBUTE_NAME, 454 new JSONString(sortKey.getAttributeName())); 455 fields.put(JSON_FIELD_REVERSE_ORDER, 456 new JSONBoolean(sortKey.reverseOrder())); 457 458 if (sortKey.getMatchingRuleID() != null) 459 { 460 fields.put(JSON_FIELD_MATCHING_RULE_ID, 461 new JSONString(sortKey.getMatchingRuleID())); 462 } 463 464 sortKeyValues.add(new JSONObject(fields)); 465 } 466 467 return new JSONObject( 468 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, 469 SERVER_SIDE_SORT_REQUEST_OID), 470 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 471 INFO_CONTROL_NAME_SORT_REQUEST.get()), 472 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 473 isCritical()), 474 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 475 new JSONObject( 476 new JSONField(JSON_FIELD_SORT_KEYS, 477 new JSONArray(sortKeyValues))))); 478 } 479 480 481 482 /** 483 * Attempts to decode the provided object as a JSON representation of a 484 * server-side sort request control. 485 * 486 * @param controlObject The JSON object to be decoded. It must not be 487 * {@code null}. 488 * @param strict Indicates whether to use strict mode when decoding 489 * the provided JSON object. If this is {@code true}, 490 * then this method will throw an exception if the 491 * provided JSON object contains any unrecognized 492 * fields. If this is {@code false}, then unrecognized 493 * fields will be ignored. 494 * 495 * @return The server-side sort request control that was decoded from 496 * the provided JSON object. 497 * 498 * @throws LDAPException If the provided JSON object cannot be parsed as a 499 * valid server-side sort request control. 500 */ 501 @NotNull() 502 public static ServerSideSortRequestControl decodeJSONControl( 503 @NotNull final JSONObject controlObject, 504 final boolean strict) 505 throws LDAPException 506 { 507 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 508 controlObject, strict, true, true); 509 510 final ASN1OctetString rawValue = jsonControl.getRawValue(); 511 if (rawValue != null) 512 { 513 return new ServerSideSortRequestControl(new Control( 514 jsonControl.getOID(), jsonControl.getCriticality(), rawValue)); 515 } 516 517 518 final JSONObject valueObject = jsonControl.getValueObject(); 519 520 final List<JSONValue> sortKeyValues = 521 valueObject.getFieldAsArray(JSON_FIELD_SORT_KEYS); 522 if (sortKeyValues == null) 523 { 524 throw new LDAPException(ResultCode.DECODING_ERROR, 525 ERR_SORT_REQUEST_JSON_MISSING_SORT_KEYS.get( 526 controlObject.toSingleLineString(), JSON_FIELD_SORT_KEYS)); 527 } 528 529 if (sortKeyValues.isEmpty()) 530 { 531 throw new LDAPException(ResultCode.DECODING_ERROR, 532 ERR_SORT_REQUEST_JSON_EMPTY_SORT_KEYS.get( 533 controlObject.toSingleLineString(), JSON_FIELD_SORT_KEYS)); 534 } 535 536 537 final List<SortKey> sortKeys = new ArrayList<>(sortKeyValues.size()); 538 for (final JSONValue sortKeyValue : sortKeyValues) 539 { 540 if (sortKeyValue instanceof JSONObject) 541 { 542 final JSONObject sortKeyObject = (JSONObject) sortKeyValue; 543 544 final String attributeName = 545 sortKeyObject.getFieldAsString(JSON_FIELD_ATTRIBUTE_NAME); 546 if (attributeName == null) 547 { 548 throw new LDAPException(ResultCode.DECODING_ERROR, 549 ERR_SORT_REQUEST_JSON_SORT_KEY_MISSING_FIELD.get( 550 controlObject.toSingleLineString(), JSON_FIELD_SORT_KEYS, 551 JSON_FIELD_ATTRIBUTE_NAME)); 552 } 553 554 final Boolean reverseOrder = 555 sortKeyObject.getFieldAsBoolean(JSON_FIELD_REVERSE_ORDER); 556 if (reverseOrder == null) 557 { 558 throw new LDAPException(ResultCode.DECODING_ERROR, 559 ERR_SORT_REQUEST_JSON_SORT_KEY_MISSING_FIELD.get( 560 controlObject.toSingleLineString(), JSON_FIELD_SORT_KEYS, 561 JSON_FIELD_REVERSE_ORDER)); 562 } 563 564 final String matchingRuleID = 565 sortKeyObject.getFieldAsString(JSON_FIELD_MATCHING_RULE_ID); 566 567 if (strict) 568 { 569 final List<String> unrecognizedFields = 570 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 571 sortKeyObject, JSON_FIELD_ATTRIBUTE_NAME, 572 JSON_FIELD_REVERSE_ORDER, JSON_FIELD_MATCHING_RULE_ID); 573 if (! unrecognizedFields.isEmpty()) 574 { 575 throw new LDAPException(ResultCode.DECODING_ERROR, 576 ERR_SORT_REQUEST_JSON_UNRECOGNIZED_SORT_KEY_FIELD.get( 577 controlObject.toSingleLineString(), 578 JSON_FIELD_SORT_KEYS, unrecognizedFields.get(0))); 579 } 580 } 581 582 sortKeys.add(new SortKey(attributeName, matchingRuleID, reverseOrder)); 583 } 584 else 585 { 586 throw new LDAPException(ResultCode.DECODING_ERROR, 587 ERR_SORT_REQUEST_JSON_SORT_KEY_VALUE_NOT_OBJECT.get( 588 controlObject.toSingleLineString(), JSON_FIELD_SORT_KEYS)); 589 } 590 } 591 592 593 if (strict) 594 { 595 final List<String> unrecognizedFields = 596 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 597 valueObject, JSON_FIELD_SORT_KEYS); 598 if (! unrecognizedFields.isEmpty()) 599 { 600 throw new LDAPException(ResultCode.DECODING_ERROR, 601 ERR_SORT_REQUEST_JSON_UNRECOGNIZED_FIELD.get( 602 controlObject.toSingleLineString(), 603 unrecognizedFields.get(0))); 604 } 605 } 606 607 608 return new ServerSideSortRequestControl(jsonControl.getCriticality(), 609 sortKeys); 610 } 611 612 613 614 /** 615 * {@inheritDoc} 616 */ 617 @Override() 618 public void toString(@NotNull final StringBuilder buffer) 619 { 620 buffer.append("ServerSideSortRequestControl(sortKeys={"); 621 622 for (int i=0; i < sortKeys.length; i++) 623 { 624 if (i > 0) 625 { 626 buffer.append(", "); 627 } 628 629 buffer.append('\''); 630 sortKeys[i].toString(buffer); 631 buffer.append('\''); 632 } 633 634 buffer.append("})"); 635 } 636}