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