/* * 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.query.engine.impl; import java.io.IOException; import java.io.Serializable; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.lucene.document.Document; import org.apache.lucene.search.TopDocs; import org.hibernate.search.bridge.spi.ConversionContext; import org.hibernate.search.bridge.util.impl.ContextualExceptionBridgeHelper; import org.hibernate.search.engine.ProjectionConstants; import org.hibernate.search.engine.impl.DocumentBuilderHelper; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity; import org.hibernate.search.engine.spi.EntityIndexBinding; import org.hibernate.search.query.engine.spi.DocumentExtractor; import org.hibernate.search.query.engine.spi.EntityInfo; /** * DocumentExtractor is a traverser over the full-text results (EntityInfo) * * This operation is as lazy as possible: * - the query is executed eagerly * - results are not retrieved until actually requested * * #getFirstIndex and #getMaxIndex define the boundaries available to #extract. * * DocumentExtractor objects *must* be closed when the results are no longer traversed. * #close * * @author Emmanuel Bernard * @author John Griffin * @author Hardy Ferentschik * @author Sanne Grinovero (C) 2011 Red Hat Inc. */ public class DocumentExtractorImpl implements DocumentExtractor { private final ExtendedSearchIntegrator extendedIntegrator; private final String[] projection; private final QueryHits queryHits; private final LazyQueryState searcher; private ReusableDocumentStoredFieldVisitor fieldLoadingVisitor; private boolean allowFieldSelection; private boolean needId; private boolean hasProjectionConstants; private final Map<String, EntityIndexBinding> targetedEntityBindings; private final int firstIndex; private final int maxIndex; private final EntityIndexBinding singleEntityBindingIfPossible; //null when not possible private final ConversionContext exceptionWrap = new ContextualExceptionBridgeHelper(); public DocumentExtractorImpl(QueryHits queryHits, ExtendedSearchIntegrator extendedIntegrator, String[] projection, Set<String> idFieldNames, boolean allowFieldSelection, LazyQueryState searcher, int firstIndex, int maxIndex, Map<String, EntityIndexBinding> targetedEntityBindings) { this.extendedIntegrator = extendedIntegrator; if ( projection != null ) { this.projection = projection.clone(); } else { this.projection = null; } this.queryHits = queryHits; this.allowFieldSelection = allowFieldSelection; if ( targetedEntityBindings.size() == 1 ) { this.singleEntityBindingIfPossible = targetedEntityBindings.values().iterator().next(); this.targetedEntityBindings = null; } else { this.singleEntityBindingIfPossible = null; this.targetedEntityBindings = targetedEntityBindings; } this.searcher = searcher; this.firstIndex = firstIndex; this.maxIndex = maxIndex; initFieldSelection( projection, idFieldNames ); } private void initFieldSelection(String[] projection, Set<String> idFieldNames) { HashSet<String> fields; if ( projection == null ) { // we're going to load hibernate entities needId = true; fields = new HashSet<String>( 2 ); // id + class } else { fields = new HashSet<String>( projection.length + 2 ); // we actually have no clue for ( String projectionName : projection ) { if ( projectionName == null ) { continue; } else if ( ProjectionConstants.THIS.equals( projectionName ) ) { needId = true; hasProjectionConstants = true; } else if ( ProjectionConstants.DOCUMENT.equals( projectionName ) ) { // if we need to project DOCUMENT do not use fieldSelector as the user might want anything allowFieldSelection = false; needId = true; hasProjectionConstants = true; return; } else if ( ProjectionConstants.SCORE.equals( projectionName ) ) { hasProjectionConstants = true; } else if ( ProjectionConstants.ID.equals( projectionName ) ) { needId = true; hasProjectionConstants = true; } else if ( ProjectionConstants.DOCUMENT_ID.equals( projectionName ) ) { hasProjectionConstants = true; } else if ( ProjectionConstants.EXPLANATION.equals( projectionName ) ) { hasProjectionConstants = true; } else if ( ProjectionConstants.OBJECT_CLASS.equals( projectionName ) ) { hasProjectionConstants = true; } else if ( ProjectionConstants.SPATIAL_DISTANCE.equals( projectionName ) ) { hasProjectionConstants = true; } else { fields.add( projectionName ); } } } if ( singleEntityBindingIfPossible == null ) { fields.add( ProjectionConstants.OBJECT_CLASS ); } if ( needId ) { for ( String idFieldName : idFieldNames ) { fields.add( idFieldName ); } } if ( fields.size() != 0 ) { this.fieldLoadingVisitor = new ReusableDocumentStoredFieldVisitor( fields ); } // else: this.fieldSelector = null; //We need no fields at all } private Serializable extractId(DocumentBuilderIndexedEntity documentBuilder, Document document) { if ( !needId ) { return null; } else { return DocumentBuilderHelper.getDocumentId( documentBuilder, document, exceptionWrap ); } } private DocumentBuilderIndexedEntity extractDocumentBuilder(Document document) throws IOException { //maybe we can avoid document extraction: if ( singleEntityBindingIfPossible != null ) { return singleEntityBindingIfPossible.getDocumentBuilder(); } String className = document.get( ProjectionConstants.OBJECT_CLASS ); //and quite likely we can avoid the Reflect helper: EntityIndexBinding entityBinding = targetedEntityBindings.get( className ); if ( entityBinding != null ) { return entityBinding.getDocumentBuilder(); } return DocumentBuilderHelper.getDocumentBuilder( className, extendedIntegrator ); } @Override public EntityInfo extract(int scoreDocIndex) throws IOException { int docId = queryHits.docId( scoreDocIndex ); Document document = extractDocument( scoreDocIndex ); DocumentBuilderIndexedEntity documentBuilder = extractDocumentBuilder( document ); Class<?> clazz = documentBuilder.getBeanClass(); String idName = documentBuilder.getIdPropertyName(); Serializable id = extractId( documentBuilder, document ); Object[] projected = null; if ( projection != null && projection.length > 0 ) { projected = DocumentBuilderHelper.getDocumentFields( documentBuilder, document, projection, exceptionWrap ); if ( hasProjectionConstants ) { for ( int x = 0; x < projection.length; x++ ) { if ( ProjectionConstants.SCORE.equals( projection[x] ) ) { projected[x] = queryHits.score( scoreDocIndex ); } else if ( ProjectionConstants.ID.equals( projection[x] ) ) { projected[x] = id; } else if ( ProjectionConstants.DOCUMENT.equals( projection[x] ) ) { projected[x] = document; } else if ( ProjectionConstants.DOCUMENT_ID.equals( projection[x] ) ) { projected[x] = docId; } else if ( ProjectionConstants.EXPLANATION.equals( projection[x] ) ) { projected[x] = queryHits.explain( scoreDocIndex ); } else if ( ProjectionConstants.OBJECT_CLASS.equals( projection[x] ) ) { projected[x] = clazz; } else if ( ProjectionConstants.SPATIAL_DISTANCE.equals( projection[x] ) ) { projected[x] = queryHits.spatialDistance( scoreDocIndex ); } else if ( ProjectionConstants.THIS.equals( projection[x] ) ) { //THIS could be projected more than once //THIS loading delayed to the Loader phase // Use EntityInfo.ENTITY_PLACEHOLDER as placeholder. // It will be replaced when we populate // the EntityInfo with the real entity. projected[x] = EntityInfo.ENTITY_PLACEHOLDER; } } } } return new EntityInfoImpl( clazz, idName, id, projected ); } @Override public int getFirstIndex() { return firstIndex; } @Override public int getMaxIndex() { return maxIndex; } @Override public void close() { searcher.close(); } private Document extractDocument(int index) throws IOException { if ( allowFieldSelection ) { if ( fieldLoadingVisitor == null ) { //we need no fields return null; } else { queryHits.visitDocument( index, fieldLoadingVisitor ); return fieldLoadingVisitor.getDocumentAndReset(); } } else { return queryHits.doc( index ); } } /** * Required by Infinispan Query. */ @Override public TopDocs getTopDocs() { return queryHits.getTopDocs(); } }