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