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