Rest client implementation Signed-off-by: David Kral <david.k.kral@oracle.com>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java index c7d54be..26df84f 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 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 @@ -26,6 +26,7 @@ import javax.ws.rs.core.UriBuilder; import org.glassfish.jersey.internal.guava.Preconditions; +import org.glassfish.jersey.internal.inject.InjectionManager; /** * Jersey implementation of {@link javax.ws.rs.client.WebTarget JAX-RS client target}
diff --git a/ext/microprofile/mp-rest-client/pom.xml b/ext/microprofile/mp-rest-client/pom.xml new file mode 100644 index 0000000..dc47382 --- /dev/null +++ b/ext/microprofile/mp-rest-client/pom.xml
@@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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 + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + 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"> + <parent> + <artifactId>jersey-microprofile</artifactId> + <groupId>org.glassfish.jersey.ext</groupId> + <version>2.29-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>jersey-mp-rest-client</artifactId> + + <dependencies> + <dependency> + <groupId>org.eclipse.microprofile.rest.client</groupId> + <artifactId>microprofile-rest-client-api</artifactId> + <version>1.2.1</version> + </dependency> + <dependency> + <groupId>org.eclipse.microprofile.config</groupId> + <artifactId>microprofile-config-api</artifactId> + <version>1.3</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.inject</groupId> + <artifactId>jersey-hk2</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-binding</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>jakarta.json</groupId> + <artifactId>jakarta.json-api</artifactId> + <version>1.1.5</version> + </dependency> + <dependency> + <groupId>javax.enterprise</groupId> + <artifactId>cdi-api</artifactId> + <version>2.0</version> + </dependency> + <dependency> + <groupId>org.glassfish</groupId> + <artifactId>jsonp-jaxrs</artifactId> + </dependency> + <dependency> + <groupId>org.jboss.weld.se</groupId> + <artifactId>weld-se-core</artifactId> + <version>3.0.3.Final</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework</groupId> + <artifactId>jersey-test-framework-core</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-bundle</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.smallrye</groupId> + <artifactId>smallrye-config</artifactId> + <version>1.3.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.microprofile.rest.client</groupId> + <artifactId>microprofile-rest-client-tck</artifactId> + <version>1.2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock</artifactId> + <version>2.21.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jboss.arquillian.testng</groupId> + <artifactId>arquillian-testng-container</artifactId> + <version>1.4.1.Final</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jboss.arquillian.container</groupId> + <artifactId>arquillian-container-test-spi</artifactId> + <version>1.4.1.Final</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.jboss.arquillian.container</groupId> + <artifactId>arquillian-weld-embedded</artifactId> + <version>2.0.1.Final</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.opentracing</groupId> + <artifactId>opentracing-noop</artifactId> + <version>0.30.0</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <!--<parallel>classes</parallel> + <perCoreThreadCount>true</perCoreThreadCount> + <threadCount>1</threadCount> + <forkCount>1C</forkCount> + <reuseForks>true</reuseForks>--> + <suiteXmlFiles> + <suiteXmlFile>tck-suite.xml</suiteXmlFile> + </suiteXmlFiles> + </configuration> + </plugin> + <plugin> + <groupId>uk.co.deliverymind</groupId> + <artifactId>wiremock-maven-plugin</artifactId> + <version>2.7.0</version> + <executions> + <execution> + <phase>test-compile</phase> + <goals> + <goal>run</goal> + </goals> + <configuration> + <dir>target/classes</dir> + <params>--port=8765 --verbose</params> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <inherited>true</inherited> + <extensions>true</extensions> + <configuration> + <instructions> + <Export-Package> + org.glassfish.jersey.restclient.*;version=${project.version} + </Export-Package> + <Import-Package> + * + </Import-Package> + </instructions> + <unpackBundle>true</unpackBundle> + </configuration> + </plugin> + </plugins> + </build> + +<!-- <properties>--> +<!-- <surefire.security.argline>-Djava.security.manager -Djava.security.policy=${project.build.directory}/test-classes/server.policy</surefire.security.argline>--> +<!-- </properties>--> + + +</project> \ 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 0000000..d5c4d74 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/BeanClassModel.java
@@ -0,0 +1,306 @@ +/* + * 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.security.AccessController; +import java.security.PrivilegedAction; +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<ParamModel> 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<ParamModel> 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<WebTarget> 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<String, Object> resolveHeaders(MultivaluedMap<String, Object> 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<String, String> resolveCookies(Map<String, String> 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<String, Object[]> resolveQuery(Map<String, Object[]> 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<WebTarget> 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) { + return AccessController.doPrivileged((PrivilegedAction<Object>) () -> { + try { + Object toReturn; + if (field.isAccessible()) { + return field.get(instance); + } + 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<ParamModel> 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<? extends Annotation> 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 0000000..348382e --- /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<Object> { + + 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<String, Object>) requestPart, instance); + } else if (CookieParam.class.equals(annotationClass)) { + return beanClassModel.resolveCookies((Map<String, String>) requestPart, instance); + } else if (QueryParam.class.equals(annotationClass)) { + return beanClassModel.resolveQuery((Map<String, Object[]>) 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> 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<ParamModel> getAllParamsWithType(Class<? extends Annotation> 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 0000000..fa5d3af --- /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 0000000..466ab38 --- /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<Class<?>, Map<Class<?>, Integer>> customProviders; + + ConfigWrapper(Configuration jerseyBuilderConfig) { + this.jerseyBuilderConfig = jerseyBuilderConfig; + this.customProviders = new HashMap<>(); + } + + void addCustomProvider(Class<?> provider, Map<Class<?>, 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<String, Object> getProperties() { + return jerseyBuilderConfig.getProperties(); + } + + @Override + public Object getProperty(String name) { + return jerseyBuilderConfig.getProperty(name); + } + + @Override + public Collection<String> getPropertyNames() { + return jerseyBuilderConfig.getPropertyNames(); + } + + @Override + public boolean isEnabled(Feature feature) { + return jerseyBuilderConfig.isEnabled(feature); + } + + @Override + public boolean isEnabled(Class<? extends Feature> 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<Class<?>, Integer> getContracts(Class<?> componentClass) { + Map<Class<?>, Integer> map = new HashMap<>(jerseyBuilderConfig.getContracts(componentClass)); + if (customProviders.containsKey(componentClass)) map.putAll(customProviders.get(componentClass)); + return map; + } + + @Override + public Set<Class<?>> getClasses() { + return jerseyBuilderConfig.getClasses(); + } + + @Override + public Set<Object> 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 0000000..9477c34 --- /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<Map<String, String>> { + + private final String cookieParamName; + + CookieParamModel(Builder builder) { + super(builder); + cookieParamName = builder.cookieParamName(); + } + + @Override + Map<String, String> handleParameter(Map<String, String> requestPart, Class<?> annotationClass, Object instance) { + Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); + requestPart.put(cookieParamName, (String) resolvedValue); + return requestPart; + } + + @Override + boolean handles(Class<Annotation> 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 0000000..b699182 --- /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 0000000..c3a8dd6 --- /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<AsyncInvocationInterceptor> asyncInterceptors; + + ExecutorServiceWrapper(ExecutorService wrapped, + List<AsyncInvocationInterceptor> asyncInterceptors) { + this.wrapped = wrapped; + this.asyncInterceptors = asyncInterceptors; + } + + @Override + public void shutdown() { + wrapped.shutdown(); + } + + @Override + public List<Runnable> 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 <T> Future<T> submit(Callable<T> task) { + return wrapped.submit(wrap(task)); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + return wrapped.submit(wrap(task), result); + } + + @Override + public Future<?> submit(Runnable task) { + return wrapped.submit(wrap(task)); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException { + return wrapped.invokeAll(wrap(tasks)); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return wrapped.invokeAll(wrap(tasks), timeout, unit); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException { + return wrapped.invokeAny(wrap(tasks)); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> 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 <T> Callable<T> wrap(Callable<T> task) { + return () -> { + asyncInterceptors.forEach(AsyncInvocationInterceptor::applyContext); + return task.call(); + }; + } + + private Runnable wrap(Runnable task) { + return () -> { + asyncInterceptors.forEach(AsyncInvocationInterceptor::applyContext); + task.run(); + }; + } + + + private <T> Collection<? extends Callable<T>> wrap(Collection<? extends Callable<T>> 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 0000000..7a8f738 --- /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<Form> { + + 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> 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 0000000..3417ada --- /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<MultivaluedMap<String, Object>> { + + private String headerParamName; + + HeaderParamModel(Builder builder) { + super(builder); + this.headerParamName = builder.headerParamName(); + } + + @Override + MultivaluedMap<String, Object> handleParameter(MultivaluedMap<String, Object> requestPart, + Class<?> annotationClass, Object instance) { + Object resolvedValue = interfaceModel.resolveParamValue(instance, parameter); + requestPart.put(headerParamName, Collections.singletonList(resolvedValue)); + return requestPart; + } + + @Override + boolean handles(Class<Annotation> 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 0000000..77bcffe --- /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<HeadersContext> HEADERS_CONTEXT = new ThreadLocal<>(); + + private final MultivaluedMap<String, String> inboundHeaders; + + /** + * The instance associated with the current thread. + * @return context for current thread or {@code empty} if none associated + */ + public static Optional<HeadersContext> 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<HeadersContext> 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<String, String> inboundHeaders) { + return new HeadersContext(inboundHeaders); + } + + public HeadersContext(MultivaluedMap<String, String> 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<String, String> 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 0000000..64ece19 --- /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 0000000..d559fb7 --- /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<String, Object> contextData; + private final List<InvocationInterceptor> 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<String, Object> 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 0000000..dad9c5e --- /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<ClientHeaderParamModel> clientHeaders; + private final List<AsyncInvocationInterceptor> asyncInterceptors; + private final Set<ResponseExceptionMapper> responseExceptionMappers; + private final Set<ParamConverterProvider> paramConverterProviders; + private final Set<Annotation> 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<ResponseExceptionMapper> responseExceptionMappers, + Set<ParamConverterProvider> paramConverterProviders, + List<AsyncInvocationInterceptor> 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<ClientHeadersFactory> getClientHeadersFactory() { + return Optional.ofNullable(clientHeadersFactory); + } + + /** + * Returns {@link List} of processed annotation {@link ClientHeaderParam} to {@link ClientHeaderParamModel} + * + * @return registered factories + */ + List<ClientHeaderParamModel> getClientHeaders() { + return clientHeaders; + } + + /** + * Returns {@link List} of registered {@link AsyncInvocationInterceptor} + * + * @return registered async interceptors + */ + List<AsyncInvocationInterceptor> getAsyncInterceptors() { + return asyncInterceptors; + } + + /** + * Returns {@link Set} of registered {@link ResponseExceptionMapper} + * + * @return registered exception mappers + */ + Set<ResponseExceptionMapper> getResponseExceptionMappers() { + return responseExceptionMappers; + } + + /** + * Returns {@link Set} of registered {@link ParamConverterProvider} + * + * @return registered param converter providers + */ + Set<ParamConverterProvider> getParamConverterProviders() { + return paramConverterProviders; + } + + /** + * Returns {@link Set} of interceptor annotations + * + * @return interceptor annotations + */ + Set<Annotation> 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<ParameterInserterProvider> parameterInserterProviders + = Providers.getAllProviders(injectionManager, ParameterInserterProvider.class); + for (final ParameterInserterProvider parameterInserterProvider : parameterInserterProviders) { + if (parameterInserterProvider != null) { + ParameterInserter<Object, Object> inserter = + (ParameterInserter<Object, Object>) 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<ClientHeaderParamModel> clientHeaders; + private List<AsyncInvocationInterceptor> asyncInterceptors; + private Set<ResponseExceptionMapper> responseExceptionMappers; + private Set<ParamConverterProvider> paramConverterProviders; + private Set<Annotation> interceptorAnnotations; + + private Builder(Class<?> restClientClass, + Set<ResponseExceptionMapper> responseExceptionMappers, + Set<ParamConverterProvider> paramConverterProviders, + List<AsyncInvocationInterceptor> 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<String> 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 0000000..83aa97a --- /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<String> parseParameters(String template) { + List<String> 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<String> 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<Method> 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<Method> getAnnotationComputeMethod(Class<?> iClass, String methodName) { + if (methodName.contains(".")) { + return getStaticComputeMethod(methodName); + } + return getComputeMethod(iClass, methodName); + } + + private static List<Method> 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<Method> 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<Class<?>> 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 0000000..db8e918 --- /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 0000000..440a414 --- /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<WebTarget> { + + 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> 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 0000000..52079eb --- /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<ParamModel> parameterModels; + private final List<ClientHeaderParamModel> clientHeaders; + private final List<InterceptorInvocationContext.InvocationInterceptor> 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<InterceptorInvocationContext.InvocationInterceptor> 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<Object> entity = new AtomicReference<>(); + AtomicReference<WebTarget> 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<actualTypeArgument> + CompletableFuture<Object> result = new CompletableFuture<>(); + Future<Response> 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<Response> completableFuture = (CompletableFuture<Response>) 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> T subResourceProxy(WebTarget webTarget, Class<T> 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<String, Object[]> 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<String, Object[]> 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<WebTarget> 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<String, String> 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<String, String> entry : cookies.entrySet()) { + toReturn = toReturn.cookie(entry.getKey(), entry.getValue()); + } + return toReturn; + } + + private MultivaluedMap<String, Object> addCustomHeaders(Object[] args) { + MultivaluedMap<String, Object> result = new MultivaluedHashMap<>(); + for (Map.Entry<String, List<String>> 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<String, String> resolveCustomHeaders(Object[] args) { + MultivaluedMap<String, String> 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<String, String> inbound = new MultivaluedHashMap<>(); + HeadersContext.get().ifPresent(headersContext -> inbound.putAll(headersContext.inboundHeaders())); + + AtomicReference<MultivaluedMap<String, String>> toReturn = new AtomicReference<>(customHeaders); + interfaceModel.getClientHeadersFactory().ifPresent(clientHeadersFactory -> toReturn + .set(clientHeadersFactory.update(inbound, customHeaders))); + return toReturn.get(); + } + + private <T> MultivaluedMap<String, String> createMultivaluedHeadersMap(List<ClientHeaderParamModel> clientHeaders) { + MultivaluedMap<String, String> 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<String> 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<Class<?>> 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<ParamModel> parameterModels(InterfaceModel classModel, Method method) { + ArrayList<ParamModel> parameterModels = new ArrayList<>(); + final List<org.glassfish.jersey.model.Parameter> 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<ParamModel> parameterModels; + private List<ClientHeaderParamModel> clientHeaders; + private List<InterceptorInvocationContext.InvocationInterceptor> 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<Annotation> 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<Interceptor<?>> 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<ParamModel> 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<ParamModel> 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<String> parameters = InterfaceUtil.parseParameters(uriBuilder.toTemplate()); + List<String> methodPathParameters = new ArrayList<>(); + List<ParamModel> 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<ParamModel> 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<String> 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 0000000..800005d --- /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<T> { + + 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> 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 0000000..4c8768c --- /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<WebTarget> { + + 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> 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 0000000..2a2675e --- /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 0000000..ce96099 --- /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<Map<String, Object[]>> { + + private final String queryParamName; + + QueryParamModel(Builder builder) { + super(builder); + queryParamName = builder.queryParamName(); + } + + @Override + public Map<String, Object[]> handleParameter(Map<String, Object[]> 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> 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 0000000..27344cb --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/ReflectionUtil.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.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Proxy; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.glassfish.jersey.internal.util.ReflectionHelper; + +/** + * Created by David Kral. + */ +class ReflectionUtil { + + static <T> T createInstance(Class<T> tClass) { + return AccessController.doPrivileged((PrivilegedAction<T>) () -> { + 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> T createProxyInstance(Class<T> restClientClass) { + return AccessController.doPrivileged((PrivilegedAction<T>) () -> (T) Proxy.newProxyInstance( + Thread.currentThread().getContextClassLoader(), + new Class[] {restClientClass}, + (proxy, m, args) -> { + Constructor<MethodHandles.Lookup> 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 0000000..bcda814 --- /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 0000000..fadca0f --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java
@@ -0,0 +1,357 @@ +/* + * 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.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Configuration; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.RestClientDefinitionException; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptor; +import org.eclipse.microprofile.rest.client.ext.AsyncInvocationInterceptorFactory; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.eclipse.microprofile.rest.client.spi.RestClientListener; +import org.glassfish.jersey.client.Initializable; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.internal.inject.InjectionManager; +import org.glassfish.jersey.internal.inject.InjectionManagerSupplier; +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<ResponseExceptionMapper> responseExceptionMappers; + private final Set<ParamConverterProvider> paramConverterProviders; + private final List<AsyncInvocationInterceptorFactory> asyncInterceptorFactories; + private final Config config; + private final ConfigWrapper configWrapper; + private URI uri; + private ClientBuilder clientBuilder; + private Supplier<ExecutorService> executorService; + + RestClientBuilderImpl() { + clientBuilder = ClientBuilder.newBuilder(); + responseExceptionMappers = new HashSet<>(); + paramConverterProviders = new HashSet<>(); + asyncInterceptorFactories = new ArrayList<>(); + config = ConfigProvider.getConfig(); + configWrapper = new ConfigWrapper(clientBuilder.getConfiguration()); + executorService = Executors::newCachedThreadPool; + } + + @Override + public RestClientBuilder baseUrl(URL url) { + try { + this.uri = url.toURI(); + return this; + } catch (URISyntaxException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + public RestClientBuilder connectTimeout(long timeout, TimeUnit unit) { + clientBuilder.connectTimeout(timeout, unit); + return this; + } + + @Override + public RestClientBuilder readTimeout(long timeout, TimeUnit unit) { + clientBuilder.readTimeout(timeout, unit); + return this; + } + + @Override + public RestClientBuilder executorService(ExecutorService executor) { + if (executor == null) { + throw new IllegalArgumentException("ExecutorService cannot be null."); + } + executorService = () -> executor; + return this; + } + + @Override + @SuppressWarnings("unchecked") + public <T> T build(Class<T> interfaceClass) throws IllegalStateException, RestClientDefinitionException { + + if (uri == null) { + throw new IllegalStateException("Base uri/url cannot be null!"); + } + + //Provider registration part + Object providersFromJerseyConfig = clientBuilder.getConfiguration() + .getProperty(interfaceClass.getName() + CONFIG_PROVIDERS); + if (providersFromJerseyConfig instanceof String && !((String) providersFromJerseyConfig).isEmpty()) { + String[] providerArray = ((String) providersFromJerseyConfig).split(PROVIDER_SEPARATOR); + processConfigProviders(interfaceClass, providerArray); + } + Optional<String> providersFromConfig = config.getOptionalValue(interfaceClass.getName() + CONFIG_PROVIDERS, String.class); + 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()); + } + InjectionManagerExposer injectionManagerExposer = new InjectionManagerExposer(); + register(injectionManagerExposer); + + 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 = clientBuilder.getConfiguration().getProperty(CONFIG_DISABLE_DEFAULT_MAPPER); + if (disableDefaultMapperJersey != null && disableDefaultMapperJersey.equals(Boolean.FALSE)) { + register(new DefaultResponseExceptionMapper()); + } else if (disableDefaultMapperJersey == null) { + //If property was not set on Jersey ClientBuilder, we need to check config. + Optional<Boolean> disableDefaultMapperConfig = config.getOptionalValue(CONFIG_DISABLE_DEFAULT_MAPPER, boolean.class); + if (!disableDefaultMapperConfig.isPresent() || !disableDefaultMapperConfig.get()) { + register(new DefaultResponseExceptionMapper()); + } + } + + //AsyncInterceptors initialization + List<AsyncInvocationInterceptor> asyncInterceptors = asyncInterceptorFactories.stream() + .map(AsyncInvocationInterceptorFactory::newInterceptor) + .collect(Collectors.toList()); + asyncInterceptors.forEach(AsyncInvocationInterceptor::prepareContext); + + clientBuilder.executorService(new ExecutorServiceWrapper(executorService.get(), asyncInterceptors)); + + Client client = clientBuilder.build(); + if (client instanceof Initializable) { + ((Initializable) client).preInitialize(); + } + WebTarget webTarget = client.target(this.uri); + + RestClientModel restClientModel = RestClientModel.from(interfaceClass, + responseExceptionMappers, + paramConverterProviders, + asyncInterceptors, + injectionManagerExposer.injectionManager); + + + 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 = clientBuilder.getConfiguration().getProperty(property); + if (providerPriorityJersey == null) { + //If property was not set on Jersey ClientBuilder, we need to check MP config. + Optional<Integer> providerPriorityMP = config.getOptionalValue(property, int.class); + if (providerPriorityMP.isPresent()) { + return providerPriorityMP.get(); + } + } else if (providerPriorityJersey instanceof Integer) { + return (int) providerPriorityJersey; + } + Priority priority = providerClass.getAnnotation(Priority.class); + return priority == null ? -1 : priority.value(); + } + + @Override + public Configuration getConfiguration() { + return configWrapper; + } + + @Override + public RestClientBuilder property(String name, Object value) { + clientBuilder.property(name, value); + return this; + } + + @Override + public RestClientBuilder register(Class<?> aClass) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass)); + } else { + clientBuilder.register(aClass); + } + return this; + } + + @Override + public RestClientBuilder register(Class<?> aClass, int i) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), i); + } else { + clientBuilder.register(aClass, i); + } + return this; + } + + @Override + public RestClientBuilder register(Class<?> aClass, Class<?>... classes) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), classes); + } else { + clientBuilder.register(aClass, classes); + } + return this; + } + + @Override + public RestClientBuilder register(Class<?> aClass, Map<Class<?>, Integer> map) { + if (isSupportedCustomProvider(aClass)) { + register(ReflectionUtil.createInstance(aClass), map); + } else { + clientBuilder.register(aClass, map); + } + return this; + } + + @Override + public RestClientBuilder register(Object o) { + if (o instanceof ResponseExceptionMapper) { + ResponseExceptionMapper mapper = (ResponseExceptionMapper) o; + registerCustomProvider(o, -1); + clientBuilder.register(mapper, mapper.getPriority()); + } else { + clientBuilder.register(o); + registerCustomProvider(o, -1); + } + return this; + } + + @Override + public RestClientBuilder register(Object o, int i) { + clientBuilder.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); + } + } + clientBuilder.register(o, classes); + return this; + } + + @Override + public RestClientBuilder register(Object o, Map<Class<?>, 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)); + } + } + clientBuilder.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<Class<?>, Integer> contracts = new HashMap<>(); + contracts.put(ResponseExceptionMapper.class, priority); + configWrapper.addCustomProvider(instance.getClass(), contracts); + } + if (instance instanceof ParamConverterProvider) { + paramConverterProviders.add((ParamConverterProvider) instance); + } + if (instance instanceof AsyncInvocationInterceptorFactory) { + asyncInterceptorFactories.add((AsyncInvocationInterceptorFactory) instance); + } + } + + private static class InjectionManagerExposer implements Feature { + InjectionManager injectionManager; + + @Override + public boolean configure(FeatureContext context) { + if (context instanceof InjectionManagerSupplier) { + this.injectionManager = ((InjectionManagerSupplier) context).getInjectionManager(); + return true; + } else { + throw new IllegalArgumentException("The client needs Jersey runtime to work properly"); + } + } + } + +}
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 0000000..05743a2 --- /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<Class<?>> 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<MpRestClientQualifier> 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 0000000..49d353c --- /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<Method, MethodModel> 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<ResponseExceptionMapper> responseExceptionMappers, + Set<ParamConverterProvider> paramConverterProviders, + List<AsyncInvocationInterceptor> 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 + */ + <T> 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<Method, MethodModel> parseMethodModels(InterfaceModel classModel) { + Map<Method, MethodModel> 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<Method, MethodModel> 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<Method, MethodModel> 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 0000000..7c0c4b3 --- /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<Object>, 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<? extends Annotation> 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<String> 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<InjectionPoint> getInjectionPoints() { + return Collections.emptySet(); + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public Object create(CreationalContext<Object> 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<Object> creationalContext) { + } + + @Override + public Set<Type> getTypes() { + return Collections.singleton(interfaceType); + } + + @Override + public Set<Annotation> getQualifiers() { + if (qualifier == null) { + return Collections.singleton(Default.Literal.INSTANCE); + } + Set<Annotation> annotations = new HashSet<>(); + annotations.add(qualifier); + annotations.add(RestClient.LITERAL); + return annotations; + } + + @Override + public Class<? extends Annotation> getScope() { + return scope; + } + + @Override + public String getName() { + if (qualifier == null) { + return interfaceType.getName() + "RestClient"; + } + return interfaceType.getName(); + } + + @Override + public Set<Class<? extends Annotation>> 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<? extends Annotation> resolveProperClientScope() { + String configScope = config.getOptionalValue(interfaceType.getName() + CONFIG_SCOPE, String.class).orElse(null); + if (configScope != null) { + Class<Annotation> scope = AccessController.doPrivileged(ReflectionHelper.classForNamePA(configScope)); + if (scope == null) { + throw new IllegalStateException("Invalid scope from config: " + configScope); + } + return scope; + } + List<Annotation> 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/microprofile/mp-rest-client/src/main/resources/META-INF/beans.xml b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..f472ff4 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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 + +--> + +<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee + http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" + version="2.0" + bean-discovery-mode="none"> +</beans>
diff --git a/ext/microprofile/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 new file mode 100644 index 0000000..bca39a9 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension
@@ -0,0 +1,17 @@ +# +# 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 +# + +org.glassfish.jersey.microprofile.restclient.RestClientExtension \ No newline at end of file
diff --git a/ext/microprofile/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 new file mode 100644 index 0000000..e23142f --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.eclipse.microprofile.rest.client.spi.RestClientBuilderResolver
@@ -0,0 +1,17 @@ +# +# 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 +# + +org.glassfish.jersey.microprofile.restclient.JerseyRestClientBuilderResolver \ No newline at end of file
diff --git a/ext/microprofile/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 new file mode 100644 index 0000000..d9a1262 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/main/resources/META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable
@@ -0,0 +1,17 @@ +# +# 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 +# + +org.glassfish.jersey.microprofile.restclient.RequestHeaderAutoDiscoverable \ No newline at end of file
diff --git a/ext/microprofile/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 new file mode 100644 index 0000000..203ecbc --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/message/internal/StringMessageProvider.java
@@ -0,0 +1,83 @@ +/* + * 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.message.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; + +import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider; + +/* + * This class was copied from Jersey Common 2.26 due to TCK workaround reasons. + */ + +/** + * + * @author Paul Sandoz + */ +@Produces +@Consumes +@Singleton +final class StringMessageProvider extends AbstractMessageReaderWriterProvider<String> { + + @Override + public boolean isReadable(Class<?> type, Type genericType, Annotation annotations[], MediaType mediaType) { + return type == String.class; + } + + @Override + public String readFrom( + Class<String> type, + Type genericType, + Annotation annotations[], + MediaType mediaType, + MultivaluedMap<String, String> httpHeaders, + InputStream entityStream) throws IOException { + return readFromAsString(entityStream, mediaType); + } + + @Override + public boolean isWriteable(Class<?> type, Type genericType, Annotation annotations[], MediaType mediaType) { + return type == String.class; + } + + @Override + public long getSize(String s, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return s.length(); + } + + @Override + public void writeTo( + String t, + Class<?> type, + Type genericType, + Annotation annotations[], + MediaType mediaType, + MultivaluedMap<String, Object> httpHeaders, + OutputStream entityStream) throws IOException { + writeToAsString(t, entityStream, mediaType); + } +}
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 0000000..caf3bb6 --- /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 0000000..4194b40 --- /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 0000000..efe6c15 --- /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 0000000..a810399 --- /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 0000000..046f528 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/java/org/glassfish/jersey/microprofile/restclient/RestClientModelTest.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.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; + +import static org.junit.Assert.assertEquals; + +/** + * 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); + assertEquals("This is default value!", app.getValue()); + assertEquals("Hi", app.sayHi()); + } +}
diff --git a/ext/microprofile/mp-rest-client/src/test/resources/arquillian.xml b/ext/microprofile/mp-rest-client/src/test/resources/arquillian.xml new file mode 100644 index 0000000..e748760 --- /dev/null +++ b/ext/microprofile/mp-rest-client/src/test/resources/arquillian.xml
@@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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 + +--> + +<arquillian xmlns="http://jboss.org/schema/arquillian" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://jboss.org/schema/arquillian + http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> + + <engine> + <property name="deploymentExportPath">target/deployments</property> + </engine> +</arquillian>
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 0000000..edff3db --- /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 "<<ALL FILES>>", "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 "<<ALL FILES>>", "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 "<<ALL FILES>>", "read,write,delete"; + permission java.net.SocketPermission "*", "connect,resolve"; +};
diff --git a/ext/microprofile/mp-rest-client/tck-suite.xml b/ext/microprofile/mp-rest-client/tck-suite.xml new file mode 100644 index 0000000..3106d69 --- /dev/null +++ b/ext/microprofile/mp-rest-client/tck-suite.xml
@@ -0,0 +1,42 @@ +<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > +<!-- + + 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 + +--> +<suite name="microprofile-rest-client-TCK" verbose="2" configfailurepolicy="continue" > + + <test name="microprofile-rest-client TCK"> + <packages> + <package name="org.eclipse.microprofile.rest.client.tck.*"> + </package> + </packages> + <classes> + <class name="org.eclipse.microprofile.rest.client.tck.InvokeWithJsonBProviderTest"> + <methods> + <exclude name="testGetExecutesForBothClients"/> <!-- Json in this test is malformed --> + </methods> + </class> + </classes> +<!-- <classes>--> +<!-- <class name="org.eclipse.microprofile.rest.client.tck.asynctests.AsyncMethodTest">--> +<!-- <methods>--> +<!-- <include name="testAsyncInvocationInterceptorProvider"/>--> +<!-- </methods>--> +<!-- </class>--> +<!-- </classes>--> + </test> + +</suite> \ No newline at end of file
diff --git a/ext/microprofile/pom.xml b/ext/microprofile/pom.xml new file mode 100644 index 0000000..7a50ddf --- /dev/null +++ b/ext/microprofile/pom.xml
@@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + 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"> + <parent> + <artifactId>project</artifactId> + <groupId>org.glassfish.jersey.ext</groupId> + <version>2.29-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>jersey-microprofile</artifactId> + <packaging>pom</packaging> + + <modules> + <module>mp-rest-client</module> + </modules> + + +</project> \ No newline at end of file
diff --git a/ext/pom.xml b/ext/pom.xml index 22245f1..df0ccd7 100644 --- a/ext/pom.xml +++ b/ext/pom.xml
@@ -56,6 +56,7 @@ <module>spring4</module> <module>spring5</module> <module>wadl-doclet</module> + <module>microprofile</module> </modules> <dependencies>