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