|
1 | 1 | package com.openelements.cardless; |
2 | 2 |
|
3 | | -import com.google.gson.JsonElement; |
4 | | -import com.google.gson.JsonObject; |
5 | | -import com.google.gson.JsonParser; |
6 | | -import com.openelements.cardless.data.AccessAndRefreshToken; |
7 | | -import com.openelements.cardless.data.AccessToken; |
8 | 3 | import com.openelements.cardless.data.Account; |
9 | 4 | import com.openelements.cardless.data.Balance; |
10 | | -import com.openelements.cardless.data.ErrorMessage; |
11 | 5 | import com.openelements.cardless.data.Institution; |
12 | | -import com.openelements.cardless.data.JsonBasedFactory; |
13 | 6 | import com.openelements.cardless.data.Requisition; |
14 | 7 | import com.openelements.cardless.data.RequisitionsPage; |
15 | 8 | import com.openelements.cardless.data.Transactions; |
| 9 | +import com.openelements.cardless.internal.CardlessClientImpl; |
16 | 10 | import java.io.IOException; |
17 | | -import java.net.URI; |
18 | | -import java.net.http.HttpClient; |
19 | | -import java.net.http.HttpRequest; |
20 | | -import java.net.http.HttpResponse; |
21 | | -import java.net.http.HttpResponse.BodyHandlers; |
22 | 11 | import java.util.List; |
23 | | -import java.util.Objects; |
24 | | -import java.util.concurrent.atomic.AtomicReference; |
25 | 12 | import org.jspecify.annotations.NonNull; |
26 | | -import org.slf4j.Logger; |
27 | | -import org.slf4j.LoggerFactory; |
28 | 13 |
|
29 | | -public class CardlessClient { |
| 14 | +public interface CardlessClient { |
30 | 15 |
|
31 | | - private final static Logger log = LoggerFactory.getLogger(CardlessClient.class); |
32 | | - |
33 | | - private final HttpClient httpClient; |
34 | | - |
35 | | - private AtomicReference<AccessAndRefreshToken> accessAndRefreshTokenRef = new AtomicReference<>(); |
36 | | - |
37 | | - public CardlessClient(@NonNull final String secretId, @NonNull final String secretKey) |
38 | | - throws IOException, InterruptedException { |
39 | | - Objects.requireNonNull(secretId, "secretId must not be null"); |
40 | | - Objects.requireNonNull(secretKey, "secretKey must not be null"); |
41 | | - httpClient = HttpClient.newBuilder().build(); |
42 | | - final JsonObject body = JsonBasedFactory.createReceiveAccessToken(secretId, secretKey); |
43 | | - final JsonElement response = handlePostRequest("https://bankaccountdata.gocardless.com/api/v2/token/new/", |
44 | | - body); |
45 | | - final AccessAndRefreshToken token = JsonBasedFactory.createAccessAndRefreshToken(response); |
46 | | - accessAndRefreshTokenRef.set(token); |
| 16 | + static CardlessClient of(@NonNull final String secretId, @NonNull final String secretKey) |
| 17 | + throws CardlessException { |
| 18 | + return new CardlessClientImpl(secretId, secretKey); |
47 | 19 | } |
48 | 20 |
|
49 | 21 | @NonNull |
50 | | - private static ErrorMessage createFromJson(@NonNull final String json) { |
51 | | - Objects.requireNonNull(json, "json must not be null"); |
52 | | - final JsonElement jsonElement = JsonParser.parseString(json); |
53 | | - if (jsonElement.isJsonObject()) { |
54 | | - final JsonObject jsonObject = jsonElement.getAsJsonObject(); |
55 | | - final String summary = jsonObject.get("summary").getAsString(); |
56 | | - final String detail = jsonObject.get("detail").getAsString(); |
57 | | - final int statusCode = jsonObject.get("status_code").getAsInt(); |
58 | | - return new ErrorMessage(summary, detail, statusCode); |
59 | | - } |
60 | | - throw new IllegalArgumentException("Invalid JSON format: " + json); |
61 | | - } |
| 22 | + RequisitionsPage getRequisitions(final int limit, final int offset) |
| 23 | + throws CardlessException; |
62 | 24 |
|
63 | 25 | @NonNull |
64 | | - private HttpRequest createGetRequest(@NonNull final String url) throws IOException, InterruptedException { |
65 | | - return createGetRequest(url, true); |
66 | | - } |
| 26 | + List<Institution> getInstitutions(@NonNull final String country) throws CardlessException; |
67 | 27 |
|
68 | 28 | @NonNull |
69 | | - private HttpRequest createGetRequest(@NonNull final String url, final boolean checkAccessToken) |
70 | | - throws IOException, InterruptedException { |
71 | | - Objects.requireNonNull(url, "url must not be null"); |
72 | | - if (checkAccessToken) { |
73 | | - checkAccessToken(); |
74 | | - } |
75 | | - final AccessAndRefreshToken accessAndRefreshToken = accessAndRefreshTokenRef.get(); |
76 | | - final HttpRequest.Builder builder = HttpRequest.newBuilder() |
77 | | - .uri(URI.create(url)) |
78 | | - .header("Content-Type", "application/json") |
79 | | - .header("accept", "application/json"); |
80 | | - if (accessAndRefreshToken != null) { |
81 | | - builder.header("Authorization", "Bearer " + accessAndRefreshTokenRef.get().access()); |
82 | | - } |
83 | | - return builder.GET() |
84 | | - .build(); |
85 | | - } |
| 29 | + Requisition createRequisition(@NonNull final Institution institution) |
| 30 | + throws CardlessException; |
86 | 31 |
|
87 | 32 | @NonNull |
88 | | - private HttpRequest createPostRequest(@NonNull final String url, @NonNull JsonElement body, |
89 | | - boolean checkAccessToken) throws IOException, InterruptedException { |
90 | | - Objects.requireNonNull(url, "url must not be null"); |
91 | | - Objects.requireNonNull(body, "body must not be null"); |
92 | | - if (checkAccessToken) { |
93 | | - checkAccessToken(); |
94 | | - } |
95 | | - final AccessAndRefreshToken accessAndRefreshToken = accessAndRefreshTokenRef.get(); |
96 | | - final HttpRequest.Builder builder = HttpRequest.newBuilder() |
97 | | - .uri(URI.create(url)) |
98 | | - .header("Content-Type", "application/json") |
99 | | - .header("accept", "application/json"); |
100 | | - if (accessAndRefreshToken != null) { |
101 | | - builder.header("Authorization", "Bearer " + accessAndRefreshTokenRef.get().access()); |
102 | | - } |
103 | | - return builder.POST(HttpRequest.BodyPublishers.ofString(body.toString())) |
104 | | - .build(); |
105 | | - } |
| 33 | + Requisition createRequisition(@NonNull final String institutionId) throws CardlessException; |
106 | 34 |
|
107 | 35 | @NonNull |
108 | | - private JsonElement handleGetRequest(@NonNull final String url) throws IOException, InterruptedException { |
109 | | - final HttpRequest request = createGetRequest(url); |
110 | | - final HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString()); |
111 | | - if (response.statusCode() != 200) { |
112 | | - try { |
113 | | - final ErrorMessage errorMessage = createFromJson(response.body()); |
114 | | - throw new ApiCallException(errorMessage); |
115 | | - } catch (Exception e) { |
116 | | - throw new IOException("Error in HTTP call", e); |
117 | | - } |
118 | | - } |
119 | | - final String json = response.body(); |
120 | | - try { |
121 | | - return JsonParser.parseString(json); |
122 | | - } catch (Exception e) { |
123 | | - throw new IOException("Error parsing JSON response: " + json, e); |
124 | | - } |
125 | | - } |
| 36 | + Transactions getTransactions(@NonNull final String account) throws CardlessException; |
126 | 37 |
|
127 | 38 | @NonNull |
128 | | - private JsonElement handlePostRequest(@NonNull final String url, @NonNull final JsonElement body) |
129 | | - throws IOException, InterruptedException { |
130 | | - final HttpRequest request = createPostRequest(url, body, false); |
131 | | - final HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString()); |
132 | | - if (response.statusCode() != 200 && response.statusCode() != 201) { |
133 | | - try { |
134 | | - final ErrorMessage errorMessage = createFromJson(response.body()); |
135 | | - throw new ApiCallException(errorMessage); |
136 | | - } catch (Exception e) { |
137 | | - throw new IOException("Error in HTTP call", e); |
138 | | - } |
139 | | - } |
140 | | - final String json = response.body(); |
141 | | - try { |
142 | | - return JsonParser.parseString(json); |
143 | | - } catch (Exception e) { |
144 | | - throw new IOException("Error parsing JSON response: " + json, e); |
145 | | - } |
146 | | - } |
147 | | - |
148 | | - @NonNull |
149 | | - private AccessToken updateAccessToken() throws IOException, InterruptedException { |
150 | | - final JsonObject body = JsonBasedFactory.createUpdateAccessTokenBody(accessAndRefreshTokenRef.get().refresh()); |
151 | | - final JsonElement jsonElement = handlePostRequest( |
152 | | - "https://bankaccountdata.gocardless.com/api/v2/token/refresh/", |
153 | | - body); |
154 | | - return JsonBasedFactory.createAccessToken(jsonElement); |
155 | | - } |
156 | | - |
157 | | - private synchronized void checkAccessToken() throws IOException, InterruptedException { |
158 | | - final AccessAndRefreshToken currentAccessToken = accessAndRefreshTokenRef.get(); |
159 | | - if (currentAccessToken.willExpireShortly()) { |
160 | | - AccessToken newAccessToken = updateAccessToken(); |
161 | | - accessAndRefreshTokenRef.set( |
162 | | - new AccessAndRefreshToken(newAccessToken.access(), newAccessToken.access_expires(), |
163 | | - currentAccessToken.refresh(), currentAccessToken.refreshExpires())); |
164 | | - } |
165 | | - } |
166 | | - |
167 | | - @NonNull |
168 | | - public RequisitionsPage getRequisitions(final int limit, final int offset) |
169 | | - throws IOException, InterruptedException { |
170 | | - log.debug("Fetching requisitions with limit: {}, offset: {}", limit, offset); |
171 | | - final JsonElement jsonElement = handleGetRequest( |
172 | | - "https://bankaccountdata.gocardless.com/api/v2/requisitions/?limit=" + limit + "&offset=" + offset); |
173 | | - log.debug("Received JSON: {}", jsonElement); |
174 | | - return JsonBasedFactory.createRequisitionsPage(jsonElement); |
175 | | - } |
| 39 | + Account getAccount(@NonNull final String id) throws CardlessException; |
176 | 40 |
|
177 | 41 | @NonNull |
178 | | - public List<Institution> getInstitutions(@NonNull final String country) throws IOException, InterruptedException { |
179 | | - Objects.requireNonNull(country, "country must not be null"); |
180 | | - log.debug("Fetching institutions for country: {}", country); |
181 | | - final JsonElement jsonElement = handleGetRequest( |
182 | | - "https://bankaccountdata.gocardless.com/api/v2/institutions/?country=" + country); |
183 | | - log.debug("Received JSON: {}", jsonElement); |
184 | | - return jsonElement.getAsJsonArray().asList().stream() |
185 | | - .map(JsonBasedFactory::createInstitution) |
186 | | - .toList(); |
187 | | - } |
188 | | - |
189 | | - @NonNull |
190 | | - public Requisition createRequisition(@NonNull final Institution institution) |
191 | | - throws IOException, InterruptedException { |
192 | | - return createRequisition(institution.id()); |
193 | | - } |
194 | | - |
195 | | - @NonNull |
196 | | - public Requisition createRequisition(@NonNull final String institutionId) throws IOException, InterruptedException { |
197 | | - Objects.requireNonNull(institutionId, "institutionId must not be null"); |
198 | | - log.debug("Creating requisition for institutionId: {}", institutionId); |
199 | | - final JsonObject body = JsonBasedFactory.createRequisitionRequestBody(institutionId); |
200 | | - final JsonElement jsonElement = handlePostRequest("https://bankaccountdata.gocardless.com/api/v2/requisitions/", |
201 | | - body); |
202 | | - log.debug("Received JSON: {}", jsonElement); |
203 | | - return JsonBasedFactory.createRequisition(jsonElement); |
204 | | - } |
205 | | - |
206 | | - @NonNull |
207 | | - public Transactions getTransactions(@NonNull final String account) throws IOException, InterruptedException { |
208 | | - Objects.requireNonNull(account, "account must not be null"); |
209 | | - log.debug("Fetching transactions for account: {}", account); |
210 | | - final JsonElement jsonElement = handleGetRequest( |
211 | | - "https://bankaccountdata.gocardless.com/api/v2/accounts/" + account + "/transactions/"); |
212 | | - log.debug("Received JSON: {}", jsonElement); |
213 | | - return JsonBasedFactory.createTransactions(jsonElement); |
214 | | - } |
215 | | - |
216 | | - @NonNull |
217 | | - public Account getAccount(@NonNull final String id) throws IOException, InterruptedException { |
218 | | - Objects.requireNonNull(id, "id must not be null"); |
219 | | - log.debug("Fetching account with id: {}", id); |
220 | | - final JsonElement jsonElement = handleGetRequest( |
221 | | - "https://bankaccountdata.gocardless.com/api/v2/accounts/" + id + "/"); |
222 | | - log.debug("Received JSON: {}", jsonElement); |
223 | | - return JsonBasedFactory.createAccount(jsonElement); |
224 | | - } |
225 | | - |
226 | | - @NonNull |
227 | | - public List<Balance> getBalances(@NonNull final String accountId) throws IOException, InterruptedException { |
228 | | - Objects.requireNonNull(accountId, "accountId must not be null"); |
229 | | - log.debug("Fetching balances for accountId: {}", accountId); |
230 | | - final JsonElement jsonElement = handleGetRequest( |
231 | | - "https://bankaccountdata.gocardless.com/api/v2/accounts/" + accountId + "/balances/"); |
232 | | - log.debug("Received JSON: {}", jsonElement); |
233 | | - return JsonBasedFactory.createBalances(jsonElement); |
234 | | - } |
| 42 | + List<Balance> getBalances(@NonNull final String accountId) throws CardlessException; |
235 | 43 | } |
0 commit comments