/*
* Copyright 2008 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package javax.management;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
*
Parser for JMX queries represented in an SQL-like language.
*/
/*
* Note that if a query starts with ( then we don't know whether it is
* a predicate or just a value that is parenthesized. So, inefficiently,
* we try to parse a predicate and if that doesn't work we try to parse
* a value.
*/
class QueryParser {
// LEXER STARTS HERE
private static class Token {
final String string;
Token(String s) {
this.string = s;
}
@Override
public String toString() {
return string;
}
}
private static final Token
END = new Token(""),
LPAR = new Token("("), RPAR = new Token(")"),
COMMA = new Token(","), DOT = new Token("."), SHARP = new Token("#"),
PLUS = new Token("+"), MINUS = new Token("-"),
TIMES = new Token("*"), DIVIDE = new Token("/"),
LT = new Token("<"), GT = new Token(">"),
LE = new Token("<="), GE = new Token(">="),
NE = new Token("<>"), EQ = new Token("="),
NOT = new Id("NOT"), INSTANCEOF = new Id("INSTANCEOF"),
FALSE = new Id("FALSE"), TRUE = new Id("TRUE"),
BETWEEN = new Id("BETWEEN"), AND = new Id("AND"),
OR = new Id("OR"), IN = new Id("IN"),
LIKE = new Id("LIKE"), CLASS = new Id("CLASS");
// Keywords that can appear where an identifier can appear.
// If an attribute is one of these, then it must be quoted when
// converting a query into a string.
// We use a TreeSet so we can look up case-insensitively.
private static final Set idKeywords =
new TreeSet(String.CASE_INSENSITIVE_ORDER);
static {
for (Token t : new Token[] {NOT, INSTANCEOF, FALSE, TRUE, LIKE, CLASS})
idKeywords.add(t.string);
};
public static String quoteId(String id) {
if (id.contains("\"") || idKeywords.contains(id))
return '"' + id.replace("\"", "\"\"") + '"';
else
return id;
}
private static class Id extends Token {
Id(String id) {
super(id);
}
// All other tokens use object identity, which means e.g. that one
// occurrence of the string constant 'x' is not the same as another.
// For identifiers, we ignore case when testing for equality so that
// for a keyword such as AND you can also spell it as "And" or "and".
// But we keep the original case of the identifier, so if it's not
// a keyword we will distinguish between the attribute Foo and the
// attribute FOO.
@Override
public boolean equals(Object o) {
return (o instanceof Id && (((Id) o).toString().equalsIgnoreCase(toString())));
}
}
private static class QuotedId extends Token {
QuotedId(String id) {
super(id);
}
@Override
public String toString() {
return '"' + string.replace("\"", "\"\"") + '"';
}
}
private static class StringLit extends Token {
StringLit(String s) {
super(s);
}
@Override
public String toString() {
return '\'' + string.replace("'", "''") + '\'';
}
}
private static class LongLit extends Token {
long number;
LongLit(long number) {
super(Long.toString(number));
this.number = number;
}
}
private static class DoubleLit extends Token {
double number;
DoubleLit(double number) {
super(Double.toString(number));
this.number = number;
}
}
private static class Tokenizer {
private final String s;
private final int len;
private int i = 0;
Tokenizer(String s) {
this.s = s;
this.len = s.length();
}
private int thisChar() {
if (i == len)
return -1;
return s.codePointAt(i);
}
private void advance() {
i += Character.charCount(thisChar());
}
private int thisCharAdvance() {
int c = thisChar();
advance();
return c;
}
Token nextToken() {
// In this method, c is the character we're looking at, and
// thisChar() is the character after that. Everything must
// preserve these invariants. When we return we then have
// thisChar() being the start of the following token, so
// the next call to nextToken() will begin from there.
int c;
// Skip space
do {
if (i == len)
return null;
c = thisCharAdvance();
} while (Character.isWhitespace(c));
// Now c is the first character of the token, and tokenI points
// to the character after that.
switch (c) {
case '(': return LPAR;
case ')': return RPAR;
case ',': return COMMA;
case '.': return DOT;
case '#': return SHARP;
case '*': return TIMES;
case '/': return DIVIDE;
case '=': return EQ;
case '-': return MINUS;
case '+': return PLUS;
case '>':
if (thisChar() == '=') {
advance();
return GE;
} else
return GT;
case '<':
c = thisChar();
switch (c) {
case '=': advance(); return LE;
case '>': advance(); return NE;
default: return LT;
}
case '!':
if (thisCharAdvance() != '=')
throw new IllegalArgumentException("'!' must be followed by '='");
return NE;
case '"':
case '\'': {
int quote = c;
StringBuilder sb = new StringBuilder();
while (true) {
while ((c = thisChar()) != quote) {
if (c < 0) {
throw new IllegalArgumentException(
"Unterminated string constant");
}
sb.appendCodePoint(thisCharAdvance());
}
advance();
if (thisChar() == quote) {
sb.appendCodePoint(quote);
advance();
} else
break;
}
if (quote == '\'')
return new StringLit(sb.toString());
else
return new QuotedId(sb.toString());
}
}
// Is it a numeric constant?
if (Character.isDigit(c) || c == '.') {
StringBuilder sb = new StringBuilder();
int lastc = -1;
while (true) {
sb.appendCodePoint(c);
c = Character.toLowerCase(thisChar());
if (c == '+' || c == '-') {
if (lastc != 'e')
break;
} else if (!Character.isDigit(c) && c != '.' && c != 'e')
break;
lastc = c;
advance();
}
String s = sb.toString();
if (s.indexOf('.') >= 0 || s.indexOf('e') >= 0) {
double d = parseDoubleCheckOverflow(s);
return new DoubleLit(d);
} else {
// Like the Java language, we allow the numeric constant
// x where -x = Long.MIN_VALUE, even though x is not
// representable as a long (it is Long.MAX_VALUE + 1).
// Code in the parser will reject this value if it is
// not the operand of unary minus.
long l = -Long.parseLong("-" + s);
return new LongLit(l);
}
}
// It must be an identifier.
if (!Character.isJavaIdentifierStart(c)) {
StringBuilder sb = new StringBuilder();
Formatter f = new Formatter(sb);
f.format("Bad character: %c (%04x)", c, c);
throw new IllegalArgumentException(sb.toString());
}
StringBuilder id = new StringBuilder();
while (true) { // identifier
id.appendCodePoint(c);
c = thisChar();
if (!Character.isJavaIdentifierPart(c))
break;
advance();
}
return new Id(id.toString());
}
}
/* Parse a double as a Java compiler would do it, throwing an exception
* if the input does not fit in a double. We assume that the input
* string is not "Infinity" and does not have a leading sign.
*/
private static double parseDoubleCheckOverflow(String s) {
double d = Double.parseDouble(s);
if (Double.isInfinite(d))
throw new NumberFormatException("Overflow: " + s);
if (d == 0.0) { // Underflow checking is hard! CR 6604864
String ss = s;
int e = s.indexOf('e'); // we already forced E to lowercase
if (e > 0)
ss = s.substring(0, e);
ss = ss.replace("0", "").replace(".", "");
if (!ss.equals(""))
throw new NumberFormatException("Underflow: " + s);
}
return d;
}
// PARSER STARTS HERE
private final List tokens;
private int tokenI;
// The current token is always tokens[tokenI].
QueryParser(String s) {
// Construct the complete list of tokens immediately and append
// a sentinel (END).
tokens = new ArrayList();
Tokenizer tokenizer = new Tokenizer(s);
Token t;
while ((t = tokenizer.nextToken()) != null)
tokens.add(t);
tokens.add(END);
}
private Token current() {
return tokens.get(tokenI);
}
// If the current token is t, then skip it and return true.
// Otherwise, return false.
private boolean skip(Token t) {
if (t.equals(current())) {
tokenI++;
return true;
}
return false;
}
// If the current token is one of the ones in 'tokens', then skip it
// and return its index in 'tokens'. Otherwise, return -1.
private int skipOne(Token... tokens) {
for (int i = 0; i < tokens.length; i++) {
if (skip(tokens[i]))
return i;
}
return -1;
}
// If the current token is t, then skip it and return.
// Otherwise throw an exception.
private void expect(Token t) {
if (!skip(t))
throw new IllegalArgumentException("Expected " + t + ", found " + current());
}
private void next() {
tokenI++;
}
QueryExp parseQuery() {
QueryExp qe = query();
if (current() != END)
throw new IllegalArgumentException("Junk at end of query: " + current());
return qe;
}
// The remainder of this class is a classical recursive-descent parser.
// We only need to violate the recursive-descent scheme in one place,
// where parentheses make the grammar not LL(1).
private QueryExp query() {
QueryExp lhs = andquery();
while (skip(OR))
lhs = Query.or(lhs, andquery());
return lhs;
}
private QueryExp andquery() {
QueryExp lhs = predicate();
while (skip(AND))
lhs = Query.and(lhs, predicate());
return lhs;
}
private QueryExp predicate() {
// Grammar hack. If we see a paren, it might be (query) or
// it might be (value). We try to parse (query), and if that
// fails, we parse (value). For example, if the string is
// "(2+3)*4 < 5" then we will try to parse the query
// "2+3)*4 < 5", which will fail at the ), so we'll back up to
// the paren and let value() handle it.
if (skip(LPAR)) {
int parenIndex = tokenI - 1;
try {
QueryExp qe = query();
expect(RPAR);
return qe;
} catch (IllegalArgumentException e) {
// OK: try parsing a value
}
tokenI = parenIndex;
}
if (skip(NOT))
return Query.not(predicate());
if (skip(INSTANCEOF))
return Query.isInstanceOf(stringvalue());
if (skip(LIKE)) {
StringValueExp sve = stringvalue();
String s = sve.getValue();
try {
return new ObjectName(s);
} catch (MalformedObjectNameException e) {
throw new IllegalArgumentException(
"Bad ObjectName pattern after LIKE: '" + s + "'", e);
}
}
ValueExp lhs = value();
return predrhs(lhs);
}
// The order of elements in the following arrays is important. The code
// in predrhs depends on integer indexes. Change with caution.
private static final Token[] relations = {
EQ, LT, GT, LE, GE, NE,
// 0, 1, 2, 3, 4, 5,
};
private static final Token[] betweenLikeIn = {
BETWEEN, LIKE, IN
// 0, 1, 2,
};
private QueryExp predrhs(ValueExp lhs) {
Token start = current(); // for errors
// Look for < > = etc
int i = skipOne(relations);
if (i >= 0) {
ValueExp rhs = value();
switch (i) {
case 0: return Query.eq(lhs, rhs);
case 1: return Query.lt(lhs, rhs);
case 2: return Query.gt(lhs, rhs);
case 3: return Query.leq(lhs, rhs);
case 4: return Query.geq(lhs, rhs);
case 5: return Query.not(Query.eq(lhs, rhs));
// There is no Query.ne so <> is shorthand for the above.
default:
throw new AssertionError();
}
}
// Must be BETWEEN LIKE or IN, optionally preceded by NOT
boolean not = skip(NOT);
i = skipOne(betweenLikeIn);
if (i < 0)
throw new IllegalArgumentException("Expected relation at " + start);
QueryExp q;
switch (i) {
case 0: { // BETWEEN
ValueExp lower = value();
expect(AND);
ValueExp upper = value();
q = Query.between(lhs, lower, upper);
break;
}
case 1: { // LIKE
if (!(lhs instanceof AttributeValueExp)) {
throw new IllegalArgumentException(
"Left-hand side of LIKE must be an attribute");
}
AttributeValueExp alhs = (AttributeValueExp) lhs;
StringValueExp sve = stringvalue();
q = Query.match(alhs, sve);
break;
}
case 2: { // IN
expect(LPAR);
List values = new ArrayList();
values.add(value());
while (skip(COMMA))
values.add(value());
expect(RPAR);
q = Query.in(lhs, values.toArray(new ValueExp[values.size()]));
break;
}
default:
throw new AssertionError();
}
if (not)
q = Query.not(q);
return q;
}
private ValueExp value() {
ValueExp lhs = factor();
int i;
while ((i = skipOne(PLUS, MINUS)) >= 0) {
ValueExp rhs = factor();
if (i == 0)
lhs = Query.plus(lhs, rhs);
else
lhs = Query.minus(lhs, rhs);
}
return lhs;
}
private ValueExp factor() {
ValueExp lhs = term();
int i;
while ((i = skipOne(TIMES, DIVIDE)) >= 0) {
ValueExp rhs = term();
if (i == 0)
lhs = Query.times(lhs, rhs);
else
lhs = Query.div(lhs, rhs);
}
return lhs;
}
private ValueExp term() {
boolean signed = false;
int sign = +1;
if (skip(PLUS))
signed = true;
else if (skip(MINUS)) {
signed = true; sign = -1;
}
Token t = current();
next();
if (t instanceof DoubleLit)
return Query.value(sign * ((DoubleLit) t).number);
if (t instanceof LongLit) {
long n = ((LongLit) t).number;
if (n == Long.MIN_VALUE && sign != -1)
throw new IllegalArgumentException("Illegal positive integer: " + n);
return Query.value(sign * n);
}
if (signed)
throw new IllegalArgumentException("Expected number after + or -");
if (t == LPAR) {
ValueExp v = value();
expect(RPAR);
return v;
}
if (t.equals(FALSE) || t.equals(TRUE)) {
return Query.value(t.equals(TRUE));
}
if (t.equals(CLASS))
return Query.classattr();
if (t instanceof StringLit)
return Query.value(t.string); // Not toString(), which would requote '
// At this point, all that remains is something that will call Query.attr
if (!(t instanceof Id) && !(t instanceof QuotedId))
throw new IllegalArgumentException("Unexpected token " + t);
String name1 = name(t);
if (skip(SHARP)) {
Token t2 = current();
next();
String name2 = name(t2);
return Query.attr(name1, name2);
}
return Query.attr(name1);
}
// Initially, t is the first token of a supposed name and current()
// is the second.
private String name(Token t) {
StringBuilder sb = new StringBuilder();
while (true) {
if (!(t instanceof Id) && !(t instanceof QuotedId))
throw new IllegalArgumentException("Unexpected token " + t);
sb.append(t.string);
if (current() != DOT)
break;
sb.append('.');
next();
t = current();
next();
}
return sb.toString();
}
private StringValueExp stringvalue() {
// Currently the only way to get a StringValueExp when constructing
// a QueryExp is via Query.value(String), so we only recognize
// string literals here. But if we expand queries in the future
// that might no longer be true.
Token t = current();
next();
if (!(t instanceof StringLit))
throw new IllegalArgumentException("Expected string: " + t);
return Query.value(t.string);
}
}