Compare commits

..

31 Commits

Author SHA1 Message Date
8c9cbecd53 v2.0.8-cxf pom 2023-06-29 10:59:37 -04:00
81198de0fb Merge branch 'develop-cxf' into stable-cxf 2023-06-29 10:59:15 -04:00
803d49e53e added CXF-specific configurable connect/received timeout 2023-06-29 10:45:55 -04:00
adad70d884 Merge branch 'develop' into develop-cxf 2023-06-29 10:32:16 -04:00
d29ccbef79 added connect/read timeout configurability 2023-06-29 10:31:42 -04:00
26c37ab737 v2.0.7-cxf pom 2023-06-12 17:17:07 -04:00
4de7968e11 Merge branch 'develop-cxf' into stable-cxf 2023-06-12 17:16:42 -04:00
378563560b Merge branch 'develop' into develop-cxf 2023-06-12 17:16:25 -04:00
f84279a148 better oauth token expiration handling 2023-06-12 17:16:03 -04:00
d23a4249f7 v2.0.6-cxf 2023-06-12 16:13:36 -04:00
8628d2dce8 Merge branch 'develop-cxf' into stable-cxf 2023-06-12 16:13:00 -04:00
e4e393a855 removing javax.annotation conflict 2023-06-12 16:06:54 -04:00
c7ce4e7c57 Merge branch 'develop' into develop-cxf 2023-06-12 16:05:48 -04:00
44e82b3a6b fixed build issue without jersey/cxf 2023-06-12 16:05:37 -04:00
ab920aedd7 added OAuth filter logging 2023-06-12 16:02:33 -04:00
ab6b0d8da7 refacter OAuth filter error handling 2023-06-12 16:02:19 -04:00
458cdad213 v2.0.5-cxf pom 2023-06-05 15:19:50 -04:00
8b03d251e8 Merge branch 'develop-cxf' into stable-cxf 2023-06-05 15:19:22 -04:00
a1acb3f9d9 updated CXF client to reflect the Jersey one 2023-06-05 15:18:18 -04:00
c25988fc8f removing errand enum enablement 2023-05-30 18:23:39 -04:00
00f25ff054 v2.0.4-cxf 2023-05-30 18:05:51 -04:00
003afc208e Merge branch 'develop-cxf' into stable-cxf 2023-05-30 18:04:35 -04:00
85df5d3ea4 Merge branch 'develop' into develop-cxf 2023-05-30 17:59:55 -04:00
c9833bb4b1 Merge branch 'develop' into develop-cxf 2023-05-30 12:44:13 -04:00
23b9c4ccb2 v2.0.2-cxf 2023-05-29 10:19:56 -04:00
6bb1561825 Merge branch 'develop-cxf' into stable-cxf 2023-05-29 10:19:30 -04:00
0ce2369452 upgraded CXF version 2023-05-29 10:11:03 -04:00
ae82bc4ec7 Merge branch 'develop' into develop-cxf 2023-05-29 10:10:21 -04:00
cc8d9e9d13 Merge branch 'stable' into stable-cxf 2022-10-02 17:56:07 -04:00
93a2eaa34e Merge branch 'develop-cxf' into stable-cxf 2022-10-02 17:53:30 -04:00
1a1cbb1246 added CXF impl 2022-10-02 17:49:48 -04:00
14 changed files with 442 additions and 104 deletions

View File

