/*
* 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.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jdo.JDOUserException;
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.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 QueryParser {
private int pos = 0;
private final String str;
private final ZooClassDef clsDef;
private final Map<String, ZooFieldDef> fields;
private final List<QueryParameter> parameters;
private final List<Pair<ZooFieldDef, Boolean>> order;
public QueryParser(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;
}
private void trim() {
while (!isFinished() && isWS(charAt0())) {
pos++;
}
}
/**
* @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 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());
}
/**
* @return remaining length.
*/
private int len() {
return str.length() - pos;
}
/**
*
* @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() {
//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 (!isFinished()) {
qn = parseTree(null, qn, negate);
}
return qn;
}
private QueryTreeNode parseTree(boolean negate) {
trim();
while (charAt0() == '!') {
negate = !negate;
inc(LOG_OP.NOT._len);
trim();
}
QueryTerm qt1 = null;
QueryTreeNode qn1 = null;
if (charAt0() == '(') {
inc();
qn1 = parseTree(negate);
trim();
} else {
qt1 = parseTerm(negate);
}
if (isFinished()) {
return new QueryTreeNode(qn1, qt1, null, null, null, negate).relateToChildren();
}
return parseTree(qt1, qn1, negate);
}
private QueryTreeNode parseTree(QueryTerm qt1, QueryTreeNode qn1, boolean negate) {
trim();
//parse log op
char c = charAt0();
if (c == ')') {
inc(1);
trim();
if (qt1 == null) {
return qn1;
} else {
return new QueryTreeNode(qn1, qt1, null, null, null, negate);
}
}
char c2 = charAt(1);
LOG_OP op = null;
if (c == '&' && c2 == '&') {
op = LOG_OP.AND;
} else if (c == '|' && c2 == '|') {
op = LOG_OP.OR;
} else if (substring(pos, pos+10).toUpperCase().equals("PARAMETERS")) {
inc(10);
trim();
parseParameters();
if (qt1 == null) {
return qn1;
} else {
return new QueryTreeNode(qn1, qt1, null, null, null, negate);
}
} else if (substring(pos, pos+9).toUpperCase().equals("VARIABLES")) {
throw new UnsupportedOperationException("JDO feature not supported: VARIABLES");
} else if (substring(pos, pos+7).toUpperCase().equals("IMPORTS")) {
throw new UnsupportedOperationException("JDO feature not supported: IMPORTS");
} else if (substring(pos, pos+8).toUpperCase().equals("GROUP BY")) {
throw new UnsupportedOperationException("JDO feature not supported: GROUP BY");
} else if (substring(pos, pos+8).toUpperCase().equals("ORDER BY")) {
inc(8);
parseOrdering(str, pos, order, clsDef);
pos = str.length(); //isFinished()!
return qn1;
//TODO this fails if ORDER BY is NOT the last part of the query...
} else if (substring(pos, pos+5).toUpperCase().equals("RANGE")) {
throw new UnsupportedOperationException("JDO feature not supported: RANGE");
} else {
//throw DBLogger.newUser("Unexpected characters: '" + c + c2 + c3 + "' at: " + pos());
throw DBLogger.newUser("Unexpected characters: '" + str.substring(pos,
pos+3 < str.length() ? pos+3 : str.length()) + "' at: " + pos() +
" query=" + str);
}
inc( op._len );
trim();
//check negations
boolean negateNext = negate;
while (charAt0() == '!') {
negateNext = !negateNext;
inc(LOG_OP.NOT._len);
trim();
}
// read next term
QueryTerm qt2 = null;
QueryTreeNode qn2 = null;
if (charAt0() == '(') {
inc();
qn2 = parseTree(negateNext);
trim();
} else {
qt2 = parseTerm(negateNext);
}
return new QueryTreeNode(qn1, qt1, op, qn2, qt2, negate);
}
private QueryTerm parseTerm(boolean negate) {
trim();
Object value = null;
String paramName = null;
COMP_OP op = null;
String fName = null;
Class<?> type = null;
int pos0 = pos();
//read field name
char c = charAt0();
while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|| (c=='_') || (c=='.')) {
inc();
if (c=='.') {
String dummy = substring(pos0, pos());
if (dummy.equals("this.")) {
//System.out.println("STUB QueryParser.parseTerm(): Ignoring 'this.'.");
pos0 = pos();
} else {
fName = substring(pos0, pos()-1);
pos0 = pos();
//TODO
// if (startsWith("")) {
//
// } else if (startsWith("")) {
//
// } else {
// throw new JDOUserException("Cannot parse query at position " + pos0 +
// ": " + dummy);
// }
}
}
c = charAt0();
}
if (fName == null) {
fName = substring(pos0, pos());
}
if (fName.equals("")) {
throw DBLogger.newUser("Cannot parse query at position " + pos0 + ": '" + c +"'");
}
pos0 = pos();
trim();
ZooFieldDef fieldDef = fields.get(fName);
if (fieldDef == null) {
throw DBLogger.newUser(
"Field name not found: '" + fName + "' in " + clsDef.getClassName());
}
try {
type = fieldDef.getJavaType();
if (type == null) {
throw DBLogger.newUser(
"Field name not found: '" + fName + "' in " + clsDef.getClassName());
}
} catch (SecurityException e) {
throw DBLogger.newUser("Field not accessible: " + fName, e);
}
//read operator
c = charAt0();
char c2 = charAt(1);
char c3 = charAt(2);
if (c == '=' && c2 == '=') {
op = COMP_OP.EQ;
} else if (c == '<') {
if (c2 == '=') {
op = COMP_OP.LE;
} else {
op = COMP_OP.L;
}
} else if (c == '>') {
if (c2 == '=') {
op = COMP_OP.AE;
} else {
op = COMP_OP.A;
}
} else if (c == '!' && c2 == '=') {
op = COMP_OP.NE;
}
if (op == null) {
throw DBLogger.newUser("Unexpected characters: '" + c + c2 + c3 + "' at: " + pos0);
}
inc( op.name().length() );
trim();
pos0 = pos();
//read value
c = charAt0();
if ((len() >= 4 && substring(pos0, pos0+4).equals("null")) &&
(len() == 4 || (len()>4 && (charAt(4) == ' ' || charAt(4) == ')')))) { //hehehe :-)
if (type.isPrimitive()) {
throw DBLogger.newUser("Cannot compare 'null' to primitive at pos:" + pos0);
}
value = QueryTerm.NULL;
inc(4);
} else if (c=='"' || c=='\'') {
//According to JDO 2.2 14.6.2, String and single characters can both be delimited by
//both single and double quotes.
boolean singleQuote = c == '\'';
//TODO allow char type!
if (!String.class.isAssignableFrom(type)) {
throw DBLogger.newUser("Incompatible types, found String, expected: " +
type.getName());
}
inc();
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();
}
value = substring(pos0, pos());
inc();
} else if (c=='-' || (c >= '0' && c <= '9')) {
pos0 = pos();
boolean isHex = false;
while (!isFinished()) {
c = charAt0();
if (c==')' || isWS(c) || c=='|' || c=='&') {
break;
// } else if (c=='.') {
// isDouble = true;
// } else if (c=='L' || c=='l') {
// //if this is not at the last position, then we will fail later anyway
// isLong = true;
} else if (c=='x') {
//if this is not at the second position, then we will fail later anyway
isHex = true;
}
inc();
}
if (type == Double.TYPE || type == Double.class) {
value = Double.parseDouble( substring(pos0, pos()) );
} else if (type == Float.TYPE || type == Float.class) {
value = Float.parseFloat( substring(pos0, pos()) );
} else if (type == Long.TYPE || type == Long.class) {
if (isHex) {
value = Long.parseLong( substring(pos0+2, pos()), 16 );
} else {
value = Long.parseLong( substring(pos0, pos()));
}
} else if (type == Integer.TYPE || type == Integer.class) {
if (isHex) {
value = Integer.parseInt( substring(pos0+2, pos()), 16 );
} else {
value = Integer.parseInt( substring(pos0, pos()) );
}
} else if (type == Short.TYPE || type == Short.class) {
if (isHex) {
value = Short.parseShort( substring(pos0+2, pos()), 16 );
} else {
value = Short.parseShort( substring(pos0, pos()) );
}
} else if (type == Byte.TYPE || type == Byte.class) {
if (isHex) {
value = Byte.parseByte( substring(pos0+2, pos()), 16 );
} else {
value = Byte.parseByte( substring(pos0, pos()) );
}
} else if (type == BigDecimal.class) {
value = new BigDecimal( substring(pos0+2, pos()) );
} else if (type == BigInteger.class) {
value = new BigInteger( substring(pos0, pos()) );
} else {
throw DBLogger.newUser("Incompatible types, found number, expected: " + type);
}
} else if (type == Boolean.TYPE || type == Boolean.class) {
if (substring(pos0, pos0+4).toLowerCase().equals("true")
&& (isFinished(4) || charAt(4)==' ' || charAt(4)==')')) {
value = true;
inc(4);
} else if (substring(pos0, pos0+5).toLowerCase().equals("false")
&& (isFinished(5) || charAt(5)==' ' || charAt(5)==')')) {
value = false;
inc(5);
} else {
throw DBLogger.newUser("Incompatible types, expected Boolean, found: " +
substring(pos0, pos0+5));
}
} else {
boolean isImplicit = false;
if (c==':') {
//implicit paramter
isImplicit = true;
inc();
pos0 = pos;
c = charAt0();
}
while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
(c=='_')) {
inc();
if (isFinished()) break;
c = charAt0();
}
paramName = substring(pos0, pos());
if (isImplicit) {
addImplicitParameter(type, paramName);
} else {
addParameter(null, paramName);
}
}
if (fName == null || (value == null && paramName == null) || op == null) {
throw DBLogger.newUser("Cannot parse query at " + pos() + ": " + str);
}
trim();
return new QueryTerm(null, fieldDef, null, op, paramName, value, null, null, negate);
}
static enum COMP_OP {
EQ(false, false, true),
NE(true, true, false),
LE(true, false, true),
AE(false, true, true),
L(true, false, false),
A(false, true, false),
//TODO remove these?
COLL_contains(Object.class), COLL_isEmpty(), COLL_size(),
MAP_containsKey(Object.class), MAP_isEmpty(), MAP_size(),
MAP_containsValue(Object.class), MAP_get(Object.class),
LIST_get(Integer.TYPE),
STR_startsWith(String.class), STR_endsWith(String.class),
STR_indexOf1(String.class), STR_indexOf2(String.class, Integer.TYPE),
STR_substring1(Integer.TYPE), STR_substring2(Integer.TYPE, Integer.TYPE),
STR_toLowerCase(), STR_toUpperCase(),
STR_matches(String.class), STR_contains_NON_JDO(String.class),
JDOHelper_getObjectId(Object.class),
Math_abs(Number.class), Math_sqrt(Number.class);
private final boolean isComparator;
private final Class<?>[] args;
private final boolean allowsLess;
private final boolean allowsMore;
private final boolean allowsEqual;
private COMP_OP(Class<?> ... args) {
this.isComparator = false;
this.args = args;
allowsLess = false;
allowsMore = false;
allowsEqual = false;
}
private COMP_OP(boolean al, boolean am, boolean ae) {
allowsLess = al;
allowsMore = am;
allowsEqual = ae;
isComparator = true;
this.args = new Class<?>[]{};
}
boolean allowsLess() {
return allowsLess;
}
boolean allowsMore() {
return allowsMore;
}
boolean allowsEqual() {
return allowsEqual;
}
COMP_OP inverstIfTrue(boolean inverse) {
if (!inverse) {
return this;
}
switch (this) {
case EQ: return NE;
case NE: return EQ;
case LE: return A;
case AE: return L;
case L: return AE;
case A: return LE;
default: throw new IllegalArgumentException();
}
}
boolean isComparator() {
return isComparator;
}
public int argCount() {
return args.length;
}
/**
*
* @param compare Result of a compareTo() call.
* @return result of the evaluation (boolean)
*/
public boolean evaluate(int compare) {
switch (this) {
case EQ: return compare == 0;
case NE: return compare != 0;
case LE: return compare <= 0;
case AE: return compare >= 0;
case L: return compare < 0;
case A: return compare > 0;
default: throw new IllegalArgumentException();
}
}
}
static enum FNCT_OP {
CONSTANT(Object.class),
REF(ZooPC.class),
FIELD(Object.class),
THIS(ZooPC.class),
PARAM(Object.class),
COLL_contains(Boolean.TYPE, Object.class),
COLL_isEmpty(Boolean.TYPE),
COLL_size(Integer.TYPE),
MAP_containsKey(Boolean.TYPE, Object.class),
MAP_isEmpty(Boolean.TYPE),
MAP_size(Integer.TYPE),
MAP_containsValue(Boolean.TYPE, Object.class),
MAP_get(Object.class, Object.class),
LIST_get(Object.class, Integer.TYPE),
STR_startsWith(Boolean.TYPE, String.class),
STR_endsWith(Boolean.TYPE, String.class),
STR_indexOf1(Integer.TYPE, String.class),
STR_indexOf2(Integer.TYPE, String.class, Integer.TYPE),
STR_substring1(String.class, Integer.TYPE),
STR_substring2(String.class, Integer.TYPE, Integer.TYPE),
STR_toLowerCase(String.class),
STR_toUpperCase(String.class),
STR_matches(Boolean.TYPE, String.class),
STR_length(Integer.TYPE),
STR_trim(String.class),
STR_contains_NON_JDO(Boolean.TYPE, String.class),
ENUM_ordinal(Integer.TYPE),
ENUM_toString(String.class),
JDOHelper_getObjectId(Long.TYPE, Object.class),
Math_abs(Number.class, Number.class),
Math_cos(Double.class, Double.class),
Math_sin(Double.class, Double.class),
Math_sqrt(Double.class, Double.class),
EQ_OBJ(Boolean.TYPE, Object.class, Object.class),
EQ_NUM(Boolean.TYPE, Number.class, Number.class),
EQ_BOOL(Boolean.TYPE, Boolean.TYPE, Boolean.TYPE),
G(Boolean.TYPE, Number.class, Number.class),
GE(Boolean.TYPE, Number.class, Number.class),
L(Boolean.TYPE, Number.class, Number.class),
LE(Boolean.TYPE, Number.class, Number.class),
PLUS_STR(String.class, String.class, String.class),
PLUS_L(Long.TYPE, Number.class, Number.class),
MINUS_L(Long.TYPE, Number.class, Number.class),
MUL_L(Long.TYPE, Number.class, Number.class),
DIV_L(Long.TYPE, Number.class, Number.class),
PLUS_D(Double.TYPE, Number.class, Number.class),
MINUS_D(Double.TYPE, Number.class, Number.class),
MUL_D(Double.TYPE, Number.class, Number.class),
DIV_D(Double.TYPE, Number.class, Number.class),
;
private final Class<?>[] args;
private final Class<?> returnType;
//reference method with same name but bigger signature
private FNCT_OP biggerAlternative = null;
static {
STR_indexOf1.biggerAlternative = STR_indexOf2;
STR_substring1.biggerAlternative = STR_substring2;
}
/**
*
* @param returnType
* @param args The first arg is the objects on which the method is called
*/
private FNCT_OP(Class<?> returnType, Class<?> ... args) {
this.returnType = returnType;
this.args = args;
}
public int argCount() {
return args.length;
}
public Class<?>[] args() {
return args;
}
public Class<?> getReturnType() {
return returnType;
}
public FNCT_OP biggerAlternative() {
return biggerAlternative;
}
}
/**
* Logical operators.
*/
enum LOG_OP {
AND(2), // &&
OR(2), // ||
//XOR(2);
NOT(1); //TODO e.g. not supported in unary-stripper or in index-advisor
private final int _len;
private LOG_OP(int len) {
_len = len;
}
LOG_OP inverstIfTrue(boolean inverse) {
if (!inverse) {
return this;
}
switch (this) {
case AND: return OR;
case OR: return AND;
default: throw new IllegalArgumentException();
}
}
}
public static Class<?> locateClassFromShortName(String className) {
if (!className.contains(".")) {
switch (className) {
case "Collection": return Collection.class;
case "String": return String.class;
case "List": return List.class;
case "Set": return Set.class;
case "Map": return Map.class;
case "Float": return Float.class;
case "Double": return Double.class;
case "Byte": return Byte.class;
case "Character": return Character.class;
case "Short": return Short.class;
case "Integer": return Integer.class;
case "Long": return Long.class;
case "float": return Float.TYPE;
case "double": return Double.TYPE;
case "byte": return Byte.TYPE;
case "character": return Character.TYPE;
case "short": return Short.TYPE;
case "int": return Integer.TYPE;
case "long": return Long.TYPE;
case "BigInteger": return BigInteger.class;
case "BigDecimal": return BigDecimal.class;
}
}
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new JDOUserException("Class not found: " + className, e);
}
}
private void parseParameters() {
while (!isFinished()) {
char c = charAt0();
int pos0 = pos;
while ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
(c=='_') || (c=='.')) {
inc();
if (isFinished()) break;
c = charAt0();
}
String typeName = substring(pos0, pos());
//TODO check here for
//IMPORTS
//GROUP_BY
//ORDER_BY
//RANGE
//TODO .. and implement according sub-methods
trim();
c = charAt0();
pos0 = pos;
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());
updateParameterType(typeName, paramName);
trim();
}
}
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);
}
public static void parseOrdering(final String input, int pos,
List<Pair<ZooFieldDef, Boolean>> ordering, ZooClassDef candClsDef) {
Map<String, ZooFieldDef> fields = candClsDef.getAllFieldsAsMap();
ordering.clear();
if (input == null) {
return;
}
String orderingStr = input.substring(pos).trim();
while (orderingStr.length() > 0) {
int p = orderingStr.indexOf(' ');
if (p < 0) {
throw DBLogger.newUser("Parse error near position " + pos + " input=" + input);
}
String attrName = orderingStr.substring(0, p).trim();
pos += attrName.length()+1;
ZooFieldDef f = fields.get(attrName);
if (f == null) {
throw DBLogger.newUser("Field '" + attrName + "' not found at position " + pos);
}
if (!f.isPrimitiveType() && !f.isString()) {
throw DBLogger.newUser("Field not sortable: " + f);
}
for (Pair<ZooFieldDef, Boolean> p2: ordering) {
if (p2.getA().equals(f)) {
throw DBLogger.newUser("Parse error, field '" + f + "' is sorted twice near "
+ "position " + pos + " input=" + input);
}
}
orderingStr = orderingStr.substring(p).trim();
int d;
if (orderingStr.toUpperCase().startsWith("ASC")) {
if (orderingStr.toUpperCase().startsWith("ASCENDING")) {
d = 9;
} else {
d = 3;
}
ordering.add(new Pair<ZooFieldDef, Boolean>(f, true));
} else if (orderingStr.toUpperCase().startsWith("DESC")) {
if (orderingStr.toUpperCase().startsWith("DESCENDING")) {
d = 10;
} else {
d = 4;
}
ordering.add(new Pair<ZooFieldDef, Boolean>(f, false));
} else {
throw DBLogger.newUser("Parse error at position " + pos);
}
pos += d;
orderingStr = orderingStr.substring(d).trim();
if (orderingStr.length() > 0) {
if (orderingStr.startsWith(",")) {
orderingStr = orderingStr.substring(1).trim();
pos += 1;
if (orderingStr.length() == 0) {
throw DBLogger.newUser("Parse error, unexpected end near position " + pos +
" input=" + input);
}
} else {
throw DBLogger.newUser("Parse error, expected ',' near position " + pos +
" input=" + input);
}
}
}
}
}