|
| 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