/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.elasticsearch.test; import static org.hibernate.search.test.util.impl.ExceptionMatcherBuilder.isException; import java.util.HashMap; import java.util.Map; import javax.persistence.Embeddable; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import org.apache.lucene.document.Document; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.IndexedEmbedded; import org.hibernate.search.bridge.LuceneOptions; import org.hibernate.search.bridge.MetadataProvidingFieldBridge; import org.hibernate.search.bridge.spi.FieldMetadataBuilder; import org.hibernate.search.bridge.spi.FieldType; import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment; import org.hibernate.search.elasticsearch.cfg.IndexSchemaManagementStrategy; import org.hibernate.search.elasticsearch.testutil.TestElasticsearchClient; import org.hibernate.search.exception.SearchException; import org.hibernate.search.test.SearchInitializationTestBase; import org.hibernate.search.test.util.ImmutableTestConfiguration; import org.hibernate.testing.TestForIssue; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; /** * Tests for conflict detection when generating the Elasticsearch schema. * * @author Yoann Rodiere */ @RunWith(Parameterized.class) public class ElasticsearchSchemaNamingErrorsIT extends SearchInitializationTestBase { private static final String COMPOSITE_CONCRETE_CONFLICT_MESSAGE_ID = "HSEARCH400036"; private static final String INDEXED_EMBEDDED_BYPASS_MESSAGE_ID = "HSEARCH400054"; private static final String CONFLICTING_FIELD_NAME = "conflictingFieldName"; private static final String BYPASSING_FIELD_NAME = "fieldNameThatBypassesIndexedEmbeddedPrefix"; private static final String BYPASSING_FIELD_INDEXED_EMBEDDED_PREFIX = "indexedEmbeddedPrefix."; private static final String BYPASSING_FIELD_DEFAULT_FIELD_NAME = "defaultFieldName"; @Parameters(name = "With strategy {0}") public static IndexSchemaManagementStrategy[] strategies() { return IndexSchemaManagementStrategy.values(); } @Rule public ExpectedException thrown = ExpectedException.none(); @Rule public TestElasticsearchClient elasticSearchClient = new TestElasticsearchClient(); private IndexSchemaManagementStrategy strategy; public ElasticsearchSchemaNamingErrorsIT(IndexSchemaManagementStrategy strategy) { super(); this.strategy = strategy; } @Test @TestForIssue(jiraKey = "HSEARCH-2448") public void detectConflict_schemaGeneration_compositeOnConcrete() throws Exception { testDetectConflictDuringSchemaGeneration( CompositeOnConcreteEntity.class ); } @Test @TestForIssue(jiraKey = "HSEARCH-2448") public void detectConflict_schemaGeneration_concreteOnComposite() throws Exception { testDetectConflictDuringSchemaGeneration( ConcreteOnCompositeEntity.class ); } private void testDetectConflictDuringSchemaGeneration(Class<?> entityClass) { Assume.assumeTrue( "The strategy " + strategy + " does not involve schema generation." + " No point running this test.", generatesSchema( strategy ) ); thrown.expect( isException( SearchException.class ) .withMessage( COMPOSITE_CONCRETE_CONFLICT_MESSAGE_ID ) .withMessage( CONFLICTING_FIELD_NAME + "'" ) .withMessage( entityClass.getName() ) .build() ); elasticSearchClient.index( entityClass ).registerForCleanup(); init( strategy, entityClass ); } @Test @TestForIssue(jiraKey = "HSEARCH-2448") public void detectConflict_indexing_compositeOnConcrete() throws Exception { testDetectConflictDuringIndexing( CompositeOnConcreteEntity.class ); } @Test @TestForIssue(jiraKey = "HSEARCH-2448") public void detectConflict_indexing_concreteOnComposite() throws Exception { testDetectConflictDuringIndexing( ConcreteOnCompositeEntity.class ); } private void testDetectConflictDuringIndexing(Class<?> entityClass) throws Exception { Assume.assumeFalse( "The strategy " + strategy + " involves schema generation," + " which means conflicts prevent search factory initialization" + " and thus prevent indexing. No point running this test.", generatesSchema( strategy ) ); elasticSearchClient.index( entityClass ).deleteAndCreate(); init( strategy, entityClass ); thrown.expect( isException( AssertionFailure.class ) .causedBy( HibernateException.class ) .causedBy( SearchException.class ) .withMessage( COMPOSITE_CONCRETE_CONFLICT_MESSAGE_ID ) .withMessage( CONFLICTING_FIELD_NAME + "'" ) .withMessage( entityClass.getName() ) .build() ); Object newEntity = entityClass.newInstance(); try ( Session session = getTestResourceManager().openSession() ) { Transaction tx = session.beginTransaction(); session.save( newEntity ); tx.commit(); } } @Test @TestForIssue(jiraKey = "HSEARCH-2488") public void detectIndexedEmbeddedPrefixBypass_schemaGeneration() { Assume.assumeTrue( "The strategy " + strategy + " does not involve schema generation." + " No point running this test.", generatesSchema( strategy ) ); Class<?> entityClass = BypassingIndexedEmbeddedPrefixEntity.class; thrown.expect( isException( SearchException.class ) .withMessage( INDEXED_EMBEDDED_BYPASS_MESSAGE_ID ) .withMessage( "'" + BYPASSING_FIELD_NAME + "'" ) .withMessage( "'" + BYPASSING_FIELD_INDEXED_EMBEDDED_PREFIX + "'" ) .withMessage( entityClass.getName() ) .build() ); elasticSearchClient.index( entityClass ).registerForCleanup(); init( strategy, entityClass ); } @Test @TestForIssue(jiraKey = "HSEARCH-2488") public void detectIndexedEmbeddedPrefixBypass_indexing() throws Exception { Assume.assumeFalse( "The strategy " + strategy + " involves schema generation," + " which means @IndexedEmbedded.prefix bypasses prevent search factory initialization" + " and thus prevent indexing. No point running this test.", generatesSchema( strategy ) ); Class<?> entityClass = BypassingIndexedEmbeddedPrefixEntity.class; elasticSearchClient.index( entityClass ).deleteAndCreate(); init( strategy, entityClass ); thrown.expect( isException( AssertionFailure.class ) .causedBy( HibernateException.class ) .causedBy( SearchException.class ) .withMessage( INDEXED_EMBEDDED_BYPASS_MESSAGE_ID ) .withMessage( "'" + BYPASSING_FIELD_NAME + "'" ) .withMessage( "'" + BYPASSING_FIELD_INDEXED_EMBEDDED_PREFIX + "'" ) .withMessage( entityClass.getName() ) .build() ); Object newEntity = entityClass.newInstance(); try ( Session session = getTestResourceManager().openSession() ) { Transaction tx = session.beginTransaction(); session.save( newEntity ); tx.commit(); } } private boolean generatesSchema(IndexSchemaManagementStrategy strategy) { return !IndexSchemaManagementStrategy.NONE.equals( strategy ); } private void init(IndexSchemaManagementStrategy strategy, Class<?> ... entityClasses) { Map<String, Object> settings = new HashMap<>(); settings.put( "hibernate.search.default." + ElasticsearchEnvironment.INDEX_SCHEMA_MANAGEMENT_STRATEGY, strategy.getExternalName() ); init( new ImmutableTestConfiguration( settings, entityClasses ) ); } @Embeddable static class EmbeddedTypeWithNonConflictingField { @Field int nonConflictingField = 0; } @Embeddable static class EmbeddedTypeWithConflictingField { @Field(name = CONFLICTING_FIELD_NAME) int conflictingField = 0; } /** * A class for which Hibernate Search will first handle the mapping for the "concrete" * (non-composite) field, and then the mapping for the composite field. */ @Entity @Indexed static class CompositeOnConcreteEntity { @DocumentId @Id @GeneratedValue Long id; @IndexedEmbedded(prefix = CONFLICTING_FIELD_NAME + ".") @Field(name = CONFLICTING_FIELD_NAME, bridge = @FieldBridge(impl = SimpleToStringBridge.class)) @Embedded EmbeddedTypeWithNonConflictingField embedded = new EmbeddedTypeWithNonConflictingField(); public static class SimpleToStringBridge implements MetadataProvidingFieldBridge { @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { luceneOptions.addFieldToDocument( name, String.valueOf( value ), document ); } @Override public void configureFieldMetadata(String name, FieldMetadataBuilder builder) { builder.field( name, FieldType.STRING ); } } } /** * A class for which Hibernate Search will first handle the mapping for the composite * field, and then the mapping for the "concrete" (non-composite) field. */ @Entity @Indexed @Table(name = "ConcreteOnCompositeEntity") static class ConcreteOnCompositeEntity { @DocumentId @Id @GeneratedValue Long id; @Embedded EmbeddedTypeWithNonConflictingField embedded = new EmbeddedTypeWithNonConflictingField(); // Hack: methods are handled first, so take advantage of that. @IndexedEmbedded(prefix = CONFLICTING_FIELD_NAME + ".") public EmbeddedTypeWithNonConflictingField getEmbedded() { return embedded; } @IndexedEmbedded(prefix = "") @Embedded EmbeddedTypeWithConflictingField otherEmbedded = new EmbeddedTypeWithConflictingField(); } @Indexed @Entity @Table(name = "BypassingIndexedEmbeddedPrefixEntity") static class BypassingIndexedEmbeddedPrefixEntity { @Id @GeneratedValue private Integer id; @IndexedEmbedded(prefix = BYPASSING_FIELD_INDEXED_EMBEDDED_PREFIX) private BypassingIndexedEmbeddedPrefixEmbedded embedded = new BypassingIndexedEmbeddedPrefixEmbedded(); } @Embeddable static class BypassingIndexedEmbeddedPrefixEmbedded { @Field(name = BYPASSING_FIELD_DEFAULT_FIELD_NAME, bridge = @FieldBridge(impl = BypassingIndexedEmbeddedPrefixFieldBridge.class)) private String field = "fieldValue"; } public static class BypassingIndexedEmbeddedPrefixFieldBridge implements MetadataProvidingFieldBridge { @Override public void configureFieldMetadata(String name, FieldMetadataBuilder builder) { builder.field( BYPASSING_FIELD_NAME, FieldType.STRING ); } @Override public void set(String name, Object value, Document document, LuceneOptions luceneOptions) { luceneOptions.addFieldToDocument( BYPASSING_FIELD_NAME, (String) value, document ); } } }