/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.access.translator.select;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.TraversalHandler;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTExtract;
import org.apache.cayenne.exp.parser.ASTFunctionCall;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.parser.PatternMatchNode;
import org.apache.cayenne.exp.parser.SimpleNode;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.JoinType;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.Transformer;
/**
* Translates query qualifier to SQL. Used as a helper class by query
* translators.
*/
public class QualifierTranslator extends QueryAssemblerHelper implements TraversalHandler {
protected DataObjectMatchTranslator objectMatchTranslator;
protected boolean matchingObject;
protected boolean caseInsensitive;
/**
* @since 4.0
*/
protected boolean useAliasForExpressions;
/**
* @since 4.0
*/
protected Expression waitingForEndNode;
/**
* @since 4.0
*/
protected Expression qualifier;
public QualifierTranslator(QueryAssembler queryAssembler) {
super(queryAssembler);
caseInsensitive = false;
}
/**
* Translates query qualifier to SQL WHERE clause. Qualifier is obtained
* from the parent queryAssembler.
*
* @since 3.0
*/
@Override
protected void doAppendPart() {
doAppendPart(extractQualifier());
}
public void setCaseInsensitive(boolean caseInsensitive) {
this.caseInsensitive = caseInsensitive;
}
/**
* Explicitly set qualifier.
* It will be used instead of extracting qualifier from the query itself.
* @since 4.0
*/
public void setQualifier(Expression qualifier) {
this.qualifier = qualifier;
}
/**
* @since 4.0
*/
public void setUseAliasForExpressions(boolean useAliasForExpressions) {
this.useAliasForExpressions = useAliasForExpressions;
}
/**
* Translates query qualifier to SQL WHERE clause. Qualifier is a method
* parameter.
*
* @since 3.0
*/
protected void doAppendPart(Expression rootNode) {
if (rootNode == null) {
return;
}
rootNode.traverse(this);
}
protected Expression extractQualifier() {
// if additional qualifier is set, use it
if(this.qualifier != null) {
return this.qualifier;
}
Query q = queryAssembler.getQuery();
Expression qualifier = ((SelectQuery<?>) q).getQualifier();
// append Entity qualifiers, taking inheritance into account
ObjEntity entity = getObjEntity();
if (entity != null) {
ClassDescriptor descriptor = queryAssembler.getEntityResolver().getClassDescriptor(entity.getName());
Expression entityQualifier = descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
if (entityQualifier != null) {
qualifier = (qualifier != null) ? qualifier.andExp(entityQualifier) : entityQualifier;
}
}
// Attaching root Db entity's qualifier
if (getDbEntity() != null) {
Expression dbQualifier = getDbEntity().getQualifier();
if (dbQualifier != null) {
dbQualifier = dbQualifier.transform(new DbEntityQualifierTransformer());
qualifier = qualifier == null ? dbQualifier : qualifier.andExp(dbQualifier);
}
}
return qualifier;
}
/**
* Called before processing an expression to initialize
* objectMatchTranslator if needed.
*/
protected void detectObjectMatch(Expression exp) {
// On demand initialization of
// objectMatchTranslator is not possible since there may be null
// object values that would not allow to detect the need for
// such translator in the right time (e.g.: null = dbpath)
matchingObject = false;
if (exp.getOperandCount() != 2) {
// only binary expressions are supported
return;
}
// check if there are DataObjects among direct children of the
// Expression
for (int i = 0; i < 2; i++) {
Object op = exp.getOperand(i);
if (op instanceof Persistent || op instanceof ObjectId) {
matchingObject = true;
if (objectMatchTranslator == null) {
objectMatchTranslator = new DataObjectMatchTranslator();
} else {
objectMatchTranslator.reset();
}
break;
}
}
}
protected void appendObjectMatch() throws IOException {
if (!matchingObject || objectMatchTranslator == null) {
throw new IllegalStateException("An invalid attempt to append object match.");
}
// turn off special handling, so that all the methods behave as a
// superclass's
// impl.
matchingObject = false;
boolean first = true;
DbRelationship relationship = objectMatchTranslator.getRelationship();
if (!relationship.isToMany() && !relationship.isToPK()) {
queryAssembler.dbRelationshipAdded(relationship, JoinType.INNER, objectMatchTranslator.getJoinSplitAlias());
}
Iterator<String> it = objectMatchTranslator.keys();
while (it.hasNext()) {
if (first) {
first = false;
} else {
out.append(" AND ");
}
String key = it.next();
DbAttribute attr = objectMatchTranslator.getAttribute(key);
Object val = objectMatchTranslator.getValue(key);
processColumn(attr);
out.append(objectMatchTranslator.getOperation());
appendLiteral(val, attr, objectMatchTranslator.getExpression());
}
objectMatchTranslator.reset();
}
@Override
public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
if(waitingForEndNode != null) {
return;
}
if (!hasMoreChildren) {
return;
}
Appendable out = (matchingObject) ? new StringBuilder() : this.out;
try {
switch (node.getType()) {
case Expression.AND:
out.append(" AND ");
break;
case Expression.OR:
out.append(" OR ");
break;
case Expression.EQUAL_TO:
// translate NULL as IS NULL
if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
out.append(" IS ");
} else {
out.append(" = ");
}
break;
case Expression.NOT_EQUAL_TO:
// translate NULL as IS NOT NULL
if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
out.append(" IS NOT ");
} else {
out.append(" <> ");
}
break;
case Expression.LESS_THAN:
out.append(" < ");
break;
case Expression.GREATER_THAN:
out.append(" > ");
break;
case Expression.LESS_THAN_EQUAL_TO:
out.append(" <= ");
break;
case Expression.GREATER_THAN_EQUAL_TO:
out.append(" >= ");
break;
case Expression.IN:
out.append(" IN ");
break;
case Expression.NOT_IN:
out.append(" NOT IN ");
break;
case Expression.LIKE:
out.append(" LIKE ");
break;
case Expression.NOT_LIKE:
out.append(" NOT LIKE ");
break;
case Expression.LIKE_IGNORE_CASE:
if (caseInsensitive) {
out.append(" LIKE ");
} else {
out.append(") LIKE UPPER(");
}
break;
case Expression.NOT_LIKE_IGNORE_CASE:
if (caseInsensitive) {
out.append(" NOT LIKE ");
} else {
out.append(") NOT LIKE UPPER(");
}
break;
case Expression.ADD:
out.append(" + ");
break;
case Expression.SUBTRACT:
out.append(" - ");
break;
case Expression.MULTIPLY:
out.append(" * ");
break;
case Expression.DIVIDE:
out.append(" / ");
break;
case Expression.BETWEEN:
if (childIndex == 0)
out.append(" BETWEEN ");
else if (childIndex == 1)
out.append(" AND ");
break;
case Expression.NOT_BETWEEN:
if (childIndex == 0)
out.append(" NOT BETWEEN ");
else if (childIndex == 1)
out.append(" AND ");
break;
case Expression.BITWISE_OR:
out.append(" ").append(operandForBitwiseOr()).append(" ");
break;
case Expression.BITWISE_AND:
out.append(" ").append(operandForBitwiseAnd()).append(" ");
break;
case Expression.BITWISE_XOR:
out.append(" ").append(operandForBitwiseXor()).append(" ");
break;
case Expression.BITWISE_LEFT_SHIFT:
out.append(" ").append(operandForBitwiseLeftShift()).append(" ");
break;
case Expression.BITWISE_RIGHT_SHIFT:
out.append(" ").append(operandForBitwiseRightShift()).append("");
break;
}
} catch (IOException ioex) {
throw new CayenneRuntimeException("Error appending content", ioex);
}
if (matchingObject) {
objectMatchTranslator.setOperation(out.toString());
objectMatchTranslator.setExpression(node);
}
}
/**
* @since 3.1
*/
protected String operandForBitwiseNot() {
return "~";
}
/**
* @since 3.1
*/
protected String operandForBitwiseOr() {
return "|";
}
/**
* @since 3.1
*/
protected String operandForBitwiseAnd() {
return "&";
}
/**
* @since 3.1
*/
protected String operandForBitwiseXor() {
return "^";
}
/**
* @since 4.0
*/
protected String operandForBitwiseLeftShift() {
return "<<";
}
/**
* @since 4.0
*/
protected String operandForBitwiseRightShift() {
return ">>";
}
@Override
public void startNode(Expression node, Expression parentNode) {
if(waitingForEndNode != null) {
return;
}
if(useAliasForExpressions) {
String alias = queryAssembler.getAliasForExpression(node);
if(alias != null) {
out.append(alias);
waitingForEndNode = node;
return;
}
}
boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
if(node.getType() == Expression.FUNCTION_CALL) {
if(node instanceof ASTExtract) {
appendExtractFunction((ASTExtract) node);
} else {
appendFunction((ASTFunctionCall) node);
}
if(parenthesisNeeded) {
out.append("(");
}
return;
}
if(node.getType() == Expression.FULL_OBJECT && parentNode != null) {
throw new CayenneRuntimeException("Expression is not supported in where clause.");
}
int count = node.getOperandCount();
if (count == 2) {
// binary nodes are the only ones that currently require this
detectObjectMatch(node);
}
if (parenthesisNeeded) {
out.append('(');
}
if (count == 0) {
// not all databases handle true/false
if (node.getType() == Expression.TRUE) {
out.append("1 = 1");
} else if (node.getType() == Expression.FALSE) {
out.append("1 = 0");
} else if (node.getType() == Expression.ASTERISK) {
out.append("*");
}
}
if (count == 1) {
if (node.getType() == Expression.NEGATIVE) {
out.append('-');
} else if (node.getType() == Expression.NOT) {
out.append("NOT ");
} else if (node.getType() == Expression.BITWISE_NOT) {
out.append(operandForBitwiseNot());
}
} else if ((node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE)
&& !caseInsensitive) {
out.append("UPPER(");
}
}
/**
* @since 1.1
*/
@Override
public void endNode(Expression node, Expression parentNode) {
if(waitingForEndNode != null) {
if(node == waitingForEndNode) {
waitingForEndNode = null;
}
return;
}
try {
// check if we need to use objectMatchTranslator to finish building the expression
if (node.getOperandCount() == 2 && matchingObject) {
appendObjectMatch();
}
boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
boolean isPatternMatchNode = PatternMatchNode.class.isAssignableFrom(node.getClass());
// closing UPPER parenthesis
if (likeIgnoreCase && !caseInsensitive) {
out.append(')');
}
if (isPatternMatchNode) {
appendLikeEscapeCharacter((PatternMatchNode) node);
}
// clean up trailing comma in function argument list
if(node.getType() == Expression.FUNCTION_CALL) {
clearLastFunctionArgDivider((ASTFunctionCall)node);
}
// closing LIKE parenthesis
if (parenthesisNeeded) {
out.append(')');
}
// if inside function call, put comma between arguments
if(parentNode != null && parentNode.getType() == Expression.FUNCTION_CALL) {
appendFunctionArgDivider((ASTFunctionCall) parentNode);
}
} catch (IOException ioex) {
throw new CayenneRuntimeException("Error appending content", ioex);
}
}
@Override
public void objectNode(Object leaf, Expression parentNode) {
if(waitingForEndNode != null) {
return;
}
try {
switch (parentNode.getType()) {
case Expression.OBJ_PATH:
appendObjPath(parentNode);
break;
case Expression.DB_PATH:
appendDbPath(parentNode);
break;
case Expression.LIST:
appendList(parentNode, paramsDbType(parentNode));
break;
case Expression.FUNCTION_CALL:
appendFunctionArg(leaf, (ASTFunctionCall)parentNode);
break;
default:
appendLiteral(leaf, paramsDbType(parentNode), parentNode);
}
} catch (IOException ioex) {
throw new CayenneRuntimeException("Error appending content", ioex);
}
}
protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
if (node.getType() == Expression.FUNCTION_CALL) {
return ((ASTFunctionCall)node).needParenthesis();
}
if (parentNode == null) {
return false;
}
// only unary expressions can go w/o parenthesis
if (node.getOperandCount() > 1) {
return true;
}
if (node.getType() == Expression.OBJ_PATH
|| node.getType() == Expression.DB_PATH
|| node.getType() == Expression.ASTERISK) {
return false;
}
return true;
}
private final void appendList(Expression listExpr, DbAttribute paramDesc) throws IOException {
Iterator<?> it;
Object list = listExpr.getOperand(0);
if (list instanceof List) {
it = ((List<?>) list).iterator();
} else if (list instanceof Object[]) {
it = IteratorUtils.arrayIterator((Object[]) list);
} else {
String className = (list != null) ? list.getClass().getName() : "<null>";
throw new IllegalArgumentException("Unsupported type for the list expressions: " + className);
}
// process first element outside the loop
// (unroll loop to avoid condition checking
if (it.hasNext()) {
appendLiteral(it.next(), paramDesc, listExpr);
} else {
return;
}
while (it.hasNext()) {
out.append(", ");
appendLiteral(it.next(), paramDesc, listExpr);
}
}
@Override
protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
if (!matchingObject) {
super.appendLiteral(val, attr, parentExpression);
} else if (val == null || (val instanceof Persistent)) {
objectMatchTranslator.setDataObject((Persistent) val);
} else if (val instanceof ObjectId) {
objectMatchTranslator.setObjectId((ObjectId) val);
} else {
throw new IllegalArgumentException("Attempt to use literal other than DataObject during object match.");
}
}
@Override
protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
if (!matchingObject) {
super.processRelTermination(rel, joinType, joinSplitAlias);
} else {
if (rel.isToMany()) {
// append joins
queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
}
objectMatchTranslator.setRelationship(rel, joinSplitAlias);
}
}
/**
* Append function name to result SQL
* Override this method to rename or skip function if generic name isn't supported on target DB.
* @since 4.0
*/
protected void appendFunction(ASTFunctionCall functionExpression) {
out.append(functionExpression.getFunctionName());
}
/**
* Special case for extract date/time parts functions as they have many variants
* @since 4.0
*/
protected void appendExtractFunction(ASTExtract functionExpression) {
appendFunction(functionExpression);
}
/**
* Append scalar argument of a function call
* Used only for values stored in ASTScalar other
* expressions appended in objectNode() method
*
* @since 4.0
*/
protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
// Create fake DbAttribute to pass argument info down to bind it to SQL prepared statement
DbAttribute dbAttrForArg = new DbAttribute();
dbAttrForArg.setType(TypesMapping.getSqlTypeByJava(value.getClass()));
super.appendLiteral(value, dbAttrForArg, functionExpression);
appendFunctionArgDivider(functionExpression);
}
/**
* Append divider between function arguments.
* In overriding methods can be replaced e.g. for " || " for CONCAT operation
* @since 4.0
*/
protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
out.append(", ");
}
/**
* Clear last divider as we currently don't now position of argument until parent element is ended.
* @since 4.0
*/
protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
if(functionExpression.getOperandCount() > 0) {
out.delete(out.length() - 2, out.length());
}
}
/**
* Class to translate DB Entity qualifiers annotation to Obj-entity
* qualifiers annotation This is done by changing all Obj-paths to Db-paths
* and rejecting all original Db-paths
*/
class DbEntityQualifierTransformer implements Transformer {
public Object transform(Object input) {
if (input instanceof ASTObjPath) {
return new ASTDbPath(((SimpleNode) input).getOperand(0));
}
return input;
}
}
}