/***************************************************************** * 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.ejbql.EJBQLBaseVisitor; import org.apache.cayenne.ejbql.EJBQLException; import org.apache.cayenne.ejbql.EJBQLExpression; 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.Entity; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.map.ObjRelationship; import org.apache.cayenne.map.Relationship; import org.apache.cayenne.reflect.ClassDescriptor; /** * A translator that walks the relationship/attribute path, appending joins to * the query. * * @since 3.0 */ public abstract class EJBQLPathTranslator extends EJBQLBaseVisitor { private EJBQLTranslationContext context; protected ObjEntity currentEntity; protected String lastPathComponent; protected boolean innerJoin; protected String lastAlias; protected String idPath; protected String joinMarker; protected String fullPath; private boolean usingAliases; public EJBQLPathTranslator(EJBQLTranslationContext context) { super(true); this.context = context; this.usingAliases = true; } protected abstract void appendMultiColumnPath(EJBQLMultiColumnOperand operand); @Override public boolean visitPath(EJBQLExpression expression, int finishedChildIndex) { if (finishedChildIndex > 0) { if (finishedChildIndex + 1 < expression.getChildrenCount()) { processIntermediatePathComponent(); } else { processLastPathComponent(); } } return true; } @Override public boolean visitIdentifier(EJBQLExpression expression) { ClassDescriptor descriptor = context.getEntityDescriptor(expression.getText()); if (descriptor == null) { throw new EJBQLException("Invalid identification variable: " + expression.getText()); } this.currentEntity = descriptor.getEntity(); this.idPath = expression.getText(); this.joinMarker = EJBQLJoinAppender.makeJoinTailMarker(idPath); this.fullPath = idPath; return true; } @Override public boolean visitIdentificationVariable(EJBQLExpression expression) { // TODO: andrus 6/11/2007 - if the path ends with relationship, the last // join will // get lost... if (lastPathComponent != null) { resolveJoin(); } resolveLastPathComponent(expression.getText()); return true; } /** * @since 4.0 */ protected void resolveLastPathComponent(String pathComponent) { if (pathComponent.endsWith(Entity.OUTER_JOIN_INDICATOR)) { this.lastPathComponent = pathComponent.substring(0, pathComponent.length() - 1); this.innerJoin = false; } else { this.lastPathComponent = pathComponent; this.innerJoin = true; } } protected void resolveJoin() { EJBQLJoinAppender joinAppender = context.getTranslatorFactory().getJoinAppender(context); String newPath = idPath + '.' + lastPathComponent; String oldPath = joinAppender.registerReusableJoin(idPath, lastPathComponent, newPath); this.fullPath = fullPath + '.' + lastPathComponent; if (oldPath != null) { this.idPath = oldPath; Relationship lastRelationship = currentEntity.getRelationship(lastPathComponent); if (lastRelationship != null) { ObjEntity targetEntity = (ObjEntity) lastRelationship.getTargetEntity(); this.lastAlias = context.getTableAlias(fullPath, context.getQuotingStrategy().quotedFullyQualifiedName(targetEntity.getDbEntity())); } else { String tableName = context.getQuotingStrategy().quotedFullyQualifiedName(currentEntity.getDbEntity()); this.lastAlias = context.getTableAlias(oldPath, tableName); } } else { Relationship lastRelationship = currentEntity.getRelationship(lastPathComponent); ObjEntity targetEntity = null; if (lastRelationship != null) { targetEntity = (ObjEntity) lastRelationship.getTargetEntity(); } else { targetEntity = currentEntity; } // register join if (innerJoin) { joinAppender.appendInnerJoin(joinMarker, new EJBQLTableId(idPath), new EJBQLTableId(fullPath)); } else { joinAppender.appendOuterJoin(joinMarker, new EJBQLTableId(idPath), new EJBQLTableId(fullPath)); } this.lastAlias = context.getTableAlias(fullPath, context.getQuotingStrategy().quotedFullyQualifiedName(targetEntity.getDbEntity())); this.idPath = newPath; } } protected void processIntermediatePathComponent() { ObjRelationship relationship = currentEntity.getRelationship(lastPathComponent); if (relationship == null) { throw new EJBQLException("Unknown relationship '" + lastPathComponent + "' for entity '" + currentEntity.getName() + "'"); } this.currentEntity = (ObjEntity) relationship.getTargetEntity(); } protected void processLastPathComponent() { ObjAttribute attribute = currentEntity.getAttribute(lastPathComponent); if (attribute != null) { processTerminatingAttribute(attribute); return; } ObjRelationship relationship = currentEntity.getRelationship(lastPathComponent); if (relationship != null) { processTerminatingRelationship(relationship); return; } throw new IllegalStateException("Invalid path component: " + lastPathComponent); } protected void processTerminatingAttribute(ObjAttribute attribute) { DbEntity table = null; Iterator<?> it = attribute.getDbPathIterator(); while (it.hasNext()) { Object pathComponent = it.next(); if (pathComponent instanceof DbAttribute) { table = (DbEntity) ((DbAttribute) pathComponent).getEntity(); } } if (isUsingAliases()) { String alias = this.lastAlias != null ? lastAlias : context.getTableAlias(idPath, context .getQuotingStrategy().quotedFullyQualifiedName(table)); context.append(' ').append(alias).append('.') .append(context.getQuotingStrategy().quotedName(attribute.getDbAttribute())); } else { context.append(' ').append(context.getQuotingStrategy().quotedName(attribute.getDbAttribute())); } } protected void processTerminatingRelationship(ObjRelationship relationship) { if (relationship.isSourceIndependentFromTargetChange()) { // (andrus) use an outer join for to-many matches.. This is somewhat // different // from traditional Cayenne SelectQuery, as EJBQL spec does not // allow regular // path matches done against to-many relationships, and instead // provides // MEMBER OF and IS EMPTY operators. Outer join is needed for IS // EMPTY... I // guess MEMBER OF could've been done with an inner join though.. this.innerJoin = false; resolveJoin(); DbRelationship dbRelationship = chooseDbRelationship(relationship); DbEntity table = (DbEntity) dbRelationship.getTargetEntity(); String alias = this.lastAlias != null ? lastAlias : context.getTableAlias(idPath, context .getQuotingStrategy().quotedFullyQualifiedName(table)); Collection<DbAttribute> pks = table.getPrimaryKeys(); if (pks.size() == 1) { DbAttribute pk = pks.iterator().next(); context.append(' '); if (isUsingAliases()) { context.append(alias).append('.'); } context.append(context.getQuotingStrategy().quotedName(pk)); } else { throw new EJBQLException("Multi-column PK to-many matches are not yet supported."); } } else { // match FK against the target object DbRelationship dbRelationship = chooseDbRelationship(relationship); DbEntity table = (DbEntity) dbRelationship.getSourceEntity(); String alias = this.lastAlias != null ? lastAlias : context.getTableAlias(idPath, context .getQuotingStrategy().quotedFullyQualifiedName(table)); List<DbJoin> joins = dbRelationship.getJoins(); if (joins.size() == 1) { DbJoin join = joins.get(0); context.append(' '); if (isUsingAliases()) { context.append(alias).append('.'); } context.append(context.getQuotingStrategy().quotedName(join.getSource())); } else { Map<String, String> multiColumnMatch = new HashMap<>(joins.size() + 2); for (DbJoin join : joins) { String column = isUsingAliases() ? alias + "." + join.getSourceName() : join.getSourceName(); multiColumnMatch.put(join.getTargetName(), column); } appendMultiColumnPath(EJBQLMultiColumnOperand.getPathOperand(context, multiColumnMatch)); } } } /** * Checks if the object relationship is flattened and then chooses the * corresponding db relationship. The last in idPath if isFlattened and the * first in list otherwise. * * @param relationship * the object relationship * * @return {@link DbRelationship} */ protected DbRelationship chooseDbRelationship(ObjRelationship relationship) { List<DbRelationship> dbRelationships = relationship.getDbRelationships(); String dbRelationshipPath = relationship.getDbRelationshipPath(); if (dbRelationshipPath.contains(".")) { String dbRelName = dbRelationshipPath.substring(dbRelationshipPath.lastIndexOf(".") + 1); for (DbRelationship dbR : dbRelationships) { if (dbR.getName().equals(dbRelName)) { return dbR; } } } return relationship.getDbRelationships().get(0); } public boolean isUsingAliases() { return usingAliases; } public void setUsingAliases(boolean usingAliases) { this.usingAliases = usingAliases; } }