com.c4_soft.springaddons.openapi.SpringServletEnumModelConverter Maven / Gradle / Ivy
The newest version!
package com.c4_soft.springaddons.openapi;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springdoc.core.providers.ObjectMapperProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import lombok.RequiredArgsConstructor;
/**
*
* A Swagger {@link ModelConverter} to produce decent possible enum values in the OpenAPI spec of Spring applications.
*
* The values are generated differently depending on the enum being:
*
* - part of a {@link RequestBody @RequestBody} or {@link ResponseBody @ResponseBody}: use {@link HttpMessageConverter}
* - a {@link RequestParam @RequestParam}, {@link RequestHeader @RequestHeader}, {@link PathVariable @PathVariable},
* {@link MatrixVariable @MatrixVariable}and {@link CookieValue @CookieValue} use the {@link FormattingConversionService}. If none
* is found, use the enum name() (which is what the default converter does). If a custom converter is registered as a bean, then try to give
* it as input in the following order: the Jackson converter output, the value of toString() and enum name()
*
*
* @author ch4mp@c4-soft.com
* @see Spring doc
* for types conversion
* @see Spring doc for
* HTTP Message Conversion
*/
@RequiredArgsConstructor
public class SpringServletEnumModelConverter implements ModelConverter {
private final ApplicationContext applicationContext;
private final FormattingConversionService formattingConversionService;
private final ObjectMapperProvider springDocObjectMapper;
@SuppressWarnings("unchecked")
@Override
public Schema> resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) {
final var mapper = springDocObjectMapper.jsonMapper();
final var javaType = mapper.constructType(type.getType());
if (javaType == null || !javaType.isEnumType()) {
return chain.hasNext() ? chain.next().resolve(type, context, chain) : null;
}
final var enumClass = (Class>) javaType.getRawClass();
final var httpMessagePossibleWrittenValues = getHttpMessagePossibleWrittenValuesFor(enumClass);
final var formattedPossibleValues = Stream.of(enumClass.getEnumConstants()).map(e -> formattingConversionService.convert(e, String.class)).toList();
if (context.getDefinedModels().size() > 0 && httpMessagePossibleWrittenValues.size() > 0) {
// Case of an enum part of a @RequestBody or @ResponseBody: use HttpMessageConverter::write
return schemaOf(httpMessagePossibleWrittenValues);
}
// Case of an enum as @RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, and @CookieValue
// FormattingConversionService provides with converters working only one way and there is no guaranty that Converter and
// Converter are bijective
// So, to find the the possible inputs for Converter, the best we can do is trying the different possible collections of
// serialized values we have for an enum (using the formatter from FormattingConversionService or or the HttpMessageConverter) and select
// the 1st for which all values are successfully deserialized
if (httpMessagePossibleWrittenValues.size() > 0 && formattingConversionServiceAcceptsAll(httpMessagePossibleWrittenValues, enumClass)) {
return schemaOf(httpMessagePossibleWrittenValues);
}
// If the output of HttpMessageConverter can't be deserialized using FormattingConversionService, use the output of
// FormattingConversionService serialization
return schemaOf(formattedPossibleValues);
}
private Set getHttpMessagePossibleWrittenValuesFor(Class> enumClass) {
if (enumClass == null) {
return Set.of();
}
final var extractors = getWrittingExtractorsFor(enumClass).iterator();
if (!extractors.hasNext()) {
return Set.of();
}
final var firstExtractor = extractors.next();
final var possibleValues = firstExtractor.getValues(enumClass);
while (extractors.hasNext()) {
final var otherExtractor = extractors.next();
final var other = otherExtractor.getValues(enumClass);
if (!possibleValues.equals(other)) {
throw new RuntimeException(
"%s and %s provide with different possible values for enum %s (%s VS %s). Can't build OpenAPI spec. Please uniformize enums serilaization accross HttpMessageConverters."
.formatted(
firstExtractor.getClass().getName(),
otherExtractor.getClass().getName(),
enumClass.getName(),
possibleValues,
other));
}
}
return possibleValues;
}
@SuppressWarnings("unchecked")
private Stream> getConvertersFor(Class> enumClass) {
if (enumClass == null) {
return Stream.empty();
}
// @formatter:off
return Stream.of(applicationContext.getBeanNamesForType(ResolvableType.forClassWithGenerics(HttpMessageConverter.class, Object.class)))
.map(name -> (HttpMessageConverter
© 2015 - 2025 Weber Informatics LLC | Privacy Policy