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