Skip to content

Commit b1e3abf

Browse files
committed
Deprecate validateAndCollect in favor of explicitly calling loadCollectors
1 parent d92caed commit b1e3abf

File tree

6 files changed

+403
-52
lines changed

6 files changed

+403
-52
lines changed

doc/collector-context.md

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### CollectorContext
22

3-
There could be use cases where we want collect the information while we are validating the data. A simple example could be fetching some value from a database or from a microservice based on the data (which could be a text or a JSON object. It should be noted that this should be a simple operation or validation might take more time to complete.) in a given JSON node and the schema keyword we are using.
3+
There could be use cases where we want collect the information while we are validating the data. A simple example could be fetching some value from a database or from a microservice based on the data (which could be a text or a JSON object. It should be noted that this should be a simple operation or validation might take more time to complete.) in a given JSON node and the schema keyword we are using.
44

55
The fetched data can be stored somewhere so that it can be used later after the validation is done. Since the current validation logic already parses the data and schema, both validation and collecting the required information can be done in one go.
66

@@ -10,6 +10,12 @@ The `CollectorContext` and `Collector` classes are designed to work with this us
1010

1111
The `CollectorContext` is stored as a variable on the `ExecutionContext` that is used during the validation. This allows users to add objects to context at many points in the framework like Formats and Validators where the `ExecutionContext` is available as a parameter.
1212

13+
By default the `CollectorContext` created by the library contains maps backed by `HashMap`. If the `CollectorContext` needs to be shared by multiple threads then a `ConcurrentHashMap` needs to be used.
14+
15+
```java
16+
CollectorContext collectorContext = new CollectorContext(new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
17+
```
18+
1319
Collectors are added to `CollectorContext`. Collectors allow to collect the objects. A `Collector` is added to `CollectorContext` with a name and corresponding `Collector` instance.
1420

