001/* 002 * Copyright 2009-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2009-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) 2009-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.Collections; 042import java.util.Iterator; 043import java.util.LinkedHashMap; 044import java.util.List; 045import java.util.Map; 046 047import com.unboundid.asn1.ASN1Element; 048import com.unboundid.asn1.ASN1Enumerated; 049import com.unboundid.asn1.ASN1OctetString; 050import com.unboundid.asn1.ASN1Sequence; 051import com.unboundid.ldap.sdk.Attribute; 052import com.unboundid.ldap.sdk.Control; 053import com.unboundid.ldap.sdk.DecodeableControl; 054import com.unboundid.ldap.sdk.JSONControlDecodeHelper; 055import com.unboundid.ldap.sdk.LDAPException; 056import com.unboundid.ldap.sdk.ResultCode; 057import com.unboundid.ldap.sdk.SearchResultEntry; 058import com.unboundid.util.Debug; 059import com.unboundid.util.NotMutable; 060import com.unboundid.util.NotNull; 061import com.unboundid.util.Nullable; 062import com.unboundid.util.StaticUtils; 063import com.unboundid.util.ThreadSafety; 064import com.unboundid.util.ThreadSafetyLevel; 065import com.unboundid.util.Validator; 066import com.unboundid.util.json.JSONArray; 067import com.unboundid.util.json.JSONField; 068import com.unboundid.util.json.JSONNumber; 069import com.unboundid.util.json.JSONObject; 070import com.unboundid.util.json.JSONString; 071import com.unboundid.util.json.JSONValue; 072 073import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 074 075 076 077/** 078 * This class provides an implementation of a control that may be included in a 079 * search result entry in response to a join request control to provide a set of 080 * entries related to the search result entry. See the class-level 081 * documentation for the {@link JoinRequestControl} class for additional 082 * information and an example demonstrating its use. 083 * <BR> 084 * <BLOCKQUOTE> 085 * <B>NOTE:</B> This class, and other classes within the 086 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 087 * supported for use against Ping Identity, UnboundID, and 088 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 089 * for proprietary functionality or for external specifications that are not 090 * considered stable or mature enough to be guaranteed to work in an 091 * interoperable way with other types of LDAP servers. 092 * </BLOCKQUOTE> 093 * <BR> 094 * The value of the join result control is encoded as follows: 095 * <PRE> 096 * JoinResult ::= SEQUENCE { 097 * COMPONENTS OF LDAPResult, 098 * entries [4] SEQUENCE OF JoinedEntry } 099 * </PRE> 100 */ 101@NotMutable() 102@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 103public final class JoinResultControl 104 extends Control 105 implements DecodeableControl 106{ 107 /** 108 * The OID (1.3.6.1.4.1.30221.2.5.9) for the join result control. 109 */ 110 @NotNull public static final String JOIN_RESULT_OID = 111 "1.3.6.1.4.1.30221.2.5.9"; 112 113 114 115 /** 116 * The BER type for the referral URLs element. 117 */ 118 private static final byte TYPE_REFERRAL_URLS = (byte) 0xA3; 119 120 121 122 /** 123 * The BER type for the join results element. 124 */ 125 private static final byte TYPE_JOIN_RESULTS = (byte) 0xA4; 126 127 128 129 /** 130 * The name of the field used to hold the diagnostic message in the JSON 131 * representation of this control. 132 */ 133 @NotNull private static final String JSON_FIELD_DIAGNOSTIC_MESSAGE = 134 "diagnostic-message"; 135 136 137 138 /** 139 * The name of the field used to hold the DN of an entry in the JSON 140 * representation of this control. 141 */ 142 @NotNull private static final String JSON_FIELD_ENTRY_DN = "_dn"; 143 144 145 146 /** 147 * The name of the field used to hold the set of joined entries in the JSON 148 * representation of this control. 149 */ 150 @NotNull private static final String JSON_FIELD_JOINED_ENTRIES = 151 "joined-entries"; 152 153 154 155 /** 156 * The name of the field used to hold the matched DN in the JSON 157 * representation of this control. 158 */ 159 @NotNull private static final String JSON_FIELD_MATCHED_DN = "matched-dn"; 160 161 162 163 /** 164 * The name of the field used to hold the nested join results in the JSON 165 * representation of this control. 166 */ 167 @NotNull private static final String JSON_FIELD_NESTED_JOIN_RESULTS = 168 "_nested-join-results"; 169 170 171 172 /** 173 * The name of the field used to hold the referral URLs in the JSON 174 * representation of this control. 175 */ 176 @NotNull private static final String JSON_FIELD_REFERRAL_URLS = 177 "referral-urls"; 178 179 180 181 /** 182 * The name of the field used to hold the join result code in the JSON 183 * representation of this control. 184 */ 185 @NotNull private static final String JSON_FIELD_RESULT_CODE = "result-code"; 186 187 188 189 /** 190 * The serial version UID for this serializable class. 191 */ 192 private static final long serialVersionUID = 681831114773253358L; 193 194 195 196 // The set of entries which have been joined with the associated search result 197 // entry. 198 @NotNull private final List<JoinedEntry> joinResults; 199 200 // The set of referral URLs for this join result. 201 @NotNull private final List<String> referralURLs; 202 203 // The result code for this join result. 204 @NotNull private final ResultCode resultCode; 205 206 // The diagnostic message for this join result. 207 @Nullable private final String diagnosticMessage; 208 209 // The matched DN for this join result. 210 @Nullable private final String matchedDN; 211 212 213 214 /** 215 * Creates a new empty control instance that is intended to be used only for 216 * decoding controls via the {@code DecodeableControl} interface. 217 */ 218 JoinResultControl() 219 { 220 resultCode = null; 221 diagnosticMessage = null; 222 matchedDN = null; 223 referralURLs = null; 224 joinResults = null; 225 } 226 227 228 229 /** 230 * Creates a new join result control indicating a successful join. 231 * 232 * @param joinResults The set of entries that have been joined with the 233 * associated search result entry. It may be 234 * {@code null} or empty if no entries were joined with 235 * the search result entry. 236 */ 237 public JoinResultControl(@Nullable final List<JoinedEntry> joinResults) 238 { 239 this(ResultCode.SUCCESS, null, null, null, joinResults); 240 } 241 242 243 244 /** 245 * Creates a new join result control with the provided information. 246 * 247 * @param resultCode The result code for the join processing. It 248 * must not be {@code null}. 249 * @param diagnosticMessage A message with additional information about the 250 * result of the join processing. It may be 251 * {@code null} if no message is needed. 252 * @param matchedDN The matched DN for the join processing. It may 253 * be {@code null} if no matched DN is needed. 254 * @param referralURLs The set of referral URLs for any referrals 255 * encountered while processing the join. It may 256 * be {@code null} or empty if no referral URLs 257 * are needed. 258 * @param joinResults The set of entries that have been joined with 259 * associated search result entry. It may be 260 * {@code null} or empty if no entries were joined 261 * with the search result entry. 262 */ 263 public JoinResultControl(@NotNull final ResultCode resultCode, 264 @Nullable final String diagnosticMessage, 265 @Nullable final String matchedDN, 266 @Nullable final List<String> referralURLs, 267 @Nullable final List<JoinedEntry> joinResults) 268 { 269 super(JOIN_RESULT_OID, false, 270 encodeValue(resultCode, diagnosticMessage, matchedDN, referralURLs, 271 joinResults)); 272 273 this.resultCode = resultCode; 274 this.diagnosticMessage = diagnosticMessage; 275 this.matchedDN = matchedDN; 276 277 if (referralURLs == null) 278 { 279 this.referralURLs = Collections.emptyList(); 280 } 281 else 282 { 283 this.referralURLs = Collections.unmodifiableList(referralURLs); 284 } 285 286 if (joinResults == null) 287 { 288 this.joinResults = Collections.emptyList(); 289 } 290 else 291 { 292 this.joinResults = Collections.unmodifiableList(joinResults); 293 } 294 } 295 296 297 298 /** 299 * Creates a new join result control with the provided information. 300 * 301 * @param oid The OID for the control. 302 * @param isCritical Indicates whether the control should be marked 303 * critical. 304 * @param value The encoded value for the control. This may be 305 * {@code null} if no value was provided. 306 * 307 * @throws LDAPException If the provided control cannot be decoded as an 308 * account usable response control. 309 */ 310 public JoinResultControl(@NotNull final String oid, final boolean isCritical, 311 @Nullable final ASN1OctetString value) 312 throws LDAPException 313 { 314 super(oid, isCritical, value); 315 316 if (value == null) 317 { 318 throw new LDAPException(ResultCode.DECODING_ERROR, 319 ERR_JOIN_RESULT_NO_VALUE.get()); 320 } 321 322 try 323 { 324 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 325 final ASN1Element[] elements = 326 ASN1Sequence.decodeAsSequence(valueElement).elements(); 327 328 resultCode = ResultCode.valueOf( 329 ASN1Enumerated.decodeAsEnumerated(elements[0]).intValue()); 330 331 final String matchedDNStr = 332 ASN1OctetString.decodeAsOctetString(elements[1]).stringValue(); 333 if (matchedDNStr.isEmpty()) 334 { 335 matchedDN = null; 336 } 337 else 338 { 339 matchedDN = matchedDNStr; 340 } 341 342 final String diagnosticMessageStr = 343 ASN1OctetString.decodeAsOctetString(elements[2]).stringValue(); 344 if (diagnosticMessageStr.isEmpty()) 345 { 346 diagnosticMessage = null; 347 } 348 else 349 { 350 diagnosticMessage = diagnosticMessageStr; 351 } 352 353 final ArrayList<String> refs = new ArrayList<>(5); 354 final ArrayList<JoinedEntry> entries = new ArrayList<>(20); 355 for (int i=3; i < elements.length; i++) 356 { 357 switch (elements[i].getType()) 358 { 359 case TYPE_REFERRAL_URLS: 360 final ASN1Element[] refElements = 361 ASN1Sequence.decodeAsSequence(elements[i]).elements(); 362 for (final ASN1Element e : refElements) 363 { 364 refs.add(ASN1OctetString.decodeAsOctetString(e).stringValue()); 365 } 366 break; 367 368 case TYPE_JOIN_RESULTS: 369 final ASN1Element[] entryElements = 370 ASN1Sequence.decodeAsSequence(elements[i]).elements(); 371 for (final ASN1Element e : entryElements) 372 { 373 entries.add(JoinedEntry.decode(e)); 374 } 375 break; 376 377 default: 378 throw new LDAPException(ResultCode.DECODING_ERROR, 379 ERR_JOIN_RESULT_INVALID_ELEMENT_TYPE.get( 380 StaticUtils.toHex(elements[i].getType()))); 381 } 382 } 383 384 referralURLs = Collections.unmodifiableList(refs); 385 joinResults = Collections.unmodifiableList(entries); 386 } 387 catch (final Exception e) 388 { 389 Debug.debugException(e); 390 391 throw new LDAPException(ResultCode.DECODING_ERROR, 392 ERR_JOIN_RESULT_CANNOT_DECODE.get( 393 StaticUtils.getExceptionMessage(e)), 394 e); 395 } 396 } 397 398 399 400 /** 401 * Encodes the provided information as appropriate for use as the value of 402 * this control. 403 * 404 * @param resultCode The result code for the join processing. It 405 * must not be {@code null}. 406 * @param diagnosticMessage A message with additional information about the 407 * result of the join processing. It may be 408 * {@code null} if no message is needed. 409 * @param matchedDN The matched DN for the join processing. It may 410 * be {@code null} if no matched DN is needed. 411 * @param referralURLs The set of referral URLs for any referrals 412 * encountered while processing the join. It may 413 * be {@code null} or empty if no referral URLs 414 * are needed. 415 * @param joinResults The set of entries that have been joined with 416 * associated search result entry. It may be 417 * {@code null} or empty if no entries were joined 418 * with the search result entry. 419 * 420 * @return An ASN.1 element containing an encoded representation of the 421 * value for this control. 422 */ 423 @NotNull() 424 private static ASN1OctetString encodeValue( 425 @NotNull final ResultCode resultCode, 426 @Nullable final String diagnosticMessage, 427 @Nullable final String matchedDN, 428 @Nullable final List<String> referralURLs, 429 @Nullable final List<JoinedEntry> joinResults) 430 { 431 Validator.ensureNotNull(resultCode); 432 433 final ArrayList<ASN1Element> elements = new ArrayList<>(5); 434 elements.add(new ASN1Enumerated(resultCode.intValue())); 435 436 if (matchedDN == null) 437 { 438 elements.add(new ASN1OctetString()); 439 } 440 else 441 { 442 elements.add(new ASN1OctetString(matchedDN)); 443 } 444 445 if (diagnosticMessage == null) 446 { 447 elements.add(new ASN1OctetString()); 448 } 449 else 450 { 451 elements.add(new ASN1OctetString(diagnosticMessage)); 452 } 453 454 if ((referralURLs != null) && (! referralURLs.isEmpty())) 455 { 456 final ArrayList<ASN1Element> refElements = 457 new ArrayList<>(referralURLs.size()); 458 for (final String s : referralURLs) 459 { 460 refElements.add(new ASN1OctetString(s)); 461 } 462 elements.add(new ASN1Sequence(TYPE_REFERRAL_URLS, refElements)); 463 } 464 465 if ((joinResults == null) || joinResults.isEmpty()) 466 { 467 elements.add(new ASN1Sequence(TYPE_JOIN_RESULTS)); 468 } 469 else 470 { 471 final ArrayList<ASN1Element> entryElements = 472 new ArrayList<>(joinResults.size()); 473 for (final JoinedEntry e : joinResults) 474 { 475 entryElements.add(e.encode()); 476 } 477 elements.add(new ASN1Sequence(TYPE_JOIN_RESULTS, entryElements)); 478 } 479 480 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 481 } 482 483 484 485 /** 486 * Retrieves the result code for this join result. 487 * 488 * @return The result code for this join result. 489 */ 490 @NotNull() 491 public ResultCode getResultCode() 492 { 493 return resultCode; 494 } 495 496 497 498 /** 499 * Retrieves the diagnostic message for this join result. 500 * 501 * @return The diagnostic message for this join result, or {@code null} if 502 * there is no diagnostic message. 503 */ 504 @Nullable() 505 public String getDiagnosticMessage() 506 { 507 return diagnosticMessage; 508 } 509 510 511 512 /** 513 * Retrieves the matched DN for this join result. 514 * 515 * @return The matched DN for this join result, or {@code null} if there is 516 * no matched DN. 517 */ 518 @Nullable() 519 public String getMatchedDN() 520 { 521 return matchedDN; 522 } 523 524 525 526 /** 527 * Retrieves the set of referral URLs for this join result. 528 * 529 * @return The set of referral URLs for this join result, or an empty list 530 * if there are no referral URLs. 531 */ 532 @NotNull() 533 public List<String> getReferralURLs() 534 { 535 return referralURLs; 536 } 537 538 539 540 /** 541 * Retrieves the set of entries that have been joined with the associated 542 * search result entry. 543 * 544 * @return The set of entries that have been joined with the associated 545 * search result entry. 546 */ 547 @NotNull() 548 public List<JoinedEntry> getJoinResults() 549 { 550 return joinResults; 551 } 552 553 554 555 /** 556 * {@inheritDoc} 557 */ 558 @Override() 559 @NotNull() 560 public JoinResultControl decodeControl(@NotNull final String oid, 561 final boolean isCritical, 562 @Nullable final ASN1OctetString value) 563 throws LDAPException 564 { 565 return new JoinResultControl(oid, isCritical, value); 566 } 567 568 569 570 /** 571 * Extracts a join result control from the provided search result entry. 572 * 573 * @param entry The search result entry from which to retrieve the join 574 * result control. 575 * 576 * @return The join result control contained in the provided search result 577 * entry, or {@code null} if the entry did not contain a join result 578 * control. 579 * 580 * @throws LDAPException If a problem is encountered while attempting to 581 * decode the join result control contained in the 582 * provided search result entry. 583 */ 584 @Nullable() 585 public static JoinResultControl get(@NotNull final SearchResultEntry entry) 586 throws LDAPException 587 { 588 final Control c = entry.getControl(JOIN_RESULT_OID); 589 if (c == null) 590 { 591 return null; 592 } 593 594 if (c instanceof JoinResultControl) 595 { 596 return (JoinResultControl) c; 597 } 598 else 599 { 600 return new JoinResultControl(c.getOID(), c.isCritical(), c.getValue()); 601 } 602 } 603 604 605 606 /** 607 * {@inheritDoc} 608 */ 609 @Override() 610 @NotNull() 611 public String getControlName() 612 { 613 return INFO_CONTROL_NAME_JOIN_RESULT.get(); 614 } 615 616 617 618 /** 619 * Retrieves a representation of this join result control as a JSON object. 620 * The JSON object uses the following fields: 621 * <UL> 622 * <LI> 623 * {@code oid} -- A mandatory string field whose value is the object 624 * identifier for this control. For the join result control, the OID is 625 * "1.3.6.1.4.1.30221.2.5.9". 626 * </LI> 627 * <LI> 628 * {@code control-name} -- An optional string field whose value is a 629 * human-readable name for this control. This field is only intended for 630 * descriptive purposes, and when decoding a control, the {@code oid} 631 * field should be used to identify the type of control. 632 * </LI> 633 * <LI> 634 * {@code criticality} -- A mandatory Boolean field used to indicate 635 * whether this control is considered critical. 636 * </LI> 637 * <LI> 638 * {@code value-base64} -- An optional string field whose value is a 639 * base64-encoded representation of the raw value for this join result 640 * control. Exactly one of the {@code value-base64} and 641 * {@code value-json} fields must be present. 642 * </LI> 643 * <LI> 644 * {@code value-json} -- An optional JSON object field whose value is a 645 * user-friendly representation of the value for this join result control. 646 * Exactly one of the {@code value-base64} and {@code value-json} fields 647 * must be present, and if the {@code value-json} field is used, then it 648 * will use the following fields: 649 * <UL> 650 * <LI> 651 * {@code result-code} -- An integer field whose value is the numeric 652 * representation of the LDAP result code for join processing. 653 * </LI> 654 * <LI> 655 * {@code matched-dn} -- An optional string field whose value is the 656 * matched DN for the join processing. 657 * </LI> 658 * <LI> 659 * {@code diagnostic-message} -- An optional string field whose value 660 * is a diagnostic message with additional information about the join 661 * processing. 662 * </LI> 663 * <LI> 664 * {@code referral-urls} -- An optional array field whose values are 665 * strings that represent referral URLs encountered while performing 666 * join processing. 667 * </LI> 668 * <LI> 669 * {@code joined-entries} -- An array field whose values are JSON 670 * objects that reference entries that were joined with the source 671 * entry. Each of these JSON objects will include a 672 * "{@code _dn}" string field whose value is the DN of the entry and 673 * an optional "{@code _nested-join-results}" array field whose values 674 * are JSON objects that represent nested join results. Any other 675 * fields in the JSON objects represent attributes in the joined 676 * entry, with the name of the field representing the name of the 677 * attribute, and the value of the field being an array of strings 678 * representing the values of that attribute. 679 * </LI> 680 * </UL> 681 * </LI> 682 * </UL> 683 * 684 * @return A JSON object that contains a representation of this control. 685 */ 686 @Override() 687 @NotNull() 688 public JSONObject toJSONControl() 689 { 690 final Map<String,JSONValue> valueFields = new LinkedHashMap<>(); 691 valueFields.put(JSON_FIELD_RESULT_CODE, 692 new JSONNumber(resultCode.intValue())); 693 694 if (matchedDN != null) 695 { 696 valueFields.put(JSON_FIELD_MATCHED_DN, new JSONString(matchedDN)); 697 } 698 699 if (diagnosticMessage != null) 700 { 701 valueFields.put(JSON_FIELD_DIAGNOSTIC_MESSAGE, 702 new JSONString(diagnosticMessage)); 703 } 704 705 if ((referralURLs != null) && (! referralURLs.isEmpty())) 706 { 707 final List<JSONValue> referralValues = 708 new ArrayList<>(referralURLs.size()); 709 for (final String referralURL : referralURLs) 710 { 711 referralValues.add(new JSONString(referralURL)); 712 } 713 714 valueFields.put(JSON_FIELD_REFERRAL_URLS, new JSONArray(referralValues)); 715 } 716 717 final List<JSONValue> entryValues = new ArrayList<>(joinResults.size()); 718 for (final JoinedEntry entry : joinResults) 719 { 720 entryValues.add(encodeEntryJSON(entry)); 721 } 722 valueFields.put(JSON_FIELD_JOINED_ENTRIES, new JSONArray(entryValues)); 723 724 return new JSONObject( 725 new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID, JOIN_RESULT_OID), 726 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME, 727 INFO_CONTROL_NAME_JOIN_RESULT.get()), 728 new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY, 729 isCritical()), 730 new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON, 731 new JSONObject(valueFields))); 732 } 733 734 735 736 /** 737 * Encodes the provided joined entry to a JSON object. 738 * 739 * @param entry The entry to be encoded. It must not be {@code null}. 740 * 741 * @return The JSON object containing the encoded entry. 742 */ 743 @NotNull() 744 private static JSONObject encodeEntryJSON(@NotNull final JoinedEntry entry) 745 { 746 final Map<String,JSONValue> fields = new LinkedHashMap<>(); 747 fields.put(JSON_FIELD_ENTRY_DN, new JSONString(entry.getDN())); 748 749 for (final Attribute a : entry.getAttributes()) 750 { 751 final List<JSONValue> attrValueValues = new ArrayList<>(a.size()); 752 for (final String value : a.getValues()) 753 { 754 attrValueValues.add(new JSONString(value)); 755 } 756 757 fields.put(a.getName(), new JSONArray(attrValueValues)); 758 } 759 760 final List<JoinedEntry> nestedEntries = entry.getNestedJoinResults(); 761 if (! nestedEntries.isEmpty()) 762 { 763 final List<JSONValue> nestedEntryValues = 764 new ArrayList<>(nestedEntries.size()); 765 for (final JoinedEntry nestedEntry : nestedEntries) 766 { 767 nestedEntryValues.add(encodeEntryJSON(nestedEntry)); 768 } 769 770 fields.put(JSON_FIELD_NESTED_JOIN_RESULTS, 771 new JSONArray(nestedEntryValues)); 772 } 773 774 return new JSONObject(fields); 775 } 776 777 778 779 /** 780 * Attempts to decode the provided object as a JSON representation of a join 781 * result control. 782 * 783 * @param controlObject The JSON object to be decoded. It must not be 784 * {@code null}. 785 * @param strict Indicates whether to use strict mode when decoding 786 * the provided JSON object. If this is {@code true}, 787 * then this method will throw an exception if the 788 * provided JSON object contains any unrecognized 789 * fields. If this is {@code false}, then unrecognized 790 * fields will be ignored. 791 * 792 * @return The join result control that was decoded from the provided JSON 793 * object. 794 * 795 * @throws LDAPException If the provided JSON object cannot be parsed as a 796 * valid join result control. 797 */ 798 @NotNull() 799 public static JoinResultControl decodeJSONControl( 800 @NotNull final JSONObject controlObject, 801 final boolean strict) 802 throws LDAPException 803 { 804 final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper( 805 controlObject, strict, true, true); 806 807 final ASN1OctetString rawValue = jsonControl.getRawValue(); 808 if (rawValue != null) 809 { 810 return new JoinResultControl(jsonControl.getOID(), 811 jsonControl.getCriticality(), rawValue); 812 } 813 814 815 final JSONObject valueObject = jsonControl.getValueObject(); 816 817 final Integer resultCodeValue = 818 valueObject.getFieldAsInteger(JSON_FIELD_RESULT_CODE); 819 if (resultCodeValue == null) 820 { 821 throw new LDAPException(ResultCode.DECODING_ERROR, 822 ERR_JOIN_RESULT_JSON_MISSING_VALUE_FIELD.get( 823 controlObject.toSingleLineString(), JSON_FIELD_RESULT_CODE)); 824 } 825 826 final ResultCode resultCode = ResultCode.valueOf(resultCodeValue); 827 828 final String matchedDN = 829 valueObject.getFieldAsString(JSON_FIELD_MATCHED_DN); 830 831 final String diagnosticMessage = 832 valueObject.getFieldAsString(JSON_FIELD_DIAGNOSTIC_MESSAGE); 833 834 final List<String> referralURLs; 835 final List<JSONValue> referralURLValues = 836 valueObject.getFieldAsArray(JSON_FIELD_REFERRAL_URLS); 837 if (referralURLValues == null) 838 { 839 referralURLs = null; 840 } 841 else 842 { 843 referralURLs = new ArrayList<>(referralURLValues.size()); 844 for (final JSONValue referralURLValue : referralURLValues) 845 { 846 if (referralURLValue instanceof JSONString) 847 { 848 referralURLs.add(((JSONString) referralURLValue).stringValue()); 849 } 850 else 851 { 852 throw new LDAPException(ResultCode.DECODING_ERROR, 853 ERR_JOIN_RESULT_JSON_REFERRAL_URL_NOT_STRING.get( 854 controlObject.toSingleLineString(), 855 JSON_FIELD_REFERRAL_URLS)); 856 } 857 } 858 } 859 860 861 final List<JSONValue> joinedEntryValues = 862 valueObject.getFieldAsArray(JSON_FIELD_JOINED_ENTRIES); 863 if (joinedEntryValues == null) 864 { 865 throw new LDAPException(ResultCode.DECODING_ERROR, 866 ERR_JOIN_RESULT_JSON_MISSING_VALUE_FIELD.get( 867 controlObject.toSingleLineString(), JSON_FIELD_JOINED_ENTRIES)); 868 } 869 870 final List<JoinedEntry> joinedEntries = 871 new ArrayList<>(joinedEntryValues.size()); 872 for (final JSONValue joinedEntryValue : joinedEntryValues) 873 { 874 if (joinedEntryValue instanceof JSONObject) 875 { 876 joinedEntries.add(decodeEntryJSON(controlObject, 877 (JSONObject) joinedEntryValue)); 878 } 879 else 880 { 881 throw new LDAPException(ResultCode.DECODING_ERROR, 882 ERR_JOIN_RESULT_JSON_ENTRY_NOT_OBJECT.get( 883 controlObject.toSingleLineString(), 884 JSON_FIELD_JOINED_ENTRIES)); 885 } 886 } 887 888 889 if (strict) 890 { 891 final List<String> unrecognizedFields = 892 JSONControlDecodeHelper.getControlObjectUnexpectedFields( 893 valueObject, JSON_FIELD_RESULT_CODE, JSON_FIELD_MATCHED_DN, 894 JSON_FIELD_DIAGNOSTIC_MESSAGE, JSON_FIELD_REFERRAL_URLS, 895 JSON_FIELD_JOINED_ENTRIES); 896 if (! unrecognizedFields.isEmpty()) 897 { 898 throw new LDAPException(ResultCode.DECODING_ERROR, 899 ERR_JOIN_RESULT_JSON_UNRECOGNIZED_FIELD.get( 900 controlObject.toSingleLineString(), 901 unrecognizedFields.get(0))); 902 } 903 } 904 905 906 return new JoinResultControl(resultCode, diagnosticMessage, matchedDN, 907 referralURLs, joinedEntries); 908 } 909 910 911 912 /** 913 * Decodes the provided JSON object as a joined entry. 914 * 915 * @param controlObject The JSON object representing the entire control 916 * being decoded. It must not be {@code null}. 917 * @param entryObject The JSON object representing the entry to decode. 918 * It must not be {@code null}. 919 * 920 * @return The joined entry that was decoded. 921 * 922 * @throws LDAPException If the provided JSON object cannot be parsed as a 923 * valid joined entry. 924 */ 925 @NotNull() 926 private static JoinedEntry decodeEntryJSON( 927 @NotNull final JSONObject controlObject, 928 @NotNull final JSONObject entryObject) 929 throws LDAPException 930 { 931 String entryDN = null; 932 List<JoinedEntry> nestedResults = null; 933 final List<Attribute> attributes = 934 new ArrayList<>(entryObject.getFields().size()); 935 for (final Map.Entry<String,JSONValue> e : 936 entryObject.getFields().entrySet()) 937 { 938 final String fieldName = e.getKey(); 939 final JSONValue fieldValue = e.getValue(); 940 941 if (fieldName.equals(JSON_FIELD_ENTRY_DN)) 942 { 943 if (fieldValue instanceof JSONString) 944 { 945 entryDN = ((JSONString) fieldValue).stringValue(); 946 } 947 else 948 { 949 throw new LDAPException(ResultCode.DECODING_ERROR, 950 ERR_JOIN_RESULT_JSON_ENTRY_DN_NOT_STRING.get( 951 controlObject.toSingleLineString(), 952 JSON_FIELD_ENTRY_DN)); 953 } 954 } 955 else if (fieldName.equals(JSON_FIELD_NESTED_JOIN_RESULTS)) 956 { 957 if (fieldValue instanceof JSONArray) 958 { 959 final List<JSONValue> nestedEntryValues = 960 ((JSONArray) fieldValue).getValues(); 961 nestedResults = new ArrayList<>(nestedEntryValues.size()); 962 for (final JSONValue nestedEntryValue : nestedEntryValues) 963 { 964 if (nestedEntryValue instanceof JSONObject) 965 { 966 nestedResults.add(decodeEntryJSON(controlObject, 967 (JSONObject) nestedEntryValue)); 968 } 969 else 970 { 971 throw new LDAPException(ResultCode.DECODING_ERROR, 972 ERR_JON_RESULT_JSON_ENTRY_NESTED_ENTRY_NOT_OBJECT.get( 973 controlObject.toSingleLineString(), 974 JSON_FIELD_NESTED_JOIN_RESULTS)); 975 } 976 } 977 } 978 else 979 { 980 throw new LDAPException(ResultCode.DECODING_ERROR, 981 ERR_JOIN_RESULT_JSON_ENTRY_NESTED_ENTRIES_NOT_ARRAY.get( 982 controlObject.toSingleLineString(), 983 JSON_FIELD_NESTED_JOIN_RESULTS)); 984 } 985 } 986 else 987 { 988 if (fieldValue instanceof JSONArray) 989 { 990 final List<JSONValue> attrValueValues = 991 ((JSONArray) fieldValue).getValues(); 992 final List<String> attributeValues = 993 new ArrayList<>(attrValueValues.size()); 994 for (final JSONValue v : attrValueValues) 995 { 996 if (v instanceof JSONString) 997 { 998 attributeValues.add(((JSONString) v).stringValue()); 999 } 1000 else 1001 { 1002 throw new LDAPException(ResultCode.DECODING_ERROR, 1003 ERR_JOIN_RESULT_JSON_ENTRY_ATTR_VALUE_NOT_STRING.get( 1004 controlObject.toSingleLineString(), fieldName)); 1005 } 1006 } 1007 1008 attributes.add(new Attribute(fieldName, attributeValues)); 1009 } 1010 else 1011 { 1012 throw new LDAPException(ResultCode.DECODING_ERROR, 1013 ERR_JOIN_RESULT_JSON_ENTRY_ATTR_VALUES_NOT_ARRAY.get( 1014 controlObject.toSingleLineString(), fieldName)); 1015 } 1016 } 1017 } 1018 1019 1020 if (entryDN == null) 1021 { 1022 throw new LDAPException(ResultCode.DECODING_ERROR, 1023 ERR_JON_RESULT_JSON_ENTRY_MISSING_DN.get( 1024 controlObject.toSingleLineString(), JSON_FIELD_ENTRY_DN)); 1025 } 1026 1027 return new JoinedEntry(entryDN, attributes, nestedResults); 1028 } 1029 1030 1031 1032 /** 1033 * {@inheritDoc} 1034 */ 1035 @Override() 1036 public void toString(@NotNull final StringBuilder buffer) 1037 { 1038 buffer.append("JoinResultControl(resultCode='"); 1039 buffer.append(resultCode.getName()); 1040 buffer.append("', diagnosticMessage='"); 1041 1042 if (diagnosticMessage != null) 1043 { 1044 buffer.append(diagnosticMessage); 1045 } 1046 1047 buffer.append("', matchedDN='"); 1048 if (matchedDN != null) 1049 { 1050 buffer.append(matchedDN); 1051 } 1052 1053 buffer.append("', referralURLs={"); 1054 final Iterator<String> refIterator = referralURLs.iterator(); 1055 while (refIterator.hasNext()) 1056 { 1057 buffer.append(refIterator.next()); 1058 if (refIterator.hasNext()) 1059 { 1060 buffer.append(", "); 1061 } 1062 } 1063 1064 buffer.append("}, joinResults={"); 1065 final Iterator<JoinedEntry> entryIterator = joinResults.iterator(); 1066 while (entryIterator.hasNext()) 1067 { 1068 entryIterator.next().toString(buffer); 1069 if (entryIterator.hasNext()) 1070 { 1071 buffer.append(", "); 1072 } 1073 } 1074 1075 buffer.append("})"); 1076 } 1077}