diff --git a/api/src/main/java/jakarta/data/BasicRestriction.java b/api/src/main/java/jakarta/data/BasicRestriction.java new file mode 100644 index 000000000..a59ab7426 --- /dev/null +++ b/api/src/main/java/jakarta/data/BasicRestriction.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +/** + * A basic restriction applies to a single field, representing conditions such as equality, + * comparisons, range checks, and pattern matches. + * + *

The {@code BasicRestriction} interface provides methods for defining simple, singular restrictions + * based on a specific field, an operator, and an optional comparison value. This interface supports + * common operators (e.g., EQUAL, GREATER_THAN) and serves as a foundation for filtering + * logic on individual fields.

+ * + * @param the type of the entity on which the restriction is applied. + */ +public interface BasicRestriction extends Restriction { + + /** + * The name of the field on which this restriction is applied. + * + * @return the field name as a String. + */ + String field(); + + /** + * The operator defining the type of comparison or condition for this restriction. + * + * @return the operator representing the restriction type (e.g., EQUAL, LIKE, BETWEEN). + */ + Operator operator(); + + /** + * The value used for comparison in this restriction, if applicable. + * + * @return the comparison value, or {@code null} if the restriction does not use a value (e.g., IS_NULL). + */ + Object value(); +} diff --git a/api/src/main/java/jakarta/data/CompositeRestriction.java b/api/src/main/java/jakarta/data/CompositeRestriction.java new file mode 100644 index 000000000..d829b62f2 --- /dev/null +++ b/api/src/main/java/jakarta/data/CompositeRestriction.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +import java.util.List; + +/** + * A composite restriction that combines multiple {@link Restriction} instances using logical operators. + * + *

The {@code MultipleRestriction} interface allows for combining multiple restrictions, enabling complex + * filtering scenarios where multiple conditions must be satisfied. Each contained {@link Restriction} + * can be evaluated based on the logical operator specified by the {@link CompositeRestrictionType} type.

+ * + *

This interface is useful for defining AND/OR conditions where multiple fields and restrictions + * are evaluated together in a repository query.

+ * + * @param the type of the entity on which the restriction is applied. + */ +public interface CompositeRestriction extends Restriction { + + /** + * The list of restrictions that are combined in this composite restriction. + * + * @return a list of individual restrictions. + */ + List> restrictions(); + + /** + * The logical operator used to combine the contained restrictions, such as AND or OR. + * + * @return the logical combination type for this composite restriction. + */ + CompositeRestrictionType type(); +} diff --git a/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java b/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java new file mode 100644 index 000000000..2cb9a7452 --- /dev/null +++ b/api/src/main/java/jakarta/data/CompositeRestrictionRecord.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +import java.util.Iterator; +import java.util.List; + +/** + * A composite restriction representing a collection of individual {@link Restriction} + * and {@link CompositeRestrictionType} instances, combined under a single logical operation. + * + *

This record allows multiple restrictions to be treated as a single entity, making + * it easy to pass complex conditions to repository methods.

+ * + * @param the entity type that the restrictions apply to. + */ +record CompositeRestrictionRecord(CompositeRestrictionType type, List> restrictions) + implements Iterable>, CompositeRestriction { + + /** + * Constructs a composite restriction with the specified operator and list of restrictions. + * + * @param restrictions the list of restrictions to combine. + */ + public CompositeRestrictionRecord { + restrictions = List.copyOf(restrictions); // Ensure immutability of the list + } + + @Override + public Iterator> iterator() { + return restrictions.iterator(); + } + + /** + * Creates a composite restriction where all specified restrictions must be true (AND logic). + * + * @param restrictions the individual restrictions to combine. + * @param the entity type that the restrictions apply to. + * @return a CompositeRestriction representing the AND combination of the provided restrictions. + */ + @SafeVarargs + public static CompositeRestrictionRecord all(Restriction... restrictions) { + return new CompositeRestrictionRecord<>(CompositeRestrictionType.ALL, List.of(restrictions)); + } + + /** + * Creates a composite restriction where any of the specified restrictions may be true (OR logic). + * + * @param restrictions the individual restrictions to combine. + * @param the entity type that the restrictions apply to. + * @return a CompositeRestriction representing the OR combination of the provided restrictions. + */ + @SafeVarargs + public static CompositeRestrictionRecord any(Restriction... restrictions) { + return new CompositeRestrictionRecord<>(CompositeRestrictionType.ANY, List.of(restrictions)); + } + +} diff --git a/api/src/main/java/jakarta/data/CompositeRestrictionType.java b/api/src/main/java/jakarta/data/CompositeRestrictionType.java new file mode 100644 index 000000000..e96b2ded4 --- /dev/null +++ b/api/src/main/java/jakarta/data/CompositeRestrictionType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + + +/** + * Represents the logical operators used to combine multiple restrictions in a query. + * + *

The `Restrict` enum defines two types of logical combinations:

+ *
    + *
  • ALL - Requires that all contained restrictions are satisfied (logical AND).
  • + *
  • ANY - Requires that at least one of the contained restrictions is satisfied (logical OR).
  • + *
+ * + *

This enum is typically used in {@link CompositeRestriction} to specify how its list of + * restrictions should be combined, allowing for flexible and complex query conditions.

+ */ +public enum CompositeRestrictionType { + /** + * Requires that all contained restrictions must be satisfied (logical AND). + */ + ALL, + + /** + * Requires that at least one of the contained restrictions must be satisfied (logical OR). + */ + ANY +} diff --git a/api/src/main/java/jakarta/data/Operator.java b/api/src/main/java/jakarta/data/Operator.java new file mode 100644 index 000000000..8a184ba8d --- /dev/null +++ b/api/src/main/java/jakarta/data/Operator.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + + +/** + * Enum representing common operators used to define query conditions in Jakarta Data queries. + * The {@code Operator} enum provides a set of operations to specify how values should be compared, + * matched, or restricted when filtering data, supporting flexible and expressive querying across + * different contexts. + */ +public enum Operator { + + /** + * Matches records where the field value is exactly equal to the specified value. + * Typically used for exact matching on unique identifiers, names, or other exact-value fields. + */ + EQUAL, + + /** + * Matches records where the field value is greater than the specified value. + * Often applied to numerical fields (e.g., dates, prices) for retrieving values above a given threshold. + */ + GREATER_THAN, + + /** + * Matches records where the field value is greater than or equal to the specified value. + * Useful for inclusive range queries, where the specified boundary is included in the results. + */ + GREATER_THAN_EQUAL, + + /** + * Matches records where the field value is contained within a specified collection of values. + * Commonly used to match against multiple possible values, such as category or status lists. + */ + IN, + + /** + * Matches records where the field value is less than the specified value. + * Frequently used in numerical comparisons to retrieve values below a certain threshold. + */ + LESS_THAN, + + /** + * Matches records where the field value is less than or equal to the specified value. + * Suitable for inclusive range queries that include the specified boundary in the results. + */ + LESS_THAN_EQUAL, + + /** + * Matches records where the field value conforms to a specified pattern, often with wildcards. + * Commonly applied to string fields for partial matches (e.g., names containing a substring). + */ + LIKE, + + /** + * Matches records where the field value falls within a specified range. + * Typically used for range-based comparisons, such as finding dates between a start and end date. + */ + BETWEEN +} + + + diff --git a/api/src/main/java/jakarta/data/Order.java b/api/src/main/java/jakarta/data/Order.java index 3cc99a502..02b7d4a69 100644 --- a/api/src/main/java/jakarta/data/Order.java +++ b/api/src/main/java/jakarta/data/Order.java @@ -17,12 +17,12 @@ */ package jakarta.data; -import java.util.Iterator; -import java.util.List; - import jakarta.data.metamodel.StaticMetamodel; import jakarta.data.repository.OrderBy; +import java.util.Iterator; +import java.util.List; + /** *

Requests sorting on various entity attributes.

* @@ -169,4 +169,4 @@ public Iterator> iterator() { public String toString() { return sorts.toString(); } -} \ No newline at end of file +} diff --git a/api/src/main/java/jakarta/data/Pattern.java b/api/src/main/java/jakarta/data/Pattern.java new file mode 100644 index 000000000..71487feb7 --- /dev/null +++ b/api/src/main/java/jakarta/data/Pattern.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + + +/** + * Represents a pattern for use in string matching conditions. This class supports options + * such as exact match, prefix, suffix, and substring matching. It is intended to be used + * with attributes that apply the pattern to create a {@link Restriction}. + * + *

Example usage:

+ *
+ * // Pattern for a case-sensitive prefix match for values starting with "Guide"
+ * Pattern prefixMatch = Pattern.startsWith("Guide");
+ *
+ * // Pattern for matching values containing "Java"
+ * Pattern containsMatch = Pattern.contains("Java");
+ * 
+ */ +public record Pattern(String value, boolean caseSensitive) { + + /** + * Creates a pattern for an exact match with the specified literal. + * + *

Example usage:

+ *
+     * Pattern exactMatch = Pattern.is("Java Guide");
+     * 
+ * + * @param literal the exact text to match. + * @return a {@code Pattern} instance for an exact match. + */ + public static Pattern is(String literal) { + return new Pattern(escape(literal), true); + } + + /** + * Creates a pattern for a custom match. + * + *

Example usage:

+ *
+     * Pattern customPatternMatch = Pattern.matches("Ja%_a");
+     * 
+ * + * @param pattern the pattern to match. + * @return a {@code Pattern} instance for a custom match. + */ + public static Pattern matches(String pattern) { + return new Pattern(pattern, true); + } + + /** + * Creates a pattern using custom single and multi-character wildcards. + * + *

Example usage:

+ *
+     * Pattern wildcardMatch = Pattern.matches("Ja?a%", '?', '*');
+     * 
+ * + * @param pattern the custom pattern to match. + * @param characterWildcard the character to use as a single-character wildcard. + * @param stringWildcard the character to use as a multi-character wildcard. + * @return a {@code Pattern} instance for a custom match with specified wildcards. + */ + public static Pattern matches(String pattern, char characterWildcard, char stringWildcard) { + final String standardized = escape(pattern) + .replace(characterWildcard, '_') + .replace(stringWildcard, '%'); + return new Pattern(standardized, true); + } + + /** + * Creates a pattern for a match where values start with the specified prefix. + * + *

Example usage:

+ *
+     * Pattern prefixMatch = Pattern.startsWith("Hibernate");
+     * 
+ * + * @param prefix the prefix to match at the beginning of the value. + * @return a {@code Pattern} instance for a prefix match. + */ + public static Pattern startsWith(String prefix) { + return new Pattern(escape(prefix) + '%', true); + } + + /** + * Creates a pattern for a match where values end with the specified suffix. + * + *

Example usage:

+ *
+     * Pattern suffixMatch = Pattern.endsWith("Guide");
+     * 
+ * + * @param suffix the suffix to match at the end of the value. + * @return a {@code Pattern} instance for a suffix match. + */ + public static Pattern endsWith(String suffix) { + return new Pattern('%' + escape(suffix), true); + } + + /** + * Creates a pattern for a match where values contain the specified substring. + * + *

Example usage:

+ *
+     * Pattern substringMatch = Pattern.contains("Java");
+     * 
+ * + * @param substring the substring to match within the value. + * @return a {@code Pattern} instance for a substring match. + */ + public static Pattern contains(String substring) { + return new Pattern('%' + escape(substring) + '%', true); + } + + /** + * Escapes special characters in the pattern, such as underscores and percent signs, + * to ensure literal matches for these characters. + * + * @param literal the text to escape. + * @return the escaped text with special characters handled. + */ + private static String escape(String literal) { + return literal.replace("_", "\\_").replace("%", "\\%"); + } +} + + diff --git a/api/src/main/java/jakarta/data/Restrict.java b/api/src/main/java/jakarta/data/Restrict.java new file mode 100644 index 000000000..217fb50f4 --- /dev/null +++ b/api/src/main/java/jakarta/data/Restrict.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +import java.util.List; +import java.util.Set; + +/** + * Utility class for constructing complex restrictions in repository queries. + * The `Restrict` class provides static methods for creating basic and composite + * restrictions, supporting conditions like equality, comparisons, and logical + * combinations (`ALL` and `ANY`). + * + *

Example usage:

+ *
+ * // Create a single equality restriction
+ * Restriction titleRestriction = Restrict.equalTo("Java Guide", "title");
+ *
+ * // Create a composite restriction using AND logic
+ * Restriction compositeRestriction = Restrict.all(
+ *     Restrict.equalTo("Java Guide", "title"),
+ *     Restrict.greaterThan(2020, "publicationYear")
+ * );
+ * 
+ */ +public final class Restrict { + + private Restrict() { + } + + /** + * Creates a composite restriction that requires all specified restrictions to be met. + * + * @param restrictions the list of restrictions to combine with AND logic. + * @param the entity type. + * @return a composite restriction using AND logic. + */ + @SafeVarargs + public static Restriction all(Restriction... restrictions) { + return new CompositeRestrictionRecord<>(CompositeRestrictionType.ALL, List.of(restrictions)); + } + + /** + * Creates a composite restriction that requires at least one of the specified restrictions to be met. + * + * @param restrictions the list of restrictions to combine with OR logic. + * @param the entity type. + * @return a composite restriction using OR logic. + */ + @SafeVarargs + public static Restriction any(Restriction... restrictions) { + return new CompositeRestrictionRecord<>(CompositeRestrictionType.ANY, List.of(restrictions)); + } + + /** + * Creates an equality restriction for the specified field and value. + * + * @param value the value to match exactly. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return an equality restriction for the specified field. + */ + public static Restriction equalTo(Object value, String field) { + return new RestrictionRecord<>(field, Operator.EQUAL, value); + } + + /** + * Creates a "less than" restriction for the specified field and value. + * + * @param value the upper bound (exclusive) for the field. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a "less than" restriction for the specified field. + */ + public static Restriction lessThan(Object value, String field) { + return new RestrictionRecord<>(field, Operator.LESS_THAN, value); + } + + /** + * Creates a "greater than or equal to" restriction for the specified field and value. + * + * @param value the lower bound (inclusive) for the field. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a "greater than or equal to" restriction for the specified field. + */ + public static Restriction greaterThanEqual(Object value, String field) { + return new RestrictionRecord<>(field, Operator.GREATER_THAN_EQUAL, value); + } + + /** + * Creates a "less than or equal to" restriction for the specified field and value. + * + * @param value the upper bound (inclusive) for the field. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a "less than or equal to" restriction for the specified field. + */ + public static Restriction lessThanEqual(Object value, String field) { + return new RestrictionRecord<>(field, Operator.LESS_THAN_EQUAL, value); + } + + /** + * Creates a "greater than" restriction for the specified field and value. + * + * @param value the lower bound (exclusive) for the field. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a "greater than" restriction for the specified field. + */ + public static Restriction greaterThan(Object value, String field) { + return new RestrictionRecord<>(field, Operator.GREATER_THAN, value); + } + + /** + * Creates an "in" restriction, restricting the field to match any value in the specified set. + * + * @param values the set of allowed values for the field. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return an "in" restriction for the specified field. + */ + public static Restriction in(Set values, String field) { + return new RestrictionRecord<>(field, Operator.IN, values); + } + + /** + * Creates a restriction that matches field values containing the specified substring. + * + *

Example usage:

+ *
+     * Restriction containsMatch = Restrict.contains("Java", "title");
+     * 
+ * + * @param value the substring to search for within the field's value. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a restriction that matches values containing the specified substring. + */ + public static Restriction contains(String value, String field) { + var contains = Pattern.contains(value); + return new RestrictionRecord<>(field, Operator.LIKE, contains.value()); + } + + /** + * Creates a restriction that matches field values starting with the specified prefix. + * + *

Example usage:

+ *
+     * Restriction startsWithMatch = Restrict.startsWith("Guide", "title");
+     * 
+ * + * @param value the prefix to match at the beginning of the field's value. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a restriction that matches values starting with the specified prefix. + */ + public static Restriction startsWith(String value, String field) { + var contains = Pattern.startsWith(value); + return new RestrictionRecord<>(field, Operator.LIKE, contains.value()); + } + + /** + * Creates a restriction that matches field values ending with the specified suffix. + * + *

Example usage:

+ *
+     * Restriction endsWithMatch = Restrict.endsWith("Guide", "title");
+     * 
+ * + * @param value the suffix to match at the end of the field's value. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a restriction that matches values ending with the specified suffix. + */ + public static Restriction endsWith(String value, String field) { + var contains = Pattern.endsWith(value); + return new RestrictionRecord<>(field, Operator.LIKE, contains.value()); + } + + /** + * Creates a restriction that matches field values ending with the specified pattern. + * + *

Example usage:

+ *
+     * Pattern pattern = Pattern.endsWith("Guide");
+     * Restriction endsWithPatternMatch = Restrict.endsWith(pattern, "title");
+     * 
+ * + * @param pattern the pattern to match at the end of the field's value. + * @param field the name of the field to apply the restriction on. + * @param the entity type. + * @return a restriction that matches values ending with the specified pattern. + */ + public static Restriction endsWith(Pattern pattern, String field) { + return new RestrictionRecord<>(field, Operator.LIKE, pattern.value()); + } +} diff --git a/api/src/main/java/jakarta/data/Restriction.java b/api/src/main/java/jakarta/data/Restriction.java new file mode 100644 index 000000000..cdb962178 --- /dev/null +++ b/api/src/main/java/jakarta/data/Restriction.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + + +/** + * Represents a condition used to filter values in repository queries. + * + *

The `Restriction` interface serves as a general contract for defining filter conditions, + * supporting various operations such as equality, comparisons, range, null checks, + * and pattern matching. Implementations of `Restriction` can be used to construct + * flexible and type-safe filtering logic in repository queries.

+ * + *

Subtypes include {@link BasicRestriction}, which handles single-field conditions, + * and {@link CompositeRestriction}, which combines multiple restrictions + * using logical operators.

+ * + * @param the type of the entity on which the restriction is applied. + */ +public interface Restriction { + + +} diff --git a/api/src/main/java/jakarta/data/RestrictionRecord.java b/api/src/main/java/jakarta/data/RestrictionRecord.java new file mode 100644 index 000000000..be1a1335e --- /dev/null +++ b/api/src/main/java/jakarta/data/RestrictionRecord.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +/** + * A basic implementation of the `Restriction` interface, representing various conditions + * used in repository queries, including equality, comparison, and range checks. + * + * @param the type of the entity on which the restriction is applied. + */ +record RestrictionRecord(String field, Operator operator, Object value) implements BasicRestriction { + + /** + * Constructs a `BasicRestriction` with the specified field, operator, and value. + * + * @param field the name of the field to apply the restriction to. + * @param operator the operator defining the comparison or condition. + * @param value the value to compare the field against (optional for null checks). + */ + public RestrictionRecord { + if (field == null || operator == null) { + throw new IllegalArgumentException("Field and operator must not be null."); + } + } +} diff --git a/api/src/main/java/jakarta/data/metamodel/Attribute.java b/api/src/main/java/jakarta/data/metamodel/Attribute.java index 2906554d6..6cb3efd9a 100644 --- a/api/src/main/java/jakarta/data/metamodel/Attribute.java +++ b/api/src/main/java/jakarta/data/metamodel/Attribute.java @@ -17,6 +17,8 @@ */ package jakarta.data.metamodel; +import jakarta.data.Restrict; +import jakarta.data.Restriction; import jakarta.data.Sort; /** @@ -32,4 +34,23 @@ public interface Attribute { * @return the entity attribute name. */ String name(); + + /** + * Creates an equality restriction for the attribute. + * + * @param value the value to match exactly. + * @return a Restriction representing an equality condition. + */ + default Restriction equal(Object value) { + return Restrict.equalTo(value, name()); + } + + /** + * Creates a restriction for checking if the attribute is null. + * + * @return a Restriction representing the condition where the attribute is null. + */ + default Restriction isNull(){ + return Restrict.equalTo(null, name()); + } } diff --git a/api/src/main/java/jakarta/data/metamodel/Range.java b/api/src/main/java/jakarta/data/metamodel/Range.java new file mode 100644 index 000000000..a7577c4f1 --- /dev/null +++ b/api/src/main/java/jakarta/data/metamodel/Range.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data.metamodel; + +import jakarta.data.BasicRestriction; +import jakarta.data.Operator; +import jakarta.data.Restriction; + +import java.util.List; + +/** + * Represents a range-based restriction for repository queries, allowing comparisons within + * a specified range or boundary on a particular entity attribute. + * + *

This class implements {@link Restriction } and provides flexible factory methods + * to create range-based conditions, supporting inclusive and exclusive bounds.

+ * + *

Usage examples:

+ *
+ * // A range between two dates (inclusive)
+ * Restriction publicationDateRange = Range.between(_Book.publicationDate, pastDate, LocalDate.now());
+ *
+ * // An exclusive range above a certain rating
+ * Restriction ratingAbove = Range.above(_Book.rating, 4.5);
+ *
+ * // An inclusive range up to a certain price
+ * Restriction priceTo = Range.to(_Book.price, 100);
+ * 
+ * + *

The `operator()` method dynamically determines the appropriate operator based on the range type:

+ *
    + *
  • If both bounds are specified, {@code Operator.BETWEEN} is used.
  • + *
  • If only the lower bound is specified and the range is exclusive, {@code Operator.GREATER_THAN} is used.
  • + *
  • If only the lower bound is specified and the range is inclusive, {@code Operator.GREATER_THAN_EQUAL} is used.
  • + *
  • If only the upper bound is specified and the range is exclusive, {@code Operator.LESS_THAN} is used.
  • + *
  • If only the upper bound is specified and the range is inclusive, {@code Operator.LESS_THAN_EQUAL} is used.
  • + *
+ * + * @param the type of the entity's attribute on which the range is applied. + */ +public record Range(String field, T lowerBound, T upperBound, boolean open) implements BasicRestriction { + + @Override + public String field() { + return field; + } + + @Override + public Operator operator() { + if (lowerBound != null && upperBound != null) { + return Operator.BETWEEN; + } else if (lowerBound != null && open) { + return Operator.GREATER_THAN; + } else if (lowerBound != null) { + return Operator.GREATER_THAN_EQUAL; + } else if (upperBound != null && open) { + return Operator.LESS_THAN; + } else { + return Operator.LESS_THAN_EQUAL; + } + } + + @Override + public Object value() { + if (lowerBound != null && upperBound != null) { + return List.of(lowerBound, upperBound); + } + return lowerBound != null ? lowerBound : upperBound; + } + + public static Range between(String field, T lowerBound, T upperBound) { + return new Range<>(field, lowerBound, upperBound, false); + } + + public static Range from(String field, T lowerBound) { + return new Range<>(field, lowerBound, null, false); + } + + public static Range to(String field, T upperBound) { + return new Range<>(field, null, upperBound, false); + } + + public static Range above(String field, T lowerBound) { + return new Range<>(field, lowerBound, null, true); + } + + public static Range below(String field, T upperBound) { + return new Range<>(field, null, upperBound, true); + } +} diff --git a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java index 559960a41..bd1a64ed0 100644 --- a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java +++ b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java @@ -17,6 +17,8 @@ */ package jakarta.data.metamodel; +import jakarta.data.Restrict; +import jakarta.data.Restriction; import jakarta.data.Sort; /** @@ -49,4 +51,95 @@ public interface SortableAttribute extends Attribute { */ Sort desc(); + /** + * Creates a restriction for values greater than the specified value. + * + * @param value the lower bound (exclusive) for the attribute. + * @return a Restriction representing a greater-than condition. + */ + default Restriction greaterThan(Object value) { + return Restrict.greaterThan(value, name()); + } + + /** + * Creates a restriction for values greater than or equal to the specified value. + * + * @param value the lower bound (inclusive) for the attribute. + * @return a Restriction representing a greater-than-or-equal condition. + */ + default Restriction greaterThanEqual(Object value) { + return Restrict.greaterThanEqual(value, name()); + } + + /** + * Creates a restriction for values less than the specified value. + * + * @param value the upper bound (exclusive) for the attribute. + * @return a Restriction representing a less-than condition. + */ + default Restriction lessThan(Object value) { + return Restrict.lessThan(value, name()); + } + + /** + * Creates a restriction for values less than or equal to the specified value. + * + * @param value the upper bound (inclusive) for the attribute. + * @return a Restriction representing a less-than-or-equal condition. + */ + default Restriction lessThanOrEqual(Object value) { + return Restrict.lessThanEqual(value, name()); + } + + /** + * Creates a restriction for values within the specified range, inclusive of both bounds. + * + * @param lowerBound the lower bound (inclusive). + * @param upperBound the upper bound (inclusive). + * @return a Restriction representing a range condition. + */ + default Range between(T lowerBound, T upperBound) { + return Range.between(name(), lowerBound, upperBound); + } + + /** + * Creates a restriction for values greater than or equal to the specified lower bound. + * + * @param lowerBound the lower bound (inclusive). + * @return a Restriction representing a "greater than or equal" condition. + */ + default Range from(T lowerBound) { + return Range.from(name(), lowerBound); + } + + /** + * Creates a restriction for values less than or equal to the specified upper bound. + * + * @param upperBound the upper bound (inclusive). + * @return a Restriction representing a "less than or equal" condition. + */ + default Range to(T upperBound) { + return Range.to(name(), upperBound); + } + + /** + * Creates a restriction for values greater than the specified lower bound (exclusive). + * + * @param lowerBound the lower bound (exclusive). + * @return a Restriction representing a "greater than" condition. + */ + default Range above(T lowerBound) { + return Range.above(name(), lowerBound); + } + + /** + * Creates a restriction for values less than the specified upper bound (exclusive). + * + * @param upperBound the upper bound (exclusive). + * @return a Restriction representing a "less than" condition. + */ + default Range below(T upperBound) { + return Range.below(name(), upperBound); + } + } diff --git a/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java b/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java index b0a4f47a5..1e644833f 100644 --- a/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java +++ b/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java @@ -17,14 +17,14 @@ */ package jakarta.data.metamodel; +import jakarta.data.Sort; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import jakarta.data.Sort; - /** *

Annotates a class which serves as a static metamodel for an entity, enabling * type-safe access to entity attribute names and related objects such as instances diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java index c00390f09..351ab5ccd 100644 --- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java +++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java @@ -17,10 +17,16 @@ */ package jakarta.data.metamodel; +import jakarta.data.Operator; +import jakarta.data.Pattern; +import jakarta.data.Restrict; +import jakarta.data.Restriction; import jakarta.data.Sort; +import jakarta.data.metamodel.impl.BasicRestrictionRecord; + /** - * Represents an textual entity attribute in the {@link StaticMetamodel}. + * Represents a textual entity attribute in the {@link StaticMetamodel}. * * @param entity class of the static metamodel. */ @@ -40,4 +46,55 @@ public interface TextAttribute extends SortableAttribute { */ Sort descIgnoreCase(); + /** + * Creates a `LIKE` restriction for a match on the specified text. + * + * @param text the text to match. + * @return a Restriction representing a `LIKE` condition. + */ + default Restriction like(String text) { + return new BasicRestrictionRecord<>(name(), Operator.LIKE, text); + } + + /** + * Creates a `LIKE` restriction for a match on the specified pattern. + * + * @param pattern the `Pattern` instance to match. + * @return a Restriction representing a `LIKE` condition. + */ + default Restriction like(Pattern pattern) { + return new BasicRestrictionRecord<>(name(), Operator.LIKE, pattern.value()); + } + + /** + * Creates a `LIKE` restriction for values that start with the specified text. + * + * @param text the text prefix to match. + * @return a Restriction representing a prefix `LIKE` condition. + */ + default Restriction startsWith(String text) { + return Restrict.startsWith(text, name()); + } + + /** + * Creates a `LIKE` restriction for values that contain the specified substring. + * + * @param text the substring to match. + * @return a Restriction representing a substring `LIKE` condition. + */ + default Restriction contains(String text) { + return Restrict.contains(text, name()); + } + + + /** + * Creates a `LIKE` restriction for values that end with the specified text. + * + * @param text the text suffix to match. + * @return a Restriction representing a suffix `LIKE` condition. + */ + default Restriction endsWith(String text) { + return Restrict.endsWith(text, name()); + } + } diff --git a/api/src/main/java/jakarta/data/metamodel/impl/AttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/AttributeRecord.java index 31d329af7..48fc78bde 100644 --- a/api/src/main/java/jakarta/data/metamodel/impl/AttributeRecord.java +++ b/api/src/main/java/jakarta/data/metamodel/impl/AttributeRecord.java @@ -28,5 +28,6 @@ */ public record AttributeRecord(String name) implements Attribute { + } diff --git a/api/src/main/java/jakarta/data/metamodel/impl/BasicRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/BasicRestrictionRecord.java new file mode 100644 index 000000000..810d80343 --- /dev/null +++ b/api/src/main/java/jakarta/data/metamodel/impl/BasicRestrictionRecord.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data.metamodel.impl; + +import jakarta.data.Operator; +import jakarta.data.BasicRestriction; + +public record BasicRestrictionRecord(String field, Operator operator, Object value) implements BasicRestriction { + +} diff --git a/api/src/main/java/jakarta/data/metamodel/impl/SortableAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/SortableAttributeRecord.java index 73f1df791..ed8c15211 100644 --- a/api/src/main/java/jakarta/data/metamodel/impl/SortableAttributeRecord.java +++ b/api/src/main/java/jakarta/data/metamodel/impl/SortableAttributeRecord.java @@ -37,5 +37,6 @@ public Sort asc() { public Sort desc() { return Sort.desc(name); } + } diff --git a/api/src/main/java/jakarta/data/metamodel/impl/TextAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/TextAttributeRecord.java index 4fe325751..720df286d 100644 --- a/api/src/main/java/jakarta/data/metamodel/impl/TextAttributeRecord.java +++ b/api/src/main/java/jakarta/data/metamodel/impl/TextAttributeRecord.java @@ -47,4 +47,5 @@ public Sort ascIgnoreCase() { public Sort descIgnoreCase() { return Sort.descIgnoreCase(name); } + } diff --git a/api/src/main/java/jakarta/data/page/CursoredPage.java b/api/src/main/java/jakarta/data/page/CursoredPage.java index 272409bb8..d8b7a51c0 100644 --- a/api/src/main/java/jakarta/data/page/CursoredPage.java +++ b/api/src/main/java/jakarta/data/page/CursoredPage.java @@ -17,9 +17,10 @@ */ package jakarta.data.page; -import jakarta.data.repository.OrderBy; import jakarta.data.Order; import jakarta.data.Sort; +import jakarta.data.repository.OrderBy; + import java.util.NoSuchElementException; /** @@ -208,4 +209,4 @@ public interface CursoredPage extends Page { */ @Override PageRequest previousPageRequest(); -} \ No newline at end of file +} diff --git a/api/src/main/java/jakarta/data/repository/OrderBy.java b/api/src/main/java/jakarta/data/repository/OrderBy.java index cd8db4988..9b2044799 100644 --- a/api/src/main/java/jakarta/data/repository/OrderBy.java +++ b/api/src/main/java/jakarta/data/repository/OrderBy.java @@ -19,6 +19,7 @@ import jakarta.data.Order; import jakarta.data.Sort; + import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index f52e8862f..eec8ab95a 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -1066,6 +1066,7 @@ * of the repository operation.