1521
```java
@@ -28,29 +34,44 @@ However there might be use cases where we want to add a simple Object like Strin
2834

2935
```java
3036
CollectorContext collectorContext = executionContext.getCollectorContext();
31-
collectorContext.add(SAMPLE_COLLECTOR, "sample-string")
37+
collectorContext.add(SAMPLE_COLLECTOR, "sample-string");
3238
```
3339

34-
To use the `CollectorContext` while validating, the `validateAndCollect` method has to be invoked on the `JsonSchema` class.
35-
This method returns a `ValidationResult` that contains the errors encountered during validation and a `ExecutionContext` instance that contains the `CollectorContext`.
36-
Objects constructed by collectors or directly added to `CollectorContext` can be retrieved from `CollectorContext` by using the name they were added with.
37-
38-
To collect across multiple validation runs, the `CollectorContext` needs to be explicitly reused by passing the `ExecutionContext` as a parameter to the validation.
40+
Implementations that need to modify values the `CollectorContext` should do so in a thread-safe manner.
3941

4042
```java
41-
ValidationResult validationResult = jsonSchema.validateAndCollect(jsonNode);
42-
ExecutionContext executionContext = validationResult.getExecutionContext();
4343
CollectorContext collectorContext = executionContext.getCollectorContext();
44-
List<String> contextValue = (List<String>) collectorContext.get(SAMPLE_COLLECTOR);
44+
AtomicInteger count = (AtomicInteger) collectorContext.getCollectorMap().computeIfAbsent(SAMPLE_COLLECTOR,
45+
(key) -> new AtomicInteger(0));
46+
count.incrementAndGet();
47+
```
48+
49+
To use the `CollectorContext` while validating, the `CollectorContext` should be instantiated outside and set for every validation execution.
4550

46-
// Do something with contextValue
47-
...
51+
At the end of all the runs the `CollectorContext.loadCollectors()` method can be called if needed for the `Collector` implementations to aggregate values.
4852

49-
// To collect more information for subsequent runs reuse the context
50-
validationResult = jsonSchema.validateAndCollect(executionContext, jsonNode);
53+
```java
54+
// This creates a CollectorContext that can be used by multiple threads although this is not neccessary in this example
55+
CollectorContext collectorContext = new CollectorContext(new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
56+
// This adds a custom collect keyword that sets values in the CollectorContext whenever it gets processed
57+
JsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012()).keyword(new CollectKeyword()).build();
58+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
59+
JsonSchema schema = factory.getSchema("{\n"
60+
+ " \"collect\": true\n"
61+
+ "}");
62+
for (int i = 0; i < 50; i++) {
63+
// The shared CollectorContext is set on the ExecutionContext for every run to aggregate data from all the runs
64+
schema.validate("1", InputFormat.JSON, executionContext -> {
65+
executionContext.setCollectorContext(collectorContext);
66+
});
67+
}
68+
// This is called for Collector implementations to aggregate data
69+
collectorContext.loadCollectors();
70+
AtomicInteger result = (AtomicInteger) collectorContext.get("collect");
71+
assertEquals(50, result.get());
5172
```
5273

53-
There might be use cases where a collector needs to collect the data at multiple touch points. For example one use case might be collecting data in a validator and a formatter. If you are using a `Collector` rather than a `Object`, the combine method of the `Collector` allows to define how we want to combine the data into existing `Collector`. `CollectorContext` `combineWithCollector` method calls the combine method on the `Collector`. User just needs to call the `CollectorContext` `combineWithCollector` method every time some data needs to merged into existing `Collector`. The `collect` method on the `Collector` is called by the framework at the end of validation to return the data that was collected.
74+
There might be use cases where a collector needs to collect the data at multiple touch points. For example one use case might be collecting data in a validator and a formatter. If you are using a `Collector` rather than a `Object`, the combine method of the `Collector` allows to define how we want to combine the data into existing `Collector`. `CollectorContext` `combineWithCollector` method calls the combine method on the `Collector`. User just needs to call the `CollectorContext` `combineWithCollector` method every time some data needs to merged into existing `Collector`. The `collect` method on the `Collector` is called by explicitly calling `CollectorContext.loadCollectors()` at the end of processing.
5475

5576
```java
5677
class CustomCollector implements Collector<List<String>> {
@@ -70,16 +91,25 @@ class CustomCollector implements Collector<List<String>> {
7091

7192
@Override
7293
public void combine(Object object) {
73-
returnList.add(referenceMap.get((String) object));
94+
synchronized(returnList) {
95+
returnList.add(referenceMap.get((String) object));
96+
}
7497
}
7598
}
99+
```
76100

77-
CollectorContext collectorContext = executionContext.getCollectorContext();
78-
if (collectorContext.get(SAMPLE_COLLECTOR) == null) {
79-
collectorContext.add(SAMPLE_COLLECTOR, new CustomCollector());
101+
```java
102+
private class CustomValidator extends AbstractJsonValidator {
103+
@Override
104+
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
105+
JsonNodePath instanceLocation) {
106+
CollectorContext collectorContext = executionContext.getCollectorContext();
107+
CustomCollector customCollector = (CustomCollector) collectorContext.getCollectorMap().computeIfAbsent(SAMPLE_COLLECTOR,
108+
key -> new CustomCollector());
109+
customCollector.combine(node.textValue());
110+
return Collections.emptySet();
111+
}
80112
}
81-
collectorContext.combineWithCollector(SAMPLE_COLLECTOR, node.textValue());
82-
83113
```
84114

85115
One important thing to note when using Collectors is if we call get method on `CollectorContext` before the validation is complete, we would get back a `Collector` instance that was added to `CollectorContext`.
@@ -96,12 +126,9 @@ List<String> data = collectorContext.get(SAMPLE_COLLECTOR);
96126
If you are using simple objects and if the data needs to be collected from multiple touch points, logic is straightforward as shown.
97127

98128
```java
99-
CollectorContext collectorContext = executionContext.getCollectorContext();
100-
// If collector name is not added to context add one.
101-
if (collectorContext.get(SAMPLE_COLLECTOR) == null) {
102-
collectorContext.add(SAMPLE_COLLECTOR, new ArrayList<String>());
129+
List<String> returnList = (List<String>) collectorContext.getCollectorMap()
130+
.computeIfAbsent(SAMPLE_COLLECTOR, key -> new ArrayList<String>());
131+
synchronized(returnList) {
132+
returnList.add(node.textValue());
103133
}
104-
// In this case we are adding a list to CollectorContext.
105-
List<String> returnList = (List<String>) collectorContext.get(SAMPLE_COLLECTOR);
106-
107-
```
134+
```

src/main/java/com/networknt/schema/CollectorContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public void combineWithCollector(String name, Object data) {
140140
/**
141141
* Loads data from all collectors.
142142
*/
143-
void loadCollectors() {
143+
public void loadCollectors() {
144144
Set<Entry<String, Object>> entrySet = this.collectorMap.entrySet();
145145
for (Entry<String, Object> entry : entrySet) {
146146
if (entry.getValue() instanceof Collector<?>) {

src/main/java/com/networknt/schema/JsonSchema.java

Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -942,11 +942,21 @@ private JsonNode deserialize(String input, InputFormat inputFormat) {
942942
}
943943
}
944944

945+
/**
946+
* Deprecated. Initialize the CollectorContext externally and call loadCollectors when done.
947+
* <p>
948+
* @param executionContext ExecutionContext
949+
* @param node JsonNode
950+
* @return ValidationResult
951+
*/
952+
@Deprecated
945953
public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) {
946954
return validateAndCollect(executionContext, node, node, atRoot());
947955
}
948956

949957
/**
958+
* Deprecated. Initialize the CollectorContext externally and call loadCollectors when done.
959+
* <p>
950960
* This method both validates and collects the data in a CollectorContext.
951961
* Unlike others this methods cleans and removes everything from collector
952962
* context before returning.
@@ -957,6 +967,7 @@ public ValidationResult validateAndCollect(ExecutionContext executionContext, Js
957967
*
958968
* @return ValidationResult
959969
*/
970+
@Deprecated
960971
private ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) {
961972
// Validate.
962973
Set<ValidationMessage> errors = validate(executionContext, jsonNode, rootNode, instanceLocation);
@@ -976,6 +987,13 @@ private ValidationResult validateAndCollect(ExecutionContext executionContext, J
976987
return new ValidationResult(errors, executionContext);
977988
}
978989

990+
/**
991+
* Deprecated. Initialize the CollectorContext externally and call loadCollectors when done.
992+
*
993+
* @param node JsonNode
994+
* @return ValidationResult
995+
*/
996+
@Deprecated
979997
public ValidationResult validateAndCollect(JsonNode node) {
980998
return validateAndCollect(createExecutionContext(), node, node, atRoot());
981999
}
@@ -984,6 +1002,38 @@ public ValidationResult validateAndCollect(JsonNode node) {
9841002

9851003
/*********************** START OF WALK METHODS **********************************/
9861004

1005+
/**
1006+
* Walk the JSON node.
1007+
*
1008+
* @param executionContext the execution context
1009+
* @param node the input
1010+
* @param validate true to validate the input against the schema
1011+
* @param executionCustomizer the customizer
1012+
*
1013+
* @return the validation result
1014+
*/
1015+
public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean validate,
1016+
ExecutionContextCustomizer executionCustomizer) {
1017+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, OutputFormat.RESULT,
1018+
executionCustomizer);
1019+
}
1020+
1021+
/**
1022+
* Walk the JSON node.
1023+
*
1024+
* @param executionContext the execution context
1025+
* @param node the input
1026+
* @param validate true to validate the input against the schema
1027+
* @param executionCustomizer the customizer
1028+
*
1029+
* @return the validation result
1030+
*/
1031+
public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean validate,
1032+
Consumer<ExecutionContext> executionCustomizer) {
1033+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, OutputFormat.RESULT,
1034+
executionCustomizer);
1035+
}
1036+
9871037
/**
9881038
* Walk the JSON node.
9891039
*
@@ -994,9 +1044,11 @@ public ValidationResult validateAndCollect(JsonNode node) {
9941044
* @return the validation result
9951045
*/
9961046
public ValidationResult walk(ExecutionContext executionContext, JsonNode node, boolean validate) {
997-
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate);
1047+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, OutputFormat.RESULT,
1048+
(ExecutionContextCustomizer) null);
9981049
}
9991050

