Compare commits

..

28 Commits

Author SHA1 Message Date
271016dcf9 v2.0.10 pom 2024-03-05 18:49:07 -05:00
00ec8c972d Merge branch 'develop' into stable 2024-03-05 18:47:56 -05:00
2cd9cc9484 removing spring annotations to support multiple clients 2024-03-05 18:46:07 -05:00
d4cd2f248c v2.0.9 pom 2024-01-23 10:42:51 -05:00
ac6f82a7f4 Merge branch 'develop' into stable 2024-01-23 10:41:11 -05:00
eb654c5488 added logging and param trimming 2024-01-23 10:40:37 -05:00
0258cfcb9a v2.0.8 pom 2023-06-29 10:46:39 -04:00
6fc43b85f7 Merge branch 'develop' into stable 2023-06-29 10:46:11 -04:00
d29ccbef79 added connect/read timeout configurability 2023-06-29 10:31:42 -04:00
512f118354 v2.0.7 pom 2023-06-12 17:23:23 -04:00
21255bfb3d Merge branch 'develop' into stable 2023-06-12 17:16:33 -04:00
f84279a148 better oauth token expiration handling 2023-06-12 17:16:03 -04:00
f10fd39e97 v2.0.6 pom 2023-06-12 16:10:25 -04:00
eaed628f00 Merge branch 'develop' into stable 2023-06-12 16:08:46 -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
c25988fc8f removing errand enum enablement 2023-05-30 18:23:39 -04:00
cd278bcc81 v2.0.4 pom 2023-05-30 18:00:33 -04:00
b64c408029 Merge branch 'develop' into stable 2023-05-30 18:00:06 -04:00
9a9a958913 allowing client ext; not just config ext 2023-05-30 17:58:38 -04:00
37e8b63179 added default enum deserialization 2023-05-30 17:54:29 -04:00
255a9c8e85 enabling more extensible configuration 2023-05-30 17:53:10 -04:00
ec6e7cc15d v2.0.3 pom 2023-05-30 12:44:41 -04:00
2451c634b7 Merge branch 'develop' into stable 2023-05-30 12:44:21 -04:00
bcb7d4a7ed adding JavaTimeModule 2023-05-30 12:43:50 -04:00
ed00346e71 v2.0.2 pom 2023-05-29 10:15:38 -04:00
c001576b65 Merge branch 'develop' into stable 2023-05-29 10:11:16 -04:00
16 changed files with 278 additions and 430 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 with classifiers for two popular JAXRS-based client frameworks: Apache CXF and Jersey. This project provides a library for Spring and POJO-based REST client instantiation.
## Usage ## Usage
@@ -15,7 +15,6 @@ 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>
<classifier>...</classifier>
<version>...</version> <version>...</version>
</dependency> </dependency>
... ...
@@ -24,69 +23,4 @@ First, you will need to include the library in your project.
</project> </project>
``` ```
Valid `classifier` values are `cxf` or `jersey`. See the `cxf` and `jersey` branches for examples and more documentation.
### Spring Framework
#### Single Client
If you will only be declaring a single client in your Spring context, this is easy. You will just need two things. First, inject the single client into any of your Spring beans. You may do inject it into more than one. An example is below:
```java
@Component
public class ... {
@Autowired
@Qualifier("client.cxf") // may be optional
private Client client;
}
```
Next, you need to configure that client. You can do that by providing a single implementation of the `ClientConfiguration` (or `ClientCxfConfiguration`) interface.
```java
@Configuration
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
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
@Component("my.client")
public class MyClient extends ClientCxfImpl {
@Autowired
public MyClient(MyClientConfiguration config) {
super(config);
}
}
```
You can then inject your client(s) into your Spring beans. Like the example below:
```java
@Component
public class ... {
@Autowired
private MyClient client;
@PostConstruct
public void init() {
MyJaxRsApi api = this.client.getApi(MyJaxRsApi.class);
}
}
### POJO
You do not have to use the Spring framework to use these classes. You can instantiate them directly. But you wil still need to create a `ClientConfiguration` as mentioned above.
```java
MyClientConfiguration config = new MyClientConfiguration();
...
ClientCxfImpl client = new ClientCxfImpl(config);
MyJaxRsApi api = client.getApi(MyJaxRsApi.class);
```

47
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.2-cxf</version> <version>2.0.10</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 CXF REST API clients</description> <description>A common library for building 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,10 +46,14 @@
<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>
<cxf.version>3.5.6</cxf.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId> <artifactId>spring-context</artifactId>
@@ -70,6 +74,11 @@
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version> <version>${jackson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
@@ -98,42 +107,10 @@
<version>4.5.14</version> <version>4.5.14</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Apache CXF libraries -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-client</artifactId>
<version>${cxf.version}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>add-jaxrs-src</id>
<goals><goal>add-source</goal></goals>
<configuration>
<sources>
<source>src/main/cxf</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-test-src</id>
<goals><goal>add-test-source</goal></goals>
<configuration>
<sources>
<source>src/test/cxf</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.1.0</version> <version>3.1.0</version>

View File

@@ -1,38 +0,0 @@
/*
* 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;
/**
* This interface defines additional configurations specific to the Apache CXF
* JAX-RS library and its nuances.
*
* @author brian@inteligr8.com
*/
public interface ClientCxfConfiguration extends ClientConfiguration {
/**
* Apache CXF uses a global bus configuration where interceptors could
* wreck havoc on your implementation. This method allows you to
* explicitly by-pass the default bus.
*
* See https://cxf.apache.org/docs/bus-configuration.html.
*
* @return true to use the default bus; false otherwise.
*/
default boolean isDefaultBusEnabled() {
return true;
}
}

View File

@@ -1,142 +0,0 @@
/*
* 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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 ClientCxfConfiguration config;
/**
* 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() {
return this.getCxfClient(null);
}
/**
* @param authFilter A post-configuration authorization filter.
* @return A CXF client (not JAX-RS).
*/
public WebClient getCxfClient(AuthorizationFilter authFilter) {
List<Object> providersAndFilters = new LinkedList<Object>();
providersAndFilters.add(new JacksonJaxbJsonProvider());
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().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());
}
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

@@ -1,52 +0,0 @@
/*
* 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

@@ -1,49 +0,0 @@
/*
* 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

@@ -20,6 +20,8 @@ import java.util.Base64;
import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import org.apache.commons.lang3.StringUtils;
/** /**
* This class implements a simple 2-credential (username &amp; password) based * This class implements a simple 2-credential (username &amp; password) based
* authorization filter. * authorization filter.
@@ -36,8 +38,8 @@ public class BasicAuthorizationFilter implements AuthorizationFilter {
* @param password A password or secret key. * @param password A password or secret key.
*/ */
public BasicAuthorizationFilter(String username, String password) { public BasicAuthorizationFilter(String username, String password) {
this.username = username; this.username = StringUtils.trimToNull(username);
this.password = password; this.password = StringUtils.trimToNull(password);
} }
/** /**

View File

@@ -14,13 +14,16 @@
*/ */
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;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
/** /**
* A class that provides pre-configured JAX-RS Client &amp; WebTarget objects. * A class that provides pre-configured JAX-RS Client &amp; WebTarget objects.
@@ -66,7 +69,12 @@ public abstract class Client {
* @return A pre-configured JAX-RS client (no URL) with the specified authorization. * @return A pre-configured JAX-RS client (no URL) with the specified authorization.
*/ */
public final javax.ws.rs.client.Client buildClient(AuthorizationFilter authFilter) { public final javax.ws.rs.client.Client buildClient(AuthorizationFilter authFilter) {
JacksonJsonProvider provider = new JacksonJaxbJsonProvider(); ObjectMapper om = new ObjectMapper();
om.registerModules(new JavaTimeModule());
this.getConfig().configureJacksonMapper(om);
JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(om, JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS);
this.getConfig().configureJacksonProvider(provider);
if (this.getConfig().isWrapRootValueEnabled()) if (this.getConfig().isWrapRootValueEnabled())
provider.enable(SerializationFeature.WRAP_ROOT_VALUE); provider.enable(SerializationFeature.WRAP_ROOT_VALUE);
@@ -77,11 +85,17 @@ public abstract class Client {
.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);
return clientBuilder.build(); return clientBuilder.build();
} }

View File

@@ -16,6 +16,11 @@ package com.inteligr8.rs;
import java.net.URI; import java.net.URI;
import javax.ws.rs.client.ClientBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
/** /**
* This interface defines the configurable parameters of the clients; primarily * This interface defines the configurable parameters of the clients; primarily
* their default authentication and authorization. * their default authentication and authorization.
@@ -115,6 +120,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.
*/ */
@@ -129,6 +144,18 @@ public interface ClientConfiguration {
return false; return false;
} }
/**
* @param mapper A Jackson object mapper to configure.
*/
default void configureJacksonMapper(ObjectMapper mapper) {
}
/**
* @param provider A Jackson JAX-RS provider to configure.
*/
default void configureJacksonProvider(JacksonJaxbJsonProvider provider) {
}
/** /**
@@ -170,4 +197,12 @@ public interface ClientConfiguration {
} }
} }
/**
* A Jackson provider, logging filter, and authentication filter are already registered.
*
* @param clientBuilder A JAX-RS client builder to configure.
*/
default void configureClient(ClientBuilder clientBuilder) {
}
} }

View File

@@ -16,6 +16,8 @@ package com.inteligr8.rs;
import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestContext;
import org.apache.commons.lang3.StringUtils;
/** /**
* This class is the base for implementations of client authorization similar * This class is the base for implementations of client authorization similar
* to OAuth-based flows. * to OAuth-based flows.
@@ -45,8 +47,8 @@ public class ClientEnforcementAuthorizationFilter implements AuthorizationFilter
* @param clientSecret A secret corresponding to the client ID. * @param clientSecret A secret corresponding to the client ID.
*/ */
public ClientEnforcementAuthorizationFilter(String clientId, String clientSecret) { public ClientEnforcementAuthorizationFilter(String clientId, String clientSecret) {
this.clientId = clientId; this.clientId = StringUtils.trimToNull(clientId);
this.clientSecret = clientSecret; this.clientSecret = StringUtils.trimToNull(clientSecret);
} }
/** /**

View File

@@ -0,0 +1,79 @@
/*
* 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 javax.annotation.PostConstruct;
import javax.ws.rs.client.ClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class that provides pre-configured JAX-RS Client &amp; WebTarget objects
* for Jersey.
*
* @author brian@inteligr8.com
*/
public class ClientImpl extends Client {
private final Logger logger = LoggerFactory.getLogger(ClientImpl.class);
private ClientConfiguration config;
/**
* This constructor is for Spring or POJO use.
* @param config The client configuration.
*/
public ClientImpl(ClientConfiguration config) {
this.config = config;
}
/**
* This method registers the Jersey library as the default provider for the
* JAX-RS specification.
*/
@PostConstruct
public void register() {
this.logger.info("API Base URL: {}", this.getConfig().getBaseUrl());
}
/**
* @param clientBuilder A client builder.
*/
@Override
public void buildClient(ClientBuilder clientBuilder) {
}
/**
* @return The client configuration.
*/
public ClientConfiguration 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) {
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

@@ -18,6 +18,10 @@ import java.net.URI;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* This class implements the OAuth Authorization Code flow as an authorization * This class implements the OAuth Authorization Code flow as an authorization
* filter. * filter.
@@ -26,6 +30,8 @@ import javax.ws.rs.core.Form;
*/ */
public class OAuthAuthorizationCodeAuthorizationFilter extends OAuthAuthorizationFilter { public class OAuthAuthorizationCodeAuthorizationFilter extends OAuthAuthorizationFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final String code; private final String code;
private final URI redirectUri; private final URI redirectUri;
@@ -68,17 +74,22 @@ public class OAuthAuthorizationCodeAuthorizationFilter extends OAuthAuthorizatio
public OAuthAuthorizationCodeAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String code, URI redirectUri) { public OAuthAuthorizationCodeAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String code, URI redirectUri) {
super(tokenUrl, clientId, clientSecret); super(tokenUrl, clientId, clientSecret);
this.code = code; this.code = StringUtils.trimToNull(code);
this.redirectUri = redirectUri; this.redirectUri = redirectUri;
} }
@Override @Override
protected Form createForm() { protected Form createForm() {
Form form = new Form().param("grant_type", "authorization_code") this.logger.debug("Using OAuth grant_type 'authorization_code'");
.param("code", this.code); Form form = new Form().param("grant_type", "authorization_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,19 @@ 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.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
@@ -33,6 +40,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 +49,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
@@ -79,9 +89,9 @@ public abstract class OAuthAuthorizationFilter implements AuthorizationFilter {
*/ */
public OAuthAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String scope) { public OAuthAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String scope) {
this.tokenUrl = tokenUrl; this.tokenUrl = tokenUrl;
this.clientId = clientId; this.clientId = StringUtils.trimToNull(clientId);
this.clientSecret = clientSecret; this.clientSecret = StringUtils.trimToNull(clientSecret);
this.scope = scope; this.scope = StringUtils.trimToNull(scope);
} }
/** /**
@@ -91,8 +101,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 +142,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();
Response response = target.request().post(entity);
this.logger.debug("Received OAuth response: {}", response.getStatus());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<String, Object> response = target.request().post(entity, Map.class); Map<String, Object> responseMap = response.readEntity(Map.class);
if (response.containsKey("error")) this.logger.trace("Received OAuth response: {}", responseMap);
throw new WebApplicationException((String)response.get("error"), 400);
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 +198,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

@@ -16,6 +16,9 @@ package com.inteligr8.rs;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* This class implements the OAuth Client Credential flow as an authorization * This class implements the OAuth Client Credential flow as an authorization
* filter. * filter.
@@ -24,6 +27,8 @@ import javax.ws.rs.core.Form;
*/ */
public class OAuthClientCredentialAuthorizationFilter extends OAuthAuthorizationFilter { public class OAuthClientCredentialAuthorizationFilter extends OAuthAuthorizationFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** /**
* @param tokenUrl The URL to the OAuth IdP token service. * @param tokenUrl The URL to the OAuth IdP token service.
* @param clientId The ID provided by the OAuth IdP administrator. * @param clientId The ID provided by the OAuth IdP administrator.
@@ -35,7 +40,12 @@ public class OAuthClientCredentialAuthorizationFilter extends OAuthAuthorization
@Override @Override
protected Form createForm() { protected Form createForm() {
this.logger.debug("Using OAuth grant_type 'client_credentials'");
return new Form().param("grant_type", "client_credentials"); return new Form().param("grant_type", "client_credentials");
} }
@Override
protected void extendFormSensitive(Form form) {
}
} }

View File

@@ -16,6 +16,10 @@ package com.inteligr8.rs;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* This class implements the OAuth Password Grant flow as an authorization * This class implements the OAuth Password Grant flow as an authorization
* filter. * filter.
@@ -24,6 +28,8 @@ import javax.ws.rs.core.Form;
*/ */
public class OAuthPasswordGrantAuthorizationFilter extends OAuthAuthorizationFilter { public class OAuthPasswordGrantAuthorizationFilter extends OAuthAuthorizationFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final String username; private final String username;
private final String password; private final String password;
@@ -46,15 +52,20 @@ public class OAuthPasswordGrantAuthorizationFilter extends OAuthAuthorizationFil
*/ */
public OAuthPasswordGrantAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String username, String password) { public OAuthPasswordGrantAuthorizationFilter(String tokenUrl, String clientId, String clientSecret, String username, String password) {
super(tokenUrl, clientId, clientSecret); super(tokenUrl, clientId, clientSecret);
this.username = username; this.username = StringUtils.trimToNull(username);
this.password = password; this.password = StringUtils.trimToNull(password);
} }
@Override @Override
protected Form createForm() { protected Form createForm() {
this.logger.debug("Using OAuth grant_type 'password': {}", this.username);
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);
} }
} }