001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 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  names        The set of names for this matching rule.  It may be
279   *                      {@code null} or empty if the matching rule should only
280   *                      be 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  isObsolete   Indicates whether this matching rule is declared
284   *                      obsolete.
285   * @param  syntaxOID    The syntax OID for this matching rule.  It must not be
286   *                      {@code null}.
287   * @param  extensions   The set of extensions for this matching rule.
288   *                      It may be {@code null} or empty if there should not be
289   *                      any extensions.
290   */
291  public MatchingRuleDefinition(final String oid, final String[] names,
292                                final String description,
293                                final boolean isObsolete,
294                                final String syntaxOID,
295                                final Map<String,String[]> extensions)
296  {
297    ensureNotNull(oid, syntaxOID);
298
299    this.oid                   = oid;
300    this.description           = description;
301    this.isObsolete            = isObsolete;
302    this.syntaxOID             = syntaxOID;
303
304    if (names == null)
305    {
306      this.names = NO_STRINGS;
307    }
308    else
309    {
310      this.names = names;
311    }
312
313    if (extensions == null)
314    {
315      this.extensions = Collections.emptyMap();
316    }
317    else
318    {
319      this.extensions = Collections.unmodifiableMap(extensions);
320    }
321
322    final StringBuilder buffer = new StringBuilder();
323    createDefinitionString(buffer);
324    matchingRuleString = buffer.toString();
325  }
326
327
328
329  /**
330   * Constructs a string representation of this matching rule definition in the
331   * provided buffer.
332   *
333   * @param  buffer  The buffer in which to construct a string representation of
334   *                 this matching rule definition.
335   */
336  private void createDefinitionString(final StringBuilder buffer)
337  {
338    buffer.append("( ");
339    buffer.append(oid);
340
341    if (names.length == 1)
342    {
343      buffer.append(" NAME '");
344      buffer.append(names[0]);
345      buffer.append('\'');
346    }
347    else if (names.length > 1)
348    {
349      buffer.append(" NAME (");
350      for (final String name : names)
351      {
352        buffer.append(" '");
353        buffer.append(name);
354        buffer.append('\'');
355      }
356      buffer.append(" )");
357    }
358
359    if (description != null)
360    {
361      buffer.append(" DESC '");
362      encodeValue(description, buffer);
363      buffer.append('\'');
364    }
365
366    if (isObsolete)
367    {
368      buffer.append(" OBSOLETE");
369    }
370
371    buffer.append(" SYNTAX ");
372    buffer.append(syntaxOID);
373
374    for (final Map.Entry<String,String[]> e : extensions.entrySet())
375    {
376      final String   name   = e.getKey();
377      final String[] values = e.getValue();
378      if (values.length == 1)
379      {
380        buffer.append(' ');
381        buffer.append(name);
382        buffer.append(" '");
383        encodeValue(values[0], buffer);
384        buffer.append('\'');
385      }
386      else
387      {
388        buffer.append(' ');
389        buffer.append(name);
390        buffer.append(" (");
391        for (final String value : values)
392        {
393          buffer.append(" '");
394          encodeValue(value, buffer);
395          buffer.append('\'');
396        }
397        buffer.append(" )");
398      }
399    }
400
401    buffer.append(" )");
402  }
403
404
405
406  /**
407   * Retrieves the OID for this matching rule.
408   *
409   * @return  The OID for this matching rule.
410   */
411  public String getOID()
412  {
413    return oid;
414  }
415
416
417
418  /**
419   * Retrieves the set of names for this matching rule.
420   *
421   * @return  The set of names for this matching rule, or an empty array if it
422   *          does not have any names.
423   */
424  public String[] getNames()
425  {
426    return names;
427  }
428
429
430
431  /**
432   * Retrieves the primary name that can be used to reference this matching
433   * rule.  If one or more names are defined, then the first name will be used.
434   * Otherwise, the OID will be returned.
435   *
436   * @return  The primary name that can be used to reference this matching rule.
437   */
438  public String getNameOrOID()
439  {
440    if (names.length == 0)
441    {
442      return oid;
443    }
444    else
445    {
446      return names[0];
447    }
448  }
449
450
451
452  /**
453   * Indicates whether the provided string matches the OID or any of the names
454   * for this matching rule.
455   *
456   * @param  s  The string for which to make the determination.  It must not be
457   *            {@code null}.
458   *
459   * @return  {@code true} if the provided string matches the OID or any of the
460   *          names for this matching rule, or {@code false} if not.
461   */
462  public boolean hasNameOrOID(final String s)
463  {
464    for (final String name : names)
465    {
466      if (s.equalsIgnoreCase(name))
467      {
468        return true;
469      }
470    }
471
472    return s.equalsIgnoreCase(oid);
473  }
474
475
476
477  /**
478   * Retrieves the description for this matching rule, if available.
479   *
480   * @return  The description for this matching rule, or {@code null} if there
481   *          is no description defined.
482   */
483  public String getDescription()
484  {
485    return description;
486  }
487
488
489
490  /**
491   * Indicates whether this matching rule is declared obsolete.
492   *
493   * @return  {@code true} if this matching rule is declared obsolete, or
494   *          {@code false} if it is not.
495   */
496  public boolean isObsolete()
497  {
498    return isObsolete;
499  }
500
501
502
503  /**
504   * Retrieves the OID of the syntax for this matching rule.
505   *
506   * @return  The OID of the syntax for this matching rule.
507   */
508  public String getSyntaxOID()
509  {
510    return syntaxOID;
511  }
512
513
514
515  /**
516   * Retrieves the set of extensions for this matching rule.  They will be
517   * mapped from the extension name (which should start with "X-") to the set
518   * of values for that extension.
519   *
520   * @return  The set of extensions for this matching rule.
521   */
522  public Map<String,String[]> getExtensions()
523  {
524    return extensions;
525  }
526
527
528
529  /**
530   * {@inheritDoc}
531   */
532  @Override()
533  public int hashCode()
534  {
535    return oid.hashCode();
536  }
537
538
539
540  /**
541   * {@inheritDoc}
542   */
543  @Override()
544  public boolean equals(final Object o)
545  {
546    if (o == null)
547    {
548      return false;
549    }
550
551    if (o == this)
552    {
553      return true;
554    }
555
556    if (! (o instanceof MatchingRuleDefinition))
557    {
558      return false;
559    }
560
561    final MatchingRuleDefinition d = (MatchingRuleDefinition) o;
562    return (oid.equals(d.oid) &&
563         syntaxOID.equals(d.syntaxOID) &&
564         stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
565         bothNullOrEqualIgnoreCase(description, d.description) &&
566         (isObsolete == d.isObsolete) &&
567         extensionsEqual(extensions, d.extensions));
568  }
569
570
571
572  /**
573   * Retrieves a string representation of this matching rule definition, in the
574   * format described in RFC 4512 section 4.1.3.
575   *
576   * @return  A string representation of this matching rule definition.
577   */
578  @Override()
579  public String toString()
580  {
581    return matchingRuleString;
582  }
583}