Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CriteriaQueries #399

Merged
merged 2 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@
<guava.version>30.0-jre</guava.version>

<!-- Versions for provided dependencies -->
<byte-buddy.version>1.10.15</byte-buddy.version>
<commons-io.version>2.8.0</commons-io.version>
<commons-text.version>1.9</commons-text.version>
<dropwizard.version>2.0.14</dropwizard.version>
<dropwizard.modules.dropwizard-jdbi.version>2.0.12</dropwizard.modules.dropwizard-jdbi.version>
<FastInfoset.version>1.2.18</FastInfoset.version>
<hibernate.version>5.4.22.Final</hibernate.version>
<hibernate-validator.version>6.1.6.Final</hibernate-validator.version>
<httpclient.version>4.5.13</httpclient.version>
<guava-retrying.version>2.0.0</guava-retrying.version>
Expand All @@ -54,6 +57,7 @@
<postgresql.version>42.2.12</postgresql.version>
<spring.version>5.3.0</spring.version>
<stax2-api.version>4.2.1</stax2-api.version>
<stax-ex.version>1.8.3</stax-ex.version>
<!--
NOTE:
There is a conflict between jackson-dataformat-xml and jaxws-rt in the
Expand Down Expand Up @@ -115,6 +119,13 @@

<!-- provided dependencies -->

<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
Expand Down Expand Up @@ -236,6 +247,13 @@
</exclusions>
</dependency>

<dependency>
<groupId>com.sun.xml.fastinfoset</groupId>
<artifactId>FastInfoset</artifactId>
<version>${FastInfoset.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
Expand All @@ -253,6 +271,31 @@
</exclusions>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.xml.fastinfoset</groupId>
<artifactId>FastInfoset</artifactId>
</exclusion>
<exclusion>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</exclusion>
<exclusion>
<groupId>org.jvnet.staxex</groupId>
<artifactId>stax-ex</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
Expand Down Expand Up @@ -440,6 +483,13 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.jvnet.staxex</groupId>
<artifactId>stax-ex</artifactId>
<version>${stax-ex.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>
Expand Down
166 changes: 166 additions & 0 deletions src/main/java/org/kiwiproject/hibernate/CriteriaQueries.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package org.kiwiproject.hibernate;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.kiwiproject.base.KiwiStrings.splitWithTrimAndOmitEmpty;
import static org.kiwiproject.collect.KiwiLists.first;
import static org.kiwiproject.collect.KiwiLists.second;

import com.google.common.annotations.VisibleForTesting;
import lombok.experimental.UtilityClass;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.Session;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Order;

import java.util.List;

/**
* Utility class for creating Hibernate {@link Criteria} queries.
*
* @implNote Suppressing all IntelliJ and Sonar deprecation warnings. We are aware that the Hibernate Criteria API
* is deprecated.
*/
@UtilityClass
@SuppressWarnings({"java:S1874", "deprecation"})
public class CriteriaQueries {

private static final String ASC = "asc";
private static final String DESC = "desc";
private static final int ORDER_SPEC_SIZE_WITH_ORDER = 2;
private static final int ORDER_SPEC_SIZE_WITHOUT_ORDER = 1;
private static final String INVALID_ORDER_SPEC_MESSAGE_TEMPLATE =
"'%s' is not a valid order specification. Must contain a property" +
" name optionally followed by (case-insensitive) asc or desc";

/**
* Creates a {@link Criteria} query for the specified persistent class, with the specified ordering,
* and setting {@link FetchMode#JOIN} on the specified {@code fetchAssociations}.
*
* @param session the Hibernate session
* @param persistentClass the class for which to create a criteria query
* @param orderClause the order specification
* @param fetchAssociations one or more associations to fetch via a join
* @return a {@code Criteria} which you can build upon
* @see #addOrder(Criteria, String)
* @see #distinctCriteriaWithFetchAssociations(org.hibernate.Session, Class, String...)
*/
public static Criteria distinctCriteria(Session session,
Class<?> persistentClass,
String orderClause,
String... fetchAssociations) {

var criteria = distinctCriteriaWithOrder(session, persistentClass, orderClause);
addFetchAssociations(criteria, fetchAssociations);
return criteria;
}

/**
* Creates a {@link Criteria} query for the specified persistent class and the specified HQL order clause.
*
* @param session the Hibernate session
* @param persistentClass the class for which to create a criteria query
* @param orderClause the order specification
* @return a {code Criteria} which you can build upon
* @see #addOrder(Criteria, String)
*/
public static Criteria distinctCriteriaWithOrder(Session session, Class<?> persistentClass, String orderClause) {
var criteria = distinctCriteria(session, persistentClass);
return addOrder(criteria, orderClause);
}

/**
* Creates a {@link Criteria} query for the specified persistent class and setting
* {@link FetchMode#JOIN} on the specified {@code fetchAssociations}.
*
* @param session the Hibernate session
* @param persistentClass the class for which to create a criteria query
* @param fetchAssociations one or more associations to fetch via a join
* @return a {@code Criteria} which you can build upon
*/
public static Criteria distinctCriteriaWithFetchAssociations(Session session,
Class<?> persistentClass,
String... fetchAssociations) {

var criteria = distinctCriteria(session, persistentClass);
addFetchAssociations(criteria, fetchAssociations);
return criteria;
}

/**
* Creates a {@link Criteria} query for the specified persistent class.
*
* @param session the Hibernate session
* @param persistentClass the class for which to create a criteria query
* @return a {@code Criteria} which you can build upon
*/
public static Criteria distinctCriteria(Session session, Class<?> persistentClass) {
return session.createCriteria(persistentClass)
.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
}

private static void addFetchAssociations(Criteria criteria, String... fetchAssociations) {
for (var association : fetchAssociations) {
criteria.setFetchMode(association, FetchMode.JOIN);
}
}

/**
* Adds the specified order clause to an existing {@link Criteria}, returning the criteria that was passed in.
* <p>
* The {@code orderClause} should contain a comma-separated list of properties optionally followed by an order
* designator, which must be {@code asc} or {@code desc}. If neither ascending nor descending is specified,
* ascending order is used. For example, the order clause for listing people by descending date of birth, then
* ascending last name, and finally ascending first name is:
* <pre>
* dateOfBirth desc, lastName, firstName
* </pre>
*
* @param criteria an existing {@code Criteria} to add ordering to
* @param orderClause the order specification
* @return the <em>same</em> {@code criteria} instance passed in, to allow building upon it
*/
public static Criteria addOrder(Criteria criteria, String orderClause) {
if (isBlank(orderClause)) {
return criteria;
}

splitToList(orderClause, ',')
.stream()
.map(CriteriaQueries::toOrderFromPropertyOrderClause)
.forEach(criteria::addOrder);

return criteria;
}

@VisibleForTesting
static Order toOrderFromPropertyOrderClause(String propertyOrdering) {
var orderSpec = splitToList(propertyOrdering, ' ');
validateOrderSpecification(orderSpec, propertyOrdering);

var propertyName = first(orderSpec);
var isAscending = orderSpec.size() < ORDER_SPEC_SIZE_WITH_ORDER || ASC.equalsIgnoreCase(second(orderSpec));

return isAscending ? Order.asc(propertyName) : Order.desc(propertyName);
}

private static void validateOrderSpecification(List<String> orderSpec, String rawPropertyOrder) {
checkArgument(
orderSpec.size() == ORDER_SPEC_SIZE_WITHOUT_ORDER || orderSpec.size() == ORDER_SPEC_SIZE_WITH_ORDER,
INVALID_ORDER_SPEC_MESSAGE_TEMPLATE, rawPropertyOrder);

if (orderSpec.size() == ORDER_SPEC_SIZE_WITH_ORDER) {
var order = second(orderSpec);
checkArgument(ASC.equalsIgnoreCase(order) || DESC.equalsIgnoreCase(order),
"'%s' is not a valid order. Property order must be either %s or %s (case-insensitive)",
order, ASC, DESC);
}
}

private static List<String> splitToList(String value, char separator) {
return newArrayList(splitWithTrimAndOmitEmpty(value, separator));
}

}
Loading