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.Nullable;
056import com.unboundid.util.StaticUtils;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059import com.unboundid.util.json.JSONArray;
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 LDAP post-read request control
071 * as defined in <A HREF="http://www.ietf.org/rfc/rfc4527.txt">RFC 4527</A>.  It
072 * may be used to request that the server retrieve a copy of the target entry as
073 * it appeared immediately after processing an add, modify, or modify DN
074 * operation.
075 * <BR><BR>
076 * If this control is included in an add, modify, or modify DN request, then the
077 * corresponding response may include a {@link PostReadResponseControl}
078 * containing a version of the entry as it appeared after applying that change.
079 * Note that this response control will only be included if the operation was
080 * successful, so it will not be provided if the operation failed for some
081 * reason (e.g., if the change would have violated the server schema, or if the
082 * requester did not have sufficient permission to perform that operation).
083 * <BR><BR>
084 * The value of this control should contain a set of requested attributes to
085 * include in the entry that is returned.  The server should treat this set of
086 * requested attributes exactly as it treats the requested attributes from a
087 * {@link com.unboundid.ldap.sdk.SearchRequest}.  As is the case with a search
088 * request, if no attributes are specified, then all user attributes will be
089 * included.
090 * <BR><BR>
091 * <H2>Example</H2>
092 * The following example demonstrates the use of the pre-read and post-read
093 * controls.  It will modify an entry to increment the value of the
094 * {@code test-counter} attribute by one, and will use the pre-read and
095 * post-read controls to determine what the previous and updated values are:
096 * <PRE>
097 * // Create a modify request that we can use to increment the value of a
098 * // custom attribute named "test-counter".
099 * ModifyRequest modifyRequest = new ModifyRequest(
100 *      "uid=test.user,ou=People,dc=example,dc=com",
101 *      new Modification(ModificationType.INCREMENT,
102 *           "test-counter", // The attribute to increment.
103 *           "1")); // The amount by which to increment the value.
104 *
105 * // Update the modify request to add both pre-read and post-read request
106 * // controls to see what the entry value was before and after the change.
107 * // We only care about getting the test-counter attribute.
108 * modifyRequest.setControls(
109 *      new PreReadRequestControl("test-counter"),
110 *      new PostReadRequestControl("test-counter"));
111 *
112 * // Process the modify operation in the server.
113 * LDAPResult modifyResult;
114 * try
115 * {
116 *   modifyResult = connection.modify(modifyRequest);
117 *   // If we got here, then the modification should have been successful.
118 * }
119 * catch (LDAPException le)
120 * {
121 *   // This indicates that the operation did not complete successfully.
122 *   modifyResult = le.toLDAPResult();
123 *   ResultCode resultCode = le.getResultCode();
124 *   String errorMessageFromServer = le.getDiagnosticMessage();
125 * }
126 * LDAPTestUtils.assertResultCodeEquals(modifyResult, ResultCode.SUCCESS);
127 *
128 * // Get the pre-read and post-read response controls from the server and
129 * // retrieve the before and after values for the test-counter attribute.
130 * LDAPTestUtils.assertHasControl(modifyResult,
131 *      PreReadResponseControl.PRE_READ_RESPONSE_OID);
132 * PreReadResponseControl preReadResponse =
133 *      PreReadResponseControl.get(modifyResult);
134 * Integer beforeValue =
135 *      preReadResponse.getEntry().getAttributeValueAsInteger("test-counter");
136 *
137 * LDAPTestUtils.assertHasControl(modifyResult,
138 *      PostReadResponseControl.POST_READ_RESPONSE_OID);
139 * PostReadResponseControl postReadResponse =
140 *      PostReadResponseControl.get(modifyResult);
141 * Integer afterValue =
142 *      postReadResponse.getEntry().getAttributeValueAsInteger("test-counter");
143 * </PRE>
144 */
145@NotMutable()
146@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
147public final class PostReadRequestControl
148       extends Control
149{
150  /**
151   * The OID (1.3.6.1.1.13.2) for the post-read request control.
152   */
153  @NotNull public static final String POST_READ_REQUEST_OID = "1.3.6.1.1.13.2";
154
155
156
157  /**
158   * The set of requested attributes that will be used if none are provided.
159   */
160  @NotNull private static final String[] NO_ATTRIBUTES = StaticUtils.NO_STRINGS;
161
162
163
164  /**
165   * The name of the field used to hold the requested attributes in the JSON
166   * representation of this control.
167   */
168  @NotNull private static final String JSON_FIELD_ATTRIBUTES = "attributes";
169
170
171
172  /**
173   * The serial version UID for this serializable class.
174   */
175  private static final long serialVersionUID = -4210061989410209462L;
176
177
178
179  // The set of requested attributes to retrieve from the target entry.
180  @NotNull private final String[] attributes;
181
182
183
184  /**
185   * Creates a new post-read request control that will retrieve the specified
186   * set of attributes from the target entry.  It will be marked critical.
187   *
188   * @param  attributes  The set of attributes to retrieve from the target
189   *                     entry.  It behaves in the same way as the set of
190   *                     requested attributes for a search operation.  If this
191   *                     is empty or {@code null}, then all user attributes
192   *                     will be returned.
193   */
194  public PostReadRequestControl(@Nullable final String... attributes)
195  {
196    this(true, attributes);
197  }
198
199
200
201  /**
202   * Creates a new post-read request control that will retrieve the specified
203   * set of attributes from the target entry.
204   *
205   * @param  isCritical  Indicates whether this control should be marked
206   *                     critical.
207   * @param  attributes  The set of attributes to retrieve from the target
208   *                     entry.  It behaves in the same way as the set of
209   *                     requested attributes for a search operation.  If this
210   *                     is empty or {@code null}, then all user attributes
211   *                     will be returned.
212   */
213  public PostReadRequestControl(final boolean isCritical,
214                                @Nullable final String... attributes)
215  {
216    super(POST_READ_REQUEST_OID, isCritical, encodeValue(attributes));
217
218    if (attributes == null)
219    {
220      this.attributes = NO_ATTRIBUTES;
221    }
222    else
223    {
224      this.attributes = attributes;
225    }
226  }
227
228
229
230  /**
231   * Creates a new post-read request control which is decoded from the provided
232   * generic control.
233   *
234   * @param  control  The generic control to be decoded as a post-read request
235   *                  control.
236   *
237   * @throws  LDAPException  If the provided control cannot be decoded as a
238   *                         post-read request control.
239   */
240  public PostReadRequestControl(@NotNull final Control control)
241         throws LDAPException
242  {
243    super(control);
244
245    final ASN1OctetString value = control.getValue();
246    if (value == null)
247    {
248      throw new LDAPException(ResultCode.DECODING_ERROR,
249                              ERR_POST_READ_REQUEST_NO_VALUE.get());
250    }
251
252    try
253    {
254      final ASN1Element valueElement = ASN1Element.decode(value.getValue());
255      final ASN1Element[] attrElements =
256           ASN1Sequence.decodeAsSequence(valueElement).elements();
257      attributes = new String[attrElements.length];
258      for (int i=0; i < attrElements.length; i++)
259      {
260        attributes[i] =
261             ASN1OctetString.decodeAsOctetString(attrElements[i]).stringValue();
262      }
263    }
264    catch (final Exception e)
265    {
266      Debug.debugException(e);
267      throw new LDAPException(ResultCode.DECODING_ERROR,
268                              ERR_POST_READ_REQUEST_CANNOT_DECODE.get(e), e);
269    }
270  }
271
272
273
274  /**
275   * Encodes the provided information into an octet string that can be used as
276   * the value for this control.
277   *
278   * @param  attributes  The set of attributes to retrieve from the target
279   *                     entry.  It behaves in the same way as the set of
280   *                     requested attributes for a search operation.  If this
281   *                     is empty or {@code null}, then all user attributes
282   *                     will be returned.
283   *
284   * @return  An ASN.1 octet string that can be used as the value for this
285   *          control.
286   */
287  @NotNull()
288  private static ASN1OctetString encodeValue(
289                      @Nullable final String[] attributes)
290  {
291    if ((attributes == null) || (attributes.length == 0))
292    {
293      return new ASN1OctetString(new ASN1Sequence().encode());
294    }
295
296    final ASN1OctetString[] elements = new ASN1OctetString[attributes.length];
297    for (int i=0; i < attributes.length; i++)
298    {
299      elements[i] = new ASN1OctetString(attributes[i]);
300    }
301
302    return new ASN1OctetString(new ASN1Sequence(elements).encode());
303  }
304
305
306
307  /**
308   * Retrieves the set of attributes that will be requested for inclusion in the
309   * entry returned in the response control.
310   *
311   * @return  The set of attributes that will be requested for inclusion in the
312   *          entry returned in the response control, or an empty array if all
313   *          user attributes should be returned.
314   */
315  @NotNull()
316  public String[] getAttributes()
317  {
318    return attributes;
319  }
320
321
322
323  /**
324   * {@inheritDoc}
325   */
326  @Override()
327  @NotNull()
328  public String getControlName()
329  {
330    return INFO_CONTROL_NAME_POST_READ_REQUEST.get();
331  }
332
333
334
335  /**
336   * Retrieves a representation of this post-read request control as a JSON
337   * object.  The JSON object uses the following fields:
338   * <UL>
339   *   <LI>
340   *     {@code oid} -- A mandatory string field whose value is the object
341   *     identifier for this control.  For the post-read request control, the
342   *     OID is "1.3.6.1.1.13.2".
343   *   </LI>
344   *   <LI>
345   *     {@code control-name} -- An optional string field whose value is a
346   *     human-readable name for this control.  This field is only intended for
347   *     descriptive purposes, and when decoding a control, the {@code oid}
348   *     field should be used to identify the type of control.
349   *   </LI>
350   *   <LI>
351   *     {@code criticality} -- A mandatory Boolean field used to indicate
352   *     whether this control is considered critical.
353   *   </LI>
354   *   <LI>
355   *     {@code value-base64} -- An optional string field whose value is a
356   *     base64-encoded representation of the raw value for this post-read
357   *     request control.  Exactly one of the {@code value-base64} and
358   *     {@code value-json} fields must be present.
359   *   </LI>
360   *   <LI>
361   *     {@code value-json} -- An optional JSON object field whose value is a
362   *     user-friendly representation of the value for this post-read request
363   *     control.  Exactly one of the {@code value-base64} and
364   *     {@code value-json} fields must be present, and if the
365   *     {@code value-json} field is used, then it will use the following
366   *     fields:
367   *     <UL>
368   *       <LI>
369   *         {@code attributes} -- An optional array field whose values are
370   *         strings that represent the names of the attributes to include in
371   *         the entry returned in the response control.
372   *       </LI>
373   *     </UL>
374   *   </LI>
375   * </UL>
376   *
377   * @return  A JSON object that contains a representation of this control.
378   */
379  @Override()
380  @NotNull()
381  public JSONObject toJSONControl()
382  {
383    final Map<String,JSONValue> valueFields = new LinkedHashMap<>();
384
385    if ((attributes != null) && (attributes.length > 0))
386    {
387      final List<JSONValue> attrValues = new ArrayList<>(attributes.length);
388      for (final String attribute : attributes)
389      {
390        attrValues.add(new JSONString(attribute));
391      }
392
393      valueFields.put(JSON_FIELD_ATTRIBUTES, new JSONArray(attrValues));
394    }
395
396    return new JSONObject(
397         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
398              POST_READ_REQUEST_OID),
399         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
400              INFO_CONTROL_NAME_POST_READ_REQUEST.get()),
401         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
402              isCritical()),
403         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
404              new JSONObject(valueFields)));
405  }
406
407
408
409  /**
410   * Attempts to decode the provided object as a JSON representation of a
411   * post-read request control.
412   *
413   * @param  controlObject  The JSON object to be decoded.  It must not be
414   *                        {@code null}.
415   * @param  strict         Indicates whether to use strict mode when decoding
416   *                        the provided JSON object.  If this is {@code true},
417   *                        then this method will throw an exception if the
418   *                        provided JSON object contains any unrecognized
419   *                        fields.  If this is {@code false}, then unrecognized
420   *                        fields will be ignored.
421   *
422   * @return  The post-read request control that was decoded from the provided
423   *          JSON object.
424   *
425   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
426   *                         valid post-read request control.
427   */
428  @NotNull()
429  public static PostReadRequestControl decodeJSONControl(
430              @NotNull final JSONObject controlObject,
431              final boolean strict)
432         throws LDAPException
433  {
434    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
435         controlObject, strict, true, true);
436
437    final ASN1OctetString rawValue = jsonControl.getRawValue();
438    if (rawValue != null)
439    {
440      return new PostReadRequestControl(new Control(jsonControl.getOID(),
441           jsonControl.getCriticality(), rawValue));
442    }
443
444
445    final JSONObject valueObject = jsonControl.getValueObject();
446
447    final String[] attributes;
448    final List<JSONValue> attributesValues =
449         valueObject.getFieldAsArray(JSON_FIELD_ATTRIBUTES);
450    if (attributesValues == null)
451    {
452      attributes = null;
453    }
454    else
455    {
456      attributes = new String[attributesValues.size()];
457      for (int i=0; i < attributes.length; i++)
458      {
459        final JSONValue v = attributesValues.get(i);
460        if (v instanceof JSONString)
461        {
462          attributes[i] = ((JSONString) v).stringValue();
463        }
464        else
465        {
466          throw new LDAPException(ResultCode.DECODING_ERROR,
467               ERR_POST_READ_REQUEST_JSON_ATTR_NOT_STRING.get(
468                    controlObject.toSingleLineString(),
469                    JSON_FIELD_ATTRIBUTES));
470        }
471      }
472    }
473
474
475    if (strict)
476    {
477      final List<String> unrecognizedFields =
478           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
479                valueObject, JSON_FIELD_ATTRIBUTES);
480      if (! unrecognizedFields.isEmpty())
481      {
482        throw new LDAPException(ResultCode.DECODING_ERROR,
483             ERR_POST_READ_REQUEST_JSON_UNRECOGNIZED_FIELD.get(
484                  controlObject.toSingleLineString(),
485                  unrecognizedFields.get(0)));
486      }
487    }
488
489
490    return new PostReadRequestControl(jsonControl.getCriticality(),
491         attributes);
492  }
493
494
495
496  /**
497   * {@inheritDoc}
498   */
499  @Override()
500  public void toString(@NotNull final StringBuilder buffer)
501  {
502    buffer.append("PostReadRequestControl(attributes={");
503    for (int i=0; i < attributes.length; i++)
504    {
505      if (i > 0)
506      {
507        buffer.append(", ");
508      }
509      buffer.append('\'');
510      buffer.append(attributes[i]);
511      buffer.append('\'');
512    }
513    buffer.append("}, isCritical=");
514    buffer.append(isCritical());
515    buffer.append(')');
516  }
517}