Repository: jakartaee/data
Pull request #962 (2025-02-05 22:35:32)

implement toString to make Restrictions more readable
Adds implementation of toString on the built-in Restrictions to display in a concise way that is meaningful.

For example,
`name LIKE "Jakarta EE %"`
Or,
`ALL (price GREATER_THAN_EQUAL 50, price LESS_THAN 100)`


Patch:
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
index c065c6a5..201c1c16 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/BasicRestrictionRecord.java
@@ -39,4 +39,29 @@ record BasicRestrictionRecord<T>(
                 comparison.negate(),
                 value);
     }
+
+    /**
+     * Textual representation of a basic restriction.
+     * For example,
+     * <pre>price LESS_THAN 50.0</pre>
+     *
+     * @return textual representation of a basic restriction.
+     */
+    @Override
+    public String toString() {
+        String valueString = value == null ? "null" : value.toString();
+        StringBuilder builder = new StringBuilder(
+                attribute.length() +
+                comparison.name().length() +
+                valueString.length() +
+                4); // number of additional characters that might be appended
+        builder.append(attribute).append(' ')
+         .append(comparison.name()).append(' ');
+        if (value instanceof CharSequence) {
+            builder.append('"').append(valueString).append('"');
+        } else {
+            builder.append(valueString);
+        }
+        return builder.toString();
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecord.java
index 0350c6af..9d19d986 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecord.java
@@ -28,6 +28,11 @@ record CompositeRestrictionRecord<T>(
         List<Restriction<T>> restrictions,
         boolean isNegated) implements CompositeRestriction<T> {
 
+    /**
+     * Initial guess of length per restriction that is being combined.
+     */
+    private static final int SINGLE_RESTRICTION_LENGTH_ESTIMATE = 100;
+
     CompositeRestrictionRecord {
         if (restrictions == null || restrictions.isEmpty()) {
             throw new IllegalArgumentException(
@@ -43,4 +48,33 @@ record CompositeRestrictionRecord<T>(
     public CompositeRestriction<T> negate() {
         return new CompositeRestrictionRecord<>(type, restrictions, !isNegated);
     }
+
+    /**
+     * Textual representation of a composite restriction.
+     * For example,
+     * <pre>ALL (price LESS_THAN 50.0, name LIKE "%Jakarta EE%")</pre>
+     *
+     * @return textual representation of a composite restriction.
+     */
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder(
+                restrictions.size() * SINGLE_RESTRICTION_LENGTH_ESTIMATE +
+                7); // number of additional characters that might be appended
+        if (isNegated) {
+            builder.append("NOT ");
+        }
+        builder.append(type.name()).append(" (");
+        boolean first = true;
+        for (Restriction<T> restriction : restrictions) {
+            if (first) {
+                first = false;
+            } else {
+                builder.append(", ");
+            }
+            builder.append(restriction);
+        }
+        builder.append(')');
+        return builder.toString();
+    }
 }
\ No newline at end of file
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
index ec593a17..9c219fd1 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/TextRestrictionRecord.java
@@ -57,4 +57,36 @@ record TextRestrictionRecord<T>(
                 isEscaped,
                 value);
     }
+
+    /**
+     * Textual representation of a text restriction.
+     * For example,
+     * <pre>name LIKE_IGNORE_CASE "Jakarta EE %"</pre>
+     *
+     * @return textual representation of a text restriction.
+     */
+    @Override
+    public String toString() {
+        String valueString = value == null ? "null" : value;
+        StringBuilder builder = new StringBuilder(
+                attribute.length() +
+                comparison.name().length() +
+                valueString.length() +
+                24); // number of additional characters that might be appended
+        builder.append(attribute).append(' ')
+               .append(comparison.name());
+        if (!isCaseSensitive) {
+            builder.append("_IGNORE_CASE");
+        }
+        builder.append(' ');
+        if (value == null) {
+            builder.append(valueString);
+        } else {
+            builder.append('"').append(valueString).append('"');
+        }
+        if (isEscaped) {
+            builder.append(" ESCAPED");
+        }
+        return builder.toString();
+    }
 }
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Unmatchable.java b/api/src/main/java/jakarta/data/metamodel/restrict/Unmatchable.java
index ec02886c..06c650b8 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Unmatchable.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Unmatchable.java
@@ -42,6 +42,16 @@ class Unmatchable<T> implements CompositeRestriction<T> {
         return List.of();
     }
 
+    /**
+     * Textual representation of the unmatchable restriction.
+     *
+     * @return textual representation of the unmatchable restriction.
+     */
+    @Override
+    public String toString() {
+        return "UNMATCHABLE";
+    }
+
     @Override
     public Type type() {
         return Type.ANY;
diff --git a/api/src/main/java/jakarta/data/metamodel/restrict/Unrestricted.java b/api/src/main/java/jakarta/data/metamodel/restrict/Unrestricted.java
index 7604ced9..2da93e5d 100644
--- a/api/src/main/java/jakarta/data/metamodel/restrict/Unrestricted.java
+++ b/api/src/main/java/jakarta/data/metamodel/restrict/Unrestricted.java
@@ -42,6 +42,16 @@ class Unrestricted<T> implements CompositeRestriction<T> {
         return List.of();
     }
 
+    /**
+     * Textual representation of the unrestricted restriction.
+     *
+     * @return textual representation of the unrestricted restriction.
+     */
+    @Override
+    public String toString() {
+        return "UNRESTRICTED";
+    }
+
     @Override
     public Type type() {
         return Type.ALL;


Test patch:
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
index a2454c57..47b93872 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/BasicRestrictionRecordTest.java
@@ -108,6 +108,16 @@ class BasicRestrictionRecordTest {
         });
     }
 
+    @Test
+    void shouldOutputToString() {
+        Restriction<Book> restriction = Restrict.greaterThan(100, "numPages");
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(restriction.toString())
+                .isEqualTo("numPages GREATER_THAN 100");
+        });
+    }
+
     @Test
     void shouldSupportNegatedRestrictionUsingDefaultConstructor() {
         BasicRestriction<Book> negatedRestriction =
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
index 5ee98503..c8547414 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/CompositeRestrictionRecordTest.java
@@ -21,6 +21,8 @@ package jakarta.data.metamodel.restrict;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
+import jakarta.data.metamodel.restrict.BasicRestrictionRecordTest.Book;
+
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 
 import java.util.List;
@@ -111,6 +113,27 @@ class CompositeRestrictionRecordTest {
         });
     }
 
