| /* |
| * Copyright (c) 2019, 2021 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.Executors; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Supplier; |
| |
| import javax.annotation.Priority; |
| import javax.net.ssl.HostnameVerifier; |
| import javax.net.ssl.SSLContext; |
| import javax.ws.rs.Priorities; |
| import javax.ws.rs.client.Client; |
| import javax.ws.rs.client.ClientBuilder; |
| import javax.ws.rs.client.WebTarget; |
| import javax.ws.rs.core.Configuration; |
| import javax.ws.rs.core.Feature; |
| import javax.ws.rs.core.FeatureContext; |
| import javax.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.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.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<ResponseExceptionMapper<?>> responseExceptionMappers; |
| private final Set<ParamConverterProvider> paramConverterProviders; |
| private final Set<InboundHeadersProvider> inboundHeaderProviders; |
| private final List<AsyncInvocationInterceptorFactoryPriorityWrapper> asyncInterceptorFactories; |
| private final Config config; |
| private final ConfigWrapper configWrapper; |
| private URI uri; |
| private ClientBuilder clientBuilder; |
| private Supplier<ExecutorService> 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 = Executors::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> T build(Class<T> 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<Boolean> disableDefaultMapperConfig = config.getOptionalValue(CONFIG_DISABLE_DEFAULT_MAPPER, boolean.class); |
| if (!disableDefaultMapperConfig.isPresent() || !disableDefaultMapperConfig.get()) { |
| register(new DefaultResponseExceptionMapper()); |
| } |
| } |
| } |
| |
| private <T> void processProviders(Class<T> 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<String> 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<Integer> providerPriorityMP = config.getOptionalValue(property, int.class); |
| if (providerPriorityMP.isPresent()) { |
| return providerPriorityMP.get(); |
| } |
| } else if (providerPriorityJersey instanceof Integer) { |
| return (int) providerPriorityJersey; |
| } |
| Priority priority = providerClass.getAnnotation(Priority.class); |
| return priority == null ? -1 : priority.value(); |
| } |
| |
| @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<Class<?>, 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<Class<?>, 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<Class<?>, 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"); |
| } |
| property(ClientProperties.PROXY_URI, proxyHost + ":" + proxyPort); |
| return this; |
| } |
| |
| @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 = Optional.ofNullable(factory.getClass().getAnnotation(Priority.class)) |
| .map(Priority::value) |
| .orElse(Priorities.USER); |
| } |
| return priority; |
| } |
| } |
| |
| } |