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