Files
sisvietnamvn_01/appointment/SisApiClient.java
T

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;
};
}
}
}