/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/query/BoundExpression.java,v 1.26 2003/07/09 01:22:50 wbiggs Exp $
This file is part of XORM.
XORM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
XORM 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with XORM; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.xorm.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import javax.jdo.JDOHelper;
import javax.jdo.JDOUserException;
import javax.jdo.PersistenceManager;
import javax.jdo.spi.PersistenceCapable;
import org.xorm.ClassMapping;
import org.xorm.InterfaceInvocationHandler;
import org.xorm.ModelMapping;
import org.xorm.RelationshipMapping;
import org.xorm.XORM;
import org.xorm.I15d;
import org.xorm.datastore.Column;
import org.xorm.datastore.Table;
import org.xorm.util.FieldDescriptor;
/**
* Represents a Query where parameters have been bound to specific
* values and mapped against the datastore. This object bridges the
* gap between the object query model (Expression) and the data query
* model (Selector). The task of a DatastoreDriver is to take a
* Selector and convert it to a native query representation.
*
* Currently only a limited set of Expressions can be transformed
* to Selectors.
*
* Note that once bound, a BoundExpression does not require any knowledge
* of JDO interfaces.
*/
public class BoundExpression extends QueryContext implements ExpressionVisitor, I15d {
private QueryLanguage query;
private ModelMapping modelMapping;
// This gets cached after first call to getSelector()
private Selector top;
private Selector selector;
private HashMap varToSelector = new HashMap();
private Column lastColumn;
private RelationshipMapping lastRelationship;
private ClassMapping mapping;
private Object lastValue;
private boolean valueMode;
/**
* Creates a new BoundExpression for the given query.
*/
public BoundExpression(QueryLanguage query, PersistenceManager mgr) {
super(query.getCandidateClass());
this.query = query;
this.modelMapping = XORM.getModelMapping(mgr);
}
private Selector.Ordering[] getOrdering() {
// Convert QueryOrdering[] to Selector.Ordering[]
QueryOrdering[] fieldOrdering = query.getOrdering();
if (fieldOrdering == null) return null;
Selector.Ordering[] ordering = new Selector.Ordering [fieldOrdering.length];
for (int i = 0; i < fieldOrdering.length; i++) {
String field = fieldOrdering[i].getField();
Column column = null;
// Handle a field of a field of a field of a...
Class currentClass = getCandidateClass();
StringTokenizer tok = new StringTokenizer(field, ".");
String fieldToken;
FieldDescriptor fd;
Selector firstSelector = null, currentSelector = null;
while (modelMapping.isManagedType(currentClass)) {
ClassMapping currentClassMapping = modelMapping.getClassMapping(currentClass);
Selector nextSelector = new Selector(currentClassMapping.getTable(), null);
if (currentSelector != null) {
Condition condition = new SimpleCondition
(column, Operator.EQUAL, nextSelector);
currentSelector.setCondition(condition);
} else {
firstSelector = nextSelector;
}
currentSelector = nextSelector;
if (!tok.hasMoreTokens()) {
throw new JDOUserException(I18N.msg("E_ordering"));
}
fieldToken = tok.nextToken();
fd = currentClassMapping.getFieldDescriptor(fieldToken);
if (fd == null) {
throw new JDOUserException(I18N.msg("E_unmapped_field", fieldToken));
}
column = currentClassMapping.getColumn(fieldToken);
currentClass = fd.type;
}
top.merge(firstSelector, Operator.ANDC);
// Ok, now that we jumped through all the field dereferencing
// hoops, let's set the column we're actually ordering on.
ordering[i] = new Selector.Ordering
(column, fieldOrdering[i].getOrder());
}
return ordering;
}
public void bindParameter(int index, Object value) {
if (query instanceof AbstractQueryLanguage) {
AbstractQueryLanguage aql = (AbstractQueryLanguage) query;
String name = (String) aql.getParameterNames().get(index);
Class clazz = aql.getParameterType(name);
if (value == null) {
if (clazz.isPrimitive()) {
throw new JDOUserException(I18N.msg("E_null_primitive"));
}
bindParameter(name, value);
} else {
if (clazz.isInstance(value) || isWrappedInstance(value, clazz)) {
bindParameter(name, value);
} else {
throw new JDOUserException(I18N.msg("E_param_type", value.getClass().getName(), clazz.getName()));
}
}
}
}
private static boolean isWrappedInstance(Object value, Class clazz) {
if (!clazz.isPrimitive()) { return false; }
if (clazz.equals(Integer.TYPE)) {
return value instanceof Integer;
} else if (clazz.equals(Short.TYPE)) {
return value instanceof Short;
} else if (clazz.equals(Long.TYPE)) {
return value instanceof Long;
} else if (clazz.equals(Boolean.TYPE)) {
return value instanceof Boolean;
} else if (clazz.equals(Byte.TYPE)) {
return value instanceof Byte;
} else if (clazz.equals(Float.TYPE)) {
return value instanceof Float;
} else if (clazz.equals(Double.TYPE)) {
return value instanceof Double;
} else if (clazz.equals(Character.TYPE)) {
return value instanceof Character;
} else {
// Never gets here
return false;
}
}
/**
* Creates or retrieves the Selector tree for this query.
*/
public Selector getSelector() {
if (top == null) {
if (query instanceof DataQuery) {
top = makeSelector((DataQuery) query);
} else {
//System.out.println("Expression in: " + query.getExpression());
getSelector(query.getExpression());
}
top.setOrdering(getOrdering());
}
//System.out.println("Selector out; " + top);
return top;
}
// Convert DataQuery to Selector
private Selector makeSelector(DataQuery dataQuery) {
Class targetClass = dataQuery.getCandidateClass();
ClassMapping mapping = modelMapping.getClassMapping(targetClass);
Table table = mapping.getTable();
Condition dataCondition = dataQuery.getCondition();
if (dataCondition instanceof RawCondition) {
// Resolve parameters
dataCondition = makeRawCondition((RawCondition) dataCondition);
}
return new Selector(table, dataCondition);
}
private RawCondition makeRawCondition(RawCondition condition) {
String where2 = condition.getRawQuery();
int pos = 0;
while ((pos = where2.indexOf('{', pos)) != -1) {
int pos2 = where2.indexOf('}', pos + 1);
if (pos2 == -1) break;
String name = where2.substring(pos+1,pos2);
boolean contains = hasParameter(name);
Object operand = resolveParameter(name);
/*
* Ignore {xxx} if xxx has no value this is to allow JDBC escape
* sequences to be passed through. Maybe XORM should use a
* different delimiter than {} so it doesn't conflict with JDBC...
*/
if (!contains) {
pos++;
continue;
}
where2 = where2.substring(0,pos)
+ condition.convertOperand(operand)
+ where2.substring(pos2 + 1);
pos = pos2 + 1;
}
return new RawCondition(condition.getTables(), where2);
}
// Called internally
private Selector getSelector(Expression expression) {
mapping = modelMapping.getClassMapping(query.getCandidateClass());
Table table = mapping.getTable();
top = new Selector(table, null);
selector = top;
expression.accept(this);
// Deal with boolean standalones
while (expression instanceof Expression.Not) {
expression = ((Expression.Not) expression).getOperand();
}
if ((expression instanceof Expression.FieldAccess)
&& (expression.getType().equals(Boolean.TYPE))) {
selector.setCondition(new SimpleCondition(lastColumn, Operator.EQUAL, Boolean.TRUE));
}
//System.out.println("getSelector(" + expression + ")\nreturns: " + top);
return top;
}
public boolean visitAnd(Expression.And exp) {
visitAndOrImpl(exp, Operator.ANDC);
return true;
}
public boolean visitConditionalAnd(Expression.ConditionalAnd exp) {
visitAndOrImpl(exp, Operator.ANDC);
return true;
}
public boolean visitConditionalOr(Expression.ConditionalOr exp) {
visitAndOrImpl(exp, Operator.ORC);
return true;
}
private void visitAndOrImpl(Expression.Comparison exp, Operator operator) {
Selector orig = top;
// Visit lhs
Selector lhs = getSelector(exp.getLHS());
// Visit rhs
Selector rhs = getSelector(exp.getRHS());
// Merge left and right
if (lhs != null) {
lhs.merge(rhs, operator);
orig.setCondition(lhs.getCondition());
} else {
orig.setCondition(rhs.getCondition());
}
top = orig;
//System.out.println("returning with top: " + top);
}
public boolean visitComparison(Expression.Comparison exp) {
Expression left = exp.getLHS();
Expression right = exp.getRHS();
// Check for some cheats
// x.indexOf(y) != -1 --> x.strstr(y)
// x.indexOf(y) >= 0 --> same (for you old-school hackers)
if ((left instanceof Expression.MethodCall)
&& (right instanceof Expression.Constant)
&& "indexOf".equals(((Expression.MethodCall) left).getName())
&& (((exp.operator() == Operator.NOT_EQUAL)
&& new Integer(-1).equals(right.evaluate(this)))
|| ((exp.operator() == Operator.GTE)
&& new Integer(0).equals(right.evaluate(this))))) {
Expression.MethodCall lmc = (Expression.MethodCall) left;
Expression exp2 = new Expression.MethodCall
(lmc.getOwner(), "strstr", lmc.getParameters(), Boolean.TYPE);
exp2.accept(this);
return true;
}
// Visit the field
exp.getLHS().accept(this);
// Get the operator
Operator operator = exp.operator();
// Get the value
valueMode = true;
exp.getRHS().accept(this);
// Convert Objects to their IDs
if (lastValue instanceof PersistenceCapable) {
lastValue = XORM.extractPrimaryKey(JDOHelper.getObjectId(lastValue));
}
valueMode = false;
SimpleCondition sc = new SimpleCondition
(lastColumn, operator, lastValue);
selector.setCondition(sc);
return true;
}
public boolean visitFieldAccess(Expression.FieldAccess exp) {
//System.out.println("Visit fieldAccess: " + exp.toString());
if (exp == Expression.FieldAccess.THIS) {
mapping = modelMapping.getClassMapping(query.getCandidateClass());
lastRelationship = null;
lastColumn = mapping.getTable().getPrimaryKey();
} else {
// Recurse up the chain to "this"
Expression owner = exp.getOwner();
owner.accept(this);
if (valueMode) {
// Introspect on lastValue (which should be a JDO object)
InterfaceInvocationHandler handler = InterfaceInvocationHandler.getHandler(lastValue);
Class returnType = exp.getType();
ClassMapping returnTypeMapping = null;
if (ClassMapping.isUserType(returnType)) {
returnTypeMapping = modelMapping.getClassMapping(returnType);
}
lastValue = handler.invokeGet(exp.getName(), returnTypeMapping, exp.getType());
} else {
// If we're hanging from another field, interpolate
// the table join
if (lastRelationship != null) {
checkJoin(owner);
}
lastColumn = mapping.getColumn(exp.getName());
lastRelationship = mapping.getRelationship(exp.getName());
}
}
return true;
}
private void checkJoin(Expression owner) {
if (lastRelationship != null) {
// Ex. x.y; lastColumn = x
// generate x_id = x.x_id
ClassMapping nextMapping = modelMapping.getClassMapping(owner.getType());
Selector se = new Selector(nextMapping.getTable(), null);
SimpleCondition join = new SimpleCondition
(mapping.getColumn(((Expression.Symbolic) owner).getName()),
Operator.EQUAL, se);
selector.setCondition(join);
selector = se;
mapping = nextMapping;
}
}
public boolean visitVariable(Expression.Variable exp) {
mapping = modelMapping.getClassMapping(exp.getType());
selector = (Selector) varToSelector.get(exp.getName());
top = (Selector) selector.clone();
lastRelationship = null;
//System.out.println("top is now: " + top);
// TODO This may not be correct if the table is touched
// previously/elsewhere in the query
selector = top.findSelector(mapping.getTable());
//System.out.println("selector is now: " + selector);
return true;
}
public boolean visitMethodCall(Expression.MethodCall exp) {
String name = exp.getName();
// Read the field
exp.getOwner().accept(this);
// Do we need a join?
if (!"contains".equals(name) && !"isEmpty".equals(name)) {
checkJoin(exp.getOwner());
}
Column column = lastColumn;
// Read the value
Object param = null;
Expression operand = null;
if (!"isEmpty".equals(name)) {
operand = exp.getParameters()[0];
if ((operand instanceof Expression.Constant)
|| (operand instanceof Expression.Parameter)) {
param = operand.evaluate(this);
}
}
if ("contains".equals(name)) {
Expression owner = exp.getOwner();
if ((owner instanceof Expression.Parameter) && (operand instanceof Expression)) {
mapping = modelMapping.getClassMapping(query.getCandidateClass());
Table table = mapping.getTable();
top = new Selector(table, null);
selector = top;
operand.accept(this);
selector.setCondition
(new SimpleCondition
(lastColumn, Operator.IN, collectionToIDs((Collection) owner.evaluate(this))));
return true;
}
String field = ((Expression.Member) owner).getName();
RelationshipMapping rm = mapping.getRelationship(field);
ClassMapping targetMapping = modelMapping.getClassMapping
(rm.getSource().getElementClass());
Selector s = null;
Selector old = selector;
if (operand instanceof Expression.Variable) {
// This field is really the variable, using targetMapping
selector = new Selector(targetMapping.getTable(),
null);
} else if (operand instanceof Expression.Parameter) {
if (param != null) {
param = XORM.extractPrimaryKey(JDOHelper.getObjectId(param));
}
selector = new Selector
(rm.getTarget().getColumn().getTable(),
new SimpleCondition
(rm.getTarget().getColumn(),
Operator.EQUAL, param));
selector.setJoinColumn(rm.getTarget().getColumn());
// If we have a parameter on a MToN mapping,
// we can skip the target table
selector.setJoinColumn(rm.getSource().getColumn());
old.setCondition
(new SimpleCondition(mapping.getTable().getPrimaryKey(),
Operator.CONTAINS, selector));
return true;
}
if (rm.isMToN()) {
s = new Selector
(rm.getTarget().getColumn().getTable(),
new SimpleCondition
(rm.getTarget().getColumn(),
Operator.EQUAL,
selector));
} else {
s = selector;
}
s.setJoinColumn(rm.getSource().getColumn());
old.setCondition
(new SimpleCondition(mapping.getTable().getPrimaryKey(),
Operator.CONTAINS, s));
if (operand instanceof Expression.Variable) {
varToSelector.put(((Expression.Variable) operand).getName(),
top);
top = null;
}
} else if ("startsWith".equals(name)) {
selector.setCondition
(new SimpleCondition(column, Operator.STARTS_WITH,
param));
} else if ("endsWith".equals(name)) {
selector.setCondition
(new SimpleCondition(column, Operator.ENDS_WITH,
param));
} else if ("strstr".equals(name)) {
selector.setCondition
(new SimpleCondition(column, Operator.STR_CONTAINS,
param));
} else if ("isEmpty".equals(name)) {
// model.model_id where model_id = model_platform
// .model_id where model_id is null
String field = ((Expression.Member) exp.getOwner()).getName();
RelationshipMapping rm = mapping.getRelationship(field);
Selector s = new Selector
(rm.getSource().getColumn().getTable(),
new SimpleCondition(rm.getSource().getColumn(),
Operator.EQUAL, null));
s.setJoinColumn(rm.getSource().getColumn());
s.setOuterJoin(true);
selector.setCondition(new SimpleCondition
(selector.getTable().getPrimaryKey(),
Operator.CONTAINS, s));
}
return true;
}
public boolean visitParameter(Expression.Parameter exp) {
//System.out.println("Visit parameter: " + exp.toString());
lastValue = resolveParameter(exp.getName());
lastRelationship = null;
return true;
}
public boolean visitConstant(Expression.Constant exp) {
lastValue = exp.getValue();
return true;
}
public boolean visitNot(Expression.Not exp) {
exp.getOperand().accept(this);
Condition c = selector.getCondition();
if (c != null) {
c.setInverted();
}
return true;
}
public boolean visitUnary(Expression.Unary exp) {
return false;
}
private Collection collectionToIDs(Collection input) {
Collection output = new ArrayList(input.size());
Iterator it = input.iterator();
while (it.hasNext()) {
Object obj = it.next();
if (obj instanceof PersistenceCapable) {
obj = XORM.extractPrimaryKey(JDOHelper.getObjectId(obj));
}
output.add(obj);
}
return output;
}
}