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