/*
* 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.test.projection;
import java.util.List;
import java.util.Locale;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.Query;
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.annotations.Store;
import org.hibernate.search.backend.spi.Work;
import org.hibernate.search.backend.spi.WorkType;
import org.hibernate.search.bridge.LuceneOptions;
import org.hibernate.search.bridge.StringBridge;
import org.hibernate.search.bridge.TwoWayFieldBridge;
import org.hibernate.search.bridge.builtin.LongBridge;
import org.hibernate.search.engine.ProjectionConstants;
import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.testsupport.TestForIssue;
import org.hibernate.search.testsupport.concurrency.ConcurrentRunner;
import org.hibernate.search.testsupport.concurrency.ConcurrentRunner.TaskFactory;
import org.hibernate.search.testsupport.junit.ElasticsearchSupportInProgress;
import org.hibernate.search.testsupport.junit.SearchFactoryHolder;
import org.hibernate.search.testsupport.setup.TransactionContextForTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.ExpectedException;
/**
* This test verifies the correct projection rules (like which FieldBridge)
* needs to be applied on each projected field.
* As witnessed by HSEARCH-1786 and HSEARCH-1814, it is possible that when
* having multiple entity types or non trivial entity graphs that the same
* field name has different mapping rules per type, and conflicts are possible
* when the projection handling code is not careful about this.
*
* @author Sanne Grinovero (C) 2015 Red Hat Inc.
*/
@TestForIssue(jiraKey = "HSEARCH-1786")
public class ProjectionConversionTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public SearchFactoryHolder sfHolder = new SearchFactoryHolder( ExampleEntity.class );
@Before
public void storeTestData() {
ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory();
ExampleEntity entity = new ExampleEntity();
entity.id = 1l;
entity.someInteger = 5;
entity.longEncodedAsText = 20l;
entity.unstoredField = "unstoredField";
entity.customBridgedKeyword = "lowercase-keyword";
entity.customOneWayBridgedKeyword = "lowercase-keyword";
ExampleEntity embedded = new ExampleEntity();
embedded.id = 2l;
embedded.someInteger = 6;
embedded.longEncodedAsText = 21l;
embedded.unstoredField = "unstoredFieldEmbedded";
embedded.customBridgedKeyword = "another-lowercase-keyword";
embedded.customOneWayBridgedKeyword = "another-lowercase-keyword";
ConflictingMappedType second = new ConflictingMappedType();
second.id = "a string";
second.customBridgedKeyword = 17l;
entity.embedded = embedded;
entity.second = second;
Work work = new Work( entity, entity.id, WorkType.ADD, false );
TransactionContextForTest tc = new TransactionContextForTest();
searchFactory.getWorker().performWork( work, tc );
tc.end();
}
@Test
public void projectingExplicitId() {
projectionTestHelper( ProjectionConstants.ID, Long.valueOf( 1l ) );
}
@Test
public void projectingIdByPropertyName() {
projectionTestHelper( "id", Long.valueOf( 1l ) );
}
@Test
public void projectingIdOnOverloadedMapping() {
projectionTestHelper( "stringTypedId", Long.valueOf( 1l ) );
}
@Test
public void projectingIntegerField() {
projectionTestHelper( "someInteger", Integer.valueOf( 5 ) );
}
@Test
public void projectingUnknownField() {
projectionTestHelper( "someNonExistingField", null );
}
@Test
@Category(ElasticsearchSupportInProgress.class) // HSEARCH-2423 Projecting an unstored field should raise an exception
public void projectingUnstoredField() {
thrown.expect( SearchException.class );
thrown.expectMessage( "HSEARCH000323" );
thrown.expectMessage( "unstoredField" );
projectionTestHelper( "unstoredField", null );
}
@Test
public void projectionWithCustomBridge() {
projectionTestHelper( "customBridgedKeyword", "lowercase-keyword" );
}
@Test
public void projectionWithCustomOneWayBridge() {
thrown.expect( SearchException.class );
thrown.expectMessage( "HSEARCH000324" );
thrown.expectMessage( "customOneWayBridgedKeyword" );
thrown.expectMessage( CustomOneWayBridge.class.getName() );
projectionTestHelper( "customOneWayBridgedKeyword", "lowercase-keyword" );
}
@Test
public void projectingEmbeddedIdByPropertyName() {
projectionTestHelper( "embedded.id", Long.valueOf( 2l ) );
}
@Test
public void projectingEmbeddedIdOnOverloadedMapping() {
projectionTestHelper( "embedded.stringTypedId", Long.valueOf( 2l ) );
}
@Test
public void projectingEmbeddedWithCustomBridge() {
projectionTestHelper( "embedded.customBridgedKeyword", "another-lowercase-keyword" );
}
@Test
public void projectingEmbeddedWithCustomOneWayBridge() {
thrown.expect( SearchException.class );
thrown.expectMessage( "HSEARCH000324" );
thrown.expectMessage( "embedded.customOneWayBridgedKeyword" );
thrown.expectMessage( CustomOneWayBridge.class.getName() );
projectionTestHelper( "embedded.customOneWayBridgedKeyword", "another-lowercase-keyword" );
}
@Test
public void projectingNotIncludedEmbeddedField() {
projectionTestHelper( "embedded.someInteger", null );
}
@Test
public void projectingOnConflictingMappingEmbeddedField() {
projectionTestHelper( "second.customBridgedKeyword", Long.valueOf( 17l ) );
}
@Test
public void projectingOnConflictingMappedIdField() {
projectionTestHelper( "second.id", "a string" );
}
@Test
public void concurrentMixedProjections() throws Exception {
//The point of this test is to "simultaneously" project multiple different types
new ConcurrentRunner( 1000, 20,
new TaskFactory() {
@Override
public Runnable createRunnable(int i) throws Exception {
return new Runnable() {
@Override
public void run() {
projectingExplicitId();
projectingIdOnOverloadedMapping();
projectingIntegerField();
projectingUnknownField();
projectionWithCustomBridge();
projectingEmbeddedIdByPropertyName();
projectingEmbeddedIdOnOverloadedMapping();
projectingEmbeddedWithCustomBridge();
projectingOnConflictingMappingEmbeddedField();
projectingOnConflictingMappedIdField();
}
};
}
}
).execute();
}
void projectionTestHelper(String projectionField, Object expectedValue) {
ExtendedSearchIntegrator searchFactory = sfHolder.getSearchFactory();
QueryBuilder queryBuilder = searchFactory.buildQueryBuilder().forEntity( ExampleEntity.class ).get();
Query queryAllGuests = queryBuilder.all().createQuery();
List<EntityInfo> queryEntityInfos = searchFactory.createHSQuery( queryAllGuests, ExampleEntity.class )
.projection( projectionField )
.queryEntityInfos();
Assert.assertEquals( 1, queryEntityInfos.size() );
EntityInfo entityInfo = queryEntityInfos.get( 0 );
Object projectedValue = entityInfo.getProjection()[0];
Assert.assertEquals( expectedValue, projectedValue );
}
@Indexed
public static class ExampleEntity {
@DocumentId @Field(name = "stringTypedId", store = Store.YES)
Long id;
@Field(store = Store.YES)
Integer someInteger;
@Field(store = Store.YES) @FieldBridge(impl = LongBridge.class)
Long longEncodedAsText;
@Field(store = Store.NO)
String unstoredField;
@Field(store = Store.YES) @FieldBridge(impl = CustomTwoWayBridge.class)
String customBridgedKeyword;
@Field(store = Store.YES) @FieldBridge(impl = CustomOneWayBridge.class)
String customOneWayBridgedKeyword;
@IndexedEmbedded(includePaths = { "id", "stringTypedId", "customBridgedKeyword", "customOneWayBridgedKeyword" },
includeEmbeddedObjectId = true)
ExampleEntity embedded;
@IndexedEmbedded(includeEmbeddedObjectId = true)
ConflictingMappedType second;
}
@Indexed
public static class ConflictingMappedType {
@DocumentId
String id;
@Field(store = Store.YES)
Long customBridgedKeyword; //Misleading field name on purpose
}
public static class CustomTwoWayBridge implements TwoWayFieldBridge {
@Override
public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
luceneOptions.addFieldToDocument( name, String.valueOf( value ).toUpperCase( Locale.ENGLISH ), document );
}
@Override
public Object get(String name, Document document) {
IndexableField field = document.getField( name );
String stringValue = field.stringValue();
return stringValue.toLowerCase( Locale.ENGLISH );
}
@Override
public String objectToString(Object object) {
return String.valueOf( object );
}
}
public static class CustomOneWayBridge implements org.hibernate.search.bridge.FieldBridge, StringBridge {
@Override
public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
luceneOptions.addFieldToDocument( name, String.valueOf( value ).toUpperCase( Locale.ENGLISH ), document );
}
@Override
public String objectToString(Object object) {
return String.valueOf( object );
}
}
}