mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-09 22:42:55 +08:00
✨ feat(jvm): 完成资源治理与诊断增强
- 新增 JMX/Endpoint/Agent 三种 JVM 连接模式与配置归一化链路 - 支持资源浏览、变更预览、写入应用、审计记录与只读约束 - 接入 AI 结构化写入计划与诊断计划回填能力 - 新增 Agent Bridge、Arthas Tunnel、JMX Helper 诊断传输实现 - 增加诊断控制台、命令模板、输出历史与自动补全交互 - 补齐前后端契约、运行夹具与 JVM 相关回归测试
This commit is contained in:
358
internal/jvm/testdata/endpointfixture/src/com/gonavi/fixture/EndpointTestServer.java
vendored
Normal file
358
internal/jvm/testdata/endpointfixture/src/com/gonavi/fixture/EndpointTestServer.java
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
package com.gonavi.fixture;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class EndpointTestServer {
|
||||
private static final String API_KEY = "secret-token";
|
||||
private static final String ROOT_PATH = "/manage/jvm";
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private static Map<String, Object> stateValue = defaultStateValue();
|
||||
private static int stateVersion = 1;
|
||||
|
||||
private EndpointTestServer() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int port = args.length > 0 ? Integer.parseInt(args[0]) : 19010;
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
|
||||
server.createContext(ROOT_PATH, EndpointTestServer::handleProbe);
|
||||
server.createContext(ROOT_PATH + "/resources", EndpointTestServer::handleResources);
|
||||
server.createContext(ROOT_PATH + "/value", EndpointTestServer::handleValue);
|
||||
server.createContext(ROOT_PATH + "/preview", EndpointTestServer::handlePreview);
|
||||
server.createContext(ROOT_PATH + "/apply", EndpointTestServer::handleApply);
|
||||
server.setExecutor(Executors.newCachedThreadPool());
|
||||
server.start();
|
||||
|
||||
System.out.println("READY");
|
||||
System.out.flush();
|
||||
|
||||
new CountDownLatch(1).await();
|
||||
}
|
||||
|
||||
private static void handleProbe(HttpExchange exchange) throws IOException {
|
||||
if (!ensureMethod(exchange, "GET", "HEAD") || !ensureApiKey(exchange)) {
|
||||
return;
|
||||
}
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
exchange.close();
|
||||
}
|
||||
|
||||
private static void handleResources(HttpExchange exchange) throws IOException {
|
||||
if (!ensureMethod(exchange, "GET") || !ensureApiKey(exchange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String parentPath = queryParam(exchange, "parentPath");
|
||||
List<Map<String, Object>> resources = new ArrayList<>();
|
||||
if (parentPath.isEmpty()) {
|
||||
resources.add(resource("cache.orders", "", "folder", "Orders", "/cache/orders", true, true, true));
|
||||
} else if ("/cache/orders".equals(parentPath)) {
|
||||
resources.add(resource("cache.orders.state", "cache.orders", "entry", "State", "/cache/orders/state", true, true, false));
|
||||
} else {
|
||||
sendStatus(exchange, 404, "unknown parentPath: " + parentPath);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(exchange, 200, resources);
|
||||
}
|
||||
|
||||
private static void handleValue(HttpExchange exchange) throws IOException {
|
||||
if (!ensureMethod(exchange, "GET") || !ensureApiKey(exchange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String resourcePath = queryParam(exchange, "resourcePath");
|
||||
if ("/cache/orders".equals(resourcePath)) {
|
||||
sendJson(exchange, 200, folderSnapshot());
|
||||
return;
|
||||
}
|
||||
if ("/cache/orders/state".equals(resourcePath)) {
|
||||
synchronized (LOCK) {
|
||||
sendJson(exchange, 200, entrySnapshot(cloneMap(stateValue), currentVersion()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sendStatus(exchange, 404, "unknown resourcePath: " + resourcePath);
|
||||
}
|
||||
|
||||
private static void handlePreview(HttpExchange exchange) throws IOException {
|
||||
if (!ensureMethod(exchange, "POST") || !ensureApiKey(exchange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> request = readJsonBody(exchange);
|
||||
String resourcePath = requiredString(request.get("resourceId"), "resourceId");
|
||||
if (!"/cache/orders/state".equals(resourcePath)) {
|
||||
sendStatus(exchange, 400, "preview only supports /cache/orders/state");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = requiredObject(request.get("payload"), "payload");
|
||||
synchronized (LOCK) {
|
||||
Map<String, Object> beforeValue = cloneMap(stateValue);
|
||||
Map<String, Object> afterValue = mergeState(beforeValue, payload);
|
||||
Map<String, Object> preview = new LinkedHashMap<>();
|
||||
preview.put("allowed", Boolean.TRUE);
|
||||
preview.put("summary", "preview orders cache state update");
|
||||
preview.put("riskLevel", "medium");
|
||||
preview.put("before", entrySnapshot(beforeValue, currentVersion()));
|
||||
preview.put("after", entrySnapshot(afterValue, currentVersion() + "-preview"));
|
||||
sendJson(exchange, 200, preview);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleApply(HttpExchange exchange) throws IOException {
|
||||
if (!ensureMethod(exchange, "POST") || !ensureApiKey(exchange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> request = readJsonBody(exchange);
|
||||
String resourcePath = requiredString(request.get("resourceId"), "resourceId");
|
||||
if (!"/cache/orders/state".equals(resourcePath)) {
|
||||
sendStatus(exchange, 400, "apply only supports /cache/orders/state");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = requiredObject(request.get("payload"), "payload");
|
||||
String expectedVersion = optionalString(request.get("expectedVersion"));
|
||||
|
||||
synchronized (LOCK) {
|
||||
String currentVersion = currentVersion();
|
||||
if (!expectedVersion.isEmpty() && !expectedVersion.equals(currentVersion)) {
|
||||
sendStatus(exchange, 409, "stale version: expected " + expectedVersion + " but current is " + currentVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
stateValue = mergeState(stateValue, payload);
|
||||
stateVersion += 1;
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("status", "applied");
|
||||
result.put("message", "orders cache state updated");
|
||||
result.put("updatedValue", entrySnapshot(cloneMap(stateValue), currentVersion()));
|
||||
sendJson(exchange, 200, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean ensureMethod(HttpExchange exchange, String... methods) throws IOException {
|
||||
String actual = exchange.getRequestMethod();
|
||||
for (String method : methods) {
|
||||
if (method.equalsIgnoreCase(actual)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exchange.getResponseHeaders().set("Allow", String.join(", ", methods));
|
||||
sendStatus(exchange, 405, "unsupported method: " + actual);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean ensureApiKey(HttpExchange exchange) throws IOException {
|
||||
String apiKey = exchange.getRequestHeaders().getFirst("X-API-Key");
|
||||
if (API_KEY.equals(apiKey)) {
|
||||
return true;
|
||||
}
|
||||
sendStatus(exchange, 401, "missing or invalid api key");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String queryParam(HttpExchange exchange, String key) {
|
||||
String rawQuery = exchange.getRequestURI().getRawQuery();
|
||||
if (rawQuery == null || rawQuery.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String[] pairs = rawQuery.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
String name = decode(parts[0]);
|
||||
if (!key.equals(name)) {
|
||||
continue;
|
||||
}
|
||||
return parts.length > 1 ? decode(parts[1]) : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static Map<String, Object> readJsonBody(HttpExchange exchange) throws IOException {
|
||||
try (InputStream inputStream = exchange.getRequestBody()) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] chunk = new byte[4096];
|
||||
int read;
|
||||
while ((read = inputStream.read(chunk)) >= 0) {
|
||||
buffer.write(chunk, 0, read);
|
||||
}
|
||||
String payload = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
|
||||
Object parsed = MiniJson.parse(payload);
|
||||
if (!(parsed instanceof Map<?, ?>)) {
|
||||
throw new IllegalArgumentException("request body must be a JSON object");
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> request = (Map<String, Object>) parsed;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendStatus(HttpExchange exchange, int statusCode, String message) throws IOException {
|
||||
byte[] body = message.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
}
|
||||
|
||||
private static void sendJson(HttpExchange exchange, int statusCode, Object payload) throws IOException {
|
||||
byte[] body = MiniJson.stringify(payload).getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
}
|
||||
|
||||
private static Map<String, Object> folderSnapshot() {
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("name", "Orders");
|
||||
value.put("entryCount", 1);
|
||||
value.put("resourcePath", "/cache/orders");
|
||||
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("resourceId", "/cache/orders");
|
||||
snapshot.put("kind", "folder");
|
||||
snapshot.put("format", "json");
|
||||
snapshot.put("version", "folder-v1");
|
||||
snapshot.put("value", value);
|
||||
snapshot.put("description", "orders cache root");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static Map<String, Object> entrySnapshot(Map<String, Object> value, String version) {
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("resourceId", "/cache/orders/state");
|
||||
snapshot.put("kind", "entry");
|
||||
snapshot.put("format", "json");
|
||||
snapshot.put("version", version);
|
||||
snapshot.put("value", value);
|
||||
snapshot.put("description", "orders cache state");
|
||||
snapshot.put("supportedActions", supportedActions());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> supportedActions() {
|
||||
List<Map<String, Object>> actions = new ArrayList<>();
|
||||
Map<String, Object> action = new LinkedHashMap<>();
|
||||
action.put("action", "put");
|
||||
action.put("label", "更新缓存状态");
|
||||
action.put("description", "将 payload 字段合并到当前订单缓存状态");
|
||||
action.put("payloadFields", Arrays.asList(
|
||||
payloadField("status", "string", false, "缓存状态"),
|
||||
payloadField("lastUpdated", "string", false, "更新时间"),
|
||||
payloadField("size", "number", false, "示例大小")
|
||||
));
|
||||
action.put("payloadExample", defaultStateValue());
|
||||
actions.add(action);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static Map<String, Object> payloadField(String name, String type, boolean required, String description) {
|
||||
Map<String, Object> field = new LinkedHashMap<>();
|
||||
field.put("name", name);
|
||||
field.put("type", type);
|
||||
field.put("required", required);
|
||||
field.put("description", description);
|
||||
return field;
|
||||
}
|
||||
|
||||
private static Map<String, Object> resource(
|
||||
String id,
|
||||
String parentId,
|
||||
String kind,
|
||||
String name,
|
||||
String path,
|
||||
boolean canRead,
|
||||
boolean canWrite,
|
||||
boolean hasChildren
|
||||
) {
|
||||
Map<String, Object> resource = new LinkedHashMap<>();
|
||||
resource.put("id", id);
|
||||
if (!parentId.isEmpty()) {
|
||||
resource.put("parentId", parentId);
|
||||
}
|
||||
resource.put("kind", kind);
|
||||
resource.put("name", name);
|
||||
resource.put("path", path);
|
||||
resource.put("providerMode", "endpoint");
|
||||
resource.put("canRead", canRead);
|
||||
resource.put("canWrite", canWrite);
|
||||
resource.put("hasChildren", hasChildren);
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static Map<String, Object> defaultStateValue() {
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("status", "warm");
|
||||
value.put("lastUpdated", "initial");
|
||||
value.put("size", 7);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Map<String, Object> mergeState(Map<String, Object> current, Map<String, Object> payload) {
|
||||
Map<String, Object> merged = cloneMap(current);
|
||||
for (Map.Entry<String, Object> entry : payload.entrySet()) {
|
||||
merged.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static Map<String, Object> cloneMap(Map<String, Object> source) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String currentVersion() {
|
||||
return "state-v" + stateVersion;
|
||||
}
|
||||
|
||||
private static String decode(String value) {
|
||||
return URLDecoder.decode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Map<String, Object> requiredObject(Object value, String field) {
|
||||
if (!(value instanceof Map<?, ?>)) {
|
||||
throw new IllegalArgumentException(field + " must be an object");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> result = (Map<String, Object>) value;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String requiredString(Object value, String field) {
|
||||
String resolved = optionalString(value);
|
||||
if (resolved.isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " is required");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static String optionalString(Object value) {
|
||||
return value == null ? "" : String.valueOf(value).trim();
|
||||
}
|
||||
}
|
||||
311
internal/jvm/testdata/endpointfixture/src/com/gonavi/fixture/MiniJson.java
vendored
Normal file
311
internal/jvm/testdata/endpointfixture/src/com/gonavi/fixture/MiniJson.java
vendored
Normal file
@@ -0,0 +1,311 @@
|
||||
package com.gonavi.fixture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class MiniJson {
|
||||
private MiniJson() {
|
||||
}
|
||||
|
||||
static Object parse(String text) {
|
||||
Parser parser = new Parser(text);
|
||||
Object value = parser.parseValue();
|
||||
parser.skipWhitespace();
|
||||
if (!parser.isDone()) {
|
||||
throw new IllegalArgumentException("JSON parsing did not consume the full payload");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String stringify(Object value) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
writeValue(builder, value);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static void writeValue(StringBuilder builder, Object value) {
|
||||
if (value == null) {
|
||||
builder.append("null");
|
||||
return;
|
||||
}
|
||||
if (value instanceof String) {
|
||||
writeString(builder, (String) value);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Number || value instanceof Boolean) {
|
||||
builder.append(String.valueOf(value));
|
||||
return;
|
||||
}
|
||||
if (value instanceof Map<?, ?>) {
|
||||
builder.append('{');
|
||||
Iterator<? extends Map.Entry<?, ?>> iterator = ((Map<?, ?>) value).entrySet().iterator();
|
||||
boolean first = true;
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<?, ?> entry = iterator.next();
|
||||
if (!first) {
|
||||
builder.append(',');
|
||||
}
|
||||
first = false;
|
||||
writeString(builder, String.valueOf(entry.getKey()));
|
||||
builder.append(':');
|
||||
writeValue(builder, entry.getValue());
|
||||
}
|
||||
builder.append('}');
|
||||
return;
|
||||
}
|
||||
if (value instanceof Iterable<?>) {
|
||||
builder.append('[');
|
||||
Iterator<?> iterator = ((Iterable<?>) value).iterator();
|
||||
boolean first = true;
|
||||
while (iterator.hasNext()) {
|
||||
if (!first) {
|
||||
builder.append(',');
|
||||
}
|
||||
first = false;
|
||||
writeValue(builder, iterator.next());
|
||||
}
|
||||
builder.append(']');
|
||||
return;
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
builder.append('[');
|
||||
int length = java.lang.reflect.Array.getLength(value);
|
||||
for (int index = 0; index < length; index++) {
|
||||
if (index > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
writeValue(builder, java.lang.reflect.Array.get(value, index));
|
||||
}
|
||||
builder.append(']');
|
||||
return;
|
||||
}
|
||||
writeString(builder, String.valueOf(value));
|
||||
}
|
||||
|
||||
private static void writeString(StringBuilder builder, String value) {
|
||||
builder.append('"');
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
char ch = value.charAt(index);
|
||||
switch (ch) {
|
||||
case '"':
|
||||
builder.append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
builder.append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (ch < 0x20) {
|
||||
builder.append(String.format("\\u%04x", (int) ch));
|
||||
} else {
|
||||
builder.append(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
builder.append('"');
|
||||
}
|
||||
|
||||
private static final class Parser {
|
||||
private final String text;
|
||||
private int index;
|
||||
|
||||
private Parser(String text) {
|
||||
this.text = text == null ? "" : text;
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
private Object parseValue() {
|
||||
skipWhitespace();
|
||||
if (isDone()) {
|
||||
throw new IllegalArgumentException("unexpected end of JSON input");
|
||||
}
|
||||
|
||||
char ch = text.charAt(index);
|
||||
switch (ch) {
|
||||
case '{':
|
||||
return parseObject();
|
||||
case '[':
|
||||
return parseArray();
|
||||
case '"':
|
||||
return parseString();
|
||||
case 't':
|
||||
expect("true");
|
||||
return Boolean.TRUE;
|
||||
case 'f':
|
||||
expect("false");
|
||||
return Boolean.FALSE;
|
||||
case 'n':
|
||||
expect("null");
|
||||
return null;
|
||||
default:
|
||||
if (ch == '-' || Character.isDigit(ch)) {
|
||||
return parseNumber();
|
||||
}
|
||||
throw new IllegalArgumentException("unexpected JSON token at index " + index);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parseObject() {
|
||||
expect("{");
|
||||
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
|
||||
skipWhitespace();
|
||||
if (peek('}')) {
|
||||
expect("}");
|
||||
return result;
|
||||
}
|
||||
while (true) {
|
||||
String key = parseString();
|
||||
skipWhitespace();
|
||||
expect(":");
|
||||
Object value = parseValue();
|
||||
result.put(key, value);
|
||||
skipWhitespace();
|
||||
if (peek('}')) {
|
||||
expect("}");
|
||||
return result;
|
||||
}
|
||||
expect(",");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Object> parseArray() {
|
||||
expect("[");
|
||||
ArrayList<Object> result = new ArrayList<>();
|
||||
skipWhitespace();
|
||||
if (peek(']')) {
|
||||
expect("]");
|
||||
return result;
|
||||
}
|
||||
while (true) {
|
||||
result.add(parseValue());
|
||||
skipWhitespace();
|
||||
if (peek(']')) {
|
||||
expect("]");
|
||||
return result;
|
||||
}
|
||||
expect(",");
|
||||
}
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
expect("\"");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (!isDone()) {
|
||||
char ch = text.charAt(index++);
|
||||
if (ch == '"') {
|
||||
return builder.toString();
|
||||
}
|
||||
if (ch != '\\') {
|
||||
builder.append(ch);
|
||||
continue;
|
||||
}
|
||||
if (isDone()) {
|
||||
throw new IllegalArgumentException("unterminated escape sequence");
|
||||
}
|
||||
char escaped = text.charAt(index++);
|
||||
switch (escaped) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
builder.append(escaped);
|
||||
break;
|
||||
case 'b':
|
||||
builder.append('\b');
|
||||
break;
|
||||
case 'f':
|
||||
builder.append('\f');
|
||||
break;
|
||||
case 'n':
|
||||
builder.append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
builder.append('\r');
|
||||
break;
|
||||
case 't':
|
||||
builder.append('\t');
|
||||
break;
|
||||
case 'u':
|
||||
if (index + 4 > text.length()) {
|
||||
throw new IllegalArgumentException("invalid unicode escape");
|
||||
}
|
||||
builder.append((char) Integer.parseInt(text.substring(index, index + 4), 16));
|
||||
index += 4;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unsupported escape sequence: \\" + escaped);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("unterminated JSON string");
|
||||
}
|
||||
|
||||
private Number parseNumber() {
|
||||
int start = index;
|
||||
if (peek('-')) {
|
||||
index += 1;
|
||||
}
|
||||
while (!isDone() && Character.isDigit(text.charAt(index))) {
|
||||
index += 1;
|
||||
}
|
||||
if (!isDone() && text.charAt(index) == '.') {
|
||||
index += 1;
|
||||
while (!isDone() && Character.isDigit(text.charAt(index))) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if (!isDone() && (text.charAt(index) == 'e' || text.charAt(index) == 'E')) {
|
||||
index += 1;
|
||||
if (!isDone() && (text.charAt(index) == '+' || text.charAt(index) == '-')) {
|
||||
index += 1;
|
||||
}
|
||||
while (!isDone() && Character.isDigit(text.charAt(index))) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
String raw = text.substring(start, index);
|
||||
if (raw.indexOf('.') >= 0 || raw.indexOf('e') >= 0 || raw.indexOf('E') >= 0) {
|
||||
return Double.parseDouble(raw);
|
||||
}
|
||||
return Long.parseLong(raw);
|
||||
}
|
||||
|
||||
private void expect(String token) {
|
||||
skipWhitespace();
|
||||
if (!text.startsWith(token, index)) {
|
||||
throw new IllegalArgumentException("expected " + token + " at index " + index);
|
||||
}
|
||||
index += token.length();
|
||||
}
|
||||
|
||||
private boolean peek(char ch) {
|
||||
return !isDone() && text.charAt(index) == ch;
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (!isDone() && Character.isWhitespace(text.charAt(index))) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDone() {
|
||||
return index >= text.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user