Repository: jakartaee/data
Pull request #976 (2025-02-14 22:34:19)

ComparableAttribute and BasicAttribute are preferred
This PR contains updates to make `ComparableAttribute` and `BasicAttribute` be preferred for the static metamodel over `SortableAttribute` and `Attribute`.

`Attribute` is now documented as the common supertype that all metamodel attribute interfaces inherit from.
`SortableAttribute` is discouraged for most usage.
Spec and JavaDoc examples are updated to show ComparableAttribute and BasicAttribute instead.
Elsewhere in spec and JavaDoc, we now list the provided attribute subtypes, along with what they are for, further promoting the use of TextAttribute/ComparableAttribute/BasicAttribute.

As a second commit in this PR, I moved the new internal implementation classes out of the public package where they would be visible as part of the API (it was a mistake that we ever allowed others there, but it was too late in the release cycle to correct it) and put them as package protected under the interface so that they will not be externalized to the user.  Instead, the proper way to obtain instances is with the *Attribute.of(name) method. See examples that were also updated. This is cleaner and avoids externalizing implementation, which not only avoids cluttering the API, but also preserves our flexibility in the future to make changes to those implementations that would otherwise be considered a breaking change.  I would like to eventually deprecate the .impl package altogether, but I did not do that in this PR.


njr-11: Merge conflicts have been resolved so this PR is ready for its review to be completed.  The prior review comments should be resolved now as well.

Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/Attribute.java b/api/src/main/java/jakarta/data/metamodel/Attribute.java
index 22622674..a7166408 100644
--- a/api/src/main/java/jakarta/data/metamodel/Attribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/Attribute.java
@@ -18,7 +18,21 @@
 package jakarta.data.metamodel;
 
 /**
- * Represents an entity attribute in the {@link StaticMetamodel}.
+ * <p>Supertype for {@link StaticMetamodel} fields representing entity attributes.</p>
+ *
+ * <p>The following subtypes are provided:</p>
+ * <ul>
+ * <li>{@link TextAttribute} for entity attributes that represent text,
+ *     typically of type {@link String}.</li>
+ * <li>{@link ComparableAttribute} for entity attributes that represent other
+ *     sortable and comparable values, such as {@code int}, {@link Long},
+ *     {@code boolean}, {@link java.time.LocalDateTime}, and enumerations.</li>
+ * <li>{@link SortableAttribute} for entity types that are sortable, but not
+ *     comparable. Generally this subtype is unused but is applicable for
+ *     databases that allow sorting on {@code byte[]} attributes. </li>
+ * <li>{@link BasicAttribute} for other types of entity attributes, such as
+ *     collections, embeddables, and other relation attributes.</li>
+ * </ul>
  *
  * @param <T> entity class of the static metamodel.
  */
diff --git a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
index 61fc4075..85e805de 100644
--- a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023,2025 Contributors to the Eclipse Foundation
+ * 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.
@@ -21,8 +21,16 @@ import jakarta.data.metamodel.constraint.Constraint;
 import jakarta.data.metamodel.restrict.Restrict;
 import jakarta.data.metamodel.restrict.Restriction;
 
+import java.util.Objects;
 import java.util.Set;
 
