cache = new HashMap();
+
+ /**
+ * Get previously parsed JSON (sub)schema.
+ *
+ * @param locator the locator to the JSON schema document
+ * @param jsonPointer JSON pointer to the (sub)schema ("/" for the root)
+ *
+ * @return either found JSON schema or null if not found
+ */
+ public AbstractJsonSchema get(JsonSchemaLocator locator, String jsonPointer) {
+ return cache.get(resolve(locator.uri, jsonPointer));
+ }
+
+ /**
+ * Get previously parsed JSON (sub)schema.
+ *
+ * @param schema JSON (sub)schema template (actually we need an $id of it)
+ *
+ * @return either found JSON schema or null if not found
+ */
+ public AbstractJsonSchema get(AbstractJsonSchema schema) {
+ return schema.getParent() instanceof JsonMultitypeSchemaWrapper ? null : cache.get(schema.getId());
+ }
+
+ /**
+ * Put the JSON (sub)schema into the cache.
+ *
+ * @param schema (sub)schema to be put
+ *
+ * @return the same document we passed to the method
+ */
+ public AbstractJsonSchema put(AbstractJsonSchema schema) {
+ if (!schema.isDynamicScope() &&
+ !(schema.getParent() instanceof JsonMultitypeSchemaWrapper)) {
+ final URI id = schema.getId();
+ cache.put(id, schema);
+
+ // syntheticId is a real document path which might differ
+ // from the contextual $id
+ final URI syntheticId = getSyntheticId(schema);
+ if (!id.equals(syntheticId)) {
+ cache.put(syntheticId, schema);
+ }
+ }
+ return schema;
+ }
+
+ /**
+ * Unlike {@code JsonSchemaElement.getId()}, synthetic identifier is
+ * relative to the document root.
+ *
+ * Example:
+ * Both URI point to the same 'items' element.
+ * While first one is the Json Schema $id, second is a JSON pointer in the
+ * 'scope_change_defs2.json' JSON document.
+ * 'http://localhost:1234/draft2019-09/baseUriChangeFolderInSubschema/#/$defs/bar/items'
+ * 'http://localhost:1234/draft2019-09/scope_change_defs2.json#/$defs/baz/$defs/bar/items'
+ *
+ * @return synthetic identifier used to resolve external $refs.
+ */
+ private URI getSyntheticId(AbstractJsonSchemaElement e) {
+ final StringBuilder sb = new StringBuilder(e.jsonPointer);
+ while (e.getParent() != null && !(e.getParent() instanceof JsonReference)) {
+ e = e.getParent();
+ if (!(e instanceof JsonMultitypeSchemaWrapper) &&
+ e.jsonPointer != e.getJsonPointer() &&
+ e.jsonPointer.length() > 1) {
+ sb.insert(0, e.jsonPointer);
+ }
+ }
+ return resolve(e.locator.uri, sb.toString());
+ }
+
+ /**
+ * Resolves identifier with JSON pointer fragment part.
+ *
+ * @param uri schema identifier
+ * @param jsonPointer JSON pointer to the schema element
+ *
+ * @return schema element identifier
+ */
+ private URI resolve(URI uri, String jsonPointer) {
+ final String fragment = uri.getFragment();
+ try {
+ return new URI(uri.getScheme(), uri.getSchemeSpecificPart(),
+ fragment == null && jsonPointer.length() > 1 ? jsonPointer : jsonPointer.length() > 1
+ ? fragment + jsonPointer : fragment);
+ } catch (URISyntaxException ex) {}
+ return null;
+ }
+}
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonReferenceImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonReferenceImpl.java
index fcfc561..ff91c1a 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonReferenceImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonReferenceImpl.java
@@ -81,7 +81,7 @@ protected void read(JsonSubschemaParser parser, JsonObject object, String tag)
ref_locator = locator;
} else {
ref_locator = locator.resolve(
- new URI(ref.getScheme(), ref.getSchemeSpecificPart(), null));
+ new URI(ref.getScheme(), ref.getSchemeSpecificPart(), null));
}
} else {
ref_pointer = "/";
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchema.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchema.java
index 23d41c2..f24d9de 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchema.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchema.java
@@ -37,7 +37,7 @@
import javax.json.JsonValue;
/**
- * This interface is used internally and exists only for the purpose to hide
+ * This class is used internally and exists only for the purpose to hide
* actual validate() method from the outside.
*
* @author Dmitry Repchevsky
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchemaElement.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchemaElement.java
index a98923c..7772fdd 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchemaElement.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/AbstractJsonSchemaElement.java
@@ -28,7 +28,7 @@
import es.elixir.bsc.json.schema.JsonSchemaLocator;
import es.elixir.bsc.json.schema.model.JsonSchemaElement;
import java.net.URI;
-import java.util.Objects;
+import java.net.URISyntaxException;
/**
* This is an root class that any JSON Schema element inherits from.
@@ -68,12 +68,22 @@ public AbstractJsonSchemaElement(AbstractJsonSchemaElement parent,
@Override
public final URI getId() {
- return locator.uri;
+ final String pointer = getJsonPointer();
+ final String fragment = locator.uri.getFragment();
+ try {
+ return new URI(locator.uri.getScheme(), locator.uri.getSchemeSpecificPart(),
+ fragment == null && pointer.length() > 1 ? pointer : pointer.length() > 1
+ ? fragment + pointer : fragment);
+ } catch (URISyntaxException ex) {}
+ return null;
}
@Override
public String getJsonPointer() {
- return parent == null || parent.locator != locator ? "/" : jsonPointer;
+ if (parent instanceof JsonMultitypeSchemaWrapper) {
+ return parent.getJsonPointer();
+ }
+ return parent != null && parent.locator.uri.equals(locator.uri) ? jsonPointer : "/";
}
@Override
@@ -81,7 +91,6 @@ public AbstractJsonSchemaElement getParent() {
return parent;
}
-
/**
* This is a marker whether this element is in the dynamic scope and
* must not be cached.
@@ -95,28 +104,6 @@ public boolean isDynamicScope() {
protected void setDynamicScope(boolean isDynamicScope) {
this.isDynamicScope = isDynamicScope;
}
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof AbstractJsonSchemaElement other &&
- this.getClass() == obj.getClass()) {
- return Objects.equals(jsonPointer, other.jsonPointer) &&
- Objects.equals(locator.uri, other.locator.uri);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int hash = 7;
- hash = 31 * hash + Objects.hashCode(this.locator.uri);
- hash = 31 * hash + Objects.hashCode(this.jsonPointer);
- hash = 31 * hash + Objects.hashCode(this.getClass().hashCode());
- return hash;
- }
@Override
public Object clone() throws CloneNotSupportedException {
@@ -124,11 +111,12 @@ public Object clone() throws CloneNotSupportedException {
}
/**
- * Predicate to create a clone of this element linked another parent.
+ * Predicate to create a clone of this element linked to another parent.
+ * If the element's parent is the same as provided parent, no clone is created.
*
* @param parent - the parent to be assigned to the cloned element
*
- * @return the clone of this element
+ * @return the clone of this element or element itself if parents are the same
*/
protected AbstractJsonSchemaElement relink(AbstractJsonSchemaElement parent) {
if (this.parent != parent) {
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/BooleanJsonSchemaImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/BooleanJsonSchemaImpl.java
index 359300a..0fbdfca 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/BooleanJsonSchemaImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/BooleanJsonSchemaImpl.java
@@ -54,7 +54,7 @@ public BooleanJsonSchemaImpl(AbstractJsonSchemaElement parent,
JsonSchemaLocator locator, String jsonPointer) {
super(parent, locator, jsonPointer);
}
-
+
@Override
public Stream getChildren() {
return Stream.empty();
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonDynamicReferenceImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonDynamicReferenceImpl.java
index b7c8df1..e7aa0c2 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonDynamicReferenceImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonDynamicReferenceImpl.java
@@ -72,37 +72,41 @@ public AbstractJsonSchemaElement getSchema() throws JsonSchemaException {
e = parser.parse(ref_locator, null, "/", value, null);
}
schema = getSchema(e, ref);
+
+ // if no default '$dynamicAnchor' found - skip further
+ // '$dynamicAnchor'(s) search - treat as usual '$ref'.
if (schema != null) {
final URI uri = new URI(null, null, fragment);
+ e = this;
while ((e = e.getParent()) != null) {
- final AbstractJsonSchemaElement s = getSchema(e, uri);
- if (s != null) {
- schema = s;
+ if ("/".equals(e.getJsonPointer())) {
+ final AbstractJsonSchemaElement s = getSchema(e, uri);
+ if (s != null) {
+ schema = s;
+ }
}
-
}
}
} catch (IOException | URISyntaxException ex) {}
}
}
-
+
if (schema == null && super.getSchema() == null) {
throw new JsonSchemaException(
new ParsingError(ParsingMessage.UNRESOLVABLE_REFERENCE, ref));
- }
-
+ }
return schema;
}
private AbstractJsonSchemaElement getSchema(AbstractJsonSchemaElement e, URI uri)
throws IOException, JsonSchemaException {
final String fragment = uri.getFragment();
- final JsonSchemaLocator l = e.locator.resolve(uri);
+ final JsonSchemaLocator l = e.locator.resolve(uri);
final JsonValue value = l.getSchema("/");
if (value instanceof JsonObject jsubschema) {
final String anchor = jsubschema.getString(DYNAMIC_ANCHOR, null);
if (fragment.equals(anchor)) {
- return parser.parse(l, this, e.getJsonPointer(), jsubschema, null);
+ return parser.parse(l, this, "/", jsubschema, null);
}
}
return null;
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonObjectSchemaImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonObjectSchemaImpl.java
index 1b209f0..6ce688b 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonObjectSchemaImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonObjectSchemaImpl.java
@@ -121,7 +121,7 @@ public StringArray getRequired() {
public JsonProperties getDependentSchemas() {
if (dependentSchemas == null) {
dependentSchemas = new JsonPropertiesImpl(this,
- locator, jsonPointer + "/" + DEPENDENT_SCHEMAS);
+ locator, getJsonPointer() + "/" + DEPENDENT_SCHEMAS);
}
return dependentSchemas;
}
@@ -130,7 +130,7 @@ public JsonProperties getDependentSchemas() {
public JsonDependentProperties getDependentRequired() {
if (dependentRequired == null) {
dependentRequired = new JsonDependentPropertiesImpl(this,
- locator, jsonPointer + "/" + DEPENDENT_REQUIRED);
+ locator, getJsonPointer() + "/" + DEPENDENT_REQUIRED);
}
return dependentRequired;
}
@@ -173,7 +173,7 @@ public JsonObjectSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
final JsonObject jproperties = JsonSchemaUtil.check(object.get(PROPERTIES), ValueType.OBJECT);
if (jproperties != null) {
- properties = new JsonPropertiesImpl(this, locator, jsonPointer + "/" + PROPERTIES)
+ properties = new JsonPropertiesImpl(this, locator, getJsonPointer() + "/" + PROPERTIES)
.read(parser, jproperties);
}
@@ -189,7 +189,7 @@ public JsonObjectSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
final JsonObject jpatternProperties = JsonSchemaUtil.check(object.get(PATTERN_PROPERTIES), ValueType.OBJECT);
if (jpatternProperties != null) {
- patternProperties = new JsonPropertiesImpl(this, locator, jsonPointer + "/" + PATTERN_PROPERTIES)
+ patternProperties = new JsonPropertiesImpl(this, locator, getJsonPointer() + "/" + PATTERN_PROPERTIES)
.read(parser, jpatternProperties);
}
@@ -228,13 +228,13 @@ public JsonObjectSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
final JsonObject jdependentSchemas = JsonSchemaUtil.check(object.get(DEPENDENT_SCHEMAS), ValueType.OBJECT);
if (jdependentSchemas != null) {
dependentSchemas = new JsonPropertiesImpl(this, locator,
- jsonPointer + "/" + DEPENDENT_SCHEMAS).read(parser, jdependentSchemas);
+ getJsonPointer() + "/" + DEPENDENT_SCHEMAS).read(parser, jdependentSchemas);
}
final JsonObject jdependentRequired = JsonSchemaUtil.check(object.get(DEPENDENT_REQUIRED), ValueType.OBJECT);
if (jdependentRequired != null) {
dependentRequired = new JsonDependentPropertiesImpl(this, locator,
- jsonPointer + "/" + DEPENDENT_REQUIRED).read(parser, jdependentRequired);
+ getJsonPointer() + "/" + DEPENDENT_REQUIRED).read(parser, jdependentRequired);
}
final JsonObject jdependencies = JsonSchemaUtil.check(object.get(DEPENDENCIES), ValueType.OBJECT);
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonReferenceImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonReferenceImpl.java
index 2571044..b20fdff 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonReferenceImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonReferenceImpl.java
@@ -32,6 +32,8 @@
import es.elixir.bsc.json.schema.impl.JsonSubschemaParser;
import es.elixir.bsc.json.schema.model.JsonReference;
import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.stream.Stream;
import javax.json.JsonException;
import javax.json.JsonObject;
@@ -78,9 +80,24 @@ public AbstractJsonSchemaElement getSchema() throws JsonSchemaException {
throw new JsonSchemaException(
new ParsingError(ParsingMessage.UNRESOLVABLE_REFERENCE, ref));
}
+
+ AbstractJsonSchemaElement root = this;
- schema = parser.parse(ref_locator, this, ref_pointer, jsubschema, null);
- } catch(IOException | JsonException | IllegalArgumentException ex) {
+ if (!locator.uri.equals(ref_locator.uri)) {
+ // if the reference goes inside other document or points to tha 'anchor',
+ // parse this document as a 'parent'
+ if (ref_pointer.length() > 1) {
+ final JsonValue val = ref_locator.getSchema("/");
+ root = parser.parse(ref_locator, this, "/", val, null);
+ } else if (ref_locator.uri.getFragment() != null) {
+ final JsonValue val = ref_locator.getSchema("/");
+ root = parser.parse(ref_locator.resolve(
+ new URI(ref_locator.uri.getScheme(), ref_locator.uri.getSchemeSpecificPart(), null)),
+ this, "/", val, null);
+ }
+ }
+ schema = parser.parse(ref_locator, root, ref_pointer, jsubschema, null);
+ } catch(IOException | JsonException | IllegalArgumentException | URISyntaxException ex) {
throw new JsonSchemaException(
new ParsingError(ParsingMessage.INVALID_REFERENCE, ref));
}
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonStringSchemaImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonStringSchemaImpl.java
index 6929636..6d75dff 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonStringSchemaImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/JsonStringSchemaImpl.java
@@ -151,7 +151,7 @@ public boolean validate(String jsonPointer, JsonValue value, JsonValue parent,
return nerrors == errors.size();
}
-
+
private void validate(String jsonPointer, String string, List errors) {
if (minLength != null && string.codePointCount(0, string.length()) < minLength) {
diff --git a/src/main/java/es/elixir/bsc/json/schema/model/impl/PrimitiveSchemaImpl.java b/src/main/java/es/elixir/bsc/json/schema/model/impl/PrimitiveSchemaImpl.java
index 4796cf2..a6f92ce 100644
--- a/src/main/java/es/elixir/bsc/json/schema/model/impl/PrimitiveSchemaImpl.java
+++ b/src/main/java/es/elixir/bsc/json/schema/model/impl/PrimitiveSchemaImpl.java
@@ -229,7 +229,7 @@ public PrimitiveSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
final JsonArray jallOf = JsonSchemaUtil.check(object.get(ALL_OF), JsonValue.ValueType.ARRAY);
if (jallOf != null) {
- final JsonAllOfImpl _allOf = new JsonAllOfImpl(this, locator, jsonPointer + "/" + ALL_OF)
+ final JsonAllOfImpl _allOf = new JsonAllOfImpl(this, locator, getJsonPointer() + "/" + ALL_OF)
.read(parser, jallOf);
if (allOf == null) {
allOf = _allOf;
@@ -242,13 +242,13 @@ public PrimitiveSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
final JsonArray janyOf = JsonSchemaUtil.check(object.get(ANY_OF), JsonValue.ValueType.ARRAY);
if (janyOf != null) {
- anyOf = new JsonAnyOfImpl(this, locator, jsonPointer + "/" + ANY_OF);
+ anyOf = new JsonAnyOfImpl(this, locator, getJsonPointer() + "/" + ANY_OF);
anyOf.read(parser, janyOf);
}
final JsonArray joneOf = JsonSchemaUtil.check(object.get(ONE_OF), JsonValue.ValueType.ARRAY);
if (joneOf != null) {
- oneOf = new JsonOneOfImpl(this, locator, jsonPointer + "/" + ONE_OF);
+ oneOf = new JsonOneOfImpl(this, locator, getJsonPointer() + "/" + ONE_OF);
oneOf.read(parser, joneOf);
}
@@ -257,7 +257,7 @@ public PrimitiveSchemaImpl read(JsonSubschemaParser parser, JsonObject object)
switch(jnot.getValueType()) {
case OBJECT:
case TRUE:
- case FALSE: not = new JsonNotImpl(this, locator, jsonPointer + "/" + NOT)
+ case FALSE: not = new JsonNotImpl(this, locator, getJsonPointer() + "/" + NOT)
.read(parser, jnot);
break;
default: throw new JsonSchemaException(new ParsingError(ParsingMessage.INVALID_ATTRIBUTE_TYPE,
diff --git a/src/test/java/es/elixir/bsc/json/schema/org/tests/JsonSchemaRefRemoteTest.java b/src/test/java/es/elixir/bsc/json/schema/org/tests/JsonSchemaRefRemoteTest.java
index ea19e58..a6ad30b 100644
--- a/src/test/java/es/elixir/bsc/json/schema/org/tests/JsonSchemaRefRemoteTest.java
+++ b/src/test/java/es/elixir/bsc/json/schema/org/tests/JsonSchemaRefRemoteTest.java
@@ -25,6 +25,7 @@
package es.elixir.bsc.json.schema.org.tests;
+import es.elixir.bsc.json.schema.JsonSchemaVersion;
import org.junit.Test;
/**
@@ -33,9 +34,33 @@
public class JsonSchemaRefRemoteTest extends JsonSchemaOrgTest {
private final static String JSON_DRAFT4_TEST_FILE = "json-schema-org/tests/draft4/refRemote.json";
+ private final static String JSON_DRAFT6_TEST_FILE = "json-schema-org/tests/draft6/refRemote.json";
+ private final static String JSON_DRAFT7_TEST_FILE = "json-schema-org/tests/draft7/refRemote.json";
+ private final static String JSON_DRAFT201909_TEST_FILE = "json-schema-org/tests/draft2019-09/refRemote.json";
+ private final static String JSON_DRAFT202012_TEST_FILE = "json-schema-org/tests/draft2020-12/refRemote.json";
-// @Test
-// public void test_draft4() {
-// test(JSON_DRAFT4_TEST_FILE);
-// }
+ @Test
+ public void test_draft4() {
+ test(JSON_DRAFT4_TEST_FILE);
+ }
+
+ @Test
+ public void test_draft6() {
+ test(JSON_DRAFT6_TEST_FILE, JsonSchemaVersion.SCHEMA_DRAFT_06);
+ }
+
+ @Test
+ public void test_draft7() {
+ test(JSON_DRAFT7_TEST_FILE, JsonSchemaVersion.SCHEMA_DRAFT_07);
+ }
+
+ @Test
+ public void test_draft201909() {
+ test(JSON_DRAFT201909_TEST_FILE, JsonSchemaVersion.SCHEMA_DRAFT_2019_09);
+ }
+
+ @Test
+ public void test_draft202012() {
+ test(JSON_DRAFT202012_TEST_FILE, JsonSchemaVersion.SCHEMA_DRAFT_2020_12);
+ }
}