001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-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) 2007-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.schema;
037
038
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.HashSet;
043import java.util.Map;
044import java.util.LinkedHashMap;
045
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.ResultCode;
048import com.unboundid.util.Debug;
049import com.unboundid.util.NotMutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055import com.unboundid.util.Validator;
056
057import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
058
059
060
061/**
062 * This class provides a data structure that describes an LDAP DIT structure
063 * rule schema element.
064 */
065@NotMutable()
066@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
067public final class DITStructureRuleDefinition
068       extends SchemaElement
069{
070  /**
071   * The serial version UID for this serializable class.
072   */
073  private static final long serialVersionUID = -3233223742542121140L;
074
075
076
077  // Indicates whether this DIT structure rule is declared obsolete.
078  private final boolean isObsolete;
079
080  // The rule ID for this DIT structure rule.
081  private final int ruleID;
082
083  // The set of superior rule IDs for this DIT structure rule.
084  @NotNull private final int[] superiorRuleIDs;
085
086  // The set of extensions for this DIT content rule.
087  @NotNull private final Map<String,String[]> extensions;
088
089  // The description for this DIT content rule.
090  @Nullable private final String description;
091
092  // The string representation of this DIT structure rule.
093  @NotNull private final String ditStructureRuleString;
094
095  // The name/OID of the name form with which this DIT structure rule is
096  // associated.
097  @NotNull private final String nameFormID;
098
099  // The set of names for this DIT structure rule.
100  @NotNull private final String[] names;
101
102
103
104  /**
105   * Creates a new DIT structure rule from the provided string representation.
106   *
107   * @param  s  The string representation of the DIT structure rule to create,
108   *            using the syntax described in RFC 4512 section 4.1.7.1.  It must
109   *            not be {@code null}.
110   *
111   * @throws  LDAPException  If the provided string cannot be decoded as a DIT
112   *                         structure rule definition.
113   */
114  public DITStructureRuleDefinition(@NotNull final String s)
115         throws LDAPException
116  {
117    Validator.ensureNotNull(s);
118
119    ditStructureRuleString = s.trim();
120
121    // The first character must be an opening parenthesis.
122    final int length = ditStructureRuleString.length();
123    if (length == 0)
124    {
125      throw new LDAPException(ResultCode.DECODING_ERROR,
126                              ERR_DSR_DECODE_EMPTY.get());
127    }
128    else if (ditStructureRuleString.charAt(0) != '(')
129    {
130      throw new LDAPException(ResultCode.DECODING_ERROR,
131                              ERR_DSR_DECODE_NO_OPENING_PAREN.get(
132                                   ditStructureRuleString));
133    }
134
135
136    // Skip over any spaces until we reach the start of the OID, then read the
137    // rule ID until we find the next space.
138    int pos = skipSpaces(ditStructureRuleString, 1, length);
139
140    StringBuilder buffer = new StringBuilder();
141    pos = readOID(ditStructureRuleString, pos, length, buffer);
142    final String ruleIDStr = buffer.toString();
143    try
144    {
145      ruleID = Integer.parseInt(ruleIDStr);
146    }
147    catch (final NumberFormatException nfe)
148    {
149      Debug.debugException(nfe);
150      throw new LDAPException(ResultCode.DECODING_ERROR,
151                              ERR_DSR_DECODE_RULE_ID_NOT_INT.get(
152                                   ditStructureRuleString),
153                              nfe);
154    }
155
156
157    // Technically, DIT structure elements are supposed to appear in a specific
158    // order, but we'll be lenient and allow remaining elements to come in any
159    // order.
160    final ArrayList<Integer> supList = new ArrayList<>(1);
161    final ArrayList<String> nameList = new ArrayList<>(1);
162    final Map<String,String[]> exts =
163         new LinkedHashMap<>(StaticUtils.computeMapCapacity(5));
164    Boolean obsolete = null;
165    String descr = null;
166    String nfID = null;
167
168    while (true)
169    {
170      // Skip over any spaces until we find the next element.
171      pos = skipSpaces(ditStructureRuleString, pos, length);
172
173      // Read until we find the next space or the end of the string.  Use that
174      // token to figure out what to do next.
175      final int tokenStartPos = pos;
176      while ((pos < length) && (ditStructureRuleString.charAt(pos) != ' '))
177      {
178        pos++;
179      }
180
181      // It's possible that the token could be smashed right up against the
182      // closing parenthesis.  If that's the case, then extract just the token
183      // and handle the closing parenthesis the next time through.
184      String token = ditStructureRuleString.substring(tokenStartPos, pos);
185      if ((token.length() > 1) && (token.endsWith(")")))
186      {
187        token = token.substring(0, token.length() - 1);
188        pos--;
189      }
190
191      final String lowerToken = StaticUtils.toLowerCase(token);
192      if (lowerToken.equals(")"))
193      {
194        // This indicates that we're at the end of the value.  There should not
195        // be any more closing characters.
196        if (pos < length)
197        {
198          throw new LDAPException(ResultCode.DECODING_ERROR,
199                                  ERR_DSR_DECODE_CLOSE_NOT_AT_END.get(
200                                       ditStructureRuleString));
201        }
202        break;
203      }
204      else if (lowerToken.equals("name"))
205      {
206        if (nameList.isEmpty())
207        {
208          pos = skipSpaces(ditStructureRuleString, pos, length);
209          pos = readQDStrings(ditStructureRuleString, pos, length, token,
210               nameList);
211        }
212        else
213        {
214          throw new LDAPException(ResultCode.DECODING_ERROR,
215                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
216                                       ditStructureRuleString, "NAME"));
217        }
218      }
219      else if (lowerToken.equals("desc"))
220      {
221        if (descr == null)
222        {
223          pos = skipSpaces(ditStructureRuleString, pos, length);
224
225          buffer = new StringBuilder();
226          pos = readQDString(ditStructureRuleString, pos, length, token,
227               buffer);
228          descr = buffer.toString();
229        }
230        else
231        {
232          throw new LDAPException(ResultCode.DECODING_ERROR,
233                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
234                                       ditStructureRuleString, "DESC"));
235        }
236      }
237      else if (lowerToken.equals("obsolete"))
238      {
239        if (obsolete == null)
240        {
241          obsolete = true;
242        }
243        else
244        {
245          throw new LDAPException(ResultCode.DECODING_ERROR,
246                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
247                                       ditStructureRuleString, "OBSOLETE"));
248        }
249      }
250      else if (lowerToken.equals("form"))
251      {
252        if (nfID == null)
253        {
254          pos = skipSpaces(ditStructureRuleString, pos, length);
255
256          buffer = new StringBuilder();
257          pos = readOID(ditStructureRuleString, pos, length, buffer);
258          nfID = buffer.toString();
259        }
260        else
261        {
262          throw new LDAPException(ResultCode.DECODING_ERROR,
263                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
264                                       ditStructureRuleString, "FORM"));
265        }
266      }
267      else if (lowerToken.equals("sup"))
268      {
269        if (supList.isEmpty())
270        {
271          final ArrayList<String> supStrs = new ArrayList<>(1);
272
273          pos = skipSpaces(ditStructureRuleString, pos, length);
274          pos = readOIDs(ditStructureRuleString, pos, length, token, supStrs);
275
276          supList.ensureCapacity(supStrs.size());
277          for (final String supStr : supStrs)
278          {
279            try
280            {
281              supList.add(Integer.parseInt(supStr));
282            }
283            catch (final NumberFormatException nfe)
284            {
285              Debug.debugException(nfe);
286              throw new LDAPException(ResultCode.DECODING_ERROR,
287                                      ERR_DSR_DECODE_SUP_ID_NOT_INT.get(
288                                           ditStructureRuleString),
289                                      nfe);
290            }
291          }
292        }
293        else
294        {
295          throw new LDAPException(ResultCode.DECODING_ERROR,
296                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
297                                       ditStructureRuleString, "SUP"));
298        }
299      }
300      else if (lowerToken.startsWith("x-"))
301      {
302        pos = skipSpaces(ditStructureRuleString, pos, length);
303
304        final ArrayList<String> valueList = new ArrayList<>(5);
305        pos = readQDStrings(ditStructureRuleString, pos, length, token,
306             valueList);
307
308        final String[] values = new String[valueList.size()];
309        valueList.toArray(values);
310
311        if (exts.containsKey(token))
312        {
313          throw new LDAPException(ResultCode.DECODING_ERROR,
314                                  ERR_DSR_DECODE_DUP_EXT.get(
315                                       ditStructureRuleString, token));
316        }
317
318        exts.put(token, values);
319      }
320      else
321      {
322        throw new LDAPException(ResultCode.DECODING_ERROR,
323                                ERR_DSR_DECODE_UNEXPECTED_TOKEN.get(
324                                     ditStructureRuleString, token));
325      }
326    }
327
328    description = descr;
329    nameFormID  = nfID;
330
331    if (nameFormID == null)
332    {
333      throw new LDAPException(ResultCode.DECODING_ERROR,
334                              ERR_DSR_DECODE_NO_FORM.get(
335                                   ditStructureRuleString));
336    }
337
338    names = new String[nameList.size()];
339    nameList.toArray(names);
340
341    superiorRuleIDs = new int[supList.size()];
342    for (int i=0; i < superiorRuleIDs.length; i++)
343    {
344      superiorRuleIDs[i] = supList.get(i);
345    }
346
347    isObsolete = (obsolete != null);
348
349    extensions = Collections.unmodifiableMap(exts);
350  }
351
352
353
354  /**
355   * Creates a new DIT structure rule with the provided information.
356   *
357   * @param  ruleID          The rule ID for this DIT structure rule.
358   * @param  name            The name for this DIT structure rule.  It may be
359   *                         {@code null} if the DIT structure rule should only
360   *                         be referenced by rule ID.
361   * @param  description     The description for this DIT structure rule.  It
362   *                         may be {@code null} if there is no description.
363   * @param  nameFormID      The name or OID of the name form with which this
364   *                         DIT structure rule is associated.  It must not be
365   *                         {@code null}.
366   * @param  superiorRuleID  The superior rule ID for this DIT structure rule.
367   *                         It may be {@code null} if there are no superior
368   *                         rule IDs.
369   * @param  extensions      The set of extensions for this DIT structure rule.
370   *                         It may be {@code null} or empty if there are no
371   *                         extensions.
372   */
373  public DITStructureRuleDefinition(final int ruleID,
374              @Nullable final String name,
375              @Nullable final String description,
376              @NotNull final String nameFormID,
377              @Nullable final Integer superiorRuleID,
378              @Nullable final Map<String,String[]> extensions)
379  {
380    this(ruleID, ((name == null) ? null : new String[] { name }), description,
381         false, nameFormID,
382         ((superiorRuleID == null) ? null : new int[] { superiorRuleID }),
383         extensions);
384  }
385
386
387
388  /**
389   * Creates a new DIT structure rule with the provided information.
390   *
391   * @param  ruleID           The rule ID for this DIT structure rule.
392   * @param  names            The set of names for this DIT structure rule.  It
393   *                          may be {@code null} or empty if the DIT structure
394   *                          rule should only be referenced by rule ID.
395   * @param  description      The description for this DIT structure rule.  It
396   *                          may be {@code null} if there is no description.
397   * @param  isObsolete       Indicates whether this DIT structure rule is
398   *                          declared obsolete.
399   * @param  nameFormID       The name or OID of the name form with which this
400   *                          DIT structure rule is associated.  It must not be
401   *                          {@code null}.
402   * @param  superiorRuleIDs  The superior rule IDs for this DIT structure rule.
403   *                          It may be {@code null} or empty if there are no
404   *                          superior rule IDs.
405   * @param  extensions       The set of extensions for this DIT structure rule.
406   *                          It may be {@code null} or empty if there are no
407   *                          extensions.
408   */
409  public DITStructureRuleDefinition(final int ruleID,
410              @Nullable final String[] names,
411              @Nullable final String description,
412              final boolean isObsolete,
413              @NotNull final String nameFormID,
414              @Nullable final int[] superiorRuleIDs,
415              @Nullable final Map<String,String[]> extensions)
416  {
417    Validator.ensureNotNull(nameFormID);
418
419    this.ruleID      = ruleID;
420    this.description = description;
421    this.isObsolete  = isObsolete;
422    this.nameFormID  = nameFormID;
423
424    if (names == null)
425    {
426      this.names = StaticUtils.NO_STRINGS;
427    }
428    else
429    {
430      this.names = names;
431    }
432
433    if (superiorRuleIDs == null)
434    {
435      this.superiorRuleIDs = StaticUtils.NO_INTS;
436    }
437    else
438    {
439      this.superiorRuleIDs = superiorRuleIDs;
440    }
441
442    if (extensions == null)
443    {
444      this.extensions = Collections.emptyMap();
445    }
446    else
447    {
448      this.extensions = Collections.unmodifiableMap(extensions);
449    }
450
451    final StringBuilder buffer = new StringBuilder();
452    createDefinitionString(buffer);
453    ditStructureRuleString = buffer.toString();
454  }
455
456
457
458  /**
459   * Constructs a string representation of this DIT content rule definition in
460   * the provided buffer.
461   *
462   * @param  buffer  The buffer in which to construct a string representation of
463   *                 this DIT content rule definition.
464   */
465  private void createDefinitionString(@NotNull final StringBuilder buffer)
466  {
467    buffer.append("( ");
468    buffer.append(ruleID);
469
470    if (names.length == 1)
471    {
472      buffer.append(" NAME '");
473      buffer.append(names[0]);
474      buffer.append('\'');
475    }
476    else if (names.length > 1)
477    {
478      buffer.append(" NAME (");
479      for (final String name : names)
480      {
481        buffer.append(" '");
482        buffer.append(name);
483        buffer.append('\'');
484      }
485      buffer.append(" )");
486    }
487
488    if (description != null)
489    {
490      buffer.append(" DESC '");
491      encodeValue(description, buffer);
492      buffer.append('\'');
493    }
494
495    if (isObsolete)
496    {
497      buffer.append(" OBSOLETE");
498    }
499
500    buffer.append(" FORM ");
501    buffer.append(nameFormID);
502
503    if (superiorRuleIDs.length == 1)
504    {
505      buffer.append(" SUP ");
506      buffer.append(superiorRuleIDs[0]);
507    }
508    else if (superiorRuleIDs.length > 1)
509    {
510      buffer.append(" SUP (");
511      for (final int supID : superiorRuleIDs)
512      {
513        buffer.append(" $ ");
514        buffer.append(supID);
515      }
516      buffer.append(" )");
517    }
518
519    for (final Map.Entry<String,String[]> e : extensions.entrySet())
520    {
521      final String   name   = e.getKey();
522      final String[] values = e.getValue();
523      if (values.length == 1)
524      {
525        buffer.append(' ');
526        buffer.append(name);
527        buffer.append(" '");
528        encodeValue(values[0], buffer);
529        buffer.append('\'');
530      }
531      else
532      {
533        buffer.append(' ');
534        buffer.append(name);
535        buffer.append(" (");
536        for (final String value : values)
537        {
538          buffer.append(" '");
539          encodeValue(value, buffer);
540          buffer.append('\'');
541        }
542        buffer.append(" )");
543      }
544    }
545
546    buffer.append(" )");
547  }
548
549
550
551  /**
552   * Retrieves the rule ID for this DIT structure rule.
553   *
554   * @return  The rule ID for this DIT structure rule.
555   */
556  public int getRuleID()
557  {
558    return ruleID;
559  }
560
561
562
563  /**
564   * Retrieves the set of names for this DIT structure rule.
565   *
566   * @return  The set of names for this DIT structure rule, or an empty array if
567   *          it does not have any names.
568   */
569  @NotNull()
570  public String[] getNames()
571  {
572    return names;
573  }
574
575
576
577  /**
578   * Retrieves the primary name that can be used to reference this DIT structure
579   * rule.  If one or more names are defined, then the first name will be used.
580   * Otherwise, the string representation of the rule ID will be returned.
581   *
582   * @return  The primary name that can be used to reference this DIT structure
583   *          rule.
584   */
585  @NotNull()
586  public String getNameOrRuleID()
587  {
588    if (names.length == 0)
589    {
590      return String.valueOf(ruleID);
591    }
592    else
593    {
594      return names[0];
595    }
596  }
597
598
599
600  /**
601   * Indicates whether the provided string matches the rule ID or any of the
602   * names for this DIT structure rule.
603   *
604   * @param  s  The string for which to make the determination.  It must not be
605   *            {@code null}.
606   *
607   * @return  {@code true} if the provided string matches the rule ID or any of
608   *          the names for this DIT structure rule, or {@code false} if not.
609   */
610  public boolean hasNameOrRuleID(@NotNull final String s)
611  {
612    for (final String name : names)
613    {
614      if (s.equalsIgnoreCase(name))
615      {
616        return true;
617      }
618    }
619
620    return s.equalsIgnoreCase(String.valueOf(ruleID));
621  }
622
623
624
625  /**
626   * Retrieves the description for this DIT structure rule, if available.
627   *
628   * @return  The description for this DIT structure rule, or {@code null} if
629   *          there is no description defined.
630   */
631  @Nullable()
632  public String getDescription()
633  {
634    return description;
635  }
636
637
638
639  /**
640   * Indicates whether this DIT structure rule is declared obsolete.
641   *
642   * @return  {@code true} if this DIT structure rule is declared obsolete, or
643   *          {@code false} if it is not.
644   */
645  public boolean isObsolete()
646  {
647    return isObsolete;
648  }
649
650
651
652  /**
653   * Retrieves the name or OID of the name form with which this DIT structure
654   * rule is associated.
655   *
656   * @return  The name or OID of the name form with which this DIT structure
657   *          rule is associated.
658   */
659  @NotNull()
660  public String getNameFormID()
661  {
662    return nameFormID;
663  }
664
665
666
667  /**
668   * Retrieves the rule IDs of the superior rules for this DIT structure rule.
669   *
670   * @return  The rule IDs of the superior rules for this DIT structure rule, or
671   *          an empty array if there are no superior rule IDs.
672   */
673  @NotNull()
674  public int[] getSuperiorRuleIDs()
675  {
676    return superiorRuleIDs;
677  }
678
679
680
681  /**
682   * Retrieves the set of extensions for this DIT structure rule.  They will be
683   * mapped from the extension name (which should start with "X-") to the set of
684   * values for that extension.
685   *
686   * @return  The set of extensions for this DIT structure rule.
687   */
688  @NotNull()
689  public Map<String,String[]> getExtensions()
690  {
691    return extensions;
692  }
693
694
695
696  /**
697   * {@inheritDoc}
698   */
699  @Override()
700  @NotNull()
701  public SchemaElementType getSchemaElementType()
702  {
703    return SchemaElementType.DIT_STRUCTURE_RULE;
704  }
705
706
707
708  /**
709   * {@inheritDoc}
710   */
711  @Override()
712  public int hashCode()
713  {
714    return ruleID;
715  }
716
717
718
719  /**
720   * {@inheritDoc}
721   */
722  @Override()
723  public boolean equals(@Nullable final Object o)
724  {
725    if (o == null)
726    {
727      return false;
728    }
729
730    if (o == this)
731    {
732      return true;
733    }
734
735    if (! (o instanceof DITStructureRuleDefinition))
736    {
737      return false;
738    }
739
740    final DITStructureRuleDefinition d = (DITStructureRuleDefinition) o;
741    if ((ruleID == d.ruleID) &&
742         nameFormID.equalsIgnoreCase(d.nameFormID) &&
743         StaticUtils.stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
744         (isObsolete == d.isObsolete) &&
745         extensionsEqual(extensions, d.extensions))
746    {
747      if (superiorRuleIDs.length != d.superiorRuleIDs.length)
748      {
749        return false;
750      }
751
752      final HashSet<Integer> s1 = new HashSet<>(
753           StaticUtils.computeMapCapacity(superiorRuleIDs.length));
754      final HashSet<Integer> s2 = new HashSet<>(
755           StaticUtils.computeMapCapacity(superiorRuleIDs.length));
756      for (final int i : superiorRuleIDs)
757      {
758        s1.add(i);
759      }
760
761      for (final int i : d.superiorRuleIDs)
762      {
763        s2.add(i);
764      }
765
766      return s1.equals(s2);
767    }
768    else
769    {
770      return false;
771    }
772  }
773
774
775
776  /**
777   * Retrieves a string representation of this DIT structure rule definition, in
778   * the format described in RFC 4512 section 4.1.7.1.
779   *
780   * @return  A string representation of this DIT structure rule definition.
781   */
782  @Override()
783  @NotNull()
784  public String toString()
785  {
786    return ditStructureRuleString;
787  }
788}