DefaultHttpClientFilterResolver.java

/*
 * Copyright 2017-2020 original 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
 *
 * https://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 io.micronaut.http.client.filter;

import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationMetadataResolver;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.order.OrderUtil;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.annotation.ClientFilter;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.annotation.FilterMatcher;
import io.micronaut.http.filter.BaseFilterProcessor;
import io.micronaut.http.filter.FilterOrder;
import io.micronaut.http.filter.FilterPatternStyle;
import io.micronaut.http.filter.GenericHttpFilter;
import io.micronaut.http.filter.HttpClientFilter;
import io.micronaut.http.filter.HttpClientFilterResolver;
import jakarta.inject.Singleton;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Resolves filters for http clients.
 *
 * @author James Kleeh
 * @author graemerocher
 * @since 1.3.0
 */
@Internal
@Singleton
@BootstrapContextCompatible
public class DefaultHttpClientFilterResolver extends BaseFilterProcessor<ClientFilter> implements HttpClientFilterResolver<ClientFilterResolutionContext> {

    private final List<ClientFilterEntry> clientFilters;

    /**
     * Default constructor.
     *
     * @param beanContext                The bean context
     * @param annotationMetadataResolver The annotation metadata resolver
     * @param legacyClientFilters        All client filters
     */
    public DefaultHttpClientFilterResolver(
        BeanContext beanContext,
        AnnotationMetadataResolver annotationMetadataResolver,
        List<HttpClientFilter> legacyClientFilters) {
        super(beanContext, ClientFilter.class);
        this.clientFilters = legacyClientFilters.stream()
            .map(legacyClientFilter -> createClientFilterEntry(annotationMetadataResolver, legacyClientFilter))
            .collect(Collectors.toList());
    }

    @Override
    public List<FilterEntry> resolveFilterEntries(ClientFilterResolutionContext context) {
        return clientFilters.stream()
            .filter(entry -> matchesClientFilterEntry(context, entry))
            .collect(Collectors.toList());
    }

    @Override
    public List<GenericHttpFilter> resolveFilters(HttpRequest<?> request, List<FilterEntry> filterEntries) {
        String requestPath = StringUtils.prependUri("/", request.getUri().getPath());
        io.micronaut.http.HttpMethod method = request.getMethod();
        List<GenericHttpFilter> filterList = new ArrayList<>(filterEntries.size());
        for (FilterEntry filterEntry : filterEntries) {
            final GenericHttpFilter filter = filterEntry.getFilter();
            if (!GenericHttpFilter.isEnabled(filter)) {
                continue;
            }
            if (matchesFilterEntry(method, requestPath, filterEntry)) {
                filterList.add(filter);
            }
        }
        return filterList;
    }

    private boolean containsIdentifier(Collection<String> clientIdentifiers, Collection<String> clients) {
        return clients.stream().anyMatch(clientIdentifiers::contains);
    }

    private boolean anyPatternMatches(String requestPath, String[] patterns, FilterPatternStyle patternStyle) {
        return Arrays.stream(patterns).anyMatch(pattern -> patternStyle.getPathMatcher().matches(pattern, requestPath));
    }

    private boolean anyMethodMatches(HttpMethod requestMethod, Collection<HttpMethod> methods) {
        return methods.contains(requestMethod);
    }

    @Override
    protected void addFilter(Supplier<GenericHttpFilter> factory, AnnotationMetadata methodAnnotations, FilterMetadata metadata) {
        clientFilters.add(new ClientFilterEntry(
            factory.get(),
            methodAnnotations,
            metadata.methods() != null ? new HashSet<HttpMethod>(metadata.methods()) : Collections.emptySet(),
            metadata.patternStyle(),
            metadata.patterns(),
            metadata.serviceId(),
            metadata.excludeServiceId()
        ));
    }

    private boolean matchesFilterEntry(@NonNull HttpMethod method,
                                       @NonNull String requestPath,
                                       @NonNull FilterEntry filterEntry) {
        boolean matches = true;
        if (filterEntry.hasMethods()) {
            matches = anyMethodMatches(method, filterEntry.getFilterMethods());
        }
        if (filterEntry.hasPatterns()) {
            matches = matches && anyPatternMatches(requestPath, filterEntry.getPatterns(), filterEntry.getPatternStyle());
        }
        return matches;
    }

