001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 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  names            The set of names for this DIT structure rule.  It
345   *                          may be {@code null} or empty if the DIT structure
346   *                          rule should only 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  isObsolete       Indicates whether this DIT structure rule is
350   *                          declared obsolete.
351   * @param  nameFormID       The name or OID of the name form with which this
352   *                          DIT structure rule is associated.  It must not be
353   *                          {@code null}.
354   * @param  superiorRuleIDs  The superior rule IDs for this DIT structure rule.
355   *                          It may be {@code null} or empty if there are no
356   *                          superior rule IDs.
357   * @param  extensions       The set of extensions for this DIT structure rule.
358   *                          It may be {@code null} or empty if there are no
359   *                          extensions.
360   */
361  public DITStructureRuleDefinition(final int ruleID, final String[] names,
362                                    final String description,
363                                    final boolean isObsolete,
364                                    final String nameFormID,
365                                    final int[] superiorRuleIDs,
366                                    final Map<String,String[]> extensions)
367  {
368    ensureNotNull(nameFormID);
369
370    this.ruleID      = ruleID;
371    this.description = description;
372    this.isObsolete  = isObsolete;
373    this.nameFormID  = nameFormID;
374
375    if (names == null)
376    {
377      this.names = NO_STRINGS;
378    }
379    else
380    {
381      this.names = names;
382    }
383
384    if (superiorRuleIDs == null)
385    {
386      this.superiorRuleIDs = NO_INTS;
387    }
388    else
389    {
390      this.superiorRuleIDs = superiorRuleIDs;
391    }
392
393    if (extensions == null)
394    {
395      this.extensions = Collections.emptyMap();
396    }
397    else
398    {
399      this.extensions = Collections.unmodifiableMap(extensions);
400    }
401
402    final StringBuilder buffer = new StringBuilder();
403    createDefinitionString(buffer);
404    ditStructureRuleString = buffer.toString();
405  }
406
407
408
409  /**
410   * Constructs a string representation of this DIT content rule definition in
411   * the provided buffer.
412   *
413   * @param  buffer  The buffer in which to construct a string representation of
414   *                 this DIT content rule definition.
415   */
416  private void createDefinitionString(final StringBuilder buffer)
417  {
418    buffer.append("( ");
419    buffer.append(ruleID);
420
421    if (names.length == 1)
422    {
423      buffer.append(" NAME '");
424      buffer.append(names[0]);
425      buffer.append('\'');
426    }
427    else if (names.length > 1)
428    {
429      buffer.append(" NAME (");
430      for (final String name : names)
431      {
432        buffer.append(" '");
433        buffer.append(name);
434        buffer.append('\'');
435      }
436      buffer.append(" )");
437    }
438
439    if (description != null)
440    {
441      buffer.append(" DESC '");
442      encodeValue(description, buffer);
443      buffer.append('\'');
444    }
445
446    if (isObsolete)
447    {
448      buffer.append(" OBSOLETE");
449    }
450
451    buffer.append(" FORM ");
452    buffer.append(nameFormID);
453
454    if (superiorRuleIDs.length == 1)
455    {
456      buffer.append(" SUP ");
457      buffer.append(superiorRuleIDs[0]);
458    }
459    else if (superiorRuleIDs.length > 1)
460    {
461      buffer.append(" SUP (");
462      for (final int supID : superiorRuleIDs)
463      {
464        buffer.append(" $ ");
465        buffer.append(supID);
466      }
467      buffer.append(" )");
468    }
469
470    for (final Map.Entry<String,String[]> e : extensions.entrySet())
471    {
472      final String   name   = e.getKey();
473      final String[] values = e.getValue();
474      if (values.length == 1)
475      {
476        buffer.append(' ');
477        buffer.append(name);
478        buffer.append(" '");
479        encodeValue(values[0], buffer);
480        buffer.append('\'');
481      }
482      else
483      {
484        buffer.append(' ');
485        buffer.append(name);
486        buffer.append(" (");
487        for (final String value : values)
488        {
489          buffer.append(" '");
490          encodeValue(value, buffer);
491          buffer.append('\'');
492        }
493        buffer.append(" )");
494      }
495    }
496
497    buffer.append(" )");
498  }
499
500
501
502  /**
503   * Retrieves the rule ID for this DIT structure rule.
504   *
505   * @return  The rule ID for this DIT structure rule.
506   */
507  public int getRuleID()
508  {
509    return ruleID;
510  }
511
512
513
514  /**
515   * Retrieves the set of names for this DIT structure rule.
516   *
517   * @return  The set of names for this DIT structure rule, or an empty array if
518   *          it does not have any names.
519   */
520  public String[] getNames()
521  {
522    return names;
523  }
524
525
526
527  /**
528   * Retrieves the primary name that can be used to reference this DIT structure
529   * rule.  If one or more names are defined, then the first name will be used.
530   * Otherwise, the string representation of the rule ID will be returned.
531   *
532   * @return  The primary name that can be used to reference this DIT structure
533   *          rule.
534   */
535  public String getNameOrRuleID()
536  {
537    if (names.length == 0)
538    {
539      return String.valueOf(ruleID);
540    }
541    else
542    {
543      return names[0];
544    }
545  }
546
547
548
549  /**
550   * Indicates whether the provided string matches the rule ID or any of the
551   * names for this DIT structure rule.
552   *
553   * @param  s  The string for which to make the determination.  It must not be
554   *            {@code null}.
555   *
556   * @return  {@code true} if the provided string matches the rule ID or any of
557   *          the names for this DIT structure rule, or {@code false} if not.
558   */
559  public boolean hasNameOrRuleID(final String s)
560  {
561    for (final String name : names)
562    {
563      if (s.equalsIgnoreCase(name))
564      {
565        return true;
566      }
567    }
568
569    return s.equalsIgnoreCase(String.valueOf(ruleID));
570  }
571
572
573
574  /**
575   * Retrieves the description for this DIT structure rule, if available.
576   *
577   * @return  The description for this DIT structure rule, or {@code null} if
578   *          there is no description defined.
579   */
580  public String getDescription()
581  {
582    return description;
583  }
584
585
586
587  /**
588   * Indicates whether this DIT structure rule is declared obsolete.
589   *
590   * @return  {@code true} if this DIT structure rule is declared obsolete, or
591   *          {@code false} if it is not.
592   */
593  public boolean isObsolete()
594  {
595    return isObsolete;
596  }
597
598
599
600  /**
601   * Retrieves the name or OID of the name form with which this DIT structure
602   * rule is associated.
603   *
604   * @return  The name or OID of the name form with which this DIT structure
605   *          rule is associated.
606   */
607  public String getNameFormID()
608  {
609    return nameFormID;
610  }
611
612
613
614  /**
615   * Retrieves the rule IDs of the superior rules for this DIT structure rule.
616   *
617   * @return  The rule IDs of the superior rules for this DIT structure rule, or
618   *          an empty array if there are no superior rule IDs.
619   */
620  public int[] getSuperiorRuleIDs()
621  {
622    return superiorRuleIDs;
623  }
624
625
626
627  /**
628   * Retrieves the set of extensions for this DIT structure rule.  They will be
629   * mapped from the extension name (which should start with "X-") to the set of
630   * values for that extension.
631   *
632   * @return  The set of extensions for this DIT structure rule.
633   */
634  public Map<String,String[]> getExtensions()
635  {
636    return extensions;
637  }
638
639
640
641  /**
642   * {@inheritDoc}
643   */
644  @Override()
645  public int hashCode()
646  {
647    return ruleID;
648  }
649
650
651
652  /**
653   * {@inheritDoc}
654   */
655  @Override()
656  public boolean equals(final Object o)
657  {
658    if (o == null)
659    {
660      return false;
661    }
662
663    if (o == this)
664    {
665      return true;
666    }
667
668    if (! (o instanceof DITStructureRuleDefinition))
669    {
670      return false;
671    }
672
673    final DITStructureRuleDefinition d = (DITStructureRuleDefinition) o;
674    if ((ruleID == d.ruleID) &&
675         nameFormID.equalsIgnoreCase(d.nameFormID) &&
676         stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
677         (isObsolete == d.isObsolete) &&
678         extensionsEqual(extensions, d.extensions))
679    {
680      if (superiorRuleIDs.length != d.superiorRuleIDs.length)
681      {
682        return false;
683      }
684
685      final HashSet<Integer> s1 = new HashSet<Integer>(superiorRuleIDs.length);
686      final HashSet<Integer> s2 = new HashSet<Integer>(superiorRuleIDs.length);
687      for (final int i : superiorRuleIDs)
688      {
689        s1.add(i);
690      }
691
692      for (final int i : d.superiorRuleIDs)
693      {
694        s2.add(i);
695      }
696
697      return s1.equals(s2);
698    }
699    else
700    {
701      return false;
702    }
703  }
704
705
706
707  /**
708   * Retrieves a string representation of this DIT structure rule definition, in
709   * the format described in RFC 4512 section 4.1.7.1.
710   *
711   * @return  A string representation of this DIT structure rule definition.
712   */
713  @Override()
714  public String toString()
715  {
716    return ditStructureRuleString;
717  }
718}