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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS
026 */
027package org.opends.server.schema;
028
029import org.opends.server.admin.std.server.AttributeSyntaxCfg;
030import org.forgerock.opendj.ldap.schema.Schema;
031import org.forgerock.opendj.ldap.schema.Syntax;
032import org.opends.server.api.AttributeSyntax;
033import static org.opends.messages.SchemaMessages.*;
034
035import org.forgerock.i18n.LocalizableMessageBuilder;
036
037import static org.opends.server.schema.SchemaConstants.*;
038import static org.opends.server.util.StaticUtils.*;
039
040/**
041 * This class implements the guide attribute syntax, which may be used to
042 * provide criteria for generating search filters for entries, optionally tied
043 * to a specified objectclass.
044 */
045public class GuideSyntax
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 GuideSyntax()
056  {
057    super();
058  }
059
060  /** {@inheritDoc} */
061  @Override
062  public Syntax getSDKSyntax(Schema schema)
063  {
064    return schema.getSyntax(SchemaConstants.SYNTAX_GUIDE_OID);
065  }
066
067  /**
068   * Retrieves the common name for this attribute syntax.
069   *
070   * @return  The common name for this attribute syntax.
071   */
072  @Override
073  public String getName()
074  {
075    return SYNTAX_GUIDE_NAME;
076  }
077
078  /**
079   * Retrieves the OID for this attribute syntax.
080   *
081   * @return  The OID for this attribute syntax.
082   */
083  @Override
084  public String getOID()
085  {
086    return SYNTAX_GUIDE_OID;
087  }
088
089  /**
090   * Retrieves a description for this attribute syntax.
091   *
092   * @return  A description for this attribute syntax.
093   */
094  @Override
095  public String getDescription()
096  {
097    return SYNTAX_GUIDE_DESCRIPTION;
098  }
099
100  /**
101   * Determines whether the provided string represents a valid criteria
102   * according to the guide syntax.
103   *
104   * @param  criteria       The portion of the criteria for which to make the
105   *                        determination.
106   * @param  valueStr       The complete guide value provided by the client.
107   * @param  invalidReason  The buffer to which to append the reason that the
108   *                        criteria is invalid if a problem is found.
109   *
110   * @return  <CODE>true</CODE> if the provided string does contain a valid
111   *          criteria, or <CODE>false</CODE> if not.
112   */
113  public static boolean criteriaIsValid(String criteria, String valueStr,
114                                        LocalizableMessageBuilder invalidReason)
115  {
116    // See if the criteria starts with a '!'.  If so, then just evaluate
117    // everything after that as a criteria.
118    char c = criteria.charAt(0);
119    if (c == '!')
120    {
121      return criteriaIsValid(criteria.substring(1), valueStr, invalidReason);
122    }
123
124
125    // See if the criteria starts with a '('.  If so, then find the
126    // corresponding ')' and parse what's in between as a criteria.
127    if (c == '(')
128    {
129      int length = criteria.length();
130      int depth  = 1;
131
132      for (int i=1; i < length; i++)
133      {
134        c = criteria.charAt(i);
135        if (c == ')')
136        {
137          depth--;
138          if (depth == 0)
139          {
140            String subCriteria = criteria.substring(1, i);
141            if (! criteriaIsValid(subCriteria, valueStr, invalidReason))
142            {
143              return false;
144            }
145
146            // If we are at the end of the value, then it was valid.  Otherwise,
147            // the next character must be a pipe or an ampersand followed by
148            // another set of criteria.
149            if (i == (length-1))
150            {
151              return true;
152            }
153            else
154            {
155              c = criteria.charAt(i+1);
156              if (c == '|' || c == '&')
157              {
158                return criteriaIsValid(criteria.substring(i+2), valueStr,
159                                       invalidReason);
160              }
161              else
162              {
163                invalidReason.append(
164                        ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(
165                                valueStr, criteria, c, i+1));
166                return false;
167              }
168            }
169          }
170        }
171        else if (c == '(')
172        {
173          depth++;
174        }
175      }
176
177
178      // If we've gotten here, then we went through the entire value without
179      // finding the appropriate closing parenthesis.
180
181      invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_MISSING_CLOSE_PAREN.get(
182              valueStr, criteria));
183      return false;
184    }
185
186
187    // See if the criteria starts with a '?'.  If so, then it must be either
188    // "?true" or "?false".
189    if (c == '?')
190    {
191      if (criteria.startsWith("?true"))
192      {
193        if (criteria.length() == 5)
194        {
195          return true;
196        }
197        else
198        {
199          // The only characters allowed next are a pipe or an ampersand.
200          c = criteria.charAt(5);
201          if (c == '|' || c == '&')
202          {
203            return criteriaIsValid(criteria.substring(6), valueStr,
204                                   invalidReason);
205          }
206          else
207          {
208            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(
209                    valueStr, criteria, c, 5));
210            return false;
211          }
212        }
213      }
214      else if (criteria.startsWith("?false"))
215      {
216        if (criteria.length() == 6)
217        {
218          return true;
219        }
220        else
221        {
222          // The only characters allowed next are a pipe or an ampersand.
223          c = criteria.charAt(6);
224          if (c == '|' || c == '&')
225          {
226            return criteriaIsValid(criteria.substring(7), valueStr,
227                                   invalidReason);
228          }
229          else
230          {
231            invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(
232                    valueStr, criteria, c, 6));
233            return false;
234          }
235        }
236      }
237      else
238      {
239        invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_QUESTION_MARK.get(
240                valueStr, criteria));
241        return false;
242      }
243    }
244
245
246    // See if the criteria is either "true" or "false".  If so, then it is
247    // valid.
248    if (criteria.equals("true") || criteria.equals("false"))
249    {
250      return true;
251    }
252
253
254    // The only thing that will be allowed is an attribute type name or OID
255    // followed by a dollar sign and a match type.  Find the dollar sign and
256    // verify whether the value before it is a valid attribute type name or OID.
257    int dollarPos = criteria.indexOf('$');
258    if (dollarPos < 0)
259    {
260      invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_DOLLAR.get(
261              valueStr, criteria));
262      return false;
263    }
264    else if (dollarPos == 0)
265    {
266      invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_ATTR.get(
267              valueStr, criteria));
268      return false;
269    }
270    else if (dollarPos == (criteria.length()-1))
271    {
272      invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_NO_MATCH_TYPE.get(
273              valueStr, criteria));
274      return false;
275    }
276    else
277    {
278      if (! isValidSchemaElement(criteria, 0, dollarPos, invalidReason))
279      {
280        return false;
281      }
282    }
283
284
285    // The substring immediately after the dollar sign must be one of "eq",
286    // "substr", "ge", "le", or "approx".  It may be followed by the end of the
287    // value, a pipe, or an ampersand.
288    int endPos;
289    c = criteria.charAt(dollarPos+1);
290    switch (c)
291    {
292      case 'e':
293        if (criteria.startsWith("eq", dollarPos+1))
294        {
295          endPos = dollarPos + 3;
296          break;
297        }
298        else
299        {
300          invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
301                  valueStr, criteria, dollarPos+1));
302          return false;
303        }
304
305      case 's':
306        if (criteria.startsWith("substr", dollarPos+1))
307        {
308          endPos = dollarPos + 7;
309          break;
310        }
311        else
312        {
313          invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
314                  valueStr, criteria, dollarPos+1));
315          return false;
316        }
317
318      case 'g':
319        if (criteria.startsWith("ge", dollarPos+1))
320        {
321          endPos = dollarPos + 3;
322          break;
323        }
324        else
325        {
326          invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
327                  valueStr, criteria, dollarPos+1));
328          return false;
329        }
330
331      case 'l':
332        if (criteria.startsWith("le", dollarPos+1))
333        {
334          endPos = dollarPos + 3;
335          break;
336        }
337        else
338        {
339          invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
340                  valueStr, criteria, dollarPos+1));
341          return false;
342        }
343
344      case 'a':
345        if (criteria.startsWith("approx", dollarPos+1))
346        {
347          endPos = dollarPos + 7;
348          break;
349        }
350        else
351        {
352          invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
353                  valueStr, criteria, dollarPos+1));
354          return false;
355        }
356
357      default:
358        invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_INVALID_MATCH_TYPE.get(
359                valueStr, criteria, dollarPos+1));
360        return false;
361    }
362
363
364    // See if we are at the end of the value.  If so, then it is valid.
365    // Otherwise, the next character must be a pipe or an ampersand.
366    if (endPos >= criteria.length())
367    {
368      return true;
369    }
370    else
371    {
372      c = criteria.charAt(endPos);
373      if (c == '|' || c == '&')
374      {
375        return criteriaIsValid(criteria.substring(endPos+1), valueStr,
376                               invalidReason);
377      }
378      else
379      {
380        invalidReason.append(ERR_ATTR_SYNTAX_GUIDE_ILLEGAL_CHAR.get(
381                valueStr, criteria, c, endPos));
382        return false;
383      }
384    }
385  }
386}
387