/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.core.query.lucene; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import javax.jcr.ItemNotFoundException; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.query.RowIterator; import org.apache.jackrabbit.api.query.JackrabbitQueryResult; import org.apache.jackrabbit.core.session.SessionContext; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Implements the <code>QueryResult</code> interface. */ public abstract class QueryResultImpl implements JackrabbitQueryResult { /** * The logger instance for this class */ private static final Logger log = LoggerFactory.getLogger(QueryResultImpl.class); /** * The search index to execute the query. */ protected final SearchIndex index; /** * Component context of the current session */ protected final SessionContext sessionContext; /** * The query instance which created this query result. */ protected final AbstractQueryImpl queryImpl; /** * The spell suggestion or <code>null</code> if not available. */ protected final SpellSuggestion spellSuggestion; /** * The columns to select. */ protected final Map<String, ColumnImpl> columns = new LinkedHashMap<String, ColumnImpl>(); /** * The result nodes including their score. This list is populated on a lazy * basis while a client iterates through the results. * <p> * The exact type is: <code>List<ScoreNode[]></code> */ private final List<ScoreNode[]> resultNodes = new ArrayList<ScoreNode[]>(); /** * This is the raw number of results that matched the query, ignoring limit and offset. Only set when accurate. */ private int totalResults = -1; /** * This is the number of results that matched the query, with limit and offset. Only set when accurate. */ private int numResults = -1; /** * The selector names associated with the score nodes. The selector names * are set when the query is executed via {@link #getResults(long)}. */ private Name[] selectorNames; /** * The number of results that are invalid, either because a node does not * exist anymore or because the session does not have access to the node. */ private int invalid = 0; /** * If <code>true</code> nodes are returned in document order. */ protected final boolean docOrder; /** * The excerpt provider or <code>null</code> if none was created yet. */ private ExcerptProvider excerptProvider; /** * The offset in the total result set */ private final long offset; /** * The maximum size of this result if limit >= 0 */ private final long limit; private final boolean sizeEstimate; /** * Creates a new query result. The concrete sub class is responsible for * calling {@link #getResults(long)} after this constructor had been called. * * @param index the search index where the query is executed. * @param sessionContext component context of the current session * @param queryImpl the query instance which created this query * result. * @param spellSuggestion the spell suggestion or <code>null</code> if none * is available. * @param columns the select properties of the query. * @param documentOrder if <code>true</code> the result is returned in * document order. * @param limit the maximum result size * @param offset the offset in the total result set * @throws RepositoryException if an error occurs while reading from the * repository. * @throws IllegalArgumentException if any of the columns does not have a * column name. */ public QueryResultImpl( SearchIndex index, SessionContext sessionContext, AbstractQueryImpl queryImpl, SpellSuggestion spellSuggestion, ColumnImpl[] columns, boolean documentOrder, long offset, long limit) throws RepositoryException { this.index = index; this.sizeEstimate = index.getSizeEstimate(); this.sessionContext = sessionContext; this.queryImpl = queryImpl; this.spellSuggestion = spellSuggestion; this.docOrder = documentOrder; this.offset = offset; this.limit = limit; for (ColumnImpl column : columns) { String cn = column.getColumnName(); if (cn == null) { String msg = column + " does not have a column name"; throw new IllegalArgumentException(msg); } this.columns.put(cn, column); } } /** * {@inheritDoc} */ public String[] getSelectorNames() throws RepositoryException { String[] names = new String[selectorNames.length]; for (int i = 0; i < selectorNames.length; i++) { names[i] = sessionContext.getJCRName(selectorNames[i]); } return names; } /** * {@inheritDoc} */ public String[] getColumnNames() throws RepositoryException { return columns.keySet().toArray(new String[columns.size()]); } /** * {@inheritDoc} */ public NodeIterator getNodes() throws RepositoryException { return new NodeIteratorImpl(sessionContext, getScoreNodes(), 0); } /** * {@inheritDoc} */ public RowIterator getRows() throws RepositoryException { if (excerptProvider == null) { try { excerptProvider = createExcerptProvider(); } catch (IOException e) { throw new RepositoryException(e); } } return new RowIteratorImpl( getScoreNodes(), columns, selectorNames, sessionContext.getItemManager(), index.getContext().getHierarchyManager(), sessionContext, sessionContext.getSessionImpl().getValueFactory(), excerptProvider, spellSuggestion); } /** * Executes the query for this result and returns hits. The caller must * close the query hits when he is done using it. * * @param resultFetchHint a hint on how many results should be fetched. * @return hits for this query result. * @throws IOException if an error occurs while executing the query. */ protected abstract MultiColumnQueryHits executeQuery(long resultFetchHint) throws IOException; /** * Creates an excerpt provider for this result set. * * @return an excerpt provider. * @throws IOException if an error occurs. */ protected abstract ExcerptProvider createExcerptProvider() throws IOException; //--------------------------------< internal >------------------------------ /** * Creates a {@link ScoreNodeIterator} over the query result. * * @return a {@link ScoreNodeIterator} over the query result. */ private ScoreNodeIterator getScoreNodes() { if (docOrder) { return new DocOrderScoreNodeIterator( sessionContext.getItemManager(), resultNodes, 0); } else { return new LazyScoreNodeIteratorImpl(); } } /** * Attempts to get <code>size</code> results and puts them into {@link * #resultNodes}. If the size of {@link #resultNodes} is less than * <code>size</code> then there are no more than <code>resultNodes.size()</code> * results for this query. * * @param size the number of results to fetch for the query. * @throws RepositoryException if an error occurs while executing the * query. */ protected void getResults(long size) throws RepositoryException { if (log.isDebugEnabled()) { log.debug("getResults({}) limit={}", size, limit); } if (!sizeEstimate) { // quick check // if numResults is set, all relevant results have been fetched if (numResults != -1) { return; } } long maxResultSize = size; // is there any limit? if (limit >= 0) { maxResultSize = limit; } if (resultNodes.size() >= maxResultSize && selectorNames != null) { // we already have them all return; } // execute it MultiColumnQueryHits result = null; try { long time = System.currentTimeMillis(); long r1 = IOCounters.getReads(); result = executeQuery(maxResultSize); long r2 = IOCounters.getReads(); log.debug("query executed in {} ms ({})", System.currentTimeMillis() - time, r2 - r1); // set selector names selectorNames = result.getSelectorNames(); List<ScoreNode[]> offsetNodes = new ArrayList<ScoreNode[]>(); if (resultNodes.isEmpty() && offset > 0) { // collect result offset into dummy list if (sizeEstimate) { collectScoreNodes(result, new ArrayList<ScoreNode[]>(), offset); } else { collectScoreNodes(result, offsetNodes, offset); } } else { int start = resultNodes.size() + invalid + (int) offset; result.skip(start); } time = System.currentTimeMillis(); collectScoreNodes(result, resultNodes, maxResultSize); long r3 = IOCounters.getReads(); log.debug("retrieved ScoreNodes in {} ms ({})", System.currentTimeMillis() - time, r3 - r2); if (sizeEstimate) { // update numResults numResults = result.getSize(); } else { // update numResults if all results have been fetched // if resultNodes.getSize() is strictly smaller than maxResultSize, it means that all results have been fetched int resultSize = resultNodes.size(); if (resultSize < maxResultSize) { if (resultNodes.isEmpty()) { // if there's no result nodes, the actual totalResults if smaller or equals than the offset totalResults = offsetNodes.size(); numResults = 0; } else { totalResults = resultSize + (int) offset; numResults = resultSize; } } else if (resultSize == limit) { // if there's "limit" results, we can't know the total size (which may be greater), but the result size is the limit numResults = (int) limit; } } } catch (IOException e) { throw new RepositoryException(e); } finally { if (result != null) { try { result.close(); } catch (IOException e) { log.warn("Unable to close query result: " + e); } } } } /** * Collect score nodes from <code>hits</code> into the <code>collector</code> * list until the size of <code>collector</code> reaches <code>maxResults</code> * or there are not more results. * * @param hits the raw hits. * @param collector where the access checked score nodes are collected. * @param maxResults the maximum number of results in the collector. * @throws IOException if an error occurs while reading from hits. * @throws RepositoryException if an error occurs while checking access rights. */ private void collectScoreNodes(MultiColumnQueryHits hits, List<ScoreNode[]> collector, long maxResults) throws IOException, RepositoryException { while (collector.size() < maxResults) { ScoreNode[] sn = hits.nextScoreNodes(); if (sn == null) { // no more results break; } // check access if (isAccessGranted(sn)) { collector.add(sn); } else { invalid++; } } } /** * Checks if access is granted to all <code>nodes</code>. * * @param nodes the nodes to check. * @return <code>true</code> if read access is granted to all * <code>nodes</code>. * @throws RepositoryException if an error occurs while checking access * rights. */ protected boolean isAccessGranted(ScoreNode[] nodes) throws RepositoryException { for (ScoreNode node : nodes) { try { if (node != null && !sessionContext.getAccessManager().canRead( null, node.getNodeId())) { return false; } } catch (ItemNotFoundException e) { // node deleted while query was executed } } return true; } /** * Returns the total number of hits. This is the number of results you * will get get if you don't set any limit or offset. This method may return * <code>-1</code> if the total size is unknown. * <p> * If the "sizeEstimate" options is enabled: * Keep in mind that this number may get smaller if nodes are found in * the result set which the current session has no permission to access. * This might be a security problem. * * @return the total number of hits. */ public int getTotalSize() { if (sizeEstimate) { if (numResults == -1) { return -1; } else { return numResults - invalid; } } else { return totalResults; } } private final class LazyScoreNodeIteratorImpl implements ScoreNodeIterator { private int position = -1; private boolean initialized = false; private ScoreNode[] next; public ScoreNode[] nextScoreNodes() { initialize(); if (next == null) { throw new NoSuchElementException(); } ScoreNode[] sn = next; fetchNext(); return sn; } /** * {@inheritDoc} */ public void skip(long skipNum) { initialize(); if (skipNum < 0) { throw new IllegalArgumentException("skipNum must not be negative"); } if (skipNum == 0) { // do nothing } else { // attempt to get enough results try { getResults(position + invalid + (int) skipNum); if (resultNodes.size() >= position + skipNum) { // skip within already fetched results position += skipNum - 1; fetchNext(); } else { // not enough results after getResults() throw new NoSuchElementException(); } } catch (RepositoryException e) { throw new NoSuchElementException(e.getMessage()); } } } /** * {@inheritDoc} * <p/> * If the "sizeEstimate" options is enabled: * This value may shrink when the query result encounters non-existing * nodes or the session does not have access to a node. */ public long getSize() { if (sizeEstimate) { int total = getTotalSize(); if (total == -1) { return -1; } long size = offset > total ? 0 : total - offset; if (limit >= 0 && size > limit) { return limit; } else { return size; } } else { return numResults; } } /** * {@inheritDoc} */ public long getPosition() { initialize(); return position; } /** * @throws UnsupportedOperationException always. */ public void remove() { throw new UnsupportedOperationException("remove"); } /** * {@inheritDoc} */ public boolean hasNext() { initialize(); return next != null; } /** * {@inheritDoc} */ public Object next() { return nextScoreNodes(); } /** * Initializes this iterator but only if it is not yet initialized. */ private void initialize() { if (!initialized) { fetchNext(); initialized = true; } } /** * Fetches the next node to return by this iterator. If this method * returns and {@link #next} is <code>null</code> then there is no next * node. */ private void fetchNext() { next = null; int nextPos = position + 1; while (next == null) { if (nextPos >= resultNodes.size()) { // quick check if there are more results at all if (sizeEstimate) { // this check is only possible if we have numResults if (numResults != -1 && (nextPos + invalid) >= numResults) { break; } } else { // if numResults is set, all relevant results have been fetched if (numResults != -1) { break; } } // fetch more results try { int num; if (resultNodes.size() == 0) { num = index.getResultFetchSize(); } else { num = resultNodes.size() * 2; } getResults(num); } catch (RepositoryException e) { log.warn("Exception getting more results: " + e); } // check again if (nextPos >= resultNodes.size()) { // no more valid results break; } } next = resultNodes.get(nextPos); } position++; } } }