content: package io.agrest.cayenne.processor;
  
  import io.agrest.AgException;
  import io.agrest.RelatedResourceEntity;
  import io.agrest.ResourceEntity;
  import io.agrest.RootResourceEntity;
  import io.agrest.cayenne.exp.ICayenneExpParser;
  import io.agrest.cayenne.exp.ICayenneExpPostProcessor;
  import io.agrest.cayenne.path.IPathResolver;
  import io.agrest.cayenne.path.PathOps;
  import io.agrest.cayenne.persister.ICayennePersister;
  import io.agrest.id.AgObjectId;
  import io.agrest.meta.AgEntity;
  import io.agrest.meta.AgIdPart;
  import io.agrest.protocol.Direction;
  import io.agrest.protocol.Sort;
  import io.agrest.runtime.EntityParent;
  import io.agrest.runtime.processor.select.SelectContext;
  import org.apache.cayenne.di.Inject;
  import org.apache.cayenne.exp.Expression;
  import org.apache.cayenne.exp.ExpressionFactory;
  import org.apache.cayenne.exp.parser.ASTDbPath;
  import org.apache.cayenne.exp.parser.ASTPath;
  import org.apache.cayenne.exp.property.Property;
  import org.apache.cayenne.exp.property.PropertyFactory;
  import org.apache.cayenne.map.EntityResolver;
  import org.apache.cayenne.map.ObjEntity;
  import org.apache.cayenne.map.ObjRelationship;
  import org.apache.cayenne.query.ColumnSelect;
  import org.apache.cayenne.query.FluentSelect;
  import org.apache.cayenne.query.ObjectSelect;
  import org.apache.cayenne.query.Ordering;
  import org.apache.cayenne.query.SortOrder;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  import java.util.function.Consumer;
  import java.util.function.Function;
  
  /**
   * @since 3.4
   */
  public class CayenneQueryAssembler implements ICayenneQueryAssembler {
  
      private final EntityResolver entityResolver;
      private final IPathResolver pathResolver;
      private final ICayenneExpParser qualifierParser;
      private final ICayenneExpPostProcessor qualifierPostProcessor;
  
      public CayenneQueryAssembler(
              @Inject ICayennePersister persister,
              @Inject IPathResolver pathResolver,
              @Inject ICayenneExpParser qualifierParser,
              @Inject ICayenneExpPostProcessor qualifierPostProcessor) {
  
          this.entityResolver = persister.entityResolver();
          this.pathResolver = pathResolver;
          this.qualifierParser = qualifierParser;
          this.qualifierPostProcessor = qualifierPostProcessor;
      }
  
      @Override
      public <T> ObjectSelect<T> createRootQuery(SelectContext<T> context) {
  
          ObjectSelect<T> query = context.getId() != null
                  ? createRootIdQuery(context.getEntity(), context.getId())
                  : createBaseQuery(context.getEntity());
  
          EntityParent<?> parent = context.getParent();
          if (parent != null) {
              query.and(CayenneUtil.parentQualifier(pathResolver, context.getSchema().getEntity(parent.getType()), parent, entityResolver));
          }
  
          return query;
      }
  
      @Override
      public <T> ColumnSelect<Object[]> createQueryWithParentQualifier(RelatedResourceEntity<T> entity) {
  
          ColumnSelect<Object[]> query = createBaseQuery(entity).columns(queryColumns(entity));
  
          // Translate expression from parent.
          // Find the closest parent in the chain that has a query of its own, and use that as a base.
          Expression parentQualifier = resolveParentQualifier(entity, null);
          if (parentQualifier != null) {
              query.and(parentQualifier);
          }
  
          return query;
      }
  
      /**
       * @since 5.0
       */
      public <T> Property<?>[] queryColumns(RelatedResourceEntity<T> entity) {
  
          // Use Cayenne metadata for query building. Agrest metadata may be missing some important parts like ids
          // (e.g. see https://github.com/agrestio/agrest/issues/473)
  
          AgEntity<?> parentEntity = entity.getParent().getAgEntity();
          ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
          ObjRelationship objRelationship = findRelationship(parentObjEntity, entity.getIncoming().getName());
          ASTDbPath reversePath = new ASTDbPath(objRelationship.getReverseDbRelationshipPath());
  
          Property<?>[] columns = new Property<?>[parentEntity.getIdParts().size() + 1];
  
          Class entityType = entity.getType();
          columns[0] = PropertyFactory.createSelf(entityType);
  
          // columns must be added in the order of id parts iteration, as this is how they will be read from result
          int i = 1;
          for (AgIdPart idPart : parentEntity.getIdParts()) {
              ASTPath idPartPath = pathResolver.resolve(parentEntity.getName(), idPart.getName()).getPathExp();
              Expression propertyExp = PathOps.concatWithDbPath(parentObjEntity, reversePath, idPartPath);
              columns[i++] = PropertyFactory.createBase(propertyExp, idPart.getType());
          }
  
          return columns;
      }
  
      @Override
      public <T> ObjectSelect<T> createQueryForIds(AgEntity<T> entity, Collection<AgObjectId> ids) {
  
          if (ids.isEmpty()) {
              throw AgException.badRequest("No ids specified");
          }
  
          ObjEntity objEntity = entityResolver.getObjEntity(entity.getType());
  
          // sanity checking...
          if (objEntity == null) {
              throw AgException.internalServerError("Unknown entity class: %s", entity.getType());
          }
  
          Function<AgObjectId, Expression> qualifierMaker = idQualifierMaker(entity);
          List<Expression> idQualifiers = new ArrayList<>(ids.size());
  
          // TODO: for single-column IDs, a better qualifier would be "IN (..)" instead of "OR"
          for (AgObjectId id : ids) {
              idQualifiers.add(qualifierMaker.apply(id));
          }
  
          return ObjectSelect.query(entity.getType()).where(ExpressionFactory.or(idQualifiers));
      }
  
      private Function<AgObjectId, Expression> idQualifierMaker(AgEntity<?> entity) {
          int idSize = entity.getIdParts().size();
  
          if (idSize == 1) {
              String partName = entity.getIdParts().iterator().next().getName();
              ASTPath idPath = pathResolver.resolve(entity.getName(), partName).getPathExp();
              return id -> ExpressionFactory.matchExp(idPath, id.get(partName));
          } else {
  
              List<String> partNames = new ArrayList<>(idSize);
              List<ASTPath> paths = new ArrayList<>(idSize);
              for (AgIdPart idPart : entity.getIdParts()) {
                  partNames.add(idPart.getName());
                  paths.add(pathResolver.resolve(entity.getName(), idPart.getName()).getPathExp());
              }
  
              return id -> {
                  List<Expression> idQualifier = new ArrayList<>(idSize);
                  for (int i = 0; i < idSize; i++) {
                      idQualifier.add(ExpressionFactory.matchExp(paths.get(i), id.get(partNames.get(i))));
                  }
  
                  return ExpressionFactory.and(idQualifier);
              };
          }
      }
  
      private ObjRelationship findRelationship(ObjEntity entity, String name) {
  
          // Take inheritance into account... ObjRelationship may not be present in the superclass, but the underlying
          // DB path is there. So let's find it in subclasses, and resolve the path.
          // TODO: this may not work with the future Cayenne horizontal inheritance
  
          ObjRelationship re = entity.getRelationship(name);
          ObjRelationship r = re != null ? re : findRelationshipInHierarchy(entity, name);
          if (r == null) {
              throw new IllegalStateException("ObjRelationship '" + name + "' is not found in '"
                      + entity.getName() + "' or in its inheritance hierarchy");
          }
  
          return r;
      }
  
      private ObjRelationship findRelationshipInHierarchy(ObjEntity parentObjEntity, String name) {
          return entityResolver.getInheritanceTree(parentObjEntity.getName()).allSubEntities().stream()
                  .filter(e -> e != parentObjEntity)
                  .map(e -> e.getRelationship(name))
                  .findFirst()
                  .orElse(null);
      }
  
      // using dbpaths for all expression operations on the theory that some object paths can be unidirectional, and
      // hence may be missing for some relationships (although all "incoming" relationships along the parents chain
      // should be present, no?)
      protected Expression resolveParentQualifier(RelatedResourceEntity<?> entity, String outgoingDbPath) {
  
          ResourceEntity<?> parent = entity.getParent();
          CayenneResourceEntityExt parentExt = CayenneProcessor.getEntity(parent);
          if (parentExt == null) {
              throw AgException.internalServerError("Parent '%s' of entity '%s' is not managed by Cayenne",
                      parent.getName(),
                      entity.getName());
          }
  
          FluentSelect<?> parentSelect = parentExt.getSelect();
  
          if (parentSelect != null) {
  
              Expression parentQualifier = parentSelect.getWhere();
              if (parentQualifier == null) {
                  return null;
              }
  
              String incomingPath = dbPath(parent.getType(), entity.getIncoming().getName());
              String fullDbPath = concatWithParentDbPath(incomingPath, outgoingDbPath);
  
              ObjEntity parentObjEntity = entityResolver.getObjEntity(parent.getType());
              Expression dbParentQualifier = parentObjEntity.translateToDbPath(parentQualifier);
              return parentObjEntity.getDbEntity().translateToRelatedEntity(dbParentQualifier, fullDbPath);
          }
  
          String incomingPath = dbPath(parent.getType(), entity.getIncoming().getName());
          String fullDbPath = concatWithParentDbPath(incomingPath, outgoingDbPath);
  
          // shouldn't really happen with any of the current built-in root strategies, but who knows what customizations
          // can be applied
          if (parent instanceof RootResourceEntity) {
              throw new IllegalStateException(
                      "Can't fetch child using parent expression strategy. Root entity '" +
                              parent.getName() +
                              "' has no SelectQuery of its own. DB path to child: " +
                              fullDbPath);
          }
  
          return resolveParentQualifier((RelatedResourceEntity) parent, fullDbPath);
      }
  
      private String dbPath(Class<?> entityType, String relationshipName) {
  
          // we can pick "relationshipName" from anywhere in the inheritance hierarchy, as it will be converted to a db path,
          // which is inheritance agnostic (at least until Cayenne starts supporting horizontal inheritance)
  
          return objRelationshipInInheritanceHierarchy(entityType, relationshipName).getDbRelationshipPath();
      }
  
      private ObjRelationship objRelationshipInInheritanceHierarchy(Class<?> superclassType, String relationshipName) {
          ObjEntity e = entityResolver.getObjEntity(superclassType);
          ObjRelationship r = e.getRelationship(relationshipName);
  
          if (r != null) {
              return r;
          }
  
          for (ObjEntity se : entityResolver.getInheritanceTree(e.getName()).allSubEntities()) {
              if (se != e) {
                  ObjRelationship sr = se.getRelationship(relationshipName);
                  if (sr != null) {
                      return sr;
                  }
              }
          }
  
          throw new IllegalStateException("No such relationship in entity or sub-entities: " + e.getName() + "." + relationshipName);
      }
  
      private String concatWithParentDbPath(String incomingDbPath, String outgoingDbPath) {
          return outgoingDbPath != null ? incomingDbPath + "." + outgoingDbPath : incomingDbPath;
      }
  
      @Override
      public <T, P> ColumnSelect<Object[]> createQueryWithParentIdsQualifier(RelatedResourceEntity<T> entity, Iterable<P> parentData) {
  
          ColumnSelect<Object[]> query = createBaseQuery(entity).columns(queryColumns(entity));
  
          // build id-based qualifier
          List<Expression> qualifiers = new ArrayList<>();
  
          // if pagination is in effect, we should only fault the requested range. It makes this particular strategy
          // very efficient in case of pagination
  
          ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName());
          ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName());
          String reversePath = objRelationship.getReverseDbRelationshipPath();
  
          consumeRange(parentData, entity.getParent().getStart(), entity.getParent().getLimit(),
                  // TODO: this only works for single column ids
                  p -> qualifiers.add(ExpressionFactory.matchDbExp(reversePath, p)));
  
          // TODO: There is some functionality in Cayenne that allows to break long OR qualifiers in a series of queries.
          //  How do we use it here?
          return query.and(ExpressionFactory.or(qualifiers));
      }
  
      static <P> void consumeRange(Iterable<P> parentData, int offset, int limit, Consumer<P> consumer) {
          if (parentData instanceof List) {
              // this optimization prevents faulting of paginated lists
              consumeRangeList((List<P>) parentData, offset, limit, consumer);
          } else {
              consumeRangeIterator(parentData.iterator(), offset, limit, consumer);
          }
      }
  
      static <P> void consumeRangeIterator(Iterator<P> parentData, int offset, int limit, Consumer<P> consumer) {
  
          int from = Math.max(0, offset);
          int to = limit > 0 ? Math.min(from + limit, Integer.MAX_VALUE) : Integer.MAX_VALUE;
  
          for (int i = 0; i < from && parentData.hasNext(); i++) {
              parentData.next();
          }
  
          for (int i = from; i < to && parentData.hasNext(); i++) {
              consumer.accept(parentData.next());
          }
      }
  
      static <P> void consumeRangeList(List<P> parents, int offset, int limit, Consumer<P> consumer) {
  
  
          int len = parents.size();
          if (len == 0) {
              return;
          }
  
          int from = Math.max(0, offset);
          if (from >= len) {
              return;
          }
  
          int to = limit > 0 ? Math.min(from + limit, len) : len;
  
          for (int i = from; i < to; i++) {
              consumer.accept(parents.get(i));
          }
      }
  
      protected <T> ObjectSelect<T> createRootIdQuery(ResourceEntity<T> entity, AgObjectId rootId) {
          return ObjectSelect.query(entity.getType())
                  .where(buildIdQualifier(entity.getAgEntity(), rootId));
      }
  
      protected <T> ObjectSelect<T> createBaseQuery(ResourceEntity<T> entity) {
  
          ObjectSelect<T> query = ObjectSelect.query(entity.getType());
  
          if (!entity.isFiltered()) {
              int limit = entity.getLimit();
              if (limit > 0) {
                  query.pageSize(limit);
              }
          }
  
          Expression parsedExp = qualifierParser.parse(entity.getExp());
          Expression finalExp = qualifierPostProcessor.process(entity.getAgEntity().getName(), parsedExp);
          query.where(finalExp);
  
          for (Sort o : entity.getOrderings()) {
              query.orderBy(toOrdering(entity, o));
          }
  
          return query;
      }
  
      protected Expression buildIdQualifier(AgEntity<?> entity, AgObjectId id) {
  
          Collection<AgIdPart> idAttributes = entity.getIdParts();
          if (idAttributes.size() != id.size()) {
              throw AgException.badRequest("Wrong ID size: expected %s, got: %s", idAttributes.size(), id.size());
          }
  
          Collection<Expression> qualifiers = new ArrayList<>();
          for (AgIdPart idAttribute : idAttributes) {
              Object idValue = id.get(idAttribute.getName());
              if (idValue == null) {
                  throw AgException.badRequest(
                          "Failed to build a Cayenne qualifier for entity %s: one of the entity's ID parts is missing in this ID: %s",
                          entity.getName(),
                          idAttribute.getName());
              }
  
              ASTPath path = pathResolver.resolve(entity.getName(), idAttribute.getName()).getPathExp();
              qualifiers.add(ExpressionFactory.matchExp(path, idValue));
          }
          return ExpressionFactory.and(qualifiers);
      }
  
      protected Ordering toOrdering(ResourceEntity<?> entity, Sort sort) {
          return new Ordering(
                  pathResolver.resolve(entity.getAgEntity().getName(), sort.getPath()).getPathExp(),
                  toSortOrder(sort.getDirection()));
      }
  
      private SortOrder toSortOrder(Direction direction) {
          switch (direction) {
              case asc:
                  return SortOrder.ASCENDING;
              case asc_ci:
                  return SortOrder.ASCENDING_INSENSITIVE;
              case desc_ci:
                  return SortOrder.DESCENDING_INSENSITIVE;
              case desc:
                  return SortOrder.DESCENDING;
              default:
                  throw new IllegalArgumentException("Missing or unexpected sort direction: " + direction);
          }
      }
  }
  