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