001/*
002 * Copyright 2009-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-2019 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}