Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -33,6 +35,13 @@ public <T> Set<Class<? extends T>> getSubTypesOf(@NonNull Class<T> clazz) {
.collect(Collectors.toSet());
}

public Set<Class<?>> getClassesAnnotatedWith(@NonNull Class<? extends Annotation> clazz) {
var result = scanResult.getClassesWithAnnotation(clazz);
return result.stream().map(
ClassInfo::loadClass
).collect(Collectors.toSet());
}

@Override
public void close() {
scanResult.close();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
}

Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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.<br>
Also supports multiple order fields by specifying the order query parameter multiple times.<br>
The format is as follows:<br><br>
<code>\\<property\\>[:direction][:nullHandling]</code><br>
<ul>
<li>
<code>property</code> is the field name you want to sort on the response Object<br>
</li><br>
<li>
<code>direction</code> is the optional sort direction (case insensitive)
<ul>
<br>
<li>
<code>asc</code> (default)
</li><br>
<li>
<code>desc</code>
</li>
</ul>
</li><br>
<li>
<code>nullHandling</code> is the optional null handling strategy (case insensitive)<br>
<ul>
<br>
<li>
<code>native</code> (default)
</li><br>
<li>
<code>first</code>
</li><br>
<li>
<code>last</code>
</li>
</ul>
</li>
</ul>
Examples:
<ul>
<li>
<code>typeFamily</code> (uses the default sort of asc and native null handling)
</li><br>
<li>
<code>typeFamily:desc</code> (sorts desc and uses the default native null handling)
</li><br>
<li>
<code>typeFamily:asc:last</code> (sorts asc and uses the null handling strategy last)
</li><br>
</ul
"""
);
var itemSchema = new StringSchema()._default("property:asc:last");
itemSchema.setDescription(
"{\"originalTypeFqn\": \"%s\"}".formatted(sortParameterSortFieldClass.getName())
);

parameter.setSchema(new ArraySchema().items(
itemSchema
));
}
}
}
}
}
}
}