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