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