org.broadleafcommerce.core.search.service.solr.SolrIndexServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.broadleafcommerce.core.search.service.solr.SolrIndexServiceImpl.java

Source

/*
 * #%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.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrInputDocument;
import org.broadleafcommerce.common.exception.ExceptionHelper;
import org.broadleafcommerce.common.exception.ServiceException;
import org.broadleafcommerce.common.extension.ExtensionResultStatusType;
import org.broadleafcommerce.common.locale.domain.Locale;
import org.broadleafcommerce.common.locale.service.LocaleService;
import org.broadleafcommerce.common.sandbox.SandBoxHelper;
import org.broadleafcommerce.common.util.BLCCollectionUtils;
import org.broadleafcommerce.common.util.StopWatch;
import org.broadleafcommerce.common.util.TransactionUtils;
import org.broadleafcommerce.common.util.TypedTransformer;
import org.broadleafcommerce.common.web.BroadleafRequestContext;
import org.broadleafcommerce.core.catalog.dao.ProductDao;
import org.broadleafcommerce.core.catalog.dao.SkuDao;
import org.broadleafcommerce.core.catalog.domain.Product;
import org.broadleafcommerce.core.catalog.domain.ProductBundle;
import org.broadleafcommerce.core.catalog.domain.Sku;
import org.broadleafcommerce.core.catalog.service.dynamic.DynamicSkuActiveDatesService;
import org.broadleafcommerce.core.catalog.service.dynamic.DynamicSkuPricingService;
import org.broadleafcommerce.core.catalog.service.dynamic.SkuActiveDateConsiderationContext;
import org.broadleafcommerce.core.catalog.service.dynamic.SkuPricingConsiderationContext;
import org.broadleafcommerce.core.search.dao.CatalogStructure;
import org.broadleafcommerce.core.search.dao.FieldDao;
import org.broadleafcommerce.core.search.dao.SolrIndexDao;
import org.broadleafcommerce.core.search.domain.Field;
import org.broadleafcommerce.core.search.domain.solr.FieldType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Resource;

/**
 * Responsible for building and rebuilding the Solr index
 * 
 * @author Andre Azzolini (apazzolini)
 * @author Jeff Fischer
 */
@Service("blSolrIndexService")
public class SolrIndexServiceImpl implements SolrIndexService {

    private static final Log LOG = LogFactory.getLog(SolrIndexServiceImpl.class);

    protected final Object LOCK_OBJECT = new Object();

    protected boolean IS_LOCKED = false;

    @Value("${solr.index.errorOnConcurrentReIndex}")
    protected boolean errorOnConcurrentReIndex = false;

    @Value("${solr.index.product.pageSize}")
    protected int pageSize;

    @Value("${solr.index.use.sku}")
    protected boolean useSku;

    @Value("${solr.index.commit}")
    protected boolean commit;

    @Value("${solr.index.softCommit}")
    protected boolean softCommit;

    @Value("${solr.index.waitSearcher}")
    protected boolean waitSearcher;

    @Value("${solr.index.waitFlush}")
    protected boolean waitFlush;

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

    @Resource(name = "blSkuDao")
    protected SkuDao skuDao;

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

    @Resource(name = "blLocaleService")
    protected LocaleService localeService;

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

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

    @Resource(name = "blTransactionManager")
    protected PlatformTransactionManager transactionManager;

    @Resource(name = "blSolrIndexDao")
    protected SolrIndexDao solrIndexDao;

    @Resource(name = "blSandBoxHelper")
    protected SandBoxHelper sandBoxHelper;

    @Override
    public void performCachedOperation(SolrIndexCachedOperation.CacheOperation cacheOperation)
            throws ServiceException {
        try {
            CatalogStructure cache = new CatalogStructure();
            SolrIndexCachedOperation.setCache(cache);
            cacheOperation.execute();
        } finally {
            if (LOG.isInfoEnabled()) {
                LOG.info("Cleaning up Solr index cache from memory - size approx: "
                        + getCacheSizeInMemoryApproximation(SolrIndexCachedOperation.getCache()) + " bytes");
            }
            SolrIndexCachedOperation.clearCache();
        }
    }

