/* * Hibernate, Relational Persistence for Idiomatic Java * * JBoss, Home of Professional Open Source * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public License, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package org.hibernate.search.test.filters; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import junit.framework.Assert; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.index.TermDocs; import org.apache.lucene.index.TermEnum; import org.apache.lucene.search.DocIdSet; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; import org.apache.lucene.util.OpenBitSet; import org.apache.lucene.util.ReaderUtil; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.engine.spi.SearchFactoryImplementor; import org.hibernate.search.query.dsl.QueryBuilder; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.test.programmaticmapping.TestingSearchFactoryHolder; import org.hibernate.search.test.util.ManualTransactionContext; import org.hibernate.search.test.util.TestForIssue; import org.junit.Rule; import org.junit.Test; /** * Verified filters don't get stale IndexReader instances after a change is applied. * Note that filters operate on per-segment subreaders, while we usually expose * top level (recursive) global IndexReader views: this usually should not affect * their usage but is relevant to how we test them. * * @author Sanne Grinovero <sanne@hibernate.org> (C) 2013 Red Hat Inc. * @since 4.2 */ @TestForIssue(jiraKey = "HSEARCH-1230") public class FreshReadersProvidedTest { @Rule public TestingSearchFactoryHolder sfHolder = new TestingSearchFactoryHolder( Guest.class ); @Test public void filtersTest() { SearchFactoryImplementor searchFactory = sfHolder.getSearchFactory(); Assert.assertNotNull( searchFactory.getAllIndexesManager().getIndexManager( "guests" ) ); {// Store guest "Thorin Oakenshield" in the index Guest lastDwarf = new Guest(); lastDwarf.id = 13l; lastDwarf.name = "Thorin Oakenshield"; Work work = new Work( lastDwarf, lastDwarf.id, WorkType.ADD, false ); ManualTransactionContext tc = new ManualTransactionContext(); searchFactory.getWorker().performWork( work, tc ); tc.end(); } QueryBuilder guestQueryBuilder = searchFactory.buildQueryBuilder() .forEntity( Guest.class ) .get(); Query queryAllGuests = guestQueryBuilder.all().createQuery(); List<EntityInfo> queryEntityInfos = searchFactory.createHSQuery() .luceneQuery( queryAllGuests ) .targetedEntities( Arrays.asList( new Class<?>[]{ Guest.class } ) ) .queryEntityInfos(); Assert.assertEquals( 1, queryEntityInfos.size() ); Assert.assertEquals( 13L, queryEntityInfos.get( 0 ).getId() ); RecordingFilter filter = new RecordingFilter(); List<EntityInfo> filteredQueryEntityInfos = searchFactory.createHSQuery() .luceneQuery( queryAllGuests ) .targetedEntities( Arrays.asList( new Class<?>[]{ Guest.class } ) ) .filter( filter ) .queryEntityInfos(); checkFilterInspectedAllSegments( filter ); expectedTermsForFilter( filter, Guest.class.getName(), "thorin", "13", "oakenshield" ); Assert.assertEquals( 1, filteredQueryEntityInfos.size() ); Assert.assertEquals( 13L, filteredQueryEntityInfos.get( 0 ).getId() ); {// Store guest "Balin" Guest balin = new Guest(); balin.id = 7l; balin.name = "Balin"; Work work = new Work( balin, balin.id, WorkType.ADD, false ); ManualTransactionContext tc = new ManualTransactionContext(); searchFactory.getWorker().performWork( work, tc ); tc.end(); } List<EntityInfo> queryEntityInfosAgain = searchFactory.createHSQuery() .luceneQuery( queryAllGuests ) .targetedEntities( Arrays.asList( new Class<?>[]{ Guest.class } ) ) .queryEntityInfos(); Assert.assertEquals( 2, queryEntityInfosAgain.size() ); Assert.assertEquals( 13L, queryEntityInfosAgain.get( 0 ).getId() ); Assert.assertEquals( 7L, queryEntityInfosAgain.get( 1 ).getId() ); RecordingFilter secondFilter = new RecordingFilter(); List<EntityInfo> secondFilteredQueryEntityInfos = searchFactory.createHSQuery() .luceneQuery( queryAllGuests ) .targetedEntities( Arrays.asList( new Class<?>[]{ Guest.class } ) ) .filter( secondFilter ) .queryEntityInfos(); checkFilterInspectedAllSegments( secondFilter ); expectedTermsForFilter( secondFilter, Guest.class.getName(), "thorin", "13", "oakenshield", Guest.class.getName(), "balin", "7" ); Assert.assertEquals( 2, secondFilteredQueryEntityInfos.size() ); Assert.assertEquals( 13L, secondFilteredQueryEntityInfos.get( 0 ).getId() ); Assert.assertEquals( 7L, secondFilteredQueryEntityInfos.get( 1 ).getId() ); } private void expectedTermsForFilter(RecordingFilter filter, String... term) { Assert.assertEquals( term.length, filter.seenTerms.size() ); Assert.assertTrue( filter.seenTerms.containsAll( Arrays.asList( term ) ) ); } /** * Verifies that the current RecordingFilter has been fed all the same sub-readers * which would be obtained from a freshly checked out IndexReader. * * @param filter */ private void checkFilterInspectedAllSegments(RecordingFilter filter) { SearchFactoryImplementor searchFactory = sfHolder.getSearchFactory(); IndexReader currentIndexReader = searchFactory.getIndexReaderAccessor().open( Guest.class ); try { List<IndexReader> allSubReaders = new ArrayList<IndexReader>(); ReaderUtil.gatherSubReaders( allSubReaders, currentIndexReader ); for ( IndexReader ir : allSubReaders ) { Assert.assertTrue( filter.visitedReaders.contains( ir ) ); } for ( IndexReader ir : filter.visitedReaders ) { Assert.assertTrue( allSubReaders.contains( ir ) ); } } finally { searchFactory.getIndexReaderAccessor().close( currentIndexReader ); } } /** * Filters are invoked once for each segment, so they are invoked multiple times * once for each segment, each time being passed a different IndexReader. * These IndexReader instances are "subreaders", not the global kind representing * the whole index. */ private static class RecordingFilter extends Filter { List<IndexReader> visitedReaders = new ArrayList<IndexReader>(); List<String> seenTerms = new ArrayList<String>(); @Override public DocIdSet getDocIdSet(IndexReader reader) throws IOException { this.visitedReaders.add( reader ); OpenBitSet bitSet = new OpenBitSet( reader.maxDoc() ); TermEnum terms = reader.terms(); terms.next(); Term firstTerm = terms.term(); TermDocs termDocs = reader.termDocs(); if ( firstTerm != null ) { seenTerms.add( firstTerm.text() ); termDocs.seek( firstTerm ); while ( termDocs.next() ) { bitSet.set( termDocs.doc() ); } while ( terms.next() ) { seenTerms.add( terms.term().text() ); } } return bitSet; } } @Indexed(index = "guests") public static final class Guest { private long id; private String name; @DocumentId public long getId() { return id; } public void setId(long id) { this.id = id; } @Field public String getName() { return name; } public void setName(String name) { this.name = name; } } }