View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.api.models;
18  
19  import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER;
20  import static org.forgerock.api.jackson.JacksonUtils.schemaFor;
21  import static org.forgerock.json.JsonValue.json;
22  
23  import com.fasterxml.jackson.annotation.JsonAnySetter;
24  import com.fasterxml.jackson.annotation.JsonIgnore;
25  import com.fasterxml.jackson.annotation.JsonProperty;
26  import com.fasterxml.jackson.databind.JsonMappingException;
27  import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
28  import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.util.HashMap;
32  import java.util.Map;
33  import java.util.Objects;
34  import org.forgerock.api.jackson.JacksonUtils;
35  import org.forgerock.json.JsonValue;
36  import org.forgerock.util.Reject;
37  import org.wrensecurity.guava.common.base.Strings;
38  
39  /**
40   * Class that represents the Schema type in API descriptor.
41   */
42  @JsonDeserialize(builder = Schema.Builder.class)
43  public abstract class Schema {
44  
45      private Schema() {
46          // This class only has two private inner sub-classes, so constructor is private.
47      }
48  
49      /**
50       * Getter for reference. May be null if the schema is specified here.
51       * @return The reference.
52       */
53      public abstract Reference getReference();
54  
55      /**
56       * Obtain the schema definition if it is not a reference.
57       * @return The schema.
58       */
59      public abstract JsonValue getSchema();
60  
61      @Override
62      public boolean equals(Object o) {
63          if (this == o) {
64              return true;
65          }
66          if (o == null || getClass() != o.getClass()) {
67              return false;
68          }
69  
70          Schema schema1 = (Schema) o;
71          return Objects.equals(getReference(), schema1.getReference())
72                  && isSchemaPropertyMatches(schema1);
73  
74      }
75  
76      private boolean isSchemaPropertyMatches(Schema schema1) {
77          return getSchema() != null && schema1.getSchema() != null
78                  ? Objects.equals(getSchema().getObject(), schema1.getSchema().getObject())
79                  : schema1.getSchema() == getSchema();
80      }
81  
82      @Override
83      public int hashCode() {
84          JsonValue schema = getSchema();
85          return Objects.hash(getReference(), schema == null ? null : schema.getObject());
86      }
87  
88      /**
89       * Create a new Builder for Schema.
90       * @return The builder.
91       */
92      public static Builder newBuilder() {
93          return new Builder();
94      }
95  
96      /**
97       * Create a new Builder for Schema. A synonym for {@link #newBuilder()} that is useful for static imports.
98       * @return The builder.
99       */
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 }