1051+
10001052
/**
10011053
* Walk the input.
10021054
*
@@ -1009,7 +1061,41 @@ public ValidationResult walk(ExecutionContext executionContext, JsonNode node, b
10091061
public ValidationResult walk(ExecutionContext executionContext, String input, InputFormat inputFormat,
10101062
boolean validate) {
10111063
JsonNode node = deserialize(input, inputFormat);
1012-
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate);
1064+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, OutputFormat.RESULT,
1065+
(ExecutionContextCustomizer) null);
1066+
}
1067+
1068+
/**
1069+
* Walk the input.
1070+
*
1071+
* @param executionContext the execution context
1072+
* @param input the input
1073+
* @param inputFormat the input format
1074+
* @param validate true to validate the input against the schema
1075+
* @param executionCustomizer the customizer
1076+
* @return the validation result
1077+
*/
1078+
public ValidationResult walk(ExecutionContext executionContext, String input, InputFormat inputFormat,
1079+
boolean validate, ExecutionContextCustomizer executionCustomizer) {
1080+
JsonNode node = deserialize(input, inputFormat);
1081+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, OutputFormat.RESULT, executionCustomizer);
1082+
}
1083+
1084+
/**
1085+
* Walk the input.
1086+
*
1087+
* @param executionContext the execution context
1088+
* @param input the input
1089+
* @param inputFormat the input format
1090+
* @param outputFormat the output format
1091+
* @param validate true to validate the input against the schema
1092+
* @param executionCustomizer the customizer
1093+
* @return the validation result
1094+
*/
1095+
public <T> T walk(ExecutionContext executionContext, String input, InputFormat inputFormat,
1096+
OutputFormat<T> outputFormat, boolean validate, ExecutionContextCustomizer executionCustomizer) {
1097+
JsonNode node = deserialize(input, inputFormat);
1098+
return walkAtNodeInternal(executionContext, node, node, atRoot(), validate, outputFormat, executionCustomizer);
10131099
}
10141100

