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 java.io.IOException;
24  import java.io.InputStream;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.Objects;
28  
29  import org.forgerock.api.jackson.JacksonUtils;
30  import org.wrensecurity.guava.common.base.Strings;
31  import org.forgerock.json.JsonValue;
32  import org.forgerock.util.Reject;
33  
34  import com.fasterxml.jackson.annotation.JsonAnySetter;
35  import com.fasterxml.jackson.annotation.JsonIgnore;
36  import com.fasterxml.jackson.annotation.JsonProperty;
37  import com.fasterxml.jackson.databind.JsonMappingException;
38  import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
39  import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
40  
41  /**
42   * Class that represents the Schema type in API descriptor.
43   */
44  @JsonDeserialize(builder = Schema.Builder.class)
45  public abstract class Schema {
46  
47      private Schema() {
48          // This class only has two private inner sub-classes, so constructor is private.
49      }
50  
51      /**
52       * Getter for reference. May be null if the schema is specified here.
53       * @return The reference.
54       */
55      public abstract Reference getReference();
56  
57      /**
58       * Obtain the schema definition if it is not a reference.
59       * @return The schema.
60       */
61      public abstract JsonValue getSchema();
62  
63      @Override
64      public boolean equals(Object o) {
65          if (this == o) {
66              return true;
67          }
68          if (o == null || getClass() != o.getClass()) {
69              return false;
70          }
71  
72          Schema schema1 = (Schema) o;
73          return Objects.equals(getReference(), schema1.getReference())
74                  && isSchemaPropertyMatches(schema1);
75  
76      }
77  
78      private boolean isSchemaPropertyMatches(Schema schema1) {
79          return getSchema() != null && schema1.getSchema() != null
80                  ? Objects.equals(getSchema().getObject(), schema1.getSchema().getObject())
81                  : schema1.getSchema() == getSchema();
82      }
83  
84      @Override
85      public int hashCode() {
86          JsonValue schema = getSchema();
87          return Objects.hash(getReference(), schema == null ? null : schema.getObject());
88      }
89  
90      /**
91       * Create a new Builder for Schema.
92       * @return The builder.
93       */
94      public static Builder newBuilder() {
95          return new Builder();
96      }
97  
98      /**
99       * 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 }