io.helidon.webclient.api.ClientRequestBase Maven / Gradle / Ivy
/*
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
*
* 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 io.helidon.webclient.api;
import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import io.helidon.common.Version;
import io.helidon.common.buffers.BufferData;
import io.helidon.common.context.Context;
import io.helidon.common.context.Contexts;
import io.helidon.common.tls.Tls;
import io.helidon.common.uri.UriEncoding;
import io.helidon.common.uri.UriFragment;
import io.helidon.http.ClientRequestHeaders;
import io.helidon.http.Header;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Headers;
import io.helidon.http.Method;
import io.helidon.http.media.MediaContext;
import io.helidon.webclient.spi.WebClientService;
/**
* Abstract base implementation of an HTTP client. Provides helpful methods to handle cookies, client services etc.
*
* @param type of the request
* @param type of the response
*/
public abstract class ClientRequestBase, R extends HttpClientResponse>
implements FullClientRequest {
/**
* Helidon user agent request header.
*/
public static final Header USER_AGENT_HEADER = HeaderValues.create(HeaderNames.USER_AGENT,
"Helidon " + Version.VERSION);
/**
* Proxy connection header.
*/
public static final Header PROXY_CONNECTION = HeaderValues.create("Proxy-Connection", "keep-alive");
private static final Map COUNTERS = new ConcurrentHashMap<>();
private static final Set SUPPORTED_SCHEMES = Set.of("https", "http");
private final Map pathParams = new HashMap<>();
private final HttpClientConfig clientConfig;
private final WebClientCookieManager cookieManager;
private final String protocolId;
private final Method method;
private final ClientUri clientUri;
private final Map properties;
private final ClientRequestHeaders headers;
private final String requestId;
private final MediaContext mediaContext;
private String uriTemplate;
private boolean skipUriEncoding;
private boolean followRedirects;
private int maxRedirects;
private Duration readTimeout;
private Duration readContinueTimeout;
private Tls tls;
private Proxy proxy;
private boolean keepAlive;
private ClientConnection connection;
protected ClientRequestBase(HttpClientConfig clientConfig,
WebClientCookieManager cookieManager,
String protocolId,
Method method,
ClientUri clientUri,
Map properties) {
this.clientConfig = clientConfig;
this.cookieManager = cookieManager;
this.protocolId = protocolId;
this.method = method;
this.clientUri = clientUri;
this.properties = new HashMap<>(properties);
this.headers = clientConfig.defaultRequestHeaders();
this.readTimeout = clientConfig.socketOptions().readTimeout();
this.readContinueTimeout = clientConfig.readContinueTimeout();
this.mediaContext = clientConfig.mediaContext();
this.followRedirects = clientConfig.followRedirects();
this.maxRedirects = clientConfig.maxRedirects();
this.tls = clientConfig.tls();
this.proxy = clientConfig.proxy();
this.keepAlive = clientConfig.keepAlive();
this.requestId = nextRequestId(protocolId);
}
@Override
public T tls(Tls tls) {
this.tls = tls;
return identity();
}
@Override
public T uri(URI uri) {
this.uriTemplate = null;
this.clientUri.resolve(uri);
return identity();
}
@Override
public T uri(ClientUri uri) {
this.uriTemplate = null;
this.clientUri.resolve(uri);
return identity();
}
@Override
public T path(String uri) {
this.clientUri.resolvePath(uri);
return identity();
}
@Override
public T uri(String uri) {
if (uri.indexOf('{') > -1) {
this.uriTemplate = uri;
} else {
uri(URI.create(UriEncoding.encodeUri(uri)));
}
return identity();
}
@Override
public ClientUri resolvedUri() {
// we do not want to update our own URI, as this method may be called multiple times
return resolveUri(ClientUri.create(this.clientUri));
}
@Override
public ClientRequestHeaders headers() {
return headers;
}
@Override
public T header(Header header) {
this.headers.set(header);
return identity();
}
@Override
public T headers(Headers headers) {
for (Header header : headers) {
this.headers.set(header);
}
return identity();
}
@Override
public T headers(Consumer headersConsumer) {
headersConsumer.accept(headers);
return identity();
}
@Override
public T fragment(UriFragment fragment) {
this.clientUri.fragment(fragment);
return identity();
}
@Override
public T skipUriEncoding(boolean skip) {
this.skipUriEncoding = skip;
this.clientUri.skipUriEncoding(skip);
return identity();
}
@Override
public T queryParam(String name, String... values) {
clientUri.writeableQuery().set(name, values);
return identity();
}
@Override
public T property(String propertyName, String propertyValue) {
this.properties.put(propertyName, propertyValue);
return identity();
}
@Override
public T pathParam(String name, String value) {
pathParams.put(name, value);
return identity();
}
@Override
public T followRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return identity();
}
@Override
public T maxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return identity();
}
@Override
public T connection(ClientConnection connection) {
this.connection = connection;
return identity();
}
@Override
public T keepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
return identity();
}
@Override
public T readTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
return identity();
}
@Override
public T readContinueTimeout(Duration readContinueTimeout) {
this.readContinueTimeout = readContinueTimeout;
return identity();
}
@Override
public T proxy(Proxy proxy) {
this.proxy = Objects.requireNonNull(proxy);
return identity();
}
@Override
public R request() {
additionalHeaders();
return validateAndSubmit(BufferData.EMPTY_BYTES);
}
@Override
public final R submit(Object entity) {
if (!(entity instanceof byte[] bytes && bytes.length == 0)) {
rejectHeadWithEntity();
}
additionalHeaders();
return validateAndSubmit(entity);
}
@Override
public final R outputStream(OutputStreamHandler outputStreamConsumer) {
rejectHeadWithEntity();
additionalHeaders();
return doOutputStream(outputStreamConsumer);
}
/**
* Append additional headers before sending the request.
*/
protected void additionalHeaders() {
headers.setIfAbsent(USER_AGENT_HEADER);
}
/**
* HTTP method to be invoked.
*
* @return HTTP method
*/
@Override
public Method method() {
return method;
}
/**
* Properties configured by a user or by other components.
*
* @return properties
*/
@Override
public Map properties() {
return properties;
}
@Override
public boolean followRedirects() {
return followRedirects;
}
@Override
public int maxRedirects() {
return maxRedirects;
}
@Override
public Tls tls() {
return tls;
}
@Override
public Proxy proxy() {
return proxy;
}
@Override
public Optional connection() {
return Optional.ofNullable(connection);
}
@Override
public Map pathParams() {
return pathParams;
}
@Override
public ClientUri uri() {
return clientUri;
}
@Override
public String requestId() {
return requestId;
}
@Override
public Duration readTimeout() {
return readTimeout;
}
@Override
public Duration readContinueTimeout() {
return readContinueTimeout;
}
@Override
public boolean keepAlive() {
return keepAlive;
}
@Override
public boolean skipUriEncoding() {
return skipUriEncoding;
}
protected abstract R doSubmit(Object entity);
protected abstract R doOutputStream(OutputStreamHandler outputStreamHandler);
/**
* Invoke configured client services.
*
* @param whenSent completable future to be completed when the request is sent over the network
* @param whenComplete completable future to be completed when the request/response interaction finishes
* @param httpCallChain invocation of the HTTP request (the actual network call)
* @param usedUri URI configured on the request, combined with the base URI of the client
* @return web client service response
*/
protected WebClientServiceResponse invokeServices(WebClientService.Chain httpCallChain,
CompletableFuture whenSent,
CompletableFuture whenComplete,
ClientUri usedUri) {
// include any stored cookies in request
cookieManager.request(usedUri, headers);
WebClientServiceRequest serviceRequest = new ServiceRequestImpl(usedUri,
method,
protocolId,
headers,
Contexts.context().orElseGet(Context::create),
requestId,
whenComplete,
whenSent,
properties);
WebClientService.Chain last = httpCallChain;
List services = clientConfig.services();
ListIterator serviceIterator = services.listIterator(services.size());
while (serviceIterator.hasPrevious()) {
last = new ServiceChainImpl(last, serviceIterator.previous());
}
WebClientServiceResponse response = last.proceed(serviceRequest);
cookieManager.response(usedUri, response.headers());
return response;
}
/**
* Associated client configuration.
*
* @return client config
*/
protected HttpClientConfig clientConfig() {
return clientConfig;
}
/**
* Media context configured for this request.
*
* @return media context
*/
protected MediaContext mediaContext() {
return mediaContext;
}
/**
* Resolve possible templated URI definition against the provided {@link ClientUri},
* extracting possible query information into the provided writable query.
*
* @param toResolve client uri to update from the template
* @return updated client uri
*/
protected ClientUri resolveUri(ClientUri toResolve) {
if (uriTemplate != null) {
String resolved = resolvePathParams(uriTemplate);
if (skipUriEncoding) {
toResolve.resolve(URI.create(resolved));
} else {
toResolve.resolve(URI.create(UriEncoding.encodeUri(resolved)));
}
}
return toResolve;
}
private static String nextRequestId(String protocolId) {
AtomicLong counter = COUNTERS.computeIfAbsent(protocolId, it -> new AtomicLong());
return "client-" + protocolId + "-" + Long.toHexString(counter.getAndIncrement());
}
private R validateAndSubmit(Object entity) {
if (!SUPPORTED_SCHEMES.contains(uri().scheme())) {
throw new IllegalArgumentException(
String.format("Not supported scheme %s, client supported schemes are: %s",
uri().scheme(),
String.join(", ", SUPPORTED_SCHEMES)
)
);
}
return doSubmit(entity);
}
private String resolvePathParams(String path) {
String result = path;
for (Map.Entry entry : pathParams.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
result = result.replace("{" + name + "}", value);
}
if (result.contains("{")) {
throw new IllegalArgumentException("Not all path parameters are defined. Template after resolving parameters: "
+ result);
}
return result;
}
private void rejectHeadWithEntity() {
if (this.method.equals(Method.HEAD)) {
throw new IllegalArgumentException("Payload in method '" + Method.HEAD + "' has no defined semantics");
}
}
@SuppressWarnings("unchecked")
private T identity() {
return (T) this;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy