feat: implement doctor schedule module, add detail fields to doctor profile, and integrate new appointment API client
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user