All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.broadleafcommerce.core.search.service.solr.SolrSearchServiceImpl Maven / Gradle / Ivy

There is a newer version: 3.1.15-GA
Show newest version
/*
 * #%L
 * BroadleafCommerce Framework
 * %%
 * Copyright (C) 2009 - 2013 Broadleaf Commerce
 * %%
 * Licensed 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.
 * #L%
 */
package org.broadleafcommerce.core.search.service.solr;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.Group;
import org.apache.solr.client.solrj.response.GroupCommand;
import org.apache.solr.client.solrj.response.GroupResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.core.CoreContainer;
import org.broadleafcommerce.common.exception.ServiceException;
import org.broadleafcommerce.common.locale.domain.Locale;
import org.broadleafcommerce.common.util.BLCMapUtils;
import org.broadleafcommerce.common.util.TypedClosure;
import org.broadleafcommerce.common.web.BroadleafRequestContext;
import org.broadleafcommerce.core.catalog.dao.ProductDao;
import org.broadleafcommerce.core.catalog.domain.Category;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.search.dao.FieldDao;
import org.broadleafcommerce.core.search.dao.SearchFacetDao;
import org.broadleafcommerce.core.search.domain.CategorySearchFacet;
import org.broadleafcommerce.core.search.domain.Field;
import org.broadleafcommerce.core.search.domain.ProductSearchCriteria;
import org.broadleafcommerce.core.search.domain.ProductSearchResult;
import org.broadleafcommerce.core.search.domain.RequiredFacet;
import org.broadleafcommerce.core.search.domain.SearchFacet;
import org.broadleafcommerce.core.search.domain.SearchFacetDTO;
import org.broadleafcommerce.core.search.domain.SearchFacetRange;
import org.broadleafcommerce.core.search.domain.SearchFacetResultDTO;
import org.broadleafcommerce.core.search.domain.solr.FieldType;
import org.broadleafcommerce.core.search.service.SearchService;
import org.springframework.beans.factory.DisposableBean;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Resource;
import javax.xml.parsers.ParserConfigurationException;

/**
 * An implementation of SearchService that uses Solr.
 * 
 * Note that prior to 2.2.0, this class used to contain all of the logic for interaction with Solr. Since 2.2.0, this class
 * has been refactored and parts of it have been split into the other classes you can find in this package.
 * 
 * @author Andre Azzolini (apazzolini)
 */
public class SolrSearchServiceImpl implements SearchService, DisposableBean {
    private static final Log LOG = LogFactory.getLog(SolrSearchServiceImpl.class);

    @Resource(name = "blProductDao")
    protected ProductDao productDao;

    @Resource(name = "blFieldDao")
    protected FieldDao fieldDao;

    @Resource(name = "blSearchFacetDao")
    protected SearchFacetDao searchFacetDao;

    @Resource(name = "blSolrHelperService")
    protected SolrHelperService shs;

    @Resource(name = "blSolrIndexService")
    protected SolrIndexService solrIndexService;

    @Resource(name = "blSolrSearchServiceExtensionManager")
    protected SolrSearchServiceExtensionManager extensionManager;

    public SolrSearchServiceImpl(String solrServer) throws IOException, ParserConfigurationException, SAXException {
        if ("solrhome".equals(solrServer)) {

            final String baseTempPath = System.getProperty("java.io.tmpdir");

            File tempDir = new File(baseTempPath + File.separator + System.getProperty("user.name") + File.separator + "solrhome");
            if (System.getProperty("tmpdir.solrhome") != null) {
                //allow for an override of tmpdir
                tempDir = new File(System.getProperty("tmpdir.solrhome"));
            }
            if (!tempDir.exists()) {
                tempDir.mkdirs();
            }
            
            solrServer = tempDir.getAbsolutePath();
            
            // create the 'lib' directory with a placeholder file that has to exist in Solr's home directory to avoid a
            // warning log message
            String libDir = FilenameUtils.concat(solrServer, "lib");
            LOG.debug("Creating Solr home lib directory: " + libDir);
            new File(libDir).mkdir();
            
            String placeholder = FilenameUtils.concat(libDir, "solrlib_placeholder.deleteme");
            LOG.debug("Creating Solr lib placeholder file: " + placeholder);
            new File(placeholder).createNewFile();
        }
        
        File solrXml = new File(new File(solrServer), "solr.xml");
        if (!solrXml.exists()) {
            copyConfigToSolrHome(this.getClass().getResourceAsStream("/solr-default.xml"), solrXml);
        }

        LOG.debug(String.format("Using [%s] as solrhome", solrServer));
        LOG.debug(String.format("Using [%s] as solr.xml", solrXml.getAbsoluteFile()));
        
        if (LOG.isTraceEnabled()) {
            LOG.trace("Contents of solr.xml:");
            BufferedReader br = null;
            try {
                br = new BufferedReader(new FileReader(solrXml));
                String line;
                while ((line = br.readLine()) != null) {
                    LOG.trace(line);
                }
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (Throwable e) {
                        //do nothing
                    }
                }
            }
            LOG.trace("Done printing solr.xml");
        }

