|
| 1 | +/* |
| 2 | + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) |
| 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.core.ref; |
| 21 | + |
| 22 | +import com.fasterxml.jackson.databind.JsonNode; |
| 23 | +import com.github.fge.jackson.jsonpointer.JsonPointer; |
| 24 | +import com.github.fge.jackson.jsonpointer.JsonPointerException; |
| 25 | +import com.github.fge.jsonschema.core.exceptions.JsonReferenceException; |
| 26 | +import com.github.fge.jsonschema.core.exceptions.ProcessingException; |
| 27 | +import com.github.fge.jsonschema.core.messages.JsonSchemaCoreMessageBundle; |
| 28 | +import com.github.fge.jsonschema.core.report.ProcessingMessage; |
| 29 | +import com.github.fge.jsonschema.core.util.URIUtils; |
| 30 | +import com.github.fge.msgsimple.bundle.MessageBundle; |
| 31 | +import com.github.fge.msgsimple.load.MessageBundles; |
| 32 | +import com.google.common.base.Optional; |
| 33 | +import com.google.common.cache.CacheBuilder; |
| 34 | +import com.google.common.cache.CacheBuilderSpec; |
| 35 | +import com.google.common.cache.CacheLoader; |
| 36 | +import com.google.common.cache.LoadingCache; |
| 37 | + |
| 38 | +import javax.annotation.Nonnull; |
| 39 | +import javax.annotation.concurrent.Immutable; |
| 40 | +import java.net.URI; |
| 41 | +import java.net.URISyntaxException; |
| 42 | +import java.util.Collections; |
| 43 | +import java.util.HashMap; |
| 44 | +import java.util.Map; |
| 45 | + |
| 46 | +/** |
| 47 | + * Representation of a JSON Reference |
| 48 | + * |
| 49 | + * <p><a href="http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03">JSON |
| 50 | + * Reference</a>, currently a draft, is a way to define a path within a JSON |
| 51 | + * document.</p> |
| 52 | + * |
| 53 | + * <p>To quote the draft, "A JSON Reference is a JSON object, which contains |
| 54 | + * a member named "$ref", which has a JSON string value." This string value |
| 55 | + * must be a URI. Example:</p> |
| 56 | + * |
| 57 | + * <pre> |
| 58 | + * { |
| 59 | + * "$ref": "http://example.com/example.json#/foo/bar" |
| 60 | + * } |
| 61 | + * </pre> |
| 62 | + * |
| 63 | + * <p>This class differs from the JSON Reference draft in that it accepts to |
| 64 | + * process illegal references, in the sense that they are URIs, but their |
| 65 | + * fragment parts are not JSON Pointers (in which case {@link #isLegal()} |
| 66 | + * returns {@code false}.</p> |
| 67 | + * |
| 68 | + * <p>The implementation is a wrapper over Java's {@link URI}, with the |
| 69 | + * following characteristics:</p> |
| 70 | + * |
| 71 | + * <ul> |
| 72 | + * <li>all URIs are normalized from the get go;</li> |
| 73 | + * <li>an empty fragment is equivalent to no fragment at all, and stands for |
| 74 | + * a root JSON Pointer, as required by the draft;</li> |
| 75 | + * <li>a reference is taken to be absolute if the underlying URI is absolute |
| 76 | + * <i>and</i> it has no fragment, or an empty fragment.</li> |
| 77 | + * </ul> |
| 78 | + * |
| 79 | + * <p>It also special cases the following:</p> |
| 80 | + * |
| 81 | + * <ul> |
| 82 | + * <li>an empty reference (for instance, used in anonymouns schemas);</li> |
| 83 | + * <li>URIs with the {@code jar} scheme (the resolving algorithm differs -- |
| 84 | + * please note that this breaks URI resolution rules).</li> |
| 85 | + * </ul> |
| 86 | + * |
| 87 | + */ |
| 88 | +@Immutable |
| 89 | +public abstract class JsonRef |
| 90 | +{ |
| 91 | + private static final MessageBundle BUNDLE |
| 92 | + = MessageBundles.getBundle(JsonSchemaCoreMessageBundle.class); |
| 93 | + |
| 94 | + /** |
| 95 | + * The empty URI |
| 96 | + */ |
| 97 | + private static final URI EMPTY_URI = URI.create(""); |
| 98 | + |
| 99 | + /** |
| 100 | + * A "hash only" URI -- used by {@link EmptyJsonRef} |
| 101 | + */ |
| 102 | + protected static final URI HASHONLY_URI = URI.create("#"); |
| 103 | + |
| 104 | + /** |
| 105 | + * Whether this JSON Reference is legal |
| 106 | + */ |
| 107 | + protected final boolean legal; |
| 108 | + |
| 109 | + /** |
| 110 | + * The URI, as provided by the input, with an appended empty fragment if |
| 111 | + * no fragment was provided |
| 112 | + */ |
| 113 | + protected final URI uri; |
| 114 | + |
| 115 | + /** |
| 116 | + * The locator of this reference. This is the URI with an empty fragment |
| 117 | + * part. |
| 118 | + */ |
| 119 | + protected final URI locator; |
| 120 | + |
| 121 | + /** |
| 122 | + * The pointer of this reference, if any |
| 123 | + * |
| 124 | + * <p>Initialized to null if the fragment part is not a JSON Pointer.</p> |
| 125 | + * |
| 126 | + * @see #isLegal() |
| 127 | + */ |
| 128 | + protected final JsonPointer pointer; |
| 129 | + |
| 130 | + /** |
| 131 | + * String representation |
| 132 | + */ |
| 133 | + private final String asString; |
| 134 | + |
| 135 | + /** |
| 136 | + * Hashcode |
| 137 | + */ |
| 138 | + private final int hashCode; |
| 139 | + |
| 140 | + /** |
| 141 | + * Main constructor, {@code protected} by design |
| 142 | + * |
| 143 | + * @param uri the URI to build that reference |
| 144 | + */ |
| 145 | + protected JsonRef(final URI uri) |
| 146 | + { |
| 147 | + final String scheme = uri.getScheme(); |
| 148 | + final String ssp = uri.getSchemeSpecificPart(); |
| 149 | + /* |
| 150 | + * Account for URIs with no fragment: substitute an empty one |
| 151 | + */ |
| 152 | + final String fragment = Optional.fromNullable(uri.getFragment()).or(""); |
| 153 | + |
| 154 | + /* |
| 155 | + * Compute the fragment |
| 156 | + */ |
| 157 | + boolean isLegal = true; |
| 158 | + JsonPointer ptr; |
| 159 | + try { |
| 160 | + ptr = fragment.isEmpty() ? JsonPointer.empty() |
| 161 | + : new JsonPointer(fragment); |
| 162 | + } catch (JsonPointerException ignored) { |
| 163 | + ptr = null; |
| 164 | + isLegal = false; |
| 165 | + } |
| 166 | + legal = isLegal; |
| 167 | + pointer = ptr; |
| 168 | + |
| 169 | + try { |
| 170 | + this.uri = new URI(scheme, ssp, fragment); |
| 171 | + locator = new URI(scheme, ssp, ""); |
| 172 | + asString = this.uri.toString(); |
| 173 | + hashCode = asString.hashCode(); |
| 174 | + } catch (URISyntaxException e) { |
| 175 | + /* |
| 176 | + * Can't happen: we did have a legal URI to start with |
| 177 | + */ |
| 178 | + throw new RuntimeException("WTF??", e); |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + private static LoadingCache<URI, JsonRef> uriToJsonRef = CacheBuilder.newBuilder().maximumSize(1000).build(new CacheLoader<URI, JsonRef>() { |
| 183 | + public JsonRef load(@Nonnull final URI uri) { |
| 184 | + final URI normalized = URIUtils.normalizeURI(uri); |
| 185 | + if (HASHONLY_URI.equals(normalized) || EMPTY_URI.equals(normalized)) return EmptyJsonRef.getInstance(); |
| 186 | + return "jar".equals(normalized.getScheme()) ? new JarJsonRef(normalized) : new HierarchicalJsonRef(normalized); |
| 187 | + } |
| 188 | + }); |
| 189 | + |
| 190 | + /** |
| 191 | + * Build a JSON Reference from a URI |
| 192 | + * |
| 193 | + * @param uri the provided URI |
| 194 | + * @return the JSON Reference |
| 195 | + * @throws NullPointerException the provided URI is null |
| 196 | + */ |
| 197 | + public static JsonRef fromURI(final URI uri) |
| 198 | + { |
| 199 | + BUNDLE.checkNotNull(uri, "jsonRef.nullURI"); |
| 200 | + return uriToJsonRef.getUnchecked(uri); |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * Build a JSON Reference from a string input |
| 205 | + * |
| 206 | + * @param s the string |
| 207 | + * @return the reference |
| 208 | + * @throws JsonReferenceException string is not a valid URI |
| 209 | + * @throws NullPointerException provided string is null |
| 210 | + */ |
| 211 | + public static JsonRef fromString(final String s) |
| 212 | + throws JsonReferenceException |
| 213 | + { |
| 214 | + BUNDLE.checkNotNull(s, "jsonRef.nullInput"); |
| 215 | + try { |
| 216 | + return fromURI(new URI(s)); |
| 217 | + } catch (URISyntaxException e) { |
| 218 | + throw new JsonReferenceException(new ProcessingMessage() |
| 219 | + .setMessage(BUNDLE.getMessage("jsonRef.invalidURI")) |
| 220 | + .putArgument("input", s), e); |
| 221 | + } |
| 222 | + |
| 223 | + } |
| 224 | + |
| 225 | + /** |
| 226 | + * Return an empty reference |
| 227 | + * |
| 228 | + * <p>An empty reference is a reference which only has an empty fragment. |
| 229 | + * </p> |
| 230 | + * |
| 231 | + * @return a statically allocated empty reference |
| 232 | + */ |
| 233 | + public static JsonRef emptyRef() |
| 234 | + { |
| 235 | + return EmptyJsonRef.getInstance(); |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Return the underlying URI for this JSON Reference |
| 240 | + * |
| 241 | + * @return the URI |
| 242 | + */ |
| 243 | + public final URI toURI() |
| 244 | + { |
| 245 | + return uri; |
| 246 | + } |
| 247 | + |
| 248 | + /** |
| 249 | + * Tell whether this reference is an absolute reference |
| 250 | + * |
| 251 | + * <p>See description.</p> |
| 252 | + * |
| 253 | + * @return {@code true} if the JSON Reference is absolute |
| 254 | + */ |
| 255 | + public abstract boolean isAbsolute(); |
| 256 | + |
| 257 | + /** |
| 258 | + * Resolve this reference against another reference |
| 259 | + * |
| 260 | + * @param other the reference to resolve |
| 261 | + * @return the resolved reference |
| 262 | + */ |
| 263 | + public abstract JsonRef resolve(final JsonRef other); |
| 264 | + |
| 265 | + /** |
| 266 | + * Return this JSON Reference's locator |
| 267 | + * |
| 268 | + * <p>This returns the reference with an empty fragment, ie the URI of the |
| 269 | + * document itself.</p> |
| 270 | + * |
| 271 | + * @return an URI |
| 272 | + */ |
| 273 | + public final URI getLocator() |
| 274 | + { |
| 275 | + return locator; |
| 276 | + } |
| 277 | + |
| 278 | + /** |
| 279 | + * Tell whether this JSON Reference is legal |
| 280 | + * |
| 281 | + * <p>Recall: it is legal if and only if its fragment part is a JSON |
| 282 | + * pointer.</p> |
| 283 | + * |
| 284 | + * @return {@code true} if legal |
| 285 | + * @see JsonPointer |
| 286 | + */ |
| 287 | + public final boolean isLegal() |
| 288 | + { |
| 289 | + return legal; |
| 290 | + } |
| 291 | + |
| 292 | + /** |
| 293 | + * Return the fragment part of this JSON Reference as a JSON Pointer |
| 294 | + * |
| 295 | + * <p>If the reference is not legal, this returns {@code null} <b>without |
| 296 | + * further notice</b>, so beware!</p> |
| 297 | + * |
| 298 | + * @return a JSON Pointer |
| 299 | + * @see JsonPointer |
| 300 | + */ |
| 301 | + public final JsonPointer getPointer() |
| 302 | + { |
| 303 | + return pointer; |
| 304 | + } |
| 305 | + |
| 306 | + /** |
| 307 | + * Tell whether the current JSON Reference "contains" another |
| 308 | + * |
| 309 | + * <p>This is considered true iif both references have the same locator, |
| 310 | + * in other words, if they differ only by their fragment part.</p> |
| 311 | + * |
| 312 | + * @param other the other reference |
| 313 | + * @return see above |
| 314 | + */ |
| 315 | + public final boolean contains(final JsonRef other) |
| 316 | + { |
| 317 | + return locator.equals(other.locator); |
| 318 | + } |
| 319 | + |
| 320 | + @Override |
| 321 | + public final int hashCode() |
| 322 | + { |
| 323 | + return hashCode; |
| 324 | + } |
| 325 | + |
| 326 | + @Override |
| 327 | + public final boolean equals(final Object obj) |
| 328 | + { |
| 329 | + if (obj == null) |
| 330 | + return false; |
| 331 | + if (this == obj) |
| 332 | + return true; |
| 333 | + |
| 334 | + if (!(obj instanceof JsonRef)) |
| 335 | + return false; |
| 336 | + |
| 337 | + final JsonRef that = (JsonRef) obj; |
| 338 | + return asString.equals(that.asString); |
| 339 | + } |
| 340 | + |
| 341 | + @Override |
| 342 | + public final String toString() |
| 343 | + { |
| 344 | + return asString; |
| 345 | + } |
| 346 | +} |
0 commit comments