content: /*****************************************************************
   *   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
   *
   *    https://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];
      }
  }
  