        CoreContainer coreContainer = CoreContainer.createAndLoad(solrServer, solrXml);
        EmbeddedSolrServer primaryServer = new EmbeddedSolrServer(coreContainer, SolrContext.PRIMARY);
        EmbeddedSolrServer reindexServer = new EmbeddedSolrServer(coreContainer, SolrContext.REINDEX);

        SolrContext.setPrimaryServer(primaryServer);
        SolrContext.setReindexServer(reindexServer);
        //NOTE: There is no reason to set the admin server here as the SolrContext will return the primary server
        //if the admin server is not set...
    }

    public void copyConfigToSolrHome(InputStream configIs, File destFile) throws IOException {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            bis = new BufferedInputStream(configIs);
            bos = new BufferedOutputStream(new FileOutputStream(destFile, false));
            boolean eof = false;
            while (!eof) {
                int temp = bis.read();
                if (temp == -1) {
                    eof = true;
                } else {
                    bos.write(temp);
                }
            }
            bos.flush();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (Throwable e) {
                    //do nothing
                }
            }
            if (bos != null) {
                try {
                    bos.close();
                } catch (Throwable e) {
                    //do nothing
                }
            }
        }
    }

    public SolrSearchServiceImpl(SolrServer solrServer) {
        SolrContext.setPrimaryServer(solrServer);
    }

    /**
     * This constructor serves to mimic the one which takes in one {@link SolrServer} argument.
     * By having this and then simply disregarding the second parameter, we can more easily support 2-core
     * Solr configurations that use embedded/standalone per environment.
     * 
     * @param solrServer
     * @param reindexServer
     * @throws SAXException 
     * @throws ParserConfigurationException 
     * @throws IOException 
     */
    public SolrSearchServiceImpl(String solrServer, String reindexServer)
            throws IOException, ParserConfigurationException, SAXException {
        this(solrServer);
    }

    /**
     * This constructor serves to mimic the one which takes in one {@link SolrServer} argument.
     * By having this and then simply disregarding the second and third parameters, we can more easily support 2-core
     * Solr configurations that use embedded/standalone per environment, along with an admin server.
     * 
     * @param solrServer
     * @param reindexServer
     * @throws SAXException 
     * @throws ParserConfigurationException 
     * @throws IOException 
     */
    public SolrSearchServiceImpl(String solrServer, String reindexServer, String adminServer)
            throws IOException, ParserConfigurationException, SAXException {
        this(solrServer);
    }

    public SolrSearchServiceImpl(SolrServer solrServer, SolrServer reindexServer) {
        SolrContext.setPrimaryServer(solrServer);
        SolrContext.setReindexServer(reindexServer);
    }

    public SolrSearchServiceImpl(SolrServer solrServer, SolrServer reindexServer, SolrServer adminServer) {
        SolrContext.setPrimaryServer(solrServer);
        SolrContext.setReindexServer(reindexServer);
        SolrContext.setAdminServer(adminServer);
    }

    @Override
    public void rebuildIndex() throws ServiceException, IOException {
        solrIndexService.rebuildIndex();
    }

    @Override
    public void destroy() throws Exception {
        if (SolrContext.getServer() instanceof EmbeddedSolrServer) {
            ((EmbeddedSolrServer) SolrContext.getServer()).shutdown();
        }
    }

    @Override
    public ProductSearchResult findExplicitProductsByCategory(Category category, ProductSearchCriteria searchCriteria)
            throws ServiceException {
        List facets = getCategoryFacets(category);
        String query = shs.getExplicitCategoryFieldName() + ":" + shs.getCategoryId(category.getId());
        return findProducts("*:*", facets, searchCriteria, shs.getCategorySortFieldName(category) + " asc", query);
    }

    @Override
    public ProductSearchResult findProductsByCategory(Category category, ProductSearchCriteria searchCriteria)
            throws ServiceException {
        List facets = getCategoryFacets(category);
        String query = shs.getCategoryFieldName() + ":" + shs.getCategoryId(category.getId());
        return findProducts("*:*", facets, searchCriteria, shs.getCategorySortFieldName(category) + " asc", query);
    }

    @Override
    public ProductSearchResult findProductsByQuery(String query, ProductSearchCriteria searchCriteria)
            throws ServiceException {
        List facets = getSearchFacets();
        query = "(" + sanitizeQuery(query) + ")";
        return findProducts(query, facets, searchCriteria, null);
    }

    @Override
    public ProductSearchResult findProductsByCategoryAndQuery(Category category, String query,
            ProductSearchCriteria searchCriteria) throws ServiceException {
        List facets = getSearchFacets();

        String catFq = shs.getCategoryFieldName() + ":" + shs.getCategoryId(category.getId());
        query = "(" + sanitizeQuery(query) + ")";
        
        return findProducts(query, facets, searchCriteria, null, catFq);
    }

    public String getLocalePrefix() {
        if (BroadleafRequestContext.getBroadleafRequestContext() != null) {
            Locale locale = BroadleafRequestContext.getBroadleafRequestContext().getLocale();
            if (locale != null) {
                return locale.getLocaleCode() + "_";
            }
        }
        return "";
    }

    protected String buildQueryFieldsString() {
        StringBuilder queryBuilder = new StringBuilder();
        List fields = fieldDao.readAllProductFields();
        for (Field currentField : fields) {
            if (currentField.getSearchable()) {
                appendFieldToQuery(queryBuilder, currentField);
            }
        }
        return queryBuilder.toString();
    }

    protected void appendFieldToQuery(StringBuilder queryBuilder, Field currentField) {
        List searchableFieldTypes = shs.getSearchableFieldTypes(currentField);
        for (FieldType currentType : searchableFieldTypes) {
            queryBuilder.append(shs.getPropertyNameForFieldSearchable(currentField, currentType)).append(" ");
        }
    }

    /**
     * @deprecated in favor of the other findProducts() method
     */
    @Deprecated
    protected ProductSearchResult findProducts(String qualifiedSolrQuery, List facets,
            ProductSearchCriteria searchCriteria, String defaultSort) throws ServiceException {
        return findProducts(qualifiedSolrQuery, facets, searchCriteria, defaultSort, null);
    }

    /**
     * Given a qualified solr query string (such as "category:2002"), actually performs a solr search. It will
     * take into considering the search criteria to build out facets / pagination / sorting.
     *
     * @param qualifiedSolrQuery
     * @param facets
     * @param searchCriteria
     * @return the ProductSearchResult of the search
     * @throws ServiceException
     */
    protected ProductSearchResult findProducts(String qualifiedSolrQuery, List facets,
            ProductSearchCriteria searchCriteria, String defaultSort, String... filterQueries) throws ServiceException {
        Map namedFacetMap = getNamedFacetMap(facets, searchCriteria);

        // Build the basic query
        // Solr queries with a 'start' parameter cannot be a negative number
        int start = (searchCriteria.getPage() <= 0) ? 0 : (searchCriteria.getPage() - 1);
        SolrQuery solrQuery = new SolrQuery()
                .setQuery(qualifiedSolrQuery)
                .setFields(shs.getProductIdFieldName())
                .setRows(searchCriteria.getPageSize())
                .setStart((start) * searchCriteria.getPageSize());
        if (filterQueries != null) {
            solrQuery.setFilterQueries(filterQueries);
        }
        solrQuery.addFilterQuery(shs.getNamespaceFieldName() + ":(\"" + shs.getCurrentNamespace() + "\")");
        solrQuery.set("defType", "edismax");
        solrQuery.set("qf", buildQueryFieldsString());

        // Attach additional restrictions
        attachSortClause(solrQuery, searchCriteria, defaultSort);
        attachActiveFacetFilters(solrQuery, namedFacetMap, searchCriteria);
        attachFacets(solrQuery, namedFacetMap);
        
        modifySolrQuery(solrQuery, qualifiedSolrQuery, facets, searchCriteria, defaultSort);
        
        extensionManager.getProxy().modifySolrQuery(solrQuery, qualifiedSolrQuery, facets,
                searchCriteria, defaultSort);

        if (LOG.isTraceEnabled()) {
            try {
                LOG.trace(URLDecoder.decode(solrQuery.toString(), "UTF-8"));
            } catch (Exception e) {
                LOG.trace("Couldn't UTF-8 URL Decode: " + solrQuery.toString());
            }
        }

        // Query solr
        QueryResponse response;
        List responseDocuments;
        int numResults = 0;
        try {
            response = SolrContext.getServer().query(solrQuery);
            responseDocuments = getResponseDocuments(response);
            numResults = (int) response.getResults().getNumFound();

            if (LOG.isTraceEnabled()) {
                LOG.trace(response.toString());

                for (SolrDocument doc : responseDocuments) {
                    LOG.trace(doc);
                }
            }
        } catch (SolrServerException e) {
            throw new ServiceException("Could not perform search", e);
        }

        // Get the facets
        setFacetResults(namedFacetMap, response);
        sortFacetResults(namedFacetMap);

        // Get the products
        List products = getProducts(responseDocuments);

        ProductSearchResult result = new ProductSearchResult();
        result.setFacets(facets);
        result.setProducts(products);
        setPagingAttributes(result, numResults, searchCriteria);
        return result;
    }

    /**
     * Provides a hook point for implementations to modify all SolrQueries before they're executed.
     * Modules should leverage the extension manager method of the same name,
     * {@link SolrSearchServiceExtensionHandler#modifySolrQuery(SolrQuery, String, List, ProductSearchCriteria, String)}
     * 
     * @param query
     * @param qualifiedSolrQuery
     * @param facets
     * @param searchCriteria
     * @param defaultSort
     */
    protected void modifySolrQuery(SolrQuery query, String qualifiedSolrQuery,
            List facets, ProductSearchCriteria searchCriteria, String defaultSort) {
    }
    
    protected List getResponseDocuments(QueryResponse response) {
        List docs;
        
        if (response.getGroupResponse() == null) {
            docs = response.getResults();
        } else {
            docs = new ArrayList();
            GroupResponse gr = response.getGroupResponse();
            for (GroupCommand gc : gr.getValues()) {
                for (Group g : gc.getValues()) {
                    for (SolrDocument d : g.getResult()) {
                        docs.add(d);
                    }
                }
            }
        }
        
        return docs;
    }

    @Override
    public List getSearchFacets() {
        return buildSearchFacetDTOs(searchFacetDao.readAllSearchFacets());
    }

    @Override
    public List getCategoryFacets(Category category) {
        List categorySearchFacets = category.getCumulativeSearchFacets();

        List searchFacets = new ArrayList();
        for (CategorySearchFacet categorySearchFacet : categorySearchFacets) {
            searchFacets.add(categorySearchFacet.getSearchFacet());
        }

        return buildSearchFacetDTOs(searchFacets);
    }

    /**
     * Sets up the sorting criteria. This will support sorting by multiple fields at a time
     * 
     * @param query
     * @param searchCriteria
     */
    protected void attachSortClause(SolrQuery query, ProductSearchCriteria searchCriteria, String defaultSort) {
        Map solrFieldKeyMap = getSolrFieldKeyMap(searchCriteria);

        String sortQuery = searchCriteria.getSortQuery();
        if (StringUtils.isBlank(sortQuery)) {
            sortQuery = defaultSort;
        }

        if (StringUtils.isNotBlank(sortQuery)) {
            String[] sortFields = sortQuery.split(",");
            for (String sortField : sortFields) {
                String field = sortField.split(" ")[0];
                if (solrFieldKeyMap.containsKey(field)) {
                    field = solrFieldKeyMap.get(field);
                }
                ORDER order = "desc".equals(sortField.split(" ")[1]) ? ORDER.desc : ORDER.asc;

                if (field != null) {
                    query.addSortField(field, order);
                }
            }
        }
    }

    /**
     * Restricts the query by adding active facet filters.
     * 
     * @param query
     * @param namedFacetMap
     * @param searchCriteria
     */
    protected void attachActiveFacetFilters(SolrQuery query, Map namedFacetMap,
            ProductSearchCriteria searchCriteria) {
        for (Entry entry : searchCriteria.getFilterCriteria().entrySet()) {
            String solrKey = null;
            for (Entry dtoEntry : namedFacetMap.entrySet()) {
                if (dtoEntry.getValue().getFacet().getField().getAbbreviation().equals(entry.getKey())) {
                    solrKey = dtoEntry.getKey();
                    dtoEntry.getValue().setActive(true);
                }
            }

            if (solrKey != null) {
                String solrTag = getSolrFieldTag(shs.getGlobalFacetTagField(), "tag");

                String[] selectedValues = entry.getValue().clone();
                for (int i = 0; i < selectedValues.length; i++) {
                    if (selectedValues[i].contains("range[")) {
                        String rangeValue = selectedValues[i].substring(selectedValues[i].indexOf('[') + 1,
                                selectedValues[i].indexOf(']'));
                        String[] rangeValues = StringUtils.split(rangeValue, ':');
                        if (rangeValues[1].equals("null")) {
                            rangeValues[1] = "*";
                        }
                        selectedValues[i] = solrKey + ":[" + rangeValues[0] + " TO " + rangeValues[1] + "]";
                    } else {
                        selectedValues[i] = solrKey + ":\"" + scrubFacetValue(selectedValues[i]) + "\"";
                    }
                }
                String valueString = StringUtils.join(selectedValues, " OR ");

                StringBuilder sb = new StringBuilder();
                sb.append(solrTag).append("(").append(valueString).append(")");

                query.addFilterQuery(sb.toString());
            }
        }
    }
    
    /**
     * Scrubs a facet value string for all Solr special characters, automatically adding escape characters
     * 
     * @param facetValue The raw facet value
     * @return The facet value with all special characters properly escaped, safe to be used in construction of a Solr query
     */
    protected String scrubFacetValue(String facetValue) {
        String scrubbedFacetValue = facetValue;
        
        String[] specialCharacters = new String[] { "\\\\", "\\+", "-", "&&", "\\|\\|", "\\!", "\\(", "\\)", "\\{", "\\}", "\\[", "\\]", "\\^", "\"", "~", "\\*", "\\?", ":" };
        for(String character : specialCharacters) {
            scrubbedFacetValue = scrubbedFacetValue.replaceAll(character, "\\\\" + character);
        }
        
        return scrubbedFacetValue;
    }

    /**
     * Notifies solr about which facets you want it to determine results and counts for
     * 
     * @param query
     * @param namedFacetMap
     */
    protected void attachFacets(SolrQuery query, Map namedFacetMap) {
        query.setFacet(true);
        for (Entry entry : namedFacetMap.entrySet()) {
            SearchFacetDTO dto = entry.getValue();
            String facetTagField = entry.getValue().isActive() ? shs.getGlobalFacetTagField() : entry.getKey();

            // Clone the list - we don't want to remove these facets from the DB
            List facetRanges = new ArrayList(dto.getFacet().getSearchFacetRanges());

            if (extensionManager != null) {
                extensionManager.getProxy().filterSearchFacetRanges(dto, facetRanges);
            }

            if (facetRanges != null && facetRanges.size() > 0) {
                for (SearchFacetRange range : facetRanges) {
                    query.addFacetQuery(getSolrTaggedFieldString(entry.getKey(), facetTagField, "ex", range));
                }
            } else {
                query.addFacetField(getSolrTaggedFieldString(entry.getKey(), facetTagField, "ex", null));
            }
        }
    }

    /**
     * Builds out the DTOs for facet results from the search. This will then be used by the view layer to
     * display which values are available given the current constraints as well as the count of the values.
     * 
     * @param namedFacetMap
     * @param response
     */
    protected void setFacetResults(Map namedFacetMap, QueryResponse response) {
        if (response.getFacetFields() != null) {
            for (FacetField facet : response.getFacetFields()) {
                String facetFieldName = facet.getName();
                SearchFacetDTO facetDTO = namedFacetMap.get(facetFieldName);

                for (Count value : facet.getValues()) {
                    SearchFacetResultDTO resultDTO = new SearchFacetResultDTO();
                    resultDTO.setFacet(facetDTO.getFacet());
                    resultDTO.setQuantity(new Long(value.getCount()).intValue());
                    resultDTO.setValue(value.getName());
                    facetDTO.getFacetValues().add(resultDTO);
                }
            }
        }

        if (response.getFacetQuery() != null) {
            for (Entry entry : response.getFacetQuery().entrySet()) {
                String key = entry.getKey();
                String facetFieldName = key.substring(key.indexOf("}") + 1, key.indexOf(':'));
                SearchFacetDTO facetDTO = namedFacetMap.get(facetFieldName);

                String minValue = key.substring(key.indexOf("[") + 1, key.indexOf(" TO"));
                String maxValue = key.substring(key.indexOf(" TO ") + 4, key.indexOf("]"));
                if (maxValue.equals("*")) {
                    maxValue = null;
                }

                SearchFacetResultDTO resultDTO = new SearchFacetResultDTO();
                resultDTO.setFacet(facetDTO.getFacet());
                resultDTO.setQuantity(entry.getValue());
                resultDTO.setMinValue(new BigDecimal(minValue));
                resultDTO.setMaxValue(maxValue == null ? null : new BigDecimal(maxValue));

                facetDTO.getFacetValues().add(resultDTO);
            }
        }
    }

    /**
     * Invoked to sort the facet results. This method will use the natural sorting of the value attribute of the
     * facet (or, if value is null, the minValue of the facet result). Override this method to customize facet
     * sorting for your given needs.
     * 
     * @param namedFacetMap
     */
    protected void sortFacetResults(Map namedFacetMap) {
        for (Entry entry : namedFacetMap.entrySet()) {
            Collections.sort(entry.getValue().getFacetValues(), new Comparator() {
                @Override
                public int compare(SearchFacetResultDTO o1, SearchFacetResultDTO o2) {
                    if (o1.getValue() != null && o2.getValue() != null) {
                        return o1.getValue().compareTo(o2.getValue());
                    } else if (o1.getMinValue() != null && o2.getMinValue() != null) {
                        return o1.getMinValue().compareTo(o2.getMinValue());
                    }
                    return 0; // Don't know how to compare
                }
            });
        }
    }

    /**
     * Sets the total results, the current page, and the page size on the ProductSearchResult. Total results comes
     * from solr, while page and page size are duplicates of the searchCriteria conditions for ease of use.
     * 
     * @param result
     * @param response
     * @param searchCriteria
     */
    public void setPagingAttributes(ProductSearchResult result, int numResults, ProductSearchCriteria searchCriteria) {
        result.setTotalResults(numResults);
        result.setPage(searchCriteria.getPage());
        result.setPageSize(searchCriteria.getPageSize());
    }

    /**
     * Given a list of product IDs from solr, this method will look up the IDs via the productDao and build out
     * actual Product instances. It will return a Products that is sorted by the order of the IDs in the passed
     * in list.
     * 
     * @param response
     * @return the actual Product instances as a result of the search
     */
    protected List getProducts(List responseDocuments) {
        final List productIds = new ArrayList();
        for (SolrDocument doc : responseDocuments) {
            productIds.add((Long) doc.getFieldValue(shs.getProductIdFieldName()));
        }

        List products = productDao.readProductsByIds(productIds);

        // We have to sort the products list by the order of the productIds list to maintain sortability in the UI
        if (products != null) {
            Collections.sort(products, new Comparator() {
                @Override
                public int compare(Product o1, Product o2) {
                    Long o1id = shs.getProductId(o1.getId());
                    Long o2id = shs.getProductId(o2.getId());
                    return new Integer(productIds.indexOf(o1id)).compareTo(productIds.indexOf(o2id));
                }
            });
        }

        return products;
    }

    /**
     * Create the wrapper DTO around the SearchFacet
     * 
     * @param searchFacets
     * @return the wrapper DTO
     */
    protected List buildSearchFacetDTOs(List searchFacets) {
        List facets = new ArrayList();
        Map requestParameters = BroadleafRequestContext.getRequestParameterMap();

        for (SearchFacet facet : searchFacets) {
            if (facetIsAvailable(facet, requestParameters)) {
                SearchFacetDTO dto = new SearchFacetDTO();
                dto.setFacet(facet);
                dto.setShowQuantity(true);
                facets.add(dto);
            }
        }

        return facets;
    }

    /**
     * Checks to see if the requiredFacets condition for a given facet is met.
     * 
     * @param facet
     * @param request
     * @return whether or not the facet parameter is available 
     */
    protected boolean facetIsAvailable(SearchFacet facet, Map params) {
        // Facets are available by default if they have no requiredFacets
        if (CollectionUtils.isEmpty(facet.getRequiredFacets())) {
            return true;
        }

        // If we have at least one required facet but no active facets, it's impossible for this facet to be available
        if (MapUtils.isEmpty(params)) {
            return false;
        }

        // We must either match all or just one of the required facets depending on the requiresAllDependentFacets flag
        int requiredMatches = facet.getRequiresAllDependentFacets() ? facet.getRequiredFacets().size() : 1;
        int matchesSoFar = 0;

        for (RequiredFacet requiredFacet : facet.getRequiredFacets()) {
            if (requiredMatches == matchesSoFar) {
                return true;
            }

            // Check to see if the required facet has a value in the current request parameters
            for (Entry entry : params.entrySet()) {
                String key = entry.getKey();
                if (key.equals(requiredFacet.getRequiredFacet().getField().getAbbreviation())) {
                    matchesSoFar++;
                    break;
                }
            }
        }

        return requiredMatches == matchesSoFar;
    }

    /**
     * Perform any necessary query sanitation here. For example, we disallow open and close parentheses, colons, and we also
     * ensure that quotes are actual quotes (") and not the URL encoding (") so that Solr is able to properly handle
     * the user's intent.
     * 
     * @param query
     * @return the sanitized query
     */
    protected String sanitizeQuery(String query) {
        return query.replace("(", "").replace("%28", "")
                .replace(")", "").replace("%29", "")
                .replace(":", "").replace("%3A", "").replace("%3a", "")
                .replace(""", "\""); // Allow quotes in the query for more finely tuned matches
    }

    /**
     * Returns a field string. Given indexField = a and a non-null range, would produce the following String:
     * a:[minVal TO maxVal]
     */
    protected String getSolrFieldString(String indexField, SearchFacetRange range) {
        StringBuilder sb = new StringBuilder();

        sb.append(indexField);

        if (range != null) {
            String minValue = range.getMinValue().toPlainString();
            String maxValue = range.getMaxValue() == null ? "*" : range.getMaxValue().toPlainString();
            sb.append(":[").append(minValue).append(" TO ").append(maxValue).append("]");
        }

        return sb.toString();
    }

    /**
     * Returns a fully composed solr field string. Given indexField = a, tag = ex, and a non-null range,
     * would produce the following String: {!ex=a}a:[minVal TO maxVal]
     */
    protected String getSolrTaggedFieldString(String indexField, String tagField, String tag, SearchFacetRange range) {
        return getSolrFieldTag(tagField, tag) + getSolrFieldString(indexField, range);
    }

    /**
     * Returns a solr field tag. Given indexField = a, tag = ex, would produce the following String:
     * {!ex=a}
     */
    protected String getSolrFieldTag(String tagField, String tag) {
        StringBuilder sb = new StringBuilder();
        if (StringUtils.isNotBlank(tag)) {
            sb.append("{!").append(tag).append("=").append(tagField).append("}");
        }
        return sb.toString();
    }

    /**
     * @param facets
     * @param searchCriteria
     * @return a map of fully qualified solr index field key to the searchFacetDTO object
     */
    protected Map getNamedFacetMap(List facets,
            final ProductSearchCriteria searchCriteria) {
        return BLCMapUtils.keyedMap(facets, new TypedClosure() {

            @Override
            public String getKey(SearchFacetDTO facet) {
                return getSolrFieldKey(facet.getFacet().getField(), searchCriteria);
            }
        });
    }

    /**
     * This method will be used to map a field abbreviation to the appropriate solr index field to use. Typically,
     * this default implementation that maps to the facet field type will be sufficient. However, there may be 
     * cases where you would want to use a different solr index depending on other currently active facets. In that
     * case, you would associate that mapping here. For example, for the "price" abbreviation, we would generally
     * want to use "defaultSku.retailPrice_td". However, if a secondary facet on item condition is selected (such
     * as "refurbished", we may want to index "price" to "refurbishedSku.retailPrice_td". That mapping occurs here.
     * 
     * @param fields
     * @param searchCriteria the searchCriteria in case it is needed to determine the field key
     * @return the solr field index key to use
     */
    protected String getSolrFieldKey(Field field, ProductSearchCriteria searchCriteria) {
        return shs.getPropertyNameForFieldFacet(field);
    }

    /**
     * @param searchCriteria
     * @return a map of abbreviated key to fully qualified solr index field key for all product fields
     */
    protected Map getSolrFieldKeyMap(ProductSearchCriteria searchCriteria) {
        List fields = fieldDao.readAllProductFields();
        Map solrFieldKeyMap = new HashMap();
        for (Field field : fields) {
            solrFieldKeyMap.put(field.getAbbreviation(), getSolrFieldKey(field, searchCriteria));
        }
        return solrFieldKeyMap;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy