From 4b4c812807ed7400bd8f184827fe4dc8c72b020c Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Sun, 22 Mar 2026 22:25:36 -0400 Subject: [PATCH] Add FilterExpressionVisitor SPI with accept() on FilterExpression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a visitor interface for traversing the sealed FilterExpression hierarchy. Since FilterExpression permits exactly 5 subtypes, visitor implementations are compile-time complete — all expression types must be handled. Adds accept(FilterExpressionVisitor) default method on FilterExpression that dispatches to the appropriate visit() method. This replaces the instanceof chain pattern needed by non-in-memory backends (LDAP, SQL, etc.) for filter-to-query translation. Generated-by: Claude Opus 4.6 (1M context) --- .../scim/spec/filter/FilterExpression.java | 17 +++ .../spec/filter/FilterExpressionVisitor.java | 87 ++++++++++++ .../filter/FilterExpressionVisitorTest.java | 133 ++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitor.java create mode 100644 scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitorTest.java diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpression.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpression.java index ee5671a1..e9f74682 100644 --- a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpression.java +++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpression.java @@ -34,4 +34,21 @@ public sealed interface FilterExpression extends Serializable default U map(Function mapper) { return mapper.apply(this); } + + /** + * Dispatches to the appropriate {@link FilterExpressionVisitor} method based on + * this expression's concrete type. + * + * @param visitor the visitor to dispatch to + * @param the result type + * @return the result of visiting this expression + */ + default R accept(FilterExpressionVisitor visitor) { + if (this instanceof AttributeComparisonExpression e) return visitor.visit(e); + if (this instanceof AttributePresentExpression e) return visitor.visit(e); + if (this instanceof LogicalExpression e) return visitor.visit(e); + if (this instanceof GroupExpression e) return visitor.visit(e); + if (this instanceof ValuePathExpression e) return visitor.visit(e); + throw new IllegalStateException("Unknown FilterExpression type: " + getClass().getName()); + } } diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitor.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitor.java new file mode 100644 index 00000000..79a7dc55 --- /dev/null +++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitor.java @@ -0,0 +1,87 @@ +/* + * 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; + +/** + * Visitor interface for traversing a SCIM {@link FilterExpression} tree. + * + *

Since {@link FilterExpression} is a sealed interface with exactly five permitted + * subtypes, implementations of this visitor are guaranteed to handle all possible + * expression types at compile time.

+ * + *

Usage: call {@link FilterExpression#accept(FilterExpressionVisitor)} to dispatch + * to the appropriate {@code visit} method based on the expression's concrete type.

+ * + *

Example — translating SCIM filters to SQL WHERE clauses:

+ *
+ * class SqlFilterVisitor implements FilterExpressionVisitor<String> {
+ *   @Override
+ *   public String visit(AttributeComparisonExpression expr) {
+ *     return expr.getAttributePath().getAttributeName() + " = ?";
+ *   }
+ *   // ... other visit methods
+ * }
+ * String sql = filter.getExpression().accept(new SqlFilterVisitor());
+ * 
+ * + * @param the result type produced by visiting each expression node + */ +public interface FilterExpressionVisitor { + + /** + * Visits an attribute comparison expression (eq, ne, co, sw, ew, gt, ge, lt, le). + * + * @param expr the comparison expression + * @return the visitor result + */ + R visit(AttributeComparisonExpression expr); + + /** + * Visits an attribute presence expression (pr). + * + * @param expr the presence expression + * @return the visitor result + */ + R visit(AttributePresentExpression expr); + + /** + * Visits a logical expression (and, or). + * + * @param expr the logical expression containing left and right operands + * @return the visitor result + */ + R visit(LogicalExpression expr); + + /** + * Visits a group expression (parenthesized sub-expression, optionally negated with not). + * + * @param expr the group expression + * @return the visitor result + */ + R visit(GroupExpression expr); + + /** + * Visits a value path expression (e.g., {@code emails[type eq "work"].value}). + * + * @param expr the value path expression + * @return the visitor result + */ + R visit(ValuePathExpression expr); +} diff --git a/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitorTest.java b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitorTest.java new file mode 100644 index 00000000..e6c68604 --- /dev/null +++ b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterExpressionVisitorTest.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.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FilterExpressionVisitorTest { + + /** + * A tracking visitor that records which visit overload was called + * and returns the simple class name of the expression type. + */ + static class TrackingVisitor implements FilterExpressionVisitor { + String visitedType; + + @Override + public String visit(AttributeComparisonExpression expr) { + visitedType = "AttributeComparisonExpression"; + return visitedType; + } + + @Override + public String visit(AttributePresentExpression expr) { + visitedType = "AttributePresentExpression"; + return visitedType; + } + + @Override + public String visit(LogicalExpression expr) { + visitedType = "LogicalExpression"; + return visitedType; + } + + @Override + public String visit(GroupExpression expr) { + visitedType = "GroupExpression"; + return visitedType; + } + + @Override + public String visit(ValuePathExpression expr) { + visitedType = "ValuePathExpression"; + return visitedType; + } + } + + @Test + void accept_withAttributeComparisonExpression_callsCorrectVisitOverload() { + TrackingVisitor visitor = new TrackingVisitor(); + AttributeReference attrRef = new AttributeReference("userName"); + // Declare as FilterExpression to test polymorphic dispatch + FilterExpression expr = new AttributeComparisonExpression(attrRef, CompareOperator.EQ, "john"); + + String result = expr.accept(visitor); + + assertThat(result).isEqualTo("AttributeComparisonExpression"); + assertThat(visitor.visitedType).isEqualTo("AttributeComparisonExpression"); + } + + @Test + void accept_withAttributePresentExpression_callsCorrectVisitOverload() { + TrackingVisitor visitor = new TrackingVisitor(); + AttributeReference attrRef = new AttributeReference("emails"); + FilterExpression expr = new AttributePresentExpression(attrRef); + + String result = expr.accept(visitor); + + assertThat(result).isEqualTo("AttributePresentExpression"); + assertThat(visitor.visitedType).isEqualTo("AttributePresentExpression"); + } + + @Test + void accept_withLogicalExpression_callsCorrectVisitOverload() { + TrackingVisitor visitor = new TrackingVisitor(); + AttributeReference leftRef = new AttributeReference("userName"); + AttributeReference rightRef = new AttributeReference("displayName"); + FilterExpression left = new AttributeComparisonExpression(leftRef, CompareOperator.EQ, "john"); + FilterExpression right = new AttributeComparisonExpression(rightRef, CompareOperator.EQ, "doe"); + FilterExpression expr = new LogicalExpression(left, LogicalOperator.AND, right); + + String result = expr.accept(visitor); + + assertThat(result).isEqualTo("LogicalExpression"); + assertThat(visitor.visitedType).isEqualTo("LogicalExpression"); + } + + @Test + void accept_withGroupExpression_callsCorrectVisitOverload() { + TrackingVisitor visitor = new TrackingVisitor(); + AttributeReference attrRef = new AttributeReference("userName"); + FilterExpression inner = new AttributeComparisonExpression(attrRef, CompareOperator.EQ, "john"); + FilterExpression expr = new GroupExpression(false, inner); + + String result = expr.accept(visitor); + + assertThat(result).isEqualTo("GroupExpression"); + assertThat(visitor.visitedType).isEqualTo("GroupExpression"); + } + + @Test + void accept_withValuePathExpression_callsCorrectVisitOverload() { + TrackingVisitor visitor = new TrackingVisitor(); + AttributeReference attrRef = new AttributeReference("emails"); + AttributeReference filterRef = new AttributeReference("type"); + FilterExpression filterExpr = new AttributeComparisonExpression(filterRef, CompareOperator.EQ, "work"); + FilterExpression expr = new ValuePathExpression(attrRef, filterExpr); + + String result = expr.accept(visitor); + + assertThat(result).isEqualTo("ValuePathExpression"); + assertThat(visitor.visitedType).isEqualTo("ValuePathExpression"); + } +}