
com.linecorp.armeria.server.Routers Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of armeria-shaded Show documentation
Show all versions of armeria-shaded Show documentation
Asynchronous HTTP/2 RPC/REST client/server library built on top of Java 8, Netty, Thrift and GRPC (armeria-shaded)
/*
* 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