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

io.helidon.webclient.WebClientRequestBuilderImpl Maven / Gradle / Ivy

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2020, 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;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.helidon.common.GenericType;
import io.helidon.common.context.Context;
import io.helidon.common.context.Contexts;
import io.helidon.common.context.spi.DataPropagationProvider;
import io.helidon.common.http.DataChunk;
import io.helidon.common.http.Headers;
import io.helidon.common.http.Http;
import io.helidon.common.http.HttpRequest;
import io.helidon.common.http.MediaType;
import io.helidon.common.http.Parameters;
import io.helidon.common.reactive.Single;
import io.helidon.common.serviceloader.HelidonServiceLoader;
import io.helidon.media.common.MessageBodyReadableContent;
import io.helidon.media.common.MessageBodyReaderContext;
import io.helidon.media.common.MessageBodyWriterContext;
import io.helidon.webclient.spi.WebClientService;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.resolver.NoopAddressResolverGroup;
import io.netty.resolver.dns.DnsServerAddressStreamProviders;
import io.netty.resolver.dns.RoundRobinDnsAddressResolverGroup;
import io.netty.util.AsciiString;
import io.netty.util.AttributeKey;

/**
 * Implementation of {@link WebClientRequestBuilder}.
 */
class WebClientRequestBuilderImpl implements WebClientRequestBuilder {

    private static final Logger LOGGER = Logger.getLogger(WebClientRequestBuilderImpl.class.getName());

    private static final List PROPAGATION_PROVIDERS = HelidonServiceLoader
            .builder(ServiceLoader.load(DataPropagationProvider.class)).build().asList();

    static final Map> CHANNEL_CACHE = new ConcurrentHashMap<>();
    static final AttributeKey REQUEST = AttributeKey.valueOf("request");
    static final AttributeKey> RECEIVED = AttributeKey.valueOf("received");
    static final AttributeKey> COMPLETED = AttributeKey.valueOf("completed");
    static final AttributeKey> RESULT = AttributeKey.valueOf("result");
    static final AttributeKey IN_USE = AttributeKey.valueOf("inUse");
    static final AttributeKey RETURN = AttributeKey.valueOf("finished");
    static final AttributeKey RESPONSE_RECEIVED = AttributeKey.valueOf("responseReceived");
    static final AttributeKey RESPONSE = AttributeKey.valueOf("response");
    static final AttributeKey CONNECTION_IDENT = AttributeKey.valueOf("connectionIdent");
    static final AttributeKey REQUEST_ID = AttributeKey.valueOf("requestID");

    /**
     * Whether the channel will be closed and keep-alive caching should not be applied.
     */
    static final AttributeKey WILL_CLOSE = AttributeKey.valueOf("willClose");

    private static final AtomicLong REQUEST_NUMBER = new AtomicLong(0);
    private static final String DEFAULT_TRANSPORT_PROTOCOL = "http";
    private static final Map DEFAULT_SUPPORTED_PROTOCOLS = new HashMap<>();

    static {
        DEFAULT_SUPPORTED_PROTOCOLS.put(DEFAULT_TRANSPORT_PROTOCOL, 80);
        DEFAULT_SUPPORTED_PROTOCOLS.put("https", 443);
    }

    private final Map properties;
    private final NioEventLoopGroup eventGroup;
    private final WebClientConfiguration configuration;
    private final Http.RequestMethod method;
    private final WebClientRequestHeaders headers;
    private final WebClientQueryParams queryParams;
    private final MessageBodyReaderContext readerContext;
    private final MessageBodyWriterContext writerContext;

    private URI uri;
    private URI finalUri;
    private Http.Version httpVersion;
    private Context context;
    private Proxy proxy;
    private String fragment;
    private boolean followRedirects;
    private boolean skipUriEncoding;
    private int redirectionCount;
    private RequestConfiguration requestConfiguration;
    private HttpRequest.Path path;
    private List services;
    private Duration readTimeout;
    private Duration connectTimeout;
    private boolean keepAlive;
    private Long requestId;
    private boolean allowChunkedEncoding;
    private DnsResolverType dnsResolverType;

    private WebClientRequestBuilderImpl(NioEventLoopGroup eventGroup,
                                        WebClientConfiguration configuration,
                                        Http.RequestMethod method) {
        this.properties = new HashMap<>();
        this.eventGroup = eventGroup;
        this.configuration = configuration;
        this.method = method;
        this.uri = configuration.uri();
        this.skipUriEncoding = false;
        this.allowChunkedEncoding = true;
        this.path = ClientPath.create(null, "", new HashMap<>());
        //Default headers added to the current headers of the request
        this.headers = new WebClientRequestHeadersImpl(this.configuration.headers());
        this.queryParams = new WebClientQueryParams();
        this.httpVersion = Http.Version.V1_1;
        this.redirectionCount = 0;
        this.services = configuration.clientServices();
        this.readerContext = MessageBodyReaderContext.create(configuration.readerContext());
        this.writerContext = MessageBodyWriterContext.create(configuration.writerContext(), headers);
        this.requestId = null;
        Context.Builder contextBuilder = Context.builder().id("webclient-" + requestId);
        configuration.context().ifPresentOrElse(contextBuilder::parent,
                                                () -> Contexts.context().ifPresent(contextBuilder::parent));
        this.context = contextBuilder.build();
        this.followRedirects = configuration.followRedirects();
        this.readTimeout = configuration.readTimout();
        this.connectTimeout = configuration.connectTimeout();
        this.proxy = configuration.proxy().orElse(Proxy.noProxy());
        this.keepAlive = configuration.keepAlive();
        this.dnsResolverType = configuration.dnsResolverType();
    }

    static WebClientRequestBuilder create(NioEventLoopGroup eventGroup,
                                          WebClientConfiguration configuration,
                                          Http.RequestMethod method) {
        return new WebClientRequestBuilderImpl(eventGroup, configuration, method);
    }

    /**
     * Creates new instance of {@link WebClientRequestBuilder} based on previous request.
     *
     * @param clientRequest previous request
     * @return client request builder
     */
    static WebClientRequestBuilder create(WebClientRequestImpl clientRequest) {
        WebClientRequestBuilderImpl builder = new WebClientRequestBuilderImpl(NettyClient.eventGroup(),
                                                                              clientRequest.configuration(),
                                                                              Http.Method.GET);
        builder.httpVersion = clientRequest.version();
        builder.proxy = clientRequest.proxy();
        builder.redirectionCount = clientRequest.redirectionCount() + 1;
        int maxRedirects = builder.configuration.maxRedirects();
        if (builder.redirectionCount > maxRedirects) {
            throw new WebClientException("Max number of redirects extended! (" + maxRedirects + ")");
        }
        return builder;
    }

    private static ChannelFuture obtainChannelFuture(RequestConfiguration configuration,
                                                     Bootstrap bootstrap) {
        ConnectionIdent connectionIdent = new ConnectionIdent(configuration);
        Set channels = CHANNEL_CACHE.computeIfAbsent(connectionIdent,
                                                                    s -> Collections.synchronizedSet(new HashSet<>()));
        synchronized (channels) {
            for (ChannelRecord channelRecord : channels) {
                Channel channel = channelRecord.channel;
                if (channel.isOpen() && channel.attr(IN_USE).get().compareAndSet(false, true)) {
                    if (LOGGER.isLoggable(Level.FINEST)) {
                        LOGGER.finest(() -> "Reusing -> " + channel.hashCode() + ", settting in use -> true");
                    }
                    return channelRecord.channelFuture;
                }
                if (LOGGER.isLoggable(Level.FINEST)) {
                    LOGGER.finest(() -> "Not accepted -> " + channel.hashCode() + ", open -> "
                            + channel.isOpen() + ", in use -> " + channel.attr(IN_USE).get());
                }
            }
            if (LOGGER.isLoggable(Level.FINEST)) {
                LOGGER.finest(() -> "New connection to -> " + connectionIdent);
            }
            URI uri = connectionIdent.base;
            ChannelFuture connect = bootstrap.connect(uri.getHost(), uri.getPort());
            Channel channel = connect.channel();
            channel.attr(IN_USE).set(new AtomicBoolean(true));
            channel.attr(RETURN).set(new AtomicBoolean(false));
            channel.attr(CONNECTION_IDENT).set(connectionIdent);
            channels.add(new ChannelRecord(connect));
            return connect;
        }
    }

    static void removeChannelFromCache(ConnectionIdent key, Channel channel) {
        if (LOGGER.isLoggable(Level.FINEST)) {
            LOGGER.finest(() -> "Removing from channel cache. Connection ident ->  " + key
                    + ", channel -> " + channel.hashCode());
        }
        Set channelSet = CHANNEL_CACHE.get(key);
        if (channelSet != null) {
            // remove entry from set
            channelSet.remove(new ChannelRecord(channel));

            // remove set from map if empty
            if (channelSet.isEmpty()) {
                CHANNEL_CACHE.remove(key);
            }
        }
    }

    @Override
    public WebClientRequestBuilder uri(String uri) {
        return uri(URI.create(uri));
    }

    @Override
    public WebClientRequestBuilder uri(URL url) {
        try {
            return uri(url.toURI());
        } catch (URISyntaxException e) {
            throw new WebClientException("Failed to create URI from URL", e);
        }
    }

    @Override
    public WebClientRequestBuilder uri(URI uri) {
        this.uri = uri;
        return this;
    }

    @Override
    public WebClientRequestBuilder skipUriEncoding() {
        this.skipUriEncoding = true;
        this.queryParams.skipEncoding();
        return this;
    }

    @Override
    public WebClientRequestBuilder followRedirects(boolean followRedirects) {
        this.followRedirects = followRedirects;
        return this;
    }

    @Override
    public WebClientRequestBuilder property(String propertyName, String propertyValue) {
        properties.put(propertyName, propertyValue);
        return this;
    }

    @Override
    public WebClientRequestBuilder context(Context context) {
        this.context = context;
        return this;
    }

    @Override
    public WebClientRequestHeaders headers() {
        return headers;
    }

    @Override
    public WebClientRequestBuilder queryParam(String name, String... values) {
        queryParams.add(name, values);
        return this;
    }

    @Override
    public WebClientRequestBuilder proxy(Proxy proxy) {
        this.proxy = proxy;
        return this;
    }

    @Override
    public WebClientRequestBuilder headers(Headers headers) {
        this.headers.clear();
        this.headers.putAll(headers);
        return this;
    }

    @Override
    public WebClientRequestBuilder headers(Function headers) {
        Headers newHeaders = headers.apply(this.headers);
        if (!newHeaders.equals(this.headers)) {
            headers(newHeaders);
        }
        return this;
    }

    @Override
    public WebClientRequestBuilder queryParams(Parameters queryParams) {
        Objects.requireNonNull(queryParams);
        queryParams.toMap().forEach((name, params) -> queryParam(name, params.toArray(new String[0])));
        return this;
    }

    @Override
    public WebClientRequestBuilder httpVersion(Http.Version httpVersion) {
        this.httpVersion = httpVersion;
        return this;
    }

    @Override
    public WebClientRequestBuilder connectTimeout(long amount, TimeUnit unit) {
        this.connectTimeout = Duration.of(amount, unit.toChronoUnit());
        return this;
    }

    @Override
    public WebClientRequestBuilder readTimeout(long amount, TimeUnit unit) {
        this.readTimeout = Duration.of(amount, unit.toChronoUnit());
        return this;
    }

    @Override
    public WebClientRequestBuilder fragment(String fragment) {
        this.fragment = fragment;
        return this;
    }

    @Override
    public WebClientRequestBuilder path(HttpRequest.Path path) {
        this.path = path;
        return this;
    }

    @Override
    public WebClientRequestBuilder path(String path) {
        this.path = ClientPath.create(null, path, new HashMap<>());
        return this;
    }

    @Override
    public WebClientRequestBuilder contentType(MediaType contentType) {
        this.headers.contentType(contentType);
        this.writerContext.contentType(contentType);
        return this;
    }

    @Override
    public WebClientRequestBuilder accept(MediaType... mediaTypes) {
        Arrays.stream(mediaTypes).forEach(headers::addAccept);
        return this;
    }

    @Override
    public WebClientRequestBuilder keepAlive(boolean keepAlive) {
        this.keepAlive = keepAlive;
        return this;
    }

