All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.linecorp.armeria.server.Routers Maven / Gradle / Ivy

Go to download

Asynchronous HTTP/2 RPC/REST client/server library built on top of Java 8, Netty, Thrift and GRPC (armeria-shaded)

There is a newer version: 0.75.0
Show newest version
/*
 * Copyright 2017 LINE Corporation
 *
 * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server;

import static com.linecorp.armeria.server.RouteCache.wrapCompositeServiceRouter;
import static com.linecorp.armeria.server.RouteCache.wrapVirtualHostRouter;
import static java.util.Objects.requireNonNull;

import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.Response;
import com.linecorp.armeria.server.RoutingTrie.Builder;
import com.linecorp.armeria.server.composition.CompositeServiceEntry;

/**
 * A factory that creates a {@link Router} instance.
 */
public final class Routers {
    private static final Logger logger = LoggerFactory.getLogger(Routers.class);

    /**
     * Returns the default implementation of the {@link Router} to find a {@link ServiceConfig}.
     * It consists of several router implementations which use one of Trie and List. It also includes
     * cache mechanism to improve its performance.
     */
    public static Router ofVirtualHost(VirtualHost virtualHost, Iterable configs,
                                                      RejectedPathMappingHandler rejectionHandler) {
        requireNonNull(virtualHost, "virtualHost");
        requireNonNull(configs, "configs");
        requireNonNull(rejectionHandler, "rejectionHandler");

        final BiConsumer rejectionConsumer = (mapping, existingMapping) -> {
            try {
                rejectionHandler.handleDuplicatePathMapping(virtualHost, mapping, existingMapping);
            } catch (Exception e) {
                logger.warn("Unexpected exception from a {}:",
                            RejectedPathMappingHandler.class.getSimpleName(), e);
            }
        };

        return wrapVirtualHostRouter(defaultRouter(configs, ServiceConfig::pathMapping, rejectionConsumer));
    }

    /**
     * Returns the default implementation of the {@link Router} to find a {@link CompositeServiceEntry}.
     */
    public static  Router> ofCompositeService(
            List> entries) {
        requireNonNull(entries, "entries");

        final Router> delegate = wrapCompositeServiceRouter(defaultRouter(
                entries, CompositeServiceEntry::pathMapping,
                (mapping, existingMapping) -> {
                    final String a = mapping.toString();
                    final String b = existingMapping.toString();
                    if (a.equals(b)) {
                        throw new IllegalStateException(
                                "Your composite service has a duplicate path mapping: " + a);
                    }

                    throw new IllegalStateException(
                            "Your composite service has path mappings with a conflict: " +
                            a + " vs. " + b);
                }));

        return new CompositeRouter<>(delegate, result ->
                result.isPresent() ? PathMapped.of(result.mapping(), result.mappingResult(),
                                                   result.value().service())
                                   : PathMapped.empty());
    }

    /**
     * Returns the default implementation of {@link Router}. It consists of several router implementations
     * which use one of Trie and List. Consecutive {@link ServiceConfig}s would be grouped according to whether
     * it is able to produce trie path string or not while traversing the list, then each group would be
     * transformed to a {@link Router}.
     */
    private static  Router defaultRouter(Iterable values,
                                               Function pathMappingResolver,
                                               BiConsumer rejectionHandler) {
        return new CompositeRouter<>(routers(values, pathMappingResolver, rejectionHandler),
                                     Function.identity());
    }

    /**
     * Returns a list of {@link Router}s.
     */
    @VisibleForTesting
    static  List> routers(Iterable values, Function pathMappingResolver,
                                       BiConsumer rejectionHandler) {
        rejectDuplicateMapping(values, pathMappingResolver, rejectionHandler);

        final ImmutableList.Builder> builder = ImmutableList.builder();
        final List group = new ArrayList<>();

        boolean addingTrie = true;

        for (V value : values) {
            final PathMapping mapping = pathMappingResolver.apply(value);
            final boolean triePathPresent = mapping.triePath().isPresent();
            if (addingTrie && triePathPresent || !addingTrie && !triePathPresent) {
                // We are adding the same type of PathMapping to 'group'.
                group.add(value);
                continue;
            }

            // Changed the router type.
            if (!group.isEmpty()) {
                builder.add(router(addingTrie, group, pathMappingResolver));
            }
            addingTrie = !addingTrie;
            group.add(value);
        }
        if (!group.isEmpty()) {
            builder.add(router(addingTrie, group, pathMappingResolver));
        }
        return builder.build();
    }

    private static  void rejectDuplicateMapping(
            Iterable values, Function pathMappingResolver,
            BiConsumer rejectionHandler) {

        final Map> triePath2mappings = new HashMap<>();
        for (V v : values) {
            final PathMapping mapping = pathMappingResolver.apply(v);
            final Optional triePathOpt = mapping.triePath();
            if (!triePathOpt.isPresent()) {
                continue;
            }
            final String triePath = triePathOpt.get();
            final List existingMappings =
                    triePath2mappings.computeIfAbsent(triePath, unused -> new ArrayList<>());
            for (PathMapping existingMapping : existingMappings) {
                if (mapping.complexity() != existingMapping.complexity()) {
                    continue;
                }

                if (mapping.getClass() != existingMapping.getClass()) {
                    continue;
                }

                if (!(mapping instanceof HttpHeaderPathMapping)) {
                    assert mapping.complexity() == 0;
                    assert existingMapping.complexity() == 0;
                    rejectionHandler.accept(mapping, existingMapping);
                    return;
                }

                final HttpHeaderPathMapping headerMapping = (HttpHeaderPathMapping) mapping;
                final HttpHeaderPathMapping existingHeaderMapping = (HttpHeaderPathMapping) existingMapping;
                if (headerMapping.supportedMethods().stream().noneMatch(
                        method -> existingHeaderMapping.supportedMethods().contains(method))) {
                    // No overlap in supported methods.
                    continue;
                }
                if (!headerMapping.consumeTypes().isEmpty() &&
                    headerMapping.consumeTypes().stream().noneMatch(
                            mediaType -> existingHeaderMapping.consumeTypes().contains(mediaType))) {
                    // No overlap in consume types.
                    continue;
                }
                if (!headerMapping.produceTypes().isEmpty() &&
                    headerMapping.produceTypes().stream().noneMatch(
                            mediaType -> existingHeaderMapping.produceTypes().contains(mediaType))) {
                    // No overlap in produce types.
                    continue;
                }

                rejectionHandler.accept(mapping, existingMapping);
                return;
            }

            existingMappings.add(mapping);
        }
    }

