/*
* Copyright 2010 Impetus Infotech.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/package com.impetus.kundera.query;
import java.util.StringTokenizer;
/**
* Parser for handling JPQL Single-String queries. Takes a JPQLQuery and the
* query string and parses it into its constituent parts, updating the JPQLQuery
* accordingly with the result that after calling the parse() method the
* JPQLQuery is populated.
*
* <pre>
* SELECT [ {result} ]
* [FROM {candidate-classes} ]
* [WHERE {filter}]
* [GROUP BY {grouping-clause} ]
* [HAVING {having-clause} ]
* [ORDER BY {ordering-clause}]
* e.g SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1
* </pre>
*
* @author animesh.kumar
*/
public class KunderaQueryParser {
/** The JPQL query to populate. */
private KunderaQuery query;
/** The single-string query string. */
private String queryString;
/**
* Record of the keyword currently being processed, so we can check for out
* of order keywords.
*/
private int keywordPosition = -1;
/**
* Constructor for the Single-String parser.
*
* @param query
* The query
* @param queryString
* The Single-String query
*/
public KunderaQueryParser(KunderaQuery query, String queryString) {
this.query = query;
this.queryString = queryString;
}
/**
* Method to parse the Single-String query.
*/
public final void parse() {
new Compiler(new Parser(queryString)).compile();
}
/**
* Method to detect whether this token is a keyword for JPQL Single-String.
*
* @param token
* The token
*
* @return Whether it is a keyword
*/
private boolean isKeyword(String token) {
// Compare the passed token against the provided keyword list, or their
// lowercase form
for (int i = 0; i < KunderaQuery.SINGLE_STRING_KEYWORDS.length; i++) {
if (token.equalsIgnoreCase(KunderaQuery.SINGLE_STRING_KEYWORDS[i])) {
return true;
}
}
return false;
}
/**
* Compiler to process keywords contents. In the query the keywords often
* have content values following them that represent the constituent parts
* of the query. This takes the keyword and sets the constituent part
* accordingly.
*/
private class Compiler {
/** The tokenizer. */
private Parser tokenizer;
// Temporary variable since grouping clause is made up of GROUP BY ...
// HAVING ...
/** The grouping clause. */
private String groupingClause;
/**
* Instantiates a new compiler.
*
* @param tokenizer
* the tokenizer
*/
Compiler(Parser tokenizer) {
this.tokenizer = tokenizer;
}
/**
* Compile.
*/
private void compile() {
// TODO Query can start "SELECT", "DELETE" or "UPDATE"
compileSelect();
// any keyword after compiling the SELECT is an error
String keyword = tokenizer.parseKeyword();
if (keyword != null) {
if (isKeyword(keyword)) {
throw new RuntimeException("out of order keyword: " + keyword);
}
}
}
/**
* Compile select.
*/
// TODO: reduce Cyclomatic complexity
private void compileSelect() {
if (!tokenizer.parseKeywordIgnoreCase("SELECT")) {
throw new RuntimeException("no select to start");
}
compileResult();
if (tokenizer.parseKeywordIgnoreCase("FROM")) {
compileFrom();
}
if (tokenizer.parseKeywordIgnoreCase("WHERE")) {
compileWhere();
}
if (tokenizer.parseKeywordIgnoreCase("GROUP BY")) {
compileGroup();
}
if (tokenizer.parseKeywordIgnoreCase("HAVING")) {
compileHaving();
}
if (groupingClause != null) {
query.setGrouping(groupingClause);
}
if (tokenizer.parseKeywordIgnoreCase("ORDER BY")) {
compileOrder();
}
}
/**
* Compile result.
*/
private void compileResult() {
String content = tokenizer.parseContent();
// content may be empty
if (content.length() > 0) {
query.setResult(content);
}
}
/**
* Compile from.
*/
private void compileFrom() {
String content = tokenizer.parseContent();
// content may be empty
if (content.length() > 0) {
query.setFrom(content);
}
}
/**
* Compile where.
*/
private void compileWhere() {
String content = tokenizer.parseContent();
// content cannot be empty
if (content.length() == 0) {
throw new RuntimeException("keyword without value[WHERE]");
}
query.setFilter(content);
}
/**
* Compile group.
*/
private void compileGroup() {
String content = tokenizer.parseContent();
// content cannot be empty
if (content.length() == 0) {
throw new RuntimeException("keyword without value: GROUP BY");
}
groupingClause = content;
}
/**
* Compile having.
*/
private void compileHaving() {
String content = tokenizer.parseContent();
// content cannot be empty
if (content.length() == 0) {
throw new RuntimeException("keyword without value: HAVING");
}
if (groupingClause != null) {
groupingClause = groupingClause.trim() + content;
} else {
groupingClause = content;
}
}
/**
* Compile order.
*/
private void compileOrder() {
String content = tokenizer.parseContent();
// content cannot be empty
if (content.length() == 0) {
throw new RuntimeException("keyword without value: ORDER BY");
}
query.setOrdering(content);
}
}
/**
* Tokenizer that provides access to current token.
*/
private class Parser {
/** tokens. */
private final String[] tokens;
/** keywords. */
private final String[] keywords;
/** current token cursor position. */
private int pos = -1;
/**
* Constructor.
*
* @param str
* the str
*/
public Parser(String str) {
StringTokenizer tokenizer = new StringTokenizer(str);
tokens = new String[tokenizer.countTokens()];
keywords = new String[tokenizer.countTokens()];
int i = 0;
while (tokenizer.hasMoreTokens()) {
tokens[i++] = tokenizer.nextToken();
}
for (i = 0; i < tokens.length; i++) {
if (isKeyword(tokens[i])) {
keywords[i] = tokens[i];
} else if (i < tokens.length - 1 && isKeyword(tokens[i] + ' ' + tokens[i + 1])) {
keywords[i] = tokens[i];
i++;
keywords[i] = tokens[i];
}
}
}
/**
* Parse the content until a keyword is found.
*
* @return the content
*/
public String parseContent() {
String content = "";
while (pos < tokens.length - 1) {
pos++;
if (isKeyword(tokens[pos])) {
pos--;
break;
} else if (pos < tokens.length - 1 && isKeyword(tokens[pos] + ' ' + tokens[pos + 1])) {
pos--;
break;
} else {
if (content.length() == 0) {
content = tokens[pos];
} else {
content += " " + tokens[pos];
}
}
}
return content;
}
/**
* Parse the next token looking for a keyword. The cursor position is
* skipped in one tick if a keyword is found
*
* @param keyword
* the searched keyword
*
* @return true if the keyword
*/
public boolean parseKeyword(String keyword) {
if (pos < tokens.length - 1) {
pos++;
if (keywords[pos] != null) {
if (keywords[pos].equals(keyword)) {
return true;
}
if (keyword.indexOf(' ') > -1) {
if (pos < keywords.length - 1) {
if ((keywords[pos] + ' ' + keywords[pos + 1]).equals(keyword)) {
pos++;
return true;
}
}
}
}
pos--;
}
return false;
}
/**
* Parse the next token looking for a keyword. The cursor position is
* skipped in one tick if a keyword is found
*
* @param keyword
* the searched keyword
*
* @return true if the keyword
*/
public boolean parseKeywordIgnoreCase(String keyword) {
if (pos < tokens.length - 1) {
pos++;
if (keywords[pos] != null) {
if (keywords[pos].equalsIgnoreCase(keyword)) {
return true;
}
if (keyword.indexOf(' ') > -1) {
if ((keywords[pos] + ' ' + keywords[pos + 1]).equalsIgnoreCase(keyword)) {
pos++;
return true;
}
}
}
pos--;
}
return false;
}
/**
* Parse the next token looking for a keyword. The cursor position is
* skipped in one tick if a keyword is found
*
* @return the parsed keyword or null
*/
public String parseKeyword() {
if (pos < tokens.length - 1) {
pos++;
if (keywords[pos] != null) {
return keywords[pos];
}
pos--;
}
return null;
}
}
}