+/**
+ * <p>Represents an entity attribute in the {@link StaticMetamodel}
+ * that is neither sortable nor comparable.</p>
+ *
+ * @param <T> entity class of the static metamodel.
+ * @param <V> type of entity attribute (or wrapper type if primitive).
+ */
 public interface BasicAttribute<T,V> extends Attribute<T> {
 
     default Restriction<T> equalTo(V value) {
@@ -55,8 +63,38 @@ public interface BasicAttribute<T,V> extends Attribute<T> {
         return Restrict.notNull(name());
     }
 
+    /**
+     * <p>Creates a static metamodel {@code BasicAttribute} representing the
+     * entity attribute with the specified name.</p>
+     *
+     * @param <T> entity class of the static metamodel.
+     * @param <V> type of entity attribute (or wrapper type if primitive).
+     * @param entityClass   the entity class.
+     * @param name          the name of the entity attribute.
+     * @param attributeType type of the entity attribute.
+     * @return instance of {@code BasicAttribute}.
+     */
+    static <T,V> BasicAttribute<T,V> of(Class<T> entityClass,
+                                        String name,
+                                        Class<V> attributeType) {
+        Objects.requireNonNull(entityClass, "entity class is required");
+        Objects.requireNonNull(name, "entity attribute name is required");
+        Objects.requireNonNull(attributeType, "entity attribute type is required");
+
+        return new BasicAttributeRecord<>(name);
+    }
+
     default Restriction<T> restrict(Constraint<V> constraint) {
         return Restrict.restrict(constraint, name());
     }
+}
 
+/**
+ * Hidden internal implementation of BasicAttribute.
+ *
+ * @param <T> entity class of the static metamodel.
+ * @param <V> type of entity attribute (or wrapper type if primitive).
+ */
+record BasicAttributeRecord<T,V>(String name)
+    implements BasicAttribute<T,V> {
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
index c145bf5d..702ffc73 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023,2025 Contributors to the Eclipse Foundation
+ * 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.
@@ -17,9 +17,43 @@
  */
 package jakarta.data.metamodel;
 
+import java.util.Objects;
+
+import jakarta.data.Sort;
 import jakarta.data.metamodel.restrict.Restrict;
 import jakarta.data.metamodel.restrict.Restriction;
 
+/**
+ * <p>Represents a comparable entity attribute in the {@link StaticMetamodel}.
+ * Comparable entity attributes can be sorted on in query results and can be
+ * compared against values in query restrictions.</p>
+ *
+ * <p>Entity attribute types that are comparable include:</p>
+ *
+ * <ul>
+ * <li>numeric attributes, such as {@code long}, {@link Float}, and
+ *     {@link java.math.BigInteger}</li>
+ * <li>time attributes, such as {@link java.time.LocalDateTime} and
+ *     {@link java.time.Instant}</li>
+ * <li>boolean attributes: {@code boolean} and {@link Boolean}</li>
+ * <li>enum attributes - Note that it is provider-specific whether order is
+ *     based on {@link Enum#ordinal()} or {@link Enum#name()}.
+ *     The Jakarta Persistence default of {@code ordinal} can be overridden
+ *     with the {@code jakarta.persistence.Enumerated} annotation.</li>
+ * <li>textual attributes - Use the {@link TextAttribute} subtype instead</li>
+ * </ul>
+ *
+ * <p>Primitive types such as {@code int} and {@code float} are considered
+ * comparable even though they do not implement the {@link Comparable}
+ * interface because the corresponding wrapper types, such as {@link Integer},
+ * do implement {@code Comparable}.</p>
+ *
+ * <p>Where possible, {@code ComparableAttribute}, which provides more function,
+ * is preferred over {@link SortableAttribute}.</p>
+ *
+ * @param <T> entity class of the static metamodel.
+ * @param <V> type of entity attribute (or wrapper type if primitive).
+ */
 public interface ComparableAttribute<T,V extends Comparable<V>>
         extends BasicAttribute<T,V>, SortableAttribute<T> {
 
@@ -47,4 +81,45 @@ public interface ComparableAttribute<T,V extends Comparable<V>>
         return Restrict.lessThanEqual(value, name());
     }
 
+    /**
+     * <p>Creates a static metamodel {@code ComparableAttribute} representing the
+     * entity attribute with the specified name.</p>
+     *
+     * @param <T> entity class of the static metamodel.
+     * @param <V> type of entity attribute (or wrapper type if primitive).
+     * @param entityClass   the entity class.
+     * @param name          the name of the entity attribute.
+     * @param attributeType type of the entity attribute.
+     * @return instance of {@code ComparableAttribute}.
+     */
+    static <T, V extends Comparable<V>> ComparableAttribute<T, V> of(
+            Class<T> entityClass,
+            String name,
+            Class<V> attributeType) {
+        Objects.requireNonNull(entityClass, "entity class is required");
+        Objects.requireNonNull(name, "entity attribute name is required");
+        Objects.requireNonNull(attributeType, "entity attribute type is required");
+
+        return new ComparableAttributeRecord<>(name);
+    }
 }
+
+/**
+ * Hidden internal implementation of ComparableAttribute.
+ *
+ * @param <T> entity class of the static metamodel.
+ * @param <V> type of entity attribute (or wrapper type if primitive).
+ */
+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);
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
index 2081c44e..3f08fc47 100644
--- a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
@@ -17,19 +17,22 @@
  */
 package jakarta.data.metamodel;
 
