Skip to content

Commit

Permalink
Hibernate Search: Add support for projection constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Jun 22, 2023
1 parent 8e7de38 commit e8d5a93
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ class ClassNames {
static final DotName INDEXED = DotName
.createSimple("org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed");

static final DotName ROOT_MAPPING = DotName
.createSimple("org.hibernate.search.mapper.pojo.mapping.definition.annotation.processing.RootMapping");
static final DotName PROJECTION_CONSTRUCTOR = DotName
.createSimple("org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor");

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.hibernate.search.orm.elasticsearch.deployment;

import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.INDEXED;
import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.PROJECTION_CONSTRUCTOR;
import static io.quarkus.hibernate.search.orm.elasticsearch.deployment.ClassNames.ROOT_MAPPING;
import static io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchElasticsearchRuntimeConfig.backendPropertyKey;
import static io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchElasticsearchRuntimeConfig.defaultBackendPropertyKeys;
import static io.quarkus.hibernate.search.orm.elasticsearch.runtime.HibernateSearchElasticsearchRuntimeConfig.elasticsearchVersionPropertyKey;
Expand Down Expand Up @@ -28,6 +30,7 @@
import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategy;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

Expand All @@ -45,6 +48,7 @@
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.deployment.util.JandexUtil;
import io.quarkus.elasticsearch.restclient.common.deployment.DevservicesElasticsearchBuildItem;
import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem;
import io.quarkus.hibernate.orm.deployment.integration.HibernateOrmIntegrationRuntimeConfiguredBuildItem;
Expand Down Expand Up @@ -174,6 +178,7 @@ void registerBeans(List<HibernateSearchElasticsearchPersistenceUnitConfiguredBui
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setStaticConfig(RecorderContext recorderContext, HibernateSearchElasticsearchRecorder recorder,
CombinedIndexBuildItem combinedIndexBuildItem,
List<HibernateSearchIntegrationStaticConfiguredBuildItem> integrationStaticConfigBuildItems,
List<HibernateSearchElasticsearchPersistenceUnitConfiguredBuildItem> configuredPersistenceUnits,
HibernateSearchElasticsearchBuildTimeConfig buildTimeConfig,
Expand All @@ -182,6 +187,9 @@ void setStaticConfig(RecorderContext recorderContext, HibernateSearchElasticsear
recorderContext.registerSubstitution(ElasticsearchVersion.class,
String.class, ElasticsearchVersionSubstitution.class);

IndexView index = combinedIndexBuildItem.getIndex();
Set<String> rootAnnotationMappedClassNames = collectRootAnnotationMappedClassNames(index);

for (HibernateSearchElasticsearchPersistenceUnitConfiguredBuildItem configuredPersistenceUnit : configuredPersistenceUnits) {
String puName = configuredPersistenceUnit.getPersistenceUnitName();
List<HibernateOrmIntegrationStaticInitListener> integrationStaticInitListeners = new ArrayList<>();
Expand All @@ -203,11 +211,37 @@ void setStaticConfig(RecorderContext recorderContext, HibernateSearchElasticsear
// we cannot pass a config group to a recorder so passing the whole config
recorder.createStaticInitListener(puName, buildTimeConfig,
configuredPersistenceUnit.getBackendAndIndexNamesForSearchExtensions(),
rootAnnotationMappedClassNames,
integrationStaticInitListeners))
.setXmlMappingRequired(xmlMappingRequired));
}
}

private static Set<String> collectRootAnnotationMappedClassNames(IndexView index) {
// Look for classes annotated with annotations meta-annotated with @RootMapping:
// those classes will have their annotations processed on every persistence unit.
// At the moment only @ProjectionConstructor is meta-annotated with @RootMapping.
Set<DotName> rootMappingAnnotationNames = new LinkedHashSet<>();
// This is built into Hibernate Search, which may not be part of the index.
rootMappingAnnotationNames.add(PROJECTION_CONSTRUCTOR);
// Users can theoretically declare their own root mapping annotations
// (replacements for @ProjectionConstructor, for example),
// so we need to consider those as well.
for (AnnotationInstance rootMappingAnnotationInstance : index.getAnnotations(ROOT_MAPPING)) {
rootMappingAnnotationNames.add(rootMappingAnnotationInstance.target().asClass().name());
}

// We'll collect all classes annotated with "root mapping" annotations
// anywhere (type level, constructor, ...)
Set<String> rootAnnotationMappedClassNames = new LinkedHashSet<>();
for (DotName rootMappingAnnotationName : rootMappingAnnotationNames) {
for (AnnotationInstance annotation : index.getAnnotations(rootMappingAnnotationName)) {
rootAnnotationMappedClassNames.add(JandexUtil.getEnclosingClass(annotation).name().toString());
}
}
return rootAnnotationMappedClassNames;
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void setRuntimeConfig(HibernateSearchElasticsearchRecorder recorder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,21 @@ public class HibernateSearchElasticsearchRecorder {
public HibernateOrmIntegrationStaticInitListener createStaticInitListener(
String persistenceUnitName, HibernateSearchElasticsearchBuildTimeConfig buildTimeConfig,
Map<String, Set<String>> backendAndIndexNamesForSearchExtensions,
Set<String> rootAnnotationMappedClassNames,
List<HibernateOrmIntegrationStaticInitListener> integrationStaticInitListeners) {
Set<Class<?>> rootAnnotationMappedClasses = new LinkedHashSet<>();
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
for (String className : rootAnnotationMappedClassNames) {
try {
rootAnnotationMappedClasses.add(Class.forName(className, true, tccl));
} catch (Exception e) {
throw new IllegalStateException("Could not initialize mapped class " + className, e);
}
}
return new HibernateSearchIntegrationStaticInitListener(persistenceUnitName,
buildTimeConfig.getPersistenceUnitConfig(persistenceUnitName),
backendAndIndexNamesForSearchExtensions, integrationStaticInitListeners);
backendAndIndexNamesForSearchExtensions, rootAnnotationMappedClasses,
integrationStaticInitListeners);
}

public HibernateOrmIntegrationStaticInitListener createStaticInitInactiveListener() {
Expand Down Expand Up @@ -173,15 +184,18 @@ private static final class HibernateSearchIntegrationStaticInitListener
private final String persistenceUnitName;
private final HibernateSearchElasticsearchBuildTimeConfigPersistenceUnit buildTimeConfig;
private final Map<String, Set<String>> backendAndIndexNamesForSearchExtensions;
private final Set<Class<?>> rootAnnotationMappedClasses;
private final List<HibernateOrmIntegrationStaticInitListener> integrationStaticInitListeners;

private HibernateSearchIntegrationStaticInitListener(String persistenceUnitName,
HibernateSearchElasticsearchBuildTimeConfigPersistenceUnit buildTimeConfig,
Map<String, Set<String>> backendAndIndexNamesForSearchExtensions,
Set<Class<?>> rootAnnotationMappedClasses,
List<HibernateOrmIntegrationStaticInitListener> integrationStaticInitListeners) {
this.persistenceUnitName = persistenceUnitName;
this.buildTimeConfig = buildTimeConfig;
this.backendAndIndexNamesForSearchExtensions = backendAndIndexNamesForSearchExtensions;
this.rootAnnotationMappedClasses = rootAnnotationMappedClasses;
this.integrationStaticInitListeners = integrationStaticInitListeners;
}

Expand All @@ -195,7 +209,7 @@ public void contributeBootProperties(BiConsumer<String, Object> propertyCollecto

addConfig(propertyCollector,
HibernateOrmMapperSettings.MAPPING_CONFIGURER,
new QuarkusHibernateOrmSearchMappingConfigurer());
new QuarkusHibernateOrmSearchMappingConfigurer(rootAnnotationMappedClasses));

addConfig(propertyCollector,
HibernateOrmMapperSettings.COORDINATION_STRATEGY,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package io.quarkus.hibernate.search.orm.elasticsearch.runtime.mapping;

import java.util.Set;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;

public class QuarkusHibernateOrmSearchMappingConfigurer implements HibernateOrmSearchMappingConfigurer {
private final Set<Class<?>> rootAnnotationMappedClasses;

public QuarkusHibernateOrmSearchMappingConfigurer(Set<Class<?>> rootAnnotationMappedClasses) {
this.rootAnnotationMappedClasses = rootAnnotationMappedClasses;
}

@Override
public void configure(HibernateOrmMappingConfigurationContext context) {
// Jandex is not available at runtime in Quarkus,
// so Hibernate Search cannot perform classpath scanning on startup.
context.annotationMapping()
.discoverJandexIndexesFromAddedTypes(false)
.buildMissingDiscoveredJandexIndexes(false);

// ... but we do better: we perform classpath scanning during the build,
// and propagate the result here.
context.annotationMapping().add(rootAnnotationMappedClasses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.search;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor;

// Ideally we'd use records, but Quarkus still has to compile with JDK 11 at the moment.
public class AddressDTO {
public final String city;

@ProjectionConstructor
public AddressDTO(String city) {
this.city = city;
}

@Override
public String toString() {
return "AddressDTO{" +
"city='" + city + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.search;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;
Expand Down Expand Up @@ -68,6 +69,25 @@ public String testSearch() {
return "OK";
}

@GET
@Path("/search-projection")
@Produces(MediaType.TEXT_PLAIN)
public String testSearchWithProjection() {
SearchSession searchSession = Search.session(entityManager);

assertThat(searchSession.search(Person.class)
.select(PersonDTO.class)
.where(f -> f.match().field("name").matching("john"))
.sort(f -> f.field("name_sort"))
.fetchHits(20))
.usingRecursiveFieldByFieldElementComparator()
.containsExactly(
new PersonDTO(4, "John Grisham", new AddressDTO("Oxford")),
new PersonDTO(1, "John Irving", new AddressDTO("Burlington")));

return "OK";
}

@PUT
@Path("/purge")
@Produces(MediaType.TEXT_PLAIN)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.it.hibernate.search.orm.elasticsearch.search;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IdProjection;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor;

// Ideally we'd use records, but Quarkus still has to compile with JDK 11 at the moment.
public class PersonDTO {
public final long id;
public final String name;
public final AddressDTO address;

@ProjectionConstructor
public PersonDTO(@IdProjection long id, String name, AddressDTO address) {
this.id = id;
this.name = name;
this.address = address;
}

@Override
public String toString() {
return "PersonDTO{" +
"id=" + id +
", name='" + name + '\'' +
", address=" + address +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
public class HibernateSearchTest {

@Test
public void testSearch() throws Exception {
public void testSearch() {
RestAssured.when().put("/test/hibernate-search/init-data").then()
.statusCode(204);

RestAssured.when().get("/test/hibernate-search/search").then()
.statusCode(200)
.body(is("OK"));

RestAssured.when().get("/test/hibernate-search/search-projection").then()
.statusCode(200)
.body(is("OK"));

RestAssured.when().put("/test/hibernate-search/purge").then()
.statusCode(200)
.body(is("OK"));
Expand Down

0 comments on commit e8d5a93

Please sign in to comment.