KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > jcorporate > expresso > core > dataobjects > jdbc > FieldRangeParser


1 /* ====================================================================
2 * The Jcorporate Apache Style Software License, Version 1.2 05-07-2002
3 *
4 * Copyright (c) 1995-2002 Jcorporate Ltd. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in
15 * the documentation and/or other materials provided with the
16 * distribution.
17 *
18 * 3. The end-user documentation included with the redistribution,
19 * if any, must include the following acknowledgment:
20 * "This product includes software developed by Jcorporate Ltd.
21 * (http://www.jcorporate.com/)."
22 * Alternately, this acknowledgment may appear in the software itself,
23 * if and wherever such third-party acknowledgments normally appear.
24 *
25 * 4. "Jcorporate" and product names such as "Expresso" must
26 * not be used to endorse or promote products derived from this
27 * software without prior written permission. For written permission,
28 * please contact info@jcorporate.com.
29 *
30 * 5. Products derived from this software may not be called "Expresso",
31 * or other Jcorporate product names; nor may "Expresso" or other
32 * Jcorporate product names appear in their name, without prior
33 * written permission of Jcorporate Ltd.
34 *
35 * 6. No product derived from this software may compete in the same
36 * market space, i.e. framework, without prior written permission
37 * of Jcorporate Ltd. For written permission, please contact
38 * partners@jcorporate.com.
39 *
40 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
41 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
42 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
43 * DISCLAIMED. IN NO EVENT SHALL JCORPORATE LTD OR ITS CONTRIBUTORS
44 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
45 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
46 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
47 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
48 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
49 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
50 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
51 * SUCH DAMAGE.
52 * ====================================================================
53 *
54 * This software consists of voluntary contributions made by many
55 * individuals on behalf of the Jcorporate Ltd. Contributions back
56 * to the project(s) are encouraged when you make modifications.
57 * Please send them to support@jcorporate.com. For more information
58 * on Jcorporate Ltd. and its products, please see
59 * <http://www.jcorporate.com/>.
60 *
61 * Portions of this software are based upon other open source
62 * products and are subject to their respective licenses.
63 */

64 package com.jcorporate.expresso.core.dataobjects.jdbc;
65
66 import com.jcorporate.expresso.core.dataobjects.DataFieldMetaData;
67 import com.jcorporate.expresso.kernel.util.FastStringBuffer;
68 import org.apache.log4j.Logger;
69 import org.apache.oro.text.regex.MalformedPatternException;
70 import org.apache.oro.text.regex.Pattern;
71 import org.apache.oro.text.regex.PatternCompiler;
72 import org.apache.oro.text.regex.PatternMatcher;
73 import org.apache.oro.text.regex.Perl5Compiler;
74 import org.apache.oro.text.regex.Perl5Matcher;
75
76 import java.util.Collections JavaDoc;
77 import java.util.HashSet JavaDoc;
78 import java.util.Set JavaDoc;
79 import java.util.StringTokenizer JavaDoc;
80
81 /**
82  * The responsibilities of this class is to verify range values for fields
83  * and throw an exception if there is any 'monkeybusiness' detected in the field.
84  * <p/>
85  * The parser does not allow everything a normal SQL query would allows due to
86  * parsing complexity. However, it will allow multiple conditions, and ranges for
87  * BETWEEN and IN.
88  * </p>
89  * <p>Accepted Range Strings:
90  * <ul>
91  * <li>[NOT] BETWEEN value1 AND value2</li>
92  * <li>[NOT] IN(value1,value2.....)</li>
93  * <li>[>|>=|<|<=] value ([AND|OR] [>|>=|<|<=] value)*</li>
94  * </ul>
95  * </p>
96  * <p>Known bugs with Range issues:
97  * <ul>
98  * <li>There can be no single quotes INSIDE string fields. The fields themselves must
99  * still be quoted.</li>
100  * <li>There cannot be the word AND or OR inside the String range fields.</li>
101  * <li>For IN statements, string field values cannot have commas in them</li>
102  * </ul>
103  * </p>
104  * <p>Instances of FieldRangeParser are meant to use shared between threads</p>
105  *
106  * @author Michael Rimov
107  * @version 2.0
108  */