    protected int getCacheSizeInMemoryApproximation(CatalogStructure structure) {
        try {
            if (structure == null) {
                return 0;
            }
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(structure);
            IOUtils.closeQuietly(oos);
            int size = baos.size();
            IOUtils.closeQuietly(baos);
            return size;
        } catch (IOException e) {
            throw ExceptionHelper.refineException(e);
        }
    }

    @Override
    public boolean isReindexInProcess() {
        synchronized (LOCK_OBJECT) {
            return IS_LOCKED;
        }
    }

    @Override
    public void rebuildIndex() throws ServiceException, IOException {
        synchronized (LOCK_OBJECT) {
            if (IS_LOCKED) {
                if (errorOnConcurrentReIndex) {
                    throw new IllegalStateException(
                            "More than one thread attempting to concurrently reindex Solr.");
                } else {
                    LOG.warn("There is more than one thread attempting to concurrently "
                            + "reindex Solr. Failing additional threads gracefully. Check your configuration.");
                    return;
                }
            } else {
                IS_LOCKED = true;
            }
        }

        try {
            LOG.info("Rebuilding the solr index...");
            StopWatch s = new StopWatch();

            LOG.info("Deleting the reindex core prior to rebuilding the index");
            deleteAllReindexCoreDocuments();

            Object[] pack = saveState();
            try {
                final Long numItemsToIndex;
                if (useSku) {
                    numItemsToIndex = skuDao.readCountAllActiveSkus();
                } else {
                    numItemsToIndex = productDao.readCountAllActiveProducts();
                }

                if (LOG.isDebugEnabled()) {
                    LOG.debug("There are at most " + numItemsToIndex + " items to index");
                }
                performCachedOperation(new SolrIndexCachedOperation.CacheOperation() {
                    @Override
                    public void execute() throws ServiceException {
                        int page = 0;
                        while ((page * pageSize) < numItemsToIndex) {
                            buildIncrementalIndex(page, pageSize);
                            page++;
                        }
                    }
                });

                //We can call optimize here because we just updated the entire index
                optimizeIndex(SolrContext.getReindexServer());
            } finally {
                restoreState(pack);
            }

            // Swap the active and the reindex cores
            shs.swapActiveCores();

            LOG.info(String.format("Finished building index in %s", s.toLapString()));
        } finally {
            synchronized (LOCK_OBJECT) {
                IS_LOCKED = false;
            }
        }
    }

    /**
     * <p>
     * This method deletes all of the documents from {@link SolrContext#getReindexServer()}
     * 
     * @throws ServiceException if there was a problem removing the documents
     * @deprecated use {@link #deleteAllReindexCoreDocuments()} instead
     */
    @Deprecated
    protected void deleteAllDocuments() throws ServiceException {
        deleteAllReindexCoreDocuments();
    }

    /**
     * <p>
     * This method deletes all of the documents from {@link SolrContext#getReindexServer()}
     * 
     * @throws ServiceException if there was a problem removing the documents
     */
    protected void deleteAllReindexCoreDocuments() throws ServiceException {
        try {
            String deleteQuery = shs.getNamespaceFieldName() + ":(\"" + shs.getCurrentNamespace() + "\")";
            LOG.debug("Deleting by query: " + deleteQuery);
            SolrContext.getReindexServer().deleteByQuery(deleteQuery);

            //Explicitly do a hard commit here since we just deleted the entire index
            SolrContext.getReindexServer().commit();
        } catch (Exception e) {
            if (ServiceException.class.isAssignableFrom(e.getClass())) {
                throw (ServiceException) e;
            }
            throw new ServiceException("Could not delete documents", e);
        }
    }

    protected void buildIncrementalIndex(int page, int pageSize) throws ServiceException {
        buildIncrementalIndex(page, pageSize, true);
    }

