diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Author.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Author.java new file mode 100644 index 0000000000..f68d6fba67 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Author.java @@ -0,0 +1,21 @@ +package org.hibernate.processor.test.data.namedquery; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; + +import java.util.Set; + +@Entity +public class Author { + @Id + String ssn; + String name; + +// @Embedded +// Address address; + + @ManyToMany + Set books; +} + diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Book.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Book.java new file mode 100644 index 0000000000..cc151f1e8c --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/Book.java @@ -0,0 +1,43 @@ +package org.hibernate.processor.test.data.namedquery; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import org.hibernate.annotations.NaturalId; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Set; + +@Entity +@Table(name = "books") +public class Book { + @Id + String isbn; + + @NaturalId + @Basic(optional = false) + String title; + + @Basic(optional = false) + String text; + + @NaturalId + LocalDate publicationDate; + + @ManyToMany(mappedBy = "books") + Set authors; + + BigDecimal price; + + int pages; + + public Book(String isbn, String title, String text) { + this.isbn = isbn; + this.title = title; + this.text = text; + } + Book() {} +} \ No newline at end of file diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository$.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository$.java new file mode 100644 index 0000000000..885d1c3776 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository$.java @@ -0,0 +1,13 @@ +package org.hibernate.processor.test.data.namedquery; + +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; + +@Repository(dataStore = "myds") +public interface BookAuthorRepository$ extends BookAuthorRepository { + @Override + @Query("from Book where title like :title") + List findByTitleLike(String title); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository.java new file mode 100644 index 0000000000..5bdc455703 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/BookAuthorRepository.java @@ -0,0 +1,30 @@ +package org.hibernate.processor.test.data.namedquery; + +import jakarta.data.Limit; +import jakarta.data.Order; +import jakarta.data.Sort; +import jakarta.data.page.CursoredPage; +import jakarta.data.page.Page; +import jakarta.data.page.PageRequest; +import jakarta.data.repository.By; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.Insert; +import jakarta.data.repository.OrderBy; +import jakarta.data.repository.Param; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; +import jakarta.data.repository.Update; +import org.hibernate.StatelessSession; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +@Repository(dataStore = "myds") +public interface BookAuthorRepository { + List findByTitleLike(String title); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/NamedQueryTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/NamedQueryTest.java new file mode 100644 index 0000000000..4b3469917f --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/namedquery/NamedQueryTest.java @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.hibernate.processor.test.data.namedquery; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.assertNoMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +/** + * @author Gavin King + */ +public class NamedQueryTest extends CompilationTest { + @Test + @WithClasses({ Author.class, Book.class, BookAuthorRepository.class, BookAuthorRepository$.class }) + public void test() { + System.out.println( getMetaModelSourceAsString( Author.class ) ); + System.out.println( getMetaModelSourceAsString( Book.class ) ); + System.out.println( getMetaModelSourceAsString( Author.class, true ) ); + System.out.println( getMetaModelSourceAsString( Book.class, true ) ); + System.out.println( getMetaModelSourceAsString( BookAuthorRepository.class ) ); + assertMetamodelClassGeneratedFor( Author.class, true ); + assertMetamodelClassGeneratedFor( Book.class, true ); + assertMetamodelClassGeneratedFor( Author.class ); + assertMetamodelClassGeneratedFor( Book.class ); + assertMetamodelClassGeneratedFor( BookAuthorRepository.class ); + assertNoMetamodelClassGeneratedFor( BookAuthorRepository$.class ); + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java index 5126043b98..c13ebba806 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java @@ -314,7 +314,10 @@ else if ( element instanceof TypeElement ) { context.logMessage( Diagnostic.Kind.OTHER, "Processing repository class '" + element + "'" ); final AnnotationMetaEntity metaEntity = AnnotationMetaEntity.create( typeElement, context ); - context.addMetaAuxiliary( metaEntity.getQualifiedName(), metaEntity ); + if ( metaEntity.isInitialized() ) { + context.addMetaAuxiliary( metaEntity.getQualifiedName(), metaEntity ); + } + // otherwise discard it (assume it has query by magical method name stuff) } } else { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index 6b877d1e8c..d18045c55e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.AssertionFailure; import org.hibernate.grammars.hql.HqlLexer; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.processor.Context; import org.hibernate.processor.ImportContextImpl; import org.hibernate.processor.ProcessLaterException; @@ -21,7 +22,6 @@ import org.hibernate.processor.util.Constants; import org.hibernate.processor.validation.ProcessorSessionFactory; import org.hibernate.processor.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; @@ -212,9 +212,29 @@ public boolean isJakartaDataStyle() { return jakartaDataStaticModel; } + public boolean isInitialized() { + return initialized; + } + @Override public final String getSimpleName() { - return element.getSimpleName().toString(); + return removeDollar( element.getSimpleName().toString() ); + } + + private String getConstructorName() { + return getSimpleName() + '_'; + } + + /** + * If this is an "intermediate" class providing {@code @Query} + * annotations for the query by magical method name crap, then + * by convention it will be named with a trailing $ sign. Strip + * that off, so we get the standard constructor. + */ + private static String removeDollar(String simpleName) { + return simpleName.endsWith("$") + ? simpleName.substring(0, simpleName.length()-1) + : simpleName; } @Override @@ -368,7 +388,7 @@ protected final void init() { else if ( containsAnnotation( method, JD_INSERT, JD_UPDATE, JD_SAVE ) ) { lifecycleMethods.add( method ); } - else if ( hasAnnotation( method, JD_DELETE) ) { + else if ( hasAnnotation( method, JD_DELETE ) ) { if ( isDeleteLifecycle(method) ) { lifecycleMethods.add( method ); } @@ -376,6 +396,16 @@ else if ( hasAnnotation( method, JD_DELETE) ) { queryMethods.add( method ); } } + else if ( !isSessionGetter(method) + && !method.getModifiers().contains(Modifier.DEFAULT) ) { + final String companionClassName = element.getQualifiedName().toString() + '$'; + if ( context.getElementUtils().getTypeElement(companionClassName) == null ) { + message( method, "repository method cannot be implemented", + Diagnostic.Kind.ERROR ); + } + // NOTE EARLY EXIT with initialized = false + return; + } } primaryEntity = primaryEntity( lifecycleMethods ); @@ -483,10 +513,9 @@ private void addMethods(TypeElement element, List methodsOfCl private void addDefaultConstructor() { final String sessionVariableName = getSessionVariableName(sessionType); - final String typeName = element.getSimpleName().toString() + '_'; putMember("_", new DefaultConstructor( this, - typeName, + getConstructorName(), sessionVariableName, sessionType, sessionVariableName, @@ -633,13 +662,12 @@ private String addDaoConstructor(@Nullable ExecutableElement method) { final String sessionType = method == null ? this.sessionType : method.getReturnType().toString(); final String sessionVariableName = getSessionVariableName( sessionType ); final String name = method == null ? sessionVariableName : method.getSimpleName().toString(); - final String typeName = element.getSimpleName().toString() + '_'; if ( method == null || !method.isDefault() ) { putMember( name, new RepositoryConstructor( this, - typeName, + getConstructorName(), name, sessionType, sessionVariableName, @@ -664,18 +692,15 @@ private String addDaoConstructor(@Nullable ExecutableElement method) { * and in HR, we define the static session getter. */ private String setupQuarkusDaoConstructor() { - final String typeName = element.getSimpleName().toString() + '_'; - final String sessionVariableName = getSessionVariableName( sessionType ); - if ( context.usesQuarkusOrm() ) { String name = "getEntityManager"; putMember( name, new RepositoryConstructor( this, - typeName, + getConstructorName(), name, sessionType, - sessionVariableName, + getSessionVariableName( sessionType ), dataStore(), context.addInjectAnnotation(), context.addNonnullAnnotation(), @@ -1047,7 +1072,7 @@ private void addQueryMethod(ExecutableElement method) { .asMemberOf((DeclaredType) element.asType(), method); final TypeMirror returnType = methodType.getReturnType(); final TypeKind kind = returnType.getKind(); - if ( kind == TypeKind.VOID || kind == TypeKind.ARRAY || kind.isPrimitive() ) { + if ( kind == TypeKind.VOID || kind == TypeKind.ARRAY || kind.isPrimitive() ) { addQueryMethod( method, returnType, null ); } else if ( kind == TypeKind.DECLARED ) { @@ -2093,81 +2118,91 @@ private void addQueryMethod( @Nullable TypeElement containerType, AnnotationMirror mirror, boolean isNative) { + // The following is quite fragile! + final String containerTypeName; + if ( containerType == null ) { + if ( returnType != null && returnType.getKind() == TypeKind.ARRAY ) { + final ArrayType arrayType = (ArrayType) returnType; + final TypeMirror componentType = arrayType.getComponentType(); + final TypeElement object = context.getElementUtils().getTypeElement(JAVA_OBJECT); + if ( !context.getTypeUtils().isSameType( object.asType(), componentType ) ) { + returnType = componentType; + containerTypeName = "[]"; + } + else { + // assume it's returning a single tuple as Object[] + containerTypeName = null; + } + } + else { + containerTypeName = null; + } + } + else { + containerTypeName = containerType.getQualifiedName().toString(); + } final AnnotationValue value = getAnnotationValue( mirror, "value" ); if ( value != null ) { - final Object query = value.getValue(); - if ( query instanceof String ) { - final String queryString = (String) query; + final Object queryString = value.getValue(); + if ( queryString instanceof String ) { + addQueryMethod(method, returnType, containerTypeName, mirror, isNative, value, (String) queryString); + } + } + } - // The following is quite fragile! - final String containerTypeName; - if ( containerType == null ) { - if ( returnType != null && returnType.getKind() == TypeKind.ARRAY ) { - final ArrayType arrayType = (ArrayType) returnType; - final TypeMirror componentType = arrayType.getComponentType(); - final TypeElement object = context.getElementUtils().getTypeElement(JAVA_OBJECT); - if ( !context.getTypeUtils().isSameType( object.asType(), componentType ) ) { - returnType = componentType; - containerTypeName = "[]"; - } - else { - // assume it's returning a single tuple as Object[] - containerTypeName = null; - } - } - else { - containerTypeName = null; - } - } - else { - containerTypeName = containerType.getQualifiedName().toString(); - } + private void addQueryMethod( + ExecutableElement method, + @Nullable TypeMirror returnType, + @Nullable String containerTypeName, + AnnotationMirror mirror, + boolean isNative, + AnnotationValue value, + String queryString) { - final List paramNames = parameterNames( method ); - final List paramTypes = parameterTypes( method ); + final List paramNames = parameterNames(method); + final List paramTypes = parameterTypes(method); - // now check that the query has a parameter for every method parameter - checkParameters( method, returnType, paramNames, paramTypes, mirror, value, queryString ); + // now check that the query has a parameter for every method parameter + checkParameters(method, returnType, paramNames, paramTypes, mirror, value, queryString); - final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes ); - final DeclaredType resultType = resultType( method, returnType, mirror, value ); - final List orderBys = resultType == null + final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes ); + final DeclaredType resultType = resultType(method, returnType, mirror, value); + final List orderBys = + resultType == null ? emptyList() : orderByList( method, (TypeElement) resultType.asElement() ); - final String processedQuery; - if ( isNative ) { - processedQuery = queryString; - validateSql( method, mirror, processedQuery, paramNames, value ); - } - else { - processedQuery = addFromClauseIfNecessary( queryString, implicitEntityName(resultType) ); - validateHql( method, returnType, mirror, value, processedQuery, paramNames, paramTypes ); - } - - final QueryMethod attribute = - new QueryMethod( - this, method, - method.getSimpleName().toString(), - processedQuery, - returnType == null ? null : returnType.toString(), - returnType == null ? null : returnTypeClass( returnType ), - containerTypeName, - paramNames, - paramTypes, - isInsertUpdateDelete( queryString ), - isNative, - repository, - sessionType[0], - sessionType[1], - orderBys, - context.addNonnullAnnotation(), - jakartaDataRepository - ); - putMember( attribute.getPropertyName() + paramTypes, attribute ); - } + final String processedQuery; + if (isNative) { + processedQuery = queryString; + validateSql(method, mirror, processedQuery, paramNames, value); } + else { + processedQuery = addFromClauseIfNecessary( queryString, implicitEntityName(resultType) ); + validateHql(method, returnType, mirror, value, processedQuery, paramNames, paramTypes); + } + + final QueryMethod attribute = + new QueryMethod( + this, method, + method.getSimpleName().toString(), + processedQuery, + returnType == null ? null : returnType.toString(), + returnType == null ? null : returnTypeClass(returnType), + containerTypeName, + paramNames, + paramTypes, + isInsertUpdateDelete(queryString), + isNative, + repository, + sessionType[0], + sessionType[1], + orderBys, + context.addNonnullAnnotation(), + jakartaDataRepository + ); + putMember( attribute.getPropertyName() + paramTypes, attribute ); } private static String returnTypeClass(TypeMirror returnType) {