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