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

io.helidon.webclient.api.LoomClient Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2022, 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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
import io.helidon.common.configurable.LruCache;
import io.helidon.common.tls.Tls;
import io.helidon.http.Method;
import io.helidon.webclient.spi.ClientProtocolProvider;
import io.helidon.webclient.spi.HttpClientSpi;
import io.helidon.webclient.spi.HttpClientSpiProvider;
import io.helidon.webclient.spi.Protocol;
import io.helidon.webclient.spi.ProtocolConfig;

/**
 * Base class for HTTP implementations of {@link WebClient}.
 */
@SuppressWarnings("rawtypes")
class LoomClient implements WebClient {
    static final LazyValue EXECUTOR =
            LazyValue.create(() -> Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
                                                                            .name("helidon-client-", 0)
                                                                            .factory()));
    private static final List PROVIDERS =
            HelidonServiceLoader.create(ServiceLoader.load(HttpClientSpiProvider.class))
                    .asList();
    private static final Map HTTP_PROVIDERS_BY_PROTOCOL;

    static {
        Map providerMap = new HashMap<>();
        PROVIDERS.forEach(it -> providerMap.put(it.protocolId(), it));
        HTTP_PROVIDERS_BY_PROTOCOL = Map.copyOf(providerMap);
    }

    private final WebClientConfig config;
    // a map of protocol ids to the client SPI implementing them
    private final Map clientSpiByProtocol;
    private final Map clientsByProtocol = new ConcurrentHashMap<>();
    private final List protocols;
    private final List tcpProtocols;
    private final ProtocolConfigs protocolConfigs;
    private final List tcpProtocolIds;
    private final WebClientCookieManager cookieManager;
    private final LruCache clientSpiLruCache = LruCache.create();

    /**
     * Construct this instance from a subclass of builder.
     *
     * @param config builder the subclass is built from
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    protected LoomClient(WebClientConfig config) {
        this.config = config;
        this.protocolConfigs = ProtocolConfigs.create(config.protocolConfigs());
        this.cookieManager = config.cookieManager().orElseGet(() -> WebClientCookieManager.builder().build());

        List providers;
        List protocolPreference = config.protocolPreference();
        if (protocolPreference.isEmpty()) {
            // use the discovered ones
            providers = new ArrayList<>(PROVIDERS);
        } else {
            providers = new ArrayList<>();
            for (String protocol : protocolPreference) {
                HttpClientSpiProvider spi = HTTP_PROVIDERS_BY_PROTOCOL.get(protocol);
                if (spi == null) {
                    throw new IllegalStateException("Requested protocol \"" + protocol + "\" is not available on classpath");
                }
                providers.add(spi);
            }
        }
        if (providers.isEmpty()) {
            throw new IllegalStateException("WebClient requires at least one protocol provider to be present on classpath,"
                                                    + " or configured through protocolPreference (such as http1)");
        }

        Map clients = new HashMap<>();
        List protocols = new ArrayList<>();
        List tcpProtocols = new ArrayList<>();
        for (HttpClientSpiProvider provider : providers) {
            Object protocolConfig = protocolConfigs.config(provider.protocolId(),
                                                           provider.configType(),
                                                           () -> (ProtocolConfig) provider.defaultConfig());

            HttpClientSpi clientSpi = (HttpClientSpi) provider.protocol(this, protocolConfig);
            String protocolId = provider.protocolId();
            ProtocolSpi spi = new ProtocolSpi(protocolId, clientSpi);
            clients.putIfAbsent(protocolId, spi);
            protocols.add(spi);
            if (clientSpi.isTcp()) {
                tcpProtocols.add(spi);
            }
        }

        this.clientSpiByProtocol = clients;
        this.protocols = protocols;
        this.tcpProtocols = tcpProtocols;
        this.tcpProtocolIds = tcpProtocols.stream()
                .map(ProtocolSpi::id)
                .toList();
    }

    @Override
    public WebClientCookieManager cookieManager() {
        return cookieManager;
    }

    @Override
    public HttpClientRequest method(Method method) {
        ClientUri clientUri = prototype().baseUri()
                .map(ClientUri::create) // create from base config
                .orElseGet(ClientUri::create); // create as empty

        prototype().baseQuery().ifPresent(clientUri.writeableQuery()::from);
        prototype().baseFragment().ifPresent(clientUri::fragment);

        return new HttpClientRequest(this,
                                     this.prototype(),
                                     method,
                                     clientUri,
                                     clientSpiByProtocol,
                                     protocols,
                                     tcpProtocols,
                                     tcpProtocolIds,
                                     clientSpiLruCache);
    }

    @Override
    public void closeResource() {
        for (ProtocolSpi o : List.copyOf(clientSpiByProtocol.values())) {
            o.spi().releaseResource();
        }
    }

    @Override
    public  T client(Protocol protocol, C protocolConfig) {
        return protocol.provider().protocol(this, protocolConfig);
    }

    @SuppressWarnings("unchecked")
    @Override
    public  T client(Protocol protocol) {
        ClientProtocolProvider provider = protocol.provider();
        return (T) clientsByProtocol.computeIfAbsent(provider.protocolId(),
                                                     protocolId -> {
                                                         C config = protocolConfigs.config(provider.protocolId(),
                                                                                           provider.configType(),
                                                                                           provider::defaultConfig);
                                                         return protocol.provider().protocol(this, config);
                                                     });
    }

    @Override
    public WebClientConfig prototype() {
        return config;
    }

    @Override
    public ExecutorService executor() {
        return EXECUTOR.get();
    }

    record ProtocolSpi(String id, HttpClientSpi spi) {
    }

    record EndpointKey(String scheme, // http/https
                       String authority, // myserver:80
                       Tls tlsConfig, // TLS configuration (may be disabled, never null)
                       Proxy proxy) { // proxy, never null
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy