All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.devonfw.module.jpa.dataaccess.api.QueryHelper Maven / Gradle / Ivy

Go to download

JPA-based persistence infrastructure of the Open Application Standard Platform for Java (devon4j).

There is a newer version: 2023.01.001
Show newest version
package com.devonfw.module.jpa.dataaccess.api;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.util.StringUtils;

import com.devonfw.module.basic.common.api.query.LikePatternSyntax;
import com.devonfw.module.basic.common.api.query.StringSearchConfigTo;
import com.devonfw.module.basic.common.api.query.StringSearchOperator;
import com.querydsl.core.JoinExpression;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.ComparablePath;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.core.types.dsl.SimpleExpression;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQuery;

/**
 * Class with utility methods for dealing with queries. Either extend this class or use {@link QueryUtil#get()}.
 *
 * @since 3.0.0
 */
public class QueryHelper {

  private static final Logger LOG = LoggerFactory.getLogger(QueryHelper.class);

  /** JPA query property to configure the timeout in milliseconds. */
  protected static final String QUERY_PROPERTY_TIMEOUT = "javax.persistence.query.timeout";

  /**
   * @param query the {@link JPAQuery} to modify.
   * @param timeout the timeout in milliseconds.
   */
  protected void applyTimeout(JPAQuery query, Number timeout) {

    if (timeout != null) {
      query.setHint(QUERY_PROPERTY_TIMEOUT, timeout.intValue());
    }
  }

