Skip to content

Commit 8e21444

Browse files
Fixing Swagger/OpenAPI generator for inner classes. (#23779)
* Fixing swagger type names for inner classes. * Handling array types & additional properties. * Fixing up type for notification parameters. * Prefixing model name for response types too. * Adding test. * Extracting constants, cleaning up.
1 parent 4bd0927 commit 8e21444

File tree

3 files changed

+163
-56
lines changed

3 files changed

+163
-56
lines changed

graylog2-server/src/main/java/org/graylog2/shared/rest/documentation/generator/Generator.java

Lines changed: 110 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.lang.reflect.ParameterizedType;
6666
import java.lang.reflect.Type;
6767
import java.util.AbstractMap;
68+
import java.util.ArrayList;
6869
import java.util.Arrays;
6970
import java.util.Collection;
7071
import java.util.Collections;
@@ -74,7 +75,9 @@
7475
import java.util.Map;
7576
import java.util.Optional;
7677
import java.util.Set;
78+
import java.util.regex.Pattern;
7779
import java.util.stream.Collectors;
80+
import java.util.stream.Stream;
7881

7982
import static com.google.common.base.Strings.isNullOrEmpty;
8083
import static com.google.common.base.Strings.nullToEmpty;
@@ -87,13 +90,22 @@
8790
* and too big for what we want to do with it.
8891
*/
8992
public class Generator {
93+
private static final String INNER_CLASSES_SEPARATOR = "__";
9094

9195
private static final Logger LOG = LoggerFactory.getLogger(Generator.class);
9296

9397
public static final String EMULATED_SWAGGER_VERSION = "1.2";
9498
public static final String CLOUD_VISIBLE = "cloud";
9599

96100
private static final Map<String, Object> overviewResult = Maps.newHashMap();
101+
private static final String PROPERTIES = "properties";
102+
private static final String ADDITIONAL_PROPERTIES = "additional_properties";
103+
private static final String ITEMS = "items";
104+
private static final String REF = "$ref";
105+
private static final String TYPE = "type";
106+
private static final String GENERIC_CLASSES_SEPARATOR = "_";
107+
private static final String ROUTE_SEPARATOR = "/";
108+
private static final String PATH = "path";
97109

98110
private final Set<Class<?>> resourceClasses;
99111
private final Map<Class<?>, String> pluginMapping;
@@ -126,12 +138,12 @@ private String prefixedPath(Class<?> resourceClass, @Nullable String resourceAnn
126138

127139
if (pluginMapping.containsKey(resourceClass)) {
128140
prefixedPath.append(pluginPathPrefix)
129-
.append("/")
141+
.append(ROUTE_SEPARATOR)
130142
.append(pluginMapping.get(resourceClass));
131143
}
132144

133-
if (!resourcePath.startsWith("/")) {
134-
prefixedPath.append("/");
145+
if (!resourcePath.startsWith(ROUTE_SEPARATOR)) {
146+
prefixedPath.append(ROUTE_SEPARATOR);
135147
}
136148

137149
return prefixedPath.append(resourcePath).toString();
@@ -160,7 +172,7 @@ public synchronized Map<String, Object> generateOverview() {
160172

161173
final Map<String, Object> apiDescription = Maps.newHashMap();
162174
apiDescription.put("name", (prefixPlugins && prefixedPath.startsWith(pluginPathPrefix)) ? "Plugins/" + info.value() : info.value());
163-
apiDescription.put("path", prefixedPath);
175+
apiDescription.put(PATH, prefixedPath);
164176
apiDescription.put("description", info.description());
165177

166178
apis.add(apiDescription);
@@ -243,7 +255,7 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
243255
produces = method.getAnnotation(Produces.class);
244256
}
245257
}
246-
api.put("path", methodPath);
258+
api.put(PATH, methodPath);
247259

248260
Map<String, Object> operation = Maps.newHashMap();
249261
operation.put("method", determineHttpMethod(method));
@@ -262,13 +274,13 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
262274
if (responseType != null) {
263275
models.putAll(responseType.models());
264276
if (responseType.name() != null && isObjectSchema(responseType.type())) {
265-
operation.put("type", responseType.name());
277+
operation.put(TYPE, responseType.name());
266278
models.put(responseType.name(), responseType.type());
267279
} else {
268280
if (responseType.type() != null) {
269281
operation.putAll(responseType.type());
270282
} else {
271-
operation.put("type", responseType.name());
283+
operation.put(TYPE, responseType.name());
272284
}
273285
}
274286
}
@@ -296,12 +308,12 @@ public Map<String, Object> generateForRoute(String route, String basePath) {
296308
}
297309
}
298310

299-
if (basePath.endsWith("/")) {
311+
if (basePath.endsWith(ROUTE_SEPARATOR)) {
300312
basePath = basePath.substring(0, basePath.length() - 1);
301313
}
302314

303315
Collections.sort(apis, (o1, o2) -> ComparisonChain.start()
304-
.compare(o1.get("path").toString(), o2.get("path").toString())
316+
.compare(o1.get(PATH).toString(), o2.get(PATH).toString())
305317
.result());
306318

307319
// generate the json schema for the auto-mapped return types
@@ -401,15 +413,15 @@ private TypeSchema typeSchema(Type genericType) {
401413
final Map<String, Object> modelItemsDefinition;
402414
if (valueType instanceof Class && isPrimitive((Class<?>) valueType)) {
403415
valueName = mapPrimitives(((Class<?>) valueType).getSimpleName());
404-
modelItemsDefinition = Collections.singletonMap("additional_properties", valueName);
416+
modelItemsDefinition = Collections.singletonMap(ADDITIONAL_PROPERTIES, valueName);
405417
} else {
406418
final TypeSchema valueSchema = typeSchema(valueType);
407419
if (valueSchema == null) {
408420
return null;
409421
}
410422
valueName = valueSchema.name();
411423
models.putAll(valueSchema.models());
412-
modelItemsDefinition = Collections.singletonMap("additional_properties", Collections.singletonMap("$ref", valueName));
424+
modelItemsDefinition = Collections.singletonMap(ADDITIONAL_PROPERTIES, Collections.singletonMap(REF, valueName));
413425
if (valueSchema.type() != null) {
414426
models.put(valueName, valueSchema.type());
415427
}
@@ -418,13 +430,13 @@ private TypeSchema typeSchema(Type genericType) {
418430

419431
final String modelName = valueName + "Map";
420432
final Map<String, Object> model = ImmutableMap.<String, Object>builder()
421-
.put("type", "object")
433+
.put(TYPE, "object")
422434
.put("id", modelName)
423-
.put("properties", Collections.emptyMap())
435+
.put(PROPERTIES, Collections.emptyMap())
424436
.putAll(modelItemsDefinition)
425437
.build();
426438
models.put(modelName, model);
427-
return createTypeSchema(modelName, Collections.singletonMap("type", modelName), models);
439+
return createTypeSchema(modelName, Collections.singletonMap(TYPE, modelName), models);
428440
}
429441
if (returnType.isAssignableFrom(Optional.class)) {
430442
final Type valueType = typeParameters(genericType)[0];
@@ -437,7 +449,7 @@ private TypeSchema typeSchema(Type genericType) {
437449
final Map<String, Object> modelItemsDefinition;
438450
if (valueType instanceof Class && isPrimitive((Class<?>) valueType)) {
439451
valueName = mapPrimitives(((Class<?>) valueType).getSimpleName());
440-
modelItemsDefinition = Collections.singletonMap("items", valueName);
452+
modelItemsDefinition = Collections.singletonMap(ITEMS, valueName);
441453
} else {
442454
final TypeSchema valueSchema = typeSchema(valueType);
443455
if (valueSchema == null) {
@@ -449,17 +461,17 @@ private TypeSchema typeSchema(Type genericType) {
449461
}
450462
models.putAll(valueSchema.models());
451463
//final String valueModelId = (String)((Map<String, Object>)models.get(valueName)).get("id");
452-
modelItemsDefinition = Collections.singletonMap("items", Collections.singletonMap("$ref", valueName));
464+
modelItemsDefinition = Collections.singletonMap(ITEMS, Collections.singletonMap(REF, valueName));
453465
}
454466
final String modelName = valueName + "Array";
455467
final Map<String, Object> model = ImmutableMap.<String, Object>builder()
456-
.put("type", "array")
468+
.put(TYPE, "array")
457469
.put("id", modelName)
458-
.put("properties", Collections.emptyMap())
470+
.put(PROPERTIES, Collections.emptyMap())
459471
.putAll(modelItemsDefinition)
460472
.build();
461473
models.put(modelName, model);
462-
return createTypeSchema(modelName, Collections.singletonMap("type", modelName), models);
474+
return createTypeSchema(modelName, Collections.singletonMap(TYPE, modelName), models);
463475
}
464476

465477
final String modelName = uniqueModelName(genericType, returnType);
@@ -473,93 +485,141 @@ private TypeSchema typeSchema(Type genericType) {
473485
}
474486

475487
private String uniqueModelName(Type genericType, Class<?> returnType) {
476-
final var simpleName = returnType.getSimpleName();
488+
final var simpleName = nestedNames(returnType).collect(Collectors.joining(INNER_CLASSES_SEPARATOR));
477489
if (genericType instanceof ParameterizedType parameterizedType) {
478490
final var classNames = Arrays.stream(parameterizedType.getActualTypeArguments())
479491
.map(type -> uniqueModelName(type, classForType(type)))
480492
.toList();
481-
return simpleName + "_" + Joiner.on("_").join(classNames);
493+
return simpleName + GENERIC_CLASSES_SEPARATOR + Joiner.on(GENERIC_CLASSES_SEPARATOR).join(classNames);
482494
}
483495
return simpleName;
484496
}
485497

498+
private Stream<String> nestedNames(Class<?> returnType) {
499+
if (returnType.getEnclosingClass() == null) {
500+
return Stream.of(returnType.getSimpleName());
501+
}
502+
return Stream.concat(nestedNames(returnType.getEnclosingClass()), Stream.of(returnType.getSimpleName()));
503+
}
504+
486505
private TypeSchema extractInlineModels(Map<String, Object> genericTypeSchema) {
487506
if (isObjectSchema(genericTypeSchema)) {
488507
final Map<String, Object> newGenericTypeSchema = new HashMap<>(genericTypeSchema);
489508
final Map<String, Object> models = new HashMap<>();
490-
if (genericTypeSchema.get("properties") instanceof Map) {
491-
final Map<String, Object> properties = (Map<String, Object>) genericTypeSchema.get("properties");
509+
if (genericTypeSchema.get(PROPERTIES) instanceof Map) {
510+
final Map<String, Object> properties = (Map<String, Object>) genericTypeSchema.get(PROPERTIES);
492511
final Map<String, Object> newProperties = properties.entrySet().stream().map(entry -> {
493512
final Map<String, Object> property = (Map<String, Object>) entry.getValue();
494513
final TypeSchema propertySchema = extractInlineModels(property);
495514
models.putAll(propertySchema.models());
515+
final Map<String, Object> type = reuseTypeRef(propertySchema.type());
496516
if (propertySchema.name() == null) {
497-
return new AbstractMap.SimpleEntry<String, Object>(entry.getKey(), propertySchema.type());
517+
return new AbstractMap.SimpleEntry<>(entry.getKey(), type);
498518
}
499519
if (propertySchema.type() != null) {
500-
models.put(propertySchema.name(), propertySchema.type());
520+
models.put(propertySchema.name(), type);
501521
}
502-
return new AbstractMap.SimpleEntry<String, Object>(entry.getKey(), Collections.singletonMap("$ref", propertySchema.name()));
522+
return new AbstractMap.SimpleEntry<String, Object>(entry.getKey(), Collections.singletonMap(REF, propertySchema.name()));
503523
})
504524
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
505-
newGenericTypeSchema.put("properties", newProperties);
525+
newGenericTypeSchema.put(PROPERTIES, newProperties);
506526
}
507-
if (genericTypeSchema.get("additional_properties") instanceof Map) {
508-
final Map<String, Object> additionalProperties = (Map<String, Object>) genericTypeSchema.get("additional_properties");
527+
if (genericTypeSchema.get(ADDITIONAL_PROPERTIES) instanceof Map) {
528+
final Map<String, Object> additionalProperties = (Map<String, Object>) genericTypeSchema.get(ADDITIONAL_PROPERTIES);
509529
final TypeSchema itemsSchema = extractInlineModels(additionalProperties);
510530
models.putAll(itemsSchema.models());
511531
if (itemsSchema.name() != null) {
512532
if (itemsSchema.type() != null) {
513533
models.put(itemsSchema.name(), itemsSchema.type());
514534
}
515-
newGenericTypeSchema.put("additional_properties", Collections.singletonMap("$ref", itemsSchema.name()));
535+
newGenericTypeSchema.put(ADDITIONAL_PROPERTIES, Collections.singletonMap(REF, itemsSchema.name()));
516536
} else {
517537
if (itemsSchema.type() != null) {
518-
newGenericTypeSchema.put("additional_properties", itemsSchema.type());
538+
final Map<String, Object> type = reuseTypeRef(itemsSchema.type());
539+
newGenericTypeSchema.put(ADDITIONAL_PROPERTIES, itemsSchema.type());
519540
}
520541
}
521542
}
522543

523-
if (!genericTypeSchema.containsKey("properties")) {
524-
newGenericTypeSchema.put("properties", Collections.emptyMap());
544+
if (!genericTypeSchema.containsKey(PROPERTIES)) {
545+
newGenericTypeSchema.put(PROPERTIES, Collections.emptyMap());
525546
}
526-
final String id = shortenJsonSchemaURN((String) genericTypeSchema.get("id"));
547+
final String id = shortenJsonSchemaURNs((String) genericTypeSchema.get("id"));
527548
return createTypeSchema(id, newGenericTypeSchema, models);
528549
}
529550

530551
if (isArraySchema(genericTypeSchema)) {
531552
final Map<String, Object> models = new HashMap<>();
532553
final Map<String, Object> newGenericTypeSchema = new HashMap<>(genericTypeSchema);
533-
if (genericTypeSchema.get("items") instanceof Map) {
534-
final Map<String, Object> items = (Map<String, Object>) genericTypeSchema.get("items");
554+
if (genericTypeSchema.get(ITEMS) instanceof Map) {
555+
final Map<String, Object> items = (Map<String, Object>) genericTypeSchema.get(ITEMS);
535556
final TypeSchema itemsSchema = extractInlineModels(items);
536557
models.putAll(itemsSchema.models());
537558
if (itemsSchema.name() != null) {
538559
if (itemsSchema.type() != null) {
539560
models.put(itemsSchema.name(), itemsSchema.type());
540561
}
541-
newGenericTypeSchema.put("items", Collections.singletonMap("$ref", itemsSchema.name()));
562+
newGenericTypeSchema.put(ITEMS, Collections.singletonMap(REF, itemsSchema.name()));
563+
} else {
564+
final Map<String, Object> type = reuseTypeRef(itemsSchema.type());
565+
newGenericTypeSchema.put(ITEMS, type);
542566
}
543567
}
544568
return createTypeSchema(null, newGenericTypeSchema, models);
545569
}
546570
return createTypeSchema(null, genericTypeSchema, Collections.emptyMap());
547571
}
548572

573+
private Map<String, Object> reuseTypeRef(Map<String, Object> type) {
574+
if (type.get(REF) != null) {
575+
type.put(REF, shortenJsonSchemaURNs((String) type.get(REF)));
576+
}
577+
578+
return type;
579+
}
580+
581+
private static final Pattern IDENT =
582+
Pattern.compile("[A-Za-z_][A-Za-z0-9_$:]*");
583+
584+
private static final Set<String> KEYWORDS =
585+
Set.of("extends", "super");
586+
587+
private List<String> splitIfGeneric(String genericFqcn) {
588+
if (genericFqcn == null) {
589+
return null;
590+
}
591+
final List<String> result = new ArrayList<>();
592+
final var m = IDENT.matcher(genericFqcn);
593+
while (m.find()) {
594+
final var token = m.group();
595+
if (!KEYWORDS.contains(token) && !token.equals("?")) {
596+
result.add(token);
597+
}
598+
}
599+
return result;
600+
}
601+
602+
private String shortenJsonSchemaURNs(@Nullable String id) {
603+
final var genericParts = splitIfGeneric(id);
604+
return genericParts != null ? genericParts.stream().map(this::shortenJsonSchemaURN).collect(Collectors.joining(GENERIC_CLASSES_SEPARATOR)) : null;
605+
}
549606
private String shortenJsonSchemaURN(@Nullable String id) {
550607
if (id == null) {
551608
return null;
552609
}
553610
final Splitter splitter = Splitter.on(":");
554611
final List<String> segments = splitter.splitToList(id);
555-
return segments.size() > 0
556-
? segments.get(segments.size() - 1)
557-
: id;
612+
if (segments.isEmpty()) {
613+
return id;
614+
}
615+
return segments.stream()
616+
.filter(segment -> Character.isUpperCase(segment.codePointAt(0)))
617+
.collect(Collectors.joining(INNER_CLASSES_SEPARATOR));
558618
}
559619

560620
private static Optional<String> typeOfSchema(@Nullable Map<String, Object> typeSchema) {
561621
return Optional.ofNullable(typeSchema)
562-
.map(schema -> Strings.emptyToNull((String) schema.get("type")));
622+
.map(schema -> Strings.emptyToNull((String) schema.get(TYPE)));
563623
}
564624

565625
private static boolean isArraySchema(Map<String, Object> genericTypeSchema) {
@@ -576,13 +636,13 @@ private Map<String, Object> schemaForType(Type valueType) {
576636
try {
577637
final JsonSchema schema = schemaGenerator.generateSchema(mapper.getTypeFactory().constructType(valueType));
578638
final Map<String, Object> schemaMap = mapper.readValue(mapper.writeValueAsBytes(schema), Map.class);
579-
if (schemaMap.containsKey("additional_properties") && !schemaMap.containsKey("properties")) {
580-
schemaMap.put("properties", Collections.emptyMap());
639+
if (schemaMap.containsKey(ADDITIONAL_PROPERTIES) && !schemaMap.containsKey(PROPERTIES)) {
640+
schemaMap.put(PROPERTIES, Collections.emptyMap());
581641
}
582-
if (schemaMap.equals(Collections.singletonMap("type", "any"))) {
642+
if (schemaMap.equals(Collections.singletonMap(TYPE, "any"))) {
583643
return ImmutableMap.of(
584-
"type", "object",
585-
"properties", Collections.emptyMap()
644+
TYPE, "object",
645+
PROPERTIES, Collections.emptyMap()
586646
);
587647
}
588648
return schemaMap;
@@ -706,11 +766,11 @@ private List<Map<String, Object>> determineResponses(Method method) {
706766

707767
// Leading slash but no trailing.
708768
private String cleanRoute(String route) {
709-
if (!route.startsWith("/")) {
710-
route = "/" + route;
769+
if (!route.startsWith(ROUTE_SEPARATOR)) {
770+
route = ROUTE_SEPARATOR + route;
711771
}
712772

713-
if (route.endsWith("/")) {
773+
if (route.endsWith(ROUTE_SEPARATOR)) {
714774
route = route.substring(0, route.length() - 1);
715775
}
716776

@@ -876,7 +936,7 @@ public Map<String, Object> jsonValue() {
876936
}
877937

878938
if (typeSchema.type() == null || isObjectSchema(typeSchema.type())) {
879-
result.put("type", typeSchema.name());
939+
result.put(TYPE, typeSchema.name());
880940
} else {
881941
result.putAll(typeSchema.type());
882942
}

0 commit comments

Comments
 (0)