001: /**********************************************************************
002: Copyright (c) 2005 Andy Jefferson 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:
016: Contributors:
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 JDOQL Single-String queries.
030: * Takes a JDOQLQuery and the query string and parses it into its constituent parts, updating
031: * the JDOQLQuery accordingly with the result that after calling the parse() method the JDOQLQuery
032: * is populated.
033: * <pre>
034: * select [unique] [ <result> ] [into <result-class-name>]
035: * [from <candidate-class-name> [exclude subclasses] ]
036: * [where <filter>]
037: * [variables <variables-clause> ]
038: * [parameters <parameters-clause>]
039: * [<imports-clause>]
040: * [group by <grouping-clause> ]
041: * [order by <ordering-clause>]
042: * [range <from-range> ,<to-range>]
043: * </pre>
044: * Note that {filter} can contain subqueries, hence containing keywords
045: * <pre>
046: * SELECT c FROM Customer c WHERE timeAvailable < (SELECT avg(hours) FROM Employee e)
047: * </pre>
048: * So the "filter" for the outer query is "timeAvailable < (SELECT avg(hours) FROM Employee e)"
049: * @version $Revision: 1.13 $
050: */
051: public class JDOQLSingleStringParser {
052: /** Localiser for messages. */
053: protected static final Localiser LOCALISER = Localiser
054: .getInstance("org.jpox.store.Localisation");
055:
056: /** The JDOQL query to populate. */
057: private AbstractJavaQuery query;
058:
059: /** The single-string query string. */
060: private String queryString;
061:
062: /** Record of the keyword currently being processed, so we can check for out of order keywords. */
063: int keywordPosition = -1;
064:
065: /**
066: * Constructor for the Single-String parser.
067: * @param query The query
068: * @param queryString The Single-String query
069: */
070: public JDOQLSingleStringParser(AbstractJavaQuery query,
071: String queryString) {
072: JPOXLogger.QUERY.debug(LOCALISER.msg("042010", queryString));
073: this .query = query;
074: this .queryString = queryString;
075: }
076:
077: /**
078: * Method to parse the Single-String query
079: */
080: public void parse() {
081: new Compiler(new Parser(queryString)).compile();
082: }
083:
084: /**
085: * Compiler to process keywords contents. In the query the keywords often have
086: * content values following them that represent the constituent parts of the query. This takes the keyword
087: * and sets the constituent part accordingly.
088: */
089: private class Compiler {
090: Parser tokenizer;
091:
092: Compiler(Parser tokenizer) {
093: this .tokenizer = tokenizer;
094: }
095:
096: private void compile() {
097: compileSelect();
098: String keyword = tokenizer.parseKeyword();
099: if (keyword != null
100: && AbstractJDOQLQuery.isKeyword(keyword)) {
101: // any keyword after compiling the SELECT is an error
102: throw new JPOXUserException(LOCALISER.msg("042011",
103: keyword));
104: }
105: }
106:
107: private void compileSelect() {
108: if (!tokenizer.parseKeyword("SELECT")
109: && !tokenizer.parseKeyword("select")) {
110: throw new JPOXUserException(LOCALISER.msg("042012"));
111: }
112: if (tokenizer.parseKeyword("UNIQUE")
113: || tokenizer.parseKeyword("unique")) {
114: compileUnique();
115: }
116: compileResult();
117: if (tokenizer.parseKeyword("INTO")
118: || tokenizer.parseKeyword("into")) {
119: compileInto();
120: }
121: if (tokenizer.parseKeyword("FROM")
122: || tokenizer.parseKeyword("from")) {
123: compileFrom();
124: }
125: if (tokenizer.parseKeyword("WHERE")
126: || tokenizer.parseKeyword("where")) {
127: compileWhere();
128: }
129: if (tokenizer.parseKeyword("VARIABLES")
130: || tokenizer.parseKeyword("variables")) {
131: compileVariables();
132: }
133: if (tokenizer.parseKeyword("PARAMETERS")
134: || tokenizer.parseKeyword("parameters")) {
135: compileParameters();
136: }
137: if (tokenizer.parseKeyword("import")) {
138: compileImport();
139: }
140: if (tokenizer.parseKeyword("GROUP")
141: || tokenizer.parseKeyword("group")) {
142: compileGroup();
143: }
144: if (tokenizer.parseKeyword("ORDER")
145: || tokenizer.parseKeyword("order")) {
146: compileOrder();
147: }
148: if (tokenizer.parseKeyword("RANGE")
149: || tokenizer.parseKeyword("range")) {
150: compileRange();
151: }
152: }
153:
154: private void compileUnique() {
155: query.setUnique(true);
156: }
157:
158: private void compileResult() {
159: String content = tokenizer.parseContent(false);
160: if (content.length() > 0) {
161: query.setResult(content);
162: }
163: }
164:
165: private void compileInto() {
166: String content = tokenizer.parseContent(false);
167: if (content.length() == 0) {
168: // content cannot be empty
169: throw new JPOXUserException(LOCALISER.msg("042014",
170: "INTO", "<result class>"));
171: }
172:
173: query.setResultClassName(content);
174: }
175:
176: private void compileFrom() {
177: String content = tokenizer.parseContent(false);
178: if (content.length() == 0) {
179: // content cannot be empty
180: throw new JPOXUserException(LOCALISER.msg("042014",
181: "FROM", "<candidate class>"));
182: }
183:
184: if (content.indexOf(' ') > 0) {
185: // Subquery accepts "<candidate-expr> alias"
186: query.setFrom(content.trim());
187: } else {
188: // Content is "<candidate-class-name>"
189: query.setCandidateClassName(content);
190: }
191:
192: if (tokenizer.parseKeyword("EXCLUDE")
193: || tokenizer.parseKeyword("exclude")) {
194: if (!tokenizer.parseKeyword("SUBCLASSES")
195: && !tokenizer.parseKeyword("subclasses")) {
196: throw new JPOXUserException(LOCALISER.msg("042015",
197: "SUBCLASSES", "EXCLUDE"));
198: }
199: content = tokenizer.parseContent(false);
200: if (content.length() > 0) {
201: throw new JPOXUserException(LOCALISER.msg("042013",
202: "EXCLUDE SUBCLASSES", content));
203: }
204: query.setSubclasses(false);
205: }
206: }
207:
208: private void compileWhere() {
209: String content = tokenizer.parseContent(true);
210: if (content.length() == 0) {
211: // content cannot be empty
212: throw new JPOXUserException(LOCALISER.msg("042014",
213: "WHERE", "<filter>"));
214: }
215:
216: if (content.indexOf("SELECT") > 0
217: || content.indexOf("select") > 0) {
218: // Subquery (or subqueries) present
219: StringBuffer contentStr = new StringBuffer(content);
220: boolean parsed = false;
221: int currentPosition = 0;
222: int subqueryNum = 1;
223: while (!parsed) {
224: // Find the next SELECT - subquery start
225: int selectPos1 = contentStr.indexOf("SELECT",
226: currentPosition);
227: int selectPos2 = contentStr.indexOf("select",
228: currentPosition);
229: if (selectPos1 < 0 && selectPos2 < 0) {
230: parsed = true;
231: break;
232: } else {
233: if (selectPos1 > 0 && selectPos2 > 0) {
234: currentPosition = Math.min(selectPos1,
235: selectPos2);
236: } else if (selectPos1 > 0 && selectPos2 < 0) {
237: currentPosition = selectPos1;
238: } else if (selectPos1 < 0 && selectPos2 > 0) {
239: currentPosition = selectPos2;
240: }
241:
242: // Find the opening brace
243: int startPosition = currentPosition;
244: for (int i = currentPosition - 1; i >= 0; i--) {
245: if (contentStr.charAt(i) == '(') {
246: startPosition = i;
247: break;
248: }
249: }
250:
251: // Process subquery from this position until we find the end
252: int level = 0;
253: for (int i = currentPosition; i < content
254: .length(); i++) {
255: if (contentStr.charAt(i) == '(') {
256: level++;
257: } else if (contentStr.charAt(i) == ')') {
258: level--;
259: }
260: if (level == -1) {
261: // End of subquery, so register it
262: String subqueryString = contentStr
263: .substring(currentPosition, i);
264: String subqueryVarName = "SUBQ"
265: + subqueryNum;
266:
267: Query subquery = (Query) ClassUtils
268: .newInstance(
269: query.getClass(),
270: new Class[] {
271: ObjectManager.class,
272: String.class },
273: new Object[] {
274: query
275: .getObjectManager(),
276: subqueryString });
277: // TODO Set the type of the variable
278: query.addSubquery(subquery, "double "
279: + subqueryVarName, null, null);
280:
281: // Strip out the subquery from the content, replacing it with the variable name
282: contentStr.replace(startPosition,
283: i + 1, subqueryVarName);
284: subqueryNum++;
285: break;
286: }
287: }
288: }
289: }
290:
291: query.setFilter(contentStr.toString());
292: } else {
293: query.setFilter(content);
294: }
295: }
296:
297: private void compileVariables() {
298: String content = tokenizer.parseContent(false);
299: if (content.length() == 0) {
300: // content cannot be empty
301: throw new JPOXUserException(LOCALISER.msg("042014",
302: "VARIABLES", "<variable declarations>"));
303: }
304: query.declareExplicitVariables(content);
305: }
306:
307: private void compileParameters() {
308: String content = tokenizer.parseContent(false);
309: if (content.length() == 0) {
310: // content cannot be empty
311: throw new JPOXUserException(LOCALISER.msg("042014",
312: "PARAMETERS", "<parameter declarations>"));
313: }
314: query.declareExplicitParameters(content);
315: }
316:
317: private void compileImport() {
318: String content = "import " + tokenizer.parseContent(false);
319: while (tokenizer.parseKeyword("import")) {
320: content += "import " + tokenizer.parseContent(false);
321: }
322: query.declareImports(content);
323: }
324:
325: private void compileGroup() {
326: String content = tokenizer.parseContent(false);
327: if (!tokenizer.parseKeyword("BY")
328: && !tokenizer.parseKeyword("by")) {
329: // GROUP must be followed by BY
330: throw new JPOXUserException(LOCALISER.msg("042015",
331: "BY", "GROUP"));
332: }
333:
334: content = tokenizer.parseContent(false);
335: if (content.length() == 0) {
336: // content cannot be empty
337: throw new JPOXUserException(LOCALISER.msg("042014",
338: "GROUP BY", "<grouping>"));
339: }
340: query.setGrouping(content);
341: }
342:
343: private void compileOrder() {
344: String content = tokenizer.parseContent(false);
345: if (!tokenizer.parseKeyword("BY")
346: && !tokenizer.parseKeyword("by")) {
347: // ORDER must be followed by BY
348: throw new JPOXUserException(LOCALISER.msg("042015",
349: "BY", "ORDER"));
350: }
351:
352: content = tokenizer.parseContent(false);
353: if (content.length() == 0) {
354: // content cannot be empty
355: throw new JPOXUserException(LOCALISER.msg("042014",
356: "ORDER BY", "<ordering>"));
357: }
358: query.setOrdering(content);
359: }
360:
361: private void compileRange() {
362: String content = tokenizer.parseContent(false);
363: if (content.length() == 0) {
364: // content cannot be empty
365: throw new JPOXUserException(LOCALISER.msg("042014",
366: "RANGE", "<range>"));
367: }
368: query.setRange(content);
369: }
370: }
371:
372: /**
373: * Tokenizer that provides access to current token.
374: */
375: private class Parser {
376: final String queryString;
377:
378: int queryStringPos = 0;
379:
380: /** tokens */
381: final String[] tokens;
382:
383: /** keywords */
384: final String[] keywords;
385:
386: /** current token cursor position */
387: int tokenIndex = -1;
388:
389: /**
390: * Constructor
391: * @param str String to parse
392: */
393: public Parser(String str) {
394: queryString = str;
395:
396: StringTokenizer tokenizer = new StringTokenizer(str);
397: tokens = new String[tokenizer.countTokens()];
398: keywords = new String[tokenizer.countTokens()];
399: int i = 0;
400: while (tokenizer.hasMoreTokens()) {
401: tokens[i] = tokenizer.nextToken();
402: if (AbstractJDOQLQuery.isKeyword(tokens[i])) {
403: keywords[i] = tokens[i];
404: }
405: i++;
406: }
407: }
408:
409: /**
410: * Parse the content until a keyword is found
411: * @param allowSubentries Whether to permit subentries (in parentheses) in this next block
412: * @return the content
413: */
414: public String parseContent(boolean allowSubentries) {
415: String content = "";
416: int level = 0;
417: while (tokenIndex < tokens.length - 1) {
418: tokenIndex++;
419:
420: if (allowSubentries) {
421: // Process this token to check level of parentheses.
422: // This is necessary because we want to ignore keywords if within a parentheses-block
423: // e.g SELECT ... FROM ... WHERE param1 < (SELECT ... FROM ...)
424: // and the "WHERE" part is "param1 < (SELECT ... FROM ...)"
425: // Consequently subqueries will be parsed into the relevant block correctly.
426: // Assumes that subqueries are placed in parentheses
427: for (int i = 0; i < tokens[tokenIndex].length(); i++) {
428: char c = tokens[tokenIndex].charAt(i);
429: if (c == '(') {
430: level++;
431: } else if (c == ')') {
432: level--;
433: }
434: }
435: }
436:
437: if (level == 0
438: && AbstractJDOQLQuery
439: .isKeyword(tokens[tokenIndex])) {
440: // Keyword encountered, and not part of any subquery so end of content block
441: tokenIndex--;
442: break;
443: } else {
444: // Append the content from the query string from the end of the last token to the end of this token
445: int endPos = queryString.indexOf(
446: tokens[tokenIndex], queryStringPos)
447: + tokens[tokenIndex].length();
448: String contentValue = queryString.substring(
449: queryStringPos, endPos);
450: queryStringPos = endPos;
451:
452: if (content.length() == 0) {
453: content = contentValue;
454: } else {
455: content += contentValue;
456: }
457: }
458: }
459:
460: return content;
461: }
462:
463: /**
464: * Parse the next token looking for a keyword. The cursor position is
465: * skipped in one tick if a keyword is found
466: * @param keyword the searched keyword
467: * @return true if the keyword
468: */
469: public boolean parseKeyword(String keyword) {
470: if (tokenIndex < tokens.length - 1) {
471: tokenIndex++;
472: if (keywords[tokenIndex] != null) {
473: if (keywords[tokenIndex].equals(keyword)) {
474: // Move query position to end of last processed token
475: queryStringPos = queryString.indexOf(
476: keywords[tokenIndex], queryStringPos)
477: + keywords[tokenIndex].length() + 1;
478: return true;
479: }
480: }
481: tokenIndex--;
482: }
483: return false;
484: }
485:
486: /**
487: * Parse the next token looking for a keyword. The cursor position is
488: * skipped in one tick if a keyword is found
489: * @return the parsed keyword or null
490: */
491: public String parseKeyword() {
492: if (tokenIndex < tokens.length - 1) {
493: tokenIndex++;
494: if (keywords[tokenIndex] != null) {
495: return keywords[tokenIndex];
496: }
497: tokenIndex--;
498: }
499: return null;
500: }
501: }
502: }
|