From 77fa99133ef2b1363d8605c5a2dc737f9da894c8 Mon Sep 17 00:00:00 2001 From: Juha Paananen Date: Tue, 10 May 2016 15:10:53 +0300 Subject: [PATCH] Fork JsonRef for improved performance. Pull request pending: https://github.com/fge/json-schema-core/pull/24 --- .../fge/jsonschema/core/ref/JsonRef.java | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 src/main/scala/com/github/fge/jsonschema/core/ref/JsonRef.java diff --git a/src/main/scala/com/github/fge/jsonschema/core/ref/JsonRef.java b/src/main/scala/com/github/fge/jsonschema/core/ref/JsonRef.java new file mode 100644 index 0000000000..bc874af559 --- /dev/null +++ b/src/main/scala/com/github/fge/jsonschema/core/ref/JsonRef.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of this file and of both licenses is available at the root of this + * project or, if you have the jar distribution, in directory META-INF/, under + * the names LGPL-3.0.txt and ASL-2.0.txt respectively. + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ + +package com.github.fge.jsonschema.core.ref; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jackson.jsonpointer.JsonPointer; +import com.github.fge.jackson.jsonpointer.JsonPointerException; +import com.github.fge.jsonschema.core.exceptions.JsonReferenceException; +import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.messages.JsonSchemaCoreMessageBundle; +import com.github.fge.jsonschema.core.report.ProcessingMessage; +import com.github.fge.jsonschema.core.util.URIUtils; +import com.github.fge.msgsimple.bundle.MessageBundle; +import com.github.fge.msgsimple.load.MessageBundles; +import com.google.common.base.Optional; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Representation of a JSON Reference + * + *

JSON + * Reference, currently a draft, is a way to define a path within a JSON + * document.

+ * + *

To quote the draft, "A JSON Reference is a JSON object, which contains + * a member named "$ref", which has a JSON string value." This string value + * must be a URI. Example:

+ * + *
+ *     {
+ *         "$ref": "http://example.com/example.json#/foo/bar"
+ *     }
+ * 
+ * + *

