Skip to content

Commit

Permalink
[ACL-263] Caching improvements (#337)
Browse files Browse the repository at this point in the history
  • Loading branch information
dili91 authored Jan 22, 2025
1 parent ecc1bb9 commit fe685e2
Show file tree
Hide file tree
Showing 23 changed files with 604 additions and 212 deletions.
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Please select multiple options if required.
# Checklist:

- [ ] I have updated the `gradle.properties` file with the new version
- [ ] I have updated the `CHANGELOG.md` file with the details of the new versio
- [ ] I have updated the `CHANGELOG.md` file with the details of the new version
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code where necessary
Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]
## [17.0.0] - 2025-01-15
### Changed
* ⚠️ Breaking: removed deprecated HPP link builder
* ⚠️ Breaking: Aligned custom cache implementation to other officially supported client libraries

## [16.3.1] - 2025-01-10
### Changed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Main properties
group=com.truelayer
archivesBaseName=truelayer-java
version=16.3.1
version=17.0.0

# Artifacts properties
sonatype_repository_url=https://s01.oss.sonatype.org/service/local/
Expand Down
9 changes: 0 additions & 9 deletions src/main/java/com/truelayer/java/ITrueLayerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.truelayer.java.auth.IAuthenticationHandler;
import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest;
import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse;
import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder;
import com.truelayer.java.http.entities.ApiResponse;
import com.truelayer.java.mandates.IMandatesHandler;
import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler;
Expand Down Expand Up @@ -60,14 +59,6 @@ public interface ITrueLayerClient {
*/
ISignupPlusHandler signupPlus();

/**
* Entrypoint for Hosted Payment Page related services.
* @return a utility to build a Hosted Payment Page URL.
* @deprecated use {@link #hppLinkBuilder()} instead.
*/
@Deprecated
IHostedPaymentPageLinkBuilder hpp();

/**
* Entrypoint for Hosted Payment Page related services.
* @return a utility to build a Hosted Payment Page URL.
Expand Down
19 changes: 0 additions & 19 deletions src/main/java/com/truelayer/java/TrueLayerClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.truelayer.java.commonapi.ICommonHandler;
import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnRequest;
import com.truelayer.java.commonapi.entities.SubmitPaymentsProviderReturnResponse;
import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder;
import com.truelayer.java.http.entities.ApiResponse;
import com.truelayer.java.mandates.IMandatesHandler;
import com.truelayer.java.merchantaccounts.IMerchantAccountsHandler;
Expand Down Expand Up @@ -33,23 +32,14 @@ public class TrueLayerClient implements ITrueLayerClient {
private IPayoutsHandler payoutsHandler;
private ISignupPlusHandler signupPlusHandler;
private ICommonHandler commonHandler;

/**
* @deprecated - use {@link #hostedPaymentPageLinkBuilder} instead.
*/
@Deprecated
private IHostedPaymentPageLinkBuilder hppLinkBuilder;

private HostedPaymentPageLinkBuilder hostedPaymentPageLinkBuilder;

public TrueLayerClient(
IAuthenticationHandler authenticationHandler,
IHostedPaymentPageLinkBuilder hostedPaymentPageLinkBuilder,
ICommonHandler commonHandler,
ISignupPlusHandler signupPlusHandler,
HostedPaymentPageLinkBuilder hppLinkBuilder) {
this.authenticationHandler = authenticationHandler;
this.hppLinkBuilder = hostedPaymentPageLinkBuilder;
this.commonHandler = commonHandler;
this.signupPlusHandler = signupPlusHandler;
this.hostedPaymentPageLinkBuilder = hppLinkBuilder;
Expand Down Expand Up @@ -128,15 +118,6 @@ public ISignupPlusHandler signupPlus() {
return signupPlusHandler;
}

/**
* {@inheritDoc}
*/
@Override
@Deprecated
public IHostedPaymentPageLinkBuilder hpp() {
return this.hppLinkBuilder;
}

@Override
public HostedPaymentPageLinkBuilder hppLinkBuilder() {
return this.hostedPaymentPageLinkBuilder;
Expand Down
17 changes: 7 additions & 10 deletions src/main/java/com/truelayer/java/TrueLayerClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@
import com.truelayer.java.commonapi.ICommonApi;
import com.truelayer.java.commonapi.ICommonHandler;
import com.truelayer.java.entities.RequestScopes;
import com.truelayer.java.hpp.IHostedPaymentPageLinkBuilder;
import com.truelayer.java.http.OkHttpClientFactory;
import com.truelayer.java.http.RetrofitFactory;
import com.truelayer.java.http.auth.cache.ICredentialsCache;
import com.truelayer.java.http.auth.cache.SimpleCredentialsCache;
import com.truelayer.java.http.auth.cache.InMemoryCredentialsCache;
import com.truelayer.java.http.interceptors.logging.DefaultLogConsumer;
import com.truelayer.java.mandates.IMandatesApi;
import com.truelayer.java.mandates.IMandatesHandler;
Expand Down Expand Up @@ -72,6 +71,9 @@ public class TrueLayerClientBuilder {

private Consumer<String> logMessageConsumer;

/**
* Holder for the cache implementation. Null if caching is disabled
*/
private ICredentialsCache credentialsCache;

private ProxyConfiguration proxyConfiguration;
Expand Down Expand Up @@ -182,12 +184,13 @@ public TrueLayerClientBuilder withHttpLogs(Consumer<String> logConsumer) {
* @return the instance of the client builder used
*/
public TrueLayerClientBuilder withCredentialsCaching() {
this.credentialsCache = new SimpleCredentialsCache(Clock.systemUTC());
this.credentialsCache = new InMemoryCredentialsCache(Clock.systemUTC());
return this;
}

/**
* Utility to enable a custom cache for Oauth credentials.
* @param credentialsCache the custom cache implementation
* @return the instance of the client builder used
*/
public TrueLayerClientBuilder withCredentialsCaching(ICredentialsCache credentialsCache) {
Expand Down Expand Up @@ -228,10 +231,6 @@ public TrueLayerClient build() {
.httpClient(RetrofitFactory.build(authServerApiHttpClient, environment.getAuthApiUri()))
.build();

IHostedPaymentPageLinkBuilder hppLinkBuilder = com.truelayer.java.hpp.HostedPaymentPageLinkBuilder.New()
.uri(environment.getHppUri())
.build();

// We're reusing a client with only User agent and Idempotency key interceptors and give it our base payment
// endpoint
ICommonApi commonApi = RetrofitFactory.build(authServerApiHttpClient, environment.getPaymentsApiUri())
Expand All @@ -241,7 +240,7 @@ public TrueLayerClient build() {
// We're building a client which has the authentication handler and the options to cache the token.
// this one represents the baseline for the client used for Signup+ and Payments
OkHttpClient authenticatedApiClient = httpClientFactory.buildAuthenticatedApiClient(
authServerApiHttpClient, authenticationHandler, credentialsCache);
clientCredentials.clientId, authServerApiHttpClient, authenticationHandler, credentialsCache);
ISignupPlusApi signupPlusApi = RetrofitFactory.build(authenticatedApiClient, environment.getPaymentsApiUri())
.create(ISignupPlusApi.class);
SignupPlusHandler.SignupPlusHandlerBuilder signupPlusHandlerBuilder =
Expand All @@ -256,7 +255,6 @@ public TrueLayerClient build() {
if (isEmpty(signingOptions)) {
return new TrueLayerClient(
authenticationHandler,
hppLinkBuilder,
commonHandler,
signupPlusHandler,
new HostedPaymentPageLinkBuilder(environment));
Expand Down Expand Up @@ -324,7 +322,6 @@ public TrueLayerClient build() {
payoutsHandler,
signupPlusHandler,
commonHandler,
hppLinkBuilder,
new HostedPaymentPageLinkBuilder(environment));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public OkHttpClient buildAuthServerApiClient(OkHttpClient baseHttpClient, Client
}

public OkHttpClient buildAuthenticatedApiClient(
String clientId,
OkHttpClient authServerApiClient,
IAuthenticationHandler authenticationHandler,
ICredentialsCache credentialsCache) {
Expand All @@ -118,7 +119,7 @@ public OkHttpClient buildAuthenticatedApiClient(
OkHttpClient.Builder authenticatedApiClientBuilder = authServerApiClient.newBuilder();

AccessTokenManager.AccessTokenManagerBuilder accessTokenManagerBuilder =
AccessTokenManager.builder().authenticationHandler(authenticationHandler);
AccessTokenManager.builder().clientId(clientId).authenticationHandler(authenticationHandler);

// setup credentials caching if required
if (isNotEmpty(credentialsCache)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.truelayer.java.auth.IAuthenticationHandler;
import com.truelayer.java.auth.entities.AccessToken;
import com.truelayer.java.entities.RequestScopes;
import com.truelayer.java.http.auth.cache.CredentialsCacheHelper;
import com.truelayer.java.http.auth.cache.ICredentialsCache;
import com.truelayer.java.http.entities.ApiResponse;
import java.util.Optional;
Expand All @@ -13,6 +14,8 @@
@Builder
public class AccessTokenManager implements IAccessTokenManager {

private final String clientId;

private final IAuthenticationHandler authenticationHandler;

private final ICredentialsCache credentialsCache;
Expand All @@ -23,10 +26,12 @@ private Optional<ICredentialsCache> getCredentialsCache() {

@Override
public AccessToken getToken(RequestScopes scopes) {
String cacheKey = CredentialsCacheHelper.buildKey(clientId, scopes);

if (getCredentialsCache().isPresent()) {
return getCredentialsCache().get().getToken(scopes).orElseGet(() -> {
return getCredentialsCache().get().getToken(cacheKey).orElseGet(() -> {
AccessToken token = tryGetToken(scopes);
credentialsCache.storeToken(scopes, token);
credentialsCache.storeToken(cacheKey, token);
return token;
});
}
Expand All @@ -37,7 +42,8 @@ public AccessToken getToken(RequestScopes scopes) {
@Override
@Synchronized
public void invalidateToken(RequestScopes scopes) {
getCredentialsCache().ifPresent(iCredentialsCache -> iCredentialsCache.clearToken(scopes));
String cacheKey = CredentialsCacheHelper.buildKey(clientId, scopes);
getCredentialsCache().ifPresent(iCredentialsCache -> iCredentialsCache.clearToken(cacheKey));
}

private AccessToken tryGetToken(RequestScopes scopes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.truelayer.java.http.auth.cache;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.lang3.ObjectUtils.isEmpty;

import com.truelayer.java.TrueLayerException;
import com.truelayer.java.entities.RequestScopes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CredentialsCacheHelper {
private static final String CACHE_KEY_PREFIX = "tl-auth-token";
private static final String SCOPES_DELIMITER = ",";

public static String buildKey(String clientId, RequestScopes requestScopes) {
if (isEmpty(clientId) || isEmpty(requestScopes) || isEmpty(requestScopes.getScopes())) {
throw new TrueLayerException("Invalid client id or request scopes provided");
}

List<String> scopes = new ArrayList<>(requestScopes.getScopes());

// Use natural ordering to make ordering not significant
Collections.sort(scopes);

byte[] md5InBytes = digest(String.join(SCOPES_DELIMITER, scopes).getBytes(UTF_8));
return MessageFormat.format("{0}:{1}:{2}", CACHE_KEY_PREFIX, clientId, bytesToHex(md5InBytes));
}

private static byte[] digest(byte[] input) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new TrueLayerException("Hashing algorithm is not available", e);
}
return md.digest(input);
}

private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
package com.truelayer.java.http.auth.cache;

import com.truelayer.java.auth.entities.AccessToken;
import com.truelayer.java.entities.RequestScopes;
import java.util.Optional;

public interface ICredentialsCache {

/**
* Gets the cached access token for the given request scopes.
* @param scopes the requested scopes
* @return an optional access token. If the token is expired an empty optional is returned
*/
Optional<AccessToken> getToken(RequestScopes scopes);
Optional<AccessToken> getToken(String key);

/**
* Stores an access token in cache for the given request scopes.
* @param token the new token to store
* @param scopes the requested scopes
*/
void storeToken(RequestScopes scopes, AccessToken token);
void storeToken(String key, AccessToken token);

/**
* Remove the entry in the cache for the given request scopes.
* @param scopes the requested scopes
*/
void clearToken(RequestScopes scopes);
void clearToken(String key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.truelayer.java.http.auth.cache;

import com.truelayer.java.auth.entities.AccessToken;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class InMemoryCredentialsCache implements ICredentialsCache {

private final Clock clock;

/**
* internal state
*/
private final Map<String, AccessTokenRecord> tokenRecords = new ConcurrentHashMap<>();

@Override
public Optional<AccessToken> getToken(String key) {
AccessTokenRecord tokenRecord = tokenRecords.get(key);
if (tokenRecord == null || !LocalDateTime.now(clock).isBefore(tokenRecord.expiresAt)) {
return Optional.empty();
}

return Optional.of(tokenRecord.token);
}

@Override
public void storeToken(String key, AccessToken token) {
AccessTokenRecord tokenRecord =
new AccessTokenRecord(token, LocalDateTime.now(clock).plusSeconds(token.getExpiresIn()));

tokenRecords.put(key, tokenRecord);
}

@Override
public void clearToken(String key) {
tokenRecords.remove(key);
}

@RequiredArgsConstructor
public static class AccessTokenRecord {
private final AccessToken token;
private final LocalDateTime expiresAt;
}
}
Loading

0 comments on commit fe685e2

Please sign in to comment.