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