001    /*
002     * Copyright 2007-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2015 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     */
021    package com.unboundid.ldap.sdk.schema;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.Map;
028    import java.util.LinkedHashMap;
029    
030    import com.unboundid.ldap.sdk.LDAPException;
031    import com.unboundid.ldap.sdk.ResultCode;
032    import com.unboundid.util.NotMutable;
033    import com.unboundid.util.ThreadSafety;
034    import com.unboundid.util.ThreadSafetyLevel;
035    
036    import static com.unboundid.ldap.sdk.schema.SchemaMessages.*;
037    import static com.unboundid.util.StaticUtils.*;
038    import 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)
048    public 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    }