10151101
/**
@@ -1035,6 +1121,34 @@ public ValidationResult walk(String input, InputFormat inputFormat, boolean vali
10351121
return walk(createExecutionContext(), deserialize(input, inputFormat), validate);
10361122
}
10371123

1124+
/**
1125+
* Walk the input.
1126+
*
1127+
* @param input the input
1128+
* @param inputFormat the input format
1129+
* @param validate true to validate the input against the schema
1130+
* @param executionCustomizer the customizer
1131+
* @return the validation result
1132+
*/
1133+
public ValidationResult walk(String input, InputFormat inputFormat, boolean validate,
1134+
ExecutionContextCustomizer executionCustomizer) {
1135+
return walk(createExecutionContext(), deserialize(input, inputFormat), validate, executionCustomizer);
1136+
}
1137+
1138+
/**
1139+
* Walk the input.
1140+
*
1141+
* @param input the input
1142+
* @param inputFormat the input format
1143+
* @param validate true to validate the input against the schema
1144+
* @param executionCustomizer the customizer
1145+
* @return the validation result
1146+
*/
1147+
public ValidationResult walk(String input, InputFormat inputFormat, boolean validate,
1148+
Consumer<ExecutionContext> executionCustomizer) {
1149+
return walk(createExecutionContext(), deserialize(input, inputFormat), validate, executionCustomizer);
1150+
}
1151+
10381152
/**
10391153
* Walk at the node.
10401154
*
@@ -1047,25 +1161,39 @@ public ValidationResult walk(String input, InputFormat inputFormat, boolean vali
10471161
*/
10481162
public ValidationResult walkAtNode(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
10491163
JsonNodePath instanceLocation, boolean validate) {
1050-
return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, validate);
1164+
return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, validate, OutputFormat.RESULT,
1165+
(ExecutionContextCustomizer) null);
10511166
}
10521167

1053-
private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
1054-
JsonNodePath instanceLocation, boolean shouldValidateSchema) {
1168+
private <T> T walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
1169+
JsonNodePath instanceLocation, boolean validate, OutputFormat<T> format, Consumer<ExecutionContext> executionCustomizer) {
1170+
return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, validate, format,
1171+
(executeContext, validationContext) -> {
1172+
executionCustomizer.accept(executeContext);
1173+
});
1174+
}
1175+
1176+
private <T> T walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
1177+
JsonNodePath instanceLocation, boolean validate, OutputFormat<T> format,
1178+
ExecutionContextCustomizer executionCustomizer) {
1179+
if (executionCustomizer != null) {
1180+
executionCustomizer.customize(executionContext, this.validationContext);
1181+
}
10551182
// Walk through the schema.
1056-
Set<ValidationMessage> errors = walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
1183+
Set<ValidationMessage> errors = walk(executionContext, node, rootNode, instanceLocation, validate);
10571184

10581185
// Get the config.
10591186
SchemaValidatorsConfig config = this.validationContext.getConfig();
10601187
// When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors.
1188+
/* When doLoadCollectors is removed after the deprecation period the following block should be removed */
10611189
if (config.doLoadCollectors()) {
10621190
// Get the collector context.
10631191
CollectorContext collectorContext = executionContext.getCollectorContext();
10641192

10651193
// Load all the data from collectors into the context.
10661194
collectorContext.loadCollectors();
10671195
}
1068-
return new ValidationResult(errors, executionContext);
1196+
return format.format(this, errors, executionContext, this.validationContext);
10691197
}
10701198

10711199
@Override

0 commit comments

Comments
 (0)