This class differs from the JSON Reference draft in that it accepts to + * process illegal references, in the sense that they are URIs, but their + * fragment parts are not JSON Pointers (in which case {@link #isLegal()} + * returns {@code false}.

+ * + *

The implementation is a wrapper over Java's {@link URI}, with the + * following characteristics:

+ * + * + * + *

It also special cases the following:

+ * + * + * + */ +@Immutable +public abstract class JsonRef +{ + private static final MessageBundle BUNDLE + = MessageBundles.getBundle(JsonSchemaCoreMessageBundle.class); + + /** + * The empty URI + */ + private static final URI EMPTY_URI = URI.create(""); + + /** + * A "hash only" URI -- used by {@link EmptyJsonRef} + */ + protected static final URI HASHONLY_URI = URI.create("#"); + + /** + * Whether this JSON Reference is legal + */ + protected final boolean legal; + + /** + * The URI, as provided by the input, with an appended empty fragment if + * no fragment was provided + */ + protected final URI uri; + + /** + * The locator of this reference. This is the URI with an empty fragment + * part. + */ + protected final URI locator; + + /** + * The pointer of this reference, if any + * + *

Initialized to null if the fragment part is not a JSON Pointer.

+ * + * @see #isLegal() + */ + protected final JsonPointer pointer; + + /** + * String representation + */ + private final String asString; + + /** + * Hashcode + */ + private final int hashCode; + + /** + * Main constructor, {@code protected} by design + * + * @param uri the URI to build that reference + */ + protected JsonRef(final URI uri) + { + final String scheme = uri.getScheme(); + final String ssp = uri.getSchemeSpecificPart(); + /* + * Account for URIs with no fragment: substitute an empty one + */ + final String fragment = Optional.fromNullable(uri.getFragment()).or(""); + + /* + * Compute the fragment + */ + boolean isLegal = true; + JsonPointer ptr; + try { + ptr = fragment.isEmpty() ? JsonPointer.empty() + : new JsonPointer(fragment); + } catch (JsonPointerException ignored) { + ptr = null; + isLegal = false; + } + legal = isLegal; + pointer = ptr; + + try { + this.uri = new URI(scheme, ssp, fragment); + locator = new URI(scheme, ssp, ""); + asString = this.uri.toString(); + hashCode = asString.hashCode(); + } catch (URISyntaxException e) { + /* + * Can't happen: we did have a legal URI to start with + */ + throw new RuntimeException("WTF??", e); + } + } + + private static LoadingCache uriToJsonRef = CacheBuilder.newBuilder().maximumSize(1000).build(new CacheLoader() { + public JsonRef load(@Nonnull final URI uri) { + final URI normalized = URIUtils.normalizeURI(uri); + if (HASHONLY_URI.equals(normalized) || EMPTY_URI.equals(normalized)) return EmptyJsonRef.getInstance(); + return "jar".equals(normalized.getScheme()) ? new JarJsonRef(normalized) : new HierarchicalJsonRef(normalized); + } + }); + + /** + * Build a JSON Reference from a URI + * + * @param uri the provided URI + * @return the JSON Reference + * @throws NullPointerException the provided URI is null + */ + public static JsonRef fromURI(final URI uri) + { + BUNDLE.checkNotNull(uri, "jsonRef.nullURI"); + return uriToJsonRef.getUnchecked(uri); + } + + /** + * Build a JSON Reference from a string input + * + * @param s the string + * @return the reference + * @throws JsonReferenceException string is not a valid URI + * @throws NullPointerException provided string is null + */ + public static JsonRef fromString(final String s) + throws JsonReferenceException + { + BUNDLE.checkNotNull(s, "jsonRef.nullInput"); + try { + return fromURI(new URI(s)); + } catch (URISyntaxException e) { + throw new JsonReferenceException(new ProcessingMessage() + .setMessage(BUNDLE.getMessage("jsonRef.invalidURI")) + .putArgument("input", s), e); + } + + } + + /** + * Return an empty reference + * + *

An empty reference is a reference which only has an empty fragment. + *

+ * + * @return a statically allocated empty reference + */ + public static JsonRef emptyRef() + { + return EmptyJsonRef.getInstance(); + } + + /** + * Return the underlying URI for this JSON Reference + * + * @return the URI + */ + public final URI toURI() + { + return uri; + } + + /** + * Tell whether this reference is an absolute reference + * + *

See description.

+ * + * @return {@code true} if the JSON Reference is absolute + */ + public abstract boolean isAbsolute(); + + /** + * Resolve this reference against another reference + * + * @param other the reference to resolve + * @return the resolved reference + */ + public abstract JsonRef resolve(final JsonRef other); + + /** + * Return this JSON Reference's locator + * + *

This returns the reference with an empty fragment, ie the URI of the + * document itself.

+ * + * @return an URI + */ + public final URI getLocator() + { + return locator; + } + + /** + * Tell whether this JSON Reference is legal + * + *

Recall: it is legal if and only if its fragment part is a JSON + * pointer.

+ * + * @return {@code true} if legal + * @see JsonPointer + */ + public final boolean isLegal() + { + return legal; + } + + /** + * Return the fragment part of this JSON Reference as a JSON Pointer + * + *

If the reference is not legal, this returns {@code null} without + * further notice, so beware!

+ * + * @return a JSON Pointer + * @see JsonPointer + */ + public final JsonPointer getPointer() + { + return pointer; + } + + /** + * Tell whether the current JSON Reference "contains" another + * + *

This is considered true iif both references have the same locator, + * in other words, if they differ only by their fragment part.

+ * + * @param other the other reference + * @return see above + */ + public final boolean contains(final JsonRef other) + { + return locator.equals(other.locator); + } + + @Override + public final int hashCode() + { + return hashCode; + } + + @Override + public final boolean equals(final Object obj) + { + if (obj == null) + return false; + if (this == obj) + return true; + + if (!(obj instanceof JsonRef)) + return false; + + final JsonRef that = (JsonRef) obj; + return asString.equals(that.asString); + } + + @Override + public final String toString() + { + return asString; + } +}