/*****************************************************************
* 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.query;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.access.types.ValueObjectType;
import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.Property;
import org.apache.cayenne.exp.TraversalHandler;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTFunctionCall;
import org.apache.cayenne.exp.parser.ASTScalar;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.EntityResult;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.map.PathComponent;
import org.apache.cayenne.map.SQLResult;
import org.apache.cayenne.reflect.AttributeProperty;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.PropertyVisitor;
import org.apache.cayenne.reflect.ToManyProperty;
import org.apache.cayenne.reflect.ToOneProperty;
import org.apache.cayenne.util.CayenneMapEntry;
/**
* @since 3.0
*/
class SelectQueryMetadata extends BaseQueryMetadata {
private static final long serialVersionUID = 7465922769303943945L;
Map<String, String> pathSplitAliases;
boolean isSingleResultSetMapping;
boolean suppressingDistinct;
@Override
void copyFromInfo(QueryMetadata info) {
super.copyFromInfo(info);
this.pathSplitAliases = new HashMap<>(info.getPathSplitAliases());
}
boolean resolve(Object root, EntityResolver resolver, SelectQuery<?> query) {
if (super.resolve(root, resolver, null)) {
// generate unique cache key, but only if we are caching..
if (cacheStrategy != null && cacheStrategy != QueryCacheStrategy.NO_CACHE) {
this.cacheKey = makeCacheKey(query, resolver);
}
resolveAutoAliases(query);
buildResultSetMappingForColumns(query, resolver);
isSingleResultSetMapping = query.canReturnScalarValue() && super.isSingleResultSetMapping();
return true;
}
return false;
}
private String makeCacheKey(SelectQuery<?> query, EntityResolver resolver) {
// create a unique key based on entity or columns, qualifier, ordering, fetch offset and limit
StringBuilder key = new StringBuilder();
// handler to create string out of expressions, created lazily
TraversalHandler traversalHandler = null;
ObjEntity entity = getObjEntity();
if (entity != null) {
key.append(entity.getName());
} else if (dbEntity != null) {
key.append("db:").append(dbEntity.getName());
}
if(query.getColumns() != null && !query.getColumns().isEmpty()) {
key.append("/");
traversalHandler = new ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
for(Property<?> property : query.getColumns()) {
key.append("c:");
property.getExpression().traverse(traversalHandler);
}
}
if (query.getQualifier() != null) {
key.append('/');
if(traversalHandler == null) {
traversalHandler = new ToCacheKeyTraversalHandler(resolver.getValueObjectTypeRegistry(), key);
}
query.getQualifier().traverse(traversalHandler);
}
if (!query.getOrderings().isEmpty()) {
for (Ordering o : query.getOrderings()) {
key.append('/').append(o.getSortSpecString());
if (!o.isAscending()) {
key.append(":d");
}
if (o.isCaseInsensitive()) {
key.append(":i");
}
}
}
if (query.getFetchOffset() > 0 || query.getFetchLimit() > 0) {
key.append('/');
if (query.getFetchOffset() > 0) {
key.append('o').append(query.getFetchOffset());
}
if (query.getFetchLimit() > 0) {
key.append('l').append(query.getFetchLimit());
}
}
return key.toString();
}
private void resolveAutoAliases(SelectQuery<?> query) {
Expression qualifier = query.getQualifier();
if (qualifier != null) {
resolveAutoAliases(qualifier);
}
// TODO: include aliases in prefetches? flattened attributes?
}
private void resolveAutoAliases(Expression expression) {
Map<String, String> aliases = expression.getPathAliases();
if (!aliases.isEmpty()) {
if (pathSplitAliases == null) {
pathSplitAliases = new HashMap<>();
}
pathSplitAliases.putAll(aliases);
}
int len = expression.getOperandCount();
for (int i = 0; i < len; i++) {
Object operand = expression.getOperand(i);
if (operand instanceof Expression) {
resolveAutoAliases((Expression) operand);
}
}
}
/**
* @since 3.0
*/
@Override
public Map<String, String> getPathSplitAliases() {
return pathSplitAliases != null ? pathSplitAliases : Collections.<String, String> emptyMap();
}
/**
* @since 3.0
*/
public void addPathSplitAliases(String path, String... aliases) {
if (aliases == null) {
throw new NullPointerException("Null aliases");
}
if (aliases.length == 0) {
throw new IllegalArgumentException("No aliases specified");
}
if (pathSplitAliases == null) {
pathSplitAliases = new HashMap<>();
}
for (String alias : aliases) {
pathSplitAliases.put(alias, path);
}
}
/**
* Build DB result descriptor, that will be used to read and convert raw result of ColumnSelect
* @since 4.0
*/
private void buildResultSetMappingForColumns(SelectQuery<?> query, EntityResolver resolver) {
if(query.getColumns() == null || query.getColumns().isEmpty()) {
return;
}
SQLResult result = new SQLResult();
for(Property<?> column : query.getColumns()) {
Expression exp = column.getExpression();
String name = column.getName() == null ? exp.expName() : column.getName();
boolean fullObject = false;
if(exp.getType() == Expression.OBJ_PATH) {
// check if this is toOne relation
Expression dbPath = this.getObjEntity().translateToDbPath(exp);
DbRelationship rel = findRelationByPath(dbEntity, dbPath);
if(rel != null && !rel.isToMany()) {
// it this path is toOne relation, than select full object for it
fullObject = true;
}
} else if(exp.getType() == Expression.FULL_OBJECT) {
fullObject = true;
}
if(fullObject) {
// detected full object column
if(getPageSize() > 0) {
// for paginated queries keep only IDs
result.addEntityResult(buildEntityIdResultForColumn(column, resolver));
} else {
// will unwrap to full set of db-columns (with join prefetch optionally)
result.addEntityResult(buildEntityResultForColumn(query, column, resolver));
}
} else {
// scalar column
result.addColumnResult(name);
}
}
resultSetMapping = result.getResolvedComponents(resolver);
}
/**
* Collect metadata for result with ObjectId (used for paginated queries with FullObject columns)
*
* @param column full object column
* @param resolver entity resolver
* @return Entity result
*/
private EntityResult buildEntityIdResultForColumn(Property<?> column, EntityResolver resolver) {
EntityResult result = new EntityResult(column.getType());
DbEntity entity = resolver.getObjEntity(column.getType()).getDbEntity();
for(DbAttribute attribute : entity.getPrimaryKeys()) {
result.addDbField(attribute.getName(), attribute.getName());
}
return result;
}
private DbRelationship findRelationByPath(DbEntity entity, Expression exp) {
DbRelationship r = null;
for (PathComponent<DbAttribute, DbRelationship> component : entity.resolvePath(exp, getPathSplitAliases())) {
r = component.getRelationship();
}
return r;
}
/**
* Collect metadata for column that will be unwrapped to full entity in the final SQL
* (possibly including joint prefetch).
* This information will be used to correctly create Persistent object back from raw result.
*
* This method is actually repeating logic of
* {@link org.apache.cayenne.access.translator.select.DefaultSelectTranslator#appendQueryColumns}.
* Here we don't care about intermediate joins and few other things so it's shorter.
* Logic of these methods should be unified and simplified, possibly to a single source of metadata,
* generated only once and used everywhere.
*
* @param query original query
* @param column full object column
* @param resolver entity resolver to get ObjEntity and ClassDescriptor
* @return Entity result
*/
private EntityResult buildEntityResultForColumn(SelectQuery<?> query, Property<?> column, EntityResolver resolver) {
final EntityResult result = new EntityResult(column.getType());
// Collecting visitor for ObjAttributes and toOne relationships
PropertyVisitor visitor = new PropertyVisitor() {
public boolean visitAttribute(AttributeProperty property) {
ObjAttribute oa = property.getAttribute();
Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
while (dbPathIterator.hasNext()) {
CayenneMapEntry pathPart = dbPathIterator.next();
if (pathPart instanceof DbAttribute) {
result.addDbField(pathPart.getName(), pathPart.getName());
}
}
return true;
}
public boolean visitToMany(ToManyProperty property) {
return true;
}
public boolean visitToOne(ToOneProperty property) {
DbRelationship dbRel = property.getRelationship().getDbRelationships().get(0);
List<DbJoin> joins = dbRel.getJoins();
for (DbJoin join : joins) {
if(!join.getSource().isPrimaryKey()) {
result.addDbField(join.getSource().getName(), join.getSource().getName());
}
}
return true;
}
};
ObjEntity oe = resolver.getObjEntity(column.getType());
DbEntity table = oe.getDbEntity();
// Additionally collect PKs
for (DbAttribute dba : table.getPrimaryKeys()) {
result.addDbField(dba.getName(), dba.getName());
}
ClassDescriptor descriptor = resolver.getClassDescriptor(oe.getName());
descriptor.visitAllProperties(visitor);
// Collection columns for joint prefetch
if(query.getPrefetchTree() != null) {
for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) {
// for each prefetch add columns from the target entity
Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath());
ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp);
DbRelationship r = findRelationByPath(table, dbPrefetch);
if (r == null) {
throw new CayenneRuntimeException("Invalid joint prefetch '%s' for entity: %s"
, prefetch, oe.getName());
}
// go via target OE to make sure that Java types are mapped correctly...
ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe);
ObjEntity targetEntity = targetRel.getTargetEntity();
prefetch.setEntityName(targetRel.getSourceEntity().getName());
String labelPrefix = dbPrefetch.getPath();
Set<String> seenNames = new HashSet<>();
for (ObjAttribute oa : targetEntity.getAttributes()) {
Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
while (dbPathIterator.hasNext()) {
Object pathPart = dbPathIterator.next();
if (pathPart instanceof DbAttribute) {
DbAttribute attribute = (DbAttribute) pathPart;
if(seenNames.add(attribute.getName())) {
result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + attribute.getName());
}
}
}
}
// append remaining target attributes such as keys
DbEntity targetDbEntity = r.getTargetEntity();
for (DbAttribute attribute : targetDbEntity.getAttributes()) {
if(seenNames.add(attribute.getName())) {
result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + attribute.getName());
}
}
}
}
return result;
}
/**
* @since 4.0
*/
@Override
public boolean isSingleResultSetMapping() {
return isSingleResultSetMapping;
}
/**
* @since 4.0
*/
@Override
public boolean isSuppressingDistinct() {
return suppressingDistinct;
}
/**
* @since 4.0
*/
public void setSuppressingDistinct(boolean suppressingDistinct) {
this.suppressingDistinct = suppressingDistinct;
}
/**
* Expression traverse handler to create cache key string out of Expression.
* {@link Expression#appendAsString(Appendable)} where previously used for that,
* but it can't handle custom value objects properly (see CAY-2210).
*
* @see ValueObjectTypeRegistry
*
* @since 4.0
*/
static class ToCacheKeyTraversalHandler implements TraversalHandler {
private ValueObjectTypeRegistry registry;
private StringBuilder out;
ToCacheKeyTraversalHandler(ValueObjectTypeRegistry registry, StringBuilder out) {
this.registry = registry;
this.out = out;
}
@Override
public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
out.append(',');
}
@Override
public void startNode(Expression node, Expression parentNode) {
if(node.getType() == Expression.FUNCTION_CALL) {
out.append(((ASTFunctionCall)node).getFunctionName()).append('(');
} else {
out.append(node.getType()).append('(');
}
}
@Override
public void endNode(Expression node, Expression parentNode) {
out.append(')');
}
@Override
public void objectNode(Object leaf, Expression parentNode) {
if(leaf == null) {
out.append("null");
return;
}
if(leaf instanceof ASTScalar) {
leaf = ((ASTScalar) leaf).getValue();
} else if(leaf instanceof Object[]) {
for(Object value : (Object[])leaf) {
objectNode(value, parentNode);
out.append(',');
}
return;
}
if (leaf instanceof Persistent) {
ObjectId id = ((Persistent) leaf).getObjectId();
Object encode = (id != null) ? id : leaf;
out.append(encode);
} else if (leaf instanceof Enum<?>) {
Enum<?> e = (Enum<?>) leaf;
out.append("e:").append(leaf.getClass().getName()).append(':').append(e.ordinal());
} else {
ValueObjectType<Object, ?> valueObjectType;
if (registry == null || (valueObjectType = registry.getValueType(leaf.getClass())) == null) {
// Registry will be null in cayenne-client context.
// Maybe we shouldn't create cache key at all in that case...
out.append(leaf);
} else {
out.append(valueObjectType.toCacheKey(leaf));
}
}
}
};
}