Skip to content

Commit dd18163

Browse files
committed
New class ValidationStack
Existing code to solve issue #102 was incomplete; it failed to account for the fact that a same instance pointer/schema URI pair may be used in a legal manner more than once. At the moment, yours truly only knows that the problem exists and how to solve it, yet I fail to explain it correctly... Signed-off-by: Francis Galiegue <[email protected]>
1 parent fb9a49d commit dd18163

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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.node.ArrayNode;
23+
import com.github.fge.jackson.JacksonUtils;
24+
import com.github.fge.jackson.jsonpointer.JsonPointer;
25+
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
26+
import com.github.fge.jsonschema.core.ref.JsonRef;
27+
import com.github.fge.jsonschema.core.report.ProcessingMessage;
28+
import com.github.fge.jsonschema.core.tree.SchemaTree;
29+
import com.github.fge.jsonschema.processors.data.FullData;
30+
import com.google.common.collect.Queues;
31+
32+
import javax.annotation.Nullable;
33+
import javax.annotation.ParametersAreNonnullByDefault;
34+
import java.net.URI;
35+
import java.net.URISyntaxException;
36+
import java.util.Deque;
37+
38+
/**
39+
* Class to keep track of instance pointer/schema pairs during validation
40+
*
41+
* <p>This class helps to detect scenarios where a same schema is visited
42+
* more than once for a same instance pointer. For instance, any instance
43+
* validated against this schema:</p>
44+
*
45+
* <pre>
46+
* { "oneOf": [ {}, { "$ref": "#" } ] }
47+
* </pre>
48+
*
49+
* <p>will trigger a validation loop.</p>
50+
*
51+
* <p>Simply keeping track of instance pointer/schema pairs alone is not
52+
* enough; it is sometimes perfectly legal to revisit a same pair during
53+
* validation (for instance, alternative definitions of a container referring to
54+
* one common schema for the same child). For this reason we use a stack, to
55+
* which we {@link #push(FullData) push} pointer/schema pairs which are then
56+
* {@link #pop() pop}ped when validation is complete.</p>
57+
*/
58+
@ParametersAreNonnullByDefault
59+
final class ValidationStack
60+
{
61+
/*
62+
* Sentinel which is always the first element of the stack; we use it in
63+
* order to simplify the pop code.
64+
*/
65+
private static final Element NULL_ELEMENT = new Element(null, null);
66+
67+
/*
68+
* Queue of visited contexts
69+
*/
70+
private final Deque<Element> validationQueue = Queues.newArrayDeque();
71+
72+
/*
73+
* Head error message when a validation loop is detected
74+
*/
75+
private final String errmsg;
76+
77+
/*
78+
* Current instance pointer and associated schema stack.
79+
*/
80+
private JsonPointer pointer = null ;
81+
private Deque<SchemaURI> schemaURIs = null;
82+
83+
ValidationStack(final String errmsg)
84+
{
85+
this.errmsg = errmsg;
86+
}
87+
88+
/**
89+
* Push one validation context onto the stack
90+
*
91+
* <p>A {@link FullData} instance contains all the necessary information to
92+
* decide what is to be done here. The most important piece of information
93+
* is the pointer into the instance being analyzed:</p>
94+
*
95+
* <ul>
96+
* <li>if it is the same pointer, then we attempt to append the schema
97+
* URI into the validation element; if there is a duplicate, this is a
98+
* validation loop, throw an exception;</li>
99+
* <li>otherwise, a new element is created with the new instance pointer
100+
* and the schema URI.</li>
101+
* </ul>
102+
*
103+
* @param data the validation data
104+
* @throws ProcessingException instance pointer is unchanged, and an
105+
* attempt is made to validate it using the exact same schema
106+
*
107+
* @see #pop()
108+
*/
109+
void push(final FullData data)
110+
throws ProcessingException
111+
{
112+
final JsonPointer ptr = data.getInstance().getPointer();
113+
final SchemaURI schemaURI = new SchemaURI(data.getSchema());
114+
115+
if (ptr.equals(pointer)) {
116+
if (schemaURIs.contains(schemaURI))
117+
throw new ProcessingException(validationLoopMessage(data));
118+
schemaURIs.addLast(schemaURI);
119+
return;
120+
}
121+
122+
validationQueue.addLast(new Element(pointer, schemaURIs));
123+
pointer = ptr;
124+
schemaURIs = Queues.newArrayDeque();
125+
schemaURIs.addLast(schemaURI);
126+
}
127+
128+
/**
129+
* Exit the current validation context
130+
*
131+
* <p>Here we remove the last schema URI visited; from then on, we have two
132+
* scenarios:</p>
133+
*
134+
* <ul>
135+
* <li>if the list of schema URIs is not empty, we do not take any
136+
* further action;</li>
137+
* <li>if the list is empty, validation of this part of the instance is
138+
* complete; we therefore remove the tail of our {@link #validationQueue
139+
* validation queue} and change the current validation context.</li>
140+
* </ul>
141+
*
142+
* <p>Note that it is safe to pop the outermost validation context, since
143+
* the first item in the validation queue is guaranteed to be {@link
144+
* #NULL_ELEMENT}.</p>
145+
*/
146+
void pop()
147+
{
148+
schemaURIs.removeLast();
149+
if (!schemaURIs.isEmpty())
150+
return;
151+
152+
final Element element = validationQueue.removeLast();
153+
pointer = element.pointer;
154+
schemaURIs = element.schemaURIs;
155+
}
156+
157+
private static final class Element
158+
{
159+
private final JsonPointer pointer;
160+
private final Deque<SchemaURI> schemaURIs;
161+
162+
private Element(@Nullable final JsonPointer pointer,
163+
@Nullable final Deque<SchemaURI> schemaURIs)
164+
{
165+
this.pointer = pointer;
166+
this.schemaURIs = schemaURIs;
167+
}
168+
}
169+
170+
private static final class SchemaURI
171+
{
172+
private final JsonRef locator;
173+
private final JsonPointer pointer;
174+
175+
private SchemaURI(final SchemaTree tree)
176+
{
177+
locator = tree.getContext();
178+
pointer = tree.getPointer();
179+
}
180+
181+
@Override
182+
public int hashCode()
183+
{
184+
return locator.hashCode() ^ pointer.hashCode();
185+
}
186+
187+
@Override
188+
public boolean equals(@Nullable final Object obj)
189+
{
190+
if (obj == null)
191+
return false;
192+
if (this == obj)
193+
return true;
194+
if (getClass() != obj.getClass())
195+
return false;
196+
final SchemaURI other = (SchemaURI) obj;
197+
return locator.equals(other.locator)
198+
&& pointer.equals(other.pointer);
199+
}
200+
201+
@Override
202+
public String toString()
203+
{
204+
final URI tmp;
205+
try {
206+
tmp = new URI(null, null, pointer.toString());
207+
} catch (URISyntaxException e) {
208+
throw new RuntimeException("How did I get there??", e);
209+
}
210+
return locator.toURI().resolve(tmp).toString();
211+
}
212+
}
213+
214+
private ProcessingMessage validationLoopMessage(final FullData input)
215+
{
216+
final ArrayNode node = JacksonUtils.nodeFactory().arrayNode();
217+
for (final SchemaURI uri: schemaURIs)
218+
node.add(uri.toString());
219+
return input.newMessage()
220+
.put("domain", "validation")
221+
.setMessage(errmsg)
222+
.putArgument("alreadyVisited", new SchemaURI(input.getSchema()))
223+
.putArgument("instancePointer", pointer.toString())
224+
.put("validationPath", node);
225+
}
226+
}

0 commit comments

Comments
 (0)