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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,4 +41,7 @@ public interface Dao {
@SQL("select * from Book where isbn = :isbn") @SQL("select * from Book where isbn = :isbn")
Book findByIsbnNative(String 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 { public class QueryMethodTest extends CompilationTest {
@Test @Test
@WithClasses({ Book.class, Dao.class, Books.class }) @WithClasses({ Book.class, Publisher.class, Dao.class, Books.class })
public void testQueryMethod() { public void testQueryMethod() {
System.out.println( TestUtil.getMetaModelSourceAsString( Dao.class ) ); System.out.println( TestUtil.getMetaModelSourceAsString( Dao.class ) );
System.out.println( TestUtil.getMetaModelSourceAsString( Books.class ) ); System.out.println( TestUtil.getMetaModelSourceAsString( Books.class ) );