+    @Test
+    void shouldOutputToString() {
+        Restriction<Person> namedJackKarta = Restrict
+                .all(Restrict.equalTo("Jack", "firstName"),
+                     Restrict.equalTo("Karta", "lastName"));
+
+        Restriction<Person> minorOrMissingName = Restrict
+                .any(Restrict.lessThan(18, "age"),
+                     Restrict.equalTo(null, "name"));
+        Restriction<Person> notMinorOrMissingName = minorOrMissingName.negate();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(namedJackKarta.toString()).isEqualTo("""
+                    ALL (firstName EQUAL "Jack", lastName EQUAL "Karta")\
+                    """);
+            soft.assertThat(notMinorOrMissingName.toString()).isEqualTo("""
+                    NOT ANY (age LESS_THAN 18, name EQUAL null)\
+                    """);
+        });
+    }
+
     @Test
     void shouldPreserveRestrictionsOrder() {
         Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
diff --git a/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java b/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
index 990391ee..8895efa6 100644
--- a/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
+++ b/api/src/test/java/jakarta/data/metamodel/restrict/TextRestrictionRecordTest.java
@@ -140,6 +140,23 @@ class TextRestrictionRecordTest {
         });
     }
 
+    @Test
+    void shouldOutputToString() {
+        TextRestriction<Book> titleRestriction =
+                Restrict.contains("Jakarta Data", "title");
+        TextRestriction<Book> authorRestriction =
+                Restrict.<Book>equalTo("Myself", "author").ignoreCase().negate();
+
+        SoftAssertions.assertSoftly(soft -> {
+            soft.assertThat(titleRestriction.toString()).isEqualTo("""
+                    title LIKE "%Jakarta Data%" ESCAPED\
+                    """);
+            soft.assertThat(authorRestriction.toString()).isEqualTo("""
+                    author NOT_EQUAL_IGNORE_CASE "Myself"\
+                    """);
+        });
+    }
+
     @Test
     void shouldSupportNegationForTextRestriction() {
         TextRestrictionRecord<String> restriction = new TextRestrictionRecord<>(