diff --git a/tooling/metamodel-generator/hibernate-jpamodelgen.gradle b/tooling/metamodel-generator/hibernate-jpamodelgen.gradle index 9e58a18ee8..2e0d4585f1 100644 --- a/tooling/metamodel-generator/hibernate-jpamodelgen.gradle +++ b/tooling/metamodel-generator/hibernate-jpamodelgen.gradle @@ -23,6 +23,7 @@ dependencies { implementation jakartaLibs.jaxb implementation libs.antlrRuntime implementation project( ':hibernate-core' ) + implementation libs.byteBuddy xjc jakartaLibs.xjc xjc jakartaLibs.jaxb diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java index 6e2defbfd0..9fd456dcaa 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/JPAMetaModelEntityProcessor.java @@ -51,7 +51,19 @@ * @author Emmanuel Bernard */ @SupportedAnnotationTypes({ - "jakarta.persistence.Entity", "jakarta.persistence.MappedSuperclass", "jakarta.persistence.Embeddable" + Constants.ENTITY, + Constants.MAPPED_SUPERCLASS, + Constants.EMBEDDABLE, + Constants.HQL, + Constants.SQL, + Constants.NAMED_QUERY, + Constants.NAMED_NATIVE_QUERY, + Constants.NAMED_ENTITY_GRAPH, + Constants.SQL_RESULT_SET_MAPPING, + Constants.HIB_FETCH_PROFILE, + Constants.HIB_FILTER_DEF, + Constants.HIB_NAMED_QUERY, + Constants.HIB_NAMED_NATIVE_QUERY }) @SupportedOptions({ JPAMetaModelEntityProcessor.DEBUG_OPTION, diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java index ff1df904a8..592fe1dfb8 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/annotation/AnnotationMetaEntity.java @@ -24,19 +24,12 @@ import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic; -import org.antlr.v4.runtime.ANTLRErrorListener; -import org.antlr.v4.runtime.BailErrorStrategy; -import org.antlr.v4.runtime.DefaultErrorStrategy; 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.atn.PredictionMode; import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.misc.ParseCancellationException; import org.checkerframework.checker.nullness.qual.Nullable; -import org.hibernate.grammars.hql.HqlLexer; -import org.hibernate.grammars.hql.HqlParser; import org.hibernate.jpamodelgen.Context; import org.hibernate.jpamodelgen.ImportContextImpl; import org.hibernate.jpamodelgen.model.ImportContext; @@ -47,8 +40,10 @@ import org.hibernate.jpamodelgen.util.Constants; import org.hibernate.jpamodelgen.util.NullnessUtil; import org.hibernate.jpamodelgen.util.TypeUtils; -import org.hibernate.query.hql.internal.HqlParseTreeBuilder; +import org.hibernate.jpamodelgen.validation.ProcessorSessionFactory; +import org.hibernate.jpamodelgen.validation.Validation; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; import static org.hibernate.jpamodelgen.util.TypeUtils.containsAnnotation; import static org.hibernate.jpamodelgen.util.TypeUtils.determineAnnotationSpecifiedAccessType; @@ -397,7 +392,14 @@ private void addQueryMethod( checkParameters( method, paramNames, mirror, hql ); if ( !isNative ) { - checkHqlSyntax( method, mirror, hql ); +// checkHqlSyntax( method, mirror, hql ); + Validation.validate( + hql, + false, + emptySet(), emptySet(), + new ErrorHandler( method, mirror, hql ), + ProcessorSessionFactory.create( context.getProcessingEnvironment() ) + ); } } } @@ -412,60 +414,88 @@ private void checkParameters(ExecutableElement method, List paramNames, } } - private void checkHqlSyntax(ExecutableElement method, AnnotationMirror mirror, String queryString) { - ANTLRErrorListener errorListener = new ANTLRErrorListener() { - @Override - public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException e) { - displayError( method, mirror, "illegal HQL syntax - " - + prettifyAntlrError( offendingSymbol, line, charPositionInLine, message, e, queryString, false ) ); - } +// private void checkHqlSyntax(ExecutableElement method, AnnotationMirror mirror, String queryString) { +// final ANTLRErrorListener errorListener = new ErrorHandler( method, mirror, queryString ); +// final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( queryString ); +// final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( queryString, hqlLexer ); +// hqlLexer.addErrorListener( errorListener ); +// hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL ); +// hqlParser.removeErrorListeners(); +// hqlParser.addErrorListener( errorListener ); +// hqlParser.setErrorHandler( new BailErrorStrategy() ); +// +// try { +// hqlParser.statement(); +// } +// catch ( ParseCancellationException e) { +// // reset the input token stream and parser state +// hqlLexer.reset(); +// hqlParser.reset(); +// +// // fall back to LL(k)-based parsing +// hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL ); +// hqlParser.setErrorHandler( new DefaultErrorStrategy() ); +// +// hqlParser.statement(); +// } +// } - @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) { - } - }; - - final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( queryString ); - final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( queryString, hqlLexer ); - hqlLexer.addErrorListener( errorListener ); - hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL ); - hqlParser.removeErrorListeners(); - hqlParser.addErrorListener( errorListener ); - hqlParser.setErrorHandler( new BailErrorStrategy() ); - - try { - hqlParser.statement(); - } - catch ( ParseCancellationException e) { - // reset the input token stream and parser state - hqlLexer.reset(); - hqlParser.reset(); - - // fall back to LL(k)-based parsing - hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL ); - hqlParser.setErrorHandler( new DefaultErrorStrategy() ); - - hqlParser.statement(); - } - } - - private void displayError(ExecutableElement method, String message) { + private void displayError(Element method, String message) { context.getProcessingEnvironment().getMessager() .printMessage( Diagnostic.Kind.ERROR, message, method ); } - private void displayError(ExecutableElement method, AnnotationMirror mirror, String message) { + 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) { + } + } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockCollectionPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockCollectionPersister.java new file mode 100644 index 0000000000..1cac8e7779 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockCollectionPersister.java @@ -0,0 +1,213 @@ +/* + * 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.jpamodelgen.validation; + +import org.hibernate.FetchMode; +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ListType; +import org.hibernate.type.MapType; +import org.hibernate.type.Type; + +import java.io.Serializable; + +import static org.hibernate.internal.util.StringHelper.root; +import static org.hibernate.jpamodelgen.validation.MockSessionFactory.typeConfiguration; + +/** + * @author Gavin King + */ +@SuppressWarnings("nullness") +public abstract class MockCollectionPersister implements QueryableCollection { + + private static final String[] ID_COLUMN = {"id"}; + private static final String[] INDEX_COLUMN = {"pos"}; + + private final String role; + private final MockSessionFactory factory; + private final CollectionType collectionType; + private final String ownerEntityName; + private final Type elementType; + + public MockCollectionPersister(String role, CollectionType collectionType, Type elementType, MockSessionFactory factory) { + this.role = role; + this.collectionType = collectionType; + this.elementType = elementType; + this.factory = factory; + this.ownerEntityName = root(role); + } + + String getOwnerEntityName() { + return ownerEntityName; + } + + @Override + public String getRole() { + return role; + } + + @Override + public String getName() { + return role; + } + + @Override + public CollectionType getCollectionType() { + return collectionType; + } + + @Override + public EntityPersister getOwnerEntityPersister() { + return factory.getMetamodel().entityPersister(ownerEntityName); + } + + abstract Type getElementPropertyType(String propertyPath); + + @Override + public Type toType(String propertyName) throws QueryException { + if ("index".equals(propertyName)) { + //this is what AbstractCollectionPersister does! + //TODO: move it to FromElementType:626 or all + // the way to CollectionPropertyMapping + return getIndexType(); + } + Type type = getElementPropertyType(propertyName); + if (type==null) { + throw new QueryException(elementType.getName() + + " has no mapped " + + propertyName); + } + else { + return type; + } + } + + @Override + public Type getKeyType() { + return getOwnerEntityPersister().getIdentifierType(); + } + + @Override + public Type getIndexType() { + if (collectionType instanceof ListType) { + return typeConfiguration.getBasicTypeForJavaType(Integer.class); + } + else if (collectionType instanceof MapType) { + //TODO!!! this is incorrect, return the correct key type + return typeConfiguration.getBasicTypeForJavaType(String.class); + } + else { + return null; + } + } + + @Override + public Type getElementType() { + return elementType; + } + + @Override + public Type getIdentifierType() { + return typeConfiguration.getBasicTypeForJavaType(Long.class); + } + + @Override + public boolean hasIndex() { + return getCollectionType() instanceof ListType + || getCollectionType() instanceof MapType; + } + + @Override + public EntityPersister getElementPersister() { + if (elementType.isEntityType()) { + return factory.getMetamodel() + .entityPersister(elementType.getName()); + } + else { + return null; + } + } + + @Override + public SessionFactoryImplementor getFactory() { + return factory; + } + + @Override + public boolean isOneToMany() { + return elementType.isEntityType(); + } + + @Override + public Serializable[] getCollectionSpaces() { + return new Serializable[] {role}; + } + + @Override + public String getMappedByProperty() { + return null; + } + + @Override + public String[] getIndexColumnNames() { + return INDEX_COLUMN; + } + + @Override + public String[] getIndexColumnNames(String alias) { + return INDEX_COLUMN; + } + + @Override + public String[] getIndexFormulas() { + return null; + } + + @Override + public String[] getElementColumnNames(String alias) { + return new String[] {""}; + } + + @Override + public String[] getElementColumnNames() { + return new String[] {""}; + } + + @Override + public FetchMode getFetchMode() { + return FetchMode.DEFAULT; + } + + @Override + public String getTableName() { + return role; + } + + @Override + public String[] getKeyColumnNames() { + return ID_COLUMN; + } + + @Override + public boolean isCollection() { + return true; + } + + @Override + public boolean consumesCollectionAlias() { + return true; + } + + @Override + public String[] toColumns(String propertyName) { + return new String[] {""}; + } + +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockEntityPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockEntityPersister.java new file mode 100644 index 0000000000..cc74f73333 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockEntityPersister.java @@ -0,0 +1,239 @@ +/* + * 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.jpamodelgen.validation; + +import jakarta.persistence.AccessType; +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.DiscriminatorMetadata; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.Queryable; +import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.Type; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static org.hibernate.jpamodelgen.validation.MockSessionFactory.typeConfiguration; + +/** + * @author Gavin King + */ +@SuppressWarnings("nullness") +public abstract class MockEntityPersister implements EntityPersister, Queryable, DiscriminatorMetadata { + + private static final String[] ID_COLUMN = {"id"}; + + private final String entityName; + private final MockSessionFactory factory; + private final List subclassPersisters = new ArrayList<>(); + final AccessType defaultAccessType; + private final Map propertyTypesByName = new HashMap<>(); + + public MockEntityPersister(String entityName, AccessType defaultAccessType, MockSessionFactory factory) { + this.entityName = entityName; + this.factory = factory; + this.defaultAccessType = defaultAccessType; + } + + void initSubclassPersisters() { + for (MockEntityPersister other: factory.getMockEntityPersisters()) { + other.addPersister(this); + this.addPersister(other); + } + } + + private void addPersister(MockEntityPersister entityPersister) { + if (isSubclassPersister(entityPersister)) { + subclassPersisters.add(entityPersister); + } + } + + private Type getSubclassPropertyType(String propertyPath) { + return subclassPersisters.stream() + .map(sp -> sp.getPropertyType(propertyPath)) + .filter(Objects::nonNull) + .findAny() + .orElse(null); + } + + abstract boolean isSubclassPersister(MockEntityPersister entityPersister); + + @Override + public boolean isSubclassEntityName(String name) { + return isSubclassPersister(subclassPersisters.stream() + .filter(persister -> persister.entityName.equals(name)) + .findFirst().orElseThrow()); + } + + @Override + public SessionFactoryImplementor getFactory() { + return factory; + } + + @Override + public EntityMetamodel getEntityMetamodel() { + throw new UnsupportedOperationException(); + } + + @Override + public String getEntityName() { + return entityName; + } + + @Override + public String getName() { + return entityName; + } + + @Override + public final Type getPropertyType(String propertyPath) { + Type result = propertyTypesByName.get(propertyPath); + if (result!=null) { + return result; + } + + result = createPropertyType(propertyPath); + if (result == null) { + //check subclasses, needed for treat() + result = getSubclassPropertyType(propertyPath); + } + + if (result!=null) { + propertyTypesByName.put(propertyPath, result); + } + return result; + } + + abstract Type createPropertyType(String propertyPath); + + @Override + public Type getIdentifierType() { + //TODO: propertyType(getIdentifierPropertyName()) + return typeConfiguration.getBasicTypeForJavaType(Long.class); + } + + @Override + public String getIdentifierPropertyName() { + //TODO: return the correct @Id property name + return "id"; + } + + @Override + public Type toType(String propertyName) throws QueryException { + Type type = getPropertyType(propertyName); + if (type == null) { + throw new QueryException(getEntityName() + + " has no mapped " + + propertyName); + } + return type; + } + + @Override + public String getRootEntityName() { + for (MockEntityPersister persister : factory.getMockEntityPersisters()) { + if (this != persister && persister.isSubclassPersister(this)) { + return persister.getRootEntityName(); + } + } + return entityName; + } + + @Override + public Set getSubclassEntityNames() { + Set names = new HashSet<>(); + names.add( entityName ); + for (MockEntityPersister persister : factory.getMockEntityPersisters()) { + if (persister.isSubclassPersister(this)) { + names.add(persister.entityName); + } + } + return names; + } + + @Override + public Declarer getSubclassPropertyDeclarer(String s) { + return Declarer.CLASS; + } + + @Override + public String[] toColumns(String propertyName) { + return new String[] { "" }; + } + + @Override + public Serializable[] getPropertySpaces() { + return new Serializable[] {entityName}; + } + + @Override + public Serializable[] getQuerySpaces() { + return new Serializable[] {entityName}; + } + + @Override + public EntityPersister getEntityPersister() { + return this; + } + + @Override + public String[] getKeyColumnNames() { + return getIdentifierColumnNames(); + } + + @Override + public String[] getIdentifierColumnNames() { + return ID_COLUMN; + } + + @Override + public DiscriminatorMetadata getTypeDiscriminatorMetadata() { + return this; + } + + @Override + public Type getResolutionType() { + return typeConfiguration.getBasicTypeForJavaType(Class.class); + } + + @Override + public String getTableName() { + return entityName; + } + + @Override + public String toString() { + return "MockEntityPersister[" + entityName + "]"; + } + + @Override + public int getVersionProperty() { + return -66; + } + + @Override + public String getMappedSuperclass() { + return null; + } + + @Override + public boolean consumesEntityAlias() { + return true; + } + + @Override + public Type getDiscriminatorType() { + return typeConfiguration.getBasicTypeForJavaType(String.class); + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockJdbcServicesInitiator.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockJdbcServicesInitiator.java new file mode 100644 index 0000000000..2c7cf795ac --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockJdbcServicesInitiator.java @@ -0,0 +1,76 @@ +/* + * 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.jpamodelgen.validation; + +import org.hibernate.annotations.processing.GenericDialect; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.internal.QualifiedObjectNameFormatterStandardImpl; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; +import org.hibernate.engine.jdbc.env.spi.QualifiedObjectNameFormatter; +import org.hibernate.engine.jdbc.internal.JdbcServicesInitiator; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; + +import java.util.Map; + +/** + * @author Gavin King + */ +@SuppressWarnings("nullness") +class MockJdbcServicesInitiator extends JdbcServicesInitiator { + + static final JdbcServicesInitiator INSTANCE = new MockJdbcServicesInitiator(); + + static final JdbcServices jdbcServices = Mocker.nullary(MockJdbcServices.class).get(); + static final GenericDialect genericDialect = new GenericDialect(); + + public abstract static class MockJdbcServices implements JdbcServices, JdbcEnvironment { + @Override + public Dialect getDialect() { + return genericDialect; + } + + @Override + public JdbcEnvironment getJdbcEnvironment() { + return this; + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory(); + } + + @Override + public Identifier getCurrentCatalog() { + return null; + } + + @Override + public Identifier getCurrentSchema() { + return null; + } + + @Override + public QualifiedObjectNameFormatter getQualifiedObjectNameFormatter() { + return new QualifiedObjectNameFormatterStandardImpl(getNameQualifierSupport()); + } + + @Override + public NameQualifierSupport getNameQualifierSupport() { + return genericDialect.getNameQualifierSupport(); + } + } + + @Override + public JdbcServices initiateService(Map configurationValues, ServiceRegistryImplementor registry) { + return jdbcServices; + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockSessionFactory.java new file mode 100644 index 0000000000..206af4f01f --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/MockSessionFactory.java @@ -0,0 +1,1068 @@ +/* + * 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.jpamodelgen.validation; + +import org.hibernate.CustomEntityDirtinessStrategy; +import org.hibernate.EntityNameResolver; +import org.hibernate.MappingException; +import org.hibernate.SessionFactoryObserver; +import org.hibernate.TimeZoneStorageStrategy; +import org.hibernate.boot.internal.DefaultCustomEntityDirtinessStrategy; +import org.hibernate.boot.internal.MetadataImpl; +import org.hibernate.boot.internal.StandardEntityNotFoundDelegate; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.naming.ImplicitNamingStrategy; +import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.boot.registry.internal.StandardServiceRegistryImpl; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.boot.spi.MappingDefaults; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.cache.internal.DisabledCaching; +import org.hibernate.cache.spi.CacheImplementor; +import org.hibernate.cache.spi.access.AccessType; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.query.internal.NativeQueryInterpreterStandardImpl; +import org.hibernate.engine.query.spi.NativeQueryInterpreter; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.id.factory.IdentifierGeneratorFactory; +import org.hibernate.id.factory.internal.StandardIdentifierGeneratorFactory; +import org.hibernate.internal.FastSessionServices; +import org.hibernate.jpa.internal.MutableJpaComplianceImpl; +import org.hibernate.jpa.spi.JpaCompliance; +import org.hibernate.jpa.spi.MutableJpaCompliance; +import org.hibernate.loader.BatchFetchStyle; +import org.hibernate.mapping.Property; +import org.hibernate.metamodel.AttributeClassification; +import org.hibernate.metamodel.CollectionClassification; +import org.hibernate.metamodel.internal.JpaMetaModelPopulationSetting; +import org.hibernate.metamodel.internal.JpaStaticMetaModelPopulationSetting; +import org.hibernate.metamodel.internal.MetadataContext; +import org.hibernate.metamodel.internal.RuntimeMetamodelsImpl; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.metamodel.model.domain.internal.AbstractAttribute; +import org.hibernate.metamodel.model.domain.internal.AbstractPluralAttribute; +import org.hibernate.metamodel.model.domain.internal.BagAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; +import org.hibernate.metamodel.model.domain.internal.EmbeddableTypeImpl; +import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl; +import org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl; +import org.hibernate.metamodel.model.domain.internal.ListAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.MapAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.MappedSuperclassTypeImpl; +import org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl; +import org.hibernate.metamodel.model.domain.internal.PluralAttributeBuilder; +import org.hibernate.metamodel.model.domain.internal.SetAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.SingularAttributeImpl; +import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.metamodel.spi.MetamodelImplementor; +import org.hibernate.metamodel.spi.RuntimeMetamodelsImplementor; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.EntityNotFoundDelegate; +import org.hibernate.query.criteria.ValueHandlingMode; +import org.hibernate.query.hql.HqlTranslator; +import org.hibernate.query.hql.internal.StandardHqlTranslator; +import org.hibernate.query.internal.NamedObjectRepositoryImpl; +import org.hibernate.query.internal.QueryInterpretationCacheDisabledImpl; +import org.hibernate.query.named.NamedObjectRepository; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.spi.QueryInterpretationCache; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder; +import org.hibernate.query.sqm.sql.SqmTranslatorFactory; +import org.hibernate.query.sqm.sql.StandardSqmTranslatorFactory; +import org.hibernate.stat.internal.StatisticsImpl; +import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.BagType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; +import org.hibernate.type.ListType; +import org.hibernate.type.MapType; +import org.hibernate.type.SetType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.hibernate.type.descriptor.jdbc.ObjectJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; + +/** + * @author Gavin King + */ +@SuppressWarnings({"nullness", "initialization"}) +public abstract class MockSessionFactory + implements SessionFactoryImplementor, QueryEngine, RuntimeModelCreationContext, MetadataBuildingOptions, + BootstrapContext, MetadataBuildingContext, FunctionContributions, SessionFactoryOptions, JdbcTypeIndicators { + + // static so other things can get at it + // TODO: make a static instance of this whole object instead! + static TypeConfiguration typeConfiguration; + + private final Map entityPersistersByName = new HashMap<>(); + private final Map collectionPersistersByName = new HashMap<>(); + + private final StandardServiceRegistryImpl serviceRegistry; + private final SqmFunctionRegistry functionRegistry; + private final MappingMetamodelImpl metamodel; + + private final MetadataImplementor bootModel; + private final MetadataContext metadataContext; + + public MockSessionFactory() { + + serviceRegistry = new StandardServiceRegistryImpl( + new BootstrapServiceRegistryBuilder().applyClassLoaderService(new ClassLoaderServiceImpl() { + @Override + @SuppressWarnings("unchecked") + public Class classForName(String className) { + try { + return super.classForName(className); + } + catch (ClassLoadingException e) { + if (isClassDefined(className)) { + return Object[].class; + } + else { + throw e; + } + } + } + }).build(), + singletonList(MockJdbcServicesInitiator.INSTANCE), + emptyList(), + emptyMap() + ); + + functionRegistry = new SqmFunctionRegistry(); + metamodel = new MockMappingMetamodelImpl(); + + bootModel = new MetadataImpl( + UUID.randomUUID(), + this, + emptyMap(), + emptyList(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + emptyMap(), + new Database(this, MockJdbcServicesInitiator.jdbcServices.getJdbcEnvironment()), + this + ); + + metadataContext = new MetadataContext( + metamodel.getJpaMetamodel(), + metamodel, + bootModel, + JpaStaticMetaModelPopulationSetting.DISABLED, + JpaMetaModelPopulationSetting.DISABLED, + this + ); + + typeConfiguration = new TypeConfiguration(); + typeConfiguration.scope((MetadataBuildingContext) this); + MockJdbcServicesInitiator.genericDialect.initializeFunctionRegistry(this); + CommonFunctionFactory functionFactory = new CommonFunctionFactory(this); + functionFactory.listagg(null); + functionFactory.inverseDistributionOrderedSetAggregates(); + functionFactory.hypotheticalOrderedSetAggregates(); + functionFactory.windowFunctions(); + typeConfiguration.scope((SessionFactoryImplementor) this); + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + + @Override + public void addObserver(SessionFactoryObserver observer) { + } + + @Override + public MetadataBuildingOptions getBuildingOptions() { + return this; + } + + @Override + public PhysicalNamingStrategy getPhysicalNamingStrategy() { + return new PhysicalNamingStrategyStandardImpl(); + } + + @Override + public ImplicitNamingStrategy getImplicitNamingStrategy() { + return new ImplicitNamingStrategyJpaCompliantImpl(); + } + + static CollectionType createCollectionType(String role, String name) { + switch (name) { + case "Set": + case "SortedSet": + //might actually be a bag! + //TODO: look for @OrderColumn on the property + return new SetType(role, null); + case "List": + case "SortedList": + return new ListType(role, null); + case "Map": + case "SortedMap": + return new MapType(role, null); + default: + return new BagType(role, null); + } + } + + /** + * Lazily create a {@link MockEntityPersister} + */ + abstract MockEntityPersister createMockEntityPersister(String entityName); + + /** + * Lazily create a {@link MockCollectionPersister} + */ + abstract MockCollectionPersister createMockCollectionPersister(String role); + + abstract boolean isEntityDefined(String entityName); + + abstract String qualifyName(String entityName); + + abstract boolean isAttributeDefined(String entityName, String fieldName); + + abstract boolean isClassDefined(String qualifiedName); + + abstract boolean isFieldDefined(String qualifiedClassName, String fieldName); + + abstract boolean isConstructorDefined(String qualifiedClassName, List argumentTypes); + + abstract Type propertyType(String typeName, String propertyPath); + + protected abstract boolean isSubtype(String entityName, String subtypeEntityName); + + protected abstract String getSupertype(String entityName); + + private EntityPersister createEntityPersister(String entityName) { + MockEntityPersister result = entityPersistersByName.get(entityName); + if (result!=null) { + return result; + } + result = createMockEntityPersister(entityName); + entityPersistersByName.put(entityName, result); + return result; + } + + private CollectionPersister createCollectionPersister(String entityName) { + MockCollectionPersister result = collectionPersistersByName.get(entityName); + if (result!=null) { + return result; + } + result = createMockCollectionPersister(entityName); + collectionPersistersByName.put(entityName, result); + return result; + } + + List getMockEntityPersisters() { + return entityPersistersByName.values() + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + public Type getIdentifierType(String className) + throws MappingException { + return createEntityPersister(className) + .getIdentifierType(); + } + + @Override + public String getIdentifierPropertyName(String className) + throws MappingException { + return createEntityPersister(className) + .getIdentifierPropertyName(); + } + + @Override + public Type getReferencedPropertyType(String className, String propertyName) + throws MappingException { + return createEntityPersister(className) + .getPropertyType(propertyName); + } + + @Override + public MetamodelImplementor getMetamodel() { + return metamodel; + } + + @Override + public StandardServiceRegistryImpl getServiceRegistry() { + return serviceRegistry; + } + + @Override + public JdbcServices getJdbcServices() { + return MockJdbcServicesInitiator.jdbcServices; +// return serviceRegistry.getService(JdbcServices.class); + } + + @Override + public String getName() { + return "mock"; + } + + @Override + public SessionFactoryOptions getSessionFactoryOptions() { + return this; + } + + @Override + public Set getDefinedFilterNames() { + return emptySet(); + } + + @Override + public CacheImplementor getCache() { + return new DisabledCaching(this); + } + + @Override + public EntityNotFoundDelegate getEntityNotFoundDelegate() { + return new StandardEntityNotFoundDelegate(); + } + + @Override + public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy() { + return new DefaultCustomEntityDirtinessStrategy(); + } + + @Override + public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver() { + return null; + } + + @Override + public FastSessionServices getFastSessionServices() { + throw new UnsupportedOperationException(); + } + + + @Override + public void close() {} + + @Override + public RootGraphImplementor findEntityGraphByName(String s) { + throw new UnsupportedOperationException(); + } + + static Class toPrimitiveClass(Class type) { + switch (type.getName()) { + case "java.lang.Boolean": + return boolean.class; + case "java.lang.Character": + return char.class; + case "java.lang.Integer": + return int.class; + case "java.lang.Short": + return short.class; + case "java.lang.Byte": + return byte.class; + case "java.lang.Long": + return long.class; + case "java.lang.Float": + return float.class; + case "java.lang.Double": + return double.class; + default: + return Object.class; + } + } + + @Override + public NativeQueryInterpreter getNativeQueryInterpreter() { + return new NativeQueryInterpreterStandardImpl(); + } + + @Override + public QueryInterpretationCache getInterpretationCache() { + return new QueryInterpretationCacheDisabledImpl(this::getStatistics); + } + + @Override + public StatisticsImplementor getStatistics() { + return new StatisticsImpl(this); + } + + @Override + public SqmFunctionRegistry getSqmFunctionRegistry() { + return functionRegistry; + } + + @Override + public NodeBuilder getCriteriaBuilder() { + return new SqmCriteriaNodeBuilder( + "", + "", + this, + false, + ValueHandlingMode.INLINE, + () -> MockSessionFactory.this + ); + } + + @Override + public void validateNamedQueries() { + } + + @Override + public NamedObjectRepository getNamedObjectRepository() { + return new NamedObjectRepositoryImpl(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + } + + @Override + public HqlTranslator getHqlTranslator() { + return new StandardHqlTranslator(MockSessionFactory.this, () -> false); + } + + @Override + public SqmTranslatorFactory getSqmTranslatorFactory() { + return new StandardSqmTranslatorFactory(); + } + + @Override + public QueryEngine getQueryEngine() { + return this; + } + + @Override + public JpaMetamodelImplementor getJpaMetamodel() { + return metamodel.getJpaMetamodel(); + } + + @Override + public MappingMetamodelImplementor getMappingMetamodel() { + return metamodel; + } + + @Override + public RuntimeMetamodelsImplementor getRuntimeMetamodels() { + RuntimeMetamodelsImpl runtimeMetamodels = new RuntimeMetamodelsImpl(); + runtimeMetamodels.setJpaMetamodel( metamodel.getJpaMetamodel() ); + runtimeMetamodels.setMappingMetamodel( metamodel ); + return runtimeMetamodels; + } + + @Override + public boolean isClosed() { + return false; + } + + private static final SessionFactoryObserver[] NO_OBSERVERS = new SessionFactoryObserver[0]; + private static final EntityNameResolver[] NO_RESOLVERS = new EntityNameResolver[0]; + + static MutableJpaCompliance jpaCompliance = new MutableJpaComplianceImpl(emptyMap()); + + @Override + public MutableJpaCompliance getJpaCompliance() { + return jpaCompliance; + } + + @Override + public String getSessionFactoryName() { + return "mock"; + } + + @Override + public String getUuid() { + return "mock"; + } + + @Override + public SessionFactoryObserver[] getSessionFactoryObservers() { + return NO_OBSERVERS; + } + + @Override + public EntityNameResolver[] getEntityNameResolvers() { + return NO_RESOLVERS; + } + + @Override + public BatchFetchStyle getBatchFetchStyle() { + return BatchFetchStyle.LEGACY; + } + + @Override + public boolean isDelayBatchFetchLoaderCreationsEnabled() { + return false; + } + + @Override + public Integer getMaximumFetchDepth() { + return null; + } + + @Override + public void setCheckNullability(boolean enabled) {} + + + private static class MockMappingDefaults implements MappingDefaults { + @Override + public String getImplicitSchemaName() { + return null; + } + + @Override + public String getImplicitCatalogName() { + return null; + } + + @Override + public boolean shouldImplicitlyQuoteIdentifiers() { + return false; + } + + @Override + public String getImplicitIdColumnName() { + return null; + } + + @Override + public String getImplicitTenantIdColumnName() { + return null; + } + + @Override + public String getImplicitDiscriminatorColumnName() { + return null; + } + + @Override + public String getImplicitPackageName() { + return null; + } + + @Override + public boolean isAutoImportEnabled() { + return false; + } + + @Override + public String getImplicitCascadeStyleName() { + return null; + } + + @Override + public String getImplicitPropertyAccessorName() { + return null; + } + + @Override + public boolean areEntitiesImplicitlyLazy() { + return false; + } + + @Override + public boolean areCollectionsImplicitlyLazy() { + return false; + } + + @Override + public AccessType getImplicitCacheAccessType() { + return null; + } + + @Override + public CollectionClassification getImplicitListClassification() { + return null; + } + } + + @Override + public Dialect getDialect() { + return MockJdbcServicesInitiator.genericDialect; + } + + @Override + public int getPreferredSqlTypeCodeForBoolean() { + return SqlTypes.BOOLEAN; + } + + @Override + public int getPreferredSqlTypeCodeForDuration() { + return SqlTypes.NUMERIC; + } + + @Override + public int getPreferredSqlTypeCodeForUuid() { + return SqlTypes.UUID; + } + + @Override + public int getPreferredSqlTypeCodeForInstant() { + return SqlTypes.TIMESTAMP_WITH_TIMEZONE; + } + + @Override + public int getPreferredSqlTypeCodeForArray() { + return SqlTypes.ARRAY; + } + + private class MockMappingMetamodelImpl extends MappingMetamodelImpl { + public MockMappingMetamodelImpl() { + super(typeConfiguration, serviceRegistry); + } + + @Override + public EntityPersister getEntityDescriptor(String entityName) { + return createEntityPersister(entityName); + } + + @Override + public EntityPersister entityPersister(String entityName) + throws MappingException { + return createEntityPersister(entityName); + } + + @Override + public EntityPersister locateEntityPersister(String entityName) + throws MappingException { + return createEntityPersister(entityName); + } + + @Override + public CollectionPersister getCollectionDescriptor(String role) { + return createCollectionPersister(role); + } + + @Override + public CollectionPersister findCollectionDescriptor(String role) { + return createCollectionPersister(role); + } + + @Override + public CollectionPersister collectionPersister(String role) { + return createCollectionPersister(role); + } + + @Override + public JpaMetamodelImplementor getJpaMetamodel() { + return new MockJpaMetamodelImpl(); + } + + @Override + public EntityPersister findEntityDescriptor(String entityName) { + return createEntityPersister(entityName); + } + } + + @Override + public SessionFactoryImplementor getSessionFactory() { + return MockSessionFactory.this; + } + + @Override + public BootstrapContext getBootstrapContext() { + return this; + } + + @Override + public MetadataImplementor getBootModel() { + return bootModel; + } + + @Override + public MappingMetamodelImplementor getDomainModel() { + return metamodel; + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return functionRegistry; + } + + @Override + public Map getSettings() { + return emptyMap(); + } + + @Override + public SqlStringGenerationContext getSqlStringGenerationContext() { + throw new UnsupportedOperationException(); + } + + @Override + public IdentifierGeneratorFactory getIdentifierGeneratorFactory() { + return new StandardIdentifierGeneratorFactory(serviceRegistry, true); + } + + @Override + public MappingDefaults getMappingDefaults() { + return new MockMappingDefaults(); + } + + @Override + public TimeZoneStorageStrategy getDefaultTimeZoneStorageStrategy() { + return TimeZoneStorageStrategy.NATIVE; + } + + private class MockJpaMetamodelImpl extends JpaMetamodelImpl { + public MockJpaMetamodelImpl() { + super(typeConfiguration, metamodel, serviceRegistry); + } + + @Override + public EntityDomainType entity(String entityName) { + if ( isEntityDefined(entityName) ) { + return new MockEntityDomainType<>(entityName); + } + else { + return null; + } + } + + @Override + public String qualifyImportableName(String queryName) { + if (isClassDefined(queryName)) { + return queryName; + } + else if (isEntityDefined(queryName)) { + return qualifyName(queryName); + } + return null; + } + + @Override + public ManagedDomainType findManagedType(Class cls) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityDomainType findEntityType(Class cls) { + if ( isEntityDefined( cls.getName() ) ) { + return new MockEntityDomainType<>( cls.getName() ); + } + else { + return null; + } + } + + @Override + public ManagedDomainType managedType(Class cls) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityDomainType entity(Class cls) { + throw new UnsupportedOperationException(); + } + + @Override + public JpaCompliance getJpaCompliance() { + return jpaCompliance; + } + } + + class MockMappedDomainType extends MappedSuperclassTypeImpl{ + public MockMappedDomainType(String typeName) { + super(typeName, false, true, false, null, null, metamodel.getJpaMetamodel()); + } + + @Override + public PersistentAttribute findDeclaredAttribute(String name) { + String typeName = getTypeName(); + return isFieldDefined(typeName, name) + ? createAttribute(name, typeName, propertyType(typeName, name), this) + : null; + } + } + + class MockEntityDomainType extends EntityTypeImpl { + + public MockEntityDomainType(String entityName) { + super(entityName, entityName, false, true, false, null, null, + metamodel.getJpaMetamodel()); + } + + @Override + public SqmPathSource findSubPathSource(String name, JpaMetamodelImplementor metamodel) { + SqmPathSource source = super.findSubPathSource(name, metamodel); + if ( source != null ) { + return source; + } + String supertype = MockSessionFactory.this.getSupertype(getHibernateEntityName()); + PersistentAttribute superattribute + = new MockMappedDomainType<>(supertype).findAttribute(name); + if (superattribute != null) { + return (SqmPathSource) superattribute; + } + for (Map.Entry entry : entityPersistersByName.entrySet()) { + if (!entry.getValue().getEntityName().equals(getHibernateEntityName()) + && isSubtype(entry.getValue().getEntityName(), getHibernateEntityName())) { + PersistentAttribute subattribute + = new MockEntityDomainType<>(entry.getValue().getEntityName()).findAttribute(name); + if (subattribute != null) { + return (SqmPathSource) subattribute; + } + } + } + return null; + } + + @Override + public PersistentAttribute findAttribute(String name) { + PersistentAttribute attribute = super.findAttribute(name); + if (attribute != null) { + return attribute; + } + String supertype = MockSessionFactory.this.getSupertype(getHibernateEntityName()); + PersistentAttribute superattribute + = new MockMappedDomainType<>(supertype).findAttribute(name); + if (superattribute != null) { + return superattribute; + } + return null; + } + + @Override + public PersistentAttribute findDeclaredAttribute(String name) { + String entityName = getHibernateEntityName(); + return isAttributeDefined(entityName, name) + ? createAttribute(name, entityName, getReferencedPropertyType(entityName, name), this) + : null; + } + } + + private AbstractAttribute createAttribute(String name, String entityName, Type type, ManagedDomainType owner) { + if (type==null) { + throw new UnsupportedOperationException(entityName + "." + name); + } + else if ( type.isCollectionType() ) { + CollectionType collectionType = (CollectionType) type; + return createPluralAttribute(collectionType, entityName, name, owner); + } + else if ( type.isEntityType() ) { + return new SingularAttributeImpl<>( + owner, + name, + AttributeClassification.MANY_TO_ONE, + new MockEntityDomainType<>(type.getName()), + null, + null, + false, + false, + true, + false, + metadataContext + ); + } + else if ( type.isComponentType() ) { + CompositeType compositeType = (CompositeType) type; + return new SingularAttributeImpl<>( + owner, + name, + AttributeClassification.EMBEDDED, + createEmbeddableDomainType(entityName, compositeType, owner), + null, + null, + false, + false, + true, + false, + metadataContext + ); + } + else { + return new SingularAttributeImpl<>( + owner, + name, + AttributeClassification.BASIC, + (DomainType) type, + type instanceof JdbcMapping + ? ((JdbcMapping) type).getJavaTypeDescriptor() + : null, + null, + false, + false, + true, + false, + metadataContext + ); + } + } + + private DomainType getElementDomainType(String entityName, CollectionType collectionType, ManagedDomainType owner) { + Type elementType = collectionType.getElementType(MockSessionFactory.this); + return getDomainType(entityName, collectionType, owner, elementType); + } + + private DomainType getMapKeyDomainType(String entityName, CollectionType collectionType, ManagedDomainType owner) { + Type keyType = getMappingMetamodel().getCollectionDescriptor( collectionType.getRole() ).getIndexType(); + return getDomainType(entityName, collectionType, owner, keyType); + } + + private DomainType getDomainType(String entityName, CollectionType collectionType, ManagedDomainType owner, Type elementType) { + if ( elementType.isEntityType() ) { + String associatedEntityName = collectionType.getAssociatedEntityName(MockSessionFactory.this); + return new MockEntityDomainType<>(associatedEntityName); + } + else if ( elementType.isComponentType() ) { + CompositeType compositeType = (CompositeType) elementType; + return createEmbeddableDomainType(entityName, compositeType, owner); + } + else if ( elementType instanceof DomainType ) { + return (DomainType) elementType; + } + else { + return new BasicTypeImpl<>(new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE); + } + } + + private AbstractPluralAttribute createPluralAttribute( + CollectionType collectionType, + String entityName, + String name, + ManagedDomainType owner) { + Property property = new Property(); + property.setName(name); + JavaType collectionJavaType = + typeConfiguration.getJavaTypeRegistry() + .getDescriptor(collectionType.getReturnedClass()); + DomainType elementDomainType = getElementDomainType(entityName, collectionType, owner); + CollectionClassification classification = collectionType.getCollectionClassification(); + switch (classification) { + case LIST: + return new ListAttributeImpl( + new PluralAttributeBuilder<>( + collectionJavaType, + true, + AttributeClassification.MANY_TO_MANY, + classification, + elementDomainType, + typeConfiguration.getBasicTypeRegistry() + .getRegisteredType(Integer.class), + owner, + property, + null + ), + metadataContext + ); + case BAG: + case ID_BAG: + return new BagAttributeImpl( + new PluralAttributeBuilder<>( + collectionJavaType, + true, + AttributeClassification.MANY_TO_MANY, + classification, + elementDomainType, + null, + owner, + property, + null + ), + metadataContext + ); + case SET: + case SORTED_SET: + case ORDERED_SET: + return new SetAttributeImpl( + new PluralAttributeBuilder<>( + collectionJavaType, + true, + AttributeClassification.MANY_TO_MANY, + classification, + elementDomainType, + null, + owner, + property, + null + ), + metadataContext + ); + case MAP: + case SORTED_MAP: + case ORDERED_MAP: + DomainType keyDomainType = getMapKeyDomainType(entityName, collectionType, owner); + return new MapAttributeImpl( + new PluralAttributeBuilder<>( + collectionJavaType, + true, + AttributeClassification.MANY_TO_MANY, + classification, + elementDomainType, + keyDomainType, + owner, + property, + null + ), + metadataContext + ); + default: + return null; + } + } + + private EmbeddableTypeImpl createEmbeddableDomainType(String entityName, CompositeType compositeType, ManagedDomainType owner) { + return new EmbeddableTypeImpl(new UnknownBasicJavaType<>(Object.class), true, metamodel.getJpaMetamodel()) { + @Override + public PersistentAttribute findAttribute(String name) { + int i = compositeType.getPropertyIndex(name); + Type subtype = compositeType.getSubtypes()[i]; + return createAttribute( + name, + entityName, //TOOD: WRONG!!! + subtype, + owner + ); + } + }; + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Mocker.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Mocker.java new file mode 100644 index 0000000000..db4d2647f9 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Mocker.java @@ -0,0 +1,90 @@ +/* + * 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.jpamodelgen.validation; + +import net.bytebuddy.ByteBuddy; + +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import static net.bytebuddy.implementation.FixedValue.value; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +/** + * @author Gavin King + */ +@FunctionalInterface +public interface Mocker { + + T make(Object... args); + + Map, Class> mocks = new HashMap<>(); + + static Supplier nullary(Class clazz) { + try { + Class mock = load(clazz); + return () -> { + try { + return mock.newInstance(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + static Mocker variadic(Class clazz) { + Constructor[] constructors = load(clazz).getDeclaredConstructors(); + if (constructors.length>1) { + throw new RuntimeException("more than one constructor for " + clazz); + } + Constructor constructor = constructors[0]; + return (args) -> { + try { + return (T) constructor.newInstance(args); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + @SuppressWarnings("unchecked") + static Class load(Class clazz) { + if (mocks.containsKey(clazz)) { + return (Class) mocks.get(clazz); + } + Class mock = + new ByteBuddy() + .subclass(clazz) + .method(returns(String.class).and(isAbstract())) + .intercept(value("")) + .method(returns(boolean.class).and(isAbstract())) + .intercept(value(false)) + .method(returns(int.class).and(isAbstract())) + .intercept(value(0)) + .method(returns(long.class).and(isAbstract())) + .intercept(value(0L)) + .method(returns(int[].class).and(isAbstract())) + .intercept(value(new int[0])) + .method(returns(String[].class).and(isAbstract())) + .intercept(value(new String[0])) + .make() + .load(clazz.getClassLoader()) + .getLoaded(); + mocks.put(clazz,mock); + return mock; + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/ProcessorSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/ProcessorSessionFactory.java new file mode 100644 index 0000000000..643beaf52e --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/ProcessorSessionFactory.java @@ -0,0 +1,924 @@ +/* + * 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.jpamodelgen.validation; + +import jakarta.persistence.AccessType; +import org.hibernate.PropertyNotFoundException; +import org.hibernate.engine.spi.Mapping; +import org.hibernate.type.BasicType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; +import org.hibernate.type.ManyToOneType; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.EnumJavaType; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; +import org.hibernate.type.internal.BasicTypeImpl; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import java.beans.Introspector; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.stream; +import static org.hibernate.internal.util.StringHelper.qualify; +import static org.hibernate.internal.util.StringHelper.root; +import static org.hibernate.internal.util.StringHelper.split; +import static org.hibernate.internal.util.StringHelper.unroot; + +/** + * Implementation of the {@code Mock} objects based on standard + * annotation processor APIs. Note that alternative implementations + * exist in the Query Validator project. + * + * @author Gavin King + */ +@SuppressWarnings("nullness") +public abstract class ProcessorSessionFactory extends MockSessionFactory { + + public static MockSessionFactory create(ProcessingEnvironment environment) { + return instance.make(environment); + } + + static final Mocker instance = Mocker.variadic(ProcessorSessionFactory.class); + private static final Mocker component = Mocker.variadic(Component.class); + private static final Mocker toManyPersister = Mocker.variadic(ToManyAssociationPersister.class); + private static final Mocker collectionPersister = Mocker.variadic(ElementCollectionPersister.class); + private static final Mocker entityPersister = Mocker.variadic(EntityPersister.class); + + private static final CharSequence jakartaPersistence = new StringBuilder("jakarta").append('.').append("persistence"); + private static final CharSequence javaxPersistence = new StringBuilder("javax").append('.').append("persistence"); + + private final Elements elementUtil; + private final Types typeUtil; + + public ProcessorSessionFactory(ProcessingEnvironment processingEnv) { + elementUtil = processingEnv.getElementUtils(); + typeUtil = processingEnv.getTypeUtils(); + } + + @Override + MockEntityPersister createMockEntityPersister(String entityName) { + TypeElement type = findEntityClass(entityName); + return type == null ? null : entityPersister.make(entityName, type, this); + } + + @Override + MockCollectionPersister createMockCollectionPersister(String role) { + String entityName = root(role); //only works because entity names don't contain dots + String propertyPath = unroot(role); + TypeElement entityClass = findEntityClass(entityName); + AccessType defaultAccessType = getDefaultAccessType(entityClass); + Element property = findPropertyByPath(entityClass, propertyPath, defaultAccessType); + CollectionType collectionType = collectionType(memberType(property), role); + if (isToManyAssociation(property)) { + return toManyPersister.make(role, collectionType, + getToManyTargetEntityName(property), this); + } + else if (isElementCollectionProperty(property)) { + Element elementType = asElement(getElementCollectionElementType(property)); + return collectionPersister.make(role, collectionType, + elementType, propertyPath, defaultAccessType, this); + } + else { + return null; + } + } + + @Override + Type propertyType(String typeName, String propertyPath) { + TypeElement type = findClassByQualifiedName(typeName); + AccessType accessType = getAccessType(type, AccessType.FIELD); + Element propertyByPath = findPropertyByPath(type, propertyPath, accessType); + return propertyByPath == null ? null + : propertyType(propertyByPath, typeName, propertyPath, accessType); + } + + private static Element findPropertyByPath(TypeElement type, + String propertyPath, + AccessType defaultAccessType) { + return stream(split(".", propertyPath)) + .reduce((Element) type, + (symbol, segment) -> dereference( defaultAccessType, symbol, segment ), + (last, current) -> current); + } + + private static Element dereference(AccessType defaultAccessType, Element symbol, String segment) { + if (symbol == null) { + return null; + } + else { + Element element = asElement(symbol.asType()); + return element instanceof TypeElement + ? findProperty((TypeElement) element, segment, defaultAccessType) + : null; + } + } + + static Type propertyType(Element member, String entityName, String path, AccessType defaultAccessType) { + TypeMirror memberType = memberType(member); + if (isEmbeddedProperty(member)) { + return component.make(asElement(memberType), entityName, path, defaultAccessType); + } + else if (isToOneAssociation(member)) { + String targetEntity = getToOneTargetEntity(member); + return new ManyToOneType(typeConfiguration, targetEntity); + } + else if (isToManyAssociation(member)) { + return collectionType(memberType, qualify(entityName, path)); + } + else if (isElementCollectionProperty(member)) { + return collectionType(memberType, qualify(entityName, path)); + } + else if (isEnumProperty(member)) { + return new BasicTypeImpl(new EnumJavaType(Enum.class), enumJdbcType(member)); + } + else { + return typeConfiguration.getBasicTypeRegistry() + .getRegisteredType(qualifiedName(memberType)); + } + } + + private static JdbcType enumJdbcType(Element member) { + VariableElement mapping = (VariableElement) + getAnnotationMember(getAnnotation(member,"Enumerated"), "value"); + return mapping != null && mapping.getSimpleName().contentEquals("STRING") + ? VarcharJdbcType.INSTANCE + : IntegerJdbcType.INSTANCE; + } + + private static Type elementCollectionElementType(TypeElement elementType, + String role, String path, + AccessType defaultAccessType) { + if (isEmbeddableType(elementType)) { + return component.make(elementType, role, path, defaultAccessType); + } + else { + return typeConfiguration.getBasicTypeRegistry() + .getRegisteredType(qualifiedName(elementType.asType())); + } + } + + private static CollectionType collectionType(TypeMirror type, String role) { + return createCollectionType(role, simpleName(type)); + } + + public static abstract class Component implements CompositeType { + private final String[] propertyNames; + private final Type[] propertyTypes; + + TypeElement type; + + public Component(TypeElement type, + String entityName, String path, + AccessType defaultAccessType) { + this.type = type; + + List names = new ArrayList<>(); + List types = new ArrayList<>(); + + while (type!=null) { + if (isMappedClass(type)) { //ignore unmapped intervening classes + AccessType accessType = getAccessType(type, defaultAccessType); + for (Element member: type.getEnclosedElements()) { + if (isPersistable(member, accessType)) { + String name = propertyName(member); + Type propertyType = + propertyType(member, entityName, + qualify(path, name), defaultAccessType); + if (propertyType != null) { + names.add(name); + types.add(propertyType); + } + } + } + } + type = (TypeElement) asElement(type.getSuperclass()); + } + + propertyNames = names.toArray(new String[0]); + propertyTypes = types.toArray(new Type[0]); + } + + @Override + public int getPropertyIndex(String name) { + String[] names = getPropertyNames(); + for ( int i = 0, max = names.length; i < max; i++ ) { + if ( names[i].equals( name ) ) { + return i; + } + } + throw new PropertyNotFoundException( + "Could not resolve attribute '" + name + "' of '" + getName() + "'" + ); + } + + @Override + public String getName() { + return type.getSimpleName().toString(); + } + + @Override + public boolean isComponentType() { + return true; + } + + @Override + public String[] getPropertyNames() { + return propertyNames; + } + + @Override + public Type[] getSubtypes() { + return propertyTypes; + } + + @Override + public boolean[] getPropertyNullability() { + return new boolean[propertyNames.length]; + } + + @Override + public int getColumnSpan(Mapping mapping) { + return propertyNames.length; + } + } + + public static abstract class EntityPersister extends MockEntityPersister { + private final TypeElement type; + private final Types typeUtil; + + public EntityPersister(String entityName, TypeElement type, ProcessorSessionFactory that) { + super(entityName, getDefaultAccessType(type), that); + this.type = type; + this.typeUtil = that.typeUtil; + initSubclassPersisters(); + } + + @Override + boolean isSubclassPersister(MockEntityPersister entityPersister) { + EntityPersister persister = (EntityPersister) entityPersister; + return typeUtil.isSubtype( persister.type.asType(), type.asType() ); + } + + @Override + Type createPropertyType(String propertyPath) { + Element symbol = findPropertyByPath(type, propertyPath, defaultAccessType); + return symbol == null ? null : + propertyType(symbol, getEntityName(), propertyPath, defaultAccessType); + } + + } + + public abstract static class ToManyAssociationPersister extends MockCollectionPersister { + public ToManyAssociationPersister(String role, CollectionType collectionType, String targetEntityName, ProcessorSessionFactory that) { + super(role, collectionType, + new ManyToOneType(typeConfiguration, targetEntityName), + that); + } + + @Override + Type getElementPropertyType(String propertyPath) { + return getElementPersister().getPropertyType(propertyPath); + } + } + + public abstract static class ElementCollectionPersister extends MockCollectionPersister { + private final TypeElement elementType; + private final AccessType defaultAccessType; + + public ElementCollectionPersister(String role, + CollectionType collectionType, + TypeElement elementType, + String propertyPath, + AccessType defaultAccessType, + ProcessorSessionFactory that) { + super(role, collectionType, + elementCollectionElementType(elementType, role, + propertyPath, defaultAccessType), + that); + this.elementType = elementType; + this.defaultAccessType = defaultAccessType; + } + + @Override + Type getElementPropertyType(String propertyPath) { + Element symbol = findPropertyByPath(elementType, propertyPath, defaultAccessType); + return symbol == null ? null : + propertyType(symbol, getOwnerEntityName(), propertyPath, defaultAccessType); + } + } + + @Override + boolean isEntityDefined(String entityName) { + return findEntityClass(entityName) != null; + } + + @Override + String qualifyName(String entityName) { + TypeElement entityClass = findEntityClass(entityName); + return entityClass == null ? null : entityClass.getSimpleName().toString(); + } + + @Override + boolean isAttributeDefined(String entityName, String fieldName) { + TypeElement entityClass = findEntityClass(entityName); + return entityClass != null + && findPropertyByPath(entityClass, fieldName, getDefaultAccessType(entityClass)) != null; + } + + private TypeElement findEntityClass(String entityName) { + if (entityName == null) { + return null; + } + else if (entityName.indexOf('.')>0) { + return findEntityByQualifiedName(entityName); + } + else { + return findEntityByUnqualifiedName(entityName); + } + } + + private TypeElement findEntityByQualifiedName(String entityName) { + TypeElement type = findClassByQualifiedName(entityName); + return type != null && isEntity(type) ? type : null; + } + + //Needed only for ECJ + private final Map entityCache = new HashMap<>(); + + private TypeElement findEntityByUnqualifiedName(String entityName) { + TypeElement cached = entityCache.get(entityName); + if ( cached != null ) { + return cached; + } + TypeElement symbol = + findEntityByUnqualifiedName(entityName, + elementUtil.getModuleElement("")); + if (symbol!=null) { + entityCache.put(entityName, symbol); + return symbol; + } + for (ModuleElement module: elementUtil.getAllModuleElements()) { + symbol = findEntityByUnqualifiedName(entityName, module); + if (symbol!=null) { + entityCache.put(entityName, symbol); + return symbol; + } + } + return null; + } + + private static TypeElement findEntityByUnqualifiedName(String entityName, ModuleElement module) { + for (Element element: module.getEnclosedElements()) { + if (element.getKind() == ElementKind.PACKAGE) { + PackageElement pack = (PackageElement) element; + try { + for (Element member : pack.getEnclosedElements()) { + if (isMatchingEntity(member, entityName)) { + return (TypeElement) member; + } + } + } + catch (Exception e) { + } + } + } + return null; + } + + private static boolean isMatchingEntity(Element symbol, String entityName) { + if (symbol.getKind() == ElementKind.CLASS) { + TypeElement type = (TypeElement) symbol; + return isEntity(type) + && getEntityName(type).equals(entityName); + } + else { + return false; + } + } + + private static Element findProperty(TypeElement type, String propertyName, AccessType defaultAccessType) { + //iterate up the superclass hierarchy + while (type!=null) { + if (isMappedClass(type)) { //ignore unmapped intervening classes + AccessType accessType = getAccessType(type, defaultAccessType); + for (Element member: type.getEnclosedElements()) { + if (isMatchingProperty(member, propertyName, accessType)) { + return member; + } + } + } + type = (TypeElement) asElement(type.getSuperclass()); + } + return null; + } + + private static boolean isMatchingProperty(Element symbol, String propertyName, AccessType accessType) { + return isPersistable(symbol, accessType) + && propertyName.equals(propertyName(symbol)); + } + + private static boolean isGetterMethod(ExecutableElement method) { + if (!method.getParameters().isEmpty()) { + return false; + } + else { + Name methodName = method.getSimpleName(); + TypeMirror returnType = method.getReturnType(); + return methodName.subSequence(0,3).toString().equals("get") && returnType.getKind() != TypeKind.VOID + || methodName.subSequence(0,2).toString().equals("is") && returnType.getKind() == TypeKind.BOOLEAN; + } + } + + private static boolean hasAnnotation(TypeMirror type, String annotationName) { + return type.getKind() == TypeKind.DECLARED + && getAnnotation(((DeclaredType) type).asElement(), annotationName)!=null; + } + + private static boolean hasAnnotation(Element member, String annotationName) { + return getAnnotation(member, annotationName)!=null; + } + + private static AnnotationMirror getAnnotation(Element member, String annotationName) { + for (AnnotationMirror mirror : member.getAnnotationMirrors()) { + TypeElement annotationType = (TypeElement) mirror.getAnnotationType().asElement(); + if ( annotationType.getSimpleName().contentEquals(annotationName) + && annotationType.getNestingKind() == NestingKind.TOP_LEVEL ) { + PackageElement pack = (PackageElement) annotationType.getEnclosingElement(); + Name packageName = pack.getQualifiedName(); + if (packageName.contentEquals(jakartaPersistence) + || packageName.contentEquals(javaxPersistence)) { + return mirror; + } + } + } + return null; + } + + private static Object getAnnotationMember(AnnotationMirror annotation, String memberName) { + if ( annotation == null ) { + return null; + } + for (Map.Entry entry : + annotation.getElementValues().entrySet()) { + if (entry.getKey().getSimpleName().contentEquals(memberName)) { + return entry.getValue().getValue(); + } + } + return null; + } + + private static boolean isMappedClass(TypeElement type) { + return hasAnnotation(type, "Entity") + || hasAnnotation(type, "Embeddable") + || hasAnnotation(type, "MappedSuperclass"); + } + + private static boolean isEntity(TypeElement member) { + return member.getKind() == ElementKind.CLASS +// && member.getAnnotation(entityAnnotation)!=null; + && hasAnnotation(member, "Entity"); + } + + private static boolean isId(Element member) { + return hasAnnotation(member, "Id"); + } + + private static boolean isStatic(Element member) { + return member.getModifiers().contains(Modifier.STATIC); + } + + private static boolean isTransient(Element member) { + return hasAnnotation(member, "Transient") + || member.getModifiers().contains(Modifier.TRANSIENT); + } + + private static boolean isEnumProperty(Element member) { + if (hasAnnotation(member, "Enumerated")) { + return true; + } + else { + TypeMirror type = member.asType(); + if (type.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) type; + TypeElement typeElement = (TypeElement) declaredType.asElement(); + //TODO: something better here! + return typeElement.getSuperclass().toString().startsWith("java.lang.Enum"); + } + else { + return false; + } + } + } + + private static boolean isEmbeddableType(TypeElement type) { + return hasAnnotation(type, "Embeddable"); + } + + private static boolean isEmbeddedProperty(Element member) { + if (hasAnnotation(member, "Embedded")) { + return true; + } + else { + TypeMirror type = member.asType(); + return type.getKind() == TypeKind.DECLARED + && hasAnnotation(type, "Embeddable"); + } + } + + private static boolean isElementCollectionProperty(Element member) { + return hasAnnotation(member, "ElementCollection"); + } + + private static boolean isToOneAssociation(Element member) { + return hasAnnotation(member, "ManyToOne") + || hasAnnotation(member, "OneToOne"); + } + + private static boolean isToManyAssociation(Element member) { + return hasAnnotation(member, "ManyToMany") + || hasAnnotation(member, "OneToMany"); + } + + private static AnnotationMirror toOneAnnotation(Element member) { + AnnotationMirror manyToOne = + getAnnotation(member, "ManyToOne"); + if (manyToOne!=null) { + return manyToOne; + } + AnnotationMirror oneToOne = + getAnnotation(member, "OneToOne"); + if (oneToOne!=null) { + return oneToOne; + } + return null; + } + + private static AnnotationMirror toManyAnnotation(Element member) { + AnnotationMirror manyToMany = + getAnnotation(member, "ManyToMany"); + if (manyToMany!=null) { + return manyToMany; + } + AnnotationMirror oneToMany = + getAnnotation(member, "OneToMany"); + if (oneToMany!=null) { + return oneToMany; + } + return null; + } + + private static String simpleName(TypeMirror type) { + return type.getKind() == TypeKind.DECLARED + ? simpleName(asElement(type)) + : type.toString(); + } + + private static String qualifiedName(TypeMirror type) { + return type.getKind() == TypeKind.DECLARED + ? qualifiedName(asElement(type)) + : type.toString(); + } + + private static String simpleName(Element type) { + return type.getSimpleName().toString(); + } + + private static String qualifiedName(Element type) { + if ( type instanceof PackageElement ) { + return ((PackageElement) type).getQualifiedName().toString(); + } + else if ( type instanceof TypeElement ) { + return ((TypeElement) type).getQualifiedName().toString(); + } + else { + Element enclosingElement = type.getEnclosingElement(); + return enclosingElement != null + ? qualifiedName(enclosingElement) + '.' + simpleName(type) + : simpleName(type); + } + } + + private static AccessType getAccessType(TypeElement type, AccessType defaultAccessType) { + AnnotationMirror annotation = + getAnnotation(type, "Access"); + if (annotation==null) { + return defaultAccessType; + } + else { + VariableElement member = (VariableElement) + getAnnotationMember(annotation, "value"); + if (member==null) { + return defaultAccessType; //does not occur + } + switch (member.getSimpleName().toString()) { + case "PROPERTY": + return AccessType.PROPERTY; + case "FIELD": + return AccessType.FIELD; + default: + throw new IllegalStateException(); + } + } + } + + private static String getEntityName(TypeElement type) { + if ( type == null ) { + return null; + } + AnnotationMirror entityAnnotation = + getAnnotation(type, "Entity"); + if (entityAnnotation==null) { + //not an entity! + return null; + } + else { + String name = (String) + getAnnotationMember(entityAnnotation, "name"); + //entity names are unqualified class names + return name==null ? simpleName(type) : name; + } + } + + private TypeMirror getCollectionElementType(Element property) { + DeclaredType declaredType = (DeclaredType) memberType(property); + List typeArguments = declaredType.getTypeArguments(); + TypeMirror elementType = typeArguments.get(typeArguments.size()-1); + return elementType==null + ? elementUtil.getTypeElement("java.lang.Object").asType() + : elementType; + } + + private static String getToOneTargetEntity(Element property) { + AnnotationMirror annotation = toOneAnnotation(property); + TypeMirror classType = (TypeMirror) + getAnnotationMember(annotation, "targetEntity"); + TypeMirror targetType = + classType == null || classType.getKind() == TypeKind.VOID + ? memberType(property) + : classType; + Element element = asElement(targetType); + return element != null && element.getKind() == ElementKind.CLASS + //entity names are unqualified class names + ? getEntityName((TypeElement) element) + : null; + } + + private String getToManyTargetEntityName(Element property) { + AnnotationMirror annotation = toManyAnnotation(property); + TypeMirror classType = (TypeMirror) + getAnnotationMember(annotation, "targetEntity"); + TypeMirror targetType = + classType == null || classType.getKind() == TypeKind.VOID + ? getCollectionElementType(property) + : classType; + Element element = asElement(targetType); + return element != null && element.getKind() == ElementKind.CLASS + //entity names are unqualified class names + ? getEntityName((TypeElement) element) + : null; + } + + private TypeMirror getElementCollectionElementType(Element property) { + AnnotationMirror annotation = getAnnotation(property, + "ElementCollection"); + TypeMirror classType = (TypeMirror) + getAnnotationMember(annotation, "getElementCollectionClass"); + return classType == null + || classType.getKind() == TypeKind.VOID + ? getCollectionElementType(property) + : classType; + } + + @Override + protected String getSupertype(String entityName) { + return asElement(findEntityClass(entityName).getSuperclass()) + .getSimpleName().toString(); + } + + @Override + protected boolean isSubtype(String entityName, String subtypeEntityName) { + return typeUtil.isSubtype( findEntityClass(entityName).asType(), + findEntityClass(subtypeEntityName).asType()); + } + + @Override + boolean isClassDefined(String qualifiedName) { + return findClassByQualifiedName(qualifiedName)!=null; + } + + @Override + boolean isFieldDefined(String qualifiedClassName, String fieldName) { + TypeElement type = findClassByQualifiedName(qualifiedClassName); + return type != null + && type.getEnclosedElements().stream() + .anyMatch(element -> element.getKind() == ElementKind.FIELD + && element.getSimpleName().contentEquals(fieldName)); + } + + @Override + boolean isConstructorDefined(String qualifiedClassName, List argumentTypes) { + TypeElement symbol = findClassByQualifiedName(qualifiedClassName); + if (symbol==null) { + return false; + } + for (Element cons: symbol.getEnclosedElements()) { + if ( cons.getKind() == ElementKind.CONSTRUCTOR ) { + ExecutableElement constructor = (ExecutableElement) cons; + List parameters = constructor.getParameters(); + if (parameters.size()==argumentTypes.size()) { + boolean argumentsCheckOut = true; + for (int i=0; i primitive; + try { + primitive = toPrimitiveClass( type.getReturnedClass() ); + } + catch (Exception e) { + continue; + } + if (!toPrimitiveClass(param).equals(primitive)) { + argumentsCheckOut = false; + break; + } + } + else { + TypeElement typeClass; + if (type instanceof EntityType) { + EntityType entityType = (EntityType) type; + String entityName = entityType.getAssociatedEntityName(); + typeClass = findEntityClass(entityName); + } + //TODO: + // else if (type instanceof CompositeCustomType) { + // typeClass = ((Component) ((CompositeCustomType) type).getUserType()).type; + // } + else if (type instanceof BasicType) { + String className; + //sadly there is no way to get the classname + //from a Hibernate Type without trying to load + //the class! + try { + className = type.getReturnedClass().getName(); + } + catch (Exception e) { + continue; + } + typeClass = findClassByQualifiedName(className); + } + else { + //TODO: what other Hibernate Types do we + // need to consider here? + continue; + } + if (typeClass != null + && !typeUtil.isSubtype( typeClass.asType(), param.asType() ) ) { + argumentsCheckOut = false; + break; + } + } + } + if (argumentsCheckOut) { + return true; //matching constructor found! + } + } + } + } + return false; + } + + private static Class toPrimitiveClass(VariableElement param) { + switch (param.asType().getKind()) { + case BOOLEAN: + return boolean.class; + case CHAR: + return char.class; + case INT: + return int.class; + case SHORT: + return short.class; + case BYTE: + return byte.class; + case LONG: + return long.class; + case FLOAT: + return float.class; + case DOUBLE: + return double.class; + default: + return Object.class; + } + } + + private TypeElement findClassByQualifiedName(String path) { + return path == null ? null : elementUtil.getTypeElement(path); + } + + private static AccessType getDefaultAccessType(TypeElement type) { + //iterate up the superclass hierarchy + while (type!=null) { + for (Element member: type.getEnclosedElements()) { + if (isId(member)) { + return member instanceof ExecutableElement + ? AccessType.PROPERTY + : AccessType.FIELD; + } + } + type = (TypeElement) asElement(type.getSuperclass()); + } + return AccessType.FIELD; + } + + private static String propertyName(Element symbol) { + String name = symbol.getSimpleName().toString(); + if (symbol.getKind() == ElementKind.METHOD) { + if (name.startsWith("get")) { + name = name.substring(3); + } + else if (name.startsWith("is")) { + name = name.substring(2); + } + return Introspector.decapitalize(name); + } + else { + return name; + } + } + + private static boolean isPersistable(Element member, AccessType accessType) { + if (isStatic(member) || isTransient(member)) { + return false; + } + else if (member.getKind() == ElementKind.FIELD) { + return accessType == AccessType.FIELD +// || member.getAnnotation( accessAnnotation ) != null; + || hasAnnotation(member, "Access"); + } + else if (member.getKind() == ElementKind.METHOD) { + return isGetterMethod((ExecutableElement) member) + && (accessType == AccessType.PROPERTY +// || member.getAnnotation( accessAnnotation ) != null); + || hasAnnotation(member, "Access")); + } + else { + return false; + } + } + + private static TypeMirror memberType(Element member) { + if (member instanceof ExecutableElement) { + return ((ExecutableElement) member).getReturnType(); + } + else if (member instanceof VariableElement) { + return member.asType(); + } + else { + throw new IllegalArgumentException("Not a member"); + } + } + + public static Element asElement(TypeMirror type) { + if ( type == null ) { + return null; + } + else { + switch (type.getKind()) { + case DECLARED: + return ((DeclaredType)type).asElement(); + case TYPEVAR: + return ((TypeVariable)type).asElement(); + default: + return null; + } + } + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Validation.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Validation.java new file mode 100644 index 0000000000..1ce821d399 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/Validation.java @@ -0,0 +1,219 @@ +/* + * 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.jpamodelgen.validation; + +import org.antlr.v4.runtime.ANTLRErrorListener; +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.DefaultErrorStrategy; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.hibernate.PropertyNotFoundException; +import org.hibernate.QueryException; +import org.hibernate.grammars.hql.HqlLexer; +import org.hibernate.grammars.hql.HqlParser; +import org.hibernate.query.hql.internal.HqlParseTreeBuilder; +import org.hibernate.query.hql.internal.SemanticQueryBuilder; +import org.hibernate.query.sqm.EntityTypeException; +import org.hibernate.query.sqm.PathElementException; +import org.hibernate.query.sqm.TerminalPathException; +import org.hibernate.type.descriptor.java.spi.JdbcTypeRecommendationException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static java.lang.Character.isJavaIdentifierStart; +import static java.lang.Integer.parseInt; +import static java.util.stream.Stream.concat; + +/** + * The entry point for HQL validation. + * + * @author Gavin King + */ +public class Validation { + + public interface Handler extends ANTLRErrorListener { + void error(int start, int end, String message); + void warn(int start, int end, String message); + + int getErrorCount(); + } + + public static void validate( + String hql, boolean checkParams, + Set setParameterLabels, + Set setParameterNames, + Handler handler, + MockSessionFactory factory) { + validate(hql, checkParams, setParameterLabels, setParameterNames, handler, factory, 0); + } + + public static void validate( + String hql, boolean checkParams, + Set setParameterLabels, + Set setParameterNames, + Handler handler, + MockSessionFactory factory, + int errorOffset) { +// handler = new Filter(handler, errorOffset); + + try { + + final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); + final HqlParser hqlParser = HqlParseTreeBuilder.INSTANCE.buildHqlParser( hql, hqlLexer ); + hqlLexer.addErrorListener( handler ); + hqlParser.getInterpreter().setPredictionMode( PredictionMode.SLL ); + hqlParser.removeErrorListeners(); + hqlParser.addErrorListener( handler ); + hqlParser.setErrorHandler( new BailErrorStrategy() ); + + HqlParser.StatementContext statementContext; + try { + statementContext = hqlParser.statement(); + } + catch ( ParseCancellationException e) { + // reset the input token stream and parser state + hqlLexer.reset(); + hqlParser.reset(); + + // fall back to LL(k)-based parsing + hqlParser.getInterpreter().setPredictionMode( PredictionMode.LL ); + hqlParser.setErrorHandler( new DefaultErrorStrategy() ); + + statementContext = hqlParser.statement(); + + } + if (handler.getErrorCount() == 0) { + try { + new SemanticQueryBuilder<>(Object[].class, () -> false, factory) + .visitStatement( statementContext ); + } + catch (JdbcTypeRecommendationException ignored) { + // just squash these for now + } + catch (QueryException | PathElementException | TerminalPathException | EntityTypeException + | PropertyNotFoundException se) { //TODO is this one really thrown by core? It should not be! + String message = se.getMessage(); + if ( message != null ) { + handler.error(-errorOffset+1, -errorOffset + hql.length(), message); + } + } + } + + if (checkParams) { + checkParameterBinding(hql, setParameterLabels, setParameterNames, handler, errorOffset); + } + } + catch (Exception e) { +// e.printStackTrace(); + } + } + + private static void checkParameterBinding( + String hql, + Set setParameterLabels, + Set setParameterNames, + Handler handler, + int errorOffset) { + try { + String unsetParams = null; + String notSet = null; + String parameters = null; + int start = -1; + int end = -1; + List names = new ArrayList<>(); + List labels = new ArrayList<>(); + final HqlLexer hqlLexer = HqlParseTreeBuilder.INSTANCE.buildHqlLexer( hql ); + loop: + while (true) { + Token token = hqlLexer.nextToken(); + int tokenType = token.getType(); + switch (tokenType) { + case HqlLexer.EOF: + break loop; + case HqlLexer.QUESTION_MARK: + case HqlLexer.COLON: + Token next = hqlLexer.nextToken(); + String text = next.getText(); + switch (tokenType) { + case HqlLexer.COLON: + if (!text.isEmpty() + && isJavaIdentifierStart(text.codePointAt(0))) { + names.add(text); + if (setParameterNames.contains(text)) { + continue; + } + } + else { + continue; + } + break; + case HqlLexer.QUESTION_MARK: + if (next.getType() == HqlLexer.INTEGER_LITERAL) { + int label; + try { + label = parseInt(text); + } + catch (NumberFormatException nfe) { + continue; + } + labels.add(label); + if (setParameterLabels.contains(label)) { + continue; + } + } + else { + continue; + } + break; + default: + continue; + } + parameters = unsetParams == null ? "Parameter " : "Parameters "; + notSet = unsetParams == null ? " is not set" : " are not set"; + unsetParams = unsetParams == null ? "" : unsetParams + ", "; + unsetParams += token.getText() + text; + if (start == -1) { + start = token.getCharPositionInLine(); //TODO: wrong for multiline query strings! + } + end = token.getCharPositionInLine() + text.length() + 1; + break; + } + } + if (unsetParams != null) { + handler.warn(start-errorOffset+1, end-errorOffset, parameters + unsetParams + notSet); + } + + setParameterNames.removeAll(names); + setParameterLabels.removeAll(labels); + + int count = setParameterNames.size() + setParameterLabels.size(); + if (count > 0) { + String missingParams = + concat(setParameterNames.stream().map(name -> ":" + name), + setParameterLabels.stream().map(label -> "?" + label)) + .reduce((x, y) -> x + ", " + y) + .orElse(null); + String params = + count == 1 ? + "Parameter " : + "Parameters "; + String notOccur = + count == 1 ? + " does not occur in the query" : + " do not occur in the query"; + handler.warn(0, 0, params + missingParams + notOccur); + } + } + finally { + setParameterNames.clear(); + setParameterLabels.clear(); + } + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/package-info.java b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/package-info.java new file mode 100644 index 0000000000..590cdbbd69 --- /dev/null +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/jpamodelgen/validation/package-info.java @@ -0,0 +1,12 @@ +/* + * 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 . + */ +/** + * Validation for HQL queries. + * + * @see org.hibernate.jpamodelgen.validation.Validation#validate + */ +package org.hibernate.jpamodelgen.validation;