-
Notifications
You must be signed in to change notification settings - Fork 1
feat: spec for new BaseFilter #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /*- | ||
| * #%L | ||
| * Commons Backend - Data Access Layer Implementations | ||
| * %% | ||
| * Copyright (C) 2020 - 2026 Flowing Code | ||
| * %% | ||
| * 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. | ||
| * #L% | ||
| */ | ||
| package com.flowingcode.backendcore.dao.jpa; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
|
|
||
| import jakarta.persistence.criteria.Expression; | ||
| import jakarta.persistence.criteria.From; | ||
| import jakarta.persistence.criteria.Join; | ||
| import jakarta.persistence.criteria.JoinType; | ||
|
|
||
| /** | ||
| * Resolves a dotted attribute path on a JPA {@code From} root into a leaf | ||
| * {@code Expression}, auto-joining associations along the way and reusing | ||
| * existing joins when one is already present on the same attribute and join | ||
| * type. | ||
| * | ||
| * <p>Instances are not thread-safe: a new resolver should be created per | ||
| * {@code CriteriaQuery}. | ||
| */ | ||
| public class AttributePathResolver { | ||
|
|
||
| private final From<?, ?> root; | ||
|
|
||
| private JoinType currentJoinType = JoinType.INNER; | ||
|
|
||
| public AttributePathResolver(From<?, ?> root) { | ||
| this.root = Objects.requireNonNull(root, "root"); | ||
| } | ||
|
|
||
| /** Returns the join type currently used when creating new joins. */ | ||
| public JoinType getCurrentJoinType() { | ||
| return currentJoinType; | ||
| } | ||
|
|
||
| /** Sets the join type used for newly created joins by subsequent resolutions. */ | ||
| public void setCurrentJoinType(JoinType joinType) { | ||
| this.currentJoinType = Objects.requireNonNull(joinType, "joinType"); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves {@code attributePath} into an {@code Expression} of the leaf | ||
| * attribute on the root, auto-joining as needed. | ||
| */ | ||
| public Expression<?> resolve(String attributePath) { | ||
| return resolve(attributePath, Object.class); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves {@code attributePath} and verifies the leaf attribute's Java type | ||
| * is assignable to {@code expectedType}. | ||
| * | ||
| * @throws IllegalArgumentException if {@code attributePath} is blank, has a | ||
| * leading or trailing dot, or contains empty segments | ||
| * @throws ClassCastException if the leaf attribute type isn't compatible | ||
| */ | ||
| @SuppressWarnings("unchecked") | ||
| public <V> Expression<V> resolve(String attributePath, Class<V> expectedType) { | ||
| Objects.requireNonNull(attributePath, "attributePath"); | ||
| if (attributePath.isBlank() || attributePath.startsWith(".") | ||
| || attributePath.endsWith(".") || attributePath.contains("..")) { | ||
| throw new IllegalArgumentException("Invalid attributePath: \"" + attributePath + "\""); | ||
| } | ||
| String[] path = attributePath.split("\\."); | ||
| String attributeName = path[path.length - 1]; | ||
| String[] joinPath = Arrays.copyOf(path, path.length - 1); | ||
| Expression<?> expression = traverse(root, joinPath).get(attributeName); | ||
| boxed(expression.getJavaType()).asSubclass(expectedType); | ||
| return (Expression<V>) expression; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| private From<?, ?> traverse(From<?, ?> source, String[] path) { | ||
| From<?, ?> from = source; | ||
| for (String name : path) { | ||
| from = join(from, name); | ||
| } | ||
| return from; | ||
| } | ||
|
|
||
| @SuppressWarnings("rawtypes") | ||
| private From<?, ?> join(From<?, ?> source, String attributeName) { | ||
| Optional<Join> existing = source.getJoins().stream() | ||
| .map(j -> (Join) j) | ||
| .filter(j -> j.getAttribute().getName().equals(attributeName)) | ||
| .filter(j -> j.getJoinType() == currentJoinType) | ||
| .findFirst(); | ||
| return existing.orElseGet(() -> source.join(attributeName, currentJoinType)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Joins are always created with select ... from Person p1_0 join City c1_0 on c1_0.id=p1_0.city_id where c1_0.name is nullMatched 1 row, not 2 — the person with no city is dropped by the join. Any Plain A |
||
| } | ||
|
|
||
| private static Class<?> boxed(Class<?> type) { | ||
|
Check failure on line 109 in backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/AttributePathResolver.java
|
||
| if (type.isPrimitive()) { | ||
| if (type == boolean.class) return Boolean.class; | ||
| if (type == int.class) return Integer.class; | ||
| if (type == long.class) return Long.class; | ||
| if (type == byte.class) return Byte.class; | ||
| if (type == short.class) return Short.class; | ||
| if (type == char.class) return Character.class; | ||
| if (type == float.class) return Float.class; | ||
| if (type == double.class) return Double.class; | ||
| } | ||
| return type; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This knob is unreachable from the filter API, but the class is
public, so it becomes committed API on merge.BaseFilterJpaProcessornever callssetCurrentJoinType— it constructs a resolver and resolves paths, so every join isINNERand no filter can change that. SinceAttributePathResolveris public incom.flowingcode.backendcore.dao.jpa, japicmp will lock this signature from 1.2.0 onward.Two reasonable directions:
@Attribute(joinType = LEFT)threaded through to the resolver per path. That also fixes the@WhenNull(IS_NULL)issue flagged above, so there's a real reason to do it now rather than later.Either is fine; the thing to avoid is shipping it public and unused.