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.io.Serializable;
041import java.util.ArrayList;
042
043import com.unboundid.asn1.ASN1Boolean;
044import com.unboundid.asn1.ASN1Element;
045import com.unboundid.asn1.ASN1Enumerated;
046import com.unboundid.asn1.ASN1Integer;
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.asn1.ASN1Sequence;
049import com.unboundid.ldap.sdk.DereferencePolicy;
050import com.unboundid.ldap.sdk.Filter;
051import com.unboundid.ldap.sdk.LDAPException;
052import com.unboundid.ldap.sdk.ResultCode;
053import com.unboundid.ldap.sdk.SearchScope;
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 contains a data structure which provides information about the
069 * value of an LDAP join request control, which may or may not include a nested
070 * join.  See the class-level documentation for the {@link JoinRequestControl}
071 * class for additional information and an example demonstrating its use.
072 * <BR>
073 * <BLOCKQUOTE>
074 *   <B>NOTE:</B>  This class, and other classes within the
075 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
076 *   supported for use against Ping Identity, UnboundID, and
077 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
078 *   for proprietary functionality or for external specifications that are not
079 *   considered stable or mature enough to be guaranteed to work in an
080 *   interoperable way with other types of LDAP servers.
081 * </BLOCKQUOTE>
082 * <BR>
083 * The value of the join request control is encoded as follows:
084 * <PRE>
085 *   LDAPJoin ::= SEQUENCE {
086 *        joinRule         JoinRule,
087 *        baseObject       CHOICE {
088 *             useSearchBaseDN      [0] NULL,
089 *             useSourceEntryDN     [1] NULL,
090 *             useCustomBaseDN      [2] LDAPDN,
091 *             ... },
092 *        scope            [0] ENUMERATED {
093 *             baseObject             (0),
094 *             singleLevel            (1),
095 *             wholeSubtree           (2),
096 *             subordinateSubtree     (3),
097 *             ... } OPTIONAL,
098 *        derefAliases     [1] ENUMERATED {
099 *             neverDerefAliases       (0),
100 *             derefInSearching        (1),
101 *             derefFindingBaseObj     (2),
102 *             derefAlways             (3),
103 *             ... } OPTIONAL,
104 *        sizeLimit        [2] INTEGER (0 .. maxInt) OPTIONAL,
105 *        filter           [3] Filter OPTIONAL,
106 *        attributes       [4] AttributeSelection OPTIONAL,
107 *        requireMatch     [5] BOOLEAN DEFAULT FALSE,
108 *        nestedJoin       [6] LDAPJoin OPTIONAL,
109 *        ... }
110 * </PRE>
111 */
112@NotMutable()
113@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
114public final class JoinRequestValue
115       implements Serializable
116{
117  /**
118   * The set of attributes that will be used if all user attributes should be
119   * requested.
120   */
121  @NotNull private static final String[] NO_ATTRIBUTES = StaticUtils.NO_STRINGS;
122
123
124
125  /**
126   * The BER type to use for the scope element.
127   */
128  private static final byte TYPE_SCOPE = (byte) 0x80;
129
130
131
132  /**
133   * The BER type to use for the dereference policy element.
134   */
135  private static final byte TYPE_DEREF_POLICY = (byte) 0x81;
136
137
138
139  /**
140   * The BER type to use for the size limit element.
141   */
142  private static final byte TYPE_SIZE_LIMIT = (byte) 0x82;
143
144
145
146  /**
147   * The BER type to use for the filter element.
148   */
149  private static final byte TYPE_FILTER = (byte) 0xA3;
150
151
152
153  /**
154   * The BER type to use for the attributes element.
155   */
156  private static final byte TYPE_ATTRIBUTES = (byte) 0xA4;
157
158
159
160  /**
161   * The BER type to use for the require match element.
162   */
163  private static final byte TYPE_REQUIRE_MATCH = (byte) 0x85;
164
165
166
167  /**
168   * The BER type to use for the nested join element.
169   */
170  private static final byte TYPE_NESTED_JOIN = (byte) 0xA6;
171
172
173
174  /**
175   * The serial version UID for this serializable class.
176   */
177  private static final long serialVersionUID = 4675881185117657177L;
178
179
180
181  // Indicates whether to require at least one entry to match the join
182  // criteria for the entry to be returned.
183  private final boolean requireMatch;
184
185  // The dereference policy for this join request value.
186  @Nullable private final DereferencePolicy derefPolicy;
187
188  // The filter for this join request value.
189  @Nullable private final Filter filter;
190
191  // The client-requested size limit for this join request value.
192  @Nullable private final Integer sizeLimit;
193
194  // The base DN to use for this join request value.
195  @NotNull private final JoinBaseDN baseDN;
196
197  // The nested join criteria for this join request value.
198  @Nullable private final JoinRequestValue nestedJoin;
199
200  // The join rule for this join request value.
201  @NotNull private final JoinRule joinRule;
202
203  // The scope for this join request value.
204  @Nullable private final SearchScope scope;
205
206  // The set of attributes to include in entries matching the join criteria.
207  @NotNull  private final String[] attributes;
208
209
210
211  /**
212   * Creates a new join request value with the provided information.
213   *
214   * @param  joinRule      The join rule for this join request value.  It must
215   *                       not be {@code null}.
216   * @param  baseDN        The base DN for this join request value.  It must
217   *                       not be {@code null}.
218   * @param  scope         The scope for this join request value.  It may be
219   *                       {@code null} if the scope from the associated search
220   *                       request should be used.
221   * @param  derefPolicy   The alias dereferencing policy for this join request
222   *                       value.  It may be {@code null} if the dereference
223   *                       policy from the associated search request should be
224   *                       used.
225   * @param  sizeLimit     The client-requested maximum number of entries to
226   *                       allow when performing the join.  It may be
227   *                       {@code null} if the size limit from the associated
228   *                       search request should be used.  Note that the server
229   *                       will impose a maximum size limit of 1000 entries, so
230   *                       size limit values greater than 1000 will be limited
231   *                       to 1000.
232   * @param  filter        An additional filter which must match target entries
233   *                       for them to be included in the join.  This may be
234   *                       {@code null} if no additional filter is required and
235   *                       the join rule should be the only criteria used when
236   *                       performing the join.
237   * @param  attributes    The set of attributes that the client wishes to be
238   *                       included in joined entries.  It may be {@code null}
239   *                       or empty to indicate that all user attributes should
240   *                       be included.  It may also contain special values like
241   *                       "1.1" to indicate that no attributes should be
242   *                       included, "*" to indicate that all user attributes
243   *                       should be included, "+" to indicate that all
244   *                       operational attributes should be included, or
245   *                       "@ocname" to indicate that all required and optional
246   *                       attributes associated with the "ocname" object class
247   *                       should be included.
248   * @param  requireMatch  Indicates whether a search result entry is required
249   *                       to be joined with at least one entry for it to be
250   *                       returned to the client.
251   * @param  nestedJoin    A set of join criteria that should be applied to
252   *                       entries joined with this join request value.  It may
253   *                       be {@code null} if no nested join is needed.
254   */
255  public JoinRequestValue(@NotNull final JoinRule joinRule,
256                          @NotNull final JoinBaseDN baseDN,
257                          @Nullable final SearchScope scope,
258                          @Nullable final DereferencePolicy derefPolicy,
259                          @Nullable final Integer sizeLimit,
260                          @Nullable final Filter filter,
261                          @Nullable final String[] attributes,
262                          final boolean requireMatch,
263                          @Nullable final JoinRequestValue nestedJoin)
264  {
265    Validator.ensureNotNull(joinRule, baseDN);
266
267    this.joinRule     = joinRule;
268    this.baseDN       = baseDN;
269    this.scope        = scope;
270    this.derefPolicy  = derefPolicy;
271    this.sizeLimit    = sizeLimit;
272    this.filter       = filter;
273    this.requireMatch = requireMatch;
274    this.nestedJoin   = nestedJoin;
275
276    if (attributes == null)
277    {
278      this.attributes = NO_ATTRIBUTES;
279    }
280    else
281    {
282      this.attributes = attributes;
283    }
284  }
285
286
287
288  /**
289   * Retrieves the join rule for this join request value.
290   *
291   * @return  The join rule for this join request value.
292   */
293  @NotNull()
294  public JoinRule getJoinRule()
295  {
296    return joinRule;
297  }
298
299
300
301  /**
302   * Retrieves the join base DN for this join request value.
303   *
304   * @return  The join base DN for this join request value.
305   */
306  @NotNull()
307  public JoinBaseDN getBaseDN()
308  {
309    return baseDN;
310  }
311
312
313
314  /**
315   * Retrieves the scope for this join request value.
316   *
317   * @return  The scope for this join request value, or {@code null} if the
318   *          scope from the associated search request should be used.
319   */
320  @Nullable()
321  public SearchScope getScope()
322  {
323    return scope;
324  }
325
326
327
328  /**
329   * Retrieves the alias dereferencing policy for this join request value.
330   *
331   * @return  The alias dereferencing policy for this join request value, or
332   *          {@code null} if the policy from the associated search request
333   *          should be used.
334   */
335  @Nullable()
336  public DereferencePolicy getDerefPolicy()
337  {
338    return derefPolicy;
339  }
340
341
342
343  /**
344   * Retrieves the client-requested size limit for this join request value.
345   * Note that the server will impose a maximum size limit of 1000 entries, so
346   * if the client-requested size limit is greater than 1000, the server will
347   * limit it to 1000 entries.
348   *
349   * @return  The size limit for this join request value, or {@code null} if the
350   *          size limit from the associated search request should be used.
351   */
352  @Nullable()
353  public Integer getSizeLimit()
354  {
355    return sizeLimit;
356  }
357
358
359
360  /**
361   * Retrieves a filter with additional criteria that must match a target entry
362   * for it to be joined with a search result entry.
363   *
364   * @return  A filter with additional criteria that must match a target entry
365   *          for it to be joined with a search result entry, or {@code null} if
366   *          no additional filter is needed.
367   */
368  @Nullable()
369  public Filter getFilter()
370  {
371    return filter;
372  }
373
374
375
376  /**
377   * Retrieves the set of requested attributes that should be included in
378   * joined entries.
379   *
380   * @return  The set of requested attributes that should be included in joined
381   *          entries, or an empty array if all user attributes should be
382   *          requested.
383   */
384  @NotNull()
385  public String[] getAttributes()
386  {
387    return attributes;
388  }
389
390
391
392  /**
393   * Indicates whether a search result entry will be required to be joined with
394   * at least one entry for that entry to be returned to the client.
395   *
396   * @return  {@code true} if a search result entry must be joined with at least
397   *          one other entry for it to be returned to the client, or
398   *          {@code false} if a search result entry may be returned even if it
399   *          is not joined with any other entries.
400   */
401  public boolean requireMatch()
402  {
403    return requireMatch;
404  }
405
406
407
408  /**
409   * Retrieves the nested join for this join request value, if defined.
410   *
411   * @return  The nested join for this join request value, or {@code null} if
412   *          there is no nested join for this join request value.
413   */
414  @Nullable()
415  public JoinRequestValue getNestedJoin()
416  {
417    return nestedJoin;
418  }
419
420
421
422  /**
423   * Encodes this join request value as appropriate for inclusion in the join
424   * request control.
425   *
426   * @return  The ASN.1 element containing the encoded join request value.
427   */
428  @NotNull()
429  ASN1Element encode()
430  {
431    final ArrayList<ASN1Element> elements = new ArrayList<>(9);
432
433    elements.add(joinRule.encode());
434    elements.add(baseDN.encode());
435
436    if (scope != null)
437    {
438      elements.add(new ASN1Enumerated(TYPE_SCOPE, scope.intValue()));
439    }
440
441    if (derefPolicy != null)
442    {
443      elements.add(new ASN1Enumerated(TYPE_DEREF_POLICY,
444           derefPolicy.intValue()));
445    }
446
447    if (sizeLimit != null)
448    {
449      elements.add(new ASN1Integer(TYPE_SIZE_LIMIT, sizeLimit));
450    }
451
452    if (filter != null)
453    {
454      elements.add(new ASN1OctetString(TYPE_FILTER, filter.encode().encode()));
455    }
456
457    if ((attributes != null) && (attributes.length > 0))
458    {
459      final ASN1Element[] attrElements = new ASN1Element[attributes.length];
460      for (int i=0; i < attributes.length; i++)
461      {
462        attrElements[i] = new ASN1OctetString(attributes[i]);
463      }
464      elements.add(new ASN1Sequence(TYPE_ATTRIBUTES, attrElements));
465    }
466
467    if (requireMatch)
468    {
469      elements.add(new ASN1Boolean(TYPE_REQUIRE_MATCH, requireMatch));
470    }
471
472    if (nestedJoin != null)
473    {
474      elements.add(new ASN1OctetString(TYPE_NESTED_JOIN,
475           nestedJoin.encode().getValue()));
476    }
477
478    return new ASN1Sequence(elements);
479  }
480
481
482
483  /**
484   * Decodes the provided ASN.1 element as a join request value.
485   *
486   * @param  element  The element to be decoded.
487   *
488   * @return  The decoded join request value.
489   *
490   * @throws  LDAPException  If the provided ASN.1 element cannot be decoded as
491   *                         a join request value.
492   */
493  @NotNull()
494  static JoinRequestValue decode(@NotNull final ASN1Element element)
495         throws LDAPException
496  {
497    try
498    {
499      final ASN1Element[] elements =
500           ASN1Sequence.decodeAsSequence(element).elements();
501      final JoinRule   joinRule = JoinRule.decode(elements[0]);
502      final JoinBaseDN baseDN   = JoinBaseDN.decode(elements[1]);
503
504      SearchScope       scope        = null;
505      DereferencePolicy derefPolicy  = null;
506      Integer           sizeLimit    = null;
507      Filter            filter       = null;
508      String[]          attributes   = NO_ATTRIBUTES;
509      boolean           requireMatch = false;
510      JoinRequestValue  nestedJoin   = null;
511
512      for (int i=2; i < elements.length; i++)
513      {
514        switch (elements[i].getType())
515        {
516          case TYPE_SCOPE:
517            scope = SearchScope.valueOf(
518                 ASN1Enumerated.decodeAsEnumerated(elements[i]).intValue());
519            break;
520
521          case TYPE_DEREF_POLICY:
522            derefPolicy = DereferencePolicy.valueOf(
523                 ASN1Enumerated.decodeAsEnumerated(elements[i]).intValue());
524            break;
525
526          case TYPE_SIZE_LIMIT:
527            sizeLimit = ASN1Integer.decodeAsInteger(elements[i]).intValue();
528            break;
529
530          case TYPE_FILTER:
531            filter = Filter.decode(ASN1Element.decode(elements[i].getValue()));
532            break;
533
534          case TYPE_ATTRIBUTES:
535            final ASN1Element[] attrElements =
536                 ASN1Sequence.decodeAsSequence(elements[i]).elements();
537            final ArrayList<String> attrList =
538                 new ArrayList<>(attrElements.length);
539            for (final ASN1Element e : attrElements)
540            {
541              attrList.add(
542                   ASN1OctetString.decodeAsOctetString(e).stringValue());
543            }
544
545            attributes = new String[attrList.size()];
546            attrList.toArray(attributes);
547            break;
548
549          case TYPE_REQUIRE_MATCH:
550            requireMatch =
551                 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue();
552            break;
553
554          case TYPE_NESTED_JOIN:
555            nestedJoin = decode(elements[i]);
556            break;
557
558          default:
559            throw new LDAPException(ResultCode.DECODING_ERROR,
560                 ERR_JOIN_REQUEST_VALUE_INVALID_ELEMENT_TYPE.get(
561                      elements[i].getType()));
562        }
563      }
564
565      return new JoinRequestValue(joinRule, baseDN, scope, derefPolicy,
566           sizeLimit, filter, attributes, requireMatch, nestedJoin);
567    }
568    catch (final Exception e)
569    {
570      Debug.debugException(e);
571
572      throw new LDAPException(ResultCode.DECODING_ERROR,
573           ERR_JOIN_REQUEST_VALUE_CANNOT_DECODE.get(
574                StaticUtils.getExceptionMessage(e)),
575           e);
576    }
577  }
578
579
580
581  /**
582   * Retrieves a string representation of this join request value.
583   *
584   * @return  A string representation of this join request value.
585   */
586  @Override()
587  @NotNull()
588  public String toString()
589  {
590    final StringBuilder buffer = new StringBuilder();
591    toString(buffer);
592    return buffer.toString();
593  }
594
595
596
597  /**
598   * Appends a string representation of this join request value to the provided
599   * buffer.
600   *
601   * @param  buffer  The buffer to which the information should be appended.
602   */
603  public void toString(@NotNull final StringBuilder buffer)
604  {
605    buffer.append("JoinRequestValue(joinRule=");
606    joinRule.toString(buffer);
607    buffer.append(", baseDN=");
608    baseDN.toString(buffer);
609    buffer.append(", scope=");
610    buffer.append(String.valueOf(scope));
611    buffer.append(", derefPolicy=");
612    buffer.append(String.valueOf(derefPolicy));
613    buffer.append(", sizeLimit=");
614    buffer.append(sizeLimit);
615    buffer.append(", filter=");
616
617    if (filter == null)
618    {
619      buffer.append("null");
620    }
621    else
622    {
623      buffer.append('\'');
624      filter.toString(buffer);
625      buffer.append('\'');
626    }
627
628    buffer.append(", attributes={");
629
630    for (int i=0; i < attributes.length; i++)
631    {
632      if (i > 0)
633      {
634        buffer.append(", ");
635      }
636      buffer.append(attributes[i]);
637    }
638
639    buffer.append("}, requireMatch=");
640    buffer.append(requireMatch);
641    buffer.append(", nestedJoin=");
642
643    if (nestedJoin == null)
644    {
645      buffer.append("null");
646    }
647    else
648    {
649      nestedJoin.toString(buffer);
650    }
651
652    buffer.append(')');
653  }
654}