com.authlete.jakarta.api.AuthleteApiJaxrsImpl Maven / Gradle / Ivy
/*
* Copyright (C) 2014-2022 Authlete, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the
* License.
*/
package com.authlete.jakarta.api;
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.ResponseProcessingException;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.StatusType;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.api.AuthleteApiException;
import com.authlete.common.api.Settings;
import com.authlete.common.conf.AuthleteConfiguration;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public abstract class AuthleteApiJaxrsImpl implements AuthleteApi
{
// "application/json;charset=UTF-8"
private static final MediaType JSON_UTF8_TYPE = APPLICATION_JSON_TYPE.withCharset("UTF-8");
protected interface AuthleteApiCall
{
TResponse call();
}
private final String mBaseUrl;
private final Settings mSettings;
private jakarta.ws.rs.client.Client mJaxRsClient;
private Object mConnectionTimeoutLock = new Object();
private int mCurrentConnectionTimeout;
private Object mReadTimeoutLock = new Object();
private int mCurrentReadTimeout;
private ClientBuilder jaxRsClientBuilder;
private JWK mDpopJwk;
private JWSSigner mJwsSigner;
/**
* The constructor with an instance of {@link AuthleteConfiguration}.
*
*
* The existence of a constructor of this type is a required by
* {@link com.authlete.common.api.AuthleteApiFactory AuthleteApiFactory}.
*
*
* @param configuration
* An instance of {@link AuthleteConfiguration}.
*/
public AuthleteApiJaxrsImpl(AuthleteConfiguration configuration)
{
if (configuration == null)
{
throw new IllegalArgumentException("configuration is null.");
}
mBaseUrl = configuration.getBaseUrl();
extractDpop(configuration); // this has to be done before the credentials calls
mSettings = new Settings();
}
private void extractDpop(AuthleteConfiguration configuration)
{
if (configuration.getDpopKey() != null)
{
try
{
mDpopJwk = JWK.parse(configuration.getDpopKey());
if (mDpopJwk.getAlgorithm() == null)
{
throw new IllegalArgumentException("DPoP JWK must contain an 'alg' field.");
}
mJwsSigner = new DefaultJWSSignerFactory().createJWSSigner(mDpopJwk);
}
catch (ParseException | JOSEException e)
{
throw new IllegalArgumentException("DPoP JWK is not valid.");
}
}
}
/**
* Get an instance of JAX-RS client.
*/
private jakarta.ws.rs.client.Client getJaxRsClient()
{
// If a JAX-RS client has not been created yet.
if (mJaxRsClient == null)
{
// Create a JAX-RS client.
jakarta.ws.rs.client.Client client = createJaxRsClient();
synchronized (this)
{
if (mJaxRsClient == null)
{
mJaxRsClient = client;
}
}
}
// Set a connection timeout.
setConnectionTimeout(mJaxRsClient);
// Set a read timeout.
setReadTimeout(mJaxRsClient);
return mJaxRsClient;
}
/**
* Create an instance of JAX-RS client.
*/
private jakarta.ws.rs.client.Client createJaxRsClient()
{
if (getJaxRsClientBuilder() != null)
{
// if we have a builder configured, use it
return getJaxRsClientBuilder().build();
}
else
{
// otherwise just use the system discovered default
return ClientBuilder.newClient();
}
}
/**
* Set a connection timeout.
*/
private void setConnectionTimeout(jakarta.ws.rs.client.Client client)
{
// The timeout value.
int timeout = mSettings.getConnectionTimeout();
synchronized (mConnectionTimeoutLock)
{
if (mCurrentConnectionTimeout == timeout)
{
// The given value is the same as the current one.
// Let's skip calling property() method.
return;
}
// The given value is different from the current value.
// Let's update the configuration.
mCurrentConnectionTimeout = timeout;
}
// ----------------------------------------------------------------------
// Note that there was no standardized way to set the connection timeout
// before JAX-RS API 2.1 (Java EE 8) (cf. ClientBuilder.connectTimeout).
// ----------------------------------------------------------------------
// Convert int to Integer before calling property() method multiple times
// in order to reduce the number of object creation by autoboxing.
Integer value = Integer.valueOf(timeout);
// For Jersey
client.property("jersey.config.client.connectTimeout", value);
// For Apache CXF
client.property("http.connection.timeout", value);
// For WebSphere (8.5.5.7+)
client.property("com.ibm.ws.jaxrs.client.connection.timeout", value);
}
/**
* Set a read timeout.
*/
private void setReadTimeout(jakarta.ws.rs.client.Client client)
{
// The timeout value.
int timeout = mSettings.getReadTimeout();
synchronized (mReadTimeoutLock)
{
if (mCurrentReadTimeout == timeout)
{
// The given value is the same as the current one.
// Let's skip calling property() method.
return;
}
// The given value is different from the current value.
// Let's update the configuration.
mCurrentReadTimeout = timeout;
}
// ----------------------------------------------------------------------
// Note that there was no standardized way to set the read timeout
// before JAX-RS API 2.1 (Java EE 8) (cf. ClientBuilder.readTimeout).
// ----------------------------------------------------------------------
// Convert int to Integer before calling property() method multiple times
// in order to reduce the number of object creation by autoboxing.
Integer value = Integer.valueOf(timeout);
// For Jersey
client.property("jersey.config.client.readTimeout", value);
// For Apache CXF
client.property("http.receive.timeout", value);
// For WebSphere (8.5.5.7+)
client.property("com.ibm.ws.jaxrs.client.receive.timeout", value);
}
protected WebTarget getTarget()
{
return getJaxRsClient().target(mBaseUrl);
}
protected Invocation.Builder wrapWithDpop(Invocation.Builder target, String path, String method)
{
if (mDpopJwk != null)
{
String htu = mBaseUrl + path;
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.type(new JOSEObjectType("dpop+jwt"))
.jwk(mDpopJwk).build();
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.claim("htm", method)
.claim("htu", htu)
.jwtID(UUID.randomUUID().toString())
.issueTime(new Date())
.build();
JWSObject dpop = new SignedJWT(header, claims);
try
{
dpop.sign(mJwsSigner);
}
catch (JOSEException e)
{
throw createApiException(e, null); // TODO: should this be a better message?
}
return target.header("DPoP", dpop.serialize());
}
else
{
// no DPoP configuration, just pass through the original target
return target;
}
}
/**
* Execute an Authlete API call.
*/
protected TResponse executeApiCall(AuthleteApiCall apiCall) throws AuthleteApiException
{
try
{
// Call the Authlete API.
return apiCall.call();
}
catch (WebApplicationException e)
{
// Throw an exception with HTTP response information.
throw createApiException(e, e.getResponse());
}
catch (ResponseProcessingException e)
{
// Throw an exception with HTTP response information.
throw createApiException(e, e.getResponse());
}
catch (Throwable t)
{
// Throw an exception without HTTP response information.
throw createApiException(t, null);
}
}
/**
* Create an {@link AuthleteApiException} instance.
*/
private AuthleteApiException createApiException(Throwable cause, Response response)
{
// Error message.
String message = cause.getMessage();
if (response == null)
{
// Create an exception without HTTP response information.
return new AuthleteApiException(message, cause);
}
// Status code and status message.
int statusCode = 0;
String statusMessage = null;
// Get the status information.
StatusType type = response.getStatusInfo();
if (type != null)
{
statusCode = type.getStatusCode();
statusMessage = type.getReasonPhrase();
}
// Response body.
String responseBody = null;
// If the response has response body.
if (hasEntity(response))
{
// Get the response body.
responseBody = extractResponseBody(response);
}
// Response headers.
Map> headers = response.getStringHeaders();
// Create an exception with HTTP response information.
return new AuthleteApiException(message, cause, statusCode, statusMessage, responseBody, headers);
}
private boolean hasEntity(Response response)
{
try
{
// True if there is an entity available in the response.
return response.hasEntity();
}
catch (IllegalStateException e)
{
// IllegalStateException is thrown in case the response has been closed.
// A typical error message is "Entity input stream has already been closed."
// Anyway, an entity is not available.
return false;
}
}
private String extractResponseBody(Response response)
{
try
{
// Convert the entity body into a String.
return response.readEntity(String.class);
}
catch (Exception e)
{
// Failed to convert the entity body into a String.
e.printStackTrace();
// Response body is not available.
return null;
}
}
protected TResponse callGetApi(
String auth, String path, Class responseClass, Map params)
{
WebTarget webTarget = getTarget().path(path);
if (params != null)
{
for (Map.Entry param : params.entrySet())
{
webTarget = webTarget.queryParam(param.getKey(), param.getValue());
}
}
return wrapWithDpop(webTarget.request(APPLICATION_JSON_TYPE), path, "GET")
.header(AUTHORIZATION, auth)
.get(responseClass);
}
protected Void callDeleteApi(String auth, String path)
{
wrapWithDpop(getTarget()
.path(path)
.request(), path, "DELETE")
.header(AUTHORIZATION, auth)
.delete();
return null;
}
protected TResponse callPostApi(String auth, String path, Object request, Class responseClass)
{
return wrapWithDpop(getTarget()
.path(path)
.request(APPLICATION_JSON_TYPE), path, "POST")
.header(AUTHORIZATION, auth)
.post(Entity.entity(request, JSON_UTF8_TYPE), responseClass);
}
public ClientBuilder getJaxRsClientBuilder()
{
return jaxRsClientBuilder;
}
public void setJaxRsClientBuilder(ClientBuilder jaxRsClientBuilder)
{
this.jaxRsClientBuilder = jaxRsClientBuilder;
}
public Settings getSettings()
{
return mSettings;
}
protected boolean isDpopEnabled()
{
return mDpopJwk != null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy