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

com.adobe.cq.social.srp.internal.UGCCResourceProvider Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2012 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.social.srp.internal;

import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.Version;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.SlingIOException;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.NonExistingResource;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.social.srp.APICommand;
import com.adobe.cq.social.srp.APIException;
import com.adobe.cq.social.srp.APIResult;
import com.adobe.cq.social.srp.FacetRangeField;
import com.adobe.cq.social.srp.FacetSearchResult;
import com.adobe.cq.social.srp.IntervalFacetRangeField;
import com.adobe.cq.social.srp.ProviderMetaData;
import com.adobe.cq.social.srp.SearchSortField;
import com.adobe.cq.social.srp.SocialResourcePrefetch;
import com.adobe.cq.social.srp.SocialResourceProvider;
import com.adobe.cq.social.srp.SocialResourceSearchResult;
import com.adobe.cq.social.srp.config.ASRPConfiguration;
import com.adobe.cq.social.srp.config.DSRPConfiguration;
import com.adobe.cq.social.srp.config.MSRPConfiguration;
import com.adobe.cq.social.srp.config.SRPConfigurationError;
import com.adobe.cq.social.srp.config.SRPConfigurationFactory;
import com.adobe.cq.social.srp.config.SocialResourceConfiguration;
import com.adobe.cq.social.srp.internal.SocialDataService.BrowseDocumentsResult;
import com.adobe.cq.social.srp.internal.SocialDataService.Counts;
import com.adobe.cq.social.srp.utilities.api.SocialResourceUtilities;
import com.adobe.cq.social.srp.utilities.internal.InternalSocialResourceUtilities;
import com.adobe.granite.crypto.CryptoSupport;
import com.adobe.granite.security.user.UserProperties;
import com.day.cq.commons.Externalizer;
import com.day.cq.wcm.webservicesupport.Configuration;

/**
 * A resource provider that can talk to a cloud.
 */
public class UGCCResourceProvider implements CachingResourceProvider {
    /**
     * Used to return an List<Resource> of matching children and a count of the matching children.
     */
    private class ChildCountAndList extends SocialResourceSearchResult {

        /**
         * This class is not intended for serialization.
         */
        private static final long serialVersionUID = 1L;
        private Callable countChildren;

        ChildCountAndList() {
            // Ensure we note that we have no idea whether there are matching children or not.
            setNumFound(-1);
        }

        /**
         * Set a Callable which can use it's creator's state to know how to find the number of matching children.
         * @param countChildren a Callable which will return the count of matching children.
         */
        public void setCountChildren(final Callable countChildren) {
            this.countChildren = countChildren;
        }

        /**
         * This override is used so that if we don't already know the number of matching children we can call
         * countChildren to obtain the number.
         */
        @Override
        public long getNumFound() {
            final long numFound = super.getNumFound();
            if (numFound < 0 && countChildren != null) {
                try {
                    return countChildren.call();
                } catch (final Exception e) {
                    LOGGER.warn("Calling count children failed.", e);
                    return -1;
                }
            }
            return numFound;
        }
    }

    // Simple regexp for the common cases
    private static final String SINGLE_AND = "\\((\\w*) AND (\\w*)\\)";
    private static final String SINGLE_OR = "\\((\\w*) OR (\\w*)\\)";
    private static final String AND_PLUS_OR = "\\(\\(((\\w*) AND (\\w*)\\)) OR \\(((\\w*) AND (\\w*))\\)\\)";

    private static final String FIELD_STATE_APPROVED = "approved";
    private static final String FIELD_STATE_PENDING = "pending";
    private static final String FIELD_STATE_DENIED = "denied";
    private static final String FIELD_STATE_SPAM = "spam";

    private static final int VISIBLE_ONLY_COUNT = 0;
    private static final int ALL_COUNT = 1;
    /** Max length for url that can be sent to AS. Don't know the exact number. */
    public static final Integer QUERY_MAX_LENGTH = 4400;

    /**
     * For use in building cache key.
     */
    private static final String RANGE_DATE_FORMAT = "yyyy-MM-dd-HH";
    /**
     * The property name for the user name.
     */
    static final String USER_NAME_PROPERTY = "userIdentifier";

    // This logic needs to correspond with what mod-3 does. In particular, this logic:
    // https://git.corp.adobe.com/Social/adobe-social-moderation/blob/3.1/providers/
    // soco/bundles/cq-social-moderation-soco-provider/src/main/java/com/adobe/cq/social/
    // moderation/soco/impl/SocoDataPostProcessor.java#119
    private static final String APPROVED_STRING = "-" + AbstractSchemaMapper.getSchemaApprovedKey() + ":false";
    private static final String PENDING_STRING = "-" + AbstractSchemaMapper.getSchemaApprovedKey() + ":[\"\" TO *]";
    private static final String DENIED_STRING = AbstractSchemaMapper.getSchemaApprovedKey() + ":false";

    /** */
    private static final Map ROOT_DOC;
    private static final Logger LOGGER = LoggerFactory.getLogger(UGCCResourceProvider.class);
    private static final String ATTACHMENT = "attachments";
    private static final String NAME = "name";

    // synthetic tallycount node where we store the values from the faceted search in the cache
    private static final String TALLYCOUNT_NODE = "/tallycount";

    private static final String CC_ASIPATH = "asipath";
    private static final String CC_HOST_URL = "hosturl";
    private static final String CC_REPORT_SUITE = "reportsuite";
    private static final String CC_CONSUMER_KEY = "consumerkey";
    private static final String CC_SECRET_KEY = "secret";

    private final SocialDataService dsClient;
    private final String providerBase;
    private final Map commandsQueue = new LinkedHashMap();
    private final Map> commandsAttachmentQueue =
        new LinkedHashMap>();
    private final Externalizer externalizer;
    private final CryptoSupport cryptoSupport;
    private final AbstractSchemaMapper mapper;
    private final SRPConfigurationFactory srpConfigFactory;

    private final ProviderCache documentCache;
    private final CountCache visibleCountCache;
    private final CountCache allCountCache;
    private final AbstractCache>> facetCache;
    private final StringListCache stringListCache;

    private boolean configSet;

    private ProviderMetaData metaData;

    static {
        ROOT_DOC = new HashMap();
        ROOT_DOC.put("jcr:title", "Social Resource Provider Root");
    }

    /**
     * Creates a new Resource Provider.
     * @param client Adobe Cloud Storage API Client
     * @param providerBase Base location of the provider.
     * @param externalizer Used to externalize urls stored in Adobe Cloud Storage
     * @param cryptoSupport a CryptoSupport impl
     * @param documentCache the cache for docs
     * @param visibleCountCache the cache for visible counts
     * @param allCountCache the cache for all counts
     * @param facetCache the cache for facets
     * @param mapper the schema mapper
     * @param stringListCache StringListCache
     * @param srpConfigFactory the configuration factory
     */
    public UGCCResourceProvider(final SocialDataService client, final String providerBase,
        final Externalizer externalizer, final CryptoSupport cryptoSupport, final ProviderCache documentCache,
        final CountCache allCountCache, final CountCache visibleCountCache,
        final AbstractCache>> facetCache,
        final StringListCache stringListCache, final AbstractSchemaMapper mapper,
        final SRPConfigurationFactory srpConfigFactory) {
        this.dsClient = client;
        this.providerBase = providerBase;
        this.externalizer = externalizer;
        this.cryptoSupport = cryptoSupport;
        this.documentCache = documentCache;
        this.visibleCountCache = visibleCountCache;
        this.allCountCache = allCountCache;
        this.mapper = mapper;
        this.facetCache = facetCache;
        this.stringListCache = stringListCache;
        this.srpConfigFactory = srpConfigFactory;
        ROOT_DOC.put("type", client.getDSClient());
        ROOT_DOC.put(SlingConstants.NAMESPACE_PREFIX + ":" + SlingConstants.PROPERTY_RESOURCE_TYPE, "social/asi/resourceprovider");
    }

    /**
     * {@inheritDoc}
     * @deprecated See {@link #getResource(ResourceResolver, String)}
     */
    @Override
    @Deprecated
    public Resource getResource(final ResourceResolver resourceResolver, final HttpServletRequest request,
        final String path) {
        return this.getResource(resourceResolver, path);
    }

