/***************************************************************** * 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.ejbql; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.cayenne.dba.QuotingStrategy; import org.apache.cayenne.ejbql.EJBQLBaseVisitor; import org.apache.cayenne.ejbql.EJBQLException; import org.apache.cayenne.ejbql.EJBQLExpression; import org.apache.cayenne.ejbql.EJBQLParserFactory; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.map.DbEntity; import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.query.EntityResultSegment; import org.apache.cayenne.util.CayenneMapEntry; /** * Handles appending joins to the content buffer at a marked position. * * @since 3.0 */ public class EJBQLJoinAppender { protected EJBQLTranslationContext context; private Map<String, String> reusableJoins; static String makeJoinTailMarker(String id) { return "FROM_TAIL" + id; } public EJBQLJoinAppender(EJBQLTranslationContext context) { this.context = context; } /** * Registers a "reusable" join, returning a preexisting ID if the join is already * registered. Reusable joins are the implicit inner joins that are added as a result * of processing of path expressions in SELECT or WHERE clauses. Note that if an * implicit INNER join overlaps with an explicit INNER join, both joins are added to * the query. */ public String registerReusableJoin( String sourceIdPath, String relationship, String targetId) { if (reusableJoins == null) { reusableJoins = new HashMap<>(); } String key = sourceIdPath + ":" + relationship; String oldId = reusableJoins.put(key, targetId); if (oldId != null) { // revert back to old id reusableJoins.put(key, oldId); return oldId; } return null; } public void appendInnerJoin(String marker, EJBQLTableId lhsId, EJBQLTableId rhsId) { appendJoin(marker, lhsId, rhsId, "INNER JOIN"); } public void appendOuterJoin(String marker, EJBQLTableId lhsId, EJBQLTableId rhsId) { appendJoin(marker, lhsId, rhsId, "LEFT OUTER JOIN"); } protected void appendJoin( String marker, EJBQLTableId lhsId, EJBQLTableId rhsId, String semantics) { List<DbRelationship> joinRelationships = context.getIncomingRelationships(rhsId); if (joinRelationships.isEmpty()) { throw new EJBQLException("No join configured for id " + rhsId); } QuotingStrategy quoter = context.getQuotingStrategy(); DbRelationship incomingDB = joinRelationships.get(0); // TODO: andrus, 1/6/2008 - move reusable join check here... DbEntity sourceEntity = incomingDB.getSourceEntity(); String tableName = quoter.quotedFullyQualifiedName(sourceEntity); String sourceAlias = context.getTableAlias(lhsId.getEntityId(), tableName); if (marker != null) { context.pushMarker(marker, false); } try { context.append(" ").append(semantics); if (joinRelationships.size() > 1) { // if size of relationship list greater than 1, // it's a flattened relationship context.append(" "); for (int i = 1; i < joinRelationships.size(); i++) { DbRelationship dbRelationship = joinRelationships.get(i); String subquerySourceTableName = quoter.quotedFullyQualifiedName(dbRelationship.getSourceEntity()); String subquerySourceAlias = context.getTableAlias( subquerySourceTableName, subquerySourceTableName); String subqueryTargetTableName = quoter.quotedFullyQualifiedName(dbRelationship.getTargetEntity()); String subqueryTargetAlias; if(i==joinRelationships.size()-1){ // it's the last table alias subqueryTargetAlias = context.getTableAlias(rhsId.getEntityId(), subqueryTargetTableName); } else { subqueryTargetAlias = context.getTableAlias( subqueryTargetTableName, subqueryTargetTableName); } if (i == 1) { // first apply the joins defined in query context.append(subquerySourceTableName).append(' ').append(subquerySourceAlias); generateJoiningExpression( incomingDB, sourceAlias, subquerySourceAlias); } context.append(" JOIN "); context.append(subqueryTargetTableName).append(' ').append(subqueryTargetAlias); generateJoiningExpression( dbRelationship, subquerySourceAlias, subqueryTargetAlias); } } else { // non-flattened relationship String targetAlias = appendTable(rhsId); // apply the joins defined in query generateJoiningExpression(incomingDB, sourceAlias, targetAlias); } } finally { if (marker != null) { context.popMarker(); } } } private void generateJoiningExpression( DbRelationship incomingDB, String sourceAlias, String targetAlias) { context.append(" ON ("); QuotingStrategy quoter = context.getQuotingStrategy(); Iterator<DbJoin> it = incomingDB.getJoins().iterator(); if (it.hasNext()) { DbJoin dbJoin = it.next(); context .append(sourceAlias) .append('.') .append(quoter.quotedSourceName(dbJoin)) .append(" = ") .append(targetAlias) .append('.') .append(quoter.quotedTargetName(dbJoin)); } while (it.hasNext()) { context.append(" AND "); DbJoin dbJoin = it.next(); context .append(sourceAlias) .append('.') .append(quoter.quotedSourceName(dbJoin)) .append(" = ") .append(targetAlias) .append('.') .append(quoter.quotedTargetName(dbJoin)); } context.append(")"); } public String appendTable(EJBQLTableId id) { DbEntity dbEntity = id.getDbEntity(context); String tableName = context.getQuotingStrategy().quotedFullyQualifiedName(dbEntity); String alias; if (context.isUsingAliases()) { // TODO: andrus 1/5/2007 - if the same table is joined more than once, this // will create an incorrect alias. alias = context.getTableAlias(id.getEntityId(), tableName); // not using "AS" to separate table name and alias name - OpenBase doesn't // support "AS", and the rest of the databases do not care context.append(' ').append(tableName).append(' ').append(alias); generateJoinsForFlattenedAttributes(id); } else { context.append(' ').append(tableName); alias = tableName; } // append inheritance qualifier... if (id.isPrimaryTable()) { Expression qualifier = context .getEntityDescriptor(id.getEntityId()) .getEntityQualifier(); if (qualifier != null) { EJBQLExpression ejbqlQualifier = ejbqlQualifierForEntityAndSubclasses( qualifier, id.getEntityId()); context.pushMarker(context.makeWhereMarker(), true); context.append(" WHERE"); context.popMarker(); context.pushMarker(context.makeEntityQualifierMarker(), false); ejbqlQualifier.visit(context .getTranslatorFactory() .getConditionTranslator(context)); context.popMarker(); } } return alias; } /** * Generates Joins statements for those flattened attributes that appear after the * FROM clause, e.g. in WHERE, ORDER BY, etc clauses. Flattened attributes of the * entity from the SELECT clause are processed earlier and therefore are omitted. * * @param id table to JOIN id */ private void generateJoinsForFlattenedAttributes(EJBQLTableId id) { String entityName = context.getEntityDescriptor(id.getEntityId()) .getEntity() .getName(); // if the dbPath is not null, all attributes of the entity are processed earlier boolean isProcessingOmitted = id.getDbPath() != null; String sourceExpression = context.getCompiledExpression().getSource(); List<Object> resultSetMapping = context.getMetadata().getResultSetMapping(); for (Object mapping : resultSetMapping) { if (mapping instanceof EntityResultSegment) { if (entityName.equals(((EntityResultSegment) mapping) .getClassDescriptor() .getEntity() .getName())) { // if entity is included into SELECT clause, all its attributes are processed earlier isProcessingOmitted = true; break; } } } if (!isProcessingOmitted) { QuotingStrategy quoter = context.getQuotingStrategy(); Collection<ObjAttribute> attributes = context.getEntityDescriptor(id.getEntityId()) .getEntity().getAttributes(); for (ObjAttribute objAttribute : attributes) { if (objAttribute.isFlattened() && sourceExpression.contains(id.getEntityId() + "." + objAttribute.getName())) { // joins for attribute are generated if it is flattened and appears in original statement Iterator<CayenneMapEntry> dbPathIterator = objAttribute.getDbPathIterator(); while (dbPathIterator.hasNext()) { CayenneMapEntry next = dbPathIterator.next(); if (next instanceof DbRelationship) { DbRelationship rel = (DbRelationship) next; context.append(" LEFT OUTER JOIN "); String targetEntityName = quoter.quotedFullyQualifiedName(rel.getTargetEntity()); String subqueryTargetAlias = context.getTableAlias(id.getEntityId(), targetEntityName); context.append(targetEntityName).append(' ').append(subqueryTargetAlias); generateJoiningExpression( rel, context.getTableAlias(id.getEntityId(), quoter.quotedFullyQualifiedName(rel.getSourceEntity())), subqueryTargetAlias); } } } } } } private EJBQLExpression ejbqlQualifierForEntityAndSubclasses(Expression qualifier, String entityId) { // parser only works on full queries, so prepend a dummy query and then strip it out... String ejbqlChunk = qualifier.toEJBQL(entityId); EJBQLExpression expression = EJBQLParserFactory.getParser().parse( "DELETE FROM DUMMY WHERE " + ejbqlChunk); final EJBQLExpression[] result = new EJBQLExpression[1]; expression.visit(new EJBQLBaseVisitor() { @Override public boolean visitWhere(EJBQLExpression expression) { result[0] = expression.getChild(0); return false; } }); return result[0]; } }