From 9f03c5f210be058d22eeb0a81f3c22ea2abde922 Mon Sep 17 00:00:00 2001 From: David Kral Date: Thu, 2 May 2019 15:22:21 +0200 Subject: [PATCH] rest client moved to the different module as requested by jansupol Signed-off-by: David Kral --- ext/{ => microprofile}/mp-rest-client/pom.xml | 11 +- .../restclient/BeanClassModel.java | 299 ++++++++ .../restclient/BeanParamModel.java | 89 +++ .../restclient/ClientHeaderParamModel.java | 78 ++ .../restclient/ConfigWrapper.java | 91 +++ .../restclient/CookieParamModel.java | 50 ++ .../DefaultResponseExceptionMapper.java | 39 + .../restclient/ExecutorServiceWrapper.java | 118 +++ .../restclient/FormParamModel.java | 57 ++ .../restclient/HeaderParamModel.java | 51 ++ .../restclient/HeadersContext.java | 103 +++ .../restclient/HeadersRequestFilter.java | 37 + .../InterceptorInvocationContext.java | 145 ++++ .../restclient/InterfaceModel.java | 366 ++++++++++ .../restclient/InterfaceUtil.java | 136 ++++ .../JerseyRestClientBuilderResolver.java | 32 + .../restclient/MatrixParamModel.java | 59 ++ .../microprofile/restclient/MethodModel.java | 670 ++++++++++++++++++ .../microprofile/restclient/ParamModel.java | 310 ++++++++ .../restclient/PathParamModel.java | 53 ++ .../restclient/ProxyInvocationHandler.java | 48 ++ .../restclient/QueryParamModel.java | 56 ++ .../restclient/ReflectionUtil.java | 53 ++ .../RequestHeaderAutoDiscoverable.java | 36 + .../restclient/RestClientBuilderImpl.java | 333 +++++++++ .../restclient/RestClientExtension.java | 134 ++++ .../restclient/RestClientModel.java | 157 ++++ .../restclient/RestClientProducer.java | 208 ++++++ .../src/main/resources/META-INF/beans.xml | 0 .../javax.enterprise.inject.spi.Extension | 2 +- ....rest.client.spi.RestClientBuilderResolver | 2 +- ...sfish.jersey.internal.spi.AutoDiscoverable | 2 +- .../internal/StringMessageProvider.java | 0 .../restclient/ApplicationResource.java | 41 ++ .../restclient/ApplicationResourceImpl.java | 32 + .../restclient/CorrectInterface.java | 72 ++ .../restclient/InterfaceValidationTest.java | 35 + .../restclient/RestClientModelTest.java | 47 ++ .../src/test/resources/arquillian.xml | 0 .../src/test/resources/server.policy | 78 ++ .../mp-rest-client/tck-suite.xml | 7 + ext/microprofile/pom.xml | 20 + ext/pom.xml | 2 +- 43 files changed, 4154 insertions(+), 5 deletions(-) rename ext/{ => microprofile}/mp-rest-client/pom.xml (93%) create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanClassModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ClientHeaderParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ConfigWrapper.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/DefaultResponseExceptionMapper.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ExecutorServiceWrapper.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersContext.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersRequestFilter.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterceptorInvocationContext.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceUtil.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/JerseyRestClientBuilderResolver.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RequestHeaderAutoDiscoverable.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientExtension.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientModel.java create mode 100644 ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java rename ext/{ => microprofile}/mp-rest-client/src/main/resources/META-INF/beans.xml (100%) rename ext/{ => microprofile}/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension (91%) rename ext/{ => microprofile}/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver (90%) rename ext/{ => microprofile}/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable (90%) rename ext/{ => microprofile}/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java (100%) create mode 100644 ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResource.java create mode 100644 ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResourceImpl.java create mode 100644 ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/CorrectInterface.java create mode 100644 ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/InterfaceValidationTest.java create mode 100644 ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/RestClientModelTest.java rename ext/{ => microprofile}/mp-rest-client/src/test/resources/arquillian.xml (100%) create mode 100644 ext/microprofile/mp-rest-client/src/test/resources/server.policy rename ext/{ => microprofile}/mp-rest-client/tck-suite.xml (80%) create mode 100644 ext/microprofile/pom.xml diff --git a/ext/mp-rest-client/pom.xml b/ext/microprofile/mp-rest-client/pom.xml similarity index 93% rename from ext/mp-rest-client/pom.xml rename to ext/microprofile/mp-rest-client/pom.xml index 17fad0e10f..39347eabb1 100644 --- a/ext/mp-rest-client/pom.xml +++ b/ext/microprofile/mp-rest-client/pom.xml @@ -20,7 +20,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - project + jersey-microprofile org.glassfish.jersey.ext 2.29-SNAPSHOT @@ -147,6 +147,11 @@ org.apache.maven.plugins maven-surefire-plugin + tck-suite.xml @@ -189,5 +194,9 @@ + + + + \ No newline at end of file diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanClassModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanClassModel.java new file mode 100644 index 0000000000..6a5efe3a37 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanClassModel.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MultivaluedMap; + +import org.glassfish.jersey.model.Parameter; + +/** + * Model of method parameter annotated by {@link BeanParam} annotation. + * + * @author David Kral + */ +class BeanClassModel { + + private final Class beanClass; + private final List parameterModels; + + /** + * Create new instance of bean annotated parameter. + * + * @param interfaceModel rest client interface model + * @param beanClass bean annotated parameter class + * @return new instance + */ + static BeanClassModel fromClass(InterfaceModel interfaceModel, Class beanClass) { + return new Builder(interfaceModel, beanClass) + .processPathFields() + .processHeaderFields() + .processCookieFields() + .processQueryFields() + .processMatrixFields() + .build(); + } + + private BeanClassModel(Builder builder) { + this.beanClass = builder.beanClass; + this.parameterModels = builder.parameterModels; + } + + /** + * List of all class fields annotated with supported parameter annotation + * + * @return parameter model list + */ + List getParameterModels() { + return parameterModels; + } + + /** + * Resolves bean path parameters. + * + * @param webTarget web target path + * @param instance actual method parameter value + * @return updated web target path + */ + @SuppressWarnings("unchecked") + WebTarget resolvePath(WebTarget webTarget, Object instance) { + AtomicReference toReturn = new AtomicReference<>(webTarget); + parameterModels.stream() + .filter(paramModel -> paramModel.handles(PathParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + toReturn.set((WebTarget) parameterModel.handleParameter(webTarget, + PathParam.class, + resolveValueFromField(field, instance))); + }); + return toReturn.get(); + } + + /** + * Resolves bean header parameters. + * + * @param headers headers + * @param instance actual method parameter value + * @return updated headers + */ + @SuppressWarnings("unchecked") + MultivaluedMap resolveHeaders(MultivaluedMap headers, + Object instance) { + parameterModels.stream() + .filter(paramModel -> paramModel.handles(HeaderParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + parameterModel.handleParameter(headers, + HeaderParam.class, + resolveValueFromField(field, instance)); + }); + return headers; + } + + /** + * Resolves bean cookie parameters. + * + * @param cookies cookies + * @param instance actual method parameter value + * @return updated cookies + */ + @SuppressWarnings("unchecked") + Map resolveCookies(Map cookies, + Object instance) { + parameterModels.stream() + .filter(paramModel -> paramModel.handles(CookieParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + parameterModel.handleParameter(cookies, + CookieParam.class, + resolveValueFromField(field, instance)); + }); + return cookies; + } + + /** + * Resolves bean query parameters. + * + * @param query queries + * @param instance actual method parameter value + * @return updated queries + */ + @SuppressWarnings("unchecked") + Map resolveQuery(Map query, + Object instance) { + parameterModels.stream() + .filter(paramModel -> paramModel.handles(QueryParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + parameterModel.handleParameter(query, + QueryParam.class, + resolveValueFromField(field, instance)); + }); + return query; + } + + /** + * Resolves bean matrix parameters. + * + * @param webTarget web target path + * @param instance actual method parameter value + * @return updated web target path + */ + @SuppressWarnings("unchecked") + WebTarget resolveMatrix(WebTarget webTarget, + Object instance) { + AtomicReference toReturn = new AtomicReference<>(webTarget); + parameterModels.stream() + .filter(paramModel -> paramModel.handles(MatrixParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + toReturn.set((WebTarget) parameterModel.handleParameter(webTarget, + MatrixParam.class, + resolveValueFromField(field, instance))); + }); + return toReturn.get(); + } + + + /** + * Resolves bean form parameters. + * + * @param form web form + * @param instance actual method parameter value + * @return updated web form + */ + @SuppressWarnings("unchecked") + Form resolveForm(Form form, + Object instance) { + parameterModels.stream() + .filter(paramModel -> paramModel.handles(FormParam.class)) + .forEach(parameterModel -> { + Field field = (Field) parameterModel.getAnnotatedElement(); + parameterModel.handleParameter(form, + FormParam.class, + resolveValueFromField(field, instance)); + }); + return form; + } + + private Object resolveValueFromField(Field field, Object instance) { + try { + Object toReturn; + field.setAccessible(true); + toReturn = field.get(instance); + field.setAccessible(false); + return toReturn; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static class Builder { + + private final InterfaceModel interfaceModel; + private final Class beanClass; + private ArrayList parameterModels = new ArrayList<>(); + + private Builder(InterfaceModel interfaceModel, Class beanClass) { + this.interfaceModel = interfaceModel; + this.beanClass = beanClass; + } + + /** + * Parses all {@link PathParam} annotated fields from bean class. + * + * @return updated builder instance + */ + Builder processPathFields() { + return processFieldsByParameterClass(PathParam.class); + } + + /** + * Parses all {@link HeaderParam} annotated fields from bean class. + * + * @return updated builder instance + */ + Builder processHeaderFields() { + return processFieldsByParameterClass(HeaderParam.class); + } + + /** + * Parses all {@link CookieParam} annotated fields from bean class. + * + * @return updated builder instance + */ + Builder processCookieFields() { + return processFieldsByParameterClass(CookieParam.class); + } + + /** + * Parses all {@link QueryParam} annotated fields from bean class. + * + * @return updated builder instance + */ + Builder processQueryFields() { + return processFieldsByParameterClass(QueryParam.class); + } + + /** + * Parses all {@link MatrixParam} annotated fields from bean class. + * + * @return updated builder instance + */ + Builder processMatrixFields() { + return processFieldsByParameterClass(MatrixParam.class); + } + + private Builder processFieldsByParameterClass(Class parameterClass) { + for (Field field : beanClass.getDeclaredFields()) { + if (field.isAnnotationPresent(parameterClass)) { + Parameter parameter = Parameter.create(parameterClass, parameterClass, false, + field.getType(), field.getGenericType(), + field.getDeclaredAnnotations()); + parameterModels.add(ParamModel.from(interfaceModel, field.getType(), field, + parameter, -1)); + } + } + return this; + } + + /** + * Creates new BeanClassModel instance. + * + * @return new instance + */ + BeanClassModel build() { + return new BeanClassModel(this); + } + } + +} 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 new file mode 100644 index 0000000000..348382ea6e --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanParamModel.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Contains information about method parameter or class field which is annotated by {@link BeanParam}. + * + * @author David Kral + */ +class BeanParamModel extends ParamModel { + + private BeanClassModel beanClassModel; + + BeanParamModel(Builder builder) { + super(builder); + beanClassModel = BeanClassModel.fromClass(interfaceModel, (Class) getType()); + } + + @Override + public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { + if (PathParam.class.equals(annotationClass)) { + return beanClassModel.resolvePath((WebTarget) requestPart, instance); + } else if (HeaderParam.class.equals(annotationClass)) { + return beanClassModel.resolveHeaders((MultivaluedMap) requestPart, instance); + } else if (CookieParam.class.equals(annotationClass)) { + return beanClassModel.resolveCookies((Map) requestPart, instance); + } else if (QueryParam.class.equals(annotationClass)) { + return beanClassModel.resolveQuery((Map) requestPart, instance); + } else if (MatrixParam.class.equals(annotationClass)) { + return beanClassModel.resolveMatrix((WebTarget) requestPart, instance); + } else if (FormParam.class.equals(annotationClass)) { + return beanClassModel.resolveForm((Form) requestPart, instance); + } + throw new UnsupportedOperationException(annotationClass.getName() + " is not supported!"); + } + + @Override + public boolean handles(Class annotation) { + return PathParam.class.equals(annotation) + || HeaderParam.class.equals(annotation) + || CookieParam.class.equals(annotation) + || QueryParam.class.equals(annotation) + || MatrixParam.class.equals(annotation) + || FormParam.class.equals(annotation); + } + + /** + * Returns {@link List} of all parameters annotated by searched annotation. + * + * @param paramAnnotation searched annotation + * @return filtered list + */ + List getAllParamsWithType(Class paramAnnotation) { + return beanClassModel.getParameterModels().stream() + .filter(paramModel -> paramModel.handles(paramAnnotation)) + .collect(Collectors.toList()); + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ClientHeaderParamModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ClientHeaderParamModel.java new file mode 100644 index 0000000000..fa5d3afe07 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ClientHeaderParamModel.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.reflect.Method; + +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; + +/** + * Contains information about method annotation {@link ClientHeaderParam}. + * + * @author David Kral + */ +class ClientHeaderParamModel { + + private final String headerName; + private final String[] headerValue; + private final Method computeMethod; + private final boolean required; + + ClientHeaderParamModel(Class iClass, ClientHeaderParam clientHeaderParam) { + headerName = clientHeaderParam.name(); + headerValue = clientHeaderParam.value(); + computeMethod = InterfaceUtil.parseComputeMethod(iClass, headerValue); + required = clientHeaderParam.required(); + } + + /** + * Returns header name. + * + * @return header name + */ + String getHeaderName() { + return headerName; + } + + /** + * Returns header value. + * + * @return header value + */ + String[] getHeaderValue() { + return headerValue; + } + + /** + * Returns method which is used to compute header value. + * + * @return compute method + */ + Method getComputeMethod() { + return computeMethod; + } + + /** + * Returns true if header is required and false if not. It header is not required and exception + * is thrown during compute method invocation, this header will be ignored and not included to request. + * + * @return if header is required + */ + boolean isRequired() { + return required; + } +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ConfigWrapper.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ConfigWrapper.java new file mode 100644 index 0000000000..466ab38895 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ConfigWrapper.java @@ -0,0 +1,91 @@ +package org.glassfish.jersey.microprofile.restclient; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; + +/** + * Configuration wrapper for {@link Configuration}. This class is needed due to custom provider registrations. + * + * @author David Kral + */ +class ConfigWrapper implements Configuration { + + private final Configuration jerseyBuilderConfig; + private final Map, Map, Integer>> customProviders; + + ConfigWrapper(Configuration jerseyBuilderConfig) { + this.jerseyBuilderConfig = jerseyBuilderConfig; + this.customProviders = new HashMap<>(); + } + + void addCustomProvider(Class provider, Map, Integer> contracts) { + if (customProviders.containsKey(provider)) { + customProviders.get(provider).putAll(contracts); + } else { + customProviders.put(provider, contracts); + } + } + + @Override + public RuntimeType getRuntimeType() { + return jerseyBuilderConfig.getRuntimeType(); + } + + @Override + public Map getProperties() { + return jerseyBuilderConfig.getProperties(); + } + + @Override + public Object getProperty(String name) { + return jerseyBuilderConfig.getProperty(name); + } + + @Override + public Collection getPropertyNames() { + return jerseyBuilderConfig.getPropertyNames(); + } + + @Override + public boolean isEnabled(Feature feature) { + return jerseyBuilderConfig.isEnabled(feature); + } + + @Override + public boolean isEnabled(Class featureClass) { + return jerseyBuilderConfig.isEnabled(featureClass); + } + + @Override + public boolean isRegistered(Object component) { + return jerseyBuilderConfig.isRegistered(component); + } + + @Override + public boolean isRegistered(Class componentClass) { + return jerseyBuilderConfig.isRegistered(componentClass); + } + + @Override + public Map, Integer> getContracts(Class componentClass) { + Map, Integer> map = new HashMap<>(jerseyBuilderConfig.getContracts(componentClass)); + if (customProviders.containsKey(componentClass)) map.putAll(customProviders.get(componentClass)); + return map; + } + + @Override + public Set> getClasses() { + return jerseyBuilderConfig.getClasses(); + } + + @Override + public Set getInstances() { + return jerseyBuilderConfig.getInstances(); + } +} 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 new file mode 100644 index 0000000000..9477c34d9b --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/CookieParamModel.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.Map; + +import javax.ws.rs.CookieParam; + +/** + * Contains information about method parameter or class field which is annotated by {@link CookieParam}. + * + * @author David Kral + */ +class CookieParamModel extends ParamModel> { + + private final String cookieParamName; + + CookieParamModel(Builder builder) { + super(builder); + cookieParamName = builder.cookieParamName(); + } + + @Override + 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) { + return CookieParam.class.equals(annotation); + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/DefaultResponseExceptionMapper.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/DefaultResponseExceptionMapper.java new file mode 100644 index 0000000000..b699182e3b --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/DefaultResponseExceptionMapper.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019 Oracle 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 javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +/** + * Default {@link ResponseExceptionMapper} implementation + * + * @author David Kral + */ +public class DefaultResponseExceptionMapper implements ResponseExceptionMapper { + @Override + public Throwable toThrowable(Response response) { + return new WebApplicationException("Unknown error, status code " + response.getStatus(), response); + } + + @Override + public int getPriority() { + return Integer.MAX_VALUE; + } +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ExecutorServiceWrapper.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ExecutorServiceWrapper.java new file mode 100644 index 0000000000..c3a8dd66ae --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ExecutorServiceWrapper.java @@ -0,0 +1,118 @@ +package org.glassfish.jersey.microprofile.restclient; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor; + +/** + * Invokes all {@link AsyncInvocationInterceptor} for every new thread. + * + * @author David Kral + */ +class ExecutorServiceWrapper implements ExecutorService { + + private final ExecutorService wrapped; + private final List asyncInterceptors; + + ExecutorServiceWrapper(ExecutorService wrapped, + List asyncInterceptors) { + this.wrapped = wrapped; + this.asyncInterceptors = asyncInterceptors; + } + + @Override + public void shutdown() { + wrapped.shutdown(); + } + + @Override + public List shutdownNow() { + return wrapped.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return wrapped.isShutdown(); + } + + @Override + public boolean isTerminated() { + return wrapped.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return wrapped.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return wrapped.submit(wrap(task)); + } + + @Override + public Future submit(Runnable task, T result) { + return wrapped.submit(wrap(task), result); + } + + @Override + public Future submit(Runnable task) { + return wrapped.submit(wrap(task)); + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + return wrapped.invokeAll(wrap(tasks)); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return wrapped.invokeAll(wrap(tasks), timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + return wrapped.invokeAny(wrap(tasks)); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return wrapped.invokeAny(wrap(tasks), timeout, unit); + } + + @Override + public void execute(Runnable command) { + wrapped.execute(wrap(command)); + } + + private Callable wrap(Callable task) { + return () -> { + asyncInterceptors.forEach(AsyncInvocationInterceptor::applyContext); + return task.call(); + }; + } + + private Runnable wrap(Runnable task) { + return () -> { + asyncInterceptors.forEach(AsyncInvocationInterceptor::applyContext); + task.run(); + }; + } + + + private Collection> wrap(Collection> tasks) { + return tasks.stream() + .map(this::wrap) + .collect(Collectors.toList()); + } +} 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 new file mode 100644 index 0000000000..7a8f738f87 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/FormParamModel.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.Collection; + +import javax.ws.rs.FormParam; +import javax.ws.rs.core.Form; + +/** + * Contains information about method parameter or class field which is annotated by {@link FormParam}. + * + * @author David Kral + */ +class FormParamModel extends ParamModel
{ + + private final String formParamName; + + FormParamModel(Builder builder) { + super(builder); + formParamName = builder.formParamName(); + } + + @Override + Form handleParameter(Form form, Class annotationClass, Object instance) { + Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); + if (resolvedValue instanceof Collection) { + for (final Object v : ((Collection) resolvedValue)) { + form.param(formParamName, v.toString()); + } + } else { + form.param(formParamName, resolvedValue.toString()); + } + return form; + } + + @Override + 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 new file mode 100644 index 0000000000..3417ada60a --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeaderParamModel.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.Collections; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Contains information about method parameter or class field which is annotated by {@link HeaderParam}. + * + * @author David Kral + */ +class HeaderParamModel extends ParamModel> { + + private String headerParamName; + + HeaderParamModel(Builder builder) { + super(builder); + this.headerParamName = builder.headerParamName(); + } + + @Override + MultivaluedMap handleParameter(MultivaluedMap requestPart, + Class annotationClass, Object instance) { + Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); + requestPart.put(headerParamName, Collections.singletonList(resolvedValue)); + return requestPart; + } + + @Override + 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/HeadersContext.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersContext.java new file mode 100644 index 0000000000..77bcffe72b --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersContext.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019 Oracle 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.util.Optional; +import java.util.function.Supplier; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * + * @author David Kral + */ +public final class HeadersContext { + + /** + * Headers context thread local, used by internal implementations of header filters. + */ + private static final ThreadLocal HEADERS_CONTEXT = new ThreadLocal<>(); + + private final MultivaluedMap inboundHeaders; + + /** + * The instance associated with the current thread. + * @return context for current thread or {@code empty} if none associated + */ + public static Optional get() { + return Optional.ofNullable(HEADERS_CONTEXT.get()); + } + + /** + * Computes the instance and associates it with current thread if none + * associated, or returns the instance already associated. + * + * @param contextSupplier supplier for header context to be associated with the thread if none is + * @return an instance associated with the current context, either from other provider, or from contextSupplier + */ + public static HeadersContext compute(Supplier contextSupplier) { + HeadersContext headersContext = HEADERS_CONTEXT.get(); + if (null == headersContext) { + set(contextSupplier.get()); + } + + return get().orElseThrow(() -> new IllegalStateException("Computed result was null")); + } + + /** + * Set the header context to be associated with current thread. + * + * @param context context to associate + */ + public static void set(HeadersContext context) { + HEADERS_CONTEXT.set(context); + } + + /** + * Remove the header context associated with current thread. + */ + public static void remove() { + HEADERS_CONTEXT.remove(); + } + + /** + * Create a new header context with client tracing enabled. + * + * @param inboundHeaders inbound header to be used for context propagation + * @return a new header context (not associated with current thread) + * @see #set(HeadersContext) + */ + public static HeadersContext create(MultivaluedMap inboundHeaders) { + return new HeadersContext(inboundHeaders); + } + + public HeadersContext(MultivaluedMap inboundHeaders) { + this.inboundHeaders = inboundHeaders; + } + + /** + * Map of headers that were received by server for an inbound call, + * may be used to propagate additional headers fro outbound request. + * + * @return map of inbound headers + */ + public MultivaluedMap inboundHeaders() { + return inboundHeaders; + } + + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersRequestFilter.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersRequestFilter.java new file mode 100644 index 0000000000..64ece19933 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/HeadersRequestFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019 Oracle 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 javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; + +/** + * Server side request filter used for propagation of request headers to server client request. + * + * @author David Kral + */ +@ConstrainedTo(RuntimeType.SERVER) +public class HeadersRequestFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext requestContext) { + HeadersContext.compute(() -> HeadersContext.create(requestContext.getHeaders())); + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterceptorInvocationContext.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterceptorInvocationContext.java new file mode 100644 index 0000000000..d559fb76d3 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterceptorInvocationContext.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.inject.spi.InterceptionType; +import javax.enterprise.inject.spi.Interceptor; +import javax.interceptor.InvocationContext; +import javax.ws.rs.client.WebTarget; + +/** + * Invokes all interceptors bound to the target. + * + * This approach needs to be used due to CDI does not handle properly interceptor invocation + * on proxy instances. + * + * @author David Kral + */ +class InterceptorInvocationContext implements InvocationContext { + + private final MethodModel methodModel; + private final Method method; + private final Map contextData; + private final List interceptors; + private final WebTarget classLevelWebTarget; + private Object[] args; + private int currentPosition; + + /** + * Creates new instance of InterceptorInvocationContext. + * + * @param classLevelWebTarget class level web target + * @param methodModel method model + * @param method reflection method + * @param args actual method arguments + */ + InterceptorInvocationContext(WebTarget classLevelWebTarget, + MethodModel methodModel, + Method method, + Object[] args) { + this.contextData = new HashMap<>(); + this.currentPosition = 0; + this.methodModel = methodModel; + this.method = method; + this.args = args; + this.classLevelWebTarget = classLevelWebTarget; + this.interceptors = methodModel.getInvocationInterceptors(); + + } + + @Override + public Object getTarget() { + return methodModel; + } + + @Override + public Object getTimer() { + return null; + } + + @Override + public Method getMethod() { + return method; + } + + @Override + public Constructor getConstructor() { + return null; + } + + @Override + public Object[] getParameters() { + return args; + } + + @Override + public void setParameters(Object[] params) { + this.args = params; + } + + @Override + public Map getContextData() { + return contextData; + } + + @Override + public Object proceed() { + if (currentPosition < interceptors.size()) { + return interceptors.get(currentPosition++).intercept(this); + } else { + return methodModel.invokeMethod(classLevelWebTarget, method, args); + } + } + + /** + * Contains actual interceptor instance and interceptor itself. + */ + static class InvocationInterceptor { + + private final Object interceptorInstance; + private final Interceptor interceptor; + + InvocationInterceptor(Object interceptorInstance, Interceptor interceptor) { + this.interceptorInstance = interceptorInstance; + this.interceptor = interceptor; + } + + /** + * Invokes interceptor with interception type AROUND_INVOKE. + * + * @param ctx invocation context + * @return interception result + */ + @SuppressWarnings("unchecked") + Object intercept(InvocationContext ctx) { + try { + return interceptor.intercept(InterceptionType.AROUND_INVOKE, interceptorInstance, ctx); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + } + } +} 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 new file mode 100644 index 0000000000..dad9c5e3b3 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceModel.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.ws.rs.Consumes; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor; +import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.glassfish.jersey.client.inject.ParameterInserter; +import org.glassfish.jersey.client.inject.ParameterInserterProvider; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.model.Parameter; + +/** + * Model of interface and its annotation. + * + * @author David Kral + */ +class InterfaceModel { + + private final InjectionManager injectionManager; + private final Class restClientClass; + private final String[] produces; + private final String[] consumes; + private final String path; + private final ClientHeadersFactory clientHeadersFactory; + private final CreationalContext creationalContext; + + private final List clientHeaders; + private final List asyncInterceptors; + private final Set responseExceptionMappers; + private final Set paramConverterProviders; + private final Set interceptorAnnotations; + + /** + * Creates new model based on interface class. Interface is parsed according to specific annotations. + * + * @param restClientClass interface class + * @param responseExceptionMappers registered exception mappers + * @param paramConverterProviders registered parameter providers + * @param asyncInterceptors async interceptors + * @param injectionManager + * @return new model instance + */ + static InterfaceModel from(Class restClientClass, + Set responseExceptionMappers, + Set paramConverterProviders, + List asyncInterceptors, + InjectionManager injectionManager) { + return new Builder(restClientClass, + responseExceptionMappers, + paramConverterProviders, + asyncInterceptors, + injectionManager) + .pathValue(restClientClass.getAnnotation(Path.class)) + .produces(restClientClass.getAnnotation(Produces.class)) + .consumes(restClientClass.getAnnotation(Consumes.class)) + .clientHeaders(restClientClass.getAnnotationsByType(ClientHeaderParam.class)) + .clientHeadersFactory(restClientClass.getAnnotation(RegisterClientHeaders.class)) + .build(); + } + + private InterfaceModel(Builder builder) { + this.injectionManager = builder.injectionManager; + this.restClientClass = builder.restClientClass; + this.path = builder.pathValue; + this.produces = builder.produces; + this.consumes = builder.consumes; + this.clientHeaders = builder.clientHeaders; + this.clientHeadersFactory = builder.clientHeadersFactory; + this.responseExceptionMappers = builder.responseExceptionMappers; + this.paramConverterProviders = builder.paramConverterProviders; + this.interceptorAnnotations = builder.interceptorAnnotations; + this.creationalContext = builder.creationalContext; + this.asyncInterceptors = builder.asyncInterceptors; + } + + /** + * Returns rest client interface class. + * + * @return interface class + */ + Class getRestClientClass() { + return restClientClass; + } + + /** + * Returns defined produces media types. + * + * @return produces + */ + String[] getProduces() { + return produces; + } + + /** + * Returns defined consumes media types. + * + * @return consumes + */ + String[] getConsumes() { + return consumes; + } + + /** + * Returns path value defined on interface level. + * + * @return path value + */ + String getPath() { + return path; + } + + /** + * Returns registered instance of {@link ClientHeadersFactory}. + * + * @return registered factory + */ + Optional getClientHeadersFactory() { + return Optional.ofNullable(clientHeadersFactory); + } + + /** + * Returns {@link List} of processed annotation {@link ClientHeaderParam} to {@link ClientHeaderParamModel} + * + * @return registered factories + */ + List getClientHeaders() { + return clientHeaders; + } + + /** + * Returns {@link List} of registered {@link AsyncInvocationInterceptor} + * + * @return registered async interceptors + */ + List getAsyncInterceptors() { + return asyncInterceptors; + } + + /** + * Returns {@link Set} of registered {@link ResponseExceptionMapper} + * + * @return registered exception mappers + */ + Set getResponseExceptionMappers() { + return responseExceptionMappers; + } + + /** + * Returns {@link Set} of registered {@link ParamConverterProvider} + * + * @return registered param converter providers + */ + Set getParamConverterProviders() { + return paramConverterProviders; + } + + /** + * Returns {@link Set} of interceptor annotations + * + * @return interceptor annotations + */ + Set getInterceptorAnnotations() { + return interceptorAnnotations; + } + + /** + * Context bound to this model. + * + * @return context + */ + CreationalContext getCreationalContext() { + return creationalContext; + } + + /** + * + * + * @return + */ + public InjectionManager getInjectionManager() { + return injectionManager; + } + + /** + * Resolves value of the method argument. + * + * @param arg actual argument value + * @return converted value of argument + */ + Object resolveParamValue(Object arg, Parameter parameter) { + final Iterable parameterInserterProviders + = Providers.getAllProviders(injectionManager, ParameterInserterProvider.class); + for (final ParameterInserterProvider parameterInserterProvider : parameterInserterProviders) { + if (parameterInserterProvider != null) { + ParameterInserter inserter = + (ParameterInserter) parameterInserterProvider.get(parameter); + return inserter.insert(arg); + } + } + return arg; + } + + private static class Builder { + + private final Class restClientClass; + + private final InjectionManager injectionManager; + private String pathValue; + private String[] produces; + private String[] consumes; + private ClientHeadersFactory clientHeadersFactory; + private CreationalContext creationalContext; + private List clientHeaders; + private List asyncInterceptors; + private Set responseExceptionMappers; + private Set paramConverterProviders; + private Set interceptorAnnotations; + + private Builder(Class restClientClass, + Set responseExceptionMappers, + Set paramConverterProviders, + List asyncInterceptors, + InjectionManager injectionManager) { + this.injectionManager = injectionManager; + this.restClientClass = restClientClass; + this.responseExceptionMappers = responseExceptionMappers; + this.paramConverterProviders = paramConverterProviders; + this.asyncInterceptors = asyncInterceptors; + filterAllInterceptorAnnotations(); + } + + private void filterAllInterceptorAnnotations() { + creationalContext = null; + interceptorAnnotations = new HashSet<>(); + try { + if (CDI.current() != null) { + BeanManager beanManager = CDI.current().getBeanManager(); + creationalContext = beanManager.createCreationalContext(null); + for (Annotation annotation : restClientClass.getAnnotations()) { + if (beanManager.isInterceptorBinding(annotation.annotationType())) { + interceptorAnnotations.add(annotation); + } + } + } + } catch (IllegalStateException ignored) { + //CDI not present. Ignore. + } + } + + /** + * Path value from {@link Path} annotation. If annotation is null, empty String is set as path. + * + * @param path {@link Path} annotation + * @return updated Builder instance + */ + Builder pathValue(Path path) { + this.pathValue = path != null ? path.value() : ""; + //if only / is added to path like this "localhost:80/test" it makes invalid path "localhost:80/test/" + this.pathValue = pathValue.equals("/") ? "" : pathValue; + return this; + } + + /** + * Extracts MediaTypes from {@link Produces} annotation. + * If annotation is null, new String array with {@link MediaType#WILDCARD} 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}; + return this; + } + + /** + * Extracts MediaTypes from {@link Consumes} annotation. + * If annotation is null, new String array with {@link MediaType#WILDCARD} 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}; + return this; + } + + /** + * Process data from {@link ClientHeaderParam} annotation to extract methods and values. + * + * @param clientHeaderParams {@link ClientHeaderParam} annotations + * @return updated Builder instance + */ + Builder clientHeaders(ClientHeaderParam[] clientHeaderParams) { + clientHeaders = Arrays.stream(clientHeaderParams) + .map(clientHeaderParam -> new ClientHeaderParamModel(restClientClass, clientHeaderParam)) + .collect(Collectors.toList()); + return this; + } + + Builder clientHeadersFactory(RegisterClientHeaders registerClientHeaders) { + clientHeadersFactory = registerClientHeaders != null + ? ReflectionUtil.createInstance(registerClientHeaders.value()) + : null; + return this; + } + + /** + * Creates new InterfaceModel instance. + * + * @return new instance + */ + InterfaceModel build() { + validateHeaderDuplicityNames(); + return new InterfaceModel(this); + } + + private void validateHeaderDuplicityNames() { + ArrayList names = new ArrayList<>(); + for (ClientHeaderParamModel clientHeaderParamModel : clientHeaders) { + String headerName = clientHeaderParamModel.getHeaderName(); + if (names.contains(headerName)) { + throw new RestClientDefinitionException("Header name cannot be registered more then once on the same target." + + "See " + restClientClass.getName()); + } + names.add(headerName); + } + } + } +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceUtil.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceUtil.java new file mode 100644 index 0000000000..83aa97a34c --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/InterfaceUtil.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.ws.rs.HttpMethod; + +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.glassfish.jersey.internal.util.ReflectionHelper; + +/** + * Utils for interface handling. + * + * @author David Kral + */ +class InterfaceUtil { + + private static final String PARAMETER_PARSE_REGEXP = "(?<=\\{).+?(?=\\})"; + private static final Pattern PATTERN = Pattern.compile(PARAMETER_PARSE_REGEXP); + + /** + * Parses all required parameters from template string. + * + * @param template template string + * @return parsed parameters + */ + static List parseParameters(String template) { + List allMatches = new ArrayList<>(); + Matcher m = PATTERN.matcher(template); + while (m.find()) { + allMatches.add(m.group()); + } + return allMatches; + } + + /** + * Validates and returns proper compute method defined in {@link ClientHeaderParam}. + * + * @param iClass interface class + * @param headerValue value of the header + * @return parsed method + */ + static Method parseComputeMethod(Class iClass, String[] headerValue) { + List computeMethodNames = InterfaceUtil.parseParameters(Arrays.toString(headerValue)); + /*if more than one string is specified as the value attribute, and one of the strings is a + compute method (surrounded by curly braces), then the implementation will throw a + RestClientDefinitionException*/ + if (headerValue.length > 1 && computeMethodNames.size() > 0) { + throw new RestClientDefinitionException("@ClientHeaderParam annotation should not contain compute method " + + "when multiple values are present in value attribute. " + + "See " + iClass.getName()); + } + if (computeMethodNames.size() == 1) { + String methodName = computeMethodNames.get(0); + List computeMethods = getAnnotationComputeMethod(iClass, methodName); + if (computeMethods.size() != 1) { + throw new RestClientDefinitionException("No valid compute method found for name: " + methodName); + } + return computeMethods.get(0); + } + return null; + } + + private static List getAnnotationComputeMethod(Class iClass, String methodName) { + if (methodName.contains(".")) { + return getStaticComputeMethod(methodName); + } + return getComputeMethod(iClass, methodName); + } + + private static List getStaticComputeMethod(String methodName) { + int lastIndex = methodName.lastIndexOf("."); + String className = methodName.substring(0, lastIndex); + String staticMethodName = methodName.substring(lastIndex + 1); + Class classWithStaticMethod = AccessController.doPrivileged(ReflectionHelper.classForNamePA(className)); + if (classWithStaticMethod == null) { + throw new IllegalStateException("No class with following name found: " + className); + } + return getComputeMethod(classWithStaticMethod, staticMethodName); + } + + private static List getComputeMethod(Class iClass, String methodName) { + return Arrays.stream(iClass.getMethods()) + // filter out methods with specified name only + .filter(method -> method.getName().equals(methodName)) + // filter out other methods than default and static + .filter(method -> method.isDefault() || Modifier.isStatic(method.getModifiers())) + // filter out methods without required return type + .filter(method -> method.getReturnType().equals(String.class) + || method.getReturnType().equals(String[].class)) + // filter out methods without required parameter types + .filter(method -> method.getParameterTypes().length == 0 || ( + method.getParameterTypes().length == 1 + && method.getParameterTypes()[0].equals(String.class))) + .collect(Collectors.toList()); + } + + /** + * Returns {@link List} of annotations which are type of {@link HttpMethod}. + * + * @param annotatedElement element with annotations + * @return annotations of given type + */ + static List> getHttpAnnotations(AnnotatedElement annotatedElement) { + return Arrays.stream(annotatedElement.getDeclaredAnnotations()) + .filter(annotation -> annotation.annotationType().getAnnotation(HttpMethod.class) != null) + .map(Annotation::annotationType) + .collect(Collectors.toList()); + } +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/JerseyRestClientBuilderResolver.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/JerseyRestClientBuilderResolver.java new file mode 100644 index 0000000000..db8e9189c7 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/JerseyRestClientBuilderResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Oracle 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 org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver; + +/** + * + * + * @author David Kral + */ +public class JerseyRestClientBuilderResolver extends RestClientBuilderResolver { + @Override + public RestClientBuilder newBuilder() { + return new RestClientBuilderImpl(); + } +} 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 new file mode 100644 index 0000000000..440a414b80 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MatrixParamModel.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.Collection; + +import javax.ws.rs.MatrixParam; +import javax.ws.rs.client.WebTarget; + +/** + * Contains information to method parameter which is annotated by {@link MatrixParam}. + * + * @author David Kral + */ +class MatrixParamModel extends ParamModel { + + private final String matrixParamName; + + /** + * Creates new matrix model. + * + * @param builder + */ + MatrixParamModel(Builder builder) { + super(builder); + matrixParamName = builder.matrixParamName(); + } + + @Override + 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()); + } else { + return requestPart.matrixParam(matrixParamName, resolvedValue); + } + } + + @Override + 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 new file mode 100644 index 0000000000..52079eb213 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/MethodModel.java @@ -0,0 +1,670 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +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.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.InterceptionType; +import javax.enterprise.inject.spi.Interceptor; +import javax.json.JsonValue; +import javax.ws.rs.Consumes; +import javax.ws.rs.CookieParam; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +/** + * Method model contains all information about method defined in rest client interface. + * + * @author David Kral + */ +class MethodModel { + + private static final String INVOKED_METHOD = "org.eclipse.microprofile.rest.client.invokedMethod"; + + private final InterfaceModel interfaceModel; + + private final Method method; + private final Class returnType; + private final String httpMethod; + private final String path; + private final String[] produces; + private final String[] consumes; + private final List parameterModels; + private final List clientHeaders; + private final List invocationInterceptors; + private final RestClientModel subResourceModel; + + /** + * Processes interface method and creates new instance of the model. + * + * @param interfaceModel + * @param method + * @return + */ + static MethodModel from(InterfaceModel interfaceModel, Method method) { + return new Builder(interfaceModel, method) + .returnType(method.getGenericReturnType()) + .httpMethod(parseHttpMethod(interfaceModel, method)) + .pathValue(method.getAnnotation(Path.class)) + .produces(method.getAnnotation(Produces.class)) + .consumes(method.getAnnotation(Consumes.class)) + .parameters(parameterModels(interfaceModel, method)) + .clientHeaders(method.getAnnotationsByType(ClientHeaderParam.class)) + .build(); + } + + private MethodModel(Builder builder) { + this.method = builder.method; + this.interfaceModel = builder.interfaceModel; + this.returnType = builder.returnType; + this.httpMethod = builder.httpMethod; + this.path = builder.pathValue; + this.produces = builder.produces; + this.consumes = builder.consumes; + this.parameterModels = builder.parameterModels; + this.clientHeaders = builder.clientHeaders; + this.invocationInterceptors = builder.invocationInterceptors; + if (httpMethod.isEmpty()) { + subResourceModel = RestClientModel.from(returnType, + interfaceModel.getResponseExceptionMappers(), + interfaceModel.getParamConverterProviders(), + interfaceModel.getAsyncInterceptors(), + interfaceModel.getInjectionManager()); + } else { + subResourceModel = null; + } + } + + /** + * Returns all registered cdi interceptors to this method. + * + * @return registered interceptors + */ + List getInvocationInterceptors() { + return invocationInterceptors; + } + + /** + * Invokes corresponding method according to + * + * @param classLevelTarget + * @param method + * @param args + * @return + */ + @SuppressWarnings("unchecked") + //I am checking the type of parameter and I know it should handle instance I am sending + Object invokeMethod(WebTarget classLevelTarget, Method method, Object[] args) { + WebTarget methodLevelTarget = classLevelTarget.path(path); + + AtomicReference entity = new AtomicReference<>(); + AtomicReference webTargetAtomicReference = new AtomicReference<>(methodLevelTarget); + parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(PathParam.class)) + .forEach(parameterModel -> + webTargetAtomicReference.set((WebTarget) + parameterModel + .handleParameter(webTargetAtomicReference.get(), + PathParam.class, + args[parameterModel + .getParamPosition()]))); + + parameterModels.stream() + .filter(ParamModel::isEntity) + .findFirst() + .ifPresent(parameterModel -> entity.set(args[parameterModel.getParamPosition()])); + + WebTarget webTarget = webTargetAtomicReference.get(); + if (httpMethod.isEmpty()) { + //sub resource method + return subResourceProxy(webTarget, returnType); + } + webTarget = addQueryParams(webTarget, args); + webTarget = addMatrixParams(webTarget, args); + + Invocation.Builder builder = webTarget + .request(produces) + .property(INVOKED_METHOD, method) + .headers(addCustomHeaders(args)); + builder = addCookies(builder, args); + + Object response; + + if (CompletionStage.class.isAssignableFrom(method.getReturnType())) { + response = asynchronousCall(builder, entity.get(), method); + } else { + response = synchronousCall(builder, entity.get(), method); + } + return response; + } + + private Object synchronousCall(Invocation.Builder builder, Object entity, Method method) { + Response response; + + if (entity != null + && !httpMethod.equals(GET.class.getSimpleName()) + && !httpMethod.equals(DELETE.class.getSimpleName())) { + response = builder.method(httpMethod, Entity.entity(entity, consumes[0])); + } else { + response = builder.method(httpMethod); + } + + evaluateResponse(response, method); + + if (returnType.equals(Void.class)) { + return null; + } else if (returnType.equals(Response.class)) { + return response; + } + return response.readEntity(returnType); + } + + private CompletableFuture asynchronousCall(Invocation.Builder builder, Object entity, Method method) { + ParameterizedType type = (ParameterizedType) method.getGenericReturnType(); + Type actualTypeArgument = type.getActualTypeArguments()[0]; //completionStage + CompletableFuture result = new CompletableFuture<>(); + Future theFuture; + if (entity != null + && !httpMethod.equals(GET.class.getSimpleName()) + && !httpMethod.equals(DELETE.class.getSimpleName())) { + theFuture = builder.async().method(httpMethod, Entity.entity(entity, consumes[0])); + } else { + theFuture = builder.async().method(httpMethod); + } + + CompletableFuture completableFuture = (CompletableFuture) theFuture; + completableFuture.thenAccept(response -> { + interfaceModel.getAsyncInterceptors().forEach(AsyncInvocationInterceptor::removeContext); + try { + evaluateResponse(response, method); + if (returnType.equals(Void.class)) { + result.complete(null); + } else if (returnType.equals(Response.class)) { + result.complete(response); + } else { + result.complete(response.readEntity(new GenericType<>(actualTypeArgument))); + } + } catch (Exception e) { + result.completeExceptionally(e); + } + }).exceptionally(throwable -> { + interfaceModel.getAsyncInterceptors().forEach(AsyncInvocationInterceptor::removeContext); + result.completeExceptionally(throwable); + return null; + }); + + return result; + } + + @SuppressWarnings("unchecked") + private T subResourceProxy(WebTarget webTarget, Class subResourceType) { + return (T) Proxy.newProxyInstance(subResourceType.getClassLoader(), + new Class[] {subResourceType}, + new ProxyInvocationHandler(webTarget, subResourceModel) + ); + } + + @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending + private WebTarget addQueryParams(WebTarget webTarget, Object[] args) { + Map queryParams = new HashMap<>(); + WebTarget toReturn = webTarget; + parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(QueryParam.class)) + .forEach(parameterModel -> parameterModel.handleParameter(queryParams, + QueryParam.class, + args[parameterModel.getParamPosition()])); + + for (Map.Entry entry : queryParams.entrySet()) { + toReturn = toReturn.queryParam(entry.getKey(), entry.getValue()); + } + return toReturn; + } + + @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending + private WebTarget addMatrixParams(WebTarget webTarget, Object[] args) { + AtomicReference toReturn = new AtomicReference<>(webTarget); + parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(MatrixParam.class)) + .forEach(parameterModel -> toReturn + .set((WebTarget) parameterModel.handleParameter(toReturn.get(), + MatrixParam.class, + args[parameterModel.getParamPosition()]))); + return toReturn.get(); + } + + @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending + private Invocation.Builder addCookies(Invocation.Builder builder, Object[] args) { + Map cookies = new HashMap<>(); + Invocation.Builder toReturn = builder; + parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(CookieParam.class)) + .forEach(parameterModel -> parameterModel.handleParameter(cookies, + CookieParam.class, + args[parameterModel.getParamPosition()])); + + for (Map.Entry entry : cookies.entrySet()) { + toReturn = toReturn.cookie(entry.getKey(), entry.getValue()); + } + return toReturn; + } + + private MultivaluedMap addCustomHeaders(Object[] args) { + MultivaluedMap result = new MultivaluedHashMap<>(); + for (Map.Entry> entry : resolveCustomHeaders(args).entrySet()) { + entry.getValue().forEach(val -> result.add(entry.getKey(), val)); + } + for (String produce : produces) { + result.add(HttpHeaders.ACCEPT, produce); + } + result.add(HttpHeaders.CONTENT_TYPE, consumes[0]); + return result; + } + + @SuppressWarnings("unchecked") //I am checking the type of parameter and I know it should handle instance I am sending + private MultivaluedMap resolveCustomHeaders(Object[] args) { + MultivaluedMap customHeaders = new MultivaluedHashMap<>(); + customHeaders.putAll(createMultivaluedHeadersMap(interfaceModel.getClientHeaders())); + customHeaders.putAll(createMultivaluedHeadersMap(clientHeaders)); + parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(HeaderParam.class)) + .forEach(parameterModel -> parameterModel.handleParameter(customHeaders, + HeaderParam.class, + args[parameterModel.getParamPosition()])); + + MultivaluedMap inbound = new MultivaluedHashMap<>(); + HeadersContext.get().ifPresent(headersContext -> inbound.putAll(headersContext.inboundHeaders())); + + AtomicReference> toReturn = new AtomicReference<>(customHeaders); + interfaceModel.getClientHeadersFactory().ifPresent(clientHeadersFactory -> toReturn + .set(clientHeadersFactory.update(inbound, customHeaders))); + return toReturn.get(); + } + + private MultivaluedMap createMultivaluedHeadersMap(List clientHeaders) { + MultivaluedMap customHeaders = new MultivaluedHashMap<>(); + for (ClientHeaderParamModel clientHeaderParamModel : clientHeaders) { + if (clientHeaderParamModel.getComputeMethod() == null) { + customHeaders + .put(clientHeaderParamModel.getHeaderName(), Arrays.asList(clientHeaderParamModel.getHeaderValue())); + } else { + try { + Method method = clientHeaderParamModel.getComputeMethod(); + if (method.isDefault()) { + //method is interface default + //we need to create instance of the interface to be able to call default method + T instance = (T) ReflectionUtil.createProxyInstance(interfaceModel.getRestClientClass()); + if (method.getParameterCount() > 0) { + customHeaders.put(clientHeaderParamModel.getHeaderName(), + createList(method.invoke(instance, clientHeaderParamModel.getHeaderName()))); + } else { + customHeaders.put(clientHeaderParamModel.getHeaderName(), + createList(method.invoke(instance, null))); + } + } else { + //Method is static + if (method.getParameterCount() > 0) { + customHeaders.put(clientHeaderParamModel.getHeaderName(), + createList(method.invoke(null, clientHeaderParamModel.getHeaderName()))); + } else { + customHeaders.put(clientHeaderParamModel.getHeaderName(), + createList(method.invoke(null, null))); + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + if (clientHeaderParamModel.isRequired()) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw new RuntimeException(e.getCause()); + } + } + } + } + return customHeaders; + } + + private static List createList(Object value) { + if (value instanceof String[]) { + String[] array = (String[]) value; + return Arrays.asList(array); + } + String s = (String) value; + return Collections.singletonList(s); + } + + /** + * Evaluation of {@link Response} if it is applicable for any of the registered {@link ResponseExceptionMapper} providers. + * + * @param response obtained response + * @param method called method + */ + void evaluateResponse(Response response, Method method) { + ResponseExceptionMapper lowestMapper = null; + Throwable throwable = null; + for (ResponseExceptionMapper responseExceptionMapper : interfaceModel.getResponseExceptionMappers()) { + if (responseExceptionMapper.handles(response.getStatus(), response.getHeaders())) { + if (lowestMapper == null + || throwable == null + || lowestMapper.getPriority() > responseExceptionMapper.getPriority()) { + lowestMapper = responseExceptionMapper; + Throwable tmp = lowestMapper.toThrowable(response); + if (tmp != null) { + throwable = tmp; + } + } + } + } + if (throwable != null) { + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } else if (throwable instanceof Error) { + throw (Error) throwable; + } + for (Class exception : method.getExceptionTypes()) { + if (throwable.getClass().isAssignableFrom(exception)) { + throw new WebApplicationException(throwable); + } + } + } + } + + private static String parseHttpMethod(InterfaceModel classModel, Method method) { + List> httpAnnotations = InterfaceUtil.getHttpAnnotations(method); + if (httpAnnotations.size() > 1) { + throw new RestClientDefinitionException("Method can't have more then one annotation of @HttpMethod type. " + + "See " + classModel.getRestClientClass().getName() + + "::" + method.getName()); + } else if (httpAnnotations.isEmpty()) { + //Sub resource method + return ""; + } + return httpAnnotations.get(0).getSimpleName(); + } + + private static List parameterModels(InterfaceModel classModel, Method method) { + ArrayList parameterModels = new ArrayList<>(); + final List jerseyParameters = org.glassfish.jersey.model.Parameter + .create(classModel.getRestClientClass(), classModel.getRestClientClass(), + method, false); + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + parameterModels.add(ParamModel.from(classModel, parameters[i].getType(), parameters[i], jerseyParameters.get(i), i)); + } + return parameterModels; + } + + private static class Builder { + + private final InterfaceModel interfaceModel; + private final Method method; + + private Class returnType; + private String httpMethod; + private String pathValue; + private String[] produces; + private String[] consumes; + private List parameterModels; + private List clientHeaders; + private List invocationInterceptors; + + private Builder(InterfaceModel interfaceModel, Method method) { + this.interfaceModel = interfaceModel; + this.method = method; + filterAllInterceptorAnnotations(); + } + + private void filterAllInterceptorAnnotations() { + invocationInterceptors = new ArrayList<>(); + try { + if (CDI.current() != null) { + Set interceptorAnnotations = new HashSet<>(); + BeanManager beanManager = CDI.current().getBeanManager(); + for (Annotation annotation : method.getAnnotations()) { + if (beanManager.isInterceptorBinding(annotation.annotationType())) { + interceptorAnnotations.add(annotation); + } + } + interceptorAnnotations.addAll(interfaceModel.getInterceptorAnnotations()); + Annotation[] allInterceptorAnnotations = interceptorAnnotations.toArray(new Annotation[0]); + if (allInterceptorAnnotations.length == 0) { + return; + } + List> interceptors = beanManager.resolveInterceptors(InterceptionType.AROUND_INVOKE, + allInterceptorAnnotations); + if (!interceptors.isEmpty()) { + for (Interceptor interceptor : interceptors) { + Object interceptorInstance = beanManager.getReference(interceptor, + interceptor.getBeanClass(), + interfaceModel.getCreationalContext()); + invocationInterceptors.add(new InterceptorInvocationContext + .InvocationInterceptor(interceptorInstance, + interceptor)); + } + } + } + } catch (IllegalStateException ignored) { + //CDI not present. Ignore. + } + } + + /** + * Return type of the method. + * + * @param returnType Method return type + * @return updated Builder instance + */ + Builder returnType(Type returnType) { + if (returnType instanceof ParameterizedType) { + this.returnType = (Class) ((ParameterizedType) returnType).getActualTypeArguments()[0]; + } else { + this.returnType = (Class) returnType; + } + return this; + } + + /** + * HTTP method of the method. + * + * @param httpMethod HTTP method of the method + * @return updated Builder instance + */ + Builder httpMethod(String httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + /** + * Path value from {@link Path} annotation. If annotation is null, empty String is set as path. + * + * @param path {@link Path} annotation + * @return updated Builder instance + */ + Builder pathValue(Path path) { + this.pathValue = path != null ? path.value() : ""; + //if only / is added to path like this "localhost:80/test" it makes invalid path "localhost:80/test/" + this.pathValue = pathValue.equals("/") ? "" : pathValue; + return this; + } + + /** + * Extracts MediaTypes from {@link Produces} annotation. + * If annotation is null, value from {@link InterfaceModel} is set. + * + * @param produces {@link Produces} annotation + * @return updated Builder instance + */ + Builder produces(Produces produces) { + this.produces = produces == null ? interfaceModel.getProduces() : produces.value(); + return this; + } + + /** + * Extracts MediaTypes from {@link Consumes} annotation. + * If annotation is null, value from {@link InterfaceModel} is set. + * + * @param consumes {@link Consumes} annotation + * @return updated Builder instance + */ + Builder consumes(Consumes consumes) { + this.consumes = consumes == null ? interfaceModel.getConsumes() : consumes.value(); + return this; + } + + /** + * {@link List} of transformed method parameters. + * + * @param parameterModels {@link List} of parameters + * @return updated Builder instance + */ + Builder parameters(List parameterModels) { + this.parameterModels = parameterModels; + return this; + } + + /** + * Process data from {@link ClientHeaderParam} annotation to extract methods and values. + * + * @param clientHeaderParams {@link ClientHeaderParam} annotations + * @return updated Builder instance + */ + Builder clientHeaders(ClientHeaderParam[] clientHeaderParams) { + clientHeaders = Arrays.stream(clientHeaderParams) + .map(clientHeaderParam -> new ClientHeaderParamModel(interfaceModel.getRestClientClass(), clientHeaderParam)) + .collect(Collectors.toList()); + return this; + } + + /** + * Creates new MethodModel instance. + * + * @return new instance + */ + MethodModel build() { + validateParameters(); + validateHeaderDuplicityNames(); + Optional entity = parameterModels.stream() + .filter(ParamModel::isEntity) + .findFirst(); + if (JsonValue.class.isAssignableFrom(returnType) + || ( + entity.isPresent() && entity.get().getType() instanceof Class + && JsonValue.class.isAssignableFrom((Class) entity.get().getType()))) { + this.consumes = new String[] {MediaType.APPLICATION_JSON}; + } + return new MethodModel(this); + } + + private void validateParameters() { + UriBuilder uriBuilder = UriBuilder.fromUri(interfaceModel.getPath()).path(pathValue); + List parameters = InterfaceUtil.parseParameters(uriBuilder.toTemplate()); + List methodPathParameters = new ArrayList<>(); + List pathHandlingParams = parameterModels.stream() + .filter(parameterModel -> parameterModel.handles(PathParam.class)) + .collect(Collectors.toList()); + for (ParamModel paramModel : pathHandlingParams) { + if (paramModel instanceof PathParamModel) { + methodPathParameters.add(((PathParamModel) paramModel).getPathParamName()); + } else if (paramModel instanceof BeanParamModel) { + for (ParamModel beanPathParams : ((BeanParamModel) paramModel).getAllParamsWithType(PathParam.class)) { + methodPathParameters.add(((PathParamModel) beanPathParams).getPathParamName()); + } + } + } + for (String parameterName : methodPathParameters) { + if (!parameters.contains(parameterName)) { + throw new RestClientDefinitionException("Parameter name " + parameterName + " on " + + interfaceModel.getRestClientClass().getName() + + "::" + method.getName() + + " doesn't match any @Path variable name."); + } + parameters.remove(parameterName); + } + if (!parameters.isEmpty()) { + throw new RestClientDefinitionException("Some variable names does not have matching @PathParam " + + "defined on method " + interfaceModel.getRestClientClass() + .getName() + + "::" + method.getName()); + } + List entities = parameterModels.stream() + .filter(ParamModel::isEntity) + .collect(Collectors.toList()); + if (entities.size() > 1) { + throw new RestClientDefinitionException("You cant have more than 1 entity method parameter! Check " + + interfaceModel.getRestClientClass().getName() + + "::" + method.getName()); + } + } + + private void validateHeaderDuplicityNames() { + ArrayList names = new ArrayList<>(); + for (ClientHeaderParamModel clientHeaderParamModel : clientHeaders) { + String headerName = clientHeaderParamModel.getHeaderName(); + if (names.contains(headerName)) { + throw new RestClientDefinitionException("Header name cannot be registered more then once on the same target." + + "See " + interfaceModel.getRestClientClass().getName()); + } + names.add(headerName); + } + } + } +} 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 new file mode 100644 index 0000000000..800005d6ff --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ParamModel.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +import org.glassfish.jersey.model.Parameter; + +/** + * Abstract model for all elements with parameter annotation. + * + * @author David Kral + */ +abstract class ParamModel { + + protected final InterfaceModel interfaceModel; + protected final Parameter parameter; + private final Type type; + private final AnnotatedElement annotatedElement; + private final int paramPosition; + private final boolean entity; + + /** + * Processes parameter annotations and creates new instance of the model corresponding model. + * + * @param interfaceModel model of the interface + * @param type annotated element type + * @param annotatedElement annotated element + * @param position position in method params + * @return new parameter instance + */ + static ParamModel from(InterfaceModel interfaceModel, Type type, AnnotatedElement annotatedElement, + Parameter parameter, int position) { + return new Builder(interfaceModel, type, annotatedElement, parameter) + .pathParamName(annotatedElement.getAnnotation(PathParam.class)) + .headerParamName(annotatedElement.getAnnotation(HeaderParam.class)) + .beanParam(annotatedElement.getAnnotation(BeanParam.class)) + .cookieParam(annotatedElement.getAnnotation(CookieParam.class)) + .queryParam(annotatedElement.getAnnotation(QueryParam.class)) + .matrixParam(annotatedElement.getAnnotation(MatrixParam.class)) + .formParam(annotatedElement.getAnnotation(FormParam.class)) + .paramPosition(position) + .build(); + } + + ParamModel(Builder builder) { + this.interfaceModel = builder.interfaceModel; + this.type = builder.type; + this.annotatedElement = builder.annotatedElement; + this.entity = builder.entity; + this.paramPosition = builder.paramPosition; + this.parameter = builder.parameter; + } + + /** + * Returns {@link Type} of the parameter. + * + * @return parameter type + */ + Type getType() { + return type; + } + + /** + * Returns annotated element. + * + * @return annotated element + */ + AnnotatedElement getAnnotatedElement() { + return annotatedElement; + } + + int getParamPosition() { + return paramPosition; + } + + /** + * Returns value if parameter is entity or not. + * + * @return if parameter is entity + */ + boolean isEntity() { + return entity; + } + + /** + * Transforms parameter to be part of the request. + * + * @param requestPart part of a request + * @param annotationClass annotation type + * @param instance actual method parameter value + * @return updated request part + */ + abstract T handleParameter(T requestPart, Class annotationClass, Object instance); + + /** + * Evaluates if the annotation passed in parameter is supported by this parameter. + * + * @param annotation checked annotation + * @return if annotation is supported + */ + abstract boolean handles(Class annotation); + + protected static class Builder { + + private InterfaceModel interfaceModel; + private Type type; + private AnnotatedElement annotatedElement; + private Parameter parameter; + private String pathParamName; + private String headerParamName; + private String cookieParamName; + private String queryParamName; + private String matrixParamName; + private String formParamName; + private boolean beanParam; + private boolean entity; + private int paramPosition; + + private Builder(InterfaceModel interfaceModel, Type type, AnnotatedElement annotatedElement, Parameter parameter) { + this.interfaceModel = interfaceModel; + this.type = type; + this.annotatedElement = annotatedElement; + this.parameter = parameter; + } + + /** + * Path parameter name. + * + * @param pathParam {@link PathParam} annotation + * @return updated Builder instance + */ + Builder pathParamName(PathParam pathParam) { + this.pathParamName = pathParam == null ? null : pathParam.value(); + return this; + } + + /** + * Header parameter name. + * + * @param headerParam {@link HeaderParam} annotation + * @return updated Builder instance + */ + Builder headerParamName(HeaderParam headerParam) { + this.headerParamName = headerParam == null ? null : headerParam.value(); + return this; + } + + /** + * Bean parameter identifier. + * + * @param beanParam {@link BeanParam} annotation + * @return updated Builder instance + */ + Builder beanParam(BeanParam beanParam) { + this.beanParam = beanParam != null; + return this; + } + + /** + * Cookie parameter. + * + * @param cookieParam {@link CookieParam} annotation + * @return updated Builder instance + */ + Builder cookieParam(CookieParam cookieParam) { + this.cookieParamName = cookieParam == null ? null : cookieParam.value(); + return this; + } + + /** + * Query parameter. + * + * @param queryParam {@link QueryParam} annotation + * @return updated Builder instance + */ + Builder queryParam(QueryParam queryParam) { + this.queryParamName = queryParam == null ? null : queryParam.value(); + return this; + } + + /** + * Matrix parameter. + * + * @param matrixParam {@link MatrixParam} annotation + * @return updated Builder instance + */ + Builder matrixParam(MatrixParam matrixParam) { + this.matrixParamName = matrixParam == null ? null : matrixParam.value(); + return this; + } + + /** + * Form parameter. + * + * @param formParam {@link FormParam} annotation + * @return updated Builder instance + */ + Builder formParam(FormParam formParam) { + this.formParamName = formParam == null ? null : formParam.value(); + return this; + } + + /** + * Position of parameter in method parameters + * + * @param paramPosition Parameter position + * @return updated Builder instance + */ + Builder paramPosition(int paramPosition) { + this.paramPosition = paramPosition; + return this; + } + + /** + * Returns path param name; + * + * @return path param name + */ + String pathParamName() { + return pathParamName; + } + + /** + * Returns header param name; + * + * @return header param name + */ + String headerParamName() { + return headerParamName; + } + + String cookieParamName() { + return cookieParamName; + } + + String queryParamName() { + return queryParamName; + } + + String matrixParamName() { + return matrixParamName; + } + + String formParamName() { + return matrixParamName; + } + + /** + * Creates new ParamModel instance. + * + * @return new instance + */ + ParamModel build() { + if (pathParamName != null) { + return new PathParamModel(this); + } else if (headerParamName != null) { + return new HeaderParamModel(this); + } else if (beanParam) { + return new BeanParamModel(this); + } else if (cookieParamName != null) { + return new CookieParamModel(this); + } else if (queryParamName != null) { + return new QueryParamModel(this); + } else if (matrixParamName != null) { + return new MatrixParamModel(this); + } else if (formParamName != null) { + return new FormParamModel(this); + } + entity = true; + return new ParamModel(this) { + @Override + public Object handleParameter(Object requestPart, Class annotationClass, Object instance) { + return requestPart; + } + + @Override + 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 new file mode 100644 index 0000000000..4c8768ce60 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/PathParamModel.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; + +import javax.ws.rs.PathParam; +import javax.ws.rs.client.WebTarget; + +/** + * Contains information about method parameter or class field which is annotated by {@link PathParam}. + * + * @author David Kral + */ +class PathParamModel extends ParamModel { + + private final String pathParamName; + + PathParamModel(Builder builder) { + super(builder); + pathParamName = builder.pathParamName(); + } + + public String getPathParamName() { + return pathParamName; + } + + @Override + 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) { + 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 new file mode 100644 index 0000000000..2a2675e7a6 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ProxyInvocationHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +import javax.ws.rs.client.WebTarget; + +/** + * Invocation handler for interface proxy. + * + * @author David Kral + */ +class ProxyInvocationHandler implements InvocationHandler { + + private final WebTarget target; + private final RestClientModel restClientModel; + + ProxyInvocationHandler(WebTarget target, + RestClientModel restClientModel) { + this.target = target; + this.restClientModel = restClientModel; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if (method.getName().contains("toString") && (args == null || args.length == 0)) { + return restClientModel.toString(); + } + 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 new file mode 100644 index 0000000000..ce96099690 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/QueryParamModel.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.util.Map; + +import javax.ws.rs.QueryParam; + +/** + * Model which contains information about query parameter + * + * @author David Kral + */ +class QueryParamModel extends ParamModel> { + + private final String queryParamName; + + QueryParamModel(Builder builder) { + super(builder); + queryParamName = builder.queryParamName(); + } + + @Override + public Map handleParameter(Map requestPart, + Class annotationClass, + Object instance) { + Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); + if (resolvedValue instanceof Object[]) { + requestPart.put(queryParamName, (Object[]) resolvedValue); + } else { + requestPart.put(queryParamName, new Object[] {resolvedValue}); + } + return requestPart; + } + + @Override + 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 new file mode 100644 index 0000000000..b3115d6eaa --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Proxy; + +/** + * Created by David Kral. + */ +class ReflectionUtil { + + static T createInstance(Class tClass) { + try { + return tClass.newInstance(); + } catch (Throwable t) { + throw new RuntimeException("No default constructor in class " + tClass + " present. Class cannot be created!", t); + } + } + + @SuppressWarnings("unchecked") + static T createProxyInstance(Class restClientClass) { + return (T) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + new Class[] {restClientClass}, + (proxy, m, args) -> { + Constructor constructor = MethodHandles.Lookup.class + .getDeclaredConstructor(Class.class); + constructor.setAccessible(true); + return constructor.newInstance(restClientClass) + .in(restClientClass) + .unreflectSpecial(m, restClientClass) + .bindTo(proxy) + .invokeWithArguments(args); + }); + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RequestHeaderAutoDiscoverable.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RequestHeaderAutoDiscoverable.java new file mode 100644 index 0000000000..bcda814e89 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RequestHeaderAutoDiscoverable.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 Oracle 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 javax.ws.rs.ConstrainedTo; +import javax.ws.rs.RuntimeType; +import javax.ws.rs.core.FeatureContext; + +import org.glassfish.jersey.internal.spi.AutoDiscoverable; + +/** + * Auto discoverable feature to bind into jersey runtime. + */ +@ConstrainedTo(RuntimeType.SERVER) +public class RequestHeaderAutoDiscoverable implements AutoDiscoverable { + @Override + public void configure(FeatureContext context) { + if (!context.getConfiguration().isRegistered(HeadersRequestFilter.class)) { + context.register(HeadersRequestFilter.class); + } + } +} 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 new file mode 100644 index 0000000000..44068f0f6d --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.reflect.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.AccessController; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +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 java.util.stream.Collectors; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Configuration; +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.ResponseExceptionMapper; +import org.eclipse.microprofile.rest.client.spi.RestClientListener; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.glassfish.jersey.client.JerseyWebTarget; +import org.glassfish.jersey.internal.util.ReflectionHelper; + +/** + * Rest client builder implementation. Creates proxy instance of requested interface. + * + * @author David Kral + */ +public 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 List asyncInterceptorFactories; + private final Config config; + private final ConfigWrapper configWrapper; + private URI uri; + private JerseyClientBuilder jerseyClientBuilder; + private Supplier executorService; + + RestClientBuilderImpl() { + jerseyClientBuilder = new JerseyClientBuilder(); + responseExceptionMappers = new HashSet<>(); + paramConverterProviders = new HashSet<>(); + asyncInterceptorFactories = new ArrayList<>(); + config = ConfigProvider.getConfig(); + configWrapper = new ConfigWrapper(jerseyClientBuilder.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) { + jerseyClientBuilder.connectTimeout(timeout, unit); + return this; + } + + @Override + public RestClientBuilder readTimeout(long timeout, TimeUnit unit) { + jerseyClientBuilder.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 { + + if (uri == null) { + throw new IllegalStateException("Base uri/url cannot be null!"); + } + + //Provider registration part + Object providersFromJerseyConfig = jerseyClientBuilder.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); + if (providersFromConfig.isPresent() && !providersFromConfig.get().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()); + } + + for (RestClientListener restClientListener : ServiceLoader.load(RestClientListener.class)) { + restClientListener.onNewClient(interfaceClass, this); + } + + //We need to check first if default exception mapper was not disabled by property on builder. + Object disableDefaultMapperJersey = jerseyClientBuilder.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()); + } + } + + //AsyncInterceptors initialization + List asyncInterceptors = asyncInterceptorFactories.stream() + .map(AsyncInvocationInterceptorFactory::newInterceptor) + .collect(Collectors.toList()); + asyncInterceptors.forEach(AsyncInvocationInterceptor::prepareContext); + + jerseyClientBuilder.executorService(new ExecutorServiceWrapper(executorService.get(), asyncInterceptors)); + + JerseyClient client = jerseyClientBuilder.build(); + client.preInitialize(); + JerseyWebTarget webTarget = client.target(this.uri); + + RestClientModel restClientModel = RestClientModel.from(interfaceClass, + responseExceptionMappers, + paramConverterProviders, + asyncInterceptors, + webTarget.getInjectionManager()); + + + return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), + new Class[] {interfaceClass}, + new ProxyInvocationHandler(webTarget, restClientModel) + ); + } + + 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 = jerseyClientBuilder.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; + } + 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) { + jerseyClientBuilder.property(name, value); + return this; + } + + @Override + public RestClientBuilder register(Class aClass) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass)); + } else { + jerseyClientBuilder.register(aClass); + } + return this; + } + + @Override + public RestClientBuilder register(Class aClass, int i) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), i); + } else { + jerseyClientBuilder.register(aClass, i); + } + return this; + } + + @Override + public RestClientBuilder register(Class aClass, Class... classes) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), classes); + } else { + jerseyClientBuilder.register(aClass, classes); + } + return this; + } + + @Override + public RestClientBuilder register(Class aClass, Map, Integer> map) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), map); + } else { + jerseyClientBuilder.register(aClass, map); + } + return this; + } + + @Override + public RestClientBuilder register(Object o) { + if (o instanceof ResponseExceptionMapper) { + ResponseExceptionMapper mapper = (ResponseExceptionMapper) o; + registerCustomProvider(o, -1); + jerseyClientBuilder.register(mapper, mapper.getPriority()); + } else { + jerseyClientBuilder.register(o); + registerCustomProvider(o, -1); + } + return this; + } + + @Override + public RestClientBuilder register(Object o, int i) { + jerseyClientBuilder.register(o, i); + registerCustomProvider(o, i); + return this; + } + + @Override + public RestClientBuilder register(Object o, Class... classes) { + for (Class clazz : classes) { + if (isSupportedCustomProvider(clazz)) { + register(o); + } + } + jerseyClientBuilder.register(o, classes); + return this; + } + + @Override + public RestClientBuilder register(Object o, Map, Integer> map) { + if (isSupportedCustomProvider(o.getClass())) { + if (o instanceof ResponseExceptionMapper) { + registerCustomProvider(o, map.get(ResponseExceptionMapper.class)); + } else if (o instanceof ParamConverterProvider) { + registerCustomProvider(o, map.get(ParamConverterProvider.class)); + } + } + jerseyClientBuilder.register(o, map); + return this; + } + + private boolean isSupportedCustomProvider(Class providerClass) { + return ResponseExceptionMapper.class.isAssignableFrom(providerClass) + || ParamConverterProvider.class.isAssignableFrom(providerClass) + || AsyncInvocationInterceptorFactory.class.isAssignableFrom(providerClass); + } + + private void registerCustomProvider(Object instance, int 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((AsyncInvocationInterceptorFactory) instance); + } + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientExtension.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientExtension.java new file mode 100644 index 0000000000..05743a2eb0 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientExtension.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.HashSet; +import java.util.Set; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.DeploymentException; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.ProcessInjectionPoint; +import javax.enterprise.inject.spi.WithAnnotations; +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Filters out all interfaces annotated with {@link RegisterRestClient} + * and creates new Producer from each of these selected interfaces. + * + * Also adds support for injection of rest client instances to fields + * without {@link RestClient} annotation. + * + * @author David Kral + */ +public class RestClientExtension implements Extension { + + private Set> interfaces = new HashSet<>(); + + /** + * Filters out all interfaces annotated with {@link RegisterRestClient} annotation and + * adds them to the collection for further processing. + * + * @param processAnnotatedType filtered annotated types + */ + public void collectClientRegistrations(@Observes + @WithAnnotations({RegisterRestClient.class}) + ProcessAnnotatedType processAnnotatedType) { + Class typeDef = processAnnotatedType.getAnnotatedType().getJavaClass(); + if (typeDef.isInterface()) { + interfaces.add(typeDef); + } else { + throw new DeploymentException("RegisterRestClient annotation has to be on interface! " + typeDef + " is not " + + "interface."); + } + } + + /** + * Iterates over all {@link ProcessInjectionPoint} to find only those annotated + * with {@link RestClient} and configures their proper injection. + * + * @param pip processed injection point + */ + public void collectClientProducer(@Observes ProcessInjectionPoint pip) { + RestClient restClient = pip.getInjectionPoint().getAnnotated().getAnnotation(RestClient.class); + if (restClient != null) { + InjectionPoint ip = pip.getInjectionPoint(); + Class type = (Class) ip.getType(); + + RestClientLiteral q = new RestClientLiteral(type); + + pip.configureInjectionPoint().addQualifier(q); + } + } + + /** + * Creates new producers based on collected interfaces. + * + * @param abd after bean discovery instance + * @param bm bean manager instance + */ + public void restClientRegistration(@Observes AfterBeanDiscovery abd, BeanManager bm) { + interfaces.forEach(type -> abd.addBean(new RestClientProducer(new RestClientLiteral(type), type, bm))); + interfaces.forEach(type -> abd.addBean(new RestClientProducer(null, type, bm))); + } + + @Qualifier + @Retention(RUNTIME) + @Target({METHOD, FIELD}) + @interface MpRestClientQualifier { + + Class interfaceType(); + + } + + private static class RestClientLiteral extends AnnotationLiteral implements MpRestClientQualifier { + + private final Class interfaceType; + + RestClientLiteral(Class interfaceType) { + this.interfaceType = interfaceType; + } + + @Override + public Class interfaceType() { + return interfaceType; + } + + @Override + public String toString() { + return "RestClientLiteral{" + + "interfaceType=" + interfaceType + + '}'; + } + + } + +} diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientModel.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientModel.java new file mode 100644 index 0000000000..49d353cef3 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientModel.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.glassfish.jersey.internal.inject.InjectionManager; + +/** + * Model of the rest client interface. + * + * @author David Kral + */ +class RestClientModel { + + private final InterfaceModel interfaceModel; + private final Map methodModels; + + /** + * Creates new instance of the {@link RestClientModel} base on interface class. + * + * @param restClientClass rest client interface + * @param responseExceptionMappers registered exception mappers + * @param paramConverterProviders registered param converters + * @param asyncInterceptors registered async interceptor factories + * @param injectionManager + * @return new instance + */ + static RestClientModel from(Class restClientClass, + Set responseExceptionMappers, + Set paramConverterProviders, + List asyncInterceptors, + InjectionManager injectionManager) { + InterfaceModel interfaceModel = InterfaceModel.from(restClientClass, + responseExceptionMappers, + paramConverterProviders, + asyncInterceptors, + injectionManager); + return new Builder() + .interfaceModel(interfaceModel) + .methodModels(parseMethodModels(interfaceModel)) + .build(); + } + + private RestClientModel(Builder builder) { + this.interfaceModel = builder.classModel; + this.methodModels = builder.methodModels; + } + + /** + * Invokes desired rest client method. + * + * @param baseWebTarget path to endpoint + * @param method desired method + * @param args actual method parameters + * @return method return value + */ + Object invokeMethod(WebTarget baseWebTarget, Method method, Object[] args) { + WebTarget classLevelTarget = baseWebTarget.path(interfaceModel.getPath()); + MethodModel methodModel = methodModels.get(method); + if (methodModel != null) { + return new InterceptorInvocationContext(classLevelTarget, methodModel, method, args).proceed(); + } + try { + if (method.isDefault()) { + T instance = (T) ReflectionUtil.createProxyInstance(interfaceModel.getRestClientClass()); + return method.invoke(instance, args); + } else { + throw new UnsupportedOperationException("This method is not supported!"); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Map parseMethodModels(InterfaceModel classModel) { + Map methodMap = new HashMap<>(); + for (Method method : classModel.getRestClientClass().getMethods()) { + if (method.isDefault() || Modifier.isStatic(method.getModifiers())) { + continue; + } + //Skip method processing if method does not have HTTP annotation + //and is not sub resource (does not have Path annotation) + methodMap.put(method, MethodModel.from(classModel, method)); + } + return methodMap; + } + + private static class Builder { + + private InterfaceModel classModel; + private Map methodModels; + + private Builder() { + } + + /** + * Rest client class converted to {@link InterfaceModel} + * + * @param classModel {@link InterfaceModel} instance + * @return Updated Builder instance + */ + Builder interfaceModel(InterfaceModel classModel) { + this.classModel = classModel; + return this; + } + + /** + * Rest client class methods converted to {@link Map} of {@link MethodModel} + * + * @param methodModels Method models + * @return Updated Builder instance + */ + Builder methodModels(Map methodModels) { + this.methodModels = methodModels; + return this; + } + + /** + * Creates new RestClientModel instance. + * + * @return new instance + */ + public RestClientModel build() { + return new RestClientModel(this); + } + } + + @Override + public String toString() { + return interfaceModel.getRestClientClass().getName(); + } +} 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 new file mode 100644 index 0000000000..7c0c4b3d8b --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientProducer.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2019 Oracle 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.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.enterprise.context.Dependent; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.DeploymentException; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.PassivationCapable; + +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.inject.RegisterRestClient; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.glassfish.jersey.internal.util.ReflectionHelper; + +/** + * Handles proper rest client injection. + * + * Contains information about the rest client interface and extracts additional parameters from + * config. + * + * @author David Kral + */ +class RestClientProducer implements Bean, PassivationCapable { + + private static final String CONFIG_URL = "/mp-rest/url"; + private static final String CONFIG_URI = "/mp-rest/uri"; + 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 final RestClientExtension.MpRestClientQualifier qualifier; + private final BeanManager beanManager; + private final Class interfaceType; + private final Class scope; + private final Config config; + private final String baseUrl; + + /** + * Creates new instance of RestClientProducer. + * + * @param qualifier qualifier which defines rest client interface + * @param interfaceType rest client interface + * @param beanManager bean manager + */ + RestClientProducer(RestClientExtension.MpRestClientQualifier qualifier, + Class interfaceType, + BeanManager beanManager) { + this.qualifier = qualifier; + 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."); + } + )); + } + + @Override + public Class getBeanClass() { + return interfaceType; + } + + @Override + public Set getInjectionPoints() { + return Collections.emptySet(); + } + + @Override + public boolean isNullable() { + return false; + } + + @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: " + baseUrl); + } + } + + @Override + public void destroy(Object instance, CreationalContext creationalContext) { + } + + @Override + public Set getTypes() { + return Collections.singleton(interfaceType); + } + + @Override + public Set getQualifiers() { + if (qualifier == null) { + return Collections.singleton(Default.Literal.INSTANCE); + } + Set annotations = new HashSet<>(); + annotations.add(qualifier); + annotations.add(RestClient.LITERAL); + return annotations; + } + + @Override + public Class getScope() { + return scope; + } + + @Override + public String getName() { + if (qualifier == null) { + return interfaceType.getName() + "RestClient"; + } + return interfaceType.getName(); + } + + @Override + public Set> getStereotypes() { + return Collections.emptySet(); + } + + @Override + public boolean isAlternative() { + return false; + } + + @Override + public String toString() { + return "RestClientProducer [ interfaceType: " + interfaceType.getSimpleName() + + " ] with Qualifiers [" + getQualifiers() + "]"; + } + + @Override + 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)); + if (scope == null) { + throw new IllegalStateException("Invalid scope from config: " + configScope); + } + return scope; + } + List possibleScopes = Arrays.stream(interfaceType.getDeclaredAnnotations()) + .filter(annotation -> beanManager.isScope(annotation.annotationType())) + .collect(Collectors.toList()); + + if (possibleScopes.size() == 1) { + return possibleScopes.get(0).annotationType(); + } else if (possibleScopes.isEmpty()) { + return Dependent.class; + } else { + throw new IllegalArgumentException("Client should have only one scope defined: " + + interfaceType + " has " + possibleScopes); + } + } +} diff --git a/ext/mp-rest-client/src/main/resources/META-INF/beans.xml b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/beans.xml similarity index 100% rename from ext/mp-rest-client/src/main/resources/META-INF/beans.xml rename to ext/microprofile/mp-rest-client/src/main/resources/META-INF/beans.xml diff --git a/ext/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension similarity index 91% rename from ext/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension rename to ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension index 69afceaa9c..bca39a90b6 100644 --- a/ext/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -14,4 +14,4 @@ # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # -org.glassfish.jersey.restclient.RestClientExtension \ No newline at end of file +org.glassfish.jersey.microprofile.restclient.RestClientExtension \ No newline at end of file diff --git a/ext/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver similarity index 90% rename from ext/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver rename to ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver index 00848c31c5..e23142fa5d 100644 --- a/ext/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver @@ -14,4 +14,4 @@ # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # -org.glassfish.jersey.restclient.JerseyRestClientBuilderResolver \ No newline at end of file +org.glassfish.jersey.microprofile.restclient.JerseyRestClientBuilderResolver \ No newline at end of file diff --git a/ext/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable similarity index 90% rename from ext/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable rename to ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable index dbe5f04ca4..d9a1262c7d 100644 --- a/ext/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable @@ -14,4 +14,4 @@ # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # -org.glassfish.jersey.restclient.RequestHeaderAutoDiscoverable \ No newline at end of file +org.glassfish.jersey.microprofile.restclient.RequestHeaderAutoDiscoverable \ No newline at end of file diff --git a/ext/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java similarity index 100% rename from ext/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java rename to ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java diff --git a/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResource.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResource.java new file mode 100644 index 0000000000..caf3bb650e --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResource.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Oracle 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 javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +/** + * Created by David Kral. + */ + +@Path("resource") +public interface ApplicationResource { + + @GET + String getValue(); + + @POST + String postAppendValue(String value); + + default String sayHi() { + return "Hi"; + } + + +} diff --git a/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResourceImpl.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResourceImpl.java new file mode 100644 index 0000000000..4194b40e8c --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/ApplicationResourceImpl.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Oracle 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; + +/** + * Created by David Kral. + */ +public class ApplicationResourceImpl implements ApplicationResource { + @Override + public String getValue() { + return "This is default value!"; + } + + @Override + public String postAppendValue(String value) { + return null; + } +} diff --git a/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/CorrectInterface.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/CorrectInterface.java new file mode 100644 index 0000000000..efe6c15717 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/CorrectInterface.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019 Oracle 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 javax.ws.rs.BeanParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; + +/** + * Correct test interface for validation + * + * @author David Kral + */ + +@Path("test/{first}") +public interface CorrectInterface { + + @GET + @Path("{second}") + @ClientHeaderParam(name = "test", value = "someValue") + void firstMethod(@PathParam("first") String first, @PathParam("second") String second); + + @GET + @ClientHeaderParam(name = "test", value = "{value}") + void secondMethod(@PathParam("first") String first, String second); + + @POST + @ClientHeaderParam(name = "test", value = "org.glassfish.jersey.restclient.CustomHeaderGenerator.customHeader") + void thirdMethod(@PathParam("first") String first); + + @GET + @Path("{second}") + void fourthMethod(@PathParam("first") String first, @BeanParam BeanWithPathParam second); + + default String value() { + return "testValue"; + } + + class CustomHeaderGenerator { + + public static String customHeader() { + return "static"; + } + + } + + class BeanWithPathParam { + + @PathParam("second") + public String pathParam; + + } + +} diff --git a/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/InterfaceValidationTest.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/InterfaceValidationTest.java new file mode 100644 index 0000000000..a8103994b0 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/InterfaceValidationTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019 Oracle 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.util.ArrayList; +import java.util.HashSet; + +import org.junit.Test; + +/** + * @author David Kral + */ +public class InterfaceValidationTest { + + @Test + public void testValidInterface() { + RestClientModel.from(CorrectInterface.class, new HashSet<>(), new HashSet<>(), new ArrayList<>(), + null); + } + +} diff --git a/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/RestClientModelTest.java b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/RestClientModelTest.java new file mode 100644 index 0000000000..fcfe911111 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/RestClientModelTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 Oracle 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.net.URI; +import java.net.URISyntaxException; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + + +/** + * Created by David Kral. + */ +public class RestClientModelTest extends JerseyTest { + @Override + protected ResourceConfig configure() { + enable(TestProperties.LOG_TRAFFIC); + return new ResourceConfig(ApplicationResourceImpl.class); + } + + @Test + public void testGetIt() throws URISyntaxException { + ApplicationResource app = RestClientBuilder.newBuilder() + .baseUri(new URI("http://localhost:9998")) + .build(ApplicationResource.class); + System.out.println(app.getValue()); + System.out.println(app.sayHi()); + } +} diff --git a/ext/mp-rest-client/src/test/resources/arquillian.xml b/ext/microprofile/mp-rest-client/src/test/resources/arquillian.xml similarity index 100% rename from ext/mp-rest-client/src/test/resources/arquillian.xml rename to ext/microprofile/mp-rest-client/src/test/resources/arquillian.xml diff --git a/ext/microprofile/mp-rest-client/src/test/resources/server.policy b/ext/microprofile/mp-rest-client/src/test/resources/server.policy new file mode 100644 index 0000000000..edff3db1a7 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/resources/server.policy @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 Oracle 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 + */ + +grant { + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + permission java.lang.RuntimePermission "loadLibrary.C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "createClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + permission java.lang.RuntimePermission "getProtectionDomain"; + permission java.lang.RuntimePermission "getenv.*"; + permission java.lang.RuntimePermission "setIO"; + permission java.io.FilePermission "C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll", "read"; + permission java.util.PropertyPermission "idea.launcher.port", "read"; + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + permission java.util.PropertyPermission "jcommander.debug", "read"; + permission java.util.PropertyPermission "user.dir", "read"; + permission java.util.PropertyPermission "debug", "read"; + permission java.util.PropertyPermission "arquillian.debug", "read"; + permission java.util.PropertyPermission "trace", "read"; + permission java.util.PropertyPermission "testng.test.classpath", "read"; + permission java.util.PropertyPermission "localscoping", "read"; + permission java.util.PropertyPermission "outfile", "read"; + permission java.util.PropertyPermission "org.eclipse.microprofile.*", "read"; + permission java.util.PropertyPermission "arquillian.xml", "read"; + permission java.util.PropertyPermission "*", "read,write"; + permission java.net.SocketPermission "127.0.0.1:*", "connect,resolve"; + permission java.net.SocketPermission "*", "connect,resolve"; + permission java.io.FilePermission "<>", "read,write,delete"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.net.NetPermission "getProxySelector"; + permission java.net.NetPermission "specifyStreamHandler"; + permission java.lang.management.ManagementPermission "monitor"; +}; + +grant codebase "file:${java.home}/-" { + permission java.security.AllPermission; +}; + +grant codebase "file:${settings.localRepository}/-" { + permission java.security.AllPermission; +}; + +grant codebase "file:${project.build.directory}/test-classes/-" { + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + permission java.lang.RuntimePermission "loadLibrary.C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll"; + permission java.io.FilePermission "C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll", "read"; + permission java.util.PropertyPermission "idea.launcher.port", "read"; + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + + permission java.io.FilePermission "<>", "read,write,delete"; + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +grant codebase "file:${project.build.directory}/classes/-" { + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + permission java.lang.RuntimePermission "loadLibrary.C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll"; + permission java.io.FilePermission "C:\\Program Files\\JetBrains\\IntelliJ IDEA 2019.1\\bin\\breakgen64.dll", "read"; + permission java.util.PropertyPermission "idea.launcher.port", "read"; + permission java.util.PropertyPermission "idea.launcher.bin.path", "read"; + + permission java.io.FilePermission "<>", "read,write,delete"; + permission java.net.SocketPermission "*", "connect,resolve"; +}; diff --git a/ext/mp-rest-client/tck-suite.xml b/ext/microprofile/mp-rest-client/tck-suite.xml similarity index 80% rename from ext/mp-rest-client/tck-suite.xml rename to ext/microprofile/mp-rest-client/tck-suite.xml index 5cce0d2018..3106d69ee5 100644 --- a/ext/mp-rest-client/tck-suite.xml +++ b/ext/microprofile/mp-rest-client/tck-suite.xml @@ -30,6 +30,13 @@ + + + + + + + \ No newline at end of file diff --git a/ext/microprofile/pom.xml b/ext/microprofile/pom.xml new file mode 100644 index 0000000000..7a50ddf493 --- /dev/null +++ b/ext/microprofile/pom.xml @@ -0,0 +1,20 @@ + + + + project + org.glassfish.jersey.ext + 2.29-SNAPSHOT + + 4.0.0 + + jersey-microprofile + pom + + + mp-rest-client + + + + \ No newline at end of file diff --git a/ext/pom.xml b/ext/pom.xml index 35470a79fa..ddf82387b4 100644 --- a/ext/pom.xml +++ b/ext/pom.xml @@ -54,7 +54,7 @@ servlet-portability spring4 wadl-doclet - mp-rest-client + microprofile