/*
 * #%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;
    }
}
