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.ASN1OctetString;
048import com.unboundid.asn1.ASN1Sequence;
049import com.unboundid.ldap.sdk.Control;
050import com.unboundid.ldap.sdk.DecodeableControl;
051import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
052import com.unboundid.ldap.sdk.LDAPException;
053import com.unboundid.ldap.sdk.ResultCode;
054import com.unboundid.ldap.sdk.SearchResult;
055import com.unboundid.util.Debug;
056import com.unboundid.util.NotMutable;
057import com.unboundid.util.NotNull;
058import com.unboundid.util.Nullable;
059import com.unboundid.util.ThreadSafety;
060import com.unboundid.util.ThreadSafetyLevel;
061import com.unboundid.util.json.JSONField;
062import com.unboundid.util.json.JSONNumber;
063import com.unboundid.util.json.JSONObject;
064import com.unboundid.util.json.JSONString;
065import com.unboundid.util.json.JSONValue;
066
067import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
068
069
070
071/**
072 * This class provides an implementation of the server-side sort response
073 * control, as defined in
074 * <A HREF="http://www.ietf.org/rfc/rfc2891.txt">RFC 2891</A>.  It may be used
075 * to provide information about the result of server-side sort processing.  If
076 * the corresponding search request included the
077 * {@link ServerSideSortRequestControl}, then the search result done message
078 * may include this response control to provide information about the state of
079 * the sorting.
080 */
081@NotMutable()
082@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
083public final class ServerSideSortResponseControl
084       extends Control
085       implements DecodeableControl
086{
087  /**
088   * The OID (1.2.840.113556.1.4.474) for the server-side sort response control.
089   */
090  @NotNull public static final String SERVER_SIDE_SORT_RESPONSE_OID =
091       "1.2.840.113556.1.4.474";
092
093
094
095  /**
096   * The BER type to use for the element that holds the attribute type.
097   */
098  private static final byte TYPE_ATTRIBUTE_TYPE = (byte) 0x80;
099
100
101
102  /**
103   * The name of the field used to hold the attribute name in the JSON
104   * representation of this control.
105   */
106  @NotNull private static final String JSON_FIELD_ATTRIBUTE_NAME =
107       "attribute-name";
108
109
110
111  /**
112   * The name of the field used to hold the result code in the JSON
113   * representation of this control.
114   */
115  @NotNull private static final String JSON_FIELD_RESULT_CODE =
116       "result-code";
117
118
119
120  /**
121   * The serial version UID for this serializable class.
122   */
123  private static final long serialVersionUID = -8707533262822875822L;
124
125
126
127  // The result code for this server-side sort response control.
128  @NotNull private final ResultCode resultCode;
129
130  // The name of the attribute associated with this result, if available.
131  @Nullable private final String attributeName;
132
133
134
135  /**
136   * Creates a new empty control instance that is intended to be used only for
137   * decoding controls via the {@code DecodeableControl} interface.
138   */
139  ServerSideSortResponseControl()
140  {
141    resultCode    = null;
142    attributeName = null;
143  }
144
145
146
147  /**
148   * Creates a new server-side sort response control with the provided
149   * information.
150   *
151   * @param  resultCode     The result code for this server-side sort response.
152   * @param  attributeName  The name of the attribute associated with this
153   *                        result.  It may be {@code null} if there is no
154   *                        associated attribute name.
155   */
156  public ServerSideSortResponseControl(@NotNull final ResultCode resultCode,
157                                       @Nullable final String attributeName)
158  {
159    this(resultCode, attributeName, false);
160  }
161
162
163
164  /**
165   * Creates a new server-side sort response control with the provided
166   * information.
167   *
168   * @param  resultCode     The result code for this server-side sort response.
169   * @param  attributeName  The name of the attribute associated with this
170   *                        result.  It may be {@code null} if there is no
171   *                        associated attribute name.
172   * @param  isCritical     Indicates whether this control should be marked
173   *                        critical.  Response controls should generally not be
174   *                        critical.
175   */
176  public ServerSideSortResponseControl(@NotNull final ResultCode resultCode,
177                                       @Nullable final String attributeName,
178                                       final boolean isCritical)
179  {
180    super(SERVER_SIDE_SORT_RESPONSE_OID, isCritical,
181          encodeValue(resultCode, attributeName));
182
183    this.resultCode    = resultCode;
184    this.attributeName = attributeName;
185  }
186
187
188
189  /**
190   * Creates a new server-side sort response control from the information
191   * contained in the provided control.
192   *
193   * @param  oid         The OID for the control.
194   * @param  isCritical  Indicates whether the control should be marked
195   *                     critical.
196   * @param  value       The encoded value for the control.  This may be
197   *                     {@code null} if no value was provided.
198   *
199   * @throws  LDAPException  If a problem occurs while attempting to decode the
200   *                         provided control as a server-side sort response
201   *                         control.
202   */
203  public ServerSideSortResponseControl(@NotNull final String oid,
204                                       final boolean isCritical,
205                                       @Nullable final ASN1OctetString value)
206         throws LDAPException
207  {
208    super(oid, isCritical, value);
209
210    if (value == null)
211    {
212      throw new LDAPException(ResultCode.DECODING_ERROR,
213                              ERR_SORT_RESPONSE_NO_VALUE.get());
214    }
215
216    final ASN1Sequence valueSequence;
217    try
218    {
219      final ASN1Element valueElement =
220           ASN1Element.decode(value.getValue());
221      valueSequence = ASN1Sequence.decodeAsSequence(valueElement);
222    }
223    catch (final ASN1Exception ae)
224    {
225      Debug.debugException(ae);
226      throw new LDAPException(ResultCode.DECODING_ERROR,
227                              ERR_SORT_RESPONSE_VALUE_NOT_SEQUENCE.get(ae), ae);
228    }
229
230    final ASN1Element[] valueElements = valueSequence.elements();
231    if ((valueElements.length < 1) || (valueElements.length > 2))
232    {
233      throw new LDAPException(ResultCode.DECODING_ERROR,
234                              ERR_SORT_RESPONSE_INVALID_ELEMENT_COUNT.get(
235                                   valueElements.length));
236    }
237
238    try
239    {
240      final int rc =
241           ASN1Enumerated.decodeAsEnumerated(valueElements[0]).intValue();
242      resultCode = ResultCode.valueOf(rc);
243    }
244    catch (final ASN1Exception ae)
245    {
246      Debug.debugException(ae);
247      throw new LDAPException(ResultCode.DECODING_ERROR,
248                              ERR_SORT_RESPONSE_FIRST_NOT_ENUM.get(ae), ae);
249    }
250
251    if (valueElements.length == 2)
252    {
253      attributeName =
254           ASN1OctetString.decodeAsOctetString(valueElements[1]).stringValue();
255    }
256    else
257    {
258      attributeName = null;
259    }
260  }
261
262
263
264  /**
265   * {@inheritDoc}
266   */
267  @Override()
268  @NotNull()
269  public ServerSideSortResponseControl decodeControl(@NotNull final String oid,
270              final boolean isCritical,
271              @Nullable final ASN1OctetString value)
272         throws LDAPException
273  {
274    return new ServerSideSortResponseControl(oid, isCritical, value);
275  }
276
277
278
279  /**
280   * Extracts a server-side sort response control from the provided result.
281   *
282   * @param  result  The result from which to retrieve the server-side sort
283   *                 response control.
284   *
285   * @return  The server-side sort response control contained in the provided
286   *          result, or {@code null} if the result did not contain a
287   *          server-side sort response control.
288   *
289   * @throws  LDAPException  If a problem is encountered while attempting to
290   *                         decode the server-side sort response control
291   *                         contained in the provided result.
292   */
293  @Nullable()
294  public static ServerSideSortResponseControl get(
295                     @NotNull final SearchResult result)
296         throws LDAPException
297  {
298    final Control c = result.getResponseControl(SERVER_SIDE_SORT_RESPONSE_OID);
299    if (c == null)
300    {
301      return null;
302    }
303
304    if (c instanceof ServerSideSortResponseControl)
305    {
306      return (ServerSideSortResponseControl) c;
307    }
308    else
309    {
310      return new ServerSideSortResponseControl(c.getOID(), c.isCritical(),
311           c.getValue());
312    }
313  }
314
315
316
317  /**
318   * Encodes the provided information into an octet string that can be used as
319   * the value for this control.
320   *
321   * @param  resultCode     The result code for this server-side sort response
322   *                        control.
323   * @param  attributeName  The attribute name to include in the control, or
324   *                        {@code null} if it should not be provided.
325   *
326   * @return  An ASN.1 octet string that can be used as the value for this
327   *          control.
328   */
329  @NotNull()
330  private static ASN1OctetString encodeValue(
331                      @NotNull final ResultCode resultCode,
332                      @Nullable final String attributeName)
333  {
334    final ASN1Element[] valueElements;
335    if (attributeName == null)
336    {
337      valueElements = new ASN1Element[]
338      {
339        new ASN1Enumerated(resultCode.intValue())
340      };
341    }
342    else
343    {
344      valueElements = new ASN1Element[]
345      {
346        new ASN1Enumerated(resultCode.intValue()),
347        new ASN1OctetString(TYPE_ATTRIBUTE_TYPE, attributeName)
348      };
349    }
350
351    return new ASN1OctetString(new ASN1Sequence(valueElements).encode());
352  }
353
354
355
356  /**
357   * Retrieves the result code for this server-side sort response control.
358   *
359   * @return  The result code for this server-side sort response control.
360   */
361  @NotNull()
362  public ResultCode getResultCode()
363  {
364    return resultCode;
365  }
366
367
368
369  /**
370   * Retrieves the attribute name for this server-side sort response control, if
371   * available.
372   *
373   * @return  The attribute name for this server-side sort response control, or
374   *          {@code null} if none was provided.
375   */
376  @Nullable()
377  public String getAttributeName()
378  {
379    return attributeName;
380  }
381
382
383
384  /**
385   * {@inheritDoc}
386   */
387  @Override()
388  @NotNull()
389  public String getControlName()
390  {
391    return INFO_CONTROL_NAME_SORT_RESPONSE.get();
392  }
393
394
395
396  /**
397   * Retrieves a representation of this server-side sort response control as a
398   * JSON object.  The JSON object uses the following fields:
399   * <UL>
400   *   <LI>
401   *     {@code oid} -- A mandatory string field whose value is the object
402   *     identifier for this control.  For the server-side sort response
403   *     control, the OID is "1.2.840.113556.1.4.474".
404   *   </LI>
405   *   <LI>
406   *     {@code control-name} -- An optional string field whose value is a
407   *     human-readable name for this control.  This field is only intended for
408   *     descriptive purposes, and when decoding a control, the {@code oid}
409   *     field should be used to identify the type of control.
410   *   </LI>
411   *   <LI>
412   *     {@code criticality} -- A mandatory Boolean field used to indicate
413   *     whether this control is considered critical.
414   *   </LI>
415   *   <LI>
416   *     {@code value-base64} -- An optional string field whose value is a
417   *     base64-encoded representation of the raw value for this server-side
418   *     sort response control.  Exactly one of the {@code value-base64} and
419   *     {@code value-json} fields must be present.
420   *   </LI>
421   *   <LI>
422   *     {@code value-json} -- An optional JSON object field whose value is a
423   *     user-friendly representation of the value for this server-side sort
424   *     response control.  Exactly one of the {@code value-base64} and
425   *     {@code value-json} fields must be present, and if the
426   *     {@code value-json} field is used, then it will use the following
427   *     fields:
428   *     <UL>
429   *       <LI>
430   *         {@code result-code} -- An integer field whose value is the numeric
431   *         representation of the result code for the sort processing.
432   *       </LI>
433   *       <LI>
434   *         {@code attribute-name} -- An optional string field whose value is
435   *         the name of the attribute with which the result code is most
436   *         closely associated.
437   *       </LI>
438   *     </UL>
439   *   </LI>
440   * </UL>
441   *
442   * @return  A JSON object that contains a representation of this control.
443   */
444  @Override()
445  @NotNull()
446  public JSONObject toJSONControl()
447  {
448    final Map<String,JSONValue> valueFields = new LinkedHashMap<>();
449    valueFields.put(JSON_FIELD_RESULT_CODE,
450         new JSONNumber(resultCode.intValue()));
451
452    if (attributeName != null)
453    {
454      valueFields.put(JSON_FIELD_ATTRIBUTE_NAME, new JSONString(attributeName));
455    }
456
457    return new JSONObject(
458         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
459              SERVER_SIDE_SORT_RESPONSE_OID),
460         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
461              INFO_CONTROL_NAME_SORT_RESPONSE.get()),
462         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
463              isCritical()),
464         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
465              new JSONObject(valueFields)));
466  }
467
468
469
470  /**
471   * Attempts to decode the provided object as a JSON representation of a
472   * server-side sort response control.
473   *
474   * @param  controlObject  The JSON object to be decoded.  It must not be
475   *                        {@code null}.
476   * @param  strict         Indicates whether to use strict mode when decoding
477   *                        the provided JSON object.  If this is {@code true},
478   *                        then this method will throw an exception if the
479   *                        provided JSON object contains any unrecognized
480   *                        fields.  If this is {@code false}, then unrecognized
481   *                        fields will be ignored.
482   *
483   * @return  The server=side sort response control that was decoded from
484   *          the provided JSON object.
485   *
486   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
487   *                         valid server-side sort response control.
488   */
489  @NotNull()
490  public static ServerSideSortResponseControl decodeJSONControl(
491              @NotNull final JSONObject controlObject,
492              final boolean strict)
493         throws LDAPException
494  {
495    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
496         controlObject, strict, true, true);
497
498    final ASN1OctetString rawValue = jsonControl.getRawValue();
499    if (rawValue != null)
500    {
501      return new ServerSideSortResponseControl(jsonControl.getOID(),
502           jsonControl.getCriticality(), rawValue);
503    }
504
505
506    final JSONObject valueObject = jsonControl.getValueObject();
507
508    final Integer resultCodeValue =
509         valueObject.getFieldAsInteger(JSON_FIELD_RESULT_CODE);
510    if (resultCodeValue == null)
511    {
512      throw new LDAPException(ResultCode.DECODING_ERROR,
513           ERR_SORT_RESPONSE_JSON_MISSING_RESULT_CODE.get(
514                controlObject.toSingleLineString(), JSON_FIELD_RESULT_CODE));
515    }
516
517    final ResultCode resultCode = ResultCode.valueOf(resultCodeValue);
518
519
520    final String attributeName =
521         valueObject.getFieldAsString(JSON_FIELD_ATTRIBUTE_NAME);
522
523
524    if (strict)
525    {
526      final List<String> unrecognizedFields =
527           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
528                valueObject, JSON_FIELD_RESULT_CODE,
529                JSON_FIELD_ATTRIBUTE_NAME);
530      if (! unrecognizedFields.isEmpty())
531      {
532        throw new LDAPException(ResultCode.DECODING_ERROR,
533             ERR_SORT_RESPONSE_JSON_UNRECOGNIZED_FIELD.get(
534                  controlObject.toSingleLineString(),
535                  unrecognizedFields.get(0)));
536      }
537    }
538
539
540    return new ServerSideSortResponseControl(resultCode, attributeName,
541         jsonControl.getCriticality());
542  }
543
544
545
546  /**
547   * {@inheritDoc}
548   */
549  @Override()
550  public void toString(@NotNull final StringBuilder buffer)
551  {
552    buffer.append("ServerSideSortResponseControl(resultCode=");
553    buffer.append(resultCode);
554
555    if (attributeName != null)
556    {
557      buffer.append(", attributeName='");
558      buffer.append(attributeName);
559      buffer.append('\'');
560    }
561
562    buffer.append(')');
563  }
564}