Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");

Copy link
Copy Markdown
Member

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.

BaseFilterJpaProcessor never calls setCurrentJoinType — it constructs a resolver and resolves paths, so every join is INNER and no filter can change that. Since AttributePathResolver is public in com.flowingcode.backendcore.dao.jpa, japicmp will lock this signature from 1.2.0 onward.

Two reasonable directions:

  • Wire it up — e.g. @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.
  • Reduce visibility to package-private until the join-type story is settled, so you're free to change the shape once the annotation surface is designed.

Either is fine; the thing to avoid is shipping it public and unused.

}

/**
* 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;
}
Comment thread
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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WhenNull(IS_NULL) on a nested path can never match a null association.

Joins are always created with JoinType.INNER, so the association is joined away before the IS NULL predicate is evaluated. Confirmed against the branch with @Attribute("city.name") @WhenNull(WhenNull.Policy.IS_NULL) over three Person rows — one in a named city, one in a city whose name is null, one with no city at all:

select ... from Person p1_0 join City c1_0 on c1_0.id=p1_0.city_id where c1_0.name is null

Matched 1 row, not 2 — the person with no city is dropped by the join. Any IS_NULL policy on a dotted path is silently unsatisfiable for exactly the rows it's meant to find, which is the common intent ("city unknown").

Plain @Attribute equality on a nested path has the same shape: rows with a null association are silently excluded. That one is standard JPA behavior rather than a bug, but it isn't mentioned in the @Attribute javadoc, and the auto-join is implicit enough that it will surprise people.

A LEFT join for the traversal fixes the IS_NULL case; see the separate note on setCurrentJoinType for how that might be exposed.

}

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_backend-core&issues=AZ712zL24L5Jot4fXeuf&open=AZ712zL24L5Jot4fXeuf&pullRequest=115
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;
}
}
Loading
Loading