/* * 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.exoplatform.services.jcr.impl.core.query.lucene; import org.exoplatform.services.jcr.access.AccessManager; import org.exoplatform.services.jcr.access.PermissionType; import org.exoplatform.services.jcr.datamodel.InternalQName; import org.exoplatform.services.jcr.datamodel.NodeData; import org.exoplatform.services.jcr.datamodel.QPath; import org.exoplatform.services.jcr.impl.core.SessionDataManager; import org.exoplatform.services.jcr.impl.core.SessionImpl; import org.exoplatform.services.security.IdentityConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import javax.jcr.ItemNotFoundException; import javax.jcr.NamespaceException; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.query.QueryResult; import javax.jcr.query.RowIterator; /** * Implements the <code>QueryResult</code> interface. */ public abstract class QueryResultImpl implements QueryResult { /** * The logger instance for this class */ private static final Logger LOG = LoggerFactory.getLogger("exo.jcr.component.core.QueryResultImpl"); /** * The search index to execute the query. */ protected final SearchIndex index; /** * The item manager of the session executing the query */ protected final SessionDataManager itemMgr; /** * The session executing the query */ protected final SessionImpl session; /** * The access manager of the session that executes the query. */ protected final AccessManager accessMgr; /** * 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 select properties */ protected final InternalQName[] selectProps; /** * The relative paths of properties to use for ordering the result set. */ protected final QPath[] orderProps; /** * The order specifier for each of the order properties. */ protected final boolean[] orderSpecs; /** * The result nodes including their score. This list is populated on a lazy * basis while a client iterates through the results. * <br> * 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. This number * also includes matches which will not be returned due to access * restrictions. This value is set whenever hits are obtained. */ 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 InternalQName[] 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; /** * If <code>true</code>, it means we're using a System session. */ private final boolean isSystemSession; /** * 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 itemMgr the item manager of the session executing the * query. * @param session the session executing the query. * @param accessMgr the access manager of the session executiong the * query. * @param queryImpl the query instance which created this query * result. * @param spellSuggestion the spell suggestion or <code>null</code> if none * is available. * @param selectProps the select properties of the query. * @param orderProps the relative paths of the order properties. * @param orderSpecs the order specs, one for each order property * name. * @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. */ public QueryResultImpl(SearchIndex index, SessionDataManager itemMgr, SessionImpl session, AccessManager accessMgr, AbstractQueryImpl queryImpl, SpellSuggestion spellSuggestion, InternalQName[] selectProps, QPath[] orderProps, boolean[] orderSpecs, boolean documentOrder, long offset, long limit) throws RepositoryException { this.index = index; this.itemMgr = itemMgr; this.session = session; this.accessMgr = accessMgr; this.queryImpl = queryImpl; this.spellSuggestion = spellSuggestion; this.selectProps = selectProps; this.orderProps = orderProps; this.orderSpecs = orderSpecs; this.docOrder = orderProps.length == 0 && documentOrder; this.offset = offset; this.limit = limit; this.isSystemSession = IdentityConstants.SYSTEM.equals(session.getUserID()); } /** * {@inheritDoc} */ public String[] getColumnNames() throws RepositoryException { try { String[] propNames = new String[selectProps.length]; for (int i = 0; i < selectProps.length; i++) { propNames[i] = session.getLocationFactory().createJCRName(selectProps[i]).getAsString(); } return propNames; } catch (NamespaceException npde) { String msg = "encountered invalid property name"; LOG.debug(msg); throw new RepositoryException(msg, npde); } } /** * {@inheritDoc} */ public NodeIterator getNodes() throws RepositoryException { return new NodeIteratorImpl(itemMgr, 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(), selectProps, selectorNames, itemMgr, session.getLocationFactory(), excerptProvider, spellSuggestion, index.getContext().getCleanerHolder()); } /** * 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. * @throws RepositoryException */ protected abstract MultiColumnQueryHits executeQuery(long resultFetchHint) throws IOException, RepositoryException; /** * 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(itemMgr, 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={}", new Long(size), new Long(limit)); } long maxResultSize = size; // is there any limit? if (limit > 0) { maxResultSize = limit; } if (resultNodes.size() >= maxResultSize) { // we already have them all return; } // execute it MultiColumnQueryHits result = null; try { long time = 0; if (LOG.isDebugEnabled()) { time = System.currentTimeMillis(); } result = executeQuery(maxResultSize); if (LOG.isDebugEnabled()) { LOG.debug("query executed in {} ms", new Long(System.currentTimeMillis() - time)); } // set selector names selectorNames = result.getSelectorNames(); if (resultNodes.isEmpty() && offset > 0) { // collect result offset into dummy list collectScoreNodes(result, new ArrayList<ScoreNode[]>(), offset, true); } else { int start = resultNodes.size() + invalid + (int)offset; result.skip(start); } if (LOG.isDebugEnabled()) { time = System.currentTimeMillis(); } collectScoreNodes(result, resultNodes, maxResultSize,false); if (LOG.isDebugEnabled()) { LOG.debug("retrieved ScoreNodes in {} ms", new Long(System.currentTimeMillis() - time)); } // update numResults numResults = result.getSize(); } catch (IndexOfflineIOException e) { throw new IndexOfflineRepositoryException(e.getMessage(), e); } catch (IOException e) { LOG.error("Exception while executing query: ", e); // todo throw? } 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. * @param isOffset if true the access is checked for the offset result. * @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, boolean isOffset) throws IOException, RepositoryException { while (collector.size() < maxResults) { ScoreNode[] sn = hits.nextScoreNodes(); if (sn == null) { // no more results break; } // check access if ((!docOrder && !isOffset ) || 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. */ private boolean isAccessGranted(ScoreNode[] nodes) throws RepositoryException { if (isSystemSession) { return true; } for (int i = 0; i < nodes.length; i++) { try { //if (nodes[i] != null && !accessMgr.isGranted(nodes[i].getNodeId(), PermissionType.READ)) { if (nodes[i] != null) { NodeData nodeData = (NodeData)itemMgr.getItemData(nodes[i].getNodeId()); if (nodeData == null || !accessMgr.hasPermission(nodeData.getACL(), PermissionType.READ, session.getUserState() .getIdentity())) { return false; } } } catch (ItemNotFoundException e) { if (LOG.isTraceEnabled()) { LOG.trace("An exception occurred: " + e.getMessage()); } } } 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. 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 method may return * <code>-1</code> if the total size is unknown. * * @return the total number of hits. */ public int getTotalSize() { if (numResults == -1) { return -1; } else { return numResults - invalid; } } 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 long expectedPosition = position + skipNum; while (position < expectedPosition) { fetchNext(); if (next == null) { // not enough results after getResults() throw new NoSuchElementException(); } } } } /** * * @see org.exoplatform.services.jcr.impl.core.query.lucene.TwoWayRangeIterator#skipBack(long) */ public void skipBack(long skipNum) { initialize(); if (skipNum < 0) { throw new IllegalArgumentException("skipNum must not be negative"); } if ((position - skipNum) < 0) { throw new NoSuchElementException(); } if (skipNum == 0) { // do nothing } else { position -= skipNum + 1; fetchNext(); } } /** * {@inheritDoc} * <br> * 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() { int total = getTotalSize(); if (total == -1) { return -1; } long size = total - offset; if (limit > 0 && size > limit) { return limit; } else { return size; } } /** * {@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 // this check is only possible if we have numResults if (numResults != -1 && (nextPos + invalid) >= numResults) { 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 = (ScoreNode[])resultNodes.get(nextPos); try { if (!isAccessGranted(next)) { next = null; invalid++; resultNodes.remove(nextPos); if (LOG.isDebugEnabled()) { LOG.debug("The node is invalid since we don't have sufficient rights to access it, " + "it will be removed from the results set"); } } } catch (RepositoryException e) { LOG.error("Could not check access permission", e); break; } } position++; } } }