/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2016 Adobe
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.adobe.acs.commons.httpcache.engine.impl;
import com.adobe.acs.commons.httpcache.config.HttpCacheConfig;
import com.adobe.acs.commons.httpcache.config.impl.HttpCacheConfigComparator;
import com.adobe.acs.commons.httpcache.config.impl.HttpCacheConfigImpl;
import com.adobe.acs.commons.httpcache.engine.CacheContent;
import com.adobe.acs.commons.httpcache.engine.HttpCacheEngine;
import com.adobe.acs.commons.httpcache.engine.HttpCacheServletResponseWrapper;
import com.adobe.acs.commons.httpcache.exception.HttpCacheConfigConflictException;
import com.adobe.acs.commons.httpcache.exception.HttpCacheDataStreamException;
import com.adobe.acs.commons.httpcache.exception.HttpCacheKeyCreationException;
import com.adobe.acs.commons.httpcache.exception.HttpCachePersistenceException;
import com.adobe.acs.commons.httpcache.exception.HttpCacheRepositoryAccessException;
import com.adobe.acs.commons.httpcache.keys.CacheKey;
import com.adobe.acs.commons.httpcache.rule.HttpCacheHandlingRule;
import com.adobe.acs.commons.httpcache.store.HttpCacheStore;
import com.adobe.granite.jmx.annotation.AnnotatedStandardMBean;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.References;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.DynamicMBean;
import javax.management.NotCompliantMBeanException;
import javax.management.openmbean.CompositeDataSupport;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import javax.management.openmbean.TabularType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Default implementation for {@link HttpCacheEngine}. Binds multiple {@link HttpCacheConfig}. Multiple {@link
* HttpCacheStore} also get bound to this.
*/
// @formatter:off
@Component(
label = "ACS AEM Commons - HTTP Cache - Engine",
description = "Controlling service for http cache implementation.",
metatype = true
)
@Properties({
@Property(name = "jmx.objectname",
value = "com.adobe.acs.httpcache:type=HTTP Cache Engine",
propertyPrivate = true),
@Property(name = "webconsole.configurationFactory.nameHint",
value = "Global handling rules: {httpcache.engine.cache-handling-rules.global}",
propertyPrivate = true)
})
@References({
@Reference(name = HttpCacheEngineImpl.METHOD_NAME_TO_BIND_CONFIG,
referenceInterface = HttpCacheConfig.class,
policy = ReferencePolicy.DYNAMIC,
cardinality = ReferenceCardinality.MANDATORY_MULTIPLE),
@Reference(name = HttpCacheEngineImpl.METHOD_NAME_TO_BIND_CACHE_HANDLING_RULES,
referenceInterface = HttpCacheHandlingRule.class,
policy = ReferencePolicy.DYNAMIC,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE),
@Reference(name = HttpCacheEngineImpl.METHOD_NAME_TO_BIND_CACHE_STORE,
referenceInterface = HttpCacheStore.class,
policy = ReferencePolicy.DYNAMIC,
cardinality = ReferenceCardinality.MANDATORY_MULTIPLE)
})
@Service(value = {DynamicMBean.class, HttpCacheEngine.class})
// @formatter:on
public class HttpCacheEngineImpl extends AnnotatedStandardMBean implements HttpCacheEngine, HttpCacheEngineMBean {
private static final Logger log = LoggerFactory.getLogger(HttpCacheConfigImpl.class);
/** Method name that binds cache configs */
static final String METHOD_NAME_TO_BIND_CONFIG = "httpCacheConfig";
/** Thread safe list to contain the registered HttpCacheConfig references. */
private CopyOnWriteArrayList<HttpCacheConfig> cacheConfigs = new CopyOnWriteArrayList<HttpCacheConfig>();
/** Method name that binds cache store */
static final String METHOD_NAME_TO_BIND_CACHE_STORE = "httpCacheStore";
/** Thread safe hash map to contain the registered cache store references. */
private static final ConcurrentHashMap<String, HttpCacheStore> cacheStoresMap = new ConcurrentHashMap<String, HttpCacheStore>();
/** Method name that binds cache handling rules */
static final String METHOD_NAME_TO_BIND_CACHE_HANDLING_RULES = "httpCacheHandlingRule";
/** Thread safe map to contain the registered HttpCacheHandlingRule references. */
private static final ConcurrentHashMap<String, HttpCacheHandlingRule> cacheHandlingRules = new
ConcurrentHashMap<String, HttpCacheHandlingRule>();
// formatter:off
@Property(label = "Global HttpCacheHandlingRules",
description = "List of Service pid of HttpCacheHandlingRule applicable for all cache configs.",
unbounded = PropertyUnbounded.ARRAY,
value = {"com.adobe.acs.commons.httpcache.rule.impl.CacheOnlyGetRequest",
"com.adobe.acs.commons.httpcache.rule.impl.CacheOnlyResponse200",
"com.adobe.acs.commons.httpcache.rule.impl.HonorCacheControlHeaders",
"com.adobe.acs.commons.httpcache.rule.impl.DoNotCacheZeroSizeResponse"
})
// formatter:on
private static final String PROP_GLOBAL_CACHE_HANDLING_RULES_PID = "httpcache.engine.cache-handling-rules.global";
private List<String> globalCacheHandlingRulesPid;
/** Thread safe list containing the OSGi configurations for the registered httpCacheConfigs. Used only for mbean.*/
private final ConcurrentHashMap<HttpCacheConfig, Map<String, Object>> cacheConfigConfigs = new
ConcurrentHashMap<HttpCacheConfig, Map<String, Object>>();
//-------------------<OSGi specific methods>---------------//
/**
* Binds cache config. Cache config could come and go at run time.
*
* @param cacheConfig
* @param configs
*/
protected void bindHttpCacheConfig(final HttpCacheConfig cacheConfig, final Map<String, Object> configs) {
// Validate cache config object
if (!cacheConfig.isValid()) {
log.info("Http cache config rejected as the request uri is absent.");
return;
}
// Check if the same object is already there in the map.
if (cacheConfigs.contains(cacheConfig)) {
log.trace("Http cache config object already exists in the cacheConfigs map and hence ignored.");
return;
}
// Sort cacheConfigs by order
final CopyOnWriteArrayList<HttpCacheConfig> tmp = new CopyOnWriteArrayList<HttpCacheConfig>(this.cacheConfigs);
tmp.add(cacheConfig);
Collections.sort(tmp, new HttpCacheConfigComparator());
this.cacheConfigs = tmp;
this.cacheConfigConfigs.put(cacheConfig, configs);
log.debug("Total number of cache configs added: {}", cacheConfigs.size());
}
/**
* Unbinds cache config.
*
* @param cacheConfig
* @param config
*/
protected void unbindHttpCacheConfig(final HttpCacheConfig cacheConfig, final Map<String, Object> config) {
if (cacheConfigs.contains(cacheConfig)) {
// Remove the associated cached items from the cache store.
if (cacheStoresMap.containsKey(cacheConfig.getCacheStoreName())) {
cacheStoresMap.get(cacheConfig.getCacheStoreName()).invalidate(cacheConfig);
} else {
log.debug("Configured cache store is unavailable and hence nothing to invalidate.");
}
// Remove the entry from the map.
cacheConfigs.remove(cacheConfig);
cacheConfigConfigs.remove(cacheConfig);
log.debug("Total number of cache configs after removal: {}", cacheConfigs.size());
return;
}
log.debug("This cache config entry was not bound and hence nothing to unbind.");
}
/**
* Binds cache store implementation
*
* @param cacheStore
* @param properties
*/
protected void bindHttpCacheStore(final HttpCacheStore cacheStore, final Map<String, Object> properties) {
final String cacheStoreType = PropertiesUtil.toString(properties.get(HttpCacheStore.KEY_CACHE_STORE_TYPE), null);
if (cacheStoreType != null && cacheStoresMap.putIfAbsent(cacheStoreType, cacheStore) == null) {
log.debug("Cache store implementation {} has been added", (String) properties.get(HttpCacheStore
.KEY_CACHE_STORE_TYPE));
log.debug("Total number of cache stores in the map: {}", cacheStoresMap.size());
}
}
/**
* Unbinds cache store.
*
* @param cacheStore
* @param properties
*/
protected void unbindHttpCacheStore(final HttpCacheStore cacheStore, final Map<String, Object> properties) {
final String cacheStoreType = PropertiesUtil.toString(properties.get(HttpCacheStore.KEY_CACHE_STORE_TYPE), null);
if (cacheStoreType != null && cacheStoresMap.remove(cacheStoreType) != null) {
log.debug("Cache store removed - {}.", (String) properties.get(HttpCacheStore.KEY_CACHE_STORE_TYPE));
log.debug("Total number of cache stores after removal: {}", cacheStoresMap.size());
}
}
/**
* Binds cache handling rule
*
* @param cacheHandlingRule
* @param properties
*/
protected void bindHttpCacheHandlingRule(final HttpCacheHandlingRule cacheHandlingRule, final Map<String, Object>
properties) {
// Get the service pid and make it as key.
String servicePid = PropertiesUtil.toString(properties.get("service.pid"), StringUtils.EMPTY);
if (cacheHandlingRules.putIfAbsent(servicePid, cacheHandlingRule) == null) {
log.debug("Cache handling rule implementation {} has been added", cacheHandlingRule.getClass().getName());
log.debug("Total number of cache handling rule available after addition: {}", cacheHandlingRules.size());
}
}
/**
* Unbinds handling rule.
*
* @param cacheHandlingRule
* @param configs
*/
protected void unbindHttpCacheHandlingRule(final HttpCacheHandlingRule cacheHandlingRule, final Map<String,
Object> configs) {
String servicePid = PropertiesUtil.toString(configs.get("service.pid"), StringUtils.EMPTY);
if (cacheHandlingRules.remove(servicePid) != null) {
log.debug("Cache handling rule removed - {}.", cacheHandlingRule.getClass().getName());
log.debug("Total number of cache handling rules available after removal: {}", cacheHandlingRules.size());
}
}
@Activate
protected void activate(Map<String, Object> configs) {
// PIDs of global cache handling rules.
globalCacheHandlingRulesPid = new ArrayList<String>(Arrays.asList(PropertiesUtil.toStringArray(configs.get
(PROP_GLOBAL_CACHE_HANDLING_RULES_PID), new String[]{})));
ListIterator<String> listIterator = globalCacheHandlingRulesPid.listIterator();
while (listIterator.hasNext()) {
String value = listIterator.next();
if (StringUtils.isBlank(value)) {
listIterator.remove();
}
}
log.info("HttpCacheEngineImpl activated.");
}
@Deactivate
protected void deactivate(Map<String, Object> configs) {
log.info("HttpCacheEngineImpl deactivated.");
}
//-----------------------<Interface specific implementation>--------//
@Override
public boolean isRequestCacheable(SlingHttpServletRequest request, HttpCacheConfig cacheConfig) throws
HttpCacheRepositoryAccessException {
// Execute custom rules.
for (final Map.Entry<String, HttpCacheHandlingRule> entry : cacheHandlingRules.entrySet()) {
// Apply rule if it's a configured global or cache-config tied rule.
if (globalCacheHandlingRulesPid.contains(entry.getKey()) || cacheConfig.acceptsRule(entry.getKey())) {
HttpCacheHandlingRule rule = entry.getValue();
if (!rule.onRequestReceive(request)) {
if (log.isDebugEnabled()) {
log.debug("Request cannot be cached for the url {} honoring the rule {}", request
.getRequestURL(), rule.getClass().getName());
}
// Only a single rule need to fail to cause the caching mechanism to be by-passed
return false;
}
}
}
// All rules have accepted this request, so request is cache-able.
return true;
}
@Override
public HttpCacheConfig getCacheConfig(SlingHttpServletRequest request) throws HttpCacheRepositoryAccessException,
HttpCacheConfigConflictException {
return getCacheConfig(request, HttpCacheConfig.FilterScope.REQUEST);
}
@Override
public HttpCacheConfig getCacheConfig(SlingHttpServletRequest request, HttpCacheConfig.FilterScope filterScope) throws HttpCacheConfigConflictException, HttpCacheRepositoryAccessException {
// Get the first accepting cache config based on the cache config order.
HttpCacheConfig bestCacheConfig = null;
for (HttpCacheConfig cacheConfig : cacheConfigs) {
if (bestCacheConfig != null) {
// A matching HttpCacheConfig has been found, so check for order + acceptance conflicts
if (bestCacheConfig.getOrder() == cacheConfig.getOrder()) {
if (cacheConfig.accepts(request)) {
// Throw an exception if two HttpCacheConfigs w the same order accept the same request
throw new HttpCacheConfigConflictException();
}
} else if (bestCacheConfig.getOrder() < cacheConfig.getOrder()) {
// Since cacheConfigs is sorted by order, this means all other orders will not match
break;
}
} else if (filterScope.equals(cacheConfig.getFilterScope()) && cacheConfig.accepts(request)) {
bestCacheConfig = cacheConfig;
}
}
if ((bestCacheConfig == null) && log.isDebugEnabled()) {
log.debug("Matching cache config not found.");
}
return bestCacheConfig;
}
@Override
public boolean isCacheHit(SlingHttpServletRequest request, HttpCacheConfig cacheConfig) throws
HttpCacheKeyCreationException, HttpCachePersistenceException {
// Build a cache key and do a lookup in the configured cache store.
return getCacheStore(cacheConfig).contains(cacheConfig.buildCacheKey(request));
}
@Override
public boolean deliverCacheContent(SlingHttpServletRequest request, SlingHttpServletResponse response,
HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException,
HttpCacheDataStreamException, HttpCachePersistenceException {
// Get the cached content from cache
CacheContent cacheContent = getCacheStore(cacheConfig).getIfPresent(cacheConfig.buildCacheKey(request));
// Execute custom rules.
for (final Map.Entry<String, HttpCacheHandlingRule> entry : cacheHandlingRules.entrySet()) {
// Apply rule if it's a configured global or cache-config tied rule.
if (globalCacheHandlingRulesPid.contains(entry.getKey()) || cacheConfig.acceptsRule(entry.getKey())) {
HttpCacheHandlingRule rule = entry.getValue();
if (!rule.onCacheDeliver(request, response, cacheConfig, cacheContent)) {
if (log.isDebugEnabled()) {
log.debug("Cache cannot be delivered for the url {} honoring the rule {}", request
.getRequestURL(), rule.getClass().getName());
}
return false;
}
}
}
response.setStatus(cacheContent.getStatus());
// Spool header info into the servlet response.
for (String headerName : cacheContent.getHeaders().keySet()) {
for (String headerValue : cacheContent.getHeaders().get(headerName)) {
response.setHeader(headerName, headerValue);
}
}
// Spool other attributes to the servlet response.
response.setContentType(cacheContent.getContentType());
response.setCharacterEncoding(cacheContent.getCharEncoding());
// Copy the cached data into the servlet output stream.
try {
IOUtils.copy(cacheContent.getInputDataStream(), response.getOutputStream());
if (log.isDebugEnabled()) {
log.debug("Response delivered from cache for the url [ {} ]", request.getRequestURI());
}
return true;
} catch (IOException e) {
throw new HttpCacheDataStreamException("Unable to copy from cached data to the servlet output stream.");
}
}
@Override
public HttpCacheServletResponseWrapper wrapResponse(SlingHttpServletRequest request, SlingHttpServletResponse
response, HttpCacheConfig cacheConfig) throws HttpCacheDataStreamException,
HttpCacheKeyCreationException, HttpCachePersistenceException {
// Wrap the response to get the copy of the stream.
// Temp sink for the duplicate stream is chosen based on the cache store configured at cache config.
try {
return new HttpCacheServletResponseWrapper(response, getCacheStore(cacheConfig).createTempSink());
} catch (IOException e) {
throw new HttpCacheDataStreamException(e);
}
}
@Override
public void cacheResponse(SlingHttpServletRequest request, SlingHttpServletResponse response, HttpCacheConfig
cacheConfig) throws HttpCacheKeyCreationException, HttpCacheDataStreamException,
HttpCachePersistenceException {
// TODO - This can be made asynchronous to avoid performance penalty on response cache.
CacheContent cacheContent = null;
try {
// Construct the cache content.
HttpCacheServletResponseWrapper responseWrapper = null;
if (response instanceof HttpCacheServletResponseWrapper) {
responseWrapper = (HttpCacheServletResponseWrapper) response;
} else {
throw new AssertionError("Programming error.");
}
CacheKey cacheKey = cacheConfig.buildCacheKey(request);
cacheContent = new CacheContent().build(responseWrapper);
// Execute custom rules.
boolean canCacheResponse = true;
for (final Map.Entry<String, HttpCacheHandlingRule> entry : cacheHandlingRules.entrySet()) {
// Apply rule if it's a configured global or cache-config tied rule.
if (globalCacheHandlingRulesPid.contains(entry.getKey()) || cacheConfig.acceptsRule(entry.getKey())) {
HttpCacheHandlingRule rule = entry.getValue();
if (!rule.onResponseCache(request, response, cacheConfig, cacheContent)) {
if (log.isDebugEnabled()) {
log.debug("Per custom rule {} caching for this request {} has been cancelled.", rule
.getClass().getName(), request.getRequestURI());
}
canCacheResponse = false;
break;
}
}
}
// Persist in cache.
if (canCacheResponse) {
getCacheStore(cacheConfig).put(cacheKey, cacheContent);
log.debug("Response for the URI cached - {}", request.getRequestURI());
}
} finally {
// Close the temp sink input stream.
if (null != cacheContent) {
IOUtils.closeQuietly(cacheContent.getInputDataStream());
}
}
}
@Override
public boolean isPathPotentialToInvalidate(String path) {
// Check all the configs to see if this path is of interest.
for (HttpCacheConfig config : cacheConfigs) {
if (config.canInvalidate(path)) {
return true;
}
}
return false;
}
@Override
public void invalidateCache(String path) throws HttpCachePersistenceException, HttpCacheKeyCreationException {
// Find out all the cache config which has this path applicable for invalidation.
for (HttpCacheConfig cacheConfig : cacheConfigs) {
if (cacheConfig.canInvalidate(path)) {
// Execute custom rules.
for (final Map.Entry<String, HttpCacheHandlingRule> entry : cacheHandlingRules.entrySet()) {
// Apply rule if it's a configured global or cache-config tied rule.
if (globalCacheHandlingRulesPid.contains(entry.getKey()) || cacheConfig.acceptsRule(entry.getKey())) {
HttpCacheHandlingRule rule = entry.getValue();
if (rule.onCacheInvalidate(path)) {
getCacheStore(cacheConfig).invalidate(cacheConfig.buildCacheKey(path));
} else {
log.debug("Cache invalidation rejected for path {} per custom rule {}", path, rule
.getClass().getName());
}
}
}
}
}
}
/**
* Get the cache store set for the config if available.
*
* @param cacheConfig
* @return
* @throws HttpCachePersistenceException
*/
private HttpCacheStore getCacheStore(HttpCacheConfig cacheConfig) throws HttpCachePersistenceException {
if (cacheStoresMap.containsKey(cacheConfig.getCacheStoreName())) {
return cacheStoresMap.get(cacheConfig.getCacheStoreName());
} else {
throw new HttpCachePersistenceException("Configured cache store unavailable " + cacheConfig
.getCacheStoreName());
}
}
//-------------------------<Mbean specific implementation>
public HttpCacheEngineImpl() throws NotCompliantMBeanException {
super(HttpCacheEngineMBean.class);
}
@Override
public TabularData getRegisteredHttpCacheRules() throws OpenDataException {
// @formatter:off
final CompositeType cacheEntryType = new CompositeType(
"HTTP Cache Handling Rule",
"HTTP Cache Handling Rule",
new String[]{"HTTP Cache Handling Rule"},
new String[]{"HTTP Cache Handling Rule"},
new OpenType[]{SimpleType.STRING});
final TabularDataSupport tabularData = new TabularDataSupport(
new TabularType(
"HTTP Cache Handling Rules",
"HTTP Cache Handling Rules",
cacheEntryType,
new String[]{"HTTP Cache Handling Rule"}));
// @formatter:on
for (final Map.Entry<String, HttpCacheHandlingRule> entry : cacheHandlingRules.entrySet()) {
final Map<String, Object> row = new HashMap<String, Object>();
row.put("HTTP Cache Handling Rule", entry.getValue().getClass().getName());
tabularData.put(new CompositeDataSupport(cacheEntryType, row));
}
return tabularData;
}
@Override
public TabularData getRegisteredHttpCacheConfigs() throws OpenDataException {
// @formatter:off
// Exposing all google guava stats.
final CompositeType cacheEntryType = new CompositeType(
"HTTP Cache Config",
"HTTP Cache Config",
new String[]{ "Order", "OSGi Component" },
new String[]{ "Order", "OSGi Component" },
new OpenType[]{ SimpleType.INTEGER, SimpleType.STRING });
final TabularDataSupport tabularData = new TabularDataSupport(
new TabularType(
"HTTP Cache Configs",
"HTTP Cache Configs",
cacheEntryType,
new String[]{ "OSGi Component" }));
// @formatter:on
for (HttpCacheConfig cacheConfig : this.cacheConfigs) {
final Map<String, Object> row = new HashMap<String, Object>();
Map<String, Object> osgiConfig = cacheConfigConfigs.get(cacheConfig);
row.put("Order", cacheConfig.getOrder());
row.put("OSGi Component", (String) osgiConfig.get(Constants.SERVICE_PID));
tabularData.put(new CompositeDataSupport(cacheEntryType, row));
}
return tabularData;
}
@Override
public TabularData getRegisteredPersistenceStores() throws OpenDataException {
// @formatter:off
final CompositeType cacheEntryType = new CompositeType(
"HTTP Cache Store",
"HTTP Cache Store",
new String[]{ "HTTP Cache Store" },
new String[]{ "HTTP Cache Store" },
new OpenType[]{ SimpleType.STRING});
final TabularDataSupport tabularData = new TabularDataSupport(
new TabularType(
"HTTP Cache Stores",
"HTTP Cache Stores",
cacheEntryType,
new String[]{ "HTTP Cache Store" }));
// @formatter:on
Enumeration<String> storeNames = cacheStoresMap.keys();
while (storeNames.hasMoreElements()) {
final String storeName = storeNames.nextElement();
final Map<String, Object> row = new HashMap<String, Object>();
row.put("HTTP Cache Store", storeName);
tabularData.put(new CompositeDataSupport(cacheEntryType, row));
}
return tabularData;
}
}