From c5dd449b14b606443f37b8ff9fe72bb9a9739573 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Fri, 27 Sep 2024 15:54:14 +0200 Subject: [PATCH 1/2] add core response objects --- .../toolbox/mvc/response/ErrorResponse.java | 13 +++++++++ .../toolbox/mvc/response/ItemResponse.java | 6 ++++ .../toolbox/mvc/response/ListResponse.java | 8 +++++ .../toolbox/mvc/response/PagedResponse.java | 29 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ErrorResponse.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ItemResponse.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ListResponse.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/mvc/response/PagedResponse.java diff --git a/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ErrorResponse.java b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ErrorResponse.java new file mode 100644 index 0000000..74529ec --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ErrorResponse.java @@ -0,0 +1,13 @@ +package it.aboutbits.springboot.toolbox.mvc.response; + +import org.springframework.lang.Nullable; + +import java.util.List; +import java.util.Map; + +public record ErrorResponse( + @Nullable String message, + @Nullable Map> errors +) { + +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ItemResponse.java b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ItemResponse.java new file mode 100644 index 0000000..71ba711 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ItemResponse.java @@ -0,0 +1,6 @@ +package it.aboutbits.springboot.toolbox.mvc.response; + +import lombok.NonNull; + +public record ItemResponse(@NonNull T data) { +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ListResponse.java b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ListResponse.java new file mode 100644 index 0000000..52face6 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/ListResponse.java @@ -0,0 +1,8 @@ +package it.aboutbits.springboot.toolbox.mvc.response; + +import lombok.NonNull; + +import java.util.List; + +public record ListResponse(@NonNull List data) { +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/PagedResponse.java b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/PagedResponse.java new file mode 100644 index 0000000..c8f122f --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/mvc/response/PagedResponse.java @@ -0,0 +1,29 @@ +package it.aboutbits.springboot.toolbox.mvc.response; + +import lombok.NonNull; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record PagedResponse( + @NonNull + List data, + @NonNull + MetaWithPagination meta +) { + + public record MetaWithPagination( + @NonNull + Pagination pagination + ) { + public record Pagination(long page, int size, long totalElements) { + public static Pagination of(@NonNull Page page) { + return new Pagination( + page.getNumber(), + page.getSize(), + page.getTotalElements() + ); + } + } + } +} From af7de53cc579ab8c9712d92f1f5cde2db12132e5 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Fri, 27 Sep 2024 15:54:25 +0200 Subject: [PATCH 2/2] add swagger tools --- pom.xml | 5 ++ .../RegisterCustomTypesWithSwagger.java | 4 +- .../annotations/SwaggerScopedAuth.java | 12 ++++ .../OrderModelsCustomizer.java | 15 +++++ .../AuthorizationDescriptor.java | 50 ++++++++++++++ .../default_not_null/NullableCustomizer.java | 24 +++++++ .../NullablePropertyCustomizer.java | 39 +++++++++++ .../error_response/ErrorCustomizer.java | 66 +++++++++++++++++++ .../logout_route/LogoutCustomizer.java | 26 ++++++++ .../{ => type}/CustomTypeModelConverter.java | 2 +- .../CustomTypePropertyCustomizer.java | 12 ++-- 11 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java create mode 100644 src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java rename src/main/java/it/aboutbits/springboot/toolbox/swagger/{ => type}/CustomTypeModelConverter.java (97%) rename src/main/java/it/aboutbits/springboot/toolbox/swagger/{ => type}/CustomTypePropertyCustomizer.java (91%) diff --git a/pom.xml b/pom.xml index e3ef8a0..776fd98 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,11 @@ spring-boot-starter-json + + org.springframework.security + spring-security-core + + org.projectlombok diff --git a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java index ed99c70..cb2cc8e 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/autoconfiguration/swagger/RegisterCustomTypesWithSwagger.java @@ -1,7 +1,7 @@ package it.aboutbits.springboot.toolbox.autoconfiguration.swagger; -import it.aboutbits.springboot.toolbox.swagger.CustomTypeModelConverter; -import it.aboutbits.springboot.toolbox.swagger.CustomTypePropertyCustomizer; +import it.aboutbits.springboot.toolbox.swagger.type.CustomTypeModelConverter; +import it.aboutbits.springboot.toolbox.swagger.type.CustomTypePropertyCustomizer; import org.springframework.context.annotation.Import; import java.lang.annotation.ElementType; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java new file mode 100644 index 0000000..cdf039c --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/annotations/SwaggerScopedAuth.java @@ -0,0 +1,12 @@ +package it.aboutbits.springboot.toolbox.swagger.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SwaggerScopedAuth { + String value(); +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java new file mode 100644 index 0000000..79d9006 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/alphabetical_model_order/OrderModelsCustomizer.java @@ -0,0 +1,15 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.alphabetical_model_order; + +import io.swagger.v3.oas.models.OpenAPI; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.TreeMap; + +public class OrderModelsCustomizer implements OpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + var components = openApi.getComponents(); + + components.schemas(new TreeMap<>(components.getSchemas())); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java new file mode 100644 index 0000000..48c548e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/authorization_docs/AuthorizationDescriptor.java @@ -0,0 +1,50 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.authorization_docs; + +import io.swagger.v3.oas.models.Operation; +import it.aboutbits.springboot.toolbox.swagger.annotations.SwaggerScopedAuth; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.method.HandlerMethod; + +import java.util.ArrayList; +import java.util.Optional; + +@Slf4j +public class AuthorizationDescriptor implements OperationCustomizer { + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + try { + var additionalDescription = new ArrayList(); + + var maybeAnnotation = Optional.ofNullable(handlerMethod.getMethodAnnotation(PreAuthorize.class)); + if (maybeAnnotation.isPresent()) { + var annotation = maybeAnnotation.get(); + additionalDescription.add("Authorization: " + annotation.value()); + } + + var maybeAnnotation2 = Optional.ofNullable(handlerMethod.getMethodAnnotation(SwaggerScopedAuth.class)); + if (maybeAnnotation2.isPresent()) { + var annotation = maybeAnnotation2.get(); + additionalDescription.add("Scoped Authorization: " + annotation.value()); + } + + if (!additionalDescription.isEmpty()) { + + var currentDescription = Optional.ofNullable(operation.getDescription()); + + var description = String.join("
", additionalDescription); + if (currentDescription.isPresent()) { + description = "

" + description + "

" + currentDescription.get(); + } + + operation.description( + description + ); + } + } catch (Exception e) { + log.error("Error when creating swagger documentation for authorities.", e); + } + return operation; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java new file mode 100644 index 0000000..41413a2 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullableCustomizer.java @@ -0,0 +1,24 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.ArrayList; + +public class NullableCustomizer implements OpenApiCustomizer { + @Override + @SuppressWarnings("unchecked") + public void customise(OpenAPI openApi) { + openApi.getComponents().getSchemas().values() + .forEach(schema -> { + var requiredProperties = new ArrayList(); + ((Schema) schema).getProperties().forEach((propertyName, property) -> { + if (property.getNullable() == null || !property.getNullable()) { + requiredProperties.add(propertyName); + } + }); + schema.setRequired(requiredProperties); + }); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java new file mode 100644 index 0000000..a8fdf57 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/default_not_null/NullablePropertyCustomizer.java @@ -0,0 +1,39 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.default_not_null; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.models.media.Schema; +import org.springdoc.core.customizers.PropertyCustomizer; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; + +@Component +public class NullablePropertyCustomizer implements PropertyCustomizer { + static { + /* + We need this because the ModelResolver will only process these whitelisted annotations. + We will then be able to manipulate each property based on the set annotations. + */ + + var list = new ArrayList<>(ModelResolver.NOT_NULL_ANNOTATIONS); + list.add("Nullable"); + + ModelResolver.NOT_NULL_ANNOTATIONS = list; + } + + @Override + public Schema customize(Schema property, AnnotatedType annotatedType) { + /* + Mark the nullable ones as nullable. + */ + + if (annotatedType.getCtxAnnotations() != null && Arrays.stream(annotatedType.getCtxAnnotations()) + .anyMatch(a -> "Nullable".equals(a.annotationType().getSimpleName()))) { + property.setNullable(true); + } + + return property; + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java new file mode 100644 index 0000000..798d0e8 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/error_response/ErrorCustomizer.java @@ -0,0 +1,66 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.error_response; + +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import it.aboutbits.springboot.toolbox.mvc.response.ErrorResponse; +import org.springdoc.core.customizers.OpenApiCustomizer; + +import java.util.Map; + +public class ErrorCustomizer implements OpenApiCustomizer { + @Override + public void customise(OpenAPI openApi) { + openApi.getComponents() + .getSchemas() + .putAll( + ModelConverters.getInstance().read(ErrorResponse.class) + ); + + var errorResponseSchema = openApi.getComponents().getSchemas().get("ErrorResponse"); + @SuppressWarnings("unchecked") + Map> props = errorResponseSchema.getProperties(); + for (var prop : props.values()) { + prop.nullable(true); + } + + openApi.getPaths() + .values() + .forEach( + pathItem -> pathItem.readOperations() + .forEach( + operation -> { + ApiResponses apiResponses = operation.getResponses(); + apiResponses.addApiResponse( + "400", + createApiResponse( + "Bad Request", + errorResponseSchema + ) + ); + apiResponses.addApiResponse( + "404", + createApiResponse( + "Not Found", + errorResponseSchema + ) + ); + } + ) + ); + } + + private ApiResponse createApiResponse(String message, Schema schema) { + var mediaType = new MediaType(); + mediaType.schema(schema); + return new ApiResponse().description(message) + .content(new Content().addMediaType( + org.springframework.http.MediaType.APPLICATION_JSON_VALUE, + mediaType + )); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java new file mode 100644 index 0000000..3bf3ee5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/logout_route/LogoutCustomizer.java @@ -0,0 +1,26 @@ +package it.aboutbits.springboot.toolbox.swagger.customization.logout_route; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.customizers.OpenApiCustomizer; + +@RequiredArgsConstructor +public class LogoutCustomizer implements OpenApiCustomizer { + private final String logoutUrl; + + @Override + public void customise(OpenAPI openApi) { + var operation = new Operation(); + operation.addTagsItem("Authentication API"); + operation.summary("Logout the current user"); + operation.responses(new ApiResponses()); + + var pathItem = new PathItem(); + pathItem.setPost(operation); + + openApi.getPaths().addPathItem(logoutUrl, pathItem); + } +} diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java similarity index 97% rename from src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java rename to src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java index 803937f..f0aa8cf 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypeModelConverter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypeModelConverter.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.swagger; +package it.aboutbits.springboot.toolbox.swagger.type; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; diff --git a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java similarity index 91% rename from src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java rename to src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java index fd3f81e..810f906 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/swagger/CustomTypePropertyCustomizer.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/swagger/type/CustomTypePropertyCustomizer.java @@ -1,4 +1,4 @@ -package it.aboutbits.springboot.toolbox.swagger; +package it.aboutbits.springboot.toolbox.swagger.type; import com.fasterxml.jackson.databind.type.SimpleType; import io.swagger.v3.core.converter.AnnotatedType; @@ -25,13 +25,17 @@ public Schema customize(Schema property, AnnotatedType annotatedType) { var displayName = rawClass.getSimpleName(); + Class wrappedType; if (EntityId.class.isAssignableFrom(rawClass)) { displayName = resolveEntityIdDisplayName(rawClass); } - var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass); - var wrappedType = constructor.getParameters()[0].getType(); - + if (rawClass.equals(EntityId.class)) { + wrappedType = simpleType.getBindings().getBoundType(0).getRawClass(); + } else { + var constructor = RecordReflectionUtil.getCanonicalConstructor(rawClass); + wrappedType = constructor.getParameters()[0].getType(); + } if (Short.class.isAssignableFrom(wrappedType)) { property.type("integer");