*/ module jakarta.data { + requires java.management; exports jakarta.data; exports jakarta.data.metamodel; exports jakarta.data.metamodel.impl; @@ -1075,4 +1076,7 @@ exports jakarta.data.exceptions; opens jakarta.data.repository; exports jakarta.data.spi; -} \ No newline at end of file + opens jakarta.data.metamodel; + opens jakarta.data; + opens jakarta.data.metamodel.impl; +} diff --git a/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java b/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java new file mode 100644 index 000000000..55b020e69 --- /dev/null +++ b/api/src/test/java/jakarta/data/CompositeRestrictionRecordTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + + +class CompositeRestrictionRecordTest { + + @Test + void shouldCreateAllCompositeRestriction() { + Restriction restriction1 = Restrict.equalTo("value1", "field1"); + Restriction restriction2 = Restrict.lessThan(10, "field2"); + + CompositeRestrictionRecord composite = CompositeRestrictionRecord.all(restriction1, restriction2); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ALL); + soft.assertThat(composite.restrictions()).containsExactly(restriction1, restriction2); + }); + } + + @Test + void shouldCreateAnyCompositeRestriction() { + Restriction restriction1 = Restrict.greaterThan(5, "field1"); + Restriction restriction2 = Restrict.in(Set.of(1, 2, 3), "field2"); + + CompositeRestrictionRecord composite = CompositeRestrictionRecord.any(restriction1, restriction2); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ANY); + soft.assertThat(composite.restrictions()).containsExactly(restriction1, restriction2); + }); + } + + @Test + void shouldBeIterable() { + Restriction restriction1 = Restrict.equalTo("value1", "field1"); + Restriction restriction2 = Restrict.lessThan(10, "field2"); + + CompositeRestrictionRecord composite = CompositeRestrictionRecord.all(restriction1, restriction2); + + assertThat(composite).containsExactly(restriction1, restriction2); + } + + @Test + void shouldReturnImmutableRestrictionsList() { + Restriction restriction1 = Restrict.equalTo("value1", "field1"); + Restriction restriction2 = Restrict.lessThan(10, "field2"); + + CompositeRestrictionRecord composite = CompositeRestrictionRecord.all(restriction1, restriction2); + + assertThatThrownBy(() -> composite.restrictions().add(Restrict.greaterThan(15, "field3"))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void shouldSupportEmptyRestrictions() { + CompositeRestrictionRecord composite = CompositeRestrictionRecord.all(); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ALL); + soft.assertThat(composite.restrictions()).isEmpty(); + }); + } + + @Test + void shouldCombineDifferentRestrictionTypes() { + Restriction restriction1 = Restrict.contains("text", "field1"); + Restriction restriction2 = Restrict.startsWith("prefix", "field2"); + Restriction restriction3 = Restrict.endsWith("suffix", "field3"); + + CompositeRestrictionRecord composite = CompositeRestrictionRecord.all(restriction1, restriction2, restriction3); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ALL); + soft.assertThat(composite.restrictions()).containsExactly(restriction1, restriction2, restriction3); + }); + } +} diff --git a/api/src/test/java/jakarta/data/PatternTest.java b/api/src/test/java/jakarta/data/PatternTest.java new file mode 100644 index 000000000..1eba8222e --- /dev/null +++ b/api/src/test/java/jakarta/data/PatternTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class PatternTest { + + @Test + void shouldCreateExactMatchPattern() { + Pattern pattern = Pattern.is("Java Guide"); + assertThat(pattern.value()).isEqualTo("Java Guide"); + assertThat(pattern.caseSensitive()).isTrue(); + } + + @Test + void shouldCreateCustomMatchPattern() { + Pattern pattern = Pattern.matches("Ja%_a"); + assertThat(pattern.value()).isEqualTo("Ja%_a"); + assertThat(pattern.caseSensitive()).isTrue(); + } + + @Test + void shouldCreateCustomWildcardPattern() { + Pattern pattern = Pattern.matches("Ja?a*", '?', '*'); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(pattern.value()).isEqualTo("Ja_a%"); + soft.assertThat(pattern.caseSensitive()).isTrue(); + }); + } + + @Test + void shouldCreatePrefixMatchPattern() { + Pattern pattern = Pattern.startsWith("Hibernate"); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(pattern.value()).isEqualTo("Hibernate%"); + soft.assertThat(pattern.caseSensitive()).isTrue(); + }); + } + + @Test + void shouldCreateSuffixMatchPattern() { + Pattern pattern = Pattern.endsWith("Guide"); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(pattern.value()).isEqualTo("%Guide"); + soft.assertThat(pattern.caseSensitive()).isTrue(); + }); + } + + @Test + void shouldCreateSubstringMatchPattern() { + Pattern pattern = Pattern.contains("Java"); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(pattern.value()).isEqualTo("%Java%"); + soft.assertThat(pattern.caseSensitive()).isTrue(); + }); + } + + @ParameterizedTest + @CsvSource({ + "Jav_%,Jav\\_\\%", + "Hello%World,Hello\\%World", + "Special_Chars,Special\\_Chars" + }) + void shouldEscapeSpecialCharacters(String input, String expected) { + Pattern pattern = Pattern.is(input); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(pattern.value()).isEqualTo(expected); + soft.assertThat(pattern.caseSensitive()).isTrue(); + }); + } +} diff --git a/api/src/test/java/jakarta/data/RestrictTest.java b/api/src/test/java/jakarta/data/RestrictTest.java new file mode 100644 index 000000000..0d02a13aa --- /dev/null +++ b/api/src/test/java/jakarta/data/RestrictTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class RestrictTest { + + @Test + void shouldCreateBasicEqualityRestriction() { + Restriction restriction = Restrict.equalTo("Java Guide", "title"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("title"); + soft.assertThat(basic.operator()).isEqualTo(Operator.EQUAL); + soft.assertThat(basic.value()).isEqualTo("Java Guide"); + }); + } + + @Test + void shouldCreateLessThanRestriction() { + Restriction restriction = Restrict.lessThan(100, "price"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("price"); + soft.assertThat(basic.operator()).isEqualTo(Operator.LESS_THAN); + soft.assertThat(basic.value()).isEqualTo(100); + }); + } + + @Test + void shouldCreateGreaterThanRestriction() { + Restriction restriction = Restrict.greaterThan(2020, "year"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("year"); + soft.assertThat(basic.operator()).isEqualTo(Operator.GREATER_THAN); + soft.assertThat(basic.value()).isEqualTo(2020); + }); + } + + @Test + void shouldCreateInRestriction() { + Restriction restriction = Restrict.in(Set.of("Java", "Spring"), "title"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("title"); + soft.assertThat(basic.operator()).isEqualTo(Operator.IN); + soft.assertThat(basic.value()).isInstanceOf(Set.class); + Set values = (Set) basic.value(); + soft.assertThat(values).containsExactlyInAnyOrder("Java", "Spring"); + }); + } + + @Test + void shouldCreateContainsRestriction() { + Restriction restriction = Restrict.contains("Hibernate", "title"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("title"); + soft.assertThat(basic.operator()).isEqualTo(Operator.LIKE); + soft.assertThat(basic.value()).isEqualTo("%Hibernate%"); + }); + } + + @Test + void shouldCreateStartsWithRestriction() { + Restriction restriction = Restrict.startsWith("Hibernate", "title"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("title"); + soft.assertThat(basic.operator()).isEqualTo(Operator.LIKE); + soft.assertThat(basic.value()).isEqualTo("Hibernate%"); + }); + } + + @Test + void shouldCreateEndsWithRestriction() { + Restriction restriction = Restrict.endsWith("Guide", "title"); + + assertThat(restriction).isInstanceOf(BasicRestriction.class); + BasicRestriction basic = (BasicRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(basic.field()).isEqualTo("title"); + soft.assertThat(basic.operator()).isEqualTo(Operator.LIKE); + soft.assertThat(basic.value()).isEqualTo("%Guide"); + }); + } + + @Test + void shouldCreateCompositeAllRestriction() { + Restriction restriction = Restrict.all( + Restrict.equalTo("Java Guide", "title"), + Restrict.greaterThan(2020, "publicationYear") + ); + + assertThat(restriction).isInstanceOf(CompositeRestriction.class); + CompositeRestriction composite = (CompositeRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ALL); + soft.assertThat(composite.restrictions()).hasSize(2); + soft.assertThat(composite.restrictions().get(0)).isInstanceOf(BasicRestriction.class); + soft.assertThat(composite.restrictions().get(1)).isInstanceOf(BasicRestriction.class); + }); + } + + @Test + void shouldCreateCompositeAnyRestriction() { + Restriction restriction = Restrict.any( + Restrict.contains("Java", "title"), + Restrict.lessThan(500, "pages") + ); + + assertThat(restriction).isInstanceOf(CompositeRestriction.class); + CompositeRestriction composite = (CompositeRestriction) restriction; + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(composite.type()).isEqualTo(CompositeRestrictionType.ANY); + soft.assertThat(composite.restrictions()).hasSize(2); + soft.assertThat(composite.restrictions().get(0)).isInstanceOf(BasicRestriction.class); + soft.assertThat(composite.restrictions().get(1)).isInstanceOf(BasicRestriction.class); + }); + } +} diff --git a/api/src/test/java/jakarta/data/metamodel/RangeTest.java b/api/src/test/java/jakarta/data/metamodel/RangeTest.java new file mode 100644 index 000000000..8de44cc8f --- /dev/null +++ b/api/src/test/java/jakarta/data/metamodel/RangeTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data.metamodel; + +import jakarta.data.Operator; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RangeTest { + + + @Test + void shouldCreateInclusiveBetweenRange() { + Range range = Range.between("publicationDate", LocalDate.of(2020, 1, 1), LocalDate.of(2023, 1, 1)); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("publicationDate"); + soft.assertThat(range.operator()).isEqualTo(Operator.BETWEEN); + soft.assertThat(range.value()).isEqualTo(List.of(LocalDate.of(2020, 1, 1), LocalDate.of(2023, 1, 1))); + }); + } + + @Test + void shouldCreateInclusiveFromRange() { + Range range = Range.from("rating", 4.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("rating"); + soft.assertThat(range.operator()).isEqualTo(Operator.GREATER_THAN_EQUAL); + soft.assertThat(range.value()).isEqualTo(4.0); + }); + } + + @Test + void shouldCreateInclusiveToRange() { + Range range = Range.to("price", 100); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("price"); + soft.assertThat(range.operator()).isEqualTo(Operator.LESS_THAN_EQUAL); + soft.assertThat(range.value()).isEqualTo(100); + }); + } + + @Test + void shouldCreateExclusiveAboveRange() { + Range range = Range.above("rating", 4.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("rating"); + soft.assertThat(range.operator()).isEqualTo(Operator.GREATER_THAN); + soft.assertThat(range.value()).isEqualTo(4.0); + }); + } + + @Test + void shouldCreateExclusiveBelowRange() { + Range range = Range.below("price", 200.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("price"); + soft.assertThat(range.operator()).isEqualTo(Operator.LESS_THAN); + soft.assertThat(range.value()).isEqualTo(200.0); + }); + } + + @Test + void shouldHandleNullLowerBoundForToRange() { + Range range = Range.to("price", 200.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("price"); + soft.assertThat(range.operator()).isEqualTo(Operator.LESS_THAN_EQUAL); + soft.assertThat(range.lowerBound()).isNull(); + soft.assertThat(range.upperBound()).isEqualTo(200.0); + }); + } + + @Test + void shouldHandleNullUpperBoundForFromRange() { + Range range = Range.from("price", 50.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("price"); + soft.assertThat(range.operator()).isEqualTo(Operator.GREATER_THAN_EQUAL); + soft.assertThat(range.lowerBound()).isEqualTo(50.0); + soft.assertThat(range.upperBound()).isNull(); + }); + } + + @Test + void shouldHandleBothBoundsForBetweenRange() { + Range range = Range.between("price", 50.0, 200.0); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("price"); + soft.assertThat(range.operator()).isEqualTo(Operator.BETWEEN); + soft.assertThat(range.lowerBound()).isEqualTo(50.0); + soft.assertThat(range.upperBound()).isEqualTo(200.0); + }); + } + + @Test + void shouldHandleOpenRangeForAbove() { + Range range = Range.above("age", 18); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("age"); + soft.assertThat(range.operator()).isEqualTo(Operator.GREATER_THAN); + soft.assertThat(range.lowerBound()).isEqualTo(18); + soft.assertThat(range.upperBound()).isNull(); + soft.assertThat(range.open()).isTrue(); + }); + } + + @Test + void shouldHandleOpenRangeForBelow() { + Range range = Range.below("age", 65); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(range.field()).isEqualTo("age"); + soft.assertThat(range.operator()).isEqualTo(Operator.LESS_THAN); + soft.assertThat(range.lowerBound()).isNull(); + soft.assertThat(range.upperBound()).isEqualTo(65); + soft.assertThat(range.open()).isTrue(); + }); + } +} diff --git a/api/src/test/java/jakarta/data/metamodel/impl/BasicRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/impl/BasicRestrictionRecordTest.java new file mode 100644 index 000000000..b151341f4 --- /dev/null +++ b/api/src/test/java/jakarta/data/metamodel/impl/BasicRestrictionRecordTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package jakarta.data.metamodel.impl; + +import jakarta.data.Operator; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + + +class BasicRestrictionRecordTest { + @Test + void shouldCreateBasicEqualityRestriction() { + BasicRestrictionRecord restriction = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide"); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(restriction.field()).isEqualTo("title"); + soft.assertThat(restriction.operator()).isEqualTo(Operator.EQUAL); + soft.assertThat(restriction.value()).isEqualTo("Java Guide"); + }); + } + + @Test + void shouldCreateGreaterThanRestriction() { + BasicRestrictionRecord restriction = new BasicRestrictionRecord<>("price", Operator.GREATER_THAN, 100); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(restriction.field()).isEqualTo("price"); + soft.assertThat(restriction.operator()).isEqualTo(Operator.GREATER_THAN); + soft.assertThat(restriction.value()).isEqualTo(100); + }); + } + + @Test + void shouldCreateLessThanRestriction() { + BasicRestrictionRecord restriction = new BasicRestrictionRecord<>("quantity", Operator.LESS_THAN, 50); + + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(restriction.field()).isEqualTo("quantity"); + soft.assertThat(restriction.operator()).isEqualTo(Operator.LESS_THAN); + soft.assertThat(restriction.value()).isEqualTo(50); + }); + } + +}