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

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

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