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 pre-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 before processing a delete, modify, or modify DN
074 * operation.
075 * <BR><BR>
076 * If this control is included in a delete, modify, or modify DN request, then
077 * the corresponding response may include a {@link PreReadResponseControl}
078 * containing a version of the entry as it before 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 * The use of the LDAP pre-read request control is virtually identical to the
092 * use of the LDAP post-read request control.  See the documentation for the
093 * {@link PostReadRequestControl} for an example that illustrates its use.
094 */
095@NotMutable()
096@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
097public final class PreReadRequestControl
098       extends Control
099{
100  /**
101   * The OID (1.3.6.1.1.13.1) for the pre-read request control.
102   */
103  @NotNull public static final String PRE_READ_REQUEST_OID = "1.3.6.1.1.13.1";
104
105
106
107  /**
108   * The set of requested attributes that will be used if none are provided.
109   */
110  @NotNull private static final String[] NO_ATTRIBUTES = StaticUtils.NO_STRINGS;
111
112
113
114  /**
115   * The name of the field used to hold the requested attributes in the JSON
116   * representation of this control.
117   */
118  @NotNull private static final String JSON_FIELD_ATTRIBUTES = "attributes";
119
120
121
122  /**
123   * The serial version UID for this serializable class.
124   */
125  private static final long serialVersionUID = 1205235290978028739L;
126
127
128
129  // The set of requested attributes to retrieve from the target entry.
130  @NotNull private final String[] attributes;
131
132
133
134  /**
135   * Creates a new pre-read request control that will retrieve the specified set
136   * of attributes from the target entry.  It will be marked critical.
137   *
138   * @param  attributes  The set of attributes to retrieve from the target
139   *                     entry.  It behaves in the same way as the set of
140   *                     requested attributes for a search operation.  If this
141   *                     is empty or {@code null}, then all user attributes
142   *                     will be returned.
143   */
144  public PreReadRequestControl(@Nullable final String... attributes)
145  {
146    this(true, attributes);
147  }
148
149
150
151  /**
152   * Creates a new pre-read request control that will retrieve the specified set
153   * of attributes from the target entry.
154   *
155   * @param  isCritical  Indicates whether this control should be marked
156   *                     critical.
157   * @param  attributes  The set of attributes to retrieve from the target
158   *                     entry.  It behaves in the same way as the set of
159   *                     requested attributes for a search operation.  If this
160   *                     is empty or {@code null}, then all user attributes
161   *                     will be returned.
162   */
163  public PreReadRequestControl(final boolean isCritical,
164                               @Nullable final String... attributes)
165  {
166    super(PRE_READ_REQUEST_OID, isCritical, encodeValue(attributes));
167
168    if (attributes == null)
169    {
170      this.attributes = NO_ATTRIBUTES;
171    }
172    else
173    {
174      this.attributes = attributes;
175    }
176  }
177
178
179
180  /**
181   * Creates a new pre-read request control which is decoded from the provided
182   * generic control.
183   *
184   * @param  control  The generic control to be decoded as a pre-read request
185   *                  control.
186   *
187   * @throws  LDAPException  If the provided control cannot be decoded as a
188   *                         pre-read request control.
189   */
190  public PreReadRequestControl(@NotNull final Control control)
191         throws LDAPException
192  {
193    super(control);
194
195    final ASN1OctetString value = control.getValue();
196    if (value == null)
197    {
198      throw new LDAPException(ResultCode.DECODING_ERROR,
199                              ERR_PRE_READ_REQUEST_NO_VALUE.get());
200    }
201
202    try
203    {
204      final ASN1Element valueElement = ASN1Element.decode(value.getValue());
205      final ASN1Element[] attrElements =
206           ASN1Sequence.decodeAsSequence(valueElement).elements();
207      attributes = new String[attrElements.length];
208      for (int i=0; i < attrElements.length; i++)
209      {
210        attributes[i] =
211             ASN1OctetString.decodeAsOctetString(attrElements[i]).stringValue();
212      }
213    }
214    catch (final Exception e)
215    {
216      Debug.debugException(e);
217      throw new LDAPException(ResultCode.DECODING_ERROR,
218                              ERR_PRE_READ_REQUEST_CANNOT_DECODE.get(e), e);
219    }
220  }
221
222
223
224  /**
225   * Encodes the provided information into an octet string that can be used as
226   * the value for this control.
227   *
228   * @param  attributes  The set of attributes to retrieve from the target
229   *                     entry.  It behaves in the same way as the set of
230   *                     requested attributes for a search operation.  If this
231   *                     is empty or {@code null}, then all user attributes
232   *                     will be returned.
233   *
234   * @return  An ASN.1 octet string that can be used as the value for this
235   *          control.
236   */
237  @NotNull()
238  private static ASN1OctetString encodeValue(
239                                      @Nullable final String[] attributes)
240  {
241    if ((attributes == null) || (attributes.length == 0))
242    {
243      return new ASN1OctetString(new ASN1Sequence().encode());
244    }
245
246    final ASN1OctetString[] elements = new ASN1OctetString[attributes.length];
247    for (int i=0; i < attributes.length; i++)
248    {
249      elements[i] = new ASN1OctetString(attributes[i]);
250    }
251
252    return new ASN1OctetString(new ASN1Sequence(elements).encode());
253  }
254
255
256
257  /**
258   * Retrieves the set of attributes that will be requested for inclusion in the
259   * entry returned in the response control.
260   *
261   * @return  The set of attributes that will be requested for inclusion in the
262   *          entry returned in the response control, or an empty array if all
263   *          user attributes should be returned.
264   */
265  @NotNull()
266  public String[] getAttributes()
267  {
268    return attributes;
269  }
270
271
272
273  /**
274   * {@inheritDoc}
275   */
276  @Override()
277  @NotNull()
278  public String getControlName()
279  {
280    return INFO_CONTROL_NAME_PRE_READ_REQUEST.get();
281  }
282
283
284
285  /**
286   * Retrieves a representation of this pre-read request control as a JSON
287   * object.  The JSON object uses the following fields:
288   * <UL>
289   *   <LI>
290   *     {@code oid} -- A mandatory string field whose value is the object
291   *     identifier for this control.  For the pre-read request control, the
292   *     OID is "1.3.6.1.1.13.1".
293   *   </LI>
294   *   <LI>
295   *     {@code control-name} -- An optional string field whose value is a
296   *     human-readable name for this control.  This field is only intended for
297   *     descriptive purposes, and when decoding a control, the {@code oid}
298   *     field should be used to identify the type of control.
299   *   </LI>
300   *   <LI>
301   *     {@code criticality} -- A mandatory Boolean field used to indicate
302   *     whether this control is considered critical.
303   *   </LI>
304   *   <LI>
305   *     {@code value-base64} -- An optional string field whose value is a
306   *     base64-encoded representation of the raw value for this pre-read
307   *     request control.  Exactly one of the {@code value-base64} and
308   *     {@code value-json} fields must be present.
309   *   </LI>
310   *   <LI>
311   *     {@code value-json} -- An optional JSON object field whose value is a
312   *     user-friendly representation of the value for this pre-read request
313   *     control.  Exactly one of the {@code value-base64} and
314   *     {@code value-json} fields must be present, and if the
315   *     {@code value-json} field is used, then it will use the following
316   *     fields:
317   *     <UL>
318   *       <LI>
319   *         {@code attributes} -- An optional array field whose values are
320   *         strings that represent the names of the attributes to include in
321   *         the entry returned in the response control.
322   *       </LI>
323   *     </UL>
324   *   </LI>
325   * </UL>
326   *
327   * @return  A JSON object that contains a representation of this control.
328   */
329  @Override()
330  @NotNull()
331  public JSONObject toJSONControl()
332  {
333    final Map<String,JSONValue> valueFields = new LinkedHashMap<>();
334
335    if ((attributes != null) && (attributes.length > 0))
336    {
337      final List<JSONValue> attrValues = new ArrayList<>(attributes.length);
338      for (final String attribute : attributes)
339      {
340        attrValues.add(new JSONString(attribute));
341      }
342
343      valueFields.put(JSON_FIELD_ATTRIBUTES, new JSONArray(attrValues));
344    }
345
346    return new JSONObject(
347         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
348              PRE_READ_REQUEST_OID),
349         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
350              INFO_CONTROL_NAME_PRE_READ_REQUEST.get()),
351         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
352              isCritical()),
353         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
354              new JSONObject(valueFields)));
355  }
356
357
358
359  /**
360   * Attempts to decode the provided object as a JSON representation of a
361   * pre-read request control.
362   *
363   * @param  controlObject  The JSON object to be decoded.  It must not be
364   *                        {@code null}.
365   * @param  strict         Indicates whether to use strict mode when decoding
366   *                        the provided JSON object.  If this is {@code true},
367   *                        then this method will throw an exception if the
368   *                        provided JSON object contains any unrecognized
369   *                        fields.  If this is {@code false}, then unrecognized
370   *                        fields will be ignored.
371   *
372   * @return  The pre-read request control that was decoded from the provided
373   *          JSON object.
374   *
375   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
376   *                         valid pre-read request control.
377   */
378  @NotNull()
379  public static PreReadRequestControl decodeJSONControl(
380              @NotNull final JSONObject controlObject,
381              final boolean strict)
382         throws LDAPException
383  {
384    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
385         controlObject, strict, true, true);
386
387    final ASN1OctetString rawValue = jsonControl.getRawValue();
388    if (rawValue != null)
389    {
390      return new PreReadRequestControl(new Control(jsonControl.getOID(),
391           jsonControl.getCriticality(), rawValue));
392    }
393
394
395    final JSONObject valueObject = jsonControl.getValueObject();
396
397    final String[] attributes;
398    final List<JSONValue> attributesValues =
399         valueObject.getFieldAsArray(JSON_FIELD_ATTRIBUTES);
400    if (attributesValues == null)
401    {
402      attributes = null;
403    }
404    else
405    {
406      attributes = new String[attributesValues.size()];
407      for (int i=0; i < attributes.length; i++)
408      {
409        final JSONValue v = attributesValues.get(i);
410        if (v instanceof JSONString)
411        {
412          attributes[i] = ((JSONString) v).stringValue();
413        }
414        else
415        {
416          throw new LDAPException(ResultCode.DECODING_ERROR,
417               ERR_PRE_READ_REQUEST_JSON_ATTR_NOT_STRING.get(
418                    controlObject.toSingleLineString(),
419                    JSON_FIELD_ATTRIBUTES));
420        }
421      }
422    }
423
424
425    if (strict)
426    {
427      final List<String> unrecognizedFields =
428           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
429                valueObject, JSON_FIELD_ATTRIBUTES);
430      if (! unrecognizedFields.isEmpty())
431      {
432        throw new LDAPException(ResultCode.DECODING_ERROR,
433             ERR_PRE_READ_REQUEST_JSON_UNRECOGNIZED_FIELD.get(
434                  controlObject.toSingleLineString(),
435                  unrecognizedFields.get(0)));
436      }
437    }
438
439
440    return new PreReadRequestControl(jsonControl.getCriticality(),
441         attributes);
442  }
443
444
445
446  /**
447   * {@inheritDoc}
448   */
449  @Override()
450  public void toString(@NotNull final StringBuilder buffer)
451  {
452    buffer.append("PreReadRequestControl(attributes={");
453    for (int i=0; i < attributes.length; i++)
454    {
455      if (i > 0)
456      {
457        buffer.append(", ");
458      }
459      buffer.append('\'');
460      buffer.append(attributes[i]);
461      buffer.append('\'');
462    }
463    buffer.append("}, isCritical=");
464    buffer.append(isCritical());
465    buffer.append(')');
466  }
467}