    /**
     * Returns a {@link Router} implementation which is using one of {@link RoutingTrie} and {@link List}.
     */
    private static  Router router(boolean isTrie, List values,
                                        Function pathMappingResolver) {
        final Comparator valueComparator =
                Comparator.comparingInt(e -> -1 * pathMappingResolver.apply(e).complexity());

        final Router router;
        if (isTrie) {
            final RoutingTrie.Builder builder = new Builder<>();
            // Set a comparator to sort services by the number of conditions to be checked in a descending
            // order.
            builder.comparator(valueComparator);
            values.forEach(v -> builder.add(pathMappingResolver.apply(v).triePath().get(), v));
            router = new TrieRouter<>(builder.build(), pathMappingResolver);
        } else {
            values.sort(valueComparator);
            router = new SequentialRouter<>(values, pathMappingResolver);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Router created for {} service(s): {}",
                         values.size(), router.getClass().getSimpleName());
            values.forEach(c -> {
                final PathMapping mapping = pathMappingResolver.apply(c);
                logger.debug("meterTag: {}, complexity: {}", mapping.meterTag(), mapping.complexity());
            });
        }
        values.clear();
        return router;
    }

    /**
     * Finds the most suitable service from the given {@link ServiceConfig} list.
     */
    private static  PathMapped findsBest(PathMappingContext mappingCtx, @Nullable List values,
                                               Function pathMappingResolver) {
        PathMapped result = PathMapped.empty();
        if (values != null) {
            for (V value : values) {
                final PathMapping mapping = pathMappingResolver.apply(value);
                final PathMappingResult mappingResult = mapping.apply(mappingCtx);
                if (mappingResult.isPresent()) {
                    //
                    // The services are sorted as follows:
                    //
                    // 1) annotated service with method and media type negotiation
                    //    (consumable and producible)
                    // 2) annotated service with method and producible media type negotiation
                    // 3) annotated service with method and consumable media type negotiation
                    // 4) annotated service with method negotiation
                    // 5) the other services (in a registered order)
                    //
                    // 1) and 2) may produce a score between the lowest and the highest because they should
                    // negotiate the produce type with the value of 'Accept' header.
                    // 3), 4) and 5) always produces the lowest score.
                    //

                    // Found the best matching.
                    if (mappingResult.hasHighestScore()) {
                        result = PathMapped.of(mapping, mappingResult, value);
                        break;
                    }

                    // This means that the 'mappingResult' is produced by one of 3), 4) and 5).
                    // So we have no more chance to find a better matching from now.
                    if (mappingResult.hasLowestScore()) {
                        if (!result.isPresent()) {
                            result = PathMapped.of(mapping, mappingResult, value);
                        }
                        break;
                    }

                    // We have still a chance to find a better matching.
                    if (result.isPresent()) {
                        if (mappingResult.score() > result.mappingResult().score()) {
                            // Replace the candidate with the new one only if the score is better.
                            // If the score is same, we respect the order of service registration.
                            result = PathMapped.of(mapping, mappingResult, value);
                        }
                    } else {
                        // Keep the result as a candidate.
                        result = PathMapped.of(mapping, mappingResult, value);
                    }
                }
            }
        }
        return result;
    }

    private static final class TrieRouter implements Router {

        private final RoutingTrie trie;
        private final Function pathMappingResolver;

        TrieRouter(RoutingTrie trie, Function pathMappingResolver) {
            this.trie = requireNonNull(trie, "trie");
            this.pathMappingResolver = requireNonNull(pathMappingResolver, "pathMappingResolver");
        }

        @Override
        public PathMapped find(PathMappingContext mappingCtx) {
            return findsBest(mappingCtx, trie.find(mappingCtx.path()), pathMappingResolver);
        }

        @Override
        public void dump(OutputStream output) {
            trie.dump(output);
        }
    }

    private static final class SequentialRouter implements Router {

        private final List values;
        private final Function pathMappingResolver;

        SequentialRouter(List values, Function pathMappingResolver) {
            this.values = ImmutableList.copyOf(requireNonNull(values, "values"));
            this.pathMappingResolver = requireNonNull(pathMappingResolver, "pathMappingResolver");
        }

        @Override
        public PathMapped find(PathMappingContext mappingCtx) {
            return findsBest(mappingCtx, values, pathMappingResolver);
        }

        @Override
        public void dump(OutputStream output) {
            // Do not close this writer in order to keep output stream open.
            final PrintWriter p = new PrintWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
            p.printf("Dump of %s:%n", this);
            for (int i = 0; i < values.size(); i++) {
                p.printf("<%d> %s%n", i, values.get(i));
            }
            p.flush();
        }
    }

    private Routers() {}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy