001/*
002 * Copyright 2012-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2012-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) 2012-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.unboundidds.controls;
037
038
039
040import java.util.ArrayList;
041import java.util.List;
042
043import com.unboundid.asn1.ASN1Boolean;
044import com.unboundid.asn1.ASN1Element;
045import com.unboundid.asn1.ASN1OctetString;
046import com.unboundid.asn1.ASN1Sequence;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.DeleteRequest;
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.JSONField;
060import com.unboundid.util.json.JSONObject;
061
062import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
063
064
065
066/**
067 * This class provides a request control which may be included in a delete
068 * request to indicate that the server should perform a soft delete rather than
069 * a hard delete.  A soft delete will leave the entry in the server, but will
070 * mark it hidden so that it can only be retrieved with a special request
071 * (e.g., one which includes the {@link SoftDeletedEntryAccessRequestControl} or
072 * a filter which includes an "(objectClass=ds-soft-deleted-entry)" component).
073 * A soft-deleted entry may later be undeleted (using an add request containing
074 * the {@link UndeleteRequestControl}) in order to restore them with the same or
075 * a different DN.
076 * <BR>
077 * <BLOCKQUOTE>
078 *   <B>NOTE:</B>  This class, and other classes within the
079 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
080 *   supported for use against Ping Identity, UnboundID, and
081 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
082 *   for proprietary functionality or for external specifications that are not
083 *   considered stable or mature enough to be guaranteed to work in an
084 *   interoperable way with other types of LDAP servers.
085 * </BLOCKQUOTE>
086 * <BR>
087 * The criticality for this control may be either {@code TRUE} or {@code FALSE},
088 * but this will only impact how the delete request is to be handled by servers
089 * which do not support this control.  A criticality of {@code TRUE} will cause
090 * any server which does not support this control to reject the request, while
091 * a criticality of {@code FALSE} should cause the delete request to be
092 * processed as if the control had not been included (i.e., as a regular "hard"
093 * delete).
094 * <BR><BR>
095 * The control may optionally have a value.  If a value is provided, then it
096 * must be the encoded representation of the following ASN.1 element:
097 * <PRE>
098 *   SoftDeleteRequestValue ::= SEQUENCE {
099 *     returnSoftDeleteResponse     [0] BOOLEAN DEFAULT TRUE,
100 *     ... }
101 * </PRE>
102 * <BR><BR>
103 * <H2>Example</H2>
104 * The following example demonstrates the use of the soft delete request control
105 * to remove the "uid=test,dc=example,dc=com" user with a soft delete operation,
106 * and then to recover it with an undelete operation:
107 * <PRE>
108 * // Perform a search to verify that the test entry exists.
109 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
110 *      SearchScope.SUB, Filter.createEqualityFilter("uid", "test"));
111 * SearchResult searchResult = connection.search(searchRequest);
112 * LDAPTestUtils.assertEntriesReturnedEquals(searchResult, 1);
113 * String originalDN = searchResult.getSearchEntries().get(0).getDN();
114 *
115 * // Perform a soft delete against the entry.
116 * DeleteRequest softDeleteRequest = new DeleteRequest(originalDN);
117 * softDeleteRequest.addControl(new SoftDeleteRequestControl());
118 * LDAPResult softDeleteResult = connection.delete(softDeleteRequest);
119 *
120 * // Verify that a soft delete response control was included in the result.
121 * SoftDeleteResponseControl softDeleteResponseControl =
122 *      SoftDeleteResponseControl.get(softDeleteResult);
123 * String softDeletedDN = softDeleteResponseControl.getSoftDeletedEntryDN();
124 *
125 * // Verify that the original entry no longer exists.
126 * LDAPTestUtils.assertEntryMissing(connection, originalDN);
127 *
128 * // Verify that the original search no longer returns any entries.
129 * searchResult = connection.search(searchRequest);
130 * LDAPTestUtils.assertNoEntriesReturned(searchResult);
131 *
132 * // Verify that the search will return an entry if we include the
133 * // soft-deleted entry access control in the request.
134 * searchRequest.addControl(new SoftDeletedEntryAccessRequestControl());
135 * searchResult = connection.search(searchRequest);
136 * LDAPTestUtils.assertEntriesReturnedEquals(searchResult, 1);
137 *
138 * // Perform an undelete operation to restore the entry.
139 * AddRequest undeleteRequest = UndeleteRequestControl.createUndeleteRequest(
140 *      originalDN, softDeletedDN);
141 * LDAPResult undeleteResult = connection.add(undeleteRequest);
142 *
143 * // Verify that the original entry is back.
144 * LDAPTestUtils.assertEntryExists(connection, originalDN);
145 *
146 * // Permanently remove the original entry with a hard delete.
147 * DeleteRequest hardDeleteRequest = new DeleteRequest(originalDN);
148 * hardDeleteRequest.addControl(new HardDeleteRequestControl());
149 * LDAPResult hardDeleteResult = connection.delete(hardDeleteRequest);
150 * </PRE>
151 * Note that this class provides convenience methods that can be used to easily
152 * create a delete request containing an appropriate soft delete request
153 * control.  Similar methods can be found in the
154 * {@link HardDeleteRequestControl} and {@link UndeleteRequestControl} classes
155 * for creating appropriate hard delete and undelete requests, respectively.
156 *
157 * @see  HardDeleteRequestControl
158 * @see  SoftDeleteResponseControl
159 * @see  SoftDeletedEntryAccessRequestControl
160 * @see  UndeleteRequestControl
161 */
162@NotMutable()
163@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
164public final class SoftDeleteRequestControl
165       extends Control
166{
167  /**
168   * The OID (1.3.6.1.4.1.30221.2.5.20) for the soft delete request control.
169   */
170  @NotNull public static final String SOFT_DELETE_REQUEST_OID =
171       "1.3.6.1.4.1.30221.2.5.20";
172
173
174
175  /**
176   * The BER type for the return soft delete response element.
177   */
178  private static final byte TYPE_RETURN_SOFT_DELETE_RESPONSE = (byte) 0x80;
179
180
181
182  /**
183   * The name of the field used to hold the return-soft-delete-response-control
184   * flag in the JSON representation of this control.
185   */
186  @NotNull private static final String
187       JSON_FIELD_RETURN_SOFT_DELETE_RESPONSE_CONTROL =
188            "return-soft-delete-response-control";
189
190
191
192  /**
193   * The serial version UID for this serializable class.
194   */
195  private static final long serialVersionUID = 4068029406430690545L;
196
197
198
199  // Indicates whether to the response should include a soft delete response
200  // control.
201  private final boolean returnSoftDeleteResponse;
202
203
204
205  /**
206   * Creates a new soft delete request control with the default settings for
207   * all elements.  It will be marked critical.
208   */
209  public SoftDeleteRequestControl()
210  {
211    this(true, true);
212  }
213
214
215
216  /**
217   * Creates a new soft delete request control with the provided information.
218   *
219   * @param  isCritical                Indicates whether this control should be
220   *                                   marked critical.  This will only have an
221   *                                   effect on the way the associated delete
222   *                                   operation is handled by servers which do
223   *                                   NOT support the soft delete request
224   *                                   control.  For such servers, a control
225   *                                   that is critical will cause the soft
226   *                                   delete attempt to fail, while a control
227   *                                   that is not critical will be processed as
228   *                                   if the control was not included in the
229   *                                   request (i.e., as a normal "hard"
230   *                                   delete).
231   * @param  returnSoftDeleteResponse  Indicates whether to return a soft delete
232   *                                   response control in the delete response
233   *                                   to the client.
234   */
235  public SoftDeleteRequestControl(final boolean isCritical,
236                                  final boolean returnSoftDeleteResponse)
237  {
238    super(SOFT_DELETE_REQUEST_OID, isCritical,
239         encodeValue(returnSoftDeleteResponse));
240
241    this.returnSoftDeleteResponse = returnSoftDeleteResponse;
242  }
243
244
245
246  /**
247   * Creates a new soft delete request control which is decoded from the
248   * provided generic control.
249   *
250   * @param  control  The generic control to be decoded as a soft delete request
251   *                  control.
252   *
253   * @throws  LDAPException  If the provided control cannot be decoded as a soft
254   *                         delete request control.
255   */
256  public SoftDeleteRequestControl(@NotNull final Control control)
257         throws LDAPException
258  {
259    super(control);
260
261    boolean returnResponse = true;
262    if (control.hasValue())
263    {
264      try
265      {
266        final ASN1Sequence valueSequence =
267             ASN1Sequence.decodeAsSequence(control.getValue().getValue());
268        for (final ASN1Element e : valueSequence.elements())
269        {
270          switch (e.getType())
271          {
272            case TYPE_RETURN_SOFT_DELETE_RESPONSE:
273              returnResponse = ASN1Boolean.decodeAsBoolean(e).booleanValue();
274              break;
275            default:
276              throw new LDAPException(ResultCode.DECODING_ERROR,
277                   ERR_SOFT_DELETE_REQUEST_UNSUPPORTED_VALUE_ELEMENT_TYPE.get(
278                        StaticUtils.toHex(e.getType())));
279          }
280        }
281      }
282      catch (final LDAPException le)
283      {
284        Debug.debugException(le);
285        throw le;
286      }
287      catch (final Exception e)
288      {
289        Debug.debugException(e);
290        throw new LDAPException(ResultCode.DECODING_ERROR,
291             ERR_SOFT_DELETE_REQUEST_CANNOT_DECODE_VALUE.get(
292                  StaticUtils.getExceptionMessage(e)),
293             e);
294      }
295    }
296
297    returnSoftDeleteResponse = returnResponse;
298  }
299
300
301
302  /**
303   * Encodes the provided information into an ASN.1 octet string suitable for
304   * use as the value of a soft delete request control.
305   *
306   * @param  returnSoftDeleteResponse  Indicates whether to return a soft delete
307   *                                   response control in the delete response
308   *                                   to the client.
309   *
310   * @return  An ASN.1 octet string with an encoding suitable for use as the
311   *          value of a soft delete request control, or {@code null} if no
312   *          value is needed for the control.
313   */
314  @Nullable()
315  private static ASN1OctetString encodeValue(
316                                      final boolean returnSoftDeleteResponse)
317  {
318    if (returnSoftDeleteResponse)
319    {
320      return null;
321    }
322
323    final ArrayList<ASN1Element> elements = new ArrayList<>(1);
324    elements.add(new ASN1Boolean(TYPE_RETURN_SOFT_DELETE_RESPONSE, false));
325    return new ASN1OctetString(new ASN1Sequence(elements).encode());
326  }
327
328
329
330  /**
331   * Indicates whether the delete response should include a
332   * {@link SoftDeleteResponseControl}.
333   *
334   * @return  {@code true} if the delete response should include a soft delete
335   *          response control, or {@code false} if not.
336   */
337  public boolean returnSoftDeleteResponse()
338  {
339    return returnSoftDeleteResponse;
340  }
341
342
343
344  /**
345   * Creates a new delete request that may be used to soft delete the specified
346   * target entry.
347   *
348   * @param  targetDN                  The DN of the entry to be soft deleted.
349   * @param  isCritical                Indicates whether this control should be
350   *                                   marked critical.  This will only have an
351   *                                   effect on the way the associated delete
352   *                                   operation is handled by servers which do
353   *                                   NOT support the soft delete request
354   *                                   control.  For such servers, a control
355   *                                   that is critical will cause the soft
356   *                                   delete attempt to fail, while a control
357   *                                   that is not critical will be processed as
358   *                                   if the control was not included in the
359   *                                   request (i.e., as a normal "hard"
360   *                                   delete).
361   * @param  returnSoftDeleteResponse  Indicates whether to return a soft delete
362   *                                   response control in the delete response
363   *                                   to the client.
364   *
365   * @return  A delete request with the specified target DN and an appropriate
366   *          soft delete request control.
367   */
368  @NotNull()
369  public static DeleteRequest createSoftDeleteRequest(
370              @NotNull final String targetDN,
371              final boolean isCritical,
372              final boolean returnSoftDeleteResponse)
373  {
374    final Control[] controls =
375    {
376      new SoftDeleteRequestControl(isCritical, returnSoftDeleteResponse)
377    };
378
379    return new DeleteRequest(targetDN, controls);
380  }
381
382
383
384  /**
385   * {@inheritDoc}
386   */
387  @Override()
388  @NotNull()
389  public String getControlName()
390  {
391    return INFO_CONTROL_NAME_SOFT_DELETE_REQUEST.get();
392  }
393
394
395
396  /**
397   * Retrieves a representation of this soft delete request control as a JSON
398   * 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 soft delete request control, the
403   *     OID is "1.3.6.1.4.1.30221.2.5.20".
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 soft delete
418   *     request 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 soft delete request
424   *     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 return-soft-delete-response-control} -- A mandatory Boolean
431   *         field that indicates whether to include a corresponding soft delete
432   *         response control in the delete result.
433   *       </LI>
434   *     </UL>
435   *   </LI>
436   * </UL>
437   *
438   * @return  A JSON object that contains a representation of this control.
439   */
440  @Override()
441  @NotNull()
442  public JSONObject toJSONControl()
443  {
444    return new JSONObject(
445         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
446              SOFT_DELETE_REQUEST_OID),
447         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
448              INFO_CONTROL_NAME_SOFT_DELETE_REQUEST.get()),
449         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
450              isCritical()),
451         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
452              new JSONObject(
453                   new JSONField(JSON_FIELD_RETURN_SOFT_DELETE_RESPONSE_CONTROL,
454                        returnSoftDeleteResponse))));
455  }
456
457
458
459  /**
460   * Attempts to decode the provided object as a JSON representation of a
461   * soft delete request control.
462   *
463   * @param  controlObject  The JSON object to be decoded.  It must not be
464   *                        {@code null}.
465   * @param  strict         Indicates whether to use strict mode when decoding
466   *                        the provided JSON object.  If this is {@code true},
467   *                        then this method will throw an exception if the
468   *                        provided JSON object contains any unrecognized
469   *                        fields.  If this is {@code false}, then unrecognized
470   *                        fields will be ignored.
471   *
472   * @return  The soft delete request control that was decoded from
473   *          the provided JSON object.
474   *
475   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
476   *                         valid soft delete request control.
477   */
478  @NotNull()
479  public static SoftDeleteRequestControl decodeJSONControl(
480              @NotNull final JSONObject controlObject,
481              final boolean strict)
482         throws LDAPException
483  {
484    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
485         controlObject, strict, true, true);
486
487    final ASN1OctetString rawValue = jsonControl.getRawValue();
488    if (rawValue != null)
489    {
490      return new SoftDeleteRequestControl(new Control(
491           jsonControl.getOID(), jsonControl.getCriticality(), rawValue));
492    }
493
494
495    final JSONObject valueObject = jsonControl.getValueObject();
496
497    final Boolean returnSoftDeleteResponseControl =
498         valueObject.getFieldAsBoolean(
499              JSON_FIELD_RETURN_SOFT_DELETE_RESPONSE_CONTROL);
500    if (returnSoftDeleteResponseControl == null)
501    {
502      throw new LDAPException(ResultCode.DECODING_ERROR,
503           ERR_SOFT_DELETE_REQUEST_JSON_VALUE_MISSING_FIELD.get(
504                controlObject.toSingleLineString(),
505                JSON_FIELD_RETURN_SOFT_DELETE_RESPONSE_CONTROL));
506    }
507
508
509    if (strict)
510    {
511      final List<String> unrecognizedFields =
512           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
513                valueObject, JSON_FIELD_RETURN_SOFT_DELETE_RESPONSE_CONTROL);
514      if (! unrecognizedFields.isEmpty())
515      {
516        throw new LDAPException(ResultCode.DECODING_ERROR,
517             ERR_SOFT_DELETE_REQUEST_JSON_VALUE_UNRECOGNIZED_FIELD.get(
518                  controlObject.toSingleLineString(),
519                  unrecognizedFields.get(0)));
520      }
521    }
522
523
524    return new SoftDeleteRequestControl(
525         jsonControl.getCriticality(), returnSoftDeleteResponseControl);
526  }
527
528
529
530  /**
531   * {@inheritDoc}
532   */
533  @Override()
534  public void toString(@NotNull final StringBuilder buffer)
535  {
536    buffer.append("SoftDeleteRequestControl(isCritical=");
537    buffer.append(isCritical());
538    buffer.append(", returnSoftDeleteResponse=");
539    buffer.append(returnSoftDeleteResponse);
540    buffer.append(')');
541  }
542}