Skip to content

Commit

Permalink
Merge pull request #905 from njr-11/829-negated-restrictions
Browse files Browse the repository at this point in the history
negated restrictions
  • Loading branch information
otaviojava authored Dec 18, 2024
2 parents 569b143 + a8d02bb commit 06ceafe
Show file tree
Hide file tree
Showing 16 changed files with 230 additions and 88 deletions.
3 changes: 3 additions & 0 deletions api/src/main/java/jakarta/data/BasicRestriction.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ public interface BasicRestriction<T> extends Restriction<T> {

String field();

@Override
BasicRestriction<T> negate();

Object value();
}
9 changes: 6 additions & 3 deletions api/src/main/java/jakarta/data/BasicRestrictionRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@

record BasicRestrictionRecord<T>(
String field,
boolean isNegated,
Operator comparison,
Object value) implements BasicRestriction<T> {

BasicRestrictionRecord {
Objects.requireNonNull(field, "Field must not be null");
}

BasicRestrictionRecord(String field, Operator comparison, Object value) {
this(field, false, comparison, value);
@Override
public BasicRestriction<T> negate() {
return new BasicRestrictionRecord<>(
field,
comparison.negate(),
value);
}
}
5 changes: 5 additions & 0 deletions api/src/main/java/jakarta/data/CompositeRestriction.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
import java.util.List;

public interface CompositeRestriction<T> extends Restriction<T> {
boolean isNegated();

@Override
CompositeRestriction<T> negate();

List<Restriction<T>> restrictions();

Type type();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ record CompositeRestrictionRecord<T>(
CompositeRestrictionRecord(Type type, List<Restriction<T>> restrictions) {
this(type, restrictions, false);
}

@Override
public CompositeRestriction<T> negate() {
return new CompositeRestrictionRecord<>(type, restrictions, !isNegated);
}
}
25 changes: 24 additions & 1 deletion api/src/main/java/jakarta/data/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,28 @@ public enum Operator {
IN,
LESS_THAN,
LESS_THAN_EQUAL,
LIKE
LIKE,
NOT_EQUAL,
NOT_IN,
NOT_LIKE;

/**
* Returns the operator that is the negation of this operator.
*
* @return the operator that is the negation of this operator.
*/
Operator negate() {
return switch (this) {
case EQUAL -> NOT_EQUAL;
case GREATER_THAN -> LESS_THAN_EQUAL;
case GREATER_THAN_EQUAL -> LESS_THAN;
case IN -> NOT_IN;
case LESS_THAN -> GREATER_THAN_EQUAL;
case LESS_THAN_EQUAL -> GREATER_THAN;
case LIKE -> NOT_LIKE;
case NOT_EQUAL -> EQUAL;
case NOT_IN -> IN;
case NOT_LIKE -> LIKE;
};
}
}
24 changes: 15 additions & 9 deletions api/src/main/java/jakarta/data/Restrict.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package jakarta.data;

import java.util.List;
import java.util.Objects;
import java.util.Set;

// TODO document
Expand All @@ -31,7 +32,6 @@ public class Restrict {

// used internally for more readable code
private static final boolean ESCAPED = true;
private static final boolean NOT = true;

private static final char STRING_WILDCARD = '%';

Expand Down Expand Up @@ -132,43 +132,49 @@ public static <T> TextRestriction<T> like(String pattern,
return new TextRestrictionRecord<>(field, Operator.LIKE, ESCAPED, p);
}

// convenience method for those who would prefer to avoid .negate()
public static <T> Restriction<T> not(Restriction<T> restriction) {
Objects.requireNonNull(restriction, "Restriction must not be null");
return restriction.negate();
}

public static <T> Restriction<T> notEqualTo(Object value, String field) {
return new BasicRestrictionRecord<>(field, NOT, Operator.EQUAL, value);
return new BasicRestrictionRecord<>(field, Operator.NOT_EQUAL, value);
}

public static <T> TextRestriction<T> notEqualTo(String value, String field) {
return new TextRestrictionRecord<>(field, NOT, Operator.EQUAL, value);
return new TextRestrictionRecord<>(field, Operator.NOT_EQUAL, value);
}

public static <T> TextRestriction<T> notContains(String substring, String field) {
String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, substring, true);
return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
}

public static <T> TextRestriction<T> notEndsWith(String suffix, String field) {
String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, true, suffix, false);
return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
}

public static <T> Restriction<T> notIn(Set<Object> values, String field) {
return new BasicRestrictionRecord<>(field, NOT, Operator.IN, values);
return new BasicRestrictionRecord<>(field, Operator.NOT_IN, values);
}

public static <T> TextRestriction<T> notLike(String pattern, String field) {
return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, pattern);
return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, pattern);
}

