Skip to content

Commit 477e7c5

Browse files
committed
New class InstanceValidator
The existing ValidationProcessor is not easily modifiable to fulfill the needs highlighted by issue #102. A ThreadLocal may be used, but that would make the code rather convoluted. Instead, create a new InstanceValidator class with the same signature; the ValidationProcessor (which is unique per JsonSchemaFactory) will then create an instance of that class for each attempted schema/instance validation pair. This means the InstanceValidator instance will be what keyword validators will use, instead of the ValidationProcessor. Signed-off-by: Francis Galiegue <[email protected]>
1 parent a2488d7 commit 477e7c5

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright (c) 2014, Francis Galiegue ([email protected])
3+
*
4+
* This software is dual-licensed under:
5+
*
6+
* - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
7+
* later version;
8+
* - the Apache Software License (ASL) version 2.0.
9+
*
10+
* The text of this file and of both licenses is available at the root of this
11+
* project or, if you have the jar distribution, in directory META-INF/, under
12+
* the names LGPL-3.0.txt and ASL-2.0.txt respectively.
13+
*
14+
* Direct link to the sources:
15+
*
16+
* - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
17+
* - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
18+
*/
19+
20+
package com.github.fge.jsonschema.processors.validation;
21+
22+
import com.fasterxml.jackson.databind.JsonNode;
23+
import com.fasterxml.jackson.databind.node.ArrayNode;
24+
import com.github.fge.jackson.JacksonUtils;
25+
import com.github.fge.jackson.jsonpointer.JsonPointer;
26+
import com.github.fge.jsonschema.core.exceptions.InvalidSchemaException;
27+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
28+
import com.github.fge.jsonschema.core.processing.Processor;
29+
import com.github.fge.jsonschema.core.report.ProcessingMessage;
30+
import com.github.fge.jsonschema.core.report.ProcessingReport;
31+
import com.github.fge.jsonschema.core.tree.JsonTree;
32+
import com.github.fge.jsonschema.core.tree.SchemaTree;
33+
import com.github.fge.jsonschema.keyword.validator.KeywordValidator;
34+
import com.github.fge.jsonschema.processors.data.FullData;
35+
import com.github.fge.jsonschema.processors.data.SchemaContext;
36+
import com.github.fge.jsonschema.processors.data.ValidatorList;
37+
import com.github.fge.msgsimple.bundle.MessageBundle;
38+
import com.google.common.base.Equivalence;
39+
import com.google.common.collect.Lists;
40+
import com.google.common.collect.Sets;
41+
42+
import javax.annotation.ParametersAreNonnullByDefault;
43+
import javax.annotation.concurrent.NotThreadSafe;
44+
import java.util.Collections;
45+
import java.util.List;
46+
import java.util.Set;
47+
48+
/**
49+
* Main validation processor
50+
*/
51+
@NotThreadSafe
52+
@ParametersAreNonnullByDefault
53+
public final class InstanceValidator
54+
implements Processor<FullData, FullData>
55+
{
56+
private final MessageBundle syntaxMessages;
57+
private final MessageBundle validationMessages;
58+
private final Processor<SchemaContext, ValidatorList> keywordBuilder;
59+
60+
private final Set<Equivalence.Wrapper<FullData>> visited
61+
= Sets.newLinkedHashSet();
62+
63+
public InstanceValidator(final MessageBundle syntaxMessages,
64+
final MessageBundle validationMessages,
65+
final Processor<SchemaContext, ValidatorList> keywordBuilder)
66+
{
67+
this.syntaxMessages = syntaxMessages;
68+
this.validationMessages = validationMessages;
69+
this.keywordBuilder = keywordBuilder;
70+
}
71+
72+
@Override
73+
public FullData process(final ProcessingReport report,
74+
final FullData input)
75+
throws ProcessingException
76+
{
77+
if (!visited.add(FULL_DATA_EQUIVALENCE.wrap(input))) {
78+
final String errmsg
79+
= validationMessages.getMessage("err.common.validationLoop");
80+
final ProcessingMessage message = input.newMessage()
81+
.put("domain", "validation")
82+
.setMessage(errmsg)
83+
.put("visited", visited);
84+
throw new ProcessingException(message);
85+
}
86+
87+
/*
88+
* Build a validation context, attach a report to it
89+
*/
90+
final SchemaContext context = new SchemaContext(input);
91+
92+
/*
93+
* Get the full context from the cache. Inject the messages into the
94+
* main report.
95+
*/
96+
final ValidatorList fullContext = keywordBuilder.process(report,
97+
context);
98+
99+
if (fullContext == null) {
100+
final ProcessingMessage message = collectSyntaxErrors(report);
101+
throw new InvalidSchemaException(message);
102+
}
103+
104+
/*
105+
* Get the calculated context. Build the data.
106+
*/
107+
final SchemaContext newContext = fullContext.getContext();
108+
final FullData data = new FullData(newContext.getSchema(),
109+
input.getInstance(), input.isDeepCheck());
110+
111+
/*
112+
* Validate against all keywords.
113+
*/
114+
for (final KeywordValidator validator: fullContext)
115+
validator.validate(this, report, validationMessages, data);
116+
117+
/*
118+
* At that point, if the report is a failure, we quit: there is no
119+
* reason to go any further. Unless the user has asked to continue even
120+
* in this case.
121+
*/
122+
if (!(report.isSuccess() || data.isDeepCheck()))
123+
return input;
124+
125+
/*
126+
* Now check whether this is a container node with a size greater than
127+
* 0. If not, no need to go see the children.
128+
*/
129+
final JsonNode node = data.getInstance().getNode();
130+
if (node.size() == 0)
131+
return input;
132+
133+
if (node.isArray())
134+
processArray(report, data);
135+
else
136+
processObject(report, data);
137+
138+
return input;
139+
}
140+
141+
private ProcessingMessage collectSyntaxErrors(final ProcessingReport report)
142+
{
143+
/*
144+
* OK, that's for issue #99 but that's ugly nevertheless.
145+
*
146+
* We want syntax error messages to appear in the exception text.
147+
*/
148+
final String msg = syntaxMessages.getMessage("core.invalidSchema");
149+
final ArrayNode arrayNode = JacksonUtils.nodeFactory().arrayNode();
150+
JsonNode node;
151+
for (final ProcessingMessage message: report) {
152+
node = message.asJson();
153+
if ("syntax".equals(node.path("domain").asText()))
154+
arrayNode.add(node);
155+
}
156+
final StringBuilder sb = new StringBuilder(msg);
157+
sb.append("\nSyntax errors:\n");
158+
sb.append(JacksonUtils.prettyPrint(arrayNode));
159+
return new ProcessingMessage().setMessage(sb.toString());
160+
}
161+
162+
private void processArray(final ProcessingReport report,
163+
final FullData input)
164+
throws ProcessingException
165+
{
166+
final SchemaTree tree = input.getSchema();
167+
final JsonTree instance = input.getInstance();
168+
169+
final JsonNode schema = tree.getNode();
170+
final JsonNode node = instance.getNode();
171+
172+
final JsonNode digest = ArraySchemaDigester.getInstance().digest(schema);
173+
final ArraySchemaSelector selector = new ArraySchemaSelector(digest);
174+
175+
final int size = node.size();
176+
177+
FullData data;
178+
JsonTree newInstance;
179+
180+
for (int index = 0; index < size; index++) {
181+
newInstance = instance.append(JsonPointer.of(index));
182+
data = input.withInstance(newInstance);
183+
for (final JsonPointer ptr: selector.selectSchemas(index)) {
184+
data = data.withSchema(tree.append(ptr));
185+
process(report, data);
186+
}
187+
}
188+
}
189+
190+
private void processObject(final ProcessingReport report,
191+
final FullData input)
192+
throws ProcessingException
193+
{
194+
final SchemaTree tree = input.getSchema();
195+
final JsonTree instance = input.getInstance();
196+
197+
final JsonNode schema = tree.getNode();
198+
final JsonNode node = instance.getNode();
199+
200+
final JsonNode digest = ObjectSchemaDigester.getInstance()
201+
.digest(schema);
202+
final ObjectSchemaSelector selector = new ObjectSchemaSelector(digest);
203+
204+
final List<String> fields = Lists.newArrayList(node.fieldNames());
205+
Collections.sort(fields);
206+
207+
FullData data;
208+
JsonTree newInstance;
209+
210+
for (final String field: fields) {
211+
newInstance = instance.append(JsonPointer.of(field));
212+
data = input.withInstance(newInstance);
213+
for (final JsonPointer ptr: selector.selectSchemas(field)) {
214+
data = data.withSchema(tree.append(ptr));
215+
process(report, data);
216+
}
217+
}
218+
}
219+
220+
@Override
221+
public String toString()
222+
{
223+
return "instance validator";
224+
}
225+
226+
@ParametersAreNonnullByDefault
227+
private static final Equivalence<FullData> FULL_DATA_EQUIVALENCE
228+
= new Equivalence<FullData>()
229+
{
230+
@Override
231+
protected boolean doEquivalent(final FullData a, final FullData b)
232+
{
233+
final JsonPointer ptra = a.getInstance().getPointer();
234+
final JsonPointer ptrb = b.getInstance().getPointer();
235+
return a.getSchema().equals(b.getSchema())
236+
&& ptra.equals(ptrb);
237+
}
238+
239+
@Override
240+
protected int doHash(final FullData t)
241+
{
242+
return t.getSchema().hashCode()
243+
^ t.getInstance().getPointer().hashCode();
244+
}
245+
};
246+
}

0 commit comments

Comments
 (0)