Skip to content

Commit 1a836c0

Browse files
committed
Add FilterExpressionVisitor SPI with accept() on FilterExpression
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<R>) 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) <noreply@anthropic.com>
1 parent 9b66a1f commit 1a836c0

3 files changed

Lines changed: 237 additions & 0 deletions

File tree

scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterExpression.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,21 @@ public sealed interface FilterExpression extends Serializable
3434
default <U> U map(Function<? super FilterExpression, U> mapper) {
3535
return mapper.apply(this);
3636
}
37+
38+
/**
39+
* Dispatches to the appropriate {@link FilterExpressionVisitor} method based on
40+
* this expression's concrete type.
41+
*
42+
* @param visitor the visitor to dispatch to
43+
* @param <R> the result type
44+
* @return the result of visiting this expression
45+
*/
46+
default <R> R accept(FilterExpressionVisitor<R> visitor) {
47+
if (this instanceof AttributeComparisonExpression e) return visitor.visit(e);
48+
if (this instanceof AttributePresentExpression e) return visitor.visit(e);
49+
if (this instanceof LogicalExpression e) return visitor.visit(e);
50+
if (this instanceof GroupExpression e) return visitor.visit(e);
51+
if (this instanceof ValuePathExpression e) return visitor.visit(e);
52+
throw new IllegalStateException("Unknown FilterExpression type: " + getClass().getName());
53+
}
3754
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.directory.scim.spec.filter;
21+
22+
/**
23+
* Visitor interface for traversing a SCIM {@link FilterExpression} tree.
24+
*
25+
* <p>Since {@link FilterExpression} is a sealed interface with exactly five permitted
26+
* subtypes, implementations of this visitor are guaranteed to handle all possible
27+
* expression types at compile time.</p>
28+
*
29+
* <p>Usage: call {@link FilterExpression#accept(FilterExpressionVisitor)} to dispatch
30+
* to the appropriate {@code visit} method based on the expression's concrete type.</p>
31+
*
32+
* <p>Example — translating SCIM filters to SQL WHERE clauses:</p>
33+
* <pre>
34+
* class SqlFilterVisitor implements FilterExpressionVisitor&lt;String&gt; {
35+
* &#64;Override
36+
* public String visit(AttributeComparisonExpression expr) {
37+
* return expr.getAttributePath().getAttributeName() + " = ?";
38+
* }
39+
* // ... other visit methods
40+
* }
41+
* String sql = filter.getExpression().accept(new SqlFilterVisitor());
42+
* </pre>
43+
*
44+
* @param <R> the result type produced by visiting each expression node
45+
*/
46+
public interface FilterExpressionVisitor<R> {
47+
48+
/**
49+
* Visits an attribute comparison expression (eq, ne, co, sw, ew, gt, ge, lt, le).
50+
*
51+
* @param expr the comparison expression
52+
* @return the visitor result
53+
*/
54+
R visit(AttributeComparisonExpression expr);
55+
56+
/**
57+
* Visits an attribute presence expression (pr).
58+
*
59+
* @param expr the presence expression
60+
* @return the visitor result
61+
*/
62+
R visit(AttributePresentExpression expr);
63+
64+
/**
65+
* Visits a logical expression (and, or).
66+
*
67+
* @param expr the logical expression containing left and right operands
68+
* @return the visitor result
69+
*/
70+
R visit(LogicalExpression expr);
71+
72+
/**
73+
* Visits a group expression (parenthesized sub-expression, optionally negated with not).
74+
*
75+
* @param expr the group expression
76+
* @return the visitor result
77+
*/
78+
R visit(GroupExpression expr);
79+
80+
/**
81+
* Visits a value path expression (e.g., {@code emails[type eq "work"].value}).
82+
*
83+
* @param expr the value path expression
84+
* @return the visitor result
85+
*/
86+
R visit(ValuePathExpression expr);
87+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.directory.scim.spec.filter;
21+
22+
import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
23+
import org.junit.jupiter.api.Test;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
class FilterExpressionVisitorTest {
28+
29+
/**
30+
* A tracking visitor that records which visit overload was called
31+
* and returns the simple class name of the expression type.
32+
*/
33+
static class TrackingVisitor implements FilterExpressionVisitor<String> {
34+
String visitedType;
35+
36+
@Override
37+
public String visit(AttributeComparisonExpression expr) {
38+
visitedType = "AttributeComparisonExpression";
39+
return visitedType;
40+
}
41+
42+
@Override
43+
public String visit(AttributePresentExpression expr) {
44+
visitedType = "AttributePresentExpression";
45+
return visitedType;
46+
}
47+
48+
@Override
49+
public String visit(LogicalExpression expr) {
50+
visitedType = "LogicalExpression";
51+
return visitedType;
52+
}
53+
54+
@Override
55+
public String visit(GroupExpression expr) {
56+
visitedType = "GroupExpression";
57+
return visitedType;
58+
}
59+
60+
@Override
61+
public String visit(ValuePathExpression expr) {
62+
visitedType = "ValuePathExpression";
63+
return visitedType;
64+
}
65+
}
66+
67+
@Test
68+
void accept_withAttributeComparisonExpression_callsCorrectVisitOverload() {
69+
TrackingVisitor visitor = new TrackingVisitor();
70+
AttributeReference attrRef = new AttributeReference("userName");
71+
// Declare as FilterExpression to test polymorphic dispatch
72+
FilterExpression expr = new AttributeComparisonExpression(attrRef, CompareOperator.EQ, "john");
73+
74+
String result = expr.accept(visitor);
75+
76+
assertThat(result).isEqualTo("AttributeComparisonExpression");
77+
assertThat(visitor.visitedType).isEqualTo("AttributeComparisonExpression");
78+
}
79+
80+
@Test
81+
void accept_withAttributePresentExpression_callsCorrectVisitOverload() {
82+
TrackingVisitor visitor = new TrackingVisitor();
83+
AttributeReference attrRef = new AttributeReference("emails");
84+
FilterExpression expr = new AttributePresentExpression(attrRef);
85+
86+
String result = expr.accept(visitor);
87+
88+
assertThat(result).isEqualTo("AttributePresentExpression");
89+
assertThat(visitor.visitedType).isEqualTo("AttributePresentExpression");
90+
}
91+
92+
@Test
93+
void accept_withLogicalExpression_callsCorrectVisitOverload() {
94+
TrackingVisitor visitor = new TrackingVisitor();
95+
AttributeReference leftRef = new AttributeReference("userName");
96+
AttributeReference rightRef = new AttributeReference("displayName");
97+
FilterExpression left = new AttributeComparisonExpression(leftRef, CompareOperator.EQ, "john");
98+
FilterExpression right = new AttributeComparisonExpression(rightRef, CompareOperator.EQ, "doe");
99+
FilterExpression expr = new LogicalExpression(left, LogicalOperator.AND, right);
100+
101+
String result = expr.accept(visitor);
102+
103+
assertThat(result).isEqualTo("LogicalExpression");
104+
assertThat(visitor.visitedType).isEqualTo("LogicalExpression");
105+
}
106+
107+
@Test
108+
void accept_withGroupExpression_callsCorrectVisitOverload() {
109+
TrackingVisitor visitor = new TrackingVisitor();
110+
AttributeReference attrRef = new AttributeReference("userName");
111+
FilterExpression inner = new AttributeComparisonExpression(attrRef, CompareOperator.EQ, "john");
112+
FilterExpression expr = new GroupExpression(false, inner);
113+
114+
String result = expr.accept(visitor);
115+
116+
assertThat(result).isEqualTo("GroupExpression");
117+
assertThat(visitor.visitedType).isEqualTo("GroupExpression");
118+
}
119+
120+
@Test
121+
void accept_withValuePathExpression_callsCorrectVisitOverload() {
122+
TrackingVisitor visitor = new TrackingVisitor();
123+
AttributeReference attrRef = new AttributeReference("emails");
124+
AttributeReference filterRef = new AttributeReference("type");
125+
FilterExpression filterExpr = new AttributeComparisonExpression(filterRef, CompareOperator.EQ, "work");
126+
FilterExpression expr = new ValuePathExpression(attrRef, filterExpr);
127+
128+
String result = expr.accept(visitor);
129+
130+
assertThat(result).isEqualTo("ValuePathExpression");
131+
assertThat(visitor.visitedType).isEqualTo("ValuePathExpression");
132+
}
133+
}

0 commit comments

Comments
 (0)