/*****************************************************************
* 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 org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.dba.QuotingStrategy;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.parser.PatternMatchNode;
import org.apache.cayenne.exp.parser.SimpleNode;
import org.apache.cayenne.map.*;
import org.apache.cayenne.util.CayenneMapEntry;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Translates parts of the query to SQL. Always works in the context of parent
* Translator.
*/
public abstract class QueryAssemblerHelper {
protected QueryAssembler queryAssembler;
protected StringBuilder out;
protected QuotingStrategy strategy;
/**
* Force joining tables for all relations, not only for toMany
* @since 4.0
*/
private boolean forceJoinForRelations;
/**
* Creates QueryAssemblerHelper initializing with parent
* {@link QueryAssembler} and output buffer object.
*/
public QueryAssemblerHelper(QueryAssembler queryAssembler) {
this.queryAssembler = queryAssembler;
strategy = queryAssembler.getAdapter().getQuotingStrategy();
}
public ObjEntity getObjEntity() {
return queryAssembler.getQueryMetadata().getObjEntity();
}
public DbEntity getDbEntity() {
return queryAssembler.getQueryMetadata().getDbEntity();
}
/**
* @since 3.0
*/
public StringBuilder appendPart(StringBuilder out) {
this.out = out;
doAppendPart();
return out;
}
/**
* Sets ouput buffer
*/
void setOut(StringBuilder out) {
this.out = out;
}
/**
* @return output buffer
*/
StringBuilder getOut() {
return out;
}
/**
* @since 3.0
*/
protected abstract void doAppendPart();
/**
* <p>
* Outputs the standard JDBC (database agnostic) expression for supplying
* the escape character to the database server when supplying a LIKE clause.
* This has been factored-out because some database adaptors handle LIKE
* differently and they need access to this common method in order not to
* repeat this code.
* <p>
* If there is no escape character defined then this method will not output
* anything. An escape character of 0 will mean no escape character.
*
* @since 3.1
*/
protected void appendLikeEscapeCharacter(PatternMatchNode patternMatchNode) throws IOException {
char escapeChar = patternMatchNode.getEscapeChar();
if ('?' == escapeChar) {
throw new CayenneRuntimeException("the escape character of '?' is illegal for LIKE clauses.");
}
if (0 != escapeChar) {
out.append(" {escape '");
out.append(escapeChar);
out.append("'}");
}
}
/**
* Processes parts of the OBJ_PATH expression.
*/
protected void appendObjPath(Expression pathExp) {
queryAssembler.resetJoinStack();
String joinSplitAlias = null;
for (PathComponent<ObjAttribute, ObjRelationship> component : getObjEntity().resolvePath(pathExp,
queryAssembler.getPathAliases())) {
if (component.isAlias()) {
joinSplitAlias = component.getName();
for (PathComponent<ObjAttribute, ObjRelationship> aliasPart : component.getAliasedPath()) {
ObjRelationship relationship = aliasPart.getRelationship();
if (relationship == null) {
throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
}
if (aliasPart.isLast() && component.isLast()) {
processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
} else {
// find and add joins ....
for (DbRelationship dbRel : relationship.getDbRelationships()) {
queryAssembler.dbRelationshipAdded(dbRel, aliasPart.getJoinType(), joinSplitAlias);
}
}
}
continue;
}
ObjRelationship relationship = component.getRelationship();
ObjAttribute attribute = component.getAttribute();
if (relationship != null) {
// if this is a last relationship in the path,
// it needs special handling
if (component.isLast()) {
processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
} else {
// find and add joins ....
for (DbRelationship dbRel : relationship.getDbRelationships()) {
queryAssembler.dbRelationshipAdded(dbRel, component.getJoinType(), joinSplitAlias);
}
}
} else {
Iterator<CayenneMapEntry> dbPathIterator = attribute.getDbPathIterator();
while (dbPathIterator.hasNext()) {
Object pathPart = dbPathIterator.next();
if (pathPart == null) {
throw new CayenneRuntimeException("ObjAttribute has no component: %s", attribute.getName());
} else if (pathPart instanceof DbRelationship) {
queryAssembler.dbRelationshipAdded((DbRelationship) pathPart, JoinType.INNER, joinSplitAlias);
} else if (pathPart instanceof DbAttribute) {
processColumnWithQuoteSqlIdentifiers((DbAttribute) pathPart, pathExp);
}
}
}
}
}
protected void appendDbPath(Expression pathExp) {
queryAssembler.resetJoinStack();
String joinSplitAlias = null;
for (PathComponent<DbAttribute, DbRelationship> component : getDbEntity().resolvePath(pathExp,
queryAssembler.getPathAliases())) {
if (component.isAlias()) {
joinSplitAlias = component.getName();
for (PathComponent<DbAttribute, DbRelationship> aliasPart : component.getAliasedPath()) {
DbRelationship relationship = aliasPart.getRelationship();
if (relationship == null) {
throw new IllegalStateException("Non-relationship aliased path part: " + aliasPart.getName());
}
if (aliasPart.isLast() && component.isLast()) {
processRelTermination(relationship, aliasPart.getJoinType(), joinSplitAlias);
} else {
queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
}
}
continue;
}
DbRelationship relationship = component.getRelationship();
if (relationship != null) {
// if this is a last relationship in the path,
// it needs special handling
if (component.isLast()) {
processRelTermination(relationship, component.getJoinType(), joinSplitAlias);
} else {
// find and add joins ....
queryAssembler.dbRelationshipAdded(relationship, component.getJoinType(), joinSplitAlias);
}
} else {
processColumnWithQuoteSqlIdentifiers(component.getAttribute(), pathExp);
}
}
}
protected void processColumn(DbAttribute dbAttr) {
processColumnWithQuoteSqlIdentifiers(dbAttr, null);
}
protected void processColumnWithQuoteSqlIdentifiers(DbAttribute dbAttr, Expression pathExp) {
String alias = (queryAssembler.supportsTableAliases()) ? queryAssembler.getCurrentAlias() : null;
out.append(strategy.quotedIdentifier(dbAttr.getEntity(), alias, dbAttr.getName()));
}
/**
* Appends SQL code to the query buffer to handle <code>val</code> as a
* parameter to the PreparedStatement being built. Adds <code>val</code>
* into QueryAssembler parameter list.
* <p>
* If <code>val</code> is null, "NULL" is appended to the query.
* </p>
* <p>
* If <code>val</code> is a DataObject, its primary key value is used as a
* parameter. <i>Only objects with a single column primary key can be
* used.</i>
*
* @param val
* object that should be appended as a literal to the query. Must
* be of one of "standard JDBC" types, null or a DataObject.
* @param attr
* DbAttribute that has information on what type of parameter is
* being appended.
*/
protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
if (val == null) {
out.append("NULL");
} else if (val instanceof Persistent) {
// TODO: see cay1796
// This check is unlikely to happen,
// since Expression got ObjectId from Persistent object.
// Left for future research.
ObjectId id = ((Persistent) val).getObjectId();
// check if this id is acceptable to be a parameter
if (id == null) {
throw new CayenneRuntimeException("Can't use TRANSIENT object as a query parameter.");
}
if (id.isTemporary()) {
throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
}
Map<String, Object> snap = id.getIdSnapshot();
if (snap.size() != 1) {
throw new CayenneRuntimeException("Object must have a single primary key column to serve " +
"as a query parameter. This object has %s: %s", snap.size(), snap);
}
// checks have been passed, use id value
appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
} else if (val instanceof ObjectId) {
ObjectId id = (ObjectId) val;
if (id.isTemporary()) {
throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
}
Map<String, Object> snap = id.getIdSnapshot();
if (snap.size() != 1) {
throw new CayenneRuntimeException("Object must have a single primary key column to serve " +
"as a query parameter. This object has %s: %s", snap.size(), snap);
}
// checks have been passed, use id value
appendLiteralDirect(snap.get(snap.keySet().iterator().next()), attr, parentExpression);
} else {
appendLiteralDirect(val, attr, parentExpression);
}
}
/**
* Appends SQL code to the query buffer to handle <code>val</code> as a
* parameter to the PreparedStatement being built. Adds <code>val</code>
* into QueryAssembler parameter list.
*/
protected void appendLiteralDirect(Object val, DbAttribute attr, Expression parentExpression) throws IOException {
out.append('?');
queryAssembler.addToParamList(attr, val);
}
/**
* Returns database type of expression parameters or null if it can not be
* determined.
*/
protected DbAttribute paramsDbType(Expression e) {
int len = e.getOperandCount();
// for unary expressions, find parent binary - this is a hack mainly to
// support
// ASTList
if (len < 2) {
if (e instanceof SimpleNode) {
Expression parent = (Expression) ((SimpleNode) e).jjtGetParent();
if (parent != null) {
return paramsDbType(parent);
}
}
return null;
}
// naive algorithm:
// if at least one of the sibling operands is a
// OBJ_PATH or DB_PATH expression, use its attribute type as
// a final answer.
// find attribute or relationship matching the value
DbAttribute attribute = null;
DbRelationship relationship = null;
for (int i = 0; i < len; i++) {
Object op = e.getOperand(i);
if (op instanceof Expression) {
Expression expression = (Expression) op;
if (expression.getType() == Expression.OBJ_PATH) {
PathComponent<ObjAttribute, ObjRelationship> last = getObjEntity().lastPathComponent(expression,
queryAssembler.getPathAliases());
// TODO: handle EmbeddableAttribute
// if (last instanceof EmbeddableAttribute)
// break;
if (last.getAttribute() != null) {
attribute = last.getAttribute().getDbAttribute();
break;
} else if (last.getRelationship() != null) {
List<DbRelationship> dbPath = last.getRelationship().getDbRelationships();
if (dbPath.size() > 0) {
relationship = dbPath.get(dbPath.size() - 1);
break;
}
}
} else if (expression.getType() == Expression.DB_PATH) {
PathComponent<DbAttribute, DbRelationship> last = getDbEntity().lastPathComponent(expression,
queryAssembler.getPathAliases());
if (last.getAttribute() != null) {
attribute = last.getAttribute();
break;
} else if (last.getRelationship() != null) {
relationship = last.getRelationship();
break;
}
}
}
}
if (attribute != null) {
return attribute;
}
if (relationship != null) {
// Can't properly handle multiple joins....
if (relationship.getJoins().size() == 1) {
DbJoin join = relationship.getJoins().get(0);
return join.getSource();
}
}
return null;
}
/**
* Processes case when an OBJ_PATH expression ends with relationship. If
* this is a "to many" relationship, a join is added and a column expression
* for the target entity primary key. If this is a "to one" relationship,
* column expression for the source foreign key is added.
*
* @since 3.0
*/
protected void processRelTermination(ObjRelationship rel, JoinType joinType, String joinSplitAlias) {
Iterator<DbRelationship> dbRels = rel.getDbRelationships().iterator();
// scan DbRelationships
while (dbRels.hasNext()) {
DbRelationship dbRel = dbRels.next();
// if this is a last relationship in the path,
// it needs special handling
if (!dbRels.hasNext()) {
processRelTermination(dbRel, joinType, joinSplitAlias);
} else {
// find and add joins ....
queryAssembler.dbRelationshipAdded(dbRel, joinType, joinSplitAlias);
}
}
}
/**
* Handles case when a DB_NAME expression ends with relationship. If this is
* a "to many" relationship, a join is added and a column expression for the
* target entity primary key. If this is a "to one" relationship, column
* expression for the source foreign key is added.
*
* @since 3.0
*/
protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
if (forceJoinForRelations || rel.isToMany()) {
// append joins
queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
}
// get last DbRelationship on the list
List<DbJoin> joins = rel.getJoins();
if (joins.size() != 1) {
String msg = "OBJ_PATH expressions are only supported for a single-join relationships. " +
"This relationship has %s joins.";
throw new CayenneRuntimeException(msg, joins.size());
}
DbJoin join = joins.get(0);
DbAttribute attribute;
if (rel.isToMany()) {
DbEntity ent = join.getRelationship().getTargetEntity();
Collection<DbAttribute> pk = ent.getPrimaryKeys();
if (pk.size() != 1) {
String msg = "DB_NAME expressions can only support targets with a single column PK. " +
"This entity has %d columns in primary key.";
throw new CayenneRuntimeException(msg, pk.size());
}
attribute = pk.iterator().next();
} else {
attribute = forceJoinForRelations ? join.getTarget() : join.getSource();
}
processColumn(attribute);
}
/**
* Force joining tables for all relations, not only for toMany
* @since 4.0
*/
protected void setForceJoinForRelations(boolean forceJoinForRelations) {
this.forceJoinForRelations = forceJoinForRelations;
}
}