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}