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