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