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