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.dba.QuotingStrategy;
  import org.apache.cayenne.ejbql.EJBQLCompiledExpression;
  import org.apache.cayenne.ejbql.EJBQLException;
  import org.apache.cayenne.map.DbEntity;
  import org.apache.cayenne.map.DbRelationship;
  import org.apache.cayenne.map.EntityResolver;
  import org.apache.cayenne.query.EJBQLQuery;
  import org.apache.cayenne.query.EntityResultSegment;
  import org.apache.cayenne.query.QueryMetadata;
  import org.apache.cayenne.query.SQLTemplate;
  import org.apache.cayenne.query.ScalarResultSegment;
  import org.apache.cayenne.reflect.ClassDescriptor;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  /**
   * A context used for translating of EJBQL to SQL.
   * 
   * @since 3.0
   */
  public class EJBQLTranslationContext {
  
      private EJBQLCompiledExpression compiledExpression;
      protected Map<String, Object> namedParameters;
      protected Map<Integer, Object> positionalParameters;
      private EJBQLTranslatorFactory translatorFactory;
      private QuotingStrategy quotingStrategy;
      private EntityResolver entityResolver;
      private List<Object> resultSetMetadata;
  
      private Map<String, String> tableAliases;
      private Map<String, Object> boundParameters;
      private Map<String, Object> attributes;
      private Map<String, String> idAliases;
      private int resultDescriptorPosition;
      private boolean usingAliases;
      private boolean caseInsensitive;
      private List<StringBuilder> bufferStack;
      private List<StringBuilder> bufferChain;
      private StringBuilder stackTop;
      private int subselectCount;
      private QueryMetadata queryMetadata;
  
      // a flag indicating whether column expressions should be treated as result columns or
      // not.
      private boolean appendingResultColumns;
  
      public EJBQLTranslationContext(EntityResolver entityResolver, EJBQLQuery query,
              EJBQLCompiledExpression compiledExpression,
              EJBQLTranslatorFactory translatorFactory, QuotingStrategy quotingStrategy) {
  
          this.entityResolver = entityResolver;
          this.compiledExpression = compiledExpression;
          this.resultSetMetadata = query.getMetaData(entityResolver).getResultSetMapping();
  
          this.namedParameters = query.getNamedParameters();
          this.positionalParameters = query.getPositionalParameters();
          this.translatorFactory = translatorFactory;
          this.usingAliases = true;
          this.caseInsensitive = false;
          this.queryMetadata = query.getMetaData(entityResolver);
          this.quotingStrategy = quotingStrategy;
  
          // buffer stack will hold named buffers during translation in the order they were
          // requested
          this.bufferStack = new ArrayList<>();
  
          // buffer chain will hold named and unnamed buffers in the order they should be
          // concatenated
          this.bufferChain = new ArrayList<>();
  
          stackTop = new StringBuilder();
          bufferChain.add(stackTop);
          bufferStack.add(stackTop);
      }
  
      public SQLTemplate getQuery() {
  
          // concatenate buffers...
          StringBuilder main = bufferChain.get(0);
          for (int i = 1; i < bufferChain.size(); i++) {
              main.append(bufferChain.get(i));
          }
  
          String sql = main.length() > 0 ? main.toString() : null;
          SQLTemplate query = new SQLTemplate(compiledExpression
                  .getRootDescriptor()
                  .getObjectClass(), sql);
          query.setParams(boundParameters);
          return query;
      }
      
      public QueryMetadata getMetadata(){
          return queryMetadata;
      }
  
      private String resolveId(String id) {
          if (idAliases == null) {
              return id;
          }
  
          String resolvedAlias = idAliases.get(id);
          if (resolvedAlias != null) {
              return resolvedAlias;
          }
  
          return id;
      }
  
      EJBQLTranslatorFactory getTranslatorFactory() {
          return translatorFactory;
      }
  
      EntityResolver getEntityResolver() {
          return entityResolver;
      }
  
      /**
       * Looks up entity descriptor for an identifier that can be a compiled expression id
       * or one of the aliases.
       */
      public ClassDescriptor getEntityDescriptor(String id) {
          return compiledExpression.getEntityDescriptor(resolveId(id));
      }
  
      List<DbRelationship> getIncomingRelationships(EJBQLTableId id) {
  
          List<DbRelationship> incoming = compiledExpression
                  .getIncomingRelationships(resolveId(id.getEntityId()));
  
          // append tail of flattened relationships...
          if (id.getDbPath() != null) {
  
              DbEntity entity;
  
              if (incoming == null || incoming.isEmpty()) {
                  entity = compiledExpression
                          .getEntityDescriptor(id.getEntityId())
                          .getEntity()
                          .getDbEntity();
              }
              else {
                  DbRelationship last = incoming.get(incoming.size() - 1);
                  entity = last.getTargetEntity();
              }
  
              incoming = new ArrayList<>(incoming);
  
              Iterator<?> it = entity.resolvePathComponents(id.getDbPath());
              while (it.hasNext()) {
                  incoming.add((DbRelationship) it.next());
              }
          }
  
          return incoming;
      }
  
      /**
       * Creates a previously unused id alias for an entity identified by an id.
       */
      String createIdAlias(String id) {
  
          if (idAliases == null) {
              idAliases = new HashMap<>();
          }
  
          for (int i = 0; i < 1000; i++) {
              String alias = id + "_alias" + i;
              if (idAliases.containsKey(alias)) {
                  continue;
              }
  
              if (compiledExpression.getEntityDescriptor(alias) != null) {
                  continue;
              }
  
              idAliases.put(alias, id);
              return alias;
          }
  
          throw new EJBQLException("Failed to create id alias");
      }
  
      /**
       * Inserts a marker in the SQL, mapped to a StringBuilder that can be later filled
       * with content.
       */
      void markCurrentPosition(String marker) {
  
          StringBuilder buffer = findOrCreateMarkedBuffer(marker);
          bufferChain.add(buffer);
  
          // immediately create unmarked buffer after the marked one and replace the bottom
          // of the stack with it
          StringBuilder tailBuffer = new StringBuilder();
          bufferChain.add(tailBuffer);
          bufferStack.set(0, tailBuffer);
          stackTop = bufferStack.get(bufferStack.size() - 1);
      }
  
      /**
       * Switches the current buffer to a marked buffer, pushing the currently used buffer
       * on the stack. Note that this can be done even before the marker is inserted in the
       * main buffer. If "reset" is true, any previous contents of the marker are cleared.
       */
      public void pushMarker(String marker, boolean reset) {
  
          stackTop = findOrCreateMarkedBuffer(marker);
          if (reset) {
              stackTop.delete(0, stackTop.length());
          }
  
          bufferStack.add(stackTop);
      }
  
      /**
       * Pops a marker stack, switching to the previously used marker.
       */
      void popMarker() {
          int lastIndex = bufferStack.size() - 1;
          bufferStack.remove(lastIndex);
          stackTop = bufferStack.get(lastIndex - 1);
      }
  
      StringBuilder findOrCreateMarkedBuffer(String marker) {
          StringBuilder buffer = (StringBuilder) getAttribute(marker);
          if (buffer == null) {
              buffer = new StringBuilder();
  
              // register mapping of internal to external marker
              setAttribute(marker, buffer);
          }
  
          return buffer;
      }
  
      /**
       * Returns a context "attribute" stored for the given name. Attributes is a state
       * preservation mechanism used by translators and have the same scope as the context.
       */
      Object getAttribute(String name) {
          return attributes != null ? attributes.get(name) : null;
      }
  
      /**
       * Sets a context "attribute". Attributes is a state preservation mechanism used by
       * translators and have the same scope as the context.
       */
      void setAttribute(String var, Object value) {
          if (attributes == null) {
              attributes = new HashMap<>();
          }
  
          attributes.put(var, value);
      }
  
      /**
       * Appends a piece of SQL to the internal buffer.
       */
      public EJBQLTranslationContext append(String chunk) {
          stackTop.append(chunk);
          return this;
      }
  
      /**
       * Appends a piece of SQL to the internal buffer.
       */
      public EJBQLTranslationContext append(char chunk) {
          stackTop.append(chunk);
          return this;
      }
  
      /**
       * Deletes a specified number of characters from the end of the current buffer.
       */
      EJBQLTranslationContext trim(int n) {
          int len = stackTop.length();
  
          if (len >= n) {
              stackTop.delete(len - n, len);
          }
          return this;
      }
  
      EJBQLCompiledExpression getCompiledExpression() {
          return compiledExpression;
      }
  
      String bindPositionalParameter(int position) {
          return bindParameter(positionalParameters.get(position));
      }
  
      /**
       * <p>This is used in the processing of parameters into lists for the IN clause and
       * is able to return a list of values that can be used to represent the bound
       * parameter.</p>
       */
  
      List<String> bindPositionalParameterFlatteningCollection(int position) {
          return bindParameters(positionalParameters.get(position));
      }
  
      String bindNamedParameter(String name) {
          return bindParameter(namedParameters.get(name));
      }
  
      /**
       * <p>This is used in the processing of parameters into lists for the IN clause and
       * is able to return a list of values that can be used to represent the bound
       * parameter.</p>
       */
  
      List<String> bindNamedParameterFlatteningCollection(String name) {
          return bindParameters(namedParameters.get(name));
      }
  
      /**
       * <p>This method takes a value object which may be a collection or a non-collection.  If it
       * is a collection then it will bind all of the values in the collection.  If it is a non-
       * collection then it will bind that single object.</p>
       * @param value
       * @return
       */
  
      List<String> bindParameters(Object value) {
          if(Collection.class.isAssignableFrom(value.getClass())) {
              Iterator<?> parameterValueIterator = ((Collection<?>) value).iterator();
              List<String> result = new ArrayList<>();
  
              while(parameterValueIterator.hasNext()) {
                  result.add(bindParameter(parameterValueIterator.next()));
              }
  
              return result;
          }
  
          return Collections.singletonList(bindParameter(value));
      }
  
      /**
       * Creates a new parameter variable, binding provided value to it.
       */
      String bindParameter(Object value) {
          return bindParameter(value, "id");
      }
  
      void rebindParameter(String boundName, Object newValue) {
          boundParameters.put(boundName, newValue);
      }
  
      /**
       * Creates a new parameter variable with the specified prefix, binding provided value
       * to it.
       */
      String bindParameter(Object value, String prefix) {
          if (boundParameters == null) {
              boundParameters = new HashMap<>();
          }
  
          String var = prefix + boundParameters.size();
          boundParameters.put(var, value);
          return var;
      }
  
      Object getBoundParameter(String name) {
          return boundParameters != null ? boundParameters.get(name) : null;
      }
  
      /**
       * Retrieves a SQL alias for the combination of EJBQL id variable and a table name. If
       * such alias hasn't been used, it is created on the fly.
       */
      protected String getTableAlias(String idPath, String tableName) {
  
          if (!isUsingAliases()) {
              return tableName;
          }
  
          StringBuilder keyBuffer = new StringBuilder();
  
          // per JPA spec, 4.4.2, "Identification variables are case insensitive.", while
          // relationship path is case-sensitive
  
          int dot = idPath.indexOf('.');
          if (dot > 0) {
              keyBuffer.append(idPath.substring(0, dot).toLowerCase()).append(
                      idPath.substring(dot));
          }
          else {
              keyBuffer.append(idPath.toLowerCase());
          }
  
          String key = keyBuffer.append(':').append(tableName).toString();
  
          String alias;
  
          if (tableAliases != null) {
              alias = tableAliases.get(key);
          }
          else {
              tableAliases = new HashMap<>();
              alias = null;
          }
  
          if (alias == null) {
              alias = "t" + tableAliases.size();
              tableAliases.put(key, alias);
          }
  
          return alias;
      }
  
      /**
       * Returns a positional EntityResult, incrementing position index on each call.
       */
      EntityResultSegment nextEntityResult() {
  
          if (resultSetMetadata == null) {
              throw new EJBQLException(
                      "No result set mapping exists for expression, can't map EntityResult");
          }
  
          return (EntityResultSegment) resultSetMetadata.get(resultDescriptorPosition++);
      }
  
      /**
       * Returns a positional column alias, incrementing position index on each call.
       */
      String nextColumnAlias() {
  
          if (resultSetMetadata == null) {
              throw new EJBQLException(
                      "No result set mapping exists for expression, can't map column aliases");
          }
  
          return ((ScalarResultSegment) resultSetMetadata.get(resultDescriptorPosition++))
                  .getColumn();
      }
  
      public boolean isAppendingResultColumns() {
          return appendingResultColumns;
      }
  
      void setAppendingResultColumns(boolean appendingResultColumns) {
          this.appendingResultColumns = appendingResultColumns;
      }
  
      public boolean isUsingAliases() {
          return usingAliases;
      }
  
      public void setUsingAliases(boolean useAliases) {
          this.usingAliases = useAliases;
      }
      
      public boolean isCaseInsensitive() {
          return caseInsensitive;
      }
      
      public void setCaseInsensitive(boolean caseInsensitive) {
          this.caseInsensitive = caseInsensitive;
      }
  
      public QuotingStrategy getQuotingStrategy() {
          return quotingStrategy;
      }
  
      public void onSubselect() {
          subselectCount++;
      }
  
      public String makeDistinctMarker() {
          return "DISTINCT_MARKER" + subselectCount;
      }
  
      String makeWhereMarker() {
          return "WHERE_MARKER" + subselectCount;
      }
  
      String makeEntityQualifierMarker() {
          return "ENTITY_QUALIIER" + subselectCount;
      }
  }
  