feat: implement doctor schedule module, add detail fields to doctor profile, and integrate new appointment API client

This commit is contained in:
2026-07-23 19:34:02 +07:00
parent 107e191e93
commit 6d84979f03
111 changed files with 5138 additions and 288 deletions
+95
View File
@@ -0,0 +1,95 @@
package vn.sis.appointment;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
public record ApiConfig(
String baseUrl,
String tenant,
String clientId,
String clientSecret,
String scope,
String cookieHeader) {
public static ApiConfig fromForm(Map<String, String> form) {
return new ApiConfig(
value(form, "baseUrl", "https://dhs.sisvietnam.vn"),
value(form, "tenant", "3a0f747c-57b6-753b-ba21-3987e8e93b9a"),
value(form, "clientId", "DigitalHealthSolutions_LabConn"),
value(form, "clientSecret", ""),
value(form, "scope", "LabConn"),
value(form, "cookie", ""));
}
public ApiConfig {
baseUrl = trimTrailingSlash(Objects.requireNonNullElse(baseUrl, ""));
tenant = Objects.requireNonNullElse(tenant, "").trim();
clientId = Objects.requireNonNullElse(clientId, "").trim();
clientSecret = Objects.requireNonNullElse(clientSecret, "");
scope = Objects.requireNonNullElse(scope, "").trim();
cookieHeader = Objects.requireNonNullElse(cookieHeader, "").trim();
}
public void validate() {
URI uri;
try {
uri = URI.create(baseUrl);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Base URL khong hop le.", ex);
}
String scheme = uri.getScheme();
if (scheme == null || !(scheme.equalsIgnoreCase("https") || scheme.equalsIgnoreCase("http"))) {
throw new IllegalArgumentException("Base URL phai bat dau bang http:// hoac https://.");
}
if (tenant.isBlank()) {
throw new IllegalArgumentException("Tenant khong duoc de trong.");
}
if (clientId.isBlank()) {
throw new IllegalArgumentException("Client ID khong duoc de trong.");
}
if (clientSecret.isBlank()) {
throw new IllegalArgumentException("Client Secret khong duoc de trong.");
}
if (scope.isBlank()) {
throw new IllegalArgumentException("Scope khong duoc de trong.");
}
}
public String tokenUrl() {
return baseUrl + "/connect/token";
}
public String doctorsUrl() {
return baseUrl + "/api/app/his/doctors/work-schedules";
}
public String scheduleUrl(String doctorCode) {
return baseUrl + "/api/app/his/doctors/" + Urls.encodePathSegment(doctorCode) + "/work-schedule-days";
}
public String effectiveCookieHeader() {
if (!cookieHeader.isBlank()) {
return cookieHeader;
}
return ".AspNetCore.Culture=c%3Den%7Cuic%3Den; __tenant=" + tenant;
}
public String cacheKeyMaterial() {
return String.join("|", baseUrl, tenant, clientId, clientSecret, scope);
}
private static String value(Map<String, String> form, String key, String defaultValue) {
String value = form.get(key);
return value == null ? defaultValue : value;
}
private static String trimTrailingSlash(String value) {
String result = value.trim();
while (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
}
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
package vn.sis.appointment;
public final class Application {
private Application() {
}
public static void main(String[] args) throws Exception {
int port = resolvePort();
AppointmentWebServer server = new AppointmentWebServer(port);
server.start();
System.out.printf("S.I.S Appointment Java Web dang chay tai http://localhost:%d%n", port);
System.out.println("Nhan Ctrl+C de dung ung dung.");
}
private static int resolvePort() {
String value = System.getenv().getOrDefault("PORT", "8080").trim();
try {
int port = Integer.parseInt(value);
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("PORT phai nam trong khoang 1-65535.");
}
return port;
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Bien moi truong PORT khong hop le: " + value, ex);
}
}
}
Binary file not shown.
+155
View File
@@ -0,0 +1,155 @@
package vn.sis.appointment;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.Executors;
final class AppointmentWebServer {
private final HttpServer server;
private final SisApiClient apiClient = new SisApiClient();
AppointmentWebServer(int port) throws IOException {
server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.createContext("/api/health", exchange -> {
if (!requireMethod(exchange, "GET")) {
return;
}
sendJson(exchange, 200, Json.stringify(Map.of(
"ok", true,
"application", "SisAppointmentJavaWeb",
"javaVersion", System.getProperty("java.version"))));
});
server.createContext("/api/token", postHandler((form, config) -> apiClient.checkToken(config)));
server.createContext("/api/doctors", postHandler((form, config) -> apiClient.getDoctors(config)));
server.createContext("/api/schedule", postHandler((form, config) ->
apiClient.getSchedule(config, form.getOrDefault("doctorCode", ""))));
server.createContext("/api/clear-token", postHandler((form, config) -> {
apiClient.clearToken(config);
return new ProxyResponse(true, 200, "OK", "-", "-", 0,
"", "", "", null, "Da xoa access token trong bo nho may chu.");
}));
server.createContext("/", new StaticHandler());
}
void start() {
server.start();
}
private HttpHandler postHandler(ApiOperation operation) {
return exchange -> {
if (!requireMethod(exchange, "POST")) {
return;
}
try {
Map<String, String> form = FormData.read(exchange);
ApiConfig config = ApiConfig.fromForm(form);
ProxyResponse result = operation.execute(form, config);
sendJson(exchange, 200, result.toJson());
} catch (IllegalArgumentException ex) {
ProxyResponse error = new ProxyResponse(false, 400, "Bad Request", "-", "-", 0,
"", "", "", null, ex.getMessage());
sendJson(exchange, 400, error.toJson());
} catch (Exception ex) {
ProxyResponse error = new ProxyResponse(false, 500, "Internal Server Error", "-", "-", 0,
"", "", "", null,
ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
sendJson(exchange, 500, error.toJson());
}
};
}
private static boolean requireMethod(HttpExchange exchange, String expected) throws IOException {
if (exchange.getRequestMethod().equalsIgnoreCase(expected)) {
return true;
}
exchange.getResponseHeaders().set("Allow", expected);
sendJson(exchange, 405, Json.stringify(Map.of(
"ok", false,
"message", "Phuong thuc khong duoc ho tro.")));
return false;
}
private static void sendJson(HttpExchange exchange, int status, String json) throws IOException {
byte[] data = json.getBytes(StandardCharsets.UTF_8);
applySecurityHeaders(exchange.getResponseHeaders());
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.getResponseHeaders().set("Cache-Control", "no-store");
exchange.sendResponseHeaders(status, data.length);
exchange.getResponseBody().write(data);
exchange.close();
}
private static void applySecurityHeaders(Headers headers) {
headers.set("X-Content-Type-Options", "nosniff");
headers.set("X-Frame-Options", "DENY");
headers.set("Referrer-Policy", "no-referrer");
headers.set("Content-Security-Policy",
"default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; connect-src 'self'");
}
@FunctionalInterface
private interface ApiOperation {
ProxyResponse execute(Map<String, String> form, ApiConfig config);
}
private static final class StaticHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) {
exchange.getResponseHeaders().set("Allow", "GET");
exchange.sendResponseHeaders(405, -1);
exchange.close();
return;
}
String path = exchange.getRequestURI().getPath();
if (path.equals("/")) {
path = "/index.html";
}
if (path.contains("..")) {
exchange.sendResponseHeaders(400, -1);
exchange.close();
return;
}
String resource = "web" + path;
try (InputStream input = AppointmentWebServer.class.getClassLoader().getResourceAsStream(resource)) {
if (input == null) {
exchange.sendResponseHeaders(404, -1);
exchange.close();
return;
}
byte[] data = input.readAllBytes();
Headers headers = exchange.getResponseHeaders();
applySecurityHeaders(headers);
headers.set("Content-Type", contentType(path));
headers.set("Cache-Control", path.endsWith("index.html") ? "no-cache" : "public, max-age=3600");
exchange.sendResponseHeaders(200, data.length);
exchange.getResponseBody().write(data);
exchange.close();
}
}
private static String contentType(String path) {
if (path.endsWith(".html")) return "text/html; charset=utf-8";
if (path.endsWith(".css")) return "text/css; charset=utf-8";
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
if (path.endsWith(".svg")) return "image/svg+xml";
if (path.endsWith(".png")) return "image/png";
return "application/octet-stream";
}
}
}
+10
View File
@@ -0,0 +1,10 @@
package vn.sis.appointment;
import java.time.Instant;
record CachedToken(String accessToken, String tokenType, Instant expiresAt) {
boolean isValid() {
return accessToken != null && !accessToken.isBlank()
&& Instant.now().isBefore(expiresAt.minusSeconds(15));
}
}
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
package vn.sis.appointment;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
final class FormData {
private static final int MAX_REQUEST_BYTES = 256 * 1024;
private FormData() {
}
static Map<String, String> read(HttpExchange exchange) throws IOException {
byte[] data = exchange.getRequestBody().readNBytes(MAX_REQUEST_BYTES + 1);
if (data.length > MAX_REQUEST_BYTES) {
throw new IllegalArgumentException("Du lieu gui len vuot qua gioi han 256 KB.");
}
String body = new String(data, StandardCharsets.UTF_8);
Map<String, String> result = new LinkedHashMap<>();
if (body.isBlank()) {
return result;
}
for (String pair : body.split("&")) {
int separator = pair.indexOf('=');
String rawKey = separator >= 0 ? pair.substring(0, separator) : pair;
String rawValue = separator >= 0 ? pair.substring(separator + 1) : "";
result.put(decode(rawKey), decode(rawValue));
}
return result;
}
private static String decode(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
}
}
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
package vn.sis.appointment;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
final class Hashes {
private Hashes() {
}
static String sha256(String value) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("JDK khong ho tro SHA-256.", ex);
}
}
}
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
package vn.sis.appointment;
import java.util.Collection;
import java.util.Map;
final class Json {
private Json() {
}
static String stringify(Object value) {
if (value == null) {
return "null";
}
if (value instanceof String text) {
return quote(text);
}
if (value instanceof Number || value instanceof Boolean) {
return value.toString();
}
if (value instanceof Map<?, ?> map) {
StringBuilder builder = new StringBuilder("{");
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) {
builder.append(',');
}
first = false;
builder.append(quote(String.valueOf(entry.getKey())))
.append(':')
.append(stringify(entry.getValue()));
}
return builder.append('}').toString();
}
if (value instanceof Collection<?> collection) {
StringBuilder builder = new StringBuilder("[");
boolean first = true;
for (Object item : collection) {
if (!first) {
builder.append(',');
}
first = false;
builder.append(stringify(item));
}
return builder.append(']').toString();
}
return quote(value.toString());
}
static String quote(String value) {
StringBuilder builder = new StringBuilder(value.length() + 16).append('"');
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
switch (ch) {
case '"' -> builder.append("\\\"");
case '\\' -> builder.append("\\\\");
case '\b' -> builder.append("\\b");
case '\f' -> builder.append("\\f");
case '\n' -> builder.append("\\n");
case '\r' -> builder.append("\\r");
case '\t' -> builder.append("\\t");
default -> {
if (ch < 0x20) {
builder.append(String.format("\\u%04x", (int) ch));
} else {
builder.append(ch);
}
}
}
}
return builder.append('"').toString();
}
}
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
package vn.sis.appointment;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
record ProxyResponse(
boolean ok,
int httpStatus,
String statusText,
String method,
String url,
int attempts,
String body,
String requestInfo,
String responseHeaders,
Instant tokenExpiresAt,
String message) {
String toJson() {
Map<String, Object> values = new LinkedHashMap<>();
values.put("ok", ok);
values.put("httpStatus", httpStatus);
values.put("statusText", statusText);
values.put("method", method);
values.put("url", url);
values.put("attempts", attempts);
values.put("body", body == null ? "" : body);
values.put("requestInfo", requestInfo == null ? "" : requestInfo);
values.put("responseHeaders", responseHeaders == null ? "" : responseHeaders);
values.put("tokenExpiresAt", tokenExpiresAt == null ? null : tokenExpiresAt.toString());
values.put("message", message == null ? "" : message);
return Json.stringify(values);
}
}
Binary file not shown.
+347
View File
@@ -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;
};
}
}
}
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
package vn.sis.appointment;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class TokenJsonParser {
private static final Pattern ACCESS_TOKEN = Pattern.compile(
"\\\"access_token\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"");
private static final Pattern TOKEN_TYPE = Pattern.compile(
"\\\"token_type\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"");
private static final Pattern EXPIRES_IN = Pattern.compile(
"\\\"expires_in\\\"\\s*:\\s*(\\d+)");
private TokenJsonParser() {
}
static ParsedToken parse(String json) {
String accessToken = group(ACCESS_TOKEN, json);
if (accessToken == null || accessToken.isBlank()) {
throw new IllegalArgumentException("API token khong tra ve access_token hop le.");
}
String tokenType = group(TOKEN_TYPE, json);
String expiresText = group(EXPIRES_IN, json);
long expiresIn = expiresText == null ? 3600 : Long.parseLong(expiresText);
return new ParsedToken(accessToken, tokenType == null ? "Bearer" : tokenType, expiresIn);
}
private static String group(Pattern pattern, String text) {
Matcher matcher = pattern.matcher(text == null ? "" : text);
return matcher.find() ? matcher.group(1) : null;
}
record ParsedToken(String accessToken, String tokenType, long expiresIn) {
}
}
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
package vn.sis.appointment;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
final class Urls {
private Urls() {
}
static String encodePathSegment(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
}
}
Binary file not shown.