EventSchemaLoader.java

package org.wikimedia.eventutilities.core.event;

import static java.util.Objects.requireNonNull;

import java.net.URI;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wikimedia.eventutilities.core.json.JsonLoader;
import org.wikimedia.eventutilities.core.json.JsonLoadingException;
import org.wikimedia.eventutilities.core.json.JsonSchemaLoader;
import org.wikimedia.eventutilities.core.util.ResourceLoader;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.google.common.base.Preconditions;

/**
 * Class to load and cache JSONSchema JsonNodes from relative schema URIs and event data.
 *
 * Usage:
 * <pre>{@code
 * EventSchemaLoader schemaLoader = EventSchemaLoader.builder()
 *  .setJsonSchemaLoader(ResourceLoader.asURLs(Arrays.asList(
 *      "file:///path/to/schemas1",
 *      "http://schema.repo.org/path/to/schemas"
 *  )))
 *  .build();
 *
 * // Load the JSONSchema at file:///path/to/schemas/test/event/0.0.2
 * schemaLoader.getEventSchema("/test/event/0.0.2");
 *
 * // Load the JsonNode or JSON String event's JSONSchema at /$schema
 * schemaLoader.getEventSchema(event);
 * }</pre>
 */
public class EventSchemaLoader {

    /**
     * When looking up latest schema versions, this will be used instead of a semver string.
     */
    protected static final String LATEST_FILE_NAME = "latest";

    /**
     * Field in an event from which to extract the schema URI.
     */
    protected final JsonPointer schemaFieldPointer;

    /**
     * JsonSchemaLoader used to load schemas from URIs.
     */
    protected final JsonSchemaLoader schemaLoader;

    /**
     * Used to parse a {@link JsonNode} into a {@link JsonSchema} that can be used for validation.
     */
    private static final JsonSchemaFactory JSON_SCHEMA_FACTORY = JsonSchemaFactory.byDefault();

    private static final Logger LOG = LoggerFactory.getLogger(EventSchemaLoader.class.getName());

    /**
     * Constructs a EventSchemaLoader that prefixes URIs with baseURI and
     * extracts schema URIs from the schemaField in events.
     * This is protected; use {@link EventSchemaLoader.Builder}.
     */
    protected EventSchemaLoader(JsonSchemaLoader loader, JsonPointer schemaFieldPointer) {
        this.schemaFieldPointer = schemaFieldPointer;
        this.schemaLoader = loader;
    }

    public static class Builder {
        private static final String SCHEMA_FIELD_DEFAULT = "/$schema";

        private JsonSchemaLoader jsonSchemaLoader;
        private String schemaField = SCHEMA_FIELD_DEFAULT;

        /**
         * Sets the {@link JsonSchemaLoader}.
         */
        public Builder setJsonSchemaLoader(JsonSchemaLoader schemaLoader) {
            this.jsonSchemaLoader = schemaLoader;
            return this;
        }

        /**
         * Sets the schema field from which a schema URI will be extracted from events.
         */
        public Builder setSchemaField(String schemaField) {
            this.schemaField = schemaField;
            return this;
        }

