AbstractHibernateAdapter.java
/*
* Copyright 2026-present the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.data.jpa.provider;
import jakarta.persistence.Query;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.hibernate.query.SelectionQuery;
import org.jspecify.annotations.Nullable;
import org.springframework.data.jpa.util.ReflectiveMethod;
import org.springframework.data.jpa.util.ReflectiveMethods;
import org.springframework.data.util.Streamable;
import org.springframework.util.ClassUtils;
/**
* Reflective support shared by the version-specific {@link HibernateAdapter} implementations.
*
* @author Oscar Fanchin
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.2
*/
abstract class AbstractHibernateAdapter implements HibernateAdapter, ReflectiveMethods {
private final Class<?> sqmQuery;
private final Class<?> namedSqmQuery;
private final Class<?> namedNativeQuery;
private final ReflectiveMethod sqmQueryStatement;
private final ReflectiveMethod namedSqmQueryHqlString;
private final ReflectiveMethod namedSqmQueryStatement;
private final ReflectiveMethod namedNativeQuerySqlString;
/**
* @param sqmQuery candidate names of the contract implemented by SQM queries.
* @param namedSqmQuery candidate names of the named SQM query memento.
* @param namedNativeQuery candidate names of the named native query memento.
*/
AbstractHibernateAdapter(ClassLoader classLoader, CandiateClassNames sqmQuery, CandiateClassNames namedSqmQuery,
CandiateClassNames namedNativeQuery) {
this.sqmQuery = sqmQuery.getClass(classLoader);
this.namedSqmQuery = namedSqmQuery.getClass(classLoader);
this.namedNativeQuery = namedNativeQuery.getClass(classLoader);
this.sqmQueryStatement = getMethod(this.sqmQuery, "getSqmStatement");
this.namedSqmQueryHqlString = getMethod(this.namedSqmQuery, "getHqlString");
this.namedSqmQueryStatement = getMethod(this.namedSqmQuery, "getSqmStatement");
this.namedNativeQuerySqlString = getMethod(this.namedNativeQuery, "getSqlString");
}
@Override
public boolean isSqmQuery(Object query) {
return sqmQuery.isInstance(query);
}
@Override
public boolean isNamedSqmQuery(Object query) {
return namedSqmQuery.isInstance(query);
}
@Override
public boolean isNamedNativeQuery(Object query) {
return namedNativeQuery.isInstance(query);
}
@Override
public String getQueryString(Object query) {
// Hibernate 8 dropped the query string from the SQM contract, but every query implementation is a Query
if (query instanceof org.hibernate.query.Query<?> hibernateQuery) {
return hibernateQuery.getQueryString();
}
throw new IllegalStateException("Cannot obtain the query string from %s".formatted(query.getClass()));
}
@Override
public String getHqlString(Object query) {
return (String) namedSqmQueryHqlString.invoke(query);
}
@Override
public String getSqlString(Object query) {
return (String) namedNativeQuerySqlString.invoke(query);
}
@Override
public String getSqmStatement(Object query) {
ReflectiveMethod method = isSqmQuery(query) ? sqmQueryStatement : namedSqmQueryStatement;
Object statement = method.invoke(query);
if (statement == null) {
throw new IllegalStateException("No SQM statement available for %s".formatted(query.getClass()));
}
ReflectiveMethod toHqlString = getMethod(statement.getClass(), "toHqlString");
return (String) toHqlString.invoke(statement);
}
@Override
public @Nullable SelectionQuery<?> asSelectionQuery(Query query) {
return null;
}
@Override
public List<ReflectiveMethod> getReflectiveMethods() {
return List.of(sqmQueryStatement, namedSqmQueryHqlString, namedSqmQueryStatement, namedNativeQuerySqlString);
}
/**
* Resolve the no-argument method named {@code methodName} on {@code type}.
*
* @throws IllegalStateException if no such method exists on {@code type}, which indicates an unsupported Hibernate
* version.
*/
static ReflectiveMethod getMethod(Class<?> type, String methodName) {
ReflectiveMethod method = ReflectiveMethod.find(type, methodName, it -> it.getParameterCount() == 0);
if (method == null) {
throw new IllegalStateException("Cannot resolve %s.%s(). The Hibernate version on the classpath is not supported"
.formatted(type.getName(), methodName));
}
return method;
}
/**
* Collection of class name candidates.
*/
static class CandiateClassNames implements Streamable<String> {
private final List<String> classNames;
private CandiateClassNames(List<String> classNames) {
this.classNames = classNames;
}
/**
* Create a new {@link CandiateClassNames} from the given class names. Resolution attempts are made in the order.
*
* @param classNames must not be {@literal null}.
* @return new instance of {@link CandiateClassNames}.
*/
public static CandiateClassNames of(String... classNames) {
return new CandiateClassNames(Arrays.asList(classNames));
}
/**
* Resolve the first of the held candidate types that is present.
*
* @throws IllegalStateException if none of the classNames is present.
*/
Class<?> getClass(ClassLoader classLoader) {
for (String candidate : classNames) {
if (ClassUtils.isPresent(candidate, classLoader)) {
return ClassUtils.resolveClassName(candidate, classLoader);
}
}
throw new IllegalStateException(
"Cannot resolve any of the required classes: [%s]. The Hibernate version on the classpath is not supported"
.formatted(classNames));
}
@Override
public Iterator<String> iterator() {
return classNames.iterator();
}
@Override
public String toString() {
return String.join(", ", classNames);
}
}
}