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