/* * #%L * ACS AEM Commons Bundle * %% * Copyright (C) 2015 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.config.impl; import com.adobe.acs.commons.httpcache.config.AuthenticationStatusConfigConstants; import com.adobe.acs.commons.httpcache.config.HttpCacheConfig; import com.adobe.acs.commons.httpcache.config.HttpCacheConfigExtension; import com.adobe.acs.commons.httpcache.exception.HttpCacheKeyCreationException; import com.adobe.acs.commons.httpcache.exception.HttpCacheRepositoryAccessException; import com.adobe.acs.commons.httpcache.keys.CacheKey; import com.adobe.acs.commons.httpcache.keys.CacheKeyFactory; import com.adobe.acs.commons.httpcache.store.HttpCacheStore; import com.adobe.acs.commons.httpcache.util.UserUtils; import org.apache.commons.collections.CollectionUtils; 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.ConfigurationPolicy; 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.PropertyOption; 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.Service; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.commons.osgi.PropertiesUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Concrete implementation of cache config for http cache. Modelled as OSGi config factory. */ @Component(label = "ACS AEM Commons - HTTP Cache - Cache config", description = "Config for request URI patterns that have to be cached.", configurationFactory = true, metatype = true, policy = ConfigurationPolicy.REQUIRE ) @Properties({ @Property(name = "webconsole.configurationFactory.nameHint", value = "Order: {httpcache.config.order}, " + "Request URIs: {httpcache.config.requesturi.patterns}, " + "Request URIs blacklist: {httpcache.config.requesturi.patterns.blacklisted}, " + "Authentication: {httpcache.config.request.authentication}, " + "Invalidation paths: {httpcache.config.invalidation.oak.paths}, " + "Cache type: {httpcache.config.cachestore}", propertyPrivate = true) }) @Service public class HttpCacheConfigImpl implements HttpCacheConfig { private static final Logger log = LoggerFactory.getLogger(HttpCacheConfigImpl.class); private static final String FILTER_SCOPE_REQUEST = "REQUEST"; private static final String FILTER_SCOPE_INCLUDE = "INCLUDE"; // Order public static final int DEFAULT_ORDER = 1000; private int order = DEFAULT_ORDER; @Property(label = "Priority order", description = "Order in which the HttpCacheEngine should evaluate the HttpCacheConfigs against the " + "request. Evaluates smallest to largest (Integer.MIN_VALUE -> Integer.MAX_VALUE). Defaults to " + "1000 ", intValue = DEFAULT_ORDER) public static final String PROP_ORDER = "httpcache.config.order"; // Request URIs - Whitelisted. @Property(label = "Request URI patterns", description = "Request URI patterns (REGEX) to be cached. Example - /content/mysite(.*).product-data" + ".json. Mandatory parameter.", cardinality = Integer.MAX_VALUE) private static final String PROP_REQUEST_URI_PATTERNS = "httpcache.config.requesturi.patterns"; private List<String> requestUriPatterns; private List<Pattern> requestUriPatternsAsRegEx; // Request URIs - Blacklisted. @Property(label = "Blacklisted request URI patterns", description = "Blacklisted request URI patterns (REGEX). Evaluated post applying the above request uri " + "" + "patterns (httpcache.config.requesturi.patterns). Optional parameter.", cardinality = Integer.MAX_VALUE) private static final String PROP_BLACKLISTED_REQUEST_URI_PATTERNS = "httpcache.config.requesturi.patterns.blacklisted"; private List<String> blacklistedRequestUriPatterns; private List<Pattern> blacklistedRequestUriPatternsAsRegEx; // Authentication requirement // @formatter:off @Property(label = "Authentication", description = "Authentication requirement.", options = { @PropertyOption(name = AuthenticationStatusConfigConstants.ANONYMOUS_REQUEST, value = AuthenticationStatusConfigConstants.ANONYMOUS_REQUEST), @PropertyOption(name = AuthenticationStatusConfigConstants.AUTHENTICATED_REQUEST, value = AuthenticationStatusConfigConstants.AUTHENTICATED_REQUEST), @PropertyOption(name = AuthenticationStatusConfigConstants.BOTH_ANONYMOUS_AUTHENTICATED_REQUESTS, value = AuthenticationStatusConfigConstants .BOTH_ANONYMOUS_AUTHENTICATED_REQUESTS) }, value = AuthenticationStatusConfigConstants.ANONYMOUS_REQUEST) // @formatter:on private static final String PROP_AUTHENTICATION_REQUIREMENT = "httpcache.config.request.authentication"; private static final String DEFAULT_AUTHENTICATION_REQUIREMENT = AuthenticationStatusConfigConstants .ANONYMOUS_REQUEST; private String authenticationRequirement; // Invalidation paths @Property(label = "JCR path pattern (REGEX) for cache invalidation ", description = "Optional set of paths in JCR (Oak) repository for which this cache has to be invalidated" + ". This accepts " + "REGEX. Example - /etc/my-products(.*)", cardinality = Integer.MAX_VALUE) private static final String PROP_CACHE_INVALIDATION_PATH_PATTERNS = "httpcache.config.invalidation.oak.paths"; private List<String> cacheInvalidationPathPatterns; private List<Pattern> cacheInvalidationPathPatternsAsRegEx; // Cache store // @formatter:off @Property(label = "Cache store", description = "Cache store for caching the response for this request URI. Example - MEM. This should " + "be one of the cache stores active in this installation. Mandatory parameter.", propertyPrivate = true, // Made private as only MEM implementation available now. options = { @PropertyOption(name = HttpCacheStore.VALUE_MEM_CACHE_STORE_TYPE, value = HttpCacheStore.VALUE_MEM_CACHE_STORE_TYPE), @PropertyOption(name = HttpCacheStore.VALUE_DISK_CACHE_STORE_TYPE, value = HttpCacheStore.VALUE_DISK_CACHE_STORE_TYPE), @PropertyOption(name = HttpCacheStore.VALUE_JCR_CACHE_STORE_TYPE, value = HttpCacheStore.VALUE_JCR_CACHE_STORE_TYPE) }, value = HttpCacheStore.VALUE_MEM_CACHE_STORE_TYPE) // @formatter:on private static final String PROP_CACHE_STORE = "httpcache.config.cachestore"; private static final String DEFAULT_CACHE_STORE = "MEM"; // Defaults to memory cache store private String cacheStore; // Cache store // @formatter:off private static final String DEFAULT_FILTER_SCOPE = FILTER_SCOPE_REQUEST; // Defaults to REQUEST scope @Property(label = "Filter scope", description = "Specify the scope of this HttpCacheConfig in the scope of the Sling Servlet Filter processing chain.", options = { @PropertyOption(name = FILTER_SCOPE_REQUEST, value = FILTER_SCOPE_REQUEST), @PropertyOption(name = FILTER_SCOPE_INCLUDE, value = FILTER_SCOPE_INCLUDE) }, value = DEFAULT_FILTER_SCOPE) // @formatter:on private static final String PROP_FILTER_SCOPE = "httpcache.config.filter-scope"; private FilterScope filterScope; // Making the cache config extension configurable. @Property(name = "cacheConfigExtension.target", label = "HttpCacheConfigExtension service pid", description = "Service pid of target implementation of HttpCacheConfigExtension to be used. Example - " + "(service.pid=com.adobe.acs.commons.httpcache.config.impl.GroupHttpCacheConfigExtension)." + " Optional parameter.", value = "(service.pid=com.adobe.acs.commons.httpcache.config.impl.GroupHttpCacheConfigExtension)") @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY, policy = ReferencePolicy.DYNAMIC, name = "cacheConfigExtension") private HttpCacheConfigExtension cacheConfigExtension; // Making the cache key factory configurable. @Property(name = "cacheKeyFactory.target", label = "CacheKeyFactory service pid", description = "Service pid of target implementation of CacheKeyFactory to be used. Example - " + "(service.pid=com.adobe.acs.commons.httpcac`he.config.impl.GroupHttpCacheConfigExtension)." + " Mandatory parameter.", value = "(service.pid=com.adobe.acs.commons.httpcache.config.impl.GroupHttpCacheConfigExtension)") @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = ReferencePolicy.DYNAMIC, name = "cacheKeyFactory") private CacheKeyFactory cacheKeyFactory; @Property(label = "Config-specific HttpCacheHandlingRules", description = "List of Service pid of HttpCacheHandlingRule applicable for this cache config. Optional " + "parameter", unbounded = PropertyUnbounded.ARRAY) private static final String PROP_CACHE_HANDLING_RULES_PID = "httpcache.config.cache-handling-rules.pid"; private List<String> cacheHandlingRulesPid; @Activate protected void activate(Map<String, Object> configs) { // Request URIs - Whitelisted. requestUriPatterns = Arrays.asList(PropertiesUtil.toStringArray(configs.get(PROP_REQUEST_URI_PATTERNS), new String[]{})); requestUriPatternsAsRegEx = compileToPatterns(requestUriPatterns); // Request URIs - Blacklisted. blacklistedRequestUriPatterns = Arrays.asList(PropertiesUtil.toStringArray(configs.get (PROP_BLACKLISTED_REQUEST_URI_PATTERNS), new String[]{})); blacklistedRequestUriPatternsAsRegEx = compileToPatterns(blacklistedRequestUriPatterns); // Authentication requirement. authenticationRequirement = PropertiesUtil.toString(configs.get(PROP_AUTHENTICATION_REQUIREMENT), DEFAULT_AUTHENTICATION_REQUIREMENT); // Cache store cacheStore = PropertiesUtil.toString(configs.get(PROP_CACHE_STORE), DEFAULT_CACHE_STORE); // Cache invalidation paths. cacheInvalidationPathPatterns = Arrays.asList(PropertiesUtil.toStringArray(configs.get (PROP_CACHE_INVALIDATION_PATH_PATTERNS), new String[]{})); cacheInvalidationPathPatternsAsRegEx = compileToPatterns(cacheInvalidationPathPatterns); order = PropertiesUtil.toInteger(configs.get(PROP_ORDER), DEFAULT_ORDER); filterScope = FilterScope.valueOf(PropertiesUtil.toString(configs.get(PROP_FILTER_SCOPE), DEFAULT_FILTER_SCOPE).toUpperCase()); // PIDs of cache handling rules. cacheHandlingRulesPid = new ArrayList<String>(Arrays.asList(PropertiesUtil.toStringArray(configs.get (PROP_CACHE_HANDLING_RULES_PID), new String[]{}))); ListIterator<String> listIterator = cacheHandlingRulesPid.listIterator(); while (listIterator.hasNext()) { String value = listIterator.next(); if (StringUtils.isBlank(value)) { listIterator.remove(); } } log.info("HttpCacheConfigImpl activated."); } /** * Converts an array of Regex strings into compiled Patterns. * * @param regexes the regex strings to compile into Patterns * @return the list of compiled Patterns */ private List<Pattern> compileToPatterns(final List<String> regexes) { final List<Pattern> patterns = new ArrayList<Pattern>(); for (String regex : regexes) { if (StringUtils.isNotBlank(regex)) { patterns.add(Pattern.compile(regex)); } } return patterns; } @Deactivate protected void deactivate(Map<String, Object> configs) { log.info("HttpCacheConfigImpl deactivated."); } //------------------------< Interface specific implementation > @Override public String getCacheStoreName() { return cacheStore; } @Override public boolean accepts(SlingHttpServletRequest request) throws HttpCacheRepositoryAccessException { // Match authentication requirement. if (UserUtils.isAnonymous(request.getResourceResolver().getUserID())) { if (AuthenticationStatusConfigConstants.AUTHENTICATED_REQUEST.equals(this.authenticationRequirement)) { log.trace("Rejected: Request is anonymous but the config accepts only authenticated request and hence" + " reject"); return false; } } else { if (AuthenticationStatusConfigConstants.ANONYMOUS_REQUEST.equals(this.authenticationRequirement)) { log.trace("Rejected: Request is authenticated but config is for anonymous and hence reject."); return false; } } // Match request URI. final String uri = request.getRequestURI(); if (!this.matches(this.requestUriPatternsAsRegEx, uri)) { // Does not match URI Whitelist log.trace("Rejected: Request URI does not match the white-listed URI patterns"); return false; } // Match blacklisted URI. if (this.matches(this.blacklistedRequestUriPatternsAsRegEx, uri)) { // Matches URI Blacklist; reject log.trace("Rejected: Request URI does match a black-listed URI pattern"); return false; } // Passing on the control to the extension point. if (null != cacheConfigExtension) { return cacheConfigExtension.accepts(request, this); } return true; } /** * Matching the given data with the set of compiled patterns. * * @param patterns * @param data * @return */ private boolean matches(List<Pattern> patterns, String data) { for (Pattern pattern : patterns) { final Matcher matcher = pattern.matcher(data); if (matcher.matches()) { return true; } } return false; } @Override public CacheKey buildCacheKey(SlingHttpServletRequest request) throws HttpCacheKeyCreationException { return this.cacheKeyFactory.build(request, this); } @Override public CacheKey buildCacheKey(String resourcePath) throws HttpCacheKeyCreationException { return this.cacheKeyFactory.build(resourcePath, this); } @Override public boolean isValid() { return CollectionUtils.isNotEmpty(this.requestUriPatterns); } @Override public boolean canInvalidate(final String path) { return matches(cacheInvalidationPathPatternsAsRegEx, path); } @Override public String getAuthenticationRequirement() { return this.authenticationRequirement; } @Override public List<Pattern> getRequestUriPatterns() { return this.requestUriPatternsAsRegEx; } @Override public List<Pattern> getBlacklistedRequestUriPatterns() { return this.blacklistedRequestUriPatternsAsRegEx; } @Override public List<Pattern> getJCRInvalidationPathPatterns() { return this.cacheInvalidationPathPatternsAsRegEx; } @Override public boolean knows(CacheKey key) throws HttpCacheKeyCreationException { return this.cacheKeyFactory.doesKeyMatchConfig(key, this); } @Override public int getOrder() { return this.order; } @Override public boolean acceptsRule(String servicePid) { return cacheHandlingRulesPid.contains(servicePid); } @Override public FilterScope getFilterScope() { return this.filterScope; } }