· 6 years ago · Nov 25, 2019, 01:54 PM
1package com.brandmaker.mms.mp.generator.validator;
2
3import com.brandmaker.mms.mp.generator.resolvers.data.bean.ResolvedBean;
4import com.brandmaker.mms.mp.generator.resolvers.data.rest.ResolvedRestDto;
5import com.brandmaker.mms.mp.generator.resolvers.data.rest.ResolvedRestDtoCollection;
6import com.brandmaker.mms.mp.generator.resolvers.data.rest.ResolvedRestDtoMap;
7import com.brandmaker.mms.mp.generator.resolvers.data.rest.ResolvedRestDtoProperty;
8import com.fasterxml.jackson.annotation.*;
9
10import java.lang.reflect.*;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Map;
14import java.util.stream.Collectors;
15
16/**
17 * @author kamen on 24.10.19 г.
18 */
19public class RestDtoApiValidator extends AbstractValidator
20{
21
22 @Override
23 protected void validateMapping(ResolvedBean resolvedBean)
24 {
25 final ResolvedRestDto resolvedRestDto = resolvedBean.resolvedRestDto;
26 final String dtoName = resolvedRestDto.qualifiedTypeName;
27
28 // 1. Validate class data
29 final Class<?> restDtoClazz = loadClass(dtoName);
30
31 assertNotNull(restDtoClazz, parse("RestDto Class [{0}] is missing in the API", dtoName));
32
33 if (restDtoClazz == null)
34 {
35 return; // We're missing the Dto, skip everything else
36 }
37
38 assertEquals(resolvedBean.isAbstract, Modifier.isAbstract(restDtoClazz.getModifiers()), parse("[{0}] abstract type mis match.", dtoName));
39 assertEquals(resolvedBean.isInterface, Modifier.isInterface(restDtoClazz.getModifiers()), parse("[{0}] interface type mis match.", dtoName));
40 assertEquals(resolvedBean.isEnumeration, restDtoClazz.isEnum(), parse("[{0}] enum type mis match.", dtoName));
41
42 final Class<?> parent = getParent(restDtoClazz);
43
44 if (resolvedRestDto.resolvedParent == null && !resolvedBean.isEnumeration)
45 {
46 assertEquals(parent.getName(), Object.class.getName(), parse("[{0}] must have no parent, ", dtoName, parent.getName()));
47 }
48 else if (!resolvedBean.isEnumeration)
49 {
50 assertEquals(
51 resolvedRestDto.resolvedParent.qualifiedTypeName,
52 parent.getName(),
53 parse("[{0}] must have parent of type [{1}], ", dtoName, parent.getName(), parent.getName())
54 );
55 }
56
57 // 2. Validate Properties data
58 validateProps(resolvedRestDto, restDtoClazz);
59
60 // 3. Validate Collections data
61 validateCollections(resolvedRestDto, restDtoClazz);
62
63 // 4. Validate explicit constructor
64 validateExplicitConstructor(resolvedRestDto, restDtoClazz);
65
66 // 5. Validate class annotations
67 validateClassAnnotations(resolvedRestDto, restDtoClazz);
68
69 // 6. Validate field annotations
70 validateMethodAnnotations(resolvedRestDto, restDtoClazz);
71 }
72
73 private void validateProps(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
74 {
75 final String restDtoName = resolvedRestDto.qualifiedTypeName;
76 final Map<String, Field> fields = getFields(restDtoClazz);
77 final boolean isEnum = resolvedRestDto.resolvedBean.isEnumeration;
78 // json value is generated from the template, it's not described in the mapping
79 int expectedPropsCount = resolvedRestDto.properties.size();
80 if (isEnum) {
81 expectedPropsCount++;
82 }
83
84 assertEquals(
85 expectedPropsCount,
86 fields.size(),
87 parse("[{0}] must have {1} properties, but has {2}", restDtoName, expectedPropsCount, fields.size())
88 );
89
90 if (isEnum)
91 {
92 assertNotNull(fields.get("jsonValue"), parse("RestDto [{0}] must have jsonValue as it's enumeration", restDtoName));
93 }
94
95 for (ResolvedRestDtoProperty prop : resolvedRestDto.properties.values())
96 {
97 final Field field = fields.get(prop.propertyName);
98 assertNotNull(field, parse("[{0}]: must have property with name [{1}]", restDtoName, prop.propertyName));
99 if (field == null)
100 {
101 continue; // no field to assert the type
102 }
103
104 assertEquals(
105 field.getType().getName(),
106 prop.qualifiedTypeName,
107 parse("[{0}]: property type mismatch, expected [{1}] but is [{2}]", restDtoName, prop.qualifiedTypeName, field.getType().getName())
108 );
109 }
110 }
111
112 private void validateCollections(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
113 {
114 final String restDtoName = resolvedRestDto.qualifiedTypeName;
115 final Map<String, Field> collections = getCollections(restDtoClazz);
116
117 assertEquals(
118 resolvedRestDto.collections.size() + resolvedRestDto.maps.size(),
119 collections.size(),
120 parse("[{0}] must have {1} collections, but has {2}", restDtoName, resolvedRestDto.collections.size(), collections.size())
121 );
122
123 for (ResolvedRestDtoCollection coll : resolvedRestDto.collections.values())
124 {
125 final Field collection = collections.get(coll.propertyName);
126 assertNotNull(collection, parse("[{0}]: must have collection with name [{1}]", restDtoName, coll.propertyName));
127 if (collection == null)
128 {
129 continue; // no field to assert the type
130 }
131
132 // validate collection type
133 assertEquals(
134 collection.getType().getName(),
135 coll.qualifiedTypeName,
136 parse("[{0}]: collection type mismatch, expected [{1}] but is [{2}]", restDtoName, coll.qualifiedTypeName, collection.getType().getName())
137 );
138
139 // validate collection item type
140 final ParameterizedType collItemType = (ParameterizedType) collection.getGenericType();
141 final String collItemQualifiedType = collItemType.getActualTypeArguments()[0].getTypeName();
142 assertEquals(
143 collItemQualifiedType,
144 coll.elementsQualifiedRestDtoName,
145 parse("[{0}]: collection item type mismatch, expected [{1}] but is [{2}]", restDtoName, coll.elementsQualifiedRestDtoName, collItemQualifiedType)
146 );
147 }
148
149 // Now validate maps
150 for (ResolvedRestDtoMap map : resolvedRestDto.maps.values())
151 {
152 final Field mapField = collections.get(map.propertyName);
153 assertNotNull(mapField, parse("[{0}]: must have map with name [{1}]", restDtoName, map.propertyName));
154 if (mapField == null)
155 {
156 continue; // no field to assert the type
157 }
158
159 // validate collection type
160 assertEquals(
161 mapField.getType().getName(),
162 map.qualifiedTypeName,
163 parse("[{0}]: map type mismatch, expected [{1}] but is [{2}]", restDtoName, map.qualifiedTypeName, mapField.getType().getName())
164 );
165
166 // validate collection item type
167 final ParameterizedType mapTypes = (ParameterizedType) mapField.getGenericType();
168 // key
169 final String keyQualifiedType = mapTypes.getActualTypeArguments()[0].getTypeName();
170 assertEquals(
171 keyQualifiedType,
172 map.key.qualifiedTypeName,
173 parse("[{0}]: map.key type mismatch, expected [{1}] but is [{2}]", restDtoName, map.key.qualifiedTypeName, keyQualifiedType)
174 );
175 // value
176 final String valueQualifiedType = mapTypes.getActualTypeArguments()[1].getTypeName();
177 assertEquals(
178 valueQualifiedType,
179 map.value.qualifiedTypeName,
180 parse("[{0}]: map.value item type mismatch, expected [{1}] but is [{2}]", restDtoName, map.value.qualifiedTypeName, valueQualifiedType)
181 );
182 }
183 }
184
185 private void validateExplicitConstructor(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
186 {
187 final String restDtoName = resolvedRestDto.qualifiedTypeName;
188 final Map<String, ResolvedRestDtoProperty> parentProps = resolvedRestDto.parentCtorProperties;
189 final Map<String, ResolvedRestDtoCollection> parentColls = resolvedRestDto.parentCtorCollections;
190 final Map<String, ResolvedRestDtoProperty> props = resolvedRestDto.ctorProperties;
191 final Map<String, ResolvedRestDtoCollection> colls = resolvedRestDto.ctorCollections;
192 final int allProps = parentProps.size() + parentColls.size() + props.size() + colls.size();
193
194 for (Constructor<?> ctor : restDtoClazz.getConstructors())
195 {
196 if (ctor.getParameterCount() == 0)
197 {
198 continue; // default ctor
199 }
200
201 final Class<?>[] args = ctor.getParameterTypes();
202 assertEquals(
203 allProps,
204 args.length,
205 parse("[{0}], constructor arguments expected count [{1}], actual [{2}]", restDtoName, allProps, args.length)
206 );
207
208 if (allProps != args.length)
209 {
210 return; // incorrect count, skip further validation
211 }
212
213 int i = 0;
214
215 for (ResolvedRestDtoProperty prop : parentProps.values())
216 {
217 final Class<?> arg = args[i++];
218 assertEquals(
219 prop.qualifiedTypeName,
220 arg.getName(),
221 parse("[{0}], constructor argument type differs, expected [{1}], actual [{2}]", restDtoName, prop.qualifiedTypeName, arg.getName())
222 );
223 }
224
225 for (ResolvedRestDtoProperty prop : props.values())
226 {
227 final Class<?> arg = args[i++];
228 assertEquals(
229 prop.qualifiedTypeName,
230 arg.getName(),
231 parse("[{0}], constructor argument type differs, expected [{1}], actual [{2}]", restDtoName, prop.qualifiedTypeName, arg.getName())
232 );
233 }
234
235 for (ResolvedRestDtoCollection coll : parentColls.values())
236 {
237 final Class<?> arg = args[i++];
238 assertEquals(
239 coll.qualifiedTypeName,
240 arg.getName(),
241 parse("[{0}], constructor argument type differs, expected [{1}], actual [{2}]", restDtoName, coll.qualifiedTypeName, arg.getName())
242 );
243 }
244
245 for (ResolvedRestDtoCollection coll : colls.values())
246 {
247 final Class<?> arg = args[i++];
248 assertEquals(
249 coll.qualifiedTypeName,
250 arg.getName(),
251 parse("[{0}], constructor argument type differs, expected [{1}], actual [{2}]", restDtoName, coll.qualifiedTypeName, arg.getName())
252 );
253 }
254 }
255 }
256
257 private void validateClassAnnotations(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
258 {
259 validateJsonInclude(resolvedRestDto, restDtoClazz);
260 validateJsonIgnoreProperties(resolvedRestDto, restDtoClazz);
261 validateJsonAutoDetect(resolvedRestDto, restDtoClazz);
262 validateJsonTypeInfo(resolvedRestDto, restDtoClazz);
263 validateJsonSubTypes(resolvedRestDto, restDtoClazz);
264 validateJsonTypeName(resolvedRestDto, restDtoClazz);
265 }
266
267 private void validateJsonInclude(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
268 {
269 final JsonInclude jsJsonInclude = getClassAnnotation(restDtoClazz, JsonInclude.class);
270 if (!resolvedRestDto.resolvedBean.isEnumeration)
271 {
272 assertNotNull(jsJsonInclude, parse("[{0}] is missing JsonInclude annotation", resolvedRestDto.qualifiedTypeName));
273 }
274
275 if (jsJsonInclude != null)
276 {
277 JsonInclude.Include include = JsonInclude.Include.NON_EMPTY;
278 if (resolvedRestDto.restDtoJsonInclude != null)
279 {
280 include = JsonInclude.Include.valueOf(resolvedRestDto.restDtoJsonInclude);
281 }
282
283 assertEquals(
284 include,
285 jsJsonInclude.value(),
286 parse("[{0}] JsonInclude annotation isn't correct, expected [{0}], actual [{1}]",
287 resolvedRestDto.qualifiedTypeName,
288 include,
289 jsJsonInclude.value()
290 )
291 );
292 }
293 }
294
295 private void validateJsonIgnoreProperties(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
296 {
297 final JsonIgnoreProperties jsonIgnoreProperties = getClassAnnotation(restDtoClazz, JsonIgnoreProperties.class);
298 if (!resolvedRestDto.resolvedBean.isEnumeration)
299 {
300 assertNotNull(jsonIgnoreProperties, parse("[{0}] is missing JsonIgnoreProperties annotation", resolvedRestDto.qualifiedTypeName));
301 }
302
303 if (jsonIgnoreProperties != null)
304 {
305 assertEquals(
306 true,
307 jsonIgnoreProperties.ignoreUnknown(),
308 parse("[{0}] JsonIgnoreProperties annotation isn't correct, expected [true], actual [{1}]",
309 resolvedRestDto.qualifiedTypeName,
310 jsonIgnoreProperties.ignoreUnknown()
311 )
312 );
313 }
314 }
315
316 private void validateJsonAutoDetect(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
317 {
318 final JsonAutoDetect jsonAutoDetect = getClassAnnotation(restDtoClazz, JsonAutoDetect.class);
319 final String restDtoName = resolvedRestDto.qualifiedTypeName;
320
321 if (!resolvedRestDto.resolvedBean.isEnumeration)
322 {
323 assertNotNull(jsonAutoDetect, parse("[{0}] is missing JsonAutoDetect annotation", restDtoName));
324 }
325
326 if (jsonAutoDetect != null)
327 {
328 assertEquals(
329 JsonAutoDetect.Visibility.NONE,
330 jsonAutoDetect.fieldVisibility(),
331 parse("[{0}] JsonAutoDetect.fieldVisibility annotation isn't correct, expected [NONE], actual [{1}]", restDtoName, jsonAutoDetect.fieldVisibility())
332 );
333
334 assertEquals(
335 JsonAutoDetect.Visibility.PUBLIC_ONLY,
336 jsonAutoDetect.getterVisibility(),
337 parse("[{0}] JsonAutoDetect.getterVisibility annotation isn't correct, expected [PUBLIC_ONLY], actual [{1}]", restDtoName, jsonAutoDetect.getterVisibility())
338 );
339
340 assertEquals(
341 JsonAutoDetect.Visibility.PUBLIC_ONLY,
342 jsonAutoDetect.setterVisibility(),
343 parse("[{0}] JsonAutoDetect.setterVisibility annotation isn't correct, expected [PUBLIC_ONLY], actual [{1}]", restDtoName, jsonAutoDetect.getterVisibility())
344 );
345 }
346 }
347
348 private void validateJsonTypeInfo(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
349 {
350 final String restDtoName = resolvedRestDto.qualifiedTypeName;
351 final JsonTypeInfo jsonTypeInfo = getClassAnnotation(restDtoClazz, JsonTypeInfo.class);
352
353 if (!resolvedRestDto.restDtoGenerateTypeInfo)
354 {
355 assertNull(jsonTypeInfo, parse("[{0}] must not have JsonTypeInfo annotation", restDtoName));
356 }
357 else
358 {
359 assertNotNull(jsonTypeInfo, parse("[{0}] must have JsonTypeInfo annotation", restDtoName));
360 assertEquals(JsonTypeInfo.Id.NAME, jsonTypeInfo.use(), parse("[{0}] JsonTypeInfo.use must be [NAME]", restDtoName));
361 assertEquals(JsonTypeInfo.As.PROPERTY, jsonTypeInfo.include(), parse("[{0}] JsonTypeInfo.include must be [PROPERTY]", restDtoName));
362 assertEquals("@type", jsonTypeInfo.property(), parse("[{0}] JsonTypeInfo.property must be [@type]", restDtoName));
363 }
364 }
365
366 private void validateJsonSubTypes(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
367 {
368 final String restDtoName = resolvedRestDto.qualifiedTypeName;
369 final JsonSubTypes jsonSubTypes = getClassAnnotation(restDtoClazz, JsonSubTypes.class);
370
371 // Only if explicitly said to generate type info and this is a hierarchy
372 if (resolvedRestDto.restDtoGenerateTypeInfo && resolvedRestDto.isGroupTransformer)
373 {
374 assertNotNull(jsonSubTypes, parse("[{0}] must have JsonSubTypes because it has JsonTypeInfo annotation", restDtoName));
375
376 if (jsonSubTypes == null)
377 {
378 return;
379 }
380
381 final List<String> children = resolvedRestDto.resolvedBean.resolvedHierarchyDownWards
382 .stream()
383 .filter(bean -> !bean.isAbstract)
384 .map(child -> child.resolvedRestDto.qualifiedTypeName)
385 .collect(Collectors.toList());
386
387 assertEquals(children.size(), jsonSubTypes.value().length, parse("[{0}] must have {1} values, but were {2}", restDtoName, children.size(), jsonSubTypes.value().length));
388
389 if (children.size() != jsonSubTypes.value().length)
390 {
391 return;
392 }
393
394 for (JsonSubTypes.Type type : jsonSubTypes.value())
395 {
396 final String typeName = type.value().getName();
397 assertTrue(
398 children.contains(typeName),
399 parse("[{0}] JsonSubTypes.Type.value must contains {1}", restDtoName, typeName)
400 );
401 }
402 }
403 else
404 {
405 assertNull(jsonSubTypes, parse("[{0}] must not have JsonSubTypes because it has NO JsonTypeInfo annotation", restDtoName));
406 }
407 }
408
409 private void validateJsonTypeName(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
410 {
411 final JsonTypeName jsonTypeName = getClassAnnotation(restDtoClazz, JsonTypeName.class);
412
413 if (resolvedRestDto.restDtoTypeName == null)
414 {
415 assertNull(jsonTypeName, parse("[{0}] must NOT have JsonTypeName annotation", resolvedRestDto.qualifiedTypeName));
416 return;
417 }
418
419 assertNotNull(jsonTypeName, parse("[{0}] must have JsonTypeName annotation", resolvedRestDto.qualifiedTypeName));
420 assertEquals(
421 resolvedRestDto.restDtoTypeName,
422 jsonTypeName.value(),
423 parse("[{0}] JsonTypeName.value must be [{1}] but was [{2}]", resolvedRestDto.qualifiedTypeName, resolvedRestDto.restDtoTypeName, jsonTypeName.value())
424 );
425 }
426
427 private void validateMethodAnnotations(ResolvedRestDto resolvedRestDto, Class<?> restDtoClazz)
428 {
429 if (resolvedRestDto.resolvedBean.isEnumeration) {
430 return; // don't bother to validate enum methods annotations
431 }
432
433 final Map<String, Method> methods = getMethods(restDtoClazz);
434
435 for (Method method : methods.values())
436 {
437 final JsonProperty jsonProperty = getMethodAnnotations(method, JsonProperty.class);
438 assertNotNull(jsonProperty, parse("[{0}] method [{1}] must have JsonProperty annotation", resolvedRestDto.qualifiedTypeName, method.getName()));
439 if (jsonProperty != null)
440 {
441 assertNotNull(
442 jsonProperty.value(),
443 parse("[{0}] JsonProperty.value must be defined", resolvedRestDto.qualifiedTypeName)
444 );
445 }
446 }
447 }
448
449 @Override
450 protected boolean shouldRender(ResolvedBean resolvedBean)
451 {
452 // Taken from DtoTemplate, we validate DtoApi after all
453 return resolvedBean != null &&
454 !resolvedBean.isInterface &&
455 resolvedBean.resolvedRestDto != null &&
456 !resolvedBean.resolvedRestDto.isPregenerated;
457 }
458}