1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.forgerock.api.jackson;
18
19 import static java.nio.charset.StandardCharsets.UTF_8;
20 import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER;
21 import static org.forgerock.api.util.ValidationUtil.isEmpty;
22
23 import java.io.IOException;
24 import java.lang.annotation.Annotation;
25 import java.math.BigDecimal;
26 import java.net.URL;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Set;
32
33 import javax.validation.constraints.DecimalMax;
34 import javax.validation.constraints.DecimalMin;
35 import javax.validation.constraints.Max;
36 import javax.validation.constraints.Min;
37 import javax.validation.constraints.NotNull;
38 import javax.validation.constraints.Pattern;
39 import javax.validation.constraints.Size;
40
41 import org.forgerock.api.annotations.Default;
42 import org.forgerock.api.annotations.Description;
43 import org.forgerock.api.annotations.EnumTitle;
44 import org.forgerock.api.annotations.Example;
45 import org.forgerock.api.annotations.Format;
46 import org.forgerock.api.annotations.MultipleOf;
47 import org.forgerock.api.annotations.PropertyOrder;
48 import org.forgerock.api.annotations.PropertyPolicies;
49 import org.forgerock.api.annotations.ReadOnly;
50 import org.forgerock.api.annotations.Title;
51 import org.forgerock.api.annotations.UniqueItems;
52 import org.forgerock.api.enums.WritePolicy;
53 import org.wrensecurity.guava.common.io.Resources;
54
55 import com.fasterxml.jackson.databind.BeanProperty;
56 import com.fasterxml.jackson.databind.JavaType;
57 import com.fasterxml.jackson.databind.JsonMappingException;
58 import com.fasterxml.jackson.databind.SerializerProvider;
59 import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
60 import com.fasterxml.jackson.databind.type.SimpleType;
61 import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
62 import com.fasterxml.jackson.module.jsonSchema.factories.ObjectVisitor;
63 import com.fasterxml.jackson.module.jsonSchema.factories.ObjectVisitorDecorator;
64 import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;
65 import com.fasterxml.jackson.module.jsonSchema.factories.VisitorContext;
66 import com.fasterxml.jackson.module.jsonSchema.factories.WrapperFactory;
67 import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema;
68 import com.fasterxml.jackson.module.jsonSchema.types.NumberSchema;
69 import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema;
70 import com.fasterxml.jackson.module.jsonSchema.types.SimpleTypeSchema;
71 import com.fasterxml.jackson.module.jsonSchema.types.StringSchema;
72 import org.forgerock.api.annotations.AdditionalProperties;
73
74
75
76
77
78 public class CrestPropertyDetailsSchemaFactoryWrapper extends SchemaFactoryWrapper {
79
80 private static final WrapperFactory WRAPPER_FACTORY = new WrapperFactory() {
81 @Override
82 public SchemaFactoryWrapper getWrapper(SerializerProvider provider) {
83 SchemaFactoryWrapper wrapper = new CrestPropertyDetailsSchemaFactoryWrapper();
84 wrapper.setProvider(provider);
85 return wrapper;
86 }
87
88 @Override
89 public SchemaFactoryWrapper getWrapper(SerializerProvider provider, VisitorContext rvc) {
90 SchemaFactoryWrapper wrapper = new CrestPropertyDetailsSchemaFactoryWrapper();
91 wrapper.setProvider(provider);
92 wrapper.setVisitorContext(rvc);
93 return wrapper;
94 }
95 };
96 private static final String CLASSPATH_RESOURCE = "classpath:";
97
98
99
100
101
102 public CrestPropertyDetailsSchemaFactoryWrapper() {
103 super(WRAPPER_FACTORY);
104 this.schemaProvider = new CrestJsonSchemaFactory();
105 }
106
107 @Override
108 public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) {
109 final ObjectVisitor objectVisitor = (ObjectVisitor) super.expectObjectFormat(convertedType);
110 final Class<?> clazz = convertedType.getRawClass();
111
112
113 if (schema instanceof SimpleTypeSchema) {
114 final Title title = clazz.getAnnotation(Title.class);
115 if (title != null && !isEmpty(title.value())) {
116 ((SimpleTypeSchema) schema).setTitle(title.value());
117 }
118 }
119
120 final Description description = clazz.getAnnotation(Description.class);
121 if (description != null && !isEmpty(description.value())) {
122 schema.setDescription(description.value());
123 }
124
125 final Set<String> requiredFieldNames;
126 if (schema instanceof RequiredFieldsSchema) {
127 requiredFieldNames = Collections.synchronizedSet(new HashSet<String>());
128 ((RequiredFieldsSchema) schema).setRequiredFields(requiredFieldNames);
129 } else {
130 requiredFieldNames = null;
131 }
132
133 final Example example = clazz.getAnnotation(Example.class);
134 if (schema instanceof WithExampleSchema && example != null && !isEmpty(example.value())) {
135 setExample(clazz, example, (WithExampleSchema<?>) schema);
136 }
137
138
139 return new ObjectVisitorDecorator(objectVisitor) {
140 @Override
141 public JsonSchema getSchema() {
142 return super.getSchema();
143 }
144
145 @Override
146 public void optionalProperty(BeanProperty writer) throws JsonMappingException {
147 super.optionalProperty(writer);
148 JsonSchema schema = schemaFor(writer);
149 addFieldPolicies(writer, schema);
150 addPropertyOrder(writer, schema);
151 addEnumTitles(writer, schema);
152 addRequired(writer, schema);
153 addStringPattern(writer, schema);
154 addStringMinLength(writer, schema);
155 addStringMaxLength(writer, schema);
156 addArrayMinItems(writer, schema);
157 addArrayMaxItems(writer, schema);
158 addNumberMaximum(writer, schema);
159 addNumberMinimum(writer, schema);
160 addNumberExclusiveMinimum(writer, schema);
161 addNumberExclusiveMaximum(writer, schema);
162 addReadOnly(writer, schema);
163 addTitle(writer, schema);
164 addDescription(writer, schema);
165 addDefault(writer, schema);
166 addUniqueItems(writer, schema);
167 addMultipleOf(writer, schema);
168 addFormat(writer, schema);
169 addExample(writer, schema);
170 addAdditionalProperties(writer, schema);
171 }
172
173 private void addExample(BeanProperty writer, JsonSchema schema) {
174 Example annotation = annotationFor(writer, Example.class);
175 if (annotation != null) {
176 WithExampleSchema<?> exampleSchema = (WithExampleSchema<?>) schema;
177 setExample(writer.getType().getRawClass(), annotation, exampleSchema);
178 }
179 }
180
181 private void addEnumTitles(BeanProperty writer, JsonSchema schema) {
182 JavaType type = writer.getType();
183 if (type.isEnumType()) {
184 Class<? extends Enum> enumClass = type.getRawClass().asSubclass(Enum.class);
185 Enum[] enumConstants = enumClass.getEnumConstants();
186 List<String> titles = new ArrayList<>(enumConstants.length);
187 boolean foundTitle = false;
188 for (Enum<?> value : enumConstants) {
189 try {
190 EnumTitle title = enumClass.getField(value.name()).getAnnotation(EnumTitle.class);
191 if (title != null) {
192 titles.add(title.value());
193 foundTitle = true;
194 } else {
195 titles.add(null);
196 }
197 } catch (NoSuchFieldException e) {
198 throw new IllegalStateException("Enum doesn't have its own value as a field", e);
199 }
200 }
201 if (foundTitle) {
202 ((EnumSchema) schema).setEnumTitles(titles);
203 }
204 }
205 }
206
207 private void addPropertyOrder(BeanProperty writer, JsonSchema schema) {
208 PropertyOrder order = annotationFor(writer, PropertyOrder.class);
209 if (order != null) {
210 ((OrderedFieldSchema) schema).setPropertyOrder(order.value());
211 }
212 }
213
214 private void addFieldPolicies(BeanProperty writer, JsonSchema schema) {
215 PropertyPolicies policies = annotationFor(writer, PropertyPolicies.class);
216 if (policies != null) {
217 CrestReadWritePoliciesSchema schemaPolicies = (CrestReadWritePoliciesSchema) schema;
218 if (policies.write() != WritePolicy.WRITABLE) {
219 schemaPolicies.setWritePolicy(policies.write());
220 schemaPolicies.setErrorOnWritePolicyFailure(policies.errorOnWritePolicyFailure());
221 }
222 schemaPolicies.setReadPolicy(policies.read());
223 schemaPolicies.setReturnOnDemand(policies.returnOnDemand());
224 }
225 }
226
227 private void addRequired(BeanProperty writer, JsonSchema schema) {
228 NotNull notNull = annotationFor(writer, NotNull.class);
229 if (notNull != null) {
230 if (requiredFieldNames != null) {
231 requiredFieldNames.add(writer.getName());
232 } else {
233
234 schema.setRequired(true);
235 }
236 }
237 }
238
239 private void addStringPattern(BeanProperty writer, JsonSchema schema) {
240 Pattern pattern = annotationFor(writer, Pattern.class);
241 if (pattern != null && !isEmpty(pattern.regexp())) {
242 ((StringSchema) schema).setPattern(pattern.regexp());
243 }
244 }
245
246 private void addStringMinLength(BeanProperty writer, JsonSchema schema) {
247 Integer size = getMinSize(writer);
248 if (size != null && schema instanceof StringSchema) {
249 ((StringSchema) schema).setMinLength(size);
250 }
251 }
252
253 private void addStringMaxLength(BeanProperty writer, JsonSchema schema) {
254 Integer size = getMaxSize(writer);
255 if (size != null && schema instanceof StringSchema) {
256 ((StringSchema) schema).setMaxLength(size);
257 }
258 }
259
260 private void addArrayMinItems(BeanProperty writer, JsonSchema schema) {
261 Integer size = getMinSize(writer);
262 if (size != null && schema instanceof ArraySchema) {
263 ((ArraySchema) schema).setMinItems(size);
264 }
265 }
266
267 private void addArrayMaxItems(BeanProperty writer, JsonSchema schema) {
268 Integer size = getMaxSize(writer);
269 if (size != null && schema instanceof ArraySchema) {
270 ((ArraySchema) schema).setMaxItems(size);
271 }
272 }
273
274 private void addNumberMinimum(BeanProperty writer, JsonSchema schema) {
275 Min min = annotationFor(writer, Min.class);
276 if (min != null) {
277 ((MinimumMaximumSchema) schema).setPropertyMinimum(new BigDecimal(min.value()));
278 }
279
280 DecimalMin decimalMin = annotationFor(writer, DecimalMin.class);
281 if (decimalMin != null) {
282 ((MinimumMaximumSchema) schema).setPropertyMinimum(new BigDecimal(decimalMin.value()));
283 }
284 }
285
286 private void addNumberMaximum(BeanProperty writer, JsonSchema schema) {
287 Max max = annotationFor(writer, Max.class);
288 if (max != null) {
289 ((MinimumMaximumSchema) schema).setPropertyMaximum(new BigDecimal(max.value()));
290 }
291
292 DecimalMax decimalMax = annotationFor(writer, DecimalMax.class);
293 if (decimalMax != null) {
294 ((MinimumMaximumSchema) schema).setPropertyMaximum(new BigDecimal(decimalMax.value()));
295 }
296 }
297
298 private void addNumberExclusiveMinimum(BeanProperty writer, JsonSchema schema) {
299 DecimalMin decimalMin = annotationFor(writer, DecimalMin.class);
300 if (decimalMin != null && !decimalMin.inclusive()) {
301 ((NumberSchema) schema).setExclusiveMinimum(true);
302 }
303 }
304
305 private void addNumberExclusiveMaximum(BeanProperty writer, JsonSchema schema) {
306 DecimalMax decimalMax = annotationFor(writer, DecimalMax.class);
307 if (decimalMax != null && !decimalMax.inclusive()) {
308 ((NumberSchema) schema).setExclusiveMaximum(true);
309 }
310 }
311
312 private void addReadOnly(BeanProperty writer, JsonSchema schema) {
313 ReadOnly readOnly = annotationFor(writer, ReadOnly.class);
314 if (readOnly != null) {
315 schema.setReadonly(readOnly.value());
316 }
317 }
318
319 private void addTitle(BeanProperty writer, JsonSchema schema) {
320 Title title = annotationFor(writer, Title.class);
321 if (title != null && !isEmpty(title.value())) {
322 ((SimpleTypeSchema) schema).setTitle(title.value());
323 }
324 }
325
326 private void addDescription(BeanProperty writer, JsonSchema schema) {
327 Description description = annotationFor(writer, Description.class);
328 if (description != null && !isEmpty(description.value())) {
329 schema.setDescription(description.value());
330 }
331 }
332
333 private void addDefault(BeanProperty writer, JsonSchema schema) {
334 Default defaultAnnotation = annotationFor(writer, Default.class);
335 if (defaultAnnotation != null && !isEmpty(defaultAnnotation.value())) {
336 ((SimpleTypeSchema) schema).setDefault(defaultAnnotation.value());
337 }
338 }
339
340 private void addUniqueItems(BeanProperty writer, JsonSchema schema) {
341 UniqueItems uniqueItems = annotationFor(writer, UniqueItems.class);
342 if (uniqueItems != null) {
343 ((ArraySchema) schema).setUniqueItems(uniqueItems.value());
344 }
345 }
346
347 private void addMultipleOf(BeanProperty writer, JsonSchema schema) {
348 MultipleOf multipleOf = annotationFor(writer, MultipleOf.class);
349 if (multipleOf != null) {
350 ((MultipleOfSchema) schema).setMultipleOf(multipleOf.value());
351 }
352 }
353
354 private void addFormat(BeanProperty writer, JsonSchema schema) {
355 if (schema instanceof PropertyFormatSchema) {
356 Format format = annotationFor(writer, Format.class);
357 if (format != null && !isEmpty(format.value())) {
358 ((PropertyFormatSchema) schema).setPropertyFormat(format.value());
359 } else if (writer.getType() instanceof SimpleType) {
360
361 final Class rawClass = writer.getType().getRawClass();
362 final String formatValue;
363 if (Integer.class.equals(rawClass) || int.class.equals(rawClass)) {
364 formatValue = "int32";
365 } else if (Long.class.equals(rawClass) || long.class.equals(rawClass)) {
366 formatValue = "int64";
367 } else if (Double.class.equals(rawClass) || double.class.equals(rawClass)) {
368 formatValue = "double";
369 } else if (Float.class.equals(rawClass) || float.class.equals(rawClass)) {
370 formatValue = "float";
371 } else {
372 return;
373 }
374 ((PropertyFormatSchema) schema).setPropertyFormat(formatValue);
375 }
376 }
377 }
378
379 private void addAdditionalProperties(BeanProperty writer, JsonSchema schema) throws JsonMappingException {
380 AdditionalProperties additionalProperties = annotationFor(writer, AdditionalProperties.class);
381 if (additionalProperties != null && !additionalProperties.value().isInstance(Void.class)) {
382 CrestPropertyDetailsSchemaFactoryWrapper visitor = new CrestPropertyDetailsSchemaFactoryWrapper();
383 OBJECT_MAPPER.acceptJsonFormatVisitor(additionalProperties.value(), visitor);
384 ObjectSchema.SchemaAdditionalProperties schemaAdditionalProperties =
385 new ObjectSchema.SchemaAdditionalProperties(visitor.finalSchema());
386 ((ObjectSchema) schema).setAdditionalProperties(schemaAdditionalProperties);
387 }
388 }
389
390 private Integer getMaxSize(BeanProperty writer) {
391 Size size = writer.getAnnotation(Size.class);
392 if (size != null) {
393 int value = size.max();
394 if (value != Integer.MAX_VALUE) {
395 return value;
396 }
397 }
398 return null;
399 }
400
401 private Integer getMinSize(BeanProperty writer) {
402 Size size = writer.getAnnotation(Size.class);
403 if (size != null) {
404 int value = size.min();
405 if (value != 0) {
406 return value;
407 }
408 }
409 return null;
410 }
411
412
413
414
415
416
417
418
419
420 private <T extends Annotation> T annotationFor(BeanProperty writer, Class<T> type) {
421 return writer.getMember().getAnnotation(type);
422 }
423
424 private JsonSchema schemaFor(BeanProperty writer) {
425 return getSchema().asObjectSchema().getProperties().get(writer.getName());
426 }
427 };
428 }
429
430 private void setExample(Class<?> contextClass, Example annotation, WithExampleSchema<?> exampleSchema) {
431 String example = annotation.value();
432 if (example.startsWith(CLASSPATH_RESOURCE)) {
433 ClassLoader classLoader = contextClass.getClassLoader();
434 try {
435 String name = example.substring(CLASSPATH_RESOURCE.length()).trim();
436 URL resource = classLoader.getResource(name);
437 if (resource != null) {
438 example = Resources.toString(resource, UTF_8);
439 } else {
440 throw new IllegalStateException("Cannot read resource: " + example);
441 }
442 } catch (IOException e) {
443 throw new IllegalStateException("Cannot read resource: " + example, e);
444 }
445 }
446 try {
447 exampleSchema.setExample(example);
448 } catch (IOException e) {
449 throw new IllegalArgumentException("Could not parse example value to type of schema", e);
450 }
451 }
452 }