public interface RateLimitKeyGenerator {

    /**
     * Returns a key based on {@link HttpServletRequest}, {@link Route} and
     * {@link RateLimitProperties.Policy}
     *
     * @param request The {@link HttpServletRequest}
     * @param route   The {@link Route}
     * @param policy  The {@link RateLimitProperties.Policy}
     * @return Generated key
     */
    String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy);
} public interface RateLimitUtils {

    /**
     * Returns the authenticated user from {@link HttpServletRequest}.
     *
     * @param request The {@link HttpServletRequest}
     * @return The authenticated user or annonymous
     */
    String getUser(HttpServletRequest request);

    /**
     * Returns the remote IP address from {@link HttpServletRequest}.
     *
     * @param request The {@link HttpServletRequest}
     * @return The remote IP address
     */
    String getRemoteAddress(HttpServletRequest request);

    /**
     * Returns the authenticated user's roles.
     *
     * @return The authenticated user's roles or empty
     */
    Set<String> getUserRoles();

} public interface RateLimiter {

    String QUOTA_SUFFIX = "-quota";

    /**
     * @param policy      Template for which rates should be created in case there's no rate limit associated with the
     *                    key
     * @param key         Unique key that identifies a request
     * @param requestTime The total time it took to handle the request
     * @return a view of a user's rate request limit
     */
    Rate consume(Policy policy, String key, Long requestTime);
} @Validated
@RefreshScope
@ConfigurationProperties(RateLimitProperties.PREFIX)
public class RateLimitProperties {

    public static final String PREFIX = "zuul.ratelimit";

    @Valid
    @NotNull
    @Policies
    @NestedConfigurationProperty
    private List<Policy> defaultPolicyList = Lists.newArrayList();

    @Valid
    @NotNull
    @Policies
    @NestedConfigurationProperty
    private Map<String, List<Policy>> policyList = Maps.newHashMap();

    private boolean behindProxy;

    private boolean enabled;

    @NotNull
    private ResponseHeadersVerbosity responseHeaders = VERBOSE;

    @NotNull
    @Value("${spring.application.name:rate-limit-application}")
    private String keyPrefix;

    @NotNull
    private RateLimitRepository repository;

    private int postFilterOrder = SEND_RESPONSE_FILTER_ORDER - 10;

    private int preFilterOrder = FORM_BODY_WRAPPER_FILTER_ORDER;

    @NestedConfigurationProperty
    private DenyRequest denyRequest = new DenyRequest();

    public List<Policy> getPolicies(String key) {
        return policyList.getOrDefault(key, defaultPolicyList);
    }

    public List<Policy> getDefaultPolicyList() {
        return defaultPolicyList;
    }

    public void setDefaultPolicyList(List<Policy> defaultPolicyList) {
        this.defaultPolicyList = defaultPolicyList;
    }

    public Map<String, List<Policy>> getPolicyList() {
        return policyList;
    }

    public void setPolicyList(Map<String, List<Policy>> policyList) {
        this.policyList = policyList;
    }

    public boolean isBehindProxy() {
        return behindProxy;
    }

