org.xwiki.query.solr.internal.SolrQueryExecutor.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.query.solr.internal.SolrQueryExecutor.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.query.solr.internal;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;

import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.slf4j.Logger;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryExecutor;
import org.xwiki.search.solr.internal.api.FieldUtils;
import org.xwiki.search.solr.internal.api.SolrInstance;

/**
 * Executes Solr queries.
 * <p/>
 * For now, the result is the direct {@link QueryResponse}, in lack of a more expressive result type than the generic
 * List that the {@link #execute(Query)} method allows.
 * 
 * @version $Id: 1f48d96de7b4eddbbac61ff1740a443e7cfac6ee $
 * @since 4.3M2
 */
@Component
@Named(SolrQueryExecutor.SOLR)
public class SolrQueryExecutor implements QueryExecutor {
    /**
     * Query language ID.
     */
    public static final String SOLR = "solr";

    /**
     * Logging framework.
     */
    @Inject
    protected Logger logger;

    /**
     * XWiki model bridge.
     */
    @Inject
    protected DocumentAccessBridge documentAccessBridge;

    /**
     * Used to retrieve user preference regarding hidden documents.
     */
    @Inject
    @Named("user")
    protected ConfigurationSource userPreferencesSource;

    /**
     * Provider for the {@link SolrInstance} that allows communication with the Solr server.
     */
    @Inject
    protected Provider<SolrInstance> solrInstanceProvider;

    @Override
    public <T> List<T> execute(Query query) throws QueryException {
        try {
            SolrInstance solrInstance = solrInstanceProvider.get();

            SolrQuery solrQuery = new SolrQuery(query.getStatement());

            // Overwrite offset and limit only if the query object explicitly says so, otherwise use whatever the query
            // statement says or the defaults
            if (query.getOffset() > 0) {
                solrQuery.setStart(query.getOffset());
            }
            if (query.getLimit() > 0) {
                solrQuery.setRows(query.getLimit());
            }

            // TODO: good idea? Any confusion? Do we really needs something like this?
            // Reuse the Query.getNamedParameters() map to get extra parameters.
            for (Entry<String, Object> entry : query.getNamedParameters().entrySet()) {
                Object value = entry.getValue();

                if (value instanceof Iterable) {
                    solrQuery.set(entry.getKey(), toStringArray((Iterable) value));
                } else if (value != null && value.getClass().isArray()) {
                    solrQuery.set(entry.getKey(), toStringArray(value));
                } else {
                    solrQuery.set(entry.getKey(), String.valueOf(value));
                }
            }

            QueryResponse response = solrInstance.query(solrQuery);

            // Check access rights need to be checked before returning the response.
            // FIXME: this is not really the best way, mostly because at this point all grouping operations
            // have already been performed and any change on the result will not ensure that the grouping
            // information (facets, highlighting, maxScore, etc.) is still relevant.
            // A better way would be using a PostFilter as described in this article:
            // http://java.dzone.com/articles/custom-security-filtering-solr
            // Basically, we would be asking
            this.filterResponse(response);

            return (List<T>) Arrays.asList(response);
        } catch (Exception e) {
            throw new QueryException("Exception while executing query", query, e);
        }
    }

    /**
     * Converts an arbitrary array to an array containing its string representations.
     * 
     * @param array an array of arbitrary type, must not be null
     * @return an array with the string representations of the passed array's items
     */
    private String[] toStringArray(Object array) {
        int length = Array.getLength(array);
        String[] args = new String[length];
        for (int i = 0; i < length; i++) {
            args[i] = String.valueOf(Array.get(array, i));
        }

        return args;
    }

    /**
     * Converts the given iterable object to an array containing its string representations.
     * 
     * @param iterable the iterable object, must not be null
     * @return an array with the string representations of the passed iterable's items
     */
    private String[] toStringArray(Iterable iterable) {
        List<String> args = new ArrayList<String>();
        for (Object obj : iterable) {
            args.add(String.valueOf(obj));
        }
        return args.toArray(new String[args.size()]);
    }

    /**
     * Filter out results from the response that the current user does not have access to view.
     * 
     * @param response the Solr response to filter
     */
    protected void filterResponse(QueryResponse response) {
        SolrDocumentList results = response.getResults();
        long numFound = results.getNumFound();

        // Since we are modifying the results collection, we need to iterate over its copy.
        for (SolrDocument result : new ArrayList<SolrDocument>(results)) {
            try {
                DocumentReference resultDocumentReference = new DocumentReference(
                        (String) result.get(FieldUtils.WIKI), (String) result.get(FieldUtils.SPACE),
                        (String) result.get(FieldUtils.NAME));

                if (!documentAccessBridge.exists(resultDocumentReference)
                        || !documentAccessBridge.isDocumentViewable(resultDocumentReference)) {

                    // Remove the current incompatible result.
                    results.remove(result);

                    // Decrement the number of results.
                    numFound--;

                    // FIXME: We should update maxScore as well when removing the top scored item. How do we do that?
                    // Sorting based on score might be a not so expensive option.

                    // FIXME: What about highlighting, facets and all the other data inside the QueryResponse?
                }
            } catch (Exception e) {
                this.logger.warn("Skipping bad result: {}", result, e);
            }
        }

        // Update the new number of results, excluding the filtered ones.
        if (numFound < 0) {
            // Lower bound guard for the total number of results.
            numFound = 0;
        }
        results.setNumFound(numFound);
    }
}