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
   *
   *        http://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.causeway.persistence.querydsl.metamodel.facets;
  
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Optional;
  import java.util.function.Function;
  
  import com.querydsl.core.BooleanBuilder;
  import com.querydsl.core.types.OrderSpecifier;
  import com.querydsl.core.types.Predicate;
  import com.querydsl.core.types.dsl.PathBuilder;
  
  import org.apache.causeway.applib.annotation.DomainObject;
  import org.apache.causeway.applib.annotation.Property;
  import org.apache.causeway.applib.exceptions.RecoverableException;
  import org.apache.causeway.persistence.querydsl.applib.query.DslQuery;
  import org.apache.causeway.persistence.querydsl.applib.services.support.QueryDslSupport;
  import org.apache.causeway.persistence.querydsl.applib.util.CaseSensitivity;
  import org.apache.causeway.persistence.querydsl.applib.util.DslExpressions;
  import org.apache.causeway.persistence.querydsl.applib.util.Wildcards;
  
  import lombok.AccessLevel;
  import lombok.Builder;
  import lombok.Getter;
  import org.jspecify.annotations.NonNull;
  
  /**
   * Dynamically generate an auto complete query on runtime using Query DSL.
   * Auto complete operates on fields of String type ONLY.
   * The autoComplete method ALWAYS applies wildcards when NONE are specified in de given search string, executeQuery does NOT.
   */
  @Builder(access = AccessLevel.PUBLIC)
  @Getter
  public class AutoCompleteGeneratedDslQuery {
  
      /**
       * Query DSL is used to dynamically generate the query on runtime
       */
      final @NonNull protected QueryDslSupport queryDslSupport;
      /**
       * the entity for which to generate the auto complete query
       */
      final @NonNull protected Class<?> entity;
  
      record SearchableProperty(
              String propertyId,
              Property.QueryDslAutoCompletePolicy queryDslAutoCompletePolicy) {
  
          @Override
          public String toString() {
              return propertyId + " (" + queryDslAutoCompletePolicy.getDescription() + ")";
          }
  
          public CaseSensitivity getCaseSensitivity() {
              return CaseSensitivity.of(this.queryDslAutoCompletePolicy.isIgnoreCase());
          }
      }
      /**
       * The properties to use in the generated query (using <code>OR</code>).
       */
      final @NonNull protected List<SearchableProperty> searchableProperties;
      /**
       * Add additional criteria that can be added to the autocomplete method in form:
       * <pre>
       * public Function<EntityPathBase<T>, Predicate> autoCompletePredicate(String search)
       * </pre>
       */
      final protected Object repository;
      final protected Method predicateMethod;
  
      @Builder.Default
      protected Integer minLength = DomainObject.QueryDslAutoCompleteConstants.MIN_LENGTH;
  
      @Builder.Default
      protected Integer limitResults = DomainObject.QueryDslAutoCompleteConstants.LIMIT_RESULTS;
  
      /**
       * Dynamically generate an auto complete query on runtime using Query DSL.
       * Auto complete operates on fields of String type ONLY.
       * The autoComplete method ALWAYS applies wildcards when NONE are specified in de given search string.
       */
      public <T> List<T> autoComplete(
              final String searchPhrase,
              final Function<PathBuilder<T>, Predicate> additionalPredicate) {
  
          Function<PathBuilder<T>, Predicate> predicate = additionalPredicate;
  
          if (additionalPredicate == null && predicateMethod != null && repository != null) {
              // Add optional additional predicate from repository
              try {
                  predicate = (Function<PathBuilder<T>, Predicate>) predicateMethod.invoke(repository, searchPhrase);
              } catch (IllegalAccessException | InvocationTargetException e) {
                  throw new RuntimeException(e);
              }
          }
  
          return executeQuery(Wildcards.toAnsiSqlWildcard(searchPhrase), predicate);
      }
  
      /**
       * Dynamically generate an auto complete query on runtime using Query DSL.
       * Auto complete operates on fields of String type ONLY.
       * The executeQuery method NEVER applies wildcards when NONE are specified in de given search string.
       */
      public <T> List<T> executeQuery(
              final String searchPhrase,
              final Function<PathBuilder<T>, Predicate> additionalPredicate) {
          var dslQueryIfAny = generateQuery(searchPhrase, additionalPredicate);
          return dslQueryIfAny.map(query -> query.fetch()).orElse(newList());
      }
  
      public <T> Optional<DslQuery> generateQuery(
              final String searchPhrase,
              final Function<PathBuilder<T>, Predicate> additionalPredicate) {
  
          if (searchableProperties.isEmpty()) {
              // not expected
              throw new RecoverableException("At least one searchable property should be specified");
          }
  
          if (isNotEmpty(searchPhrase) && searchPhrase.trim().length() >= getMinLength()) {
  
              // define entity
              PathBuilder<T> entityPath = new PathBuilder(entity, "e");
              BooleanBuilder where = new BooleanBuilder();
              List<OrderSpecifier<String>> orderSpecifiers = newList();
  
              // Build where and order clause
              searchableProperties.forEach(se -> {
  
                  // Only string type fields are supported
                  var propertyPath = entityPath.getString(se.propertyId());
                  var searchReplaced = Wildcards.toAnsiSqlWildcard(searchPhrase);
  
                  var expr = DslExpressions.search(propertyPath, searchReplaced, se.getCaseSensitivity());
                  where.or(expr);
  
                  // Build order by clause
                  orderSpecifiers.add(propertyPath.asc());
              });
  
              // Build query
              var dslQuery = queryDslSupport.selectFrom(entityPath);
  
              // add additional expression if any
              if(additionalPredicate!=null){
                  where.and(additionalPredicate.apply(entityPath));
              }
  
              dslQuery.where(where);
              dslQuery.orderBy(orderSpecifiers.toArray(new OrderSpecifier[0]));
              dslQuery.limit(limitResults==null ? DomainObject.QueryDslAutoCompleteConstants.LIMIT_RESULTS : limitResults);
  
              return Optional.of(dslQuery);
          }
          return Optional.empty();
      }
  
      static <T> List<T> newList(final T... objs) {
          return newArrayList(objs);
      }
  
      static <T> ArrayList<T> newArrayList(final T... objs) {
          ArrayList<T> result = new ArrayList();
          Collections.addAll(result, objs);
          return result;
      }
  
      static boolean isNotEmpty(final CharSequence cs) {
          return !isEmpty(cs);
      }
  
      static boolean isEmpty(final CharSequence cs) {
          return cs == null || cs.length() == 0;
      }
  
  }
  