@@ -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