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}