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

org.glassfish.jersey.microprofile.restclient.RestClientBuilderImpl Maven / Gradle / Ivy

/*
 * Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2019, 2021 Payara Foundation and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.microprofile.restclient;

import java.io.Closeable;
import java.lang.reflect.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessController;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import jakarta.ws.rs.ext.ParamConverterProvider;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.RestClientDefinitionException;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor;
import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptorFactory;
import org.eclipse.microprofile.rest.client.ext.QueryParamStyle;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.eclipse.microprofile.rest.client.spi.RestClientListener;
import org.glassfish.jersey.JerseyPriorities;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.Initializable;
import org.glassfish.jersey.client.spi.ConnectorProvider;
import org.glassfish.jersey.ext.cdi1x.internal.CdiUtil;
import org.glassfish.jersey.innate.VirtualThreadUtil;
import org.glassfish.jersey.internal.ServiceFinder;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.InjectionManagerSupplier;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.uri.JerseyQueryParamStyle;

/**
 * Rest client builder implementation. Creates proxy instance of requested interface.
 *
 * @author David Kral
 * @author Patrik Dudits
 * @author Tomas Langer
 */
class RestClientBuilderImpl implements RestClientBuilder {

    private static final String CONFIG_DISABLE_DEFAULT_MAPPER = "microprofile.rest.client.disable.default.mapper";
    private static final String CONFIG_PROVIDERS = "/mp-rest/providers";
    private static final String CONFIG_PROVIDER_PRIORITY = "/priority";
    private static final String PROVIDER_SEPARATOR = ",";

    private final Set> responseExceptionMappers;
    private final Set paramConverterProviders;
    private final Set inboundHeaderProviders;
    private final List asyncInterceptorFactories;
    private final Config config;
    private final ConfigWrapper configWrapper;
    private URI uri;
    private ClientBuilder clientBuilder;
    private Supplier executorService;
    private HostnameVerifier sslHostnameVerifier;
    private SSLContext sslContext;
    private KeyStore sslTrustStore;
    private KeyStore sslKeyStore;
    private char[] sslKeyStorePassword;
    private ConnectorProvider connector;
    private boolean followRedirects;

    RestClientBuilderImpl() {
        clientBuilder = ClientBuilder.newBuilder();
        responseExceptionMappers = new HashSet<>();
        paramConverterProviders = new HashSet<>();
        inboundHeaderProviders = new HashSet<>();
        asyncInterceptorFactories = new ArrayList<>();
        config = ConfigProvider.getConfig();
        configWrapper = new ConfigWrapper(clientBuilder.getConfiguration());
        executorService = () -> VirtualThreadUtil.withConfig(configWrapper).newCachedThreadPool();
    }

