0001: /*
0002:
0003: This software is OSI Certified Open Source Software.
0004: OSI Certified is a certification mark of the Open Source Initiative.
0005:
0006: The license (Mozilla version 1.0) can be read at the MMBase site.
0007: See http://www.MMBase.org/license
0008:
0009: */
0010: package org.mmbase.storage.search.legacy;
0011:
0012: import java.util.*;
0013: import org.mmbase.bridge.Field;
0014: import org.mmbase.core.CoreField;
0015: import org.mmbase.module.core.*;
0016: import org.mmbase.storage.StorageManagerFactory;
0017: import org.mmbase.storage.search.*;
0018: import org.mmbase.storage.search.implementation.*;
0019: import org.mmbase.util.logging.*;
0020: import org.mmbase.bridge.NodeQuery;
0021:
0022: /**
0023: * Parser, tries to parse a <em>SQL-search-condition</em> for a query to a
0024: * {@link org.mmbase.storage.search.Constraint Constraint} object.
0025: * <p>
0026: * This class is provided for the sole purpose of alignment of old code with
0027: * the new {@link org.mmbase.storage.search.SearchQuery SearchQuery} framework,
0028: * and should not be called by new code.
0029: * <p>
0030: * A <em>SQL-search-condition</em> can be one of these forms:
0031: * <ul>
0032: * <li>[<b>NOT</b>] <b>(</b><em>SQL-search-condition</em><b>)</b>
0033: * <li>[<b>NOT</b>] <em>simple-SQL-search-condition</em>
0034: * <li><em>SQL-search-condition</em> <b>AND</b> <em>SQL-search-condition</em>
0035: * <li><em>SQL-search-condition</em> <b>OR</b> <em>SQL-search-condition</em>
0036: * </ul>
0037: * A <em>simple-SQL-search-condition</em> string can be of one of these forms:
0038: * <ul>
0039: * <li><em>field</em> [<b>NOT</b>] <b>LIKE</b> <em>value</em>
0040: * <li><b>UPPER(</b><em>field</em><b>)</b> [<b>NOT</b>] <b>LIKE</b> <em>value</em>
0041: * <li><b>LOWER(</b><em>field</em><b>)</b> [<b>NOT</b>] <b>LIKE</b> <em>value</em>
0042: * <li><em>field</em> <b>IS</b> [<b>NOT</b>] <b>NULL</b>
0043: * <li><em>field</em> [<b>NOT</b>] <b>IN
0044: * (</b><em>value1</em><b>,</b> <em>value2</em><b>,</b> ..<b>)</b>
0045: * <li><em>field</em> [<b>NOT</b>] <b>BETWEEN</b> <em>value1</em> <b>AND</b> <em>value2</em>
0046: * <li><b>UPPER(</b><em>field</em><b>)</b> [<b>NOT</b>] <b>BETWEEN</b> <em>value1</em> <b>AND</b> <em>value2</em>
0047: * <li><b>LOWER(</b><em>field</em><b>)</b> [<b>NOT</b>] <b>BETWEEN</b> <em>value1</em> <b>AND</b> <em>value2</em>
0048: * <li><em>field</em> <b>=</b> <em>value</em>
0049: * <li><em>field</em> <b>=</b> <em>field2</em>
0050: * <li><b>UPPER(</b><em>field</em><b>) =</b> <em>value</em>
0051: * <li><b>LOWER(</b><em>field</em><b>) =</b> <em>value</em>
0052: * <li><em>field</em> <b>==</b> <em>value</em>
0053: * <li><em>field</em> <b>==</b> <em>field2</em>
0054: * <li><em>field</em> <b><=</b> <em>value</em>
0055: * <li><em>field</em> <b><=</b> <em>field2</em>
0056: * <li><em>field</em> <b><</b> <em>value</em>
0057: * <li><em>field</em> <b><</b> <em>field2</em>
0058: * <li><em>field</em> <b>>=</b> <em>value</em>
0059: * <li><em>field</em> <b>>=</b> <em>field2</em>
0060: * <li><em>field</em> <b>></b> <em>value</em>
0061: * <li><em>field</em> <b>></b> <em>field2</em>
0062: * <li><em>field</em> <b><></b> <em>value</em>
0063: * <li><em>field</em> <b><></b> <em>field2</em>
0064: * <li><em>field</em> <b>!=</b> <em>value</em>
0065: * <li><em>field</em> <b>!=</b> <em>field2</em>
0066: * <li><em>string-search-condition</em>
0067: * </ul>
0068: * A <em>field</em> can be one of these forms:
0069: * <ul>
0070: * <li><em>stepalias</em><b>.</b><em>fieldname</em>
0071: * <li><em>fieldname</em> (only when the query has just one step).
0072: * </ul>
0073: * A <em>value</em> can be one of these forms:
0074: * <ul>
0075: * <li><em>string between single quotes</em> for string fields, example: <code>'A string value'</code>
0076: * <li><em>numerical value</em> for numerical fields, example: <code>123.456</code>
0077: * <li><em>numerical value between single quotes</em> for numerical fields, example" <code>'123.456'</code>
0078: * </ul>
0079: * A <em>string-search-condition</em> can be of this form:
0080: * <ul>
0081: * <li><b>StringSearch(</b><em>field</em><b>,</b>PHRASE|PROXIMITY|WORD<b>,</b>
0082: * FUZZY|LITERAL|SYNONYM<b>,</b>
0083: * <em>searchterms</em><b>,</b>
0084: * <em>casesensitive</em><b>)</b>
0085: * [<b>.set(FUZZINESS,</b><em>fuzziness</em><b>)</b>]
0086: * [<b>.set(PROXIMITY_LIMIT,</b><em>proximity</em><b>)</b>]
0087: * </ul>
0088: * <em>searchterms</em> can be of one of these forms:
0089: * <ul>
0090: * <li><b>'</b>term1<b>'</b>
0091: * <li><b>"</b>term1<b>"</b>
0092: * <li><b>'</b>term1 term2<b>'</b>
0093: * <li><b>"</b>term1 term2<b>"</b>
0094: * <li> etc...
0095: * </ul>
0096: * <em>casesensitive</em> can be of one on these forms:
0097: * <ul>
0098: * <li><b>true</b>
0099: * <li><b>false</b>
0100: * </ul>
0101: * <em>fuzziness</em> must be a float value between 0.0 and 1.0,
0102: * <em>proximity</em> must be a int value > 0<br />
0103: * <p>
0104: * See {@link org.mmbase.storage.search.StringSearchConstraint
0105: * StringSearchConstraint} for more info on string-search constraints.
0106: * <p>
0107: * A search condition that is not of one of these forms will be converted to a
0108: * {@link org.mmbase.storage.search.LegacyConstraint LegacyConstraint}, i.e.
0109: * in that case the search condition string will not be interpreted, but
0110: * instead be used "as-is".
0111: * Each time this occurs is logged with priority <code>service</code> to
0112: * category <code>org.mmbase.storage.search.legacyConstraintParser.fallback</code>.
0113: *
0114: * @author Rob van Maris
0115: * @version $Id: ConstraintParser.java,v 1.35 2007/11/28 10:17:47 michiel Exp $
0116: * @since MMBase-1.7
0117: */
0118: public class ConstraintParser {
0119:
0120: private final static Logger log = Logging
0121: .getLoggerInstance(ConstraintParser.class);
0122:
0123: /** Logger instance dedicated to logging fallback to legacy constraint. */
0124: private final static Logger fallbackLog = Logging
0125: .getLoggerInstance(ConstraintParser.class.getName()
0126: + ".fallback");
0127:
0128: /**
0129: * Converts a constraint by turning all 'quoted' fields into
0130: * database supported fields.
0131: * XXX: todo: escape characters for '[' and ']'.
0132: * @param constraints constraint to convert
0133: * @return Converted constraint
0134: * @since MMBase-1.8.1 (moved from org.mmbase.bridge.util.Queries)
0135: */
0136: private static String convertClausePartToDBS(String constraints) {
0137: StorageManagerFactory<?> factory = MMBase.getMMBase()
0138: .getStorageManagerFactory();
0139: StringBuilder result = new StringBuilder();
0140: int posa = constraints.indexOf('[');
0141: while (posa > -1) {
0142: int posb = constraints.indexOf(']', posa);
0143: if (posb == -1) {
0144: posa = -1;
0145: } else {
0146: String fieldName = constraints
0147: .substring(posa + 1, posb);
0148: int posc = fieldName.indexOf('.');
0149: if (posc == -1) {
0150: fieldName = factory != null ? factory
0151: .getStorageIdentifier(fieldName).toString()
0152: : fieldName;
0153: } else {
0154: fieldName = fieldName.substring(0, posc + 1)
0155: + (factory != null ? factory
0156: .getStorageIdentifier(fieldName
0157: .substring(posc + 1))
0158: : fieldName.substring(posc + 1));
0159: }
0160: result.append(constraints.substring(0, posa)).append(
0161: fieldName);
0162: constraints = constraints.substring(posb + 1);
0163: posa = constraints.indexOf('[');
0164: }
0165: }
0166: result.append(constraints);
0167: return result.toString();
0168: }
0169:
0170: /**
0171: * Converts a constraint by turning all 'quoted' fields into
0172: * database supported fields.
0173: * XXX: todo: escape characters for '[' and ']'.
0174: * @param constraints constraints to convert
0175: * @return converted constraint
0176: * @since MMBase-1.8.1 (moved from org.mmbase.bridge.util.Queries)
0177: */
0178: public static String convertClauseToDBS(String constraints) {
0179: if (constraints.startsWith("MMNODE")) {
0180: // wil probably not work
0181: // @todo check
0182: return constraints;
0183: } else if (constraints.startsWith("ALTA")) {
0184: // wil probably not work
0185: // @todo check
0186: return constraints.substring(5);
0187: }
0188:
0189: //keesj: what does this code do?
0190:
0191: StringBuilder result = new StringBuilder();
0192: //if there is a quote in the constraints posa will not be equals -1
0193:
0194: int quoteOpen = constraints.indexOf('\'');
0195: while (quoteOpen > -1) {
0196: //keesj: posb can be the same a posa maybe the method should read indexOf("\"",posa) ?
0197: int quoteClose = constraints.indexOf('\'', quoteOpen + 1);
0198: if (quoteClose == -1) {
0199: // unmatching quote?
0200: log.warn("unbalanced quote in " + constraints);
0201: break;
0202: }
0203:
0204: //keesj:part is now the first part of the constraints if there is a quote in the query
0205: String part = constraints.substring(0, quoteOpen);
0206:
0207: //append to the string buffer "part" the first part
0208: result.append(convertClausePartToDBS(part));
0209: result.append(constraints.substring(quoteOpen,
0210: quoteClose + 1));
0211:
0212: constraints = constraints.substring(quoteClose + 1);
0213: quoteOpen = constraints.indexOf('\'');
0214:
0215: }
0216: result.append(convertClausePartToDBS(constraints));
0217: return result.toString();
0218: }
0219:
0220: /**
0221: * returns false, when escaping wasnt closed, or when a ";" was found outside a escaped part (to prefent spoofing)
0222: * This is used by createQuery (i wonder if it still makes sense)
0223: * @param constraints constraint to check
0224: * @return is valid constraint
0225: * @since MMBase-1.8.1 (moved from org.mmbase.bridge.util.Queries)
0226: */
0227: static public boolean validConstraints(String constraints) {
0228: // first remove all the escaped "'" ('' occurences) chars...
0229: String remaining = constraints;
0230: while (remaining.indexOf("''") != -1) {
0231: int start = remaining.indexOf("''");
0232: int stop = start + 2;
0233: if (stop < remaining.length()) {
0234: String begin = remaining.substring(0, start);
0235: String end = remaining.substring(stop);
0236: remaining = begin + end;
0237: } else {
0238: remaining = remaining.substring(0, start);
0239: }
0240: }
0241: // assume we are not escaping... and search the string..
0242: // Keep in mind that at this point, the remaining string could contain different information
0243: // than the original string. This doesnt matter for the next sequence...
0244: // but it is important to realize!
0245: while (remaining.length() > 0) {
0246: if (remaining.indexOf('\'') != -1) {
0247: // we still contain a "'"
0248: int start = remaining.indexOf('\'');
0249:
0250: // escaping started, but no stop
0251: if (start == remaining.length()) {
0252: log
0253: .warn("reached end, but we are still escaping(you should sql-escape the search query inside the jsp-page?)\noriginal:"
0254: + constraints);
0255: return false;
0256: }
0257:
0258: String notEscaped = remaining.substring(0, start);
0259: if (notEscaped.indexOf(';') != -1) {
0260: log
0261: .warn("found a ';' outside the constraints(you should sql-escape the search query inside the jsp-page?)\noriginal:"
0262: + constraints
0263: + "\nnot excaped:"
0264: + notEscaped);
0265: return false;
0266: }
0267:
0268: int stop = remaining.substring(start + 1).indexOf('\'');
0269: if (stop < 0) {
0270: log
0271: .warn("reached end, but we are still escaping(you should sql-escape the search query inside the jsp-page?)\noriginal:"
0272: + constraints
0273: + "\nlast escaping:"
0274: + remaining.substring(start + 1));
0275: return false;
0276: }
0277: // we added one to to start, thus also add this one to stop...
0278: stop = start + stop + 1;
0279:
0280: // when the last character was the stop of our escaping
0281: if (stop == remaining.length()) {
0282: return true;
0283: }
0284:
0285: // cut the escaped part from the string, and continue with resting sting...
0286: remaining = remaining.substring(stop + 1);
0287: } else {
0288: if (remaining.indexOf(';') != -1) {
0289: log.warn("found a ';' inside our constrain:"
0290: + constraints);
0291: return false;
0292: }
0293: return true;
0294: }
0295: }
0296: return true;
0297: }
0298:
0299: private SearchQuery query = null;
0300: private List<? extends Step> steps = null;
0301:
0302: /**
0303: * Parses string or numerical value from list of tokens, to match the type
0304: * of the specified field.
0305: * If the first token is not "'", it is interpreted as a numerical value,
0306: * otherwise it is required to be the first token of the sequence
0307: * "'", "value", "'", representing a string value for string fields, or
0308: * a numerical value for numerical fields.
0309: *
0310: * @param iTokens Tokens iterator, must be positioned before the (first)
0311: * token representing the value.
0312: * @param field The field.
0313: * @return A <code>String</code> or <code>Double</code> object representing
0314: * the value.
0315: * @throws NumberFormatException when the first token is not (the start of)
0316: * a valid value expression (it may be a <em>field</em> instead).
0317: */
0318: // package visibility!
0319: static Object parseValue(Iterator<String> iTokens, StepField field)
0320: throws NumberFormatException {
0321: Object result = null;
0322: String token = iTokens.next();
0323: if (token.equals("'")) {
0324: // String value.
0325: result = iTokens.next();
0326: token = iTokens.next();
0327: if (!token.equals("'")) {
0328: throw new IllegalArgumentException(
0329: "Unexpected token (expected \"'\"): \"" + token
0330: + "\"");
0331: }
0332:
0333: int fieldType = field.getType();
0334: if (fieldType == Field.TYPE_BINARY
0335: || fieldType == Field.TYPE_DOUBLE
0336: || fieldType == Field.TYPE_FLOAT
0337: || fieldType == Field.TYPE_INTEGER
0338: || fieldType == Field.TYPE_LONG
0339: || fieldType == Field.TYPE_NODE) {
0340: // String represents a numerical value.
0341: result = Double.valueOf((String) result);
0342: }
0343: } else {
0344: result = Double.valueOf(token);
0345: }
0346: return result;
0347: }
0348:
0349: /**
0350: * Parses SQL search condition string into separate tokens, discarding
0351: * white spaces, concatenating strings between (single/double) quotes,
0352: * and replacing escaped (single/double) quotes in strings by the
0353: * original character.
0354: *
0355: * @param sqlConstraint The SQL constraint string.
0356: * @return List of tokens.
0357: */
0358: // package visibility!
0359: static List<String> tokenize(String sqlConstraint) {
0360: // Parse into separate tokens.
0361: List<String> tokens = new ArrayList<String>();
0362: StringTokenizer st = new StringTokenizer(sqlConstraint,
0363: " ()'\"=<>!,", true);
0364: tokenize: while (st.hasMoreTokens()) {
0365: String token = st.nextToken(" ()'\"=<>!,");
0366:
0367: // String, delimited by single or double quotes.
0368: if (token.equals("'") || token.equals("\"")) {
0369: tokens.add("'");
0370: StringBuilder sb = new StringBuilder();
0371: while (true) {
0372: String token2 = st.nextToken(token);
0373: if (token2.equals(token)) {
0374: if (!st.hasMoreTokens()) {
0375: // Token 2 is end delimiter and last token.
0376: tokens.add(sb.toString());
0377: tokens.add("'");
0378: break tokenize;
0379: } else {
0380: String token3 = st.nextToken(" ()'\"=<>!,");
0381: if (token3.equals(token)) {
0382: // Token 2 and 3 are escaped delimiter.
0383: sb.append(token);
0384: } else {
0385: // Token 2 is end delimiter, but not last token.
0386: tokens.add(sb.toString());
0387: tokens.add("'");
0388: token = token3;
0389: break;
0390: }
0391: }
0392: } else {
0393: // Token 2 is string.
0394: sb.append(token2);
0395: }
0396: }
0397: }
0398:
0399: // Add token, but skip white spaces.
0400: if (!token.equals(" ")) {
0401: tokens.add(token);
0402: }
0403: }
0404: return tokens;
0405: }
0406:
0407: /**
0408: * Creates <code>StepField</code> corresponding to field indicated by
0409: * token, of one of the specified steps.
0410: * <p>
0411: * A <em>field</em> can be one of these forms:
0412: * <ul>
0413: * <li><em>stepalias</em><b>.</b><em>fieldname</em>
0414: * <li>[<em>stepalias</em><b>.</b><em>fieldname</em>]
0415: * <li><em>fieldname</em> (only when just one step is specified).
0416: * <li>[<em>fieldname</em>] (only when just one step is specified).
0417: * </ul>
0418: *
0419: * @param token The token.
0420: * @param steps The steps.
0421: * @return The field.
0422: */
0423:
0424: public static StepField getField(String token,
0425: List<? extends Step> steps) {
0426: return getField(token, (List<BasicStep>) steps, null);
0427: }
0428:
0429: /**
0430: * Creates <code>StepField</code> corresponding to field indicated by
0431: * token, of one of the specified steps.
0432: * <p>
0433: * A <em>field</em> can be one of these forms:
0434: * <ul>
0435: * <li><em>stepalias</em><b>.</b><em>fieldname</em>
0436: * <li>[<em>stepalias</em><b>.</b><em>fieldname</em>]
0437: * <li><em>fieldname</em> (only when just one step is specified, or query is an instance of NodeQuery).
0438: * <li>[<em>fieldname</em>] (only when just one step is specified, or query is an instance of NodeQuery).
0439: * </ul>
0440: *
0441: * @param token The token.
0442: * @param steps The steps.
0443: * @param query The used query
0444: * @return The field.
0445: * @since MMBase-1.7.1
0446: */
0447:
0448: static StepField getField(String token, List<BasicStep> steps,
0449: SearchQuery query) {
0450: BasicStep step = null;
0451: int bracketOffset = (token.startsWith("[") && token
0452: .endsWith("]")) ? 1 : 0;
0453: int idx = token.indexOf('.');
0454: if (idx == -1) {
0455: if (steps.size() > 1) {
0456: if (query != null && query instanceof NodeQuery) {
0457: step = (BasicStep) ((NodeQuery) query)
0458: .getNodeStep();
0459: if (step == null) {
0460: throw new IllegalArgumentException(
0461: "NodeQuery has no step; Fieldname not prefixed with table alias: \""
0462: + token + "\"");
0463: }
0464: } else {
0465: throw new IllegalArgumentException(
0466: "Fieldname not prefixed with table alias: \""
0467: + token + "\"");
0468: }
0469: } else {
0470: step = steps.get(0);
0471: }
0472: } else {
0473: step = getStep(token.substring(bracketOffset, idx), steps);
0474: }
0475: MMObjectBuilder builder = step.getBuilder();
0476: String fieldName;
0477: if (idx == -1) {
0478: fieldName = token.substring(bracketOffset, token.length()
0479: - bracketOffset);
0480: } else {
0481: fieldName = token.substring(idx + 1, token.length()
0482: - bracketOffset);
0483: }
0484:
0485: CoreField coreField = builder.getField(fieldName);
0486: // maybe the field was already escaped with getAllowedField
0487: // otherwise it will definitely fail!
0488: if (coreField == null) {
0489: String escapedFieldName = MMBase.getMMBase()
0490: .getStorageManagerFactory().getStorageIdentifier(
0491: fieldName).toString();
0492: if (!escapedFieldName.equals(fieldName)) {
0493: coreField = builder.getField(fieldName);
0494: if (coreField == null) {
0495: throw new IllegalArgumentException(
0496: "Unknown field (of builder "
0497: + builder.getTableName() + "): \""
0498: + escapedFieldName + "\"");
0499: }
0500: }
0501: }
0502: if (coreField == null) {
0503: throw new IllegalArgumentException(
0504: "Unknown field (of builder "
0505: + builder.getTableName() + "): \""
0506: + fieldName + "\"");
0507: }
0508: BasicStepField field = new BasicStepField(step, coreField);
0509: return field;
0510: }
0511:
0512: /**
0513: * Finds step by alias.
0514: *
0515: * @param alias The alias.
0516: * @param steps The steps
0517: * @return The step.
0518: */
0519: private static BasicStep getStep(String alias, List<BasicStep> steps) {
0520: Iterator<BasicStep> iSteps = steps.iterator();
0521: while (iSteps.hasNext()) {
0522: BasicStep step = iSteps.next();
0523: String alias2 = step.getAlias();
0524: if (alias2 == null) {
0525: alias2 = step.getTableName();
0526: }
0527: if (alias2.equals(alias)) {
0528: return step;
0529: }
0530: }
0531:
0532: // Not found.
0533: throw new IllegalArgumentException("Unknown table alias: \""
0534: + alias + "\"");
0535: }
0536:
0537: /** Creates a new instance of ConstraintParser
0538: * @param query
0539: */
0540: public ConstraintParser(SearchQuery query) {
0541: this .query = query;
0542: this .steps = query.getSteps();
0543: }
0544:
0545: /**
0546: * Parses <em>SQL-search-condition</em>, and produces a corresponding
0547: * {@link org.mmbase.storage.search.Constraint Constraint} object.
0548: * <p>
0549: * See {@link ConstraintParser above} for the format of a
0550: * <em>SQL-search-condition</em>.
0551: *
0552: * @param sqlConstraint The non-null SQL constraint string.
0553: * @return The constraint.
0554: */
0555: public Constraint toConstraint(String sqlConstraint) {
0556: Constraint result = null;
0557: try {
0558: ListIterator<String> iTokens = tokenize(sqlConstraint)
0559: .listIterator();
0560: result = parseCondition(iTokens);
0561:
0562: // If this doesn't work, fall back to legacy code.
0563: } catch (Exception e) {
0564: // Log to fallback logger.
0565: if (fallbackLog.isServiceEnabled()) {
0566: fallbackLog
0567: .service(
0568: "Failed to parse Constraint from search condition string: "
0569: + "\n sqlConstraint = "
0570: + sqlConstraint
0571: + "\n exception: "
0572: + e.getMessage()
0573: + "\nFalling back to BasicLegacyConstraint...",
0574: e);
0575: }
0576: String escapedSqlConstraint = convertClauseToDBS(sqlConstraint);
0577: if (!validConstraints(escapedSqlConstraint)) {
0578: throw new IllegalArgumentException(
0579: "Invalid constraints: " + sqlConstraint);
0580: }
0581: result = new BasicLegacyConstraint(escapedSqlConstraint);
0582: }
0583:
0584: if (log.isDebugEnabled()) {
0585: log.debug("Parsed constraint \"" + sqlConstraint
0586: + "\" to :\n" + result);
0587: }
0588: return result;
0589: }
0590:
0591: /**
0592: * Parses a <em>field</em> string, and produces a corresponding
0593: * <code>StepField</code> object.
0594: * <p>
0595: * See {@link ConstraintParser above} for the format of a
0596: * <em>field</em>
0597: *
0598: * @param token The token.
0599: * @return The field.
0600: */
0601: // package visibility!
0602: StepField getField(String token) {
0603: return getField(token, (List<BasicStep>) steps, query);
0604: }
0605:
0606: /**
0607: * Parses <em>SQL-search-condition</em> string from list of tokens, and
0608: * produces a corresponding <code>BasicConstraint</code> object.
0609: * <p>
0610: * See {@link ConstraintParser above} for the format of a
0611: * <em>SQL-search-condition</em>
0612: *
0613: * @param iTokens Tokens iterator, must be positioned before the (first)
0614: * token representing the condition.
0615: * @return The constraint.
0616: */
0617: // package visibility!
0618: BasicConstraint parseCondition(ListIterator<String> iTokens) {
0619: BasicCompositeConstraint composite = null;
0620: BasicConstraint constraint = null;
0621: while (iTokens.hasNext()) {
0622: boolean inverse = false;
0623: String token = iTokens.next();
0624: if (token.equalsIgnoreCase("NOT")) {
0625: // NOT.
0626: inverse = true;
0627: token = iTokens.next();
0628: }
0629:
0630: if (token.equals("(")) {
0631: // Start of (simple or composite) constraint
0632: // between parenthesis.
0633: constraint = parseCondition(iTokens);
0634: } else {
0635: // Simple condition.
0636: iTokens.previous();
0637: constraint = parseSimpleCondition(iTokens);
0638: }
0639: if (inverse) {
0640: constraint.setInverse(!constraint.isInverse());
0641: }
0642: if (composite != null) {
0643: composite.addChild(constraint);
0644: }
0645:
0646: if (iTokens.hasNext()) {
0647: token = iTokens.next();
0648: if (token.equals(")")) {
0649: // Start of (simple or composite) constraint
0650: // between parenthesis.
0651: break;
0652: }
0653: int logicalOperator = 0;
0654: if (token.equalsIgnoreCase("OR")) {
0655: logicalOperator = CompositeConstraint.LOGICAL_OR;
0656: } else if (token.equalsIgnoreCase("AND")) {
0657: logicalOperator = CompositeConstraint.LOGICAL_AND;
0658: } else {
0659: throw new IllegalArgumentException(
0660: "Unexpected token (expected \"AND\" or \"OR\"): \""
0661: + token + "\"");
0662: }
0663: if (composite == null) {
0664: composite = new BasicCompositeConstraint(
0665: logicalOperator).addChild(constraint);
0666: }
0667:
0668: if (composite.getLogicalOperator() != logicalOperator) {
0669: composite = new BasicCompositeConstraint(
0670: logicalOperator).addChild(composite);
0671: }
0672:
0673: if (!iTokens.hasNext()) {
0674: throw new IllegalArgumentException(
0675: "Unexpected end of tokens after \"" + token
0676: + "\"");
0677: }
0678: }
0679: }
0680: if (composite != null) {
0681: return composite;
0682: } else {
0683: return constraint;
0684: }
0685: }
0686:
0687: /**
0688: * Parses a <em>simple-SQL-search-condition</em> string from list of tokens,
0689: * and produces a corresponding <code>BasicConstraint</code> object.
0690: * <p>
0691: * See {@link ConstraintParser above} for the format of a
0692: * <em>simple-SQL-search-condition</em>
0693: *
0694: * @param iTokens Tokens iterator, must be positioned before the (first)
0695: * token representing the condition.
0696: * @return The constraint.
0697: */
0698: // package visibility!
0699: BasicConstraint parseSimpleCondition(ListIterator<String> iTokens) {
0700: BasicConstraint result = null;
0701:
0702: String token = iTokens.next();
0703: if (token.equalsIgnoreCase("StringSearch")) {
0704: // StringSearch constraint.
0705: return parseStringSearchCondition(iTokens);
0706: }
0707:
0708: String function = token.toUpperCase();
0709: if (function.equals("LOWER") || function.equals("UPPER")) {
0710: if (iTokens.next().equals("(")) {
0711: // Function.
0712: token = iTokens.next();
0713: } else {
0714: // Not a function.
0715: iTokens.previous();
0716: function = null;
0717: }
0718: } else {
0719: function = null;
0720: }
0721:
0722: StepField field = getField(token);
0723:
0724: token = iTokens.next();
0725: if (function != null) {
0726: if (!token.equals(")")) {
0727: throw new IllegalArgumentException(
0728: "Unexpected token (expected \")\"): \"" + token
0729: + "\"");
0730: }
0731: token = iTokens.next();
0732: }
0733:
0734: boolean inverse = false;
0735: if (token.equalsIgnoreCase("NOT")) {
0736: // NOT LIKE/NOT IN/NOT BETWEEN
0737: inverse = true;
0738: token = iTokens.next();
0739: if (!token.equalsIgnoreCase("LIKE")
0740: && !token.equalsIgnoreCase("IN")
0741: && !token.equalsIgnoreCase("BETWEEN")) {
0742: throw new IllegalArgumentException(
0743: "Unexpected token (expected "
0744: + "\"LIKE\" OR \"IN\" OR \"BETWEEN\"): \""
0745: + token + "\"");
0746: }
0747: }
0748:
0749: if (token.equalsIgnoreCase("LIKE")) {
0750: // LIKE 'value'
0751: String value = (String) parseValue(iTokens, field);
0752: boolean caseSensitive = true;
0753: if (function != null) {
0754: if ((function.equals("LOWER") && value.equals(value
0755: .toLowerCase()))
0756: || (function.equals("UPPER") && value
0757: .equals(value.toUpperCase()))) {
0758: caseSensitive = false;
0759: }
0760: }
0761: result = new BasicFieldValueConstraint(field, value)
0762: .setOperator(FieldCompareConstraint.LIKE)
0763: .setCaseSensitive(caseSensitive);
0764:
0765: } else if (token.equalsIgnoreCase("IS")) {
0766: // IS [NOT] NULL
0767: token = iTokens.next();
0768: if (token.equalsIgnoreCase("NOT")) {
0769: inverse = !inverse;
0770: token = iTokens.next();
0771: }
0772: if (token.equalsIgnoreCase("NULL")) {
0773: result = new BasicFieldNullConstraint(field);
0774: } else {
0775: throw new IllegalArgumentException(
0776: "Unexpected token (expected \"NULL\"): \""
0777: + token + "\"");
0778: }
0779: } else if (token.equalsIgnoreCase("IN")) {
0780: // IN (value1, value2, ...)
0781: String separator = iTokens.next();
0782: if (!separator.equals("(")) {
0783: throw new IllegalArgumentException(
0784: "Unexpected token (expected \"(\"): \""
0785: + separator + "\"");
0786: }
0787: BasicFieldValueInConstraint fieldValueInConstraint = new BasicFieldValueInConstraint(
0788: field);
0789: if (!iTokens.next().equals(")")) {
0790:
0791: iTokens.previous();
0792: do {
0793: Object value = parseValue(iTokens, field);
0794: separator = iTokens.next();
0795: if (separator.equals(",") || separator.equals(")")) {
0796: fieldValueInConstraint.addValue(value);
0797: } else {
0798: throw new IllegalArgumentException(
0799: "Unexpected token (expected \",\" or \")\"): \""
0800: + separator + "\"");
0801: }
0802: } while (separator.equals(","));
0803: }
0804: result = fieldValueInConstraint;
0805:
0806: } else if (token.equalsIgnoreCase("BETWEEN")) {
0807: // BETWEEN value1 AND value2
0808: Object value1 = parseValue(iTokens, field);
0809: String separator = iTokens.next();
0810: if (!separator.equals("AND")) {
0811: throw new IllegalArgumentException(
0812: "Unexpected token (expected \"AND\"): \""
0813: + separator + "\"");
0814: }
0815: Object value2 = parseValue(iTokens, field);
0816: boolean caseSensitive = true;
0817: if (function != null && value1 instanceof String
0818: && value2 instanceof String) {
0819: String strValue1 = (String) value1;
0820: String strValue2 = (String) value2;
0821: if ((function.equals("LOWER")
0822: && strValue1.equals(strValue1.toLowerCase()) && strValue2
0823: .equals(strValue2.toLowerCase()))
0824: || (function.equals("UPPER")
0825: && strValue1.equals(strValue1
0826: .toUpperCase()) && strValue2
0827: .equals(strValue2.toUpperCase()))) {
0828: caseSensitive = false;
0829: }
0830: }
0831:
0832: BasicFieldValueBetweenConstraint fieldValueBetweenConstraint = (BasicFieldValueBetweenConstraint) new BasicFieldValueBetweenConstraint(
0833: field, value1, value2)
0834: .setCaseSensitive(caseSensitive);
0835: result = fieldValueBetweenConstraint;
0836:
0837: } else if (token.equals("=")) {
0838: token = iTokens.next();
0839: if (token.equals("=")) {
0840: try {
0841: // == value
0842: Object value = parseValue(iTokens, field);
0843: result = new BasicFieldValueConstraint(field, value)
0844: .setOperator(FieldCompareConstraint.EQUAL);
0845: } catch (NumberFormatException e) {
0846: // == field2
0847: iTokens.previous();
0848: token = iTokens.next();
0849: StepField field2 = getField(token);
0850: result = new BasicCompareFieldsConstraint(field,
0851: field2)
0852: .setOperator(FieldCompareConstraint.EQUAL);
0853: }
0854: } else {
0855: iTokens.previous();
0856: try {
0857: // = value
0858: Object value = parseValue(iTokens, field);
0859: boolean caseSensitive = true;
0860: if (function != null && value instanceof String) {
0861: String strValue = (String) value;
0862: if ((function.equals("LOWER") && strValue
0863: .equals(strValue.toLowerCase()))
0864: || (function.equals("UPPER") && strValue
0865: .equals(strValue.toUpperCase()))) {
0866: caseSensitive = false;
0867: }
0868: }
0869: result = new BasicFieldValueConstraint(field, value)
0870: .setOperator(FieldCompareConstraint.EQUAL)
0871: .setCaseSensitive(caseSensitive);
0872: } catch (NumberFormatException e) {
0873: // = field2
0874: iTokens.previous();
0875: token = iTokens.next();
0876: StepField field2 = getField(token);
0877: result = new BasicCompareFieldsConstraint(field,
0878: field2)
0879: .setOperator(FieldCompareConstraint.EQUAL);
0880: }
0881: }
0882: } else if (token.equals("<")) {
0883: token = iTokens.next();
0884: if (token.equals("=")) {
0885: try {
0886: // <= value
0887: Object value = parseValue(iTokens, field);
0888: result = new BasicFieldValueConstraint(field, value)
0889: .setOperator(FieldCompareConstraint.LESS_EQUAL);
0890: } catch (NumberFormatException e) {
0891: // <= field2
0892: iTokens.previous();
0893: token = iTokens.next();
0894: StepField field2 = getField(token);
0895: result = new BasicCompareFieldsConstraint(field,
0896: field2)
0897: .setOperator(FieldCompareConstraint.LESS_EQUAL);
0898: }
0899: } else if (token.equals(">")) {
0900: try {
0901: // <> value
0902: Object value = parseValue(iTokens, field);
0903: result = new BasicFieldValueConstraint(field, value)
0904: .setOperator(FieldCompareConstraint.NOT_EQUAL);
0905: } catch (NumberFormatException e) {
0906: // <> field2
0907: iTokens.previous();
0908: token = iTokens.next();
0909: StepField field2 = getField(token);
0910: result = new BasicCompareFieldsConstraint(field,
0911: field2)
0912: .setOperator(FieldCompareConstraint.NOT_EQUAL);
0913: }
0914: } else {
0915: try {
0916: // < value
0917: iTokens.previous();
0918: Object value = parseValue(iTokens, field);
0919: result = new BasicFieldValueConstraint(field, value)
0920: .setOperator(FieldCompareConstraint.LESS);
0921: } catch (NumberFormatException e) {
0922: // < field2
0923: iTokens.previous();
0924: token = iTokens.next();
0925: StepField field2 = getField(token);
0926: result = new BasicCompareFieldsConstraint(field,
0927: field2)
0928: .setOperator(FieldCompareConstraint.LESS);
0929: }
0930: }
0931: } else if (token.equals(">")) {
0932: token = iTokens.next();
0933: if (token.equals("=")) {
0934: try {
0935: // >= value
0936: Object value = parseValue(iTokens, field);
0937: result = new BasicFieldValueConstraint(field, value)
0938: .setOperator(FieldCompareConstraint.GREATER_EQUAL);
0939: } catch (NumberFormatException e) {
0940: // >= field2
0941: iTokens.previous();
0942: token = iTokens.next();
0943: StepField field2 = getField(token);
0944: result = new BasicCompareFieldsConstraint(field,
0945: field2)
0946: .setOperator(FieldCompareConstraint.GREATER_EQUAL);
0947: }
0948: } else {
0949: try {
0950: // > value
0951: iTokens.previous();
0952: Object value = parseValue(iTokens, field);
0953: result = new BasicFieldValueConstraint(field, value)
0954: .setOperator(FieldCompareConstraint.GREATER);
0955: } catch (NumberFormatException e) {
0956: // > field2
0957: iTokens.previous();
0958: token = iTokens.next();
0959: StepField field2 = getField(token);
0960: result = new BasicCompareFieldsConstraint(field,
0961: field2)
0962: .setOperator(FieldCompareConstraint.GREATER);
0963: }
0964: }
0965: } else if (token.equals("!")) {
0966: token = iTokens.next();
0967: if (token.equals("=")) {
0968: try {
0969: // != value
0970: Object value = parseValue(iTokens, field);
0971: result = new BasicFieldValueConstraint(field, value)
0972: .setOperator(FieldCompareConstraint.NOT_EQUAL);
0973: } catch (NumberFormatException e) {
0974: // != field2
0975: iTokens.previous();
0976: token = iTokens.next();
0977: StepField field2 = getField(token);
0978: result = new BasicCompareFieldsConstraint(field,
0979: field2)
0980: .setOperator(FieldCompareConstraint.NOT_EQUAL);
0981: }
0982: } else {
0983: throw new IllegalArgumentException(
0984: "Unexpected token (expected \"=\"): \"" + token
0985: + "\"");
0986: }
0987: } else {
0988: throw new IllegalArgumentException("Unexpected token: \""
0989: + token + "\"");
0990: }
0991:
0992: if (inverse) {
0993: result.setInverse(!result.isInverse());
0994: }
0995:
0996: return result;
0997: }
0998:
0999: /**
1000: * Parses a <em>stringsearch-condition</em> string from list of tokens,
1001: * and produces a corresponding <code>BasicStringSearchConstraint</code> object.
1002: * <p>
1003: * See {@link ConstraintParser above} for the format of a
1004: * <em>stringsearch-condition</em>
1005: *
1006: * @param iTokens Tokens iterator, must be positioned after the (first)
1007: * token representing the condition (e.g. after "StringSearch").
1008: * @return The constraint.
1009: */
1010: private BasicStringSearchConstraint parseStringSearchCondition(
1011: ListIterator<String> iTokens) {
1012:
1013: String token = iTokens.next();
1014: if (!token.equals("(")) {
1015: throw new IllegalArgumentException(
1016: "Unexpected token (expected \"(\"): \"" + token
1017: + "\"");
1018: }
1019:
1020: // Field
1021: token = iTokens.next();
1022: StepField field = getField(token);
1023:
1024: token = iTokens.next();
1025: if (!token.equals(",")) {
1026: throw new IllegalArgumentException(
1027: "Unexpected token (expected \",\"): \"" + token
1028: + "\"");
1029: }
1030:
1031: // Searchtype
1032: int searchType;
1033: token = iTokens.next();
1034: if (token.equalsIgnoreCase("PHRASE")) {
1035: searchType = StringSearchConstraint.SEARCH_TYPE_PHRASE_ORIENTED;
1036: } else if (token.equalsIgnoreCase("PROXIMITY")) {
1037: searchType = StringSearchConstraint.SEARCH_TYPE_PROXIMITY_ORIENTED;
1038: } else if (token.equalsIgnoreCase("WORD")) {
1039: searchType = StringSearchConstraint.SEARCH_TYPE_WORD_ORIENTED;
1040: } else {
1041: throw new IllegalArgumentException(
1042: "Invalid searchtype (expected \"PHRASE\", \"PROXIMITY\" or \"WORD\": \""
1043: + token + "\"");
1044: }
1045:
1046: token = iTokens.next();
1047: if (!token.equals(",")) {
1048: throw new IllegalArgumentException(
1049: "Unexpected token (expected \",\"): \"" + token
1050: + "\"");
1051: }
1052:
1053: // Matchtype
1054: int matchType;
1055: token = iTokens.next();
1056: if (token.equalsIgnoreCase("FUZZY")) {
1057: matchType = StringSearchConstraint.MATCH_TYPE_FUZZY;
1058: } else if (token.equalsIgnoreCase("LITERAL")) {
1059: matchType = StringSearchConstraint.MATCH_TYPE_LITERAL;
1060: } else if (token.equalsIgnoreCase("SYNONYM")) {
1061: matchType = StringSearchConstraint.MATCH_TYPE_SYNONYM;
1062: } else {
1063: throw new IllegalArgumentException(
1064: "Invalid matchtype (expected \"FUZZY\", \"LITERAL\" or \"SYNONYM\": \""
1065: + token + "\"");
1066: }
1067:
1068: token = iTokens.next();
1069: if (!token.equals(",")) {
1070: throw new IllegalArgumentException(
1071: "Unexpected token (expected \",\"): \"" + token
1072: + "\"");
1073: }
1074:
1075: // SearchTerms
1076: String searchTerms;
1077: token = iTokens.next();
1078: if (!token.equals("'")) {
1079: throw new IllegalArgumentException(
1080: "Unexpected token (expected \"'\" or \"\"\"): \""
1081: + token + "\"");
1082: }
1083: searchTerms = iTokens.next();
1084: token = iTokens.next();
1085: if (!token.equals("'")) {
1086: throw new IllegalArgumentException(
1087: "Unexpected token (expected \"'\" or \"\"\"): \""
1088: + token + "\"");
1089: }
1090:
1091: token = iTokens.next();
1092: if (!token.equals(",")) {
1093: throw new IllegalArgumentException(
1094: "Unexpected token (expected \",\"): \"" + token
1095: + "\"");
1096: }
1097:
1098: // CaseSensitive property
1099: boolean caseSensitive;
1100: token = iTokens.next();
1101: if (token.equalsIgnoreCase("true")) {
1102: caseSensitive = true;
1103: } else if (token.equalsIgnoreCase("false")) {
1104: caseSensitive = false;
1105: } else {
1106: throw new IllegalArgumentException(
1107: "Invalid caseSensitive value (expected \"true\" "
1108: + "or \"false\": \"" + token + "\"");
1109: }
1110:
1111: token = iTokens.next();
1112: if (!token.equals(")")) {
1113: throw new IllegalArgumentException(
1114: "Unexpected token (expected \")\"): \"" + token
1115: + "\"");
1116: }
1117:
1118: BasicStringSearchConstraint result = (BasicStringSearchConstraint) new BasicStringSearchConstraint(
1119: field, searchType, matchType, searchTerms)
1120: .setCaseSensitive(caseSensitive);
1121:
1122: // .set(parametername, value)
1123: while (iTokens.hasNext()) {
1124: token = iTokens.next();
1125: if (!token.equalsIgnoreCase(".set")) {
1126: iTokens.previous();
1127: break;
1128: }
1129:
1130: token = iTokens.next();
1131: if (!token.equals("(")) {
1132: throw new IllegalArgumentException(
1133: "Unexpected token (expected \"(\"): \"" + token
1134: + "\"");
1135: }
1136:
1137: String parameterName = iTokens.next();
1138:
1139: token = iTokens.next();
1140: if (!token.equals(",")) {
1141: throw new IllegalArgumentException(
1142: "Unexpected token (expected \",\"): \"" + token
1143: + "\"");
1144: }
1145:
1146: String parameterValue = iTokens.next();
1147:
1148: token = iTokens.next();
1149: if (!token.equals(")")) {
1150: throw new IllegalArgumentException(
1151: "Unexpected token (expected \")\"): \"" + token
1152: + "\"");
1153: }
1154:
1155: if (parameterName.equalsIgnoreCase("FUZZINESS")) {
1156: result.setParameter(
1157: StringSearchConstraint.PARAM_FUZZINESS, Float
1158: .valueOf(parameterValue));
1159: } else if (parameterName
1160: .equalsIgnoreCase("PROXIMITY_LIMIT")) {
1161: result.setParameter(
1162: StringSearchConstraint.PARAM_PROXIMITY_LIMIT,
1163: parameterValue);
1164: } else {
1165: throw new IllegalArgumentException(
1166: "Invalid parameter name (expected \"FUZZINESS\" or \"PROXIMITY\": \""
1167: + parameterName + "\"");
1168: }
1169: }
1170:
1171: return result;
1172: }
1173: }
|