001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 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  name         The names for this matching rule.  It may be
279       *                      {@code null} if the matching rule should only be
280       *                      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  syntaxOID    The syntax OID for this matching rule.  It must not be
284       *                      {@code null}.
285       * @param  extensions   The set of extensions for this matching rule.
286       *                      It may be {@code null} or empty if there should not be
287       *                      any extensions.
288       */
289      public MatchingRuleDefinition(final String oid, final String name,
290                                    final String description,
291                                    final String syntaxOID,
292                                    final Map<String,String[]> extensions)
293      {
294        this(oid, ((name == null) ? null : new String[] { name }), description,
295             false, syntaxOID, extensions);
296      }
297    
298    
299    
300      /**
301       * Creates a new matching rule with the provided information.
302       *
303       * @param  oid          The OID for this matching rule.  It must not be
304       *                      {@code null}.
305       * @param  names        The set of names for this matching rule.  It may be
306       *                      {@code null} or empty if the matching rule should only
307       *                      be referenced by OID.
308       * @param  description  The description for this matching rule.  It may be
309       *                      {@code null} if there is no description.
310       * @param  isObsolete   Indicates whether this matching rule is declared
311       *                      obsolete.
312       * @param  syntaxOID    The syntax OID for this matching rule.  It must not be
313       *                      {@code null}.
314       * @param  extensions   The set of extensions for this matching rule.
315       *                      It may be {@code null} or empty if there should not be
316       *                      any extensions.
317       */
318      public MatchingRuleDefinition(final String oid, final String[] names,
319                                    final String description,
320                                    final boolean isObsolete,
321                                    final String syntaxOID,
322                                    final Map<String,String[]> extensions)
323      {
324        ensureNotNull(oid, syntaxOID);
325    
326        this.oid                   = oid;
327        this.description           = description;
328        this.isObsolete            = isObsolete;
329        this.syntaxOID             = syntaxOID;
330    
331        if (names == null)
332        {
333          this.names = NO_STRINGS;
334        }
335        else
336        {
337          this.names = names;
338        }
339    
340        if (extensions == null)
341        {
342          this.extensions = Collections.emptyMap();
343        }
344        else
345        {
346          this.extensions = Collections.unmodifiableMap(extensions);
347        }
348    
349        final StringBuilder buffer = new StringBuilder();
350        createDefinitionString(buffer);
351        matchingRuleString = buffer.toString();
352      }
353    
354    
355    
356      /**
357       * Constructs a string representation of this matching rule definition in the
358       * provided buffer.
359       *
360       * @param  buffer  The buffer in which to construct a string representation of
361       *                 this matching rule definition.
362       */
363      private void createDefinitionString(final StringBuilder buffer)
364      {
365        buffer.append("( ");
366        buffer.append(oid);
367    
368        if (names.length == 1)
369        {
370          buffer.append(" NAME '");
371          buffer.append(names[0]);
372          buffer.append('\'');
373        }
374        else if (names.length > 1)
375        {
376          buffer.append(" NAME (");
377          for (final String name : names)
378          {
379            buffer.append(" '");
380            buffer.append(name);
381            buffer.append('\'');
382          }
383          buffer.append(" )");
384        }
385    
386        if (description != null)
387        {
388          buffer.append(" DESC '");
389          encodeValue(description, buffer);
390          buffer.append('\'');
391        }
392    
393        if (isObsolete)
394        {
395          buffer.append(" OBSOLETE");
396        }
397    
398        buffer.append(" SYNTAX ");
399        buffer.append(syntaxOID);
400    
401        for (final Map.Entry<String,String[]> e : extensions.entrySet())
402        {
403          final String   name   = e.getKey();
404          final String[] values = e.getValue();
405          if (values.length == 1)
406          {
407            buffer.append(' ');
408            buffer.append(name);
409            buffer.append(" '");
410            encodeValue(values[0], buffer);
411            buffer.append('\'');
412          }
413          else
414          {
415            buffer.append(' ');
416            buffer.append(name);
417            buffer.append(" (");
418            for (final String value : values)
419            {
420              buffer.append(" '");
421              encodeValue(value, buffer);
422              buffer.append('\'');
423            }
424            buffer.append(" )");
425          }
426        }
427    
428        buffer.append(" )");
429      }
430    
431    
432    
433      /**
434       * Retrieves the OID for this matching rule.
435       *
436       * @return  The OID for this matching rule.
437       */
438      public String getOID()
439      {
440        return oid;
441      }
442    
443    
444    
445      /**
446       * Retrieves the set of names for this matching rule.
447       *
448       * @return  The set of names for this matching rule, or an empty array if it
449       *          does not have any names.
450       */
451      public String[] getNames()
452      {
453        return names;
454      }
455    
456    
457    
458      /**
459       * Retrieves the primary name that can be used to reference this matching
460       * rule.  If one or more names are defined, then the first name will be used.
461       * Otherwise, the OID will be returned.
462       *
463       * @return  The primary name that can be used to reference this matching rule.
464       */
465      public String getNameOrOID()
466      {
467        if (names.length == 0)
468        {
469          return oid;
470        }
471        else
472        {
473          return names[0];
474        }
475      }
476    
477    
478    
479      /**
480       * Indicates whether the provided string matches the OID or any of the names
481       * for this matching rule.
482       *
483       * @param  s  The string for which to make the determination.  It must not be
484       *            {@code null}.
485       *
486       * @return  {@code true} if the provided string matches the OID or any of the
487       *          names for this matching rule, or {@code false} if not.
488       */
489      public boolean hasNameOrOID(final String s)
490      {
491        for (final String name : names)
492        {
493          if (s.equalsIgnoreCase(name))
494          {
495            return true;
496          }
497        }
498    
499        return s.equalsIgnoreCase(oid);
500      }
501    
502    
503    
504      /**
505       * Retrieves the description for this matching rule, if available.
506       *
507       * @return  The description for this matching rule, or {@code null} if there
508       *          is no description defined.
509       */
510      public String getDescription()
511      {
512        return description;
513      }
514    
515    
516    
517      /**
518       * Indicates whether this matching rule is declared obsolete.
519       *
520       * @return  {@code true} if this matching rule is declared obsolete, or
521       *          {@code false} if it is not.
522       */
523      public boolean isObsolete()
524      {
525        return isObsolete;
526      }
527    
528    
529    
530      /**
531       * Retrieves the OID of the syntax for this matching rule.
532       *
533       * @return  The OID of the syntax for this matching rule.
534       */
535      public String getSyntaxOID()
536      {
537        return syntaxOID;
538      }
539    
540    
541    
542      /**
543       * Retrieves the set of extensions for this matching rule.  They will be
544       * mapped from the extension name (which should start with "X-") to the set
545       * of values for that extension.
546       *
547       * @return  The set of extensions for this matching rule.
548       */
549      public Map<String,String[]> getExtensions()
550      {
551        return extensions;
552      }
553    
554    
555    
556      /**
557       * {@inheritDoc}
558       */
559      @Override()
560      public int hashCode()
561      {
562        return oid.hashCode();
563      }
564    
565    
566    
567      /**
568       * {@inheritDoc}
569       */
570      @Override()
571      public boolean equals(final Object o)
572      {
573        if (o == null)
574        {
575          return false;
576        }
577    
578        if (o == this)
579        {
580          return true;
581        }
582    
583        if (! (o instanceof MatchingRuleDefinition))
584        {
585          return false;
586        }
587    
588        final MatchingRuleDefinition d = (MatchingRuleDefinition) o;
589        return (oid.equals(d.oid) &&
590             syntaxOID.equals(d.syntaxOID) &&
591             stringsEqualIgnoreCaseOrderIndependent(names, d.names) &&
592             bothNullOrEqualIgnoreCase(description, d.description) &&
593             (isObsolete == d.isObsolete) &&
594             extensionsEqual(extensions, d.extensions));
595      }
596    
597    
598    
599      /**
600       * Retrieves a string representation of this matching rule definition, in the
601       * format described in RFC 4512 section 4.1.3.
602       *
603       * @return  A string representation of this matching rule definition.
604       */
605      @Override()
606      public String toString()
607      {
608        return matchingRuleString;
609      }
610    }