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 org.apache.cayenne.CayenneRuntimeException;
  import org.apache.cayenne.dba.TypesMapping;
  import org.apache.cayenne.ejbql.EJBQLBaseVisitor;
  import org.apache.cayenne.ejbql.EJBQLExpression;
  import org.apache.cayenne.exp.Expression;
  import org.apache.cayenne.exp.ExpressionFactory;
  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.ObjAttribute;
  import org.apache.cayenne.map.ObjEntity;
  import org.apache.cayenne.map.ObjRelationship;
  import org.apache.cayenne.map.PathComponent;
  import org.apache.cayenne.query.PrefetchTreeNode;
  import org.apache.cayenne.reflect.ArcProperty;
  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 java.util.HashSet;
  import java.util.Iterator;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * @since 3.0
   */
  class EJBQLIdentifierColumnsTranslator extends EJBQLBaseVisitor {
  
      private EJBQLTranslationContext context;
      private Set<String> columns;
  
      EJBQLIdentifierColumnsTranslator(EJBQLTranslationContext context) {
          this.context = context;
      }
  
      @Override
      public boolean visitIdentifier(EJBQLExpression expression) {
  
          Map<String, String> xfields = null;
          if (context.isAppendingResultColumns()) {
              xfields = context.nextEntityResult().getFields();
          }
  
          // assign whatever we have to a final ivar so that it can be accessed
          // within
          // the inner class
          final Map<String, String> fields = xfields;
          final String idVar = expression.getText();
  
          // append all table columns ... the trick is to follow the algorithm for
          // describing the fields in the expression compiler, so that we could
          // assign
          // columns labels from FieldResults in the order we encounter them
          // here...
          // TODO: andrus 2008/02/17 - this is a bit of a hack, think of a better
          // solution
  
          ClassDescriptor descriptor = context.getEntityDescriptor(idVar);
  
          PropertyVisitor visitor = new PropertyVisitor() {
  
              public boolean visitAttribute(AttributeProperty property) {
                  ObjAttribute oa = property.getAttribute();
                  Iterator<?> dbPathIterator = oa.getDbPathIterator();
  
                  EJBQLJoinAppender joinAppender = null;
                  String marker = null;
                  EJBQLTableId lhsId = new EJBQLTableId(idVar);
  
                  while (dbPathIterator.hasNext()) {
                      Object pathPart = dbPathIterator.next();
  
                      if (pathPart == null) {
                          throw new CayenneRuntimeException("ObjAttribute has no component: %s", oa.getName());
                      } else if (pathPart instanceof DbRelationship) {
  
                          if (marker == null) {
                              marker = EJBQLJoinAppender.makeJoinTailMarker(idVar);
                              joinAppender = context.getTranslatorFactory().getJoinAppender(context);
                          }
  
                          DbRelationship dr = (DbRelationship) pathPart;
  
                          EJBQLTableId rhsId = new EJBQLTableId(lhsId, dr.getName());
                          joinAppender.appendOuterJoin(marker, lhsId, rhsId);
                          lhsId = rhsId;
                      } else if (pathPart instanceof DbAttribute) {
                          appendColumn(idVar, oa, (DbAttribute) pathPart, fields, oa.getType());
                      }
                  }
                  return true;
              }
  
              public boolean visitToMany(ToManyProperty property) {
                  visitRelationship(property);
                  return true;
              }
  
              public boolean visitToOne(ToOneProperty property) {
                  visitRelationship(property);
                  return true;
              }
  
              private void visitRelationship(ArcProperty property) {
                  ObjRelationship rel = property.getRelationship();
                  DbRelationship dbRel = rel.getDbRelationships().get(0);
  
                  for (DbJoin join : dbRel.getJoins()) {
                      DbAttribute src = join.getSource();
                      appendColumn(idVar, null, src, fields);
                  }
              }
          };
  
          // EJBQL queries are polymorphic by definition - there is no distinction
          // between
          // inheritance/no-inheritance fetch
          descriptor.visitAllProperties(visitor);
  
          // append id columns ... (some may have been appended already via
          // relationships)
          DbEntity table = descriptor.getEntity().getDbEntity();
          for (DbAttribute pk : table.getPrimaryKeys()) {
              appendColumn(idVar, null, pk, fields);
          }
  
          // append inheritance discriminator columns...
          for (ObjAttribute column : descriptor.getDiscriminatorColumns()) {
              appendColumn(idVar, column, column.getDbAttribute(), fields);
          }
  
          addPrefetchedColumnsIfAny(idVar);
  
          return false;
      }
  
      private void addPrefetchedColumnsIfAny(final String visitedIdentifier) {
          PrefetchTreeNode prefetchTree = context.getCompiledExpression().getPrefetchTree();
          if (prefetchTree != null) {
              for (PrefetchTreeNode prefetch : prefetchTree.adjacentJointNodes()) {
                  ClassDescriptor descriptor = context.getEntityDescriptor(prefetch.getEjbqlPathEntityId());
                  if (visitedIdentifier.equals(prefetch.getEjbqlPathEntityId())) {
                      DbEntity table = descriptor.getRootDbEntities().iterator().next();
                      ObjEntity objectEntity = descriptor.getEntity();
                      prefetch.setEntityName(objectEntity.getName());
                      Expression prefetchExp = ExpressionFactory.pathExp(prefetch.getPath());
                      Expression dbPrefetch = objectEntity.translateToDbPath(prefetchExp);
  
                      DbRelationship r = null;
                      for (PathComponent<DbAttribute, DbRelationship> component : table.resolvePath(dbPrefetch, context
                              .getMetadata().getPathSplitAliases())) {
                          r = component.getRelationship();
                      }
  
                      if (r == null) {
                          throw new CayenneRuntimeException("Invalid joint prefetch '%s' for entity: %s"
                                  , prefetch, objectEntity.getName());
                      }
  
                      for (DbAttribute attribute : r.getTargetEntity().getAttributes()) {
                          appendColumn(prefetch.getEjbqlPathEntityId() + "." + prefetch.getPath(), attribute, "",
                                  prefetch.getPath() + "." + attribute.getName(), null);
                      }
                  }
              }
          }
      }
  
      public void appendColumn(String identifier, ObjAttribute property, DbAttribute column, Map<String, String> fields) {
          appendColumn(identifier, property, column, fields, null);
      }
  
      public void appendColumn(String identifier, ObjAttribute property, DbAttribute column, Map<String, String> fields,
              String javaType) {
          String columnLabel = "";
          if (context.isAppendingResultColumns()) {
              columnLabel = fields.get(property != null ? property.getDbAttributePath().value() : column.getName());
          }
          appendColumn(identifier, column, columnLabel, columnLabel, javaType);
      }
  
      public void appendColumn(String identifier, DbAttribute column, String columnAlias, String dataRowKey,
              String javaType) {
  
          DbEntity table = column.getEntity();
          String alias = context.getTableAlias(identifier, context.getQuotingStrategy().quotedFullyQualifiedName(table));
          String columnName = alias + "." + context.getQuotingStrategy().quotedName(column);
  
          Set<String> columns = getColumns();
  
          if (columns.add(columnName)) {
  
              context.append(columns.size() > 1 ? ", " : " ");
  
              if (context.isAppendingResultColumns()) {
                  context.append("#result('");
              }
  
              context.append(columnName);
  
              if (context.isAppendingResultColumns()) {
                  if (javaType == null) {
                      javaType = TypesMapping.getJavaBySqlType(column);
                  }
  
                  // TODO: andrus 6/27/2007 - the last parameter is an unofficial
                  //       "jdbcType" pending CAY-813 implementation, switch to #column directive
                  context.append("' '").append(javaType).append("' '").append(columnAlias).append("' '")
                          .append(dataRowKey).append("' " + column.getType()).append(")");
              }
          }
      }
  
      private Set<String> getColumns() {
  
          if (columns == null) {
              columns = new HashSet<>();
          }
  
          return columns;
      }
  }
  