001: /**********************************************************************
002: Copyright (c) 2006 Erik Bengtson and others. All rights reserved.
003: Licensed under the Apache License, Version 2.0 (the "License");
004: you may not use this file except in compliance with the License.
005: You may obtain a copy of the License at
006:
007: http://www.apache.org/licenses/LICENSE-2.0
008:
009: Unless required by applicable law or agreed to in writing, software
010: distributed under the License is distributed on an "AS IS" BASIS,
011: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: See the License for the specific language governing permissions and
013: limitations under the License.
014:
015: Contributors:
016: 2008 Andy Jefferson - cater for subqueries, bulk update, bulk delete parsing
017: ...
018: **********************************************************************/package org.jpox.store.query;
019:
020: import java.util.StringTokenizer;
021:
022: import org.jpox.ObjectManager;
023: import org.jpox.exceptions.JPOXUserException;
024: import org.jpox.util.ClassUtils;
025: import org.jpox.util.JPOXLogger;
026: import org.jpox.util.Localiser;
027:
028: /**
029: * Parser for handling JPQL Single-String queries.
030: * Takes a JPQLQuery and the query string and parses it into its constituent parts, updating
031: * the JPQLQuery accordingly with the result that after calling the parse() method the JPQLQuery
032: * is populated.
033: * <pre>
034: * SELECT [ {result} ]
035: * [FROM {candidate-classes} ]
036: * [WHERE {filter}]
037: * [GROUP BY {grouping-clause} ]
038: * [HAVING {having-clause} ]
039: * [ORDER BY {ordering-clause}]
040: * e.g SELECT c FROM Customer c INNER JOIN c.orders o WHERE c.status = 1
041: * </pre>
042: * or
043: * <pre>
044: * UPDATE {update-clause}
045: * WHERE {filter}
046: * and update-clause is of the form
047: * "Entity [[AS] identifier] SET {field = new_value}, ..."
048: * </pre>
049: * or
050: * <pre>
051: * DELETE {delete-clause}
052: * WHERE {filter}
053: * and delete-clause is of the form
054: * "FROM Entity [[AS] identifier]"
055: * </pre>
056: * <p>
057: * Note that {filter} and {having-clause} can contain subqueries, hence containing keywords
058: * <pre>
059: * SELECT c FROM Customer c WHERE NOT EXISTS (SELECT o1 FROM c.orders o1)
060: * </pre>
061: * So the "filter" for the outer query is "NOT EXISTS (SELECT o1 FROM c.orders o1)"
062: * @version $Revision: 1.5 $
063: */
064: public class JPQLSingleStringParser {
065: /** Localiser for messages. */
066: protected static final Localiser LOCALISER = Localiser
067: .getInstance("org.jpox.store.Localisation");
068:
069: /** The JPQL query to populate. */
070: private AbstractJPQLQuery query;
071:
072: /** The single-string query string. */
073: private String queryString;
074:
075: /** Record of the keyword currently being processed, so we can check for out of order keywords. */
076: int keywordPosition = -1;
077:
078: /**
079: * Constructor for the Single-String parser.
080: * @param query The query into which we populate the components of the query
081: * @param queryString The Single-String query
082: */
083: public JPQLSingleStringParser(AbstractJPQLQuery query,
084: String queryString) {
085: if (JPOXLogger.QUERY.isDebugEnabled()) {
086: JPOXLogger.QUERY
087: .debug(LOCALISER.msg("043000", queryString));
088: }
089: this .query = query;
090: this .queryString = queryString;
091: }
092:
093: /**
094: * Method to parse the Single-String query
095: */
096: public void parse() {
097: new Compiler(new Parser(queryString)).compile();
098: }
099:
100: /**
101: * Compiler to process keywords contents. In the query the keywords often have
102: * content values following them that represent the constituent parts of the query. This takes the keyword
103: * and sets the constituent part accordingly.
104: */
105: private class Compiler {
106: Parser tokenizer;
107:
108: Compiler(Parser tokenizer) {
109: this .tokenizer = tokenizer;
110: }
111:
112: private void compile() {
113: compileQuery();
114:
115: // any keyword after compiling the SELECT is an error
116: String keyword = tokenizer.parseKeyword();
117: if (keyword != null) {
118: if (AbstractJPQLQuery.isKeyword(keyword)) {
119: throw new JPOXUserException(LOCALISER.msg("043001",
120: keyword));
121: } else {
122: // unexpected token
123: }
124: }
125: }
126:
127: private void compileQuery() {
128: boolean update = false;
129: boolean delete = false;
130: if (tokenizer.parseKeywordIgnoreCase("SELECT")) {
131: // Do nothing
132: } else if (tokenizer.parseKeywordIgnoreCase("UPDATE")) {
133: update = true;
134: query.setType(Query.BULK_UPDATE);
135: } else if (tokenizer.parseKeywordIgnoreCase("DELETE")) {
136: delete = true;
137: query.setType(Query.BULK_DELETE);
138: } else {
139: throw new JPOXUserException(LOCALISER.msg("043002"));
140: }
141:
142: if (update) {
143: compileUpdate();
144: } else if (!delete) {
145: compileResult();
146: }
147:
148: if (tokenizer.parseKeywordIgnoreCase("FROM")) {
149: compileFrom();
150: }
151: if (tokenizer.parseKeywordIgnoreCase("WHERE")) {
152: compileWhere();
153: }
154: if (tokenizer.parseKeywordIgnoreCase("GROUP BY")) {
155: if (update || delete) {
156: throw new JPOXUserException(LOCALISER.msg("043007"));
157: }
158: compileGroup();
159: }
160: if (tokenizer.parseKeywordIgnoreCase("HAVING")) {
161: if (update || delete) {
162: throw new JPOXUserException(LOCALISER.msg("043008"));
163: }
164: compileHaving();
165: }
166: if (tokenizer.parseKeywordIgnoreCase("ORDER BY")) {
167: if (update || delete) {
168: throw new JPOXUserException(LOCALISER.msg("043009"));
169: }
170: compileOrder();
171: }
172: }
173:
174: private void compileResult() {
175: String content = tokenizer.parseContent(null, false);
176: if (content.length() > 0) {
177: //content may be empty
178: query.setResult(content);
179: }
180: }
181:
182: private void compileUpdate() {
183: String content = tokenizer.parseContent(null, false);
184: if (content.length() == 0) {
185: // No UPDATE clause
186: throw new JPOXUserException(LOCALISER.msg("043010"));
187: }
188:
189: String contentUpper = content.toUpperCase();
190: int setIndex = contentUpper.indexOf("SET");
191: if (setIndex < 0) {
192: // UPDATE clause has no "SET ..." !
193: throw new JPOXUserException(LOCALISER.msg("043011"));
194: }
195: query.setFrom(content.substring(0, setIndex).trim());
196: query.setUpdate(content.substring(setIndex + 3).trim());
197: }
198:
199: private void compileFrom() {
200: String content = tokenizer.parseContent(null, false);
201: if (content.length() > 0) {
202: //content may be empty
203: query.setFrom(content);
204: }
205: }
206:
207: private void compileWhere() {
208: // "TRIM" may include "FROM" keyword so ignore subsequent FROMs
209: String content = tokenizer.parseContent("FROM", true);
210: if (content.length() == 0) {
211: // content cannot be empty
212: throw new JPOXUserException(LOCALISER.msg("043004",
213: "WHERE", "<filter>"));
214: }
215:
216: String contentUpper = content.toUpperCase();
217: if (contentUpper.indexOf("SELECT") > 0) // Case insensitive search
218: {
219: // Subquery (or subqueries) present
220: StringBuffer contentStr = new StringBuffer(content);
221: boolean parsed = false;
222: int currentPosition = 0;
223: int subqueryNum = 1;
224: while (!parsed) {
225: // Find the next SELECT - subquery start
226: contentUpper = contentStr.toString().toUpperCase();
227: int selectPos = contentUpper.indexOf("SELECT",
228: currentPosition); // Case insensitive
229: if (selectPos < 0) {
230: parsed = true;
231: break;
232: } else {
233: currentPosition = selectPos;
234:
235: // Find the opening brace
236: int startPosition = currentPosition;
237: for (int i = currentPosition - 1; i >= 0; i--) {
238: if (contentStr.charAt(i) == '(') {
239: startPosition = i;
240: break;
241: }
242: }
243:
244: // Process subquery from this position until we find the end
245: int level = 0;
246: for (int i = currentPosition; i < content
247: .length(); i++) {
248: if (contentStr.charAt(i) == '(') {
249: level++;
250: } else if (contentStr.charAt(i) == ')') {
251: level--;
252: }
253: if (level == -1) {
254: // End of subquery, so register it
255: String subqueryString = contentStr
256: .substring(currentPosition, i);
257: String subqueryVarName = "SUBQ"
258: + subqueryNum;
259:
260: Query subquery = (Query) ClassUtils
261: .newInstance(
262: query.getClass(),
263: new Class[] {
264: ObjectManager.class,
265: String.class },
266: new Object[] {
267: query
268: .getObjectManager(),
269: subqueryString });
270: // TODO Set the type of the variable
271: query.addSubquery(subquery, "double "
272: + subqueryVarName, null, null);
273:
274: // Strip out the subquery from the content, replacing it with the variable name
275: contentStr.replace(startPosition,
276: i + 1, " " + subqueryVarName
277: + " ");
278: subqueryNum++;
279: break;
280: }
281: }
282: }
283: }
284:
285: query.setFilter(contentStr.toString());
286: } else {
287: query.setFilter(content);
288: }
289: }
290:
291: private void compileGroup() {
292: String content = tokenizer.parseContent(null, false);
293: if (content.length() == 0) {
294: // content cannot be empty
295: throw new JPOXUserException(LOCALISER.msg("043004",
296: "GROUP BY", "<grouping>"));
297: }
298: query.setGrouping(content);
299: }
300:
301: private void compileHaving() {
302: // "TRIM" may include "FROM" keyword so ignore subsequent FROMs
303: String content = tokenizer.parseContent("FROM", true);
304: if (content.length() == 0) {
305: // content cannot be empty
306: throw new JPOXUserException(LOCALISER.msg("043004",
307: "HAVING", "<having>"));
308: }
309: query.setHaving(content);
310: }
311:
312: private void compileOrder() {
313: String content = tokenizer.parseContent(null, false);
314: if (content.length() == 0) {
315: // content cannot be empty
316: throw new JPOXUserException(LOCALISER.msg("043004",
317: "ORDER BY", "<ordering>"));
318: }
319: query.setOrdering(content);
320: }
321: }
322:
323: /**
324: * Tokenizer that provides access to current token.
325: */
326: private class Parser {
327: final String queryString;
328:
329: int queryStringPos = 0;
330:
331: /** tokens */
332: final String[] tokens;
333:
334: /** keywords */
335: final String[] keywords;
336:
337: /** current token cursor position */
338: int tokenIndex = -1;
339:
340: /**
341: * Constructor
342: * @param str Query string
343: */
344: public Parser(String str) {
345: queryString = str;
346:
347: StringTokenizer tokenizer = new StringTokenizer(str);
348: tokens = new String[tokenizer.countTokens()];
349: keywords = new String[tokenizer.countTokens()];
350: int i = 0;
351: while (tokenizer.hasMoreTokens()) {
352: tokens[i++] = tokenizer.nextToken();
353: }
354: for (i = 0; i < tokens.length; i++) {
355: if (AbstractJPQLQuery.isKeyword(tokens[i])) {
356: keywords[i] = tokens[i];
357: } else if (i < tokens.length - 1
358: && AbstractJPQLQuery.isKeyword(tokens[i] + ' '
359: + tokens[i + 1])) {
360: keywords[i] = tokens[i];
361: i++;
362: keywords[i] = tokens[i];
363: }
364: }
365: }
366:
367: /**
368: * Parse the content until a keyword is found.
369: * @param keywordToIgnore Ignore this keyword if found first
370: * @param allowSubentries Whether to permit subentries (in parentheses) in this next block
371: * @return the content
372: */
373: public String parseContent(String keywordToIgnore,
374: boolean allowSubentries) {
375: String content = "";
376: int level = 0;
377:
378: while (tokenIndex < tokens.length - 1) {
379: tokenIndex++;
380:
381: if (allowSubentries) {
382: // Process this token to check level of parentheses.
383: // This is necessary because we want to ignore keywords if within a parentheses-block
384: // e.g SELECT ... FROM ... WHERE ... EXISTS (SELECT FROM ...)
385: // and the "WHERE" part is "... EXISTS (SELECT FROM ...)"
386: // Consequently subqueries will be parsed into the relevant block correctly.
387: // Assumes that subqueries are placed in parentheses
388: for (int i = 0; i < tokens[tokenIndex].length(); i++) {
389: char c = tokens[tokenIndex].charAt(i);
390: if (c == '(') {
391: level++;
392: } else if (c == ')') {
393: level--;
394: }
395: }
396: }
397:
398: if (level == 0
399: && AbstractJPQLQuery
400: .isKeyword(tokens[tokenIndex])
401: && !tokens[tokenIndex].equals(keywordToIgnore)) {
402: // Invalid keyword encountered and not currently in subquery block
403: tokenIndex--;
404: break;
405: } else if (level == 0
406: && tokenIndex < tokens.length - 1
407: && AbstractJPQLQuery
408: .isKeyword(tokens[tokenIndex] + ' '
409: + tokens[tokenIndex + 1])) {
410: // Invalid keyword entered ("GROUP BY", "ORDER BY") and not currently in subquery block
411: tokenIndex--;
412: break;
413: } else {
414: // Append the content from the query string from the end of the last token to the end of this token
415: int endPos = queryString.indexOf(
416: tokens[tokenIndex], queryStringPos)
417: + tokens[tokenIndex].length();
418: String contentValue = queryString.substring(
419: queryStringPos, endPos);
420: queryStringPos = endPos;
421:
422: if (content.length() == 0) {
423: content = contentValue;
424: } else {
425: content += contentValue;
426: }
427: }
428: }
429: return content;
430: }
431:
432: /**
433: * Parse the next token looking for a keyword.
434: * The cursor position is skipped in one tick if a keyword is found
435: * @param keyword the searched keyword
436: * @return true if the keyword
437: */
438: public boolean parseKeywordIgnoreCase(String keyword) {
439: if (tokenIndex < tokens.length - 1) {
440: tokenIndex++;
441: if (keywords[tokenIndex] != null) {
442: if (keywords[tokenIndex].equalsIgnoreCase(keyword)) {
443: // Move query position to end of last processed token
444: queryStringPos = queryString.indexOf(
445: keywords[tokenIndex], queryStringPos)
446: + keywords[tokenIndex].length() + 1;
447: return true;
448: }
449: if (keyword.indexOf(' ') > -1) {
450: if ((keywords[tokenIndex] + ' ' + keywords[tokenIndex + 1])
451: .equalsIgnoreCase(keyword)) {
452: // Move query position to end of last processed token
453: queryStringPos = queryString.indexOf(
454: keywords[tokenIndex],
455: queryStringPos)
456: + keywords[tokenIndex].length() + 1;
457: queryStringPos = queryString.indexOf(
458: keywords[tokenIndex + 1],
459: queryStringPos)
460: + keywords[tokenIndex + 1].length()
461: + 1;
462: tokenIndex++;
463: return true;
464: }
465: }
466: }
467: tokenIndex--;
468: }
469: return false;
470: }
471:
472: /**
473: * Parse the next token looking for a keyword. The cursor position is
474: * skipped in one tick if a keyword is found
475: * @return the parsed keyword or null
476: */
477: public String parseKeyword() {
478: if (tokenIndex < tokens.length - 1) {
479: tokenIndex++;
480: if (keywords[tokenIndex] != null) {
481: return keywords[tokenIndex];
482: }
483: tokenIndex--;
484: }
485: return null;
486: }
487: }
488: }
|