    /**
     * Get the data service for this provider.
     * @return the data service
     */
    public SocialDataService getSocialDataService() {
        return dsClient;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Resource getResource(final ResourceResolver resourceResolver, final String path) {
        if (!StringUtils.startsWith(path, providerBase)) {
            return null;
        }
        return getResourceWithoutCheck(resourceResolver, path);
    }

    /**
     * Skip checking the provider base when getting a "special" resource.
     * @param resourceResolver the resolver
     * @param path the path
     * @return the resource
     */
    public Resource getResourceWithoutCheck(final ResourceResolver resourceResolver, final String path) {
        return getResourceWithoutCheck(resourceResolver, path, false);
    }

    private CacheEntry createEntry(final Long result) {
        return new CacheEntry(result);
    }

    /**
     * Fetch a pending resource update (one that has been started, but not commited) if any.
     * @param resolver the resolver
     * @param path the path for the resource
     * @return The updated resource if there is one pending. A NER if the requested resource has a pending delete.
     *         Null if there is nothing pending for this resource.
     */
    private Resource getPendingResource(final ResourceResolver resolver, final String path) {
        if (commandsQueue.containsKey(path)) {
            final CommandResource command = commandsQueue.get(path);
            if (command.methodType == APICommand.DELETE) {
                return new NonExistingResource(resolver, path);
            } else if (command.methodType == APICommand.CREATE) {
                LOGGER.debug("Returning {} from pending creates cache.", path);
            } else {
                LOGGER.debug("Returning {} from pending updates cache.", path);
            }
            final MapResource cachedResource = command.getResource();
            cachedResource.unlockMetadata();
            return cachedResource;
        }
        if (commandsAttachmentQueue.containsKey(path)) {
            final LinkedList commands = commandsAttachmentQueue.get(path);
            final CommandResource lastCommand = commands.getLast();
            if (lastCommand.methodType == APICommand.DELETE) {
                return new NonExistingResource(resolver, path);
            } else {
                LOGGER.debug("Returning {} from pending creates cache.", path);
                final MapResource cachedResource = lastCommand.getResource();
                cachedResource.unlockMetadata();
                return cachedResource;
            }
        }

        return null;
    }

    private Resource getResourceWithoutCheck(final ResourceResolver resourceResolver, final String path,
        final boolean recursedOnce) {

        if (StringUtils.equals(providerBase, path)) {
            // Identify this provider by returning a resource at it's root
            return new MapResourceImpl(resourceResolver, this, path, ROOT_DOC, false);
        }

        if (SocialProviderUtils.isExtraneousSlingPath(resourceResolver, this, path)) {
            return null;
        }
        if (!SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(path), Session.ACTION_READ)) {
            return null;
        }

        if (noConfig()) {
            return null;
        }

        final Resource pendingResource = getPendingResource(resourceResolver, path);
        if (pendingResource != null) {
            if (ResourceUtil.isNonExistingResource(pendingResource)) {
                return null;
            } else {
                return pendingResource;
            }
        }

        Map doc = Collections.emptyMap();
        final boolean isAttach = isAttachment(path);

        final CacheEntry> cacheVal = documentCache.get(path);
        if (cacheVal != null) {
            LOGGER.debug("Got {} from cache.", path);
            doc = new HashMap(cacheVal.get());
        } else if (isAttach) {
            try {
                doc = dsClient.readAttachment(path);
            } catch (final IOException e1) {
                throw new SlingIOException(e1);
            }

            doc = mapper.fromAttachmentSchema(doc);

            documentCache.put(path, documentCache.createEntry(doc, Collections.emptyMap()));
        } else {
            try {
                doc = dsClient.readDocument(path);
            } catch (final IOException e) {
                LOGGER.error("Received exception when reading " + path, e);
                throw new SlingIOException(e);
            }
            if (!doc.isEmpty()) {
                doc = mapper.fromSchema(doc);
            }
            documentCache.put(path, documentCache.createEntry(doc, Collections.emptyMap()));
        }

        if (doc.isEmpty()) {
            if (recursedOnce || isAttach) {
                return null;
            }

            final int index = path.lastIndexOf('/');
            if (index == -1) {
                return null;
            }
            final String parentPath = path.substring(0, index);
            final Resource resource = getResourceWithoutCheck(resourceResolver, parentPath, true);
            if (resource != null) {
                final String propertyKey = path.substring(index + 1);
                final ValueMap map = resource.adaptTo(ValueMap.class);
                final Object value = map.get(propertyKey);
                if (value != null) {
                    // JCR uses the parent's resourcetype + the key value as the resource type. This does the
                    // same..
                    final Object resType =
                        map.get(SlingConstants.NAMESPACE_PREFIX + ":" + SlingConstants.PROPERTY_RESOURCE_TYPE);
                    if (resType != null) {
                        return new SocialPropertyResourceImpl(path, (String) resType, this, resourceResolver,
                            propertyKey, value);
                    }
                }
            }
            return null;
        }
        return new MapResourceImpl(resourceResolver, this, path, doc, isAttach);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Resource create(final ResourceResolver resourceResolver, final String path,
        final Map properties) throws PersistenceException {
        boolean isAttachment = false;
        String commitPath;
        if (!SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(path), Session.ACTION_ADD_NODE)) {
            throw new PersistenceException(String.format("Not allowed to create resource at : %s", path));
        }

        if (StringUtils.equals(path, providerBase)) {
            return null;
        }

        final Map propertiesCopy =
            new HashMap(properties == null ? Collections.emptyMap() : properties);

        if ((propertiesCopy.containsKey("nt:file")) && (propertiesCopy.get("nt:file") instanceof InputStream)) {
            final String[] info = getPathSuffix(path);
            commitPath = StringUtils.removeEnd(path, info[1]);
            commitPath = StringUtils.stripEnd(commitPath, "/");
            commitPath = StringUtils.stripEnd(commitPath, info[0]);
            commitPath = commitPath + ATTACHMENT + "/" + info[1];
            propertiesCopy.put(AbstractSchemaMapper.getSocoKey(), commitPath);
            isAttachment = true;
        } else {
            if (!propertiesCopy.containsKey(AbstractSchemaMapper.getSocoParentIdKey())) {
                final String parentId = ResourceUtil.getParent(path);
                propertiesCopy.put(AbstractSchemaMapper.getSocoParentIdKey(), parentId);
            }
            commitPath = path;
        }

        final MapResource res = new MapResourceImpl(resourceResolver, this, commitPath, propertiesCopy, isAttachment);
        if (isAttachment) {
            if (propertiesCopy.containsKey(NAME)) {
                String name = (String) (propertiesCopy.get(NAME));
                if (!StringUtils.isEmpty(name) && (name.contains("[") || name.contains("]") || name.contains("/")
                        || name.contains(":") || name.contains("|") || name.contains("*"))) {
                    throw new PersistenceException("FileName contains not supporting characters");
                }
            }
            LinkedList commandResources;
            if (commandsAttachmentQueue.containsKey(commitPath)) {
                commandResources = commandsAttachmentQueue.get(commitPath);
                commandResources.add(new CommandResource(path, APICommand.CREATE, res));
            } else {
                commandResources = new LinkedList();
                commandResources.add(new CommandResource(path, APICommand.CREATE, res));
                commandsAttachmentQueue.put(commitPath, commandResources);
            }
        } else {
            commandsQueue.put(commitPath, new CommandResource(path, APICommand.CREATE, res));
        }
        return res;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete(final ResourceResolver resourceResolver, final String path) throws PersistenceException {
        if (StringUtils.isEmpty(path)) {
            return;
        }

        if (!SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(path), Session.ACTION_REMOVE)) {
            throw new PersistenceException(String.format("Not allowed to delete resource at : %s", path));
        }
        if (isAttachment(path)) {
            LinkedList commandResources;
            if (commandsAttachmentQueue.containsKey(path)) {
                commandResources = commandsAttachmentQueue.get(path);
                commandResources.add(new CommandResource(path, APICommand.DELETE, null));
            } else {
                commandResources = new LinkedList();
                commandResources.add(new CommandResource(path, APICommand.DELETE, null));
                commandsAttachmentQueue.put(path, commandResources);
            }
        } else {
            commandsQueue.put(path, new CommandResource(path, APICommand.DELETE, null));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void revert(final ResourceResolver resourceResolver) {
        commandsAttachmentQueue.clear();
        commandsQueue.clear();
    }

    private void addModerationDetails(final ResourceResolver resolver, final Map map) {
        if (map.containsKey(InternalSocialResourceUtilities.PN_ENTITY)
                && !map.containsKey(AbstractSchemaMapper.getSocoEntityUrlKey())) {
            LOGGER.debug("Updating an existing entity URL which was previously not allowed: {}",
                map.get(InternalSocialResourceUtilities.PN_ENTITY));
        }

        if (externalizer != null && map.containsKey(InternalSocialResourceUtilities.PN_ENTITY)
                && !StringUtils.isEmpty((String) map.get(InternalSocialResourceUtilities.PN_ENTITY))) {
            map.put(AbstractSchemaMapper.getSocoEntityUrlKey(),
                externalizer.publishLink(resolver, (String) map.remove(InternalSocialResourceUtilities.PN_ENTITY)));
        }
        addSocialSpecificFields(resolver, map);
    }

    private void addSocialSpecificFields(final ResourceResolver resolver, final Map map) {
        if (map.containsKey(InternalSocialResourceUtilities.PN_CS_ROOT)
                && map.containsKey(InternalSocialResourceUtilities.PN_PARENTID)) {
            final String parent = (String) map.get(InternalSocialResourceUtilities.PN_PARENTID);
            final String root = (String) map.get(InternalSocialResourceUtilities.PN_CS_ROOT);
            map.put(InternalSocialResourceUtilities.PN_IS_REPLY, !StringUtils.equals(parent, root));
        }
        if (map.containsKey(USER_NAME_PROPERTY) && map.containsKey(InternalSocialResourceUtilities.PN_CS_ROOT)) {
            final UserProperties up =
                SocialResourceUtils.getUserProperties(resolver, (String) map.get(USER_NAME_PROPERTY));
            if (up != null) {
                try {
                    final String displayName = up.getDisplayName();
                    map.put(AbstractSchemaMapper.getSocoAuthorDisplayNameKey(), displayName);
                } catch (final RepositoryException e) {
                    LOGGER.error("Could not get display name!", e);
                }
            } else {
                LOGGER.warn("Could not get user properties.");
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public void commit(final ResourceResolver resourceResolver) throws PersistenceException {
        try {
            MapResource res = null;
            // first deal with documents
            final List commandList = new ArrayList();
            for (final String key : commandsQueue.keySet()) {
                final CommandResource command = commandsQueue.get(key);
                final APICommand batchCommand;
                if (command.methodType == APICommand.DELETE) {
                    batchCommand = new APICommand(command.methodType, command.key, null);
                } else if (command.methodType == APICommand.INCREMENT) {
                    res = command.getResource();
                    final Map map = res.adaptTo(ModifiableValueMap.class);
                    // make sure there is a map under the key "$inc"
                    if (!map.containsKey(CachingResourceProvider.INC)
                            || !(map.get(CachingResourceProvider.INC) instanceof Map)) {
                        throw new PersistenceException("Command to increment sent without an increment map");
                    }
                    final Map incMap = new HashMap();
                    incMap.put(CachingResourceProvider.INC, map.get(CachingResourceProvider.INC));
                    final Map temp = mapper.toSchema(incMap, command.key);
                    if (temp.containsKey(INC)) {
                        incMap.put(INC, temp.get(INC));
                    } else {
                        incMap.remove(INC);
                    }
                    if (temp.containsKey("cqdata")) {
                        incMap.put("cqdata", temp.get("cqdata"));
                    }
                    batchCommand =
                        new APICommand(APICommand.UPDATE, command.key, incMap, Collections.singletonMap(INC,
                            map.get(INC)));
                } else {
                    res = command.getResource();
                    final Map map = res.adaptTo(Map.class);
                    addModerationDetails(resourceResolver, map);
                    final Map documentData = mapper.toSchema(map, command.key);
                    batchCommand = new APICommand(command.methodType, command.key, documentData, map);
                }
                commandList.add(batchCommand);
            }
            final List errorCodes = new ArrayList();
            final List errorKeys = new ArrayList();
            final List errorMethodTypes = new ArrayList();
            if (!commandList.isEmpty()) {
                final List apiResults = dsClient.batchCommit(commandList);
                // iterate over each result, update the cache as appropriate depending on whether the result succeeded
                for (int i = 0; i < apiResults.size(); i++) {
                    final APIResult apiResult = apiResults.get(i);
                    final APICommand apiCommand = commandList.get(i);
                    if (apiResult.getResult()) {
                        switch (apiCommand.methodType) {
                            case APICommand.CREATE:
                                // create cache entry (eventually, this should take the reportSuite as part of the
                                // key)
                                final CacheEntry> entry =
                                    documentCache.createEntry(apiCommand.map,
                                        ((DocumentResult) apiResult).getDocument());
                                documentCache.put(apiCommand.providerId, entry);
                                // next, populate the count cache
                                final List cacheKey = new ArrayList(1);
                                final List cacheKeyWithBase = new ArrayList(2);
                                cacheKey.add(apiCommand.providerId);
                                cacheKeyWithBase.add(apiCommand.providerId);
                                final Long cacheCounts = 0L;
                                visibleCountCache.put(cacheKey, new CacheEntry(cacheCounts));
                                allCountCache.put(cacheKey, new CacheEntry(cacheCounts));
                                cacheKeyWithBase.add((String) apiCommand.map
                                    .get(InternalSocialResourceUtilities.PN_BASETYPE));
                                visibleCountCache.put(cacheKeyWithBase, new CacheEntry(cacheCounts));
                                allCountCache.put(cacheKeyWithBase, new CacheEntry(cacheCounts));
                                LOGGER.debug("Creating cache entry for {}: {}", apiCommand.providerId, entry);
                                if (apiCommand.map.containsKey(AbstractSchemaMapper.getSocoParentIdKey())) {
                                    final String parentKey =
                                        (String) apiCommand.map.get(AbstractSchemaMapper.getSocoParentIdKey());
                                    // remove parent count entry if exist and force a re-read for the count
                                    removeAllMatchesFromCountCache(parentKey);
                                    // remove listChidren cache entries with this parent and force a re-read next time
                                    deleteListChildrenCache(parentKey);
                                    facetCache.remove(parentKey);
                                }
                                break;
                            case APICommand.INCREMENT:
                            case APICommand.UPDATE:
                                mergeCacheContents(apiCommand.providerId, apiCommand.map,
                                    mapper.fromSchema(((DocumentResult) apiResult).getDocument())); // update cache
                                                                                                    // entry
                                break;
                            case APICommand.DELETE:
                                deleteDocumentFromCache(apiCommand.providerId); // delete cache entry
                                break;
                        }
                    } else {
                        // log the error and prepare the APIException arguments
                        final ErrorResult errorResult = (ErrorResult) apiResult;
                        if (apiCommand.methodType == APICommand.CREATE
                                && errorResult.getCode() == ErrorResult.ERROR_DOCUMENT_ID_EXISTS) {
                            // make sure we don't hold onto a "doesn't exist" entry.
                            deleteDocumentFromCache(apiCommand.providerId);
                        } else {
                            LOGGER.error(errorResult.toString());
                        }
                        errorCodes.add(errorResult.getCode());
                        errorKeys.add(apiCommand.providerId);
                        errorMethodTypes.add(apiCommand.methodType);
                    }
                }
            }
            // next, deal with attachments, respecting the order in which the commands were sent
            for (final String key : commandsAttachmentQueue.keySet()) {
                final LinkedList commandResources = commandsAttachmentQueue.get(key);
                for (final CommandResource command : commandResources) {
                    switch (command.methodType) {
                        case APICommand.CREATE:
                            res = command.getResource();
                            final Map map = res.adaptTo(Map.class);
                            Map mappedMap;
                            mappedMap = mapper.toAttachmentSchema(map, key);
                            dsClient.addAttachment(key, mappedMap);
                            break;
                        case APICommand.DELETE:
                            dsClient.deleteAttachment(key);
                            break;
                    }
                }
            }
            // clear the pending updates
            commandsAttachmentQueue.clear();
            commandsQueue.clear();
            // finally, throw an error if needed
            if (!errorKeys.isEmpty()) {
                throw new APIException("Some of the modification operations failed", errorCodes, errorKeys,
                    errorMethodTypes);
            }
        } catch (final PersistenceException e) {
            throw e;
        } catch (final IOException e) {
            throw new PersistenceException("Could not commit changes", e);
        }
    }

    private void mergeCacheContents(final String key, final Map data,
        final Map returnedResult) {
        documentCache.merge(key, data, returnedResult);

        // Need to update the facet cache entry of parent to reflect child update. However, for
        // performance
        // reasons, only if the doc being updated is already in the cache. That means it's possible
        // that the parent child facets can be out of date and not reflect on the doc update

        final CacheEntry> cached = documentCache.get(key);
        if (cached != null) {
            final Map docCacheEntry = cached.get();
            if ((docCacheEntry != null) && docCacheEntry.containsKey(AbstractSchemaMapper.getSocoParentIdKey())) {
                // just remove from cache for simplicity instead of doing math and updating the cache
                facetCache.remove(docCacheEntry.get(AbstractSchemaMapper.getSocoParentIdKey()));
            }
        } else {
            LOGGER.debug("Did not find key {} in document cache, so did not delete the parent in facet cache.", key);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasChanges(final ResourceResolver resourceResolver) {
        return !commandsAttachmentQueue.isEmpty() || !commandsQueue.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long countChildren(final Resource parent) {
        return countChildren(parent, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long countChildren(final Resource parent, final boolean visibleOnly) {

        return countChildren(parent, null, visibleOnly);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long countChildren(final Resource parent, final String childType) {
        return countChildren(parent, childType, false);
    }

    @Override
    public Map countChildren(final List parent, final String baseType,
        final boolean visibleOnly) {
        final Map retVal = new HashMap();
        final CountCache countCache = visibleOnly ? visibleCountCache : allCountCache;
        final List toFetch = new ArrayList(parent.size());
        for (final Resource resource : parent) {
            if (!SocialResourceUtils.checkPermission(resource.getResourceResolver(),
                getJcrAclPath(resource.getPath()), Session.ACTION_READ)) {
                retVal.put(resource.getPath(), 0L);
            } else {

                final List cacheKey = new ArrayList(2);
                cacheKey.add(resource.getPath());
                if (StringUtils.isNotEmpty(baseType)) {
                    cacheKey.add(baseType);
                }

                final CacheEntry cacheEntry = countCache.get(cacheKey);
                if (cacheEntry != null) { // in cache
                    Long cacheCounts;
                    cacheCounts = cacheEntry.get();
                    if (cacheCounts != null && cacheCounts >= 0) {
                        retVal.put(resource.getPath(), cacheCounts);
                    } else {
                        toFetch.add(resource.getPath());
                    }
                } else {
                    toFetch.add(resource.getPath());
                }

            }
        }

        if (!toFetch.isEmpty()) {
            final Map fetched = this.countChildren(toFetch, visibleOnly, baseType);
            retVal.putAll(fetched);
        }
        return retVal;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public Iterator listChildren(final Resource parent) {
        return listChildren(parent.getPath(), parent.getResourceResolver(), 0, -1, Collections.EMPTY_LIST);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator listChildren(final String path, final ResourceResolver resourceResolver,
        final int offset, final int size, final List> sortBy) {

        return listChildren(path, null, resourceResolver, offset, size, sortBy, false);

    }

    @Override
    public Iterator listChildren(final String path, final String baseType,
        final ResourceResolver resourceResolver, final int offset, final int size,
        final List> sortBy) {

        return listChildren(path, baseType, resourceResolver, offset, size, sortBy, false);
    }

    /**
     * Given a list of source paths, return a list of corresponding resources read from the cache only. If at least
     * one resource is missing in the cache, then return null.
     * @param stringList a List of resource paths
     * @return a List of the corresponding resources read from the cache only. If at least 1 resource is missing from
     *         the cache, returns null.
     */
    private ChildCountAndList childrenListFromCache(final ResourceResolver resolver, final List stringList) {
        final ChildCountAndList resourceList = new ChildCountAndList();

        for (final String path : stringList) {
            final CacheEntry> cacheVal = documentCache.get(path);
            if (cacheVal != null) {
                final Map doc = new HashMap(cacheVal.get());
                if (doc.isEmpty()) {
                    return null;
                }
                resourceList.add(new MapResourceImpl(resolver, this, path, doc, false));
            } else {
                return null;
            }
        }

        // if we get here, we've found all resources in the cache
        return resourceList;
    }

    /**
     * Build a unique key to cache listChildren results using the listChildren params.
     * @return a key that can used by StringListCache.
     */
    private String buildListChildrenCacheKey(final String path, final String baseType, final int offset,
        final int size, final List> sortBy, final boolean visibleOnly) {

        // build a unique key to cache results
        final List key = new ArrayList();
        key.add(path);
        key.add(baseType);
        key.add(offset);
        key.add(size);
        key.add(sortBy);
        key.add(visibleOnly);

        return key.toString();
    }

    /**
     * Keep a list of listChildren cache entries with this parent so we can delete the cache entries if children are
     * added. Another way to look as this list, is that this list is a list of all the listChildren cache entries that
     * is easy to lookup via just with the parent and without all the other params such as offset and basetype.
     * Without this list, we would need to iterate over the entire listChildren cache and then compare the parent that
     * is part of the key. Note that this method should be called after storing the listChildren results because this
     * list is kept in the same cache and we don't want this list to expire before the actual listChildren cache
     * entries.
     * @param parent parentid
     * @param cacheKey the cache entry key
     */
    private void updateListChildrenCacheList(final String parent, final String cacheKey) {

        if (parent == null || cacheKey == null) {
            return;
        }

        final CacheEntry> cacheVal = stringListCache.get(parent);
        if (cacheVal != null) {
            final List cacheEntries = cacheVal.get();
            cacheEntries.add(cacheKey);
            // replace with the update value
            stringListCache.put(parent, new CacheEntry>(cacheEntries));
        } else {
            // first listChildren cache entry with this parent, don't use singleton since we might add to it later
            final List entry = new ArrayList();
            entry.add(cacheKey);
            stringListCache.put(parent, new CacheEntry>(entry));
        }
    }

    /**
     * Give a parent, deleted all listChildren cache entries with this parent (using the the list that keeps track of
     * all listChildren cache entries by parentid), and the list itself.
     * @param parent
     */
    private void deleteListChildrenCache(final String parent) {
        final CacheEntry> cacheVal = stringListCache.get(parent);
        if (cacheVal != null) {
            final List values = cacheVal.get();
            for (final String entry : values) {
                stringListCache.remove(entry);
            }
        }
        // and remove the list itself
        stringListCache.remove(parent);
    }

    /**
     * Given a list of parent ids, query the children count for each and write the results to the count cache.
     * @param children list of parent ids to pre-fetch children counts and write to countCache
     */
    // This function was commented out due to performance problems with the dsClient.countChildren endpoints, but will
    // be restored once those problems have been fixed. -- Mason Wolf 6/19/2015 private
    private Map countChildrenNonSearch(final List children, final boolean visible,
        final String baseType) {
        if (noConfig()) {
            return Collections.emptyMap();
        }

        final Map retVal = new HashMap(children.size());
        long start = 0;

        if (LOGGER.isDebugEnabled()) {
            start = System.currentTimeMillis();
        }
        final CountCache countCache = visible ? visibleCountCache : allCountCache;
        Map childCounts;
        try {
            childCounts = dsClient.countChildren(children, baseType, visible);
        } catch (final IOException e) {
            LOGGER.error("Could not prefetch children including " + children.get(0), e);
            return retVal;
        }
        for (final String child : children) {
            final Long cacheCounts = childCounts.get(child);
            final List cacheKey = new ArrayList();
            cacheKey.add(child);
            if (StringUtils.isNotEmpty(baseType)) {
                cacheKey.add(baseType);
            }
            countCache.put(cacheKey, createEntry(cacheCounts));
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Reply counts for: " + child + " " + cacheCounts);
            }
            retVal.put(child, cacheCounts);
        }
        if (LOGGER.isDebugEnabled()) {
            final long end = System.currentTimeMillis();
            LOGGER.debug("Call to prefetchChildrenCount for list {} completed in {} ms", children.toString(),
                Long.toString(end - start));
        }
        return retVal;
    }

    /**
     * Given a list of parent ids, query the children count for each and write the results to the count cache.
     * @param children list of parent ids to pre-fetch children counts and write to countCache
     */
    private Map countChildren(final List children, final boolean visible, final String baseType) {

        final Map retVal = new HashMap(children.size());

        long start = 0;
        if (LOGGER.isDebugEnabled()) {
            start = System.currentTimeMillis();
        }

        final CountCache countCache = visible ? visibleCountCache : allCountCache;

        final StringBuilder facetPart = new StringBuilder("&facet=true&facet.field=\"");
        facetPart.append(AbstractSchemaMapper.getSocoParentIdKey());
        facetPart.append("\"&facet.mincount=1&rows=0");

        final StringBuilder filterPart = new StringBuilder("AND (-(");
        filterPart.append(AbstractSchemaMapper.getSocoFlaggedHiddenKey()).append(":true) -(");
        filterPart.append(AbstractSchemaMapper.getSocoDraftKey()).append(":true) +(");
        filterPart.append(AbstractSchemaMapper.getSocoApprovedKey()).append(":true)) ");
        // Append isDraft to be true
        // Append the base type if not empty
        final StringBuilder baseTypePart = new StringBuilder("");
        if (StringUtils.isNotEmpty(baseType)) {
            baseTypePart.append(" AND (").append(AbstractSchemaMapper.getSchemaBaseType()).append(":\"")
                .append(baseType).append("\")");
        }
        final StringBuilder sb = new StringBuilder();
        // The parent_id_s list as a set of social:parentid:(a OR b OR c ... )
        sb.append(AbstractSchemaMapper.escapeForSolr(AbstractSchemaMapper.getSocoParentIdKey()));
        sb.append(":(");
        boolean firstTime = true;
        for (final String s : children) {
            if (!firstTime) {
                sb.append(" OR ");
            }
            sb.append("\"");
            sb.append(s);
            sb.append("\" ");
            firstTime = false;
        }
        sb.append(") ");

        final StringBuilder theQuery = new StringBuilder(sb);
        if (visible) {
            theQuery.append(filterPart);
        }
        theQuery.append(baseTypePart);
        theQuery.append(facetPart);

        // if query string is too long for http, using the count children endpoint. might be slower but should be
        // faster than multiple calls.
        if (theQuery.toString().length() > QUERY_MAX_LENGTH) {
            return countChildrenNonSearch(children, visible, baseType);
        }

        List> results;
        try {
            results = searchDocuments(theQuery.toString());
        } catch (final IOException e) {
            LOGGER.error("Could not prefetch children including " + children.get(0), e);
            return retVal;
        }

        Map map = Collections.emptyMap();
        if (results != null && !results.isEmpty()) {
            // searchDocuments should only return 1 Map since we specified rows=0 in the search
            // e.g. no docs only the facet result
            map = results.get(0);
        }

        // write results to cache
        for (final String child : children) {
            final Long cacheCounts;
            // Zero counts not returned, so may have null returned from the get
            // Integer counts now returned from OP provider to match what AS provider returns
            final Object oVal = map.get(child);
            if (oVal != null) {
                cacheCounts = ((Integer) oVal).longValue();
            } else {
                cacheCounts = 0L;
            }

            final List cacheKey = new ArrayList();
            cacheKey.add(child);
            if (StringUtils.isNotEmpty(baseType)) {
                cacheKey.add(baseType);
            }
            countCache.put(cacheKey, createEntry(cacheCounts));
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Reply counts for: " + child + " " + cacheCounts);
            }
            retVal.put(child, cacheCounts);
        }

        if (LOGGER.isDebugEnabled()) {
            final long end = System.currentTimeMillis();
            LOGGER.debug("Call to prefetchChildrenCount for list {} completed in {} ms", children.toString(),
                Long.toString(end - start));
        }

        return retVal;
    }

    private String parseQuery(final String query) {
        final String baseQuery = StringUtils.substringBefore(query, "&");

        final Analyzer analyzer = new WhitespaceAnalyzer(Version.LUCENE_47);
        final QueryParser parser = new SolrQueryParser(mapper, Version.LUCENE_47, "f", analyzer);
        Query parsedCQQuery;
        try {
            parsedCQQuery = parser.parse(baseQuery);
        } catch (final ParseException e) {
            LOGGER.error("Could not parse Lucene query:", e);
            return null;
        }
        return parsedCQQuery.toString();
    }

    private List> searchDocuments(final String query) throws IOException {
        if (noConfig()) {
            return Collections.emptyList();
        }

        final String queryInSchema = parseQuery(query);
        if (null == queryInSchema) {
            return Collections.emptyList();
        }

        final String facetInfo = StringUtils.substringAfter(query, "&");
        if (StringUtils.contains(facetInfo, "facet=true")) {
            return facetedSearch(facetInfo, queryInSchema);
        } else {
            final List> schemaDocs = dsClient.nonFacetedSearch(queryInSchema);

            final List> returnDocs = new ArrayList>(schemaDocs.size());
            for (final Map schemaDoc : schemaDocs) {
                returnDocs.add(mapper.fromSchema(schemaDoc));
            }

            // this is a pre-fetch doc search, cache found and not found docs
            if (StringUtils.contains(facetInfo, "cache.empty=true")) {
                cacheResultsAndEmptyResources(queryInSchema, returnDocs);
            }

            return returnDocs;
        }
    }

    /**
     * For a query search, cache results and cache empty values for docs that were not found.
     * @param query
     */
    @SuppressWarnings("unchecked")
    private void cacheResultsAndEmptyResources(final String query, final List> results) {
        if (query == null) {
            return;
        }

        final Map docsToCache = new HashMap();

        // pre-populate with empty docs
        // split into individual property:resource_value
        final String[] tokens = StringUtils.split(query);
        for (final String str : tokens) {
            final String resourcePath = resourceFromQuery(str, AbstractSchemaMapper.getSchemaProviderIdKey());
            if (StringUtils.isNotEmpty(resourcePath)) {
                docsToCache.put(resourcePath, Collections.emptyMap());
            }
        }

        // overwrite empty docs if there are actual docs
        for (final Map doc : results) {
            final String key = (String) doc.get(AbstractSchemaMapper.getSocoKey());
            if (key != null) {
                docsToCache.put(key, doc);
            }
        }

        // write docs to cache
        for (final Map.Entry entry : docsToCache.entrySet()) {
            documentCache.put(
                entry.getKey(),
                documentCache.createEntry((Map) entry.getValue(),
                    Collections.emptyMap()));
            LOGGER.debug("\nCaching doc done in a query {}", entry);

        }
    }

    private List> facetedSearch(final String facetInfo, final String queryInSchema)
        throws IOException {
        if (noConfig()) {
            return Collections.emptyList();
        }

        boolean isPivot = false;
        final List facetFields = new ArrayList();
        final List pivotFields = new ArrayList();
        String numRows = null;
        String mincount = null;
        final List commands = new ArrayList(Arrays.asList(StringUtils.split(facetInfo, "&")));
        String[] pivot = null;

        for (final String command : commands) {
            if (StringUtils.equals(command, "facet=true")) {
                continue;
            } else if (StringUtils.startsWith(command, "facet.field=")) {
                final String facet = StringUtils.removeStart(command, "facet.field=");
                facetFields.add(mapper.toSchemaKey(StringUtils.remove(facet, "\"")));
            } else if (StringUtils.startsWith(command, "facet.pivot=")) {
                isPivot = true;
                pivot = StringUtils.split(StringUtils.removeStart(command, "facet.pivot="), ',');
                for (int i = 0; i < pivot.length; i++) {
                    pivot[i] = mapper.toSchemaKey(StringUtils.remove(pivot[i], "\""));
                }
                pivotFields.add(StringUtils.join(pivot, ','));
            } else if (StringUtils.startsWith(command, "rows=")) {
                numRows = StringUtils.removeStart(command, "rows=");
            } else if (StringUtils.startsWith(command, "facet.mincount=")) {
                mincount = StringUtils.removeStart(command, "facet.mincount=");
            }
        }

        final FacetResults results =
            dsClient.facetedSearch(queryInSchema, facetFields, pivotFields, numRows, mincount, isPivot);
        if (results == null) {
            return Collections.emptyList();
        }

        if (StringUtils.contains(facetInfo, "cache.empty=true")) {
            // this is a multi-tallycount search
            cacheTallyFacetPivotResources(queryInSchema, results.getPivotAggregation(), pivot);
        }

        final List> unmappedDocs = new ArrayList>(results.getDocs().size());
        for (final Map schemaDoc : results.getDocs()) {
            unmappedDocs.add(mapper.fromSchema(schemaDoc));
        }
        return unmappedDocs;
    }

    /**
     * After a tally pivot search, cache the individual tallies. Cache an empty one if there was no tally for a
     * parent.
     * @param query the query with the list of parents
     * @param pivotResults facet pivot search results for the query
     */
    private void cacheTallyFacetPivotResources(final String query, final Map pivotResults,
        final String[] pivotFields) {
        if (query == null || pivotFields == null) {
            return;
        }

        final List cacheFields = new ArrayList();
        for (final String pivotField : pivotFields) {
            if (!AbstractSchemaMapper.getSchemaParentIdKey().equals(pivotField)) {
                cacheFields.add(mapper.fromSchemaKey(pivotField) + ":0");
            }
        }

        // We only know how to cache when there is 1 field outside the parent currently.
        if (cacheFields.size() != 1) {
            return;
        }

        // split into individual property:resource_value
        final String[] tokens = StringUtils.split(query);

        for (final String str : tokens) {
            final String parentPath = resourceFromQuery(str, AbstractSchemaMapper.getSchemaParentIdKey());
            if (StringUtils.isNotEmpty(parentPath)) {
                @SuppressWarnings("unchecked")
                Map result = (Map) pivotResults.get(parentPath);
                if (result == null) {
                    result = Collections.emptyMap();
                }
                Map> cacheValue;
                final CacheEntry>> entry = facetCache.get(parentPath);
                if (entry != null && entry.get() != null) {
                    cacheValue = entry.get();
                } else {
                    cacheValue = new HashMap>();
                }
                cacheValue.put(cacheFields.get(0), result);
                facetCache.put(parentPath, new CacheEntry>>(cacheValue));
                LOGGER.debug("tallycount stored in cache: {}", parentPath);
            }
        }
    }

    /**
     * Parse a string with pattern property:value and return the value.
     * @param query the query string. e.g. parent_id_s:\"/some/path/to/parent\"
     * @param property the property to strip
     * @return the value
     */
    private String resourceFromQuery(final String query, final String property) {
        if (query == null) {
            return null;
        }
        return query.replace("\"", "").replace(property + ":", "");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator findResources(final ResourceResolver resourceResolver, final String queryString,
        final String language) {
        List> docs;
        try {
            docs = searchDocuments(queryString);
        } catch (final IOException e) {
            throw new SlingIOException(e);
        }

        if (docs.isEmpty()) {
            return Collections.emptyList().iterator();
        }
        final List resources = new ArrayList();
        for (final Map doc : docs) {
            final Resource res =
                new MapResourceImpl(resourceResolver, this, (String) doc.get(AbstractSchemaMapper.getSocoKey()), doc,
                    false);
            // if there's not path, no need to check for perms. e.g. a tally faceted search
            if ((res.getPath() == null)
                    || SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(res.getPath()),
                        Session.ACTION_READ)) {
                resources.add(res);
            }
        }
        return resources.iterator();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator queryResources(final ResourceResolver resourceResolver, final String query,
        final String language) {
        // TODO Auto-generated method stub
        return null;
    }

    /**
     * Mark that this resource has been updated.
     * @param resource the updated resource
     */
    @Override
    public void update(final Resource resource) {
        if (!SocialResourceUtils.checkPermission(resource.getResourceResolver(), getJcrAclPath(resource.getPath()),
            Session.ACTION_SET_PROPERTY)) {
            LOGGER.error("Not allowed to update resource at: {}", resource.getPath());
            return;
        }
        final String path = resource.getPath();
        if (!commandsQueue.containsKey(path)) {
            commandsQueue.put(path, new CommandResource(path, APICommand.UPDATE, (MapResource) resource));
        }
    }

    private boolean isAttachment(final String resourcePath) {
        if (resourcePath.startsWith(providerBase)) {
            if (resourcePath.length() <= providerBase.length() + 1) {
                return false;
            }
            final String info = resourcePath.substring(providerBase.length() + 1);
            final int slashPos = info.indexOf('/');
            if (slashPos != -1) {
                return ATTACHMENT.equals(info.substring(0, slashPos));
            }
        }
        return false;
    }

    private String[] getPathSuffix(final String resourcePath) {
        if (resourcePath.startsWith(providerBase)) {
            if (resourcePath.length() <= providerBase.length() + 1) {
                return new String[0];
            }
            final String info = resourcePath.substring(providerBase.length() + 1);
            final int slashPos = info.indexOf('/');
            if (slashPos != -1) {
                return new String[]{info.substring(0, slashPos), info.substring(slashPos + 1)};
            }
        }
        return new String[0];
    }

    /**
     * Set the cloud config associated with this provider.
     * @param cloudConfig the cloud config
     * @deprecated use setConfig
     */
    @Deprecated
    public void setCloudConfig(final Configuration cloudConfig) {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Deprecated setCloudConfig called.");
            LOGGER.debug("StackTrace: ", new Throwable("Deprecated setCloudConfig"));
        }
        if (configSet) {
            return;
        }
        final String asiPath = cloudConfig.get(CC_ASIPATH, null);
        final String hostUrl = cloudConfig.get(CC_HOST_URL, null);
        final String reportSuite = cloudConfig.get(CC_REPORT_SUITE, null);
        final String consumerKey = cloudConfig.get(CC_CONSUMER_KEY, null);
        final String secretKey = cloudConfig.get(CC_SECRET_KEY, null);

        final ValueMap vm = new ValueMapDecorator(new HashMap());
        vm.put(CC_ASIPATH, asiPath);
        vm.put(CC_HOST_URL, hostUrl);
        vm.put(CC_REPORT_SUITE, reportSuite);
        vm.put(CC_CONSUMER_KEY, consumerKey);
        vm.put(CC_CONSUMER_KEY, secretKey);

        try {
            setConfig(srpConfigFactory.createConfiguration(vm));
        } catch (final SRPConfigurationError e) {
            LOGGER.error("Could not configure the provider: ", e);
        }
    }

    /**
     * Set the config associated with this provider.
     * @param socialConfiguration the SRP configuration
     */
    @Override
    public void setConfig(final SocialResourceConfiguration socialConfiguration) {

        if (configSet) {
            return;
        }
        if (socialConfiguration instanceof ASRPConfiguration) {
            final ASRPConfiguration asrpConfig = (ASRPConfiguration) socialConfiguration;
            mapper.setReportSuite(asrpConfig.getReportSuite());
        } else if (socialConfiguration instanceof MSRPConfiguration) {
            final MSRPConfiguration msrpConfig = (MSRPConfiguration) socialConfiguration;
            mapper.setReportSuite(msrpConfig.getTenantId());
        } else if (socialConfiguration instanceof DSRPConfiguration) {
            final DSRPConfiguration dsrpConfig = (DSRPConfiguration) socialConfiguration;
            mapper.setReportSuite(dsrpConfig.getTenantId());
        }
        dsClient.setConfiguration(socialConfiguration);
        configSet = true;
    }

    /**
     * Get the repository path associated with this provider.
     * @return the path
     */
    @Override
    public String getASIPath() {
        return providerBase;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator getMLTResults(final ResourceResolver resolver, final String query,
        final String statusFilter, final String resourceTypeFilter, final String componentFilter,
        final String[] mltFields, final int maxResults, final int minTermFreq, final int minDocFreq) {
        throw new UnsupportedOperationException("getMLTResults is not currently implemented");
    }

    /**
     * Get the path that gives the permissions for the provided path.
     * @param path the provided path
     * @return the path for ACL checking
     */
    public String getJcrAclPath(final String path) {
        return MapResourceImpl.getAclPathGivenBase(path, providerBase);
    }

    private String getSolrStatusQuery(final String status) {
        if (FIELD_STATE_APPROVED.equals(status)) {
            return APPROVED_STRING;
        } else if (FIELD_STATE_PENDING.equals(status)) {
            return PENDING_STRING;
        } else if (FIELD_STATE_DENIED.equals(status)) {
            return DENIED_STRING;
        } else if (FIELD_STATE_SPAM.equals(status)) {
            return DENIED_STRING;
        } else {
            return null;
        }
    }

    private String convertStatusFilter(final String statusFilter) {
        if (statusFilter == null) {
            return null;
        }

        final String[] pieces = statusFilter.split(":");
        if (pieces.length != 2) {
            return null;
        }

        return getSolrStatusQuery(pieces[1].trim());
    }

    /**
     * Convert resource filter query to Solr format.
     * @param resourceTypeFilter the filter
     * @return sorl format for the filter
     */
    public static String convertResourceTypeFilter(final String resourceTypeFilter) {
        // Need to return a filter with wildcard matching since the resourceType is not tokenized
        // in Solr, so given:
        // ((forum AND topic) OR (forum AND post)) -> ((*forum*topic) OR (*forum*post))
        // TODO: do this properly, for now it just handles the common cases
        // (term1 AND term2) -> (*term1*term2)
        // (term1 OR term2) -> (*term1 OR *term2)
        // ((term1 AND term1) OR (term3 AND term4)) -> ((*term1*term1) OR (*term3*term4))

        if (resourceTypeFilter == null) {
            return null;
        }

        String filter = resourceTypeFilter;
        if (!resourceTypeFilter.startsWith("(")) {
            filter = "(" + resourceTypeFilter + ")";
        }
        String replaced = filter;
        Pattern pattern = Pattern.compile(SINGLE_AND);
        Matcher matcher = pattern.matcher(filter);
        if (matcher.find()) {
            replaced = matcher.replaceAll("\\(*$1*$2\\)");
        } else {
            pattern = Pattern.compile(SINGLE_OR);
            matcher = pattern.matcher(filter);
            if (matcher.find()) {
                replaced = matcher.replaceAll("\\(*$1 OR *$2\\)");
            } else {
                pattern = Pattern.compile(AND_PLUS_OR);
                matcher = pattern.matcher(filter);
                if (matcher.find()) {
                    replaced = matcher.replaceAll("\\(\\(*$2*$3\\) OR \\(*$5*$6\\)\\)");
                }
            }
        }

        return replaced;
    }

    @Override
    public Iterator getMLTResults(final ResourceResolver resolver, final String query,
        final String statusFilter, final String resourceTypeFilter, final String componentFilter,
        final String mltField, final int maxResults, final int minTermFreq, final int minDocFreq) {
        if (noConfig()) {
            return Collections.emptyList().iterator();
        }
        List> docs;
        try {
            docs =
                dsClient.getMLTResults(query, convertStatusFilter(statusFilter),
                    convertResourceTypeFilter(resourceTypeFilter), componentFilter, mapper.toSchemaKeys(mltField),
                    maxResults, minTermFreq, minDocFreq);
        } catch (final IOException e) {
            throw new SlingIOException(e);
        }

        if (docs.isEmpty()) {
            return Collections.emptyList().iterator();
        }

        final List resources = new ArrayList();
        for (final Map document : docs) {
            final Map mappedDoc = mapper.fromSchema(document);
            documentCache.put((String) mappedDoc.get(AbstractSchemaMapper.getSocoKey()),
                documentCache.createEntry(mappedDoc, Collections.emptyMap()));

            if (SocialResourceUtils.checkPermission(resolver,
                getJcrAclPath((String) mappedDoc.get(AbstractSchemaMapper.getSocoKey())), Session.ACTION_READ)) {
                resources.add(new MapResourceImpl(resolver, this, (String) mappedDoc.get(AbstractSchemaMapper
                    .getSocoKey()), mappedDoc, false));
            }
        }
        return resources.iterator();
    }

    /**
     * Search in AS.
     * @param resolver resolver for permission checking
     * @param component The component to filter on
     * @param luceneQuery query
     * @param sortFields sort fields
     * @param offset offset to return results from
     * @param limit maximum number of results to return
     * @param requiresTotal true iff the total number of documents is required.
     * @return the search result as resources, with the Solr hit count.
     */
    @Override
    public SocialResourceSearchResult find(final ResourceResolver resolver, final String component,
        final String luceneQuery, final List sortFields, final int offset, final int limit,
        final boolean requiresTotal) {
        return find(resolver, component, luceneQuery, sortFields, offset, limit, requiresTotal, null, null);
    }

    @Override
    public SocialResourceSearchResult find(final ResourceResolver resolver, final String component,
        final String luceneQuery, final List sortFields, final int offset, final int limit,
        final boolean requiresTotal, final String fieldLanguage, final Map signals) {

        if (noConfig()) {
            return new SocialResourceSearchResult();
        }

        LOGGER.debug("AS side of SocialResourceSearchResult find Component: {} query: {}", component, luceneQuery);
        final List mappedSortFields = new ArrayList();
        if (sortFields.size() > 0) {
            for (final SearchSortField sortField : sortFields) {
                final String sortProperty = sortField.getPropertyName();
                final String mappedName = mapper.toSchemaKey(sortProperty);
                mappedSortFields.add(new SearchSortField(mappedName, sortField.isAscending()));
            }
        }

        // The query string
        final LuceneToSolr l2s = new LuceneToSolr(mapper, luceneQuery);
        final String solrQueryString = l2s.getSolrQuery();

        SocialResourceSearchResult> docs;
        try {
            docs = dsClient.find(component, solrQueryString, mappedSortFields, offset, limit, signals);
        } catch (final IOException e) {
            throw new SlingIOException(e);
        }

        // Search does not know ACLs
        final SocialResourceSearchResult searchResult = new SocialResourceSearchResult();

        for (final Map document : docs) {
            final Map mappedDocument = mapper.fromSchema(document);
            documentCache.put((String) mappedDocument.get(AbstractSchemaMapper.getSocoKey()),
                documentCache.createEntry(mappedDocument, Collections.emptyMap()));

            if (SocialResourceUtils.checkPermission(resolver,
                getJcrAclPath((String) mappedDocument.get(AbstractSchemaMapper.getSocoKey())), Session.ACTION_READ)) {
                searchResult.add(new MapResourceImpl(resolver, this, (String) mappedDocument.get(AbstractSchemaMapper
                    .getSocoKey()), mappedDocument, false));
            }
        }

        searchResult.setNumFound(docs.getNumFound());

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("AS side of SocialResourceSearchResult find.  Num Solr hits {}", docs.getNumFound());
        }

        return searchResult;
    }

    @Override
    public SocialResourceSearchResult> findFacets(final ResourceResolver resolver,
        final List parentFilter, final List fieldNames, final List facetRanges,
        final String scoredQueryString, final int maxFacetCount, final List sortFields,
        final int offset, final int pageSize) {

        if (noConfig()) {
            return new SocialResourceSearchResult>();
        }
        // convert the scored and unscored query strings into the proper solr schema
        final String scoredQueryInSchema = parseQuery(scoredQueryString);
        if (null == scoredQueryInSchema) {
            return new SocialResourceSearchResult>();
        }
        final List pathFilter = new ArrayList();
        final List pathExclusions = new ArrayList();
        final InternalSocialResourceUtilities isru = resolver.adaptTo(InternalSocialResourceUtilities.class);
        final SocialResourceUtilities sru = resolver.adaptTo(SocialResourceUtilities.class);
        if (null != parentFilter && !parentFilter.isEmpty()) {
            Resource root = null;
            for (String pathToCheck : parentFilter) {
                if (!pathToCheck.startsWith(getASIPath())) {
                    root = resolver.getResource(pathToCheck);
                    pathToCheck = sru.resourceToUGCStoragePath(root);
                } else {
                    root = resolver.getResource(sru.ugcToResourcePath(pathToCheck));
                }
                if (null == root || root instanceof NonExistingResource) {
                    LOGGER.warn("attempted to search a non-existing resource path");
                    continue;
                }
                if (SocialResourceUtils.checkPermission(resolver, pathToCheck, Session.ACTION_READ)) {
                    pathFilter.add(pathToCheck);
                    final List exc =
                        isru.getUnreadableDescendants(resolver, root, Collections.emptyList());
                    pathExclusions.addAll(exc);
                }
            }
        } else {
            LOGGER.error("attempted to search without any parent paths");
            return new SocialResourceSearchResult>();
        }
        if (pathFilter.isEmpty()) {
            return new SocialResourceSearchResult>();
        }

        Collections.sort(pathFilter);
        if (!pathExclusions.isEmpty()) {
            Collections.sort(pathExclusions);
        }
        // build the cache key
        final StringBuilder baseKey = new StringBuilder("q=" + scoredQueryString.replaceAll("&", "&"));
        baseKey.append("&in=");
        boolean first = true;
        for (final String path : pathFilter) {
            if (first) {
                first = false;
            } else {
                baseKey.append(",");
            }
            baseKey.append(path.replaceAll("&", "&"));
        }
        if (!pathExclusions.isEmpty()) {
            first = true;
            baseKey.append("¬In=");
            for (final String path : pathExclusions) {
                if (first) {
                    first = false;
                } else {
                    baseKey.append(",");
                }
                baseKey.append(path.replaceAll("&", "&"));
            }
        }
        final String cacheKey = baseKey.toString();
        Map> fromCache = null;
        if (facetCache.get(cacheKey) != null) {
            fromCache = facetCache.get(cacheKey).get();
        }
        final Map> unmappedCountResult =
            new LinkedHashMap>();
        final List mappedCountFields = new ArrayList();
        if (null != fieldNames && !fieldNames.isEmpty()) {
            for (final String fieldName : fieldNames) {
                if (fromCache != null && fromCache.containsKey(fieldName)) {
                    unmappedCountResult.put(fieldName, fromCache.get(fieldName));
                } else {
                    mappedCountFields.add(mapper.toSchemaKey(fieldName));
                }
            }
        }
        final Map> unmappedRangeResult =
            new LinkedHashMap>();
        final List mappedRangeFields = new ArrayList();

        if (null != facetRanges && !facetRanges.isEmpty()) {
            for (final FacetRangeField rangeField : facetRanges) {
                final String key = facetRangeFieldHash(rangeField);
                if (fromCache != null && fromCache.containsKey(key)) {
                    unmappedRangeResult.put(rangeField.getFieldName(), fromCache.get(key));
                } else {
                    final String mappedName = mapper.toSchemaKey(rangeField.getFieldName());
                    FacetRangeField mappedField;
                    if (rangeField.getIsDateRange()) {
                        if (rangeField instanceof IntervalFacetRangeField) {
                            mappedField =
                                new IntervalFacetRangeField(mappedName, rangeField.getStartDate(),
                                    rangeField.getEndDate(), ((IntervalFacetRangeField) rangeField).getGaps());
                        } else {
                            mappedField =
                                new FacetRangeField(mappedName, rangeField.getStartDate(), rangeField.getEndDate(),
                                    rangeField.getDateGap());
                        }
                    } else {
                        mappedField =
                            new FacetRangeField(mappedName, rangeField.getStartValue(), rangeField.getEndValue(),
                                rangeField.getGapValue());
                    }
                    mappedRangeFields.add(mappedField);
                }
            }
        }

        final SocialResourceSearchResult> searchResult =
            new SocialResourceSearchResult>();
        final List sortFieldsInSchema = new ArrayList();
        if (null != sortFields && sortFields.size() > 0) {
            for (final SearchSortField sortField : sortFields) {
                sortFieldsInSchema.add(new SearchSortField(mapper.toSchemaKey(sortField.getPropertyName()), sortField
                    .isAscending()));
            }
        }
        try {
            final SocialResourceSearchResult> result =
                dsClient.findFacets(pathFilter, pathExclusions, mappedCountFields, mappedRangeFields,
                    scoredQueryInSchema, maxFacetCount, sortFieldsInSchema, offset, pageSize);

            for (final Map document : result) {
                final Map unmappedDocument = mapper.fromSchema(document);
                documentCache.put((String) unmappedDocument.get(AbstractSchemaMapper.getSocoKey()),
                    documentCache.createEntry(unmappedDocument, Collections.emptyMap()));

                searchResult.add(unmappedDocument);
            }
            final FacetSearchResult facetSearchResult = result.getFacetSearchResult();
            if (!mappedCountFields.isEmpty()) {
                final Map> countResult = facetSearchResult.getCountResult();
                for (final String mappedFacetField : countResult.keySet()) {
                    unmappedCountResult
                        .put(mapper.fromSchemaKey(mappedFacetField), countResult.get(mappedFacetField));
                }
            }
            if (!mappedRangeFields.isEmpty()) {
                final Map> rangeResult = facetSearchResult.getRangeResult();
                for (final String mappedFacetField : rangeResult.keySet()) {
                    unmappedRangeResult
                        .put(mapper.fromSchemaKey(mappedFacetField), rangeResult.get(mappedFacetField));
                }
            }
            searchResult.setFacetSearchResult(new FacetSearchResult(unmappedCountResult, unmappedRangeResult));
            searchResult.setNumFound(result.getNumFound());
            return searchResult;
        } catch (final IOException e) {
            throw new SlingIOException(e);
        }
    }

    @Override
    public Map> findFacets(final ResourceResolver resolver,
        final List fieldNames, final String resourceTypeFilter, final String componentFilter, final int count) {

        return findFacets(resolver, fieldNames, resourceTypeFilter, componentFilter, count, false);
    }

    @Override
    public Map> findFacets(final ResourceResolver resolver,
        final List fieldNames, final String resourceTypeFilter, final String componentFilter,
        final int count, final boolean visibleOnly) {
        return this.findFacets(resolver, fieldNames, resourceTypeFilter, componentFilter, count, visibleOnly, false);
    }

    @Override
    public Map> findFacets(final ResourceResolver resolver,
        final List fieldNames, final String resourceTypeFilter, final String componentFilter,
        final int count, final boolean visibleOnly, final boolean includeChildren) {
        final FacetSearchResult facets =
            findFacets(resolver, fieldNames, Collections.emptyList(), resourceTypeFilter,
                componentFilter, count, visibleOnly, includeChildren);
        return facets.getCountResult();
    }

    @Override
    public Map> findFacetRanges(final ResourceResolver resolver,
        final List rangeFields, final String resourceTypeFilter, final String componentFilter,
        final int count) {
        return findFacetRanges(resolver, rangeFields, resourceTypeFilter, componentFilter, count, false);
    }

    @Override
    public Map> findFacetRanges(final ResourceResolver resolver,
        final List rangeFields, final String resourceTypeFilter, final String componentFilter,
        final int count, final boolean visibleOnly) {
        final FacetSearchResult facets =
            findFacets(resolver, Collections.emptyList(), rangeFields, resourceTypeFilter, componentFilter,
                count, visibleOnly);
        return facets.getRangeResult();
    }

    @Override
    public FacetSearchResult findFacets(final ResourceResolver resolver, final List countFields,
        final List rangeFields, final String resourceTypeFilter, final String componentFilter,
        final int count) {
        return findFacets(resolver, countFields, rangeFields, resourceTypeFilter, componentFilter, count, false);
    }

    @Override
    public FacetSearchResult findFacets(final ResourceResolver resolver, final List countFields,
        final List rangeFields, final String resourceTypeFilter, final String componentFilter,
        final int count, final boolean visibleOnly) {
        return findFacets(resolver, countFields, rangeFields, resourceTypeFilter, componentFilter, count,
            visibleOnly, false);
    }

    @Override
    public FacetSearchResult findFacets(final ResourceResolver resolver, final List countFields,
        final List rangeFields, final String resourceTypeFilter, final String componentFilter,
        final int count, final boolean visibleOnly, final boolean includeChidren) {
        if (noConfig()) {
            return new FacetSearchResult();
        }

        // Map to schema names. Can't do it in impls since no access to the mapper.
        Map> fromCache = null;
        String facetCacheKey;
        if (componentFilter != null) {
            if (resourceTypeFilter != null) {
                facetCacheKey = componentFilter + ":" + resourceTypeFilter;
            } else {
                facetCacheKey = componentFilter;
            }
            if (facetCache.get(facetCacheKey) != null) {
                fromCache = facetCache.get(facetCacheKey).get();
            }
        } else {
            // this is not allowed to be null
            throw new SlingIOException(new IOException("componentFilter not specified for faceted search"));
        }
        // check whether the user has access to this content
        final String pathToCheck;
        if (componentFilter.startsWith(getASIPath())) {
            pathToCheck = componentFilter;
        } else {
            pathToCheck = getASIPath() + componentFilter;
        }
        if (!SocialResourceUtils.checkPermission(resolver, pathToCheck, Session.ACTION_READ)) {
            // if the user doesn't have permission to check this repository, return an empty result
            return new FacetSearchResult();
        }
        boolean doSearch = false;  // Assume everything is in cache

        final Map> unmappedCountResult =
            new LinkedHashMap>();
        final List mappedCountFields = new ArrayList();
        for (final String fieldName : countFields) {
            final String key = (visibleOnly ? fieldName + ":1" : fieldName + ":0");
            if (fromCache != null && fromCache.containsKey(key)) {
                unmappedCountResult.put(fieldName, fromCache.get(key));
            } else {
                mappedCountFields.add(mapper.toSchemaKey(fieldName));
                doSearch = true; // Need to do search.
            }
        }
        // Range fields. Cache key is a hash of the rage info
        final Map> unmappedRangeResult =
            new LinkedHashMap>();
        final List mappedRangeFields = new ArrayList();

        for (final FacetRangeField rangeField : rangeFields) {
            final String facetRangeHash = facetRangeFieldHash(rangeField);
            final String key = (visibleOnly ? facetRangeHash + ":1" : facetRangeHash + ":0");
            if (fromCache != null && fromCache.containsKey(key)) {
                unmappedRangeResult.put(rangeField.getFieldName(), fromCache.get(key));
            } else {
                doSearch = true;

                final String mappedName = mapper.toSchemaKey(rangeField.getFieldName());
                FacetRangeField mappedField;
                if (rangeField.getIsDateRange()) {
                    if (rangeField instanceof IntervalFacetRangeField) {
                        mappedField =
                            new IntervalFacetRangeField(mappedName, rangeField.getStartDate(),
                                rangeField.getEndDate(), ((IntervalFacetRangeField) rangeField).getGaps());
                    } else {
                        mappedField =
                            new FacetRangeField(mappedName, rangeField.getStartDate(), rangeField.getEndDate(),
                                rangeField.getDateGap());
                    }
                } else {
                    mappedField =
                        new FacetRangeField(mappedName, rangeField.getStartValue(), rangeField.getEndValue(),
                            rangeField.getGapValue());
                }
                mappedRangeFields.add(mappedField);
            }
        }

        // If not in cache, get values.
        FacetSearchResult searchResult = null;
        try {
            if (doSearch) {
                searchResult =
                    dsClient.findFacets(mappedCountFields, mappedRangeFields, resourceTypeFilter, componentFilter,
                        count, visibleOnly, includeChidren);
                // Have to map back to CQ names in the result.
                final Map> countResult = searchResult.getCountResult();
                for (final Entry> entry : countResult.entrySet()) {
                    final String unmappedName = mapper.fromSchemaKey(entry.getKey());
                    unmappedCountResult.put(unmappedName, entry.getValue());
                }
                final Map> rangeResult = searchResult.getRangeResult();
                for (final Entry> entry : rangeResult.entrySet()) {
                    final String unmappedName = mapper.fromSchemaKey(entry.getKey());
                    unmappedRangeResult.put(unmappedName, entry.getValue());
                }

                // Update cache
                final Map> cacheEntry = new HashMap>();
                for (final FacetRangeField rangeField : rangeFields) {
                    final String facetRangeHash = facetRangeFieldHash(rangeField);
                    final String key = (visibleOnly ? facetRangeHash + ":1" : facetRangeHash + ":0");
                    cacheEntry.put(key, unmappedRangeResult.get(rangeField.getFieldName()));
                }
                // Cache count fields
                for (final String countField : countFields) {
                    final String key = (visibleOnly ? countField + ":1" : countField + ":0");
                    cacheEntry.put(key, unmappedCountResult.get(countField));
                }
                facetCache.put(facetCacheKey, new CacheEntry>>(cacheEntry));
            }

            searchResult = new FacetSearchResult(unmappedCountResult, unmappedRangeResult);

        } catch (final IOException e) {
            throw new SlingIOException(e);
        }

        return searchResult;
    }

    /**
     * Endpoint for using batch methods defined by the SocialDataService implementing layer.
     * @param key - the key
     * @return - the result set
     * @throws IOException
     */
    private void deleteDocumentFromCache(final String key) {

        // Need to update the count cache entry of parent to reflect child deletion. However, for
        // performance
        // reasons, only if the doc being deleted is already in the cache. That means it's possible
        // that the parent child count can be out of date and not reflect on the doc deletion

        final CacheEntry> cached = documentCache.get(key);
        if (cached != null) {
            final Map docCacheEntry = cached.get();
            if ((docCacheEntry != null) && docCacheEntry.containsKey(AbstractSchemaMapper.getSocoParentIdKey())) {
                // just remove from cache for simplicity instead of doing math and updating the cache
                // count value
                removeAllMatchesFromCountCache((String) docCacheEntry.get(AbstractSchemaMapper.getSocoParentIdKey()));
                facetCache.remove(docCacheEntry.get(AbstractSchemaMapper.getSocoParentIdKey()));
            }
        } else {
            LOGGER.debug("Did not find key {} in document cache, so did not delete the parent in count cache.", key);
        }

        documentCache.remove(key);
        removeAllMatchesFromCountCache(key);
        facetCache.remove(key);
    }

    private void removeAllMatchesFromCountCache(final String key) {
        List> toRemove = new ArrayList>();

        for (final List keys : allCountCache.keySet()) {
            if (keys.size() > 0 && keys.get(0).equals(key)) {
                toRemove.add(keys);
            }
        }

        allCountCache.removeAll(toRemove);

        toRemove = new ArrayList>();

        for (final List keys : visibleCountCache.keySet()) {
            if (keys.size() > 0 && keys.get(0).equals(key)) {
                toRemove.add(keys);
            }
        }

        visibleCountCache.removeAll(toRemove);
    }

    /**
     * Clear the cache.
     */
    public void clearCache() {
        this.documentCache.clear();
        this.visibleCountCache.clear();
        this.allCountCache.clear();
        this.facetCache.clear();
    }

    @Override
    public void incrementBy(final Resource resource, final Map incrementMap) throws SlingIOException,
        PersistenceException {
        if (!SocialResourceUtils.checkPermission(resource.getResourceResolver(), getJcrAclPath(resource.getPath()),
            Session.ACTION_SET_PROPERTY)) {
            LOGGER.error("Not allowed to update resource at: {}", resource.getPath());
            return;
        }
        ModifiableValueMap map;
        boolean inQueue = false;
        CommandResource commandResource = null;
        if (commandsQueue.containsKey(resource.getPath())) {
            commandResource = commandsQueue.get(resource.getPath());
            if (commandResource.methodType == APICommand.DELETE) {
                throw new PersistenceException("attempting to increment a resource that is being deleted - "
                        + resource.getPath());
            } else {
                map = commandResource.getResource().adaptTo(ModifiableValueMap.class);
                inQueue = true;
            }
        } else {
            map = resource.adaptTo(ModifiableValueMap.class);
        }
        if (map.containsKey(INC) && !(map.get(INC) == null) && !(map.get(INC) instanceof Map)) {
            throw new PersistenceException("The $inc field has already been used by an invalid (non-map) property - "
                    + resource.getPath());
        }

        Map mergedIncrementMap;
        if (map.containsKey(INC) && !(map.get(INC) == null)) {
            mergedIncrementMap = (Map) map.get(INC);
        } else {
            mergedIncrementMap = new HashMap();
        }

        for (final Entry incEntry : incrementMap.entrySet()) {
            final String property = incEntry.getKey();
            final Long increment = incEntry.getValue();
            if (map.containsKey(property) && !(map.get(property) instanceof Long)) {
                try {
                    Long.parseLong(map.get(property).toString());
                } catch (final NumberFormatException e) {
                    throw new SlingIOException(new IOException("attempted to increment a non-integer property - "
                            + resource.getPath(), e));
                }
            }

            if (mergedIncrementMap.containsKey(property)) {
                mergedIncrementMap.put(property, mergedIncrementMap.get(property) + increment);
            } else {
                mergedIncrementMap.put(property, increment);
            }
        }

        map.put(INC, mergedIncrementMap);
        if (!inQueue) {
            final MapResource mapResource =
                new MapResourceImpl(resource.getResourceResolver(), this, resource.getPath(), map, false);
            commandResource = new CommandResource(resource.getPath(), APICommand.INCREMENT, mapResource);
        } else {
            final MapResource mapResource =
                new MapResourceImpl(resource.getResourceResolver(), this, resource.getPath(), map, false);
            commandResource = new CommandResource(resource.getPath(), commandResource.methodType, mapResource);
        }
        commandsQueue.put(resource.getPath(), commandResource);
    }

    @Override
    public void incrementBy(final Resource resource, final String property, final Long increment)
        throws SlingIOException, PersistenceException {

        final Map incrementMap = new HashMap();
        incrementMap.put(property, increment);
        incrementBy(resource, incrementMap);
        return;

    }

    @Override
    public void increment(final Resource resource, final String property) throws PersistenceException {
        incrementBy(resource, property, 1L);
    }

    @Override
    public void decrement(final Resource resource, final String property) throws PersistenceException {
        incrementBy(resource, property, -1L);
    }

    @Override
    public long getCount(final Resource resource, final String property) {
        final ValueMap map = resource.adaptTo(ValueMap.class);
        if (map.containsKey(property)) {
            final Object value = map.get(property);
            if (value instanceof Long) {
                return (Long) value;
            } else if (value instanceof Integer) {
                return ((Integer) value).longValue();
            }
        }
        return 0;
    }

    @Override
    public List getLanguages() {
        final List result = new ArrayList();
        result.add(SocialResourceProvider.SOLRQUERY);
        return result;
    }

    @Override
    public String getContentType() {
        return "cq:Comment";
    }

    @Override
    public ProviderMetaData getMetaData() {
        if (this.metaData == null) {
            boolean bMSRPClient = false;
            if (this.dsClient != null) {
                bMSRPClient = (this.dsClient.getDSClient().equals("ASRP")) ? false : true;
            }
            this.metaData = new AbstractProviderMetaData(bMSRPClient) {
            };
        }
        return this.metaData;
    }

    /**
     * Hash a range field for use as cache key.
     * @param rangeField the range field to hash
     * @return the hash
     */
    public String facetRangeFieldHash(final FacetRangeField rangeField) {
        final StringBuilder facetRangeHash = new StringBuilder(rangeField.getFieldName()).append("-");
        if (rangeField.getIsDateRange()) {
            final SimpleDateFormat dateFormat = new SimpleDateFormat(RANGE_DATE_FORMAT);
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            final String startRange = dateFormat.format(rangeField.getStartDate());
            final String endRange = dateFormat.format(rangeField.getEndDate());
            if (rangeField instanceof IntervalFacetRangeField) {
                facetRangeHash.append(startRange).append(endRange)
                    .append(StringUtils.join(((IntervalFacetRangeField) rangeField).getGaps(), ","));
            } else {
                facetRangeHash.append(startRange).append(endRange).append(rangeField.getDateGap());
            }
        } else {
            facetRangeHash.append(rangeField.getStartValue()).append("-").append(rangeField.getEndValue())
                .append("-").append(rangeField.getGapValue());
        }
        return facetRangeHash.toString();
    }

    @Override
    public Map getResources(final ResourceResolver resolver, final List paths) {
        if (noConfig()) {
            return Collections.emptyMap();
        }

        final List fetchPaths = new ArrayList();
        final Map retVal = new HashMap(paths.size());

        final List allowedPaths = new ArrayList();
        for (final String path : paths) {
            if (!StringUtils.startsWith(path, providerBase)) {
                retVal.put(path, new NonExistingResource(resolver, path));
            } else if (!SocialResourceUtils.checkPermission(resolver, getJcrAclPath(path), Session.ACTION_READ)) {
                retVal.put(path, new NonExistingResource(resolver, path));
            } else {
                allowedPaths.add(path);
            }
        }

        for (final String path : allowedPaths) {

            final Resource pendingRes = getPendingResource(resolver, path);
            if (pendingRes != null) {
                retVal.put(path, pendingRes);
            } else {
                final CacheEntry> cacheVal = documentCache.get(path);
                if (cacheVal != null) {
                    LOGGER.debug("Got {} from cache.", path);
                    final Map doc = new HashMap(cacheVal.get());
                    retVal.put(path, new MapResourceImpl(resolver, this, path, doc, false));
                } else {
                    fetchPaths.add(path);
                }
            }
        }

        if (!fetchPaths.isEmpty()) {

            Map results;
            try {
                results = dsClient.readDocuments(fetchPaths);
            } catch (final IOException e) {
                throw new SlingIOException(e);
            }
            postProcessDocuments(resolver, results, retVal);
        }

        return retVal;
    }

    private void postProcessDocuments(final ResourceResolver resolver, final Map results,
        final Map retVal) {

        for (final Entry result : results.entrySet()) {
            if (result.getValue() instanceof DocumentResult) {
                final Map mappedDocument =
                    mapper.fromSchema(((DocumentResult) result.getValue()).getDocument());
                documentCache.put(result.getKey(),
                    documentCache.createEntry(mappedDocument, Collections.emptyMap()));
                if (retVal != null) {
                    retVal.put(result.getKey(), new MapResourceImpl(resolver, this, result.getKey(), mappedDocument,
                        false));
                }
            } else if (result.getValue() instanceof AttachmentResult) {
                final Map mappedAttachment =
                    mapper.fromAttachmentSchema(((AttachmentResult) result.getValue()).getAttachment());
                documentCache.put(result.getKey(),
                    documentCache.createEntry(mappedAttachment, Collections.emptyMap()));
                if (retVal != null) {
                    retVal.put(result.getKey(), new MapResourceImpl(resolver, this, result.getKey(),
                        mappedAttachment, true));
                }
            } else if (result.getValue() instanceof ErrorResult) {
                final ErrorResult err = (ErrorResult) result.getValue();
                if (err.getCode() == ErrorResult.ERROR_DOCUMENT_NOT_FOUND) {
                    documentCache.put(
                        result.getKey(),
                        documentCache.createEntry(Collections.emptyMap(),
                            Collections.emptyMap()));
                } else {
                    LOGGER.warn("Could not fetch document {}. Error was {}", result.getKey(), err.getCode());
                }
                if (retVal != null) {
                    retVal.put(result.getKey(), new NonExistingResource(resolver, result.getKey()));
                }
            }
        }
    }

    @Override
    public InputStream getAttachmentInputStream(final ResourceResolver resolver, final String path)
        throws IOException {

        if (noConfig() || !StringUtils.startsWith(path, providerBase)
                || !SocialResourceUtils.checkPermission(resolver, getJcrAclPath(path), Session.ACTION_READ)) {
            return null;
        }

        return dsClient.getAttachmentInputStream(path);
    }

    // TODO - when the update ResourceProvider spec is released, un-comment this override for it
    // see
    // https://github.com/apache/sling/blob/trunk/bundles/api/src/main/java/org/apache/sling/spi/resource/provider/ResourceProvider.java#L645
    /*
     * @Override public boolean move(final ResolverContext ctx, final String srcAbsPath, final String destAbsPath)
     * throws PersistenceException { return (null != move(ctx.getResourceResolver(), srcAbsPath, destAbsPath)); }
     */
    @Override
    public Resource move(final ResourceResolver resolver, final String srcAbsPath, final String destAbsPath)
        throws PersistenceException {
        if (!SocialResourceUtils.checkPermission(resolver, getJcrAclPath(destAbsPath), Session.ACTION_ADD_NODE)) {
            LOGGER.error("Not allowed to update resource at: {}", destAbsPath);
            return null;
        }
        final Resource src = resolver.getResource(srcAbsPath);
        if (null == src) {
            throw new PersistenceException("Resource to be moved not found at: " + srcAbsPath);
        }
        final Resource parent = resolver.getResource(destAbsPath);
        if (null == parent) {
            throw new PersistenceException("Intended parent of a moved resource not found at: " + destAbsPath);
        }
        Resource newResource = null;
        try {
            newResource = copy(resolver, src, parent, null);
            delete(resolver, srcAbsPath);
            return newResource;
        } catch (final PersistenceException e) {
            if (null != newResource) {
                // if copy succeeded but delete failed, delete the new resource subgraph under newResource before
                // tossing the PersistenceException up the stack
                resolver.delete(newResource);
            }
            throw e;
        }
    }

    private Resource copy(final ResourceResolver resolver, final Resource child, final Resource parent,
        String rootCommentSystem) throws PersistenceException {
        final Map props = child.adaptTo(Map.class);
        final String parentPath = parent.getPath();
        props.put(AbstractSchemaMapper.getSocoParentIdKey(), parentPath);
        String newPath;
        if (!SocialResourceUtils.isCloudUGC(parentPath)) {
            // presumably the new parent is a root comment system
            final SocialResourceUtilities socialResourceUtils =
                child.getResourceResolver().adaptTo(SocialResourceUtilities.class);
            newPath = socialResourceUtils.resourceToUGCStoragePath(parent) + "/" + child.getName();
            rootCommentSystem = parentPath;
            props.put(InternalSocialResourceUtilities.PN_CS_ROOT, rootCommentSystem);
        } else {
            // special case for translation, since it needs a reference to the id of the translated node
            final String cqdata = AbstractSchemaMapper.getSchemaCQData();
            if (child.getName().equals("translation") && props.containsKey(cqdata)
                    && props.get(cqdata) instanceof Map && ((Map) props.get(cqdata)).containsKey("id")) {
                ((Map) props.get(cqdata)).put("id", parentPath);
            }
            newPath = parentPath + "/" + child.getName();
        }
        if (newPath.equals(child.getPath())) {
            throw new PersistenceException("Cannot move item: Origin path and destination path are the same");
        }
        props.put(AbstractSchemaMapper.getSocoKey(), newPath);
        final Resource newChild = create(resolver, newPath, props);
        if (null == newChild) {
            throw new PersistenceException("Failed to copy resource at " + child.getPath() + " to " + newPath);
        }
        if (props.containsKey(InternalSocialResourceUtilities.PN_ATTACHMENT_LIST)) {
            final String[] attachments = (String[]) props.get(InternalSocialResourceUtilities.PN_ATTACHMENT_LIST);
            moveAttachments(resolver, newChild, attachments, rootCommentSystem, newPath);
        }
        if (child.hasChildren()) {
            try {
                for (final Resource grandchild : child.getChildren()) {
                    if (isAttachment(grandchild.getPath())) { // handled in social:attachments, above
                        continue;
                    }
                    copy(resolver, grandchild, newChild, rootCommentSystem);
                }
            } catch (final PersistenceException e) {
                // if we had a failure during copy, delete the copied resource before tossing exception up the stack
                delete(resolver, newPath);
                throw e;
            }
        }
        return newChild;
    }

    private void moveAttachments(final ResourceResolver resolver, final Resource newChild,
        final String[] attachments, final String rootCommentSystem, final String newPath) throws PersistenceException {

        final List newAttachments = new ArrayList();

        for (final String attachment : attachments) {
            final Resource attResource = getResource(resolver, attachment);
            if (null == attResource) {
                continue;
            }
            final Map mvm = attResource.adaptTo(Map.class);
            if (null != rootCommentSystem) {
                mvm.put(InternalSocialResourceUtilities.PN_CS_ROOT, rootCommentSystem);
            }
            mvm.put(AbstractSchemaMapper.getSocoParentIdKey(), newPath);
            mvm.put(AbstractSchemaMapper.getSocoKey(), newPath + "/" + attResource.getName());
            try {
                mvm.put("nt:file", getAttachmentInputStream(resolver, attachment));
                ((InputStream) mvm.get("nt:file")).reset();
            } catch (final IOException e) {
                throw new PersistenceException("Could not get attachment input stream", e);
            }
            if (mvm.containsKey(InternalSocialResourceUtilities.PN_DATE)) {
                // not needed by attachments, but can be inherited from the content
                mvm.remove(InternalSocialResourceUtilities.PN_DATE);
            }
            final Resource newAttResource = create(resolver, newPath + "/" + attResource.getName(), mvm);
            newAttachments.add(newAttResource.getPath());
        }
        newChild.adaptTo(ModifiableValueMap.class).put(InternalSocialResourceUtilities.PN_ATTACHMENT_LIST,
            newAttachments.toArray(attachments));
    }

    /**
     * @return true if config is not set of this SRP client.
     */
    private boolean noConfig() {
        if (dsClient.isConfigured()) {
            return false;
        }
        LOGGER.warn("SRP being used without configuration.");
        LOGGER.debug("Stack trace:", new RepositoryException(""));

        return true;

    }

    @Override
    public int getCommentIndex(final String path, final String baseType, final ResourceResolver resourceResolver,
        final String commentToCheck, final boolean visibleOnly) {
        int index = 0;

        if (!noConfig() && SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(path), Session.ACTION_READ)) {
            index = dsClient.getCommentIndex(AbstractSchemaMapper.getSchemaParentIdKey(), path, commentToCheck, baseType, visibleOnly);
        } else {
            LOGGER.warn("Missing srp client config or persmission not granted. Direct link to comment won't work");
        }
        return index;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public SocialResourceSearchResult getCountAndListChildren(final String path, final String baseType,
        final ResourceResolver resourceResolver, final int offset, final int size,
        final List> sortBy, final boolean visibleOnly) {
        return getCountAndListChildren(path, baseType, resourceResolver, offset, size, sortBy, visibleOnly,
            visibleOnly ? Counts.VISIBLE_ONLY : Counts.ALL);
    }

    /**
     * Retrieves a 'page' of children at a given path of the given type, sorted by the given criteria. Default
     * visibility criteria are applied if visibleOnly is true. Otherwise all children are considered. Children
     * @param path where the children should be found.
     * @param baseType all children returned will be of this base type.
     * @param resourceResolver the resource resolver to use.
     * @param offset where to start in the list of the candidate children
     * @param size how many of the candidate children to return.
     * @param sortBy how to sort the candidate children.
     * @param visibleOnly whether to restrict the candidates according to default visibility criteria.
     * @param prefetch a PrefetchPerChild specifying what to prefetch in relation to each child found.
     * @return the matching resources and the count of matching resources.
     */
    private SocialResourceSearchResult getCountAndListChildren(final String path, final String baseType,
        final ResourceResolver resourceResolver, final int offset, final int size,
        final List> sortBy, final boolean visibleOnly, final Counts counts) {
        if (noConfig()
                || !SocialResourceUtils.checkPermission(resourceResolver, getJcrAclPath(path), Session.ACTION_READ)) {
            return new SocialResourceSearchResult(); // return emtpy list

        }

        final Long cacheCounts;
        final CountCache countCache = counts == Counts.VISIBLE_ONLY ? visibleCountCache : allCountCache;
        final List cacheKey = new ArrayList();

        if (counts != Counts.NONE) {
            cacheKey.add(path);
            // Cache has to take the child type into account.
            if (StringUtils.isNotEmpty(baseType)) {
                cacheKey.add(baseType);
            }
            CacheEntry cacheEntry = null;

            cacheEntry = countCache.get(cacheKey);
            if (cacheEntry != null) { // in cache
                cacheCounts = cacheEntry.get();
            } else {
                cacheCounts = -1L;
            }
        } else {
            cacheCounts = -1L;
        }

        final boolean countCacheHit = cacheCounts != null && cacheCounts >= 0;

        /*
         * at this point we have settled whether we have a hit on the count cache, and it is in cacheCounts *iff*
         * countCacheHit is true.
         */

        final boolean getChildren = offset >= 0;
        final int fetchSize = getChildren ? size : 1;
        ChildCountAndList resourcesFromCache = null;
        String key = null;

        /*
         * Now check for a hit on the children cache.
         */
        if (getChildren) {
            // build a unique key to cache results
            key = buildListChildrenCacheKey(path, baseType, offset, size, sortBy, visibleOnly);

            final CacheEntry> cacheVal = stringListCache.get(key);
            if (cacheVal != null) {
                final List cachePaths = cacheVal.get();
                resourcesFromCache = childrenListFromCache(resourceResolver, cachePaths);
                if (resourcesFromCache != null) {
                    LOGGER.debug("listChildren results from cache with parent {}.", path);
                }
            }
        }

        /*
         * Use a separate block to handle a children cache miss, avoid nesting this block in the one above to improve
         * clarity.
         */
        if (getChildren && resourcesFromCache == null) {

            List> mappedSortBy = null;

            if (sortBy != null && offset >= 0) {
                mappedSortBy = new ArrayList>(sortBy.size());
                for (final Entry sortEntry : sortBy) {
                    mappedSortBy.add(new AbstractMap.SimpleEntry(mapper.toSchemaKey(sortEntry
                        .getKey()), sortEntry.getValue()));
                }
            }

            try {
                final BrowseDocumentsResult result =
                    dsClient.browseDocuments(AbstractSchemaMapper.getSchemaParentIdKey(), path, baseType, size,
                        (int) Math.floor((double) offset / fetchSize), mappedSortBy, visibleOnly, countCacheHit
                            ? Counts.NONE : counts);

                // browseDocuments definitely settles our children cache miss
                final List> documentResults =
                    result != null ? result.getDocuments() : Collections.>emptyList();
                resourcesFromCache = updateCachesForListChildren(resourceResolver, documentResults, path, key);

                /*
                 * Before we return first load the returned counts, documents and attachments into the cache.
                 */
                if (result != null && SocialResourcePrefetch.getPrefetches().hasNext()) {
                    final Map> allBaseTypeCounts = result.getBaseTypeCounts();
                    for (final Entry> e : allBaseTypeCounts.entrySet()) {
                        final String countBaseType = e.getKey();
                        final Map baseTypeCounts = e.getValue();
                        for (final Entry ce : baseTypeCounts.entrySet()) {
                            final String countPath = ce.getKey();
                            final Long count = ce.getValue();

                            final List countCacheKey = new ArrayList();
                            countCacheKey.add(countPath);
                            // Cache has to take the child type into account.
                            if (StringUtils.isNotEmpty(countBaseType)) {
                                countCacheKey.add(countBaseType);
                            }
                            countCache.put(countCacheKey, createEntry(count));
                        }

                    }
                    postProcessDocuments(resourceResolver, result.getRelatedDocuments(), null);
                    postProcessDocuments(resourceResolver, result.getAttachments(), null);

                }

                /*
                 * the browseDocuments call *may* settle our count cache miss, but if we didn't request counts don't
                 */
                if (counts != Counts.NONE && !countCacheHit && result != null) {
                    final long count = result.getCount();
                    /*
                     * Only cache a returned count if we requested the count. This helps avoid any bugs where we
                     * change behavior of existing code.
                     */
                    if (count >= 0) {
                        countCache.put(cacheKey, createEntry(count));
                        resourcesFromCache.setNumFound(count);
                        // we have children (not from cache) and counts (not from cache), we're ready to return.
                        return resourcesFromCache;
                    }
                }

            } catch (final IOException e) {
                throw new SlingIOException(e);
            }
        }

        if (resourcesFromCache == null) {
            resourcesFromCache = new ChildCountAndList();
        }
        // Any case not covered by browseDocuments returning both the children and count we wanted:
        // * Deliberately getting count only (with or without cache miss).
        // * No counts requested, getting resources only (with or without a cache miss).
        // * Counts came from cache, but resources did not.
        // * ASRP servers which don't yet support counts in browseDocuments/callListAPI.
        if (countCacheHit) {
            resourcesFromCache.setNumFound(cacheCounts);
        } else {
            resourcesFromCache.setCountChildren(new Callable() {
                @Override
                public Long call() {
                    try {
                        final long count = dsClient.countChildren(path, baseType, visibleOnly);
                        countCache.put(cacheKey, createEntry(count));
                        return count;
                    } catch (final IOException ioe) {
                        LOGGER.warn("Count of children of {} on {} failed.", baseType, path);
                        return -1L;
                    }
                }
            });

        }
        return resourcesFromCache;
    }

    private ChildCountAndList updateCachesForListChildren(final ResourceResolver resourceResolver,
        final List> docs, final String path, final String key) {
        if (docs.isEmpty()) {
            stringListCache.put(key, new CacheEntry>(Collections.emptyList()));
            updateListChildrenCacheList(path, key);
            return new ChildCountAndList();
        }

        final List children = new ArrayList(docs.size());

        final ChildCountAndList resources = new ChildCountAndList();
        for (final Map document : docs) {
            final Map mappedDocument = mapper.fromSchema(document);
            documentCache.put((String) mappedDocument.get(AbstractSchemaMapper.getSocoKey()),
                documentCache.createEntry(mappedDocument, Collections.emptyMap()));
            children.add((String) mappedDocument.get(AbstractSchemaMapper.getSocoKey()));
            resources.add(new MapResourceImpl(resourceResolver, this, (String) mappedDocument
                .get(AbstractSchemaMapper.getSocoKey()), mappedDocument, false));
        }

        // save the list of children (as paths) to cache in case this method is called with the exact same params
        stringListCache.put(key, new CacheEntry>(children));
        // keep a list of listChildren cache entries with this parent so we can delete the cache entries if children
        // are added.
        updateListChildrenCacheList(path, key);
        return resources;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long countChildren(final Resource parent, final String baseType, final boolean visibleOnly) {
        return getCountAndListChildren(parent.getPath(), baseType, parent.getResourceResolver(), -1, 1, null,
            visibleOnly).getNumFound();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator listChildren(final String path, final String baseType,
        final ResourceResolver resourceResolver, final int offset, final int size,
        final List> sortBy, final boolean visibleOnly) {
        return getCountAndListChildren(path, baseType, resourceResolver, offset, size, sortBy, visibleOnly,
            Counts.NONE).iterator();
    }

}