diff --git a/ext/microprofile/mp-rest-client/pom.xml b/ext/microprofile/mp-rest-client/pom.xml index 50fff35ae6..0ef960dcd3 100644 --- a/ext/microprofile/mp-rest-client/pom.xml +++ b/ext/microprofile/mp-rest-client/pom.xml @@ -32,7 +32,7 @@ org.eclipse.microprofile.rest.client microprofile-rest-client-api - 1.2.1 + 1.3.3 org.eclipse.microprofile.config diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java index edd8253fd6..7d0c3109a8 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java @@ -37,6 +37,7 @@ * Contains information about method parameter or class field which is annotated by {@link BeanParam}. * * @author David Kral + * @author Tomas Langer */ class BeanParamModel extends ParamModel { @@ -68,7 +69,7 @@ class BeanParamModel extends ParamModel { } @Override - public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { + public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { ParamHandler handler = PARAM_HANDLERS.get(annotationClass); if (null == handler) { @@ -79,7 +80,7 @@ public Object handleParameter(Object requestPart, Class annotationClass, Obje } @Override - public boolean handles(Class annotation) { + public boolean handles(Class annotation) { return PARAM_HANDLERS.containsKey(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java index 7daf3679c6..fe6d921351 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java @@ -25,6 +25,7 @@ * Contains information about method parameter or class field which is annotated by {@link CookieParam}. * * @author David Kral + * @author Tomas Langer */ class CookieParamModel extends ParamModel> { @@ -36,14 +37,16 @@ class CookieParamModel extends ParamModel> { } @Override - Map handleParameter(Map requestPart, Class annotationClass, Object instance) { + Map handleParameter(Map requestPart, + Class annotationClass, + Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); requestPart.put(cookieParamName, (String) resolvedValue); return requestPart; } @Override - boolean handles(Class annotation) { + boolean handles(Class annotation) { return CookieParam.class.equals(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java index 46ed76e06f..a8255b9db6 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java @@ -26,6 +26,7 @@ * Contains information about method parameter or class field which is annotated by {@link FormParam}. * * @author David Kral + * @author Tomas Langer */ class FormParamModel extends ParamModel
{ @@ -37,7 +38,7 @@ class FormParamModel extends ParamModel { } @Override - Form handleParameter(Form form, Class annotationClass, Object instance) { + Form handleParameter(Form form, Class annotationClass, Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); if (resolvedValue instanceof Collection) { for (final Object v : ((Collection) resolvedValue)) { @@ -50,7 +51,7 @@ Form handleParameter(Form form, Class annotationClass, Object instance) { } @Override - boolean handles(Class annotation) { + boolean handles(Class annotation) { return FormParam.class.equals(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java index 17d53c46b0..6668b4a9a3 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java @@ -26,6 +26,7 @@ * Contains information about method parameter or class field which is annotated by {@link HeaderParam}. * * @author David Kral + * @author Tomas Langer */ class HeaderParamModel extends ParamModel> { @@ -38,14 +39,14 @@ class HeaderParamModel extends ParamModel> { @Override MultivaluedMap handleParameter(MultivaluedMap requestPart, - Class annotationClass, Object instance) { + Class annotationClass, Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); requestPart.put(headerParamName, Collections.singletonList(resolvedValue)); return requestPart; } @Override - boolean handles(Class annotation) { + boolean handles(Class annotation) { return HeaderParam.class.equals(annotation); } } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java index 8422a2015a..1a045eede4 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java @@ -51,6 +51,7 @@ * * @author David Kral * @author Patrik Dudits + * @author Tomas Langer */ class InterfaceModel { @@ -298,25 +299,25 @@ Builder pathValue(Path path) { /** * Extracts MediaTypes from {@link Produces} annotation. - * If annotation is null, new String array with {@link MediaType#WILDCARD} is set. + * If annotation is null, new String array with {@link MediaType#APPLICATION_JSON} is set. * * @param produces {@link Produces} annotation * @return updated Builder instance */ Builder produces(Produces produces) { - this.produces = produces != null ? produces.value() : new String[] {MediaType.WILDCARD}; + this.produces = produces != null ? produces.value() : new String[] {MediaType.APPLICATION_JSON}; return this; } /** * Extracts MediaTypes from {@link Consumes} annotation. - * If annotation is null, new String array with {@link MediaType#WILDCARD} is set. + * If annotation is null, new String array with {@link MediaType#APPLICATION_JSON} is set. * * @param consumes {@link Consumes} annotation * @return updated Builder instance */ Builder consumes(Consumes consumes) { - this.consumes = consumes != null ? consumes.value() : new String[] {MediaType.WILDCARD}; + this.consumes = consumes != null ? consumes.value() : new String[] {MediaType.APPLICATION_JSON}; return this; } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java index 1788d31deb..feed77339c 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java @@ -26,6 +26,7 @@ * Contains information to method parameter which is annotated by {@link MatrixParam}. * * @author David Kral + * @author Tomas Langer */ class MatrixParamModel extends ParamModel { @@ -43,7 +44,7 @@ class MatrixParamModel extends ParamModel { } @Override - public WebTarget handleParameter(WebTarget requestPart, Class annotationClass, Object instance) { + public WebTarget handleParameter(WebTarget requestPart, Class annotationClass, Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); if (resolvedValue instanceof Collection) { return requestPart.matrixParam(matrixParamName, ((Collection) resolvedValue).toArray()); @@ -53,7 +54,7 @@ public WebTarget handleParameter(WebTarget requestPart, Class annotationClass } @Override - public boolean handles(Class annotation) { + public boolean handles(Class annotation) { return MatrixParam.class.equals(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java index 3189e8f060..bfb64ab86f 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java @@ -76,6 +76,7 @@ * * @author David Kral * @author Patrik Dudits + * @author Tomas Langer */ class MethodModel { @@ -445,7 +446,8 @@ private static String parseHttpMethod(InterfaceModel classModel, Method method) throw new RestClientDefinitionException("Method can't have more then one annotation of @HttpMethod type. " + "See " + classModel.getRestClientClass().getName() + "::" + method.getName()); - } else if (httpAnnotations.isEmpty()) { + } + if (httpAnnotations.isEmpty()) { //Sub resource method return ""; } @@ -453,7 +455,7 @@ private static String parseHttpMethod(InterfaceModel classModel, Method method) } private static List parameterModels(InterfaceModel classModel, Method method) { - ArrayList parameterModels = new ArrayList<>(); + List parameterModels = new ArrayList<>(); final List jerseyParameters = org.glassfish.jersey.model.Parameter .create(classModel.getRestClientClass(), classModel.getRestClientClass(), method, false); diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java index 02afd7d6a0..4ba7e40fdb 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java @@ -37,6 +37,7 @@ * Abstract model for all elements with parameter annotation. * * @author David Kral + * @author Tomas Langer */ abstract class ParamModel { @@ -129,7 +130,7 @@ boolean isEntity() { * @param instance actual method parameter value * @return updated request part */ - abstract T handleParameter(T requestPart, Class annotationClass, Object instance); + abstract T handleParameter(T requestPart, Class annotationClass, Object instance); /** * Evaluates if the annotation passed in parameter is supported by this parameter. @@ -137,7 +138,7 @@ boolean isEntity() { * @param annotation checked annotation * @return if annotation is supported */ - abstract boolean handles(Class annotation); + abstract boolean handles(Class annotation); protected static class Builder { @@ -174,14 +175,14 @@ ParamModel build() { } entity = true; - return new ParamModel(this) { + return new ParamModel(this) { @Override - public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { + public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { return requestPart; } @Override - public boolean handles(Class annotation) { + public boolean handles(Class annotation) { return false; } }; diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java index 6c8cd5fd56..36700a95bf 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java @@ -25,6 +25,7 @@ * Contains information about method parameter or class field which is annotated by {@link PathParam}. * * @author David Kral + * @author Tomas Langer */ class PathParamModel extends ParamModel { @@ -40,13 +41,13 @@ public String getPathParamName() { } @Override - public WebTarget handleParameter(WebTarget requestPart, Class annotationClass, Object instance) { + public WebTarget handleParameter(WebTarget requestPart, Class annotationClass, Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); return requestPart.resolveTemplate(pathParamName, resolvedValue); } @Override - public boolean handles(Class annotation) { + public boolean handles(Class annotation) { return PathParam.class.equals(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java index 2a2675e7a6..9f317c785e 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java @@ -18,30 +18,55 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.ws.rs.client.Client; import javax.ws.rs.client.WebTarget; /** * Invocation handler for interface proxy. * * @author David Kral + * @author Tomas Langer */ class ProxyInvocationHandler implements InvocationHandler { - + private final Client client; private final WebTarget target; private final RestClientModel restClientModel; + private final AtomicBoolean closed = new AtomicBoolean(false); - ProxyInvocationHandler(WebTarget target, + // top level + ProxyInvocationHandler(Client client, + WebTarget target, RestClientModel restClientModel) { + + this.client = client; this.target = target; this.restClientModel = restClientModel; } + // used for sub-resources + ProxyInvocationHandler(WebTarget target, + RestClientModel restClientModel) { + this(null, target, restClientModel); + } + @Override public Object invoke(Object proxy, Method method, Object[] args) { - if (method.getName().contains("toString") && (args == null || args.length == 0)) { + if (method.getName().equals("toString") && (args == null || args.length == 0)) { return restClientModel.toString(); } + if (method.getName().equals("close") && (args == null || args.length == 0)) { + closed.set(true); + if (null != client) { + client.close(); + } + return null; + } + + if (closed.get()) { + throw new IllegalStateException("Attempting to invoke a method on a closed client."); + } return restClientModel.invokeMethod(target, method, args); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java index 1f6fcfe1f7..ff9c4d5f86 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java @@ -25,6 +25,7 @@ * Model which contains information about query parameter * * @author David Kral + * @author Tomas Langer */ class QueryParamModel extends ParamModel> { @@ -37,7 +38,7 @@ class QueryParamModel extends ParamModel> { @Override public Map handleParameter(Map requestPart, - Class annotationClass, + Class annotationClass, Object instance) { Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); if (resolvedValue instanceof Object[]) { @@ -49,7 +50,7 @@ public Map handleParameter(Map requestPart, } @Override - public boolean handles(Class annotation) { + public boolean handles(Class annotation) { return QueryParam.class.equals(annotation); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java index 27344cbd1c..b741e14916 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java @@ -22,17 +22,19 @@ import java.security.AccessController; import java.security.PrivilegedAction; -import org.glassfish.jersey.internal.util.ReflectionHelper; - /** - * Created by David Kral. + * @author David Kral + * @author Tomas Langer */ -class ReflectionUtil { +final class ReflectionUtil { + + private ReflectionUtil() { + } static T createInstance(Class tClass) { return AccessController.doPrivileged((PrivilegedAction) () -> { try { - return tClass.newInstance(); + return tClass.getConstructor().newInstance(); } catch (Throwable t) { throw new RuntimeException("No default constructor in class " + tClass + " present. Class cannot be created!", t); } diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java index 00116a7b18..ded8ec5e0c 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java @@ -17,11 +17,13 @@ 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.HashMap; import java.util.HashSet; @@ -37,6 +39,8 @@ import java.util.stream.Collectors; 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; @@ -66,6 +70,7 @@ * * @author David Kral * @author Patrik Dudits + * @author Tomas Langer */ class RestClientBuilderImpl implements RestClientBuilder { @@ -82,6 +87,11 @@ class RestClientBuilderImpl implements RestClientBuilder { private URI uri; private ClientBuilder clientBuilder; private Supplier executorService; + private HostnameVerifier sslHostnameVerifier; + private SSLContext sslContext; + private KeyStore sslTrustStore; + private KeyStore sslKeyStore; + private char[] sslKeyStorePassword; RestClientBuilderImpl() { clientBuilder = ClientBuilder.newBuilder(); @@ -152,6 +162,22 @@ public T build(Class interfaceClass) throws IllegalStateException, RestCl clientBuilder.executorService(new ExecutorServiceWrapper(executorService.get(), asyncInterceptors)); + 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(); @@ -166,11 +192,36 @@ public T build(Class interfaceClass) throws IllegalStateException, RestCl CdiUtil.getBeanManager()); return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), - new Class[] {interfaceClass}, - new ProxyInvocationHandler(webTarget, restClientModel) + 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)) { diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java index eb3acb9d60..22a2af9a0d 100644 --- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java @@ -16,11 +16,19 @@ package org.glassfish.jersey.microprofile.restclient; +import java.io.IOException; +import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.AccessController; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -28,6 +36,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.enterprise.context.Dependent; @@ -40,6 +49,7 @@ import javax.enterprise.inject.spi.InjectionPoint; import javax.enterprise.inject.spi.PassivationCapable; import javax.enterprise.util.AnnotationLiteral; +import javax.net.ssl.HostnameVerifier; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -55,6 +65,7 @@ * config. * * @author David Kral + * @author Tomas Langer */ class RestClientProducer implements Bean, PassivationCapable { @@ -63,12 +74,24 @@ class RestClientProducer implements Bean, PassivationCapable { private static final String CONFIG_SCOPE = "/mp-rest/scope"; private static final String CONFIG_CONNECTION_TIMEOUT = "/mp-rest/connectTimeout"; private static final String CONFIG_READ_TIMEOUT = "/mp-rest/readTimeout"; + private static final String CONFIG_SSL_TRUST_STORE_LOCATION = "/mp-rest/trustStore"; + private static final String CONFIG_SSL_TRUST_STORE_TYPE = "/mp-rest/trustStoreType"; + private static final String CONFIG_SSL_TRUST_STORE_PASSWORD = "/mp-rest/trustStorePassword"; + private static final String CONFIG_SSL_KEY_STORE_LOCATION = "/mp-rest/keyStore"; + private static final String CONFIG_SSL_KEY_STORE_TYPE = "/mp-rest/keyStoreType"; + private static final String CONFIG_SSL_KEY_STORE_PASSWORD = "/mp-rest/keyStorePassword"; + private static final String CONFIG_SSL_HOSTNAME_VERIFIER = "/mp-rest/hostnameVerifier"; + private static final String CONFIG_PROVIDERS = "/mp-rest/providers"; + private static final String DEFAULT_KEYSTORE_TYPE = "JKS"; + private static final String CLASSPATH_LOCATION = "classpath:"; + private static final String FILE_LOCATION = "file:"; - private final BeanManager beanManager; private final Class interfaceType; - private final Class scope; private final Config config; - private final String baseUrl; + private final String fqcn; + private final Optional restClientAnnotation; + private final Optional configKey; + private final Class scope; /** * Creates new instance of RestClientProducer. @@ -76,26 +99,13 @@ class RestClientProducer implements Bean, PassivationCapable { * @param interfaceType rest client interface * @param beanManager bean manager */ - RestClientProducer(Class interfaceType, - BeanManager beanManager) { + RestClientProducer(Class interfaceType, BeanManager beanManager) { this.interfaceType = interfaceType; - this.beanManager = beanManager; this.config = ConfigProvider.getConfig(); - this.baseUrl = getBaseUrl(interfaceType); - this.scope = resolveProperClientScope(); - } - - private String getBaseUrl(Class interfaceType) { - Optional uri = config.getOptionalValue(interfaceType.getName() + CONFIG_URI, String.class); - return uri.orElse(config.getOptionalValue(interfaceType.getName() + CONFIG_URL, String.class).orElseGet( - () -> { - RegisterRestClient registerRestClient = interfaceType.getAnnotation(RegisterRestClient.class); - if (registerRestClient != null) { - return registerRestClient.baseUri(); - } - throw new DeploymentException("This interface has to be annotated with @RegisterRestClient annotation."); - } - )); + this.fqcn = interfaceType.getName(); + this.restClientAnnotation = Optional.ofNullable(interfaceType.getAnnotation(RegisterRestClient.class)); + this.configKey = restClientAnnotation.map(RegisterRestClient::configKey); + this.scope = resolveClientScope(interfaceType, beanManager, config, fqcn, configKey); } @Override @@ -115,17 +125,27 @@ public boolean isNullable() { @Override public Object create(CreationalContext creationalContext) { - try { - RestClientBuilder restClientBuilder = RestClientBuilder.newBuilder().baseUrl(new URL(baseUrl)); - config.getOptionalValue(interfaceType.getName() + CONFIG_CONNECTION_TIMEOUT, Long.class) - .ifPresent(aLong -> restClientBuilder.connectTimeout(aLong, TimeUnit.MILLISECONDS)); - config.getOptionalValue(interfaceType.getName() + CONFIG_READ_TIMEOUT, Long.class) - .ifPresent(aLong -> restClientBuilder.readTimeout(aLong, TimeUnit.MILLISECONDS)); - return restClientBuilder.build(interfaceType); - } catch (MalformedURLException e) { - throw new IllegalStateException("URL is not in valid format for Rest interface " + interfaceType.getName() - + ": " + baseUrl); - } + // Base URL + RestClientBuilder restClientBuilder = RestClientBuilder.newBuilder().baseUrl(getBaseUrl()); + // Connection timeout (if configured) + getConfigOption(Long.class, CONFIG_CONNECTION_TIMEOUT) + .ifPresent(aLong -> restClientBuilder.connectTimeout(aLong, TimeUnit.MILLISECONDS)); + // Connection read timeout (if configured) + getConfigOption(Long.class, CONFIG_READ_TIMEOUT) + .ifPresent(aLong -> restClientBuilder.readTimeout(aLong, TimeUnit.MILLISECONDS)); + + // Providers from configuration + addConfiguredProviders(restClientBuilder); + + // SSL configuration + getHostnameVerifier() + .ifPresent(restClientBuilder::hostnameVerifier); + getKeyStore(CONFIG_SSL_KEY_STORE_LOCATION, CONFIG_SSL_KEY_STORE_TYPE, CONFIG_SSL_KEY_STORE_PASSWORD) + .ifPresent(keyStore -> restClientBuilder.keyStore(keyStore.keyStore, keyStore.password)); + getKeyStore(CONFIG_SSL_TRUST_STORE_LOCATION, CONFIG_SSL_TRUST_STORE_TYPE, CONFIG_SSL_TRUST_STORE_PASSWORD) + .ifPresent(keystore -> restClientBuilder.trustStore(keystore.keyStore)); + + return restClientBuilder.build(interfaceType); } @Override @@ -140,8 +160,8 @@ public Set getTypes() { @Override public Set getQualifiers() { Set annotations = new HashSet<>(); - annotations.add(new AnnotationLiteral() {}); - annotations.add(new AnnotationLiteral() {}); + annotations.add(new AnnotationLiteral() { }); + annotations.add(new AnnotationLiteral() { }); annotations.add(RestClient.LITERAL); return annotations; } @@ -177,15 +197,190 @@ public String getId() { return interfaceType.getName(); } - private Class resolveProperClientScope() { - String configScope = config.getOptionalValue(interfaceType.getName() + CONFIG_SCOPE, String.class).orElse(null); - if (configScope != null) { - Class scope = AccessController.doPrivileged(ReflectionHelper.classForNamePA(configScope)); + private void addConfiguredProviders(RestClientBuilder restClientBuilder) { + Optional configOption = getConfigOption(String[].class, CONFIG_PROVIDERS); + if (!configOption.isPresent()) { + return; + } + + String[] classNames = configOption.get(); + for (String className : classNames) { + Class providerClass = AccessController.doPrivileged(ReflectionHelper.classForNamePA(className)); + Optional priority = getConfigOption(Integer.class, CONFIG_PROVIDERS + "/" + + className + + "/priority"); + + if (priority.isPresent()) { + restClientBuilder.register(providerClass, priority.get()); + } else { + restClientBuilder.register(providerClass); + } + } + } + + private URL getBaseUrl() { + Supplier baseUrlDefault = () -> { + throw new DeploymentException("This interface has to be annotated with @RegisterRestClient annotation."); + }; + + String baseUrl = getOption(config, + fqcn, + configKey, + restClientAnnotation.map(RegisterRestClient::baseUri), + baseUrlDefault, + String.class, + CONFIG_URI, + CONFIG_URL); + + try { + return new URL(baseUrl); + } catch (MalformedURLException e) { + throw new IllegalStateException("URL is not in valid format for Rest interface " + interfaceType.getName() + + ": " + baseUrl, e); + } + } + + // a helper to get a long option from configuration based on fully qualified class name or config key + private Optional getConfigOption(Class optionType, String propertySuffix) { + return Optional.ofNullable(getOption(config, + fqcn, + configKey, + Optional.empty(), + () -> null, + optionType, + propertySuffix)); + } + + // a helper to find an option from configuration based on fully qualified class name or config key, from annotation, + // or using a default value + private static T getOption(Config config, + String fqcn, + Optional configKey, + Optional valueFromAnnotation, + Supplier defaultValue, + Class propertyType, + String... propertySuffixes) { + + /* + * Spec: + * 1. if explicit configuration for class exists, use it + * 2. if explicit configuration for config key exists, use it + * 3. if annotated and explicitly configured, use it + * 4. use default + */ + + // configuration for fully qualified class name + for (String propertySuffix : propertySuffixes) { + // 1. + Optional value = config.getOptionalValue(fqcn + propertySuffix, propertyType); + if (value.isPresent()) { + return value.get(); + } + } + + // configuration for config key + if (configKey.isPresent()) { + String theKey = configKey.get(); + if (!theKey.isEmpty()) { + for (String propertySuffix : propertySuffixes) { + // 2. + Optional value = config.getOptionalValue(theKey + propertySuffix, propertyType); + if (value.isPresent()) { + return value.get(); + } + } + } + } + + // 3. and 4. + return valueFromAnnotation.orElseGet(defaultValue); + } + + private Optional getKeyStore(String configLocation, String configType, String configPassword) { + String keyStoreLocation = getConfigOption(String.class, configLocation).orElse(null); + if (keyStoreLocation == null) { + return Optional.empty(); + } + + String keyStoreType = getConfigOption(String.class, configType).orElse(DEFAULT_KEYSTORE_TYPE); + String password = getConfigOption(String.class, configPassword).orElse(null); + + KeyStore keyStore; + try { + keyStore = KeyStore.getInstance(keyStoreType); + } catch (KeyStoreException e) { + throw new IllegalStateException("Failed to create keystore of type: " + keyStoreType + " for " + interfaceType, e); + } + + try (InputStream storeStream = locationToStream(keyStoreLocation)) { + keyStore.load(storeStream, password.toCharArray()); + } catch (IOException | NoSuchAlgorithmException | CertificateException e) { + throw new IllegalStateException("Failed to load keystore from " + keyStoreLocation, e); + } + + return Optional.of(new KeyStoreConfig(keyStore, password)); + } + + private InputStream locationToStream(String location) throws IOException { + // location in config has two flavors: + // file:/home/user/some.jks + // classpath:/client-keystore.jks + + if (location.startsWith(CLASSPATH_LOCATION)) { + String resource = location.substring(CLASSPATH_LOCATION.length()); + // first try to read from teh same classloader as the rest client interface + InputStream result = interfaceType.getResourceAsStream(resource); + if (null == result) { + // and if not found, use the context classloader (for example in TCK, this is needed) + result = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource); + } + return result; + } else if (location.startsWith(FILE_LOCATION)) { + return Files.newInputStream(Paths.get(location.substring(FILE_LOCATION.length()))); + } else { + throw new IllegalStateException("Location of keystore must start with either classpath: or file:, but is: " + + location + + " for " + + interfaceType); + } + } + + private Optional getHostnameVerifier() { + Optional verifier = getConfigOption(String.class, CONFIG_SSL_HOSTNAME_VERIFIER); + + return verifier.map(className -> { + Class theClass = + AccessController.doPrivileged(ReflectionHelper.classForNamePA(className)); + if (theClass == null) { + throw new IllegalStateException("Invalid hostname verifier class: " + className); + } + + return ReflectionUtil.createInstance(theClass); + }); + } + + private static Class resolveClientScope(Class interfaceType, + BeanManager beanManager, + Config config, + String fqcn, + Optional configKey) { + + String configuredScope = getOption(config, + fqcn, + configKey, + Optional.empty(), + () -> null, + String.class, + CONFIG_SCOPE); + + if (configuredScope != null) { + Class scope = AccessController.doPrivileged(ReflectionHelper.classForNamePA(configuredScope)); if (scope == null) { - throw new IllegalStateException("Invalid scope from config: " + configScope); + throw new IllegalStateException("Invalid scope from config: " + configuredScope); } return scope; } + List possibleScopes = Arrays.stream(interfaceType.getDeclaredAnnotations()) .filter(annotation -> beanManager.isScope(annotation.annotationType())) .collect(Collectors.toList()); @@ -199,4 +394,14 @@ private Class resolveProperClientScope() { + interfaceType + " has " + possibleScopes); } } + + private static final class KeyStoreConfig { + private final KeyStore keyStore; + private final String password; + + private KeyStoreConfig(KeyStore keyStore, String password) { + this.keyStore = keyStore; + this.password = password; + } + } } diff --git a/tests/integration/microprofile/rest-client/pom.xml b/tests/integration/microprofile/rest-client/pom.xml index c35a587de0..a4c5e036a1 100644 --- a/tests/integration/microprofile/rest-client/pom.xml +++ b/tests/integration/microprofile/rest-client/pom.xml @@ -39,7 +39,7 @@ org.eclipse.microprofile.rest.client microprofile-rest-client-tck - 1.2.1 + 1.3.3 test diff --git a/tests/integration/microprofile/rest-client/tck-suite.xml b/tests/integration/microprofile/rest-client/tck-suite.xml index 3106d69ee5..592f1360fe 100644 --- a/tests/integration/microprofile/rest-client/tck-suite.xml +++ b/tests/integration/microprofile/rest-client/tck-suite.xml @@ -16,27 +16,13 @@ SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 --> - + - - - - - - - - - - - - - - \ No newline at end of file