348 lines
13 KiB
Java
348 lines
13 KiB
Java
package vn.sis.appointment;
|
|
|
|
import java.io.IOException;
|
|
import java.net.URI;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
final class SisApiClient {
|
|
private final HttpClient httpClient;
|
|
private final Map<String, CachedToken> tokenCache = new ConcurrentHashMap<>();
|
|
|
|
SisApiClient() {
|
|
this.httpClient = HttpClient.newBuilder()
|
|
.connectTimeout(Duration.ofSeconds(30))
|
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
|
.version(HttpClient.Version.HTTP_1_1)
|
|
.build();
|
|
}
|
|
|
|
ProxyResponse checkToken(ApiConfig config) {
|
|
try {
|
|
config.validate();
|
|
TokenResult tokenResult = obtainToken(config, true);
|
|
if (!tokenResult.success()) {
|
|
return tokenResult.proxyResponse();
|
|
}
|
|
|
|
CachedToken token = tokenResult.token();
|
|
String safeBody = Json.stringify(Map.of(
|
|
"access_token", "[da an]",
|
|
"token_type", token.tokenType(),
|
|
"expires_at", token.expiresAt().toString()));
|
|
|
|
ProxyResponse source = tokenResult.proxyResponse();
|
|
return new ProxyResponse(
|
|
true,
|
|
source.httpStatus(),
|
|
source.statusText(),
|
|
source.method(),
|
|
source.url(),
|
|
1,
|
|
safeBody,
|
|
source.requestInfo(),
|
|
source.responseHeaders(),
|
|
token.expiresAt(),
|
|
"Lay access token thanh cong.");
|
|
} catch (IllegalArgumentException ex) {
|
|
return validationError(ex.getMessage());
|
|
} catch (Exception ex) {
|
|
return errorResponse(ex);
|
|
}
|
|
}
|
|
|
|
ProxyResponse getDoctors(ApiConfig config) {
|
|
return authorizedGet(config, config.doctorsUrl());
|
|
}
|
|
|
|
ProxyResponse getSchedule(ApiConfig config, String doctorCode) {
|
|
if (doctorCode == null || doctorCode.isBlank()) {
|
|
return validationError("Ma bac si khong duoc de trong.");
|
|
}
|
|
return authorizedGet(config, config.scheduleUrl(doctorCode.trim()));
|
|
}
|
|
|
|
void clearToken(ApiConfig config) {
|
|
tokenCache.remove(cacheKey(config));
|
|
}
|
|
|
|
private ProxyResponse authorizedGet(ApiConfig config, String url) {
|
|
try {
|
|
config.validate();
|
|
int attempts = 1;
|
|
TokenResult tokenResult = obtainToken(config, false);
|
|
if (!tokenResult.success()) {
|
|
return tokenResult.proxyResponse();
|
|
}
|
|
|
|
CachedToken token = tokenResult.token();
|
|
HttpCall call = sendGet(config, url, token.accessToken());
|
|
if (call.statusCode() == 401) {
|
|
attempts = 2;
|
|
clearToken(config);
|
|
tokenResult = obtainToken(config, true);
|
|
if (!tokenResult.success()) {
|
|
return tokenResult.proxyResponse();
|
|
}
|
|
token = tokenResult.token();
|
|
call = sendGet(config, url, token.accessToken());
|
|
}
|
|
|
|
boolean ok = call.statusCode() >= 200 && call.statusCode() <= 299;
|
|
return new ProxyResponse(
|
|
ok,
|
|
call.statusCode(),
|
|
call.statusText(),
|
|
"GET",
|
|
url,
|
|
attempts,
|
|
call.body(),
|
|
requestInfo(config, "GET", url, true),
|
|
call.responseHeaders(),
|
|
token.expiresAt(),
|
|
ok ? "Goi API thanh cong." : "API tra ve loi HTTP " + call.statusCode() + ".");
|
|
} catch (IllegalArgumentException ex) {
|
|
return validationError(ex.getMessage());
|
|
} catch (Exception ex) {
|
|
return errorResponse(ex);
|
|
}
|
|
}
|
|
|
|
private TokenResult obtainToken(ApiConfig config, boolean forceRefresh)
|
|
throws IOException, InterruptedException {
|
|
String key = cacheKey(config);
|
|
CachedToken cached = tokenCache.get(key);
|
|
if (!forceRefresh && cached != null && cached.isValid()) {
|
|
ProxyResponse cachedResponse = new ProxyResponse(
|
|
true,
|
|
200,
|
|
"OK",
|
|
"CACHE",
|
|
config.tokenUrl(),
|
|
0,
|
|
"",
|
|
"Su dung access token dang con han trong bo nho may chu.",
|
|
"",
|
|
cached.expiresAt(),
|
|
"Token con hieu luc.");
|
|
return new TokenResult(true, cached, cachedResponse);
|
|
}
|
|
|
|
String form = formEncode(Map.of(
|
|
"grant_type", "client_credentials",
|
|
"client_id", config.clientId(),
|
|
"client_secret", config.clientSecret(),
|
|
"scope", config.scope()));
|
|
|
|
HttpRequest request = HttpRequest.newBuilder(URI.create(config.tokenUrl()))
|
|
.timeout(Duration.ofSeconds(60))
|
|
.version(HttpClient.Version.HTTP_1_1)
|
|
.header("__tenant", config.tenant())
|
|
.header("Accept", "application/json")
|
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
.header("User-Agent", "SisAppointmentJavaWeb/1.0")
|
|
.POST(HttpRequest.BodyPublishers.ofString(form, StandardCharsets.UTF_8))
|
|
.build();
|
|
|
|
HttpResponse<String> response = httpClient.send(
|
|
request,
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
|
|
HttpCall call = HttpCall.from(response);
|
|
boolean ok = call.statusCode() >= 200 && call.statusCode() <= 299;
|
|
if (!ok) {
|
|
ProxyResponse proxyResponse = new ProxyResponse(
|
|
false,
|
|
call.statusCode(),
|
|
call.statusText(),
|
|
"POST",
|
|
config.tokenUrl(),
|
|
1,
|
|
call.body(),
|
|
requestInfo(config, "POST", config.tokenUrl(), false),
|
|
call.responseHeaders(),
|
|
null,
|
|
"Khong lay duoc access token.");
|
|
return new TokenResult(false, null, proxyResponse);
|
|
}
|
|
|
|
TokenJsonParser.ParsedToken parsed;
|
|
try {
|
|
parsed = TokenJsonParser.parse(call.body());
|
|
} catch (RuntimeException ex) {
|
|
ProxyResponse proxyResponse = new ProxyResponse(
|
|
false,
|
|
call.statusCode(),
|
|
call.statusText(),
|
|
"POST",
|
|
config.tokenUrl(),
|
|
1,
|
|
limit(call.body()),
|
|
requestInfo(config, "POST", config.tokenUrl(), false),
|
|
call.responseHeaders(),
|
|
null,
|
|
ex.getMessage());
|
|
return new TokenResult(false, null, proxyResponse);
|
|
}
|
|
|
|
long safeLifetime = Math.max(parsed.expiresIn() - 60, 60);
|
|
CachedToken token = new CachedToken(
|
|
parsed.accessToken(),
|
|
parsed.tokenType(),
|
|
Instant.now().plusSeconds(safeLifetime));
|
|
tokenCache.put(key, token);
|
|
|
|
ProxyResponse proxyResponse = new ProxyResponse(
|
|
true,
|
|
call.statusCode(),
|
|
call.statusText(),
|
|
"POST",
|
|
config.tokenUrl(),
|
|
1,
|
|
"",
|
|
requestInfo(config, "POST", config.tokenUrl(), false),
|
|
call.responseHeaders(),
|
|
token.expiresAt(),
|
|
"Lay access token thanh cong.");
|
|
return new TokenResult(true, token, proxyResponse);
|
|
}
|
|
|
|
private HttpCall sendGet(ApiConfig config, String url, String accessToken)
|
|
throws IOException, InterruptedException {
|
|
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
|
|
.timeout(Duration.ofSeconds(60))
|
|
.version(HttpClient.Version.HTTP_1_1)
|
|
.header("__tenant", config.tenant())
|
|
.header("Authorization", "Bearer " + accessToken)
|
|
.header("Accept", "application/json")
|
|
.header("Cache-Control", "no-cache, no-store, max-age=0")
|
|
.header("Pragma", "no-cache")
|
|
.header("User-Agent", "SisAppointmentJavaWeb/1.0")
|
|
.GET();
|
|
|
|
String cookie = config.effectiveCookieHeader();
|
|
if (!cookie.isBlank()) {
|
|
builder.header("Cookie", cookie);
|
|
}
|
|
|
|
HttpResponse<String> response = httpClient.send(
|
|
builder.build(),
|
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
|
return HttpCall.from(response);
|
|
}
|
|
|
|
private static String requestInfo(ApiConfig config, String method, String url, boolean includeCookie) {
|
|
StringBuilder info = new StringBuilder()
|
|
.append(method).append(' ').append(url).append('\n')
|
|
.append("__tenant: ").append(config.tenant()).append('\n')
|
|
.append("Authorization: Bearer [da an]").append('\n');
|
|
|
|
if (method.equals("POST")) {
|
|
info.append("Content-Type: application/x-www-form-urlencoded\n");
|
|
} else {
|
|
info.append("Content-Type: --\n");
|
|
}
|
|
|
|
if (includeCookie) {
|
|
info.append("Cookie: ").append(maskCookie(config.effectiveCookieHeader()));
|
|
}
|
|
return info.toString();
|
|
}
|
|
|
|
private static String maskCookie(String cookie) {
|
|
if (cookie == null || cookie.isBlank()) {
|
|
return "--";
|
|
}
|
|
String[] parts = cookie.split(";");
|
|
for (int i = 0; i < parts.length; i++) {
|
|
String part = parts[i].trim();
|
|
int equals = part.indexOf('=');
|
|
if (equals > 0 && part.substring(0, equals).trim().equalsIgnoreCase("affinity")) {
|
|
parts[i] = " affinity=[da an]";
|
|
}
|
|
}
|
|
return String.join(";", parts).trim();
|
|
}
|
|
|
|
private static String formEncode(Map<String, String> values) {
|
|
StringBuilder result = new StringBuilder();
|
|
for (Map.Entry<String, String> entry : values.entrySet()) {
|
|
if (!result.isEmpty()) {
|
|
result.append('&');
|
|
}
|
|
result.append(java.net.URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
|
|
.append('=')
|
|
.append(java.net.URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
|
|
}
|
|
return result.toString();
|
|
}
|
|
|
|
private static String cacheKey(ApiConfig config) {
|
|
return Hashes.sha256(config.cacheKeyMaterial());
|
|
}
|
|
|
|
private static ProxyResponse validationError(String message) {
|
|
return new ProxyResponse(false, 400, "Bad Request", "-", "-", 0,
|
|
"", "", "", null, message);
|
|
}
|
|
|
|
private static ProxyResponse errorResponse(Exception ex) {
|
|
String message = ex.getMessage() == null || ex.getMessage().isBlank()
|
|
? ex.getClass().getSimpleName()
|
|
: ex.getMessage();
|
|
return new ProxyResponse(false, 500, "Internal Server Error", "-", "-", 0,
|
|
"", "", "", null, message);
|
|
}
|
|
|
|
private static String limit(String value) {
|
|
if (value == null || value.length() <= 4000) {
|
|
return value == null ? "" : value;
|
|
}
|
|
return value.substring(0, 4000) + "...";
|
|
}
|
|
|
|
private record TokenResult(boolean success, CachedToken token, ProxyResponse proxyResponse) {
|
|
}
|
|
|
|
private record HttpCall(int statusCode, String statusText, String body, String responseHeaders) {
|
|
static HttpCall from(HttpResponse<String> response) {
|
|
StringBuilder headers = new StringBuilder();
|
|
response.headers().map().forEach((name, values) ->
|
|
headers.append(name).append(": ").append(String.join(", ", values)).append('\n'));
|
|
return new HttpCall(
|
|
response.statusCode(),
|
|
statusText(response.statusCode()),
|
|
Optional.ofNullable(response.body()).orElse(""),
|
|
headers.toString().trim());
|
|
}
|
|
|
|
private static String statusText(int code) {
|
|
return switch (code) {
|
|
case 200 -> "OK";
|
|
case 201 -> "Created";
|
|
case 204 -> "No Content";
|
|
case 400 -> "Bad Request";
|
|
case 401 -> "Unauthorized";
|
|
case 403 -> "Forbidden";
|
|
case 404 -> "Not Found";
|
|
case 405 -> "Method Not Allowed";
|
|
case 408 -> "Request Timeout";
|
|
case 429 -> "Too Many Requests";
|
|
case 500 -> "Internal Server Error";
|
|
case 502 -> "Bad Gateway";
|
|
case 503 -> "Service Unavailable";
|
|
case 504 -> "Gateway Timeout";
|
|
default -> "HTTP " + code;
|
|
};
|
|
}
|
|
}
|
|
}
|