xpertss.json.schema.processors.validation.ValidationStack Maven / Gradle / Ivy
Show all versions of json-schema Show documentation
package xpertss.json.schema.processors.validation;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.github.fge.jackson.JacksonUtils;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import xpertss.json.schema.core.exceptions.ProcessingException;
import xpertss.json.schema.core.ref.JsonRef;
import xpertss.json.schema.core.report.ProcessingMessage;
import xpertss.json.schema.core.tree.SchemaTree;
import xpertss.json.schema.processors.data.FullData;
import com.google.common.collect.Queues;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Deque;
/**
* Class to keep track of instance pointer/schema pairs during validation
*
* This class helps to detect scenarios where a same schema is visited
* more than once for a same instance pointer. For instance, any instance
* validated against this schema:
*
*
* { "oneOf": [ {}, { "$ref": "#" } ] }
*
*
* will trigger a validation loop.
*
* Simply keeping track of instance pointer/schema pairs alone is not
* enough; it is sometimes perfectly legal to revisit a same pair during
* validation (for instance, alternative definitions of a container referring to
* one common schema for the same child). For this reason we use a stack, to
* which we {@link #push(FullData) push} pointer/schema pairs which are then
* {@link #pop() pop}ped when validation is complete.
*/
@ParametersAreNonnullByDefault
final class ValidationStack {
/*
* Sentinel which is always the first element of the stack; we use it in
* order to simplify the pop code.
*/
private static final Element NULL_ELEMENT = new Element(null, null);
/*
* Queue of visited contexts
*/
private final Deque validationQueue = Queues.newArrayDeque();
/*
* Head error message when a validation loop is detected
*/
private final String errmsg;
/*
* Current instance pointer and associated schema stack.
*/
private JsonPointer pointer = null ;
private Deque schemaURIs = null;
ValidationStack(String errmsg)
{
this.errmsg = errmsg;
}
/**
* Push one validation context onto the stack
*
* A {@link FullData} instance contains all the necessary information to
* decide what is to be done here. The most important piece of information
* is the pointer into the instance being analyzed:
*
*
* - if it is the same pointer, then we attempt to append the schema
* URI into the validation element; if there is a duplicate, this is a
* validation loop, throw an exception;
* - otherwise, a new element is created with the new instance pointer
* and the schema URI.
*
*
* @param data the validation data
* @throws ProcessingException instance pointer is unchanged, and an
* attempt is made to validate it using the exact same schema
*
* @see #pop()
*/
void push(FullData data)
throws ProcessingException
{
JsonPointer ptr = data.getInstance().getPointer();
SchemaURI schemaURI = new SchemaURI(data.getSchema());
if (ptr.equals(pointer)) {
if (schemaURIs.contains(schemaURI))
throw new ProcessingException(validationLoopMessage(data));
schemaURIs.addLast(schemaURI);
return;
}
validationQueue.addLast(new Element(pointer, schemaURIs));
pointer = ptr;
schemaURIs = Queues.newArrayDeque();
schemaURIs.addLast(schemaURI);
}
/**
* Exit the current validation context
*
* Here we remove the last schema URI visited; from then on, we have two
* scenarios:
*
*
* - if the list of schema URIs is not empty, we do not take any
* further action;
* - if the list is empty, validation of this part of the instance is
* complete; we therefore remove the tail of our {@link #validationQueue
* validation queue} and change the current validation context.
*
*
* Note that it is safe to pop the outermost validation context, since
* the first item in the validation queue is guaranteed to be {@link
* #NULL_ELEMENT}.
*/
void pop()
{
schemaURIs.removeLast();
if (!schemaURIs.isEmpty())
return;
Element element = validationQueue.removeLast();
pointer = element.pointer;
schemaURIs = element.schemaURIs;
}
private static final class Element {
private final JsonPointer pointer;
private final Deque schemaURIs;
private Element(@Nullable JsonPointer pointer, @Nullable Deque schemaURIs)
{
this.pointer = pointer;
this.schemaURIs = schemaURIs;
}
}
private static final class SchemaURI {
private final JsonRef locator;
private final JsonPointer pointer;
private SchemaURI(SchemaTree tree)
{
locator = tree.getContext();
pointer = tree.getPointer();
}
@Override
public int hashCode()
{
return locator.hashCode() ^ pointer.hashCode();
}
@Override
public boolean equals(@Nullable Object obj)
{
if (obj == null)
return false;
if (this == obj)
return true;
if (getClass() != obj.getClass())
return false;
SchemaURI other = (SchemaURI) obj;
return locator.equals(other.locator)
&& pointer.equals(other.pointer);
}
@Override
public String toString()
{
try {
URI tmp = new URI(null, null, pointer.toString());
return locator.toURI().resolve(tmp).toString();
} catch (URISyntaxException e) {
throw new RuntimeException("How did I get there??", e);
}
}
}
private ProcessingMessage validationLoopMessage(FullData input)
{
ArrayNode node = JacksonUtils.nodeFactory().arrayNode();
for (final SchemaURI uri: schemaURIs)
node.add(uri.toString());
return input.newMessage()
.put("domain", "validation")
.setMessage(errmsg)
.putArgument("alreadyVisited", new SchemaURI(input.getSchema()))
.putArgument("instancePointer", pointer.toString())
.put("validationPath", node);
}
}