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