diff --git a/backend-core-business-impl/pom.xml b/backend-core-business-impl/pom.xml
index fed8748..6f090e6 100644
--- a/backend-core-business-impl/pom.xml
+++ b/backend-core-business-impl/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-business-impl
diff --git a/backend-core-business-spring-impl/pom.xml b/backend-core-business-spring-impl/pom.xml
index 7603d91..0978e64 100644
--- a/backend-core-business-spring-impl/pom.xml
+++ b/backend-core-business-spring-impl/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-business-spring-impl
diff --git a/backend-core-business/pom.xml b/backend-core-business/pom.xml
index 29b016a..7d32139 100644
--- a/backend-core-business/pom.xml
+++ b/backend-core-business/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-business
diff --git a/backend-core-data-impl/pom.xml b/backend-core-data-impl/pom.xml
index 846eaa3..1335632 100644
--- a/backend-core-data-impl/pom.xml
+++ b/backend-core-data-impl/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-data-impl
diff --git a/backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java b/backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
index 52c1c8f..1b38387 100644
--- a/backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
+++ b/backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/ConstraintTransformerJpaImpl.java
@@ -2,7 +2,7 @@
* #%L
* Commons Backend - Data Access Layer Implementations
* %%
- * Copyright (C) 2020 - 2021 Flowing Code
+ * Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@
import com.flowingcode.backendcore.model.constraints.AttributeLikeConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeNullConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeRelationalConstraint;
+import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
import com.flowingcode.backendcore.model.constraints.RelationalConstraint;
@@ -44,6 +45,11 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
+/**
+ * JPA/Criteria implementation of {@link ConstraintTransformer}.
+ *
+ *
Instances are not thread-safe. A new instance must be created for each query.
+ */
@RequiredArgsConstructor
public class ConstraintTransformerJpaImpl extends ConstraintTransformer {
@@ -80,10 +86,12 @@ private From,?> join(From,?> root, String[] path) {
return from;
}
+ private JoinType currentJoinType = JoinType.INNER;
+
@SuppressWarnings("rawtypes")
private From,?> join(From,?> source, String attributeName) {
Optional existingJoin = source.getJoins().stream().filter(join->join.getAttribute().getName().equals(attributeName)).map(join->(Join)join).findFirst();
- return existingJoin.orElseGet(()->source.join(attributeName, JoinType.INNER));
+ return existingJoin.orElseGet(()->source.join(attributeName, currentJoinType));
}
private static Class> boxed(Class> type) {
@@ -166,4 +174,18 @@ protected Predicate transformNullConstraint(AttributeNullConstraint c) {
protected Predicate transformILikeConstraint(AttributeILikeConstraint c) {
return criteriaBuilder.like(criteriaBuilder.lower(getExpression(c, String.class)), c.getPattern().toLowerCase());
}
+
+ @Override
+ protected Predicate transformDisjunctionConstraint(DisjunctionConstraint c) {
+ JoinType saved = currentJoinType;
+ currentJoinType = JoinType.LEFT;
+ try {
+ Predicate[] predicates = c.getConstraints().stream()
+ .map(this::apply)
+ .toArray(Predicate[]::new);
+ return criteriaBuilder.or(predicates);
+ } finally {
+ currentJoinType = saved;
+ }
+ }
}
diff --git a/backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java b/backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
index ff8b99f..1d1cb54 100644
--- a/backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
+++ b/backend-core-data-impl/src/test/java/com/flowingcode/backendcore/dao/jpa/JpaDaoSupportTest.java
@@ -2,7 +2,7 @@
* #%L
* Commons Backend - Data Access Layer Implementations
* %%
- * Copyright (C) 2020 - 2021 Flowing Code
+ * Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -147,6 +147,27 @@ void testFilterByState() {
assertEquals(5, count);
}
+ @Test
+ void testFilterWithOrConstraint() {
+ // OR of both city ids must match all 10 persons that have a city assigned
+ PersonFilter pf = new PersonFilter();
+ pf.addConstraint(
+ ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
+ .or(ConstraintBuilder.of("city", "id").equal(cities.get(1).getId())));
+ assertEquals(10, dao.count(pf));
+ }
+
+ @Test
+ void testFilterWithOrConstraintPartialMatch() {
+ // city.id branch matches 5 persons; id branch matches persistedPerson (who has no city).
+ // LEFT JOIN on city must keep persistedPerson in the result set so the OR can match them.
+ PersonFilter pf = new PersonFilter();
+ pf.addConstraint(
+ ConstraintBuilder.of("city", "id").equal(cities.get(0).getId())
+ .or(ConstraintBuilder.of("id").equal(persistedPerson.getId())));
+ assertEquals(6, dao.count(pf));
+ }
+
@Test
@Disabled
void testDelete() {
diff --git a/backend-core-data/pom.xml b/backend-core-data/pom.xml
index efda66f..275aedf 100644
--- a/backend-core-data/pom.xml
+++ b/backend-core-data/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-data
diff --git a/backend-core-model/pom.xml b/backend-core-model/pom.xml
index f60ab9a..9504f7b 100644
--- a/backend-core-model/pom.xml
+++ b/backend-core-model/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
backend-core-model
diff --git a/backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
index 337e2d1..5ae1d30 100644
--- a/backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
+++ b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/Constraint.java
@@ -2,7 +2,7 @@
* #%L
* Commons Backend - Model
* %%
- * Copyright (C) 2020 - 2021 Flowing Code
+ * Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
*/
package com.flowingcode.backendcore.model;
+import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
public interface Constraint {
@@ -26,5 +27,24 @@ public interface Constraint {
default Constraint not() {
return new NegatedConstraint(this);
}
-
+
+ /**
+ * Returns a constraint that is satisfied when this constraint or any of the given constraints is
+ * satisfied (logical OR).
+ *
+ * @param first the first additional constraint
+ * @param rest optional additional constraints
+ * @return a {@link DisjunctionConstraint} combining this and the given constraints
+ */
+ default Constraint or(Constraint first, Constraint... rest) {
+ return DisjunctionConstraint.of(this, prepend(first, rest));
+ }
+
+ private static Constraint[] prepend(Constraint first, Constraint[] rest) {
+ Constraint[] result = new Constraint[1 + rest.length];
+ result[0] = first;
+ System.arraycopy(rest, 0, result, 1, rest.length);
+ return result;
+ }
+
}
diff --git a/backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
index 923d086..8c2213f 100644
--- a/backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
+++ b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/ConstraintTransformer.java
@@ -2,7 +2,7 @@
* #%L
* Commons Backend - Model
* %%
- * Copyright (C) 2020 - 2021 Flowing Code
+ * Copyright (C) 2020 - 2026 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
import com.flowingcode.backendcore.model.constraints.AttributeLikeConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeNullConstraint;
import com.flowingcode.backendcore.model.constraints.AttributeRelationalConstraint;
+import com.flowingcode.backendcore.model.constraints.DisjunctionConstraint;
import com.flowingcode.backendcore.model.constraints.NegatedConstraint;
/**
@@ -80,6 +81,10 @@ protected T transform(Constraint c) {
return transformILikeConstraint((AttributeILikeConstraint) c);
}
+ if (c instanceof DisjunctionConstraint) {
+ return transformDisjunctionConstraint((DisjunctionConstraint) c);
+ }
+
return null;
}
@@ -125,4 +130,10 @@ protected T transformNullConstraint(AttributeNullConstraint c) {
protected T transformILikeConstraint(AttributeILikeConstraint c) {
return null;
}
+
+ /** Return an implementation-specific representation of a {@code DisjunctionConstraint} constraint.
+ * @return an implementation-specific representation of the constraint, or {@code null} if it cannot be transformed.*/
+ protected T transformDisjunctionConstraint(DisjunctionConstraint c) {
+ return null;
+ }
}
diff --git a/backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
new file mode 100644
index 0000000..7cc1859
--- /dev/null
+++ b/backend-core-model/src/main/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraint.java
@@ -0,0 +1,57 @@
+/*-
+ * #%L
+ * Commons Backend - Model
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.backendcore.model.constraints;
+
+import com.flowingcode.backendcore.model.Constraint;
+import java.util.List;
+import java.util.Objects;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+/** A constraint that is satisfied when any of its member constraints is satisfied (logical OR). */
+@Getter
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
+public final class DisjunctionConstraint implements Constraint {
+
+ @NonNull List constraints;
+
+ public static DisjunctionConstraint of(Constraint first, Constraint... rest) {
+ List list = new java.util.ArrayList<>();
+ Objects.requireNonNull(rest, "constraints must not be null");
+ add(list, Objects.requireNonNull(first, "constraint must not be null"));
+ for (Constraint c : rest) {
+ add(list, Objects.requireNonNull(c, "constraint must not be null"));
+ }
+ return new DisjunctionConstraint(List.copyOf(list));
+ }
+
+ private static void add(List list, Constraint c) {
+ if (c instanceof DisjunctionConstraint) {
+ list.addAll(((DisjunctionConstraint) c).constraints);
+ } else {
+ list.add(c);
+ }
+ }
+
+}
diff --git a/backend-core-model/src/test/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraintTest.java b/backend-core-model/src/test/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraintTest.java
new file mode 100644
index 0000000..bc6badc
--- /dev/null
+++ b/backend-core-model/src/test/java/com/flowingcode/backendcore/model/constraints/DisjunctionConstraintTest.java
@@ -0,0 +1,77 @@
+/*-
+ * #%L
+ * Commons Backend - Model
+ * %%
+ * Copyright (C) 2020 - 2026 Flowing Code
+ * %%
+ * 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.
+ * #L%
+ */
+package com.flowingcode.backendcore.model.constraints;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.flowingcode.backendcore.model.Constraint;
+import com.flowingcode.backendcore.model.ConstraintBuilder;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class DisjunctionConstraintTest {
+
+ private static final Constraint A = ConstraintBuilder.of("a").equal(1);
+ private static final Constraint B = ConstraintBuilder.of("b").equal(2);
+ private static final Constraint C = ConstraintBuilder.of("c").equal(3);
+
+ @Test
+ void testOfProducesCorrectMembers() {
+ DisjunctionConstraint d = DisjunctionConstraint.of(A, B, C);
+ assertEquals(List.of(A, B, C), d.getConstraints());
+ }
+
+ @Test
+ void testChainedOrFlattens() {
+ // a.or(b).or(c) must produce OR(a, b, c), not OR(OR(a, b), c)
+ Constraint chained = A.or(B).or(C);
+ DisjunctionConstraint d = (DisjunctionConstraint) chained;
+ assertEquals(3, d.getConstraints().size());
+ assertSame(A, d.getConstraints().get(0));
+ assertSame(B, d.getConstraints().get(1));
+ assertSame(C, d.getConstraints().get(2));
+ }
+
+ @Test
+ void testOrWithExistingDisjunctionInRestFlattens() {
+ // DisjunctionConstraint passed as a rest element is also flattened
+ DisjunctionConstraint ab = DisjunctionConstraint.of(A, B);
+ DisjunctionConstraint d = DisjunctionConstraint.of(C, ab);
+ assertEquals(List.of(C, A, B), d.getConstraints());
+ }
+
+ @Test
+ void testNullFirstThrows() {
+ assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(null, B));
+ }
+
+ @Test
+ void testNullRestArrayThrows() {
+ assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(A, (Constraint[]) null));
+ }
+
+ @Test
+ void testNullElementInRestThrows() {
+ assertThrows(NullPointerException.class, () -> DisjunctionConstraint.of(A, B, null));
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 3c7a4f3..64dd272 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
com.flowingcode.backend-core
backend-core
pom
- 1.1.1-SNAPSHOT
+ 1.2.0-SNAPSHOT
Backend Core
Common utilities for backend enterprise application development
https://www.flowingcode.com/en/open-source/