+import java.util.Objects;
+
 import jakarta.data.Sort;
+import jakarta.data.metamodel.impl.SortableAttributeRecord;
 
 /**
- * Represents a sortable entity attribute in the {@link StaticMetamodel}.
- * Entity attribute types that are sortable include:
+ * <p>Represents a entity attribute in the {@link StaticMetamodel}
+ * that is sortable, but not comparable.</p>
  *
- * <ul>
- * <li>numeric attributes</li>
- * <li>enum attributes</li>
- * <li>time attributes</li>
- * <li>boolean attributes</li>
- * <li>{@linkplain TextAttribute textual attributes}</li>
- * </ul>
+ *<p>A {@code SortableAttribute} may be used to sort query results.
+ * When an attribute type (or if a primitive, its wrapper class)
+ * is a subtype of {@link java.lang.Comparable},
+ * use of {@link ComparableAttribute} is usually preferred, since a
+ * {@code SortableAttribute} cannot be used in order-based query
+ * restrictions. Direct use of {@code SortableAttribute} is appropriate 
+ * for attributes of type {@code byte[]}.</p>
  *
  * @param <T> entity class of the static metamodel.
  */
@@ -49,4 +52,24 @@ public interface SortableAttribute<T> extends Attribute<T> {
      */
     Sort<T> desc();
 
+    /**
+     * <p>Creates a static metamodel {@code SortableAttribute} representing the
+     * entity attribute with the specified name.</p>
+     *
+     * @param <T> entity class of the static metamodel.
+     * @param <V> type of entity attribute (or wrapper type if primitive).
+     * @param entityClass   the entity class.
+     * @param name          the name of the entity attribute.
+     * @param attributeType type of the entity attribute.
+     * @return instance of {@code SortableAttribute}.
+     */
+    static <T, V> SortableAttribute<T> of(Class<T> entityClass,
+                                          String name,
+                                          Class<V> attributeType) {
+        Objects.requireNonNull(entityClass, "entity class is required");
+        Objects.requireNonNull(name, "entity attribute name is required");
+        Objects.requireNonNull(attributeType, "entity attribute type is required");
+
+        return new SortableAttributeRecord<>(name);
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java b/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java
index b029f0b6..d2b79cf0 100644
--- a/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java
+++ b/api/src/main/java/jakarta/data/metamodel/StaticMetamodel.java
@@ -33,13 +33,26 @@ import jakarta.data.Sort;
  * The type of each of these fields must be either {@link String}, {@link Attribute},
  * or a subinterface of {@code Attribute} defined in this package.</p>
  *
+ * <p>The following subinterfaces of {@code Attribute} are recommended to obtain
+ * the full benefit of the static metamodel:</p>
+ * <ul>
+ * <li>{@link TextAttribute} for entity attributes that represent text,
+ *     typically of type {@link String}.</li>
+ * <li>{@link ComparableAttribute} for entity attributes that represent other
+ *     sortable and comparable values, such as {@code int}, {@link Double},
+ *     {@code boolean}, {@link java.time.LocalDateTime}, and enumerations.</li>
+ * <li>{@link BasicAttribute} for other types of entity attributes, such as
+ *     collections, embeddables, and other relation attributes.</li>
+ * </ul>
+ *
  * <p>Jakarta Data defines the following conventions for static metamodel classes:</p>
  * <ul>
  * <li>The metamodel class can be an interface or concrete class.</li>
  * <li>The name of the static metamodel class should consist of underscore ({@code _})
  *     followed by the entity class name.</li>
  * <li>Fields of type {@code String} should be named with all upper case.</li>
- * <li>Fields of type {@code Attribute} should be named in lower case or mixed case.</li>
+ * <li>Fields that are subtypes of {@code Attribute} should be named in lower case
+ *     or mixed case.</li>
  * </ul>
  *
  * <p>For example, for the following entity,</p>
@@ -74,11 +87,16 @@ import jakarta.data.Sort;
  *     String NAME_LAST = "name.last";
  *     String YEAROFBIRTH = "yearOfBirth";
  *
- *     SortableAttribute&lt;Person&gt; ssn = new SortableAttributeRecord&lt;&gt;(SSN);
- *     Attribute&lt;Person&gt; name = new AttributeRecord&lt;&gt;(NAME);
- *     TextAttribute&lt;Person&gt; name_first = new TextAttributeRecord&lt;&gt;(NAME_FIRST);
- *     TextAttribute&lt;Person&gt; name_last = new TextAttributeRecord&lt;&gt;(NAME_LAST);
- *     SortableAttribute&lt;Person&gt; yearOfBirth = new SortableAttributeRecord&lt;&gt;(YEAROFBIRTH);
+ *     ComparableAttribute&lt;Person,Long&gt; ssn = ComparableAttribute.of(
+ *             Person.class, SSN, long.class);
+ *     BasicAttribute&lt;Person,Name&gt; name = BasicAttribute.of(
+ *             Person.class, NAME, Name.class);
+ *     TextAttribute&lt;Person&gt; name_first = TextAttribute.of(
+ *             Person.class, NAME_FIRST);
+ *     TextAttribute&lt;Person&gt; name_last = TextAttribute.of(
+ *             Person.class, NAME_LAST);
+ *     ComparableAttribute&lt;Person,Integer&gt; yearOfBirth = ComparableAttribute.of(
+ *             Person.class, YEAROFBIRTH, int.class);
  * }
  * </pre>
  *
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
index 5157da1f..ff2c75ca 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttribute.java
@@ -17,8 +17,11 @@
  */
 package jakarta.data.metamodel;
 
+import java.util.Objects;
+
 import jakarta.data.Sort;
 import jakarta.data.metamodel.constraint.Like;
+import jakarta.data.metamodel.impl.TextAttributeRecord;
 import jakarta.data.metamodel.restrict.Restrict;
 import jakarta.data.metamodel.restrict.TextRestriction;
 
@@ -97,6 +100,22 @@ public interface TextAttribute<T> extends ComparableAttribute<T,String> {
         return Restrict.notEndsWith(suffix, name());
     }
 
+    /**
+     * <p>Creates a static metamodel {@code TextAttribute} representing the
+     * entity attribute with the specified name.</p>
+     *
+     * @param <T> entity class of the static metamodel.
+     * @param entityClass the entity class.
+     * @param name        the name of the entity attribute.
+     * @return instance of {@code TextAttribute}.
+     */
+    static <T> TextAttribute<T> of(Class<T> entityClass, String name) {
+        Objects.requireNonNull(entityClass, "entity class is required");
+        Objects.requireNonNull(name, "entity attribute name is required");
+
+        return new TextAttributeRecord<>(name);
+    }
+
     default TextRestriction<T> startsWith(String prefix) {
         return Restrict.startsWith(prefix, name());
     }
diff --git a/api/src/main/java/jakarta/data/metamodel/package-info.java b/api/src/main/java/jakarta/data/metamodel/package-info.java
index edc49210..68663918 100644
--- a/api/src/main/java/jakarta/data/metamodel/package-info.java
+++ b/api/src/main/java/jakarta/data/metamodel/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023,2024 Contributors to the Eclipse Foundation
+ * 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.
@@ -39,9 +39,12 @@
  *     String NAME = "name";
  *     String PRICE = "price";
  *
- *     SortableAttribute&lt;Product&gt; id = new SortableAttributeRecord&lt;&gt;(ID);
- *     TextAttribute&lt;Product&gt; name = new TextAttributeRecord&lt;&gt;(NAME);
- *     SortableAttribute&lt;Product&gt; price = new SortableAttributeRecord&lt;&gt;(PRICE);
+ *     ComparableAttribute&lt;Product,Long&gt; id = ComparableAttribute.of(
+ *             Product.class, ID, long.class);
+ *     TextAttribute&lt;Product&gt; name = TextAttribute.of(
+ *             Product.class, NAME);
+ *     ComparableAttribute&lt;Product,Float&gt; price = ComparableAttribute.of(
+ *             Product.class, PRICE, float.class);
  * }
  *
  * ...
