com.palantir.conjure.java.undertow.runtime.ConjureHandler Maven / Gradle / Ivy
/*
* (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.conjure.java.undertow.runtime;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.palantir.conjure.java.undertow.lib.Endpoint;
import com.palantir.conjure.java.undertow.lib.UndertowRuntime;
import com.palantir.conjure.java.undertow.lib.UndertowService;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.exceptions.SafeIllegalStateException;
import com.palantir.tracing.undertow.TracedRequestHandler;
import com.palantir.tracing.undertow.TracedStateHandler;
import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.RoutingHandler;
import io.undertow.server.handlers.BlockingHandler;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Conjure routing mechanism which can be registered as an Undertow {@link HttpHandler}. This handler takes care of
* exception handling, tracing, and web-security headers.
*/
public final class ConjureHandler implements HttpHandler {
private final RoutingHandler routingHandler;
private ConjureHandler(HttpHandler fallback, List endpoints) {
this.routingHandler = Handlers.routing(false)
.setFallbackHandler(fallback)
// The method may be valid for another handlers, the
// fallback handler will be used instead of 405 status.
.setInvalidMethodHandler(null);
endpoints.forEach(this::register);
registerSyntheticEndpoints(routingHandler, endpoints);
}
private static List applyHeadEndpoints(RoutingHandler routingHandler, List endpoints) {
List result = new ArrayList<>(endpoints.size());
for (Endpoint endpoint : endpoints) {
result.add(endpoint);
if (Methods.GET.equals(endpoint.method())) {
Endpoint headEndpoint =
Endpoint.builder().from(endpoint).method(Methods.HEAD).build();
result.add(headEndpoint);
routingHandler.add(headEndpoint.method(), headEndpoint.template(), headEndpoint.handler());
}
}
return result;
}
private static void registerSyntheticEndpoints(RoutingHandler routingHandler, List endpoints) {
List updatedEndpoints = applyHeadEndpoints(routingHandler, endpoints);
registerOptionsEndpoints(routingHandler, updatedEndpoints);
}
private static void registerOptionsEndpoints(RoutingHandler routingHandler, List endpoints) {
endpoints.stream()
.collect(ImmutableSetMultimap.toImmutableSetMultimap(
endpoint -> normalizeTemplate(endpoint.template()), Endpoint::method))
.asMap()
.forEach((normalizedPath, methods) -> {
if (!methods.contains(Methods.OPTIONS)) {
routingHandler.add(
Methods.OPTIONS,
normalizedPath,
new WebSecurityHandler(new OptionsHandler(ImmutableSet.copyOf(methods))));
}
});
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
routingHandler.handleRequest(exchange);
}
private void register(Endpoint endpoint) {
routingHandler.add(endpoint.method(), endpoint.template(), endpoint.handler());
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private static final ImmutableSet ALLOWED_METHODS =
ImmutableSet.of(Methods.GET, Methods.PUT, Methods.POST, Methods.DELETE, Methods.OPTIONS, Methods.PATCH);
private final List wrappersJustBeforeBlocking = new ArrayList<>();
private final List endpoints = new ArrayList<>();
private final List services = new ArrayList<>();
private HttpHandler fallback = ResponseCodeHandler.HANDLE_404;
private UndertowRuntime runtime = ConjureUndertowRuntime.builder().build();
private Builder() {}
/**
* Use {@link #services(UndertowService)} instead to take advantage of the configured {@link UndertowRuntime}
* automatically.
*
* @deprecated prefer {@link #services}
*/
@Deprecated
@CanIgnoreReturnValue
public Builder endpoints(Endpoint value) {
endpoints.add(checkEndpoint(value));
return this;
}
/**
* Use {@link #services(UndertowService)} instead to take advantage of the configured {@link UndertowRuntime}
* automatically.
*
* @deprecated prefer {@link #services}
*/
@Deprecated
@CanIgnoreReturnValue
public Builder addAllEndpoints(Iterable values) {
Preconditions.checkNotNull(values, "Values is required");
for (Endpoint endpoint : values) {
endpoints(endpoint);
}
return this;
}
@CanIgnoreReturnValue
public Builder services(UndertowService value) {
services.add(Preconditions.checkNotNull(value, "Value is required"));
return this;
}
@CanIgnoreReturnValue
public Builder addAllServices(Iterable extends UndertowService> values) {
Preconditions.checkNotNull(values, "Values is required");
for (UndertowService service : values) {
services(service);
}
return this;
}
/**
* The fallback {@link HttpHandler handler} is invoked when no {@link Endpoint} matches a request. By default a
* 404 response status will be served.
*/
@CanIgnoreReturnValue
public Builder fallback(HttpHandler value) {
fallback = Preconditions.checkNotNull(value, "Value is required");
return this;
}
/**
* This MUST only be used for non-blocking operations that are meant to be run on the io-thread. For blocking
* operations, please wrap the UndertowService themselves If you call this multiple time, the last wrapper will
* be applied last, meaning it will be wrapped by the previously added {@link EndpointHandlerWrapper}s.
*/
@CanIgnoreReturnValue
public Builder addWrapperBeforeBlocking(EndpointHandlerWrapper wrapper) {
wrappersJustBeforeBlocking.add(wrapper);
return this;
}
/** Configures the runtime instance to use with this handler. */
@CanIgnoreReturnValue
public Builder runtime(UndertowRuntime value) {
this.runtime = Preconditions.checkNotNull(value, "UndertowRuntime");
return this;
}
public HttpHandler build() {
ImmutableList serviceEndpoints = services.stream()
.flatMap(service -> service.endpoints(runtime).stream())
.map(Builder::checkEndpoint)
.collect(ImmutableList.toImmutableList());
ImmutableList allEndpoints = ImmutableList.builder()
.addAll(endpoints)
.addAll(serviceEndpoints)
.build();
checkOverlappingPaths(allEndpoints);
ImmutableList wrappers = ImmutableList.builder()
.add(
// Begin the server span as early as possible to capture the most of the request.
endpoint -> Optional.of(new TracedRequestHandler(
endpoint.handler(),
"Undertow: " + endpoint.method() + " " + endpoint.template(),
CompletedRequestTagTranslator.INSTANCE.andThen(
new EndpointTagTranslator(endpoint)))),
// Allow the server to configure UndertowOptions.DECODE_URL = false to allow slashes in
// parameters. Servers which do not configure DECODE_URL will still work properly except
// for encoded slash values. When DECODE_URL has not been disabled, the following handler
// will no-op
GlobRetainingDecodingHandler.WRAPPER,
// no-cache and web-security handlers add listeners for the response to be committed,
// they can be executed on the IO thread.
endpoint -> Methods.GET.equals(endpoint.method())
// Only applies to GET methods
? Optional.of(new NoCachingResponseHandler(endpoint.handler()))
: Optional.empty(),
endpoint -> Optional.of(new WebSecurityHandler(endpoint.handler())),
endpoint -> endpoint.deprecated()
.map(_reason -> new DeprecationReportingResponseHandler(endpoint.handler())))
// Apply custom non-blocking handlers just before the BlockingHandler
.addAll(wrappersJustBeforeBlocking)
// It is vitally important to never run blocking operations on the initial IO thread otherwise
// the server will not process new requests. all handlers executed after BlockingHandler
// use the larger task pool which is allowed to block. Any operation which sets thread
// state (e.g. SLF4J MDC or Tracer) must execute on the blocking thread otherwise state
// will not propagate to the wrapped service.
.add(
endpoint -> Optional.of(new BlockingHandler(endpoint.handler())),
// Logging context and trace handler must execute prior to the exception
// to provide user and trace information on exceptions.
endpoint -> Optional.of(new LoggingContextHandler(endpoint.handler())),
endpoint -> Optional.of(new TracedStateHandler(endpoint.handler())),
endpoint -> Optional.of(
new ConjureExceptionHandler(endpoint.handler(), runtime.exceptionHandler())))
.build()
.reverse();
return new ConjureHandler(
fallback,
allEndpoints.stream()
.map(endpoint -> wrap(endpoint, wrappers))
.collect(ImmutableList.toImmutableList()));
}
private static Endpoint wrap(Endpoint input, List wrappers) {
Endpoint current = input;
for (EndpointHandlerWrapper wrapper : wrappers) {
current = Endpoints.map(current, wrapper);
}
return current;
}
private static Endpoint checkEndpoint(Endpoint value) {
Preconditions.checkNotNull(value, "Value is required");
if (!ALLOWED_METHODS.contains(value.method())) {
throw new SafeIllegalStateException(
"Endpoint method is not recognized",
SafeArg.of("method", value.method()),
SafeArg.of("template", value.template()),
SafeArg.of("service", value.serviceName()),
SafeArg.of("name", value.name()));
}
return value;
}
private static void checkOverlappingPaths(List endpoints) {
Set duplicates = endpoints.stream()
.collect(Collectors.groupingBy(endpoint ->
String.format("%s: %s", endpoint.method(), normalizeTemplate(endpoint.template()))))
.entrySet()
.stream()
.filter(groups -> groups.getValue().size() > 1)
.map(entry -> {
String descriptions = entry.getValue().stream()
.map(endpoint -> String.format("%s.%s", endpoint.serviceName(), endpoint.name()))
.collect(Collectors.joining(", "));
return String.format("%s: %s", entry.getKey(), descriptions);
})
.collect(Collectors.toSet());
if (!duplicates.isEmpty()) {
throw new SafeIllegalArgumentException(
"The same route is declared by multiple UndertowServices",
SafeArg.of("duplicates", duplicates));
}
}
}
private static String normalizeTemplate(String template) {
return template.replaceAll("\\{.*?\\}", "{param}");
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy