/* * Hibernate, Relational Persistence for Idiomatic Java * * JBoss, Home of Professional Open Source * Copyright 2011 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.query.engine.impl; import java.io.IOException; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.apache.lucene.document.Document; import org.apache.lucene.document.FieldSelector; import org.apache.lucene.document.FieldSelectorResult; import org.apache.lucene.document.MapFieldSelector; import org.apache.lucene.search.TopDocs; import org.hibernate.search.ProjectionConstants; import org.hibernate.search.bridge.spi.ConversionContext; import org.hibernate.search.bridge.util.impl.ContextualExceptionBridgeHelper; import org.hibernate.search.engine.impl.DocumentBuilderHelper; import org.hibernate.search.engine.spi.SearchFactoryImplementor; import org.hibernate.search.query.engine.spi.DocumentExtractor; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.query.collector.impl.FieldCacheCollector; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.hibernate.search.util.logging.impl.Log; /** * 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 <sanne@hibernate.org> (C) 2011 Red Hat Inc. */ public class DocumentExtractorImpl implements DocumentExtractor { private static final Log log = LoggerFactory.make(); private final SearchFactoryImplementor searchFactoryImplementor; private final String[] projection; private final QueryHits queryHits; private final IndexSearcherWithPayload searcher; private FieldSelector fieldSelector; private boolean allowFieldSelection; private boolean needId; private final Map<String, Class> targetedClasses; private int firstIndex; private int maxIndex; private Object query; private final Class singleClassIfPossible; //null when not possible private final FieldCacheCollector classTypeCollector; //null when not used private final FieldCacheCollector idsCollector; //null when not used private final ConversionContext exceptionWrap = new ContextualExceptionBridgeHelper(); public DocumentExtractorImpl(QueryHits queryHits, SearchFactoryImplementor searchFactoryImplementor, String[] projection, Set<String> idFieldNames, boolean allowFieldSelection, IndexSearcherWithPayload searcher, Object query, int firstIndex, int maxIndex, Set<Class<?>> classesAndSubclasses) { this.searchFactoryImplementor = searchFactoryImplementor; if ( projection != null ) { this.projection = projection.clone(); } else { this.projection = null; } this.queryHits = queryHits; this.allowFieldSelection = allowFieldSelection; this.targetedClasses = new HashMap<String, Class>( classesAndSubclasses.size() ); for ( Class<?> clazz : classesAndSubclasses ) { //useful to reload classes from index without using reflection targetedClasses.put( clazz.getName(), clazz ); } if ( classesAndSubclasses.size() == 1 ) { singleClassIfPossible = classesAndSubclasses.iterator().next(); } else { singleClassIfPossible = null; } this.searcher = searcher; this.query = query; this.firstIndex = firstIndex; this.maxIndex = maxIndex; this.classTypeCollector = queryHits.getClassTypeCollector(); this.idsCollector = queryHits.getIdsCollector(); initFieldSelection( projection, idFieldNames ); } private void initFieldSelection(String[] projection, Set<String> idFieldNames) { Map<String, FieldSelectorResult> fields; if ( projection == null ) { // we're going to load hibernate entities needId = true; fields = new HashMap<String, FieldSelectorResult>( 2 ); // id + class } else { fields = new HashMap<String, FieldSelectorResult>( projection.length + 2 ); // we actually have no clue for ( String projectionName : projection ) { if ( projectionName == null ) { continue; } else if ( ProjectionConstants.THIS.equals( projectionName ) ) { needId = 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; return; } else if ( ProjectionConstants.SCORE.equals( projectionName ) ) { continue; } else if ( ProjectionConstants.ID.equals( projectionName ) ) { needId = true; } else if ( ProjectionConstants.DOCUMENT_ID.equals( projectionName ) ) { continue; } else if ( ProjectionConstants.EXPLANATION.equals( projectionName ) ) { continue; } else if ( ProjectionConstants.OBJECT_CLASS.equals( projectionName ) ) { continue; } else if ( ProjectionConstants.SPATIAL_DISTANCE.equals( projectionName ) ) { continue; } else { fields.put( projectionName, FieldSelectorResult.LOAD ); } } } if ( singleClassIfPossible == null && classTypeCollector == null ) { fields.put( ProjectionConstants.OBJECT_CLASS, FieldSelectorResult.LOAD ); } if ( needId && idsCollector == null ) { for ( String idFieldName : idFieldNames ) { fields.put( idFieldName, FieldSelectorResult.LOAD ); } } if ( fields.size() == 1 ) { // surprised: from unit tests it seems this case is possible quite often // so apply an additional optimization using LOAD_AND_BREAK instead: String key = fields.keySet().iterator().next(); fields.put( key, FieldSelectorResult.LOAD_AND_BREAK ); } if ( fields.size() != 0 ) { this.fieldSelector = new MapFieldSelector( fields ); } // else: this.fieldSelector = null; //We need no fields at all } private EntityInfo extractEntityInfo(int docId, Document document, int scoreDocIndex, ConversionContext exceptionWrap) throws IOException { Class clazz = extractClass( docId, document, scoreDocIndex ); String idName = DocumentBuilderHelper.getDocumentIdName( searchFactoryImplementor, clazz ); Serializable id = extractId( docId, document, clazz ); Object[] projected = null; if ( projection != null && projection.length > 0 ) { projected = DocumentBuilderHelper.getDocumentFields( searchFactoryImplementor, clazz, document, projection, exceptionWrap ); } return new EntityInfoImpl( clazz, idName, id, projected ); } private Serializable extractId(int docId, Document document, Class clazz) { if ( !needId ) { return null; } else if ( this.idsCollector != null ) { return (Serializable) this.idsCollector.getValue( docId ); } else { return DocumentBuilderHelper.getDocumentId( searchFactoryImplementor, clazz, document, exceptionWrap ); } } private Class extractClass(int docId, Document document, int scoreDocIndex) throws IOException { //maybe we can avoid document extraction: if ( singleClassIfPossible != null ) { return singleClassIfPossible; } String className; if ( classTypeCollector != null ) { className = (String) classTypeCollector.getValue( docId ); if ( className == null ) { log.forceToUseDocumentExtraction(); className = forceClassNameExtraction( scoreDocIndex ); } } else { className = document.get( ProjectionConstants.OBJECT_CLASS ); } //and quite likely we can avoid the Reflect helper: Class clazz = targetedClasses.get( className ); if ( clazz != null ) { return clazz; } else { return DocumentBuilderHelper.getDocumentClass( className ); } } public EntityInfo extract(int scoreDocIndex) throws IOException { int docId = queryHits.docId( scoreDocIndex ); Document document = extractDocument( scoreDocIndex ); EntityInfo entityInfo = extractEntityInfo( docId, document, scoreDocIndex, exceptionWrap ); Object[] eip = entityInfo.getProjection(); if ( eip != null && eip.length > 0 ) { for ( int x = 0; x < projection.length; x++ ) { if ( ProjectionConstants.SCORE.equals( projection[x] ) ) { eip[x] = queryHits.score( scoreDocIndex ); } else if ( ProjectionConstants.ID.equals( projection[x] ) ) { eip[x] = entityInfo.getId(); } else if ( ProjectionConstants.DOCUMENT.equals( projection[x] ) ) { eip[x] = document; } else if ( ProjectionConstants.DOCUMENT_ID.equals( projection[x] ) ) { eip[x] = docId; } else if ( ProjectionConstants.EXPLANATION.equals( projection[x] ) ) { eip[x] = queryHits.explain( scoreDocIndex ); } else if ( ProjectionConstants.OBJECT_CLASS.equals( projection[x] ) ) { eip[x] = entityInfo.getClazz(); } else if ( ProjectionConstants.SPATIAL_DISTANCE.equals( projection[x] ) ) { eip[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 entityInfo.getIndexesOfThis().add( x ); } } } return entityInfo; } public int getFirstIndex() { return firstIndex; } public int getMaxIndex() { return maxIndex; } public void close() { searcher.closeSearcher( query, searchFactoryImplementor ); } private Document extractDocument(int index) throws IOException { if ( allowFieldSelection ) { if ( fieldSelector == null ) { //we need no fields return null; } else { return queryHits.doc( index, fieldSelector ); } } else { return queryHits.doc( index ); } } /** * In rare cases the Lucene FieldCache might fail to return a value, at this point we already extracted * the Document so we need to repeat the process to extract the missing field only. * @param scoreDocIndex * @return * @throws IOException */ private String forceClassNameExtraction(int scoreDocIndex) throws IOException { Map<String, FieldSelectorResult> fields = new HashMap<String, FieldSelectorResult>( 1 ); fields.put( ProjectionConstants.OBJECT_CLASS, FieldSelectorResult.LOAD_AND_BREAK ); MapFieldSelector classOnly = new MapFieldSelector( fields ); Document doc = queryHits.doc( scoreDocIndex, classOnly ); return doc.get( ProjectionConstants.OBJECT_CLASS ); } @Override public TopDocs getTopDocs() { return queryHits.getTopDocs(); } }