path expressions in finder method parameter names

this sounds a bit crazy but why not?
This commit is contained in:
Gavin King 2023-07-14 21:21:42 +02:00
parent c5c3bb8ac8
commit ec8d574e4a
11 changed files with 147 additions and 61 deletions

View File

@ -7,12 +7,12 @@
package org.hibernate.jpamodelgen.annotation;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants;
import java.util.List;
import java.util.Locale;
import static org.hibernate.internal.util.StringHelper.qualifier;
import static org.hibernate.jpamodelgen.util.StringUtil.getUpperUnderscoreCaseFromLowerCamelCase;
/**
@ -23,7 +23,7 @@ public abstract class AbstractFinderMethod extends AbstractQueryMethod {
final List<String> fetchProfiles;
public AbstractFinderMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName,
String entity,
boolean belongsToDao,
@ -104,13 +104,14 @@ public abstract class AbstractFinderMethod extends AbstractQueryMethod {
.append(", ");
}
}
final String path = param.replace('$', '.');
declaration
.append("{@link ")
.append(annotationMetaEntity.importType(entity))
.append('#')
.append(param)
.append(qualifier(path))
.append(' ')
.append(param)
.append(path)
.append("}");
}
declaration

View File

@ -19,7 +19,7 @@ import static org.hibernate.jpamodelgen.util.Constants.SESSION_TYPES;
* @author Gavin King
*/
public abstract class AbstractQueryMethod implements MetaAttribute {
final Metamodel annotationMetaEntity;
final AnnotationMetaEntity annotationMetaEntity;
final String methodName;
final List<String> paramNames;
final List<String> paramTypes;
@ -30,7 +30,7 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
final boolean addNonnullAnnotation;
public AbstractQueryMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName,
List<String> paramNames, List<String> paramTypes,
@Nullable String returnTypeName,

View File

@ -6,12 +6,12 @@
*/
package org.hibernate.jpamodelgen.annotation;
import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
@ -43,10 +43,12 @@ import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.tree.SqmStatement;
import org.hibernate.query.sqm.tree.expression.SqmParameter;
import static java.beans.Introspector.decapitalize;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static javax.lang.model.util.ElementFilter.fieldsIn;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.jpamodelgen.annotation.QueryMethod.isOrderParam;
import static org.hibernate.jpamodelgen.annotation.QueryMethod.isPageParam;
import static org.hibernate.jpamodelgen.util.Constants.SESSION_TYPES;
@ -109,6 +111,8 @@ public class AnnotationMetaEntity extends AnnotationMeta {
*/
private String sessionType = Constants.ENTITY_MANAGER;
private final Map<String,String> memberTypes = new HashMap<>();
public AnnotationMetaEntity(TypeElement element, Context context) {
this.element = element;
this.context = context;
@ -124,6 +128,10 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return annotationMetaEntity;
}
public @Nullable String getMemberType(String entityType, String memberName) {
return memberTypes.get( qualify(entityType, memberName) );
}
public AccessTypeInformation getEntityAccessTypeInfo() {
return entityAccessTypeInfo;
}
@ -739,19 +747,9 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
private @Nullable FieldType validateFinderParameter(TypeElement entity, VariableElement param) {
final String entityClassName = entity.getQualifiedName().toString();
determineAccessTypeForHierarchy( entity, context );
final AccessType accessType = castNonNull( context.getAccessTypeInfo( entityClassName ) ).getAccessType();
final AccessType accessType = getAccessType(entity);
for ( Element member : entity.getEnclosedElements() ) {
if ( fieldMatchesParameter( param, member, accessType ) ) {
final String memberType = memberType( member ).toString();
final String paramType = param.asType().toString();
if ( !memberType.equals(paramType) ) {
context.message( param,
"matching field has type '" + memberType
+ "' in entity class '" + entityClassName + "'",
Diagnostic.Kind.ERROR );
}
if ( fieldMatchesParameter( entity, param, member, accessType ) ) {
if ( containsAnnotation( member, Constants.ID, Constants.EMBEDDED_ID ) ) {
return FieldType.ID;
}
@ -773,15 +771,22 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
}
context.message( param,
"no matching field named '" + param.getSimpleName()
+ "' in entity class '" + entityClassName + "'",
"no matching field named '"
+ param.getSimpleName().toString().replace('$', '.')
+ "' in entity class '" + entity + "'",
Diagnostic.Kind.ERROR );
return null;
}
private AccessType getAccessType(TypeElement entity) {
final String entityClassName = entity.getQualifiedName().toString();
determineAccessTypeForHierarchy(entity, context );
return castNonNull( context.getAccessTypeInfo( entityClassName ) ).getAccessType();
}
private static TypeMirror memberType(Element member) {
if (member.getKind() == ElementKind.METHOD) {
ExecutableElement method = (ExecutableElement) member;
if ( member.getKind() == ElementKind.METHOD ) {
final ExecutableElement method = (ExecutableElement) member;
return method.getReturnType();
}
else {
@ -789,21 +794,84 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
}
private static boolean fieldMatchesParameter(VariableElement param, Element member, AccessType accessType) {
final Name name = member.getSimpleName();
final Name paramName = param.getSimpleName();
if ( accessType == AccessType.FIELD ) {
return name.contentEquals(paramName);
private boolean fieldMatchesParameter(TypeElement entityType, VariableElement param, Element member, AccessType accessType) {
final StringTokenizer tokens = new StringTokenizer( param.getSimpleName().toString(), "$" );
return fieldMatchesParameter( entityType, param, member, accessType, tokens, tokens.nextToken() );
}
private boolean fieldMatchesParameter(
TypeElement entityType,
VariableElement param,
Element member,
AccessType accessType,
StringTokenizer tokens,
String token) {
final Name memberName = member.getSimpleName();
final TypeMirror type;
if ( accessType == AccessType.FIELD && member.getKind() == ElementKind.FIELD ) {
if ( !fieldMatches(token, memberName) ) {
return false;
}
else {
type = member.asType();
}
}
else if ( accessType == AccessType.PROPERTY && member.getKind() == ElementKind.METHOD ) {
if ( !getterMatches(token, memberName) ) {
return false;
}
else {
final ExecutableElement method = (ExecutableElement) member;
type = method.getReturnType();
}
}
else {
if ( name.length() > 3 && name.subSequence(0,3).toString().equals("get")) {
final String propertyName = Introspector.decapitalize(name.subSequence(3, name.length()).toString());
return paramName.contentEquals(propertyName);
return false;
}
if ( tokens.hasMoreTokens() ) {
final String nextToken = tokens.nextToken();
if ( type.getKind() == TypeKind.DECLARED ) {
final DeclaredType declaredType = (DeclaredType) type;
final TypeElement memberType = (TypeElement) declaredType.asElement();
memberTypes.put( qualify(entityType.getQualifiedName().toString(), memberName.toString()),
memberType.getQualifiedName().toString() );
final AccessType memberAccessType = getAccessType(memberType);
for ( Element entityMember : memberType.getEnclosedElements() ) {
if ( fieldMatchesParameter(memberType, param, entityMember, memberAccessType, tokens, nextToken) ) {
return true;
}
}
}
if ( name.length() > 2 && name.subSequence(0,2).toString().equals("is")) {
final String propertyName = Introspector.decapitalize(name.subSequence(2, name.length()).toString());
return paramName.contentEquals(propertyName);
return false;
}
else {
final String memberType = memberType( member ).toString();
final String paramType = param.asType().toString();
if ( !isLegalAssignment( paramType, memberType ) ) {
context.message( param,
"matching field has type '" + memberType
+ "' in entity class '" + entityType + "'",
Diagnostic.Kind.ERROR );
}
return true;
}
}
private static boolean fieldMatches(String token, Name fieldName) {
return fieldName.contentEquals(token);
}
private static boolean getterMatches(String token, Name methodName) {
if ( methodName.length() > 3 && methodName.subSequence(0,3).toString().equals("get")) {
final String propertyName = decapitalize(methodName.subSequence(3, methodName.length()).toString());
return token.equals(propertyName);
}
else if ( methodName.length() > 2 && methodName.subSequence(0,2).toString().equals("is")) {
final String propertyName = decapitalize(methodName.subSequence(2, methodName.length()).toString());
return token.equals(propertyName);
}
else {
return false;
}
}

View File

@ -7,11 +7,11 @@
package org.hibernate.jpamodelgen.annotation;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
/**
* @author Gavin King
@ -22,7 +22,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
private final boolean isId;
public CriteriaFinderMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName, String entity,
@Nullable String containerType,
List<String> paramNames, List<String> paramTypes,
@ -94,20 +94,20 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
.append(paramName)
.append("==null")
.append("\n\t\t\t\t? ")
.append("entity.get(");
attributeRef( declaration, paramName );
.append("entity");
path( declaration, paramName );
declaration
.append(").isNull()")
.append(".isNull()")
.append("\n\t\t\t\t: ");
}
declaration
.append("builder.equal(entity.get(");
attributeRef( declaration, paramName );
.append("builder.equal(entity");
path( declaration, paramName );
declaration
.append("), ")
.append(", ")
//TODO: only safe if we are binding literals as parameters!!!
.append(paramName)
.append(")");
.append(')');
}
}
declaration
@ -136,7 +136,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
.append(".getSingleResult()");
}
else if ( containerType.equals(Constants.LIST) ) {
if ( unwrap || hasEnabledFetchProfiles) {
if ( unwrap || hasEnabledFetchProfiles ) {
declaration.append("\n\t\t\t");
}
declaration
@ -147,6 +147,21 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
return declaration.toString();
}
private void path(StringBuilder declaration, String paramName) {
final StringTokenizer tokens = new StringTokenizer(paramName, "$");
String typeName = entity;
while ( typeName!= null && tokens.hasMoreTokens() ) {
final String memberName = tokens.nextToken();
declaration
.append(".get(")
.append(annotationMetaEntity.importType(typeName + '_'))
.append('.')
.append(memberName)
.append(')');
typeName = annotationMetaEntity.getMemberType(typeName, memberName);
}
}
private static boolean isPrimitive(String paramType) {
return PRIMITIVE_TYPES.contains( paramType );
}
@ -154,13 +169,6 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
private static final Set<String> PRIMITIVE_TYPES =
Set.of("boolean", "char", "long", "int", "short", "byte", "double", "float");
private void attributeRef(StringBuilder declaration, String paramName) {
declaration
.append(annotationMetaEntity.importType(entity + '_'))
.append('.')
.append(paramName);
}
private StringBuilder returnType() {
StringBuilder type = new StringBuilder();
boolean returnsUni = isReactive()

View File

@ -6,9 +6,6 @@
*/
package org.hibernate.jpamodelgen.annotation;
import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants;
import java.util.List;
/**
@ -19,7 +16,7 @@ public class IdFinderMethod extends AbstractFinderMethod {
private final String paramName;
public IdFinderMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName, String entity,
List<String> paramNames, List<String> paramTypes,
boolean belongsToDao,

View File

@ -6,8 +6,6 @@
*/
package org.hibernate.jpamodelgen.annotation;
import org.hibernate.jpamodelgen.model.Metamodel;
import java.util.List;
/**
@ -16,7 +14,7 @@ import java.util.List;
public class NaturalIdFinderMethod extends AbstractFinderMethod {
public NaturalIdFinderMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName, String entity,
List<String> paramNames, List<String> paramTypes,
boolean belongsToDao,

View File

@ -8,7 +8,6 @@ package org.hibernate.jpamodelgen.annotation;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.jpamodelgen.model.Metamodel;
import org.hibernate.jpamodelgen.util.Constants;
import org.hibernate.query.Order;
import org.hibernate.query.Page;
@ -28,7 +27,7 @@ public class QueryMethod extends AbstractQueryMethod {
private final boolean isNative;
public QueryMethod(
Metamodel annotationMetaEntity,
AnnotationMetaEntity annotationMetaEntity,
String methodName,
String queryString,
@Nullable

View File

@ -2,6 +2,7 @@ package org.hibernate.jpamodelgen.test.hqlsql;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
@Entity
@ -10,4 +11,5 @@ public class Book {
@NaturalId String title;
String text;
@NaturalId String authorName;
@ManyToOne Publisher publisher;
}

View File

@ -41,4 +41,7 @@ public interface Dao {
@SQL("select * from Book where isbn = :isbn")
Book findByIsbnNative(String isbn);
@Find
List<Book> publishedBooks(String publisher$name);
}

View File

@ -0,0 +1,10 @@
package org.hibernate.jpamodelgen.test.hqlsql;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Publisher {
@Id Long id;
String name;
}

View File

@ -18,7 +18,7 @@ import static org.hibernate.jpamodelgen.test.util.TestUtil.assertMetamodelClassG
*/
public class QueryMethodTest extends CompilationTest {
@Test
@WithClasses({ Book.class, Dao.class, Books.class })
@WithClasses({ Book.class, Publisher.class, Dao.class, Books.class })
public void testQueryMethod() {
System.out.println( TestUtil.getMetaModelSourceAsString( Dao.class ) );
System.out.println( TestUtil.getMetaModelSourceAsString( Books.class ) );