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