    public void setBehindProxy(boolean behindProxy) {
        this.behindProxy = behindProxy;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * Tells if rate limit response headers should be added to response.
     *
     * @deprecated use {{@link #responseHeaders}
     * @return Whether the response headers should be added
     */
    @Deprecated
    @DeprecatedConfigurationProperty(replacement = "zuul.ratelimit.response-headers")
    public boolean isAddResponseHeaders() {
        return !NONE.equals(responseHeaders);
    }

    public void setAddResponseHeaders(boolean addResponseHeaders) {
        setResponseHeaders(addResponseHeaders ? VERBOSE : NONE);
    }

    public ResponseHeadersVerbosity getResponseHeaders() {
        return this.responseHeaders;
    }

    public void setResponseHeaders(ResponseHeadersVerbosity responseHeaders) {
        this.responseHeaders = responseHeaders;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public RateLimitRepository getRepository() {
        return repository;
    }

    public void setRepository(RateLimitRepository repository) {
        this.repository = repository;
    }

    public int getPostFilterOrder() {
        return postFilterOrder;
    }

    public void setPostFilterOrder(int postFilterOrder) {
        this.postFilterOrder = postFilterOrder;
    }

    public int getPreFilterOrder() {
        return preFilterOrder;
    }

    public void setPreFilterOrder(int preFilterOrder) {
        this.preFilterOrder = preFilterOrder;
    }

    public DenyRequest getDenyRequest() {
        return denyRequest;
    }

    public void setDenyRequest(DenyRequest denyRequest) {
        this.denyRequest = denyRequest;
    }

    public static class Policy {
        /**
         * Refresh interval window (in seconds).
         */
        @NotNull
        @DurationUnit(ChronoUnit.SECONDS)
        private Duration refreshInterval = Duration.ofSeconds(60);

        /**
         * Request number limit per refresh interval window.
         */
        private Long limit;

        /**
         * Request time limit per refresh interval window (in seconds).
         */
        @DurationUnit(ChronoUnit.SECONDS)
        private Duration quota;

        private boolean breakOnMatch;

        @Valid
        @NotNull
        @NestedConfigurationProperty
        private List<MatchType> type = Lists.newArrayList();

        public Duration getRefreshInterval() {
            return refreshInterval;
        }

        public void setRefreshInterval(Duration refreshInterval) {
            this.refreshInterval = refreshInterval;
        }

        public Long getLimit() {
            return limit;
        }

        public void setLimit(Long limit) {
            this.limit = limit;
        }

        public Duration getQuota() {
            return quota;
        }

        public void setQuota(Duration quota) {
            this.quota = quota;
        }

        public boolean isBreakOnMatch() {
            return breakOnMatch;
        }

        public void setBreakOnMatch(boolean breakOnMatch) {
            this.breakOnMatch = breakOnMatch;
        }

        public List<MatchType> getType() {
            return type;
        }

        public void setType(List<MatchType> type) {
            this.type = type;
        }

        public static class MatchType {

            @Valid
            @NotNull
            private RateLimitType type;

            private String matcher;

            public MatchType(@Valid @NotNull RateLimitType type, String matcher) {
                this.type = type;
                this.matcher = matcher;
            }

            public boolean apply(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
                return StringUtils.isEmpty(matcher) || type.apply(request, route, rateLimitUtils, matcher);
            }

            public String key(HttpServletRequest request, Route route, RateLimitUtils rateLimitUtils) {
                return type.key(request, route, rateLimitUtils, matcher) +
                        (StringUtils.isEmpty(matcher) ? StringUtils.EMPTY : (":" + matcher));
            }

            public RateLimitType getType() {
                return type;
            }

            public void setType(RateLimitType type) {
                this.type = type;
            }

            public String getMatcher() {
                return matcher;
            }

            public void setMatcher(String matcher) {
                this.matcher = matcher;
            }
        }
    }

    public static class DenyRequest {

        /**
         * List of origins that will have the request denied.
         */
        @NotNull
        private List<String> origins = Lists.newArrayList();

        /**
         * Status code returned when a blocked origin tries to reach the server.
         */
        private int responseStatusCode = HttpStatus.FORBIDDEN.value();

        public List<String> getOrigins() {
            return origins;
        }

        public void setOrigins(List<String> origins) {
            this.origins = origins;
        }

        public int getResponseStatusCode() {
            return responseStatusCode;
        }

        public void setResponseStatusCode(int responseStatusCode) {
            this.responseStatusCode = responseStatusCode;
        }
    }
} public class ConsulRateLimiter extends AbstractRateLimiter {

    private static Logger log = LoggerFactory.getLogger(ConsulRateLimiter.class);

    private final ConsulClient consulClient;
    private final ObjectMapper objectMapper;

    public ConsulRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler,
        ConsulClient consulClient, ObjectMapper objectMapper) {
        super(rateLimiterErrorHandler);
        this.consulClient = consulClient;
        this.objectMapper = objectMapper;
    }

    @Override
    protected Rate getRate(final String key) {
        Rate rate = null;
        GetValue value = this.consulClient.getKVValue(buildValidConsulKey(key)).getValue();
        if (value != null && value.getDecodedValue() != null) {
            try {
                rate = this.objectMapper.readValue(value.getDecodedValue(), Rate.class);
            } catch (IOException e) {
                log.error("Failed to deserialize Rate", e);
            }
        }
        return rate;
    }

    @Override
    protected void saveRate(Rate rate) {
        String value = "";
        try {
            value = this.objectMapper.writeValueAsString(rate);
        } catch (JsonProcessingException e) {
            log.error("Failed to serialize Rate", e);
        }

        if (hasText(value)) {
            this.consulClient.setKVValue(buildValidConsulKey(rate.getKey()), value);
        }
    }

    // Slash will corrupt Consul Call , to be changed to _
    private String buildValidConsulKey(String key){
        if(isNotBlank(key)){
            return key.replaceAll("/", "_");
        }
        return key;
    }

} public class DefaultRateLimiterErrorHandler implements RateLimiterErrorHandler {

    private static final Logger log = LoggerFactory.getLogger(DefaultRateLimiterErrorHandler.class);

    @Override
    public void handleSaveError(String key, Exception e) {
        log.error("Failed saving rate for " + key + ", returning unsaved rate", e);
    }

    @Override
    public void handleFetchError(String key, Exception e) {
        log.error("Failed retrieving rate for " + key + ", will create new rate", e);
    }

    @Override
    public void handleError(String msg, Exception e) {
        log.error(msg, e);
    }
} public interface RateLimiterErrorHandler {

    void handleSaveError(String key, Exception e);

    void handleFetchError(String key, Exception e);

    void handleError(String msg, Exception e);

} public class RedisRateLimiter extends AbstractNonBlockCacheRateLimiter {

    private final RateLimiterErrorHandler rateLimiterErrorHandler;
    private final StringRedisTemplate redisTemplate;
    private final RedisScript<Long> redisScript;

    public RedisRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler,
                            final StringRedisTemplate redisTemplate) {
        this.rateLimiterErrorHandler = rateLimiterErrorHandler;
        this.redisTemplate = redisTemplate;
        this.redisScript = getScript();
    }

    @Override
    protected void calcRemainingLimit(final Long limit, final Duration refreshInterval,
                                      final Long requestTime, final String key, final Rate rate) {
        if (Objects.nonNull(limit)) {
            long usage = requestTime == null ? 1L : 0L;
            Long remaining = calcRemaining(limit, refreshInterval, usage, key, rate);
            rate.setRemaining(remaining);
        }
    }

    @Override
    protected void calcRemainingQuota(final Long quota, final Duration refreshInterval,
                                      final Long requestTime, final String key, final Rate rate) {
        if (Objects.nonNull(quota)) {
            String quotaKey = key + QUOTA_SUFFIX;
            long usage = requestTime != null ? requestTime : 0L;
            Long remaining = calcRemaining(quota, refreshInterval, usage, quotaKey, rate);
            rate.setRemainingQuota(remaining);
        }
    }

    private Long calcRemaining(Long limit, Duration refreshInterval, long usage, String key, Rate rate) {
        rate.setReset(refreshInterval.toMillis());
        Long current = 0L;
        try {
            current = redisTemplate.execute(redisScript, Collections.singletonList(key), Long.toString(usage),
                    Long.toString(refreshInterval.getSeconds()));
        } catch (RuntimeException e) {
            String msg = "Failed retrieving rate for " + key + ", will return the current value";
            rateLimiterErrorHandler.handleError(msg, e);
        }
        return Math.max(-1, limit - (current != null ? current.intValue() : 0));
    }

    private RedisScript<Long> getScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setLocation(new ClassPathResource("/scripts/ratelimit.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
} public class Bucket4jHazelcastRateLimiter extends AbstractBucket4jRateLimiter<HazelcastBucketBuilder, Hazelcast> {

    private final IMap<String, GridBucketState> rateLimit;

    public Bucket4jHazelcastRateLimiter(final IMap<String, GridBucketState> rateLimit) {
        super(io.github.bucket4j.grid.hazelcast.Hazelcast.class);
        this.rateLimit = rateLimit;
        super.init();
    }

    @Override
    protected ProxyManager<String> getProxyManager(io.github.bucket4j.grid.hazelcast.Hazelcast extension) {
        return extension.proxyManagerForMap(rateLimit);
    }
} public class Bucket4jIgniteRateLimiter extends AbstractBucket4jRateLimiter<IgniteBucketBuilder, Ignite> {

    private final IgniteCache<String, GridBucketState> cache;

    public Bucket4jIgniteRateLimiter(final IgniteCache<String, GridBucketState> cache) {
        super(io.github.bucket4j.grid.ignite.Ignite.class);
        this.cache = cache;
        super.init();
    }

    @Override
    protected ProxyManager<String> getProxyManager(io.github.bucket4j.grid.ignite.Ignite extension) {
        return extension.proxyManagerForCache(cache);
    }
} public class Bucket4jInfinispanRateLimiter extends AbstractBucket4jRateLimiter<InfinispanBucketBuilder, Infinispan> {

    private final ReadWriteMap<String, GridBucketState> readWriteMap;

    public Bucket4jInfinispanRateLimiter(final ReadWriteMap<String, GridBucketState> readWriteMap) {
        super(io.github.bucket4j.grid.infinispan.Infinispan.class);
        this.readWriteMap = readWriteMap;
        super.init();
    }

    @Override
    protected ProxyManager<String> getProxyManager(io.github.bucket4j.grid.infinispan.Infinispan extension) {
        return extension.proxyManagerForMap(readWriteMap);
    }
} public class Bucket4jJCacheRateLimiter extends AbstractBucket4jRateLimiter<JCacheBucketBuilder, JCache> {

    private final Cache<String, GridBucketState> cache;

    public Bucket4jJCacheRateLimiter(final Cache<String, GridBucketState> cache) {
        super(io.github.bucket4j.grid.jcache.JCache.class);
        this.cache = cache;
        super.init();
    }

    @Override
    protected ProxyManager<String> getProxyManager(io.github.bucket4j.grid.jcache.JCache extension) {
        return extension.proxyManagerForCache(cache);
    }
} public class JpaRateLimiter extends AbstractRateLimiter {

    private final RateLimiterRepository repository;

    public JpaRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler,
                          final RateLimiterRepository repository) {
        super(rateLimiterErrorHandler);
        this.repository = repository;
    }

    @Override
    protected Rate getRate(String key) {
        return this.repository.findById(key).orElse(null);
    }

    @Override
    protected void saveRate(Rate rate) {
        this.repository.save(rate);
    }

} public interface RateLimiterRepository extends CrudRepository<Rate, String> {

} public class RateLimitPostFilter extends AbstractRateLimitFilter {

    private final RateLimiter rateLimiter;
    private final RateLimitKeyGenerator rateLimitKeyGenerator;

    public RateLimitPostFilter(final RateLimitProperties properties, final RouteLocator routeLocator,
                               final UrlPathHelper urlPathHelper, final RateLimiter rateLimiter,
                               final RateLimitKeyGenerator rateLimitKeyGenerator, final RateLimitUtils rateLimitUtils) {
        super(properties, routeLocator, urlPathHelper, rateLimitUtils);
        this.rateLimiter = rateLimiter;
        this.rateLimitKeyGenerator = rateLimitKeyGenerator;
    }

    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return properties.getPostFilterOrder();
    }

    @Override
    public boolean shouldFilter() {
        return super.shouldFilter() && getRequestStartTime() != null;
    }

    private Long getRequestStartTime() {
        final RequestContext ctx = RequestContext.getCurrentContext();
        final HttpServletRequest request = ctx.getRequest();
        return (Long) request.getAttribute(REQUEST_START_TIME);
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        Route route = route(request);

        policy(route, request).forEach(policy -> {
            long requestTime = System.currentTimeMillis() - getRequestStartTime();
            String key = rateLimitKeyGenerator.key(request, route, policy);
            rateLimiter.consume(policy, key, requestTime > 0 ? requestTime : 1);
        });

        return null;
    }
} public class RateLimitPreFilter extends AbstractRateLimitFilter {

    private final RateLimiter rateLimiter;
    private final RateLimitKeyGenerator rateLimitKeyGenerator;
    private final ApplicationEventPublisher eventPublisher;

    public RateLimitPreFilter(final RateLimitProperties properties, final RouteLocator routeLocator,
                              final UrlPathHelper urlPathHelper, final RateLimiter rateLimiter,
                              final RateLimitKeyGenerator rateLimitKeyGenerator, final RateLimitUtils rateLimitUtils,
                              final ApplicationEventPublisher eventPublisher) {
        super(properties, routeLocator, urlPathHelper, rateLimitUtils);
        this.rateLimiter = rateLimiter;
        this.rateLimitKeyGenerator = rateLimitKeyGenerator;
        this.eventPublisher = eventPublisher;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return properties.getPreFilterOrder();
    }

    @Override
    public Object run() {
        final RequestContext ctx = RequestContext.getCurrentContext();
        final HttpServletResponse response = ctx.getResponse();
        final HttpServletRequest request = ctx.getRequest();
        final Route route = route(request);

        policy(route, request).forEach(policy -> {
            Map<String, String> responseHeaders = Maps.newHashMap();

            final String key = rateLimitKeyGenerator.key(request, route, policy);
            final Rate rate = rateLimiter.consume(policy, key, null);

            final Long limit = policy.getLimit();
            final Long remaining = rate.getRemaining();
            if (limit != null) {
                responseHeaders.put(HEADER_LIMIT, String.valueOf(limit));
                responseHeaders.put(HEADER_REMAINING, String.valueOf(Math.max(remaining, 0)));
            }

            final Duration quota = policy.getQuota();
            final Long remainingQuota = rate.getRemainingQuota();
            if (quota != null) {
                request.setAttribute(REQUEST_START_TIME, System.currentTimeMillis());
                responseHeaders.put(HEADER_QUOTA, String.valueOf(quota.getSeconds()));
                responseHeaders.put(HEADER_REMAINING_QUOTA, String.valueOf(MILLISECONDS.toSeconds(Math.max(remainingQuota, 0))));
            }

            responseHeaders.put(HEADER_RESET, String.valueOf(rate.getReset()));

            if (!NONE.equals(properties.getResponseHeaders())) {
                final String httpHeaderKey = key.replaceAll("[^A-Za-z0-9-.]", "_").replaceAll("__", "_");
                for (Map.Entry<String, String> headersEntry : responseHeaders.entrySet()) {
                    String header = VERBOSE.equals(properties.getResponseHeaders()) ? headersEntry.getKey() + "-" + httpHeaderKey : headersEntry.getKey();
                    response.setHeader(header, headersEntry.getValue());
                }
            }

            if ((limit != null && remaining < 0) || (quota != null && remainingQuota < 0)) {
                ctx.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
                ctx.put(RATE_LIMIT_EXCEEDED, "true");
                ctx.setSendZuulResponse(false);

                eventPublisher.publishEvent(new RateLimitExceededEvent(this, policy, rateLimitUtils.getRemoteAddress(request)));

                throw new RateLimitExceededException();
            }
        });

        return null;
    }
} public class DefaultRateLimitKeyGenerator implements RateLimitKeyGenerator {

    private final RateLimitProperties properties;
    private final RateLimitUtils rateLimitUtils;

    public DefaultRateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
        this.properties = properties;
        this.rateLimitUtils = rateLimitUtils;
    }

    @Override
    public String key(final HttpServletRequest request, final Route route, final Policy policy) {
        final StringJoiner joiner = new StringJoiner(":");
        joiner.add(properties.getKeyPrefix());
        if (route != null) {
            joiner.add(route.getId());
        }
        policy.getType().forEach(matchType -> {
            String key = matchType.key(request, route, rateLimitUtils);
            if (StringUtils.isNotEmpty(key)) {
                joiner.add(key);
            }
        });
        return joiner.toString();
    }
} public class DefaultRateLimitUtils implements RateLimitUtils {

    private static final String ANONYMOUS_USER = "anonymous";
    private static final String X_FORWARDED_FOR_HEADER_DELIMITER = ",";

    private final RateLimitProperties properties;

    public DefaultRateLimitUtils(RateLimitProperties properties) {
        this.properties = properties;
    }

    @Override
    public String getUser(final HttpServletRequest request) {
        return request.getRemoteUser() != null ? request.getRemoteUser() : ANONYMOUS_USER;
    }

    @Override
    public String getRemoteAddress(final HttpServletRequest request) {
        String xForwardedFor = request.getHeader(X_FORWARDED_FOR_HEADER);
        if (properties.isBehindProxy() && xForwardedFor != null) {
            return xForwardedFor.split(X_FORWARDED_FOR_HEADER_DELIMITER)[0].trim();
        }
        return request.getRemoteAddr();
    }

    @Override
    public Set<String> getUserRoles() {
        throw new UnsupportedOperationException("Not supported");
    }
} public class SecuredRateLimitUtils extends DefaultRateLimitUtils {

    public SecuredRateLimitUtils(final RateLimitProperties properties) {
        super(properties);
    }

    @Override
    public Set<String> getUserRoles() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return emptySet();
        }
        return AuthorityUtils.authorityListToSet(authentication.getAuthorities());
    }
} public final class StringToMatchTypeConverter implements Converter<String, MatchType> {

    private static final String DELIMITER = "=";

    @Override
    public MatchType convert(@NotNull String type) {
        if (type.contains(DELIMITER)) {
            String[] matchType = type.split(DELIMITER);
            return new MatchType(RateLimitType.valueOf(matchType[0].toUpperCase()), matchType[1]);
        }
        return new MatchType(RateLimitType.valueOf(type.toUpperCase()), null);
    }
} public static final String PREFIX = "zuul.ratelimit";
import com.ecwid.consul.v1.ConsulClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hazelcast.map.IMap;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitKeyGenerator;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.ConsulRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.DefaultRateLimiterErrorHandler;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RedisRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j.Bucket4jHazelcastRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j.Bucket4jIgniteRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j.Bucket4jInfinispanRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j.Bucket4jJCacheRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.JpaRateLimiter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.RateLimiterRepository;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPostFilter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.SecuredRateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.StringToMatchTypeConverter;
import com.netflix.zuul.ZuulFilter;
import io.github.bucket4j.grid.GridBucketState;
import io.github.bucket4j.grid.hazelcast.Hazelcast;
import io.github.bucket4j.grid.ignite.Ignite;
import io.github.bucket4j.grid.infinispan.Infinispan;
import io.github.bucket4j.grid.jcache.JCache;
import org.apache.ignite.IgniteCache;
import org.infinispan.functional.FunctionalMap.ReadWriteMap;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.consul.ConditionalOnConsulEnabled;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.util.UrlPathHelper;
import javax.cache.Cache;
import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.PREFIX;
/*
 * Copyright 2012-2018 the original author or authors.
 *
 * 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.marcosbarbero.cloud.autoconfigure.zuul.ratelimit;




/**
 * @author Marcos Barbero
 * @author Liel Chayoun
 */
