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