DefaultOpenApiConfiguration.java

package org.imageconverter.config;

import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;

/**
 * @author Fernando Romulo da Silva
 *
 */
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
public class DefaultOpenApiConfiguration {

    @Value("${spring.application.name:Please set the project's name ('spring.application.name')}")
    private String appName;

    @Value("${spring.application.description:Please set the project's name ('spring.application.description')}")
    private String appDesciption;

    public OpenAPI openAPI() {
	final var appVersion = getClass().getPackage().getImplementationVersion();
	final var appVendor = getClass().getPackage().getSpecificationVendor();

	return new OpenAPI() //
			.info(new Info() //
					.title(appName) //
					.version(appVersion + " " + appVendor) //
					.description(appDesciption) //
					.contact(new Contact().name("Fernando Romulo da Silva")).termsOfService("http://swagger.io/terms/") //
					.license(new License().name("Apache 2.0").url("http://springdoc.org")) //
			);
    }

//    @Bean
//    public GroupedOpenApi internalGroupedOpenApi(OpenApiCustomiser apiFromResourceCustomizer) {
//        return GroupedOpenApi.builder()
//                .setGroup("testGroup")
//                .pathsToMatch("/nothingreally")
//                .addOpenApiCustomiser(apiFromResourceCustomizer)
//                .build();
//    }

    @Bean
    OpenApiCustomiser defaultOpenApiCustomiser() {
	return openApiCustomiser -> {

	    openApiCustomiser.getPaths().values().forEach(valuePath -> {

		final var getOperation = valuePath.getGet();
		if (Objects.nonNull(getOperation)) {
		    createResponse(getOperation, RequestMethod.GET);
		}

		final var postOperation = valuePath.getPost();
		if (Objects.nonNull(postOperation)) {
		    createResponse(postOperation, RequestMethod.POST);
		}

		final var putOperation = valuePath.getPut();
		if (Objects.nonNull(putOperation)) {
		    createResponse(putOperation, RequestMethod.PUT);
		}

		final var deleteOperation = valuePath.getDelete();
		if (Objects.nonNull(deleteOperation)) {
		    createResponse(deleteOperation, RequestMethod.DELETE);
		}

		final var headOperation = valuePath.getHead();
		if (Objects.nonNull(headOperation)) {
		    createResponse(headOperation, RequestMethod.HEAD);
		}

		final var optionOperation = valuePath.getOptions();
		if (Objects.nonNull(optionOperation)) {
		    createResponse(optionOperation, RequestMethod.OPTIONS);
		}
	    });

	    // openapi spring boot programmatically example
//	    final var mySchema = new ObjectSchema();
//	    mySchema.name("MySchema");
//	    mySchema.addExample("""
//	    		{
//	    		  "timestamp": "2021-07-19T15:25:32.389836763",
//	    		  "status": 500,
//	    		  "error": "Internal Server Error",
//	    		  "message": "Unexpected error. Please, check the log with traceId and spanId for more detail",
//	    		  "traceId": "3d4144eeb01e3682",
//	    		  "spanId": "3d4144eeb01e3682"
//	    		}""");
//
//	    final var schemas = openApiCustomiser.getComponents().getSchemas();
//	    schemas.put(mySchema.getName(), mySchema);
	};
    }

    private void createResponse(final Operation operation, final RequestMethod requestMethod) {
	final var apiResponses = operation.getResponses();

	final var ex500 = """
			{
			    "timestamp": "1669564355551",
			    "status": 500,
			    "error": "Internal Server Error",
			    "message": "Unexpected error. Please, check the log with traceId and spanId for more detail",
			    "traceId": "3d4144eeb01e3682",
			    "spanId": "3d4144eeb01e3682"
			}""";

	final var ex401 = """
			{
			    "timestamp": 1669564355551,
			    "status": 401,
			    "error": "Unauthorized",
			    "message": "Unauthorized",
			    "path": "/rest/images/type/2"
			}""";

	final var ex403 = """
			{
			    "timestamp": 1669563774179,
			    "status": 403,
			    "error": "Forbidden",
			    "message": "Forbidden",
			    "path": "/rest/images/type"
			}""";

	final var httpStatusMap = Map.of( //
			INTERNAL_SERVER_ERROR, ex500, //
			UNAUTHORIZED, ex401, //
			FORBIDDEN, ex403 //
	);

	for (final var httpStatusEntrySet : httpStatusMap.entrySet()) {

	    final var httpStatus = httpStatusEntrySet.getKey();
	    final var httpStatusExample = httpStatusEntrySet.getValue();

	    apiResponses.computeIfAbsent( //
			    String.valueOf(httpStatus.value()), //
			    s -> {
				final var mediaType = new MediaType();
				mediaType.setExample(httpStatusExample);

				final var content = new Content();
				content.addMediaType(APPLICATION_JSON_VALUE, mediaType);
				
				return new ApiResponse() //
						.description(httpStatus.getReasonPhrase()) //
						.content(content);
			    });
	}
    }

//    private PathItem addExamples(PathItem pathItem) {
//	    if(pathItem.getPost() !=null)  {
//	        //Note you can also Do this to APIResponses to insert info from a file into examples in say, a 200 response.
//	            pathItem.getPost().getRequestBody().getContent().values().stream()
//	                    .forEach(c ->{
//	                        String fileName = c.getExample().toString().replaceFirst("@","");
//	                        ObjectNode node = null;
//	                        try {
//	                            //load file from where you want. also don't insert is as a string, it wont format properly
//	                            node = (ObjectNode) new ObjectMapper().readTree(methodToReadInFileToString(fileName)); 
//	                        } catch (JsonProcessingException e) {
//	                            throw new RuntimeException(e);
//	                        }
//	                        c.setExample(node);
//	                    }
//	            );
//	    }
//	    return pathItem;
//	}

