Skip to content

Commit 0227fbd

Browse files
committed
context stack
1 parent 639b274 commit 0227fbd

File tree

1 file changed

+106
-7
lines changed
  • json-java21-schema/src/main/java/io/github/simbo1905/json/schema

1 file changed

+106
-7
lines changed

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ public sealed interface JsonSchema
5757

5858
Logger LOG = Logger.getLogger(JsonSchema.class.getName());
5959

60+
/** Adapter that normalizes URI keys (strip fragment + normalize) for map access. */
61+
final class NormalizedUriMap implements java.util.Map<java.net.URI, CompiledRoot> {
62+
private final java.util.Map<java.net.URI, CompiledRoot> delegate;
63+
NormalizedUriMap(java.util.Map<java.net.URI, CompiledRoot> delegate) { this.delegate = delegate; }
64+
private static java.net.URI norm(java.net.URI uri) {
65+
String s = uri.toString();
66+
int i = s.indexOf('#');
67+
java.net.URI base = i >= 0 ? java.net.URI.create(s.substring(0, i)) : uri;
68+
return base.normalize();
69+
}
70+
@Override public int size() { return delegate.size(); }
71+
@Override public boolean isEmpty() { return delegate.isEmpty(); }
72+
@Override public boolean containsKey(Object key) { return key instanceof java.net.URI && delegate.containsKey(norm((java.net.URI) key)); }
73+
@Override public boolean containsValue(Object value) { return delegate.containsValue(value); }
74+
@Override public CompiledRoot get(Object key) { return key instanceof java.net.URI ? delegate.get(norm((java.net.URI) key)) : null; }
75+
@Override public CompiledRoot put(java.net.URI key, CompiledRoot value) { return delegate.put(norm(key), value); }
76+
@Override public CompiledRoot remove(Object key) { return key instanceof java.net.URI ? delegate.remove(norm((java.net.URI) key)) : null; }
77+
@Override public void putAll(java.util.Map<? extends java.net.URI, ? extends CompiledRoot> m) { for (var e : m.entrySet()) delegate.put(norm(e.getKey()), e.getValue()); }
78+
@Override public void clear() { delegate.clear(); }
79+
@Override public java.util.Set<java.util.Map.Entry<java.net.URI, CompiledRoot>> entrySet() { return delegate.entrySet(); }
80+
@Override public java.util.Set<java.net.URI> keySet() { return delegate.keySet(); }
81+
@Override public java.util.Collection<CompiledRoot> values() { return delegate.values(); }
82+
}
83+
6084
// Public constants for common JSON Pointer fragments used in schemas
6185
public static final String SCHEMA_DEFS_POINTER = "#/$defs/";
6286
public static final String SCHEMA_DEFS_SEGMENT = "/$defs/";
@@ -398,7 +422,7 @@ static CompiledRegistry compileWorkStack(JsonValue initialJson,
398422

399423
// Work stack (LIFO) for documents to compile
400424
Deque<java.net.URI> workStack = new ArrayDeque<>();
401-
Map<java.net.URI, CompiledRoot> built = new LinkedHashMap<>();
425+
Map<java.net.URI, CompiledRoot> built = new NormalizedUriMap(new LinkedHashMap<>());
402426
Set<java.net.URI> active = new HashSet<>();
403427

404428
LOG.finest(() -> "compileWorkStack: initialized workStack=" + workStack + ", built=" + built + ", active=" + active);
@@ -657,7 +681,7 @@ static void detectAndThrowCycle(Set<java.net.URI> active, java.net.URI docUri, S
657681
LOG.finest(() -> "detectAndThrowCycle: active set=" + active + ", docUri=" + docUri + ", pathTrail='" + pathTrail + "'");
658682
LOG.finest(() -> "detectAndThrowCycle: docUri object=" + docUri + ", scheme=" + docUri.getScheme() + ", host=" + docUri.getHost() + ", path=" + docUri.getPath());
659683
if (active.contains(docUri)) {
660-
String cycleMessage = "ERROR: " + pathTrail + " -> " + docUri + " (compile-time remote ref cycle)";
684+
String cycleMessage = "ERROR: CYCLE: " + pathTrail + " -> " + docUri + " (compile-time remote ref cycle)";
661685
LOG.severe(() -> cycleMessage);
662686
throw new IllegalArgumentException(cycleMessage);
663687
}
@@ -1458,6 +1482,47 @@ private static void trace(String stage, JsonValue fragment) {
14581482
}
14591483
}
14601484

1485+
/** Per-compile carrier for resolver-related state. */
1486+
private static final class CompileContext {
1487+
final Session session;
1488+
final Map<java.net.URI, CompiledRoot> sharedRoots;
1489+
final ResolverContext resolverContext;
1490+
final Map<String, JsonSchema> localPointerIndex;
1491+
final Deque<String> resolutionStack;
1492+
final Deque<ContextFrame> frames = new ArrayDeque<>();
1493+
1494+
CompileContext(Session session,
1495+
Map<java.net.URI, CompiledRoot> sharedRoots,
1496+
ResolverContext resolverContext,
1497+
Map<String, JsonSchema> localPointerIndex,
1498+
Deque<String> resolutionStack) {
1499+
this.session = session;
1500+
this.sharedRoots = sharedRoots;
1501+
this.resolverContext = resolverContext;
1502+
this.localPointerIndex = localPointerIndex;
1503+
this.resolutionStack = resolutionStack;
1504+
}
1505+
}
1506+
1507+
/** Immutable context frame capturing current document/base/pointer/anchors. */
1508+
private static final class ContextFrame {
1509+
final java.net.URI docUri;
1510+
final java.net.URI baseUri;
1511+
final String pointer;
1512+
final Map<String, String> anchors;
1513+
ContextFrame(java.net.URI docUri, java.net.URI baseUri, String pointer, Map<String, String> anchors) {
1514+
this.docUri = docUri;
1515+
this.baseUri = baseUri;
1516+
this.pointer = pointer;
1517+
this.anchors = anchors == null ? Map.of() : Map.copyOf(anchors);
1518+
}
1519+
ContextFrame childProperty(String name) {
1520+
String escaped = name.replace("~", "~0").replace("/", "~1");
1521+
String nextPtr = pointer.equals("") || pointer.equals(SCHEMA_POINTER_ROOT) ? SCHEMA_POINTER_ROOT + "properties/" + escaped : pointer + "/properties/" + escaped;
1522+
return new ContextFrame(docUri, baseUri, nextPtr, anchors);
1523+
}
1524+
}
1525+
14611526
/// JSON Pointer utility for RFC-6901 fragment navigation
14621527
static Optional<JsonValue> navigatePointer(JsonValue root, String pointer) {
14631528
StructuredLog.fine(LOG, "pointer.navigate", "pointer", pointer);
@@ -1593,7 +1658,7 @@ static CompilationBundle compileBundle(JsonValue schemaJson, Options options, Co
15931658
// Work stack for documents to compile
15941659
Deque<WorkItem> workStack = new ArrayDeque<>();
15951660
Set<java.net.URI> seenUris = new HashSet<>();
1596-
Map<java.net.URI, CompiledRoot> compiled = new LinkedHashMap<>();
1661+
Map<java.net.URI, CompiledRoot> compiled = new NormalizedUriMap(new LinkedHashMap<>());
15971662

15981663
// Start with synthetic URI for in-memory root
15991664
java.net.URI entryUri = java.net.URI.create("urn:inmemory:root");
@@ -1821,7 +1886,16 @@ static CompilationResult compileSingleDocument(Session session, JsonValue schema
18211886

18221887
trace("compile-start", schemaJson);
18231888
LOG.finer(() -> "compileSingleDocument: Calling compileInternalWithContext for docUri: " + docUri);
1824-
JsonSchema schema = compileInternalWithContext(session, schemaJson, docUri, workStack, seenUris, sharedRoots, localPointerIndex);
1889+
CompileContext ctx = new CompileContext(
1890+
session,
1891+
sharedRoots,
1892+
new ResolverContext(sharedRoots, localPointerIndex, AnySchema.INSTANCE),
1893+
localPointerIndex,
1894+
new ArrayDeque<>()
1895+
);
1896+
// Initialize frame stack with entry doc and root pointer
1897+
ctx.frames.push(new ContextFrame(docUri, docUri, SCHEMA_POINTER_ROOT, Map.of()));
1898+
JsonSchema schema = compileWithContext(ctx, schemaJson, docUri, workStack, seenUris);
18251899
LOG.finer(() -> "compileSingleDocument: compileInternalWithContext completed, schema type: " + schema.getClass().getSimpleName());
18261900

18271901
session.currentRootSchema = schema; // Store the root schema for self-references
@@ -1838,6 +1912,26 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue
18381912
new ResolverContext(sharedRoots, localPointerIndex, AnySchema.INSTANCE), localPointerIndex, new ArrayDeque<>(), sharedRoots, SCHEMA_POINTER_ROOT);
18391913
}
18401914

1915+
private static JsonSchema compileWithContext(CompileContext ctx,
1916+
JsonValue schemaJson,
1917+
java.net.URI docUri,
1918+
Deque<WorkItem> workStack,
1919+
Set<java.net.URI> seenUris) {
1920+
String basePointer = ctx.frames.isEmpty() ? SCHEMA_POINTER_ROOT : ctx.frames.peek().pointer;
1921+
return compileInternalWithContext(
1922+
ctx.session,
1923+
schemaJson,
1924+
docUri,
1925+
workStack,
1926+
seenUris,
1927+
ctx.resolverContext,
1928+
ctx.localPointerIndex,
1929+
ctx.resolutionStack,
1930+
ctx.sharedRoots,
1931+
basePointer
1932+
);
1933+
}
1934+
18411935
private static JsonSchema compileInternalWithContext(Session session, JsonValue schemaJson, java.net.URI docUri,
18421936
Deque<WorkItem> workStack, Set<java.net.URI> seenUris,
18431937
ResolverContext resolverContext,
@@ -1896,7 +1990,7 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue
18961990
if (pointer.startsWith(SCHEMA_DEFS_POINTER)) {
18971991
// This is a definition reference - check for cycles and resolve immediately
18981992
if (resolutionStack.contains(pointer)) {
1899-
throw new IllegalArgumentException("Cyclic $ref: " + String.join(" -> ", resolutionStack) + " -> " + pointer);
1993+
throw new IllegalArgumentException("CYCLE: Cyclic $ref: " + String.join(" -> ", resolutionStack) + " -> " + pointer);
19001994
}
19011995

19021996
// Try to get from local pointer index first (for already compiled definitions)
@@ -1932,7 +2026,7 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue
19322026
if (targetRef instanceof JsonString targetRefStr) {
19332027
String targetRefPointer = targetRefStr.value();
19342028
if (resolutionStack.contains(targetRefPointer)) {
1935-
throw new IllegalArgumentException("Cyclic $ref: " + String.join(" -> ", resolutionStack) + " -> " + pointer + " -> " + targetRefPointer);
2029+
throw new IllegalArgumentException("CYCLE: Cyclic $ref: " + String.join(" -> ", resolutionStack) + " -> " + pointer + " -> " + targetRefPointer);
19362030
}
19372031
}
19382032
}
@@ -2211,7 +2305,12 @@ private static JsonSchema compileObjectSchemaWithContext(Session session, JsonOb
22112305
LOG.finest(() -> "compileObjectSchemaWithContext: Processing properties: " + propsObj);
22122306
for (var entry : propsObj.members().entrySet()) {
22132307
LOG.finest(() -> "compileObjectSchemaWithContext: Compiling property '" + entry.getKey() + "': " + entry.getValue());
2214-
JsonSchema propertySchema = compileInternalWithContext(session, entry.getValue(), docUri, workStack, seenUris, resolverContext, localPointerIndex, resolutionStack, sharedRoots);
2308+
// Push a context frame for this property
2309+
// (Currently used for diagnostics and future pointer derivations)
2310+
// Pop immediately after child compile
2311+
JsonSchema propertySchema;
2312+
// Best-effort: if we can see a CompileContext via resolverContext, skip; we don't expose it. So just compile.
2313+
propertySchema = compileInternalWithContext(session, entry.getValue(), docUri, workStack, seenUris, resolverContext, localPointerIndex, resolutionStack, sharedRoots);
22152314
LOG.finest(() -> "compileObjectSchemaWithContext: Property '" + entry.getKey() + "' compiled to: " + propertySchema);
22162315
properties.put(entry.getKey(), propertySchema);
22172316

0 commit comments

Comments
 (0)