diff --git a/spec/src/main/asciidoc/entity.asciidoc b/spec/src/main/asciidoc/entity.asciidoc
index 75b96329..18c749e1 100644
--- a/spec/src/main/asciidoc/entity.asciidoc
+++ b/spec/src/main/asciidoc/entity.asciidoc
@@ -400,7 +400,7 @@ When an application programmer writes a static metamodel class for an entity by
 
 The static metamodel class is not required to include a field for every persistent attribute of the entity.
 
-A convenience implementation of each subinterface of `Attribute` is provided in the package `jakarta.data.metamodel.impl`.
+A convenience implementation of each subinterface of `Attribute` is provided by the `.of` method of each subinterface.
 
 ==== Compile-time Annotation Processor Requirements for a Metamodel Class
 
@@ -423,6 +423,12 @@ The following are conventions for static metamodel classes:
 - Initialized fields must have modifiers `public`, `static`, and `final`.
 - Fields of type `String` must always be statically initialized, enabling their use in annotation values.
 
+The following subinterfaces of `Attribute` are recommended to obtain the full benefit of the static metamodel:
+
+- `TextAttribute` for entity attributes that represent text, typically of type `String`.
+- `ComparableAttribute` for entity attributes that represent other comparable values, such as `long`, `Integer`, `boolean`, `java.time.Instant`, and enumerations.
+- `BasicAttribute` for other types of entity attributes, such as collections, embeddables, and other relation attributes.
+
 ==== Example Metamodel Class and Usage
 
 Example entity class:
@@ -442,14 +448,17 @@ Example metamodel class for the entity:
 [source,java]
 ----
 @StaticMetamodel(Product.class)
-public class _Product {
-  public static final String ID = "id";
-  public static final String NAME = "name";
-  public static final String PRICE = "price";
-
-  public static final SortableAttribute<Product> id = new SortableAttributeRecord<>("id");
-  public static final TextAttribute<Product> name = new TextAttributeRecord<>("name");
-  public static final SortableAttribute<Product> price = new SortableAttributeRecord<>("price");
+public interface _Product {
+  String ID = "id";
+  String NAME = "name";
+  String PRICE = "price";
+
+  ComparableAttribute<Product,Long> id = ComparableAttribute.of(
+          Product.class, ID, long.class);
+  TextAttribute<Product> name = TextAttribute.of(
+          Product.class, NAME);
+  ComparableAttribute<Product,Float> price = ComparableAttribute.of(
+          Product.class, PRICE, float.class);
 }
 ----
 


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
index f778265a..737fae60 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/RestrictTest.java
@@ -22,10 +22,8 @@ import jakarta.data.metamodel.constraint.Like;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
-import jakarta.data.metamodel.SortableAttribute;
+import jakarta.data.metamodel.ComparableAttribute;
 import jakarta.data.metamodel.TextAttribute;
-import jakarta.data.metamodel.impl.SortableAttributeRecord;
-import jakarta.data.metamodel.impl.TextAttributeRecord;
 
 import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -40,10 +38,14 @@ class RestrictTest {
         String POSITION = "position";
         String YEARHIRED = "yearHired";
 
-        SortableAttribute<Employee> badgeNum = new SortableAttributeRecord<>(BADGENUM);
-        TextAttribute<Employee> name = new TextAttributeRecord<>(NAME);
-        TextAttribute<Employee> position = new TextAttributeRecord<>(POSITION);
-        SortableAttribute<Employee> yearHired = new SortableAttributeRecord<>(YEARHIRED);
+        ComparableAttribute<Employee, Integer> badgeNum = ComparableAttribute.of(
+                Employee.class, BADGENUM, int.class);
+        TextAttribute<Employee> name = TextAttribute.of(
+                Employee.class, NAME);
+        TextAttribute<Employee> position = TextAttribute.of(
+                Employee.class, POSITION);
+        ComparableAttribute<Employee, Integer> yearHired = ComparableAttribute.of(
+                Employee.class, YEARHIRED, int.class);
     }
 
     // Mock entity class for tests