Skip to content

Commit bb8e359

Browse files
author
Faron Dutton
committed
Simplifies how evaluated properties and array items are tracked in order to improve performance.
Resolves #721
1 parent e9dea3b commit bb8e359

18 files changed

+368
-319
lines changed

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

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import com.fasterxml.jackson.databind.JsonNode;
2222
import com.fasterxml.jackson.databind.node.ObjectNode;
23+
import com.networknt.schema.CollectorContext.Scope;
24+
2325
import org.slf4j.Logger;
2426
import org.slf4j.LoggerFactory;
2527

@@ -47,21 +49,11 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
4749

4850
Set<ValidationMessage> childSchemaErrors = new LinkedHashSet<>();
4951

50-
Collection<String> newEvaluatedItems = Collections.emptyList();
51-
Collection<String> newEvaluatedProperties = Collections.emptyList();
52-
5352
for (JsonSchema schema : this.schemas) {
54-
// As AllOf might contain multiple schemas take a backup of evaluated stuff.
55-
Collection<String> backupEvaluatedItems = collectorContext.getEvaluatedItems();
56-
Collection<String> backupEvaluatedProperties = collectorContext.getEvaluatedProperties();
57-
5853
Set<ValidationMessage> localErrors = new HashSet<>();
5954

55+
Scope parentScope = collectorContext.enterDynamicScope();
6056
try {
61-
// Make the evaluated lists empty.
62-
collectorContext.resetEvaluatedItems();
63-
collectorContext.resetEvaluatedProperties();
64-
6557
if (!state.isWalkEnabled()) {
6658
localErrors = schema.validate(node, rootNode, at);
6759
} else {
@@ -70,12 +62,6 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
7062

7163
childSchemaErrors.addAll(localErrors);
7264

73-
// Keep Collecting total evaluated properties.
74-
if (localErrors.isEmpty()) {
75-
newEvaluatedItems = collectorContext.getEvaluatedItems();
76-
newEvaluatedProperties = collectorContext.getEvaluatedProperties();
77-
}
78-
7965
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
8066
final Iterator<JsonNode> arrayElements = this.schemaNode.elements();
8167
while (arrayElements.hasNext()) {
@@ -108,14 +94,10 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
10894
}
10995
}
11096
} finally {
111-
collectorContext.setEvaluatedItems(backupEvaluatedItems);
112-
collectorContext.setEvaluatedProperties(backupEvaluatedProperties);
97+
Scope scope = collectorContext.exitDynamicScope();
11398
if (localErrors.isEmpty()) {
114-
collectorContext.getEvaluatedItems().addAll(newEvaluatedItems);
115-
collectorContext.getEvaluatedProperties().addAll(newEvaluatedProperties);
99+
parentScope.mergeWith(scope);
116100
}
117-
newEvaluatedItems = Collections.emptyList();
118-
newEvaluatedProperties = Collections.emptyList();
119101
}
120102
}
121103

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

Lines changed: 54 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.networknt.schema;
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.networknt.schema.CollectorContext.Scope;
21+
2022
import org.slf4j.Logger;
2123
import org.slf4j.LoggerFactory;
2224

@@ -61,64 +63,64 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
6163

6264
Set<ValidationMessage> allErrors = new LinkedHashSet<>();
6365

