ModelCriteriaBuilder.java
/*
* 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;
import org.keycloak.storage.SearchableModelField;
/**
* Builder for criteria that can be used to limit results obtained from the store.
* This class is used for similar purpose as e.g. JPA's {@code CriteriaBuilder},
* however it is much simpler version as it is tailored to very specific needs
* of future Keycloak store.
* <p>
* Implementations are expected to be immutable. The expected use is like this:
* <pre>
* cb = storage.getCriteriaBuilder();
* storage.read(
* cb.or(
* cb.compare(FIELD1, EQ, 1).compare(FIELD2, EQ, 2),
* cb.compare(FIELD1, EQ, 3).compare(FIELD2, EQ, 4)
* )
* );
* </pre>
* The above code should read items where
* {@code (FIELD1 == 1 && FIELD2 == 2) || (FIELD1 == 3 && FIELD2 == 4)}.
*
* <p>
* It is equivalent to this:
* <pre>
* cb = storage.getCriteriaBuilder();
* storage.read(
* cb.or(
* cb.and(cb.compare(FIELD1, EQ, 1), cb.compare(FIELD2, EQ, 2)),
* cb.and(cb.compare(FIELD1, EQ, 3), cb.compare(FIELD2, EQ, 4))
* )
* );
* </pre>
*
* @author hmlnarik
*/
public interface ModelCriteriaBuilder<M, Self extends ModelCriteriaBuilder<M, Self>> {
/**
* The operators are very basic ones for this use case. In the real scenario,
* new operators can be added, possibly with different arity, e.g. {@code IN}.
* The {@link ModelCriteriaBuilder#compare} method would need an adjustment
* then, likely to taky vararg {@code value} instead of single value as it
* is now.
*/
public enum Operator {
/** Equals to */
EQ,
/** Not equals to */
NE,
/** Less than */
LT,
/** Less than or equal */
LE,
/** Greater than */
GT,
/** Greater than or equal */
GE,
/** Similar to SQL case-sensitive LIKE Whole string is matched.
* Percent sign means "any characters", question mark means "any single character":
* <ul>
* <li>{@code field LIKE "abc"} means <i>value of the field {@code field} must match exactly {@code abc}</i></li>
* <li>{@code field LIKE "abc%"} means <i>value of the field {@code field} must start with {@code abc}</i></li>
* <li>{@code field LIKE "%abc"} means <i>value of the field {@code field} must end with {@code abc}</i></li>
* <li>{@code field LIKE "%abc%"} means <i>value of the field {@code field} must contain {@code abc}</i></li>
* </ul>
*/
LIKE,
/**
* Similar to SQL case-insensitive LIKE. Whole string is matched.
* Percent sign means "any characters", question mark means "any single character":
* <ul>
* <li>{@code field ILIKE "abc"} means <i>value of the field {@code field} must match exactly {@code abc}, {@code ABC}, {@code aBc} etc.</i></li>
* <li>{@code field ILIKE "abc%"} means <i>value of the field {@code field} must start with {@code abc}, {@code ABC}, {@code aBc} etc.</i></li>
* <li>{@code field ILIKE "%abc"} means <i>value of the field {@code field} must end with {@code abc}, {@code ABC}, {@code aBc} etc.</i></li>
* <li>{@code field ILIKE "%abc%"} means <i>value of the field {@code field} must contain {@code abc}, {@code ABC}, {@code aBc} etc.</i></li>
* </ul>
*/
ILIKE,
/**
* Operator for belonging into a collection of values. Operand in {@code value}
* can be an array (via an implicit conversion of the vararg), a {@link java.util.Collection} or a {@link java.util.stream.Stream}.
*/
IN,
/** Is not null and, in addition, in case of collection not empty */
EXISTS,
/** Is null or, in addition, in case of collection empty */
NOT_EXISTS,
}
/**
* Adds a constraint for the given model field to this criteria builder
* and returns a criteria builder that is combined with the the new constraint.
* The resulting constraint is a logical conjunction (i.e. AND) of the original
* constraint present in this {@link ModelCriteriaBuilder} and the given operator.
*
* @param modelField Field on the logical <i>model</i> to be constrained
* @param op Operator
* @param value Additional operands of the operator.
* @return
* @throws CriterionNotSupportedException If the operator is not supported for the given field.
*/
Self compare(SearchableModelField<? super M> modelField, Operator op, Object... value);
/**
* Creates and returns a new instance of {@code ModelCriteriaBuilder} that
* combines the given builders with the Boolean AND operator.
* <p>
* Predicate coming out of {@code and} on an empty array of {@code builders}
* (i.e. empty conjunction) is always {@code true}.
*
* <pre>
* cb = storage.getCriteriaBuilder();
* storage.read(cb.or(
* cb.and(cb.compare(FIELD1, EQ, 1), cb.compare(FIELD2, EQ, 2)),
* cb.and(cb.compare(FIELD1, EQ, 3), cb.compare(FIELD2, EQ, 4))
* );
* </pre>
*
* @throws CriterionNotSupportedException If the operator is not supported for the given field.
*/
@SuppressWarnings("unchecked")
Self and(Self... builders);
/**
* Creates and returns a new instance of {@code ModelCriteriaBuilder} that
* combines the given builders with the Boolean OR operator.
* <p>
* Predicate coming out of {@code or} on an empty array of {@code builders}
* (i.e. empty disjunction) is always {@code false}.
*
* <pre>
* cb = storage.getCriteriaBuilder();
* storage.read(cb.or(
* cb.compare(FIELD1, EQ, 1).compare(FIELD2, EQ, 2),
* cb.compare(FIELD1, EQ, 3).compare(FIELD2, EQ, 4)
* );
* </pre>
*
* @throws CriterionNotSupportedException If the operator is not supported for the given field.
*/
@SuppressWarnings("unchecked")
Self or(Self... builders);
/**
* Creates and returns a new instance of {@code ModelCriteriaBuilder} that
* negates the given builder.
* <p>
* Note that if the {@code builder} has no condition yet, there is nothing
* to negate: empty negation is always {@code true}.
*
* @param builder
* @return
* @throws CriterionNotSupportedException If the operator is not supported for the given field.
*/
Self not(Self builder);
}