diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/ModelCriteriaBuilder.java b/model/map/src/main/java/org/keycloak/models/map/storage/ModelCriteriaBuilder.java index ab3b12e1ed..ed14d281e1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/ModelCriteriaBuilder.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/ModelCriteriaBuilder.java @@ -117,7 +117,7 @@ public interface ModelCriteriaBuilder { * @return * @throws CriterionNotSupported If the operator is not supported for the given field. */ - ModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value); + ModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value); /** * Creates and returns a new instance of {@code ModelCriteriaBuilder} that diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java index 491e8e864b..1e24e2c3de 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java @@ -53,7 +53,7 @@ public class ConcurrentHashMapStorage store = new ConcurrentHashMap<>(); - protected final Map, UpdatePredicatesFunc> fieldPredicates; + protected final Map, UpdatePredicatesFunc> fieldPredicates; protected final StringKeyConvertor keyConvertor; protected final DeepCloner cloner; diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index dade4bad46..1d73c710dc 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -527,7 +527,7 @@ public class MapFieldPredicates { } @SuppressWarnings("unchecked") - public static Map, UpdatePredicatesFunc> getPredicates(Class clazz) { + public static Map, UpdatePredicatesFunc> getPredicates(Class clazz) { return PREDICATES.get(clazz); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapModelCriteriaBuilder.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapModelCriteriaBuilder.java index 0f42a6d602..3dac1d3516 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapModelCriteriaBuilder.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapModelCriteriaBuilder.java @@ -43,14 +43,14 @@ public class MapModelCriteriaBuilder implements private static final Predicate ALWAYS_FALSE = (e) -> false; private final Predicate keyFilter; private final Predicate entityFilter; - private final Map, UpdatePredicatesFunc> fieldPredicates; + private final Map, UpdatePredicatesFunc> fieldPredicates; private final StringKeyConvertor keyConvertor; - public MapModelCriteriaBuilder(StringKeyConvertor keyConvertor, Map, UpdatePredicatesFunc> fieldPredicates) { + public MapModelCriteriaBuilder(StringKeyConvertor keyConvertor, Map, UpdatePredicatesFunc> fieldPredicates) { this(keyConvertor, fieldPredicates, ALWAYS_TRUE, ALWAYS_TRUE); } - private MapModelCriteriaBuilder(StringKeyConvertor keyConvertor, Map, UpdatePredicatesFunc> fieldPredicates, Predicate indexReadFilter, Predicate sequentialReadFilter) { + private MapModelCriteriaBuilder(StringKeyConvertor keyConvertor, Map, UpdatePredicatesFunc> fieldPredicates, Predicate indexReadFilter, Predicate sequentialReadFilter) { this.keyConvertor = keyConvertor; this.fieldPredicates = fieldPredicates; this.keyFilter = indexReadFilter; @@ -58,7 +58,7 @@ public class MapModelCriteriaBuilder implements } @Override - public MapModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... values) { + public MapModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... values) { UpdatePredicatesFunc method = fieldPredicates.get(modelField); if (method == null) { throw new IllegalArgumentException("Filter not implemented for field " + modelField); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteria.java b/model/map/src/main/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteria.java index 105c38672d..0062d5101d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteria.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteria.java @@ -19,6 +19,8 @@ package org.keycloak.models.map.storage.criteria; import org.keycloak.models.map.storage.ModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.ModelCriteriaNode.ExtOperator; import org.keycloak.storage.SearchableModelField; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; /** * Descriptive model criteria implementation which in other words represents a Boolean formula on searchable fields. @@ -26,55 +28,113 @@ import org.keycloak.storage.SearchableModelField; */ public class DefaultModelCriteria implements ModelCriteriaBuilder { - private final ModelCriteriaNode node; + private static final DefaultModelCriteria INSTANCE = new DefaultModelCriteria<>(null); - public DefaultModelCriteria() { - this.node = null; - } + private final ModelCriteriaNode node; private DefaultModelCriteria(ModelCriteriaNode node) { this.node = node; } + public static DefaultModelCriteria criteria() { + return (DefaultModelCriteria) INSTANCE; + } + @Override - public DefaultModelCriteria compare(SearchableModelField modelField, Operator op, Object... value) { - final ModelCriteriaNode targetNode; + public DefaultModelCriteria compare(SearchableModelField modelField, Operator op, Object... value) { + return compare(new ModelCriteriaNode<>(modelField, op, value)); + } + + private DefaultModelCriteria compare(final ModelCriteriaNode nodeToAdd) { + ModelCriteriaNode targetNode; + if (isEmpty()) { - targetNode = new ModelCriteriaNode<>(modelField, op, value); + targetNode = nodeToAdd; } else if (node.getNodeOperator() == ExtOperator.AND) { targetNode = node.cloneTree(); - targetNode.addChild(new ModelCriteriaNode<>(modelField, op, value)); + targetNode.addChild(nodeToAdd); } else { targetNode = new ModelCriteriaNode<>(ExtOperator.AND); targetNode.addChild(node.cloneTree()); - targetNode.addChild(new ModelCriteriaNode<>(modelField, op, value)); + targetNode.addChild(nodeToAdd); } + return new DefaultModelCriteria<>(targetNode); } @Override public DefaultModelCriteria and(ModelCriteriaBuilder... mcbs) { - final ModelCriteriaNode targetNode = new ModelCriteriaNode<>(ExtOperator.AND); - for (ModelCriteriaBuilder mcb : mcbs) { - targetNode.addChild(((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node); + if (mcbs.length == 1) { + ModelCriteriaNode toBeChild = ((DefaultModelCriteria) mcbs[0].unwrap(DefaultModelCriteria.class)).node; + if (toBeChild.getNodeOperator() == ExtOperator.AND || toBeChild.getNodeOperator() == ExtOperator.OR) { + return ((DefaultModelCriteria) mcbs[0].unwrap(DefaultModelCriteria.class)); + } } - return new DefaultModelCriteria<>(targetNode); + + final ModelCriteriaNode targetNode = new ModelCriteriaNode<>(ExtOperator.AND); + AtomicBoolean hasFalseNode = new AtomicBoolean(false); + for (ModelCriteriaBuilder mcb : mcbs) { + final ModelCriteriaNode nodeToAdd = ((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node; + getNodesToAddForAndOr(nodeToAdd, ExtOperator.AND) + .filter(ModelCriteriaNode::isNotTrueNode) + .peek(n -> { if (n.isFalseNode()) hasFalseNode.lazySet(true); }) + .map(ModelCriteriaNode::cloneTree) + .forEach(targetNode::addChild); + + if (hasFalseNode.get()) { + return compare(new ModelCriteriaNode<>(ExtOperator.__FALSE__)); + } + } + + if (targetNode.getChildren().isEmpty()) { + // AND on empty set of formulae is TRUE: It does hold that there all formulae are satisfied + return compare(new ModelCriteriaNode<>(ExtOperator.__TRUE__)); + } + + return compare(targetNode); } @Override public DefaultModelCriteria or(ModelCriteriaBuilder... mcbs) { - final ModelCriteriaNode targetNode = new ModelCriteriaNode<>(ExtOperator.OR); - for (ModelCriteriaBuilder mcb : mcbs) { - targetNode.addChild(((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node); + if (mcbs.length == 1) { + ModelCriteriaNode toBeChild = ((DefaultModelCriteria) mcbs[0].unwrap(DefaultModelCriteria.class)).node; + if (toBeChild.getNodeOperator() == ExtOperator.AND || toBeChild.getNodeOperator() == ExtOperator.OR) { + return ((DefaultModelCriteria) mcbs[0].unwrap(DefaultModelCriteria.class)); + } } - return new DefaultModelCriteria<>(targetNode); + + final ModelCriteriaNode targetNode = new ModelCriteriaNode<>(ExtOperator.OR); + AtomicBoolean hasTrueNode = new AtomicBoolean(false); + for (ModelCriteriaBuilder mcb : mcbs) { + final ModelCriteriaNode nodeToAdd = ((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node; + getNodesToAddForAndOr(nodeToAdd, ExtOperator.OR) + .filter(ModelCriteriaNode::isNotFalseNode) + .peek(n -> { if (n.isTrueNode()) hasTrueNode.lazySet(true); }) + .map(ModelCriteriaNode::cloneTree) + .forEach(targetNode::addChild); + + if (hasTrueNode.get()) { + return compare(new ModelCriteriaNode<>(ExtOperator.__TRUE__)); + } + } + + if (targetNode.getChildren().isEmpty()) { + // OR on empty set of formulae is FALSE: It does not hold that there is at least one satisfied formula + return compare(new ModelCriteriaNode<>(ExtOperator.__FALSE__)); + } + + return compare(targetNode); } @Override public DefaultModelCriteria not(ModelCriteriaBuilder mcb) { final ModelCriteriaNode targetNode = new ModelCriteriaNode<>(ExtOperator.NOT); - targetNode.addChild(((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node); - return new DefaultModelCriteria<>(targetNode); + ModelCriteriaNode toBeChild = ((DefaultModelCriteria) mcb.unwrap(DefaultModelCriteria.class)).node; + if (toBeChild.getNodeOperator() == ExtOperator.NOT) { + return compare(toBeChild.getChildren().get(0).cloneTree()); + } + targetNode.addChild(toBeChild.cloneTree()); + return compare(targetNode); } /** @@ -83,10 +143,35 @@ public class DefaultModelCriteria implements ModelCriteriaBuilder { * @param mcb {@code ModelCriteriaBuilder} to copy the contents onto * @return Updated {@code ModelCriteriaBuilder} */ - public ModelCriteriaBuilder flashToModelCriteriaBuilder(ModelCriteriaBuilder mcb) { + public > C flashToModelCriteriaBuilder(C mcb) { return mcb == null ? null : node.flashToModelCriteriaBuilder(mcb); } + /** + * Optimizes this formula into another {@code ModelCriteriaBuilder}, using the values of + * {@link ExtOperator#__TRUE__} and {@link ExtOperator#__FALSE__} accordingly. + * @return New instance of {@code } + */ + public DefaultModelCriteria optimize() { + return flashToModelCriteriaBuilder(criteria()); + } + + @FunctionalInterface + public interface AtomicFormulaTester { + public Boolean test(SearchableModelField field, Operator operator, Object[] operatorArguments); + } + + public DefaultModelCriteria partiallyEvaluate(AtomicFormulaTester tester) { + return new DefaultModelCriteria<>(node.cloneTree((field, operator, operatorArguments) -> { + Boolean res = tester.test(field, operator, operatorArguments); + if (res == null) { + return new ModelCriteriaNode<>(field, operator, operatorArguments); + } else { + return new ModelCriteriaNode<>(res ? ExtOperator.__TRUE__ : ExtOperator.__FALSE__); + } + }, ModelCriteriaNode::new)); + } + public boolean isEmpty() { return node == null; } @@ -96,4 +181,14 @@ public class DefaultModelCriteria implements ModelCriteriaBuilder { return isEmpty() ? "" : node.toString(); } + private Stream> getNodesToAddForAndOr(ModelCriteriaNode nodeToAdd, ExtOperator operatorBeingAdded) { + final ExtOperator op = nodeToAdd.getNodeOperator(); + + if (op == operatorBeingAdded) { + return nodeToAdd.getChildren().stream(); + } + + return Stream.of(nodeToAdd); + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/criteria/ModelCriteriaNode.java b/model/map/src/main/java/org/keycloak/models/map/storage/criteria/ModelCriteriaNode.java index 0c634db39f..fe8470b996 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/criteria/ModelCriteriaNode.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/criteria/ModelCriteriaNode.java @@ -23,17 +23,19 @@ import org.keycloak.storage.SearchableModelField; import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** - * + * TODO: Introduce separation of parameter values and the structure * @author hmlnarik */ public class ModelCriteriaNode extends DefaultTreeNode> { public static enum ExtOperator { AND { - @Override public ModelCriteriaBuilder apply(ModelCriteriaBuilder mcb, ModelCriteriaNode node) { + @Override public > C apply(C mcb, ModelCriteriaNode node) { if (node.getChildren().isEmpty()) { return null; } @@ -41,14 +43,14 @@ public class ModelCriteriaNode extends DefaultTreeNode> .map(n -> n.flashToModelCriteriaBuilder(mcb)) .filter(Objects::nonNull) .toArray(ModelCriteriaBuilder[]::new); - return operands.length == 0 ? null : mcb.and(operands); + return operands.length == 0 ? null : (C) mcb.and(operands); } @Override public String toString(ModelCriteriaNode node) { return "(" + node.getChildren().stream().map(ModelCriteriaNode::toString).collect(Collectors.joining(" && ")) + ")"; } }, OR { - @Override public ModelCriteriaBuilder apply(ModelCriteriaBuilder mcb, ModelCriteriaNode node) { + @Override public > C apply(C mcb, ModelCriteriaNode node) { if (node.getChildren().isEmpty()) { return null; } @@ -56,23 +58,23 @@ public class ModelCriteriaNode extends DefaultTreeNode> .map(n -> n.flashToModelCriteriaBuilder(mcb)) .filter(Objects::nonNull) .toArray(ModelCriteriaBuilder[]::new); - return operands.length == 0 ? null : mcb.or(operands); + return operands.length == 0 ? null : (C) mcb.or(operands); } @Override public String toString(ModelCriteriaNode node) { return "(" + node.getChildren().stream().map(ModelCriteriaNode::toString).collect(Collectors.joining(" || ")) + ")"; } }, NOT { - @Override public ModelCriteriaBuilder apply(ModelCriteriaBuilder mcb, ModelCriteriaNode node) { - return mcb.not(node.getChildren().iterator().next().flashToModelCriteriaBuilder(mcb)); + @Override public > C apply(C mcb, ModelCriteriaNode node) { + return (C) mcb.not(node.getChildren().iterator().next().flashToModelCriteriaBuilder(mcb)); } @Override public String toString(ModelCriteriaNode node) { return "! " + node.getChildren().iterator().next().toString(); } }, - SIMPLE_OPERATOR { - @Override public ModelCriteriaBuilder apply(ModelCriteriaBuilder mcb, ModelCriteriaNode node) { - return mcb.compare( + ATOMIC_FORMULA { + @Override public > C apply(C mcb, ModelCriteriaNode node) { + return (C) mcb.compare( node.field, node.simpleOperator, node.simpleOperatorArguments @@ -82,9 +84,25 @@ public class ModelCriteriaNode extends DefaultTreeNode> return node.field.getName() + " " + node.simpleOperator + " " + Arrays.deepToString(node.simpleOperatorArguments); } }, + __FALSE__ { + @Override public > C apply(C mcb, ModelCriteriaNode node) { + return (C) mcb.or(); + } + @Override public String toString(ModelCriteriaNode node) { + return "__FALSE__"; + } + }, + __TRUE__ { + @Override public > C apply(C mcb, ModelCriteriaNode node) { + return (C) mcb.and(); + } + @Override public String toString(ModelCriteriaNode node) { + return "__TRUE__"; + } + } ; - public abstract ModelCriteriaBuilder apply(ModelCriteriaBuilder mcbCreator, ModelCriteriaNode node); + public abstract > C apply(C mcbCreator, ModelCriteriaNode node); public abstract String toString(ModelCriteriaNode node); } @@ -92,16 +110,27 @@ public class ModelCriteriaNode extends DefaultTreeNode> private final Operator simpleOperator; - private final SearchableModelField field; + private final SearchableModelField field; private final Object[] simpleOperatorArguments; - public ModelCriteriaNode(SearchableModelField field, Operator simpleOperator, Object... simpleOperatorArguments) { + public ModelCriteriaNode(SearchableModelField field, Operator simpleOperator, Object[] simpleOperatorArguments) { super(Collections.emptyMap()); this.simpleOperator = simpleOperator; this.field = field; this.simpleOperatorArguments = simpleOperatorArguments; - this.nodeOperator = ExtOperator.SIMPLE_OPERATOR; + this.nodeOperator = ExtOperator.ATOMIC_FORMULA; + + if (simpleOperatorArguments != null) { + for (int i = 0; i < simpleOperatorArguments.length; i ++) { + Object arg = simpleOperatorArguments[i]; + if (arg instanceof Stream) { + try (Stream sArg = (Stream) arg) { + simpleOperatorArguments[i] = sArg.collect(Collectors.toList()); + } + } + } + } } public ModelCriteriaNode(ExtOperator nodeOperator) { @@ -112,7 +141,7 @@ public class ModelCriteriaNode extends DefaultTreeNode> this.simpleOperatorArguments = null; } - private ModelCriteriaNode(ExtOperator nodeOperator, Operator simpleOperator, SearchableModelField field, Object[] simpleOperatorArguments) { + private ModelCriteriaNode(ExtOperator nodeOperator, Operator simpleOperator, SearchableModelField field, Object[] simpleOperatorArguments) { super(Collections.emptyMap()); this.nodeOperator = nodeOperator; this.simpleOperator = simpleOperator; @@ -125,11 +154,40 @@ public class ModelCriteriaNode extends DefaultTreeNode> } public ModelCriteriaNode cloneTree() { - return cloneTree(n -> new ModelCriteriaNode<>(n.nodeOperator, n.simpleOperator, n.field, n.simpleOperatorArguments)); + return cloneTree(ModelCriteriaNode::new, ModelCriteriaNode::new); } - public ModelCriteriaBuilder flashToModelCriteriaBuilder(ModelCriteriaBuilder mcb) { - final ModelCriteriaBuilder res = nodeOperator.apply(mcb, this); + @FunctionalInterface + public interface AtomicFormulaInstantiator { + public ModelCriteriaNode instantiate(SearchableModelField field, Operator operator, Object[] operatorArguments); + } + + public ModelCriteriaNode cloneTree(AtomicFormulaInstantiator atomicFormulaInstantiator, Function> booleanNodeInstantiator) { + return cloneTree(n -> + n.getNodeOperator() == ExtOperator.ATOMIC_FORMULA + ? atomicFormulaInstantiator.instantiate(n.field, n.simpleOperator, n.simpleOperatorArguments) + : booleanNodeInstantiator.apply(n.nodeOperator) + ); + } + + public boolean isFalseNode() { + return getNodeOperator() == ExtOperator.__FALSE__; + } + + public boolean isNotFalseNode() { + return getNodeOperator() != ExtOperator.__FALSE__; + } + + public boolean isTrueNode() { + return getNodeOperator() == ExtOperator.__TRUE__; + } + + public boolean isNotTrueNode() { + return getNodeOperator() != ExtOperator.__TRUE__; + } + + public > C flashToModelCriteriaBuilder(C mcb) { + final C res = nodeOperator.apply(mcb, this); return res == null ? mcb : res; } diff --git a/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java b/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java index fb3b4fa529..90b54d99f4 100644 --- a/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java +++ b/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java @@ -17,11 +17,14 @@ package org.keycloak.models.map.storage.criteria; import org.keycloak.models.ClientModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; +import java.util.Arrays; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.Test; import static org.hamcrest.Matchers.hasToString; import static org.keycloak.models.ClientModel.SearchableFields.*; +import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; /** * @@ -31,42 +34,71 @@ public class DefaultModelCriteriaTest { @Test public void testSimpleCompare() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); assertThat(v.compare(CLIENT_ID, Operator.EQ, 3), hasToString("clientId EQ [3]")); assertThat(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5), hasToString("(clientId EQ [4] && id EQ [5])")); } @Test public void testSimpleCompareAnd() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); + assertThat(v.and(), hasToString("__TRUE__")); + assertThat(v.and(v.or()), hasToString("__FALSE__")); + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 3)), hasToString("(clientId EQ [3])")); - assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)), hasToString("((clientId EQ [4] && id EQ [5]))")); + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 3), v.or()), hasToString("__FALSE__")); + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] && id EQ [5])")); assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] && id EQ [5])")); } @Test public void testSimpleCompareOr() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); + assertThat(v.or(), hasToString("__FALSE__")); + assertThat(v.or(v.and()), hasToString("__TRUE__")); + assertThat(v.or(v.compare(CLIENT_ID, Operator.EQ, 3)), hasToString("(clientId EQ [3])")); - assertThat(v.or(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)), hasToString("((clientId EQ [4] && id EQ [5]))")); + assertThat(v.or(v.compare(CLIENT_ID, Operator.EQ, 3), v.and()), hasToString("__TRUE__")); + assertThat(v.or(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] && id EQ [5])")); assertThat(v.or(v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] || id EQ [5])")); + assertThat(v.or(v.or(v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(ID, Operator.EQ, 5))), hasToString("(clientId EQ [4] || id EQ [5])")); + assertThat(v.and(v.or(v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(ID, Operator.EQ, 5))), hasToString("(clientId EQ [4] || id EQ [5])")); } @Test public void testSimpleCompareAndOr() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); assertThat(v.or( v.and( v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(ID, Operator.EQ, 5) ), - v.compare(ATTRIBUTE, Operator.EQ, "city", "Ankh-Morpork") + v.not(v.not(v.compare(ATTRIBUTE, Operator.EQ, "city", "Ankh-Morpork"))) ), hasToString("((clientId EQ [4] && id EQ [5]) || attribute EQ [city, Ankh-Morpork])")); + + DefaultModelCriteria mcb = criteria(); + assertThat(mcb.and( + mcb.compare(RoleModel.SearchableFields.REALM_ID, Operator.EQ, "realmId"), + mcb.compare(RoleModel.SearchableFields.CLIENT_ID, Operator.EQ, "clientId"), + mcb.or( + mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%search%"), + mcb.compare(RoleModel.SearchableFields.DESCRIPTION, Operator.ILIKE, "%search%") + ) + ), hasToString("(realmId EQ [realmId] && clientId EQ [clientId] && (name ILIKE [%search%] || description ILIKE [%search%]))")); + + assertThat(mcb + .compare(RoleModel.SearchableFields.REALM_ID, Operator.EQ, "realmId") + .compare(RoleModel.SearchableFields.CLIENT_ID, Operator.EQ, "clientId") + .or( + mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%search%"), + mcb.compare(RoleModel.SearchableFields.DESCRIPTION, Operator.ILIKE, "%search%") + ), + hasToString("(realmId EQ [realmId] && clientId EQ [clientId] && (name ILIKE [%search%] || description ILIKE [%search%]))")); } @Test public void testComplexCompareAndOr() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); assertThat(v.or( v.and( v.compare(CLIENT_ID, Operator.EQ, 4), @@ -88,18 +120,49 @@ public class DefaultModelCriteriaTest { ), hasToString("((clientId EQ [4] && realmId EQ [aa]) || (! id EQ [5] && enabled EQ [true]))") ); + + assertThat(v.or( + v.and( + v.compare(CLIENT_ID, Operator.EQ, 4), + v.compare(REALM_ID, Operator.EQ, "aa") + ), + v.not( + v.not( + v.compare(ID, Operator.EQ, 5) + ).compare(ENABLED, Operator.EQ, "true") + ) + ), + hasToString("((clientId EQ [4] && realmId EQ [aa]) || ! (! id EQ [5] && enabled EQ [true]))") + ); + + assertThat(v.or( + v.and( + v.compare(CLIENT_ID, Operator.EQ, 4), + v.compare(REALM_ID, Operator.EQ, "aa") + ), + v.not( + v.not( + v.and( + v.compare(ID, Operator.EQ, 5) + .compare(ENABLED, Operator.EQ, "true") + ) + ) + ) + ), + hasToString("((clientId EQ [4] && realmId EQ [aa]) || (id EQ [5] && enabled EQ [true]))") + ); } @Test public void testFlashingToAnotherMCB() { - DefaultModelCriteria v = new DefaultModelCriteria<>(); + DefaultModelCriteria v = criteria(); assertThat(v.or( v.and( v.compare(CLIENT_ID, Operator.EQ, 4), v.compare(REALM_ID, Operator.EQ, "aa") ), v.not(v.compare(ID, Operator.EQ, 5)) - ).flashToModelCriteriaBuilder(new DefaultModelCriteria<>()), + ).flashToModelCriteriaBuilder(criteria()), hasToString("((clientId EQ [4] && realmId EQ [aa]) || ! id EQ [5])") ); @@ -111,9 +174,35 @@ public class DefaultModelCriteriaTest { v.not( v.compare(ID, Operator.EQ, 5) ).compare(ENABLED, Operator.EQ, "true") - ).flashToModelCriteriaBuilder(new DefaultModelCriteria<>()), + ).flashToModelCriteriaBuilder(criteria()), hasToString("((clientId EQ [4] && realmId EQ [aa]) || (! id EQ [5] && enabled EQ [true]))") ); } + @Test + public void testCloning() { + DefaultModelCriteria v = criteria(); + + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)) + .partiallyEvaluate((field, operator, operatorArguments) -> + (field == CLIENT_ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(4)) + || (field == ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(5)) + ? true + : null + ), + hasToString("(__TRUE__ && __TRUE__)")); + + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)) + .partiallyEvaluate((field, operator, operatorArguments) -> + (field == CLIENT_ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(4)) + || (field == ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(5)) + ? true + : null + ).optimize(), + hasToString("__TRUE__")); + + assertThat(v.and(v.compare(CLIENT_ID, Operator.EQ, 4).compare(ID, Operator.EQ, 5)) + .partiallyEvaluate((field, operator, operatorArguments) -> field == CLIENT_ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(6) ? true : null), + hasToString("(clientId EQ [4] && id EQ [5])")); + } }