    @Override
    public WebClientRequestBuilder requestId(long requestId) {
        this.requestId = requestId;
        return this;
    }

    @Override
    public WebClientRequestBuilder allowChunkedEncoding(boolean allowChunkedEncoding) {
        this.allowChunkedEncoding = allowChunkedEncoding;
        return this;
    }

    @Override
    public  Single request(Class responseType) {
        return request(GenericType.create(responseType));
    }

    @Override
    public  Single request(GenericType responseType) {
        return Contexts.runInContext(context, () -> invokeWithEntity(Single.empty(), responseType));
    }

    @Override
    public Single request() {
        return Contexts.runInContext(context, () -> invoke(Single.empty()));
    }

    @Override
    public Single submit() {
        return request();
    }

    @Override
    public  Single submit(Flow.Publisher requestEntity, Class responseType) {
        return Contexts.runInContext(context, () -> invokeWithEntity(requestEntity, GenericType.create(responseType)));
    }

    @Override
    public  Single submit(Object requestEntity, Class responseType) {
        GenericType responseGenericType = GenericType.create(responseType);
        Flow.Publisher dataChunkPublisher = writerContext.marshall(
                Single.just(requestEntity), GenericType.create(requestEntity));
        return Contexts.runInContext(context, () -> invokeWithEntity(dataChunkPublisher, responseGenericType));
    }

    @Override
    public Single submit(Flow.Publisher requestEntity) {
        return Contexts.runInContext(context, () -> invoke(requestEntity));
    }

    @Override
    public Single submit(Object requestEntity) {
        Flow.Publisher dataChunkPublisher = writerContext.marshall(
                Single.just(requestEntity), GenericType.create(requestEntity));
        return submit(dataChunkPublisher);
    }

    @Override
    public Single submit(Function> function) {
        return submit(function.apply(writerContext));
    }

    @Override
    public MessageBodyReaderContext readerContext() {
        return readerContext;
    }

    @Override
    public MessageBodyWriterContext writerContext() {
        return writerContext;
    }

    long requestId() {
        return requestId;
    }

    Http.RequestMethod method() {
        return method;
    }

    Http.Version httpVersion() {
        return httpVersion;
    }

    URI uri() {
        return finalUri;
    }

    Parameters queryParams() {
        return queryParams;
    }

    String query() {
        return finalUri.getRawQuery() == null ? "" : finalUri.getRawQuery();
    }

    String queryFromParams() {
        StringBuilder queries = new StringBuilder();
        for (Map.Entry> entry : queryParams.pickCorrectParameters().toMap().entrySet()) {
            for (String value : entry.getValue()) {
                if (queries.length() > 0) {
                    queries.append("&");
                }
                if (entry.getKey().isEmpty()) {
                    queries.append(value);
                } else {
                    queries.append(entry.getKey()).append("=").append(value);
                }
            }
        }
        return queries.toString();
    }

    String fragment() {
        return fragment;
    }

    HttpRequest.Path path() {
        return path;
    }

    RequestConfiguration requestConfiguration() {
        return requestConfiguration;
    }

    Map properties() {
        return properties;
    }

    Proxy proxy() {
        return proxy;
    }

    int redirectionCount() {
        return redirectionCount;
    }

    Context context() {
        return context;
    }

    private  Single invokeWithEntity(Flow.Publisher requestEntity, GenericType responseType) {
        return invoke(requestEntity)
                .map(this::getContentFromClientResponse)
                .flatMapSingle(content -> content.as(responseType));
    }

