001/* 002 * Copyright 2009-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2015-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk.unboundidds.controls; 022 023 024 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.Iterator; 028import java.util.List; 029 030import com.unboundid.asn1.ASN1Element; 031import com.unboundid.asn1.ASN1Enumerated; 032import com.unboundid.asn1.ASN1OctetString; 033import com.unboundid.asn1.ASN1Sequence; 034import com.unboundid.ldap.sdk.Control; 035import com.unboundid.ldap.sdk.DecodeableControl; 036import com.unboundid.ldap.sdk.LDAPException; 037import com.unboundid.ldap.sdk.ResultCode; 038import com.unboundid.ldap.sdk.SearchResultEntry; 039import com.unboundid.util.Debug; 040import com.unboundid.util.NotMutable; 041import com.unboundid.util.StaticUtils; 042import com.unboundid.util.ThreadSafety; 043import com.unboundid.util.ThreadSafetyLevel; 044import com.unboundid.util.Validator; 045 046import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 047 048 049 050/** 051 * This class provides an implementation of a control that may be included in a 052 * search result entry in response to a join request control to provide a set of 053 * entries related to the search result entry. See the class-level 054 * documentation for the {@link JoinRequestControl} class for additional 055 * information and an example demonstrating its use. 056 * <BR> 057 * <BLOCKQUOTE> 058 * <B>NOTE:</B> This class, and other classes within the 059 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 060 * supported for use against Ping Identity, UnboundID, and 061 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 062 * for proprietary functionality or for external specifications that are not 063 * considered stable or mature enough to be guaranteed to work in an 064 * interoperable way with other types of LDAP servers. 065 * </BLOCKQUOTE> 066 * <BR> 067 * The value of the join result control is encoded as follows: 068 * <PRE> 069 * JoinResult ::= SEQUENCE { 070 * COMPONENTS OF LDAPResult, 071 * entries [4] SEQUENCE OF JoinedEntry } 072 * </PRE> 073 */ 074@NotMutable() 075@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 076public final class JoinResultControl 077 extends Control 078 implements DecodeableControl 079{ 080 /** 081 * The OID (1.3.6.1.4.1.30221.2.5.9) for the join result control. 082 */ 083 public static final String JOIN_RESULT_OID = "1.3.6.1.4.1.30221.2.5.9"; 084 085 086 087 /** 088 * The BER type for the referral URLs element. 089 */ 090 private static final byte TYPE_REFERRAL_URLS = (byte) 0xA3; 091 092 093 094 /** 095 * The BER type for the join results element. 096 */ 097 private static final byte TYPE_JOIN_RESULTS = (byte) 0xA4; 098 099 100 101 /** 102 * The serial version UID for this serializable class. 103 */ 104 private static final long serialVersionUID = 681831114773253358L; 105 106 107 108 // The set of entries which have been joined with the associated search result 109 // entry. 110 private final List<JoinedEntry> joinResults; 111 112 // The set of referral URLs for this join result. 113 private final List<String> referralURLs; 114 115 // The result code for this join result. 116 private final ResultCode resultCode; 117 118 // The diagnostic message for this join result. 119 private final String diagnosticMessage; 120 121 // The matched DN for this join result. 122 private final String matchedDN; 123 124 125 126 /** 127 * Creates a new empty control instance that is intended to be used only for 128 * decoding controls via the {@code DecodeableControl} interface. 129 */ 130 JoinResultControl() 131 { 132 resultCode = null; 133 diagnosticMessage = null; 134 matchedDN = null; 135 referralURLs = null; 136 joinResults = null; 137 } 138 139 140 141 /** 142 * Creates a new join result control indicating a successful join. 143 * 144 * @param joinResults The set of entries that have been joined with the 145 * associated search result entry. It may be 146 * {@code null} or empty if no entries were joined with 147 * the search result entry. 148 */ 149 public JoinResultControl(final List<JoinedEntry> joinResults) 150 { 151 this(ResultCode.SUCCESS, null, null, null, joinResults); 152 } 153 154 155 156 /** 157 * Creates a new join result control with the provided information. 158 * 159 * @param resultCode The result code for the join processing. It 160 * must not be {@code null}. 161 * @param diagnosticMessage A message with additional information about the 162 * result of the join processing. It may be 163 * {@code null} if no message is needed. 164 * @param matchedDN The matched DN for the join processing. It may 165 * be {@code null} if no matched DN is needed. 166 * @param referralURLs The set of referral URLs for any referrals 167 * encountered while processing the join. It may 168 * be {@code null} or empty if no referral URLs 169 * are needed. 170 * @param joinResults The set of entries that have been joined with 171 * associated search result entry. It may be 172 * {@code null} or empty if no entries were joined 173 * with the search result entry. 174 */ 175 public JoinResultControl(final ResultCode resultCode, 176 final String diagnosticMessage, final String matchedDN, 177 final List<String> referralURLs, 178 final List<JoinedEntry> joinResults) 179 { 180 super(JOIN_RESULT_OID, false, 181 encodeValue(resultCode, diagnosticMessage, matchedDN, referralURLs, 182 joinResults)); 183 184 this.resultCode = resultCode; 185 this.diagnosticMessage = diagnosticMessage; 186 this.matchedDN = matchedDN; 187 188 if (referralURLs == null) 189 { 190 this.referralURLs = Collections.emptyList(); 191 } 192 else 193 { 194 this.referralURLs = Collections.unmodifiableList(referralURLs); 195 } 196 197 if (joinResults == null) 198 { 199 this.joinResults = Collections.emptyList(); 200 } 201 else 202 { 203 this.joinResults = Collections.unmodifiableList(joinResults); 204 } 205 } 206 207 208 209 /** 210 * Creates a new join result control with the provided information. 211 * 212 * @param oid The OID for the control. 213 * @param isCritical Indicates whether the control should be marked 214 * critical. 215 * @param value The encoded value for the control. This may be 216 * {@code null} if no value was provided. 217 * 218 * @throws LDAPException If the provided control cannot be decoded as an 219 * account usable response control. 220 */ 221 public JoinResultControl(final String oid, final boolean isCritical, 222 final ASN1OctetString value) 223 throws LDAPException 224 { 225 super(oid, isCritical, value); 226 227 if (value == null) 228 { 229 throw new LDAPException(ResultCode.DECODING_ERROR, 230 ERR_JOIN_RESULT_NO_VALUE.get()); 231 } 232 233 try 234 { 235 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 236 final ASN1Element[] elements = 237 ASN1Sequence.decodeAsSequence(valueElement).elements(); 238 239 resultCode = ResultCode.valueOf( 240 ASN1Enumerated.decodeAsEnumerated(elements[0]).intValue()); 241 242 final String matchedDNStr = 243 ASN1OctetString.decodeAsOctetString(elements[1]).stringValue(); 244 if (matchedDNStr.isEmpty()) 245 { 246 matchedDN = null; 247 } 248 else 249 { 250 matchedDN = matchedDNStr; 251 } 252 253 final String diagnosticMessageStr = 254 ASN1OctetString.decodeAsOctetString(elements[2]).stringValue(); 255 if (diagnosticMessageStr.isEmpty()) 256 { 257 diagnosticMessage = null; 258 } 259 else 260 { 261 diagnosticMessage = diagnosticMessageStr; 262 } 263 264 final ArrayList<String> refs = new ArrayList<>(5); 265 final ArrayList<JoinedEntry> entries = new ArrayList<>(20); 266 for (int i=3; i < elements.length; i++) 267 { 268 switch (elements[i].getType()) 269 { 270 case TYPE_REFERRAL_URLS: 271 final ASN1Element[] refElements = 272 ASN1Sequence.decodeAsSequence(elements[i]).elements(); 273 for (final ASN1Element e : refElements) 274 { 275 refs.add(ASN1OctetString.decodeAsOctetString(e).stringValue()); 276 } 277 break; 278 279 case TYPE_JOIN_RESULTS: 280 final ASN1Element[] entryElements = 281 ASN1Sequence.decodeAsSequence(elements[i]).elements(); 282 for (final ASN1Element e : entryElements) 283 { 284 entries.add(JoinedEntry.decode(e)); 285 } 286 break; 287 288 default: 289 throw new LDAPException(ResultCode.DECODING_ERROR, 290 ERR_JOIN_RESULT_INVALID_ELEMENT_TYPE.get( 291 StaticUtils.toHex(elements[i].getType()))); 292 } 293 } 294 295 referralURLs = Collections.unmodifiableList(refs); 296 joinResults = Collections.unmodifiableList(entries); 297 } 298 catch (final Exception e) 299 { 300 Debug.debugException(e); 301 302 throw new LDAPException(ResultCode.DECODING_ERROR, 303 ERR_JOIN_RESULT_CANNOT_DECODE.get( 304 StaticUtils.getExceptionMessage(e)), 305 e); 306 } 307 } 308 309 310 311 /** 312 * Encodes the provided information as appropriate for use as the value of 313 * this control. 314 * 315 * @param resultCode The result code for the join processing. It 316 * must not be {@code null}. 317 * @param diagnosticMessage A message with additional information about the 318 * result of the join processing. It may be 319 * {@code null} if no message is needed. 320 * @param matchedDN The matched DN for the join processing. It may 321 * be {@code null} if no matched DN is needed. 322 * @param referralURLs The set of referral URLs for any referrals 323 * encountered while processing the join. It may 324 * be {@code null} or empty if no referral URLs 325 * are needed. 326 * @param joinResults The set of entries that have been joined with 327 * associated search result entry. It may be 328 * {@code null} or empty if no entries were joined 329 * with the search result entry. 330 * 331 * @return An ASN.1 element containing an encoded representation of the 332 * value for this control. 333 */ 334 private static ASN1OctetString encodeValue(final ResultCode resultCode, 335 final String diagnosticMessage, final String matchedDN, 336 final List<String> referralURLs, 337 final List<JoinedEntry> joinResults) 338 { 339 Validator.ensureNotNull(resultCode); 340 341 final ArrayList<ASN1Element> elements = new ArrayList<>(5); 342 elements.add(new ASN1Enumerated(resultCode.intValue())); 343 344 if (matchedDN == null) 345 { 346 elements.add(new ASN1OctetString()); 347 } 348 else 349 { 350 elements.add(new ASN1OctetString(matchedDN)); 351 } 352 353 if (diagnosticMessage == null) 354 { 355 elements.add(new ASN1OctetString()); 356 } 357 else 358 { 359 elements.add(new ASN1OctetString(diagnosticMessage)); 360 } 361 362 if ((referralURLs != null) && (! referralURLs.isEmpty())) 363 { 364 final ArrayList<ASN1Element> refElements = 365 new ArrayList<>(referralURLs.size()); 366 for (final String s : referralURLs) 367 { 368 refElements.add(new ASN1OctetString(s)); 369 } 370 elements.add(new ASN1Sequence(TYPE_REFERRAL_URLS, refElements)); 371 } 372 373 if ((joinResults == null) || joinResults.isEmpty()) 374 { 375 elements.add(new ASN1Sequence(TYPE_JOIN_RESULTS)); 376 } 377 else 378 { 379 final ArrayList<ASN1Element> entryElements = 380 new ArrayList<>(joinResults.size()); 381 for (final JoinedEntry e : joinResults) 382 { 383 entryElements.add(e.encode()); 384 } 385 elements.add(new ASN1Sequence(TYPE_JOIN_RESULTS, entryElements)); 386 } 387 388 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 389 } 390 391 392 393 /** 394 * Retrieves the result code for this join result. 395 * 396 * @return The result code for this join result. 397 */ 398 public ResultCode getResultCode() 399 { 400 return resultCode; 401 } 402 403 404 405 /** 406 * Retrieves the diagnostic message for this join result. 407 * 408 * @return The diagnostic message for this join result, or {@code null} if 409 * there is no diagnostic message. 410 */ 411 public String getDiagnosticMessage() 412 { 413 return diagnosticMessage; 414 } 415 416 417 418 /** 419 * Retrieves the matched DN for this join result. 420 * 421 * @return The matched DN for this join result, or {@code null} if there is 422 * no matched DN. 423 */ 424 public String getMatchedDN() 425 { 426 return matchedDN; 427 } 428 429 430 431 /** 432 * Retrieves the set of referral URLs for this join result. 433 * 434 * @return The set of referral URLs for this join result, or an empty list 435 * if there are no referral URLs. 436 */ 437 public List<String> getReferralURLs() 438 { 439 return referralURLs; 440 } 441 442 443 444 /** 445 * Retrieves the set of entries that have been joined with the associated 446 * search result entry. 447 * 448 * @return The set of entries that have been joined with the associated 449 * search result entry. 450 */ 451 public List<JoinedEntry> getJoinResults() 452 { 453 return joinResults; 454 } 455 456 457 458 /** 459 * {@inheritDoc} 460 */ 461 @Override() 462 public JoinResultControl decodeControl(final String oid, 463 final boolean isCritical, 464 final ASN1OctetString value) 465 throws LDAPException 466 { 467 return new JoinResultControl(oid, isCritical, value); 468 } 469 470 471 472 /** 473 * Extracts a join result control from the provided search result entry. 474 * 475 * @param entry The search result entry from which to retrieve the join 476 * result control. 477 * 478 * @return The join result control contained in the provided search result 479 * entry, or {@code null} if the entry did not contain a join result 480 * control. 481 * 482 * @throws LDAPException If a problem is encountered while attempting to 483 * decode the join result control contained in the 484 * provided search result entry. 485 */ 486 public static JoinResultControl get(final SearchResultEntry entry) 487 throws LDAPException 488 { 489 final Control c = entry.getControl(JOIN_RESULT_OID); 490 if (c == null) 491 { 492 return null; 493 } 494 495 if (c instanceof JoinResultControl) 496 { 497 return (JoinResultControl) c; 498 } 499 else 500 { 501 return new JoinResultControl(c.getOID(), c.isCritical(), c.getValue()); 502 } 503 } 504 505 506 507 /** 508 * {@inheritDoc} 509 */ 510 @Override() 511 public String getControlName() 512 { 513 return INFO_CONTROL_NAME_JOIN_RESULT.get(); 514 } 515 516 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override() 522 public void toString(final StringBuilder buffer) 523 { 524 buffer.append("JoinResultControl(resultCode='"); 525 buffer.append(resultCode.getName()); 526 buffer.append("', diagnosticMessage='"); 527 528 if (diagnosticMessage != null) 529 { 530 buffer.append(diagnosticMessage); 531 } 532 533 buffer.append("', matchedDN='"); 534 if (matchedDN != null) 535 { 536 buffer.append(matchedDN); 537 } 538 539 buffer.append("', referralURLs={"); 540 final Iterator<String> refIterator = referralURLs.iterator(); 541 while (refIterator.hasNext()) 542 { 543 buffer.append(refIterator.next()); 544 if (refIterator.hasNext()) 545 { 546 buffer.append(", "); 547 } 548 } 549 550 buffer.append("}, joinResults={"); 551 final Iterator<JoinedEntry> entryIterator = joinResults.iterator(); 552 while (entryIterator.hasNext()) 553 { 554 entryIterator.next().toString(buffer); 555 if (entryIterator.hasNext()) 556 { 557 buffer.append(", "); 558 } 559 } 560 561 buffer.append("})"); 562 } 563}