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