    @Bean
    OperationCustomizer operationCustomizer() {
	return (operation, handleMethod) -> {

	    if (StringUtils.isNotBlank(operation.getDescription()) || StringUtils.isNotBlank(operation.getSummary())) {
		return operation;
	    }

	    final var method = handleMethod.getMethod();

	    final var classEntity = getClassRequestMapping(method);

	    final var listAnnotationTypes = Stream.of(method.getAnnotations()) //
			    .map(a -> a.annotationType()) //
			    .toList();

	    if (listAnnotationTypes.contains(GetMapping.class)) {

		final var methodEntity = getMethodRequestMapping(method, GetMapping.class);

		operation.description("Get " + classEntity + methodEntity);
		operation.operationId("getItem");
		operation.summary("Get items bla bla");
		operation.addExtension("x-operationWeight", "200");

		return operation;
	    }

	    if (listAnnotationTypes.contains(PostMapping.class)) {

		final var methodEntity = getMethodRequestMapping(method, GetMapping.class);

		operation.description("Create " + classEntity + methodEntity);
		operation.operationId("createItem");
		operation.summary("Create items bla bla");
		operation.addExtension("x-operationWeight", "300");

		return operation;
	    }

	    if (listAnnotationTypes.contains(PutMapping.class)) {

		final var methodEntity = getMethodRequestMapping(method, PutMapping.class);

		operation.description("Update " + classEntity + methodEntity);
		operation.operationId("updateItem");
		operation.summary("Update items bla bla");
		operation.addExtension("x-operationWeight", "400");

		return operation;
	    }

	    if (listAnnotationTypes.contains(DeleteMapping.class)) {

		final var methodEntity = getMethodRequestMapping(method, PutMapping.class);

		operation.description("Delete " + classEntity + methodEntity);
		operation.operationId("deleteItem");
		operation.summary("Delete items bla bla");
		operation.addExtension("x-operationWeight", "500");

		return operation;
	    }

	    return operation;
	};
    }

    private String getClassRequestMapping(final Method method) {

	final var requestMappingAnnotationClass = Stream.of(method.getDeclaringClass().getAnnotations()) //
			.filter(p -> Objects.equals(p.annotationType(), RequestMapping.class)) //
			.findFirst() //
			.orElse(null);

	if (Objects.nonNull(requestMappingAnnotationClass)) {

	    final var values = (String[]) AnnotationUtils.getValue(requestMappingAnnotationClass, "value");

	    if (!ArrayUtils.isEmpty(values)) {

		return Stream.of(StringUtils.split(values[0], "/")) //
				.filter(s -> !StringUtils.containsAny(s, "{}")) //
				.collect(Collectors.joining("'s"));
	    }
	}

	return StringUtils.EMPTY;
    }

    private String getMethodRequestMapping(final Method method, final Class<? extends Annotation> clazz) {

	final var requestMappingAnnotationClass = Stream.of(method.getDeclaringClass().getAnnotations()) //
			.filter(p -> Objects.equals(p.annotationType(), clazz)) //
			.findFirst() //
			.orElse(null);

	if (Objects.nonNull(requestMappingAnnotationClass)) {

	    final var values = (String[]) AnnotationUtils.getValue(requestMappingAnnotationClass, "value");

	    if (!ArrayUtils.isEmpty(values)) {

		return Stream.of(StringUtils.split(values[0], "/")) //
				.filter(s -> !StringUtils.containsAny(s, "{}")) //
				.reduce((first, second) -> second) //
				.map(m -> "'s " + m) //
				.orElse(StringUtils.EMPTY);
	    }
	}

	return StringUtils.EMPTY;
    }
}