diff --git a/README.md b/README.md index e312f3a3b..6e0323da1 100644 --- a/README.md +++ b/README.md @@ -341,8 +341,8 @@ Assertions contains the following additional information | Arguments | The arguments used for generating the message. | Type | The keyword that generated the message. | Property | The property name that caused the validation error for example for the `required` keyword. Note that this is not part of the instance location as that points to the instance node. -| Schema Node | The `JsonNode` pointed to by the Schema Location. -| Instance Node | The `JsonNode` pointed to by the Instance Location. +| Schema Node | The `JsonNode` pointed to by the Schema Location. This is the schema data that caused the input data to fail. It is possible to get the location information by configuring the `JsonSchemaFactory` with the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(schemaNode)`. +| Instance Node | The `JsonNode` pointed to by the Instance Location. This is the input data that failed validation. It is possible to get the location information by configuring the `JsonSchemaFactory` with the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(instanceNode)`. | Details | Additional details that can be set by custom keyword validator implementations. This is not used by the library. Annotations contains the following additional information @@ -351,6 +351,58 @@ Annotations contains the following additional information |-------------------|------------------- | Value | The annotation value generated +##### Line and Column Information + +The library can be configured to store line and column information in the `JsonNode` instances for the instance and schema nodes. This will adversely affect performance and is not configured by default. + +This is done by configuring a `LocationJsonNodeFactoryFactory` on the `JsonSchemaFactory`. The `JsonLocation` information can then be retrieved using `JsonNodes.tokenLocationOf(jsonNode)`. + +```java +String schemaData = "{\r\n" + + " \"$id\": \"https://schema/myschema\",\r\n" + + " \"properties\": {\r\n" + + " \"startDate\": {\r\n" + + " \"format\": \"date\",\r\n" + + " \"minLength\": 6\r\n" + + " }\r\n" + + " }\r\n" + + "}"; +String inputData = "{\r\n" + + " \"startDate\": \"1\"\r\n" + + "}"; +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.jsonNodeFactoryFactory(LocationJsonNodeFactoryFactory.getInstance())); +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setPathType(PathType.JSON_POINTER); +JsonSchema schema = factory.getSchema(schemaData, InputFormat.JSON, config); +Set messages = schema.validate(inputData, InputFormat.JSON, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +}); +List list = messages.stream().collect(Collectors.toList()); +ValidationMessage format = list.get(0); +JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode()); +JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode()); +ValidationMessage minLength = list.get(1); +JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode()); +JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode()); + +assertEquals("format", format.getType()); +assertEquals("date", format.getSchemaNode().asText()); +assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); +assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr()); +assertEquals("1", format.getInstanceNode().asText()); +assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); +assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr()); +assertEquals("minLength", minLength.getType()); +assertEquals("6", minLength.getSchemaNode().asText()); +assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); +assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr()); +assertEquals("1", minLength.getInstanceNode().asText()); +assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr()); +assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); +assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); +``` + #### Output formats @@ -479,9 +531,9 @@ The following is sample output from the Hierarchical format. When the library creates a schema from the schema factory, it creates a distinct validator instance for each location on the evaluation path. This means if there are different `$ref` that reference the same schema location, different validator instances are created for each evaluation path. -When the schema is created, the library will automatically preload all the validators needed and resolve references. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved. +When the schema is created, the library will by default automatically preload all the validators needed and resolve references. This can be disabled with the `preloadJsonSchema` option in the `SchemaValidatorsConfig`. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved. -The `JsonSchema` created from the factory should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved. +Instances for `JsonSchemaFactory` and the `JsonSchema` created from it are designed to be thread-safe provided its configuration is not modified and should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved. When references are resolved, the validators created will be cached. For schemas that have deeply nested references, the memory needed for the validators may be very high, in which case the caching may need to be disabled using the `cacheRefs` option in the `JsonSchemaValidatorsConfig`. Disabling this will mean the validators from the references need to be re-created for each validation run which will impact performance. Collecting annotations will adversely affect validation performance. diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 0c5519122..4ae26bb4c 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -369,7 +369,7 @@ public static JsonMetaSchema getV202012() { * Use {@link #getV4()} for the Draft 4 Metaschema, or if you need a builder based on Draft4, use * * - * JsonMetaSchema.builder("http://your-metaschema-iri", JsonMetaSchema.getDraftV4()).build(); + * JsonMetaSchema.builder("http://your-metaschema-iri", JsonMetaSchema.getV4()).build(); * * * @param iri the IRI of the metaschema that will be defined via this builder. diff --git a/src/main/java/com/networknt/schema/JsonNodeLocation.java b/src/main/java/com/networknt/schema/JsonNodeLocation.java new file mode 100644 index 000000000..7f888c135 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonNodeLocation.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +/** + * JsonNode location information. + */ +public class JsonNodeLocation { + private final int line; + private final int column; + + public JsonNodeLocation(int line, int column) { + super(); + this.line = line; + this.column = column; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + + @Override + public String toString() { + return "JsonNodeLocation [line=" + line + ", column=" + column + "]"; + } +} diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 9e535527b..d7661234c 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -16,15 +16,13 @@ package com.networknt.schema; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.serialization.JsonMapperFactory; -import com.networknt.schema.serialization.YamlMapperFactory; import com.networknt.schema.utils.JsonNodes; import com.networknt.schema.utils.SetView; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; @@ -46,6 +44,9 @@ * This is the core of json constraint implementation. It parses json constraint * file and generates JsonValidators. The class is thread safe, once it is * constructed, it can be used to validate multiple json data concurrently. + *

+ * JsonSchema instances are thread-safe provided its configuration is not + * modified. */ public class JsonSchema extends BaseJsonValidator { private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue(); @@ -871,15 +872,10 @@ public T validate(ExecutionContext executionContext, JsonNode node, OutputFo */ private JsonNode deserialize(String input, InputFormat inputFormat) { try { - if (InputFormat.JSON.equals(inputFormat)) { - return JsonMapperFactory.getInstance().readTree(input); - } else if (InputFormat.YAML.equals(inputFormat)) { - return YamlMapperFactory.getInstance().readTree(input); - } - } catch (JsonProcessingException e) { + return this.getValidationContext().getJsonSchemaFactory().readTree(input, inputFormat); + } catch (IOException e) { throw new IllegalArgumentException("Invalid input", e); } - throw new IllegalArgumentException("Unsupported input format "+inputFormat); } public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) { diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 3a537cad9..ce8363dad 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -26,6 +26,8 @@ import com.networknt.schema.resource.SchemaMappers; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.serialization.YamlMapperFactory; +import com.networknt.schema.serialization.node.JsonNodeFactoryFactory; +import com.networknt.schema.utils.JsonNodes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,10 +43,12 @@ import java.util.function.Consumer; /** - * Factory for building {@link JsonSchema} instances. + * Factory for building {@link JsonSchema} instances. The factory should be + * typically be created using {@link #getInstance(VersionFlag, Consumer)} and + * should be cached for performance. *

- * The factory should be typically be created using - * {@link #getInstance(VersionFlag, Consumer)}. + * JsonSchemaFactory instances are thread-safe provided its configuration is not + * modified. */ public class JsonSchemaFactory { private static final Logger logger = LoggerFactory.getLogger(JsonSchemaFactory.class); @@ -52,6 +56,7 @@ public class JsonSchemaFactory { public static class Builder { private ObjectMapper jsonMapper = null; private ObjectMapper yamlMapper = null; + private JsonNodeFactoryFactory jsonNodeFactoryFactory = null; private String defaultMetaSchemaIri; private final ConcurrentMap metaSchemas = new ConcurrentHashMap(); private SchemaLoaders.Builder schemaLoadersBuilder = null; @@ -59,6 +64,21 @@ public static class Builder { private boolean enableSchemaCache = true; private JsonMetaSchemaFactory metaSchemaFactory = null; + /** + * Configures the {@link JsonNodeFactoryFactory} to use. + *

+ * To get location information from {@link JsonNode} the + * {@link com.networknt.schema.serialization.node.LocationJsonNodeFactoryFactory} + * can be used. + * + * @param jsonNodeFactoryFactory the factory to create json node factories + * @return the builder + */ + public Builder jsonNodeFactoryFactory(JsonNodeFactoryFactory jsonNodeFactoryFactory) { + this.jsonNodeFactoryFactory = jsonNodeFactoryFactory; + return this; + } + public Builder jsonMapper(final ObjectMapper jsonMapper) { this.jsonMapper = jsonMapper; return this; @@ -131,6 +151,7 @@ public JsonSchemaFactory build() { return new JsonSchemaFactory( jsonMapper, yamlMapper, + jsonNodeFactoryFactory, defaultMetaSchemaIri, schemaLoadersBuilder, schemaMappersBuilder, @@ -143,6 +164,7 @@ public JsonSchemaFactory build() { private final ObjectMapper jsonMapper; private final ObjectMapper yamlMapper; + private final JsonNodeFactoryFactory jsonNodeFactoryFactory; private final String defaultMetaSchemaIri; private final SchemaLoaders.Builder schemaLoadersBuilder; private final SchemaMappers.Builder schemaMappersBuilder; @@ -158,6 +180,7 @@ public JsonSchemaFactory build() { private JsonSchemaFactory( ObjectMapper jsonMapper, ObjectMapper yamlMapper, + JsonNodeFactoryFactory jsonNodeFactoryFactory, String defaultMetaSchemaIri, SchemaLoaders.Builder schemaLoadersBuilder, SchemaMappers.Builder schemaMappersBuilder, @@ -174,6 +197,7 @@ private JsonSchemaFactory( } this.jsonMapper = jsonMapper; this.yamlMapper = yamlMapper; + this.jsonNodeFactoryFactory = jsonNodeFactoryFactory; this.defaultMetaSchemaIri = defaultMetaSchemaIri; this.schemaLoadersBuilder = schemaLoadersBuilder; this.schemaMappersBuilder = schemaMappersBuilder; @@ -265,7 +289,8 @@ public static Builder builder(final JsonSchemaFactory blueprint) { .metaSchemas(blueprint.metaSchemas.values()) .defaultMetaSchemaIri(blueprint.defaultMetaSchemaIri) .jsonMapper(blueprint.jsonMapper) - .yamlMapper(blueprint.yamlMapper); + .yamlMapper(blueprint.yamlMapper) + .jsonNodeFactoryFactory(blueprint.jsonNodeFactoryFactory); if (blueprint.schemaLoadersBuilder != null) { builder.schemaLoadersBuilder = SchemaLoaders.builder().with(blueprint.schemaLoadersBuilder); } @@ -416,6 +441,31 @@ protected JsonMetaSchema loadMetaSchema(String iri, SchemaValidatorsConfig confi : DefaultJsonMetaSchemaFactory.getInstance().getMetaSchema(iri, this, config); } + JsonNode readTree(String content, InputFormat inputFormat) throws IOException { + if (this.jsonNodeFactoryFactory == null) { + return getObjectMapper(inputFormat).readTree(content); + } else { + return JsonNodes.readTree(getObjectMapper(inputFormat), content, this.jsonNodeFactoryFactory); + } + } + + JsonNode readTree(InputStream content, InputFormat inputFormat) throws IOException { + if (this.jsonNodeFactoryFactory == null) { + return getObjectMapper(inputFormat).readTree(content); + } else { + return JsonNodes.readTree(getObjectMapper(inputFormat), content, this.jsonNodeFactoryFactory); + } + } + + ObjectMapper getObjectMapper(InputFormat inputFormat) { + if (InputFormat.JSON.equals(inputFormat)) { + return getJsonMapper(); + } else if (InputFormat.YAML.equals(inputFormat)) { + return getYamlMapper(); + } + throw new IllegalArgumentException("Unsupported input format "+inputFormat); + } + /** * Gets the schema. *

@@ -427,8 +477,23 @@ protected JsonMetaSchema loadMetaSchema(String iri, SchemaValidatorsConfig confi * @return the schema */ public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) { + return getSchema(schema, InputFormat.JSON, config); + } + + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schema the schema data as a string + * @param inputFormat input format + * @param config the config + * @return the schema + */ + public JsonSchema getSchema(final String schema, InputFormat inputFormat, final SchemaValidatorsConfig config) { try { - final JsonNode schemaNode = getJsonMapper().readTree(schema); + final JsonNode schemaNode = readTree(schema, inputFormat); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); @@ -436,6 +501,7 @@ public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig co } } + /** * Gets the schema. *

@@ -449,6 +515,20 @@ public JsonSchema getSchema(final String schema) { return getSchema(schema, createSchemaValidatorsConfig()); } + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schema the schema data as a string + * @param inputFormat input format + * @return the schema + */ + public JsonSchema getSchema(final String schema, InputFormat inputFormat) { + return getSchema(schema, inputFormat, createSchemaValidatorsConfig()); + } + /** * Gets the schema. *

@@ -460,8 +540,23 @@ public JsonSchema getSchema(final String schema) { * @return the schema */ public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) { + return getSchema(schemaStream, InputFormat.JSON, config); + } + + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + * + * @param schemaStream the input stream with the schema data + * @param inputFormat input format + * @param config the config + * @return the schema + */ + public JsonSchema getSchema(final InputStream schemaStream, InputFormat inputFormat, final SchemaValidatorsConfig config) { try { - final JsonNode schemaNode = getJsonMapper().readTree(schemaStream); + final JsonNode schemaNode = readTree(schemaStream, inputFormat); return newJsonSchema(null, schemaNode, config); } catch (IOException ioe) { logger.error("Failed to load json schema!", ioe); @@ -524,14 +619,18 @@ protected JsonSchema loadSchema(final SchemaLocation schemaUri, final SchemaVali return getMappedSchema(schemaUri, config); } - protected ObjectMapper getYamlMapper() { + ObjectMapper getYamlMapper() { return this.yamlMapper != null ? this.yamlMapper : YamlMapperFactory.getInstance(); } - - protected ObjectMapper getJsonMapper() { + + ObjectMapper getJsonMapper() { return this.jsonMapper != null ? this.jsonMapper : JsonMapperFactory.getInstance(); } + JsonNodeFactoryFactory getJsonNodeFactoryFactory() { + return this.jsonNodeFactoryFactory; + } + /** * Creates a schema validators config. * @@ -548,9 +647,9 @@ protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValid } final JsonNode schemaNode; if (isYaml(schemaUri)) { - schemaNode = getYamlMapper().readTree(inputStream); + schemaNode = readTree(inputStream, InputFormat.YAML); } else { - schemaNode = getJsonMapper().readTree(inputStream); + schemaNode = readTree(inputStream, InputFormat.JSON); } final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config); diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAware.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAware.java new file mode 100644 index 000000000..5e70699d8 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAware.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; + +/** + * JsonNodes that are aware of the token location will implement this interface. + */ +public interface JsonLocationAware { + /** + * Gets the token location. + * + * @return the token location + */ + JsonLocation tokenLocation(); +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareArrayNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareArrayNode.java new file mode 100644 index 000000000..1e59f8f2f --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareArrayNode.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import java.util.List; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + +/** + * {@link ArrayNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareArrayNode extends ArrayNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareArrayNode(JsonNodeFactory nf, int capacity, JsonLocation tokenLocation) { + super(nf, capacity); + this.tokenLocation = tokenLocation; + } + + public JsonLocationAwareArrayNode(JsonNodeFactory nf, List children, JsonLocation tokenLocation) { + super(nf, children); + this.tokenLocation = tokenLocation; + } + + public JsonLocationAwareArrayNode(JsonNodeFactory nf, JsonLocation tokenLocation) { + super(nf); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBigIntegerNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBigIntegerNode.java new file mode 100644 index 000000000..d1cd995fa --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBigIntegerNode.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import java.math.BigInteger; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.BigIntegerNode; + +/** + * {@link BigIntegerNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareBigIntegerNode extends BigIntegerNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareBigIntegerNode(BigInteger v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBinaryNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBinaryNode.java new file mode 100644 index 000000000..04bec537a --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBinaryNode.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.BinaryNode; + +/** + * {@link BinaryNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareBinaryNode extends BinaryNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareBinaryNode(byte[] data, JsonLocation tokenLocation) { + super(data); + this.tokenLocation = tokenLocation; + } + + public JsonLocationAwareBinaryNode(byte[] data, int offset, int length, JsonLocation tokenLocation) { + super(data, offset, length); + this.tokenLocation = tokenLocation; + } + + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBooleanNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBooleanNode.java new file mode 100644 index 000000000..2b6fee413 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareBooleanNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.BooleanNode; + +/** + * {@link BooleanNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareBooleanNode extends BooleanNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareBooleanNode(boolean v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDecimalNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDecimalNode.java new file mode 100644 index 000000000..f850cb4a5 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDecimalNode.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.DecimalNode; + +/** + * {@link DecimalNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareDecimalNode extends DecimalNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareDecimalNode(BigDecimal v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDoubleNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDoubleNode.java new file mode 100644 index 000000000..110a0836b --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareDoubleNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.DoubleNode; + +/** + * {@link DoubleNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareDoubleNode extends DoubleNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareDoubleNode(double v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareFloatNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareFloatNode.java new file mode 100644 index 000000000..54067ae0f --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareFloatNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.FloatNode; + +/** + * {@link FloatNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareFloatNode extends FloatNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareFloatNode(float v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareIntNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareIntNode.java new file mode 100644 index 000000000..e72fd28dc --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareIntNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.IntNode; + +/** + * {@link IntNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareIntNode extends IntNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareIntNode(int v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareLongNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareLongNode.java new file mode 100644 index 000000000..0ceb5714d --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareLongNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.LongNode; + +/** + * {@link LongNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareLongNode extends LongNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareLongNode(long v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareNullNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareNullNode.java new file mode 100644 index 000000000..e73463488 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareNullNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.NullNode; + +/** + * {@link NullNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareNullNode extends NullNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareNullNode(JsonLocation tokenLocation) { + super(); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareObjectNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareObjectNode.java new file mode 100644 index 000000000..6bf776c3a --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareObjectNode.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import java.util.Map; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * {@link ObjectNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareObjectNode extends ObjectNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareObjectNode(JsonNodeFactory nc, Map children, JsonLocation tokenLocation) { + super(nc, children); + this.tokenLocation = tokenLocation; + } + + public JsonLocationAwareObjectNode(JsonNodeFactory nc, JsonLocation tokenLocation) { + super(nc); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwarePOJONode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwarePOJONode.java new file mode 100644 index 000000000..7ca728363 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwarePOJONode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.POJONode; + +/** + * {@link POJONode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwarePOJONode extends POJONode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwarePOJONode(Object v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareShortNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareShortNode.java new file mode 100644 index 000000000..60027c05d --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareShortNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.ShortNode; + +/** + * {@link ShortNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareShortNode extends ShortNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareShortNode(short v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareTextNode.java b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareTextNode.java new file mode 100644 index 000000000..6f3b16cec --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonLocationAwareTextNode.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.databind.node.TextNode; + +/** + * {@link TextNode} that is {@link JsonLocationAware}. + */ +public class JsonLocationAwareTextNode extends TextNode implements JsonLocationAware { + /** + * + */ + private static final long serialVersionUID = 1L; + private final JsonLocation tokenLocation; + + public JsonLocationAwareTextNode(String v, JsonLocation tokenLocation) { + super(v); + this.tokenLocation = tokenLocation; + } + + @Override + public JsonLocation tokenLocation() { + return this.tokenLocation; + } +} diff --git a/src/main/java/com/networknt/schema/serialization/node/JsonNodeFactoryFactory.java b/src/main/java/com/networknt/schema/serialization/node/JsonNodeFactoryFactory.java new file mode 100644 index 000000000..cfaf6e5ee --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/JsonNodeFactoryFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + +/** + * Factory that produces a {@link JsonNodeFactory}. + */ +public interface JsonNodeFactoryFactory { + /** + * Gets the JsonNodeFactory. + * + * @param jsonParser the json parser + * @return the json node factory + */ + JsonNodeFactory getJsonNodeFactory(JsonParser jsonParser); +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactory.java b/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactory.java new file mode 100644 index 000000000..1f027fb71 --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactory.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BinaryNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import com.fasterxml.jackson.databind.util.RawValue; + +/** + * {@link JsonNodeFactory} that creates {@link JsonLocationAware} nodes. + *

+ * Note that this will adversely affect performance as nodes with the same value + * can no longer be cached and reused. + */ +public class LocationJsonNodeFactory extends JsonNodeFactory { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final JsonParser jsonParser; + + /** + * Constructor. + * + * @param jsonParser the json parser + */ + public LocationJsonNodeFactory(JsonParser jsonParser) { + this.jsonParser = jsonParser; + } + + @Override + public BooleanNode booleanNode(boolean v) { + return new JsonLocationAwareBooleanNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public NullNode nullNode() { + return new JsonLocationAwareNullNode(this.jsonParser.currentTokenLocation()); + } + + @Override + public JsonNode missingNode() { + return super.missingNode(); + } + + @Override + public NumericNode numberNode(byte v) { + return new JsonLocationAwareIntNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Byte v) { + return (v == null) ? nullNode() : numberNode(v.intValue()); + } + + @Override + public NumericNode numberNode(short v) { + return new JsonLocationAwareShortNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Short value) { + return (value == null) ? nullNode() : numberNode(value.shortValue()); + } + + @Override + public NumericNode numberNode(int v) { + return new JsonLocationAwareIntNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Integer v) { + return (v == null) ? nullNode() : numberNode(v.intValue()); + } + + @Override + public NumericNode numberNode(long v) { + return new JsonLocationAwareLongNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Long v) { + return (v == null) ? nullNode() : numberNode(v.longValue()); + } + + @Override + public ValueNode numberNode(BigInteger v) { + return (v == null) ? nullNode() : new JsonLocationAwareBigIntegerNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public NumericNode numberNode(float v) { + return new JsonLocationAwareFloatNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Float v) { + return (v == null) ? nullNode() : numberNode(v.floatValue()); + } + + @Override + public NumericNode numberNode(double v) { + return new JsonLocationAwareDoubleNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode numberNode(Double v) { + return (v == null) ? nullNode() : numberNode(v.doubleValue()); + } + + @Override + public ValueNode numberNode(BigDecimal v) { + return (v == null) ? nullNode() : new JsonLocationAwareDecimalNode(v, this.jsonParser.currentTokenLocation()); + } + + @Override + public TextNode textNode(String text) { + return new JsonLocationAwareTextNode(text, this.jsonParser.currentTokenLocation()); + } + + @Override + public BinaryNode binaryNode(byte[] data) { + return new JsonLocationAwareBinaryNode(data, this.jsonParser.currentTokenLocation()); + } + + @Override + public BinaryNode binaryNode(byte[] data, int offset, int length) { + return new JsonLocationAwareBinaryNode(data, offset, length, this.jsonParser.currentTokenLocation()); + } + + @Override + public ArrayNode arrayNode() { + return new JsonLocationAwareArrayNode(this, this.jsonParser.currentTokenLocation()); + } + + @Override + public ArrayNode arrayNode(int capacity) { + return new JsonLocationAwareArrayNode(this, capacity, this.jsonParser.currentTokenLocation()); + } + + @Override + public ObjectNode objectNode() { + return new JsonLocationAwareObjectNode(this, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode pojoNode(Object pojo) { + return new JsonLocationAwarePOJONode(pojo, this.jsonParser.currentTokenLocation()); + } + + @Override + public ValueNode rawValueNode(RawValue value) { + return new JsonLocationAwarePOJONode(value, this.jsonParser.currentTokenLocation()); + } + +} diff --git a/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactoryFactory.java b/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactoryFactory.java new file mode 100644 index 000000000..2adee508a --- /dev/null +++ b/src/main/java/com/networknt/schema/serialization/node/LocationJsonNodeFactoryFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.serialization.node; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + +/** + * {@link JsonNodeFactoryFactory} that produces {@link LocationJsonNodeFactory}. + *

+ * Note that this will adversely affect performance as nodes with the same value + * can no longer be cached and reused. + */ +public class LocationJsonNodeFactoryFactory implements JsonNodeFactoryFactory { + + private static final LocationJsonNodeFactoryFactory INSTANCE = new LocationJsonNodeFactoryFactory(); + + public static LocationJsonNodeFactoryFactory getInstance() { + return INSTANCE; + } + + @Override + public JsonNodeFactory getJsonNodeFactory(JsonParser jsonParser) { + return new LocationJsonNodeFactory(jsonParser); + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/utils/JsonNodes.java b/src/main/java/com/networknt/schema/utils/JsonNodes.java index 5cbf7cc1c..d9dbad879 100644 --- a/src/main/java/com/networknt/schema/utils/JsonNodes.java +++ b/src/main/java/com/networknt/schema/utils/JsonNodes.java @@ -15,8 +15,19 @@ */ package com.networknt.schema.utils; +import java.io.IOException; +import java.io.InputStream; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.networknt.schema.JsonNodePath; +import com.networknt.schema.serialization.node.JsonLocationAware; +import com.networknt.schema.serialization.node.JsonNodeFactoryFactory; /** * Utility methods for JsonNode. @@ -61,4 +72,59 @@ public static T get(JsonNode node, Object propertyOrIndex) } return (T) value; } + + /** + * Read a {@link JsonNode} from {@link String} content. + * + * @param objectMapper the object mapper + * @param content the string content + * @param jsonNodeFactoryFactory the factory + * @return the json node + */ + public static JsonNode readTree(ObjectMapper objectMapper, String content, + JsonNodeFactoryFactory jsonNodeFactoryFactory) { + JsonFactory factory = objectMapper.getFactory(); + try (JsonParser parser = factory.createParser(content)) { + JsonNodeFactory nodeFactory = jsonNodeFactoryFactory.getJsonNodeFactory(parser); + ObjectReader reader = objectMapper.reader(nodeFactory); + JsonNode result = reader.readTree(parser); + return (result != null) ? result : nodeFactory.missingNode(); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid input", e); + } + } + + /** + * Read a {@link JsonNode} from an {@link InputStream}. + * + * @param objectMapper the object mapper + * @param inputStream the string content + * @param jsonNodeFactoryFactory the factory + * @return the json node + */ + public static JsonNode readTree(ObjectMapper objectMapper, InputStream inputStream, + JsonNodeFactoryFactory jsonNodeFactoryFactory) { + JsonFactory factory = objectMapper.getFactory(); + try (JsonParser parser = factory.createParser(inputStream)) { + JsonNodeFactory nodeFactory = jsonNodeFactoryFactory.getJsonNodeFactory(parser); + ObjectReader reader = objectMapper.reader(nodeFactory); + JsonNode result = reader.readTree(parser); + return (result != null) ? result : nodeFactory.missingNode(); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid input", e); + } + } + + /** + * Gets the token location of the {@link JsonNode} that implements {@link JsonLocationAware}. + * + * @param jsonNode the node + * @return the JsonLocation + */ + public static JsonLocation tokenLocationOf(JsonNode jsonNode) { + if (jsonNode instanceof JsonLocationAware) { + return ((JsonLocationAware) jsonNode).tokenLocation(); + } + throw new IllegalArgumentException("JsonNode does not contain the location information."); + } } diff --git a/src/test/java/com/networknt/schema/JsonSchemaFactoryTest.java b/src/test/java/com/networknt/schema/JsonSchemaFactoryTest.java new file mode 100644 index 000000000..e68f9966d --- /dev/null +++ b/src/test/java/com/networknt/schema/JsonSchemaFactoryTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Tests for JsonSchemaFactory. + */ +class JsonSchemaFactoryTest { + @Test + void concurrency() { + String metaSchemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$id\": \"https://www.example.com/no-validation-no-format/schema\",\r\n" + + " \"$vocabulary\": {\r\n" + + " \"https://www.example.com/vocab/validation\": false,\r\n" + + " \"https://json-schema.org/draft/2020-12/vocab/applicator\": true,\r\n" + + " \"https://json-schema.org/draft/2020-12/vocab/core\": true\r\n" + + " },\r\n" + + " \"allOf\": [\r\n" + + " { \"$ref\": \"https://json-schema.org/draft/2020-12/meta/applicator\" },\r\n" + + " { \"$ref\": \"https://json-schema.org/draft/2020-12/meta/core\" }\r\n" + + " ]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(Collections + .singletonMap("https://www.example.com/no-validation-no-format/schema", + metaSchemaData)))); + AtomicBoolean failed = new AtomicBoolean(false); + JsonMetaSchema[] instance = new JsonMetaSchema[1];; + CountDownLatch latch = new CountDownLatch(1); + List threads = new ArrayList<>(); + for (int i = 0; i < 50; ++i) { + Runnable runner = new Runnable() { + public void run() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + JsonMetaSchema metaSchema = factory.getMetaSchema("https://www.example.com/no-validation-no-format/schema", config); + synchronized(instance) { + if (instance[0] == null) { + instance[0] = metaSchema; + } + // Ensure references are the same despite concurrency + if (!(instance[0] == metaSchema)) { + failed.set(true); + } + } + } + }; + Thread thread = new Thread(runner, "Thread" + i); + thread.start(); + threads.add(thread); + } + latch.countDown(); // Release the latch for threads to run concurrently + threads.forEach(t -> { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + assertFalse(failed.get()); + } +} diff --git a/src/test/java/com/networknt/schema/JsonSchemaTest.java b/src/test/java/com/networknt/schema/JsonSchemaTest.java new file mode 100644 index 000000000..506763c54 --- /dev/null +++ b/src/test/java/com/networknt/schema/JsonSchemaTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Tests for JsonSchemaFactory. + */ +class JsonSchemaTest { + @Test + void concurrency() throws Exception { + String schemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\r\n" + + " \"$id\": \"http://example.org/schema.json\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"$ref\": \"ref.json\"\r\n" + + "}"; + String refSchemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"$id\": \"http://example.org/ref.json\",\r\n" + + " \"properties\": {\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"description\": \"The name\"\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"name\"\r\n" + + " ]\r\n" + + " }\r\n" + + "}"; + String inputData = "{\r\n" + + " \"name\": 1\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders + .schemas(Collections.singletonMap("http://example.org/ref.json", refSchemaData)))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPreloadJsonSchema(false); + JsonSchema schema = factory.getSchema(schemaData, config); + Exception[] instance = new Exception[1]; + CountDownLatch latch = new CountDownLatch(1); + List threads = new ArrayList<>(); + for (int i = 0; i < 50; ++i) { + Runnable runner = new Runnable() { + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + try { + List messages = schema.validate(inputData, InputFormat.JSON) + .stream() + .collect(Collectors.toList()); + assertEquals(1, messages.size()); + } catch(Exception e) { + synchronized(instance) { + instance[0] = e; + } + } + } + }; + Thread thread = new Thread(runner, "Thread" + i); + thread.start(); + threads.add(thread); + } + latch.countDown(); // Release the latch for threads to run concurrently + threads.forEach(t -> { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + if (instance[0] != null) { + throw instance[0]; + } + } +} diff --git a/src/test/java/com/networknt/schema/utils/JsonNodesTest.java b/src/test/java/com/networknt/schema/utils/JsonNodesTest.java new file mode 100644 index 000000000..71a2abb0c --- /dev/null +++ b/src/test/java/com/networknt/schema/utils/JsonNodesTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.InputFormat; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.PathType; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.serialization.JsonMapperFactory; +import com.networknt.schema.serialization.node.LocationJsonNodeFactoryFactory; +/** + * Tests for JsonNodes. + */ +public class JsonNodesTest { + @Test + void location() throws JsonParseException, IOException { + String schemaData = "{\r\n" + + " \"$id\": \"https://schema/myschema\",\r\n" + + " \"properties\": {\r\n" + + " \"startDate\": {\r\n" + + " \"format\": \"date\",\r\n" + + " \"minLength\": 6\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + JsonNode jsonNode = JsonNodes.readTree(JsonMapperFactory.getInstance(), schemaData, + LocationJsonNodeFactoryFactory.getInstance()); + JsonNode idNode = jsonNode.at("/$id"); + JsonLocation location = JsonNodes.tokenLocationOf(idNode); + assertEquals(2, location.getLineNr()); + assertEquals(10, location.getColumnNr()); + + JsonNode formatNode = jsonNode.at("/properties/startDate/format"); + location = JsonNodes.tokenLocationOf(formatNode); + assertEquals(5, location.getLineNr()); + assertEquals(17, location.getColumnNr()); + + JsonNode minLengthNode = jsonNode.at("/properties/startDate/minLength"); + location = JsonNodes.tokenLocationOf(minLengthNode); + assertEquals(6, location.getLineNr()); + assertEquals(20, location.getColumnNr()); + } + + @Test + void jsonLocation() { + String schemaData = "{\r\n" + + " \"$id\": \"https://schema/myschema\",\r\n" + + " \"properties\": {\r\n" + + " \"startDate\": {\r\n" + + " \"format\": \"date\",\r\n" + + " \"minLength\": 6\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + String inputData = "{\r\n" + + " \"startDate\": \"1\"\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.jsonNodeFactoryFactory(LocationJsonNodeFactoryFactory.getInstance())); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, InputFormat.JSON, config); + Set messages = schema.validate(inputData, InputFormat.JSON, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + List list = messages.stream().collect(Collectors.toList()); + ValidationMessage format = list.get(0); + JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode()); + JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode()); + ValidationMessage minLength = list.get(1); + JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode()); + JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode()); + + assertEquals("format", format.getType()); + + assertEquals("date", format.getSchemaNode().asText()); + assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); + assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr()); + + assertEquals("1", format.getInstanceNode().asText()); + assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); + assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr()); + + assertEquals("minLength", minLength.getType()); + + assertEquals("6", minLength.getSchemaNode().asText()); + assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); + assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr()); + + assertEquals("1", minLength.getInstanceNode().asText()); + assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr()); + assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); + } + + @Test + void yamlLocation() { + String schemaData = "---\r\n" + + "\"$id\": 'https://schema/myschema'\r\n" + + "properties:\r\n" + + " startDate:\r\n" + + " format: 'date'\r\n" + + " minLength: 6\r\n" + + ""; + String inputData = "---\r\n" + + "startDate: '1'\r\n" + + ""; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.jsonNodeFactoryFactory(LocationJsonNodeFactoryFactory.getInstance())); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, InputFormat.YAML, config); + Set messages = schema.validate(inputData, InputFormat.YAML, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + List list = messages.stream().collect(Collectors.toList()); + ValidationMessage format = list.get(0); + JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode()); + JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode()); + ValidationMessage minLength = list.get(1); + JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode()); + JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode()); + + assertEquals("format", format.getType()); + + assertEquals("date", format.getSchemaNode().asText()); + assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); + assertEquals(13, formatSchemaNodeTokenLocation.getColumnNr()); + + assertEquals("1", format.getInstanceNode().asText()); + assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); + assertEquals(12, formatInstanceNodeTokenLocation.getColumnNr()); + + assertEquals("minLength", minLength.getType()); + + assertEquals("6", minLength.getSchemaNode().asText()); + assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); + assertEquals(16, minLengthSchemaNodeTokenLocation.getColumnNr()); + + assertEquals("1", minLength.getInstanceNode().asText()); + assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr()); + assertEquals(12, minLengthInstanceNodeTokenLocation.getColumnNr()); + } + + @Test + void missingNode() { + JsonNode missing = JsonNodes.readTree(JsonMapperFactory.getInstance(), + new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)), + LocationJsonNodeFactoryFactory.getInstance()); + assertTrue(missing.isMissingNode()); + } + + @Test + void types() { + String json = "{\r\n" + + " \"properties\": {\r\n" + + " \"number\": 1234.56789,\r\n" + + " \"string\": \"value\",\r\n" + + " \"boolean\": true,\r\n" + + " \"array\": [],\r\n" + + " \"object\": {},\r\n" + + " \"null\": null\r\n" + + " }\r\n" + + "}"; + ObjectMapper objectMapper = JsonMapperFactory.getInstance() + .copy() + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + JsonNode root = JsonNodes.readTree(objectMapper, json, LocationJsonNodeFactoryFactory.getInstance()); + JsonNode numberNode = root.at("/properties/number"); + assertEquals(3, JsonNodes.tokenLocationOf(numberNode).getLineNr()); + JsonNode stringNode = root.at("/properties/string"); + assertEquals(4, JsonNodes.tokenLocationOf(stringNode).getLineNr()); + JsonNode booleanNode = root.at("/properties/boolean"); + assertEquals(5, JsonNodes.tokenLocationOf(booleanNode).getLineNr()); + JsonNode arrayNode = root.at("/properties/array"); + assertEquals(6, JsonNodes.tokenLocationOf(arrayNode).getLineNr()); + JsonNode objectNode = root.at("/properties/object"); + assertEquals(7, JsonNodes.tokenLocationOf(objectNode).getLineNr()); + JsonNode nullNode = root.at("/properties/null"); + assertEquals(8, JsonNodes.tokenLocationOf(nullNode).getLineNr()); + } +}