64-
// As anyOf might contain multiple schemas take a backup of evaluated stuff.
65-
Collection<String> backupEvaluatedItems = collectorContext.getEvaluatedItems();
66-
Collection<String> backupEvaluatedProperties = collectorContext.getEvaluatedProperties();
67-
68-
// Make the evaluated lists empty.
69-
collectorContext.resetEvaluatedItems();
70-
collectorContext.resetEvaluatedProperties();
71-
66+
Scope grandParentScope = collectorContext.enterDynamicScope();
7267
try {
7368
int numberOfValidSubSchemas = 0;
74-
for (int i = 0; i < this.schemas.size(); ++i) {
75-
JsonSchema schema = this.schemas.get(i);
76-
state.setMatchedNode(initialHasMatchedNode);
77-
Set<ValidationMessage> errors;
78-
79-
if (schema.hasTypeValidator()) {
80-
TypeValidator typeValidator = schema.getTypeValidator();
81-
//If schema has type validator and node type doesn't match with schemaType then ignore it
82-
//For union type, it is a must to call TypeValidator
83-
if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
84-
allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
85-
continue;
69+
for (JsonSchema schema: this.schemas) {
70+
Set<ValidationMessage> errors = Collections.emptySet();
71+
Scope parentScope = collectorContext.enterDynamicScope();
72+
try {
73+
state.setMatchedNode(initialHasMatchedNode);
74+
75+
if (schema.hasTypeValidator()) {
76+
TypeValidator typeValidator = schema.getTypeValidator();
77+
//If schema has type validator and node type doesn't match with schemaType then ignore it
78+
//For union type, it is a must to call TypeValidator
79+
if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
80+
allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
81+
continue;
82+
}
8683
}
87-
}
88-
if (!state.isWalkEnabled()) {
89-
errors = schema.validate(node, rootNode, at);
90-
} else {
91-
errors = schema.walk(node, rootNode, at, true);
92-
}
93-
94-
// check if any validation errors have occurred
95-
if (errors.isEmpty()) {
96-
// check whether there are no errors HOWEVER we have validated the exact validator
97-
if (!state.hasMatchedNode()) {
98-
continue;
84+
if (!state.isWalkEnabled()) {
85+
errors = schema.validate(node, rootNode, at);
86+
} else {
87+
errors = schema.walk(node, rootNode, at, true);
9988
}
100-
// we found a valid subschema, so increase counter
101-
numberOfValidSubSchemas++;
102-
}
10389

104-
if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) {
105-
// Clear all errors.
106-
allErrors.clear();
107-
// return empty errors.
108-
return errors;
109-
} else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
110-
if (this.discriminatorContext.isDiscriminatorMatchFound()) {
111-
if (!errors.isEmpty()) {
112-
errors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK));
113-
allErrors.addAll(errors);
114-
} else {
115-
// Clear all errors.
116-
allErrors.clear();
90+
// check if any validation errors have occurred
91+
if (errors.isEmpty()) {
92+
// check whether there are no errors HOWEVER we have validated the exact validator
93+
if (!state.hasMatchedNode()) {
94+
continue;
11795
}
96+
// we found a valid subschema, so increase counter
97+
numberOfValidSubSchemas++;
98+
}
99+
100+
if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) {
101+
// Clear all errors.
102+
allErrors.clear();
103+
// return empty errors.
118104
return errors;
105+
} else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
106+
if (this.discriminatorContext.isDiscriminatorMatchFound()) {
107+
if (!errors.isEmpty()) {
108+
allErrors.addAll(errors);
109+
allErrors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK));
110+
} else {
111+
// Clear all errors.
112+
allErrors.clear();
113+
}
114+
return errors;
115+
}
116+
}
117+
allErrors.addAll(errors);
118+
} finally {
119+
Scope scope = collectorContext.exitDynamicScope();
120+
if (errors.isEmpty()) {
121+
parentScope.mergeWith(scope);
119122
}
120123
}
121-
allErrors.addAll(errors);
122124
}
123125

