Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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