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