  /**
   * @param query the {@link JPAQuery} to modify.
   * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause}
   *        from.
   * @param pattern the pattern for the LIKE-clause to create.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   */
  protected void whereLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax,
      boolean ignoreCase, boolean matchSubstring) {

    BooleanExpression like = newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, false);
    if (like != null) {
      query.where(like);
    }
  }

  /**
   * @param query the {@link JPAQuery} to modify.
   * @param expression the {@link StringExpression} to {@link StringExpression#notLike(String) create the NOT
   *        LIKE-clause} from.
   * @param pattern the pattern for the NOT LIKE-clause to create.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   */
  protected void whereNotLike(JPAQuery query, StringExpression expression, String pattern, LikePatternSyntax syntax,
      boolean ignoreCase, boolean matchSubstring) {

    BooleanExpression like = newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, true);
    if (like != null) {
      query.where(like);
    }
  }

  /**
   * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause}
   *        from.
   * @param pattern the pattern for the LIKE-clause to create.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   * @return the LIKE-clause as {@link BooleanExpression}.
   */
  protected BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax,
      boolean ignoreCase, boolean matchSubstring) {

    return newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, false);
  }

  /**
   * @param expression the {@link StringExpression} to {@link StringExpression#notLike(String) create the NOT
   *        LIKE-clause} from.
   * @param pattern the pattern for the NOT LIKE-clause to create.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   * @return the NOT LIKE-clause as {@link BooleanExpression}.
   */
  protected BooleanExpression newNotLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax,
      boolean ignoreCase, boolean matchSubstring) {

    return newLikeClause(expression, pattern, syntax, ignoreCase, matchSubstring, true);
  }

  /**
   * @param expression the {@link StringExpression} to {@link StringExpression#like(String) create the LIKE-clause}
   *        from.
   * @param pattern the pattern for the LIKE-clause to create.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   * @param negate - {@code true} for {@link StringExpression#notLike(String) NOT LIKE}, {@code false} for
   *        {@link StringExpression#like(String) LIKE}.
   * @return the LIKE-clause as {@link BooleanExpression}.
   */
  protected BooleanExpression newLikeClause(StringExpression expression, String pattern, LikePatternSyntax syntax,
      boolean ignoreCase, boolean matchSubstring, boolean negate) {

    if (syntax == null) {
      syntax = LikePatternSyntax.autoDetect(pattern);
      if (syntax == null) {
        syntax = LikePatternSyntax.SQL;
      }
    }
    String likePattern = LikePatternSyntax.SQL.convert(pattern, syntax, matchSubstring);
    StringExpression exp = expression;
    if (ignoreCase) {
      likePattern = likePattern.toUpperCase(Locale.US);
      exp = exp.upper();
    }
    BooleanExpression clause;
    if (syntax != LikePatternSyntax.SQL) {
      clause = exp.like(likePattern, LikePatternSyntax.ESCAPE);
    } else {
      clause = exp.like(likePattern);
    }
    if (negate) {
      clause = clause.not();
    }
    return clause;
  }

  /**
   * @param expression the {@link StringExpression} to search on.
   * @param value the string value or pattern to search for.
   * @param config the {@link StringSearchConfigTo} to configure the search. May be {@code null} for regular equals
   *        search as default fallback.
   * @return the new {@link BooleanExpression} for the specified string comparison clause.
   */
  protected BooleanExpression newStringClause(StringExpression expression, String value, StringSearchConfigTo config) {

    StringSearchOperator operator = StringSearchOperator.EQ;
    LikePatternSyntax syntax = null;
    boolean ignoreCase = false;
    boolean matchSubstring = false;
    if (config != null) {
      operator = config.getOperator();
      syntax = config.getLikeSyntax();
      ignoreCase = config.isIgnoreCase();
      matchSubstring = config.isMatchSubstring();
    }
    return newStringClause(expression, value, operator, syntax, ignoreCase, matchSubstring);
  }

  /**
   * @param expression the {@link StringExpression} to search on.
   * @param value the string value or pattern to search for.
   * @param syntax the {@link LikePatternSyntax} of the given {@code pattern}.
   * @param operator the {@link StringSearchOperator} used to compare the search string {@code value}.
   * @param ignoreCase - {@code true} to ignore the case, {@code false} otherwise (to search case-sensitive).
   * @param matchSubstring - {@code true} to match also if the given {@code pattern} shall also match substrings on the
   *        given {@link StringExpression}.
   * @return the new {@link BooleanExpression} for the specified string comparison clause.
   */
  protected BooleanExpression newStringClause(StringExpression expression, String value, StringSearchOperator operator,
      LikePatternSyntax syntax, boolean ignoreCase, boolean matchSubstring) {

    if (operator == null) {
      if (syntax == null) {
        syntax = LikePatternSyntax.autoDetect(value);
        if (syntax == null) {
          operator = StringSearchOperator.EQ;
        } else {
          operator = StringSearchOperator.LIKE;
        }
      } else {
        operator = StringSearchOperator.LIKE;
      }
    }
    if (matchSubstring && ((operator == StringSearchOperator.EQ) || (operator == StringSearchOperator.NE))) {
      if (syntax == null) {
        syntax = LikePatternSyntax.SQL;
      }
      if (operator == StringSearchOperator.EQ) {
        operator = StringSearchOperator.LIKE;
      } else {
        operator = StringSearchOperator.NOT_LIKE;
      }
    }
    String v = value;
    if (v == null) {
      switch (operator) {
        case LIKE:
        case EQ:
          return expression.isNull();
        case NE:
          return expression.isNotNull();
        default:
          throw new IllegalArgumentException("Operator " + operator + " does not accept null!");
      }
    } else if (v.isEmpty()) {
      switch (operator) {
        case LIKE:
        case EQ:
          return expression.isEmpty();
        case NOT_LIKE:
        case NE:
          return expression.isNotEmpty();
        default:
          // continue;
      }
    }
    StringExpression exp = expression;
    if (ignoreCase) {
      v = v.toUpperCase(Locale.US);
      exp = exp.upper();
    }
    switch (operator) {
      case LIKE:
        return newLikeClause(exp, v, syntax, false, matchSubstring, false);
      case NOT_LIKE:
        return newLikeClause(exp, v, syntax, false, matchSubstring, true);
      case EQ:
        return exp.eq(v);
      case NE:
        return exp.ne(v);
      case LT:
        return exp.lt(v);
      case LE:
        return exp.loe(v);
      case GT:
        return exp.gt(v);
      case GE:
        return exp.goe(v);
      default:
        throw new IllegalStateException("" + operator);
    }
  }

  /**
   * @param query the {@link JPAQuery} to modify.
   * @param expression the {@link StringExpression} to search on.
   * @param value the string value or pattern to search for.
   * @param config the {@link StringSearchConfigTo} to configure the search. May be {@code null} for regular equals
   *        search.
   */
  protected void whereString(JPAQuery query, StringExpression expression, String value,
      StringSearchConfigTo config) {

    BooleanExpression clause = newStringClause(expression, value, config);
    if (clause != null) {
      query.where(clause);
    }
  }

  /**
   * @param  generic type of the {@link Collection} values for the {@link SimpleExpression#in(Collection)
   *        IN-clause}(s).
   * @param expression the {@link SimpleExpression} used to create the {@link SimpleExpression#in(Collection)
   *        IN-clause}(s).
   * @param inValues the {@link Collection} of values for the {@link SimpleExpression#in(Collection) IN-clause}(s).
   * @return the {@link BooleanExpression} for the {@link SimpleExpression#in(Collection) IN-clause}(s) oder
   *         {@code null} if {@code inValues} is {@code null} or {@link Collection#isEmpty() empty}.
   */
  protected  BooleanExpression newInClause(SimpleExpression expression, Collection inValues) {

    if ((inValues == null) || (inValues.isEmpty())) {
      return null;
    }
    int size = inValues.size();
    if (size == 1) {
      return expression.eq(inValues.iterator().next());
    }
    int maxSizeOfInClause = DatabaseConfigProperties.getInstance().getMaxSizeOfInClause();
    if (size <= maxSizeOfInClause) {
      return expression.in(inValues);
    }
    LOG.warn("Creating workaround for IN-clause with {} items - this can cause performance problems.", size);
    List values;
    if (inValues instanceof List) {
      values = (List) inValues;
    } else {
      values = new ArrayList<>(inValues);
    }
    BooleanExpression predicate = null;
    int start = 0;
    while (start < size) {
      int end = start + maxSizeOfInClause;
      if (end > size) {
        end = size;
      }
      List partition = values.subList(start, end);
      if (predicate == null) {
        predicate = expression.in(partition);
      } else {
        predicate = predicate.or(expression.in(partition));
      }
      start = end;
    }
    return predicate;
  }

  /**
   * @param  generic type of the {@link Collection} values for the {@link SimpleExpression#in(Collection)
   *        IN-clause}(s).
   * @param query the {@link JPAQuery} where to {@link JPAQuery#where(com.querydsl.core.types.Predicate) add} the
   *        {@link #newInClause(SimpleExpression, Collection) IN-clause(s)} from the other given parameters.
   * @param expression the {@link SimpleExpression} used to create the {@link SimpleExpression#in(Collection)
   *        IN-clause}(s).
   * @param inValues the {@link Collection} of values for the {@link SimpleExpression#in(Collection) IN-clause}(s).
   * @see #newInClause(SimpleExpression, Collection)
   */
  protected  void whereIn(JPAQuery query, SimpleExpression expression, Collection inValues) {

    BooleanExpression inClause = newInClause(expression, inValues);
    if (inClause != null) {
      query.where(inClause);
    }
  }

  /**
   * Returns a {@link Page} of entities according to the supplied {@link Pageable} and {@link JPAQuery}.
   *
   * @param  generic type of the entity.
   * @param pageable contains information about the requested page and sorting.
   * @param query is a query which is pre-configured with the desired conditions for the search.
   * @param determineTotal - {@code true} to determine the {@link Page#getTotalElements() total number of hits},
   *        {@code false} otherwise.
   * @return a paginated list.
   */
  protected  Page findPaginatedGeneric(Pageable pageable, JPAQuery query, boolean determineTotal) {

    long total = -1;
    if (determineTotal) {
      total = query.clone().fetchCount();
    }
    long offset = 0;
    if (pageable != null) {
      offset = pageable.getOffset();
      query.offset(offset);
      query.limit(pageable.getPageSize());
      applySort(query, pageable.getSort());
    }
    List hits = query.fetch();
    if (total == -1) {
      total = offset + hits.size();
    }
    return new PageImpl<>(hits, pageable, total);
  }

  /**
   * @param query the {@link JPAQuery} to apply the {@link Sort} to.
   * @param sort the {@link Sort} to apply as ORDER BY to the given {@link JPAQuery}.
   */
  @SuppressWarnings("rawtypes")
  protected void applySort(JPAQuery query, Sort sort) {

    if (sort == null) {
      return;
    }
    PathBuilder alias = findAlias(query);
    for (Order order : sort) {
      String property = order.getProperty();
      Direction direction = order.getDirection();
      ComparablePath path = alias.getComparable(property, Comparable.class);
      OrderSpecifier orderSpecifier;
      if (direction == Direction.ASC) {
        orderSpecifier = path.asc();
      } else {
        orderSpecifier = path.desc();
      }
      query.orderBy(orderSpecifier);
    }
  }

  private  PathBuilder findAlias(JPAQuery query) {

    String alias = null;
    List joins = query.getMetadata().getJoins();
    if ((joins != null) && !joins.isEmpty()) {
      JoinExpression join = joins.get(0);
      Expression target = join.getTarget();
      if (target instanceof EntityPath) {
        alias = target.toString(); // no safe API
      }
    }
    Class type = query.getType();
    if (alias == null) {
      // should actually never happen, but fallback is provided as buest guess
      alias = StringUtils.uncapitalize(type.getSimpleName());
    }
    return new PathBuilder<>(type, alias);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy