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 pre-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 before 079 * processing a delete, modify, or modify DN operation. 080 * <BR><BR> 081 * If the corresponding delete, modify, or modify DN request included the 082 * {@link PreReadRequestControl} and the operation was successful, then the 083 * response for that operation should include the pre-read response control with 084 * a read-only copy of the entry as it appeared immediately before processing 085 * the request. If the operation was not successful, then the pre-read response 086 * control will not be returned. 087 */ 088@NotMutable() 089@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 090public final class PreReadResponseControl 091 extends Control 092 implements DecodeableControl 093{ 094 /** 095 * The OID (1.3.6.1.1.13.1) for the pre-read response control. 096 */ 097 @NotNull public static final String PRE_READ_RESPONSE_OID = "1.3.6.1.1.13.1"; 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 = -4719875382095056686L; 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 PreReadResponseControl() 126 { 127 entry = null; 128 } 129 130 131 132 /** 133 * Creates a new pre-read response control including the provided entry. 134 * 135 * @param entry The entry to include in this pre-read response control. It 136 * must not be {@code null}. 137 */ 138 public PreReadResponseControl(@NotNull final ReadOnlyEntry entry) 139 { 140 super(PRE_READ_RESPONSE_OID, false, encodeValue(entry)); 141 142 this.entry = entry; 143 } 144 145 146 147 /** 148 * Creates a new pre-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 * pre-read response control. 158 */ 159 public PreReadResponseControl(@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_PRE_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_PRE_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_PRE_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_PRE_READ_RESPONSE_ATTRIBUTES_NOT_SEQUENCE.get(ae), ae); 207 } 208 209 final ASN1Element[] attrElements = attrSequence.elements(); 210 final Attribute[] attrs = new Attribute[attrElements.length]; 211 for (int i=0; i < attrElements.length; i++) 212 { 213 try 214 { 215 attrs[i] = 216 Attribute.decode(ASN1Sequence.decodeAsSequence(attrElements[i])); 217 } 218 catch (final ASN1Exception ae) 219 { 220 Debug.debugException(ae); 221 throw new LDAPException(ResultCode.DECODING_ERROR, 222 ERR_PRE_READ_RESPONSE_ATTR_NOT_SEQUENCE.get(ae), ae); 223 } 224 } 225 226 entry = new ReadOnlyEntry(dn, attrs); 227 } 228 229 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override() 235 @NotNull() 236 public PreReadResponseControl decodeControl(@NotNull final String oid, 237 final boolean isCritical, 238 @Nullable final ASN1OctetString value) 239 throws LDAPException 240 { 241 return new PreReadResponseControl(oid, isCritical, value); 242 } 243 244 245 246 /** 247 * Extracts a pre-read response control from the provided result. 248 * 249 * @param result The result from which to retrieve the pre-read response 250 * control. 251 * 252 * @return The pre-read response control contained in the provided result, or 253 * {@code null} if the result did not contain a pre-read response 254 * control. 255 * 256 * @throws LDAPException If a problem is encountered while attempting to 257 * decode the pre-read response control contained in 258 * the provided result. 259 */ 260 @Nullable() 261 public static PreReadResponseControl get(@NotNull final LDAPResult result) 262 throws LDAPException 263 { 264 final Control c = result.getResponseControl(PRE_READ_RESPONSE_OID); 265 if (c == null) 266 { 267 return null; 268 } 269 270 if (c instanceof PreReadResponseControl) 271 { 272 return (PreReadResponseControl) c; 273 } 274 else 275 { 276 return new PreReadResponseControl(c.getOID(), c.isCritical(), 277 c.getValue()); 278 } 279 } 280 281 282 283 /** 284 * Encodes the provided information into an octet string that can be used as 285 * the value for this control. 286 * 287 * @param entry The entry to include in this pre-read response control. It 288 * must not be {@code null}. 289 * 290 * @return An ASN.1 octet string that can be used as the value for this 291 * control. 292 */ 293 @NotNull() 294 private static ASN1OctetString encodeValue(@NotNull final ReadOnlyEntry entry) 295 { 296 Validator.ensureNotNull(entry); 297 298 final Collection<Attribute> attrs = entry.getAttributes(); 299 final ArrayList<ASN1Element> attrElements = new ArrayList<>(attrs.size()); 300 for (final Attribute a : attrs) 301 { 302 attrElements.add(a.encode()); 303 } 304 305 final ASN1Element[] entryElements = 306 { 307 new ASN1OctetString(entry.getDN()), 308 new ASN1Sequence(attrElements) 309 }; 310 311 return new ASN1OctetString(new ASN1Sequence(entryElements).encode()); 312 } 313 314 315 316 /** 317 * Retrieves a read-only copy of the entry returned by this post-read response 318 * control. 319 * 320 * @return A read-only copy of the entry returned by this post-read response 321 * control. 322 */ 323 @NotNull() 324 public ReadOnlyEntry getEntry() 325 { 326 return entry; 327 } 328 329 330 331 /** 332 * {@inheritDoc} 333 */ 334 @Override() 335 @NotNull() 336 public String getControlName() 337 { 338 return INFO_CONTROL_NAME_PRE_READ_RESPONSE.get(); 339 } 340 341 342 343 /** 344 * Retrieves a representation of this pre-read response control as a JSON 345 * object. The JSON object uses the following fields: 346 * <UL> 347 * <LI> 348 * {@code oid} -- A mandatory string field whose value is the object 349 * identifier for this control. For the pre-read response control, the 350 * OID is "1.3.6.1.1.13.1". 351 * </LI> 352 * <LI> 353 * {@code control-name} -- An optional string field whose value is a 354 * human-readable name for this control. This field is only intended for 355 * descriptive purposes, and when decoding a control, the {@code oid} 356 * field should be used to identify the type of control. 357 * </LI> 358 * <LI> 359 * {@code criticality} -- A mandatory Boolean field used to indicate 360 * whether this control is considered critical. 361 * </LI> 362 * <LI> 363 * {@code value-base64} -- An optional string field whose value is a 364 * base64-encoded representation of the raw value for this pre-read 365 * response control. Exactly one of the {@code value-base64} and 366 * {@code value-json} fields must be present. 367 * </LI> 368 * <LI> 369 * {@code value-json} -- An optional JSON object field whose value is a 370 * user-friendly representation of the value for this pre-read response 371 * control. Exactly one of the {@code value-base64} and 372 * {@code value-json} fields must be present, and if the 373 * {@code value-json} field is used, it must include a 374 * "{@code _dn}" field whose value is the DN of the entry, and all other 375 * fields will have a name that is the name of an LDAP attribute in the 376 * entry and a value that is an array containing the string 377 * representations of the values for that attribute. 378 * </LI> 379 * </UL> 380 * 381 * @return A JSON object that contains a representation of this control. 382 */ 383 @Override() 384 @NotNull() 385 public JSONObject toJSONControl() 386 { 387 final Map<String,JSONValue> valueFields = new LinkedHashMap<>(); 388 valueFields.put(JSON_FIELD_DN, new JSONString(entry.getDN())); 389 390 for (final Attribute a : entry.getAttributes()) 391 { 392 final List<JSONValue> attrValueValues = new ArrayList<>(a.size()); 393 for (final String value : a.getValues()) 394 { 395 attrValueValues.add(new JSONString(value)); 396 } 397 398 valueFields.put(a.getName(), new JSONArray(attrValueValues)); 399 } 400 401 return new JSONObject( 402 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, 403 PRE_READ_RESPONSE_OID), 404 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 405 INFO_CONTROL_NAME_PRE_READ_RESPONSE.get()), 406 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 407 isCritical()), 408 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 409 new JSONObject(valueFields))); 410 } 411 412 413 414 /** 415 * Attempts to decode the provided object as a JSON representation of a 416 * pre-read response control. 417 * 418 * @param controlObject The JSON object to be decoded. It must not be 419 * {@code null}. 420 * @param strict Indicates whether to use strict mode when decoding 421 * the provided JSON object. If this is {@code true}, 422 * then this method will throw an exception if the 423 * provided JSON object contains any unrecognized 424 * fields. If this is {@code false}, then unrecognized 425 * fields will be ignored. 426 * 427 * @return The pre-read response control that was decoded from the provided 428 * JSON object. 429 * 430 * @throws LDAPException If the provided JSON object cannot be parsed as a 431 * valid pre-read response control. 432 */ 433 @NotNull() 434 public static PreReadResponseControl decodeJSONControl( 435 @NotNull final JSONObject controlObject, 436 final boolean strict) 437 throws LDAPException 438 { 439 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 440 controlObject, strict, true, true); 441 442 final ASN1OctetString rawValue = jsonControl.getRawValue(); 443 if (rawValue != null) 444 { 445 return new PreReadResponseControl(jsonControl.getOID(), 446 jsonControl.getCriticality(), rawValue); 447 } 448 449 450 final JSONObject valueObject = jsonControl.getValueObject(); 451 452 String dn = null; 453 final List<Attribute> attributes = 454 new ArrayList<>(valueObject.getFields().size()); 455 for (final Map.Entry<String,JSONValue> e : 456 valueObject.getFields().entrySet()) 457 { 458 final String fieldName = e.getKey(); 459 final JSONValue fieldValue = e.getValue(); 460 if (fieldName.equals(JSON_FIELD_DN)) 461 { 462 if (fieldValue instanceof JSONString) 463 { 464 dn = ((JSONString) fieldValue).stringValue(); 465 } 466 else 467 { 468 throw new LDAPException(ResultCode.DECODING_ERROR, 469 ERR_PRE_READ_RESPONSE_JSON_DN_NOT_STRING.get( 470 controlObject.toSingleLineString(), JSON_FIELD_DN)); 471 } 472 } 473 else 474 { 475 if (fieldValue instanceof JSONArray) 476 { 477 final List<JSONValue> attrValueValues = 478 ((JSONArray) fieldValue).getValues(); 479 final List<String> attributeValues = 480 new ArrayList<>(attrValueValues.size()); 481 for (final JSONValue v : attrValueValues) 482 { 483 if (v instanceof JSONString) 484 { 485 attributeValues.add(((JSONString) v).stringValue()); 486 } 487 else 488 { 489 throw new LDAPException(ResultCode.DECODING_ERROR, 490 ERR_PRE_READ_RESPONSE_JSON_ATTR_VALUE_NOT_STRING.get( 491 controlObject.toSingleLineString(), fieldName)); 492 } 493 } 494 495 attributes.add(new Attribute(fieldName, attributeValues)); 496 } 497 else 498 { 499 throw new LDAPException(ResultCode.DECODING_ERROR, 500 ERR_PRE_READ_RESPONSE_JSON_ATTR_VALUE_NOT_ARRAY.get( 501 controlObject.toSingleLineString(), fieldName)); 502 } 503 } 504 } 505 506 507 if (dn == null) 508 { 509 throw new LDAPException(ResultCode.DECODING_ERROR, 510 ERR_PRE_READ_RESPONSE_JSON_MISSING_DN.get( 511 controlObject.toSingleLineString(), JSON_FIELD_DN)); 512 } 513 514 515 return new PreReadResponseControl(new ReadOnlyEntry(dn, attributes)); 516 } 517 518 519 520 /** 521 * {@inheritDoc} 522 */ 523 @Override() 524 public void toString(@NotNull final StringBuilder buffer) 525 { 526 buffer.append("PreReadResponseControl(entry="); 527 entry.toString(buffer); 528 buffer.append(", isCritical="); 529 buffer.append(isCritical()); 530 buffer.append(')'); 531 } 532}