001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.schema;
018import static org.opends.messages.SchemaMessages.*;
019import static org.opends.server.schema.SchemaConstants.*;
020import static org.opends.server.util.StaticUtils.*;
021
022import java.util.LinkedHashMap;
023import java.util.LinkedHashSet;
024import java.util.LinkedList;
025import java.util.List;
026
027import org.forgerock.i18n.LocalizableMessage;
028import org.forgerock.opendj.ldap.ByteSequence;
029import org.forgerock.opendj.ldap.ResultCode;
030import org.forgerock.opendj.ldap.schema.AttributeType;
031import org.forgerock.opendj.ldap.schema.MatchingRule;
032import org.forgerock.opendj.ldap.schema.Syntax;
033import org.opends.server.admin.std.server.AttributeSyntaxCfg;
034import org.opends.server.api.AttributeSyntax;
035import org.opends.server.core.DirectoryServer;
036import org.opends.server.types.DirectoryException;
037import org.opends.server.types.MatchingRuleUse;
038import org.opends.server.types.Schema;
039
040/**
041 * This class implements the matching rule use description syntax, which is used
042 * to hold matching rule use definitions in the server schema.  The format of
043 * this syntax is defined in RFC 2252.
044 */
045public class MatchingRuleUseSyntax
046       extends AttributeSyntax<AttributeSyntaxCfg>
047{
048
049  /**
050   * Creates a new instance of this syntax.  Note that the only thing that
051   * should be done here is to invoke the default constructor for the
052   * superclass.  All initialization should be performed in the
053   * <CODE>initializeSyntax</CODE> method.
054   */
055  public MatchingRuleUseSyntax()
056  {
057    super();
058  }
059
060  /** {@inheritDoc} */
061  @Override
062  public Syntax getSDKSyntax(org.forgerock.opendj.ldap.schema.Schema schema)
063  {
064    return schema.getSyntax(SchemaConstants.SYNTAX_MATCHING_RULE_USE_OID);
065  }
066
067  /** {@inheritDoc} */
068  @Override
069  public String getName()
070  {
071    return SYNTAX_MATCHING_RULE_USE_NAME;
072  }
073
074  /** {@inheritDoc} */
075  @Override
076  public String getOID()
077  {
078    return SYNTAX_MATCHING_RULE_USE_OID;
079  }
080
081  /** {@inheritDoc} */
082  @Override
083  public String getDescription()
084  {
085    return SYNTAX_MATCHING_RULE_USE_DESCRIPTION;
086  }
087
088  /**
089   * Decodes the contents of the provided ASN.1 octet string as a matching rule
090   * use definition according to the rules of this syntax.  Note that the
091   * provided octet string value does not need to be normalized (and in fact, it
092   * should not be in order to allow the desired capitalization to be
093   * preserved).
094   *
095   * @param  value                 The ASN.1 octet string containing the value
096   *                               to decode (it does not need to be
097   *                               normalized).
098   * @param  schema                The schema to use to resolve references to
099   *                               other schema elements.
100   * @param  allowUnknownElements  Indicates whether to allow values that
101   *                               reference a name form and/or superior rules
102   *                               which are not defined in the server schema.
103   *                               This should only be true when called by
104   *                               {@code valueIsAcceptable}.
105   *
106   * @return  The decoded matching rule use definition.
107   *
108   * @throws  DirectoryException  If the provided value cannot be decoded as a
109   *                              matching rule use definition.
110   */
111  public static MatchingRuleUse decodeMatchingRuleUse(ByteSequence value,
112                                     Schema schema,
113                                     boolean allowUnknownElements)
114         throws DirectoryException
115  {
116    // Get string representations of the provided value using the provided form
117    // and with all lowercase characters.
118    String valueStr = value.toString();
119    String lowerStr = toLowerCase(valueStr);
120
121
122    // We'll do this a character at a time.  First, skip over any leading
123    // whitespace.
124    int pos    = 0;
125    int length = valueStr.length();
126    while (pos < length && valueStr.charAt(pos) == ' ')
127    {
128      pos++;
129    }
130
131    if (pos >= length)
132    {
133      // This means that the value was empty or contained only whitespace.  That
134      // is illegal.
135      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE.get();
136      throw new DirectoryException(
137              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
138    }
139
140
141    // The next character must be an open parenthesis.  If it is not, then that
142    // is an error.
143    char c = valueStr.charAt(pos++);
144    if (c != '(')
145    {
146      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS.get(valueStr, pos-1, c);
147      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
148    }
149
150
151    // Skip over any spaces immediately following the opening parenthesis.
152    while (pos < length && ((c = valueStr.charAt(pos)) == ' '))
153    {
154      pos++;
155    }
156
157    if (pos >= length)
158    {
159      // This means that the end of the value was reached before we could find
160      // the OID.  Ths is illegal.
161      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
162      throw new DirectoryException(
163              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
164    }
165
166
167    // The next set of characters must be the OID.  Strictly speaking, this
168    // should only be a numeric OID, but we'll also allow for the
169    // "ocname-oid" case as well.  Look at the first character to figure out
170    // which we will be using.
171    int oidStartPos = pos;
172    if (isDigit(c))
173    {
174      // This must be a numeric OID.  In that case, we will accept only digits
175      // and periods, but not consecutive periods.
176      boolean lastWasPeriod = false;
177      while (pos < length && ((c = valueStr.charAt(pos++)) != ' '))
178      {
179        if (c == '.')
180        {
181          if (lastWasPeriod)
182          {
183            LocalizableMessage message =
184                ERR_ATTR_SYNTAX_MRUSE_DOUBLE_PERIOD_IN_NUMERIC_OID.get(valueStr, pos-1);
185            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
186          }
187          lastWasPeriod = true;
188        }
189        else if (! isDigit(c))
190        {
191          // This must have been an illegal character.
192          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR_IN_NUMERIC_OID.get(valueStr, c, pos-1);
193          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
194        }
195        else
196        {
197          lastWasPeriod = false;
198        }
199      }
200    }
201    else
202    {
203      // This must be a "fake" OID.  In this case, we will only accept
204      // alphabetic characters, numeric digits, and the hyphen.
205      while (pos < length && ((c = valueStr.charAt(pos++)) != ' '))
206      {
207        if (isAlpha(c) || isDigit(c) || c == '-' ||
208            (c == '_' && DirectoryServer.allowAttributeNameExceptions()))
209        {
210          // This is fine.  It is an acceptable character.
211        }
212        else
213        {
214          // This must have been an illegal character.
215          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR_IN_STRING_OID.
216              get(valueStr, c, pos-1);
217          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
218        }
219      }
220    }
221
222
223    // If we're at the end of the value, then it isn't a valid matching rule use
224    // description.  Otherwise, parse out the OID.
225    if (pos >= length)
226    {
227      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
228      throw new DirectoryException(
229              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
230    }
231
232    String oid = lowerStr.substring(oidStartPos, pos-1);
233
234    // Get the matching rule with the specified OID.
235    MatchingRule matchingRule = schema.getMatchingRule(oid);
236    if (matchingRule == null)
237    {
238      // This is bad because the matching rule use is associated with a matching
239      // rule that we don't know anything about.
240      LocalizableMessage message =
241          ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_MATCHING_RULE.get(valueStr, oid);
242      throw new DirectoryException(
243              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
244    }
245
246
247    // Skip over the space(s) after the OID.
248    while (pos < length && ((c = valueStr.charAt(pos)) == ' '))
249    {
250      pos++;
251    }
252
253    if (pos >= length)
254    {
255      // This means that the end of the value was reached before we could find
256      // the OID.  Ths is illegal.
257      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
258      throw new DirectoryException(
259              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
260    }
261
262
263    // At this point, we should have a pretty specific syntax that describes
264    // what may come next, but some of the components are optional and it would
265    // be pretty easy to put something in the wrong order, so we will be very
266    // flexible about what we can accept.  Just look at the next token, figure
267    // out what it is and how to treat what comes after it, then repeat until
268    // we get to the end of the value.  But before we start, set default values
269    // for everything else we might need to know.
270    LinkedHashMap<String,String> names = new LinkedHashMap<>();
271    String description = null;
272    boolean isObsolete = false;
273    LinkedHashSet<AttributeType> attributes = null;
274    LinkedHashMap<String,List<String>> extraProperties = new LinkedHashMap<>();
275
276    while (true)
277    {
278      StringBuilder tokenNameBuffer = new StringBuilder();
279      pos = readTokenName(valueStr, tokenNameBuffer, pos);
280      String tokenName = tokenNameBuffer.toString();
281      String lowerTokenName = toLowerCase(tokenName);
282      if (tokenName.equals(")"))
283      {
284        // We must be at the end of the value.  If not, then that's a problem.
285        if (pos < length)
286        {
287          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_UNEXPECTED_CLOSE_PARENTHESIS.
288              get(valueStr, pos-1);
289          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
290        }
291
292        break;
293      }
294      else if (lowerTokenName.equals("name"))
295      {
296        // This specifies the set of names for the matching rule use.  It may be
297        // a single name in single quotes, or it may be an open parenthesis
298        // followed by one or more names in single quotes separated by spaces.
299        c = valueStr.charAt(pos++);
300        if (c == '\'')
301        {
302          StringBuilder userBuffer  = new StringBuilder();
303          StringBuilder lowerBuffer = new StringBuilder();
304          pos = readQuotedString(valueStr, lowerStr, userBuffer, lowerBuffer, pos-1);
305          names.put(lowerBuffer.toString(), userBuffer.toString());
306        }
307        else if (c == '(')
308        {
309          StringBuilder userBuffer  = new StringBuilder();
310          StringBuilder lowerBuffer = new StringBuilder();
311          pos = readQuotedString(valueStr, lowerStr, userBuffer, lowerBuffer,
312                                 pos);
313          names.put(lowerBuffer.toString(), userBuffer.toString());
314
315
316          while (true)
317          {
318            if (valueStr.charAt(pos) == ')')
319            {
320              // Skip over any spaces after the parenthesis.
321              pos++;
322              while (pos < length && ((c = valueStr.charAt(pos)) == ' '))
323              {
324                pos++;
325              }
326
327              break;
328            }
329            else
330            {
331              userBuffer  = new StringBuilder();
332              lowerBuffer = new StringBuilder();
333
334              pos = readQuotedString(valueStr, lowerStr, userBuffer,
335                                     lowerBuffer, pos);
336              names.put(lowerBuffer.toString(), userBuffer.toString());
337            }
338          }
339        }
340        else
341        {
342          // This is an illegal character.
343          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR.get(valueStr, c, pos-1);
344          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
345        }
346      }
347      else if (lowerTokenName.equals("desc"))
348      {
349        // This specifies the description for the matching rule use.  It is an
350        // arbitrary string of characters enclosed in single quotes.
351        StringBuilder descriptionBuffer = new StringBuilder();
352        pos = readQuotedString(valueStr, descriptionBuffer, pos);
353        description = descriptionBuffer.toString();
354      }
355      else if (lowerTokenName.equals("obsolete"))
356      {
357        // This indicates whether the matching rule use should be considered
358        // obsolete.  We do not need to do any more parsing for this token.
359        isObsolete = true;
360      }
361      else if (lowerTokenName.equals("applies"))
362      {
363        LinkedList<AttributeType> attrs = new LinkedList<>();
364
365        // This specifies the set of attribute types that may be used with the
366        // associated matching rule.  It may be a single name or OID (not in
367        // quotes), or it may be an open parenthesis followed by one or more
368        // names separated by spaces and the dollar sign character, followed
369        // by a closing parenthesis.
370        c = valueStr.charAt(pos++);
371        if (c == '(')
372        {
373          while (true)
374          {
375            StringBuilder woidBuffer = new StringBuilder();
376            pos = readWOID(lowerStr, woidBuffer, pos);
377            attrs.add(getAttributeType(schema, allowUnknownElements, oid, woidBuffer));
378
379            // The next character must be either a dollar sign or a closing parenthesis.
380            c = valueStr.charAt(pos++);
381            if (c == ')')
382            {
383              // This denotes the end of the list.
384              break;
385            }
386            else if (c != '$')
387            {
388              LocalizableMessage message =
389                  ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR.get(valueStr, c, pos-1);
390              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
391            }
392          }
393        }
394        else
395        {
396          StringBuilder woidBuffer = new StringBuilder();
397          pos = readWOID(lowerStr, woidBuffer, pos-1);
398          attrs.add(getAttributeType(schema, allowUnknownElements, oid, woidBuffer));
399        }
400
401        attributes = new LinkedHashSet<>(attrs);
402      }
403      else
404      {
405        // This must be a non-standard property and it must be followed by
406        // either a single value in single quotes or an open parenthesis
407        // followed by one or more values in single quotes separated by spaces
408        // followed by a close parenthesis.
409        LinkedList<String> valueList = new LinkedList<>();
410        pos = readExtraParameterValues(valueStr, valueList, pos);
411        extraProperties.put(tokenName, valueList);
412      }
413    }
414
415
416    // Make sure that the set of attributes was defined.
417    if (attributes == null)
418    {
419      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_NO_ATTR.get(valueStr);
420      throw new DirectoryException(
421              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
422    }
423
424
425    return new MatchingRuleUse(value.toString(), matchingRule, names,
426                               description, isObsolete, attributes,
427                               extraProperties);
428  }
429
430  private static AttributeType getAttributeType(
431      Schema schema, boolean allowUnknownElements, String oid, StringBuilder woidBuffer) throws DirectoryException
432  {
433    String woidString = woidBuffer.toString();
434    AttributeType attr = schema.getAttributeType(woidString);
435    if (attr.isPlaceHolder() && !allowUnknownElements)
436    {
437      throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
438          ERR_ATTR_SYNTAX_MRUSE_UNKNOWN_ATTR.get(oid, woidString));
439    }
440    return attr;
441  }
442
443  /**
444   * Reads the next token name from the matching rule use definition, skipping
445   * over any leading or trailing spaces, and appends it to the provided buffer.
446   *
447   * @param  valueStr   The string representation of the matching rule use
448   *                    definition.
449   * @param  tokenName  The buffer into which the token name will be written.
450   * @param  startPos   The position in the provided string at which to start
451   *                    reading the token name.
452   *
453   * @return  The position of the first character that is not part of the token
454   *          name or one of the trailing spaces after it.
455   *
456   * @throws  DirectoryException  If a problem is encountered while reading the
457   *                              token name.
458   */
459  private static int readTokenName(String valueStr, StringBuilder tokenName,
460                                   int startPos)
461          throws DirectoryException
462  {
463    // Skip over any spaces at the beginning of the value.
464    char c = '\u0000';
465    int  length = valueStr.length();
466    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
467    {
468      startPos++;
469    }
470
471    if (startPos >= length)
472    {
473      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
474      throw new DirectoryException(
475              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
476    }
477
478
479    // Read until we find the next space.
480    while (startPos < length && ((c = valueStr.charAt(startPos++)) != ' '))
481    {
482      tokenName.append(c);
483    }
484
485
486    // Skip over any trailing spaces after the value.
487    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
488    {
489      startPos++;
490    }
491
492
493    // Return the position of the first non-space character after the token.
494    return startPos;
495  }
496
497  /**
498   * Reads the value of a string enclosed in single quotes, skipping over the
499   * quotes and any leading or trailing spaces, and appending the string to the
500   * provided buffer.
501   *
502   * @param  valueStr     The user-provided representation of the matching rule
503   *                      use definition.
504   * @param  valueBuffer  The buffer into which the user-provided representation
505   *                      of the value will be placed.
506   * @param  startPos     The position in the provided string at which to start
507   *                      reading the quoted string.
508   *
509   * @return  The position of the first character that is not part of the quoted
510   *          string or one of the trailing spaces after it.
511   *
512   * @throws  DirectoryException  If a problem is encountered while reading the
513   *                              quoted string.
514   */
515  private static int readQuotedString(String valueStr,
516                                      StringBuilder valueBuffer, int startPos)
517          throws DirectoryException
518  {
519    // Skip over any spaces at the beginning of the value.
520    char c = '\u0000';
521    int  length = valueStr.length();
522    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
523    {
524      startPos++;
525    }
526
527    if (startPos >= length)
528    {
529      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
530      throw new DirectoryException(
531              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
532    }
533
534
535    // The next character must be a single quote.
536    if (c != '\'')
537    {
538      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_QUOTE_AT_POS.get(
539          valueStr, startPos, c);
540      throw new DirectoryException(
541              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
542    }
543
544
545    // Read until we find the closing quote.
546    startPos++;
547    while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
548    {
549      valueBuffer.append(c);
550      startPos++;
551    }
552
553
554    // Skip over any trailing spaces after the value.
555    startPos++;
556    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
557    {
558      startPos++;
559    }
560
561
562    // If we're at the end of the value, then that's illegal.
563    if (startPos >= length)
564    {
565      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
566      throw new DirectoryException(
567              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
568    }
569
570
571    // Return the position of the first non-space character after the token.
572    return startPos;
573  }
574
575  /**
576   * Reads the value of a string enclosed in single quotes, skipping over the
577   * quotes and any leading or trailing spaces, and appending the string to the
578   * provided buffer.
579   *
580   * @param  valueStr     The user-provided representation of the matching rule
581   *                      use definition.
582   * @param  lowerStr     The all-lowercase representation of the matching rule
583   *                      use definition.
584   * @param  userBuffer   The buffer into which the user-provided representation
585   *                      of the value will be placed.
586   * @param  lowerBuffer  The buffer into which the all-lowercase representation
587   *                      of the value will be placed.
588   * @param  startPos     The position in the provided string at which to start
589   *                      reading the quoted string.
590   *
591   * @return  The position of the first character that is not part of the quoted
592   *          string or one of the trailing spaces after it.
593   *
594   * @throws  DirectoryException  If a problem is encountered while reading the
595   *                              quoted string.
596   */
597  private static int readQuotedString(String valueStr, String lowerStr,
598                                      StringBuilder userBuffer,
599                                      StringBuilder lowerBuffer, int startPos)
600          throws DirectoryException
601  {
602    // Skip over any spaces at the beginning of the value.
603    char c = '\u0000';
604    int  length = lowerStr.length();
605    while (startPos < length && ((c = lowerStr.charAt(startPos)) == ' '))
606    {
607      startPos++;
608    }
609
610    if (startPos >= length)
611    {
612      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(lowerStr);
613      throw new DirectoryException(
614              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
615    }
616
617
618    // The next character must be a single quote.
619    if (c != '\'')
620    {
621      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_QUOTE_AT_POS.get(valueStr, startPos, c);
622      throw new DirectoryException(
623              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
624    }
625
626
627    // Read until we find the closing quote.
628    startPos++;
629    while (startPos < length && ((c = lowerStr.charAt(startPos)) != '\''))
630    {
631      lowerBuffer.append(c);
632      userBuffer.append(valueStr.charAt(startPos));
633      startPos++;
634    }
635
636
637    // Skip over any trailing spaces after the value.
638    startPos++;
639    while (startPos < length && ((c = lowerStr.charAt(startPos)) == ' '))
640    {
641      startPos++;
642    }
643
644
645    // If we're at the end of the value, then that's illegal.
646    if (startPos >= length)
647    {
648      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(lowerStr);
649      throw new DirectoryException(
650              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
651    }
652
653
654    // Return the position of the first non-space character after the token.
655    return startPos;
656  }
657
658  /**
659   * Reads the attribute type description or numeric OID from the provided
660   * string, skipping over any leading or trailing spaces, and appending the
661   * value to the provided buffer.
662   *
663   * @param  lowerStr    The string from which the name or OID is to be read.
664   * @param  woidBuffer  The buffer into which the name or OID should be
665   *                     appended.
666   * @param  startPos    The position at which to start reading.
667   *
668   * @return  The position of the first character after the name or OID that is
669   *          not a space.
670   *
671   * @throws  DirectoryException  If a problem is encountered while reading the
672   *                              name or OID.
673   */
674  private static int readWOID(String lowerStr, StringBuilder woidBuffer,
675                              int startPos)
676          throws DirectoryException
677  {
678    // Skip over any spaces at the beginning of the value.
679    char c = '\u0000';
680    int  length = lowerStr.length();
681    while (startPos < length && ((c = lowerStr.charAt(startPos)) == ' '))
682    {
683      startPos++;
684    }
685
686    if (startPos >= length)
687    {
688      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(lowerStr);
689      throw new DirectoryException(
690              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
691    }
692
693
694    // The next character must be either numeric (for an OID) or alphabetic (for
695    // an attribute type description).
696    if (isDigit(c))
697    {
698      // This must be a numeric OID.  In that case, we will accept only digits
699      // and periods, but not consecutive periods.
700      boolean lastWasPeriod = false;
701      while (startPos < length && ((c = lowerStr.charAt(startPos++)) != ' '))
702      {
703        if (c == '.')
704        {
705          if (lastWasPeriod)
706          {
707            LocalizableMessage message =
708                ERR_ATTR_SYNTAX_MRUSE_DOUBLE_PERIOD_IN_NUMERIC_OID.get(lowerStr, startPos-1);
709            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
710          }
711          woidBuffer.append(c);
712          lastWasPeriod = true;
713        }
714        else if (! isDigit(c))
715        {
716          // Technically, this must be an illegal character.  However, it is
717          // possible that someone just got sloppy and did not include a space
718          // between the name/OID and a closing parenthesis.  In that case,
719          // we'll assume it's the end of the value.  What's more, we'll have
720          // to prematurely return to nasty side effects from stripping off
721          // additional characters.
722          if (c == ')')
723          {
724            return startPos-1;
725          }
726
727          // This must have been an illegal character.
728          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR_IN_NUMERIC_OID.
729              get(lowerStr, c, startPos-1);
730          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
731        }
732        else
733        {
734          woidBuffer.append(c);
735          lastWasPeriod = false;
736        }
737      }
738    }
739    else if (isAlpha(c))
740    {
741      // This must be an attribute type description.  In this case, we will only
742      // accept alphabetic characters, numeric digits, and the hyphen.
743      while (startPos < length && ((c = lowerStr.charAt(startPos++)) != ' '))
744      {
745        if (isAlpha(c) || isDigit(c) || c == '-' ||
746            (c == '_' && DirectoryServer.allowAttributeNameExceptions()))
747        {
748          woidBuffer.append(c);
749        }
750        else
751        {
752          // Technically, this must be an illegal character.  However, it is
753          // possible that someone just got sloppy and did not include a space
754          // between the name/OID and a closing parenthesis.  In that case,
755          // we'll assume it's the end of the value.  What's more, we'll have
756          // to prematurely return to nasty side effects from stripping off
757          // additional characters.
758          if (c == ')')
759          {
760            return startPos-1;
761          }
762
763          // This must have been an illegal character.
764          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR_IN_STRING_OID.
765              get(lowerStr, c, startPos-1);
766          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
767        }
768      }
769    }
770    else
771    {
772      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR.get(lowerStr, c, startPos);
773      throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
774    }
775
776
777    // Skip over any trailing spaces after the value.
778    while (startPos < length && ((c = lowerStr.charAt(startPos)) == ' '))
779    {
780      startPos++;
781    }
782
783
784    // If we're at the end of the value, then that's illegal.
785    if (startPos >= length)
786    {
787      LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(lowerStr);
788      throw new DirectoryException(
789              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
790    }
791
792
793    // Return the position of the first non-space character after the token.
794    return startPos;
795  }
796
797  /**
798   * Reads the value for an "extra" parameter.  It will handle a single unquoted
799   * word (which is technically illegal, but we'll allow it), a single quoted
800   * string, or an open parenthesis followed by a space-delimited set of quoted
801   * strings or unquoted words followed by a close parenthesis.
802   *
803   * @param  valueStr   The string containing the information to be read.
804   * @param  valueList  The list of "extra" parameter values read so far.
805   * @param  startPos   The position in the value string at which to start
806   *                    reading.
807   *
808   * @return  The "extra" parameter value that was read.
809   *
810   * @throws  DirectoryException  If a problem occurs while attempting to read
811   *                              the value.
812   */
813  private static int readExtraParameterValues(String valueStr,
814                        List<String> valueList, int startPos)
815          throws DirectoryException
816  {
817    // Skip over any leading spaces.
818    int length = valueStr.length();
819    char c = '\u0000';
820    while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
821    {
822      startPos++;
823    }
824
825    if (startPos >= length)
826    {
827      LocalizableMessage message =
828          ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
829      throw new DirectoryException(
830              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
831    }
832
833
834    // Look at the next character.  If it is a quote, then parse until the next
835    // quote and end.  If it is an open parenthesis, then parse individual
836    // values until the close parenthesis and end.  Otherwise, parse until the
837    // next space and end.
838    if (c == '\'')
839    {
840      // Parse until the closing quote.
841      StringBuilder valueBuffer = new StringBuilder();
842      startPos++;
843      while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
844      {
845        valueBuffer.append(c);
846        startPos++;
847      }
848      startPos++;
849      valueList.add(valueBuffer.toString());
850    }
851    else if (c == '(')
852    {
853      startPos++;
854      // We're expecting a list of values. Quoted, space separated.
855      while (true)
856      {
857        // Skip over any leading spaces;
858        while (startPos < length && ((c = valueStr.charAt(startPos)) == ' '))
859        {
860          startPos++;
861        }
862
863        if (startPos >= length)
864        {
865          LocalizableMessage message =
866              ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
867          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
868                                       message);
869        }
870
871        if (c == ')')
872        {
873          // This is the end of the list.
874          startPos++;
875          break;
876        }
877        else if (c == '(')
878        {
879          // This is an illegal character.
880          LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_CHAR.get(valueStr, c, startPos);
881          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
882        }
883        else if (c == '\'')
884        {
885          // We have a quoted string
886          StringBuilder valueBuffer = new StringBuilder();
887          startPos++;
888          while (startPos < length && ((c = valueStr.charAt(startPos)) != '\''))
889          {
890            valueBuffer.append(c);
891            startPos++;
892          }
893
894          valueList.add(valueBuffer.toString());
895          startPos++;
896        }
897        else
898        {
899          //Consider unquoted string
900          StringBuilder valueBuffer = new StringBuilder();
901          while (startPos < length && ((c = valueStr.charAt(startPos)) != ' '))
902          {
903            valueBuffer.append(c);
904            startPos++;
905          }
906
907          valueList.add(valueBuffer.toString());
908        }
909
910        if (startPos >= length)
911        {
912          LocalizableMessage message =
913              ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
914          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
915                                       message);
916        }
917      }
918    }
919    else
920    {
921      // Parse until the next space.
922      StringBuilder valueBuffer = new StringBuilder();
923      while (startPos < length && ((c = valueStr.charAt(startPos)) != ' '))
924      {
925        valueBuffer.append(c);
926        startPos++;
927      }
928
929      valueList.add(valueBuffer.toString());
930    }
931
932    // Skip over any trailing spaces.
933    while (startPos < length && valueStr.charAt(startPos) == ' ')
934    {
935      startPos++;
936    }
937
938    if (startPos >= length)
939    {
940      LocalizableMessage message =
941          ERR_ATTR_SYNTAX_MRUSE_TRUNCATED_VALUE.get(valueStr);
942      throw new DirectoryException(
943              ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
944    }
945
946    return startPos;
947  }
948}
949