@@ -1,7 +1,7 @@
# Common ReST Client Library # Common ReST Client Library
This project provides a library for Spring and POJO-based REST client instantiation. It includes special classes for the Jersey JAXRS-based client frameworks. This project provides a library for Spring and POJO-based REST client instantiation. It includes special classes with classifiers for two popular JAXRS-based client frameworks: Apache CXF and Jersey.
## Usage ## Usage
@@ -15,7 +15,8 @@ First, you will need to include the library in your project.
<dependency> <dependency>
<groupId>com.inteligr8</groupId> <groupId>com.inteligr8</groupId>
<artifactId>common-rest-client</artifactId> <artifactId>common-rest-client</artifactId>
<version>...-jersey</version> <classifier>...</classifier>
<version>...</version>
</dependency> </dependency>
... ...
</dependencies> </dependencies>
@@ -23,6 +24,8 @@ First, you will need to include the library in your project.
</project> </project>
``` ```
Valid `classifier` values are `cxf` or `jersey`.
### Spring Framework ### Spring Framework
#### Single Client #### Single Client
@@ -33,27 +36,29 @@ If you will only be declaring a single client in your Spring context, this is ea
@Component @Component
public class ... { public class ... {
@Autowired @Autowired
@Qualifier("client.jersey") // may be optional @Qualifier("client.cxf") // may be optional
private Client client; private Client client;
} }
``` ```
Next, you need to configure that client. You can do that by providing a single implementation of the `ClientConfiguration` (or `ClientJerseyConfiguration`) interface. Next, you need to configure that client. You can do that by providing a single implementation of the `ClientConfiguration` (or `ClientCxfConfiguration`) interface.
```java ```java
@Configuration @Configuration
public class ... implements ClientJerseyConfiguration { public class ... implements ClientCxfConfiguration {
... ...
} }
``` ```
For Jersey implementations, just use `client.jersey` and `ClientJerseyConfiguration`. If you want to provide one of each, then follow the instructions for multiple clients below.
#### Multiple Clients #### Multiple Clients
If you will or may have multiple clients in your Spring context, there is an extra step. You will still need to define a `ClientConfiguration` for each. On top of that, you will need to create specialized implementations of each client. That special implementation will reference the configuration directly. An example is below. If you will or may have multiple clients in your Spring context, there is an extra step. You will still need to define a `ClientConfiguration` for each. On top of that, you will need to create specialized implementations of each client. That special implementation will reference the configuration directly. An example is below.
```java ```java
@Component("my.client") @Component("my.client")
public class MyClient extends ClientJerseyImpl { public class MyClient extends ClientCxfImpl {
@Autowired @Autowired
public MyClient(MyClientConfiguration config) { public MyClient(MyClientConfiguration config) {
super(config); super(config);
@@ -82,6 +87,6 @@ You do not have to use the Spring framework to use these classes. You can insta
```java ```java
MyClientConfiguration config = new MyClientConfiguration(); MyClientConfiguration config = new MyClientConfiguration();
... ...
ClientJerseyImpl client = new ClientJerseyImpl(config); ClientCxfImpl client = new ClientCxfImpl(config);
MyJaxRsApi api = client.getApi(MyJaxRsApi.class); MyJaxRsApi api = client.getApi(MyJaxRsApi.class);
``` ```

40
pom.xml
View File

@@ -6,11 +6,11 @@
<groupId>com.inteligr8</groupId> <groupId>com.inteligr8</groupId>
<artifactId>common-rest-client</artifactId> <artifactId>common-rest-client</artifactId>
<version>2.0.4-jersey</version> <version>2.0.8-cxf</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>ReST API Client for Java</name> <name>ReST API Client for Java</name>
<description>A common library for building Jersey REST API clients</description> <description>A common library for building CXF REST API clients</description>
<url>https://bitbucket.org/inteligr8/common-rest-client</url> <url>https://bitbucket.org/inteligr8/common-rest-client</url>
<licenses> <licenses>
@@ -46,7 +46,7 @@
<junit.version>5.7.2</junit.version> <junit.version>5.7.2</junit.version>
<spring.version>5.3.27</spring.version> <spring.version>5.3.27</spring.version>
<jackson.version>2.15.1</jackson.version> <jackson.version>2.15.1</jackson.version>
<jersey.version>2.39.1</jersey.version> <cxf.version>3.5.6</cxf.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -99,33 +99,11 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Jersey libraries --> <!-- Apache CXF libraries -->
<dependency> <dependency>
<groupId>org.glassfish.jersey.ext</groupId> <groupId>org.apache.cxf</groupId>
<artifactId>jersey-proxy-client</artifactId> <artifactId>cxf-rt-rs-client</artifactId>
<version>${jersey.version}</version> <version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -141,7 +119,7 @@
<goals><goal>add-source</goal></goals> <goals><goal>add-source</goal></goals>
<configuration> <configuration>
<sources> <sources>
<source>src/main/jersey</source> <source>src/main/cxf</source>
</sources> </sources>
</configuration> </configuration>
</execution> </execution>
@@ -150,7 +128,7 @@
<goals><goal>add-test-source</goal></goals> <goals><goal>add-test-source</goal></goals>
<configuration> <configuration>
<sources> <sources>
<source>src/test/jersey</source> <source>src/test/cxf</source>
</sources> </sources>
</configuration> </configuration>
</execution> </execution>

View File

@@ -14,22 +14,35 @@
*/ */
package com.inteligr8.rs; package com.inteligr8.rs;
import org.apache.cxf.jaxrs.client.WebClient;
/** /**
* This interface defines additional configurations specific to the Jersey * This interface defines additional configurations specific to the Apache CXF
* JAX-RS library and its nuances. * JAX-RS library and its nuances.
* *
* @author brian@inteligr8.com * @author brian@inteligr8.com
*/ */
public interface ClientJerseyConfiguration extends ClientConfiguration { public interface ClientCxfConfiguration extends ClientConfiguration {
/** /**
* Jersey is automatically strict in its adherence to the ReST API * Apache CXF uses a global bus configuration where interceptors could
* specifications. It requires a body to PUT calls by default. * wreck havoc on your implementation. This method allows you to
* explicitly by-pass the default bus.
* *
* @return true to require body in PUT calls; false to make it optional * See https://cxf.apache.org/docs/bus-configuration.html.
*
* @return true to use the default bus; false otherwise.
*/ */
default boolean isPutBodyRequired() { default boolean isDefaultBusEnabled() {
return true; return true;
} }
/**
* A Jackson provider, logging filter, and authentication filter are already registered.
*
* @param client A CXF client to configure.
*/
default void configureClient(WebClient client) {
}
} }

View File

@@ -0,0 +1,185 @@
/*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.inteligr8.rs;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ws.rs.ext.RuntimeDelegate;
import org.apache.cxf.BusFactory;
import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.impl.RuntimeDelegateImpl;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
/**
* A class that provides pre-configured JAX-RS Client &amp; WebTarget &amp;
* CXF WebClient objects.
*
* @author brian@inteligr8.com
*/
@Component("client.cxf")
public class ClientCxfImpl extends Client {
private final Logger logger = LoggerFactory.getLogger(ClientCxfImpl.class);
private final Object sync = new Object();
private ClientCxfConfiguration config;
private WebClient client;
/**
* This constructor is for Spring or POJO use.
* @param config The client configuration.
*/
@Autowired
public ClientCxfImpl(ClientCxfConfiguration config) {
this.config = config;
}
/**
* This method registers the Apache CXF library as the default provider for
* the JAX-RS specification.
*/
@PostConstruct
public void register() {
if (RuntimeDelegate.getInstance() == null) {
this.logger.info("Setting JAX-RS runtime delegate to the CXF library");
RuntimeDelegate.setInstance(new RuntimeDelegateImpl());
} else if (RuntimeDelegate.getInstance() instanceof RuntimeDelegateImpl) {
this.logger.info("JAX-RS runtime delegate already the CXF library");
} else {
this.logger.warn("Setting JAX-RS runtime delegate to the CXF library; was: " + RuntimeDelegate.getInstance().getClass().getName());
RuntimeDelegate.setInstance(new RuntimeDelegateImpl());
}
if (this.logger.isInfoEnabled())
this.logger.info("API Base URL: " + this.getConfig().getBaseUrl());
}
/**
* @return A CXF client (not JAX-RS).
*/
public WebClient getCxfClient() {
synchronized (this.sync) {
if (this.client == null)
this.client = this.buildCxfClient(null);
}
return this.client;
}
/**
* @param authFilter A dynamic authorization filter.
* @return A pre-configured CXF client (no URL) with the specified authorization.
*/
public WebClient getCxfClient(AuthorizationFilter authFilter) {
if (authFilter == null) {
return this.getCxfClient();
} else {
return this.buildCxfClient(authFilter);
}
}
/**
* @param authFilter A post-configuration authorization filter.
* @return A CXF client (not JAX-RS).
*/
public WebClient buildCxfClient(AuthorizationFilter authFilter) {
ObjectMapper om = new ObjectMapper();
om.registerModules(new JavaTimeModule());
this.getConfig().configureJacksonMapper(om);
JacksonJaxbJsonProvider jacksonProvider = new JacksonJaxbJsonProvider(om, JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS);
this.getConfig().configureJacksonProvider(jacksonProvider);
List<Object> providersAndFilters = new LinkedList<Object>();
providersAndFilters.add(jacksonProvider);
providersAndFilters.add(new CxfLoggingFilter());
providersAndFilters.add(new CxfMultipartProvider());
if (authFilter == null)
authFilter = this.getConfig().createAuthorizationFilter();
if (authFilter != null)
providersAndFilters.add(authFilter);
this.addProvidersAndFilters(providersAndFilters);
// we can't use JAXRSClientFactory with a JAXRS client (duh!)
// so we need to create a CXF client
WebClient client = WebClient.create(this.getConfig().getBaseUrl(), providersAndFilters);
if (this.getConfig().getConnectTimeoutInMillis() != null || this.getConfig().getResponseTimeoutInMillis() != null) {
HTTPConduit conduit = client.getConfiguration().getHttpConduit();
HTTPClientPolicy policy = conduit.getClient();
if (policy == null)
conduit.setClient(policy = new HTTPClientPolicy());
if (this.getConfig().getConnectTimeoutInMillis() != null)
policy.setConnectionTimeout(this.getConfig().getConnectTimeoutInMillis());
if (this.getConfig().getResponseTimeoutInMillis() != null)
policy.setReceiveTimeout(this.getConfig().getResponseTimeoutInMillis());
}
if (!this.getConfig().isDefaultBusEnabled()) {
// Some applications (like ACS) add interceptors to the default bus
// those interceptors may treat all messages as SOAP messages (like ACS), resulting in ClassCastExceptions
// we need to ignore the default bus
org.apache.cxf.jaxrs.client.ClientConfiguration config = WebClient.getConfig(client);
config.setBus(BusFactory.newInstance().createBus());
}
this.config.configureClient(client);
return client;
}
/**
* @param providersAndFilters A list of JAX-RS and CXF providers.
*/
public void addProvidersAndFilters(List<Object> providersAndFilters) {
// for extension purposes
}
/**
* @return The client configuration.
*/
public ClientCxfConfiguration getConfig() {
return this.config;
}
/**
* This method retrieves a JAX-RS implementation of the specified API with
* the specified authorization.
*
* @param authFilter A dynamic authorization filter.
* @param apiClass A JAX-RS annotation API class.
* @return An instance of the API class.
*/
@Override
public <T> T getApi(AuthorizationFilter authFilter, Class<T> apiClass) {
return JAXRSClientFactory.fromClient(this.getCxfClient(authFilter), apiClass);
}
}

View File

@@ -0,0 +1,52 @@
/*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.inteligr8.rs;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.core.MediaType;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.slf4j.Logger;
/**
* This is a CXF specific handling of the logging of multipart requests, which
* would otherwise be ignored by the base LoggingFilter. It is meant to be
* used for debugging purposes. When used, it will write to 'jaxrs.request' and
* 'jaxrs.response' loggers at the 'trace' level.
*
* @author brian@inteligr8.com
*/
public class CxfLoggingFilter extends LoggingFilter {
@Override
protected void logUnhandledRequest(ClientRequestContext requestContext, Logger logger) throws IOException {
if (MediaType.MULTIPART_FORM_DATA_TYPE.equals(requestContext.getMediaType())) {
if (requestContext.getEntity() instanceof MultipartBody) {
List<String> attIds = new LinkedList<>();
for (Attachment att : ((MultipartBody)requestContext.getEntity()).getAllAttachments())
attIds.add(att.getContentId());
logger.trace("request: {} {}: {}", requestContext.getMethod(), requestContext.getUri(), attIds);
} else {
logger.trace("request: {} {}: failed to output form", requestContext.getMethod(), requestContext.getUri());
}
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.inteligr8.rs;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.Provider;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.provider.MultipartProvider;
/**
* This implements a JAX-RS provider that adds support for the handling of CXF
* MultipartBody.
*
* @author brian@inteligr8.com
*/
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.MULTIPART_FORM_DATA)
@Provider
public class CxfMultipartProvider extends MultipartProvider {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return MultipartBody.class.isAssignableFrom(type) || super.isReadable(type, genericType, annotations, mediaType);
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return MultipartBody.class.isAssignableFrom(type) || super.isWriteable(type, genericType, annotations, mediaType);
}
}

View File

@@ -14,6 +14,8 @@
*/ */
package com.inteligr8.rs; package com.inteligr8.rs;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget; import javax.ws.rs.client.WebTarget;
@@ -72,9 +74,6 @@ public abstract class Client {
this.getConfig().configureJacksonMapper(om); this.getConfig().configureJacksonMapper(om);
JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(om, JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS); JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(om, JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS);
provider.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
provider.disable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
this.getConfig().configureJacksonProvider(provider); this.getConfig().configureJacksonProvider(provider);
if (this.getConfig().isWrapRootValueEnabled()) if (this.getConfig().isWrapRootValueEnabled())
@@ -85,13 +84,17 @@ public abstract class Client {
ClientBuilder clientBuilder = ClientBuilder.newBuilder() ClientBuilder clientBuilder = ClientBuilder.newBuilder()
.register(provider) .register(provider)
.register(new LoggingFilter()); .register(new LoggingFilter());
if (this.getConfig().getConnectTimeoutInMillis() != null)
clientBuilder.connectTimeout(this.getConfig().getConnectTimeoutInMillis(), TimeUnit.MILLISECONDS);
if (this.getConfig().getResponseTimeoutInMillis() != null)
clientBuilder.readTimeout(this.getConfig().getResponseTimeoutInMillis(), TimeUnit.MILLISECONDS);
if (authFilter == null) if (authFilter == null)
authFilter = this.getConfig().createAuthorizationFilter(); authFilter = this.getConfig().createAuthorizationFilter();
if (authFilter != null) if (authFilter != null)
clientBuilder.register(authFilter); clientBuilder.register(authFilter);
this.buildClient(clientBuilder); this.buildClient(clientBuilder);
this.getConfig().configureClient(clientBuilder); this.getConfig().configureClient(clientBuilder);
return clientBuilder.build(); return clientBuilder.build();

View File

@@ -119,6 +119,16 @@ public interface ClientConfiguration {
} }
default Integer getConnectTimeoutInMillis() {
return null;
}
default Integer getResponseTimeoutInMillis() {
return null;
}
/** /**
* @return true to enable Jackson UNWRAP_ROOT_VALUE feature; false otherwise. * @return true to enable Jackson UNWRAP_ROOT_VALUE feature; false otherwise.

View File

@@ -16,12 +16,7 @@ package com.inteligr8.rs;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.ext.RuntimeDelegate;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.proxy.WebResourceFactory;
import org.glassfish.jersey.internal.RuntimeDelegateImpl;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -33,19 +28,19 @@ import org.springframework.stereotype.Component;
* *
* @author brian@inteligr8.com * @author brian@inteligr8.com
*/ */
@Component("client.jersey") @Component("client.jaxrs")
public class ClientJerseyImpl extends Client { public class ClientImpl extends Client {
private final Logger logger = LoggerFactory.getLogger(ClientJerseyImpl.class); private final Logger logger = LoggerFactory.getLogger(ClientImpl.class);
private ClientJerseyConfiguration config; private ClientConfiguration config;
/** /**
* This constructor is for Spring or POJO use. * This constructor is for Spring or POJO use.
* @param config The client configuration. * @param config The client configuration.
*/ */
@Autowired @Autowired
public ClientJerseyImpl(ClientJerseyConfiguration config) { public ClientImpl(ClientConfiguration config) {
this.config = config; this.config = config;
} }
@@ -55,18 +50,7 @@ public class ClientJerseyImpl extends Client {
*/ */
@PostConstruct @PostConstruct
public void register() { public void register() {
if (RuntimeDelegate.getInstance() == null) { this.logger.info("API Base URL: {}", this.getConfig().getBaseUrl());
this.logger.info("Setting JAX-RS runtime delegate to the Jersey library");
RuntimeDelegate.setInstance(new RuntimeDelegateImpl());
} else if (RuntimeDelegate.getInstance() instanceof RuntimeDelegateImpl) {
this.logger.info("JAX-RS runtime delegate already the Jersey library");
} else {
this.logger.warn("Setting JAX-RS runtime delegate to the Jersey library; was: " + RuntimeDelegate.getInstance().getClass().getName());
RuntimeDelegate.setInstance(new RuntimeDelegateImpl());
}
if (this.logger.isInfoEnabled())
this.logger.info("API Base URL: " + this.getConfig().getBaseUrl());
} }
/** /**
@@ -74,18 +58,12 @@ public class ClientJerseyImpl extends Client {
*/ */
@Override @Override
public void buildClient(ClientBuilder clientBuilder) { public void buildClient(ClientBuilder clientBuilder) {
clientBuilder.register(MultiPartFeature.class);
if (!this.getConfig().isPutBodyRequired()) {
// allow PUT operations without body data
clientBuilder.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
}
} }
/** /**
* @return The client configuration. * @return The client configuration.
*/ */
public ClientJerseyConfiguration getConfig() { public ClientConfiguration getConfig() {
return this.config; return this.config;
} }
@@ -99,7 +77,7 @@ public class ClientJerseyImpl extends Client {
*/ */
@Override @Override
public <T> T getApi(AuthorizationFilter authFilter, Class<T> apiClass) { public <T> T getApi(AuthorizationFilter authFilter, Class<T> apiClass) {
return WebResourceFactory.newResource(apiClass, this.getTarget(authFilter)); throw new UnsupportedOperationException();
} }
} }

View File

@@ -58,7 +58,7 @@ public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter
logger.trace("request: {} {}: {}", requestContext.getMethod(), requestContext.getUri(), logger.trace("request: {} {}: {}", requestContext.getMethod(), requestContext.getUri(),
((Form)requestContext.getEntity()).asMap()); ((Form)requestContext.getEntity()).asMap());
} else { } else {
this.loggerRequest.trace("request: {} {}: failed to output form", requestContext.getMethod(), requestContext.getUri()); logger.trace("request: {} {}: failed to output form", requestContext.getMethod(), requestContext.getUri());
} }
} else { } else {
this.logUnhandledRequest(requestContext, logger); this.logUnhandledRequest(requestContext, logger);

View File

@@ -74,11 +74,15 @@ public class OAuthAuthorizationCodeAuthorizationFilter extends OAuthAuthorizatio
@Override @Override
protected Form createForm() { protected Form createForm() {
Form form = new Form().param("grant_type", "authorization_code") Form form = new Form().param("grant_type", "authorization_code");
.param("code", this.code);
if (this.redirectUri != null) if (this.redirectUri != null)
form.param("redirect_uri", this.redirectUri.toString()); form.param("redirect_uri", this.redirectUri.toString());
return form; return form;
} }
@Override
protected void extendFormSensitive(Form form) {
form.param("code", this.code);
}
} }

View File

@@ -17,12 +17,18 @@ package com.inteligr8.rs;
import java.util.Map; import java.util.Map;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.Entity; import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget; import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status.Family;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
@@ -33,6 +39,8 @@ import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
*/ */
public abstract class OAuthAuthorizationFilter implements AuthorizationFilter { public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final String tokenUrl; private final String tokenUrl;
private final String clientId; private final String clientId;
private final String clientSecret; private final String clientSecret;
@@ -40,6 +48,7 @@ public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
private String accessToken; private String accessToken;
private long expiration; private long expiration;
private String refreshToken; private String refreshToken;
private Long refreshTokenExpiration;
/** /**
* This constructor creates an OAuth-based authorization filter using the * This constructor creates an OAuth-based authorization filter using the
@@ -91,8 +100,29 @@ public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
*/ */
@Override @Override
public void filter(ClientRequestContext requestContext) { public void filter(ClientRequestContext requestContext) {
if (this.accessToken == null || System.currentTimeMillis() > this.expiration) if (this.accessToken == null) {
this.requestToken(); this.requestToken();
} else if (System.currentTimeMillis() >= this.expiration) {
this.logger.trace("Access token expired; retrieving new one with refresh token");
if (this.refreshTokenExpiration != null && System.currentTimeMillis() >= this.refreshTokenExpiration.longValue()) {
this.logger.debug("Refresh token expired; performing full authentication");
this.refreshToken = null;
this.requestToken();
} else {
try {
this.requestToken();
} catch (WebApplicationException wae) {
if (wae.getResponse().getStatusInfo().getFamily() == Family.CLIENT_ERROR) {
this.logger.debug("Received OAuth response {} using refresh token; performing full authentication", wae.getResponse().getStatus());
this.refreshToken = null;
this.requestToken();
} else {
throw wae;
}
}
}
}
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + this.accessToken); requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + this.accessToken);
} }
@@ -111,29 +141,53 @@ public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
} }
form.param("client_id", this.clientId); form.param("client_id", this.clientId);
if (this.clientSecret != null)
form.param("client_secret", this.clientSecret);
if (this.scope != null) if (this.scope != null)
form.param("scope", this.scope); form.param("scope", this.scope);
this.extendRefreshTokenForm(form);
this.logger.trace("Sending OAuth request: {}", form);
if (this.refreshToken != null) {
this.extendRefreshFormSensitive(form);
} else {
this.extendFormSensitive(form);
}
if (this.clientSecret != null)
form.param("client_secret", this.clientSecret);
Entity<Form> entity = Entity.form(form); Entity<Form> entity = Entity.form(form);
WebTarget target = ClientBuilder.newBuilder() Client client = ClientBuilder.newBuilder()
.register(new JacksonJaxbJsonProvider()) .register(new JacksonJaxbJsonProvider())
.build() .build();
.target(this.tokenUrl); WebTarget target = client.target(this.tokenUrl);
long requestSendTime = System.currentTimeMillis();
@SuppressWarnings("unchecked") Response response = target.request().post(entity);
Map<String, Object> response = target.request().post(entity, Map.class);
if (response.containsKey("error")) this.logger.debug("Received OAuth response: {}", response.getStatus());
throw new WebApplicationException((String)response.get("error"), 400);
@SuppressWarnings("unchecked")
Map<String, Object> responseMap = response.readEntity(Map.class);
this.logger.trace("Received OAuth response: {}", responseMap);
this.accessToken = (String)response.get("access_token"); if (response.getStatusInfo().getFamily() != Family.SUCCESSFUL) {
this.expiration = System.currentTimeMillis() + ((Number)response.get("expires_in")).longValue() * 1000L; String code = (String) responseMap.get("error");
this.refreshToken = (String)response.get("refresh_token"); if (code != null) {
this.extendRefreshTokenResponse(response); String description = (String) responseMap.get("error_description");
throw new WebApplicationException(code + ": " + description, response.getStatus());
} else {
throw new WebApplicationException(response);
}
}
this.accessToken = (String)responseMap.get("access_token");
this.expiration = requestSendTime + ((Number)responseMap.get("expires_in")).longValue() * 1000L;
this.refreshToken = (String)responseMap.get("refresh_token");
if (responseMap.containsKey("refresh_token_expires_in"))
this.refreshTokenExpiration = requestSendTime + ((Number)responseMap.get("refresh_token_expires_in")).longValue() * 1000L;
} }
protected Form createRefreshForm() { protected Form createRefreshForm() {
@@ -143,10 +197,9 @@ public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
protected abstract Form createForm(); protected abstract Form createForm();
protected void extendRefreshTokenForm(Form form) { protected void extendRefreshFormSensitive(Form form) {
}
protected void extendRefreshTokenResponse(Map<String, Object> response) {
} }
protected abstract void extendFormSensitive(Form form);
} }

View File

@@ -37,5 +37,9 @@ public class OAuthClientCredentialAuthorizationFilter extends OAuthAuthorization
protected Form createForm() { protected Form createForm() {
return new Form().param("grant_type", "client_credentials"); return new Form().param("grant_type", "client_credentials");
} }
@Override
protected void extendFormSensitive(Form form) {
}
} }

View File

@@ -53,8 +53,12 @@ public class OAuthPasswordGrantAuthorizationFilter extends OAuthAuthorizationFil
@Override @Override
protected Form createForm() { protected Form createForm() {
return new Form().param("grant_type", "password") return new Form().param("grant_type", "password")
.param("username", this.username) .param("username", this.username);
.param("password", this.password); }
@Override
protected void extendFormSensitive(Form form) {
form.param("password", this.password);
} }
} }