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.ObjectId;
  import org.apache.cayenne.Persistent;
  import org.apache.cayenne.dba.QuotingStrategy;
  import org.apache.cayenne.dba.TypesMapping;
  import org.apache.cayenne.ejbql.EJBQLBaseVisitor;
  import org.apache.cayenne.ejbql.EJBQLException;
  import org.apache.cayenne.ejbql.EJBQLExpression;
  import org.apache.cayenne.ejbql.parser.AggregateConditionNode;
  import org.apache.cayenne.ejbql.parser.EJBQLDecimalLiteral;
  import org.apache.cayenne.ejbql.parser.EJBQLEquals;
  import org.apache.cayenne.ejbql.parser.EJBQLIdentificationVariable;
  import org.apache.cayenne.ejbql.parser.EJBQLIntegerLiteral;
  import org.apache.cayenne.ejbql.parser.EJBQLNamedInputParameter;
  import org.apache.cayenne.ejbql.parser.EJBQLPath;
  import org.apache.cayenne.ejbql.parser.EJBQLPositionalInputParameter;
  import org.apache.cayenne.ejbql.parser.EJBQLSubselect;
  import org.apache.cayenne.ejbql.parser.EJBQLTrimBoth;
  import org.apache.cayenne.ejbql.parser.EJBQLTrimSpecification;
  import org.apache.cayenne.ejbql.parser.Node;
  import org.apache.cayenne.ejbql.parser.SimpleNode;
  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.ObjRelationship;
  import org.apache.cayenne.reflect.AttributeProperty;
  import org.apache.cayenne.reflect.ClassDescriptor;
  import org.apache.cayenne.reflect.PropertyDescriptor;
  
  import java.math.BigDecimal;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  
  /**
   * @since 3.0
   */
  public class EJBQLConditionTranslator extends EJBQLBaseVisitor {
  
      protected EJBQLTranslationContext context;
      protected List<EJBQLMultiColumnOperand> multiColumnOperands;
  
      public EJBQLConditionTranslator(EJBQLTranslationContext context) {
          this.context = context;
      }
  
      protected void addMultiColumnOperand(EJBQLMultiColumnOperand operand) {
          if (multiColumnOperands == null) {
              multiColumnOperands = new ArrayList<>(2);
          }
  
          multiColumnOperands.add(operand);
      }
  
      @Override
      public boolean visitAggregate(EJBQLExpression expression) {
          expression.visit(context.getTranslatorFactory().getAggregateColumnTranslator(context));
          return false;
      }
  
      @Override
      public boolean visitAnd(EJBQLExpression expression, int finishedChildIndex) {
          visitConditional((AggregateConditionNode) expression, " AND", finishedChildIndex);
          return true;
      }
  
      @Override
      public boolean visitBetween(EJBQLExpression expression, int finishedChildIndex) {
          switch (finishedChildIndex) {
          case 0:
              if (expression.isNegated()) {
                  context.append(" NOT");
              }
              context.append(" BETWEEN");
              break;
          case 1:
              context.append(" AND");
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitExists(EJBQLExpression expression) {
          context.append(" EXISTS");
          return true;
      }
  
      @Override
      public boolean visitIsEmpty(EJBQLExpression expression) {
  
          // handle as "path is [not] null" (an alt. way would've been a
          // correlated subquery
          // on the target entity)...
  
          if (expression.isNegated()) {
              context.pushMarker(context.makeDistinctMarker(), true);
              context.append(" DISTINCT");
              context.popMarker();
          }
  
          visitIsNull(expression, -1);
          for (int i = 0; i < expression.getChildrenCount(); i++) {
              expression.getChild(i).visit(this);
              visitIsNull(expression, i);
          }
  
          return false;
      }
  
      @Override
      public boolean visitSize(EJBQLExpression expression) {
  
          // run as a correlated subquery.
          // see "visitMemberOf" for correlated subquery logic
          // also note that the code below is mostly copy/paste from MEMBER OF
          // method ...
          // maybe there's enough commonality in building correlated subqueries to
          // make it
          // reusable???
  
          if (expression.getChildrenCount() != 1) {
              throw new EJBQLException("SIZE must have exactly one child, got: " + expression.getChildrenCount());
          }
  
          if (!(expression.getChild(0) instanceof EJBQLPath)) {
              throw new EJBQLException("First child of SIZE must be a collection path, got: " + expression.getChild(1));
          }
  
          QuotingStrategy quoter = context.getQuotingStrategy();
  
          EJBQLPath path = (EJBQLPath) expression.getChild(0);
  
          String id = path.getAbsolutePath();
  
          String correlatedEntityId = path.getId();
          ClassDescriptor correlatedEntityDescriptor = context.getEntityDescriptor(correlatedEntityId);
          String correlatedTableName = quoter.quotedFullyQualifiedName(correlatedEntityDescriptor.getEntity()
                  .getDbEntity());
          String correlatedTableAlias = context.getTableAlias(correlatedEntityId, correlatedTableName);
  
          String subqueryId = context.createIdAlias(id);
          ClassDescriptor targetDescriptor = context.getEntityDescriptor(subqueryId);
  
          if (expression.isNegated()) {
              context.append(" NOT");
          }
  
          context.append(" EXISTS (SELECT 1 FROM ");
  
          String subqueryTableName = quoter.quotedFullyQualifiedName(targetDescriptor.getEntity().getDbEntity());
          String subqueryRootAlias = context.getTableAlias(subqueryId, subqueryTableName);
  
          ObjRelationship relationship = correlatedEntityDescriptor.getEntity().getRelationship(path.getRelativePath());
  
          if (relationship.getDbRelationshipPath().length() > 1) {
              // if the DbRelationshipPath contains '.', the relationship is flattened
              subqueryRootAlias = processFlattenedRelationShip(subqueryRootAlias, relationship);
          } else {
              // 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(subqueryTableName).append(' ').append(subqueryRootAlias);
  
          }
          context.append(" WHERE");
  
          DbRelationship correlatedJoinRelationship = context.getIncomingRelationships(new EJBQLTableId(id)).get(0);
          Iterator<DbJoin> it = correlatedJoinRelationship.getJoins().iterator();
          while (it.hasNext()) {
              DbJoin join = it.next();
              context.append(' ').append(subqueryRootAlias).append('.').append(join.getTargetName()).append(" = ");
              context.append(correlatedTableAlias).append('.').append(quoter.quotedSourceName(join));
  
              if (it.hasNext()) {
                  context.append(" AND");
              }
          }
  
          context.append(")");
  
          return false;
      }
  
      @Override
      public boolean visitMemberOf(EJBQLExpression expression) {
  
          // create a correlated subquery, using the following transformation:
  
          // * Subquery Root is always an entity that is a target of relationship
          // * A subquery has a join based on reverse relationship, pointing to
          // the
          // original ID.
          // * Join must be translated as a part of the subquery WHERE clause
          // instead of
          // FROM.
          // * A condition is added: subquery_root_id = LHS_memberof
  
          if (expression.getChildrenCount() != 2) {
              throw new EJBQLException("MEMBER OF must have exactly two children, got: " + expression.getChildrenCount());
          }
  
          if (!(expression.getChild(1) instanceof EJBQLPath)) {
              throw new EJBQLException("Second child of the MEMBER OF must be a collection path, got: "
                      + expression.getChild(1));
          }
  
          QuotingStrategy quoter = context.getQuotingStrategy();
  
          EJBQLPath path = (EJBQLPath) expression.getChild(1);
  
          // make sure the ID for the path does not overlap with other condition
          // joins...
          String id = path.getAbsolutePath();
  
          String correlatedEntityId = path.getId();
          ClassDescriptor correlatedEntityDescriptor = context.getEntityDescriptor(correlatedEntityId);
          String correlatedTableName = quoter.quotedFullyQualifiedName(correlatedEntityDescriptor.getEntity()
                  .getDbEntity());
          String correlatedTableAlias = context.getTableAlias(correlatedEntityId, correlatedTableName);
  
          String subqueryId = context.createIdAlias(id);
          ClassDescriptor targetDescriptor = context.getEntityDescriptor(subqueryId);
  
          if (expression.isNegated()) {
              context.append(" NOT");
          }
  
          context.append(" EXISTS (SELECT 1 FROM ");
  
          String subqueryTableName = quoter.quotedFullyQualifiedName(targetDescriptor.getEntity().getDbEntity());
          String subqueryRootAlias = context.getTableAlias(subqueryId, subqueryTableName);
  
          ObjRelationship relationship = correlatedEntityDescriptor.getEntity().getRelationship(path.getRelativePath());
  
          if (relationship.getDbRelationshipPath().length() > 1) {
              // if the DbRelationshipPath contains '.', the relationship is flattened
              subqueryRootAlias = processFlattenedRelationShip(subqueryRootAlias, relationship);
          } else {
              // 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(subqueryTableName).append(' ').append(subqueryRootAlias);
  
          }
  
          context.append(" WHERE");
  
          DbRelationship correlatedJoinRelationship = context.getIncomingRelationships(new EJBQLTableId(id)).get(0);
  
          for (DbJoin join : correlatedJoinRelationship.getJoins()) {
              context.append(' ').append(subqueryRootAlias).append('.').append(join.getTargetName()).append(" = ");
              context.append(correlatedTableAlias).append('.').append(quoter.quotedSourceName(join));
              context.append(" AND");
          }
  
          // translate subquery_root_id = LHS_of_memberof
          EJBQLEquals equals = new EJBQLEquals(-1);
          EJBQLIdentificationVariable identifier = new EJBQLIdentificationVariable(-1);
          identifier.setText(subqueryId);
          equals.jjtAddChild(identifier, 0);
          equals.jjtAddChild((Node) expression.getChild(0), 1);
          equals.visit(this);
  
          context.append(")");
  
          return false;
      }
  
      private String processFlattenedRelationShip(String subqueryRootAlias, ObjRelationship relationship) {
  
          QuotingStrategy quoter = context.getQuotingStrategy();
  
          List<DbRelationship> dbRelationships = relationship.getDbRelationships();
          // reverse order to get the nearest to the correlated of the direct
          // relation
          for (int i = dbRelationships.size() - 1; i > 0; i--) {
              DbRelationship dbRelationship = dbRelationships.get(i);
              String subqueryTargetTableName = quoter.quotedFullyQualifiedName(dbRelationship.getTargetEntity());
              String subqueryTargetAlias;
              if (i == dbRelationships.size() - 1) {
                  subqueryTargetAlias = subqueryRootAlias;
                  context.append(subqueryTargetTableName).append(' ').append(subqueryTargetAlias);
              } else {
                  subqueryTargetAlias = context.getTableAlias(subqueryTargetTableName, subqueryTargetTableName);
              }
  
              context.append(" JOIN ");
  
              String subquerySourceTableName = quoter.quotedFullyQualifiedName(dbRelationship.getSourceEntity());
              String subquerySourceAlias = context.getTableAlias(subquerySourceTableName, subquerySourceTableName);
  
              context.append(subquerySourceTableName).append(' ').append(subquerySourceAlias);
  
              context.append(" ON (");
  
              List<DbJoin> joins = dbRelationship.getJoins();
              Iterator<DbJoin> it = joins.iterator();
              while (it.hasNext()) {
                  DbJoin join = it.next();
                  context.append(' ').append(subqueryTargetAlias).append('.').append(join.getTargetName()).append(" = ");
                  context.append(subquerySourceAlias).append('.').append(join.getSourceName());
                  if (it.hasNext()) {
                      context.append(" AND");
                  }
              }
              context.append(" )");
              subqueryRootAlias = subquerySourceAlias;
  
          }
          return subqueryRootAlias;
      }
  
      @Override
      public boolean visitAll(EJBQLExpression expression) {
          context.append(" ALL");
          return true;
      }
  
      @Override
      public boolean visitAny(EJBQLExpression expression) {
          context.append(" ANY");
          return true;
      }
  
      @Override
      public boolean visitOr(EJBQLExpression expression, int finishedChildIndex) {
          visitConditional((AggregateConditionNode) expression, " OR", finishedChildIndex);
          return true;
      }
  
      /**
       * Checks expression for containing null input parameter. For that, we'll
       * append IS NULL or IS NOT NULL instead of =null or &lt;&gt;null
       * 
       * @return whether replacement was done and there's no need for normal
       *         expression processing
       */
      protected boolean checkNullParameter(EJBQLExpression expression, String toAppend) {
          if (expression.getChildrenCount() == 2) {
              // We rewrite expression "parameter = :x" where x=null
              // as "parameter IS NULL"
              // BUT in such as ":x = parameter" (where x=null) we don't do
              // anything
              // as a result it can be unsupported in some DB
              if (expression.getChild(1) instanceof EJBQLNamedInputParameter) {
                  EJBQLNamedInputParameter par = (EJBQLNamedInputParameter) expression.getChild(1);
                  if (context.namedParameters.containsKey(par.getText())
                          && context.namedParameters.get(par.getText()) == null) {
                      context.append(toAppend);
                      return true;
                  }
              } else if (expression.getChild(1) instanceof EJBQLPositionalInputParameter) {
                  EJBQLPositionalInputParameter par = (EJBQLPositionalInputParameter) expression.getChild(1);
                  if (context.positionalParameters.containsKey(par.getPosition())
                          && context.positionalParameters.get(par.getPosition()) == null) {
                      context.append(toAppend);
                      return true;
                  }
              }
          }
          return false;
      }
  
      @Override
      public boolean visitEquals(EJBQLExpression expression, int finishedChildIndex) {
          switch (finishedChildIndex) {
          case 0:
              if (checkNullParameter(expression, " IS NULL")) {
                  return false;
              }
              context.append(" =");
              break;
          case 1:
              // check multicolumn match condition and undo op insertion and
              // append it
              // from scratch if needed
              if (multiColumnOperands != null) {
  
                  if (multiColumnOperands.size() != 2) {
                      throw new EJBQLException(
                              "Invalid multi-column equals expression. Expected 2 multi-column operands, got "
                                      + multiColumnOperands.size());
                  }
  
                  context.trim(2);
  
                  EJBQLMultiColumnOperand lhs = multiColumnOperands.get(0);
                  EJBQLMultiColumnOperand rhs = multiColumnOperands.get(1);
  
                  Iterator<?> it = lhs.getKeys().iterator();
                  while (it.hasNext()) {
                      Object key = it.next();
  
                      lhs.appendValue(key);
                      context.append(" =");
                      rhs.appendValue(key);
  
                      if (it.hasNext()) {
                          context.append(" AND");
                      }
                  }
  
                  multiColumnOperands = null;
              }
  
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitNamedInputParameterForIn(EJBQLExpression expression) {
          List<String> parameters = context.bindNamedParameterFlatteningCollection(expression.getText());
  
          if(0==parameters.size()) {
              throw new IllegalStateException("it is not possible for a named parameter to not be bound");
          }
  
          for(int i=0;i<parameters.size();i++) {
              if(0!=i) {
                  context.append(',');
              }
  
              processParameter(parameters.get(i), expression);
          }
  
          return true;
      }
  
      @Override
      public boolean visitNamedInputParameter(EJBQLExpression expression) {
          String parameter = context.bindNamedParameter(expression.getText());
          processParameter(parameter, expression);
          return true;
      }
  
      @Override
      public boolean visitNot(EJBQLExpression expression) {
          context.append(" NOT");
          return true;
      }
  
      @Override
      public boolean visitNotEquals(EJBQLExpression expression, int finishedChildIndex) {
          switch (finishedChildIndex) {
          case 0:
              if (checkNullParameter(expression, " IS NOT NULL")) {
                  return false;
              }
              context.append(" <>");
              break;
          case 1:
              // check multicolumn match condition and undo op insertion and
              // append it
              // from scratch if needed
              if (multiColumnOperands != null) {
  
                  if (multiColumnOperands.size() != 2) {
                      throw new EJBQLException(
                              "Invalid multi-column equals expression. Expected 2 multi-column operands, got "
                                      + multiColumnOperands.size());
                  }
  
                  context.trim(3);
  
                  EJBQLMultiColumnOperand lhs = multiColumnOperands.get(0);
                  EJBQLMultiColumnOperand rhs = multiColumnOperands.get(1);
  
                  Iterator<?> it = lhs.getKeys().iterator();
                  while (it.hasNext()) {
                      Object key = it.next();
  
                      lhs.appendValue(key);
                      context.append(" <>");
                      rhs.appendValue(key);
  
                      if (it.hasNext()) {
                          context.append(" OR");
                      }
                  }
  
                  multiColumnOperands = null;
              }
  
              break;
          }
          return true;
      }
  
      @Override
      public boolean visitGreaterThan(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              context.append(" >");
          }
  
          return true;
      }
  
      @Override
      public boolean visitGreaterOrEqual(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              context.append(" >=");
          }
  
          return true;
      }
  
      @Override
      public boolean visitLessOrEqual(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              context.append(" <=");
          }
  
          return true;
      }
  
      @Override
      public boolean visitLessThan(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              context.append(" <");
          }
  
          return true;
      }
  
      @Override
      public boolean visitLike(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              if (checkNullParameter(expression, " IS NULL")) {
                  return false;
              }
  
              if (expression.isNegated()) {
                  context.append(" NOT");
              }
              context.append(" LIKE");
          }
  
          return true;
      }
  
      @Override
      public boolean visitIn(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              if (expression.isNegated()) {
                  context.append(" NOT");
              }
              context.append(" IN");
  
              // a cosmetic hack for preventing extra pair of parenthesis from
              // being
              // appended in 'visitSubselect'
              if (expression.getChildrenCount() == 2 && expression.getChild(1) instanceof EJBQLSubselect) {
                  visitSubselect(expression.getChild(1));
                  return false;
              }
  
              context.append(" (");
          } else if (finishedChildIndex == expression.getChildrenCount() - 1) {
              context.append(")");
          } else if (finishedChildIndex > 0) {
              context.append(',');
          }
  
          return true;
      }
  
      /**
       * Visits conditional node, suppling brackets if needed
       */
      void visitConditional(AggregateConditionNode e, String afterText, int childIndex) {
          if (childIndex == -1 && needBracket(e)) {
              context.append(" (");
          }
  
          afterChild(e, afterText, childIndex);
  
          if (childIndex == e.getChildrenCount() - 1 && needBracket(e)) {
              context.append(")");
          }
      }
  
      /**
       * Checks whether expression needs to be rounded by brackets
       */
      boolean needBracket(AggregateConditionNode e) {
          return (e.jjtGetParent() instanceof AggregateConditionNode)
                  && e.getPriority() > ((AggregateConditionNode) e.jjtGetParent()).getPriority();
      }
  
      protected void afterChild(EJBQLExpression e, String text, int childIndex) {
          if (childIndex >= 0) {
              if (childIndex + 1 < e.getChildrenCount()) {
                  context.append(text);
              }
          }
      }
  
      @Override
      public boolean visitIdentificationVariable(EJBQLExpression expression) {
          // this is a match on a variable, like "x = :x"
  
          ClassDescriptor descriptor = context.getEntityDescriptor(expression.getText());
          if (descriptor == null) {
              throw new EJBQLException("Invalid identification variable: " + expression.getText());
          }
  
          DbEntity table = descriptor.getEntity().getDbEntity();
          String alias = context.getTableAlias(expression.getText(), context.getQuotingStrategy()
                  .quotedFullyQualifiedName(table));
  
          Collection<DbAttribute> pks = table.getPrimaryKeys();
  
          if (pks.size() == 1) {
              DbAttribute pk = pks.iterator().next();
              context.append(' ').append(alias).append('.').append(context.getQuotingStrategy().quotedName(pk));
          } else {
              throw new EJBQLException("Multi-column PK to-many matches are not yet supported.");
          }
          return false;
      }
  
      @Override
      public boolean visitDbPath(EJBQLExpression expression, int finishedChildIndex) {
          expression.visit(new EJBQLDbPathTranslator(context) {
  
              @Override
              protected void appendMultiColumnPath(EJBQLMultiColumnOperand operand) {
                  EJBQLConditionTranslator.this.addMultiColumnOperand(operand);
              }
          });
          return false;
      }
  
      @Override
      public boolean visitPath(EJBQLExpression expression, int finishedChildIndex) {
  
          expression.visit(new EJBQLPathTranslator(context) {
  
              @Override
              protected void appendMultiColumnPath(EJBQLMultiColumnOperand operand) {
                  EJBQLConditionTranslator.this.addMultiColumnOperand(operand);
              }
          });
          return false;
      }
  
      @Override
      public boolean visitIntegerLiteral(EJBQLIntegerLiteral expression) {
          if (expression.getText() == null) {
              context.append("null");
          } else {
  
              String text = expression.getText();
  
              if (expression.isNegative() && text != null) {
                  if (text.startsWith("-")) {
                      text = text.substring(1);
                  } else {
                      text = "-" + text;
                  }
              }
  
              Long longValue;
  
              try {
                  longValue = Long.valueOf(text);
              } catch (NumberFormatException nfex) {
                  throw new EJBQLException("Invalid integer: " + expression.getText());
              }
  
              if(longValue > Integer.MAX_VALUE) {
                  String var = context.bindParameter(longValue);
                  context.append(" #bind($").append(var).append(" 'BIGINT')");
              }
              else {
                  String var = context.bindParameter(longValue.intValue());
                  context.append(" #bind($").append(var).append(" 'INTEGER')");
              }
          }
          return true;
      }
  
      @Override
      public boolean visitDecimalLiteral(EJBQLDecimalLiteral expression) {
          if (expression.getText() == null) {
              context.append("null");
          } else {
  
              String text = expression.getText();
  
              if (expression.isNegative() && text != null) {
                  if (text.startsWith("-")) {
                      text = text.substring(1);
                  } else {
                      text = "-" + text;
                  }
              }
  
              Object value;
  
              try {
                  value = new BigDecimal(text);
              } catch (NumberFormatException nfex) {
                  throw new EJBQLException("Invalid decimal: " + expression.getText());
              }
  
              String var = context.bindParameter(value);
              context.append(" #bind($").append(var).append(" 'DECIMAL')");
          }
          return true;
      }
  
      @Override
      public boolean visitEscapeCharacter(EJBQLExpression expression) {
          // note that EscapeChar text is already wrapped in single quotes
          context.append(" ESCAPE ").append(expression.getText());
          return false;
      }
  
      @Override
      public boolean visitIsNull(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex == 0) {
              context.append(expression.isNegated() ? " IS NOT NULL" : " IS NULL");
          }
  
          return true;
      }
  
      @Override
      public boolean visitPositionalInputParameterForIn(EJBQLPositionalInputParameter expression) {
          List<String> parameters = context.bindPositionalParameterFlatteningCollection(expression.getPosition());
  
          if(0==parameters.size()) {
              throw new IllegalStateException("it is not possible for a positional parameter to not be bound");
          }
  
          for(int i=0;i<parameters.size();i++) {
              if(0!=i) {
                  context.append(',');
              }
  
              processParameter(parameters.get(i), expression);
          }
  
          return true;
      }
  
      @Override
      public boolean visitPositionalInputParameter(EJBQLPositionalInputParameter expression) {
  
          String parameter = context.bindPositionalParameter(expression.getPosition());
          processParameter(parameter, expression);
          return true;
      }
  
      @Override
      public boolean visitBooleanLiteral(EJBQLExpression expression) {
          if (expression.getText() == null) {
              context.append("null");
          } else {
              Object value = Boolean.valueOf(expression.getText());
              String var = context.bindParameter(value);
              context.append(" #bind($").append(var).append(" 'BOOLEAN')");
          }
  
          return true;
      }
  
      @Override
      public boolean visitStringLiteral(EJBQLExpression expression) {
          if (expression.getText() == null) {
              context.append("null");
          } else {
              // note that String Literal text is already wrapped in single
              // quotes, with
              // quotes that are part of the string escaped.
              context.append(" #bind(").append(expression.getText()).append(" 'VARCHAR')");
          }
          return true;
      }
  
      @Override
      public boolean visitSubselect(EJBQLExpression expression) {
          context.onSubselect();
          context.append(" (");
          expression.visit(new EJBQLSelectTranslator(context));
          context.append(')');
          return false;
      }
  
      private void processParameter(String boundName, EJBQLExpression expression) {
          Object object = context.getBoundParameter(boundName);
  
          Map<?, ?> map = null;
          if (object instanceof Persistent) {
              map = ((Persistent) object).getObjectId().getIdSnapshot();
          } else if (object instanceof ObjectId) {
              map = ((ObjectId) object).getIdSnapshot();
          } else if (object instanceof Map) {
              map = (Map<?, ?>) object;
          }
  
          if (map != null) {
              if (map.size() == 1) {
                  context.rebindParameter(boundName, map.values().iterator().next());
              } else {
                  addMultiColumnOperand(EJBQLMultiColumnOperand.getObjectOperand(context, map));
                  return;
              }
          }
  
          if (object != null) {
              context.append(" #bind($").append(boundName).append(")");
          } else {
  
              String type = null;
              Node parent = ((SimpleNode) expression).jjtGetParent();
  
              context.pushMarker("@processParameter", true);
  
              EJBQLPathAnaliserTranslator translator = new EJBQLPathAnaliserTranslator(context);
              parent.visit(translator);
              translator.visitPath(parent, parent.getChildrenCount());
  
              String id = translator.idPath;
              if (id != null) {
  
                  ClassDescriptor descriptor = context.getEntityDescriptor(id);
                  if (descriptor == null) {
                      throw new EJBQLException("Unmapped id variable: " + id);
                  }
                  String pathChunk = translator.lastPathComponent;
  
                  PropertyDescriptor property = descriptor.getProperty(pathChunk);
                  if (property instanceof AttributeProperty) {
                      String atrType = ((AttributeProperty) property).getAttribute().getType();
  
                      type = TypesMapping.getSqlNameByType(TypesMapping.getSqlTypeByJava(atrType));
                  }
              }
              context.popMarker();
  
              if (type == null) {
                  type = "VARCHAR";
              }
              // this is a hack to prevent exceptions on DB's like Derby for
              // expressions
              // "X = NULL". The 'VARCHAR' parameter is totally bogus, but seems
              // to work on
              // all tested DB's... Also note what JPA spec, chapter 4.11 says:
              // "Comparison
              // or arithmetic operations with a NULL value always yield an
              // unknown value."
  
              // TODO: andrus 6/28/2007 Ideally we should track the type of the
              // current
              // expression to provide a meaningful type.
  
              context.append(" #bind($").append(boundName).append(" '" + type + "')");
          }
      }
  
      @Override
      public boolean visitAdd(EJBQLExpression expression, int finishedChildIndex) {
  
          switch (finishedChildIndex) {
          case -1:
              context.append(" (");
              break;
          case 0:
              context.append(" +");
              break;
  
          case 1:
              context.append(")");
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitSubtract(EJBQLExpression expression, int finishedChildIndex) {
  
          switch (finishedChildIndex) {
          case -1:
              context.append(" (");
              break;
          case 0:
              context.append(" -");
              break;
  
          case 1:
              context.append(")");
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitMultiply(EJBQLExpression expression, int finishedChildIndex) {
          switch (finishedChildIndex) {
          case -1:
              context.append(" (");
              break;
          case 0:
              context.append(" *");
              break;
  
          case 1:
              context.append(")");
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitDivide(EJBQLExpression expression, int finishedChildIndex) {
          switch (finishedChildIndex) {
          case -1:
              context.append(" (");
              break;
          case 0:
              context.append(" /");
              break;
  
          case 1:
              context.append(")");
              break;
          }
  
          return true;
      }
  
      @Override
      public boolean visitCurrentDate(EJBQLExpression expression) {
          context.append(" {fn CURDATE()}");
          return false;
      }
  
      @Override
      public boolean visitCurrentTime(EJBQLExpression expression) {
          context.append(" {fn CURTIME()}");
          return false;
      }
  
      @Override
      public boolean visitCurrentTimestamp(EJBQLExpression expression) {
          context.append(" {fn NOW()}");
          return false;
      }
  
      @Override
      public boolean visitAbs(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn ABS(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          }
  
          return true;
      }
  
      @Override
      public boolean visitSqrt(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn SQRT(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          }
  
          return true;
      }
  
      @Override
      public boolean visitMod(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn MOD(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          } else {
              context.append(',');
          }
  
          return true;
      }
  
      @Override
      public boolean visitConcat(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn CONCAT(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          } else {
              context.append(',');
          }
  
          return true;
      }
  
      @Override
      public boolean visitSubstring(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn SUBSTRING(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          } else {
              context.append(',');
          }
  
          return true;
      }
  
      @Override
      public boolean visitLower(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn LCASE(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          }
  
          return true;
      }
  
      @Override
      public boolean visitUpper(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn UCASE(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          }
  
          return true;
      }
  
      @Override
      public boolean visitLength(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn LENGTH(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          }
  
          return true;
      }
  
      @Override
      public boolean visitLocate(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
              context.append(" {fn LOCATE(");
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              context.append(")}");
          } else {
              context.append(',');
          }
  
          return true;
      }
  
      @Override
      public boolean visitTrim(EJBQLExpression expression, int finishedChildIndex) {
          if (finishedChildIndex < 0) {
  
              if (!(expression.getChild(0) instanceof EJBQLTrimSpecification)) {
                  context.append(" {fn LTRIM({fn RTRIM(");
              }
          } else if (finishedChildIndex + 1 == expression.getChildrenCount()) {
              if (!(expression.getChild(0) instanceof EJBQLTrimSpecification)
                      || expression.getChild(0) instanceof EJBQLTrimBoth) {
                  context.append(")})}");
              } else {
                  context.append(")}");
              }
          }
  
          return true;
      }
  
      @Override
      public boolean visitTrimCharacter(EJBQLExpression expression) {
          // this is expected to be overwritten in adapter-specific translators
          if (!"' '".equals(expression.getText())) {
              throw new UnsupportedOperationException(
                      "TRIM character other than space is not supported by a generic adapter: " + expression.getText());
          }
  
          return false;
      }
  
      @Override
      public boolean visitTrimLeading(EJBQLExpression expression) {
          context.append(" {fn LTRIM(");
          return false;
      }
  
      @Override
      public boolean visitTrimTrailing(EJBQLExpression expression) {
          context.append(" {fn RTRIM(");
          return false;
      }
  
      @Override
      public boolean visitTrimBoth(EJBQLExpression expression) {
          context.append(" {fn LTRIM({fn RTRIM(");
          return false;
      }
  
  }
  
  class EJBQLPathAnaliserTranslator extends EJBQLPathTranslator {
  
      private boolean isPath;
  
      public EJBQLPathAnaliserTranslator(EJBQLTranslationContext context) {
          super(context);
          isPath = false;
      }
  
      @Override
      protected void appendMultiColumnPath(EJBQLMultiColumnOperand operand) {
      }
  
      @Override
      public boolean visitPath(EJBQLExpression expression, int finishedChildIndex) {
          if (isPath) {
              return false;
          } else {
  
              if (finishedChildIndex > 0) {
  
                  if (finishedChildIndex + 1 < expression.getChildrenCount()) {
                      processIntermediatePathComponent();
                  } else {
                      processLastPathComponent();
                      if (idPath != null && lastPathComponent != null) {
                          isPath = true;
                      }
                      return false;
                  }
              }
          }
          return true;
      }
  }
  