diff --git a/src/main/kotlin/net/pwall/json/schema/JSONSchema.kt b/src/main/kotlin/net/pwall/json/schema/JSONSchema.kt index df5aad5..38c8da9 100644 --- a/src/main/kotlin/net/pwall/json/schema/JSONSchema.kt +++ b/src/main/kotlin/net/pwall/json/schema/JSONSchema.kt @@ -132,6 +132,9 @@ sealed class JSONSchema( override fun hashCode(): Int = uri.hashCode() xor location.hashCode() + override fun toString() = + "${this::class.java.simpleName}(uri=$uri, location=$location, description=$description, title=$title)" + @Suppress("EqualsOrHashCode") class True(uri: URI?, location: JSONPointer) : JSONSchema(uri, location) { @@ -267,6 +270,9 @@ sealed class JSONSchema( override fun hashCode(): Int = super.hashCode() xor schemaVersion.hashCode() xor title.hashCode() xor description.hashCode() xor children.hashCode() + override fun toString() = "General(uri=$uri, location=$location, " + + "schemaVersion='$schemaVersion', title=$title, description=$description, children=$children)}" + } companion object { diff --git a/src/main/kotlin/net/pwall/json/schema/parser/Parser.kt b/src/main/kotlin/net/pwall/json/schema/parser/Parser.kt index c188bad..3737816 100644 --- a/src/main/kotlin/net/pwall/json/schema/parser/Parser.kt +++ b/src/main/kotlin/net/pwall/json/schema/parser/Parser.kt @@ -105,6 +105,8 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream private val schemaCache = mutableMapOf() + private val pendingRefs = mutableMapOf>() + fun preLoad(filename: String) { jsonReader.preLoad(filename) } @@ -181,9 +183,9 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream } uri?.let { val fragmentURI = uri.withFragment(pointer) - schemaCache[fragmentURI]?.let { - return if (it !is JSONSchema.False) it else fatal("Recursive \$ref", uri, pointer) - } + // Always return an existing schema, recursion is handled in parseRef() and below. + schemaCache[fragmentURI]?.let { return it } + // Create a "dummy entry" to detect recursions before the actual parsing. schemaCache[fragmentURI] = JSONSchema.False(uri, pointer) } val title = schemaJSON.getStringOrNull(uri, "title") @@ -268,7 +270,12 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream } if (options.validateDefault && schemaJSON.containsKey("default")) validateExample(result, pointer, json, pointer.child("default"), defaultValidationErrors) - uri?.let { schemaCache[uri.withFragment(pointer)] = result } + uri?.let { + val fragmentUri = uri.withFragment(pointer) + schemaCache[fragmentUri] = result + // Complete pending (i.e. recursive) RefSchema instances. + pendingRefs.remove(fragmentUri)?.forEach { it.target = result } + } return result } @@ -374,12 +381,20 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream } if (!refPointer.exists(refJSON)) fatal("\$ref not found $refString", uri, pointer) - return RefSchema( + val target = parseSchema(refJSON, refPointer, refURI) + val refSchema = RefSchema( uri = uri, location = pointer, - target = parseSchema(refJSON, refPointer, refURI), + target = target, fragment = refURIFragment, ) + if (target is JSONSchema.False) { + val fragmentUri = target.uri?.withFragment(target.location) + ?: fatal("Cannot build fragmentUri for recursive \$ref", uri, pointer) + // Register refSchema for future completion when schema becomes available. + pendingRefs.computeIfAbsent(fragmentUri) { mutableListOf() } += refSchema + } + return refSchema } private fun parseItems(json: JSONValue, pointer: JSONPointer, uri: URI?, value: JSONValue?): JSONSchema.SubSchema { diff --git a/src/main/kotlin/net/pwall/json/schema/subschema/RefSchema.kt b/src/main/kotlin/net/pwall/json/schema/subschema/RefSchema.kt index fc990ed..1197dae 100644 --- a/src/main/kotlin/net/pwall/json/schema/subschema/RefSchema.kt +++ b/src/main/kotlin/net/pwall/json/schema/subschema/RefSchema.kt @@ -30,12 +30,21 @@ import java.net.URI import net.pwall.json.JSONValue import net.pwall.json.pointer.JSONPointer import net.pwall.json.schema.JSONSchema +import net.pwall.json.schema.JSONSchemaException import net.pwall.json.schema.output.BasicOutput import net.pwall.json.schema.output.DetailedOutput -class RefSchema(uri: URI?, location: JSONPointer, val target: JSONSchema, val fragment: String?) : +class RefSchema(uri: URI?, location: JSONPointer, target: JSONSchema, val fragment: String?) : JSONSchema.SubSchema(uri, location) { + var target: JSONSchema = target + set(value) { + if (field !is False) { + throw JSONSchemaException("Modification of resolved RefSchema target is prohibited") + } + field = value + } + override fun childLocation(pointer: JSONPointer): JSONPointer = pointer.child("\$ref") override fun validate(json: JSONValue?, instanceLocation: JSONPointer): Boolean = @@ -58,4 +67,24 @@ class RefSchema(uri: URI?, location: JSONPointer, val target: JSONSchema, val fr override fun hashCode(): Int = super.hashCode() xor target.hashCode() + private var toStringVisiting = false + + /** + * toString() implementation with loop protection. + * The var "toStringVisiting" is set to true before construction of the resulting String. + * When it is found to be already set, the contents will not be evaluated recursively. + */ + override fun toString(): String { + return if (toStringVisiting) { + "RefSchema()" + } else { + try { + toStringVisiting = true + "RefSchema(uri=$uri, location=$location, fragment=$fragment, target=$target)" + } finally { + toStringVisiting = false + } + } + } + }