Skip to content

Commit 4f8e8c9

Browse files
committed
feat: try to refactor ApiClient
1 parent 1d133b7 commit 4f8e8c9

11 files changed

Lines changed: 666 additions & 518 deletions

File tree

‎src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java‎

Lines changed: 29 additions & 512 deletions
Large diffs are not rendered by default.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.DesktopApplicationClientJava.Api;
2+
3+
import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse;
4+
import com.DesktopApplicationClientJava.Api.Dto.request.Auth.RefreshTokenRequest;
5+
import com.DesktopApplicationClientJava.Api.Dto.response.Jwt.JwtTokenResponse;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import java.io.IOException;
8+
import java.net.URI;
9+
import java.net.http.HttpClient;
10+
import java.net.http.HttpRequest;
11+
import java.net.http.HttpResponse;
12+
import lombok.AllArgsConstructor;
13+
import lombok.Setter;
14+
15+
@Setter
16+
@AllArgsConstructor
17+
public class HttpController {
18+
public final String baseUrl;
19+
public final HttpClient httpClient;
20+
public final ObjectMapper objectMapper;
21+
public String refreshToken;
22+
public String accessToken;
23+
24+
// Парсер ответа
25+
public <T> T parseResponse(HttpResponse<String> response, Class<T> clazz) throws IOException {
26+
if (response.body() == null || response.body().isBlank()) {
27+
return null;
28+
}
29+
return objectMapper.readValue(response.body(), clazz);
30+
}
31+
32+
public HttpResponse<String> send(HttpRequest request) throws IOException, InterruptedException {
33+
return executeWithRetry(request, HttpResponse.BodyHandlers.ofString());
34+
}
35+
36+
public <T> HttpResponse<T> executeWithRetry(
37+
HttpRequest request, HttpResponse.BodyHandler<T> handler)
38+
throws IOException, InterruptedException {
39+
40+
HttpResponse<T> response = httpClient.send(request, handler);
41+
42+
if (response.statusCode() != 401) return response;
43+
44+
if (request.uri().getPath().startsWith("/api/v1/auth/")) return response;
45+
46+
if (refreshToken == null) return response;
47+
48+
try {
49+
refreshInternal();
50+
} catch (Exception e) {
51+
// refresh не удался — отдаём оригинальный 401, вызывающий код разберётся
52+
return response;
53+
}
54+
55+
// повторяем исходный запрос с новым access-токеном
56+
HttpRequest retried =
57+
HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Authorization"))
58+
.header("Authorization", "Bearer " + accessToken)
59+
.build();
60+
61+
return httpClient.send(retried, handler);
62+
}
63+
64+
// Проверка ошибок сервера
65+
public void checkError(HttpResponse<String> response) {
66+
if (response.statusCode() < 400) return;
67+
try {
68+
ErrorResponse error = objectMapper.readValue(response.body(), ErrorResponse.class);
69+
throw new ApiException(error);
70+
} catch (ApiException e) {
71+
throw e;
72+
} catch (Exception parseFail) {
73+
throw new ApiException(
74+
new ErrorResponse("Ошибка сервера: " + response.body(), response.statusCode()));
75+
}
76+
}
77+
78+
public JwtTokenResponse refresh() throws IOException, InterruptedException {
79+
refreshInternal();
80+
return new JwtTokenResponse(accessToken, refreshToken);
81+
}
82+
83+
private void refreshInternal() throws IOException, InterruptedException {
84+
if (refreshToken == null) {
85+
throw new ApiException(new ErrorResponse("Ошибка: нет refresh токена", 401));
86+
}
87+
88+
String jsonBody = objectMapper.writeValueAsString(new RefreshTokenRequest(refreshToken));
89+
HttpRequest request =
90+
HttpRequest.newBuilder()
91+
.uri(URI.create(baseUrl + "/api/v1/auth/refresh"))
92+
.header("Content-Type", "application/json")
93+
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
94+
.build();
95+
96+
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
97+
98+
if (response.statusCode() >= 400) {
99+
throw new ApiException(
100+
new ErrorResponse("Refresh failed: " + response.body(), response.statusCode()));
101+
}
102+
103+
JwtTokenResponse tokens = objectMapper.readValue(response.body(), JwtTokenResponse.class);
104+
this.accessToken = tokens.accessToken();
105+
this.refreshToken = tokens.refreshToken();
106+
}
107+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.DesktopApplicationClientJava.Api.handlers;
2+
3+
import com.DesktopApplicationClientJava.Api.Dto.request.Auth.LoginRequest;
4+
import com.DesktopApplicationClientJava.Api.Dto.request.Auth.RefreshTokenRequest;
5+
import com.DesktopApplicationClientJava.Api.Dto.response.Jwt.JwtTokenResponse;
6+
import com.DesktopApplicationClientJava.Api.HttpController;
7+
import java.io.IOException;
8+
import java.net.URI;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
12+
public class AuthHandler extends Handler {
13+
public AuthHandler(HttpController httpController) {
14+
super(httpController);
15+
}
16+
17+
// Вход в систему
18+
public JwtTokenResponse login(LoginRequest request) throws IOException, InterruptedException {
19+
String jsonBody = httpController.objectMapper.writeValueAsString(request);
20+
HttpRequest httpRequest =
21+
HttpRequest.newBuilder()
22+
.uri(URI.create(httpController.baseUrl + "/api/v1/auth/login"))
23+
.header("Content-Type", "application/json")
24+
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
25+
.build();
26+
HttpResponse<String> response = httpController.send(httpRequest);
27+
httpController.checkError(response);
28+
JwtTokenResponse tokenResponse = httpController.parseResponse(response, JwtTokenResponse.class);
29+
httpController.accessToken = tokenResponse.accessToken();
30+
httpController.refreshToken = tokenResponse.refreshToken();
31+
return tokenResponse;
32+
}
33+
34+
// Выход из системы
35+
public void logout() throws IOException, InterruptedException {
36+
if (httpController.accessToken == null) {
37+
return;
38+
}
39+
String jsonBody =
40+
httpController.objectMapper.writeValueAsString(
41+
new RefreshTokenRequest(httpController.refreshToken));
42+
HttpRequest httpRequest =
43+
HttpRequest.newBuilder()
44+
.uri(URI.create(httpController.baseUrl + "/api/v1/auth/logout"))
45+
.header("Content-Type", "application/json")
46+
.header("Authorization", "Bearer " + httpController.accessToken)
47+
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
48+
.build();
49+
HttpResponse<String> response = httpController.send(httpRequest);
50+
httpController.checkError(response);
51+
httpController.accessToken = null;
52+
httpController.refreshToken = null;
53+
}
54+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.DesktopApplicationClientJava.Api.handlers;
2+
3+
import com.DesktopApplicationClientJava.Api.ApiException;
4+
import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse;
5+
import com.DesktopApplicationClientJava.Api.HttpController;
6+
import java.io.IOException;
7+
import java.net.URI;
8+
import java.net.http.HttpRequest;
9+
import java.net.http.HttpResponse;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.UUID;
12+
import lombok.Setter;
13+
14+
@Setter
15+
public class ExportHandler extends Handler {
16+
public ExportHandler(HttpController httpController) {
17+
super(httpController);
18+
}
19+
20+
// Выгрузка данных (пользователей, документов и их файлов)
21+
public byte[] exportUsers() throws IOException, InterruptedException {
22+
return downloadBinary("/api/v1/admin/users/export");
23+
}
24+
25+
public byte[] exportResources() throws IOException, InterruptedException {
26+
return downloadBinary("/api/v1/admin/resource/export");
27+
}
28+
29+
public byte[] exportResourceFiles(UUID resourceId) throws IOException, InterruptedException {
30+
return downloadBinary("/api/v1/admin/resource/" + resourceId + "/files/export");
31+
}
32+
33+
private byte[] downloadBinary(String path) throws IOException, InterruptedException {
34+
HttpRequest httpRequest =
35+
HttpRequest.newBuilder()
36+
.uri(URI.create(httpController.baseUrl + path))
37+
.header("Authorization", "Bearer " + httpController.accessToken)
38+
.GET()
39+
.build();
40+
41+
HttpResponse<byte[]> response =
42+
httpController.executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofByteArray());
43+
44+
if (response.statusCode() >= 400) {
45+
String body = new String(response.body(), StandardCharsets.UTF_8);
46+
try {
47+
ErrorResponse error = httpController.objectMapper.readValue(body, ErrorResponse.class);
48+
throw new ApiException(error);
49+
} catch (ApiException e) {
50+
throw e;
51+
} catch (Exception parseFail) {
52+
throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode()));
53+
}
54+
}
55+
return response.body();
56+
}
57+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.DesktopApplicationClientJava.Api.handlers;
2+
3+
import com.DesktopApplicationClientJava.Api.ApiException;
4+
import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse;
5+
import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
6+
import com.DesktopApplicationClientJava.Api.HttpController;
7+
import java.io.ByteArrayOutputStream;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.net.URI;
11+
import java.net.URLDecoder;
12+
import java.net.http.HttpRequest;
13+
import java.net.http.HttpResponse;
14+
import java.nio.charset.StandardCharsets;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.util.Optional;
18+
import java.util.UUID;
19+
import lombok.Setter;
20+
21+
@Setter
22+
public class FileHandler extends Handler {
23+
public FileHandler(HttpController httpController) {
24+
super(httpController);
25+
}
26+
27+
public FileDto uploadFile(UUID resourceUuid, Path filePath)
28+
throws IOException, InterruptedException {
29+
String boundary = "----JavaPksBoundary" + System.currentTimeMillis();
30+
byte[] fileBytes = Files.readAllBytes(filePath);
31+
String filename = filePath.getFileName().toString();
32+
String contentType =
33+
Optional.ofNullable(Files.probeContentType(filePath)).orElse("application/octet-stream");
34+
35+
ByteArrayOutputStream body = new ByteArrayOutputStream();
36+
37+
body.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
38+
body.write(
39+
("Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n")
40+
.getBytes(StandardCharsets.UTF_8));
41+
body.write(("Content-Type: " + contentType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
42+
body.write(fileBytes);
43+
body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
44+
45+
HttpRequest httpRequest =
46+
HttpRequest.newBuilder()
47+
.uri(URI.create(httpController.baseUrl + "/api/v1/resource/" + resourceUuid + "/file"))
48+
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
49+
.header("Authorization", "Bearer " + httpController.accessToken)
50+
.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
51+
.build();
52+
53+
HttpResponse<String> response = httpController.send(httpRequest);
54+
httpController.checkError(response);
55+
return httpController.parseResponse(response, FileDto.class);
56+
}
57+
58+
public Path downloadFile(UUID resourceUuid, UUID fileUuid, Path destinationDir)
59+
throws IOException, InterruptedException {
60+
61+
HttpRequest httpRequest =
62+
HttpRequest.newBuilder()
63+
.uri(
64+
URI.create(
65+
httpController.baseUrl
66+
+ "/api/v1/resource/"
67+
+ resourceUuid
68+
+ "/file/"
69+
+ fileUuid))
70+
.header("Authorization", "Bearer " + httpController.accessToken)
71+
.GET()
72+
.build();
73+
74+
HttpResponse<InputStream> response =
75+
httpController.executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
76+
77+
if (response.statusCode() >= 400) {
78+
String body = new String(response.body().readAllBytes(), StandardCharsets.UTF_8);
79+
response.body().close();
80+
try {
81+
ErrorResponse error = httpController.objectMapper.readValue(body, ErrorResponse.class);
82+
throw new ApiException(error);
83+
} catch (ApiException e) {
84+
throw e;
85+
} catch (Exception parseFail) {
86+
throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode()));
87+
}
88+
}
89+
90+
String filename =
91+
parseFilename(response.headers().firstValue("Content-Disposition").orElse(null));
92+
if (filename == null || filename.isBlank()) {
93+
filename = fileUuid.toString();
94+
}
95+
96+
Files.createDirectories(destinationDir);
97+
Path target = destinationDir.resolve(filename);
98+
99+
try (InputStream in = response.body()) {
100+
Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
101+
}
102+
return target;
103+
}
104+
105+
public void deleteFile(UUID resourceUuid, UUID fileUuid)
106+
throws IOException, InterruptedException {
107+
HttpRequest httpRequest =
108+
HttpRequest.newBuilder()
109+
.uri(
110+
URI.create(
111+
httpController.baseUrl
112+
+ "/api/v1/resource/"
113+
+ resourceUuid
114+
+ "/file/"
115+
+ fileUuid))
116+
.header("Authorization", "Bearer " + httpController.accessToken)
117+
.DELETE()
118+
.build();
119+
HttpResponse<String> response = httpController.send(httpRequest);
120+
httpController.checkError(response);
121+
}
122+
123+
private static String parseFilename(String cd) {
124+
if (cd == null) return null;
125+
126+
int starIdx = cd.indexOf("filename*=");
127+
if (starIdx >= 0) {
128+
String val = cd.substring(starIdx + "filename*=".length()).trim();
129+
int semi = val.indexOf(';');
130+
if (semi >= 0) val = val.substring(0, semi).trim();
131+
int encIdx = val.indexOf("''");
132+
if (encIdx >= 0) {
133+
String encoded = val.substring(encIdx + 2);
134+
return URLDecoder.decode(encoded, StandardCharsets.UTF_8);
135+
}
136+
}
137+
138+
int idx = cd.indexOf("filename=");
139+
if (idx >= 0) {
140+
String val = cd.substring(idx + "filename=".length()).trim();
141+
if (val.startsWith("\"")) {
142+
int end = val.indexOf('"', 1);
143+
if (end > 0) return val.substring(1, end);
144+
} else {
145+
int semi = val.indexOf(';');
146+
if (semi >= 0) val = val.substring(0, semi);
147+
return val.trim();
148+
}
149+
}
150+
return null;
151+
}
152+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.DesktopApplicationClientJava.Api.handlers;
2+
3+
import com.DesktopApplicationClientJava.Api.HttpController;
4+
import java.net.URLEncoder;
5+
import java.nio.charset.StandardCharsets;
6+
7+
public abstract class Handler {
8+
protected final HttpController httpController;
9+
10+
protected Handler(HttpController httpController) {
11+
this.httpController = httpController;
12+
}
13+
14+
public String encode(String value) {
15+
return URLEncoder.encode(value, StandardCharsets.UTF_8);
16+
}
17+
}

0 commit comments

Comments
 (0)