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