Repository: jakartaee/data
Pull request #970 (2025-02-12 21:10:29)

introduce BasicAttribute, ComparableAttribute to make restrictions typesafe
See explanation here: https://github.com/jakartaee/data/issues/829#issuecomment-2654769536


Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/Attribute.java b/api/src/main/java/jakarta/data/metamodel/Attribute.java
index 0d3b91bd..22622674 100644
--- a/api/src/main/java/jakarta/data/metamodel/Attribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/Attribute.java
@@ -17,11 +17,6 @@
  */
 package jakarta.data.metamodel;
 
-import java.util.Set;
-
-import jakarta.data.metamodel.restrict.Restrict;
-import jakarta.data.metamodel.restrict.Restriction;
-
 /**
  * Represents an entity attribute in the {@link StaticMetamodel}.
  *
@@ -29,41 +24,11 @@ import jakarta.data.metamodel.restrict.Restriction;
  */
 public interface Attribute<T> {
 
-    default Restriction<T> equalTo(Object value) {
-        return Restrict.equalTo(value, name());
-    }
-
-    default Restriction<T> in(Object... values) {
-        if (values == null || values.length == 0)
-            throw new IllegalArgumentException("values are required");
-
-        return Restrict.in(Set.of(values), name());
-    }
-
-    default Restriction<T> isNull() {
-        return Restrict.equalTo(null, name());
-    }
-
     /**
      * Obtain the entity attribute name, suitable for use wherever the specification requires
-     * an entity attribute name. For example, as the parameter to {@link Sort#asc(String)}.
+     * an entity attribute name. For example, as the parameter to {@link jakarta.data.Sort#asc(String)}.
      *
      * @return the entity attribute name.
      */
     String name();
-
-    default Restriction<T> notEqualTo(Object value) {
-        return Restrict.notEqualTo(value, name());
-    }
-
-    default Restriction<T> notIn(Object... values) {
-        if (values == null || values.length == 0)
-            throw new IllegalArgumentException("values are required");
-
-        return Restrict.notIn(Set.of(values), name());
-    }
-
-    default Restriction<T> notNull() {
-        return Restrict.notEqualTo(null, name());
-    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
new file mode 100644
index 00000000..58498b3d
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023,2025 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.metamodel.restrict.Restrict;
+import jakarta.data.metamodel.restrict.Restriction;
+
+import java.util.Set;
+
+public interface BasicAttribute<T,V> extends Attribute<T> {
+
+    default Restriction<T> equalTo(V value) {
+        return Restrict.equalTo(value, name());
+    }
+
+    default Restriction<T> in(Set<V> values) {
+        if (values == null || values.isEmpty())
+            throw new IllegalArgumentException("values are required");
+
+        return Restrict.in(values, name());
+    }
+
+    default Restriction<T> isNull() {
+        //TODO: fix this "= NULL" is not the same thing as "IS NULL" in JDQL
+        return Restrict.equalTo(null, name());
+    }
+
+    default Restriction<T> notEqualTo(V value) {
+        return Restrict.notEqualTo(value, name());
+    }
+
+    default Restriction<T> notIn(Set<V> values) {
+        if (values == null || values.isEmpty())
+            throw new IllegalArgumentException("values are required");
+
+        return Restrict.notIn(values, name());
+    }
+
+    default Restriction<T> notNull() {
+        //TODO: fix this "<> NULL" is not the same thing as "IS NOT NULL" in JDQL
+        return Restrict.notEqualTo(null, name());
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
new file mode 100644
index 00000000..10cc6fab
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023,2025 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.metamodel.restrict.Restrict;
+import jakarta.data.metamodel.restrict.Restriction;
+
+public interface ComparableAttribute<T,V extends Comparable<V>>
+        extends BasicAttribute<T,V>, SortableAttribute<T> {
+
+    default Restriction<T> between(V min, V max) {
+        return Restrict.between(min, max, name());
+    }
+
+    default Restriction<T> greaterThan(V value) {
+        return Restrict.greaterThan(value, name());
+    }
+
+    default Restriction<T> greaterThanEqual(V value) {
+        return Restrict.greaterThanEqual(value, name());
+    }
+
+    default Restriction<T> lessThan(V value) {
+        return Restrict.lessThan(value, name());
+    }
+
+    default Restriction<T> lessThanEqual(V value) {
+        return Restrict.lessThanEqual(value, name());
+    }
+
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
index 47cee815..2081c44e 100644
--- a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
@@ -18,8 +18,6 @@
 package jakarta.data.metamodel;
 
 import jakarta.data.Sort;
-import jakarta.data.metamodel.restrict.Restrict;
-import jakarta.data.metamodel.restrict.Restriction;
 
 /**
  * Represents a sortable entity attribute in the {@link StaticMetamodel}.
@@ -30,7 +28,7 @@ import jakarta.data.metamodel.restrict.Restriction;
  * <li>enum attributes</li>
  * <li>time attributes</li>
  * <li>boolean attributes</li>
- * <li>{@link TextAttribute textual attributes}</li>
+ * <li>{@linkplain TextAttribute textual attributes}</li>
  * </ul>
  *
  * @param <T> entity class of the static metamodel.
@@ -44,10 +42,6 @@ public interface SortableAttribute<T> extends Attribute<T> {
      */
     Sort<T> asc();
 
-    default <V extends Comparable<V>> Restriction<T> between(V min, V max) {
-        return Restrict.between(min, max, name());
-    }
-
     /**
      * Obtain a request for a descending {@link Sort} based on the entity attribute.
      *
@@ -55,19 +49,4 @@ public interface SortableAttribute<T> extends Attribute<T> {
      */
     Sort<T> desc();
 
-    default <V extends Comparable<V>> Restriction<T> greaterThan(V value) {
-        return Restrict.greaterThan(value, name());
-    }
-
-    default <V extends Comparable<V>> Restriction<T> greaterThanEqual(V value) {
-        return Restrict.greaterThanEqual(value, name());
-    }
-
-    default <V extends Comparable<V>> Restriction<T> lessThan(V value) {
-        return Restrict.lessThan(value, name());
-    }
-
-    default <V extends Comparable<V>> Restriction<T> lessThanEqual(V value) {
-        return Restrict.lessThanEqual(value, name());
-    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
index 3beb362e..51c34800 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
@@ -26,12 +26,12 @@ import jakarta.data.metamodel.restrict.TextRestriction;
  *
  * @param <T> entity class of the static metamodel.
  */
-public interface TextAttribute<T> extends SortableAttribute<T> {
+public interface TextAttribute<T> extends ComparableAttribute<T,String> {
 
     /**
-     * Obtain a request for an ascending, case insensitive {@link Sort} based on the entity attribute.
+     * Obtain a request for an ascending, case-insensitive {@link Sort} based on the entity attribute.
      *
-     * @return a request for an ascending, case insensitive sort on the entity attribute.
+     * @return a request for an ascending, case-insensitive sort on the entity attribute.
      */
     Sort<T> ascIgnoreCase();
 
diff --git a/api/src/main/java/jakarta/data/metamodel/impl/BasicAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/BasicAttributeRecord.java
new file mode 100644
index 00000000..bbae3ca1
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/impl/BasicAttributeRecord.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025 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.metamodel.BasicAttribute;
+
+public record BasicAttributeRecord<T,V>(String name)
+        implements BasicAttribute<T,V> {
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/impl/ComparableAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/impl/ComparableAttributeRecord.java
new file mode 100644
index 00000000..5c6e93c2
--- /dev/null
+++ b/api/src/main/java/jakarta/data/metamodel/impl/ComparableAttributeRecord.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2025 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.Sort;
+import jakarta.data.metamodel.ComparableAttribute;
+
+public record ComparableAttributeRecord<T,V extends Comparable<V>>(String name)
+        implements ComparableAttribute<T,V> {
+    @Override
+    public Sort<T> asc() {
+        return Sort.asc(name);
+    }
+
+    @Override
+    public Sort<T> desc() {
+        return Sort.desc(name);
+    }
+}
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java b/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
index cd4a89e7..26b72038 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Restrict.java
@@ -95,7 +95,7 @@ public class Restrict {
         return new TextRestrictionRecord<>(attribute, Operator.GREATER_THAN_EQUAL, value);
     }
 
-    public static <T> Restriction<T> in(Set<Object> values, String attribute) {
+    public static <T> Restriction<T> in(Set<?> values, String attribute) {
         return new BasicRestrictionRecord<>(attribute, Operator.IN, values);
     }
 
@@ -156,7 +156,7 @@ public class Restrict {
         return new TextRestrictionRecord<>(attribute, Operator.NOT_LIKE, ESCAPED, pattern);
     }
 
-    public static <T> Restriction<T> notIn(Set<Object> values, String attribute) {
+    public static <T> Restriction<T> notIn(Set<?> values, String attribute) {
         return new BasicRestrictionRecord<>(attribute, Operator.NOT_IN, values);
     }
 


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
index 2073dc64..304ee982 100644
--- a/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/AttributeTest.java
@@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class AttributeTest {
 
-    private final Attribute<String> testAttribute = () -> "testAttribute";
+    private final BasicAttribute<String, String> testAttribute = () -> "testAttribute";
     @Test
     void shouldCreateEqualToRestriction() {
         Restriction<String> restriction = testAttribute.equalTo("testValue");
@@ -59,7 +59,7 @@ class AttributeTest {
 
     @Test
     void shouldCreateInRestriction() {
-        Restriction<String> restriction = testAttribute.in("value1", "value2");
+        Restriction<String> restriction = testAttribute.in(Set.of("value1", "value2"));
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
@@ -72,14 +72,14 @@ class AttributeTest {
 
     @Test
     void shouldThrowExceptionForEmptyInRestriction() {
-        assertThatThrownBy(() -> testAttribute.in())
+        assertThatThrownBy(() -> testAttribute.in(Set.of()))
                 .isInstanceOf(IllegalArgumentException.class)
                 .hasMessage("values are required");
     }
 
     @Test
     void shouldCreateNotInRestriction() {
-        Restriction<String> restriction = testAttribute.notIn("value1", "value2");
+        Restriction<String> restriction = testAttribute.notIn(Set.of("value1", "value2"));
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(restriction).isInstanceOf(BasicRestriction.class);
@@ -92,7 +92,7 @@ class AttributeTest {
 
     @Test
     void shouldThrowExceptionForEmptyNotInRestriction() {
-        assertThatThrownBy(() -> testAttribute.notIn())
+        assertThatThrownBy(() -> testAttribute.notIn(Set.of()))
                 .isInstanceOf(IllegalArgumentException.class)
                 .hasMessage("values are required");
     }
diff --git a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
index 5b484ea7..f5e9ce68 100644
--- a/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/SortableAttributeTest.java
@@ -30,7 +30,7 @@ import org.junit.jupiter.api.Test;
 class SortableAttributeTest {
 
     //it ignores the implementation of the SortableAttribute interface and uses an anonymous class to test the methods
-    private final SortableAttribute<String> testAttribute = new SortableAttribute<String>() {
+    private final ComparableAttribute<String,Integer> testAttribute = new ComparableAttribute<>() {
         @Override
         public Sort<String> asc() {
            throw new UnsupportedOperationException("It is not the focus of this test");