Schema.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */

package org.forgerock.api.models;

import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER;
import static org.forgerock.api.jackson.JacksonUtils.schemaFor;
import static org.forgerock.json.JsonValue.json;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.forgerock.api.jackson.JacksonUtils;
import org.wrensecurity.guava.common.base.Strings;
import org.forgerock.json.JsonValue;
import org.forgerock.util.Reject;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;

/**
 * Class that represents the Schema type in API descriptor.
 */
@JsonDeserialize(builder = Schema.Builder.class)
public abstract class Schema {

    private Schema() {
        // This class only has two private inner sub-classes, so constructor is private.
    }

    /**
     * Getter for reference. May be null if the schema is specified here.
     * @return The reference.
     */
    public abstract Reference getReference();

    /**
     * Obtain the schema definition if it is not a reference.
     * @return The schema.
     */
    public abstract JsonValue getSchema();

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Schema schema1 = (Schema) o;
        return Objects.equals(getReference(), schema1.getReference())
                && isSchemaPropertyMatches(schema1);

    }

    private boolean isSchemaPropertyMatches(Schema schema1) {
        return getSchema() != null && schema1.getSchema() != null
                ? Objects.equals(getSchema().getObject(), schema1.getSchema().getObject())
                : schema1.getSchema() == getSchema();
    }

    @Override
    public int hashCode() {
        JsonValue schema = getSchema();
        return Objects.hash(getReference(), schema == null ? null : schema.getObject());
    }

    /**
     * Create a new Builder for Schema.
     * @return The builder.
     */
    public static Builder newBuilder() {
        return new Builder();
    }

    /**
     * Create a new Builder for Schema. A synonym for {@link #newBuilder()} that is useful for static imports.
     * @return The builder.
     */
    public static Builder schema() {
        return newBuilder();
    }

    /**
     * Builds Schema object from the data in the annotation parameter. If the {@code schema} has an {@code id} defined,
     * or if the type being used for the schema definition has an {@code id} defined, the schema will be defined in the
     * top-level {@code descriptor}, and a reference to that definition will be returned.
     *
     * @param schema The annotation that holds the data
     * @param descriptor The root descriptor to add definitions to.
     * @param relativeType The type relative to which schema resources should be resolved.
     * @return Schema instance
     */
    public static Schema fromAnnotation(org.forgerock.api.annotations.Schema schema, ApiDescription descriptor,
            final Class<?> relativeType) {
        Class<?> type = schema.fromType();
        if (type.equals(Void.class) && Strings.isNullOrEmpty(schema.schemaResource())) {
            return null;
        }
        Builder builder = schema();
        String id = schema.id();
        if (!type.equals(Void.class)) {
            // the annotation declares a type to use as the schema
            builder.type(type);
            if (Strings.isNullOrEmpty(id)) {
                // if the schema annotation passed to this method does not have an id, check to see if the type being
                // used to generate the schema is annotated with an id.
                org.forgerock.api.annotations.Schema typeSchema =
                        type.getAnnotation(org.forgerock.api.annotations.Schema.class);
                if (typeSchema != null && !Strings.isNullOrEmpty(typeSchema.id())) {
                    id = typeSchema.id();
                }
            }
        } else {
            // not using a type, so must be using a resource file containing JSON Schema json.
            InputStream resource = relativeType.getResourceAsStream(schema.schemaResource());
            try {
                JsonValue json = json(JacksonUtils.OBJECT_MAPPER.readValue(resource, Object.class))
                        .as(new TranslateJsonSchema(relativeType.getClassLoader()));
                builder.schema(json);
            } catch (IOException e) {
                throw new IllegalArgumentException("Could not read declared resource " + schema.schemaResource(), e);
            }
        }
        if (!Strings.isNullOrEmpty(id)) {
            // we've got an id for this schema, so define it at the top level and return a reference.
            descriptor.addDefinition(id, builder.build());
            return schema().reference(Reference.reference().value("#/definitions/" + id).build()).build();
        } else {
            return builder.build();
        }
    }

    /**
     * A builder class for {@code Schema} instances.
     */
    public static final class Builder {

        private JsonValue schema;
        private Reference reference;
        private Map<String, Object> jsonValueObject = new HashMap<>();

        /**
         * Private default constructor.
         */
        private Builder() { }

        /**
         * Sets the schema reference.
         * @param reference The reference.
         * @return This builder.
         */
        @JsonProperty("$ref")
        public Builder reference(Reference reference) {
            Reject.ifNull(reference);
            this.reference = reference;
            return this;
        }

        /**
         * Sets the schema.
         * @param schema The schema.
         * @return This builder.
         */
        public Builder schema(JsonValue schema) {
            Reject.ifNull(schema);
            this.schema = schema;
            return this;
        }

        /**
         * Sets the schema by json key-value pairs.
         * @param key json parameter name
         * @param value json parametr value
         * @return This builder.
         */
        @JsonAnySetter
        public Builder schema(String key, Object value) {
            jsonValueObject.put(key, value);
            return this;
        }

        /**
         * Sets the schema.
         * @param type The type to derive the schema from.
         * @return This builder.
         */
        public Builder type(Class<?> type) {
            Reject.ifNull(type);
            try {
                JsonSchema jsonSchema = schemaFor(type);
                String schemaString = OBJECT_MAPPER.writer().writeValueAsString(jsonSchema);
                this.schema = json(OBJECT_MAPPER.readValue(schemaString, Object.class))
                        .as(new TranslateJsonSchema(type.getClassLoader()));
            } catch (JsonMappingException e) {
                throw new IllegalArgumentException(e);
            } catch (IOException e) {
                throw new IllegalStateException("Jackson cannot read its own JSON", e);
            }
            return this;
        }

        /**
         * Builds the Schema instance.
         *
         * @return Schema instance.
         */
        public Schema build() {
            if (!jsonValueObject.isEmpty()) {
                schema(json(jsonValueObject));
            }
            return reference == null ? new SchemaSchema(schema) : new ReferenceSchema(reference);
        }
    }

    private static final class ReferenceSchema extends Schema {

        private final Reference reference;

        private ReferenceSchema(Reference reference) {
            this.reference = reference;
        }

        @Override
        @JsonProperty("$ref")
        public Reference getReference() {
            return reference;
        }

        @Override
        @JsonIgnore
        public JsonValue getSchema() {
            return null;
        }
    }

    private static final class SchemaSchema extends Schema {

        private final JsonValue schema;

        private SchemaSchema(JsonValue schema) {
            this.schema = schema;
        }

        @Override
        @JsonIgnore
        public Reference getReference() {
            return null;
        }

        @Override
        @com.fasterxml.jackson.annotation.JsonValue
        public JsonValue getSchema() {
            return schema;
        }
    }
}