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