001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016
017package org.forgerock.api.models;
018
019import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER;
020import static org.forgerock.api.jackson.JacksonUtils.schemaFor;
021import static org.forgerock.json.JsonValue.json;
022
023import java.io.IOException;
024import java.io.InputStream;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.Objects;
028
029import org.forgerock.api.jackson.JacksonUtils;
030import org.wrensecurity.guava.common.base.Strings;
031import org.forgerock.json.JsonValue;
032import org.forgerock.util.Reject;
033
034import com.fasterxml.jackson.annotation.JsonAnySetter;
035import com.fasterxml.jackson.annotation.JsonIgnore;
036import com.fasterxml.jackson.annotation.JsonProperty;
037import com.fasterxml.jackson.databind.JsonMappingException;
038import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
039import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
040
041/**
042 * Class that represents the Schema type in API descriptor.
043 */
044@JsonDeserialize(builder = Schema.Builder.class)
045public abstract class Schema {
046
047    private Schema() {
048        // This class only has two private inner sub-classes, so constructor is private.
049    }
050
051    /**
052     * Getter for reference. May be null if the schema is specified here.
053     * @return The reference.
054     */
055    public abstract Reference getReference();
056
057    /**
058     * Obtain the schema definition if it is not a reference.
059     * @return The schema.
060     */
061    public abstract JsonValue getSchema();
062
063    @Override
064    public boolean equals(Object o) {
065        if (this == o) {
066            return true;
067        }
068        if (o == null || getClass() != o.getClass()) {
069            return false;
070        }
071
072        Schema schema1 = (Schema) o;
073        return Objects.equals(getReference(), schema1.getReference())
074                && isSchemaPropertyMatches(schema1);
075
076    }
077
078    private boolean isSchemaPropertyMatches(Schema schema1) {
079        return getSchema() != null && schema1.getSchema() != null
080                ? Objects.equals(getSchema().getObject(), schema1.getSchema().getObject())
081                : schema1.getSchema() == getSchema();
082    }
083
084    @Override
085    public int hashCode() {
086        JsonValue schema = getSchema();
087        return Objects.hash(getReference(), schema == null ? null : schema.getObject());
088    }
089
090    /**
091     * Create a new Builder for Schema.
092     * @return The builder.
093     */
094    public static Builder newBuilder() {
095        return new Builder();
096    }
097
098    /**
099     * Create a new Builder for Schema. A synonym for {@link #newBuilder()} that is useful for static imports.
100     * @return The builder.
101     */
102    public static Builder schema() {
103        return newBuilder();
104    }
105
106    /**
107     * Builds Schema object from the data in the annotation parameter. If the {@code schema} has an {@code id} defined,
108     * or if the type being used for the schema definition has an {@code id} defined, the schema will be defined in the
109     * top-level {@code descriptor}, and a reference to that definition will be returned.
110     *
111     * @param schema The annotation that holds the data
112     * @param descriptor The root descriptor to add definitions to.
113     * @param relativeType The type relative to which schema resources should be resolved.
114     * @return Schema instance
115     */
116    public static Schema fromAnnotation(org.forgerock.api.annotations.Schema schema, ApiDescription descriptor,
117            final Class<?> relativeType) {
118        Class<?> type = schema.fromType();
119        if (type.equals(Void.class) && Strings.isNullOrEmpty(schema.schemaResource())) {
120            return null;
121        }
122        Builder builder = schema();
123        String id = schema.id();
124        if (!type.equals(Void.class)) {
125            // the annotation declares a type to use as the schema
126            builder.type(type);
127            if (Strings.isNullOrEmpty(id)) {
128                // if the schema annotation passed to this method does not have an id, check to see if the type being
129                // used to generate the schema is annotated with an id.
130                org.forgerock.api.annotations.Schema typeSchema =
131                        type.getAnnotation(org.forgerock.api.annotations.Schema.class);
132                if (typeSchema != null && !Strings.isNullOrEmpty(typeSchema.id())) {
133                    id = typeSchema.id();
134                }
135            }
136        } else {
137            // not using a type, so must be using a resource file containing JSON Schema json.
138            InputStream resource = relativeType.getResourceAsStream(schema.schemaResource());
139            try {
140                JsonValue json = json(JacksonUtils.OBJECT_MAPPER.readValue(resource, Object.class))
141                        .as(new TranslateJsonSchema(relativeType.getClassLoader()));
142                builder.schema(json);
143            } catch (IOException e) {
144                throw new IllegalArgumentException("Could not read declared resource " + schema.schemaResource(), e);
145            }
146        }
147        if (!Strings.isNullOrEmpty(id)) {
148            // we've got an id for this schema, so define it at the top level and return a reference.
149            descriptor.addDefinition(id, builder.build());
150            return schema().reference(Reference.reference().value("#/definitions/" + id).build()).build();
151        } else {
152            return builder.build();
153        }
154    }
155
156    /**
157     * A builder class for {@code Schema} instances.
158     */
159    public static final class Builder {
160
161        private JsonValue schema;
162        private Reference reference;
163        private Map<String, Object> jsonValueObject = new HashMap<>();
164
165        /**
166         * Private default constructor.
167         */
168        private Builder() { }
169
170        /**
171         * Sets the schema reference.
172         * @param reference The reference.
173         * @return This builder.
174         */
175        @JsonProperty("$ref")
176        public Builder reference(Reference reference) {
177            Reject.ifNull(reference);
178            this.reference = reference;
179            return this;
180        }
181
182        /**
183         * Sets the schema.
184         * @param schema The schema.
185         * @return This builder.
186         */
187        public Builder schema(JsonValue schema) {
188            Reject.ifNull(schema);
189            this.schema = schema;
190            return this;
191        }
192
193        /**
194         * Sets the schema by json key-value pairs.
195         * @param key json parameter name
196         * @param value json parametr value
197         * @return This builder.
198         */
199        @JsonAnySetter
200        public Builder schema(String key, Object value) {
201            jsonValueObject.put(key, value);
202            return this;
203        }
204
205        /**
206         * Sets the schema.
207         * @param type The type to derive the schema from.
208         * @return This builder.
209         */
210        public Builder type(Class<?> type) {
211            Reject.ifNull(type);
212            try {
213                JsonSchema jsonSchema = schemaFor(type);
214                String schemaString = OBJECT_MAPPER.writer().writeValueAsString(jsonSchema);
215                this.schema = json(OBJECT_MAPPER.readValue(schemaString, Object.class))
216                        .as(new TranslateJsonSchema(type.getClassLoader()));
217            } catch (JsonMappingException e) {
218                throw new IllegalArgumentException(e);
219            } catch (IOException e) {
220                throw new IllegalStateException("Jackson cannot read its own JSON", e);
221            }
222            return this;
223        }
224
225        /**
226         * Builds the Schema instance.
227         *
228         * @return Schema instance.
229         */
230        public Schema build() {
231            if (!jsonValueObject.isEmpty()) {
232                schema(json(jsonValueObject));
233            }
234            return reference == null ? new SchemaSchema(schema) : new ReferenceSchema(reference);
235        }
236    }
237
238    private static final class ReferenceSchema extends Schema {
239
240        private final Reference reference;
241
242        private ReferenceSchema(Reference reference) {
243            this.reference = reference;
244        }
245
246        @Override
247        @JsonProperty("$ref")
248        public Reference getReference() {
249            return reference;
250        }
251
252        @Override
253        @JsonIgnore
254        public JsonValue getSchema() {
255            return null;
256        }
257    }
258
259    private static final class SchemaSchema extends Schema {
260
261        private final JsonValue schema;
262
263        private SchemaSchema(JsonValue schema) {
264            this.schema = schema;
265        }
266
267        @Override
268        @JsonIgnore
269        public Reference getReference() {
270            return null;
271        }
272
273        @Override
274        @com.fasterxml.jackson.annotation.JsonValue
275        public JsonValue getSchema() {
276            return schema;
277        }
278    }
279}