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