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