/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2010-2011, Red Hat, Inc. and/or its affiliates or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat, Inc. * * 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, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY 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 * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.hibernate.search.query.hibernate.impl; import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Blob; import java.sql.Clob; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.HibernateException; import org.hibernate.ScrollableResults; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.search.SearchException; import org.hibernate.search.query.engine.spi.DocumentExtractor; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.hibernate.type.Type; /** * Implements scrollable and paginated resultsets. * Contrary to Query#iterate() or Query#list(), this implementation is * exposed to returned null objects (if the index is out of date). * <p/> * <p/> * The following methods that change the value of 'current' will check * and set its value to either 'afterLast' or 'beforeFirst' depending * on direction. This is to prevent rogue values from setting it outside * the boundaries of the results. * <ul> * <li>next()</li> * <li>previous()</li> * <li>scroll(i)</li> * <li>last()</li> * <li>first()</li> * </ul> * * @see org.hibernate.Query * * @author Emmanuel Bernard * @author John Griffin * @author Sanne Grinovero */ public class ScrollableResultsImpl implements ScrollableResults { private static final Log log = LoggerFactory.make(); private final int first; private final int max; private final int fetchSize; private final Loader loader; private final DocumentExtractor documentExtractor; private final SessionImplementor session; /** * Caches result rows and EntityInfo from * <code>first</code> to <code>max</code> */ private final LoadedObject[] resultsContext; private int current; public ScrollableResultsImpl(int fetchSize, DocumentExtractor extractor, Loader loader, SessionImplementor sessionImplementor ) { this.loader = loader; this.documentExtractor = extractor; this.fetchSize = fetchSize; this.session = sessionImplementor; this.first = extractor.getFirstIndex(); this.max = extractor.getMaxIndex(); int size = Math.max( max - first + 1, 0 ); this.resultsContext = new LoadedObject[size]; beforeFirst(); } private LoadedObject ensureCurrentLoaded() { LoadedObject currentCacheRef = resultsContext[current - first]; if ( currentCacheRef != null ) { return currentCacheRef; } // the loading window is optimized for scrolling in both directions: int windowStop = Math.min( max + 1 , current + fetchSize ); int windowStart = Math.max( first, current - fetchSize + 1 ); List<EntityInfo> entityInfosToLoad = new ArrayList<EntityInfo>( fetchSize ); int sizeToLoad = 0; for (int x = windowStart; x < windowStop; x++) { int arrayIdx = x - first; LoadedObject lo = resultsContext[arrayIdx]; if ( lo == null ) { lo = new LoadedObject(); // makes hard references and extract EntityInfos: entityInfosToLoad.add( lo.getEntityInfo( x ) ); resultsContext[arrayIdx] = lo; sizeToLoad++; if ( sizeToLoad >= fetchSize ) break; } } //preload efficiently by batches: if ( sizeToLoad > 1 ) { loader.load( entityInfosToLoad.toArray( new EntityInfo[sizeToLoad] ) ); //(no references stored at this point: they still need to be loaded one by one to inject null results) } return resultsContext[ current - first ]; } /** * {@inheritDoc} */ public boolean next() { // Increases cursor pointer by one. If this places it > // max + 1 (afterLast) then set it to afterLast and return // false. if ( ++current > max ) { afterLast(); return false; } return true; } public boolean previous() { // Decreases cursor pointer by one. If this places it < // first - 1 (beforeFirst) then set it to beforeFirst and // return false. if ( --current < first ) { beforeFirst(); return false; } return true; } public boolean scroll(int i) { // Since we have to take into account that we can scroll any // amount positive or negative, we perform the same tests that // we performed in next() and previous(). current = current + i; if ( current > max ) { afterLast(); return false; } else if ( current < first ) { beforeFirst(); return false; } else { return true; } } public boolean last() { current = max; if ( current < first ) { beforeFirst(); return false; } return max >= first; } public boolean first() { current = first; if ( current > max ) { afterLast(); return false; } return max >= first; } public void beforeFirst() { current = first - 1; } public void afterLast() { current = max + 1; //TODO help gc by clearing all structures when using forwardonly scrollmode. } public boolean isFirst() { return current == first; } public boolean isLast() { return current == max; } public void close() { try { documentExtractor.close(); } catch (SearchException e) { log.unableToCloseSearcherInScrollableResult( e ); } } public Object[] get() throws HibernateException { // don't throw an exception here just // return 'null' this is similar to the // RowSet spec in JDBC. It returns false // (or 0 I can't remember) but we can't // do that since we have to make up for // an Object[]. J.G if ( current < first || current > max ) return null; LoadedObject cacheEntry = ensureCurrentLoaded(); return cacheEntry.getManagedResult( current ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Object get(int i) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Type getType(int i) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Integer getInteger(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Long getLong(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Float getFloat(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Boolean getBoolean(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Double getDouble(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Short getShort(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Byte getByte(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Character getCharacter(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public byte[] getBinary(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public String getText(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Blob getBlob(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Clob getClob(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public String getString(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public BigDecimal getBigDecimal(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public BigInteger getBigInteger(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Date getDate(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Locale getLocale(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public Calendar getCalendar(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } /** * This method is not supported on Lucene based queries * @throws UnsupportedOperationException always thrown */ public TimeZone getTimeZone(int col) { throw new UnsupportedOperationException( "Lucene does not work on columns" ); } public int getRowNumber() { if ( max < first ) return -1; return current - first; } public boolean setRowNumber(int rowNumber) { if ( rowNumber >= 0 ) { current = first + rowNumber; } else { current = max + rowNumber + 1; //max row start at -1 } return current >= first && current <= max; } private final class LoadedObject { private Reference<Object[]> entity; //never==null but Reference.get can return null private Reference<EntityInfo> einfo; //never==null but Reference.get can return null /** * Gets the objects from cache if it is available and attached to session, * or reload them and update the cache entry. * @param x absolute position in fulltext result. * @return the managed objects */ private Object[] getManagedResult(int x) { EntityInfo entityInfo = getEntityInfo( x ); Object[] objects = entity==null ? null : entity.get(); if ( objects!=null && areAllEntitiesManaged( objects, entityInfo ) ) { return objects; } else { Object loaded = loader.load( entityInfo ); if ( ! loaded.getClass().isArray() ) loaded = new Object[] { loaded }; objects = (Object[]) loaded; this.entity = new SoftReference<Object[]>( objects ); return objects; } } /** * Extract an entityInfo, either from cache or from the index. * @param x the position in the index. * @return */ private EntityInfo getEntityInfo(int x) { EntityInfo entityInfo = einfo==null ? null : einfo.get(); if ( entityInfo==null ) { try { entityInfo = documentExtractor.extract( x ); } catch (IOException e) { throw new SearchException( "Unable to read Lucene topDocs[" + x + "]", e ); } einfo = new SoftReference<EntityInfo>( entityInfo ); } return entityInfo; } } private boolean areAllEntitiesManaged(Object[] objects, EntityInfo entityInfo) { //check if all entities are session-managed and skip the check on projected values org.hibernate.Session hibSession = (org.hibernate.Session) session; if ( entityInfo.getProjection() != null ) { // using projection: test only for entities for ( int idx : entityInfo.getIndexesOfThis() ) { Object o = objects[idx]; //TODO improve: is it useful to check for proxies and have them reassociated to persistence context? if ( ! hibSession.contains( o ) ) return false; } return true; } else { return hibSession.contains( objects[0] ); } } }