orquestador/demo/providers-mock/MockServer.java

127 lines
7.5 KiB
Java

import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.util.*;
import java.util.concurrent.*;
/** Synthetic demo provider. No database, provider calls or request-body logging. */
public final class MockServer {
static final Set<String> NAMES = Set.of("clientes_consulta", "buro_consulta", "documento_valida", "documents_metadata");
static final int MAX_BYTES = 262144;
final Path data;
final String token;
final Map<String, Long> calls = new HashMap<>();
record Reply(int status, int delay, String body) {}
MockServer(Path data, String token) throws IOException {
this.data = data; this.token = token;
Files.createDirectories(data);
for (String name : NAMES) {
calls.put(name, 0L);
if (!Files.exists(data.resolve(name + ".properties"))) {
String body = switch (name) {
case "clientes_consulta" -> "{\"found\":true,\"customerId\":\"synthetic-customer\"}";
case "buro_consulta" -> "{\"score\":71,\"consultationId\":\"synthetic-consultation\"}";
case "documento_valida" -> "{\"status\":\"VALID\",\"reasonCodes\":[]}";
default -> "{\"error\":\"Configure metadata for this request and document before use\"}";
};
save(name, new Reply(name.equals("documents_metadata") ? 404 : 200, 0, body));
}
}
}
synchronized Reply read(String name) throws IOException {
Properties p = new Properties();
try (Reader r = Files.newBufferedReader(data.resolve(name + ".properties"))) { p.load(r); }
return checked(p.getProperty("status"), p.getProperty("delayMs"), p.getProperty("body", ""));
}
static Reply checked(String status, String delay, String body) {
int s = Integer.parseInt(status), d = Integer.parseInt(delay);
if (s < 200 || s > 599 || s == 204 || s == 205 || s == 304 || d < 0 || d > 60000
|| body.getBytes(StandardCharsets.UTF_8).length > MAX_BYTES) throw new IllegalArgumentException();
return new Reply(s, d, body);
}
synchronized void save(String name, Reply reply) throws IOException {
Properties p = new Properties();
p.setProperty("status", Integer.toString(reply.status()));
p.setProperty("delayMs", Integer.toString(reply.delay())); p.setProperty("body", reply.body());
Path tmp = Files.createTempFile(data, name, ".tmp");
try {
try (Writer w = Files.newBufferedWriter(tmp)) { p.store(w, "Demo response; updated via admin API"); }
Files.move(tmp, data.resolve(name + ".properties"), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} finally { Files.deleteIfExists(tmp); }
}
synchronized long count(String name, boolean increment) {
if (increment) calls.put(name, calls.get(name) + 1);
return calls.get(name);
}
static void send(HttpExchange x, int status, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
x.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
x.getResponseHeaders().set("Cache-Control", "no-store");
x.sendResponseHeaders(status, bytes.length);
x.getResponseBody().write(bytes);
}
void handle(HttpExchange x) throws IOException {
try {
String path = x.getRequestURI().getPath(), method = x.getRequestMethod();
if (path.equals("/health") && method.equals("GET")) { send(x, 200, "{\"status\":\"UP\"}"); return; }
boolean admin = path.startsWith("/__admin/");
if (admin && !MessageDigest.isEqual(("Bearer " + token).getBytes(StandardCharsets.UTF_8),
Objects.toString(x.getRequestHeaders().getFirst("Authorization"), "").getBytes(StandardCharsets.UTF_8))) {
send(x, 401, "{\"error\":\"Unauthorized\"}"); return;
}
String name = path.substring(admin ? "/__admin/".length() : 1);
if (!NAMES.contains(name)) { send(x, 404, "{\"error\":\"Unknown provider\"}"); return; }
if (admin && method.equals("PUT")) {
byte[] bytes = x.getRequestBody().readNBytes(MAX_BYTES + 1);
if (bytes.length > MAX_BYTES) { send(x, 413, "{\"error\":\"Body too large\"}"); return; }
Reply reply = checked(Objects.toString(x.getRequestHeaders().getFirst("X-Mock-Status"), "200"),
Objects.toString(x.getRequestHeaders().getFirst("X-Mock-Delay-Ms"), "0"), new String(bytes, StandardCharsets.UTF_8));
save(name, reply); send(x, 200, "{\"saved\":true}"); return;
}
if ((admin && !method.equals("GET")) || (!admin && !method.equals("POST"))) {
send(x, 405, "{\"error\":\"Method not allowed\"}"); return;
}
if (!admin && x.getRequestBody().readNBytes(MAX_BYTES + 1).length > MAX_BYTES) {
send(x, 413, "{\"error\":\"Body too large\"}"); return;
}
Reply reply = read(name);
x.getResponseHeaders().set("X-Mock-Status", Integer.toString(reply.status()));
x.getResponseHeaders().set("X-Mock-Delay-Ms", Integer.toString(reply.delay()));
x.getResponseHeaders().set("X-Mock-Calls", Long.toString(count(name, !admin)));
if (!admin) Thread.sleep(reply.delay());
send(x, admin ? 200 : reply.status(), reply.body());
} catch (IllegalArgumentException e) {
send(x, 400, "{\"error\":\"Invalid status or delay; status 200..599 excluding 204/205/304, delay 0..60000\"}");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
// A client timeout/disconnect is expected when simulating a slow provider.
if (x.getResponseCode() == -1) send(x, 500, "{\"error\":\"Response storage unavailable\"}");
} finally { x.close(); }
}
public static void main(String[] args) throws Exception {
if (args.length == 1 && args[0].equals("--health")) {
var connection = (HttpURLConnection) URI.create("http://localhost:" + System.getenv().getOrDefault("MOCK_PORT", "8788") + "/health").toURL().openConnection();
connection.setConnectTimeout(2000); connection.setReadTimeout(2000);
try { if (connection.getResponseCode() != 200) System.exit(1); }
finally { connection.disconnect(); }
return;
}
Map<String, String> env = System.getenv();
String token = env.getOrDefault("MOCK_ADMIN_TOKEN", "");
if (token.isBlank()) throw new IllegalArgumentException("MOCK_ADMIN_TOKEN is required");
String host = env.getOrDefault("MOCK_BIND", "127.0.0.1");
int port = Integer.parseInt(env.getOrDefault("MOCK_PORT", "8788"));
HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 32);
MockServer handler = new MockServer(Path.of(env.getOrDefault("MOCK_DATA", "data")), token);
ExecutorService executor = Executors.newFixedThreadPool(16);
server.setExecutor(executor); server.createContext("/", handler::handle);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { server.stop(1); executor.shutdownNow(); }));
server.start(); System.out.println("Demo providers listening on " + host + ":" + server.getAddress().getPort());
}
}