    @Override
    public RestClientBuilder baseUrl(URL url) {
        try {
            this.uri = url.toURI();
            return this;
        } catch (URISyntaxException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public RestClientBuilder connectTimeout(long timeout, TimeUnit unit) {
        clientBuilder.connectTimeout(timeout, unit);
        return this;
    }

    @Override
    public RestClientBuilder readTimeout(long timeout, TimeUnit unit) {
        clientBuilder.readTimeout(timeout, unit);
        return this;
    }

    @Override
    public RestClientBuilder executorService(ExecutorService executor) {
        if (executor == null) {
            throw new IllegalArgumentException("ExecutorService cannot be null.");
        }
        executorService = () -> executor;
        return this;
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T build(Class interfaceClass) throws IllegalStateException, RestClientDefinitionException {
        for (RestClientListener restClientListener : ServiceFinder.find(RestClientListener.class)) {
            restClientListener.onNewClient(interfaceClass, this);
        }

        if (uri == null) {
            throw new IllegalStateException("Base uri/url cannot be null!");
        }

        //Provider registration part
        processProviders(interfaceClass);
        InjectionManagerExposer injectionManagerExposer = new InjectionManagerExposer();
        register(injectionManagerExposer);
        register(SseMessageBodyReader.class);

        //We need to check first if default exception mapper was not disabled by property on builder.
        registerExceptionMapper();
        //sort all AsyncInvocationInterceptorFactory by priority
        asyncInterceptorFactories.sort(Comparator.comparingInt(AsyncInvocationInterceptorFactoryPriorityWrapper::getPriority));
        if (connector != null) {
            ClientConfig config = new ClientConfig();
            config.loadFrom(getConfiguration());
            config.connectorProvider(connector);
            clientBuilder = clientBuilder.withConfig(config); // apply config...
        }

        // override ClientConfig with values that have been set explicitly
        clientBuilder.executorService(new ExecutorServiceWrapper(executorService.get()));

        if (null != sslContext) {
            clientBuilder.sslContext(sslContext);
        }

        if (null != sslHostnameVerifier) {
            clientBuilder.hostnameVerifier(sslHostnameVerifier);
        }

        if (null != sslTrustStore) {
            clientBuilder.trustStore(sslTrustStore);
        }

        if (null != sslKeyStore) {
            clientBuilder.keyStore(sslKeyStore, sslKeyStorePassword);
        }

        Client client = clientBuilder.build();

        if (client instanceof Initializable) {
            ((Initializable) client).preInitialize();
        }
        WebTarget webTarget = client.target(this.uri);
        webTarget.property(ClientProperties.FOLLOW_REDIRECTS, followRedirects);

        RestClientContext context = RestClientContext.builder(interfaceClass)
                .responseExceptionMappers(responseExceptionMappers)
                .paramConverterProviders(paramConverterProviders)
                .inboundHeadersProviders(inboundHeaderProviders)
                .asyncInterceptorFactories(new ArrayList<>(asyncInterceptorFactories))
                .injectionManager(injectionManagerExposer.injectionManager)
                .beanManager(CdiUtil.getBeanManager())
                .build();

        RestClientModel restClientModel = RestClientModel.from(context);

        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                                          new Class[] {interfaceClass, AutoCloseable.class, Closeable.class},
                                          new ProxyInvocationHandler(client, webTarget, restClientModel)
        );
    }

    @Override
    public RestClientBuilder sslContext(SSLContext sslContext) {
        this.sslContext = sslContext;
        return this;
    }

    @Override
    public RestClientBuilder trustStore(KeyStore keyStore) {
        this.sslTrustStore = keyStore;
        return this;
    }

    @Override
    public RestClientBuilder keyStore(KeyStore keyStore, String password) {
        this.sslKeyStore = keyStore;
        this.sslKeyStorePassword = ((null == password) ? new char[0] : password.toCharArray());
        return this;
    }

    @Override
    public RestClientBuilder hostnameVerifier(HostnameVerifier hostnameVerifier) {
        this.sslHostnameVerifier = hostnameVerifier;
        return this;
    }

    private void registerExceptionMapper() {
        Object disableDefaultMapperJersey = clientBuilder.getConfiguration().getProperty(CONFIG_DISABLE_DEFAULT_MAPPER);
        if (disableDefaultMapperJersey != null && disableDefaultMapperJersey.equals(Boolean.FALSE)) {
            register(new DefaultResponseExceptionMapper());
        } else if (disableDefaultMapperJersey == null) {
            //If property was not set on Jersey ClientBuilder, we need to check config.
            Optional disableDefaultMapperConfig = config.getOptionalValue(CONFIG_DISABLE_DEFAULT_MAPPER, boolean.class);
            if (!disableDefaultMapperConfig.isPresent() || !disableDefaultMapperConfig.get()) {
                register(new DefaultResponseExceptionMapper());
            }
        }
    }

    private  void processProviders(Class interfaceClass) {
        Object providersFromJerseyConfig = clientBuilder.getConfiguration()
                .getProperty(interfaceClass.getName() + CONFIG_PROVIDERS);
        if (providersFromJerseyConfig instanceof String && !((String) providersFromJerseyConfig).isEmpty()) {
            String[] providerArray = ((String) providersFromJerseyConfig).split(PROVIDER_SEPARATOR);
            processConfigProviders(interfaceClass, providerArray);
        }
        Optional providersFromConfig = config.getOptionalValue(interfaceClass.getName() + CONFIG_PROVIDERS, String.class);
        providersFromConfig.ifPresent(providers -> {
            if (!providers.isEmpty()) {
                String[] providerArray = providersFromConfig.get().split(PROVIDER_SEPARATOR);
                processConfigProviders(interfaceClass, providerArray);
            }
        });
        RegisterProvider[] registerProviders = interfaceClass.getAnnotationsByType(RegisterProvider.class);
        for (RegisterProvider registerProvider : registerProviders) {
            register(registerProvider.value(), registerProvider.priority() < 0 ? Priorities.USER : registerProvider.priority());
        }
    }

    private void processConfigProviders(Class restClientInterface, String[] providerArray) {
        for (String provider : providerArray) {
            Class providerClass = AccessController.doPrivileged(ReflectionHelper.classForNamePA(provider));
            if (providerClass == null) {
                throw new IllegalStateException("No provider class with following name found: " + provider);
            }
            int priority = getProviderPriority(restClientInterface, providerClass);
            register(providerClass, priority);
        }
    }

    private int getProviderPriority(Class restClientInterface, Class providerClass) {
        String property = restClientInterface.getName() + CONFIG_PROVIDERS + "/"
                + providerClass.getName() + CONFIG_PROVIDER_PRIORITY;
        Object providerPriorityJersey = clientBuilder.getConfiguration().getProperty(property);
        if (providerPriorityJersey == null) {
            //If property was not set on Jersey ClientBuilder, we need to check MP config.
            Optional providerPriorityMP = config.getOptionalValue(property, int.class);
            if (providerPriorityMP.isPresent()) {
                return providerPriorityMP.get();
            }
        } else if (providerPriorityJersey instanceof Integer) {
            return (int) providerPriorityJersey;
        }
        return JerseyPriorities.getPriorityValue(providerClass, -1);
    }

    @Override
    public Configuration getConfiguration() {
        return configWrapper;
    }

    @Override
    public RestClientBuilder property(String name, Object value) {
        clientBuilder.property(name, value);
        return this;
    }

    @Override
    public RestClientBuilder register(Class componentClass) {
        if (isSupportedCustomProvider(componentClass)) {
            register(ReflectionUtil.createInstance(componentClass));
        } else {
            clientBuilder.register(componentClass);
        }
        return this;
    }

    @Override
    public RestClientBuilder register(Class componentClass, int priority) {
        if (isSupportedCustomProvider(componentClass)) {
            register(ReflectionUtil.createInstance(componentClass), priority);
        } else {
            clientBuilder.register(componentClass, priority);
        }
        return this;
    }

    @Override
    public RestClientBuilder register(Class componentClass, Class... contracts) {
        if (isSupportedCustomProvider(componentClass)) {
            register(ReflectionUtil.createInstance(componentClass), contracts);
        } else {
            clientBuilder.register(componentClass, contracts);
        }
        return this;
    }

    @Override
    public RestClientBuilder register(Class componentClass, Map, Integer> contracts) {
        if (isSupportedCustomProvider(componentClass)) {
            register(ReflectionUtil.createInstance(componentClass), contracts);
        } else {
            clientBuilder.register(componentClass, contracts);
        }
        return this;
    }

    @Override
    public RestClientBuilder register(Object component) {
        if (component instanceof ResponseExceptionMapper) {
            ResponseExceptionMapper mapper = (ResponseExceptionMapper) component;
            registerCustomProvider(component, mapper.getPriority());
            clientBuilder.register(mapper, mapper.getPriority());
        } else {
            clientBuilder.register(component);
            registerCustomProvider(component, null);
        }
        return this;
    }

    @Override
    public RestClientBuilder register(Object component, int priority) {
        clientBuilder.register(component, priority);
        registerCustomProvider(component, priority);
        return this;
    }

    @Override
    public RestClientBuilder register(Object component, Class... contracts) {
        for (Class contract : contracts) {
            if (isSupportedCustomProvider(contract)) {
                register(component);
            }
        }
        clientBuilder.register(component, contracts);
        return this;
    }

    @Override
    public RestClientBuilder register(Object component, Map, Integer> contracts) {
        if (isSupportedCustomProvider(component.getClass())) {
            if (component instanceof ResponseExceptionMapper) {
                registerCustomProvider(component, contracts.get(ResponseExceptionMapper.class));
            } else if (component instanceof ParamConverterProvider) {
                registerCustomProvider(component, contracts.get(ParamConverterProvider.class));
            }
        }
        clientBuilder.register(component, contracts);
        return this;
    }

    private boolean isSupportedCustomProvider(Class providerClass) {
        return ResponseExceptionMapper.class.isAssignableFrom(providerClass)
                || ParamConverterProvider.class.isAssignableFrom(providerClass)
                || AsyncInvocationInterceptorFactory.class.isAssignableFrom(providerClass)
                || ConnectorProvider.class.isAssignableFrom(providerClass)
                || InboundHeadersProvider.class.isAssignableFrom(providerClass);
    }

    private void registerCustomProvider(Object instance, Integer priority) {
        if (!isSupportedCustomProvider(instance.getClass())) {
            return;
        }
        if (instance instanceof ResponseExceptionMapper) {
            responseExceptionMappers.add((ResponseExceptionMapper) instance);
            //needs to be registered separately due to it is not possible to register custom provider in jersey
            Map, Integer> contracts = new HashMap<>();
            contracts.put(ResponseExceptionMapper.class, priority);
            configWrapper.addCustomProvider(instance.getClass(), contracts);
        }
        if (instance instanceof ParamConverterProvider) {
            paramConverterProviders.add((ParamConverterProvider) instance);
        }
        if (instance instanceof AsyncInvocationInterceptorFactory) {
            asyncInterceptorFactories
                    .add(new AsyncInvocationInterceptorFactoryPriorityWrapper((AsyncInvocationInterceptorFactory) instance,
                                                                              priority));
        }
        if (instance instanceof ConnectorProvider) {
            connector = (ConnectorProvider) instance;
        }
        if (instance instanceof InboundHeadersProvider) {
            inboundHeaderProviders.add((InboundHeadersProvider) instance);
        }
    }

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

    @Override
    public RestClientBuilder proxyAddress(String proxyHost, int proxyPort) {
        if (proxyHost == null) {
            throw new IllegalArgumentException("Proxy host must not be null");
        }
        if (proxyPort <= 0 || proxyPort > 65535) {
            throw new IllegalArgumentException("Invalid proxy port");
        }

        // If proxyString is something like "localhost:8765" we need to add a scheme since the connectors expect one
        String proxyString = createProxyString(proxyHost, proxyPort);

        property(ClientProperties.PROXY_URI, proxyString);
        return this;
    }

    static String createProxyString(String proxyHost, int proxyPort) {
        boolean prependScheme = false;
        String proxyString = proxyHost + ":" + proxyPort;

        if (proxyString.split(":").length == 2) {
            // Check if first character is a number to account for if proxyHost is given as an IP rather than a name
            // URI.create("127.0.0.1:8765") will lead to an IllegalArgumentException
            if (proxyString.matches("\\d.*")) {
                prependScheme = true;
            } else {
                // "localhost:8765" will set the scheme as "localhost" and the host as "null"
                URI proxyURI = URI.create(proxyString);
                if (proxyURI.getHost() == null && proxyURI.getScheme().equals(proxyHost)) {
                    prependScheme = true;
                }
            }
        }

        if (prependScheme) {
            proxyString = "http://" + proxyString;
            Logger.getLogger(RestClientBuilderImpl.class.getName()).log(Level.FINE,
                    "No scheme provided with proxyHost: " + proxyHost + ". Defaulting to HTTP, proxy address = "
                            + proxyString);
        }

        return proxyString;
    }

    @Override
    public RestClientBuilder queryParamStyle(QueryParamStyle queryParamStyle) {
         if (queryParamStyle != null) {
            property(ClientProperties.QUERY_PARAM_STYLE,
                    JerseyQueryParamStyle.valueOf(queryParamStyle.toString()));
        }
        return this;
    }

    private static class InjectionManagerExposer implements Feature {
        InjectionManager injectionManager;

        @Override
        public boolean configure(FeatureContext context) {
            if (context instanceof InjectionManagerSupplier) {
                this.injectionManager = ((InjectionManagerSupplier) context).getInjectionManager();
                return true;
            } else {
                throw new IllegalArgumentException("The client needs Jersey runtime to work properly");
            }
        }
    }

    private static class AsyncInvocationInterceptorFactoryPriorityWrapper
            implements AsyncInvocationInterceptorFactory {

        private AsyncInvocationInterceptorFactory factory;
        private Integer priority;

        AsyncInvocationInterceptorFactoryPriorityWrapper(AsyncInvocationInterceptorFactory factory, Integer priority) {
            this.factory = factory;
            this.priority = priority;
        }

        @Override
        public AsyncInvocationInterceptor newInterceptor() {
            return factory.newInterceptor();
        }

        Integer getPriority() {
            if (priority == null) {
                priority = JerseyPriorities.getPriorityValue(factory.getClass(), Priorities.USER);
            }
            return priority;
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy