001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.schema;
028    import org.opends.messages.Message;
029    
030    
031    
032    import org.opends.server.admin.std.server.AttributeSyntaxCfg;
033    import org.opends.server.api.ApproximateMatchingRule;
034    import org.opends.server.api.AttributeSyntax;
035    import org.opends.server.api.EqualityMatchingRule;
036    import org.opends.server.api.OrderingMatchingRule;
037    import org.opends.server.api.SubstringMatchingRule;
038    import org.opends.server.config.ConfigException;
039    import org.opends.server.core.DirectoryServer;
040    import org.opends.server.types.ByteString;
041    import org.opends.server.types.DirectoryException;
042    
043    
044    import org.opends.server.types.ResultCode;
045    
046    import static org.opends.server.loggers.debug.DebugLogger.*;
047    import org.opends.server.loggers.debug.DebugTracer;
048    import static org.opends.server.loggers.ErrorLogger.*;
049    import org.opends.server.types.DebugLogLevel;
050    import static org.opends.messages.SchemaMessages.*;
051    import org.opends.messages.MessageBuilder;
052    import static org.opends.server.schema.SchemaConstants.*;
053    import static org.opends.server.util.StaticUtils.*;
054    
055    
056    /**
057     * This class defines the LDAP syntax description syntax, which is used to
058     * hold attribute syntax definitions in the server schema.  The format of this
059     * syntax is defined in RFC 2252.
060     */
061    public class LDAPSyntaxDescriptionSyntax
062           extends AttributeSyntax<AttributeSyntaxCfg>
063    {
064      /**
065       * The tracer object for the debug logger.
066       */
067      private static final DebugTracer TRACER = getTracer();
068    
069    
070    
071    
072      // The default equality matching rule for this syntax.
073      private EqualityMatchingRule defaultEqualityMatchingRule;
074    
075      // The default ordering matching rule for this syntax.
076      private OrderingMatchingRule defaultOrderingMatchingRule;
077    
078      // The default substring matching rule for this syntax.
079      private SubstringMatchingRule defaultSubstringMatchingRule;
080    
081    
082    
083      /**
084       * Creates a new instance of this syntax.  Note that the only thing that
085       * should be done here is to invoke the default constructor for the
086       * superclass.  All initialization should be performed in the
087       * <CODE>initializeSyntax</CODE> method.
088       */
089      public LDAPSyntaxDescriptionSyntax()
090      {
091        super();
092      }
093    
094    
095    
096      /**
097       * {@inheritDoc}
098       */
099      public void initializeSyntax(AttributeSyntaxCfg configuration)
100             throws ConfigException
101      {
102        defaultEqualityMatchingRule =
103             DirectoryServer.getEqualityMatchingRule(EMR_CASE_IGNORE_OID);
104        if (defaultEqualityMatchingRule == null)
105        {
106          logError(ERR_ATTR_SYNTAX_UNKNOWN_EQUALITY_MATCHING_RULE.get(
107              EMR_CASE_IGNORE_OID, SYNTAX_LDAP_SYNTAX_NAME));
108        }
109    
110        defaultOrderingMatchingRule =
111             DirectoryServer.getOrderingMatchingRule(OMR_CASE_IGNORE_OID);
112        if (defaultOrderingMatchingRule == null)
113        {
114          logError(ERR_ATTR_SYNTAX_UNKNOWN_ORDERING_MATCHING_RULE.get(
115              OMR_CASE_IGNORE_OID, SYNTAX_LDAP_SYNTAX_NAME));
116        }
117    
118        defaultSubstringMatchingRule =
119             DirectoryServer.getSubstringMatchingRule(SMR_CASE_IGNORE_OID);
120        if (defaultSubstringMatchingRule == null)
121        {
122          logError(ERR_ATTR_SYNTAX_UNKNOWN_SUBSTRING_MATCHING_RULE.get(
123              SMR_CASE_IGNORE_OID, SYNTAX_LDAP_SYNTAX_NAME));
124        }
125      }
126    
127    
128    
129      /**
130       * Retrieves the common name for this attribute syntax.
131       *
132       * @return  The common name for this attribute syntax.
133       */
134      public String getSyntaxName()
135      {
136        return SYNTAX_LDAP_SYNTAX_NAME;
137      }
138    
139    
140    
141      /**
142       * Retrieves the OID for this attribute syntax.
143       *
144       * @return  The OID for this attribute syntax.
145       */
146      public String getOID()
147      {
148        return SYNTAX_LDAP_SYNTAX_OID;
149      }
150    
151    
152    
153      /**
154       * Retrieves a description for this attribute syntax.
155       *
156       * @return  A description for this attribute syntax.
157       */
158      public String getDescription()
159      {
160        return SYNTAX_LDAP_SYNTAX_DESCRIPTION;
161      }
162    
163    
164    
165      /**
166       * Retrieves the default equality matching rule that will be used for
167       * attributes with this syntax.
168       *
169       * @return  The default equality matching rule that will be used for
170       *          attributes with this syntax, or <CODE>null</CODE> if equality
171       *          matches will not be allowed for this type by default.
172       */
173      public EqualityMatchingRule getEqualityMatchingRule()
174      {
175        return defaultEqualityMatchingRule;
176      }
177    
178    
179    
180      /**
181       * Retrieves the default ordering matching rule that will be used for
182       * attributes with this syntax.
183       *
184       * @return  The default ordering matching rule that will be used for
185       *          attributes with this syntax, or <CODE>null</CODE> if ordering
186       *          matches will not be allowed for this type by default.
187       */
188      public OrderingMatchingRule getOrderingMatchingRule()
189      {
190        return defaultOrderingMatchingRule;
191      }
192    
193    
194    
195      /**
196       * Retrieves the default substring matching rule that will be used for
197       * attributes with this syntax.
198       *
199       * @return  The default substring matching rule that will be used for
200       *          attributes with this syntax, or <CODE>null</CODE> if substring
201       *          matches will not be allowed for this type by default.
202       */
203      public SubstringMatchingRule getSubstringMatchingRule()
204      {
205        return defaultSubstringMatchingRule;
206      }
207    
208    
209    
210      /**
211       * Retrieves the default approximate matching rule that will be used for
212       * attributes with this syntax.
213       *
214       * @return  The default approximate matching rule that will be used for
215       *          attributes with this syntax, or <CODE>null</CODE> if approximate
216       *          matches will not be allowed for this type by default.
217       */
218      public ApproximateMatchingRule getApproximateMatchingRule()
219      {
220        // There is no approximate matching rule by default.
221        return null;
222      }
223    
224    
225    
226      /**
227       * Indicates whether the provided value is acceptable for use in an attribute
228       * with this syntax.  If it is not, then the reason may be appended to the
229       * provided buffer.
230       *
231       * @param  value          The value for which to make the determination.
232       * @param  invalidReason  The buffer to which the invalid reason should be
233       *                        appended.
234       *
235       * @return  <CODE>true</CODE> if the provided value is acceptable for use with
236       *          this syntax, or <CODE>false</CODE> if not.
237       */
238      public boolean valueIsAcceptable(ByteString value,
239                                       MessageBuilder invalidReason)
240      {
241        // Get string representations of the provided value using the provided form
242        // and with all lowercase characters.
243        String valueStr = value.stringValue();
244        String lowerStr = toLowerCase(valueStr);
245    
246    
247        // We'll do this a character at a time.  First, skip over any leading
248        // whitespace.
249        int pos    = 0;
250        int length = valueStr.length();
251        while ((pos < length) && (valueStr.charAt(pos) == ' '))
252        {
253          pos++;
254        }
255    
256        if (pos >= length)
257        {
258          // This means that the value was empty or contained only whitespace.  That
259          // is illegal.
260    
261          invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE.get());
262          return false;
263        }
264    
265    
266        // The next character must be an open parenthesis.  If it is not, then that
267        // is an error.
268        char c = valueStr.charAt(pos++);
269        if (c != '(')
270        {
271    
272          invalidReason.append(
273                  ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS.get(
274                          valueStr, (pos-1), String.valueOf(c)));
275          return false;
276        }
277    
278    
279        // Skip over any spaces immediately following the opening parenthesis.
280        while ((pos < length) && ((c = valueStr.charAt(pos)) == ' '))
281        {
282          pos++;
283        }
284    
285        if (pos >= length)
286        {
287          // This means that the end of the value was reached before we could find
288          // the OID.  Ths is illegal.
289          invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(
290                  valueStr));
291          return false;
292        }
293    
294    
295        // The next set of characters must be the OID.  Strictly speaking, this
296        // should only be a numeric OID, but we'll also allow for the
297        // "attrname-oid" case as well.  Look at the first character to figure out
298        // which we will be using.
299        int oidStartPos = pos;
300        if (isDigit(c))
301        {
302          // This must be a numeric OID.  In that case, we will accept only digits
303          // and periods, but not consecutive periods.
304          boolean lastWasPeriod = false;
305          while ((pos < length) && ((c = valueStr.charAt(pos++)) != ' '))
306          {
307            if (c == '.')
308            {
309              if (lastWasPeriod)
310              {
311                invalidReason.append(
312                        ERR_ATTR_SYNTAX_ATTRSYNTAX_DOUBLE_PERIOD_IN_NUMERIC_OID.get(
313                                valueStr, (pos-1)));
314                return false;
315              }
316              else
317              {
318                lastWasPeriod = true;
319              }
320            }
321            else if (! isDigit(c))
322            {
323              // This must have been an illegal character.
324              invalidReason.append(
325                      ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_CHAR_IN_NUMERIC_OID.get(
326                              valueStr, String.valueOf(c), (pos-1)));
327              return false;
328            }
329            else
330            {
331              lastWasPeriod = false;
332            }
333          }
334        }
335        else
336        {
337          // This must be a "fake" OID.  In this case, we will only accept
338          // alphabetic characters, numeric digits, and the hyphen.
339          while ((pos < length) && ((c = valueStr.charAt(pos++)) != ' '))
340          {
341            if (isAlpha(c) || isDigit(c) || (c == '-') ||
342                ((c == '_') && DirectoryServer.allowAttributeNameExceptions()))
343            {
344              // This is fine.  It is an acceptable character.
345            }
346            else
347            {
348              // This must have been an illegal character.
349    
350              invalidReason.append(
351                      ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_CHAR_IN_STRING_OID.get(
352                              valueStr, String.valueOf(c), (pos-1)));
353              return false;
354            }
355          }
356        }
357    
358    
359        // If we're at the end of the value, then it isn't a valid attribute type
360        // description.  Otherwise, parse out the OID.
361        String oid;
362        if (pos >= length)
363        {
364          invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(
365                  valueStr));
366          return false;
367        }
368        else
369        {
370          oid = lowerStr.substring(oidStartPos, pos);
371        }
372    
373    
374        // Skip over the space(s) after the OID.
375        while ((pos < length) && ((c = valueStr.charAt(pos)) == ' '))
376        {
377          pos++;
378        }
379    
380        if (pos >= length)
381        {
382          // This means that the end of the value was reached before we could find
383          // the OID.  Ths is illegal.
384          invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(
385                  valueStr));
386          return false;
387        }
388    
389    
390        // If the next character is a closing parenthesis, then we must be at the
391        // end of the value.
392        if (c == ')')
393        {
394          if (pos < length)
395          {
396            invalidReason.append(
397                    ERR_ATTR_SYNTAX_ATTRSYNTAX_UNEXPECTED_CLOSE_PARENTHESIS.get(
398                            valueStr, (pos-1)));
399            return false;
400          }
401    
402          return true;
403        }
404    
405    
406        // The next token must be "DESC" followed by a quoted string.
407        String tokenName;
408        try
409        {
410          StringBuilder tokenNameBuffer = new StringBuilder();
411          pos = readTokenName(lowerStr, tokenNameBuffer, pos);
412          tokenName = tokenNameBuffer.toString();
413        }
414        catch (Exception e)
415        {
416          if (debugEnabled())
417          {
418            TRACER.debugCaught(DebugLogLevel.ERROR, e);
419          }
420    
421          invalidReason.append(
422                  ERR_ATTR_SYNTAX_ATTRSYNTAX_CANNOT_READ_DESC_TOKEN.get(
423                          valueStr, pos, getExceptionMessage(e)));
424          return false;
425        }
426    
427        if (! tokenName.equals("desc"))
428        {
429          invalidReason.append(ERR_ATTR_SYNTAX_ATTRSYNTAX_TOKEN_NOT_DESC.get(
430                  valueStr, tokenName));
431          return false;
432        }
433    
434    
435        // The next component must be the quoted description.
436        try
437        {
438          StringBuilder descriptionBuffer = new StringBuilder();
439          pos = readQuotedString(valueStr, descriptionBuffer, pos);
440        }
441        catch (Exception e)
442        {
443          if (debugEnabled())
444          {
445            TRACER.debugCaught(DebugLogLevel.ERROR, e);
446          }
447    
448          invalidReason.append(
449                  ERR_ATTR_SYNTAX_ATTRSYNTAX_CANNOT_READ_DESC_VALUE.get(
450                          valueStr, pos, getExceptionMessage(e)));
451          return false;
452        }
453        //Check if we have a RFC 4512 style extension.
454        if ((c = valueStr.charAt(pos)) != ')')
455        {
456            try {
457                pos=parseExtension(valueStr, pos);
458            } catch (Exception e) {
459              if (debugEnabled())
460              {
461                TRACER.debugCaught(DebugLogLevel.ERROR, e);
462              }
463                invalidReason.append(
464                        ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID_EXTENSION.get(
465                                getExceptionMessage(e)));
466                return false;
467            }
468        }
469    
470        // The next character must be the closing parenthesis and there should not
471        // be anything after it (except maybe some spaces).
472        if ((c = valueStr.charAt(pos++)) != ')')
473        {
474    
475          invalidReason.append(
476                  ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_CLOSE_PARENTHESIS.get(
477                          valueStr, pos, String.valueOf(c)));
478          return false;
479        }
480    
481        while (pos < length)
482        {
483          c = valueStr.charAt(pos++);
484          if (c != ' ')
485          {
486    
487            invalidReason.append(
488                    ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_CHAR_AFTER_CLOSE.get(
489                            valueStr, String.valueOf(c), pos));
490            return false;
491          }
492        }
493    
494    
495        // If we've gotten here, then the value is OK.
496        return true;
497      }
498    
499    
500    
501      /**
502       * Reads the next token name from the attribute syntax definition, skipping
503       * over any leading or trailing spaces, and appends it to the provided buffer.
504       *
505       * @param  valueStr   The string representation of the attribute syntax
506       *                    definition.
507       * @param  tokenName  The buffer into which the token name will be written.
508       * @param  startPos   The position in the provided string at which to start
509       *                    reading the token name.
510       *
511       * @return  The position of the first character that is not part of the token
512       *          name or one of the trailing spaces after it.
513       *
514       * @throws  DirectoryException  If a problem is encountered while reading the
515       *                              token name.
516       */
517      private static int readTokenName(String valueStr, StringBuilder tokenName,
518                                       int startPos)
519              throws DirectoryException
520      {
521        // Skip over any spaces at the beginning of the value.
522        char c = '\u0000';
523        int  length = valueStr.length();
524        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
525        {
526          startPos++;
527        }
528    
529        if (startPos >= length)
530        {
531          Message message =
532              ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(valueStr);
533          throw new DirectoryException(
534                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
535        }
536    
537    
538        // Read until we find the next space.
539        while ((startPos < length) && ((c = valueStr.charAt(startPos++)) != ' '))
540        {
541          tokenName.append(c);
542        }
543    
544    
545        // Skip over any trailing spaces after the value.
546        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
547        {
548          startPos++;
549        }
550    
551    
552        // Return the position of the first non-space character after the token.
553        return startPos;
554      }
555    
556    
557    
558      /**
559       * Reads the value of a string enclosed in single quotes, skipping over the
560       * quotes and any leading or trailing spaces, and appending the string to the
561       * provided buffer.
562       *
563       * @param  valueStr     The user-provided representation of the attribute type
564       *                      definition.
565       * @param  valueBuffer  The buffer into which the user-provided representation
566       *                      of the value will be placed.
567       * @param  startPos     The position in the provided string at which to start
568       *                      reading the quoted string.
569       *
570       * @return  The position of the first character that is not part of the quoted
571       *          string or one of the trailing spaces after it.
572       *
573       * @throws  DirectoryException  If a problem is encountered while reading the
574       *                              quoted string.
575       */
576      private static int readQuotedString(String valueStr,
577                                          StringBuilder valueBuffer, int startPos)
578              throws DirectoryException
579      {
580        // Skip over any spaces at the beginning of the value.
581        char c = '\u0000';
582        int  length = valueStr.length();
583        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
584        {
585          startPos++;
586        }
587    
588        if (startPos >= length)
589        {
590          Message message =
591              ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(valueStr);
592          throw new DirectoryException(
593                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
594        }
595    
596    
597        // The next character must be a single quote.
598        if (c != '\'')
599        {
600          Message message = WARN_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_QUOTE_AT_POS.get(
601              valueStr, startPos, String.valueOf(c));
602          throw new DirectoryException(
603                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
604        }
605    
606    
607        // Read until we find the closing quote.
608        startPos++;
609        while ((startPos < length) && ((c = valueStr.charAt(startPos)) != '\''))
610        {
611          valueBuffer.append(c);
612          startPos++;
613        }
614    
615    
616        // Skip over any trailing spaces after the value.
617        startPos++;
618        while ((startPos < length) && ((c = valueStr.charAt(startPos)) == ' '))
619        {
620          startPos++;
621        }
622    
623    
624        // If we're at the end of the value, then that's illegal.
625        if (startPos >= length)
626        {
627          Message message =
628              ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(valueStr);
629          throw new DirectoryException(
630                  ResultCode.INVALID_ATTRIBUTE_SYNTAX, message);
631        }
632    
633    
634        // Return the position of the first non-space character after the token.
635        return startPos;
636      }
637    
638      /** Parses a RFC 4512 extensions (see 4.1.5 and 4.1 of the RFC) definition.
639       *
640       * From 4.1.5 of the spec:
641       *
642       *  LDAP syntax definitions are written according to the ABNF:
643       *
644       *  SyntaxDescription = LPAREN WSP
645       *      numericoid                 ; object identifier
646       *      [ SP "DESC" SP qdstring ]  ; description
647       *      extensions WSP RPAREN      ; extensions
648       *
649       * @param valueStr The user-provided representation of the extensions
650       *                      definition.
651       *
652       * @param startPos The position in the provided string at which to start
653       *                      reading the quoted string.
654       *
655       * @return The position of the first character that is not part of the quoted
656       *          string or one of the trailing spaces after it.
657       *
658       * @throws DirectoryException If the extensions definition could not be
659       *                            parsed.
660       */
661    private static int parseExtension(String valueStr, int startPos)
662      throws DirectoryException {
663    
664          int pos=startPos, len=valueStr.length();
665          char c;
666          while(true)
667          {
668              StringBuilder tokenNameBuffer = new StringBuilder();
669              pos = readTokenName(valueStr, tokenNameBuffer, pos);
670              String tokenName = tokenNameBuffer.toString();
671              if((tokenName.length() <= 2) || (!tokenName.startsWith("X-")))
672              {
673                  Message message =
674                    ERR_ATTR_SYNTAX_ATTRSYNTAX_EXTENSION_INVALID_CHARACTER.get(
675                            valueStr, pos);
676                  throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
677                          message);
678              }
679              String xstring = tokenName.substring(2);
680              //Only allow a-z,A-Z,-,_ characters after X-
681              if(xstring.split("^[A-Za-z_-]+").length > 0)
682              {
683                  Message message =
684                    ERR_ATTR_SYNTAX_ATTRSYNTAX_EXTENSION_INVALID_CHARACTER.get(
685                            valueStr, pos);
686                  throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
687                          message);
688              }
689              if((c=valueStr.charAt(pos)) == '\'')
690              {
691                  StringBuilder qdString = new StringBuilder();
692                  pos = readQuotedString(valueStr, qdString, pos);
693    
694              } else if(c == '(')
695              {
696                  pos++;
697                  StringBuilder qdString = new StringBuilder();
698                  while ((c=valueStr.charAt(pos)) != ')')
699                      pos = readQuotedString(valueStr, qdString, pos);
700                  pos++;
701              } else
702              {
703                  Message message =
704                    ERR_ATTR_SYNTAX_ATTRSYNTAX_EXTENSION_INVALID_CHARACTER.get(
705                            valueStr, pos);
706                  throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
707                          message);
708              }
709              if (pos >= len)
710              {
711                Message message =
712                    ERR_ATTR_SYNTAX_ATTRSYNTAX_TRUNCATED_VALUE.get(valueStr);
713                throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
714                                             message);
715              }
716              if(valueStr.charAt(pos) == ')')
717                  break;
718          }
719          return pos;
720      }
721    }
722