KEYCLOAK-18854 Introduce storage-independent ModelCriteriaBuilder
This commit is contained in:
parent
b0b4d01760
commit
877ae96590
4 changed files with 389 additions and 11 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]))")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue