/**
* Speedo: an implementation of JDO compliant personality on top of JORM generic
* I/O sub-system.
* Copyright (C) 2001-2005 France Telecom R&D
*
* This library 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 2 of the License, or (at your option) any later version.
*
* This library 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 copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
*
* Authors: S.Chassande-Barrioz.
* Created on 3 fevr. 2005
*
*/
package org.objectweb.speedo.query.jdo.parser;
import org.objectweb.jorm.api.PMapper;
import org.objectweb.jorm.metainfo.api.ClassRef;
import org.objectweb.jorm.metainfo.api.GenClassRef;
import org.objectweb.jorm.metainfo.api.Reference;
import org.objectweb.jorm.type.api.PType;
import org.objectweb.medor.api.Field;
import org.objectweb.medor.api.MedorException;
import org.objectweb.medor.expression.api.Expression;
import org.objectweb.medor.expression.api.ExpressionException;
import org.objectweb.medor.expression.api.Operator;
import org.objectweb.medor.filter.api.FieldOperand;
import org.objectweb.medor.filter.lib.Avg;
import org.objectweb.medor.filter.lib.BasicFieldOperand;
import org.objectweb.medor.filter.lib.Count;
import org.objectweb.medor.filter.lib.Max;
import org.objectweb.medor.filter.lib.Min;
import org.objectweb.medor.filter.lib.Sum;
import org.objectweb.medor.query.api.PropagatedField;
import org.objectweb.medor.query.api.QueryNode;
import org.objectweb.medor.query.api.QueryTree;
import org.objectweb.medor.query.api.QueryTreeField;
import org.objectweb.medor.query.jorm.lib.ClassExtent;
import org.objectweb.medor.query.jorm.lib.JormQueryTreeHelper;
import org.objectweb.medor.query.jorm.lib.PNameField;
import org.objectweb.medor.query.lib.Nest;
import org.objectweb.medor.query.lib.SelectProject;
import org.objectweb.speedo.api.SpeedoException;
import org.objectweb.speedo.mim.api.HomeItf;
import org.objectweb.speedo.query.jdo.JDOQueryDefinitionImpl;
import org.objectweb.speedo.query.jdo.JDOQueryEvalContext;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
/**
* This visitor parses select and group by clauses in order to build the
* projected field of a MEDOR query.
*
* @author S.Chassande-Barrioz
*/
public class SelectGroupByVisitor {
/**
* key word for the count operator
*/
private static final String COUNT = "count";
/**
* key word for the sum operator
*/
private static final String SUM = "sum";
/**
* key word for the min operator
*/
private static final String MIN = "min";
/**
* key word for the max operator
*/
private static final String MAX = "max";
/**
* key word for the avg operator
*/
private static final String AVG = "avg";
/**
* key word for the distrinct constraint
*/
private static final String DISTINCT = "distinct";
/**
* key word for the unique constraint
*/
private static final String UNIQUE = "unique";
/**
* list of keywords
*/
private static final String[] keywords = new String[] {
COUNT.toLowerCase(), //0
COUNT.toUpperCase(), //1
SUM.toLowerCase(), //2
SUM.toUpperCase(), //3
MIN.toLowerCase(), //4
MIN.toUpperCase(), //5
MAX.toLowerCase(), //6
MAX.toUpperCase(), //7
AVG.toLowerCase(), //8
AVG.toUpperCase(), //9
DISTINCT.toLowerCase(), //10
DISTINCT.toUpperCase(), //11
"(", //12
")", //13
",", //14
"as", //15
"AS", //16
UNIQUE.toLowerCase(), //17
UNIQUE.toUpperCase() //18
};
/**
* Retrieves the String representation of the function identifier
*/
private final static String getFunctionAsString(int functionIdx) {
switch (functionIdx / 2) {
case 0:
return "COUNT";
case 1:
return "SUM";
case 2:
return "MIN";
case 3:
return "MAX";
case 4:
return "AVG";
default:
return "UNKNOWN";
}
}
/**
* SelectProjet where the simple (not aggregat operation) selected fields
* must be projected.
*/
private QueryNode top;
/**
* The query tree corresponding to the query
*/
private QueryTree qt;
/**
* The JORM PMapper permitting to fetch PClassMapping for prefetching
*/
private PMapper mapper;
private ClassLoader classloader;
/**
* The visitor of variable used in Speedo for building the query. The
* visitor is used here for building paths of the select. Indeed the select
* clause can contains navigation path.
*/
private SpeedoQLVariableVisitor sqvv;
/**
* The QueryEvalContext used in Speedo for evaluate the MEDOR query. During
* the visit of select clause, the algorithm determines if prefetching is
* possible and on which class. If prefetching is possible, the
* PClassMapping of the prefetched class and the pname index are assigned on
* the #qec.
*/
private JDOQueryEvalContext qec;
/**
* indicate if a distinct constraint is specified in the select clause This
* field is filled after the call of the parse(String) method.
*/
private boolean unique = false;
/**
* indicate if a distinct constraint is specified in the select clause This
* field is filled after the call of the parse(String) method.
*/
private boolean distinct = false;
/**
* indicate if the select clause contains aggregat operator(s) This field is
* filled after the call of the parse(String) method.
*/
private boolean hasAggregate = false;
/**
* contains the MEDOR object permitting to build the projection part of the
* query. MEDOR object type can be one of the following: - Field : it means
* the selected part is a simple navigation (this, this.a.f1, this.a.myB,
* ...) - Expression: it means the selected part is a calculated field such
* as aggregate function (AVG(this.a.f1), this.f1 + this.f2, ...).
*
* This field is filled after the call of the parse(String) method.
*/
private ArrayList selectfields = new ArrayList();
/**
* contains the alias name (String) of the projected fields. The content of
* this field is linked to the #selectfields field. A null value in the list
* means the corresponding selected fields (same index) has no alias.
*
* This field is filled after the call of the parse(String) method.
*/
private ArrayList aliases = new ArrayList();
/**
* contains the list of grouped fields. The grouped fields are the one used
* in an aggregat operator.
*
* This field is filled after the call of the parse(String) method.
*/
private ArrayList groupedFields = new ArrayList();
/**
* contains the list of fields specified in the group by clause.
*
* This field is filled after the call of the parse(String) method.
*/
private ArrayList groupByFields = new ArrayList();
private ArrayList usedFields = new ArrayList();
private ArrayList selectFieldTypes = new ArrayList();
/**
* @param qt
* @param sp
* @param mapper
* @param sqvv
* @param qd
* @param qec
*/
public SelectGroupByVisitor(SelectProject sp,
QueryTree qt,
PMapper mapper,
SpeedoQLVariableVisitor sqvv,
JDOQueryEvalContext qec,
ClassLoader classloader) {
super();
this.qt = qt;
this.top = sp;
this.mapper = mapper;
this.sqvv = sqvv;
this.qec = qec;
this.classloader = classloader;
}
public SelectGroupByVisitor(SelectProject sp,
QueryTree qt,
SpeedoQLVariableVisitor sqvv,
ClassLoader classloader) {
this(sp, qt, null, sqvv, null, classloader);
}
public Class[] getSelectFieldTypes() {
return (Class[]) selectFieldTypes.toArray(new Class[selectFieldTypes.size()]);
}
/**
* Visit the select and Groupby clause
* @param qd is the definition of the query
*/
public void visit(JDOQueryDefinitionImpl qd)
throws SpeedoException {
try {
_visit(qd);
} catch (ExpressionException e) {
throw new SpeedoException(e);
} catch (MedorException e) {
throw new SpeedoException(e);
}
}
/**
* Visit the select and Groupby clause
* @param qd is the definition of the query
*/
private void _visit(JDOQueryDefinitionImpl qd)
throws SpeedoException, MedorException, ExpressionException {
String select = qd.getResult();
String groupby = qd.getGrouping();
boolean withPrefetch = qd.withPrefetch();
if (select == null || select.length() == 0) {
select = "this";
}
parseSelect(select);
parseGroupBy(groupby);
//Prefetching or not
if (!hasAggregate //not aggregation
&& withPrefetch //Prefetching asked
&& selectfields.size() == 1 //only one part in the select
) {
addPrefetchField(qd);
}
if (hasAggregate) {
top = addAggregateNode();
}
top.setDistinct(distinct);
projectFields();
if (qec != null) {
qec.query = top;
}
}
/**
* Add the prefetch field to the current query tree
* @param qd is the definition of the query
*/
private void addPrefetchField(JDOQueryDefinitionImpl qd) throws MedorException {
Field f = ((PropagatedField) selectfields.get(0)).getOriginFields()[0];
if (f instanceof PNameField) {
PNameField pnf = (PNameField) f;
if (pnf.isClassPName() && !pnf.isInGenClass()) {
//Add prefeched fields
ClassExtent ce = (ClassExtent) pnf.getQueryTree();
String prefetchedClassName = ce.getJormName();
HomeItf sh = (HomeItf) mapper.lookup(prefetchedClassName);
if (sh.getPrefetchOnQuery()) {
qec.pcm = sh;
JormQueryTreeHelper.addPrefetchFields(ce, qt, top,
qd.getIncludeSubClasses());
qec.pnIndex = top.getTupleStructure().getSize() + 1;
}
}//else the selected field is a class identifier
} //else the selected field is not an identifier or a reference
}
/**
* @return an aggregate node for aggregate operators. the returned node must
* become the root the query tree.
*/
private Nest addAggregateNode() throws SpeedoException, MedorException {
Map old2newFields = new HashMap();
//project used fields on the QueryNode #top
for (int i = 0; i < usedFields.size(); i++) {
QueryTreeField qtf = (QueryTreeField) usedFields.get(i);
Field newf = top.addPropagatedField(qtf.getName(), qtf
.getType(), new QueryTreeField[] { qtf });
old2newFields.put(qtf, newf);
}
//replace the use of old fields in 'grouped' by the new projected
replaceFieldsInList(groupedFields, old2newFields);
//replace the use of old fields in 'groupby' by the new projected
replaceFieldsInList(groupByFields, old2newFields);
//replace the use of old fields in 'selected' by the new projected
replaceFieldsInList(selectfields, old2newFields);
return new Nest((QueryTreeField[]) groupedFields
.toArray(new QueryTreeField[groupedFields.size()]),
"grouped_fields", (QueryTreeField[]) groupByFields
.toArray(new QueryTreeField[groupByFields
.size()]), "aggregate_node");
//The nest becomes the top node, selected field will add on it
}
/**
* Projects the fields defined in the select clause on the top node.
*/
private void projectFields()
throws SpeedoException, MedorException, ExpressionException {
//project field on the #top query node
for (int i = 0; i < selectfields.size(); i++) {
Object o = selectfields.get(i);
Field fieldOnTop = null;
String fieldName = (String) aliases.get(i);
if (o instanceof Expression) {
if (fieldName == null || fieldName.length() == 0) {
fieldName = "field_" + i;
}
Expression e = (Expression) o;
e.compileExpression();
fieldOnTop = top.addCalculatedField(fieldName, e.getType(), e);
} else if (o instanceof Field) {
Field f = (Field) o;
if (fieldName == null || fieldName.length() == 0) {
fieldName = f.getName();
}
if (!groupByFields.contains(f) || (top instanceof Nest)) {
fieldOnTop = top.addPropagatedField(fieldName, f.getType(),
new QueryTreeField[] { (QueryTreeField) f });
} else {
String fn = top.getName();
if (fn == null || fn.length() == 0) {
fn = "";
} else {
fn += ".";
}
fn += f.getName();
fieldOnTop = top.getTupleStructure().getField(fn);
}
}
//Compute the field type
selectFieldTypes.add(getFieldClass(fieldOnTop));
}
}
/**
* Computes the type of a field.
*/
private Class getFieldClass(Field field) throws SpeedoException {
PType ptype = field.getType();
String className = null;
switch(ptype.getTypeCode()) {
case PType.TYPECODE_BIGDECIMAL:
return BigDecimal.class;
case PType.TYPECODE_BIGINTEGER:
return BigInteger.class;
case PType.TYPECODE_BOOLEAN:
case PType.TYPECODE_OBJBOOLEAN:
return Boolean.class;
case PType.TYPECODE_BYTE:
case PType.TYPECODE_OBJBYTE:
return Byte.class;
case PType.TYPECODE_BYTEARRAY:
return Byte[].class;
case PType.TYPECODE_CHAR:
case PType.TYPECODE_OBJCHAR:
return Character.class;
case PType.TYPECODE_CHARARRAY:
return Character[].class;
case PType.TYPECODE_DATE:
return Date.class;
case PType.TYPECODE_DOUBLE:
case PType.TYPECODE_OBJDOUBLE:
return Double.class;
case PType.TYPECODE_FLOAT:
case PType.TYPECODE_OBJFLOAT:
return Float.class;
case PType.TYPECODE_INT:
case PType.TYPECODE_OBJINT:
return Integer.class;
case PType.TYPECODE_LONG:
case PType.TYPECODE_OBJLONG:
return Long.class;
case PType.TYPECODE_SERIALIZED:
return Serializable.class;
case PType.TYPECODE_SHORT:
case PType.TYPECODE_OBJSHORT:
return Integer.class;
case PType.TYPECODE_STRING:
return String.class;
case PType.TYPECODE_REFERENCE:
if (field instanceof PropagatedField) {
Field f = ((PropagatedField) field).getOriginFields()[0];
if (f instanceof PNameField) {
PNameField pnf = (PNameField) f;
if (pnf.isClassPName()) {
if (pnf.isInGenClass()) {
//identifier of the genclass
className = pnf.getGenClassRef().getGenClassId();
} else {
//identifier of a class
className = pnf.getMetaObjectClass().getFQName();
}
} else {
Reference ref = pnf.getReference();
if (ref instanceof ClassRef) {
//reference to a class
className = ((ClassRef) ref).getMOClass().getFQName();
} else if (ref instanceof GenClassRef) {
//reference to a genclass
className = ((GenClassRef) ref).getGenClassId();
}
}
}
}
break;
default:
className = ptype.getJavaName();
}
if (className == null) {
throw new SpeedoException(
"Type '" + ptype.getJavaName()
+ "' not found for projected field:"
+ field.getName());
} else {
try {
return classloader.loadClass(className);
} catch (ClassNotFoundException e) {
throw new SpeedoException(
"Type '" + className
+ "' not found for projected field:"
+ field.getName(), e);
}
}
}
/**
* Replaces the list elements by the associated value from the map.
* If an element of the list is an Expression (MEDOR), the use of Fields
* in the Expression is replaced.
* @param list
* is the list containg elements to replace
* @param old2newElement
* is the table associating the old element (to replace) to the
* new element.
*/
private void replaceFieldsInList(ArrayList list, Map old2newElement)
throws SpeedoException {
for (int i = 0; i < list.size(); i++) {
Object old = list.get(i);
Object neo = old2newElement.get(old);
if (neo != null && old != neo) {
list.set(i, neo);
} else if (old instanceof Expression) {
replaceFieldsInExpression((Expression) old, old2newElement);
}
}
}
/**
* Replaces field usage in expression.
* @param e is the expression to treat
* @param old2newElement
* is the table associating the old element (to replace) to the
* new element.
* @throws SpeedoException
*/
private void replaceFieldsInExpression(Expression e, Map old2newElement)
throws SpeedoException {
if (e instanceof FieldOperand) {
FieldOperand fo = (FieldOperand) e;
Field old = fo.getField();
Field neo = (Field) old2newElement.get(old);
if (neo != null && old != neo) {
((FieldOperand) e).setField(neo);
}
} else if (e instanceof Operator) {
Operator operator = (Operator) e;
for (int i = 0; i < operator.getOperandNumber(); i++) {
replaceFieldsInExpression(operator.getExpression(i),
old2newElement);
}
}
}
/**
* Parses a select clause (with or without 'SELECT' keyword).
*
* @see #selectfields, #aliases, #groupedFields, #distinct, #hasAggregate .
* It
* @param select
* @throws SpeedoException
*/
private void parseSelect(String select) throws SpeedoException {
StringTokenizer st = new StringTokenizer(select, " (,)", true);
Stack stack = new Stack();
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (token.length() == 0) {
continue;
}
if (token.equalsIgnoreCase("select")) {
continue;
}
int keywordIdx = isKeyword(token);
if (keywordIdx < 0) {
parseFieldExpression(token, stack);
} else if (keywordIdx < 10) {
//Aggregate operator
stack.push(new Integer(keywordIdx));
} else if (keywordIdx == 10 || keywordIdx == 11) {
//distinct
if (stack.size() > 0) {
//the distinct is used in aggregate operator
stack.push(new Boolean(true));
} else {
distinct = true;
}
} else if (keywordIdx == 17 || keywordIdx == 18) {
//unique
unique = true;
} else if (keywordIdx == 14) {
parseComma(stack);
} else if (keywordIdx == 12) {
//(
stack.push(new Integer(keywordIdx));
} else if (keywordIdx == 13) {
//)
parseRightParenthesis(stack);
}
}
if (stack.size() == 1) {
//There is only one element in the select clause
selectfields.add(stack.pop());
aliases.add(null);
}
}
private void parseFieldExpression(String token, Stack stack) throws SpeedoException {
//field expression
if (token.equals("*")) {
token = "this";
}
Field f = sqvv.getField(token);
if (!usedFields.contains(f)) {
usedFields.add(f);
}
stack.push(f);
}
private void parseComma(Stack stack) throws SpeedoException {
if (stack.size() > 0) {
Object o1 = stack.pop();
if (stack.size() > 1) {
//with an alias
Object o2 = stack.pop();
if (stack.size() > 2) {
//not the end of the selected field
stack.push(o2);
stack.push(o1);
} else if (!(o1 instanceof String)
|| !(o2 instanceof Expression)) {
throw new SpeedoException(
"Malformed selected field "
+ (selectfields.size() + 1));
} else {
selectfields.add(o2);
aliases.add(o1);
}
} else {
//without an alias
selectfields.add(o1);
aliases.add(null);
}
} else {
throw new SpeedoException(
"Empty definition of the selected field "
+ (selectfields.size() + 1));
}
}
private void parseRightParenthesis(Stack stack) throws SpeedoException {
ArrayList operands = new ArrayList();
boolean isOperand = true;
Integer functionIdx = null;
//pop all operand until the '(' and the function identifier
// (Integer)
boolean distinctOnOperand = false;
do {
Object o = stack.pop();
isOperand = !new Integer(12).equals(o);
if (isOperand) {
//register the operand
operands.add(o);
} else {
if (stack.peek() instanceof Boolean) {
distinctOnOperand = ((Boolean) stack.pop())
.booleanValue();
}
// fetch the function identifier
functionIdx = (Integer) stack.pop();
}
} while (isOperand);
Expression e;
//Verify the operand number
if (functionIdx.intValue() > 0 && functionIdx.intValue() < 10) {
if (operands.size() != 1) {
throw new SpeedoException(
"Bad number of operand for the function "
+ getFunctionAsString(functionIdx
.intValue())
+ ", expected 1 operand, found "
+ operands.size());
}
}
//instanciate the medor Operator corresponding to the function
Object operand = operands.get(0);
if (operand instanceof Expression) {
e = (Expression) operand;
} else if (operand instanceof Field) {
e = new BasicFieldOperand((Field) operand);
} else {
throw new SpeedoException("Unexpect operand: " + operand);
}
getFieldsFromExpression(e, groupedFields);
hasAggregate = true;
switch (functionIdx.intValue() / 2) {
case 0:
e = new Count(e, distinctOnOperand);
break;
case 1:
e = new Sum(e, distinctOnOperand);
break;
case 2:
e = new Min(e, distinctOnOperand);
break;
case 3:
e = new Max(e, distinctOnOperand);
break;
case 4:
e = new Avg(e, distinctOnOperand);
break;
default:
throw new SpeedoException("Unknown function identifier: "
+ functionIdx.intValue());
}
stack.push(e);
}
/**
* Parses a groupby clause (with or without 'GROUP BY' keyword).
*
* @see #selectfields, #aliases, #groupedFields, #distinct, #hasAggregate .
* It
* @param select
* @throws SpeedoException
*/
private void parseGroupBy(String groupby) throws SpeedoException {
if (groupby == null || groupby.length() == 0) {
return;
}
StringTokenizer st = new StringTokenizer(groupby, ",", false);
while (st.hasMoreTokens()) {
groupByFields.add(sqvv.getField(st.nextToken().trim()));
}
for (int i = 0; i < groupByFields.size(); i++) {
Field f = (Field) groupByFields.get(i);
if (!usedFields.contains(f)) {
usedFields.add(f);
}
}
}
/**
* Parses an expression and register the list of Fiefd used in the
* expression.
*
* @param e
* is the expression to parse
* @param result
* is the list to fill with Field used in the expression
*/
private void getFieldsFromExpression(Expression e, List result) {
if (e instanceof FieldOperand) {
Field f = ((FieldOperand) e).getField();
if (!result.contains(f)) {
result.add(f);
}
} else if (e instanceof Operator) {
Operator operator = (Operator) e;
for (int i = 0; i < operator.getOperandNumber(); i++) {
getFieldsFromExpression(operator.getExpression(i), result);
}
}
}
private int isKeyword(String str) {
if (str == null || str.length() == 0) {
return -1;
}
for (int i = 0; i < keywords.length; i++) {
if (keywords[i].equals(str)) {
return i;
}
}
return -1;
}
}