You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by bd...@apache.org on 2023/01/17 19:38:04 UTC

[directory-scimple] 01/01: Abstract out basic FilterExpression mapping logic

This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a commit to branch expression-mapper
in repository https://gitbox.apache.org/repos/asf/directory-scimple.git

commit bd9574400c87c2cf36f4e0ee6a871a08ed461dff
Author: Brian Demers <bd...@apache.org>
AuthorDate: Tue Jan 17 14:37:56 2023 -0500

    Abstract out basic FilterExpression mapping logic
    
    This should allow for creation of custom mapping implementations e.g. a conversion to SQL
---
 .../spec/filter/BaseFilterExpressionMapper.java    | 133 +++++++++++++++++++++
 .../scim/spec/filter/FilterExpressions.java        |   8 +-
 .../spec/filter/InMemoryScimFilterMatcher.java     | 114 +++++-------------
 3 files changed, 164 insertions(+), 91 deletions(-)

diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/BaseFilterExpressionMapper.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/BaseFilterExpressionMapper.java
new file mode 100644
index 00000000..b89cba63
--- /dev/null
+++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/BaseFilterExpressionMapper.java
@@ -0,0 +1,133 @@
+/*
+ * 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.filter;
+
+import org.apache.directory.scim.spec.filter.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.function.BiFunction;
+
+/**
+ * The {@code BaseFilterExpressionMapper} is a utility class to aid in conversion of a {@link FilterExpression} into a
+ * class that can be used to look up or search for SCIM Resources. For example, convert a
+ * FilterExpression into a SQL query.
+ * @param <R>
+ */
+public abstract class BaseFilterExpressionMapper<R> implements BiFunction<FilterExpression, AttributeContainer, R> {
+
+  private static final Logger log = LoggerFactory.getLogger(BaseFilterExpressionMapper.class);
+
+  @Override
+  public R apply(FilterExpression expression, AttributeContainer attributeContainer) {
+
+    // attribute EQ "something"
+    if (expression instanceof AttributeComparisonExpression) {
+      return apply((AttributeComparisonExpression) expression, attributeContainer);
+    }
+    // (attribute EQ "something") AND (otherAttribute EQ "something else")
+    else if (expression instanceof LogicalExpression) {
+      return apply((LogicalExpression) expression, attributeContainer);
+    }
+    // NOT (attribute EQ "something")
+    else if (expression instanceof GroupExpression) {
+      return apply((GroupExpression) expression, attributeContainer);
+    }
+    // attribute PR
+    else if (expression instanceof AttributePresentExpression) {
+      return apply((AttributePresentExpression) expression, attributeContainer);
+    }
+    // addresses[type EQ "work"]
+    else if (expression instanceof ValuePathExpression) {
+      return apply((ValuePathExpression) expression, attributeContainer);
+    }
+    return unhandledExpression(expression, attributeContainer);
+  }
+
+  protected abstract R apply(AttributeComparisonExpression expression, AttributeContainer attributeContainer);
+
+  protected R apply(LogicalExpression expression, AttributeContainer attributeContainer) {
+    R left = apply(expression.getLeft(), attributeContainer);
+    R right = apply(expression.getRight(), attributeContainer);
+
+    LogicalOperator op = expression.getOperator();
+    return apply(op, left, right);
+  }
+
+  protected abstract R apply(LogicalOperator op, R left, R right);
+
+  protected R apply(GroupExpression expression, AttributeContainer attributeContainer) {
+    R result = apply(expression.getFilterExpression(), attributeContainer);
+    return expression.isNot()
+      ? negate(result)
+      : result;
+  }
+
+  protected abstract R negate(R expression);
+
+  protected abstract R apply(AttributePresentExpression expression, AttributeContainer attributeContainer);
+
+  protected abstract R apply(ValuePathExpression expression, AttributeContainer attributeContainer);
+
+  protected R unhandledExpression(FilterExpression expression, AttributeContainer attributeContainer) {
+    throw new IllegalArgumentException("FilterExpression '" + expression + "' is not supported");
+  }
+
+  protected 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;
+  }
+
+  protected 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;
+  }
+}
diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressions.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressions.java
index 531788dc..f6cedf2a 100644
--- a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressions.java
+++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressions.java
@@ -15,7 +15,7 @@ public final class FilterExpressions {
    * the appropriate query language.
    * <p>
    *
-   * <b>This implementation should only be used for demo proposes.</b>
+   * <b>This implementation should only be used for small collections or demo proposes.</b>
    */
   public static Predicate<ScimResource> inMemory(Filter filter, Schema schema) {
     if (filter == null) {
@@ -25,10 +25,10 @@ public final class FilterExpressions {
     if (expression == null) {
       return x -> true;
     }
-    return InMemoryScimFilterMatcher.toPredicate(expression, schema);
+    return new InMemoryScimFilterMatcher<ScimResource>().apply(expression, schema);
   }
 
-  public static Predicate<Object> inMemory(FilterExpression expression, Schema schema) {
-    return InMemoryScimFilterMatcher.toPredicate(expression, schema);
+  public static <R> Predicate<R> inMemory(FilterExpression expression, Schema schema) {
+    return new InMemoryScimFilterMatcher<R>().apply(expression, schema);
   }
 }
diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/InMemoryScimFilterMatcher.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/InMemoryScimFilterMatcher.java
index 53746867..77dc0cff 100644
--- a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/InMemoryScimFilterMatcher.java
+++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/InMemoryScimFilterMatcher.java
@@ -30,104 +30,44 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.function.Predicate;
 
-final class InMemoryScimFilterMatcher {
+final class InMemoryScimFilterMatcher<R> extends BaseFilterExpressionMapper<Predicate<R>> {
 
   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;
+  @Override
+  protected Predicate<R> apply(AttributeComparisonExpression expression, AttributeContainer attributeContainer) {
+    return new AttributeComparisonPredicate<>(expression, attributeContainer);
   }
 
-
-  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;
+  @Override
+  protected Predicate<R> apply(LogicalOperator op, Predicate<R> left, Predicate<R> right) {
+    if (op == LogicalOperator.AND) {
+      return left.and(right);
+    } else {
+      return left.or(right);
     }
-    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;
-    }
+  @Override
+  protected Predicate<R> negate(Predicate<R> expression) {
+    return expression.negate();
+  }
 
-    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;
-      }
-    }
+  @Override
+  protected Predicate<R> apply(AttributePresentExpression expression, AttributeContainer attributeContainer) {
+    return new AttributePresentPredicate<>(expression, attributeContainer);
+  }
 
-    // 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;
-    }
+  @Override
+  protected Predicate<R> apply(ValuePathExpression expression, AttributeContainer attributeContainer) {
+    Predicate<Object> nestedPredicate = new InMemoryScimFilterMatcher<>().apply(expression.getAttributeExpression(), attribute(attributeContainer, expression.getAttributePath()));
+    return new ValuePathPredicate<>(expression, attributeContainer, nestedPredicate);
+  }
 
-    return schemaAttribute;
+  @Override
+  protected Predicate<R> unhandledExpression(FilterExpression expression, AttributeContainer attributeContainer) {
+    log.debug("Unsupported Filter expression of type: " + expression.getClass());
+    return scimResource -> false;
   }
 
   private static abstract class AbstractAttributePredicate<T extends FilterExpression, R> implements Predicate<R> {