@Configuration
@EnableConfigurationProperties(RateLimitProperties.class)
@ConditionalOnProperty(prefix = PREFIX, name = "enabled", havingValue = "true")
public class RateLimitAutoConfiguration {

    private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper();

    @Bean
    @ConfigurationPropertiesBinding
    public StringToMatchTypeConverter stringToMatchTypeConverter() {
        return new StringToMatchTypeConverter();
    }

    @Bean
    @ConditionalOnMissingBean(RateLimiterErrorHandler.class)
    public RateLimiterErrorHandler rateLimiterErrorHandler() {
        return new DefaultRateLimiterErrorHandler();
    }

    @Bean
    public ZuulFilter rateLimiterPreFilter(final RateLimiter rateLimiter, final RateLimitProperties rateLimitProperties,
                                           final RouteLocator routeLocator, final RateLimitKeyGenerator rateLimitKeyGenerator,
                                           final RateLimitUtils rateLimitUtils, final ApplicationEventPublisher eventPublisher) {
        return new RateLimitPreFilter(rateLimitProperties, routeLocator, URL_PATH_HELPER, rateLimiter,
                rateLimitKeyGenerator, rateLimitUtils, eventPublisher);
    }

    @Bean
    public ZuulFilter rateLimiterPostFilter(final RateLimiter rateLimiter, final RateLimitProperties rateLimitProperties,
                                            final RouteLocator routeLocator, final RateLimitKeyGenerator rateLimitKeyGenerator,
                                            final RateLimitUtils rateLimitUtils) {
        return new RateLimitPostFilter(rateLimitProperties, routeLocator, URL_PATH_HELPER, rateLimiter,
                rateLimitKeyGenerator, rateLimitUtils);
    }

    @Bean
    @ConditionalOnMissingBean(RateLimitKeyGenerator.class)
    public RateLimitKeyGenerator ratelimitKeyGenerator(final RateLimitProperties properties,
                                                       final RateLimitUtils rateLimitUtils) {
        return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils);