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