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;
042import java.util.List;
043
044import com.unboundid.asn1.ASN1Boolean;
045import com.unboundid.asn1.ASN1Element;
046import com.unboundid.asn1.ASN1OctetString;
047import com.unboundid.asn1.ASN1Sequence;
048import com.unboundid.asn1.ASN1Set;
049import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.util.Debug;
053import com.unboundid.util.NotMutable;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.StaticUtils;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059import com.unboundid.util.Validator;
060import com.unboundid.util.json.JSONArray;
061import com.unboundid.util.json.JSONField;
062import com.unboundid.util.json.JSONObject;
063import com.unboundid.util.json.JSONValue;
064
065import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
066
067
068
069/**
070 * This class provides an implementation of a join rule as used by the LDAP join
071 * request control.  See the class-level documentation for the
072 * {@link JoinRequestControl} class for additional information and an example
073 * demonstrating its use.
074 * <BR>
075 * <BLOCKQUOTE>
076 *   <B>NOTE:</B>  This class, and other classes within the
077 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
078 *   supported for use against Ping Identity, UnboundID, and
079 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
080 *   for proprietary functionality or for external specifications that are not
081 *   considered stable or mature enough to be guaranteed to work in an
082 *   interoperable way with other types of LDAP servers.
083 * </BLOCKQUOTE>
084 * <BR>
085 * Join rules are encoded as follows:
086 * <PRE>
087 *   JoinRule ::= CHOICE {
088 *        and               [0] SET (1 .. MAX) of JoinRule,
089 *        or                [1] SET (1 .. MAX) of JoinRule,
090 *        dnJoin            [2] AttributeDescription,
091 *        equalityJoin      [3] JoinRuleAssertion,
092 *        containsJoin      [4] JoinRuleAssertion,
093 *        reverseDNJoin     [5] AttributeDescription,
094 *        ... }
095 *
096 *   JoinRuleAssertion ::= SEQUENCE {
097 *        sourceAttribute     AttributeDescription,
098 *        targetAttribute     AttributeDescription,
099 *        matchAll            BOOLEAN DEFAULT FALSE }
100 * </PRE>
101 */
102@NotMutable()
103@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
104public final class JoinRule
105       implements Serializable
106{
107  /**
108   * The join rule type that will be used for AND join rules.
109   */
110  public static final byte JOIN_TYPE_AND = (byte) 0xA0;
111
112
113
114  /**
115   * The join rule type that will be used for OR join rules.
116   */
117  public static final byte JOIN_TYPE_OR = (byte) 0xA1;
118
119
120
121  /**
122   * The join rule type that will be used for DN join rules.
123   */
124  public static final byte JOIN_TYPE_DN = (byte) 0x82;
125
126
127
128  /**
129   * The join rule type that will be used for equality join rules.
130   */
131  public static final byte JOIN_TYPE_EQUALITY = (byte) 0xA3;
132
133
134
135  /**
136   * The join rule type that will be used for contains join rules.
137   */
138  public static final byte JOIN_TYPE_CONTAINS = (byte) 0xA4;
139
140
141
142  /**
143   * The join rule type that will be used for reverse DN join rules.
144   */
145  public static final byte JOIN_TYPE_REVERSE_DN = (byte) 0x85;
146
147
148
149  /**
150   * The name of the field used to indicate whether to match all source
151   * attribute values in the JSON representation of this join rule.
152   */
153  @NotNull private static final String JSON_FIELD_MATCH_ALL = "match-all";
154
155
156
157  /**
158   * The name of the field used to hold the nested join rules in the JSON
159   * representation of this join rule.
160   */
161  @NotNull private static final String JSON_FIELD_RULES = "rules";
162
163
164
165  /**
166   * The name of the field used to hold the name of a source entry attribute in
167   * the JSON representation of this join rule.
168   */
169  @NotNull private static final String JSON_FIELD_SOURCE_ATTRIBUTE =
170       "source-attribute";
171
172
173
174  /**
175   * The name of the field used to hold the name of a target entry attribute in
176   * the JSON representation of this join rule.
177   */
178  @NotNull private static final String JSON_FIELD_TARGET_ATTRIBUTE =
179       "target-attribute";
180
181
182
183  /**
184   * The name of the field used to hold the join rule type in the JSON
185   * representation of this join rule.
186   */
187  @NotNull private static final String JSON_FIELD_TYPE = "type";
188
189
190
191  /**
192   * The string that should be used to represent the AND join rule type in JSON
193   * object representations.
194   */
195  @NotNull private static final String JSON_TYPE_AND = "and";
196
197
198
199  /**
200   * The string that should be used to represent the contains join rule type in
201   * JSON object representations.
202   */
203  @NotNull private static final String JSON_TYPE_CONTAINS = "contains";
204
205
206
207  /**
208   * The string that should be used to represent the DN join rule type in JSON
209   * object representations.
210   */
211  @NotNull private static final String JSON_TYPE_DN = "dn";
212
213
214
215  /**
216   * The string that should be used to represent the equality join rule type in
217   * JSON object representations.
218   */
219  @NotNull private static final String JSON_TYPE_EQUALITY = "equality";
220
221
222
223  /**
224   * The string that should be used to represent the OR join rule type in JSON
225   * object representations.
226   */
227  @NotNull private static final String JSON_TYPE_OR = "or";
228
229
230
231  /**
232   * The string that should be used to represent the reverse DN join rule type
233   * in JSON object representations.
234   */
235  @NotNull private static final String JSON_TYPE_REVERSE_DN = "reverse-dn";
236
237
238
239  /**
240   * An empty array of join rules that will be used as the set of components
241   * for DN and equality join rules.
242   */
243  @NotNull private static final JoinRule[] NO_RULES = new JoinRule[0];
244
245
246
247  /**
248   * The serial version UID for this serializable class.
249   */
250  private static final long serialVersionUID = 9041070342511946580L;
251
252
253
254  // Indicates whether all values of a multivalued source attribute must be
255  // present in the target entry for it to be considered a match.
256  private final boolean matchAll;
257
258  // The BER type for this join rule.
259  private final byte type;
260
261  // The set of subordinate components for this join rule.
262  @NotNull private final JoinRule[] components;
263
264  // The name of the source attribute for this join rule.
265  @Nullable private final String sourceAttribute;
266
267  // The name of the target attribute for this join rule.
268  @Nullable private final String targetAttribute;
269
270
271
272  /**
273   * Creates a new join rule with the provided information.
274   *
275   * @param  type             The BER type for this join rule.
276   * @param  components       The set of subordinate components for this join
277   *                          rule.
278   * @param  sourceAttribute  The name of the source attribute for this join
279   *                          rule.
280   * @param  targetAttribute  The name of the target attribute for this join
281   *                          rule.
282   * @param  matchAll         Indicates whether all values of a multivalued
283   *                          source attribute must be present in the target
284   *                          entry for it to be considered a match.
285   */
286  private JoinRule(final byte type, @NotNull final JoinRule[] components,
287                   @Nullable final String sourceAttribute,
288                   @Nullable final String targetAttribute,
289                   final boolean matchAll)
290  {
291    this.type            = type;
292    this.components      = components;
293    this.sourceAttribute = sourceAttribute;
294    this.targetAttribute = targetAttribute;
295    this.matchAll        = matchAll;
296  }
297
298
299
300  /**
301   * Creates an AND join rule in which all of the contained join rules must
302   * match an entry for it to be included in the join.
303   *
304   * @param  components  The set of components to include in this join.  It must
305   *                     not be {@code null} or empty.
306   *
307   * @return  The created AND join rule.
308   */
309  @NotNull()
310  public static JoinRule createANDRule(@NotNull final JoinRule... components)
311  {
312    Validator.ensureNotNull(components);
313    Validator.ensureFalse(components.length == 0);
314
315    return new JoinRule(JOIN_TYPE_AND, components, null, null, false);
316  }
317
318
319
320  /**
321   * Creates an AND join rule in which all of the contained join rules must
322   * match an entry for it to be included in the join.
323   *
324   * @param  components  The set of components to include in this join.  It must
325   *                     not be {@code null} or empty.
326   *
327   * @return  The created AND join rule.
328   */
329  @NotNull()
330  public static JoinRule createANDRule(@NotNull final List<JoinRule> components)
331  {
332    Validator.ensureNotNull(components);
333    Validator.ensureFalse(components.isEmpty());
334
335    final JoinRule[] compArray = new JoinRule[components.size()];
336    return new JoinRule(JOIN_TYPE_AND, components.toArray(compArray), null,
337                        null, false);
338  }
339
340
341
342  /**
343   * Creates an OR join rule in which at least one of the contained join rules
344   * must match an entry for it to be included in the join.
345   *
346   * @param  components  The set of components to include in this join.  It must
347   *                     not be {@code null} or empty.
348   *
349   * @return  The created OR join rule.
350   */
351  @NotNull()
352  public static JoinRule createORRule(@NotNull final JoinRule... components)
353  {
354    Validator.ensureNotNull(components);
355    Validator.ensureFalse(components.length == 0);
356
357    return new JoinRule(JOIN_TYPE_OR, components, null, null, false);
358  }
359
360
361
362  /**
363   * Creates an OR join rule in which at least one of the contained join rules
364   * must match an entry for it to be included in the join.
365   *
366   * @param  components  The set of components to include in this join.  It must
367   *                     not be {@code null} or empty.
368   *
369   * @return  The created OR join rule.
370   */
371  @NotNull()
372  public static JoinRule createORRule(@NotNull final List<JoinRule> components)
373  {
374    Validator.ensureNotNull(components);
375    Validator.ensureFalse(components.isEmpty());
376
377    final JoinRule[] compArray = new JoinRule[components.size()];
378    return new JoinRule(JOIN_TYPE_OR, components.toArray(compArray), null,
379                        null, false);
380  }
381
382
383
384  /**
385   * Creates a DN join rule in which the value(s) of the source attribute must
386   * specify the DN(s) of the target entries to include in the join.
387   *
388   * @param  sourceAttribute  The name or OID of the attribute in the source
389   *                          entry whose values contain the DNs of the entries
390   *                          to be included in the join.  It must not be
391   *                          {@code null}, and it must be associated with a
392   *                          distinguished name or name and optional UID
393   *                          syntax.
394   *
395   * @return  The created DN join rule.
396   */
397  @NotNull()
398  public static JoinRule createDNJoin(@NotNull final String sourceAttribute)
399  {
400    Validator.ensureNotNull(sourceAttribute);
401
402    return new JoinRule(JOIN_TYPE_DN, NO_RULES, sourceAttribute, null, false);
403  }
404
405
406
407  /**
408   * Creates an equality join rule in which the value(s) of the source attribute
409   * in the source entry must be equal to the value(s) of the target attribute
410   * of a target entry for it to be included in the join.
411   *
412   * @param  sourceAttribute  The name or OID of the attribute in the source
413   *                          entry whose value(s) should be matched in target
414   *                          entries to be included in the join.  It must not
415   *                          be {@code null}.
416   * @param  targetAttribute  The name or OID of the attribute whose value(s)
417   *                          must match the source value(s) in entries included
418   *                          in the join.  It must not be {@code null}.
419   * @param  matchAll         Indicates whether all values of a multivalued
420   *                          source attribute must be present in the target
421   *                          entry for it to be considered a match.
422   *
423   * @return  The created equality join rule.
424   */
425  @NotNull()
426  public static JoinRule createEqualityJoin(
427              @NotNull final String sourceAttribute,
428              @NotNull final String targetAttribute,
429              final boolean matchAll)
430  {
431    Validator.ensureNotNull(sourceAttribute, targetAttribute);
432
433    return new JoinRule(JOIN_TYPE_EQUALITY, NO_RULES, sourceAttribute,
434                        targetAttribute, matchAll);
435  }
436
437
438
439  /**
440   * Creates an equality join rule in which the value(s) of the source attribute
441   * in the source entry must be equal to or a substring of the value(s) of the
442   * target attribute of a target entry for it to be included in the join.
443   *
444   * @param  sourceAttribute  The name or OID of the attribute in the source
445   *                          entry whose value(s) should be matched in target
446   *                          entries to be included in the join.  It must not
447   *                          be {@code null}.
448   * @param  targetAttribute  The name or OID of the attribute whose value(s)
449   *                          must equal or contain the source value(s) in
450   *                          entries included in the join.  It must not be
451   *                          {@code null}.
452   * @param  matchAll         Indicates whether all values of a multivalued
453   *                          source attribute must be present in the target
454   *                          entry for it to be considered a match.
455   *
456   * @return  The created equality join rule.
457   */
458  @NotNull()
459  public static JoinRule createContainsJoin(
460              @NotNull final String sourceAttribute,
461              @NotNull final String targetAttribute,
462              final boolean matchAll)
463  {
464    Validator.ensureNotNull(sourceAttribute, targetAttribute);
465
466    return new JoinRule(JOIN_TYPE_CONTAINS, NO_RULES, sourceAttribute,
467                        targetAttribute, matchAll);
468  }
469
470
471
472  /**
473   * Creates a reverse DN join rule in which the target entries to include in
474   * the join must include a specified attribute that contains the DN of the
475   * source entry.
476   *
477   * @param  targetAttribute  The name or OID of the attribute in the target
478   *                          entries which must contain the DN of the source
479   *                          entry.  It must not be {@code null}, and it must
480   *                          be associated with a distinguished nme or name and
481   *                          optional UID syntax.
482   *
483   * @return  The created reverse DN join rule.
484   */
485  @NotNull()
486  public static JoinRule createReverseDNJoin(
487              @NotNull final String targetAttribute)
488  {
489    Validator.ensureNotNull(targetAttribute);
490
491    return new JoinRule(JOIN_TYPE_REVERSE_DN, NO_RULES, null, targetAttribute,
492         false);
493  }
494
495
496
497  /**
498   * Retrieves the join rule type for this join rule.
499   *
500   * @return  The join rule type for this join rule.
501   */
502  public byte getType()
503  {
504    return type;
505  }
506
507
508
509  /**
510   * Retrieves the set of subordinate components for this AND or OR join rule.
511   *
512   * @return  The set of subordinate components for this AND or OR join rule, or
513   *          an empty list if this is not an AND or OR join rule.
514   */
515  @NotNull()
516  public JoinRule[] getComponents()
517  {
518    return components;
519  }
520
521
522
523  /**
524   * Retrieves the name of the source attribute for this DN, equality, or
525   * contains join rule.
526   *
527   * @return  The name of the source attribute for this DN, equality, or
528   *          contains join rule, or {@code null} if this is some other type of
529   *          join rule.
530   */
531  @Nullable()
532  public String getSourceAttribute()
533  {
534    return sourceAttribute;
535  }
536
537
538
539  /**
540   * Retrieves the name of the target attribute for this reverse DN, equality,
541   * or contains join rule.
542   *
543   * @return  The name of the target attribute for this reverse DN, equality, or
544   *          contains join rule, or {@code null} if this is some other type of
545   *          join rule.
546   */
547  @Nullable()
548  public String getTargetAttribute()
549  {
550    return targetAttribute;
551  }
552
553
554
555  /**
556   * Indicates whether all values of a multivalued source attribute must be
557   * present in a target entry for it to be considered a match.  The return
558   * value will only be meaningful for equality join rules.
559   *
560   * @return  {@code true} if all values of the source attribute must be
561   *          included in the target attribute of an entry for it to be
562   *          considered for inclusion in the join, or {@code false} if it is
563   *          only necessary for at least one of the values to be included in a
564   *          target entry for it to be considered for inclusion in the join.
565   */
566  public boolean matchAll()
567  {
568    return matchAll;
569  }
570
571
572
573  /**
574   * Encodes this join rule as appropriate for inclusion in an LDAP join
575   * request control.
576   *
577   * @return  The encoded representation of this join rule.
578   */
579  @NotNull()
580  ASN1Element encode()
581  {
582    switch (type)
583    {
584      case JOIN_TYPE_AND:
585      case JOIN_TYPE_OR:
586        final ASN1Element[] compElements = new ASN1Element[components.length];
587        for (int i=0; i < components.length; i++)
588        {
589          compElements[i] = components[i].encode();
590        }
591        return new ASN1Set(type, compElements);
592
593      case JOIN_TYPE_DN:
594        return new ASN1OctetString(type, sourceAttribute);
595
596      case JOIN_TYPE_EQUALITY:
597      case JOIN_TYPE_CONTAINS:
598        if (matchAll)
599        {
600          return new ASN1Sequence(type,
601               new ASN1OctetString(sourceAttribute),
602               new ASN1OctetString(targetAttribute),
603               new ASN1Boolean(matchAll));
604        }
605        else
606        {
607          return new ASN1Sequence(type,
608               new ASN1OctetString(sourceAttribute),
609               new ASN1OctetString(targetAttribute));
610        }
611    case JOIN_TYPE_REVERSE_DN:
612      return new ASN1OctetString(type, targetAttribute);
613
614      default:
615        // This should never happen.
616        return null;
617    }
618  }
619
620
621
622  /**
623   * Decodes the provided ASN.1 element as a join rule.
624   *
625   * @param  element  The element to be decoded.
626   *
627   * @return  The decoded join rule.
628   *
629   * @throws  LDAPException  If a problem occurs while attempting to decode the
630   *                         provided element as a join rule.
631   */
632  @NotNull()
633  static JoinRule decode(@NotNull final ASN1Element element)
634         throws LDAPException
635  {
636    final byte elementType = element.getType();
637    switch (elementType)
638    {
639      case JOIN_TYPE_AND:
640      case JOIN_TYPE_OR:
641        try
642        {
643          final ASN1Element[] elements =
644               ASN1Set.decodeAsSet(element).elements();
645          final JoinRule[] rules = new JoinRule[elements.length];
646          for (int i=0; i < rules.length; i++)
647          {
648            rules[i] = decode(elements[i]);
649          }
650
651          return new JoinRule(elementType, rules, null, null, false);
652        }
653        catch (final Exception e)
654        {
655          Debug.debugException(e);
656
657          throw new LDAPException(ResultCode.DECODING_ERROR,
658               ERR_JOIN_RULE_CANNOT_DECODE.get(
659                    StaticUtils.getExceptionMessage(e)),
660               e);
661        }
662
663
664      case JOIN_TYPE_DN:
665        return new JoinRule(elementType, NO_RULES,
666             ASN1OctetString.decodeAsOctetString(element).stringValue(), null,
667             false);
668
669
670      case JOIN_TYPE_EQUALITY:
671      case JOIN_TYPE_CONTAINS:
672        try
673        {
674          final ASN1Element[] elements =
675               ASN1Sequence.decodeAsSequence(element).elements();
676
677          final String sourceAttribute =
678               elements[0].decodeAsOctetString().stringValue();
679          final String targetAttribute =
680               elements[1].decodeAsOctetString().stringValue();
681
682          boolean matchAll = false;
683          if (elements.length == 3)
684          {
685            matchAll = elements[2].decodeAsBoolean().booleanValue();
686          }
687
688          return new JoinRule(elementType, NO_RULES, sourceAttribute,
689               targetAttribute, matchAll);
690        }
691        catch (final Exception e)
692        {
693          Debug.debugException(e);
694
695          throw new LDAPException(ResultCode.DECODING_ERROR,
696               ERR_JOIN_RULE_CANNOT_DECODE.get(
697                    StaticUtils.getExceptionMessage(e)),
698               e);
699        }
700
701
702    case JOIN_TYPE_REVERSE_DN:
703      return new JoinRule(elementType, NO_RULES, null,
704           ASN1OctetString.decodeAsOctetString(element).stringValue(), false);
705
706
707      default:
708        throw new LDAPException(ResultCode.DECODING_ERROR,
709             ERR_JOIN_RULE_DECODE_INVALID_TYPE.get(
710                  StaticUtils.toHex(elementType)));
711    }
712  }
713
714
715
716  /**
717   * Retrieve a JSON object representation of this join rule.
718   *
719   * @return  A JSON object representation of this join rule.
720   */
721  @NotNull()
722  public JSONObject toJSON()
723  {
724    switch (type)
725    {
726      case JOIN_TYPE_DN:
727        return new JSONObject(
728             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_DN),
729             new JSONField(JSON_FIELD_SOURCE_ATTRIBUTE, sourceAttribute));
730
731      case JOIN_TYPE_REVERSE_DN:
732        return new JSONObject(
733             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_REVERSE_DN),
734             new JSONField(JSON_FIELD_TARGET_ATTRIBUTE, targetAttribute));
735
736      case JOIN_TYPE_EQUALITY:
737        return new JSONObject(
738             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_EQUALITY),
739             new JSONField(JSON_FIELD_SOURCE_ATTRIBUTE, sourceAttribute),
740             new JSONField(JSON_FIELD_TARGET_ATTRIBUTE, targetAttribute),
741             new JSONField(JSON_FIELD_MATCH_ALL, matchAll));
742
743      case JOIN_TYPE_CONTAINS:
744        return new JSONObject(
745             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_CONTAINS),
746             new JSONField(JSON_FIELD_SOURCE_ATTRIBUTE, sourceAttribute),
747             new JSONField(JSON_FIELD_TARGET_ATTRIBUTE, targetAttribute),
748             new JSONField(JSON_FIELD_MATCH_ALL, matchAll));
749
750      case JOIN_TYPE_AND:
751        final List<JSONValue> andRuleValues =
752             new ArrayList<>(components.length);
753        for (final JoinRule rule : components)
754        {
755          andRuleValues.add(rule.toJSON());
756        }
757
758        return new JSONObject(
759             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_AND),
760             new JSONField(JSON_FIELD_RULES, new JSONArray(andRuleValues)));
761
762      case JOIN_TYPE_OR:
763        final List<JSONValue> orRuleValues =
764             new ArrayList<>(components.length);
765        for (final JoinRule rule : components)
766        {
767          orRuleValues.add(rule.toJSON());
768        }
769
770        return new JSONObject(
771             new JSONField(JSON_FIELD_TYPE, JSON_TYPE_OR),
772             new JSONField(JSON_FIELD_RULES, new JSONArray(orRuleValues)));
773
774      default:
775        // This should never happen.
776        return null;
777    }
778  }
779
780
781
782  /**
783   * Decodes the provided JSON object as a join rule.
784   *
785   * @param  o       The JSON object that represents the join rule to decode.
786   *                 It must not be {@code null}.
787   * @param  strict  Indicates whether to use strict mode when decoding the
788   *                 provided JSON object.  If this is {@code true}, then this
789   *                 method will throw an exception if the provided JSON object
790   *                 contains any unrecognized fields.  If this is
791   *                 {@code false}, then unrecognized fields will be ignored.
792   *
793   * @return  The join rule decoded from the provided JSON object.
794   *
795   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
796   *                         valid join rule.
797   */
798  @NotNull()
799  public static JoinRule decodeJSONJoinRule(@NotNull final JSONObject o,
800                                            final boolean strict)
801         throws LDAPException
802  {
803    final String type = o.getFieldAsString(JSON_FIELD_TYPE);
804    if (type == null)
805    {
806      throw new LDAPException(ResultCode.DECODING_ERROR,
807           ERR_JOIN_RULE_JSON_MISSING_TYPE.get(JSON_FIELD_TYPE));
808    }
809
810    switch (type)
811    {
812      case JSON_TYPE_DN:
813        return decodeJSONDNJoinRule(o, strict);
814
815      case JSON_TYPE_REVERSE_DN:
816        return decodeJSONReverseDNJoinRule(o, strict);
817
818      case JSON_TYPE_EQUALITY:
819        return decodeJSONEqualityJoinRule(o, strict);
820
821      case JSON_TYPE_CONTAINS:
822        return decodeJSONContainsJoinRule(o, strict);
823
824      case JSON_TYPE_AND:
825        return decodeJSONANDJoinRule(o, strict);
826
827      case JSON_TYPE_OR:
828        return decodeJSONORJoinRule(o, strict);
829
830      default:
831        throw new LDAPException(ResultCode.DECODING_ERROR,
832             ERR_JOIN_RULE_JSON_UNRECOGNIZED_TYPE.get(type));
833    }
834  }
835
836
837
838  /**
839   * Decodes the provided JSON object as a DN join rule.
840   *
841   * @param  o       The JSON object that represents the join rule to decode.
842   *                 It must not be {@code null}.
843   * @param  strict  Indicates whether to use strict mode when decoding the
844   *                 provided JSON object.  If this is {@code true}, then this
845   *                 method will throw an exception if the provided JSON object
846   *                 contains any unrecognized fields.  If this is
847   *                 {@code false}, then unrecognized fields will be ignored.
848   *
849   * @return  The DN join rule decoded from the provided JSON object.
850   *
851   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
852   *                         valid DN join rule.
853   */
854  @NotNull()
855  private static JoinRule decodeJSONDNJoinRule(@NotNull final JSONObject o,
856                                               final boolean strict)
857          throws LDAPException
858  {
859    final String sourceAttribute =
860         o.getFieldAsString(JSON_FIELD_SOURCE_ATTRIBUTE);
861    if (sourceAttribute == null)
862    {
863      throw new LDAPException(ResultCode.DECODING_ERROR,
864           ERR_JOIN_RULE_JSON_DN_MISSING_SOURCE_ATTR.get(
865                JSON_FIELD_SOURCE_ATTRIBUTE));
866    }
867
868    if (strict)
869    {
870      final List<String> unrecognizedFields =
871           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
872                o, JSON_FIELD_TYPE, JSON_FIELD_SOURCE_ATTRIBUTE);
873      if (! unrecognizedFields.isEmpty())
874      {
875        throw new LDAPException(ResultCode.DECODING_ERROR,
876             ERR_JOIN_RULE_JSON_UNRECOGNIZED_DN_FIELD.get(
877                  unrecognizedFields.get(0)));
878      }
879    }
880
881    return createDNJoin(sourceAttribute);
882  }
883
884
885
886  /**
887   * Decodes the provided JSON object as a reverse DN join rule.
888   *
889   * @param  o       The JSON object that represents the join rule to decode.
890   *                 It must not be {@code null}.
891   * @param  strict  Indicates whether to use strict mode when decoding the
892   *                 provided JSON object.  If this is {@code true}, then this
893   *                 method will throw an exception if the provided JSON object
894   *                 contains any unrecognized fields.  If this is
895   *                 {@code false}, then unrecognized fields will be ignored.
896   *
897   * @return  The reverse DN join rule decoded from the provided JSON object.
898   *
899   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
900   *                         valid reverse DN join rule.
901   */
902  @NotNull()
903  private static JoinRule decodeJSONReverseDNJoinRule(
904               @NotNull final JSONObject o,
905               final boolean strict)
906          throws LDAPException
907  {
908    final String targetAttribute =
909         o.getFieldAsString(JSON_FIELD_TARGET_ATTRIBUTE);
910    if (targetAttribute == null)
911    {
912      throw new LDAPException(ResultCode.DECODING_ERROR,
913           ERR_JOIN_RULE_JSON_REVERSE_DN_MISSING_TARGET_ATTR.get(
914                JSON_FIELD_TARGET_ATTRIBUTE));
915    }
916
917    if (strict)
918    {
919      final List<String> unrecognizedFields =
920           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
921                o, JSON_FIELD_TYPE, JSON_FIELD_TARGET_ATTRIBUTE);
922      if (! unrecognizedFields.isEmpty())
923      {
924        throw new LDAPException(ResultCode.DECODING_ERROR,
925             ERR_JOIN_RULE_JSON_UNRECOGNIZED_REVERSE_DN_FIELD.get(
926                  unrecognizedFields.get(0)));
927      }
928    }
929
930    return createReverseDNJoin(targetAttribute);
931  }
932
933
934
935  /**
936   * Decodes the provided JSON object as an equality join rule.
937   *
938   * @param  o       The JSON object that represents the join rule to decode.
939   *                 It must not be {@code null}.
940   * @param  strict  Indicates whether to use strict mode when decoding the
941   *                 provided JSON object.  If this is {@code true}, then this
942   *                 method will throw an exception if the provided JSON object
943   *                 contains any unrecognized fields.  If this is
944   *                 {@code false}, then unrecognized fields will be ignored.
945   *
946   * @return  The equality join rule decoded from the provided JSON object.
947   *
948   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
949   *                         valid equality join rule.
950   */
951  @NotNull()
952  private static JoinRule decodeJSONEqualityJoinRule(
953               @NotNull final JSONObject o,
954               final boolean strict)
955          throws LDAPException
956  {
957    final String sourceAttribute =
958         o.getFieldAsString(JSON_FIELD_SOURCE_ATTRIBUTE);
959    if (sourceAttribute == null)
960    {
961      throw new LDAPException(ResultCode.DECODING_ERROR,
962           ERR_JOIN_RULE_JSON_EQUALITY_MISSING_FIELD.get(
963                JSON_FIELD_SOURCE_ATTRIBUTE));
964    }
965
966    final String targetAttribute =
967         o.getFieldAsString(JSON_FIELD_TARGET_ATTRIBUTE);
968    if (targetAttribute == null)
969    {
970      throw new LDAPException(ResultCode.DECODING_ERROR,
971           ERR_JOIN_RULE_JSON_EQUALITY_MISSING_FIELD.get(
972                JSON_FIELD_TARGET_ATTRIBUTE));
973    }
974
975    final Boolean matchAll = o.getFieldAsBoolean(JSON_FIELD_MATCH_ALL);
976    if (matchAll == null)
977    {
978      throw new LDAPException(ResultCode.DECODING_ERROR,
979           ERR_JOIN_RULE_JSON_EQUALITY_MISSING_FIELD.get(JSON_FIELD_MATCH_ALL));
980    }
981
982    if (strict)
983    {
984      final List<String> unrecognizedFields =
985           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
986                o, JSON_FIELD_TYPE, JSON_FIELD_SOURCE_ATTRIBUTE,
987                JSON_FIELD_TARGET_ATTRIBUTE, JSON_FIELD_MATCH_ALL);
988      if (! unrecognizedFields.isEmpty())
989      {
990        throw new LDAPException(ResultCode.DECODING_ERROR,
991             ERR_JOIN_RULE_JSON_UNRECOGNIZED_EQUALITY_FIELD.get(
992                  unrecognizedFields.get(0)));
993      }
994    }
995
996    return createEqualityJoin(sourceAttribute, targetAttribute, matchAll);
997  }
998
999
1000
1001  /**
1002   * Decodes the provided JSON object as a contains join rule.
1003   *
1004   * @param  o       The JSON object that represents the join rule to decode.
1005   *                 It must not be {@code null}.
1006   * @param  strict  Indicates whether to use strict mode when decoding the
1007   *                 provided JSON object.  If this is {@code true}, then this
1008   *                 method will throw an exception if the provided JSON object
1009   *                 contains any unrecognized fields.  If this is
1010   *                 {@code false}, then unrecognized fields will be ignored.
1011   *
1012   * @return  The contains join rule decoded from the provided JSON object.
1013   *
1014   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
1015   *                         valid contains join rule.
1016   */
1017  @NotNull()
1018  private static JoinRule decodeJSONContainsJoinRule(
1019               @NotNull final JSONObject o,
1020               final boolean strict)
1021          throws LDAPException
1022  {
1023    final String sourceAttribute =
1024         o.getFieldAsString(JSON_FIELD_SOURCE_ATTRIBUTE);
1025    if (sourceAttribute == null)
1026    {
1027      throw new LDAPException(ResultCode.DECODING_ERROR,
1028           ERR_JOIN_RULE_JSON_CONTAINS_MISSING_FIELD.get(
1029                JSON_FIELD_SOURCE_ATTRIBUTE));
1030    }
1031
1032    final String targetAttribute =
1033         o.getFieldAsString(JSON_FIELD_TARGET_ATTRIBUTE);
1034    if (targetAttribute == null)
1035    {
1036      throw new LDAPException(ResultCode.DECODING_ERROR,
1037           ERR_JOIN_RULE_JSON_CONTAINS_MISSING_FIELD.get(
1038                JSON_FIELD_TARGET_ATTRIBUTE));
1039    }
1040
1041    final Boolean matchAll = o.getFieldAsBoolean(JSON_FIELD_MATCH_ALL);
1042    if (matchAll == null)
1043    {
1044      throw new LDAPException(ResultCode.DECODING_ERROR,
1045           ERR_JOIN_RULE_JSON_CONTAINS_MISSING_FIELD.get(JSON_FIELD_MATCH_ALL));
1046    }
1047
1048    if (strict)
1049    {
1050      final List<String> unrecognizedFields =
1051           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
1052                o, JSON_FIELD_TYPE, JSON_FIELD_SOURCE_ATTRIBUTE,
1053                JSON_FIELD_TARGET_ATTRIBUTE, JSON_FIELD_MATCH_ALL);
1054      if (! unrecognizedFields.isEmpty())
1055      {
1056        throw new LDAPException(ResultCode.DECODING_ERROR,
1057             ERR_JOIN_RULE_JSON_UNRECOGNIZED_CONTAINS_FIELD.get(
1058                  unrecognizedFields.get(0)));
1059      }
1060    }
1061
1062    return createContainsJoin(sourceAttribute, targetAttribute, matchAll);
1063  }
1064
1065
1066
1067  /**
1068   * Decodes the provided JSON object as an AND join rule.
1069   *
1070   * @param  o       The JSON object that represents the join rule to decode.
1071   *                 It must not be {@code null}.
1072   * @param  strict  Indicates whether to use strict mode when decoding the
1073   *                 provided JSON object.  If this is {@code true}, then this
1074   *                 method will throw an exception if the provided JSON object
1075   *                 contains any unrecognized fields.  If this is
1076   *                 {@code false}, then unrecognized fields will be ignored.
1077   *
1078   * @return  The AND join rule decoded from the provided JSON object.
1079   *
1080   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
1081   *                         valid AND join rule.
1082   */
1083  @NotNull()
1084  private static JoinRule decodeJSONANDJoinRule(
1085               @NotNull final JSONObject o,
1086               final boolean strict)
1087          throws LDAPException
1088  {
1089    final List<JSONValue> ruleValues = o.getFieldAsArray(JSON_FIELD_RULES);
1090    if (ruleValues == null)
1091    {
1092      throw new LDAPException(ResultCode.DECODING_ERROR,
1093           ERR_JOIN_RULE_JSON_AND_MISSING_RULES.get(JSON_FIELD_RULES));
1094    }
1095
1096    if (ruleValues.isEmpty())
1097    {
1098      throw new LDAPException(ResultCode.DECODING_ERROR,
1099           ERR_JOIN_RULE_JSON_AND_EMPTY_RULES.get(JSON_FIELD_RULES));
1100    }
1101
1102    final List<JoinRule> rules = new ArrayList<>(ruleValues.size());
1103    for (final JSONValue v : ruleValues)
1104    {
1105      if (v instanceof JSONObject)
1106      {
1107        rules.add(decodeJSONJoinRule((JSONObject) v, strict));
1108      }
1109      else
1110      {
1111        throw new LDAPException(ResultCode.DECODING_ERROR,
1112             ERR_JOIN_RULE_JSON_AND_RULE_NOT_OBJECT.get(JSON_FIELD_RULES));
1113      }
1114    }
1115
1116    if (strict)
1117    {
1118      final List<String> unrecognizedFields =
1119           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
1120                o, JSON_FIELD_TYPE, JSON_FIELD_RULES);
1121      if (! unrecognizedFields.isEmpty())
1122      {
1123        throw new LDAPException(ResultCode.DECODING_ERROR,
1124             ERR_JOIN_RULE_JSON_UNRECOGNIZED_AND_FIELD.get(
1125                  unrecognizedFields.get(0)));
1126      }
1127    }
1128
1129    return createANDRule(rules);
1130  }
1131
1132
1133
1134  /**
1135   * Decodes the provided JSON object as an OR join rule.
1136   *
1137   * @param  o       The JSON object that represents the join rule to decode.
1138   *                 It must not be {@code null}.
1139   * @param  strict  Indicates whether to use strict mode when decoding the
1140   *                 provided JSON object.  If this is {@code true}, then this
1141   *                 method will throw an exception if the provided JSON object
1142   *                 contains any unrecognized fields.  If this is
1143   *                 {@code false}, then unrecognized fields will be ignored.
1144   *
1145   * @return  The OR join rule decoded from the provided JSON object.
1146   *
1147   * @throws  LDAPException  If the provided JSON object cannot be decoded as a
1148   *                         valid OR join rule.
1149   */
1150  @NotNull()
1151  private static JoinRule decodeJSONORJoinRule(
1152               @NotNull final JSONObject o,
1153               final boolean strict)
1154          throws LDAPException
1155  {
1156    final List<JSONValue> ruleValues = o.getFieldAsArray(JSON_FIELD_RULES);
1157    if (ruleValues == null)
1158    {
1159      throw new LDAPException(ResultCode.DECODING_ERROR,
1160           ERR_JOIN_RULE_JSON_OR_MISSING_RULES.get(JSON_FIELD_RULES));
1161    }
1162
1163    if (ruleValues.isEmpty())
1164    {
1165      throw new LDAPException(ResultCode.DECODING_ERROR,
1166           ERR_JOIN_RULE_JSON_OR_EMPTY_RULES.get(JSON_FIELD_RULES));
1167    }
1168
1169    final List<JoinRule> rules = new ArrayList<>(ruleValues.size());
1170    for (final JSONValue v : ruleValues)
1171    {
1172      if (v instanceof JSONObject)
1173      {
1174        rules.add(decodeJSONJoinRule((JSONObject) v, strict));
1175      }
1176      else
1177      {
1178        throw new LDAPException(ResultCode.DECODING_ERROR,
1179             ERR_JOIN_RULE_JSON_OR_RULE_NOT_OBJECT.get(JSON_FIELD_RULES));
1180      }
1181    }
1182
1183    if (strict)
1184    {
1185      final List<String> unrecognizedFields =
1186           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
1187                o, JSON_FIELD_TYPE, JSON_FIELD_RULES);
1188      if (! unrecognizedFields.isEmpty())
1189      {
1190        throw new LDAPException(ResultCode.DECODING_ERROR,
1191             ERR_JOIN_RULE_JSON_UNRECOGNIZED_OR_FIELD.get(
1192                  unrecognizedFields.get(0)));
1193      }
1194    }
1195
1196    return createORRule(rules);
1197  }
1198
1199
1200
1201  /**
1202   * Retrieves a string representation of this join rule.
1203   *
1204   * @return  A string representation of this join rule.
1205   */
1206  @Override()
1207  @NotNull()
1208  public String toString()
1209  {
1210    final StringBuilder buffer = new StringBuilder();
1211    toString(buffer);
1212    return buffer.toString();
1213  }
1214
1215
1216
1217  /**
1218   * Appends a string representation of this join rule to the provided buffer.
1219   *
1220   * @param  buffer  The buffer to which the information should be appended.
1221   */
1222  public void toString(@NotNull final StringBuilder buffer)
1223  {
1224    switch (type)
1225    {
1226      case JOIN_TYPE_AND:
1227        buffer.append("ANDJoinRule(components={");
1228        for (int i=0; i < components.length; i++)
1229        {
1230          if (i > 0)
1231          {
1232            buffer.append(", ");
1233          }
1234          components[i].toString(buffer);
1235        }
1236        buffer.append("})");
1237        break;
1238
1239      case JOIN_TYPE_OR:
1240        buffer.append("ORJoinRule(components={");
1241        for (int i=0; i < components.length; i++)
1242        {
1243          if (i > 0)
1244          {
1245            buffer.append(", ");
1246          }
1247          components[i].toString(buffer);
1248        }
1249        buffer.append("})");
1250        break;
1251
1252      case JOIN_TYPE_DN:
1253        buffer.append("DNJoinRule(sourceAttr=");
1254        buffer.append(sourceAttribute);
1255        buffer.append(')');
1256        break;
1257
1258      case JOIN_TYPE_EQUALITY:
1259        buffer.append("EqualityJoinRule(sourceAttr=");
1260        buffer.append(sourceAttribute);
1261        buffer.append(", targetAttr=");
1262        buffer.append(targetAttribute);
1263        buffer.append(", matchAll=");
1264        buffer.append(matchAll);
1265        buffer.append(')');
1266        break;
1267
1268      case JOIN_TYPE_CONTAINS:
1269        buffer.append("ContainsJoinRule(sourceAttr=");
1270        buffer.append(sourceAttribute);
1271        buffer.append(", targetAttr=");
1272        buffer.append(targetAttribute);
1273        buffer.append(", matchAll=");
1274        buffer.append(matchAll);
1275        buffer.append(')');
1276        break;
1277
1278    case JOIN_TYPE_REVERSE_DN:
1279      buffer.append("ReverseDNJoinRule(targetAttr=");
1280      buffer.append(targetAttribute);
1281      buffer.append(')');
1282      break;
1283    }
1284  }
1285}