Add in memory Filter matcher
This implementation can be used for demo purposes. Actual production implementations will need to translate a FilterExpresion into a query language.
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpression.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpression.java
index 0e95ae6..2fd1b2d 100644
--- a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpression.java
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpression.java
@@ -19,6 +19,8 @@
package org.apache.directory.scim.spec.protocol.filter;
+import java.util.function.Function;
+
public interface FilterExpression {
String toFilter();
@@ -26,4 +28,8 @@
void setAttributePath(String urn, String parentAttributeName);
String toUnqualifiedFilter();
+
+ default <U> U map(Function<? super FilterExpression, U> mapper) {
+ return mapper.apply(this);
+ }
}
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpressions.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpressions.java
new file mode 100644
index 0000000..3356355
--- /dev/null
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/FilterExpressions.java
@@ -0,0 +1,31 @@
+package org.apache.directory.scim.spec.protocol.filter;
+
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.apache.directory.scim.spec.resources.ScimResource;
+import org.apache.directory.scim.spec.schema.Schema;
+
+import java.util.function.Predicate;
+
+
+public final class FilterExpressions {
+
+ private FilterExpressions() {}
+
+ /**
+ * Converts a filter into a Predicate used for in-memory evaluation. Production implementations should translate the Filter into
+ * the appropriate query language.
+ * <p>
+ *
+ * <b>This implementation should only be used for demo proposes.</b>
+ */
+ public static Predicate<ScimResource> inMemory(Filter filter, Schema schema) {
+ if (filter == null) {
+ return x -> true;
+ }
+ FilterExpression expression = filter.getExpression();
+ if (expression == null) {
+ return x -> true;
+ }
+ return InMemoryScimFilterMatcher.toPredicate(expression, schema);
+ }
+}
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java
new file mode 100644
index 0000000..5faec26
--- /dev/null
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.scim.spec.protocol.filter;
+
+import org.apache.directory.scim.spec.exception.ScimResourceInvalidException;
+import org.apache.directory.scim.spec.protocol.attribute.AttributeReference;
+import org.apache.directory.scim.spec.schema.AttributeContainer;
+import org.apache.directory.scim.spec.schema.Schema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.function.Predicate;
+
+final class InMemoryScimFilterMatcher {
+
+ private static final Logger log = LoggerFactory.getLogger(InMemoryScimFilterMatcher.class);
+
+ private InMemoryScimFilterMatcher() {}
+
+ /**
+ * Converts a FilterExpression to a Predicate that can be used to later test a ScimResource (or child attribute).
+ * <p>
+ * The {@code attributeContainer} should be a top level Schema for the initial call, but recursive calls to this method
+ * will pass the AttributeContainer (a child-schema). For example, if given a
+ * {@link org.apache.directory.scim.spec.resources.ScimUser ScimUser} expression of {@code emails.value co "example.org"},
+ * The initial call to this method pass the ScimUser Schema object. When this method is called recursively, the
+ * {@link org.apache.directory.scim.spec.resources.Email Email}'s AttributeContainer will be used.
+ *
+ * @param expression the FilterExpression to build a predicate from.
+ * @param attributeContainer the Schema (or other sub attribute container) to build the predicate from.
+ * @return A Predicate that can be used to later test a ScimResource (or child attribute).
+ */
+ static <R> Predicate<R> toPredicate(FilterExpression expression, AttributeContainer attributeContainer) {
+
+ // attribute EQ "something"
+ if (expression instanceof AttributeComparisonExpression) {
+ return new AttributeComparisonPredicate<>((AttributeComparisonExpression) expression, attributeContainer);
+ }
+ // (attribute EQ "something") AND (otherAttribute EQ "something else")
+ else if (expression instanceof LogicalExpression) {
+ LogicalExpression logicalExpression = (LogicalExpression) expression;
+ Predicate<R> left = toPredicate(logicalExpression.getLeft(), attributeContainer);
+ Predicate<R> right = toPredicate(logicalExpression.getRight(), attributeContainer);
+
+ LogicalOperator op = logicalExpression.getOperator();
+ if (op == LogicalOperator.AND) {
+ return left.and(right);
+ } else {
+ return left.or(right);
+ }
+ }
+ // NOT (attribute EQ "something")
+ else if (expression instanceof GroupExpression) {
+ GroupExpression groupExpression = (GroupExpression) expression;
+ Predicate<R> predicate = toPredicate(groupExpression.getFilterExpression(), attributeContainer);
+ return groupExpression.isNot()
+ ? predicate.negate()
+ : predicate;
+ }
+ // attribute PR
+ else if (expression instanceof AttributePresentExpression) {
+ return new AttributePresentPredicate<>((AttributePresentExpression) expression, attributeContainer);
+ }
+ // addresses[type EQ "work"]
+ else if (expression instanceof ValuePathExpression) {
+ ValuePathExpression valuePathExpression = (ValuePathExpression) expression;
+ Predicate<Object> nestedPredicate = toPredicate(valuePathExpression.getAttributeExpression(), attribute(attributeContainer, valuePathExpression.getAttributePath()));
+ return new ValuePathPredicate<>(valuePathExpression, attributeContainer, nestedPredicate);
+ }
+ log.debug("Unsupported Filter expression of type: " + expression.getClass());
+ return scimResource -> false;
+ }
+
+
+ private static boolean isStringExpression(Schema.Attribute attribute, Object compareValue) {
+ if (attribute.getType() != Schema.Attribute.Type.STRING) {
+ log.debug("Invalid query, non String value for expression : " + attribute.getType());
+ return false;
+ }
+ if (compareValue == null) {
+ log.debug("Invalid query, empty value for expression : " + attribute.getType());
+ return false;
+ }
+ return true;
+ }
+
+ private static Schema.Attribute attribute(AttributeContainer attributeContainer, AttributeReference attributeReference) {
+ String baseAttributeName = attributeReference.getAttributeName();
+
+ Schema.Attribute schemaAttribute = attributeContainer.getAttribute(baseAttributeName);
+ if (schemaAttribute == null) {
+ log.warn("Invalid filter: attribute '" + baseAttributeName + "' is NOT a valid SCIM attribute.");
+ return null;
+ }
+
+ String subAttribute = attributeReference.getSubAttributeName();
+ if (subAttribute != null) {
+ schemaAttribute = schemaAttribute.getAttribute(subAttribute);
+ if (schemaAttribute == null) {
+ log.warn("Invalid filter: attribute '" + attributeReference.getFullyQualifiedAttributeName() + "' is NOT a valid SCIM attribute.");
+ return null;
+ }
+ }
+
+ // filter out fields like passwords that should not be queried against
+ if (schemaAttribute.getReturned() == Schema.Attribute.Returned.NEVER) {
+ log.warn("Invalid filter: attribute '" + attributeReference.getAttributeName() + "' is not filterable.");
+ return null;
+ }
+
+ return schemaAttribute;
+ }
+
+ private static abstract class AbstractAttributePredicate<T extends FilterExpression, R> implements Predicate<R> {
+
+ private static final Logger log = LoggerFactory.getLogger(InMemoryScimFilterMatcher.class);
+
+ final T expression;
+
+ final AttributeContainer attributeContainer;
+
+ final AttributeReference attributeReference;
+
+ private AbstractAttributePredicate(T expression, AttributeContainer attributeContainer, AttributeReference attributeReference) {
+ this.expression = expression;
+ this.attributeContainer = attributeContainer;
+ this.attributeReference = attributeReference;
+ }
+
+ protected abstract boolean test(Schema.Attribute attribute, Object actualValue);
+
+ @Override
+ public boolean test(R actual) {
+
+ try {
+ // get and validate attribute
+ Schema.Attribute resolvedAttribute = attribute(attributeContainer, attributeReference);
+ if (resolvedAttribute != null) {
+
+ // now walk the attribute path again to get the accessor and value
+ Schema.Attribute schemaAttribute = attributeContainer.getAttribute(attributeReference.getAttributeName());
+ actual = schemaAttribute.getAccessor().get(actual);
+
+ // if the attribute has a sub-level, continue on
+ String subAttribute = attributeReference.getSubAttributeName();
+ if (subAttribute != null) {
+ schemaAttribute = schemaAttribute.getAttribute(subAttribute);
+ actual = schemaAttribute.getAccessor().get(actual);
+ }
+ return test(schemaAttribute, actual);
+ }
+ } catch (Exception e) {
+ // The SCIM spec states to ignore the query instead of rejecting it - rfc7644 - 3.4.2
+ log.debug("Invalid SCIM filter received", e);
+ }
+
+ return false;
+ }
+ }
+
+ private static class ValuePathPredicate<R> extends AbstractAttributePredicate<ValuePathExpression, R> {
+
+ final private Predicate<Object> nestedPredicate;
+
+ ValuePathPredicate(ValuePathExpression expression, AttributeContainer attributeContainer, Predicate<Object> nestedPredicate) {
+ super(expression, attributeContainer, expression.getAttributePath());
+ this.nestedPredicate = nestedPredicate;
+ }
+
+ @Override
+ protected boolean test(Schema.Attribute attribute, Object actualValue) {
+ // actualValue must be a Collection
+ if (attribute.isMultiValued()) {
+ return ((Collection<?>) actualValue).stream().anyMatch(nestedPredicate);
+ }
+
+ return false;
+ }
+ }
+
+ private static class AttributeComparisonPredicate<R> extends AbstractAttributePredicate<AttributeComparisonExpression, R> {
+
+ private AttributeComparisonPredicate(AttributeComparisonExpression expression, AttributeContainer attributeContainer) {
+ super(expression, attributeContainer, expression.getAttributePath());
+ }
+
+ @Override
+ protected boolean test(Schema.Attribute attribute, Object actualValue) {
+
+ if (actualValue == null) {
+ return false;
+ }
+
+ if (attribute.isMultiValued()) {
+ log.warn("Invalid expression, target is collection");
+ return false;
+ }
+
+ CompareOperator op = expression.getOperation();
+ Object compareValue = expression.getCompareValue();
+
+ if (op == CompareOperator.EQ) {
+ return compareValue.equals(actualValue);
+ }
+ if (op == CompareOperator.NE) {
+ return !compareValue.equals(actualValue);
+ }
+ if (op == CompareOperator.SW) {
+ return isStringExpression(attribute, compareValue)
+ && actualValue.toString().startsWith(compareValue.toString());
+ }
+ if (op == CompareOperator.EW) {
+ return isStringExpression(attribute, compareValue)
+ && actualValue.toString().endsWith(compareValue.toString());
+ }
+ if (op == CompareOperator.CO) {
+ return isStringExpression(attribute, compareValue)
+ && actualValue.toString().contains(compareValue.toString());
+ }
+
+ if (actualValue instanceof Comparable) {
+ Comparable actual = (Comparable) actualValue;
+ Comparable compare = (Comparable) compareValue;
+ return CompareOperatorPredicate.naturalOrder(op, compare).test(actual);
+ }
+
+ throw new ScimResourceInvalidException("Unsupported operation in filter: " + op.name());
+ }
+ }
+
+ private static class CompareOperatorPredicate<T> implements Predicate<T> {
+ private final CompareOperator op;
+
+ private final Comparator<T> comparator;
+
+ private final T comparedValue;
+
+
+ private CompareOperatorPredicate(CompareOperator op, T comparedValue, Comparator<T> comparator) {
+ this.op = op;
+ this.comparator = comparator;
+ this.comparedValue = comparedValue;
+ }
+
+ @Override
+ public boolean test(T actualValue) {
+
+ int compareResult = comparator.compare(actualValue, comparedValue);
+
+ if (op == CompareOperator.LT) {
+ return compareResult < 0;
+ } else if (op == CompareOperator.GT) {
+ return compareResult > 0;
+ } else if (op == CompareOperator.LE) {
+ return compareResult <= 0;
+ } else if (op == CompareOperator.GE) {
+ return compareResult >= 0;
+ }
+ return false;
+ }
+
+ static <T extends Comparable<T>> CompareOperatorPredicate<T> naturalOrder(CompareOperator op, T comparedValue) {
+ return new CompareOperatorPredicate<>(op, comparedValue, Comparator.naturalOrder());
+ }
+ }
+
+ private static class AttributePresentPredicate<R> extends AbstractAttributePredicate<AttributePresentExpression, R> {
+ private AttributePresentPredicate(AttributePresentExpression expression, AttributeContainer attributeContainer) {
+ super(expression, attributeContainer, expression.getAttributePath());
+ }
+
+ @Override
+ protected boolean test(Schema.Attribute attribute, Object actualValue) {
+ if (attribute.isMultiValued()) {
+ log.debug("Invalid expression, target is collection");
+ return false;
+ }
+ return actualValue != null;
+ }
+ }
+}
diff --git a/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/LuckyNumberExtension.java b/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/LuckyNumberExtension.java
new file mode 100644
index 0000000..482cad8
--- /dev/null
+++ b/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/LuckyNumberExtension.java
@@ -0,0 +1,61 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you 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.apache.directory.scim.spec.protocol;
+
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import lombok.Data;
+import org.apache.directory.scim.spec.annotation.ScimAttribute;
+import org.apache.directory.scim.spec.annotation.ScimExtensionType;
+import org.apache.directory.scim.spec.resources.ScimExtension;
+import org.apache.directory.scim.spec.schema.Schema;
+
+/**
+ * Allows a User's lucky number to be passed as part of the User's entry via
+ * the SCIM protocol.
+ *
+ * @author Chris Harm <crh5255@psu.edu>
+ */
+@XmlRootElement( name = "LuckyNumberExtension", namespace = "http://www.psu.edu/schemas/psu-scim" )
+@XmlAccessorType(XmlAccessType.NONE)
+@Data
+@ScimExtensionType(id = LuckyNumberExtension.SCHEMA_URN, description="Lucky Numbers", name="LuckyNumbers", required=true)
+public class LuckyNumberExtension implements ScimExtension {
+
+ public static final String SCHEMA_URN = "urn:mem:params:scim:schemas:extension:LuckyNumberExtension";
+
+ @ScimAttribute(returned=Schema.Attribute.Returned.DEFAULT, required=true)
+ @XmlElement
+ private long luckyNumber;
+
+ /**
+ * Provides the URN associated with this extension which, as defined by the
+ * SCIM specification is the extension's unique identifier.
+ *
+ * @return The extension's URN.
+ */
+ @Override
+ public String getUrn() {
+ return SCHEMA_URN;
+ }
+
+}
diff --git a/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcherTest.java b/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcherTest.java
new file mode 100644
index 0000000..87b7681
--- /dev/null
+++ b/scim-spec/scim-spec-protocol/src/test/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcherTest.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.directory.scim.spec.protocol.filter;
+
+import org.apache.directory.scim.spec.protocol.LuckyNumberExtension;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.apache.directory.scim.spec.resources.*;
+import org.apache.directory.scim.spec.schema.Meta;
+import org.apache.directory.scim.spec.schema.Schemas;
+import org.assertj.core.api.AbstractAssert;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class InMemoryScimFilterMatcherTest {
+
+ private final static ScimUser USER1 = user("user1", "User", "One")
+ .setNickName("one")
+ .setAddresses(List.of(
+ new Address()
+ .setType("home")
+ .setPrimary(true)
+ .setStreetAddress("742 Evergreen Terrace")
+ .setRegion("Springfield")
+ .setLocality("Unknown")
+ .setPostalCode("012345")
+ .setCountry("USA"),
+ new Address()
+ .setType("work")
+ .setPrimary(true)
+ .setStreetAddress("101 Reactor Way")
+ .setRegion("Springfield")
+ .setLocality("Unknown")
+ .setPostalCode("012345")
+ .setCountry("USA")
+ )); static {
+ USER1.addExtension(new LuckyNumberExtension().setLuckyNumber(111));
+ }
+
+ private final static ScimUser USER2 = user("user2", "User", "Two")
+ .setAddresses(List.of(
+ new Address()
+ .setType("home")
+ .setPrimary(true)
+ .setStreetAddress("11234 Slumvillage Pass")
+ .setRegion("Burfork")
+ .setLocality("CA")
+ .setPostalCode("221134")
+ .setCountry("USA")
+ )); static {
+ USER2.addExtension(new LuckyNumberExtension().setLuckyNumber(8));
+ }
+
+ @Test
+ public void userNameMatch() {
+ FilterAssert.assertThat(FilterBuilder.create().equalTo("userName", "user1"))
+ .matches(USER1)
+ .notMatches(USER2);
+ }
+
+ @Test
+ public void familyNameMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().equalTo("name.familyName", "Two"))
+ .matches(USER2)
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void startsWithMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().startsWith("name.familyName", "Tw"))
+ .matches(USER2)
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void endsWithMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().endsWith("name.familyName", "wo"))
+ .matches(USER2)
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void containsMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().contains("name.familyName", "w"))
+ .matches(USER2)
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void givenAndFamilyNameMatches() {
+
+ FilterBuilder filter = FilterBuilder.create()
+ .equalTo("name.givenName", "User")
+ .and(builder -> builder.equalTo("name.familyName", "Two"));
+
+ FilterAssert.assertThat(filter)
+ .matches(USER2)
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void familyNameOrMatches() {
+ FilterBuilder filter = FilterBuilder.create()
+ .equalTo("name.familyName", "One")
+ .or(builder -> builder.equalTo("name.familyName", "Two"));
+
+ FilterAssert.assertThat(filter)
+ .matches(USER2)
+ .matches(USER1);
+ }
+
+ @Test
+ public void noMatchPassword() {
+ FilterAssert.assertThat(FilterBuilder.create().equalTo("password", "super-secret"))
+ .notMatches(USER1);
+ }
+
+ @Test
+ public void invertUserNameMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().not(filter -> filter.equalTo("userName", "user1")))
+ .notMatches(USER1)
+ .matches(USER2);
+ }
+
+ @Test
+ public void presentAttributeMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().present("nickName"))
+ .matches(USER1)
+ .notMatches(USER2);
+ }
+
+ @Test
+ public void valuePathExpressionMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().attributeHas("addresses", filter -> filter.equalTo("type", "work")))
+ .matches(USER1)
+ .notMatches(USER2);
+ }
+
+// @Test
+// public void extensionValueMatches() {
+// assertThat(FilterBuilder.create().equalTo("luckyNumber", 111))
+// .matches(USER1)
+// .notMatches(USER2);
+// }
+
+ @Test
+ public void metaMatches() {
+ FilterAssert.assertThat(FilterBuilder.create().lessThan("meta.lastModified", LocalDateTime.now()))
+ .matches(USER1);
+
+ FilterAssert.assertThat(FilterBuilder.create().greaterThan("meta.lastModified", LocalDateTime.now().minusYears(1)))
+ .matches(USER1);
+
+ FilterAssert.assertThat(FilterBuilder.create().greaterThanOrEquals("meta.lastModified", USER1.getMeta().getLastModified()))
+ .matches(USER1);
+
+ FilterAssert.assertThat(FilterBuilder.create().lessThanOrEquals("meta.lastModified", USER1.getMeta().getLastModified()))
+ .matches(USER1);
+
+ FilterAssert.assertThat(FilterBuilder.create().equalTo("meta.lastModified", USER1.getMeta().getLastModified()))
+ .matches(USER1);
+ }
+
+ static class FilterAssert extends AbstractAssert<FilterAssert, Filter> {
+
+ protected FilterAssert(Filter actual) {
+ super(actual, FilterAssert.class);
+ }
+
+ public FilterAssert matches(ScimUser user) {
+ isNotNull();
+ if (!FilterExpressions.inMemory(actual, Schemas.schemaFor(ScimUser.class)).test(user)) {
+ failWithMessage("Expected filter '%s' to match user '%s'", actual.toString(), user);
+ }
+ return this;
+ }
+
+ public FilterAssert notMatches(ScimUser user) {
+ isNotNull();
+ if (FilterExpressions.inMemory(actual, Schemas.schemaFor(ScimUser.class)).test(user)) {
+ failWithMessage("Expected filter '%s' to NOT match user '%s'", actual.toString(), user);
+ }
+ return this;
+ }
+
+ public static FilterAssert assertThat(Filter actual) {
+ return new FilterAssert(actual);
+ }
+
+ public static FilterAssert assertThat(FilterBuilder actual) {
+ return new FilterAssert(actual.build());
+ }
+ }
+
+ private static ScimUser user(String username, String givenName, String familyName) {
+ ScimUser user = new ScimUser()
+ .setUserName(username)
+ .setPassword("super-secret")
+ .setActive(true)
+ .setName(new Name()
+ .setGivenName(givenName)
+ .setFamilyName(familyName)
+ )
+ .setEmails(List.of(
+ new Email()
+ .setType("work")
+ .setPrimary(true)
+ .setValue(username + "@example.com"),
+ new Email()
+ .setType("personal")
+ .setValue(givenName + "." + familyName + "@example.com")
+ ));
+
+ user.setMeta(new Meta().setLastModified(LocalDateTime.now()));
+
+ return user;
+ }
+}