Repository: jakartaee/data
Pull request #1089 (2025-05-01 09:50:25)

reify the attribute type
It's extremely useful to be able to reflect on the metamodel.

(Among other things, it lets you check correctness before doing an unchecked type cast.)


Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/Attribute.java b/api/src/main/java/jakarta/data/metamodel/Attribute.java
index 9c8e26ca..a39ddce8 100644
--- a/api/src/main/java/jakarta/data/metamodel/Attribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/Attribute.java
@@ -43,6 +43,54 @@ package jakarta.data.metamodel;
  *     collections, embeddables, and other relation attributes.</li>
  * </ul>
  *
+ * <p>To represent an entity, a static metamodel class defines a field
+ * corresponding to each entity attribute. The type of the field should be the
+ * most precise subtype of {@code Attribute} that describes the entity
+ * attribute type.</p>
+ *
+ * <p>For example, given the following entity class,</p>
+ *
+ * <pre>
+ * &#64;Entity
+ * public class Car {
+ *     &#64;Id
+ *     public String vin;
+ *     public LocalDate listed;
+ *     public String make;
+ *     public String model;
+ *     public int price;
+ *     public int year;
+ * }
+ * </pre>
+ *
+ * <p>The static metamodel class (typically generated from the entity class)
+ * would be,</p>
+ *
+ * <pre>
+ * &#64;StaticMetamodel
+ * public interface _Car {
+ *     String LISTED = "listed";
+ *     String MAKE = "make";
+ *     String MODEL = "model";
+ *     String PRICE = "price";
+ *     String VIN = "vin";
+ *     String YEAR = "year";
+ *
+ *     TemporalAttribute&lt;Car,LocalDate&gt; price = TemporalAttribute.of(
+ *             Car.class, LISTED, LocalDate.class);
+ *     TextAttribute&lt;Car&gt; make = TextAttribute.of(
+ *             Car.class, MAKE);
+ *     TextAttribute&lt;Car&gt; model = TextAttribute.of(
+ *             Car.class, MODEL);
+ *     NumericAttribute&lt;Car,Integer&gt; price = NumericAttribute.of(
+ *             Car.class, PRICE, int.class);
+ *     NumericAttribute&lt;Car,Integer&gt; year = NumericAttribute.of(
+ *             Car.class, YEAR, int.class);
+ *     TextAttribute&lt;Car&gt; vin = TextAttribute.of(
+ *             Car.class, VIN);
+ * }
+ * </pre>
+ *
  * @param <T> entity class of the static metamodel.
  */
 public interface Attribute<T> {
@@ -75,4 +123,24 @@ public interface Attribute<T> {
                  to provide the entity class.\
                 """);
     }
+
+    /**
+     * Obtain the Java class which declares this entity attribute.
+     *
+     * @return the declaring class
+     * @throws UnsupportedOperationException if the declaring type is not
+     *                                       known.
+     * @apiNote This is only guaranteed to be known if a static <code>of</code>
+     * method, such as {@link BasicAttribute#of(Class, String, Class)}, was used
+     * to obtain the instance.
+     * @since 1.1
+     */
+    default Class<?> attributeType() {
+        throw new UnsupportedOperationException(getClass().getName() + """
+                 was obtained in a way that does not identify the type\
+                 of the attribute. Static metamodel classes should\
+                 use the .of method that is defined on the Attribute subtype\
+                 to provide the entity class.\
+                """);
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
index cf9c6dda..71cfa599 100644
--- a/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/BasicAttribute.java
@@ -30,6 +30,16 @@ import jakarta.data.expression.Expression;
  */
 public interface BasicAttribute<T, V> extends Attribute<T>, Expression<T, V> {
 
+    @Override
+    default Class<V> attributeType() {
+        throw new UnsupportedOperationException(getClass().getName() + """
+                 was obtained in a way that does not identify the type\
+                 of the attribute. Static metamodel classes should\
+                 use the .of method that is defined on the Attribute subtype\
+                 to provide the entity class.\
+                """);
+    }
+
     /**
      * <p>Creates a static metamodel {@code BasicAttribute} representing the
      * entity attribute with the specified name.</p>
@@ -49,7 +59,7 @@ public interface BasicAttribute<T, V> extends Attribute<T>, Expression<T, V> {
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new BasicAttributeRecord<>(entityClass, name);
+        return new BasicAttributeRecord<>(entityClass, name, attributeType);
     }
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/BasicAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/BasicAttributeRecord.java
index d94ac8ac..e7951689 100644
--- a/api/src/main/java/jakarta/data/metamodel/BasicAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/BasicAttributeRecord.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel;
 
-record BasicAttributeRecord<T, V>(Class<T> declaringType, String name)
+record BasicAttributeRecord<T, V>
+        (Class<T> declaringType, String name, Class<V> attributeType)
         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 f837d875..77d16cc1 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttribute.java
@@ -80,7 +80,7 @@ public interface ComparableAttribute<T, V extends Comparable<?>>
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new ComparableAttributeRecord<>(entityClass, name);
+        return new ComparableAttributeRecord<>(entityClass, name, attributeType);
     }
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/ComparableAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/ComparableAttributeRecord.java
index 068680f2..5b54f7b7 100644
--- a/api/src/main/java/jakarta/data/metamodel/ComparableAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/ComparableAttributeRecord.java
@@ -18,6 +18,6 @@
 package jakarta.data.metamodel;
 
 record ComparableAttributeRecord<T, V extends Comparable<?>>
-        (Class<T> declaringType, String name)
+        (Class<T> declaringType, String name, Class<V> attributeType)
         implements ComparableAttribute<T, V> {
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/NavigableAttribute.java b/api/src/main/java/jakarta/data/metamodel/NavigableAttribute.java
index bf463e72..020a3f56 100644
--- a/api/src/main/java/jakarta/data/metamodel/NavigableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/NavigableAttribute.java
@@ -33,6 +33,9 @@ import jakarta.data.expression.NavigableExpression;
 public interface NavigableAttribute<T, U>
         extends Attribute<T>, NavigableExpression<T, U> {
 
+    @Override
+    Class<U> attributeType();
+
     /**
      * <p>Creates a static metamodel {@code NavigableAttribute} representing
      * the
@@ -52,7 +55,7 @@ public interface NavigableAttribute<T, U>
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new NavigableAttributeRecord<>(entityClass, name);
+        return new NavigableAttributeRecord<>(entityClass, name, attributeType);
     }
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/NavigableAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/NavigableAttributeRecord.java
index 181d469b..18aa2a02 100644
--- a/api/src/main/java/jakarta/data/metamodel/NavigableAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/NavigableAttributeRecord.java
@@ -17,6 +17,7 @@
  */
 package jakarta.data.metamodel;
 
-record NavigableAttributeRecord<T, U>(Class<T> declaringType, String name)
+record NavigableAttributeRecord<T, U>
+        (Class<T> declaringType, String name, Class<U> attributeType)
         implements NavigableAttribute<T, U> {
 }
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/metamodel/NumericAttribute.java b/api/src/main/java/jakarta/data/metamodel/NumericAttribute.java
index 789aed0f..cd6de101 100644
--- a/api/src/main/java/jakarta/data/metamodel/NumericAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/NumericAttribute.java
@@ -68,7 +68,7 @@ public interface NumericAttribute<T, N extends Number & Comparable<N>>
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new NumericAttributeRecord<>(entityClass, name);
+        return new NumericAttributeRecord<>(entityClass, name, attributeType);
     }
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/NumericAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/NumericAttributeRecord.java
index f3bd6e9b..036879e9 100644
--- a/api/src/main/java/jakarta/data/metamodel/NumericAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/NumericAttributeRecord.java
@@ -18,6 +18,6 @@
 package jakarta.data.metamodel;
 
 record NumericAttributeRecord<T, V extends Number & Comparable<V>>
-        (Class<T> declaringType, String name)
+        (Class<T> declaringType, String name, Class<V> attributeType)
         implements NumericAttribute<T, V> {
 }
\ 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 74dfec74..c0f6be25 100644
--- a/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/SortableAttribute.java
@@ -77,6 +77,6 @@ public interface SortableAttribute<T> extends Attribute<T> {
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new SortableAttributeRecord<>(entityClass, name);
+        return new SortableAttributeRecord<>(entityClass, name, attributeType);
     }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/SortableAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/SortableAttributeRecord.java
index bb8f2d30..541e10e7 100644
--- a/api/src/main/java/jakarta/data/metamodel/SortableAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/SortableAttributeRecord.java
@@ -17,7 +17,8 @@
  */
 package jakarta.data.metamodel;
 
-record SortableAttributeRecord<T>(Class<T> declaringType, String name)
+record SortableAttributeRecord<T>
+        (Class<T> declaringType, String name, Class<?> attributeType)
         implements SortableAttribute<T> {
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/TemporalAttribute.java b/api/src/main/java/jakarta/data/metamodel/TemporalAttribute.java
index dfa51010..682db6c4 100644
--- a/api/src/main/java/jakarta/data/metamodel/TemporalAttribute.java
+++ b/api/src/main/java/jakarta/data/metamodel/TemporalAttribute.java
@@ -65,7 +65,7 @@ public interface TemporalAttribute<T, V extends Temporal & Comparable<? extends
         Objects.requireNonNull(name, "The name is required");
         Objects.requireNonNull(attributeType, "The attributeType is required");
 
-        return new TemporalAttributeRecord<T, V>(entityClass, name);
+        return new TemporalAttributeRecord<>(entityClass, name, attributeType);
     }
 }
 
diff --git a/api/src/main/java/jakarta/data/metamodel/TemporalAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/TemporalAttributeRecord.java
index b6c79a38..ec3fb0bd 100644
--- a/api/src/main/java/jakarta/data/metamodel/TemporalAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/TemporalAttributeRecord.java
@@ -19,8 +19,7 @@ package jakarta.data.metamodel;
 
 import java.time.temporal.Temporal;
 
-record TemporalAttributeRecord<T, V extends Temporal & Comparable<? extends Temporal>>(
-        Class<T> declaringType,
-        String name)
+record TemporalAttributeRecord<T, V extends Temporal & Comparable<? extends Temporal>>
+        (Class<T> declaringType, String name, Class<V> attributeType)
         implements TemporalAttribute<T, V> {
 }
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/metamodel/TextAttributeRecord.java b/api/src/main/java/jakarta/data/metamodel/TextAttributeRecord.java
index c544d806..e0a37a64 100644
--- a/api/src/main/java/jakarta/data/metamodel/TextAttributeRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/TextAttributeRecord.java
@@ -19,4 +19,8 @@ package jakarta.data.metamodel;
 
 record TextAttributeRecord<T>(Class<T> declaringType, String name)
         implements TextAttribute<T> {
+    @Override
+    public Class<String> attributeType() {
+        return String.class;
+    }
 }
diff --git a/api/src/main/java/jakarta/data/restrict/BasicRestriction.java b/api/src/main/java/jakarta/data/restrict/BasicRestriction.java
index 723c8b72..7c1d0dc3 100644
--- a/api/src/main/java/jakarta/data/restrict/BasicRestriction.java
+++ b/api/src/main/java/jakarta/data/restrict/BasicRestriction.java
@@ -19,17 +19,66 @@ package jakarta.data.restrict;
 
 import jakarta.data.constraint.Constraint;
 import jakarta.data.expression.Expression;
+import jakarta.data.metamodel.Attribute;
+import jakarta.data.metamodel.StaticMetamodel;
 
+/**
+ * <p>A Jakarta Data provider's view of a restriction on a single
+ * {@linkplain Attribute entity attribute} or
+ * {@linkplain Expression expression}.</p>
+ *
+ * <p>This class is used by the Jakarta Data provider to interpret a
+ * restriction that is supplied by the application. The application should use
+ * the instructions in the {@link Restriction} class and the
+ * {@linkplain StaticMetamodel static metamodel} to obtain a restriction.</p>
+ *
+ * @param <T> entity type.
+ * @param <V> entity attribute type.
+ * @since 1.1
+ */
 public interface BasicRestriction<T, V> extends Restriction<T> {
 
-    @Override
-    BasicRestriction<T, V> negate();
-
+    /**
+     * <p>Returns the {@linkplain Attribute entity attribute} or
+     * {@linkplain Expression expression} to which this restriction applies.</p>
+     *
+     * <p>This method is intended for Jakarta Data providers.</p>
+     *
+     * @return the entity attribute or expression.
+     */
     Expression<T, V> expression();
 
+    /**
+     * <p>Returns the constraint that this restriction applies to the entity
+     * attribute or expression.</p>
+     *
+     * <p>This method is intended for Jakarta Data providers.</p>
+     *
+     * @return the constraint that is applied by this restriction.
+     */
     Constraint<V> constraint();
 
-    static <T, V> BasicRestriction<T, V> of(Expression<T, V> expression, Constraint<V> constraint) {
+    /**
+     * <p>Creates a new restriction that represents a constraint on the
+     * specified {@linkplain Attribute entity attribute} or
+     * {@linkplain Expression expression}.</p>
+     *
+     * <p>Restrictions on {@linkplain Attribute entity attributes} can often be
+     * obtained from the {@linkplain StaticMetamodel static metamodel} more
+     * conveniently than from this method.</p>
+     *
+     * @param <T>        entity class.
+     * @param <V>        entity attribute class.
+     * @param expression the {@linkplain Attribute entity attribute} or
+     *                   {@linkplain Expression expression} to which the
+     *                   restriction applies.
+     * @param constraint the constraint that is applied by the restriction.
+     * @return the restriction.
+     * @throws NullPointerException if the expression or constraint is 
+     *                              {@code null}.
+     */
+    static <T, V> Restriction<T> of(Expression<T, V> expression,
+                                    Constraint<V> constraint) {
         return new BasicRestrictionRecord<>(expression, constraint);
     }
 }
diff --git a/api/src/main/java/jakarta/data/restrict/BasicRestrictionRecord.java b/api/src/main/java/jakarta/data/restrict/BasicRestrictionRecord.java
index a22bc6d0..591ae0ed 100644
--- a/api/src/main/java/jakarta/data/restrict/BasicRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/restrict/BasicRestrictionRecord.java
@@ -38,7 +38,7 @@ record BasicRestrictionRecord<T, V>(Expression<T, V> expression,
     }
 
     @Override
-    public BasicRestriction<T, V> negate() {
+    public Restriction<T> negate() {
         return BasicRestriction.of(expression, constraint.negate());
     }
 
diff --git a/api/src/main/java/jakarta/data/restrict/CompositeRestriction.java b/api/src/main/java/jakarta/data/restrict/CompositeRestriction.java
index 6e31b421..72ddbdc3 100644
--- a/api/src/main/java/jakarta/data/restrict/CompositeRestriction.java
+++ b/api/src/main/java/jakarta/data/restrict/CompositeRestriction.java
@@ -19,24 +19,85 @@ package jakarta.data.restrict;
 
 import java.util.List;
 
+/**
+ * <p>A Jakarta Data provider's view of a restriction that combines other
+ * restrictions.</p>
+ *
+ * <p>This class is used by the Jakarta Data provider to interpret a
+ * restriction that is supplied by the application. The application should use
+ * the {@link Restrict#all(Restriction...)} and
+ * {@link Restrict#any(Restriction...)} methods to obtain composite
+ * restrictions.</p>
+ *
+ * @param <T> entity type.
+ * @since 1.1
+ */
 public interface CompositeRestriction<T> extends Restriction<T> {
+    /**
+     * <p>Indicates if the collective combination of {@link #restrictions()}
+     * should be negated. The combination of restrictions is evaluated first,
+     * according to its {@link #type()}, after which negation is applied to the
+     * evaluated result.</p>
+     *
+     * @return {@code true} if negated, otherwise {@code false}.
+     */
     boolean isNegated();
 
-    @Override
-    CompositeRestriction<T> negate();
-
+    /**
+     * <p>An ordered list of restrictions. The order must match the order that
+     * is specified by the application.</p>
+     *
+     * <p>The elements can be {@link BasicRestriction} or
+     * {@link CompositeRestriction} and must not be {@code null}.</p>
+     *
+     * <p>Any negation that is indicated by the {@link #isNegated()} method has
+     * not been applied to the restrictions in the list because the negation
+     * instead applies to the entire combination.</p>
+     *
+     * @return the ordered list of restriction.
+     */
     List<Restriction<T>> restrictions();
 
+    /**
+     * <p>Indicates how to combine the list of {@link #restrictions()}.</p>
+     *
+     * @return how to combine the list of restrictions.
+     */
     Type type();
 
+    /**
+     * <p>Indicates how to combine a list of
+     * {@linkplain CompositeRestriction#restrictions() restrictions}.</p>
+     */
     enum Type {
+        /**
+         * <p>Indicates that all restrictions within a list must be satisfied
+         * in order for the combination of restrictions to be considered
+         * satisfied. If the list is empty, the combination is considered
+         * satisfied.</p>
+         *
+         * <p>{@linkplain CompositeRestriction#isNegated() Negation} is applied
+         * to a composite restriction after evaluating the combination of
+         * restrictions.</p>
+         */
         ALL,
+
+        /**
+         * <p>Indicates that at least one of the restrictions within a list
+         * must be satisfied in order for the combination of restriction to be
+         * considered satisfied. If the list is empty, the combination is
+         * is considered unsatisfied.</p>
+         *
+         * <p>{@linkplain CompositeRestriction#isNegated() Negation} is applied
+         * to a composite restriction after evaluating the combination of
+         * restrictions.</p>
+         */
         ANY;
 
         /**
          * Representation of the composite restriction type as a query language
-         * logical operator. For example, {@link #ALL} is represented as
-         * {@code AND} in query language.
+         * logical operator. {@link #ALL} is represented as {@code AND} in
+         * query language, and {@link #ANY} is represented as {@code OR}.
          *
          * @return the representation as a logical operator in query language.
          */
diff --git a/api/src/main/java/jakarta/data/restrict/Restrict.java b/api/src/main/java/jakarta/data/restrict/Restrict.java
index 10a32966..184a02b5 100644
--- a/api/src/main/java/jakarta/data/restrict/Restrict.java
+++ b/api/src/main/java/jakarta/data/restrict/Restrict.java
@@ -17,36 +17,123 @@
  */
 package jakarta.data.restrict;
 
+import jakarta.data.metamodel.Attribute;
+
 import java.util.List;
 import java.util.Objects;
 
-// TODO document
-// This is one of two places from which to obtain restrictions.
-// The other place is from static metamodel attributes.
+/**
+ * <p>Creates composite restrictions.</p>
+ *
+ * <p>Composite restrictions are created by combining multiple restrictions,
+ * which might themselves be composite restrictions or can be obtained from
+ * static metamodel {@link Attribute} subtypes.</p>
+ *
+ * <p>For example, the following constructs a composite restriction (from a
+ * basic restriction and another composite restriction) that matches cars that
+ * either cost less than $35000 or have newer model years than 2023 and cost
+ * less than $40000.</p>
+ *
+ * <pre>
+ * List&lt;Car&gt; found =
+ *         cars.search(make,
+ *                     model,
+ *                     Restrict.any(_Car.price.lessThan(35000),
+ *                                  Restrict.all(_Car.year.greaterThan(2023),
+ *                                               _Car.price.lessThan(40000))),
+ *                     Order.by(_Car.year.desc(),
+ *                              _Car.price.desc()));
+ * </pre>
+ *
+ * @since 1.1
+ */
 public class Restrict {
 
     // prevent instantiation
     private Restrict() {
     }
 
+    /**
+     * <p>Returns a composite restriction that is satisfied when all of the
+     * supplied restrictions are satisfied. The order of the restrictions is
+     * preserved in the composite restriction.</p>
+     *
+     * <p>For example,</p>
+     * <pre>
+     * List&lt;Book&gt; jakartaEEBooksByAuthor =
+     *         books.writtenBy(author,
+     *                         Restrict.all(_Book.title.notNull(),
+     *                                      _Book.title.upper().contains("JAKARTA EE")));
+     * </pre>
+     *
+     * @param <T>          entity type.
+     * @param restrictions one or more restrictions obtained from a method of
+     *                     {@code Restrict} or from a static metamodel
+     *                     {@link Attribute} subtype.
+     * @return
+     * @throws IllegalArgumentException if the supplied restrictions array is
+     *                                  empty or {@code null}.
+     * @throws NullPointerException     if the supplied restrictions array
+     *                                  includes a {@code null} value.
+     */
     @SafeVarargs
     public static <T> Restriction<T> all(Restriction<T>... restrictions) {
         return new CompositeRestrictionRecord<>(CompositeRestriction.Type.ALL,
                 List.of(restrictions));
     }
 
+    /**
+     * <p>Returns a composite restriction that is satisfied when at least one
+     * of the supplied restrictions is satisfied. The order of the restrictions
+     * is preserved in the composite restriction.</p>
+     *
+     * <p>For example,</p>
+     * <pre>
+     * List&lt;Product&gt; found =
+     *         products.search(productName,
+     *                         Restrict.any(_Product.color.equalTo(Color.BLUE),
+     *                                      _Product.color.equalTo(Color.GREEN)));
+     * </pre>
+     *
+     * @param <T>          entity type.
+     * @param restrictions one or more restrictions obtained from a method of
+     *                     {@code Restrict} or from a static metamodel
+     *                     {@link Attribute} subtype.
+     * @return
+     * @throws IllegalArgumentException if the supplied restrictions array is
+     *                                  empty or {@code null}.
+     * @throws NullPointerException     if the supplied restrictions array
+     *                                  includes a {@code null} value.
+     */
     @SafeVarargs
     public static <T> Restriction<T> any(Restriction<T>... restrictions) {
         return new CompositeRestrictionRecord<>(CompositeRestriction.Type.ANY,
                 List.of(restrictions));
     }
 
-    // convenience method for those who would prefer to avoid .negate()
+    /**
+     * <p>Returns the negation of the specified restriction.</p>
+     *
+     * <p>This restriction returned by this method is obtained by invoking
+     * {@link Restriction#negate()} on the supplied restriction.</p>
+     *
+     * @return the negated restriction.
+     * @throws NullPointerException if the supplied restriction is
+     *                              {@code null}.
+     */
     public static <T> Restriction<T> not(Restriction<T> restriction) {
         Objects.requireNonNull(restriction, "The restriction is required");
         return restriction.negate();
     }
 
+    /**
+     * <p>Returns a restriction that always evaluates to satisfied. This can be
+     * used to avoid imposing additional restrictions in places where a
+     * {@link Restriction} value is required.</p>
+     *
+     * @param <T> entity type.
+     * @return a restriction that is always considered to be satisfied.
+     */
     @SuppressWarnings("unchecked")
     public static <T> Restriction<T> unrestricted() {
         return (Restriction<T>) Unrestricted.INSTANCE;
diff --git a/api/src/main/java/jakarta/data/restrict/Restriction.java b/api/src/main/java/jakarta/data/restrict/Restriction.java
index 655605ac..49b81889 100644
--- a/api/src/main/java/jakarta/data/restrict/Restriction.java
+++ b/api/src/main/java/jakarta/data/restrict/Restriction.java
@@ -17,6 +17,102 @@
  */
 package jakarta.data.restrict;
 
+import jakarta.data.constraint.Constraint;
+import jakarta.data.constraint.GreaterThan;
+import jakarta.data.constraint.LessThanOrEqual;
+import jakarta.data.constraint.Like;
+import jakarta.data.constraint.NotLike;
+import jakarta.data.metamodel.Attribute;
+import jakarta.data.metamodel.StaticMetamodel;
+import jakarta.data.repository.Find;
+
+/**
+ * <p>A restriction imposes constraints on entity attribute values or
+ * expressions.</p>
+ *
+ * <p>Basic restrictions that impose a single
+ * {@linkplain Constraint constraint} are obtained from {@linkplain Attribute}
+ * subtypes of the {@linkplain StaticMetamodel static metamodel}.</p>
+ *
+ * <p>Composite restrictions that impose multiple
+ * {@linkplain Constraint constraints} are obtained from the
+ * {@link Restrict#all(Restriction...)} and
+ * {@link Restrict#any(Restriction...)} methods.</p>
+ *
+ * <p>A repository {@link Find} method can optionally accept a parameter of
+ * type {@code Restriction}. For example,</p>
+ * <pre>
+ * &#64;Repository
+ * public interface Cars extends CrudRepository&lt;Car, String&gt; {
+ *
+ *     &#64;Find
+ *     List&lt;Car&gt; search(&#64;By(_Car.MAKE) String manufacturer,
+ *                      &#64;By(_Car.MODEL) String model,
+ *                      Restriction&lt;Car&gt; restriction,
+ *                      Order&lt;Car&gt;... sort);
+ * }
+ * </pre>
+ *
+ * <p>Instances of restriction obtained from the static metamodel or the
+ * {@link Restrict} class can be supplied as the parameter when invoking the
+ * respository method. For example,</p>
+ * <pre>
+ * List&lt;Car&gt; withinPriceRange =
+ *         cars.search(make,
+ *                     model,
+ *                     _Car.price.between(20000, 30000),
+ *                     Order.by(_Car.price.desc()));
+ *
+ * List&lt;Car&gt; pricedUnder30kWhenDiscounted =
+ *         cars.search(make,
+ *                     model,
+ *                     _Car.price.minus(discount).lessThan(30000),
+ *                     Order.by(_Car.price.desc()));
+ *
+ * List&lt;Car&gt; atLeast2020AndPricedBelow30k =
+ *         cars.search(make,
+ *                     model,
+ *                     Restrict.all(_Car.year.greaterThanEqual(2020),
+ *                                  _Car.price.lessThan(30000)),
+ *                     Order.by(_Car.price.desc()));
+ * </pre>
+ *
+ * <p>The {@linkplain Attribute example entity and static metamodel} for the
+ * above are provided in the {@link Attribute} Javadoc.</p>
+ *
+ * <p>Restrictions are immutable and do not change after they are created.</p>
+ *
+ * @param <T> entity type.
+ * @since 1.1
+ */
 public interface Restriction<T> {
+
+    /**
+     * <p>Returns the negation of this restriction.</p>
+     *
+     * <p>For example, a basic restriction that represents an
+     * {@linkplain GreaterThan exclusive upper bound} on a value is negated
+     * as an {@linkplain LessThanOrEqual inclusive lower bound} on the value.
+     * </p>
+     *
+     * <p>A basic restriction that represents {@linkplain Like matching} a
+     * pattern is negated as a restriction to {@linkplain NotLike not match}
+     * the pattern.</p>
+     *
+     * <p>A composite restriction that requires satisfying
+     * {@linkplain CompositeRestriction.Type#ANY at least one} restriction
+     * is negated as requiring that not any (in other words, none) of the
+     * restrictions are satisfied.</p>
+     *
+     * <p>A composite restriction that requires satisfying
+     * {@linkplain CompositeRestriction.Type#ALL all} restrictions
+     * is negated as requiring that not all of the restrictions are satisfied
+     * (in other words, at least one is unsatisfed).</p>
+     *
+     * <p>This method does not modify the restriction upon which it is invoked.
+     * </p>
+     *
+     * @return the negated restriction.
+     */
     Restriction<T> negate();
 }


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/ComparableExpressionTest.java b/api/src/test/java/jakarta/data/metamodel/ComparableExpressionTest.java
index d5a77219..72782d67 100644
--- a/api/src/test/java/jakarta/data/metamodel/ComparableExpressionTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/ComparableExpressionTest.java
@@ -40,7 +40,7 @@ class ComparableExpressionTest {
     interface _Person {
         String AGE = "age";
         ComparableExpression<Person, Integer> age =
-                new ComparableAttributeRecord<>(Person.class, AGE);
+                ComparableAttribute.of(Person.class, AGE, Integer.class);
     }
 
     @Test
diff --git a/api/src/test/java/jakarta/data/restrict/BasicRestrictionRecordTest.java b/api/src/test/java/jakarta/data/restrict/BasicRestrictionRecordTest.java
index 67a57760..65283c6a 100644
--- a/api/src/test/java/jakarta/data/restrict/BasicRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/restrict/BasicRestrictionRecordTest.java
@@ -21,13 +21,11 @@ import jakarta.data.constraint.*;
 import jakarta.data.metamodel.BasicAttribute;
 import jakarta.data.mock.entity.Book;
 import jakarta.data.mock.entity._Book;
-import jakarta.data.restrict.BasicRestriction;
 
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-import java.util.List;
 import java.util.Set;
 
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -65,7 +63,7 @@ class BasicRestrictionRecordTest {
     @DisplayName("should negate LessThanOrEqual into GreaterThan")
     void shouldNegateLTERestriction() {
         var lessThanEqual = (BasicRestriction<Book, Integer>) _Book.numChapters.lessThanEqual(10);
-        var negated = lessThanEqual.negate();
+        var negated = (BasicRestriction<Book, Integer>) lessThanEqual.negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(lessThanEqual.expression()).isEqualTo(_Book.numChapters);
@@ -81,8 +79,10 @@ class BasicRestrictionRecordTest {
     @Test
     @DisplayName("should return to the original constraint after double negation")
     void shouldNegateNegatedRestriction() {
-        var original = (BasicRestriction<Book, String>) _Book.title.equalTo("A Developer's Guide to Jakarta Data");
-        var doubleNegated = original.negate().negate();
+        var original = (BasicRestriction<Book, String>)
+                _Book.title.equalTo("A Developer's Guide to Jakarta Data");
+        var doubleNegated = (BasicRestriction<Book, Integer>)
+                original.negate().negate();
 
         SoftAssertions.assertSoftly(soft -> {
             soft.assertThat(original.expression()).isEqualTo(_Book.title);
diff --git a/api/src/test/java/jakarta/data/restrict/CompositeRestrictionRecordTest.java b/api/src/test/java/jakarta/data/restrict/CompositeRestrictionRecordTest.java
index 7ab0e2d5..6718f229 100644
--- a/api/src/test/java/jakarta/data/restrict/CompositeRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/restrict/CompositeRestrictionRecordTest.java
@@ -19,10 +19,6 @@ package jakarta.data.restrict;
 
 import jakarta.data.metamodel.ComparableAttribute;
 import jakarta.data.metamodel.TextAttribute;
-import jakarta.data.restrict.CompositeRestriction;
-import jakarta.data.restrict.CompositeRestrictionRecord;
-import jakarta.data.restrict.Restrict;
-import jakarta.data.restrict.Restriction;
 
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.DisplayName;
@@ -127,7 +123,7 @@ class CompositeRestrictionRecordTest {
         var nameStartsWithDuke = _Author.name.startsWith("Duke ");
 
         var all = (CompositeRestriction<Author>) Restrict.all(ageLessThan50, nameStartsWithDuke);
-        var negated = all.negate();
+        var negated = (CompositeRestriction<Author>) all.negate();
         var fromNot = (CompositeRestriction<Author>) Restrict.not(all);
 
         SoftAssertions.assertSoftly(soft -> {
@@ -146,7 +142,7 @@ class CompositeRestrictionRecordTest {
         var nameContainsDuke = _Author.name.contains("Duke");
 
         var any = (CompositeRestriction<Author>) Restrict.any(ageBetween20and30, nameContainsDuke);
-        var negated = any.negate();
+        var negated = (CompositeRestriction<Author>) any.negate();
         var unNegated = (CompositeRestriction<Author>) Restrict.not(negated);
 
         SoftAssertions.assertSoftly(soft -> {
diff --git a/api/src/test/java/jakarta/data/restrict/TextRestrictionRecordTest.java b/api/src/test/java/jakarta/data/restrict/TextRestrictionRecordTest.java
index 95ed4cfd..49e6c4eb 100644
--- a/api/src/test/java/jakarta/data/restrict/TextRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/restrict/TextRestrictionRecordTest.java
@@ -24,7 +24,6 @@ import jakarta.data.expression.literal.StringLiteral;
 import jakarta.data.metamodel.TextAttribute;
 import jakarta.data.mock.entity.Book;
 import jakarta.data.mock.entity._Book;
-import jakarta.data.restrict.BasicRestriction;
 
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
@@ -130,7 +129,9 @@ class TextRestrictionRecordTest {
         @SuppressWarnings("unchecked")
         BasicRestriction<Book,String> likeJakartaEE =
                 (BasicRestriction<Book, String>) _Book.title.endsWith("Jakarta EE");
-        BasicRestriction<Book,String> notLikeJakartaEE = likeJakartaEE.negate();
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book,String> notLikeJakartaEE =
+                (BasicRestriction<Book, String>)likeJakartaEE.negate();
         // TODO TextRestriction.ignoreCase vs TextAttribute.upper/lowercased
         //BasicRestriction<Book,String> anyCaseNotLikeJakartaEE = likeJakartaEE.ignoreCase().negate();
         //BasicRestriction<Book,String> notLikeJakartaEEAnyCase = likeJakartaEE.negate().ignoreCase();
@@ -148,8 +149,12 @@ class TextRestrictionRecordTest {
         @SuppressWarnings("unchecked")
         BasicRestriction<Book,String> endsWithJakartaEE =
                 (BasicRestriction<Book, String>) _Book.title.endsWith("Jakarta EE");
-        BasicRestriction<Book,String> notEndsWithJakartaEE = endsWithJakartaEE.negate();
-        BasicRestriction<Book,String> notNotEndsWithJakartaEE = notEndsWithJakartaEE.negate();
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book,String> notEndsWithJakartaEE =
+                (BasicRestriction<Book, String>) endsWithJakartaEE.negate();
+        @SuppressWarnings("unchecked")
+        BasicRestriction<Book,String> notNotEndsWithJakartaEE =
+                (BasicRestriction<Book, String>) notEndsWithJakartaEE.negate();
 
         Like endsWithJakartaEEConstraint =
                 (Like) endsWithJakartaEE.constraint();