/* * Rapid Beans Framework: Query.java * * Copyright (C) 2009 Martin Bluemel * * Creation Date: 02/09/2006 * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; * either version 3 of the License, or (at your option) any later version. * This program 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 Lesser General Public License for more details. * You should have received a copies of the GNU Lesser General Public License and the * GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. */ package org.rapidbeans.datasource.query; import java.util.List; import java.util.logging.Logger; import org.rapidbeans.core.basic.BeanSorter; import org.rapidbeans.core.basic.Container; import org.rapidbeans.core.basic.Property; import org.rapidbeans.core.basic.RapidBean; import org.rapidbeans.core.basic.RapidBeanImplParent; import org.rapidbeans.core.type.TypeRapidBean; import org.rapidbeans.datasource.DatasourceException; import org.rapidbeans.presentation.ApplicationManager; /** * The base class for every bean query expression. * * @author Martin Bluemel */ public final class Query { private static final Logger log = Logger.getLogger(Query.class.getName()); /** * constant for buffer length. */ private static final int BUFFER_LENGTH = 1024; /** * the query expression tree root. */ private QueryExpression queryExpressionTreeRoot = null; /** * the query expresion that was created before. */ private QueryExpression lastCreatedExpression = null; /** * the query's sorter. */ private BeanSorter sorter = null; /** * @param s * the sorter */ public void setSorter(final BeanSorter s) { this.sorter = s; } /** * the constructor for a bean query. * * @param queryString * the query string */ public Query(final String queryString) { this.parse(queryString); } /** * the evaluation method. * * @param db * the database to query beans from * * @return the collection of found beans. */ public List<RapidBean> findBeans(final Container db) { if (this.queryExpressionTreeRoot == null) { throw new QueryException("QueryExpression.eval(): no query parsed."); } List<RapidBean> results = this.queryExpressionTreeRoot.eval(db, null); if (this.sorter != null) { results = this.sorter.sort(results); } return results; } /** * evaluates the query but anticipates that only one single object is found. * * @param db * the database to query beans from * * @return a single bean found by that query or null if no bean has been * found */ public RapidBean findBean(final Container db) { List<RapidBean> resultSet = this.findBeans(db); switch (resultSet.size()) { case 0: return null; case 1: return resultSet.iterator().next(); default: throw new DatasourceException("Unexpectedly found " + resultSet.size() + " beans instead of one"); } } private int depth1 = 0; // counts open '[' private int depth2 = 0; // counts open ']' private StringBuffer buf = new StringBuffer(BUFFER_LENGTH); private StringBuffer buf1 = new StringBuffer(BUFFER_LENGTH); private int index; private int mult; private boolean sortAsc; /** * the bean query parser. * * @param queryString * the query string */ private void parse(final String queryString) { this.queryExpressionTreeRoot = null; this.lastCreatedExpression = null; int len = queryString.length(); if (len == 0) { throw new QueryException("empty query!"); } int state = 0; char c; this.buf = new StringBuffer(BUFFER_LENGTH); this.buf1 = new StringBuffer(BUFFER_LENGTH); this.depth1 = 0; this.depth2 = 0; for (this.index = 0; this.index < len; this.index++) { c = queryString.charAt(this.index); switch (state) { // parse a class name case 0: state = parseClassnameOrPath(state, c); break; // decide where to go after 0 case 12: state = parseClassnameOrPathAfter(state, c); break; // parse an attribute or (link) set name case 1: state = parseAttributeOrLinkSetName(state, c, queryString); break; // parse an attribute value case 2: state = parseAttributeValue(state, c, queryString); break; // parse function parameters case 13: if (c != ')') { throw new QueryException("Error in query \"" + queryString + "\", unexpected character '" + c + "'," + " expected ')' to close parameterless function."); } state = 2; break; // parse a quoted attribute value constant case 3: state = parseQuotedAttributeValue(state, c, queryString); break; // parse a quoted attribute type constant case 4: state = parseQuotedAttributeTypeConstant(state, c, queryString); break; // parse a sorting attribute path case 5: state = parseSortingAttributePath(state, c); break; // parse a sorting attribute path case 6: state = parseSortingAttributePathAfter(state, c); break; default: throw new QueryException("Error in query \"" + queryString + "\", undefined state " + state); } } switch (state) { case 0: this.addNewBeanTypeOrPathCondition(buf.toString()); break; case 5: this.addSortingCriteria(buf.toString(), this.sortAsc); break; default: break; // throw new QueryException("Error in query \"" + queryString // + "\", undefined state " + state); } } /** * parse a class name or path query part. * * @param curState * the current state * @param c * the character to parse * * @return the parser's next state */ private int parseClassnameOrPath(final int curState, final char c) { int state = curState; switch (c) { default: this.buf.append(c); break; case '[': this.addNewBeanTypeOrPathCondition(this.buf.toString()); this.buf.delete(0, BUFFER_LENGTH); this.depth1++; state = 1; break; case '>': state = 5; break; case '<': state = 5; break; case ']': throw new QueryException("']' not expexted while parsing class name"); case '=': throw new QueryException("'=' not expexted while parsing class name"); case ' ': case '\n': case '\t': this.addNewBeanTypeOrPathCondition(buf.toString()); this.buf.delete(0, BUFFER_LENGTH); state = 12; break; } return state; } /** * parse a class name or path query part. * * @param curState * the current state * @param c * the character to parse * * @return the parser's next state */ private int parseClassnameOrPathAfter(final int curState, final char c) { int state = curState; switch (c) { default: throw new QueryException("'" + c + "' not expexted while" + " parsing blanks after classname or path"); case '[': this.depth1++; state = 1; break; case '>': this.sortAsc = true; state = 5; break; case '<': this.sortAsc = false; state = 5; break; case ' ': case '\n': case '\t': break; } return state; } /** * parse a class name or path query part. * * @param curState * the current state * @param c * the character to parse * @param queryString * the query string to parse * * @return the parser's next state */ private int parseAttributeOrLinkSetName(final int curState, final char c, final String queryString) { int state = curState; switch (c) { default: this.buf.append(c); break; case '\'': if (this.buf.length() > 0) { throw new QueryException("Buffer not empty before scanning quoted constant"); } this.buf.append("'"); state = 4; break; case '(': this.buf.delete(0, BUFFER_LENGTH); this.addNewOpenBrace(); this.depth2++; break; case ')': if (this.buf.length() > 0) { if (this.buf.toString().startsWith("'")) { this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_EQ); } else { this.addNewLinkSetCondition(this.buf.toString(), -1); } } this.buf.delete(0, BUFFER_LENGTH); this.lastCreatedExpression = this.closeBrace2(this.depth2); this.depth2--; break; case '[': if (this.buf.length() > 0) { this.mult = -1; char c1 = queryString.charAt(this.index + 1); if (c1 > '0' && c1 < '9') { while (c1 > '0' && c1 < '9') { this.index++; this.buf1.append(c1); c1 = queryString.charAt(this.index + 1); } this.mult = Integer.parseInt(this.buf1.toString()); this.buf1.delete(0, BUFFER_LENGTH); if (c1 == ',') { this.index++; } else { if (c1 != ']') { throw new QueryException("expected ',' or ']' after multiplicity " + mult + " for link set condition \"" + buf.toString() + "\""); } } } this.addNewLinkSetCondition(buf.toString(), mult); this.buf.delete(0, BUFFER_LENGTH); } this.depth1++; state = 1; break; case ']': if (this.buf.length() > 0) { this.addNewLinkSetCondition(this.buf.toString(), -1); } this.buf.delete(0, BUFFER_LENGTH); this.lastCreatedExpression = this.closeBrace1(this.depth1); this.depth1--; if (this.depth1 == 0) { if (this.depth2 > 0) { throw new DatasourceException("Closing ')' missing before last closing ']'"); } state = 6; } break; case '=': if (queryString.charAt(this.index + 1) == '=') { this.index++; } this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_EQ); this.buf.delete(0, BUFFER_LENGTH); state = 2; break; case '$': this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_MATCH); this.buf.delete(0, BUFFER_LENGTH); state = 2; break; case '!': if (queryString.charAt(this.index + 1) == '=') { this.index++; } this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_NE); this.buf.delete(0, BUFFER_LENGTH); state = 2; break; case '>': if (queryString.charAt(this.index + 1) == '=') { this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_GE); this.index++; } else { this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_GT); } this.buf.delete(0, BUFFER_LENGTH); state = 2; break; case '<': if (queryString.charAt(this.index + 1) == '=') { this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_LE); this.index++; } else { this.addNewAttrValueCondition(this.buf.toString(), QueryExprConditionAttrval.OPERATOR_COMP_LT); } this.buf.delete(0, BUFFER_LENGTH); state = 2; break; case ' ': case '\n': case '\t': break; } return state; } /** * parse attribute value. * * @param curState * the current state * @param c * the character to parse * @param queryString * the query string to parse * * @return the parser's next state */ private int parseAttributeValue(final int curState, final char c, final String queryString) { int state = curState; switch (c) { case '\'': if (this.buf.length() > 0) { throw new QueryException("Buffer not empty before scanning quoted constant"); } state = 3; break; case ']': case ')': if (this.buf.length() > 0) { this.setValueOfAttrValueCondition(this.buf.toString()); this.buf.delete(0, BUFFER_LENGTH); } switch (c) { case ']': this.lastCreatedExpression = this.closeBrace1(this.depth1); this.depth1--; break; case ')': this.lastCreatedExpression = this.closeBrace2(this.depth2); this.depth2--; break; default: throw new QueryException("Unexpected character '" + c + "\'. Expected ']' or ')'."); } break; case '&': if (queryString.charAt(this.index + 1) == '&') { this.index++; } if (this.buf.length() > 0) { this.setValueOfAttrValueCondition(buf.toString()); this.buf.delete(0, BUFFER_LENGTH); } this.addNewBoolAndExpression(); state = 1; break; case '|': if (c == '|' && queryString.charAt(this.index + 1) == '|') { this.index++; } if (this.buf.length() > 0) { this.setValueOfAttrValueCondition(this.buf.toString()); this.buf.delete(0, BUFFER_LENGTH); } this.addNewBoolOrExpression(); state = 1; break; case '(': // quick hack for primitive functions if (this.buf.length() > 0 && this.buf.toString().equals("authenticatedUser")) { this.setValueOfAttrValueCondition(ApplicationManager.getApplication().getAuthenticatedUser() .getProperty("accountname").toString()); this.buf.delete(0, BUFFER_LENGTH); } else { throw new DatasourceException("Unexpected fuction '" + this.buf.toString() + "' in query"); } state = 13; break; case '[': case '=': case '>': case '<': throw new DatasourceException("Unexpected character '" + c + "' in query"); case ' ': case '\n': case '\t': break; default: this.buf.append(c); break; } return state; } /** * parse a quoted attribute value. * * @param curState * the current state * @param c * the character to parse * @param queryString * the query string to parse * * @return the parser's next state */ private int parseQuotedAttributeValue(final int curState, final char c, final String queryString) { int state = curState; switch (c) { case '\'': state = 2; break; case '\\': this.index++; final char c1 = queryString.charAt(this.index); switch (c1) { case '\\': case '\'': this.buf.append(c1); break; case 'n': this.buf.append('\n'); break; case 't': this.buf.append('\t'); break; case '0': this.buf.append('\0'); break; default: throw new QueryException("Error in query \"" + queryString + "\", invalid escape sequence \\" + c + ".\n" + "Valid escape sequences \\\\, \\n, \\t, \\0"); } break; default: this.buf.append(c); break; } return state; } /** * parse a quoted attribute type constant. * * @param curState * the current state * @param c * the character to parse * * @return the parser's next state */ private int parseQuotedAttributeTypeConstant(final int curState, final char c, final String queryString) { int state = curState; switch (c) { case '\'': state = 1; break; case '\\': this.index++; final char c1 = queryString.charAt(this.index); switch (c1) { case '\\': case '\'': this.buf.append(c1); break; case 'n': this.buf.append('\n'); break; case 't': this.buf.append('\t'); break; case '0': this.buf.append('\0'); break; default: throw new QueryException("Error in query \"" + queryString + "\", invalid escape sequence \\" + c1 + ".\n" + "Valid escape sequences \\\\, \\n, \\t, \\0"); } break; default: this.buf.append(c); break; } return state; } /** * parse a sorting criteria. * * @param curState * the current state * @param c * the character to parse * * @return the parser's next state */ private int parseSortingAttributePath(final int curState, final char c) { int state = curState; switch (c) { default: this.buf.append(c); break; case ',': case ' ': case '\n': case '\t': this.addSortingCriteria(buf.toString(), this.sortAsc); this.buf.delete(0, BUFFER_LENGTH); state = 6; break; } return state; } /** * parse blanks until next sorting attribute path. * * @param curState * the current state * @param c * the character to parse * * @return the parser's next state */ private int parseSortingAttributePathAfter(final int curState, final char c) { int state = curState; switch (c) { default: throw new QueryException("missing '>' or '<' for additional sorting criteria"); case '>': this.sortAsc = true; state = 5; break; case '<': this.sortAsc = false; state = 5; break; case ' ': case '\n': case '\t': break; } return state; } /** * create a bean type condition expression. * * @param typenameOrPath * the bean type name or a path */ private void addNewBeanTypeOrPathCondition(final String typenameOrPath) { if (typenameOrPath.indexOf('/') != -1) { this.lastCreatedExpression = new QueryExprConditionPath(typenameOrPath); } else { this.lastCreatedExpression = new QueryExprConditionType(typenameOrPath); } this.queryExpressionTreeRoot = this.lastCreatedExpression; } /** * create an attribute value condition expression. * * @param attrName * the bean type name * @param operator * the operator */ private void addNewAttrValueCondition(final String attrName, final int operator) { if (this.lastCreatedExpression instanceof QueryExprConditionAttrval) { throw new QueryException("can't create an AttrValueCondition directliy after another"); } this.lastCreatedExpression = new QueryExprConditionAttrval(attrName, operator, this.lastCreatedExpression); } /** * set the value for an attribute value condition expression. * * @param value * the value string */ private void setValueOfAttrValueCondition(final String value) { final QueryExprConditionAttrval attrExpr = (QueryExprConditionAttrval) this.lastCreatedExpression; if (attrExpr.getCompOperator() == QueryExprConditionAttrval.OPERATOR_COMP_MATCH) { attrExpr.setAttrRegExpPattern(value); } else { attrExpr.setAttrValue(value); } log.fine(" ---------------------------------------"); log.fine(this.queryExpressionTreeRoot.dump("", 0, false, false, "")); log.fine(" ---------------------------------------"); log.fine(" set value \"" + value + "\" of attr value condition"); } /** * create a new association condition expression. * * @param colPropName * the name of the collection attribute * @param multiplicity * ??? */ private void addNewLinkSetCondition(final String colPropName, final int multiplicity) { this.lastCreatedExpression = new QueryExprConditionLinkSet(colPropName, multiplicity, this.lastCreatedExpression); log.fine(" adding LinkSetExpression"); log.fine(" ---------------------------------------"); log.fine(this.queryExpressionTreeRoot.dump("", 0, false, false, "")); log.fine(" ---------------------------------------"); } /** * create and AND expression. */ private void addNewBoolAndExpression() { // if (!(this.lastCreatedExpression instanceof QueryExprConditionAttrval // || this.lastCreatedExpression instanceof QueryExprConditionLinkSet // || this.lastCreatedExpression instanceof QueryExprBrace)) { // throw new // QueryException("can't create a BoolAndExpression after a \"" // + this.lastCreatedExpression.getClass().getName() + "\"\n" // + "only directly after an " // + "AttributeValueCondition\n" + // "or LinkSetCondition or a BraceExpression"); // } this.lastCreatedExpression = new QueryExprBoolAnd(this.lastCreatedExpression); log.fine(" adding new BoolAndExpression"); log.fine(" ---------------------------------------"); log.fine(this.queryExpressionTreeRoot.dump("", 0, false, false, "")); log.fine(" ---------------------------------------"); } /** * create and OR expression. */ private void addNewBoolOrExpression() { if (!(this.lastCreatedExpression instanceof QueryExprConditionAttrval)) { throw new QueryException("can create a BoolOrExpression only" + " directly after an AttributeValueCondition"); } this.lastCreatedExpression = new QueryExprBoolOr(this.lastCreatedExpression); log.fine(" adding new BoolOrExpression"); log.fine(" ---------------------------------------"); log.fine(this.queryExpressionTreeRoot.dump("", 0, false, false, "")); log.fine(" ---------------------------------------"); } private void addSortingCriteria(final String propPath, final boolean asc) { if (this.sorter == null) { this.sorter = new BeanSorter(); } if (this.queryExpressionTreeRoot instanceof QueryExprConditionType) { final QueryExprConditionType rootExpr = (QueryExprConditionType) this.queryExpressionTreeRoot; final TypeRapidBean type = TypeRapidBean.forName(rootExpr.getTypename()); RapidBean bbExample; if (type.getAbstract()) { final List<TypeRapidBean> concreteSubtypes = type.getConcreteSubtypes(); if (concreteSubtypes.size() == 0) { throw new QueryException("no concrete subtype found for type \"" + type.getName() + "\""); } final TypeRapidBean firstConcreteSubtype = concreteSubtypes.get(0); bbExample = RapidBeanImplParent.createInstance(firstConcreteSubtype); } else { bbExample = RapidBeanImplParent.createInstance(rootExpr.getTypename()); } final Property prop = bbExample.getProperty(propPath); if (prop == null) { throw new DatasourceException("invalid sort criteria \"" + propPath + "\"" + " for bean type \"" + bbExample.getType().getName() + "\""); } this.sorter.addSortCriteria(prop.getType()); } else { throw new DatasourceException("queryExpressionTreeRoot has unexpected class"); } } /** * opens a brace expression. */ private void addNewOpenBrace() { log.fine(" adding new Brace Expression"); if (this.lastCreatedExpression instanceof QueryExprConditionAttrval) { throw new QueryException("can't create a BraceExpression directly after an AttributeValue Condition"); } this.lastCreatedExpression = new QueryExprBrace(this.lastCreatedExpression); } /** * finishes a [] brace expression. * * @param depth * the depth * * @return the [] brace expression */ private QueryExpression closeBrace1(final int depth) { QueryExpression lastCreatedBraceExpression = this.lastCreatedExpression; log.fine(" closing Brace 1a ']': lastCreatedExpression:" + lastCreatedBraceExpression.getClass().getName()); while (!(lastCreatedBraceExpression instanceof QueryExprConditionLinkSet || lastCreatedBraceExpression instanceof QueryExprConditionType || (lastCreatedBraceExpression instanceof QueryExprConditionAttrval && depth <= 1))) { lastCreatedBraceExpression = lastCreatedBraceExpression.getParentExpression(); log.fine(" closing Brace 1b ']': lastCreatedExpression:" + lastCreatedBraceExpression.getClass().getName()); } return lastCreatedBraceExpression; } /** * finishes a () brace expression. * * @param depth * the depth * * @return the () brace expression */ private QueryExpression closeBrace2(final int depth) { QueryExpression lastCreatedBraceExpression = this.lastCreatedExpression; log.fine(" closing Brace 2a ')': lastCreatedExpression:" + lastCreatedBraceExpression.getClass().getName()); while (!(lastCreatedBraceExpression instanceof QueryExprBrace)) { lastCreatedBraceExpression = lastCreatedBraceExpression.getParentExpression(); log.fine(" closing Brace 2b ')': lastCreatedExpression:" + lastCreatedBraceExpression.getClass().getName()); } ((QueryExprBrace) lastCreatedBraceExpression).close(); return lastCreatedBraceExpression; } /** * @return the queryExpressionTreeRoot */ public QueryExpression getQueryExpressionTreeRoot() { return queryExpressionTreeRoot; } }