001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 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.Map;
028    import java.util.LinkedHashMap;
029    
030    import com.unboundid.ldap.sdk.LDAPException;
031    import com.unboundid.ldap.sdk.ResultCode;
032    
033    import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
034    import static com.unboundid.util.StaticUtils.*;
035    import static com.unboundid.util.Validator.*;
036    
037    
038    
039    /**
040     * This class provides a data structure that describes an LDAP DIT content rule
041     * schema element.
042     */
043    public final class DITContentRuleDefinition
044           extends SchemaElement
045    {
046      /**
047       * The serial version UID for this serializable class.
048       */
049      private static final long serialVersionUID = 3224440505307817586L;
050    
051    
052    
053      // Indicates whether this DIT content rule is declared obsolete.
054      private final boolean isObsolete;
055    
056      // The set of extensions for this DIT content rule.
057      private final Map<String,String[]> extensions;
058    
059      // The description for this DIT content rule.
060      private final String description;
061    
062      // The string representation of this DIT content rule.
063      private final String ditContentRuleString;
064    
065      // The OID of the structural object class with which this DIT content rule is
066      // associated.
067      private final String oid;
068    
069      // The names/OIDs of the allowed auxiliary classes.
070      private final String[] auxiliaryClasses;
071    
072      // The set of names for this DIT content rule.
073      private final String[] names;
074    
075      // The names/OIDs of the optional attributes.
076      private final String[] optionalAttributes;
077    
078      // The names/OIDs of the prohibited attributes.
079      private final String[] prohibitedAttributes;
080    
081      // The names/OIDs of the required attributes.
082      private final String[] requiredAttributes;
083    
084    
085    
086      /**
087       * Creates a new DIT content rule from the provided string representation.
088       *
089       * @param  s  The string representation of the DIT content rule to create,
090       *            using the syntax described in RFC 4512 section 4.1.6.  It must
091       *            not be {@code null}.
092       *
093       * @throws  LDAPException  If the provided string cannot be decoded as a DIT
094       *                         content rule definition.
095       */
096      public DITContentRuleDefinition(final String s)
097             throws LDAPException
098      {
099        ensureNotNull(s);
100    
101        ditContentRuleString = s.trim();
102    
103        // The first character must be an opening parenthesis.
104        final int length = ditContentRuleString.length();
105        if (length == 0)
106        {
107          throw new LDAPException(ResultCode.DECODING_ERROR,
108                                  ERR_DCR_DECODE_EMPTY.get());
109        }
110        else if (ditContentRuleString.charAt(0) != '(')
111        {
112          throw new LDAPException(ResultCode.DECODING_ERROR,
113                                  ERR_DCR_DECODE_NO_OPENING_PAREN.get(
114                                       ditContentRuleString));
115        }
116    
117    
118        // Skip over any spaces until we reach the start of the OID, then read the
119        // OID until we find the next space.
120        int pos = skipSpaces(ditContentRuleString, 1, length);
121    
122        StringBuilder buffer = new StringBuilder();
123        pos = readOID(ditContentRuleString, pos, length, buffer);
124        oid = buffer.toString();
125    
126    
127        // Technically, DIT content elements are supposed to appear in a specific
128        // order, but we'll be lenient and allow remaining elements to come in any
129        // order.
130        final ArrayList<String>    nameList = new ArrayList<String>(1);
131        final ArrayList<String>    reqAttrs = new ArrayList<String>();
132        final ArrayList<String>    optAttrs = new ArrayList<String>();
133        final ArrayList<String>    notAttrs = new ArrayList<String>();
134        final ArrayList<String>    auxOCs   = new ArrayList<String>();
135        final Map<String,String[]> exts     = new LinkedHashMap<String,String[]>();
136        Boolean obsolete = null;
137        String  descr    = null;
138    
139        while (true)
140        {
141          // Skip over any spaces until we find the next element.
142          pos = skipSpaces(ditContentRuleString, pos, length);
143    
144          // Read until we find the next space or the end of the string.  Use that
145          // token to figure out what to do next.
146          final int tokenStartPos = pos;
147          while ((pos < length) && (ditContentRuleString.charAt(pos) != ' '))
148          {
149            pos++;
150          }
151    
152          // It's possible that the token could be smashed right up against the
153          // closing parenthesis.  If that's the case, then extract just the token
154          // and handle the closing parenthesis the next time through.
155          String token = ditContentRuleString.substring(tokenStartPos, pos);
156          if ((token.length() > 1) && (token.endsWith(")")))
157          {
158            token = token.substring(0, token.length() - 1);
159            pos--;
160          }
161    
162          final String lowerToken = toLowerCase(token);
163          if (lowerToken.equals(")"))
164          {
165            // This indicates that we're at the end of the value.  There should not
166            // be any more closing characters.
167            if (pos < length)
168            {
169              throw new LDAPException(ResultCode.DECODING_ERROR,
170                                      ERR_DCR_DECODE_CLOSE_NOT_AT_END.get(
171                                           ditContentRuleString));
172            }
173            break;
174          }
175          else if (lowerToken.equals("name"))
176          {
177            if (nameList.isEmpty())
178            {
179              pos = skipSpaces(ditContentRuleString, pos, length);
180              pos = readQDStrings(ditContentRuleString, pos, length, nameList);
181            }
182            else
183            {
184              throw new LDAPException(ResultCode.DECODING_ERROR,
185                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
186                                           ditContentRuleString, "NAME"));
187            }
188          }
189          else if (lowerToken.equals("desc"))
190          {
191            if (descr == null)
192            {
193              pos = skipSpaces(ditContentRuleString, pos, length);
194    
195              buffer = new StringBuilder();
196              pos = readQDString(ditContentRuleString, pos, length, buffer);
197              descr = buffer.toString();
198            }
199            else
200            {
201              throw new LDAPException(ResultCode.DECODING_ERROR,
202                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
203                                           ditContentRuleString, "DESC"));
204            }
205          }
206          else if (lowerToken.equals("obsolete"))
207          {
208            if (obsolete == null)
209            {
210              obsolete = true;
211            }
212            else
213            {
214              throw new LDAPException(ResultCode.DECODING_ERROR,
215                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
216                                           ditContentRuleString, "OBSOLETE"));
217            }
218          }
219          else if (lowerToken.equals("aux"))
220          {
221            if (auxOCs.isEmpty())
222            {
223              pos = skipSpaces(ditContentRuleString, pos, length);
224              pos = readOIDs(ditContentRuleString, pos, length, auxOCs);
225            }
226            else
227            {
228              throw new LDAPException(ResultCode.DECODING_ERROR,
229                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
230                                           ditContentRuleString, "AUX"));
231            }
232          }
233          else if (lowerToken.equals("must"))
234          {
235            if (reqAttrs.isEmpty())
236            {
237              pos = skipSpaces(ditContentRuleString, pos, length);
238              pos = readOIDs(ditContentRuleString, pos, length, reqAttrs);
239            }
240            else
241            {
242              throw new LDAPException(ResultCode.DECODING_ERROR,
243                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
244                                           ditContentRuleString, "MUST"));
245            }
246          }
247          else if (lowerToken.equals("may"))
248          {
249            if (optAttrs.isEmpty())
250            {
251              pos = skipSpaces(ditContentRuleString, pos, length);
252              pos = readOIDs(ditContentRuleString, pos, length, optAttrs);
253            }
254            else
255            {
256              throw new LDAPException(ResultCode.DECODING_ERROR,
257                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
258                                           ditContentRuleString, "MAY"));
259            }
260          }
261          else if (lowerToken.equals("not"))
262          {
263            if (notAttrs.isEmpty())
264            {
265              pos = skipSpaces(ditContentRuleString, pos, length);
266              pos = readOIDs(ditContentRuleString, pos, length, notAttrs);
267            }
268            else
269            {
270              throw new LDAPException(ResultCode.DECODING_ERROR,
271                                      ERR_DCR_DECODE_MULTIPLE_ELEMENTS.get(
272                                           ditContentRuleString, "NOT"));
273            }
274          }
275          else if (lowerToken.startsWith("x-"))
276          {
277            pos = skipSpaces(ditContentRuleString, pos, length);
278    
279            final ArrayList<String> valueList = new ArrayList<String>();
280            pos = readQDStrings(ditContentRuleString, pos, length, valueList);
281    
282            final String[] values = new String[valueList.size()];
283            valueList.toArray(values);
284    
285            if (exts.containsKey(token))
286            {
287              throw new LDAPException(ResultCode.DECODING_ERROR,
288                                      ERR_DCR_DECODE_DUP_EXT.get(
289                                           ditContentRuleString, token));
290            }
291    
292            exts.put(token, values);
293          }
294          else
295          {
296            throw new LDAPException(ResultCode.DECODING_ERROR,
297                                    ERR_DCR_DECODE_DUP_EXT.get(
298                                         ditContentRuleString, token));
299          }
300        }
301    
302        description = descr;
303    
304        names = new String[nameList.size()];
305        nameList.toArray(names);
306    
307        auxiliaryClasses = new String[auxOCs.size()];
308        auxOCs.toArray(auxiliaryClasses);
309    
310        requiredAttributes = new String[reqAttrs.size()];
311        reqAttrs.toArray(requiredAttributes);
312    
313        optionalAttributes = new String[optAttrs.size()];
314        optAttrs.toArray(optionalAttributes);
315    
316        prohibitedAttributes = new String[notAttrs.size()];
317        notAttrs.toArray(prohibitedAttributes);
318    
319        isObsolete = (obsolete != null);
320    
321        extensions = Collections.unmodifiableMap(exts);
322      }
323    
324    
325    
326      /**
327       * Creates a new DIT content rule with the provided information.
328       *
329       * @param  oid                   The OID for the structural object class with
330       *                               which this DIT content rule is associated.
331       *                               It must not be {@code null}.
332       * @param  names                 The set of names for this DIT content rule.
333       *                               It may be {@code null} or empty if the DIT
334       *                               content rule should only be referenced by
335       *                               OID.
336       * @param  description           The description for this DIT content rule.
337       *                               It may be {@code null} if there is no
338       *                               description.
339       * @param  isObsolete            Indicates whether this DIT content rule is
340       *                               declared obsolete.
341       * @param  auxiliaryClasses      The names/OIDs of the auxiliary object
342       *                               classes that may be present in entries
343       *                               containing this DIT content rule.
344       * @param  requiredAttributes    The names/OIDs of the attributes which must
345       *                               be present in entries containing this DIT
346       *                               content rule.
347       * @param  optionalAttributes    The names/OIDs of the attributes which may be
348       *                               present in entries containing this DIT
349       *                               content rule.
350       * @param  prohibitedAttributes  The names/OIDs of the attributes which may
351       *                               not be present in entries containing this DIT
352       *                               content rule.
353       * @param  extensions            The set of extensions for this DIT content
354       *                               rule.  It may be {@code null} or empty if
355       *                               there should not be any extensions.
356       */
357      public DITContentRuleDefinition(final String oid, final String[] names,
358                                      final String description,
359                                      final boolean isObsolete,
360                                      final String[] auxiliaryClasses,
361                                      final String[] requiredAttributes,
362                                      final String[] optionalAttributes,
363                                      final String[] prohibitedAttributes,
364                                      final Map<String,String[]> extensions)
365      {
366        ensureNotNull(oid);
367    
368        this.oid             = oid;
369        this.isObsolete      = isObsolete;
370        this.description     = description;
371    
372        if (names == null)
373        {
374          this.names = NO_STRINGS;
375        }
376        else
377        {
378          this.names = names;
379        }
380    
381        if (auxiliaryClasses == null)
382        {
383          this.auxiliaryClasses = NO_STRINGS;
384        }
385        else
386        {
387          this.auxiliaryClasses  = auxiliaryClasses;
388        }
389    
390        if (requiredAttributes == null)
391        {
392          this.requiredAttributes = NO_STRINGS;
393        }
394        else
395        {
396          this.requiredAttributes = requiredAttributes;
397        }
398    
399        if (optionalAttributes == null)
400        {
401          this.optionalAttributes = NO_STRINGS;
402        }
403        else
404        {
405          this.optionalAttributes = optionalAttributes;
406        }
407    
408        if (prohibitedAttributes == null)
409        {
410          this.prohibitedAttributes = NO_STRINGS;
411        }
412        else
413        {
414          this.prohibitedAttributes = prohibitedAttributes;
415        }
416    
417        if (extensions == null)
418        {
419          this.extensions = Collections.emptyMap();
420        }
421        else
422        {
423          this.extensions = Collections.unmodifiableMap(extensions);
424        }
425    
426        final StringBuilder buffer = new StringBuilder();
427        createDefinitionString(buffer);
428        ditContentRuleString = buffer.toString();
429      }
430    
431    
432    
433      /**
434       * Constructs a string representation of this DIT content rule definition in
435       * the provided buffer.
436       *
437       * @param  buffer  The buffer in which to construct a string representation of
438       *                 this DIT content rule definition.
439       */
440      private void createDefinitionString(final StringBuilder buffer)
441      {
442        buffer.append("( ");
443        buffer.append(oid);
444    
445        if (names.length == 1)
446        {
447          buffer.append(" NAME '");
448          buffer.append(names[0]);
449          buffer.append('\'');
450        }
451        else if (names.length > 1)
452        {
453          buffer.append(" NAME (");
454          for (final String name : names)
455          {
456            buffer.append(" '");
457            buffer.append(name);
458            buffer.append('\'');
459          }
460          buffer.append(" )");
461        }
462    
463        if (description != null)
464        {
465          buffer.append(" DESC '");
466          encodeValue(description, buffer);
467          buffer.append('\'');
468        }
469    
470        if (isObsolete)
471        {
472          buffer.append(" OBSOLETE");
473        }
474    
475        if (auxiliaryClasses.length == 1)
476        {
477          buffer.append(" AUX ");
478          buffer.append(auxiliaryClasses[0]);
479        }
480        else if (auxiliaryClasses.length > 1)
481        {
482          buffer.append(" AUX (");
483          for (int i=0; i < auxiliaryClasses.length; i++)
484          {
485            if (i >0)
486            {
487              buffer.append(" $ ");
488            }
489            else
490            {
491              buffer.append(' ');
492            }
493            buffer.append(auxiliaryClasses[i]);
494          }
495          buffer.append(" )");
496        }
497    
498        if (requiredAttributes.length == 1)
499        {
500          buffer.append(" MUST ");
501          buffer.append(requiredAttributes[0]);
502        }
503        else if (requiredAttributes.length > 1)
504        {
505          buffer.append(" MUST (");
506          for (int i=0; i < requiredAttributes.length; i++)
507          {
508            if (i >0)
509            {
510              buffer.append(" $ ");
511            }
512            else
513            {
514              buffer.append(' ');
515            }
516            buffer.append(requiredAttributes[i]);
517          }
518          buffer.append(" )");
519        }
520    
521        if (optionalAttributes.length == 1)
522        {
523          buffer.append(" MAY ");
524          buffer.append(optionalAttributes[0]);
525        }
526        else if (optionalAttributes.length > 1)
527        {
528          buffer.append(" MAY (");
529          for (int i=0; i < optionalAttributes.length; i++)
530          {
531            if (i > 0)
532            {
533              buffer.append(" $ ");
534            }
535            else
536            {
537              buffer.append(' ');
538            }
539            buffer.append(optionalAttributes[i]);
540          }
541          buffer.append(" )");
542        }
543    
544        if (prohibitedAttributes.length == 1)
545        {
546          buffer.append(" NOT ");
547          buffer.append(prohibitedAttributes[0]);
548        }
549        else if (prohibitedAttributes.length > 1)
550        {
551          buffer.append(" NOT (");
552          for (int i=0; i < prohibitedAttributes.length; i++)
553          {
554            if (i > 0)
555            {
556              buffer.append(" $ ");
557            }
558            else
559            {
560              buffer.append(' ');
561            }
562            buffer.append(prohibitedAttributes[i]);
563          }
564          buffer.append(" )");
565        }
566    
567        for (final Map.Entry<String,String[]> e : extensions.entrySet())
568        {
569          final String   name   = e.getKey();
570          final String[] values = e.getValue();
571          if (values.length == 1)
572          {
573            buffer.append(' ');
574            buffer.append(name);
575            buffer.append(" '");
576            encodeValue(values[0], buffer);
577            buffer.append('\'');
578          }
579          else
580          {
581            buffer.append(' ');
582            buffer.append(name);
583            buffer.append(" (");
584            for (final String value : values)
585            {
586              buffer.append(" '");
587              encodeValue(value, buffer);
588              buffer.append('\'');
589            }
590            buffer.append(" )");
591          }
592        }
593    
594        buffer.append(" )");
595      }
596    
597    
598    
599      /**
600       * Retrieves the OID for the structural object class associated with this
601       * DIT content rule.
602       *
603       * @return  The OID for the structural object class associated with this DIT
604       *          content rule.
605       */
606      public String getOID()
607      {
608        return oid;
609      }
610    
611    
612    
613      /**
614       * Retrieves the set of names for this DIT content rule.
615       *
616       * @return  The set of names for this DIT content rule, or an empty array if
617       *          it does not have any names.
618       */
619      public String[] getNames()
620      {
621        return names;
622      }
623    
624    
625    
626      /**
627       * Retrieves the primary name that can be used to reference this DIT content
628       * rule.  If one or more names are defined, then the first name will be used.
629       * Otherwise, the structural object class OID will be returned.
630       *
631       * @return  The primary name that can be used to reference this DIT content
632       *          rule.
633       */
634      public String getNameOrOID()
635      {
636        if (names.length == 0)
637        {
638          return oid;
639        }
640        else
641        {
642          return names[0];
643        }
644      }
645    
646    
647    
648      /**
649       * Indicates whether the provided string matches the OID or any of the names
650       * for this DIT content rule.
651       *
652       * @param  s  The string for which to make the determination.  It must not be
653       *            {@code null}.
654       *
655       * @return  {@code true} if the provided string matches the OID or any of the
656       *          names for this DIT content rule, or {@code false} if not.
657       */
658      public boolean hasNameOrOID(final String s)
659      {
660        for (final String name : names)
661        {
662          if (s.equalsIgnoreCase(name))
663          {
664            return true;
665          }
666        }
667    
668        return s.equalsIgnoreCase(oid);
669      }
670    
671    
672    
673      /**
674       * Retrieves the description for this DIT content rule, if available.
675       *
676       * @return  The description for this DIT content rule, or {@code null} if
677       *          there is no description defined.
678       */
679      public String getDescription()
680      {
681        return description;
682      }
683    
684    
685    
686      /**
687       * Indicates whether this DIT content rule is declared obsolete.
688       *
689       * @return  {@code true} if this DIT content rule is declared obsolete, or
690       *          {@code false} if it is not.
691       */
692      public boolean isObsolete()
693      {
694        return isObsolete;
695      }
696    
697    
698    
699      /**
700       * Retrieves the names or OIDs of the auxiliary object classes that may be
701       * present in entries containing the structural class for this DIT content
702       * rule.
703       *
704       * @return  The names or OIDs of the auxiliary object classes that may be
705       *          present in entries containing the structural class for this DIT
706       *          content rule.
707       */
708      public String[] getAuxiliaryClasses()
709      {
710        return auxiliaryClasses;
711      }
712    
713    
714    
715      /**
716       * Retrieves the names or OIDs of the attributes that are required to be
717       * present in entries containing the structural object class for this DIT
718       * content rule.
719       *
720       * @return  The names or OIDs of the attributes that are required to be
721       *          present in entries containing the structural object class for this
722       *          DIT content rule, or an empty array if there are no required
723       *          attributes.
724       */
725      public String[] getRequiredAttributes()
726      {
727        return requiredAttributes;
728      }
729    
730    
731    
732      /**
733       * Retrieves the names or OIDs of the attributes that are optionally allowed
734       * to be present in entries containing the structural object class for this
735       * DIT content rule.
736       *
737       * @return  The names or OIDs of the attributes that are optionally allowed to
738       *          be present in entries containing the structural object class for
739       *          this DIT content rule, or an empty array if there are no required
740       *          attributes.
741       */
742      public String[] getOptionalAttributes()
743      {
744        return optionalAttributes;
745      }
746    
747    
748    
749      /**
750       * Retrieves the names or OIDs of the attributes that are not allowed to be
751       * present in entries containing the structural object class for this DIT
752       * content rule.
753       *
754       * @return  The names or OIDs of the attributes that are not allowed to be
755       *          present in entries containing the structural object class for this
756       *          DIT content rule, or an empty array if there are no required
757       *          attributes.
758       */
759      public String[] getProhibitedAttributes()
760      {
761        return prohibitedAttributes;
762      }
763    
764    
765    
766      /**
767       * Retrieves the set of extensions for this DIT content rule.  They will be
768       * mapped from the extension name (which should start with "X-") to the set of
769       * values for that extension.
770       *
771       * @return  The set of extensions for this DIT content rule.
772       */
773      public Map<String,String[]> getExtensions()
774      {
775        return extensions;
776      }
777    
778    
779    
780      /**
781       * {@inheritDoc}
782       */
783      @Override()
784      public int hashCode()
785      {
786        return oid.hashCode();
787      }
788    
789    
790    
791      /**
792       * {@inheritDoc}
793       */
794      @Override()
795      public boolean equals(final Object o)
796      {
797        if (o == null)
798        {
799          return false;
800        }
801    
802        if (o == this)
803        {
804          return true;
805        }
806    
807        if (! (o instanceof DITContentRuleDefinition))
808        {
809          return false;
810        }
811    
812        final DITContentRuleDefinition d = (DITContentRuleDefinition) o;
813        return (oid.equals(d.oid) &&
814             stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
815             stringsEqualIgnoreCaseOrderIndependent(auxiliaryClasses,
816                  d.auxiliaryClasses) &&
817             stringsEqualIgnoreCaseOrderIndependent(requiredAttributes,
818                  d.requiredAttributes) &&
819             stringsEqualIgnoreCaseOrderIndependent(optionalAttributes,
820                  d.optionalAttributes) &&
821             stringsEqualIgnoreCaseOrderIndependent(prohibitedAttributes,
822                  d.prohibitedAttributes) &&
823             bothNullOrEqualIgnoreCase(description, d.description) &&
824             (isObsolete == d.isObsolete) &&
825             extensionsEqual(extensions, d.extensions));
826      }
827    
828    
829    
830      /**
831       * Retrieves a string representation of this DIT content rule definition, in
832       * the format described in RFC 4512 section 4.1.6.
833       *
834       * @return  A string representation of this DIT content rule definition.
835       */
836      @Override()
837      public String toString()
838      {
839        return ditContentRuleString;
840      }
841    }