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