        /**
         * Retuns a new EventSchemaLoader.
         */
        public EventSchemaLoader build() {
            Preconditions.checkState(
                    jsonSchemaLoader != null,
                    "Must call setJsonSchemaLoader() before calling build().");

            return new EventSchemaLoader(jsonSchemaLoader, JsonPointer.compile(schemaField));
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    /**
     * Returns the content at schemaURI as a JsonNode JSONSchema.
     *
     * @return the jsonschema at schemaURI.
     */
    public JsonNode load(URI schemaUri) throws JsonLoadingException {
        LOG.debug("Loading event schema at {}", schemaUri);
        return schemaLoader.load(schemaUri);
    }

    /**
     * Extracts the value at schemaFieldPointer from the event as a URI.
     */
    public URI extractSchemaUri(JsonNode event) {
        String uriString = requireNonNull(event.at(schemaFieldPointer).textValue(),
                () -> String.format(Locale.ROOT, "Could not extract %s field from event, field does not exist", schemaFieldPointer));
        try {
            return new URI(uriString);
        } catch (java.net.URISyntaxException e) {
            throw new IllegalArgumentException(
                    "Failed building new URI from " + uriString + ". " + e.getMessage(), e
            );
        }
    }

    /**
     * Converts the given schemaUri to a 'latest' schema URI.  E.g.
     * "/my/schema/1.0.0" returns "/my/schema/latest".
     */
    public URI getLatestSchemaUri(URI schemaUri) {
        return schemaUri.resolve(LATEST_FILE_NAME);
    }

    /**
     * Builds the URI to the latest schema using this title. E.g.
     * "my/schema" returns "/my/schema/latest".
     */
    public URI latestSchemaURI(String title) {
        return schemaUri(title, LATEST_FILE_NAME);
    }

    /**
     * Builds the URI to the schema using this title and this version.
     * E.g. "my/schema", "1.1.0" returns "/my/schema/1.1.0".
     */
    public URI schemaUri(String title, String version) {
        return URI.create("/" + title + "/" + version);
    }

    /**
     * Extracts the event's schema URI and converts it to a latest schema URI.
     * @return 'latest' version of this event's schema URI
     */
    public URI getLatestSchemaUri(JsonNode event) {
        return getLatestSchemaUri(extractSchemaUri(event));
    }

    /**
     * Get a JsonSchema at schemaUri looking in all baseUris.
     */
    public JsonNode getSchema(URI schemaUri) throws JsonLoadingException {
        return this.load(schemaUri);
    }

    /**
     * Get a 'latest' JsonSchema given a schema URI looking in all baseUris.
     */
    public JsonNode getLatestSchema(URI schemaUri) throws JsonLoadingException {
        return this.load(getLatestSchemaUri(schemaUri));
    }


//    GET SCHEMA FROM EVENT or EVENT STRING

    /**
     * Given an event object, this extracts its schema URI at schemaField
     * (prefixed with baseURI) and returns the schema there.
     */
    public JsonNode getEventSchema(JsonNode event) throws JsonLoadingException {
        return this.getSchema(extractSchemaUri(event));
    }

    /**
     * Given a JSON event string, get its schema URI,
     * and load and return schema for the event.
     */
    public JsonNode getEventSchema(String eventString) throws JsonLoadingException {
        JsonNode event = this.schemaLoader.parse(eventString);
        return getEventSchema(event);
    }

    /**
     * Given a JSON event, get its schema URI,
     * and load and return schema for the event as a JsonSchema (suited for validation).
     */
    public JsonSchema getEventJsonSchema(String eventString) throws ProcessingException, JsonLoadingException {
        return getEventJsonSchema(this.schemaLoader.parse(eventString));
    }

    /**
     * Given a JSON event, get its schema URI,
     * and load and return schema for the event as a JsonSchema (suited for validation).
     */
    public JsonSchema getEventJsonSchema(JsonNode event) throws ProcessingException, JsonLoadingException {
        JsonNode schemaAsJson = this.getEventSchema(event);
        return getJsonSchema(schemaAsJson);
    }

    /**
     * Given a json schema parsed as a JsonNode materialize JsonSchema suited for validation.
     */
    public JsonSchema getJsonSchema(JsonNode schema) throws ProcessingException {
        return JSON_SCHEMA_FACTORY.getJsonSchema(schema);
    }

    /**
     * Given an event object, this extracts its schema URI at schemaField
     * (prefixed with baseURI) and resolves it to the latest schema URI and returns
     * the schema there.
     */
    public JsonNode getLatestEventSchema(JsonNode event) throws JsonLoadingException {
        return getSchema(getLatestSchemaUri(event));
    }

    /**
     * Given a JSON event string, get its schema URI,
     * and load and return latest schema for the event.
     */
    public JsonNode getLatestEventSchema(String eventString) throws JsonLoadingException {
        JsonNode event = schemaLoader.parse(eventString);
        return getLatestEventSchema(event);
    }

    public JsonLoader getJsonLoader() {
        return schemaLoader.getJsonLoader();
    }

    public ResourceLoader getResourceLoader() {
        return getJsonLoader().getResourceLoader();
    }

    public String getLatestVersionFileName() {
        return LATEST_FILE_NAME;
    }

    public String toString() {
        return "EventSchemaLoader(" + getResourceLoader() + ", " + schemaFieldPointer + ")";
    }
}