Adobe-Consulting-Services/acs-aem-commons

View on GitHub
bundle/src/main/java/com/adobe/acs/commons/redirects/filter/RedirectFilter.java

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * ACS AEM Commons
 *
 * Copyright (C) 2013 - 2023 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.
 */
package com.adobe.acs.commons.redirects.filter;

import com.adobe.acs.commons.redirects.LocationHeaderAdjuster;
import com.adobe.acs.commons.redirects.models.RedirectConfiguration;
import com.adobe.acs.commons.redirects.models.RedirectMatch;
import com.adobe.acs.commons.redirects.models.RedirectRule;
import com.adobe.acs.commons.redirects.models.RedirectState;
import com.adobe.acs.commons.redirects.models.Redirects;
import com.adobe.granite.jmx.annotation.AnnotatedStandardMBean;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationEvent;
import com.day.cq.wcm.api.WCMMode;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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 javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
import org.apache.sling.engine.EngineConstants;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.adobe.acs.commons.redirects.models.RedirectRule.CACHE_CONTROL_HEADER_NAME;
import static com.adobe.acs.commons.redirects.models.Redirects.CFG_PROP_IGNORE_SELECTORS;
import static org.apache.sling.engine.EngineConstants.SLING_FILTER_SCOPE;
import static org.osgi.framework.Constants.SERVICE_DESCRIPTION;
import static org.osgi.framework.Constants.SERVICE_ID;
import static org.osgi.framework.Constants.SERVICE_RANKING;

/**
 * A request filter that implements support for virtual redirects.
 */
@Component(service = {Filter.class, RedirectFilterMBean.class, EventHandler.class},
        configurationPolicy = ConfigurationPolicy.REQUIRE, property = {
        SERVICE_DESCRIPTION + "=A request filter implementing support for virtual redirects",
        SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
        // to correctly work in Author RedirectFilter needs to run after WCMRequestFilter which has rank 2000 in
        // AEM 6.5 and Cloud SDK, see issue 2707
        SERVICE_RANKING + ":Integer=1900",
        "jmx.objectname=" + "com.adobe.acs.commons:type=Redirect Manager",
        EventConstants.EVENT_TOPIC + "=" + ReplicationAction.EVENT_TOPIC,
        EventConstants.EVENT_TOPIC + "=" + ReplicationEvent.EVENT_TOPIC

})
@Designate(ocd = RedirectFilter.Configuration.class)
public class RedirectFilter extends AnnotatedStandardMBean
        implements Filter, EventHandler, ResourceChangeListener, RedirectFilterMBean {

    public static final String ACS_REDIRECTS_RESOURCE_TYPE = "acs-commons/components/utilities/manage-redirects";
    public static final String REDIRECT_RULE_RESOURCE_TYPE = ACS_REDIRECTS_RESOURCE_TYPE + "/redirect-row";

    public static final String DEFAULT_CONFIG_BUCKET = "settings";
    public static final String DEFAULT_CONFIG_NAME = "redirects";

    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    private static final String SERVICE_NAME = "redirect-manager";

    @ObjectClassDefinition(name = "ACS Commons Redirect Filter")
    public @interface Configuration {
        @AttributeDefinition(name = "Enable Redirect Filter", description = "Indicates whether the redirect filter is enabled or not.", type = AttributeType.BOOLEAN)
        boolean enabled() default false;

        @AttributeDefinition(name = "Rewrite Location Header", description = "Apply Sling Resource Mappings (/etc/map) to Location header. "
                + "Use if Location header should be rewritten using ResourceResolver#map", type = AttributeType.BOOLEAN)
        boolean mapUrls() default true;

        @AttributeDefinition(name = "Request Extensions", description = "List of extensions for which redirection is allowed", type = AttributeType.STRING)
        String[] extensions() default {};

        @AttributeDefinition(name = "Request Paths", description = "List of paths for which redirection is allowed", type = AttributeType.STRING)
        String[] paths() default {"/content"};

        @AttributeDefinition(name = "Preserve Query String", description = "Preserve query string in redirects", type = AttributeType.BOOLEAN)
        boolean preserveQueryString() default true;

        @AttributeDefinition(name = "Preserve Extension", description = "Whether to preserve extensions. "
                + "When this flag is checked (default), redirect filter will preserve the extension from the request, "
                + "e.g. append .html to the Location header. ", type = AttributeType.BOOLEAN)
        boolean preserveExtension() default true;

        @AttributeDefinition(name = "Additional Response Headers", description = "Optional response headers in the name:value format to apply on delivery,"
                + " e.g. Cache-Control: max-age=3600", type = AttributeType.STRING)
        String[] additionalHeaders() default {};

        @AttributeDefinition(name = "Configuration bucket name", description = "name of the parent folder where to store redirect rules."
                + " Default is settings. ", type = AttributeType.STRING)
        String bucketName() default DEFAULT_CONFIG_BUCKET;

        @AttributeDefinition(name = "Configuration Name", description = "The node name to store redirect configurations. Default is 'redirects' "
                + " which means the default path to store redirects is /conf/global/settings/redirects "
                + " where 'settings' is the bucket and 'redirects' is the config name", type = AttributeType.STRING)
        String configName() default  DEFAULT_CONFIG_NAME;
    }

    @Reference
    ResourceResolverFactory resourceResolverFactory;

    @Reference
    ConfigurationResourceResolver configResolver;

    @Reference(
            cardinality = ReferenceCardinality.OPTIONAL,
            policy = ReferencePolicy.STATIC,
            policyOption = ReferencePolicyOption.GREEDY
    )
    LocationHeaderAdjuster urlAdjuster;

    private ServiceRegistration<?> listenerRegistration;
    private boolean enabled;
    private boolean mapUrls;
    private boolean preserveQueryString;
    private List<Header> onDeliveryHeaders = Collections.emptyList();
    private Collection<String> methods = Arrays.asList("GET", "HEAD");
    private Collection<String> exts = Collections.emptySet();
    private Collection<String> paths = Collections.emptySet();
    private Configuration config;
    private ExecutorService executor;
    Cache<String, RedirectConfiguration> rulesCache;

    public RedirectFilter() throws NotCompliantMBeanException {
        super(RedirectFilterMBean.class);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // no op
    }

    @Activate
    @Modified
    protected final void activate(Configuration config, BundleContext context) {
        this.config = config;
        enabled = config.enabled();

        if (enabled) {
            Dictionary<String, Object> properties = new Hashtable<>();
            properties.put(ResourceChangeListener.PATHS, "/conf");
            listenerRegistration = context.registerService(ResourceChangeListener.class, this, properties);
            log.debug("Registered {}:{}", SERVICE_ID, listenerRegistration.getReference().getProperty(SERVICE_ID));

            exts = config.extensions() == null ? Collections.emptySet()
                    : Arrays.stream(config.extensions()).filter(ext -> !ext.isEmpty()).collect(Collectors.toSet());
            paths = config.paths() == null ? Collections.emptySet() : Arrays.stream(config.paths()).filter(path -> !path.isEmpty()).collect(Collectors.toSet());
            mapUrls = config.mapUrls();
            onDeliveryHeaders = new ArrayList<>();
            for(String kv : config.additionalHeaders()){
                int idx = kv.indexOf(':');
                if(idx == -1 || idx > kv.length() - 1) {
                    log.error("invalid on-delivery header: {}", kv);
                    continue;
                }
                String name = kv.substring(0, idx).trim();
                String value = kv.substring(idx + 1).trim();
                onDeliveryHeaders.add(new BasicHeader(name, value));
            }
            preserveQueryString = config.preserveQueryString();
            log.debug("exts: {}, paths: {}, rewriteUrls: {}",
                    exts, paths, mapUrls);
            executor = Executors.newSingleThreadExecutor();

            rulesCache = CacheBuilder.newBuilder().build();

        }
    }

    @Modified
    protected void modify(BundleContext context, Configuration config) {
        deactivate();
        activate(config, context);
    }


    @Deactivate
    public void deactivate() {
        if(enabled) {
            executor.shutdown();
        }
        if (listenerRegistration != null) {
            log.debug("unregistering ... ");
            listenerRegistration.unregister();
            listenerRegistration = null;
        }
    }

    Configuration getConfiguration(){
        return config; // for testing
    }

    @Override
    public void handleEvent(Event event) {
        ReplicationEvent replicationEvent = ReplicationEvent.fromEvent(event);
        if(enabled && replicationEvent != null){
            String redirectSubPath = config.bucketName() + "/" + config.configName();
            String[] replicationPaths = replicationEvent.getReplicationAction().getPaths();
            if(replicationPaths != null) {
                for (String path : replicationPaths) {
                    if (path.contains(redirectSubPath)) {
                        // loading redirect configurations can be expensive and needs to run
                        // asynchronously,
                        // outside of the Sling event processing chain
                        executor.submit(() -> invalidate(path));
                    }
                }
            }
        }
    }

    @Override
    public void onChange(List<ResourceChange> changes) {
        if(!enabled){
            return;
        }
        String redirectSubPath = config.bucketName() + "/" + config.configName();
        for(ResourceChange e : changes){
            String path = e.getPath();
            if(path.contains(redirectSubPath)){
                executor.submit(() -> invalidate(path));
            }
        }
    }

    /**
     * Detect the redirect configuration and invalidate the cached rules
     *
     * Given an even path, e.g. /conf/global/settings/redirects/redirect-rule-2
     * this method will figure out the corresponding configuration (/conf/global/settings/redirects)
     * and invalidate the cached rules
     *
     * @param changePath    the event path
     */
    void invalidate(String changePath) {
        String redirectSubPath = config.bucketName() + "/" + config.configName();
        try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(
                Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_NAME))) {
            Resource resource = resolver.resolve(changePath);
            while(resource != null){
                if(resource.getPath().endsWith(redirectSubPath)){
                    log.debug("invalidating {}", resource.getPath());
                    rulesCache.invalidate(resource.getPath());
                    break;
                }
                resource = resource.getParent();
            }
        } catch (LoginException e) {
            log.error("Failed to get resolver for {}", SERVICE_NAME, e);
        }
    }

    @Override
    public void invalidateAll() {
        rulesCache.invalidateAll();
    }

    RedirectConfiguration loadRules(String storagePath) {
        RedirectConfiguration rules = null;
        long t0 = System.currentTimeMillis();
        try (ResourceResolver resolver = resourceResolverFactory.getServiceResourceResolver(
                Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_NAME))) {
            Resource storageResource = resolver.getResource(storagePath);
            if(storageResource != null) {
                String storageSuffix = getBucket() + "/" + getConfigName();
                rules = new RedirectConfiguration(storageResource, storageSuffix);
                log.debug("{} rules loaded from {} in {} ms", rules.getPathRules().size() + rules.getPatternRules().size(),
                        storagePath, System.currentTimeMillis() - t0);
            } else {
                log.warn("redirects not found in {}", storagePath);
            }
        } catch (LoginException e) {
            log.error("Failed to get resolver for {}", SERVICE_NAME, e);
        }
        return rules;
    }

    public static Collection<RedirectRule> getRules(Resource resource) {
        Collection<RedirectRule> rules = new ArrayList<>();
        for (Resource res : resource.getChildren()) {
            if(res.isResourceType(REDIRECT_RULE_RESOURCE_TYPE)){
                RedirectRule rule = res.adaptTo(RedirectRule.class);
                if(rule != null) {
                    rules.add(rule);
                }
            }
        }
        return rules;
    }

    Cache<String, RedirectConfiguration> getRulesCache(){
        return rulesCache;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!(request instanceof SlingHttpServletRequest)
                || !(response instanceof SlingHttpServletResponse)) {
            chain.doFilter(request, response);
            return;
        }

        SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
        SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;

        if (isEnabled() && doesRequestMatch(slingRequest) && handleRedirect(slingRequest, slingResponse)) {
            return;
        }
        chain.doFilter(request, response);
    }

    boolean handleRedirect(SlingHttpServletRequest slingRequest, SlingHttpServletResponse slingResponse) {
        long t0 = System.currentTimeMillis();
        boolean redirected = false;

        RedirectMatch match = match(slingRequest);
        if (match != null) {

            RedirectRule redirectRule = match.getRule();

            if (redirectRule.getState() != RedirectState.ACTIVE) {
                log.debug("redirect rule matched, but didn't meet on/off time criteria: untilDate: {}, effectiveFrom: {}",
                        redirectRule.getUntilDate(), redirectRule.getEffectiveFrom());
            } else {
                RequestPathInfo pathInfo = slingRequest.getRequestPathInfo();
                String resourcePath = pathInfo.getResourcePath();

                String location = evaluate(match, slingRequest);
                log.trace("matched {} to {} in {} ms", resourcePath, redirectRule.toString(),
                        System.currentTimeMillis() - t0);

                log.debug("Redirecting {} to {}, statusCode: {}",
                        resourcePath, location, redirectRule.getStatusCode());
                slingResponse.setHeader("Location", location);
                setAdditionalHeaders(redirectRule, slingResponse);
                slingResponse.setStatus(redirectRule.getStatusCode());
                redirected = true;
            }
        }
        return redirected;
    }

    /**
     * Evaluate the rule and return the value to put in Location header
     *
     * Depending on the configuration appends query string and rewrites the result using
     * {@link ResourceResolver#map(HttpServletRequest, String)}
     */
    String evaluate(RedirectMatch match, SlingHttpServletRequest slingRequest){
        //fetches optional contextPrefix
        Resource configResource = configResolver.getResource(slingRequest.getResource(), config.bucketName(), config.configName());
        ValueMap properties = configResource.getValueMap();
        String contextPrefix = properties.get(Redirects.CFG_PROP_CONTEXT_PREFIX, "");

        RequestPathInfo pathInfo = slingRequest.getRequestPathInfo();
        String location = createFullPath(match.getRule().evaluate(match.getMatcher()), match.getRule(), contextPrefix);

        if (StringUtils.startsWith(location, "/") && !StringUtils.startsWith(location, "//")) {
            String ext = pathInfo.getExtension();
            if (ext != null && config.preserveExtension() && !location.endsWith(ext)) {
                location += "." + ext;
            }
            if (mapUrls()) {
                location = mapUrl(location, slingRequest);
            }
            if(urlAdjuster != null){
                location = urlAdjuster.adjust(slingRequest, location);
            }
        }
        if (preserveQueryString) {
            String queryString = slingRequest.getQueryString();
            if (queryString != null) {
                location = preserveQueryString(location, queryString);
            }
        }
        return location;
    }

    String preserveQueryString(String location, String queryString){
        int idx = location.indexOf('?');
        if (idx == -1) {
            idx = location.indexOf('#');
        }
        if (idx != -1) {
            location = location.substring(0, idx);
        }

        location += "?" + queryString;

        return location;
    }

    String mapUrl(String url, SlingHttpServletRequest slingRequest) {
        return slingRequest.getResourceResolver().map(slingRequest, url);
    }

    @Override
    public void destroy() {
        // no op
    }

    protected boolean mapUrls() {
        return mapUrls;
    }

    /**
     * @return whether redirection is enabled
     */
    protected boolean isEnabled() {
        return enabled;
    }

    protected Collection<String> getExtensions() {
        return Collections.unmodifiableCollection(exts);
    }

    protected Collection<String> getPaths() {
        return Collections.unmodifiableCollection(paths);
    }

    protected Collection<String> getMethods() {
        return Collections.unmodifiableCollection(methods);
    }

    protected List<Header> getOnDeliveryHeaders() {
        return Collections.unmodifiableList(onDeliveryHeaders);
    }

    /**
     * Check whether redirection for the given request is allowed.
     * <ol>
     * <li>On author redirects are disabled in EDIT, PREVIEW and DESIGN WCM Modes.
     * To test on author you need to disable WCM mode and append &wcmmode=disabled
     * to the query string</li>
     * <li>Redirects are supported only for GET and HEAD methods</li>
     * This can be changed in the OSGi configuration</li>
     * <li>If configured, redirects are allowed only for the specified extensions,
     * e.g. only *.html requests will be redirected. Same path with .json extension
     * will <i>not</i> be redirected. This feature is disabled by default.</li>
     * </ol>
     *
     * @param request the request to check
     * @return whether redirection for the given is allowed
     */
    private boolean doesRequestMatch(SlingHttpServletRequest request) {
        WCMMode wcmMode = WCMMode.fromRequest(request);
        if (wcmMode != null && wcmMode != WCMMode.DISABLED) {
            log.trace("Request in author mode: {}, no redirection.", wcmMode);
            return false;
        }

        String method = request.getMethod();
        if (!getMethods().contains(method)) {
            log.trace("Request method [{}] does not match any of {}.", method, methods);
            return false;
        }

        String ext = request.getRequestPathInfo().getExtension();
        if (ext != null && !getExtensions().isEmpty() && !getExtensions().contains(ext)) {
            log.trace("Request extension [{}] does not match any of {}.", ext, exts);
            return false;
        }

        String resourcePath = request.getRequestPathInfo().getResourcePath();
        boolean matches = getPaths().isEmpty() || getPaths().stream().anyMatch(p -> p.equals("/") || resourcePath.startsWith(p + "/"));
        if (!matches) {
            log.trace("Request path [{}] not within any of {}.", resourcePath, paths);
            return false;
        }
        return true;
    }

    /**
     * Match a path to a redirect configuration.
     *
     * @param slingRequest the request to match
     * @return redirect match or <code>null</code>
     */
    RedirectMatch match(SlingHttpServletRequest slingRequest) {
        Resource resource = slingRequest.getResource();
        // find context aware configuration for the requested resource, e.g. /conf/my-site/settings/redirects
        Resource configResource = configResolver.getResource(resource, config.bucketName(), config.configName());
        if(configResource == null){
            log.warn("no caconfig found for {}, bucketName: {}, configName: {}, user: {}",
                    resource.getPath(), config.bucketName(), config.configName(), slingRequest.getResourceResolver().getUserID());
            return null;
        }
        String configPath = configResource.getPath();
        try {
            RedirectConfiguration rules = rulesCache.get(configPath, () -> {
                RedirectConfiguration cfg = loadRules(configPath);
                return cfg == null ? RedirectConfiguration.EMPTY : cfg;
            });
            RequestPathInfo requestPathInfo = slingRequest.getRequestPathInfo();
            String resourcePath = requestPathInfo.getResourcePath(); // /content/mysite/en/page.html

            ValueMap properties = configResource.getValueMap();
            String contextPrefix = properties.get(Redirects.CFG_PROP_CONTEXT_PREFIX, "");
            boolean ignoreSelectors = properties.get(CFG_PROP_IGNORE_SELECTORS, false);
            if(ignoreSelectors && requestPathInfo.getSelectorString() != null){
                resourcePath = removeSelectors(resourcePath, resource.getResourceMetadata().getResolutionPathInfo());
            }
            RedirectMatch m = rules.match(resourcePath, contextPrefix, slingRequest);
            if (m == null && mapUrls()) { // try mapped url
                String mappedUrl= mapUrl(resourcePath, slingRequest); // https://www.mysite.com/en/page.html
                if(!resourcePath.equals(mappedUrl)) { // don't bother if sling mappings are not defined for this path
                    String mappedPath = URI.create(mappedUrl).getPath();  // /en/page.html
                    m = rules.match(mappedPath, "", slingRequest);
                }
            }
            return m;
        } catch (ExecutionException e){
            log.error("failed to load redirect rules from {}", configPath, e);
            return null;
        }
    }

    /**
     * Merges the context prefix with the path if needed.<br>
     * This means
     * <ul>
     *     <li>relative paths are joined with the context prefix</li>
     *     <li>absolute urls are returned unchanged</li>
     *     <li>rules with an <code>contextPrefixIgnored=true</code> flag are returned unchanged</li>
     * </ul>
     * Absolute urls and escaped paths will not be changed
     * @param path  the path to complete
     * @param redirectRule the redirect rule that is being handled
     * @param contextPrefix the context prefix
     * @return the correct path to redirect to
     */
    private String createFullPath(String path, RedirectRule redirectRule, String contextPrefix) {
        if(path == null) {
            return "";
        } else if(redirectRule.getContextPrefixIgnored()
                || isAbsoluteUrl(path)
                || path.startsWith(contextPrefix)) {
            return path;
        }
        return contextPrefix + path;
    }

    private boolean isAbsoluteUrl(String path) {
        Pattern httpRegex = Pattern.compile("^(https?:\\/\\/|www\\.|\\/\\/)(.*)");
        Matcher httpMatcher = httpRegex.matcher(path);
        return httpMatcher.matches();
    }

    static String removeSelectors(String resolutionPath, String resolutionPathInfo){
        if(resolutionPathInfo != null){
            return resolutionPath.replace(resolutionPathInfo, "");
        } else {
            return resolutionPath;
        }
    }

    /**
     * JMX Operation: Display loaded rules for a path, e.g. /conf/global/settings/redirects
     *
     * @return the redirect configurations in a tabular format for the MBean
     */
    @Override
    public TabularData getRedirectRules(String storagePath) throws OpenDataException {
        String sourceUrl = "Source Url";
        String targetUrl = "Target Url";
        String statusCode = "Status Code";
        String redirectRules = "Redirect Rules";
        CompositeType cacheEntryType = new CompositeType(redirectRules, redirectRules,
                new String[]{sourceUrl, targetUrl, statusCode},
                new String[]{sourceUrl, targetUrl, statusCode},
                new OpenType[]{SimpleType.STRING, SimpleType.STRING, SimpleType.INTEGER});

        TabularDataSupport tabularData = new TabularDataSupport(
                new TabularType(redirectRules, redirectRules, cacheEntryType, new String[]{sourceUrl}));


        RedirectConfiguration cfg = rulesCache.getIfPresent(storagePath);
        if(cfg != null) {
            Collection<RedirectRule> rules = new ArrayList<>();
            Map<String, RedirectRule> pathMatchingRules = cfg.getPathRules();
            if (pathMatchingRules != null) {
                rules.addAll(pathMatchingRules.values());
            }
            Map<String, RedirectRule> ignoreCaseRules = cfg.getCaseInsensitivePathRules();
            if (ignoreCaseRules != null) {
                rules.addAll(ignoreCaseRules.values());
            }
            Map<Pattern, RedirectRule> patternMatchingRules = cfg.getPatternRules();
            if (patternMatchingRules != null) {
                rules.addAll(patternMatchingRules.values());
            }
            for (RedirectRule rule : rules) {
                Map<String, Object> row = new LinkedHashMap<>();

                row.put(sourceUrl, rule.getSource());
                row.put(targetUrl, rule.getTarget());
                row.put(statusCode, rule.getStatusCode());
                tabularData.put(new CompositeDataSupport(cacheEntryType, row));
            }
        }
        return tabularData;
    }

    /**
     * JMX Operation: get a list of loaded configurations,
     * e.g. [/conf/global/settings/redirects, /conf/wknd/settings/redirects]
     */
    @Override
    public Collection<String> getRedirectConfigurations() {
        return rulesCache.asMap().keySet();
    }

    @Override
    public String getBucket(){
        return config.bucketName();
    }

    @Override
    public String getConfigName(){
        return config.configName();
    }

    void setAdditionalHeaders(RedirectRule redirectRule, HttpServletResponse response){
        for(Header header : onDeliveryHeaders){
            response.addHeader(header.getName(), header.getValue());
        }
        String ccHeader = redirectRule.getCacheControlHeader();
        if(StringUtils.isEmpty(ccHeader)) {
            ccHeader = redirectRule.getDefaultCacheControlHeader();
        }
        if(!StringUtils.isEmpty(ccHeader)){
            response.addHeader("Cache-Control", ccHeader);
        }
    }
}