public static <T> TextRestriction<T> notLike(String pattern,
char charWildcard,
char stringWildcard,
String field) {
String p = toLikeEscaped(charWildcard, stringWildcard, false, pattern, false);
return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, p);
return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, p);
}

public static <T> TextRestriction<T> notStartsWith(String prefix, String field) {
String pattern = toLikeEscaped(CHAR_WILDCARD, STRING_WILDCARD, false, prefix, true);
return new TextRestrictionRecord<>(field, NOT, Operator.LIKE, ESCAPED, pattern);
return new TextRestrictionRecord<>(field, Operator.NOT_LIKE, ESCAPED, pattern);
}

public static <T> TextRestriction<T> startsWith(String prefix, String field) {
Expand Down
2 changes: 1 addition & 1 deletion api/src/main/java/jakarta/data/Restriction.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
package jakarta.data;

public interface Restriction<T> {
boolean isNegated();
Restriction<T> negate();
}
5 changes: 4 additions & 1 deletion api/src/main/java/jakarta/data/TextRestriction.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
package jakarta.data;

public interface TextRestriction<T> extends BasicRestriction<T> {
Restriction<T> ignoreCase();
TextRestriction<T> ignoreCase();

// TODO can mention in the JavaDoc that a value of true will be ignored
// if the database is not not capable of case sensitive comparisons
boolean isCaseSensitive();

boolean isEscaped();

@Override
TextRestriction<T> negate();

@Override
String value();
}
28 changes: 15 additions & 13 deletions api/src/main/java/jakarta/data/TextRestrictionRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

record TextRestrictionRecord<T>(
String field,
boolean isNegated,
Operator comparison,
boolean isCaseSensitive,
boolean isEscaped,
Expand All @@ -35,24 +34,27 @@ record TextRestrictionRecord<T>(
Objects.requireNonNull(field, "Field must not be null");
}

TextRestrictionRecord(String field, boolean negated, Operator comparison, boolean escaped, String value) {
this(field, negated, comparison, true, escaped, value);
}

TextRestrictionRecord(String field, boolean negated, Operator comparison, String value) {
this(field, negated, comparison, true, false, value);
}

TextRestrictionRecord(String field, Operator comparison, boolean escaped, String value) {
this(field, false, comparison, true, escaped, value);
this(field, comparison, true, escaped, value);
}

TextRestrictionRecord(String field, Operator comparison, String value) {
this(field, false, comparison, true, false, value);
this(field, comparison, true, false, value);
}

@Override
public TextRestriction<T> ignoreCase() {
return new TextRestrictionRecord<>(field, comparison, false, isEscaped, value);
}

@Override
public Restriction<T> ignoreCase() {
return new TextRestrictionRecord<>(field, isNegated, comparison, false, isEscaped, value);
public TextRestriction<T> negate() {

return new TextRestrictionRecord<>(
field,
comparison.negate(),
isCaseSensitive,
isEscaped,
value);
}
}
63 changes: 54 additions & 9 deletions api/src/test/java/jakarta/data/BasicRestrictionRecordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,30 @@


class BasicRestrictionRecordTest {
// A mock entity class for tests
static class Book {
}

@Test
void shouldCreateBasicRestrictionWithDefaultNegation() {
BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(restriction.field()).isEqualTo("title");
soft.assertThat(restriction.isNegated()).isFalse();
soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
soft.assertThat(restriction.value()).isEqualTo("Java Guide");
});
}

@Test
void shouldCreateBasicRestrictionWithExplicitNegation() {
BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("title", true, Operator.EQUAL, "Java Guide");
BasicRestriction<Book> restriction =
(BasicRestriction<Book>) Restrict.<Book>equalTo("Java Guide", "title")
.negate();

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(restriction.field()).isEqualTo("title");
soft.assertThat(restriction.isNegated()).isTrue();
soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
soft.assertThat(restriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
soft.assertThat(restriction.value()).isEqualTo("Java Guide");
});
}
Expand All @@ -56,21 +59,63 @@ void shouldCreateBasicRestrictionWithNullValue() {

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(restriction.field()).isEqualTo("title");
soft.assertThat(restriction.isNegated()).isFalse();
soft.assertThat(restriction.comparison()).isEqualTo(Operator.EQUAL);
soft.assertThat(restriction.value()).isNull();
});
}

@Test
void shouldNegateLTERestriction() {
Restriction<Book> numChaptersLTE10 = Restrict.lessThanEqual(10, "numChapters");
BasicRestriction<Book> numChaptersLTE10Basic = (BasicRestriction<Book>) numChaptersLTE10;
BasicRestriction<Book> numChaptersGT10Basic = (BasicRestriction<Book>) numChaptersLTE10Basic.negate();

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(numChaptersLTE10Basic.comparison()).isEqualTo(Operator.LESS_THAN_EQUAL);
soft.assertThat(numChaptersLTE10Basic.value()).isEqualTo(10);

soft.assertThat(numChaptersGT10Basic.comparison()).isEqualTo(Operator.GREATER_THAN);
soft.assertThat(numChaptersGT10Basic.value()).isEqualTo(10);
});
}

@Test
void shouldNegateNegatedRestriction() {
Restriction<Book> titleRestriction =
Restrict.equalTo("A Developer's Guide to Jakarta Data", "title");
BasicRestriction<Book> titleRestrictionBasic =
(BasicRestriction<Book>) titleRestriction;
BasicRestriction<Book> negatedTitleRestrictionBasic =
(BasicRestriction<Book>) titleRestriction.negate();
BasicRestriction<Book> negatedNegatedTitleRestrictionBasic =
(BasicRestriction<Book>) negatedTitleRestrictionBasic.negate();

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(titleRestrictionBasic.comparison())
.isEqualTo(Operator.EQUAL);
soft.assertThat(titleRestrictionBasic.value())
.isEqualTo("A Developer's Guide to Jakarta Data");

soft.assertThat(negatedTitleRestrictionBasic.comparison())
.isEqualTo(Operator.NOT_EQUAL);
soft.assertThat(negatedTitleRestrictionBasic.value())
.isEqualTo("A Developer's Guide to Jakarta Data");

soft.assertThat(negatedNegatedTitleRestrictionBasic.comparison())
.isEqualTo(Operator.EQUAL);
soft.assertThat(negatedNegatedTitleRestrictionBasic.value())
.isEqualTo("A Developer's Guide to Jakarta Data");
});
}

@Test
void shouldSupportNegatedRestrictionUsingDefaultConstructor() {
BasicRestrictionRecord<String> restriction = new BasicRestrictionRecord<>("author", Operator.EQUAL, "Unknown");
BasicRestrictionRecord<String> negatedRestriction = new BasicRestrictionRecord<>(restriction.field(), true, restriction.comparison(), restriction.value());
BasicRestriction<Book> negatedRestriction =
(BasicRestriction<Book>) Restrict.<Book>notEqualTo((Object) "Unknown", "author");

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(negatedRestriction.field()).isEqualTo("author");
soft.assertThat(negatedRestriction.isNegated()).isTrue();
soft.assertThat(negatedRestriction.comparison()).isEqualTo(Operator.EQUAL);
soft.assertThat(negatedRestriction.comparison()).isEqualTo(Operator.NOT_EQUAL);
soft.assertThat(negatedRestriction.value()).isEqualTo("Unknown");
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@


class CompositeRestrictionRecordTest {

// A mock entity class for tests
static class Person {
}

@Test
void shouldCreateCompositeRestrictionWithDefaultNegation() {
Expand Down Expand Up @@ -71,6 +73,44 @@ void shouldFailIfEmptyRestrictions() {
.hasMessage("Cannot create a composite restriction without any restrictions to combine.");
}

@Test
void shouldNegateCompositeRestriction() {
Restriction<Person> ageLessThan50 = Restrict.lessThan(50, "age");
Restriction<Person> nameStartsWithDuke = Restrict.startsWith("Duke ", "name");
CompositeRestriction<Person> all =
(CompositeRestriction<Person>) Restrict.all(ageLessThan50, nameStartsWithDuke);
CompositeRestriction<Person> allNegated = all.negate();
CompositeRestriction<Person> notAll =
(CompositeRestriction<Person>) Restrict.not(all);

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(all.isNegated()).isEqualTo(false);

soft.assertThat(allNegated.isNegated()).isEqualTo(true);

soft.assertThat(notAll.isNegated()).isEqualTo(true);
});
}

@Test
void shouldNegateNegatedCompositeRestriction() {
Restriction<Person> ageBetween20and30 = Restrict.between(20, 30, "age");
Restriction<Person> nameContainsDuke = Restrict.contains("Duke", "name");
CompositeRestriction<Person> any =
(CompositeRestriction<Person>) Restrict.any(ageBetween20and30, nameContainsDuke);
CompositeRestriction<Person> anyNegated = any.negate();
CompositeRestriction<Person> anyNotNegated =
(CompositeRestriction<Person>) Restrict.not(anyNegated);

SoftAssertions.assertSoftly(soft -> {
soft.assertThat(any.isNegated()).isEqualTo(false);

soft.assertThat(anyNegated.isNegated()).isEqualTo(true);

soft.assertThat(anyNotNegated.isNegated()).isEqualTo(false);
});
}

@Test
void shouldPreserveRestrictionsOrder() {
Restriction<String> restriction1 = new BasicRestrictionRecord<>("title", Operator.EQUAL, "Java Guide");
Expand Down
Loading

0 comments on commit 06ceafe

Please sign in to comment.