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