From 0db5e5b7eea25119de16f0122946cc510bb89aaf Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 17 Sep 2025 15:39:09 +0200 Subject: [PATCH] add more common swagger code --- .../reflection/util/ClassScannerUtil.java | 9 ++ .../annotation/ForceSwaggerSchema.java | 15 +++ .../force_schema/ForceSchemaCustomizer.java | 50 +++++++++ .../swagger/resolver/CustomModelResolver.java | 18 ++++ .../SortParameterCustomizer.java | 100 ++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/annotation/ForceSwaggerSchema.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/force_schema/ForceSchemaCustomizer.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/resolver/CustomModelResolver.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/sort_parameter/SortParameterCustomizer.java diff --git a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java index 7bfa48c..8f8f52e 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/reflection/util/ClassScannerUtil.java @@ -1,9 +1,11 @@ package it.aboutbits.springboot.toolbox.reflection.util; import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; import io.github.classgraph.ScanResult; import lombok.NonNull; +import java.lang.annotation.Annotation; import java.util.Set; import java.util.stream.Collectors; @@ -33,6 +35,13 @@ public Set> getSubTypesOf(@NonNull Class clazz) { .collect(Collectors.toSet()); } + public Set> getClassesAnnotatedWith(@NonNull Class clazz) { + var result = scanResult.getClassesWithAnnotation(clazz); + return result.stream().map( + ClassInfo::loadClass + ).collect(Collectors.toSet()); + } + @Override public void close() { scanResult.close(); diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotation/ForceSwaggerSchema.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotation/ForceSwaggerSchema.java new file mode 100644 index 0000000..0688108 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotation/ForceSwaggerSchema.java @@ -0,0 +1,15 @@ +package it.aboutbits.springboot.toolbox.swagger.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark classes that should be included in Swagger schema even if unused. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ForceSwaggerSchema { + +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/force_schema/ForceSchemaCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/force_schema/ForceSchemaCustomizer.java new file mode 100644 index 0000000..38e5c83 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/force_schema/ForceSchemaCustomizer.java @@ -0,0 +1,50 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.force_schema; + +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil; +import it.aboutbits.springboot.toolbox.swagger.annotation.ForceSwaggerSchema; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.LinkedHashMap; + +@RequiredArgsConstructor +public class ForceSchemaCustomizer implements OpenApiCustomizer { + private final ModelResolver modelResolver; + private final ClassScannerUtil.ClassScanner classScanner; + + @Override + public void customise(OpenAPI openApi) { + addAnnotatedSchemas(openApi); + } + + private void addAnnotatedSchemas(OpenAPI openAPI) { + if (openAPI.getComponents() == null) { + openAPI.setComponents(new Components()); + } + if (openAPI.getComponents().getSchemas() == null) { + openAPI.getComponents().setSchemas(new LinkedHashMap<>()); + } + + // Create a custom ModelConverters instance with the same configuration + var customModelConverters = new ModelConverters(); + customModelConverters.addConverter(modelResolver); + + // Scan for classes with @ForceSwaggerSchema annotation + var annotatedClasses = classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class); + + for (var clazz : annotatedClasses) { + var schemas = customModelConverters.read(clazz); + // Only add if not already present (to avoid overriding naturally discovered schemas) + schemas.forEach((key, schema) -> { + if (!openAPI.getComponents().getSchemas().containsKey(key)) { + openAPI.getComponents().getSchemas().put(key, schema); + } + }); + } + } +} + diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/resolver/CustomModelResolver.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/resolver/CustomModelResolver.java new file mode 100644 index 0000000..e6808a6 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/resolver/CustomModelResolver.java @@ -0,0 +1,18 @@ +package it.aboutbits.springboot.toolbox.swagger.resolver; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.core.jackson.TypeNameResolver; + +public class CustomModelResolver extends ModelResolver { + public CustomModelResolver(ObjectMapper mapper) { + super(mapper, new CustomTypeNameResolver()); + } + + public static class CustomTypeNameResolver extends TypeNameResolver { + @Override + protected String getNameOfClass(Class cls) { + return cls.getName(); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/sort_parameter/SortParameterCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/sort_parameter/SortParameterCustomizer.java new file mode 100644 index 0000000..fe5a85a --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/sort_parameter/SortParameterCustomizer.java @@ -0,0 +1,100 @@ +package it.aboutbits.springboot.toolbox.swagger.sort_parameter; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.StringSchema; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.customizers.OpenApiCustomizer; + +@RequiredArgsConstructor +public class SortParameterCustomizer implements OpenApiCustomizer { + private final Class sortParameterSortFieldClass; + + @Override + public void customise(OpenAPI openApi) { + if (openApi.getPaths() != null) { + for (var path : openApi.getPaths().entrySet()) { + var pathItemOperations = path.getValue().readOperations(); + if (pathItemOperations == null) { + continue; + } + + for (var operation : pathItemOperations) { + if (operation.getParameters() == null) { + continue; + } + + for (var parameter : operation.getParameters()) { + if (parameter.getSchema() == null || parameter.getSchema().get$ref() == null) { + continue; + } + + if (parameter.getSchema().get$ref().endsWith(".SortParameter")) { + parameter.required(false); + parameter.description( + """ + Defines the sort order and if left empty the implementation specific default order will be used.
+ Also supports multiple order fields by specifying the order query parameter multiple times.
+ The format is as follows:

+ \\[:direction][:nullHandling]
+
    +
  • + property is the field name you want to sort on the response Object
    +

  • +
  • + direction is the optional sort direction (case insensitive) +
      +
      +
    • + asc (default) +

    • +
    • + desc +
    • +
    +

  • +
  • + nullHandling is the optional null handling strategy (case insensitive)
    +
      +
      +
    • + native (default) +

    • +
    • + first +

    • +
    • + last +
    • +
    +
  • +
+ Examples: +
    +
  • + typeFamily (uses the default sort of asc and native null handling) +

  • +
  • + typeFamily:desc (sorts desc and uses the default native null handling) +

  • +
  • + typeFamily:asc:last (sorts asc and uses the null handling strategy last) +

  • +