    @Override
    public void buildIncrementalProductIndex(List<Product> products, boolean useReindexServer)
            throws ServiceException {
        TransactionStatus status = TransactionUtils.createTransaction("executeIncrementalProductIndex",
                TransactionDefinition.PROPAGATION_REQUIRED, transactionManager, true);
        if (SolrIndexCachedOperation.getCache() == null) {
            LOG.warn("Consider using SolrIndexService.performCachedOperation() in combination with "
                    + "SolrIndexService.buildIncrementalIndex() for better caching performance during solr indexing");
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Building incremental product index - pageSize: [%s]...", products.size()));
        }

        StopWatch s = new StopWatch();
        boolean cacheOperationManaged = false;
        try {
            Collection<SolrInputDocument> documents = new ArrayList<SolrInputDocument>();
            List<Locale> locales = getAllLocales();

            CatalogStructure cache = SolrIndexCachedOperation.getCache();
            if (cache != null) {
                cacheOperationManaged = true;
            } else {
                cache = new CatalogStructure();
                SolrIndexCachedOperation.setCache(cache);
            }

            List<Field> fields = fieldDao.readAllProductFields();
            List<Long> productIds = BLCCollectionUtils.collectList(products, new TypedTransformer<Long>() {

                @Override
                public Long transform(Object input) {
                    return shs.getProductId((Product) input);
                }
            });

            solrIndexDao.populateProductCatalogStructure(productIds, SolrIndexCachedOperation.getCache());

            for (Product product : products) {
                SolrInputDocument doc = buildDocument(product, fields, locales);
                //If someone overrides the buildDocument method and determines that they don't want a product 
                //indexed, then they can return null. If the document is null it does not get added to 
                //to the index.
                if (doc != null) {
                    documents.add(doc);
                }
            }

            extensionManager.getProxy().modifyBuiltDocuments(documents, products, fields, locales);

            logDocuments(documents);

            if (!CollectionUtils.isEmpty(documents)) {
                SolrServer server = useReindexServer ? SolrContext.getReindexServer() : SolrContext.getServer();
                server.add(documents);
                commit(server);
            }
            TransactionUtils.finalizeTransaction(status, transactionManager, false);
        } catch (SolrServerException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw new ServiceException("Could not rebuild index", e);
        } catch (IOException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw new ServiceException("Could not rebuild index", e);
        } catch (RuntimeException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw e;
        } finally {
            if (!cacheOperationManaged) {
                SolrIndexCachedOperation.clearCache();
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Built incremental product index - pageSize: [%s] in [%s]", products.size(),
                    s.toLapString()));
        }
    }

    @Override
    public void buildIncrementalSkuIndex(List<Sku> skus, boolean useReindexServer) throws ServiceException {
        TransactionStatus status = TransactionUtils.createTransaction("executeIncrementalSkuIndex",
                TransactionDefinition.PROPAGATION_REQUIRED, transactionManager, true);
        if (SolrIndexCachedOperation.getCache() == null) {
            LOG.warn("Consider using SolrIndexService.performCachedOperation() in combination with "
                    + "SolrIndexService.buildIncrementalIndex() for better caching performance during solr indexing");
        }

        StopWatch s = new StopWatch();
        boolean cacheOperationManaged = false;
        try {
            Collection<SolrInputDocument> documents = new ArrayList<SolrInputDocument>();
            List<Locale> locales = getAllLocales();

            CatalogStructure cache = SolrIndexCachedOperation.getCache();
            if (cache != null) {
                cacheOperationManaged = true;
            } else {
                cache = new CatalogStructure();
                SolrIndexCachedOperation.setCache(cache);
            }

            List<Field> fields = fieldDao.readAllSkuFields();
            List<Long> productIds = new ArrayList<Long>();
            for (Sku sku : skus) {
                productIds.add(sku.getProduct().getId());
            }

            solrIndexDao.populateProductCatalogStructure(productIds, SolrIndexCachedOperation.getCache());

            for (Sku sku : skus) {
                SolrInputDocument doc = buildDocument(sku, fields, locales);
                //If someone overrides the buildDocument method and determines that they don't want a product 
                //indexed, then they can return null. If the document is null it does not get added to 
                //to the index.
                if (doc != null) {
                    documents.add(doc);
                }
            }

            logDocuments(documents);

            if (!CollectionUtils.isEmpty(documents)) {
                SolrServer server = useReindexServer ? SolrContext.getReindexServer() : SolrContext.getServer();
                server.add(documents);
                commit(server);
            }
            TransactionUtils.finalizeTransaction(status, transactionManager, false);
        } catch (SolrServerException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw new ServiceException("Could not rebuild index", e);
        } catch (IOException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw new ServiceException("Could not rebuild index", e);
        } catch (RuntimeException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw e;
        } finally {
            if (!cacheOperationManaged) {
                SolrIndexCachedOperation.clearCache();
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Built incremental sku index - pageSize: [%s] in [%s]", skus.size(),
                    s.toLapString()));
        }
    }

    @Override
    public void buildIncrementalIndex(int page, int pageSize, boolean useReindexServer) throws ServiceException {
        TransactionStatus status = TransactionUtils.createTransaction("readItemsToIndex",
                TransactionDefinition.PROPAGATION_REQUIRED, transactionManager, true);
        if (SolrIndexCachedOperation.getCache() == null) {
            LOG.warn("Consider using SolrIndexService.performCachedOperation() in combination with "
                    + "SolrIndexService.buildIncrementalIndex() for better caching performance during solr indexing");
        }

        try {
            if (useSku) {
                List<Sku> skus = readAllActiveSkus(page, pageSize);
                buildIncrementalSkuIndex(skus, useReindexServer);
            } else {
                List<Product> products = readAllActiveProducts(page, pageSize);
                buildIncrementalProductIndex(products, useReindexServer);
            }

            TransactionUtils.finalizeTransaction(status, transactionManager, false);
        } catch (RuntimeException e) {
            TransactionUtils.finalizeTransaction(status, transactionManager, true);
            throw e;
        }
    }

    /**
     * This method to read all active products will be slow if you have a large catalog. In this case, you will want to
     * read the products in a different manner. For example, if you know the fields that will be indexed, you can configure
     * a DAO object to only load those fields. You could also use a JDBC based DAO for even faster access. This default
     * implementation is only suitable for small catalogs.
     * 
     * @return the list of all active products to be used by the index building task
     */
    protected List<Product> readAllActiveProducts() {
        return productDao.readAllActiveProducts();
    }

    /**
     * This method to read active products utilizes paging to improve performance over {@link #readAllActiveProducts()}.
     * While not optimal, this will reduce the memory required to load large catalogs.
     * 
     * It could still be improved for specific implementations by only loading fields that will be indexed or by accessing
     * the database via direct JDBC (instead of Hibernate).
     * 
     * @return the list of all active products to be used by the index building task
     * @since 2.2.0
     */
    protected List<Product> readAllActiveProducts(int page, int pageSize) {
        return productDao.readAllActiveProducts(page, pageSize);
    }

    /**
     * This method to read active skus utilizes paging to improve performance.
     * While not optimal, this will reduce the memory required to load large catalogs.
     * 
     * It could still be improved for specific implementations by only loading fields that will be indexed or by accessing
     * the database via direct JDBC (instead of Hibernate).
     * 
     * @return the list of all active SKUs to be used by the index building task
     * @since 2.2.0
     */
    protected List<Sku> readAllActiveSkus(int page, int pageSize) {
        List<Sku> skus = skuDao.readAllActiveSkus(page, pageSize);
        ArrayList<Sku> skusToIndex = new ArrayList<Sku>();

        if (skus != null && !skus.isEmpty()) {
            for (Sku sku : skus) {
                //If the sku is not active, don't index it...
                if (!sku.isActive()) {
                    continue;
                }

                //If this is the default sku and the product has product options
                //and is not allowed to be sold without product options
                if (sku.getDefaultProduct() != null && !sku.getProduct().getCanSellWithoutOptions()
                        && !sku.getProduct().getAdditionalSkus().isEmpty()) {
                    continue;
                }

                if (sku.getDefaultProduct() instanceof ProductBundle) {
                    continue;
                }

                skusToIndex.add(sku);
            }
        }

        return skusToIndex;
    }

    @Override
    public List<Locale> getAllLocales() {
        return localeService.findAllLocales();
    }

    @Override
    public SolrInputDocument buildDocument(final Sku sku, List<Field> fields, List<Locale> locales) {
        final SolrInputDocument document = new SolrInputDocument();

        attachBasicDocumentFields(sku, document);

        // Keep track of searchable fields added to the index.   We need to also add the search facets if 
        // they weren't already added as a searchable field.
        List<String> addedProperties = new ArrayList<String>();

        for (Field field : fields) {
            try {
                // Index the searchable fields
                if (field.getSearchable()) {
                    List<FieldType> searchableFieldTypes = shs.getSearchableFieldTypes(field);
                    for (FieldType sft : searchableFieldTypes) {
                        Map<String, Object> propertyValues = getPropertyValues(sku, field, sft, locales);

                        // Build out the field for every prefix
                        for (Entry<String, Object> entry : propertyValues.entrySet()) {
                            String prefix = entry.getKey();
                            prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";

                            String solrPropertyName = shs.getPropertyNameForFieldSearchable(field, sft, prefix);
                            Object value = entry.getValue();

                            document.addField(solrPropertyName, value);
                            addedProperties.add(solrPropertyName);
                        }
                    }
                }

                // Index the faceted field type as well
                FieldType facetType = field.getFacetFieldType();
                if (facetType != null) {
                    Map<String, Object> propertyValues = getPropertyValues(sku, field, facetType, locales);

                    // Build out the field for every prefix
                    for (Entry<String, Object> entry : propertyValues.entrySet()) {
                        String prefix = entry.getKey();
                        prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";

                        String solrFacetPropertyName = shs.getPropertyNameForFieldFacet(field, prefix);
                        Object value = entry.getValue();

                        if (!addedProperties.contains(solrFacetPropertyName)) {
                            document.addField(solrFacetPropertyName, value);
                        }
                    }
                }
            } catch (Exception e) {
                LOG.error("Could not get value for property[" + field.getQualifiedFieldName() + "] for sku id["
                        + sku.getId() + "]", e);
            }
        }

        attachAdditionalDocumentFields(sku, document);

        return document;
    }

    @Override
    public SolrInputDocument buildDocument(final Product product, List<Field> fields, List<Locale> locales) {
        final SolrInputDocument document = new SolrInputDocument();

        attachBasicDocumentFields(product, document);

        // Keep track of searchable fields added to the index.   We need to also add the search facets if 
        // they weren't already added as a searchable field.
        List<String> addedProperties = new ArrayList<String>();

        for (Field field : fields) {
            try {
                // Index the searchable fields
                if (field.getSearchable()) {
                    List<FieldType> searchableFieldTypes = shs.getSearchableFieldTypes(field);
                    for (FieldType sft : searchableFieldTypes) {
                        Map<String, Object> propertyValues = getPropertyValues(product, field, sft, locales);

                        // Build out the field for every prefix
                        for (Entry<String, Object> entry : propertyValues.entrySet()) {
                            String prefix = entry.getKey();
                            prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";

                            String solrPropertyName = shs.getPropertyNameForFieldSearchable(field, sft, prefix);
                            Object value = entry.getValue();

                            document.addField(solrPropertyName, value);
                            addedProperties.add(solrPropertyName);
                        }
                    }
                }

                // Index the faceted field type as well
                FieldType facetType = field.getFacetFieldType();
                if (facetType != null) {
                    Map<String, Object> propertyValues = getPropertyValues(product, field, facetType, locales);

                    // Build out the field for every prefix
                    for (Entry<String, Object> entry : propertyValues.entrySet()) {
                        String prefix = entry.getKey();
                        prefix = StringUtils.isBlank(prefix) ? prefix : prefix + "_";

                        String solrFacetPropertyName = shs.getPropertyNameForFieldFacet(field, prefix);
                        Object value = entry.getValue();

                        if (!addedProperties.contains(solrFacetPropertyName)) {
                            document.addField(solrFacetPropertyName, value);
                        }
                    }
                }
            } catch (Exception e) {
                LOG.error("Could not get value for property[" + field.getQualifiedFieldName() + "] for product id["
                        + product.getId() + "]", e);
                throw ExceptionHelper.refineException(e);
            }
        }

        attachAdditionalDocumentFields(product, document);

        return document;
    }

    /**
     * Implementors can extend this and override this method to add additional fields to the Solr document.
     * 
     * @param sku
     * @param document
     */
    protected void attachAdditionalDocumentFields(Sku sku, SolrInputDocument document) {
        //Empty implementation. Placeholder for others to extend and add additional fields
    }

    /**
     * Implementors can extend this and override this method to add additional fields to the Solr document.
     * 
     * @param product
     * @param document
     */
    protected void attachAdditionalDocumentFields(Product product, SolrInputDocument document) {
        //Empty implementation. Placeholder for others to extend and add additional fields
    }

    /**
     * Adds the ID, category, and explicitCategory fields for the product or sku to the document
     * 
     * @param product
     * @param sku
     * @param document
     */
    protected void attachBasicDocumentFields(Sku sku, SolrInputDocument document) {
        boolean cacheOperationManaged = false;
        Product product = sku.getProduct();
        try {
            CatalogStructure cache = SolrIndexCachedOperation.getCache();
            if (cache != null) {
                cacheOperationManaged = true;
            } else {
                cache = new CatalogStructure();
                SolrIndexCachedOperation.setCache(cache);
                solrIndexDao.populateProductCatalogStructure(Arrays.asList(product.getId()),
                        SolrIndexCachedOperation.getCache());
            }
            // Add the namespace and ID fields for this product
            document.addField(shs.getNamespaceFieldName(), shs.getCurrentNamespace());
            document.addField(shs.getIdFieldName(), shs.getSolrDocumentId(document, sku));
            document.addField(shs.getSkuIdFieldName(), shs.getSkuId(sku));
            extensionManager.getProxy().attachAdditionalBasicFields(sku, document, shs);

            // The explicit categories are the ones defined by the product itself
            if (cache.getParentCategoriesByProduct().containsKey(shs.getProductId(product))) {
                for (Long categoryId : cache.getParentCategoriesByProduct().get(shs.getProductId(product))) {
                    document.addField(shs.getExplicitCategoryFieldName(), shs.getCategoryId(categoryId));

                    String categorySortFieldName = shs.getCategorySortFieldName(shs.getCategoryId(categoryId));
                    String displayOrderKey = categoryId + "-" + shs.getProductId(product);
                    BigDecimal displayOrder = cache.getDisplayOrdersByCategoryProduct().get(displayOrderKey);

                    if (document.getField(categorySortFieldName) == null) {
                        document.addField(categorySortFieldName, displayOrder);
                    }

                    // This is the entire tree of every category defined on the product
                    buildFullCategoryHierarchy(document, cache, categoryId, new HashSet<Long>());
                }
            }
        } finally {
            if (!cacheOperationManaged) {
                SolrIndexCachedOperation.clearCache();
            }
        }
    }

    protected void attachBasicDocumentFields(Product product, SolrInputDocument document) {
        boolean cacheOperationManaged = false;
        try {
            CatalogStructure cache = SolrIndexCachedOperation.getCache();
            if (cache != null) {
                cacheOperationManaged = true;
            } else {
                cache = new CatalogStructure();
                SolrIndexCachedOperation.setCache(cache);
                solrIndexDao.populateProductCatalogStructure(Arrays.asList(product.getId()),
                        SolrIndexCachedOperation.getCache());
            }
            // Add the namespace and ID fields for this product
            document.addField(shs.getNamespaceFieldName(), shs.getCurrentNamespace());
            document.addField(shs.getIdFieldName(), shs.getSolrDocumentId(document, product));
            document.addField(shs.getProductIdFieldName(), shs.getProductId(product));
            extensionManager.getProxy().attachAdditionalBasicFields(product, document, shs);

            Long originalId = sandBoxHelper.getOriginalId(product);
            originalId = (originalId == null) ? product.getId() : originalId;

            // The explicit categories are the ones defined by the product itself
            if (cache.getParentCategoriesByProduct().containsKey(originalId)) {
                for (Long categoryId : cache.getParentCategoriesByProduct().get(originalId)) {
                    document.addField(shs.getExplicitCategoryFieldName(), shs.getCategoryId(categoryId));

                    String categorySortFieldName = shs.getCategorySortFieldName(shs.getCategoryId(categoryId));
                    String displayOrderKey = categoryId + "-" + originalId;
                    BigDecimal displayOrder = cache.getDisplayOrdersByCategoryProduct().get(displayOrderKey);

                    if (document.getField(categorySortFieldName) == null) {
                        document.addField(categorySortFieldName, displayOrder);
                    }

                    // This is the entire tree of every category defined on the product
                    buildFullCategoryHierarchy(document, cache, categoryId, new HashSet<Long>());
                }
            }
        } finally {
            if (!cacheOperationManaged) {
                SolrIndexCachedOperation.clearCache();
            }
        }
    }

    /**
     * Walk the category hierarchy upwards, adding a field for each level to the solr document
     *
     * @param document the solr document for the product
     * @param cache the catalog structure cache
     * @param categoryId the current category id
     */
    protected void buildFullCategoryHierarchy(SolrInputDocument document, CatalogStructure cache, Long categoryId,
            Set<Long> indexedParents) {
        Long catIdToAdd = shs.getCategoryId(categoryId);

        Collection<Object> existingValues = document.getFieldValues(shs.getCategoryFieldName());
        if (existingValues == null || !existingValues.contains(catIdToAdd)) {
            document.addField(shs.getCategoryFieldName(), catIdToAdd);
        }

        Set<Long> parents = cache.getParentCategoriesByCategory().get(categoryId);
        for (Long parent : parents) {
            if (!indexedParents.contains(parent)) {
                indexedParents.add(parent);
                buildFullCategoryHierarchy(document, cache, parent, indexedParents);
            }
        }
    }

    /**
     * Returns a map of prefix to value for the requested attributes. For example, if the requested field corresponds to
     * a Sku's description and the locales list has the en_US locale and the es_ES locale, the resulting map could be
     * 
     * { "en_US" : "A description",
     *   "es_ES" : "Una descripcion" }
     * 
     * @param product
     * @param sku
     * @param field
     * @param fieldType
     * @param locales
     * @return the value of the property
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     */
    protected Map<String, Object> getPropertyValues(Object indexedItem, Field field, FieldType fieldType,
            List<Locale> locales) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

        String propertyName = field.getPropertyName();
        Map<String, Object> values = new HashMap<String, Object>();

        ExtensionResultStatusType extensionResult = ExtensionResultStatusType.NOT_HANDLED;
        if (extensionManager != null) {
            if (Product.class.isAssignableFrom(indexedItem.getClass())) {
                extensionResult = extensionManager.getProxy().addPropertyValues((Product) indexedItem, field,
                        fieldType, values, propertyName, locales);
            } else if (Sku.class.isAssignableFrom(indexedItem.getClass())) {
                extensionResult = extensionManager.getProxy().addPropertyValues((Sku) indexedItem, field, fieldType,
                        values, propertyName, locales);
            }
        }

        if (ExtensionResultStatusType.NOT_HANDLED.equals(extensionResult)) {
            Object propertyValue = shs.getPropertyValue(indexedItem, field);
            if (propertyValue != null) {
                values.put("", propertyValue);
            }
        }

        return values;
    }

    /**
     * Converts a propertyName to one that is able to reference inside a map. For example, consider the property
     * in Product that references a List<ProductAttribute>, "productAttributes". Also consider the utility method
     * in Product called "mappedProductAttributes", which returns a map of the ProductAttributes keyed by the name
     * property in the ProductAttribute. Given the parameters "productAttributes.heatRange", "productAttributes", 
     * "mappedProductAttributes" (which would represent a property called "productAttributes.heatRange" that 
     * references a specific ProductAttribute inside of a product whose "name" property is equal to "heatRange", 
     * this method will convert this property to mappedProductAttributes(heatRange).value, which is then usable 
     * by the standard beanutils PropertyUtils class to get the value.
     * 
     * @param propertyName
     * @param listPropertyName
     * @param mapPropertyName
     * @return the converted property name
     * 
     * @deprecated see SolrHelperService.getPropertyValue()
     */
    @Deprecated
    protected String convertToMappedProperty(String propertyName, String listPropertyName, String mapPropertyName) {
        String[] splitName = StringUtils.split(propertyName, "\\.");
        StringBuilder convertedProperty = new StringBuilder();
        for (int i = 0; i < splitName.length; i++) {
            if (convertedProperty.length() > 0) {
                convertedProperty.append(".");
            }

            if (splitName[i].equals(listPropertyName)) {
                convertedProperty.append(mapPropertyName).append("(");
                convertedProperty.append(splitName[i + 1]).append(").value");
                i++;
            } else {
                convertedProperty.append(splitName[i]);
            }
        }
        return convertedProperty.toString();
    }

    @Override
    public Object[] saveState() {
        return new Object[] { BroadleafRequestContext.getBroadleafRequestContext(),
                SkuPricingConsiderationContext.getSkuPricingConsiderationContext(),
                SkuPricingConsiderationContext.getSkuPricingService(),
                SkuActiveDateConsiderationContext.getSkuActiveDatesService() };
    }

    @Override
    @SuppressWarnings("rawtypes")
    public void restoreState(Object[] pack) {
        BroadleafRequestContext.setBroadleafRequestContext((BroadleafRequestContext) pack[0]);
        SkuPricingConsiderationContext.setSkuPricingConsiderationContext((HashMap) pack[1]);
        SkuPricingConsiderationContext.setSkuPricingService((DynamicSkuPricingService) pack[2]);
        SkuActiveDateConsiderationContext.setSkuActiveDatesService((DynamicSkuActiveDatesService) pack[3]);
    }

    @Override
    public void optimizeIndex(SolrServer server) throws ServiceException, IOException {
        shs.optimizeIndex(server);
    }

    @Override
    public void commit(SolrServer server) throws ServiceException, IOException {
        if (this.commit) {
            commit(server, this.softCommit, this.waitSearcher, this.waitFlush);
        } else if (LOG.isDebugEnabled()) {
            LOG.debug(
                    "The flag / property \"solr.index.commit\" is false. Not committing! Ensure autoCommit is configured.");
        }
    }

    @Override
    public void commit(SolrServer server, boolean softCommit, boolean waitSearcher, boolean waitFlush)
            throws ServiceException, IOException {
        try {
            if (!this.commit) {
                LOG.warn(
                        "The flag / property \"solr.index.commit\" is set to false but a commit is being forced via the API.");
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Committing changes to Solr index: softCommit: " + softCommit + ", waitSearcher: "
                        + waitSearcher + ", waitFlush: " + waitFlush);
            }

            server.commit(waitFlush, waitSearcher, softCommit);
        } catch (SolrServerException e) {
            throw new ServiceException("Could not commit changes to Solr index", e);
        }
    }

    @Override
    public void logDocuments(Collection<SolrInputDocument> documents) {
        if (LOG.isTraceEnabled()) {
            for (SolrInputDocument document : documents) {
                LOG.trace(document);
            }
        }
    }
}