KEYCLOAK-18854 Introduce storage-independent ModelCriteriaBuilder

This commit is contained in:
Hynek Mlnarik 2021-10-25 16:56:37 +02:00 committed by Hynek Mlnařík
parent b0b4d01760
commit 877ae96590
4 changed files with 389 additions and 11 deletions

View file

@ -0,0 +1,99 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
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;
/**
* Descriptive model criteria implementation which in other words represents a Boolean formula on searchable fields.
* @author hmlnarik
*/
public class DefaultModelCriteria<M> implements ModelCriteriaBuilder<M> {
private final ModelCriteriaNode<M> node;
public DefaultModelCriteria() {
this.node = null;
}
private DefaultModelCriteria(ModelCriteriaNode<M> node) {
this.node = node;
}
@Override
public DefaultModelCriteria<M> compare(SearchableModelField<M> modelField, Operator op, Object... value) {
final ModelCriteriaNode<M> targetNode;
if (isEmpty()) {
targetNode = new ModelCriteriaNode<>(modelField, op, value);
} else if (node.getNodeOperator() == ExtOperator.AND) {
targetNode = node.cloneTree();
targetNode.addChild(new ModelCriteriaNode<>(modelField, op, value));
} else {
targetNode = new ModelCriteriaNode<>(ExtOperator.AND);
targetNode.addChild(node.cloneTree());
targetNode.addChild(new ModelCriteriaNode<>(modelField, op, value));
}
return new DefaultModelCriteria<>(targetNode);
}
@Override
public DefaultModelCriteria<M> and(ModelCriteriaBuilder<M>... mcbs) {
final ModelCriteriaNode<M> targetNode = new ModelCriteriaNode<>(ExtOperator.AND);
for (ModelCriteriaBuilder<M> mcb : mcbs) {
targetNode.addChild(((DefaultModelCriteria<M>) mcb.unwrap(DefaultModelCriteria.class)).node);
}
return new DefaultModelCriteria<>(targetNode);
}
@Override
public DefaultModelCriteria<M> or(ModelCriteriaBuilder<M>... mcbs) {
final ModelCriteriaNode<M> targetNode = new ModelCriteriaNode<>(ExtOperator.OR);
for (ModelCriteriaBuilder<M> mcb : mcbs) {
targetNode.addChild(((DefaultModelCriteria<M>) mcb.unwrap(DefaultModelCriteria.class)).node);
}
return new DefaultModelCriteria<>(targetNode);
}
@Override
public DefaultModelCriteria<M> not(ModelCriteriaBuilder<M> mcb) {
final ModelCriteriaNode<M> targetNode = new ModelCriteriaNode<>(ExtOperator.NOT);
targetNode.addChild(((DefaultModelCriteria<M>) mcb.unwrap(DefaultModelCriteria.class)).node);
return new DefaultModelCriteria<>(targetNode);
}
/**
* Copies contents of this {@code ModelCriteriaBuilder} into
* another {@code ModelCriteriaBuilder}.
* @param mcb {@code ModelCriteriaBuilder} to copy the contents onto
* @return Updated {@code ModelCriteriaBuilder}
*/
public ModelCriteriaBuilder<M> flashToModelCriteriaBuilder(ModelCriteriaBuilder<M> mcb) {
return mcb == null ? null : node.flashToModelCriteriaBuilder(mcb);
}
public boolean isEmpty() {
return node == null;
}
@Override
public String toString() {
return isEmpty() ? "" : node.toString();
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.models.map.storage.criteria;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.storage.tree.DefaultTreeNode;
import org.keycloak.storage.SearchableModelField;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
/**
*
* @author hmlnarik
*/
public class ModelCriteriaNode<M> extends DefaultTreeNode<ModelCriteriaNode<M>> {
public static enum ExtOperator {
AND {
@Override public <M> ModelCriteriaBuilder<M> apply(ModelCriteriaBuilder<M> mcb, ModelCriteriaNode<M> node) {
if (node.getChildren().isEmpty()) {
return null;
}
final ModelCriteriaBuilder[] operands = node.getChildren().stream()
.map(n -> n.flashToModelCriteriaBuilder(mcb))
.filter(Objects::nonNull)
.toArray(ModelCriteriaBuilder[]::new);
return operands.length == 0 ? null : mcb.and(operands);
}
@Override public String toString(ModelCriteriaNode<?> node) {
return "(" + node.getChildren().stream().map(ModelCriteriaNode::toString).collect(Collectors.joining(" && ")) + ")";
}
},
OR {
@Override public <M> ModelCriteriaBuilder<M> apply(ModelCriteriaBuilder<M> mcb, ModelCriteriaNode<M> node) {
if (node.getChildren().isEmpty()) {
return null;
}
final ModelCriteriaBuilder[] operands = node.getChildren().stream()
.map(n -> n.flashToModelCriteriaBuilder(mcb))
.filter(Objects::nonNull)
.toArray(ModelCriteriaBuilder[]::new);
return operands.length == 0 ? null : mcb.or(operands);
}
@Override public String toString(ModelCriteriaNode<?> node) {
return "(" + node.getChildren().stream().map(ModelCriteriaNode::toString).collect(Collectors.joining(" || ")) + ")";
}
},
NOT {
@Override public <M> ModelCriteriaBuilder<M> apply(ModelCriteriaBuilder<M> mcb, ModelCriteriaNode<M> node) {
return mcb.not(node.getChildren().iterator().next().flashToModelCriteriaBuilder(mcb));
}
@Override public String toString(ModelCriteriaNode<?> node) {
return "! " + node.getChildren().iterator().next().toString();
}
},
SIMPLE_OPERATOR {
@Override public <M> ModelCriteriaBuilder<M> apply(ModelCriteriaBuilder<M> mcb, ModelCriteriaNode<M> node) {
return mcb.compare(
node.field,
node.simpleOperator,
node.simpleOperatorArguments
);
}
@Override public String toString(ModelCriteriaNode<?> node) {
return node.field.getName() + " " + node.simpleOperator + " " + Arrays.deepToString(node.simpleOperatorArguments);
}
},
;
public abstract <M> ModelCriteriaBuilder<M> apply(ModelCriteriaBuilder<M> mcbCreator, ModelCriteriaNode<M> node);
public abstract String toString(ModelCriteriaNode<?> node);
}
private final ExtOperator nodeOperator;
private final Operator simpleOperator;
private final SearchableModelField<M> field;
private final Object[] simpleOperatorArguments;
public ModelCriteriaNode(SearchableModelField<M> field, Operator simpleOperator, Object... simpleOperatorArguments) {
super(Collections.emptyMap());
this.simpleOperator = simpleOperator;
this.field = field;
this.simpleOperatorArguments = simpleOperatorArguments;
this.nodeOperator = ExtOperator.SIMPLE_OPERATOR;
}
public ModelCriteriaNode(ExtOperator nodeOperator) {
super(Collections.emptyMap());
this.nodeOperator = nodeOperator;
this.simpleOperator = null;
this.field = null;
this.simpleOperatorArguments = null;
}
private ModelCriteriaNode(ExtOperator nodeOperator, Operator simpleOperator, SearchableModelField<M> field, Object[] simpleOperatorArguments) {
super(Collections.emptyMap());
this.nodeOperator = nodeOperator;
this.simpleOperator = simpleOperator;
this.field = field;
this.simpleOperatorArguments = simpleOperatorArguments;
}
public ExtOperator getNodeOperator() {
return nodeOperator;
}
public ModelCriteriaNode<M> cloneTree() {
return cloneTree(n -> new ModelCriteriaNode<>(n.nodeOperator, n.simpleOperator, n.field, n.simpleOperatorArguments));
}
public ModelCriteriaBuilder<M> flashToModelCriteriaBuilder(ModelCriteriaBuilder<M> mcb) {
final ModelCriteriaBuilder<M> res = nodeOperator.apply(mcb, this);
return res == null ? mcb : res;
}
@Override
public String toString() {
return nodeOperator.toString(this);
}
}

View file

@ -26,8 +26,8 @@ import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
@ -39,8 +39,8 @@ import java.util.function.Predicate;
*/
public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements TreeNode<Self> {
private final Map<String, Object> edgeProperties = new HashMap<>();
private final Map<String, Object> nodeProperties = new HashMap<>();
private final Map<String, Object> nodeProperties;
private final Map<String, Object> edgeProperties;
private final Map<String, Object> treeProperties;
private final LinkedList<Self> children = new LinkedList<>();
private String id;
@ -51,6 +51,14 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
*/
protected DefaultTreeNode(Map<String, Object> treeProperties) {
this.treeProperties = treeProperties;
this.edgeProperties = new HashMap<>();
this.nodeProperties = new HashMap<>();
}
public DefaultTreeNode(Map<String, Object> nodeProperties, Map<String, Object> edgeProperties, Map<String, Object> treeProperties) {
this.nodeProperties = nodeProperties;
this.edgeProperties = edgeProperties;
this.treeProperties = treeProperties;
}
@Override
@ -106,7 +114,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
@Override
public Optional<Self> findFirstDfs(Predicate<Self> visitor) {
Deque<Self> stack = new LinkedList<>();
stack.add((Self) this);
stack.add(getThis());
while (! stack.isEmpty()) {
Self node = stack.pop();
if (visitor.test(node)) {
@ -124,7 +132,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
@Override
public Optional<Self> findFirstBottommostDfs(Predicate<Self> visitor) {
Deque<Self> stack = new LinkedList<>();
stack.add((Self) this);
stack.add(getThis());
while (! stack.isEmpty()) {
Self node = stack.pop();
if (visitor.test(node)) {
@ -149,7 +157,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
@Override
public Optional<Self> findFirstBfs(Predicate<Self> visitor) {
Queue<Self> queue = new LinkedList<>();
queue.add((Self) this);
queue.add(getThis());
while (! queue.isEmpty()) {
Self node = queue.poll();
if (visitor.test(node)) {
@ -165,7 +173,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
public List<Self> getPathToRoot(PathOrientation orientation) {
LinkedList<Self> res = new LinkedList<>();
Consumer<Self> addFunc = orientation == PathOrientation.BOTTOM_FIRST ? res::addLast : res::addFirst;
Optional<Self> p = Optional.of((Self) this);
Optional<Self> p = Optional.of(getThis());
while (p.isPresent()) {
addFunc.accept(p.get());
p = p.get().getParent();
@ -186,7 +194,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
if (! this.children.contains(node)) {
this.children.add(node);
}
node.setParent((Self) this);
node.setParent(getThis());
// Prevent setting a parent of this node as a child of this node. In such a case, remove the parent of this node
for (Optional<Self> p = getParent(); p.isPresent(); p = p.get().getParent()) {
@ -205,7 +213,7 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
if (! this.children.contains(node)) {
this.children.add(index, node);
}
node.setParent((Self) this);
node.setParent(getThis());
// Prevent setting a parent of this node as a child of this node. In such a case, remove the parent of this node
for (Optional<Self> p = getParent(); p.isPresent(); p = p.get().getParent()) {
@ -276,12 +284,23 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
if (this.parent != null) {
Self previousParent = this.parent;
this.parent = null;
previousParent.removeChild((Self) this);
previousParent.removeChild(getThis());
}
if (parent != null) {
this.parent = parent;
parent.addChild((Self) this);
parent.addChild(getThis());
}
}
public <RNode extends TreeNode<? super RNode>> RNode cloneTree(Function<Self, RNode> instantiateFunc) {
final RNode res = instantiateFunc.apply(getThis());
this.getChildren().forEach(c -> res.addChild(c.cloneTree(instantiateFunc)));
return res;
}
@SuppressWarnings("unchecked")
private Self getThis() {
return (Self) this;
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.models.map.storage.criteria;
import org.keycloak.models.ClientModel;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import static org.hamcrest.MatcherAssert.assertThat;
import org.junit.Test;
import static org.hamcrest.Matchers.hasToString;
import static org.keycloak.models.ClientModel.SearchableFields.*;
/**
*
* @author hmlnarik
*/
public class DefaultModelCriteriaTest {
@Test
public void testSimpleCompare() {
DefaultModelCriteria<ClientModel> v = new DefaultModelCriteria<>();
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<ClientModel> v = new DefaultModelCriteria<>();
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, 4), v.compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] && id EQ [5])"));
}
@Test
public void testSimpleCompareOr() {
DefaultModelCriteria<ClientModel> v = new DefaultModelCriteria<>();
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, 4), v.compare(ID, Operator.EQ, 5)), hasToString("(clientId EQ [4] || id EQ [5])"));
}
@Test
public void testSimpleCompareAndOr() {
DefaultModelCriteria<ClientModel> v = new DefaultModelCriteria<>();
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")
), hasToString("((clientId EQ [4] && id EQ [5]) || attribute EQ [city, Ankh-Morpork])"));
}
@Test
public void testComplexCompareAndOr() {
DefaultModelCriteria<ClientModel> v = new DefaultModelCriteria<>();
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))
),
hasToString("((clientId EQ [4] && realmId EQ [aa]) || ! id EQ [5])")
);
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)
).compare(ENABLED, Operator.EQ, "true")
),
hasToString("((clientId EQ [4] && realmId EQ [aa]) || (! id EQ [5] && enabled EQ [true]))")
);
}
@Test
public void testFlashingToAnotherMCB() {
DefaultModelCriteria<ClientModel> v = new DefaultModelCriteria<>();
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<>()),
hasToString("((clientId EQ [4] && realmId EQ [aa]) || ! id EQ [5])")
);
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)
).compare(ENABLED, Operator.EQ, "true")
).flashToModelCriteriaBuilder(new DefaultModelCriteria<>()),
hasToString("((clientId EQ [4] && realmId EQ [aa]) || (! id EQ [5] && enabled EQ [true]))")
);
}
}