124126
// determine only those errors which are NOT of type "required" property missing
@@ -138,14 +140,12 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
138140
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
139141
this.validationContext.leaveDiscriminatorContextImmediately(at);
140142
}
143+
144+
Scope parentScope = collectorContext.exitDynamicScope();
141145
if (allErrors.isEmpty()) {
142146
state.setMatchedNode(true);
143-
} else {
144-
collectorContext.getEvaluatedItems().clear();
145-
collectorContext.getEvaluatedProperties().clear();
147+
grandParentScope.mergeWith(parentScope);
146148
}
147-
collectorContext.getEvaluatedItems().addAll(backupEvaluatedItems);
148-
collectorContext.getEvaluatedProperties().addAll(backupEvaluatedProperties);
149149
}
150150
return Collections.unmodifiableSet(allErrors);
151151
}

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

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
20+
import java.util.Deque;
2021
import java.util.HashMap;
22+
import java.util.LinkedList;
2123
import java.util.Map;
2224
import java.util.Map.Entry;
2325
import java.util.Set;
@@ -47,38 +49,45 @@ public static CollectorContext getInstance() {
4749
*/
4850
private Map<String, Object> collectorLoadMap = new HashMap<>();
4951

50-
/**
51-
* Used to track which array items have been evaluated.
52-
*/
53-
private Collection<String> evaluatedItems = new ArrayList<>();
52+
private final Deque<Scope> dynamicScopes = new LinkedList<>();
53+
54+
CollectorContext() {
55+
this.dynamicScopes.push(new Scope());
56+
}
5457

5558
/**
56-
* Used to track which properties have been evaluated.
59+
* Creates a new scope
60+
* @return the previous, parent scope
5761
*/
58-
private Collection<String> evaluatedProperties = new ArrayList<>();
62+
public Scope enterDynamicScope() {
63+
Scope parent = this.dynamicScopes.peek();
64+
this.dynamicScopes.push(new Scope());
65+
return parent;
66+
}
5967

6068
/**
61-
* Identifies which array items have been evaluated.
62-
*
63-
* @return the set of evaluated items (never null)
69+
* Restores the previous, parent scope
70+
* @return the exited scope
6471
*/
65-
public Collection<String> getEvaluatedItems() {
66-
return this.evaluatedItems;
72+
public Scope exitDynamicScope() {
73+
return this.dynamicScopes.pop();
6774
}
6875

6976
/**
70-
* Set the array items that have been evaluated.
71-
* @param paths the set of evaluated array items (may be null)
77+
* Provides the currently active scope
78+
* @return the active scope
7279
*/
73-
public void setEvaluatedItems(Collection<String> paths) {
74-
this.evaluatedItems = null != paths ? paths : new ArrayList<>();
80+
public Scope getDynamicScope() {
81+
return this.dynamicScopes.peek();
7582
}
7683

7784
/**
78-
* Replaces the array items that have been evaluated with an empty collection.
85+
* Identifies which array items have been evaluated.
86+
*
87+
* @return the set of evaluated items (never null)
7988
*/
80-
public void resetEvaluatedItems() {
81-
this.evaluatedItems = new ArrayList<>();
89+
public Collection<String> getEvaluatedItems() {
90+
return getDynamicScope().getEvaluatedItems();
8291
}
8392

8493
/**
@@ -87,22 +96,7 @@ public void resetEvaluatedItems() {
8796
* @return the set of evaluated properties (never null)
8897
*/
8998
public Collection<String> getEvaluatedProperties() {
90-
return this.evaluatedProperties;
91-
}
92-
93-
/**
94-
* Set the properties that have been evaluated.
95-
* @param paths the set of evaluated properties (may be null)
96-
*/
97-
public void setEvaluatedProperties(Collection<String> paths) {
98-
this.evaluatedProperties = null != paths ? paths : new ArrayList<>();
99-
}
100-
101-
/**
102-
* Replaces the properties that have been evaluated with an empty collection.
103-
*/
104-
public void resetEvaluatedProperties() {
105-
this.evaluatedProperties = new ArrayList<>();
99+
return getDynamicScope().getEvaluatedProperties();
106100
}
107101

108102
/**
@@ -181,8 +175,8 @@ public void combineWithCollector(String name, Object data) {
181175
public void reset() {
182176
this.collectorMap = new HashMap<>();
183177
this.collectorLoadMap = new HashMap<>();
184-
this.evaluatedItems.clear();
185-
this.evaluatedProperties.clear();
178+
this.dynamicScopes.clear();
179+
this.dynamicScopes.push(new Scope());
186180
}
187181

188182
/**
@@ -199,4 +193,55 @@ void loadCollectors() {
199193

200194
}
201195

196+
public static class Scope {
197+
198+
/**
199+
* Used to track which array items have been evaluated.
200+
*/
201+
private final Collection<String> evaluatedItems = new ArrayList<>();
202+
203+
/**
204+
* Used to track which properties have been evaluated.
205+
*/
206+
private final Collection<String> evaluatedProperties = new ArrayList<>();
207+
208+
/**
209+
* Identifies which array items have been evaluated.
210+
*
211+
* @return the set of evaluated items (never null)
212+
*/
213+
public Collection<String> getEvaluatedItems() {
214+
return this.evaluatedItems;
215+
}
216+
217+
/**
218+
* Identifies which properties have been evaluated.
219+
*
220+
* @return the set of evaluated properties (never null)
221+
*/
222+
public Collection<String> getEvaluatedProperties() {
223+
return this.evaluatedProperties;
224+
}
225+
226+
/**
227+
* Merges the provided scope into this scope.
228+
* @param scope the scope to merge
229+
* @return this scope
230+
*/
231+
public Scope mergeWith(Scope scope) {
232+
getEvaluatedItems().addAll(scope.getEvaluatedItems());
233+
getEvaluatedProperties().addAll(scope.getEvaluatedProperties());
234+
return this;
235+
}
236+
237+
@Override
238+
public String toString() {
239+
return new StringBuilder("{ ")
240+
.append("\"evaluatedItems\": ").append(this.evaluatedItems)
241+
.append(", ")
242+
.append("\"evaluatedProperties\": ").append(this.evaluatedProperties)
243+
.append(" }").toString();
244+
}
245+
246+
}
202247
}

0 commit comments

Comments
 (0)