HHH-16887 also validate arguments of @NamedQuery if @CheckHQL is specified

This commit is contained in:
Gavin King 2023-07-04 16:58:24 +02:00
parent 13877a9a3e
commit 28b1670d18
8 changed files with 175 additions and 85 deletions

View File

@ -19,29 +19,32 @@ import static java.lang.annotation.RetentionPolicy.CLASS;
/** /**
* Indicates that a package or top-level type contains HQL or JPQL * Indicates that a package or top-level type contains HQL or JPQL
* queries encoded as static strings that should be validated at * queries encoded as static strings that should be validated at
* compile time by the Hibernate Query Validator. The Query Validator * compile time by the Metamodel Generator or Query Validator.
* must be enabled as an annotation processor in the project build. * Errors in queries are reported by the Java compiler.
* Otherwise, this annotation has no effect.
* <p> * <p>
* Within a scope annotated {@code @CheckHQL}, any static string * The Metamodel Generator or Query Validator must be enabled as an
* argument to any one the methods: * annotation processor in the project build. Otherwise, if neither
* is enabled, this annotation has no effect.
* <p>
* If only the Metamodel Generator is enabled, only arguments to the
* following annotations are validated:
* <ul>
* <li>{@link jakarta.persistence.NamedQuery#query},
* <li>{@link org.hibernate.annotations.NamedQuery#query}.
* </ul>
* <p>
* Otherwise, if the Query validator is enabled, then, within the
* scope annotated {@code @CheckHQL}, any static string argument to
* any one of the following methods is interpreted as HQL/JPQL and
* validated:
* <ul> * <ul>
* <li>{@link jakarta.persistence.EntityManager#createQuery(String,Class)}, * <li>{@link jakarta.persistence.EntityManager#createQuery(String,Class)},
* <li>{@link jakarta.persistence.EntityManager#createQuery(String)}, * <li>{@link jakarta.persistence.EntityManager#createQuery(String)},
* <li>{@link org.hibernate.Session#createSelectionQuery(String,Class)}, or * <li>{@link org.hibernate.Session#createSelectionQuery(String,Class)}, or
* <li>{@link org.hibernate.Session#createMutationQuery(String)} * <li>{@link org.hibernate.Session#createMutationQuery(String)}
* </ul> * </ul>
* or to any one of the annotation members:
* <ul>
* <li>{@link jakarta.persistence.NamedQuery#query},
* <li>{@link org.hibernate.annotations.NamedQuery#query}, or
* <li>{@link HQL#value}
* </ul>
* <p> * <p>
* is interpreted as HQL/JPQL and validated. Errors in the query are * The entity classes referred to by the queries must be annotated
* reported by the Java compiler.
* <p>
* The entity classes referred to in the queries must be annotated
* with basic JPA metadata annotations like {@code @Entity}, * with basic JPA metadata annotations like {@code @Entity},
* {@code @ManyToOne}, {@code @Embeddable}, {@code @MappedSuperclass}, * {@code @ManyToOne}, {@code @Embeddable}, {@code @MappedSuperclass},
* {@code @ElementCollection}, and {@code @Access}. Metadata specified * {@code @ElementCollection}, and {@code @Access}. Metadata specified
@ -50,7 +53,6 @@ import static java.lang.annotation.RetentionPolicy.CLASS;
* Syntax errors, unknown entity names and unknown entity member names, * Syntax errors, unknown entity names and unknown entity member names,
* and typing errors all result in compile-time errors. * and typing errors all result in compile-time errors.
* *
* @see HQL#value()
* @see jakarta.persistence.NamedQuery#query() * @see jakarta.persistence.NamedQuery#query()
* @see jakarta.persistence.EntityManager#createQuery(String,Class) * @see jakarta.persistence.EntityManager#createQuery(String,Class)
* @see org.hibernate.Session#createSelectionQuery(String,Class) * @see org.hibernate.Session#createSelectionQuery(String,Class)

View File

@ -52,6 +52,10 @@ import static java.lang.annotation.RetentionPolicy.CLASS;
* <p> * <p>
* The method parameters must match the parameters of the HQL query, * The method parameters must match the parameters of the HQL query,
* either by name or by position. * either by name or by position.
* <p>
* Queries specified using this annotation are always validated by
* the Metamodel Generator, and so it isn't necessary to specify the
* {@link CheckHQL} annotation.
* *
* @author Gavin King * @author Gavin King
* @since 6.3 * @since 6.3

View File

@ -14,6 +14,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements; import javax.lang.model.util.Elements;
import javax.lang.model.util.Types; import javax.lang.model.util.Types;
@ -241,6 +243,19 @@ public final class Context {
return lazyXmlParsing; return lazyXmlParsing;
} }
public void message(Element method, String message, Diagnostic.Kind severity) {
getProcessingEnvironment().getMessager()
.printMessage( severity, message, method );
}
public void message(Element method, AnnotationMirror mirror, String message, Diagnostic.Kind severity) {
getProcessingEnvironment().getMessager()
.printMessage( severity, message, method, mirror,
mirror.getElementValues().entrySet().stream()
.filter( entry -> entry.getKey().getSimpleName().contentEquals("query")
|| entry.getKey().getSimpleName().contentEquals("value") )
.map(Map.Entry::getValue).findAny().orElseThrow() );
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();

View File

@ -10,10 +10,14 @@ import org.hibernate.jpamodelgen.model.MetaAttribute;
import org.hibernate.jpamodelgen.model.Metamodel; import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants; import org.hibernate.jpamodelgen.util.Constants;
import org.hibernate.jpamodelgen.util.TypeUtils; import org.hibernate.jpamodelgen.util.TypeUtils;
import org.hibernate.jpamodelgen.validation.ProcessorSessionFactory;
import org.hibernate.jpamodelgen.validation.Validation;
import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationMirror;
import java.util.List; import java.util.List;
import static java.util.Collections.emptySet;
public abstract class AnnotationMeta implements Metamodel { public abstract class AnnotationMeta implements Metamodel {
void addAuxiliaryMembers() { void addAuxiliaryMembers() {
@ -36,6 +40,54 @@ public abstract class AnnotationMeta implements Metamodel {
addAuxiliaryMembersForRepeatableAnnotation( Constants.HIB_FILTER_DEFS, "FILTER_" ); addAuxiliaryMembersForRepeatableAnnotation( Constants.HIB_FILTER_DEFS, "FILTER_" );
} }
void checkNamedQueries() {
if ( TypeUtils.containsAnnotation( getElement(), Constants.CHECK_HQL )
|| TypeUtils.containsAnnotation( getElement().getEnclosingElement(), Constants.CHECK_HQL ) ) {
checkNamedQueriesForAnnotation( Constants.NAMED_QUERY );
checkNamedQueriesForRepeatableAnnotation( Constants.NAMED_QUERIES );
checkNamedQueriesForAnnotation( Constants.HIB_NAMED_QUERY );
checkNamedQueriesForRepeatableAnnotation( Constants.HIB_NAMED_QUERIES );
}
}
private void checkNamedQueriesForAnnotation(String annotationName) {
AnnotationMirror mirror = TypeUtils.getAnnotationMirror(getElement(), annotationName);
if ( mirror != null ) {
checkNamedQueriesForMirror( mirror );
}
}
private void checkNamedQueriesForRepeatableAnnotation(String annotationName) {
AnnotationMirror mirror = TypeUtils.getAnnotationMirror(getElement(), annotationName);
if ( mirror != null ) {
mirror.getElementValues().forEach((key, value) -> {
if ( key.getSimpleName().contentEquals("value") ) {
List<? extends AnnotationMirror> values =
(List<? extends AnnotationMirror>) value.getValue();
for ( AnnotationMirror annotationMirror : values ) {
checkNamedQueriesForMirror( annotationMirror );
}
}
});
}
}
private void checkNamedQueriesForMirror(AnnotationMirror mirror) {
mirror.getElementValues().forEach((key, value) -> {
if ( key.getSimpleName().contentEquals("query") ) {
String hql = value.getValue().toString();
Validation.validate(
hql,
false,
emptySet(), emptySet(),
new ErrorHandler( getElement(), mirror, hql, getContext()),
ProcessorSessionFactory.create( getContext().getProcessingEnvironment() )
);
}
});
}
private void addAuxiliaryMembersForRepeatableAnnotation(String annotationName, String prefix) { private void addAuxiliaryMembersForRepeatableAnnotation(String annotationName, String prefix) {
AnnotationMirror mirror = TypeUtils.getAnnotationMirror( getElement(), annotationName ); AnnotationMirror mirror = TypeUtils.getAnnotationMirror( getElement(), annotationName );
if ( mirror != null ) { if ( mirror != null ) {

View File

@ -7,7 +7,6 @@
package org.hibernate.jpamodelgen.annotation; package org.hibernate.jpamodelgen.annotation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -24,11 +23,6 @@ import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter; import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic; import javax.tools.Diagnostic;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.jpamodelgen.Context; import org.hibernate.jpamodelgen.Context;
import org.hibernate.jpamodelgen.ImportContextImpl; import org.hibernate.jpamodelgen.ImportContextImpl;
@ -49,7 +43,6 @@ import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation;
import static org.hibernate.jpamodelgen.util.TypeUtils.determineAnnotationSpecifiedAccessType; import static org.hibernate.jpamodelgen.util.TypeUtils.determineAnnotationSpecifiedAccessType;
import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationMirror; import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationMirror;
import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationValue; import static org.hibernate.jpamodelgen.util.TypeUtils.getAnnotationValue;
import static org.hibernate.query.hql.internal.StandardHqlTranslator.prettifyAntlrError;
/** /**
* Class used to collect meta information about an annotated type (entity, embeddable or mapped superclass). * Class used to collect meta information about an annotated type (entity, embeddable or mapped superclass).
@ -231,6 +224,8 @@ public class AnnotationMetaEntity extends AnnotationMeta {
addAuxiliaryMembers(); addAuxiliaryMembers();
checkNamedQueries();
addQueryMethods( queryMethods ); addQueryMethods( queryMethods );
initialized = true; initialized = true;
@ -320,11 +315,11 @@ public class AnnotationMetaEntity extends AnnotationMeta {
addQueryMethod( method, methodName, returnTypeName, containerTypeName ); addQueryMethod( method, methodName, returnTypeName, containerTypeName );
} }
else { else {
displayError( method, "incorrect return type '" + containerTypeName + "'" ); context.message( method, "incorrect return type '" + containerTypeName + "'", Diagnostic.Kind.ERROR );
} }
} }
else { else {
displayError( method, "incorrect return type '" + declaredType + "'" ); context.message( method, "incorrect return type '" + declaredType + "'", Diagnostic.Kind.ERROR );
} }
} }
} }
@ -397,7 +392,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
hql, hql,
false, false,
emptySet(), emptySet(), emptySet(), emptySet(),
new ErrorHandler( method, mirror, hql ), new ErrorHandler( method, mirror, hql, context),
ProcessorSessionFactory.create( context.getProcessingEnvironment() ) ProcessorSessionFactory.create( context.getProcessingEnvironment() )
); );
} }
@ -408,8 +403,8 @@ public class AnnotationMetaEntity extends AnnotationMeta {
for (int i = 1; i <= paramNames.size(); i++) { for (int i = 1; i <= paramNames.size(); i++) {
final String param = paramNames.get(i-1); final String param = paramNames.get(i-1);
if ( !hql.contains(":" + param) && !hql.contains("?" + i) ) { if ( !hql.contains(":" + param) && !hql.contains("?" + i) ) {
displayError( method, mirror, "missing query parameter for '" + param context.message( method, mirror, "missing query parameter for '" + param
+ "' (no parameter named :" + param + " or ?" + i + ")" ); + "' (no parameter named :" + param + " or ?" + i + ")", Diagnostic.Kind.ERROR );
} }
} }
} }
@ -440,62 +435,5 @@ public class AnnotationMetaEntity extends AnnotationMeta {
// } // }
// } // }
private void displayError(Element method, String message) {
context.getProcessingEnvironment().getMessager()
.printMessage( Diagnostic.Kind.ERROR, message, method );
}
private void displayError(Element method, AnnotationMirror mirror, String message) {
context.getProcessingEnvironment().getMessager()
.printMessage( Diagnostic.Kind.ERROR, message, method, mirror,
mirror.getElementValues().entrySet().stream()
.filter( entry -> entry.getKey().getSimpleName().toString().equals("value") )
.map(Map.Entry::getValue).findAny().orElseThrow() );
}
private class ErrorHandler implements Validation.Handler {
private final ExecutableElement method;
private final AnnotationMirror mirror;
private final String queryString;
private int errorcount;
public ErrorHandler(ExecutableElement method, AnnotationMirror mirror, String queryString) {
this.method = method;
this.mirror = mirror;
this.queryString = queryString;
}
@Override
public void error(int start, int end, String message) {
errorcount++;
displayError( method, mirror, message );
}
@Override
public void warn(int start, int end, String message) {
}
@Override
public int getErrorCount() {
return errorcount;
}
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException e) {
errorcount++;
displayError( method, mirror, "illegal HQL syntax - "
+ prettifyAntlrError( offendingSymbol, line, charPositionInLine, message, e, queryString, false ) );
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs) {
}
}
} }

View File

@ -118,6 +118,8 @@ public class AnnotationMetaPackage extends AnnotationMeta {
addAuxiliaryMembers(); addAuxiliaryMembers();
checkNamedQueries();
initialized = true; initialized = true;
} }

View File

@ -0,0 +1,75 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpamodelgen.annotation;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.hibernate.jpamodelgen.Context;
import org.hibernate.jpamodelgen.validation.Validation;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import java.util.BitSet;
import static org.hibernate.query.hql.internal.StandardHqlTranslator.prettifyAntlrError;
/**
* @author Gavin King
*/
class ErrorHandler implements Validation.Handler {
private final Element element;
private final AnnotationMirror mirror;
private final String queryString;
private final Context context;
private int errorCount;
public ErrorHandler(Element element, AnnotationMirror mirror, String queryString, Context context) {
this.element = element;
this.mirror = mirror;
this.queryString = queryString;
this.context = context;
}
@Override
public int getErrorCount() {
return errorCount;
}
@Override
public void error(int start, int end, String message) {
errorCount++;
context.message( element, mirror, message, Diagnostic.Kind.ERROR );
}
@Override
public void warn(int start, int end, String message) {
}
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException e) {
errorCount++;
String prettyMessage = "illegal HQL syntax - "
+ prettifyAntlrError( offendingSymbol, line, charPositionInLine, message, e, queryString, false );
context.message( element, mirror, prettyMessage, Diagnostic.Kind.ERROR );
}
@Override
public void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs) {
}
@Override
public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs) {
}
}

View File

@ -53,6 +53,8 @@ public final class Constants {
public static final String HQL = "org.hibernate.annotations.processing.HQL"; public static final String HQL = "org.hibernate.annotations.processing.HQL";
public static final String SQL = "org.hibernate.annotations.processing.SQL"; public static final String SQL = "org.hibernate.annotations.processing.SQL";
public static final String CHECK_HQL = "org.hibernate.annotations.processing.CheckHQL";
public static final Map<String, String> COLLECTIONS = allCollectionTypes(); public static final Map<String, String> COLLECTIONS = allCollectionTypes();
private static Map<String, String> allCollectionTypes() { private static Map<String, String> allCollectionTypes() {