diff --git a/README.md b/README.md index b0d9482..6a37f14 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ ## Description Provides a variety of components that reduce the overhead of composing and maintaining Specifications. -SpecificationFactory is used to generate Specification instances. It encapsulates anonymous Specification subclasses and provides null-safe handling. +[SpecificationFactory ](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactory.java)is used to generate Specification instances. It encapsulates anonymous Specification subclasses and provides null-safe handling. -SpecificationBuilder puts a fluent API on top of SpecificationFactory to compose compound Specifications with ease. +[SpecificationBuilder](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilder.java) puts a fluent API on top of SpecificationFactory to compose compound Specifications with ease. -SpecificationUtil is used by SpecificationFactory to assist with null checking, wildcard detection and String conversions, etc. +[SpecificationUtil](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtil.java) is used by SpecificationFactory to assist with null checking, wildcard detection and String conversions, etc. -The Specifications Annotation is available as a convenience, an alias of Spring's Component Annotation to mark Specification Beans as a distinct type. +The [Specifications Annotation](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/main/java/io/github/quinnandrews/spring/data/specification/annotations/Specifications.java) is available as a convenience, an alias of Spring's Component Annotation to mark Specification Beans as a particular kind of Bean. SpecificationFactory and SpecificationUtil may be used independently, if desired. However, the intent is to use SpecificationBuilder exclusively, without being aware of either SpecificationFactory or SpecificationUtil, but it is not mandatory. Both SpecificationFactory and SpecificationUtil are declared with public access. @@ -17,8 +17,8 @@ SpecificationFactory and SpecificationUtil may be used independently, if desired - Built-in null handling makes conditional query composition simple and easy – no need to wrap Specification conjunctions with a check for parameter state when parameters are optional. - Built-in support for efficient eager fetching provides query optimization – an entire Aggregate can be loaded with one query instead of many. -- A fluent API encapsulating boilerplate code makes queries that are both strongly typed and easy to read – the risk of error is reduced while comprehension of query logic is enhanced. -- A `@Specifications` Annotation complements Spring's `@Controller`, `@Service` and `@Repository` Annotations, allowing one to declare Specification Beans in a similar fashion and being able to identify these kinds of Beans for special use cases, like when defining rules with ArchUnit, for example. +- A fluent API encapsulating boilerplate code makes queries that are both strongly typed and easy to read – the risk of error is reduced while query logic is more coherent. +- A `@Specifications` Annotation complements Spring's `@Controller`, `@Service` and `@Repository` Annotations – Specification Beans can be identified as a special kind of Bean by both developers and processes (like the execution of rules with [ArchUnit](https://github.com/TNG/ArchUnit), for example). ## Requirements ### Java 17 @@ -26,12 +26,23 @@ https://adoptium.net/temurin/releases/?version=17 ### Hibernate JPA Metamodel Generator (or equivalent) https://hibernate.org/orm/tooling/ + +Add the Hibernate JPA Metamodel Generator to the Maven Compiler Plugin as an Annotation Processor: ```xml - - org.hibernate.orm - hibernate-jpamodelgen - 6.3.1.Final - + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.hibernate.orm + hibernate-jpamodelgen + 6.3.1.Final + + + + ``` ## Dependencies @@ -44,12 +55,12 @@ Add this project's artifact to your project as a dependency: io.github.quinnandrews spring-data-specification-builder - 1.0.0 + 2.0.0 ``` (NOTE: This project's artifact is NOT yet available in Maven Central, but is available from GitHub Packages.) -Then extend your Repository Interfaces with JPASpecificationExecutor: +Then extend your Repository Interfaces with JPASpecificationExecutor so that Specification methods are available: ```java import io.github.quinnandrews.spring.data.specification.builder.application.data.guitarpedals.GuitarPedal; import org.springframework.data.jpa.repository.JpaRepository; @@ -61,7 +72,7 @@ public interface GuitarPedalRepository extends JpaRepository, JpaSpecificationExecutor { } ``` -Next, define your Specifications: +Next, define your Specifications in a Specifications Bean (this is optional – you can also define Specification queries where they are used, in a Service, a Test, etc.) ```java import io.github.quinnandrews.spring.data.specification.annotations.Specifications; @@ -111,26 +122,26 @@ public class GuitarPedalService { ``` ## Examples -The examples and tests use the Domain of Guitar Pedals (I'm also a musician and I enjoy exploring the vast array of sounds offered by the world of guitar pedals). It was simply more fun than using the Domains of TODOs or Employees, for example. +The examples and tests use the Domain of Guitar Pedals (I'm a musician). It was simply more fun than using the Domains of TODOs or Employees. ### GuitarPedalSpecifications.class -GuitarPedalSpecifications contains the most comprehensive set of examples. It compares defining the same queries with and without the SpecificationBuilder, details gotchas and goes into more complex things like working with collections, for instance. Begin from the top and work your way down. +[GuitarPedalSpecifications](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/application/data/guitarpedals/specifications/GuitarPedalSpecifications.java) contains the most comprehensive set of examples. It compares defining the same queries with and without the SpecificationBuilder, details gotchas and goes into more complex things like working with collections, for instance. Begin from the top and work your way down. ### GuitarPedalSpecificationsIntegrationTest.class -GuitarPedalSpecificationsIntegrationTest contains the Integrations Tests of the examples in GuitarPedalSpecifications. This may be useful to look at as well, or to run the examples yourself and see the sql output with your own eyes. +[GuitarPedalSpecificationsIntegrationTest](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/GuitarPedalSpecificationsIntegrationTest.java) contains the Integrations Tests of the examples in GuitarPedalSpecifications. This may be useful to look at as well, or to run the examples yourself and see the sql output with your own eyes. ### SpecificationBuilderIntegrationTest.class -SpecificationBuilderIntegrationTest contains Integration Tests for the methods in SpecificationBuilder. Some of these tests cover cases that are not included in GuitarPedalSpecifications. +[SpecificationBuilderIntegrationTest](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilderIntegrationTest.java) contains Integration Tests for the methods in SpecificationBuilder. Some of these tests cover cases that are not included in GuitarPedalSpecifications. ### Other Test Classes -SpecificationBuilderTest, SpecificationFactoryTest and SpecificationUtilTest contain Unit Tests for the methods in their corresponding Classes. These may useful to look at as well, in order to understand more about how things work under the hood, but it is not necessary. +[SpecificationBuilderTest](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilderTest.java), [SpecificationFactoryTest](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactoryTest.java) and [SpecificationUtilTest](https://github.com/quinnandrews/spring-data-specification-builder/blob/a93b9a84805d3c20b1461ca634abd3a50695d245/src/test/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtilTest.java) contain Unit Tests for the methods in their corresponding Classes. These may be useful to look at as well, in order to understand more about how things work under the hood, but it is not necessary. ## Roadmap -1) ~~**Add `and()` Methods in the Builder**
-Add `and()` Methods to make the fluent-API more fluent & legible, and to better resemble the Specification Interface.~~ -2) **Build Specifications on Associations**
+1) **Build Specifications on Associations**
Add versions of `where` methods that operate on Associations. It is expected the builder will need to maintain an instance variable containing Joins already created, so that they can be re-used during the build process if there is more than one Specification to apply to an Association. -3) **Define JoinType of Associations**
-Add versions of `withFetch()` that allow definition of JoinType. Should it be applied to `where` methods on Associations as well? -4) **Consider Adding a `not()` Method in the Builder** -5) **Consider Adding a `clear()` Method in the Builder** +2) **Define JoinType of Associations**
+Add versions of `fetchOf()` that allow definition of JoinType. (Should it be applied to `where` methods on Associations as well?) +3) **Add a `not()` Method in the Builder** +4) **Add a `clear()` Method in the Builder** +5) **Implement a SortBuilder to complement the SpecificationBuilder**
+Implement with the same sort of fluent-api and require Attributes instead of Strings for type safety. diff --git a/src/main/java/io/github/quinnandrews/spring/data/specification/annotations/Specifications.java b/src/main/java/io/github/quinnandrews/spring/data/specification/annotations/Specifications.java index 18165b5..670db29 100644 --- a/src/main/java/io/github/quinnandrews/spring/data/specification/annotations/Specifications.java +++ b/src/main/java/io/github/quinnandrews/spring/data/specification/annotations/Specifications.java @@ -5,6 +5,19 @@ import java.lang.annotation.*; +/** + * An alias of {@link Component @Component} indicating that the + * annotated class contains Specifications. Complements + * {@link org.springframework.stereotype.Controller @Controller}, + * {@link org.springframework.stereotype.Service @Service}, and + * {@link org.springframework.stereotype.Repository @Repository}. + * + * @author Quinn Andrews + * @see Component + * @see org.springframework.stereotype.Controller + * @see org.springframework.stereotype.Service + * @see org.springframework.stereotype.Repository + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilder.java b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilder.java index 7576d4d..7fbdd65 100644 --- a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilder.java +++ b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationBuilder.java @@ -10,7 +10,7 @@ /** * Generates and composes Specifications with a fluent-API that is easy to read. * - * @param The Entity Type to query on as an Aggregate Root. + * @param The Entity Type to query from as the Aggregate Root. * * @author Quinn Andrews */ @@ -18,23 +18,64 @@ public class SpecificationBuilder { private Specification specification; - public SpecificationBuilder() { + /** + * Default Constructor. Private since this Class is meant + * to be instantiated with the from(final Class root) + * method. + */ + private SpecificationBuilder() { // no-op } + /** + * Returns a new instance of SpecificationBuilder with the + * given root as the Aggregate Root of the Specification. + * + * @param root The Entity Class to query from as the + * Aggregate Root. + * @return A new instance of SpecificationBuilder + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given root is null. + */ public static SpecificationBuilder from(final Class root) { Objects.requireNonNull(root, "Argument 'root' cannot be null."); return new SpecificationBuilder<>(); } + /** + * Returns the underlying composite Specification in + * its current state. WARNING: Can be null under some + * circumstances. + * + * @return The underlying composite Specification that + * represents the result of the build. + */ public Specification toSpecification() { return specification; } + /** + * Simply returns the current instance of the + * SpecificationBuilder. Used to maintain fluency + * of the code, to better resemble SQL and the + * way the underlying query is spoken. + * + * @return The current instance of the SpecificationBuilder. + */ public SpecificationBuilder where() { return this; } + /** + * Adds the given Specification to the current Specification + * with a conjunction (unless no Specification has yet been + * added) using the language of 'where'. + * + * @param specification The Specification to add the current + * Specification. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given Specification is null. + */ public SpecificationBuilder where(final Specification specification) { Objects.requireNonNull(specification, "Argument 'specification' cannot be null."); this.specification = this.specification == null ? @@ -42,14 +83,42 @@ public SpecificationBuilder where(final Specification specification) { return this; } + /** + * Simply returns the current instance of the + * SpecificationBuilder. Used to maintain fluency + * of the code, to better resemble SQL and the + * way the underlying query is spoken. + * + * @return The current instance of the SpecificationBuilder. + */ public SpecificationBuilder and() { return this; } + /** + * Adds the given Specification to the current Specification + * with a conjunction (unless no Specification has yet been + * added) using the language of 'and'. + * + * @param specification The Specification to add the current + * Specification. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given Specification is null. + */ public SpecificationBuilder and(final Specification specification) { return where(specification); } + /** + * Adds the given Specification to the current Specification + * with a disjunction (unless no Specification has yet been + * added) using the language of 'or'. + * + * @param specification The Specification to add the current + * Specification. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given Specification is null. + */ public SpecificationBuilder or(final Specification specification) { Objects.requireNonNull(specification, "Argument 'specification' cannot be null."); this.specification = this.specification == null ? @@ -57,91 +126,293 @@ public SpecificationBuilder or(final Specification specification) { return this; } + /** + * Simply returns the current instance of the + * SpecificationBuilder. Used to maintain fluency + * of the code, to better resemble the way the + * underlying query is spoken. + * + * @return The current instance of the SpecificationBuilder. + */ public SpecificationBuilder with() { return this; } + /** + * Adds a Specification with a Predicate representing an + * SQL equals clause, or a no-op "ghost" Predicate if the + * given value is null, to the current Specification. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isEqualTo(final SingularAttribute attribute, final Object value) { return where(SpecificationFactory.isEqualTo(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL not equals clause, or a no-op "ghost" Predicate if + * the given value is null, to the current Specification. + * + * @param attribute The attribute to check against the value. + * @param value The value to check against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isNotEqualTo(final SingularAttribute attribute, final Object value) { return where(SpecificationFactory.isNotEqualTo(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL like clause, or a no-op "ghost" Predicate if the + * given value is null, to the current Specification. + * Matching is case-insensitive. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isLike(final SingularAttribute attribute, final String value) { return where(SpecificationFactory.isLike(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL not like clause, or a no-op "ghost" Predicate if the + * given value is null, to the current Specification. + * Matching is case-insensitive. + * + * @param attribute The attribute to check against the value. + * @param value The value to check against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isNotLike(final SingularAttribute attribute, final String value) { return where(SpecificationFactory.isNotLike(attribute, value)); } + /** + * If the value contains one or more SQL wildcard characters, + * adds a Specification with a Predicate representing an + * SQL like clause to the current Specification. Otherwise, + * if the value does not contain any SQL wildcard characters, + * adds a Specification with a Predicate representing an SQL + * equals clause to the current Specification. Adds a + * Specification with a no-op "ghost" Predicate if the given + * value is null. Like matching is case-insensitive. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isEqualToOrLike(final SingularAttribute attribute, final String value) { return where(SpecificationFactory.isEqualToOrLike(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL is null clause to the current Specification. + * + * @param attribute The attribute to check. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isNull(final SingularAttribute attribute) { return where(SpecificationFactory.isNull(attribute)); } + /** + * Adds a Specification with a Predicate representing an + * SQL is not null clause to the current Specification. + * + * @param attribute The attribute to check. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isNotNull(final SingularAttribute attribute) { return where(SpecificationFactory.isNotNull(attribute)); } + /** + * Adds a Specification with a Predicate representing an + * SQL equals clause that checks if the given boolean attribute + * is true to the current Specification. + * + * @param attribute The attribute to check. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isTrue(final SingularAttribute attribute) { return where(SpecificationFactory.isTrue(attribute)); } + /** + * Adds a Specification with a Predicate representing an + * SQL equals clause that checks if the given boolean attribute + * is false to the current Specification. + * + * @param attribute The attribute to check. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isFalse(final SingularAttribute attribute) { return where(SpecificationFactory.isFalse(attribute)); } + /** + * Adds a Specification with a Predicate representing an + * SQL greater than clause, or a no-op "ghost" Predicate if + * the given value is null, to the current Specification. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public > SpecificationBuilder isGreaterThan(final SingularAttribute attribute, final V value) { return where(SpecificationFactory.isGreaterThan(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL greater than or equal to clause, or a no-op "ghost" + * Predicate if the given value is null, to the current + * Specification. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public > SpecificationBuilder isGreaterThanOrEqualTo(final SingularAttribute attribute, final V value) { return where(SpecificationFactory.isGreaterThanOrEqualTo(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL less than clause, or a no-op "ghost" Predicate if + * the given value is null, to the current Specification. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public > SpecificationBuilder isLessThan(final SingularAttribute attribute, final V value) { return where(SpecificationFactory.isLessThan(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL less than or equal to clause, or a no-op "ghost" + * Predicate if the given value is null, to the current + * Specification. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public > SpecificationBuilder isLessThanOrEqualTo(final SingularAttribute attribute, final V value) { return where(SpecificationFactory.isLessThanOrEqualTo(attribute, value)); } + /** + * Adds a Specification with a Predicate representing an + * SQL between clause, or a no-op "ghost" Predicate if either + * of the given values are null, to the current Specification. + * + * @param attribute The attribute to match against the value. + * @param firstValue The value to match against the attribute + * as the inclusive first half of the between + * clause. + * @param secondValue The value to match against the attribute + * as the exclusive second half of the between + * clause. + * @return The current instance of the SpecificationBuilder. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public > SpecificationBuilder isBetween(final SingularAttribute attribute, final V firstValue, final V secondValue) { return where(SpecificationFactory.isBetween(attribute, firstValue, secondValue)); } + /** + * Adds a Specification with a Predicate representing an + * SQL in clause, or a no-op "ghost" Predicate if the given + * collection is null, to the current Specification. + * + * @param attribute The attribute to match against the values. + * @param collection The collection of values to match against + * the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isIn(final SingularAttribute attribute, final Collection collection) { return where(SpecificationFactory.isIn(attribute, collection)); } + /** + * Adds a Specification with a Predicate representing an + * SQL in clause, or a no-op "ghost" Predicate if the given + * collection is null, to the current Specification. + * + * @param attribute The attribute to match against the values. + * @param values The values to match against the attribute. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given attribute is null. + */ public SpecificationBuilder isIn(final SingularAttribute attribute, final Object... values) { return where(SpecificationFactory.isIn(attribute, values)); } + /** + * Defines a join with the given singular association + * in order to fetch it eagerly as part of the SQL query. + * A useful optimization technique to fetch an entire + * Aggregate with one query instead of many. + * + * @param attribute The singular association to fetch. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given association is null. + */ public SpecificationBuilder fetchOf(final SingularAttribute attribute) { return and(SpecificationFactory.fetchOf(attribute)); } + /** + * Defines a join with the given collection association + * in order to fetch it eagerly as part of the SQL query. + * A useful optimization technique to fetch an entire + * Aggregate with one query instead of many. + * + * @param attribute The collection association to fetch. + * @return The current instance of the SpecificationBuilder. + * @throws NullPointerException if the given association is null. + */ public SpecificationBuilder fetchOf(final PluralAttribute attribute) { return and(SpecificationFactory.fetchOf(attribute)); } diff --git a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactory.java b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactory.java index 9a4858d..b801527 100644 --- a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactory.java +++ b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationFactory.java @@ -21,12 +21,28 @@ public class SpecificationFactory { private static final String ATTRIBUTE_CANNOT_BE_NULL = "Argument 'attribute' cannot be null."; + /** + * Default Constructor. Private since this Class is not + * meant to be instantiated. + */ private SpecificationFactory() { // no-op } + /** + * Returns a Specification with a Predicate representing an + * SQL equals clause, or a no-op "ghost" Predicate if the given + * value is null. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL equals clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isEqualTo(final SingularAttribute attribute, - final Object value) { + final Object value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); if (noneAreNull(value)) { return (root, query, builder) -> builder.equal(root.get(attribute), value); @@ -34,8 +50,20 @@ public static Specification isEqualTo(final SingularAttribute attri return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL not equals clause, or a no-op "ghost" Predicate if the + * given value is null. + * + * @param attribute The attribute to check against the value. + * @param value The value to check against the attribute. + * @return A Specification with a Predicate that defines an + * SQL equals clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isNotEqualTo(final SingularAttribute attribute, - final Object value) { + final Object value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); if (noneAreNull(value)) { return (root, query, builder) -> builder.notEqual(root.get(attribute), value); @@ -43,6 +71,18 @@ public static Specification isNotEqualTo(final SingularAttribute at return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL like clause, or a no-op "ghost" Predicate if the given + * value is null. Matching is case-insensitive. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL like clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isLike(final SingularAttribute attribute, final String value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -56,6 +96,18 @@ public static Specification isLike(final SingularAttribute att return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL not like clause, or a no-op "ghost" Predicate if the + * given value is null. Matching is case-insensitive. + * + * @param attribute The attribute to check against the value. + * @param value The value to check against the attribute. + * @return A Specification with a Predicate that defines an + * SQL not like clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isNotLike(final SingularAttribute attribute, final String value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -69,32 +121,105 @@ public static Specification isNotLike(final SingularAttribute return ghost(); } + /** + * If the value contains one or more SQL wildcard characters, + * returns a Specification with a Predicate representing an + * SQL like clause. Otherwise, if the value does not contain + * any SQL wildcard characters, returns a Specification with + * a Predicate representing an SQL equals clause. Returns a + * Specification with a no-op "ghost" Predicate if the given + * value is null. Like matching is case-insensitive. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL like clause or equals clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isEqualToOrLike(final SingularAttribute attribute, - final String value) { + final String value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return isWildcardExpression(value) ? isLike(attribute, value) : isEqualTo(attribute, value); } + /** + * Returns a Specification with a Predicate representing an + * SQL is null clause. + * + * @param attribute The attribute to check. + * @return A Specification with a Predicate that defines an + * SQL is null clause. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isNull(final SingularAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> builder.isNull(root.get(attribute)); } + /** + * Returns a Specification with a Predicate representing an + * SQL is not null clause. + * + * @param attribute The attribute to check. + * @return A Specification with a Predicate that defines an + * SQL is not null clause. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isNotNull(final SingularAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> builder.isNotNull(root.get(attribute)); } + /** + * Returns a Specification with a Predicate representing an + * SQL equals clause that checks if the given boolean attribute + * is true. + * + * @param attribute The attribute to check. + * @return A Specification with a Predicate that defines an + * SQL equals clause that checks if the given boolean + * attribute is true. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isTrue(final SingularAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> builder.isTrue(root.get(attribute).as(Boolean.class)); } + /** + * Returns a Specification with a Predicate representing an + * SQL equals clause that checks if the given boolean attribute + * is false. + * + * @param attribute The attribute to check. + * @return A Specification with a Predicate that defines an + * SQL equals clause that checks if the given boolean + * attribute is false. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isFalse(final SingularAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> builder.isFalse(root.get(attribute).as(Boolean.class)); } + /** + * Returns a Specification with a Predicate representing an + * SQL greater than clause, or a no-op "ghost" Predicate if + * the given value is null. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL greater than clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public static > Specification isGreaterThan(final SingularAttribute attribute, final V value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -104,6 +229,20 @@ public static > Specification isGreaterTha return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL greater than or equal to clause, or a no-op "ghost" + * Predicate if the given value is null. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL greater than or equal to clause, or a no-op + * Predicate. + * @param The Aggregate Root of the Specification. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public static > Specification isGreaterThanOrEqualTo(final SingularAttribute attribute, final V value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -113,6 +252,19 @@ public static > Specification isGreaterTha return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL less than clause, or a no-op "ghost" Predicate if + * the given value is null. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL less than clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public static > Specification isLessThan(final SingularAttribute attribute, final V value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -122,6 +274,20 @@ public static > Specification isLessThan(f return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL less than or equal to clause, or a no-op "ghost" + * Predicate if the given value is null. + * + * @param attribute The attribute to match against the value. + * @param value The value to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL less than or equal to clause, or a no-op + * Predicate. + * @param The Aggregate Root of the Specification. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public static > Specification isLessThanOrEqualTo(final SingularAttribute attribute, final V value) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -131,6 +297,24 @@ public static > Specification isLessThanOr return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL between clause, or a no-op "ghost" Predicate if either + * of the given values are null. + * + * @param attribute The attribute to match against the value. + * @param firstValue The value to match against the attribute + * as the inclusive first half of the between + * clause. + * @param secondValue The value to match against the attribute + * as the exclusive second half of the between + * clause. + * @return A Specification with a Predicate that defines an + * SQL between clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @param The type assigned to the attribute value. + * @throws NullPointerException if the given attribute is null. + */ public static > Specification isBetween(final SingularAttribute attribute, final V firstValue, final V secondValue) { @@ -141,6 +325,19 @@ public static > Specification isBetween(fi return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL in clause, or a no-op "ghost" Predicate if the given + * collection is null. + * + * @param attribute The attribute to match against the values. + * @param collection The collection of values to match against + * the attribute. + * @return A Specification with a Predicate that defines an + * SQL in clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isIn(final SingularAttribute attribute, final Collection collection) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -150,6 +347,18 @@ public static Specification isIn(final SingularAttribute attribute, return ghost(); } + /** + * Returns a Specification with a Predicate representing an + * SQL in clause, or a no-op "ghost" Predicate if the given + * values are null. + * + * @param attribute The attribute to match against the values. + * @param values The values to match against the attribute. + * @return A Specification with a Predicate that defines an + * SQL in clause, or a no-op Predicate. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given attribute is null. + */ public static Specification isIn(final SingularAttribute attribute, final Object... values) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); @@ -158,6 +367,18 @@ public static Specification isIn(final SingularAttribute attribute, .collect(Collectors.toSet())); } + /** + * Defines a join with the given singular association + * in order to fetch it eagerly as part of the SQL query. + * A useful optimization technique to fetch an entire + * Aggregate with one query instead of many. + * + * @param attribute The singular association to fetch. + * @return A Specification with a Predicate that defines + * an eager fetch of the given association. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given association is null. + */ public static Specification fetchOf(final SingularAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> { @@ -166,6 +387,18 @@ public static Specification fetchOf(final SingularAttribute attribu }; } + /** + * Defines a join with the given collection association + * in order to fetch it eagerly as part of the SQL query. + * A useful optimization technique to fetch an entire + * Aggregate with one query instead of many. + * + * @param attribute The collection association to fetch. + * @return A Specification with a Predicate that defines + * an eager fetch of the given association. + * @param The Aggregate Root of the Specification. + * @throws NullPointerException if the given association is null. + */ public static Specification fetchOf(final PluralAttribute attribute) { Objects.requireNonNull(attribute, ATTRIBUTE_CANNOT_BE_NULL); return (root, query, builder) -> { @@ -174,6 +407,15 @@ public static Specification fetchOf(final PluralAttribute attrib }; } + /** + * Returns a Specification that returns a null Predicate. + * Essentially a no-op. Convenient when composing + * Specifications, so that a Specification will not + * be null. + * + * @return A Specification with a null Predicate. + * @param The Aggregate Root of the Specification. + */ public static Specification ghost() { return (root, query, builder) -> null; } diff --git a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtil.java b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtil.java index f0ce1d8..3bedee2 100644 --- a/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtil.java +++ b/src/main/java/io/github/quinnandrews/spring/data/specification/builder/SpecificationUtil.java @@ -12,30 +12,84 @@ */ public class SpecificationUtil { + /** + * Default Constructor. Private since this Class is not + * meant to be instantiated. + */ private SpecificationUtil() { // no-op } + /** + * Converts any upper case characters in the given String + * to lower case characters and returns a new String. Returns + * null if the given String is null. + * + * @param string The String to convert. + * @return A new converted String or null if the given String + * is null. + */ public static String toLowerCase(final String string) { return StringUtils.lowerCase(string); } + /** + * If the given Object is a String, then all leading and trailing + * whitespace characters are removed and a new String is returned. + * Otherwise, if the given Object is not a String, then the given + * Object is simply returned without any modifications. Alternatively, + * if the given Object is null, then null is returned. + * + * @param object The Object to check. + * @return Object that is either a stripped String, the original + * Object or null. + */ public static Object stripToNull(final Object object) { return object instanceof String ? StringUtils.stripToNull(object.toString()) : object; } + /** + * Escapes any SQL wildcard characters in the given String and + * returns a new String. Returns null if the given String is null. + * + * @param string The String to convert. + * @return A new converted String or null if the given String + * is null. + */ public static String escapeWildcardCharacters(final String string) { return StringUtils.replaceEach(string, new String[]{"%", "_"}, new String[]{"\\%", "\\_"}); } + /** + * Returns true if the given String, when stripped of leading and + * trailing whitespace, contains any SQL wildcard characters. + * + * @param string The String to check. + * @return Boolean indicating whether the given String contains any + * SQL wildcard characters. + */ public static boolean isWildcardExpression(final String string) { return StringUtils.containsAny(StringUtils.stripToNull(string), "_%"); } + /** + * Returns true if the given String, when stripped of leading and + * trailing whitespace, contains only SQL wildcard characters. + * + * @param string The String to check. + * @return Boolean indicating whether the given String contains only + * SQL wildcard characters. + */ public static boolean isEmptyWildcardExpression(final String string) { return StringUtils.containsOnly(StringUtils.stripToNull(string), "_%"); } + /** + * Returns true if all given Objects are null. + * + * @param objects The Objects to check. + * @return Boolean indicating whether all given Objects are null. + */ public static boolean noneAreNull(final Object... objects) { return Arrays.stream(objects).noneMatch(obj -> Objects.isNull(stripToNull(obj))); }