    private boolean matchesClientFilterEntry(@NonNull ClientFilterResolutionContext context,
                                             @NonNull ClientFilterEntry entry) {
        AnnotationMetadata annotationMetadata = entry.getAnnotationMetadata();
        boolean matches = !annotationMetadata.hasStereotype(FilterMatcher.class);
        String filterAnnotation = annotationMetadata.getAnnotationNameByStereotype(FilterMatcher.class).orElse(null);
        if (filterAnnotation != null && !matches) {
            matches = context.getAnnotationMetadata().hasStereotype(filterAnnotation);
        }

        if (matches && entry.serviceIds != null) {
            matches = containsIdentifier(context.getClientIds(), entry.serviceIds);
        }
        if (matches && entry.excludeServiceIds != null) {
            matches = !containsIdentifier(context.getClientIds(), entry.excludeServiceIds);
        }
        return matches;
    }

    @NonNull
    private ClientFilterEntry createClientFilterEntry(@NonNull AnnotationMetadataResolver annotationMetadataResolver,
                                                      @NonNull HttpClientFilter httpClientFilter) {
        AnnotationMetadata annotationMetadata = annotationMetadataResolver.resolveMetadata(httpClientFilter);
        FilterPatternStyle patternStyle = annotationMetadata.enumValue(Filter.class,
            "patternStyle", FilterPatternStyle.class).orElse(FilterPatternStyle.ANT);
        return new ClientFilterEntry(
            GenericHttpFilter.createLegacyFilter(httpClientFilter, new FilterOrder.Dynamic(OrderUtil.getOrder(annotationMetadata))),
            annotationMetadata,
            methodsForFilter(annotationMetadata),
            patternStyle,
            List.of(annotationMetadata.stringValues(Filter.class)),
            serviceIdsForFilter(annotationMetadata),
            excludeServiceIdsForFilter(annotationMetadata)
        );
    }

    @Nullable
    private static List<String> excludeServiceIdsForFilter(@NonNull AnnotationMetadata annotationMetadata) {
        return idsForFilter(annotationMetadata, "excludeServiceId");
    }

    @Nullable
    private static List<String> serviceIdsForFilter(@NonNull AnnotationMetadata annotationMetadata) {
        return idsForFilter(annotationMetadata, "serviceId");
    }

    @Nullable
    private static List<String> idsForFilter(@NonNull AnnotationMetadata annotationMetadata, @NonNull String member) {
        String[] ids = annotationMetadata.stringValues(Filter.class, member);
        return ArrayUtils.isNotEmpty(ids) ? List.of(ids) : null;
    }

    @NonNull
    private static Set<HttpMethod> methodsForFilter(@NonNull AnnotationMetadata annotationMetadata) {
        HttpMethod[] methods = annotationMetadata.enumValues(Filter.class, "methods", HttpMethod.class);
        final Set<HttpMethod> httpMethods = new HashSet<>(Arrays.asList(methods));
        if (annotationMetadata.hasStereotype(FilterMatcher.class)) {
            httpMethods.addAll(
                Arrays.asList(annotationMetadata.enumValues(FilterMatcher.class, "methods", HttpMethod.class))
            );
        }
        return httpMethods;
    }

    private record ClientFilterEntry(
        GenericHttpFilter filter,
        AnnotationMetadata annotationMetadata,
        Set<HttpMethod> httpMethods,
        FilterPatternStyle patternStyle,
        List<String> patterns,
        @Nullable List<String> serviceIds,
        @Nullable List<String> excludeServiceIds
    ) implements FilterEntry {

        @NonNull
        @Override
        public AnnotationMetadata getAnnotationMetadata() {
            return annotationMetadata;
        }

        @Override
        public GenericHttpFilter getFilter() {
            return filter;
        }

        @Override
        public Set<HttpMethod> getFilterMethods() {
            return httpMethods;
        }

        @Override
        public String[] getPatterns() {
            return patterns.toArray(String[]::new);
        }

        @Override
        public FilterPatternStyle getPatternStyle() {
            return patternStyle;
        }

        @Override
        public boolean hasMethods() {
            return CollectionUtils.isNotEmpty(httpMethods);
        }

        @Override
        public boolean hasPatterns() {
            return CollectionUtils.isNotEmpty(patterns);
        }
    }
}