HHH-16633 validate the return type of @HQL query methods
(including constructors for record returns)
This commit is contained in:
parent
df11070c77
commit
52bfbe06f2
|
@ -24,7 +24,6 @@ import org.hibernate.metamodel.model.domain.MapPersistentAttribute;
|
||||||
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
|
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
|
||||||
import org.hibernate.metamodel.model.domain.SetPersistentAttribute;
|
import org.hibernate.metamodel.model.domain.SetPersistentAttribute;
|
||||||
import org.hibernate.metamodel.model.domain.SingularPersistentAttribute;
|
import org.hibernate.metamodel.model.domain.SingularPersistentAttribute;
|
||||||
import org.hibernate.query.PathException;
|
|
||||||
import org.hibernate.query.criteria.JpaCrossJoin;
|
import org.hibernate.query.criteria.JpaCrossJoin;
|
||||||
import org.hibernate.query.criteria.JpaCteCriteria;
|
import org.hibernate.query.criteria.JpaCteCriteria;
|
||||||
import org.hibernate.query.criteria.JpaDerivedJoin;
|
import org.hibernate.query.criteria.JpaDerivedJoin;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import javax.lang.model.element.Modifier;
|
||||||
import javax.lang.model.element.Name;
|
import javax.lang.model.element.Name;
|
||||||
import javax.lang.model.element.TypeElement;
|
import javax.lang.model.element.TypeElement;
|
||||||
import javax.lang.model.element.VariableElement;
|
import javax.lang.model.element.VariableElement;
|
||||||
|
import javax.lang.model.type.ArrayType;
|
||||||
import javax.lang.model.type.DeclaredType;
|
import javax.lang.model.type.DeclaredType;
|
||||||
import javax.lang.model.type.ExecutableType;
|
import javax.lang.model.type.ExecutableType;
|
||||||
import javax.lang.model.type.TypeKind;
|
import javax.lang.model.type.TypeKind;
|
||||||
|
@ -39,9 +40,14 @@ import org.hibernate.jpamodelgen.util.AccessTypeInformation;
|
||||||
import org.hibernate.jpamodelgen.util.Constants;
|
import org.hibernate.jpamodelgen.util.Constants;
|
||||||
import org.hibernate.jpamodelgen.validation.ProcessorSessionFactory;
|
import org.hibernate.jpamodelgen.validation.ProcessorSessionFactory;
|
||||||
import org.hibernate.jpamodelgen.validation.Validation;
|
import org.hibernate.jpamodelgen.validation.Validation;
|
||||||
|
import org.hibernate.metamodel.model.domain.EntityDomainType;
|
||||||
|
import org.hibernate.query.criteria.JpaEntityJoin;
|
||||||
|
import org.hibernate.query.criteria.JpaRoot;
|
||||||
|
import org.hibernate.query.criteria.JpaSelection;
|
||||||
import org.hibernate.query.sqm.SqmExpressible;
|
import org.hibernate.query.sqm.SqmExpressible;
|
||||||
import org.hibernate.query.sqm.tree.SqmStatement;
|
import org.hibernate.query.sqm.tree.SqmStatement;
|
||||||
import org.hibernate.query.sqm.tree.expression.SqmParameter;
|
import org.hibernate.query.sqm.tree.expression.SqmParameter;
|
||||||
|
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
|
||||||
|
|
||||||
import static java.beans.Introspector.decapitalize;
|
import static java.beans.Introspector.decapitalize;
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
|
@ -922,18 +928,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
||||||
putMember( attribute.getPropertyName() + paramTypes, attribute );
|
putMember( attribute.getPropertyName() + paramTypes, attribute );
|
||||||
|
|
||||||
if ( !isNative ) {
|
if ( !isNative ) {
|
||||||
final SqmStatement<?> statement =
|
validateHql( method, returnType, mirror, value, hql, paramNames, paramTypes );
|
||||||
Validation.validate(
|
|
||||||
hql,
|
|
||||||
true,
|
|
||||||
new ErrorHandler( context, method, mirror, value, hql ),
|
|
||||||
ProcessorSessionFactory.create( context.getProcessingEnvironment() )
|
|
||||||
);
|
|
||||||
if ( statement != null ) {
|
|
||||||
for ( SqmParameter<?> param : statement.getSqmParameters() ) {
|
|
||||||
checkParameter( param, paramNames, paramTypes, method, mirror, value );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//TODO: for SQL queries check that there is a method parameter for every query parameter
|
//TODO: for SQL queries check that there is a method parameter for every query parameter
|
||||||
|
|
||||||
|
@ -943,6 +938,159 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateHql(
|
||||||
|
ExecutableElement method,
|
||||||
|
@Nullable TypeMirror returnType,
|
||||||
|
AnnotationMirror mirror,
|
||||||
|
AnnotationValue value,
|
||||||
|
String hql,
|
||||||
|
List<String> paramNames, List<String> paramTypes) {
|
||||||
|
final SqmStatement<?> statement =
|
||||||
|
Validation.validate(
|
||||||
|
hql,
|
||||||
|
true,
|
||||||
|
new ErrorHandler( context, method, mirror, value, hql),
|
||||||
|
ProcessorSessionFactory.create( context.getProcessingEnvironment() )
|
||||||
|
);
|
||||||
|
if ( statement != null ) {
|
||||||
|
if ( statement instanceof SqmSelectStatement && returnType != null ) {
|
||||||
|
final SqmSelectStatement<?> select = (SqmSelectStatement<?>) statement;
|
||||||
|
final JpaSelection<?> selection = select.getSelection();
|
||||||
|
boolean returnTypeCorrect;
|
||||||
|
if ( selection.isCompoundSelection() ) {
|
||||||
|
switch ( returnType.getKind() ) {
|
||||||
|
case ARRAY:
|
||||||
|
returnTypeCorrect = checkReturnedArrayType((ArrayType) returnType);
|
||||||
|
break;
|
||||||
|
case DECLARED:
|
||||||
|
if ( !checkConstructorReturn( (DeclaredType) returnType, selection ) ) {
|
||||||
|
context.message(method, mirror, value,
|
||||||
|
"return type '" + returnType
|
||||||
|
+ "' of method has no constructor matching query selection list",
|
||||||
|
Diagnostic.Kind.ERROR);
|
||||||
|
}
|
||||||
|
returnTypeCorrect = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
returnTypeCorrect = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( selection instanceof JpaEntityJoin ) {
|
||||||
|
final JpaEntityJoin<?> from = (JpaEntityJoin<?>) selection;
|
||||||
|
returnTypeCorrect = checkReturnedEntity( from.getModel(), returnType );
|
||||||
|
}
|
||||||
|
else if ( selection instanceof JpaRoot ) {
|
||||||
|
final JpaRoot<?> from = (JpaRoot<?>) selection;
|
||||||
|
returnTypeCorrect = checkReturnedEntity( from.getModel(), returnType );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO: anything more we can do here? e.g. check constructor
|
||||||
|
returnTypeCorrect = true;
|
||||||
|
}
|
||||||
|
if ( !returnTypeCorrect ) {
|
||||||
|
context.message(method, mirror, value,
|
||||||
|
"return type of query did not match return type '" + returnType + "' of method",
|
||||||
|
Diagnostic.Kind.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ( SqmParameter<?> param : statement.getSqmParameters() ) {
|
||||||
|
checkParameter( param, paramNames, paramTypes, method, mirror, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean checkConstructorReturn(DeclaredType returnType, JpaSelection<?> selection) {
|
||||||
|
final List<? extends JpaSelection<?>> selectionItems = selection.getSelectionItems();
|
||||||
|
if ( selectionItems == null ) {
|
||||||
|
// should not occur
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final TypeElement typeElement = (TypeElement) returnType.asElement();
|
||||||
|
final Name qualifiedName = typeElement.getQualifiedName();
|
||||||
|
if ( qualifiedName.contentEquals(Constants.TUPLE)
|
||||||
|
|| qualifiedName.contentEquals(Constants.LIST)
|
||||||
|
|| qualifiedName.contentEquals(Constants.MAP) ) {
|
||||||
|
// these are exceptionally allowed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// otherwise we need appropriate constructor
|
||||||
|
for ( Element member : typeElement.getEnclosedElements() ) {
|
||||||
|
if ( member.getKind() == ElementKind.CONSTRUCTOR ) {
|
||||||
|
final ExecutableElement constructor = (ExecutableElement) member;
|
||||||
|
if ( constructorMatches( selectionItems, constructor.getParameters() ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean constructorMatches(
|
||||||
|
List<? extends JpaSelection<?>> selectionItems,
|
||||||
|
List<? extends VariableElement> parameters) {
|
||||||
|
int itemCount = selectionItems.size();
|
||||||
|
if ( parameters.size() == itemCount ) {
|
||||||
|
for (int i = 0; i < itemCount; i++ ) {
|
||||||
|
final JpaSelection<?> item = selectionItems.get(i);
|
||||||
|
if ( item != null && item.getJavaType() != null ) {
|
||||||
|
if ( !parameterMatches( parameters.get(i), item ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean parameterMatches(VariableElement parameter, JpaSelection<?> item) {
|
||||||
|
final Class<?> itemType = item.getJavaType();
|
||||||
|
final TypeMirror parameterType = parameter.asType();
|
||||||
|
final TypeKind kind = parameterType.getKind();
|
||||||
|
final String itemTypeName = itemType.getName();
|
||||||
|
if ( kind == TypeKind.DECLARED ) {
|
||||||
|
final DeclaredType declaredType = (DeclaredType) parameterType;
|
||||||
|
final TypeElement paramTypeElement = (TypeElement) declaredType.asElement();
|
||||||
|
return paramTypeElement.getQualifiedName().contentEquals(itemTypeName);
|
||||||
|
}
|
||||||
|
else if ( kind.isPrimitive() ) {
|
||||||
|
return parameterType.toString().equals(itemTypeName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean checkReturnedArrayType(ArrayType returnType) {
|
||||||
|
final TypeMirror componentType = returnType.getComponentType();
|
||||||
|
if ( componentType.getKind() == TypeKind.DECLARED ) {
|
||||||
|
final DeclaredType declaredType = (DeclaredType) componentType;
|
||||||
|
final TypeElement typeElement = (TypeElement) declaredType.asElement();
|
||||||
|
return typeElement.getQualifiedName().contentEquals("java.lang.Object");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkReturnedEntity(EntityDomainType<?> model, TypeMirror returnType) {
|
||||||
|
if ( returnType.getKind() == TypeKind.DECLARED ) {
|
||||||
|
final DeclaredType declaredType = (DeclaredType) returnType;
|
||||||
|
final TypeElement typeElement = (TypeElement) declaredType.asElement();
|
||||||
|
final AnnotationMirror mirror = getAnnotationMirror(typeElement, Constants.ENTITY );
|
||||||
|
if ( mirror != null ) {
|
||||||
|
final Object value = getAnnotationValue( mirror, "name" );
|
||||||
|
final String entityName = value instanceof String ? (String) value : typeElement.getSimpleName().toString();
|
||||||
|
return model.getHibernateEntityName().equals( entityName );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void checkParameter(
|
private void checkParameter(
|
||||||
SqmParameter<?> param, List<String> paramNames, List<String> paramTypes,
|
SqmParameter<?> param, List<String> paramNames, List<String> paramTypes,
|
||||||
ExecutableElement method, AnnotationMirror mirror, AnnotationValue value) {
|
ExecutableElement method, AnnotationMirror mirror, AnnotationValue value) {
|
||||||
|
|
|
@ -72,6 +72,8 @@ public final class Constants {
|
||||||
public static final String HIB_STATELESS_SESSION = "org.hibernate.StatelessSession";
|
public static final String HIB_STATELESS_SESSION = "org.hibernate.StatelessSession";
|
||||||
public static final String MUTINY_SESSION = "org.hibernate.reactive.mutiny.Mutiny.Session";
|
public static final String MUTINY_SESSION = "org.hibernate.reactive.mutiny.Mutiny.Session";
|
||||||
|
|
||||||
|
public static final String TUPLE = "jakarta.persistence.Tuple";
|
||||||
|
|
||||||
public static final String UNI = "io.smallrye.mutiny.Uni";
|
public static final String UNI = "io.smallrye.mutiny.Uni";
|
||||||
|
|
||||||
public static final String SINGULAR_ATTRIBUTE = "jakarta.persistence.metamodel.SingularAttribute";
|
public static final String SINGULAR_ATTRIBUTE = "jakarta.persistence.metamodel.SingularAttribute";
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.hibernate.Session;
|
||||||
import org.hibernate.StatelessSession;
|
import org.hibernate.StatelessSession;
|
||||||
import org.hibernate.annotations.processing.Find;
|
import org.hibernate.annotations.processing.Find;
|
||||||
import org.hibernate.annotations.processing.HQL;
|
import org.hibernate.annotations.processing.HQL;
|
||||||
|
import org.hibernate.query.Order;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -28,5 +29,8 @@ public abstract class Books {
|
||||||
@HQL("from Book where title like ?2 order by title fetch first ?3 rows only")
|
@HQL("from Book where title like ?2 order by title fetch first ?3 rows only")
|
||||||
abstract List<Book> findFirstNByTitle(Session session, String title, int N);
|
abstract List<Book> findFirstNByTitle(Session session, String title, int N);
|
||||||
|
|
||||||
|
static class Summary { Summary(String title, String publisher, String isbn) {} }
|
||||||
|
|
||||||
|
@HQL("select title, publisher.name, isbn from Book")
|
||||||
|
abstract List<Summary> summarize(Session session, Order order);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue