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,116 @@
/*-
* #%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 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");
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 on lines +75 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail fast on malformed attribute paths.

resolve accepts malformed values (e.g. blank, leading/trailing dot, a..b) and then fails later with provider-specific exceptions. Add explicit validation here to return a deterministic IllegalArgumentException.

Proposed fix
 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("\\.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend-core-data-impl/src/main/java/com/flowingcode/backendcore/dao/jpa/AttributePathResolver.java`
around lines 75 - 83, The resolve method accepts malformed attribute paths that
fail later with provider-specific exceptions instead of failing fast. Add
explicit validation for the attributePath parameter after the null check to
ensure the path is not blank, does not have leading or trailing dots, and does
not contain consecutive dots (like a..b). Throw an IllegalArgumentException with
a descriptive message if any of these conditions are detected, before proceeding
to split and process the path. This will catch invalid inputs deterministically
at the entry point of the resolve method.


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));
}

private static Class<?> boxed(Class<?> type) {

Check failure on line 103 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