    private Single invoke(Flow.Publisher requestEntity) {
        finalUri = prepareFinalURI();
        if (requestId == null) {
            requestId = REQUEST_NUMBER.incrementAndGet();
        }
        //        LOGGER.finest(() -> "(client reqID: " + requestId + ") Request final URI: " + uri);
        CompletableFuture sent = new CompletableFuture<>();
        CompletableFuture responseReceived = new CompletableFuture<>();
        CompletableFuture complete = new CompletableFuture<>();
        WebClientServiceRequest completedRequest = new WebClientServiceRequestImpl(this, sent, responseReceived, complete);
        CompletionStage rcs = CompletableFuture.completedFuture(completedRequest);

        for (WebClientService service : services) {
            rcs = rcs.thenCompose(service::request)
                    .thenApply(servReq -> {
                        finalUri = recreateURI(servReq);
                        return servReq;
                    });
        }

        Single single =  Single.create(rcs.thenCompose(serviceRequest -> {
            URI requestUri = relativizeNoProxy(finalUri, proxy, configuration.relativeUris());
            requestId = serviceRequest.requestId();
            HttpHeaders headers = toNettyHttpHeaders();
            DefaultHttpRequest request = new DefaultHttpRequest(toNettyHttpVersion(httpVersion),
                                                                toNettyMethod(method),
                                                                requestUri.toASCIIString(),
                                                                headers);
            boolean keepAlive = HttpUtil.isKeepAlive(request);

            requestConfiguration = RequestConfiguration.builder(finalUri)
                    .update(configuration)
                    .followRedirects(followRedirects)
                    .clientServiceRequest(serviceRequest)
                    .readerContext(readerContext)
                    .writerContext(writerContext)
                    .connectTimeout(connectTimeout)
                    .readTimeout(readTimeout)
                    .services(services)
                    .context(context)
                    .proxy(proxy)
                    .keepAlive(keepAlive)
                    .requestId(requestId)
                    .build();
            WebClientRequestImpl clientRequest = new WebClientRequestImpl(this);

            CompletableFuture result = new CompletableFuture<>();

            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new NettyClientInitializer(requestConfiguration))
                    .option(ChannelOption.SO_KEEPALIVE, keepAlive)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis());

            switch (dnsResolverType) {
                case ROUND_ROBIN:
                    bootstrap.resolver(new RoundRobinDnsAddressResolverGroup(NioDatagramChannel.class,
                                                                             DnsServerAddressStreamProviders.platformDefault()));
                    break;
                case NONE:
                    bootstrap.resolver(NoopAddressResolverGroup.INSTANCE);
                    break;
                default:
                    // Do nothing and default bootstrap resolver will be used
            }

            ChannelFuture channelFuture = keepAlive
                    ? obtainChannelFuture(requestConfiguration, bootstrap)
                    : bootstrap.connect(finalUri.getHost(), finalUri.getPort());

            channelFuture.addListener((ChannelFutureListener) future -> {
                if (LOGGER.isLoggable(Level.FINEST)) {
                    LOGGER.finest(() -> "(client reqID: " + requestId + ") "
                            + "Channel hashcode -> " + channelFuture.channel().hashCode());
                }
                channelFuture.channel().attr(REQUEST).set(clientRequest);
                channelFuture.channel().attr(RESPONSE_RECEIVED).set(false);
                channelFuture.channel().attr(RECEIVED).set(responseReceived);
                channelFuture.channel().attr(COMPLETED).set(complete);
                channelFuture.channel().attr(WILL_CLOSE).set(!keepAlive);
                channelFuture.channel().attr(RESULT).set(result);
                channelFuture.channel().attr(REQUEST_ID).set(requestId);
                Throwable cause = future.cause();
                if (null == cause) {
                    RequestContentSubscriber requestContentSubscriber = new RequestContentSubscriber(request,
                                                                                                     channelFuture.channel(),
                                                                                                     result,
                                                                                                     sent,
                                                                                                     allowChunkedEncoding);
                    requestEntity.subscribe(requestContentSubscriber);
                } else {
                    sent.completeExceptionally(cause);
                    responseReceived.completeExceptionally(cause);
                    complete.completeExceptionally(cause);
                    result.completeExceptionally(new WebClientException(finalUri.toString(), cause));
                }
            });
            return result;
        }));
        return wrapWithContext(single);
    }

    @SuppressWarnings(value = "unchecked")
    private void runInContext(Map, Object> data, Runnable command) {
        try {
            PROPAGATION_PROVIDERS.forEach(provider -> provider.propagateData(data.get(provider.getClass())));
            Contexts.runInContext(context, command);
        } finally {
            PROPAGATION_PROVIDERS.forEach(provider -> provider.clearData(data.get(provider.getClass())));
        }
    }

    /**
     * Wraps a single into another that runs all subscriber methods using the current
     * context. This will enable calls to {@code Contexts.context()} in reactive handlers
     * to return a non-empty optional.
     *
     * @param single single to be wrapped
     * @param  type parameter
     * @return wrapped single
     */
    private  Single wrapWithContext(Single single) {
        Map, Object> contextProperties = new HashMap<>();
        PROPAGATION_PROVIDERS.forEach(provider -> contextProperties.put(provider.getClass(), provider.data()));
        return Single.create(subscriber -> single.subscribe(new Flow.Subscriber<>() {
            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                runInContext(contextProperties, () -> subscriber.onSubscribe(subscription));
            }

            @Override
            public void onNext(T item) {
                runInContext(contextProperties, () -> subscriber.onNext(item));
            }

            @Override
            @SuppressWarnings("unchecked")
            public void onError(Throwable throwable) {
                runInContext(contextProperties, () -> subscriber.onError(throwable));
                PROPAGATION_PROVIDERS.forEach(provider -> provider.clearData(provider.getClass()));
            }

            @Override
            @SuppressWarnings("unchecked")
            public void onComplete() {
                runInContext(contextProperties, subscriber::onComplete);
                PROPAGATION_PROVIDERS.forEach(provider -> provider.clearData(provider.getClass()));
            }
        }));
    }

    private MessageBodyReadableContent getContentFromClientResponse(WebClientResponse response) {
        //If the response status is greater then 300, ask user to change requested entity to ClientResponse
        if (response.status().code() >= Http.Status.MOVED_PERMANENTLY_301.code()) {
            throw new WebClientException("Request failed with code " + response.status().code());
        }
        return response.content();
    }

    private URI recreateURI(WebClientServiceRequest request) {
        clearUri(request.schema(), request.host(), request.port());
        return prepareFinalURI();
    }

    private URI prepareFinalURI() {
        if (uri == null) {
            throw new WebClientException("There is no specified uri for the request.");
        } else if (uri.getHost() == null) {
            throw new WebClientException("Invalid uri " + uri + ". Uri.getHost() returned null.");
        }
        String scheme = Optional.ofNullable(uri.getScheme())
                .orElseThrow(() -> new WebClientException("Transport protocol has be to be specified in uri: "
                                                                  + uri.toString()));
        if (!DEFAULT_SUPPORTED_PROTOCOLS.containsKey(scheme)) {
            throw new WebClientException(scheme + " transport protocol is not supported!");
        }
        int port = uri.getPort() > -1 ? uri.getPort() : DEFAULT_SUPPORTED_PROTOCOLS.getOrDefault(scheme, -1);
        if (port == -1) {
            throw new WebClientException("Client could not get port for schema " + scheme + ". "
                                                 + "Please specify correct port to use.");
        }
        String path = resolvePath();
        this.path = ClientPath.create(null, path, new HashMap<>());
        //We need null values for query and fragment if we dont want to have trailing ?# chars
        String query = resolveQuery();
        String fragment = resolveFragment();
        StringBuilder sb = new StringBuilder();
        sb.append(scheme).append("://").append(uri.getHost()).append(":").append(port);
        constructRelativeURI(sb, path, query, fragment);
        clearUri(scheme, uri.getHost(), port);
        return URI.create(sb.toString());
    }

    private void clearUri(String scheme, String host, int port) {
        try {
            this.uri = new URI(scheme,
                               null,
                               host,
                               port,
                               null,
                               null,
                               null);
        } catch (URISyntaxException e) {
            throw new WebClientException("Could not create URI!", e);
        }
    }

    private String resolveFragment() {
        if (fragment == null) {
            fragment(uri.getRawFragment());
        }
        if (skipUriEncoding || fragment == null) {
            return fragment;
        }
        return UriComponentEncoder.encode(fragment, UriComponentEncoder.Type.FRAGMENT);
    }


    /**
     * Relativize final URI if no proxy or if host in no-proxy list or if forced via
     * the {@code relative-uris} config property.
     *
     * @param finalUri the final URI
     * @param proxy the proxy
     * @param relativeUris flag to force all URIs to be relative
     * @return possibly converted URI
     */
    static URI relativizeNoProxy(URI finalUri, Proxy proxy, boolean relativeUris) {
        if (proxy == Proxy.noProxy() || proxy.noProxyPredicate().apply(finalUri) || relativeUris) {
            String path = finalUri.getRawPath();
            String fragment = finalUri.getRawFragment();
            String query = finalUri.getRawQuery();
            StringBuilder sb = new StringBuilder();
            constructRelativeURI(sb, path, query, fragment);
            return URI.create(sb.toString());
        }
        return finalUri;
    }

    private static void constructRelativeURI(StringBuilder stringBuilder, String path, String query, String fragment) {
        if (path != null) {
            stringBuilder.append(path);
        }
        if (query != null) {
            stringBuilder.append('?').append(query);
        }
        if (fragment != null) {
            stringBuilder.append('#').append(fragment);
        }
    }

    private String resolveQuery() {
        String queries = queryFromParams();
        String uriQuery = uri.getRawQuery();
        if (queries.isEmpty()) {
            queries = uriQuery;
        } else if (uriQuery != null) {
            queries = uriQuery + "&" + queries;
        }
        if (uriQuery != null) {
            String[] uriQueries = uriQuery.split("&");
            Arrays.stream(uriQueries)
                    .map(s -> s.split("="))
                    .forEach(keyValue -> {
                        if (keyValue.length == 1) {
                            queryParam("", keyValue[0]);
                        } else {
                            queryParam(keyValue[0], keyValue[1]);
                        }
                    });
        }
        return queries;
    }

    private String resolvePath() {
        String uriPath = uri.getRawPath();
        String extendedPath = this.path.toRawString();
        String finalPath;
        if (uriPath.endsWith("/") && extendedPath.startsWith("/")) {
            finalPath = uriPath.substring(0, uriPath.length() - 1) + extendedPath;
        } else if (extendedPath.isEmpty()) {
            finalPath = uriPath;
        } else {
            finalPath = uriPath.endsWith("/") || extendedPath.startsWith("/")
                    ? uriPath + extendedPath
                    : uriPath + "/" + extendedPath;
        }
        if (skipUriEncoding) {
            return finalPath;
        }
        return UriComponentEncoder.encode(finalPath, UriComponentEncoder.Type.PATH);
    }

    private HttpMethod toNettyMethod(Http.RequestMethod method) {
        //This method creates also new netty HttpMethod.
        return HttpMethod.valueOf(method.name());
    }

    private HttpVersion toNettyHttpVersion(Http.Version version) {
        return HttpVersion.valueOf(version.value());
    }

    private HttpHeaders toNettyHttpHeaders() {
        HttpHeaders headers = new DefaultHttpHeaders(this.configuration.validateHeaders());
        try {
            Map> cookieHeaders = this.configuration.cookieManager().get(finalUri, new HashMap<>());
            List cookies = new ArrayList<>(cookieHeaders.get(Http.Header.COOKIE));
            cookies.addAll(this.headers.values(Http.Header.COOKIE));
            if (!cookies.isEmpty()) {
                headers.add(Http.Header.COOKIE, String.join("; ", cookies));
            }
        } catch (IOException e) {
            throw new WebClientException("An error occurred while setting cookies.", e);
        }
        this.headers.toMap().forEach(headers::add);
        addHeaderIfAbsent(headers, HttpHeaderNames.HOST, finalUri.getHost() + ":" + finalUri.getPort());
        addHeaderIfAbsent(headers, HttpHeaderNames.CONNECTION, keepAlive ? HttpHeaderValues.KEEP_ALIVE : HttpHeaderValues.CLOSE);
        addHeaderIfAbsent(headers, HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
        addHeaderIfAbsent(headers, HttpHeaderNames.USER_AGENT, configuration.userAgent());
        return headers;
    }

    private void addHeaderIfAbsent(HttpHeaders headers, AsciiString header, Object headerValue) {
        if (!headers.contains(header)) {
            headers.set(header, headerValue);
        }
    }

    /**
     * {@link HttpRequest.Path} client implementation.
     * Temporal implementation until {@link HttpRequest.Path} has implementation in common.
     */
    private static class ClientPath implements HttpRequest.Path {

        private final String path;
        private final String rawPath;
        private final Map params;
        private final ClientPath absolutePath;
        private List segments;

        /**
         * Creates new instance.
         *
         * @param path         actual relative URI path.
         * @param rawPath      actual relative URI path without any decoding.
         * @param params       resolved path parameters.
         * @param absolutePath absolute path.
         */
        ClientPath(String path, String rawPath, Map params,
                   ClientPath absolutePath) {

            this.path = path;
            this.rawPath = rawPath;
            this.params = params == null ? Collections.emptyMap() : params;
            this.absolutePath = absolutePath;
        }

        @Override
        public String param(String name) {
            return params.get(name);
        }

        @Override
        public List segments() {
            List result = segments;
            // No synchronisation needed, worth case is multiple splitting.
            if (result == null) {
                StringTokenizer stok = new StringTokenizer(path, "/");
                result = new ArrayList<>();
                while (stok.hasMoreTokens()) {
                    result.add(stok.nextToken());
                }
                this.segments = result;
            }
            return result;
        }

        @Override
        public String toString() {
            return path;
        }

        @Override
        public String toRawString() {
            return rawPath;
        }

        @Override
        public HttpRequest.Path absolute() {
            return absolutePath == null ? this : absolutePath;
        }

        static HttpRequest.Path create(ClientPath contextual, String path,
                                       Map params) {

            return create(contextual, path, path, params);
        }

        static HttpRequest.Path create(ClientPath contextual, String path, String rawPath,
                                       Map params) {

            if (contextual == null) {
                return new ClientPath(path, rawPath, params, null);
            } else {
                return contextual.createSubpath(path, rawPath, params);
            }
        }

        HttpRequest.Path createSubpath(String path, String rawPath,
                                       Map params) {

            if (params == null) {
                params = Collections.emptyMap();
            }
            if (absolutePath == null) {
                HashMap map =
                        new HashMap<>(this.params.size() + params.size());
                map.putAll(this.params);
                map.putAll(params);
                return new ClientPath(path, rawPath, params, new ClientPath(this.path, this.rawPath, map, null));
            } else {
                int size = this.params.size() + params.size()
                        + absolutePath.params.size();
                HashMap map = new HashMap<>(size);
                map.putAll(absolutePath.params);
                map.putAll(this.params);
                map.putAll(params);
                return new ClientPath(path, rawPath, params, new ClientPath(absolutePath.path, absolutePath.rawPath, map,
                        /* absolute path */ null));
            }
        }
    }

    static class ChannelRecord {

        private final ChannelFuture channelFuture;
        private final Channel channel;

        ChannelRecord(ChannelFuture channelFuture) {
            this.channelFuture = channelFuture;
            this.channel = channelFuture.channel();
        }

        ChannelRecord(Channel channel) {
            this.channelFuture = null;
            this.channel = channel;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ChannelRecord that = (ChannelRecord) o;
            //Intentional comparison without equals
            return channel == that.channel;
        }

        @Override
        public int hashCode() {
            return channel.hashCode();
        }
    }

    static class ConnectionIdent {

        private final URI base;
        private final Duration readTimeout;
        private final Proxy proxy;
        private final WebClientTls tls;

        private ConnectionIdent(RequestConfiguration requestConfiguration) {
            URI uri = requestConfiguration.requestURI();
            this.base = URI.create(uri.getScheme() + "://" + uri.getAuthority());
            this.readTimeout = requestConfiguration.readTimout();
            this.proxy = requestConfiguration.proxy().orElse(null);
            this.tls = requestConfiguration.tls();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ConnectionIdent that = (ConnectionIdent) o;
            return Objects.equals(base, that.base)
                    && Objects.equals(readTimeout, that.readTimeout)
                    && Objects.equals(proxy, that.proxy)
                    && Objects.equals(tls, that.tls);
        }

        @Override
        public int hashCode() {
            return Objects.hash(base, readTimeout, proxy, tls);
        }

        @Override
        public String toString() {
            return "ConnectionIdent{"
                    + "base=" + base
                    + ", readTimeout=" + readTimeout
                    + ", proxy=" + proxy
                    + ", tls=" + tls
                    + '}';
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy