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