/*
* Copyright 2009-2016 Tilmann Zaeschke. All rights reserved.
*
* This file is part of ZooDB.
*
* ZooDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ZooDB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ZooDB. If not, see <http://www.gnu.org/licenses/>.
*
* See the README and COPYING files for further information.
*/
package org.zoodb.internal.query;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.zoodb.api.impl.ZooPC;
import org.zoodb.internal.ZooClassDef;
import org.zoodb.internal.ZooFieldDef;
import org.zoodb.internal.query.QueryParameter.DECLARATION;
import org.zoodb.internal.query.QueryParser.COMP_OP;
import org.zoodb.internal.query.QueryParser.LOG_OP;
import org.zoodb.internal.util.DBLogger;
import org.zoodb.internal.util.Pair;
/**
* The query parser. This class builds a query tree from a query string.
* The tree consists of QueryTerms (comparative statements) and QueryNodes (logical operations on
* two children (QueryTerms or QueryNodes).
* The root of the tree is a QueryNode. QueryNodes may have only a single child.
*
* Negation is implemented by simply negating all operators inside the negated term.
*
* TODO QueryOptimiser:
* E.g. "((( A==B )))"Will create something like Node(Node(Node(Term))). Optimize this to
* Node(Term). That means pulling up all terms where the parent node has no other children. The
* only exception is the root node, which is allowed to have only one child.
*
* @author Tilmann Zaeschke
*/
public final class QueryParserV2 {
private int pos = 0;
private String str; //TODO final
private final ZooClassDef clsDef;
private final Map<String, ZooFieldDef> fields;
private final List<QueryParameter> parameters;
private final List<Pair<ZooFieldDef, Boolean>> order;
private ArrayList<Token> tokens;
private int tPos = 0;
public QueryParserV2(String query, ZooClassDef clsDef, List<QueryParameter> parameters,
List<Pair<ZooFieldDef, Boolean>> order) {
this.str = query;
this.clsDef = clsDef;
this.fields = clsDef.getAllFieldsAsMap();
this.parameters = parameters;
this.order = order;
}
/**
* @param c
* @return true if c is a whitespace character
*/
private static boolean isWS(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f';
}
private char charAt0() {
return str.charAt(pos);
}
private char charAt(int i) {
return str.charAt(pos + i);
}
private Token token() {
if (tPos >= tokens.size()) {
throw DBLogger.newUser("Parsing error: unexpected end at pos " +
tokens.get(tokens.size()-1).pos + " input=" + str);
}
return tokens.get(tPos);
}
private Token token(int i) {
return tokens.get(tPos + i);
}
private void tInc() {
tPos++;
}
private void tInc(int i) {
tPos += i;
}
private boolean match(String str) {
return str.equals(token().str);
}
private boolean match(T_TYPE type) {
Token t = token();
return (type == t.type || type.matches(t));
}
private boolean match(int offs, T_TYPE type) {
Token t = token(offs);
return (type == t.type || type.matches(t));
}
private boolean hasMoreTokens() {
return tPos < tokens.size();
}
private void inc() {
pos++;
}
private void inc(int i) {
pos += i;
}
private int pos() {
return pos;
}
private boolean isFinished() {
return !(pos < str.length());
}
/**
*
* @param ofs
* @return Whether the string is finished after the givven offset
*/
private boolean isFinished(int ofs) {
return !(pos + ofs < str.length());
}
/**
*
* @param pos0 start, absolute position, inclusive
* @param pos1 end, absolute position, exclusive
* @return sub-String
*/
private String substring(int pos0, int pos1) {
if (pos1 > str.length()) {
throw DBLogger.newUser("Unexpected end of query: '" + str.substring(pos0,
str.length()) + "' at: " + pos() + " query=" + str);
}
return str.substring(pos0, pos1);
}
public QueryTreeNode parseQuery() {
tokens = tokenize(str);
if (match(T_TYPE.SELECT)) {
tInc();
}
if (match(T_TYPE.UNIQUE)) {
//TODO set UNIQUE!
tInc();
}
if (match(T_TYPE.FROM)) {
tInc();
if (!clsDef.getClassName().equals(token().str)) {
//TODO class name
throw DBLogger.newUser("Class mismatch: " + token().pos + " / " + clsDef);
}
tInc();
}
if (match(T_TYPE.WHERE)) {
tInc();
}
//Negation is used to invert negated operand.
//We just pass it down the tree while parsing, always inverting the flag if a '!' is
//encountered. When popping out of a function, the flag is reset to the value outside
//the term that was parsed in a function. Actually, it is not reset, it is never modified.
boolean negate = false;
QueryTreeNode qn = parseTree(negate);
while (hasMoreTokens()) {
qn = parseTree(null, qn, negate);
}
return qn;
}
private QueryTreeNode parseTree(boolean negate) {
while (match(T_TYPE.L_NOT)) {
negate = !negate;
tInc();
}
QueryTerm qt1 = null;
QueryTreeNode qn1 = null;
if (match(T_TYPE.OPEN)) {
tInc();
qn1 = parseTree(negate);
} else {
qt1 = parseTerm(negate);
}
if (!hasMoreTokens()) {
return new QueryTreeNode(qn1, qt1, null, null, null, negate).relateToChildren();
}
return parseTree(qt1, qn1, negate);
}
private QueryTreeNode parseTree(QueryTerm qt1, QueryTreeNode qn1, boolean negate) {
//parse log op
if (match(T_TYPE.CLOSE)) {
tInc();
if (qt1 == null) {
return qn1;
} else {
return new QueryTreeNode(qn1, qt1, null, null, null, negate);
}
}
LOG_OP op = null;
if (match(T_TYPE.L_AND)) {
tInc();
op = LOG_OP.AND;
} else if (match(T_TYPE.L_OR)) {
tInc();
op = LOG_OP.OR;
} else if (match(T_TYPE.PARAMETERS)) {
tInc();
parseParameters();
if (qt1 == null) {
return qn1;
} else {
return new QueryTreeNode(qn1, qt1, null, null, null, negate);
}
} else if (match(T_TYPE.VARIABLES)) {
throw new UnsupportedOperationException("JDO feature not supported: VARIABLES");
} else if (match(T_TYPE.IMPORTS)) {
throw new UnsupportedOperationException("JDO feature not supported: IMPORTS");
} else if (match(T_TYPE.GROUP) && match(1, T_TYPE.BY)) {
throw new UnsupportedOperationException("JDO feature not supported: GROUP BY");
} else if (match(T_TYPE.ORDER) && match(1, T_TYPE.BY)) {
tInc(2);
parseOrdering();
if (qt1 == null) {
return qn1;
} else {
return new QueryTreeNode(qn1, qt1, null, null, null, negate);
}
} else if (match(T_TYPE.RANGE)) {
throw new UnsupportedOperationException("JDO feature not supported: RANGE");
} else {
throw DBLogger.newUser("Unexpected characters: '" + token().msg() + "' at: " +
token().pos + " query=" + str);
}
//check negations
boolean negateNext = negate;
while (match(T_TYPE.L_NOT)) {
negateNext = !negateNext;
tInc();
}
// read next term
QueryTerm qt2 = null;
QueryTreeNode qn2 = null;
if (match(T_TYPE.OPEN)) {
tInc();
qn2 = parseTree(negateNext);
} else {
qt2 = parseTerm(negateNext);
}
return new QueryTreeNode(qn1, qt1, op, qn2, qt2, negate);
}
private QueryTerm parseTerm(boolean negate) {
//read field name
if (match("this") && match(1, T_TYPE.DOT)) {
tInc(2);
}
String lhsFName = token().str;
ZooFieldDef lhsFieldDef = fields.get(lhsFName);
if (lhsFieldDef == null) {
throw DBLogger.newUser(
"Field name not found: '" + lhsFName + "' in " + clsDef.getClassName());
}
Class<?> lhsType = null;
try {
lhsType = lhsFieldDef.getJavaType();
if (lhsType == null) {
throw DBLogger.newUser(
"Field name not found: '" + lhsFName + "' in " + clsDef.getClassName());
}
} catch (SecurityException e) {
throw DBLogger.newUser("Field not accessible: " + lhsFName, e);
}
tInc();
//read operator
boolean requiresParenthesis = false;
COMP_OP op = null;
switch (token().type) {
case EQ: op = COMP_OP.EQ; break;
case LE: op = COMP_OP.LE; break;
case L: op = COMP_OP.L; break;
case GE: op = COMP_OP.AE; break;
case G: op = COMP_OP.A; break;
case NE: op = COMP_OP.NE; break;
case DOT:
if (lhsFieldDef.isPersistentType()) {
// //TODO follow references
// QueryFunction fn = new QueryFunction.Path(lhsFName, lhsFieldDef, lhsFieldDef.getJavaField());
// if (lhsFn == null) {
// lhsFn = fn;
// } else {
// lhsFn.setInner(fn);
// }
throw new UnsupportedOperationException("Path queries are currently not supported");
} else {
requiresParenthesis = true;
op=parseFunctions(lhsFieldDef);
if (op != null) {
tInc();
if (!match(T_TYPE.OPEN)) {
throw DBLogger.newUser("Expected '(' at " + token().pos + " but got: \"" +
token().str + "\" query=" + str);
}
}
if (op.argCount() == 0) {
tInc();
if (!match(T_TYPE.CLOSE)) {
throw DBLogger.newUser("Expected '(' at " + token().pos + " but got: \"" +
token().str + "\" query=" + str);
}
tInc();
return new QueryTerm(null, lhsFieldDef, null, op,
null, null, null, null, negate);
}
}
break;
default:
throw DBLogger.newUser("Error: Comparator expected at pos " + token().pos + ": " + str);
}
if (op == null) {
throw DBLogger.newUser("Unexpected token: '" + token().msg() + "' at: " + token().pos);
}
tInc();
//read value
Object rhsValue = null;
String rhsParamName = null;
ZooFieldDef rhsFieldDef = null;
if (match(T_TYPE.NULL)) {
if (lhsType.isPrimitive()) {
throw DBLogger.newUser("Cannot compare 'null' to primitive at pos:" + token().pos);
}
rhsValue = QueryTerm.NULL;
tInc();
} else if (match(T_TYPE.STRING)) {
//TODO allow char type!
if (!(String.class.isAssignableFrom(lhsType) ||
Collection.class.isAssignableFrom(lhsType) ||
Map.class.isAssignableFrom(lhsType))) {
throw DBLogger.newUser("Incompatible types, found String, expected: " +
lhsType.getName());
}
rhsValue = token().str;
tInc();
} else if (match(T_TYPE.NUMBER)) {
String nStr = token().str;
int base = 10;
if (nStr.contains("x")) {
base = 16;
nStr = nStr.substring(2);
} else if (nStr.contains("b")) {
base = 2;
nStr = nStr.substring(2);
}
if (lhsType == Double.TYPE || lhsType == Double.class) {
rhsValue = Double.parseDouble( nStr );
} else if (lhsType == Float.TYPE || lhsType == Float.class) {
rhsValue = Float.parseFloat( nStr );
} else if (lhsType == Long.TYPE || lhsType == Long.class) {
rhsValue = Long.parseLong( nStr, base );
} else if (lhsType == Integer.TYPE || lhsType == Integer.class) {
rhsValue = Integer.parseInt( nStr, base );
} else if (lhsType == Short.TYPE || lhsType == Short.class) {
rhsValue = Short.parseShort( nStr, base );
} else if (lhsType == Byte.TYPE || lhsType == Byte.class) {
rhsValue = Byte.parseByte( nStr, base );
} else if (lhsType == BigDecimal.class) {
rhsValue = new BigDecimal( nStr );
} else if (lhsType == BigInteger.class) {
rhsValue = new BigInteger( nStr );
} else if (Collection.class.isAssignableFrom(lhsType) ||
Map.class.isAssignableFrom(lhsType)) {
rhsValue = parseNumber(nStr, base);
} else {
throw DBLogger.newUser("Incompatible types, found number, expected: " + lhsType);
}
tInc();
} else if (lhsType == Boolean.TYPE || lhsType == Boolean.class) {
if (match(T_TYPE.TRUE)) {
rhsValue = true;
} else if (match(T_TYPE.FALSE)) {
rhsValue = false;
} else {
throw DBLogger.newUser("Incompatible types, expected Boolean, found: " +
token().msg());
}
tInc();
} else {
boolean isImplicit = match(T_TYPE.COLON);
if (isImplicit) {
tInc();
rhsParamName = token().str;
addImplicitParameter(lhsType, rhsParamName);
} else {
String rhsFName = token().str;
rhsFieldDef = fields.get(rhsFName);
if (rhsFieldDef != null) {
try {
Class<?> rhsType = rhsFieldDef.getJavaType();
if (rhsType == null) {
throw DBLogger.newUser("Field name not found: '" + rhsFName +
"' in " + clsDef.getClassName());
}
} catch (SecurityException e) {
throw DBLogger.newUser("Field not accessible: " + rhsFName, e);
}
} else {
//okay, not a field, let's assume this is a parameter...
rhsParamName = token().str;
addParameter(null, rhsParamName);
}
}
tInc();
}
if (rhsValue == null && rhsParamName == null && rhsFieldDef == null) {
System.out.println("t=" + token(-1).type);
throw DBLogger.newUser("Cannot parse query at " + token().pos + ", got: \"" +
token().str + "\"" + token().type + " query=" + str);
}
if (requiresParenthesis) {
if (!match(T_TYPE.CLOSE)) {
throw DBLogger.newUser("Expected ')' at " + token().pos + " but got: \"" +
token().str + "\" query=" + str);
}
tInc();
}
return new QueryTerm(null, lhsFieldDef, null, op,
rhsParamName, rhsValue, rhsFieldDef, null, negate);
}
private Object parseNumber(String nStr, int base) {
int len = nStr.length();
if (nStr.indexOf('.') >= 0) {
if (nStr.charAt(len-1) == 'f' || nStr.charAt(len-1) == 'F') {
return Float.parseFloat(nStr.substring(0, len-2));
}
return Double.parseDouble(nStr);
}
if (nStr.charAt(len-1) == 'l' || nStr.charAt(len-1) == 'L') {
return Long.parseLong(nStr.substring(0, len-2), base);
}
return Integer.parseInt(nStr, base);
}
private COMP_OP parseFunctions(ZooFieldDef field) {
tInc();
if (field.isPersistentType()) {
//TODO follow references
throw new UnsupportedOperationException("Path queries are currently not supported");
}
Token t = token();
Field f = field.getJavaField();
if (String.class == f.getType()) {
switch (t.str) {
case "contains": return COMP_OP.STR_contains_NON_JDO;
case "matches": return COMP_OP.STR_matches;
case "startsWith": return COMP_OP.STR_startsWith;
case "endsWith": return COMP_OP.STR_endsWith;
}
}
if (Map.class.isAssignableFrom(f.getType())) {
switch (t.str) {
case "containsKey": return COMP_OP.MAP_containsKey;
case "containsValue": return COMP_OP.MAP_containsValue;
case "isEmpty": return COMP_OP.MAP_isEmpty;
case "size": return COMP_OP.MAP_size;
case "get": return COMP_OP.MAP_get;
}
}
if (List.class.isAssignableFrom(f.getType())) {
switch (t.str) {
case "get": return COMP_OP.LIST_get;
}
}
if (Collection.class.isAssignableFrom(f.getType())) {
switch (t.str) {
case "contains": return COMP_OP.COLL_contains;
case "isEmpty": return COMP_OP.COLL_isEmpty;
case "size": return COMP_OP.COLL_size;
}
}
throw DBLogger.newUser("Cannot parse query at " + token().pos + ": " + token().msg());
}
private void parseParameters() {
while (hasMoreTokens()) {
String typeName = token().str;
tInc();
String paramName = token().str;
tInc();
updateParameterType(typeName, paramName);
if (!hasMoreTokens() || !match(T_TYPE.COMMA)) {
return;
}
tInc(); // COMMA
}
}
private QueryParameter addImplicitParameter(Class<?> type, String name) {
for (int i = 0; i < parameters.size(); i++) {
if (parameters.get(i).getName().equals(name)) {
throw DBLogger.newUser("Duplicate parameter name: " + name);
}
}
QueryParameter param = new QueryParameter(type, name, DECLARATION.IMPLICIT);
this.parameters.add(param);
return param;
}
private void addParameter(Class<?> type, String name) {
for (QueryParameter p: parameters) {
if (p.getName().equals(name)) {
throw DBLogger.newUser("Duplicate parameter name: " + name);
}
}
this.parameters.add(new QueryParameter(type, name, QueryParameter.DECLARATION.UNDECLARED));
}
private void updateParameterType(String typeName, String name) {
for (QueryParameter p: parameters) {
if (p.getName().equals(name)) {
if (p.getDeclaration() != DECLARATION.UNDECLARED) {
throw DBLogger.newUser("Duplicate parameter name: " + name);
}
Class<?> type = QueryParser.locateClassFromShortName(typeName);
p.setType(type);
if (ZooPC.class.isAssignableFrom(type)) {
//TODO we should have a local session field here...
ZooClassDef typeDef = clsDef.getProvidedContext().getSession(
).getSchemaManager().locateSchema(typeName).getSchemaDef();
p.setTypeDef(typeDef);
}
p.setDeclaration(DECLARATION.PARAMETERS);
return;
}
}
throw DBLogger.newUser("Parameter not used in query: " + name);
}
private void parseOrdering() {
order.clear();
while (true) {
String attrName = token().str;
ZooFieldDef f = fields.get(attrName);
if (f == null) {
throw DBLogger.newUser(
"Field '" + attrName + "' not found at position " + token().pos + " - " +
token().msg());
}
if (!f.isPrimitiveType() && !f.isString()) {
throw DBLogger.newUser("Field not sortable: " + f);
}
for (Pair<ZooFieldDef, Boolean> p2: order) {
if (p2.getA().equals(f)) {
throw DBLogger.newUser("Parse error, field '" + f + "' is sorted twice near "
+ "position " + token().pos + " input=" + str);
}
}
tInc();
if (match(T_TYPE.ASC) || match(T_TYPE.ASCENDING)) {
order.add(new Pair<ZooFieldDef, Boolean>(f, true));
} else if (match(T_TYPE.DESC) || match(T_TYPE.DESCENDING)) {
order.add(new Pair<ZooFieldDef, Boolean>(f, false));
} else {
throw DBLogger.newUser("Parse error at position " + token().pos);
}
tInc();
if (!hasMoreTokens() || !match(T_TYPE.COMMA)) {
return;
}
tInc(); //comma
}
}
public static void parseOrdering(final String input, int pos,
List<Pair<ZooFieldDef, Boolean>> ordering, ZooClassDef candClsDef) {
ordering.clear();
if (input == null) {
return;
}
QueryParserV2 p2 = new QueryParserV2(input, candClsDef, null, ordering);
p2.str = input;
p2.tokens = p2.tokenize(input);
if (p2.tokens.isEmpty()) {
return;
}
p2.parseOrdering();
if (p2.hasMoreTokens()) {
throw DBLogger.newUser("Unexpected characters at pos " + p2.token().pos + ": " +
p2.token().msg());
}
ordering.addAll(p2.order);
}
private static enum T_TYPE {
L_AND, L_OR, L_NOT,
B_AND, B_OR, B_NOT,
EQ, NE, LE, GE, L, G,
PLUS, MINUS, MUL, DIV, MOD,
OPEN, CLOSE, // ( + )
COMMA, DOT, COLON, //QUOTE, DQUOTE,
AVG, SUM, MIN, MAX, COUNT,
//PARAM, PARAM_IMPLICIT,
SELECT, UNIQUE, INTO, FROM, WHERE,
ASC, DESC, ASCENDING, DESCENDING, TO,
PARAMETERS, VARIABLES, IMPORTS, GROUP, ORDER, BY, RANGE,
J_STR_STARTS_WITH, J_STR_ENDSWITH,
J_COL_CONTAINS,
F_NAME, STRING, NUMBER, TRUE, FALSE, NULL,
//MAP
CONTAINSKEY, CONTAINSVALUE,
LETTERS; //fieldName, paramName, JDOQL keyword
private final String str;
T_TYPE(String str) {
this.str = str;
}
T_TYPE() {
this.str = this.name();
}
public boolean matches(Token token) {
if (token.str != null && token.str.toUpperCase().equals(str)) {
return true;
}
return false;
}
}
private static class Token {
final T_TYPE type;
final String str;
final int pos;
public Token(T_TYPE type, String str, int pos) {
this.type = type;
this.str = str;
this.pos = pos;
}
public Token(T_TYPE type, int pos) {
this(type, null, pos);
}
@Override
public String toString() {
return type.name() + " - \"" + str + "\" -- " + pos;
}
public String msg() {
return str == null ? type.name() : str;
}
}
public ArrayList<Token> tokenize(String query) {
ArrayList<Token> r = new ArrayList<>();
pos = 0;
str = query;
while (pos() < query.length()) {
skipWS();
if (isFinished()) {
break;
}
r.add( readToken() );
}
return r;
}
private Token readToken() {
Token t = null;
//first: check single-chars
char c = charAt0();
switch (c) {
case '(': t = new Token(T_TYPE.OPEN, pos); break;
case ')': t = new Token(T_TYPE.CLOSE, pos); break;
case ',': t = new Token(T_TYPE.COMMA, pos); break;
case ':': t = new Token(T_TYPE.COLON, pos); break;
case '.': t = new Token(T_TYPE.DOT, pos); break;
case '~': t = new Token(T_TYPE.B_NOT, pos); break;
case '+': t = new Token(T_TYPE.PLUS, pos); break;
// case '-': t = new Token(T_TYPE.MINUS, pos); break;
case '*': t = new Token(T_TYPE.MUL, pos); break;
case '/': t = new Token(T_TYPE.DIV, pos); break;
case '%': t = new Token(T_TYPE.MOD, pos); break;
// case '&': t = new Token(TOKEN.B_AND, pos); break;
// case '|': t = new Token(TOKEN.B_OR, pos); break;
// case '!': t = new Token(TOKEN.NOT, pos); break;
// case '>': t = new Token(TOKEN.G, pos); break;
// case '<': t = new Token(TOKEN.L, pos); break;
}
if (t != null) {
inc();
return t;
}
if (isFinished()) {
throw DBLogger.newUser("Error parsing query at pos " + pos + ": " + str);
}
if (!isFinished(1)) {
char c2 = charAt(1);
String tok = "" + c + c2;
switch (tok) {
case "&&": t = new Token(T_TYPE.L_AND, pos); break;
case "||": t = new Token(T_TYPE.L_OR, pos); break;
case "==" : t = new Token(T_TYPE.EQ, pos); break;
case "!=": t = new Token(T_TYPE.NE, pos); break;
case "<=" : t = new Token(T_TYPE.LE, pos); break;
case ">=": t = new Token(T_TYPE.GE, pos); break;
}
if (t != null) {
inc(2);
return t;
}
}
//separate check to avoid collision with 2-chars
switch (c) {
case '&': t = new Token(T_TYPE.B_AND, pos); break;
case '|': t = new Token(T_TYPE.B_OR, pos); break;
case '!': t = new Token(T_TYPE.L_NOT, pos); break;
case '>': t = new Token(T_TYPE.G, pos); break;
case '<': t = new Token(T_TYPE.L, pos); break;
}
if (t != null) {
inc();
return t;
}
if (isFinished()) {
throw DBLogger.newUser("Error parsing query at pos " + pos + ": " + str);
}
if (c >= '0' && c <= '9') {
return parseNumber();
}
if (c=='-') {
inc();
c = charAt0();
if (c >= '0' && c <= '9') {
return parseNumber();
}
return new Token(T_TYPE.MINUS, pos);
}
if (c=='"' || c=='\'') {
return parseString();
}
if (c==':') {
return parseFieldOrParam();
}
if (c=='t' || c=='T' || c=='F' || c=='F') {
t = parseBoolean();
if (t != null) {
return t;
}
}
if (c=='n' || c=='N') {
t = parseNull();
if (t != null) {
return t;
}
}
return parseFieldOrParam();
}
private Token parseNumber() {
char c = charAt0();
String v = "";
if (c=='-') {
v += c;
pos++;
}
int pos0 = pos();
int base = 10;
while (!isFinished()) {
c = charAt0();
if ((c >= '0' && c <= '9') || c=='x' || c=='b' || c==',' || c=='.' ||
c=='L' || c=='l' || c=='F' || c=='f' ||
(base==16 && ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))) {
switch (c) {
case 'x' : base = 16; break;
case 'b' : base = 2; break;
}
v += c;
inc();
continue;
}
break;
}
return new Token(T_TYPE.NUMBER, v, pos0);
}
private Token parseString() {
//According to JDO 2.2 14.6.2, String and single characters can both be delimited by
//both single and double quotes.
char c = charAt0();
boolean singleQuote = c == '\'';
inc();
int pos0 = pos();
c = charAt0();
while (true) {
if ( (!singleQuote && c=='"') || (singleQuote && c=='\'')) {
break;
} else if (c=='\\') {
inc();
if (isFinished(pos()+1)) {
throw DBLogger.newUser("Try using \\\\\\\\ for double-slashes.");
}
}
inc();
c = charAt0();
}
String value = substring(pos0, pos());
Token t = new Token(T_TYPE.STRING, value, pos0);
inc();
return t;
}
private Token parseFieldOrParam() {
int pos0 = pos();
char c = charAt0();
while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
(c=='_')) {
inc();
if (isFinished()) break;
c = charAt0();
}
String paramName = substring(pos0, pos());
if (paramName.length() == 0) {
throw DBLogger.newUser("Cannot parse query at " + pos + ": " + c);
}
return new Token(T_TYPE.LETTERS, paramName, pos0);
}
private Token parseBoolean() {
int pos0 = pos();
if (substring(pos0, pos0+4).toLowerCase().equals("true")
&& (isFinished(4) || isWS(charAt(4)) || charAt(4)==')')) {
inc(4);
return new Token(T_TYPE.TRUE, pos0);
} else if (substring(pos0, pos0+5).toLowerCase().equals("false")
&& (isFinished(5) || isWS(charAt(5)) || charAt(5)==')')) {
inc(5);
return new Token(T_TYPE.FALSE, pos0);
}
return null; //not a boolean...
}
private Token parseNull() {
int pos0 = pos();
if (substring(pos0, pos0+4).toLowerCase().equals("null")
&& (isFinished(4) || isWS(charAt(4)) || charAt(4)==')')) {
inc(4);
return new Token(T_TYPE.NULL, pos0);
}
return null; //not a null...
}
private void skipWS() {
while (!isFinished() && isWS(charAt0())) {
inc();
}
return;
}
}