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) 2008-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.StaticUtils;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.Validator;
054
055import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
056
057
058
059/**
060 * This class provides a data structure that describes an LDAP DIT structure
061 * rule schema element.
062 */
063@NotMutable()
064@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
065public final class DITStructureRuleDefinition
066       extends SchemaElement
067{
068  /**
069   * A pre-allocated zero-element integer array.
070   */
071  private static final int[] NO_INTS = new int[0];
072
073
074
075  /**
076   * The serial version UID for this serializable class.
077   */
078  private static final long serialVersionUID = -3233223742542121140L;
079
080
081
082  // Indicates whether this DIT structure rule is declared obsolete.
083  private final boolean isObsolete;
084
085  // The rule ID for this DIT structure rule.
086  private final int ruleID;
087
088  // The set of superior rule IDs for this DIT structure rule.
089  private final int[] superiorRuleIDs;
090
091  // The set of extensions for this DIT content rule.
092  private final Map<String,String[]> extensions;
093
094  // The description for this DIT content rule.
095  private final String description;
096
097  // The string representation of this DIT structure rule.
098  private final String ditStructureRuleString;
099
100  // The name/OID of the name form with which this DIT structure rule is
101  // associated.
102  private final String nameFormID;
103
104  // The set of names for this DIT structure rule.
105  private final String[] names;
106
107
108
109  /**
110   * Creates a new DIT structure rule from the provided string representation.
111   *
112   * @param  s  The string representation of the DIT structure rule to create,
113   *            using the syntax described in RFC 4512 section 4.1.7.1.  It must
114   *            not be {@code null}.
115   *
116   * @throws  LDAPException  If the provided string cannot be decoded as a DIT
117   *                         structure rule definition.
118   */
119  public DITStructureRuleDefinition(final String s)
120         throws LDAPException
121  {
122    Validator.ensureNotNull(s);
123
124    ditStructureRuleString = s.trim();
125
126    // The first character must be an opening parenthesis.
127    final int length = ditStructureRuleString.length();
128    if (length == 0)
129    {
130      throw new LDAPException(ResultCode.DECODING_ERROR,
131                              ERR_DSR_DECODE_EMPTY.get());
132    }
133    else if (ditStructureRuleString.charAt(0) != '(')
134    {
135      throw new LDAPException(ResultCode.DECODING_ERROR,
136                              ERR_DSR_DECODE_NO_OPENING_PAREN.get(
137                                   ditStructureRuleString));
138    }
139
140
141    // Skip over any spaces until we reach the start of the OID, then read the
142    // rule ID until we find the next space.
143    int pos = skipSpaces(ditStructureRuleString, 1, length);
144
145    StringBuilder buffer = new StringBuilder();
146    pos = readOID(ditStructureRuleString, pos, length, buffer);
147    final String ruleIDStr = buffer.toString();
148    try
149    {
150      ruleID = Integer.parseInt(ruleIDStr);
151    }
152    catch (final NumberFormatException nfe)
153    {
154      Debug.debugException(nfe);
155      throw new LDAPException(ResultCode.DECODING_ERROR,
156                              ERR_DSR_DECODE_RULE_ID_NOT_INT.get(
157                                   ditStructureRuleString),
158                              nfe);
159    }
160
161
162    // Technically, DIT structure elements are supposed to appear in a specific
163    // order, but we'll be lenient and allow remaining elements to come in any
164    // order.
165    final ArrayList<Integer> supList = new ArrayList<>(1);
166    final ArrayList<String> nameList = new ArrayList<>(1);
167    final Map<String,String[]> exts =
168         new LinkedHashMap<>(StaticUtils.computeMapCapacity(5));
169    Boolean obsolete = null;
170    String descr = null;
171    String nfID = null;
172
173    while (true)
174    {
175      // Skip over any spaces until we find the next element.
176      pos = skipSpaces(ditStructureRuleString, pos, length);
177
178      // Read until we find the next space or the end of the string.  Use that
179      // token to figure out what to do next.
180      final int tokenStartPos = pos;
181      while ((pos < length) && (ditStructureRuleString.charAt(pos) != ' '))
182      {
183        pos++;
184      }
185
186      // It's possible that the token could be smashed right up against the
187      // closing parenthesis.  If that's the case, then extract just the token
188      // and handle the closing parenthesis the next time through.
189      String token = ditStructureRuleString.substring(tokenStartPos, pos);
190      if ((token.length() > 1) && (token.endsWith(")")))
191      {
192        token = token.substring(0, token.length() - 1);
193        pos--;
194      }
195
196      final String lowerToken = StaticUtils.toLowerCase(token);
197      if (lowerToken.equals(")"))
198      {
199        // This indicates that we're at the end of the value.  There should not
200        // be any more closing characters.
201        if (pos < length)
202        {
203          throw new LDAPException(ResultCode.DECODING_ERROR,
204                                  ERR_DSR_DECODE_CLOSE_NOT_AT_END.get(
205                                       ditStructureRuleString));
206        }
207        break;
208      }
209      else if (lowerToken.equals("name"))
210      {
211        if (nameList.isEmpty())
212        {
213          pos = skipSpaces(ditStructureRuleString, pos, length);
214          pos = readQDStrings(ditStructureRuleString, pos, length, nameList);
215        }
216        else
217        {
218          throw new LDAPException(ResultCode.DECODING_ERROR,
219                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
220                                       ditStructureRuleString, "NAME"));
221        }
222      }
223      else if (lowerToken.equals("desc"))
224      {
225        if (descr == null)
226        {
227          pos = skipSpaces(ditStructureRuleString, pos, length);
228
229          buffer = new StringBuilder();
230          pos = readQDString(ditStructureRuleString, pos, length, buffer);
231          descr = buffer.toString();
232        }
233        else
234        {
235          throw new LDAPException(ResultCode.DECODING_ERROR,
236                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
237                                       ditStructureRuleString, "DESC"));
238        }
239      }
240      else if (lowerToken.equals("obsolete"))
241      {
242        if (obsolete == null)
243        {
244          obsolete = true;
245        }
246        else
247        {
248          throw new LDAPException(ResultCode.DECODING_ERROR,
249                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
250                                       ditStructureRuleString, "OBSOLETE"));
251        }
252      }
253      else if (lowerToken.equals("form"))
254      {
255        if (nfID == null)
256        {
257          pos = skipSpaces(ditStructureRuleString, pos, length);
258
259          buffer = new StringBuilder();
260          pos = readOID(ditStructureRuleString, pos, length, buffer);
261          nfID = buffer.toString();
262        }
263        else
264        {
265          throw new LDAPException(ResultCode.DECODING_ERROR,
266                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
267                                       ditStructureRuleString, "FORM"));
268        }
269      }
270      else if (lowerToken.equals("sup"))
271      {
272        if (supList.isEmpty())
273        {
274          final ArrayList<String> supStrs = new ArrayList<>(1);
275
276          pos = skipSpaces(ditStructureRuleString, pos, length);
277          pos = readOIDs(ditStructureRuleString, pos, length, supStrs);
278
279          supList.ensureCapacity(supStrs.size());
280          for (final String supStr : supStrs)
281          {
282            try
283            {
284              supList.add(Integer.parseInt(supStr));
285            }
286            catch (final NumberFormatException nfe)
287            {
288              Debug.debugException(nfe);
289              throw new LDAPException(ResultCode.DECODING_ERROR,
290                                      ERR_DSR_DECODE_SUP_ID_NOT_INT.get(
291                                           ditStructureRuleString),
292                                      nfe);
293            }
294          }
295        }
296        else
297        {
298          throw new LDAPException(ResultCode.DECODING_ERROR,
299                                  ERR_DSR_DECODE_MULTIPLE_ELEMENTS.get(
300                                       ditStructureRuleString, "SUP"));
301        }
302      }
303      else if (lowerToken.startsWith("x-"))
304      {
305        pos = skipSpaces(ditStructureRuleString, pos, length);
306
307        final ArrayList<String> valueList = new ArrayList<>(5);
308        pos = readQDStrings(ditStructureRuleString, pos, length, valueList);
309
310        final String[] values = new String[valueList.size()];
311        valueList.toArray(values);
312
313        if (exts.containsKey(token))
314        {
315          throw new LDAPException(ResultCode.DECODING_ERROR,
316                                  ERR_DSR_DECODE_DUP_EXT.get(
317                                       ditStructureRuleString, token));
318        }
319
320        exts.put(token, values);
321      }
322      else
323      {
324        throw new LDAPException(ResultCode.DECODING_ERROR,
325                                ERR_DSR_DECODE_UNEXPECTED_TOKEN.get(
326                                     ditStructureRuleString, token));
327      }
328    }
329
330    description = descr;
331    nameFormID  = nfID;
332
333    if (nameFormID == null)
334    {
335      throw new LDAPException(ResultCode.DECODING_ERROR,
336                              ERR_DSR_DECODE_NO_FORM.get(
337                                   ditStructureRuleString));
338    }
339
340    names = new String[nameList.size()];
341    nameList.toArray(names);
342
343    superiorRuleIDs = new int[supList.size()];
344    for (int i=0; i < superiorRuleIDs.length; i++)
345    {
346      superiorRuleIDs[i] = supList.get(i);
347    }
348
349    isObsolete = (obsolete != null);
350
351    extensions = Collections.unmodifiableMap(exts);
352  }
353
354
355
356  /**
357   * Creates a new DIT structure rule with the provided information.
358   *
359   * @param  ruleID          The rule ID for this DIT structure rule.
360   * @param  name            The name for this DIT structure rule.  It may be
361   *                         {@code null} if the DIT structure rule should only
362   *                         be referenced by rule ID.
363   * @param  description     The description for this DIT structure rule.  It
364   *                         may be {@code null} if there is no description.
365   * @param  nameFormID      The name or OID of the name form with which this
366   *                         DIT structure rule is associated.  It must not be
367   *                         {@code null}.
368   * @param  superiorRuleID  The superior rule ID for this DIT structure rule.
369   *                         It may be {@code null} if there are no superior
370   *                         rule IDs.
371   * @param  extensions      The set of extensions for this DIT structure rule.
372   *                         It may be {@code null} or empty if there are no
373   *                         extensions.
374   */
375  public DITStructureRuleDefinition(final int ruleID, final String name,
376                                    final String description,
377                                    final String nameFormID,
378                                    final Integer superiorRuleID,
379                                    final Map<String,String[]> extensions)
380  {
381    this(ruleID, ((name == null) ? null : new String[] { name }), description,
382         false, nameFormID,
383         ((superiorRuleID == null) ? null : new int[] { superiorRuleID }),
384         extensions);
385  }
386
387
388
389  /**
390   * Creates a new DIT structure rule with the provided information.
391   *
392   * @param  ruleID           The rule ID for this DIT structure rule.
393   * @param  names            The set of names for this DIT structure rule.  It
394   *                          may be {@code null} or empty if the DIT structure
395   *                          rule should only be referenced by rule ID.
396   * @param  description      The description for this DIT structure rule.  It
397   *                          may be {@code null} if there is no description.
398   * @param  isObsolete       Indicates whether this DIT structure rule is
399   *                          declared obsolete.
400   * @param  nameFormID       The name or OID of the name form with which this
401   *                          DIT structure rule is associated.  It must not be
402   *                          {@code null}.
403   * @param  superiorRuleIDs  The superior rule IDs for this DIT structure rule.
404   *                          It may be {@code null} or empty if there are no
405   *                          superior rule IDs.
406   * @param  extensions       The set of extensions for this DIT structure rule.
407   *                          It may be {@code null} or empty if there are no
408   *                          extensions.
409   */
410  public DITStructureRuleDefinition(final int ruleID, final String[] names,
411                                    final String description,
412                                    final boolean isObsolete,
413                                    final String nameFormID,
414                                    final int[] superiorRuleIDs,
415                                    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 = 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(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  public String[] getNames()
570  {
571    return names;
572  }
573
574
575
576  /**
577   * Retrieves the primary name that can be used to reference this DIT structure
578   * rule.  If one or more names are defined, then the first name will be used.
579   * Otherwise, the string representation of the rule ID will be returned.
580   *
581   * @return  The primary name that can be used to reference this DIT structure
582   *          rule.
583   */
584  public String getNameOrRuleID()
585  {
586    if (names.length == 0)
587    {
588      return String.valueOf(ruleID);
589    }
590    else
591    {
592      return names[0];
593    }
594  }
595
596
597
598  /**
599   * Indicates whether the provided string matches the rule ID or any of the
600   * names for this DIT structure rule.
601   *
602   * @param  s  The string for which to make the determination.  It must not be
603   *            {@code null}.
604   *
605   * @return  {@code true} if the provided string matches the rule ID or any of
606   *          the names for this DIT structure rule, or {@code false} if not.
607   */
608  public boolean hasNameOrRuleID(final String s)
609  {
610    for (final String name : names)
611    {
612      if (s.equalsIgnoreCase(name))
613      {
614        return true;
615      }
616    }
617
618    return s.equalsIgnoreCase(String.valueOf(ruleID));
619  }
620
621
622
623  /**
624   * Retrieves the description for this DIT structure rule, if available.
625   *
626   * @return  The description for this DIT structure rule, or {@code null} if
627   *          there is no description defined.
628   */
629  public String getDescription()
630  {
631    return description;
632  }
633
634
635
636  /**
637   * Indicates whether this DIT structure rule is declared obsolete.
638   *
639   * @return  {@code true} if this DIT structure rule is declared obsolete, or
640   *          {@code false} if it is not.
641   */
642  public boolean isObsolete()
643  {
644    return isObsolete;
645  }
646
647
648
649  /**
650   * Retrieves the name or OID of the name form with which this DIT structure
651   * rule is associated.
652   *
653   * @return  The name or OID of the name form with which this DIT structure
654   *          rule is associated.
655   */
656  public String getNameFormID()
657  {
658    return nameFormID;
659  }
660
661
662
663  /**
664   * Retrieves the rule IDs of the superior rules for this DIT structure rule.
665   *
666   * @return  The rule IDs of the superior rules for this DIT structure rule, or
667   *          an empty array if there are no superior rule IDs.
668   */
669  public int[] getSuperiorRuleIDs()
670  {
671    return superiorRuleIDs;
672  }
673
674
675
676  /**
677   * Retrieves the set of extensions for this DIT structure rule.  They will be
678   * mapped from the extension name (which should start with "X-") to the set of
679   * values for that extension.
680   *
681   * @return  The set of extensions for this DIT structure rule.
682   */
683  public Map<String,String[]> getExtensions()
684  {
685    return extensions;
686  }
687
688
689
690  /**
691   * {@inheritDoc}
692   */
693  @Override()
694  public int hashCode()
695  {
696    return ruleID;
697  }
698
699
700
701  /**
702   * {@inheritDoc}
703   */
704  @Override()
705  public boolean equals(final Object o)
706  {
707    if (o == null)
708    {
709      return false;
710    }
711
712    if (o == this)
713    {
714      return true;
715    }
716
717    if (! (o instanceof DITStructureRuleDefinition))
718    {
719      return false;
720    }
721
722    final DITStructureRuleDefinition d = (DITStructureRuleDefinition) o;
723    if ((ruleID == d.ruleID) &&
724         nameFormID.equalsIgnoreCase(d.nameFormID) &&
725         StaticUtils.stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
726         (isObsolete == d.isObsolete) &&
727         extensionsEqual(extensions, d.extensions))
728    {
729      if (superiorRuleIDs.length != d.superiorRuleIDs.length)
730      {
731        return false;
732      }
733
734      final HashSet<Integer> s1 = new HashSet<>(
735           StaticUtils.computeMapCapacity(superiorRuleIDs.length));
736      final HashSet<Integer> s2 = new HashSet<>(
737           StaticUtils.computeMapCapacity(superiorRuleIDs.length));
738      for (final int i : superiorRuleIDs)
739      {
740        s1.add(i);
741      }
742
743      for (final int i : d.superiorRuleIDs)
744      {
745        s2.add(i);
746      }
747
748      return s1.equals(s2);
749    }
750    else
751    {
752      return false;
753    }
754  }
755
756
757
758  /**
759   * Retrieves a string representation of this DIT structure rule definition, in
760   * the format described in RFC 4512 section 4.1.7.1.
761   *
762   * @return  A string representation of this DIT structure rule definition.
763   */
764  @Override()
765  public String toString()
766  {
767    return ditStructureRuleString;
768  }
769}