109 public class FieldRangeParser {
110
111
112     /**
113      * The log4j logger
114      */

115     private static final transient Logger log = Logger.getLogger(FieldRangeParser.class);
116
117     /**
118      * Acceptable modifiers
119      */

120     private static final String JavaDoc MODIFIERS[] = {"AND", "OR", "and", "or"};
121
122     /**
123      * Acceptable Ranges
124      */

125     private static final String JavaDoc RANGES[] = {"<", "<=", ">=", ">", "<>"};
126
127     /**
128      * A set of modifiers
129      */

130     private static final Set JavaDoc MODIFIER_SET = Collections.synchronizedSet(new HashSet JavaDoc(MODIFIERS.length));
131
132     /**
133      * A set of acceptable ranges
134      */

135     private static final Set JavaDoc RANGE_SET = Collections.synchronizedSet(new HashSet JavaDoc(RANGES.length));
136
137
138     /**
139      * Dates/Integers, etc should not have wild cards, or single quotes or
140      * escape sequences like \'s. This guards against that.
141      */

142     private static final String JavaDoc LEGAL_NUMBER_RE = "^[^\\\\'%\"]+";
143
144     /**
145      * String Fields should start and end in ' and ultimately escape all single
146      * quotes in the middle with double single quotes. Unfortunately, there
147      * seems to be a bug in ORO for matchin that second case, so right now
148      * all single quotes in-between are prohibited.
149      */

150     private static final String JavaDoc LEGAL_STRING_RE = "^'[^']*'$";
151
152
153     /**
154      * Regular Expression for the basic format of IN statements. Verifies
155      * that it begins with IN, then has open and close parenthesis and closes
156      * with a parenthesis. The Regular expression does not in itself check the
157      * format of the fields inside the parenthesis.
158      */

159     private static final String JavaDoc IN_FORMAT_RE = "^IN\\s*\\(.+\\)$";
160
161
162     /**
163      * The compiled pattern for ILLEGAL_CHAR_RE
164      */

165     private Pattern legalCharacters = null;
166
167
168     /**
169      * A compiled patter for ILLEGAL_STRING_RE
170      */

171     private Pattern legalStrings = null;
172
173
174     /**
175      * Compiled pattern for IN_FORMAT
176      */

177     private Pattern inStatementPattern = null;
178
179     /**
180      * A Pattern Matcher for examining fields vs validation regular expressions
181      * It has been modified for thead local instantiation to reduce synchronization.
182      */

183     private transient static ThreadLocal JavaDoc patternMatcher = new ThreadLocal JavaDoc() {
184         protected synchronized Object JavaDoc initialValue() {
185             return new Perl5Matcher();
186         }
187     };
188
189
190     /**
191      * The singleton instance of this.
192      */

193     private static FieldRangeParser myInstance = new FieldRangeParser();
194
195
196     /**
197      * Retrieves an instance of a perl5 pattern matcher that is thread local
198      * to be thread safe.
199      *
200      * @return PatternMatcher instance.
201      */

202     protected PatternMatcher getMatcher() {
203         return (PatternMatcher) patternMatcher.get();
204     }
205
206     /**
207      * Default constructor. This creates all the regular expression matchers,
208      * etc.
209      */

210     public FieldRangeParser() {
211         try {
212             PatternCompiler compiler = new Perl5Compiler();
213             legalCharacters = compiler.compile(LEGAL_NUMBER_RE, Perl5Compiler.READ_ONLY_MASK);
214             legalStrings = compiler.compile(LEGAL_STRING_RE, Perl5Compiler.READ_ONLY_MASK);
215             inStatementPattern = compiler.compile(IN_FORMAT_RE, Perl5Compiler.READ_ONLY_MASK);
216
217             for (int i = 0; i < MODIFIERS.length; i++) {
218                 MODIFIER_SET.add(MODIFIERS[i]);
219             }
220
221             for (int i = 0; i < RANGES.length; i++) {
222                 RANGE_SET.add(RANGES[i]);
223             }
224
225
226         } catch (MalformedPatternException ex) {
227             log.error("Error setting up Illegal Character Regular Expression");
228         }
229     }
230
231     /**
232      * Retrieve an instance of the field range parser.
233      *
234      * @return FieldRangeParser instance
235      */

236     public static synchronized FieldRangeParser getInstance() {
237         return FieldRangeParser.myInstance;
238
239     }
240
241
242     /**
243      * Performs some basic checks to validate the syntax of the range field value
244      * given the system to help protect against nasty things like sql injection
245      *
246      * @param metadata the field metadata to be checking against.
247      * @param fieldValue the field value to check
248      * @return false if the range is invalid.
249      */

250     public boolean isValidRange(DataFieldMetaData metadata, String JavaDoc fieldValue) {
251
252         //Obviously null values are not valid ranges.
253
if (fieldValue == null) {
254             return false;
255         }
256
257         //Take out any potentially leading whitespace
258
String JavaDoc examineString = fieldValue.trim();
259
260         switch (examineString.charAt(0)) {
261             case 'N':
262             case 'n':
263                 if (examineString.startsWith("NOT") || examineString.startsWith("not")) {
264                     examineString = examineString.substring(3).trim();
265                 } else {
266                     //If we have an n to begin with, we need it to be NOT or we don't
267
//consider this valid.
268
return false;
269                 }
270
271                 //Fall through since NOT is a special case of IN or BETWEEN
272
case 'I':
273             case 'i':
274             case 'B':
275             case 'b':
276                 if (examineString.equalsIgnoreCase("is null") ||
277                         examineString.equalsIgnoreCase("is not null")) {
278                     return true;
279                 }
280                 if (examineString.startsWith("BETWEEN") || examineString.startsWith("between")) {
281                     return validateBetweenStatement(metadata, examineString);
282                 } else if (examineString.startsWith("IN") || examineString.startsWith("in")) {
283                     return validateInStatement(metadata, examineString);
284                 } else {
285                     return false;
286                 }
287
288             case '>':
289             case '<':
290                 return validateComparisonStatement(metadata, examineString);
291
292             default:
293                 return false;
294
295         }
296     }
297
298
299     /**
300      * Checks a single value and see if it is kosher for the expected data type.
301      *
302      * @param metadata the metadata for the particular field
303      * @param fieldValue the value of the field.
304      * @return true if the field value is valid.
305      */

306     private boolean validateFieldValue(DataFieldMetaData metadata, String JavaDoc fieldValue) {
307         if (metadata.isDateType() || metadata.isDateTimeType() || metadata.isTimeType()) {
308             return validateFieldDateValue(fieldValue);
309         } else if (metadata.isNumericType()) {
310             return validateFieldNumberValue(fieldValue);
311         } else {
312             return validateFieldStringValue(fieldValue);
313         }
314     }
315
316     /**
317      * Make sure there are no in-between single quotes.
318      *
319      * @param fieldValue the value of the field to check
320      * @return true if the field value is valid.
321      */

322     private boolean validateFieldStringValue(String JavaDoc fieldValue) {
323         return getMatcher().matches(fieldValue, legalStrings);
324     }
325
326     /**
327      * Instead of parsing the date, just make sure it doesn't have likely injection
328      * values.
329      *
330      * @param fieldValue the value of the field to check
331      * @return true if the field value is valid
332      */

333     private boolean validateFieldNumberValue(String JavaDoc fieldValue) {
334         return getMatcher().matches(fieldValue, legalCharacters);
335     }
336
337     /**
338      * Instead of parsing the date, just make sure it doesn't have likely injection
339      * values.
340      *
341      * @param fieldValue the value of the field to check
342      * @return true if the field value is valid
343      */

344     private boolean validateFieldDateValue(String JavaDoc fieldValue) {
345         return getMatcher().matches(fieldValue, legalStrings);
346     }
347
348
349     /**
350      * Validates BETWEEN type statements which should have: BETWEEN [field1] AND [field2]
351      *
352      * @param metadata field metadata. This specifies the types that should be
353      * validated in the field.
354      * @param expressionValue The range value for the field.
355      * @return true if the expression is valid.
356      */

357     private boolean validateBetweenStatement(DataFieldMetaData metadata, String JavaDoc expressionValue) {
358         StringTokenizer JavaDoc stok = new StringTokenizer JavaDoc(expressionValue);
359         if (!stok.hasMoreTokens()) {
360             return false;
361         }
362
363         //
364
//Must start with "Between"
365
//
366
String JavaDoc betweenStatement = stok.nextToken();
367         if (!("BETWEEN".equalsIgnoreCase(betweenStatement)) || !stok.hasMoreTokens()) {
368             return false;
369         }
370
371         boolean foundAnd = false;
372
373         //Now look for AND, and validate field 1
374
FastStringBuffer fsb = FastStringBuffer.getInstance();
375         String JavaDoc fieldValue;
376         try {
377             while (!foundAnd && stok.hasMoreTokens()) {
378                 String JavaDoc token = stok.nextToken();
379                 if ("AND".equalsIgnoreCase(token)) {
380                     foundAnd = true;
381                 } else {
382                     fsb.append(" ");
383                     fsb.append(token);
384                 }
385             }
386             fieldValue = fsb.toString().trim();
387         } finally {
388             fsb.release();
389         }
390
391         if (!foundAnd) {
392             return false;
393         } else {
394             if (!validateFieldValue(metadata, fieldValue)) {
395                 return false;
396             }
397         }
398
399         //OK, now we look for the end.
400
if (!stok.hasMoreTokens()) {
401             //Nothing for a field value!
402
return false;
403         }
404
405         fsb = FastStringBuffer.getInstance();
406         try {
407             while (stok.hasMoreTokens()) {
408                 String JavaDoc token = stok.nextToken();
409                 fsb.append(" ");
410                 fsb.append(token);
411             }
412
413             fieldValue = fsb.toString().trim();
414         } finally {
415             fsb.release();
416         }
417
418         //Ok, we found the last field... validate it and we're done.
419
return (validateFieldValue(metadata, fieldValue));
420     }
421
422     /**
423      * Validates IN statements which are of the format: IN (value1,value2....)
424      *
425      * @param metadata the data field's metadata
426      * @param expressionValue the range string to parse and validate.
427      * @return true if the IN statement seems to be of valid syntax.
428      */

429     private boolean validateInStatement(DataFieldMetaData metadata, String JavaDoc expressionValue) {
430
431         boolean match;
432
433         //It seems that ORO has a race condition in it :(
434
match = getMatcher().matches(expressionValue, inStatementPattern);
435
436         if (!match) {
437             //Basic syntax is not IN ( .... )
438
return false;
439         }
440
441         int openParen = expressionValue.indexOf("(");
442         int closeParen = expressionValue.lastIndexOf(")");
443
444         /**
445          * @todo check this substring
446          */

447         String JavaDoc toEvaluate = expressionValue.substring(openParen + 1,
448                 closeParen);
449
450         StringTokenizer JavaDoc stok = new StringTokenizer JavaDoc(toEvaluate, ",");
451         while (stok.hasMoreTokens()) {
452             String JavaDoc oneFieldValue = stok.nextToken();
453             boolean result = validateFieldValue(metadata, oneFieldValue);
454             if (result == false) {
455                 return false;
456             }
457         }
458
459         return true;
460     }
461
462     /**
463      * This function evaluates the formats of greater than / less than types of
464      * ranges. It supports multiple comparisons as well.
465      *
466      * @param metadata The metadata for the field.
467      * @param expressionValue value for the field to validate
468      * @return true if the field appears to be of proper formatting.
469      */

470     private boolean validateComparisonStatement(DataFieldMetaData metadata, String JavaDoc expressionValue) {
471         StringTokenizer JavaDoc stok = new StringTokenizer JavaDoc(expressionValue);
472
473         //WE got a weird expression.
474
if (!stok.hasMoreTokens()) {
475             return false;
476         }
477
478         return validateComparisonRecusive(metadata, stok);
479     }
480
481     /**
482      * Validates operator and operand and returns their values.
483      *
484      * @param expression the expression to consume
485      * @return String[0] == operator; String[1] = operand (may be legally null);
486      */

487     private String JavaDoc[] consumeOperator(String JavaDoc expression) {
488         String JavaDoc returnValue[] = new String JavaDoc[2];
489
490         int index = 0;
491         boolean done = false;
492         int length = expression.length();
493
494         StringBuffer JavaDoc buffer = new StringBuffer JavaDoc();
495
496
497         while (!done && index < length) {
498             char opChar = expression.charAt(index);
499             switch (opChar) {
500                 case '>':
501                 case '<':
502                 case '=':
503                     buffer.append(opChar);
504                     index++;
505                     break;
506                 default:
507                     returnValue[1] = expression.substring(index);
508                     done = true;
509             }
510
511         }
512
513         String JavaDoc operator = buffer.toString();
514         if (RANGE_SET.contains(operator)) {
515             returnValue[0] = operator;
516         }
517
518         return returnValue;
519     }
520
521     /**
522      * Performs a recursive parsing of the string value. Each recursion checks for
523      * proper &lt &gt type of modifications, a field value proper for the data type,
524      * and looks for AND or OR... if it find those, then it calls itself with the
525      * rest of the string
526      *
527      * @param metadata the metadata for the field.
528      * @param tokenizer the tokenizer that is progressing through the range string.
529      * @return true if the validation is successfull, or false if there is bogus
530      * syntax detected.
531      */

532     private boolean validateComparisonRecusive(DataFieldMetaData metadata, StringTokenizer JavaDoc tokenizer) {
533         String JavaDoc operator = tokenizer.nextToken();
534
535         FastStringBuffer fsb = FastStringBuffer.getInstance();
536
537         String JavaDoc[] result = consumeOperator(operator);
538         String JavaDoc fieldValue;
539         boolean moreComparisons = false;
540
541         //No operator found case
542
if (result[0] == null) {
543             return false;
544         }
545
546         if (result[1] == null && !tokenizer.hasMoreTokens()) {
547             return false;
548         }
549
550         //Append the first value.
551
if (result[1] != null) {
552             fsb.append(result[1]);
553         }
554
555         try {
556             boolean done = false;
557             while (!done && tokenizer.hasMoreTokens()) {
558                 String JavaDoc oneValue = tokenizer.nextToken();
559                 //If we hit AND or OR then we need to evaluate what we've got so
560
//far
561
if (MODIFIER_SET.contains(oneValue)) {
562                     moreComparisons = true;
563                     done = true;
564                 } else {
565                     fsb.append(" ");
566                     fsb.append(oneValue);
567                 }
568             }
569
570             fieldValue = fsb.toString();
571
572         } finally {
573             fsb.release();
574         }
575
576         if (validateFieldValue(metadata, fieldValue.trim())) {
577             if (moreComparisons) {
578                 return validateComparisonRecusive(metadata, tokenizer);
579             } else {
580                 return true;
581             }
582         } else {
583             //The field test itself flunked.
584
return false;
585         }
586     }
587
588     /**
589      * Does a given field value denote a range?
590      *
591      * @param fieldValue The field value to check against.
592      * @return The "range" string if the value starts with a range indicator, null if not
593      */

594     public String JavaDoc denotesRange(String JavaDoc fieldValue) {
595
596         //Quick dummy tests to save us all the string checking.
597
if (fieldValue == null || fieldValue.length() == 0) {
598             return null;
599         }
600
601
602         char firstChar = fieldValue.charAt(0);
603         switch (firstChar) {
604             case 'b':
605             case 'B':
606                 if (fieldValue.startsWith("between ")) {
607                     return "between ";
608                 } else if (fieldValue.startsWith("BETWEEN ")) {
609                     return "BETWEEN ";
610                 }
611                 break;
612
613             case 'i':
614             case 'I':
615                 if (fieldValue.startsWith("in ")) {
616                     return "in ";
617                 } else if (fieldValue.startsWith("IN ")) {
618                     return "IN ";
619                 } else if (fieldValue.trim().equalsIgnoreCase("is not null") ||
620                         fieldValue.trim().equalsIgnoreCase("is null")) {
621                     return "";
622                 }
623                 break;
624
625             case 'n':
626             case 'N':
627                 if (fieldValue.startsWith("not in ")) {
628                     return "not in ";
629                 } else if (fieldValue.startsWith("NOT IN")) {
630                     return "NOT IN ";
631                 } else if (fieldValue.startsWith("not between ")) {
632                     return "not between ";
633                 } else if (fieldValue.startsWith("NOT BETWEEN ")) {
634                     return "NOT BETWEEN ";
635                 }
636                 break;
637
638
639             case '<':
640                 if (fieldValue.startsWith("<>")) {
641                     return "<> ";
642                 } else if (fieldValue.startsWith("<=")) {
643                     return "<=";
644                 } else if (fieldValue.startsWith("< ")) {
645                     return "< ";
646
647
648                 } else if (fieldValue.startsWith("<")) {
649                     return "<";
650                 }
651                 break;
652
653             case '>':
654                 if (fieldValue.startsWith("> ")) {
655                     return "> ";
656                 } else if (fieldValue.startsWith(">=")) {
657                     return ">=";
658                 } else if (fieldValue.startsWith(">")) {
659                     return ">";
660                 }
661                 break;
662
663             default:
664                 return null;
665         }
666
667         return null;
668     } /* denotesRange(String) */
669
670 }
Popular Tags