View Javadoc
1   package org.imageconverter.config;
2   
3   import static org.springframework.http.HttpStatus.FORBIDDEN;
4   import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
5   import static org.springframework.http.HttpStatus.UNAUTHORIZED;
6   import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
7   
8   import java.lang.annotation.Annotation;
9   import java.lang.reflect.Method;
10  import java.util.Map;
11  import java.util.Objects;
12  import java.util.stream.Collectors;
13  import java.util.stream.Stream;
14  
15  import org.apache.commons.lang3.ArrayUtils;
16  import org.apache.commons.lang3.StringUtils;
17  import org.springdoc.core.customizers.OpenApiCustomiser;
18  import org.springdoc.core.customizers.OperationCustomizer;
19  import org.springframework.beans.factory.annotation.Value;
20  import org.springframework.context.annotation.Bean;
21  import org.springframework.context.annotation.Configuration;
22  import org.springframework.core.annotation.AnnotationUtils;
23  import org.springframework.hateoas.config.EnableHypermediaSupport;
24  import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
25  import org.springframework.web.bind.annotation.DeleteMapping;
26  import org.springframework.web.bind.annotation.GetMapping;
27  import org.springframework.web.bind.annotation.PostMapping;
28  import org.springframework.web.bind.annotation.PutMapping;
29  import org.springframework.web.bind.annotation.RequestMapping;
30  import org.springframework.web.bind.annotation.RequestMethod;
31  
32  import io.swagger.v3.oas.models.OpenAPI;
33  import io.swagger.v3.oas.models.Operation;
34  import io.swagger.v3.oas.models.info.Contact;
35  import io.swagger.v3.oas.models.info.Info;
36  import io.swagger.v3.oas.models.info.License;
37  import io.swagger.v3.oas.models.media.Content;
38  import io.swagger.v3.oas.models.media.MediaType;
39  import io.swagger.v3.oas.models.responses.ApiResponse;
40  
41  /**
42   * @author Fernando Romulo da Silva
43   *
44   */
45  @Configuration
46  @EnableHypermediaSupport(type = HypermediaType.HAL)
47  public class DefaultOpenApiConfiguration {
48  
49      @Value("${spring.application.name:Please set the project's name ('spring.application.name')}")
50      private String appName;
51  
52      @Value("${spring.application.description:Please set the project's name ('spring.application.description')}")
53      private String appDesciption;
54  
55      public OpenAPI openAPI() {
56  	final var appVersion = getClass().getPackage().getImplementationVersion();
57  	final var appVendor = getClass().getPackage().getSpecificationVendor();
58  
59  	return new OpenAPI() //
60  			.info(new Info() //
61  					.title(appName) //
62  					.version(appVersion + " " + appVendor) //
63  					.description(appDesciption) //
64  					.contact(new Contact().name("Fernando Romulo da Silva")).termsOfService("http://swagger.io/terms/") //
65  					.license(new License().name("Apache 2.0").url("http://springdoc.org")) //
66  			);
67      }
68  
69  //    @Bean
70  //    public GroupedOpenApi internalGroupedOpenApi(OpenApiCustomiser apiFromResourceCustomizer) {
71  //        return GroupedOpenApi.builder()
72  //                .setGroup("testGroup")
73  //                .pathsToMatch("/nothingreally")
74  //                .addOpenApiCustomiser(apiFromResourceCustomizer)
75  //                .build();
76  //    }
77  
78      @Bean
79      OpenApiCustomiser defaultOpenApiCustomiser() {
80  	return openApiCustomiser -> {
81  
82  	    openApiCustomiser.getPaths().values().forEach(valuePath -> {
83  
84  		final var getOperation = valuePath.getGet();
85  		if (Objects.nonNull(getOperation)) {
86  		    createResponse(getOperation, RequestMethod.GET);
87  		}
88  
89  		final var postOperation = valuePath.getPost();
90  		if (Objects.nonNull(postOperation)) {
91  		    createResponse(postOperation, RequestMethod.POST);
92  		}
93  
94  		final var putOperation = valuePath.getPut();
95  		if (Objects.nonNull(putOperation)) {
96  		    createResponse(putOperation, RequestMethod.PUT);
97  		}
98  
99  		final var deleteOperation = valuePath.getDelete();
100 		if (Objects.nonNull(deleteOperation)) {
101 		    createResponse(deleteOperation, RequestMethod.DELETE);
102 		}
103 
104 		final var headOperation = valuePath.getHead();
105 		if (Objects.nonNull(headOperation)) {
106 		    createResponse(headOperation, RequestMethod.HEAD);
107 		}
108 
109 		final var optionOperation = valuePath.getOptions();
110 		if (Objects.nonNull(optionOperation)) {
111 		    createResponse(optionOperation, RequestMethod.OPTIONS);
112 		}
113 	    });
114 
115 	    // openapi spring boot programmatically example
116 //	    final var mySchema = new ObjectSchema();
117 //	    mySchema.name("MySchema");
118 //	    mySchema.addExample("""
119 //	    		{
120 //	    		  "timestamp": "2021-07-19T15:25:32.389836763",
121 //	    		  "status": 500,
122 //	    		  "error": "Internal Server Error",
123 //	    		  "message": "Unexpected error. Please, check the log with traceId and spanId for more detail",
124 //	    		  "traceId": "3d4144eeb01e3682",
125 //	    		  "spanId": "3d4144eeb01e3682"
126 //	    		}""");
127 //
128 //	    final var schemas = openApiCustomiser.getComponents().getSchemas();
129 //	    schemas.put(mySchema.getName(), mySchema);
130 	};
131     }
132 
133     private void createResponse(final Operation operation, final RequestMethod requestMethod) {
134 	final var apiResponses = operation.getResponses();
135 
136 	final var ex500 = """
137 			{
138 			    "timestamp": "1669564355551",
139 			    "status": 500,
140 			    "error": "Internal Server Error",
141 			    "message": "Unexpected error. Please, check the log with traceId and spanId for more detail",
142 			    "traceId": "3d4144eeb01e3682",
143 			    "spanId": "3d4144eeb01e3682"
144 			}""";
145 
146 	final var ex401 = """
147 			{
148 			    "timestamp": 1669564355551,
149 			    "status": 401,
150 			    "error": "Unauthorized",
151 			    "message": "Unauthorized",
152 			    "path": "/rest/images/type/2"
153 			}""";
154 
155 	final var ex403 = """
156 			{
157 			    "timestamp": 1669563774179,
158 			    "status": 403,
159 			    "error": "Forbidden",
160 			    "message": "Forbidden",
161 			    "path": "/rest/images/type"
162 			}""";
163 
164 	final var httpStatusMap = Map.of( //
165 			INTERNAL_SERVER_ERROR, ex500, //
166 			UNAUTHORIZED, ex401, //
167 			FORBIDDEN, ex403 //
168 	);
169 
170 	for (final var httpStatusEntrySet : httpStatusMap.entrySet()) {
171 
172 	    final var httpStatus = httpStatusEntrySet.getKey();
173 	    final var httpStatusExample = httpStatusEntrySet.getValue();
174 
175 	    apiResponses.computeIfAbsent( //
176 			    String.valueOf(httpStatus.value()), //
177 			    s -> {
178 				final var mediaType = new MediaType();
179 				mediaType.setExample(httpStatusExample);
180 
181 				final var content = new Content();
182 				content.addMediaType(APPLICATION_JSON_VALUE, mediaType);
183 				
184 				return new ApiResponse() //
185 						.description(httpStatus.getReasonPhrase()) //
186 						.content(content);
187 			    });
188 	}
189     }
190 
191 //    private PathItem addExamples(PathItem pathItem) {
192 //	    if(pathItem.getPost() !=null)  {
193 //	        //Note you can also Do this to APIResponses to insert info from a file into examples in say, a 200 response.
194 //	            pathItem.getPost().getRequestBody().getContent().values().stream()
195 //	                    .forEach(c ->{
196 //	                        String fileName = c.getExample().toString().replaceFirst("@","");
197 //	                        ObjectNode node = null;
198 //	                        try {
199 //	                            //load file from where you want. also don't insert is as a string, it wont format properly
200 //	                            node = (ObjectNode) new ObjectMapper().readTree(methodToReadInFileToString(fileName)); 
201 //	                        } catch (JsonProcessingException e) {
202 //	                            throw new RuntimeException(e);
203 //	                        }
204 //	                        c.setExample(node);
205 //	                    }
206 //	            );
207 //	    }
208 //	    return pathItem;
209 //	}
210 
211     @Bean
212     OperationCustomizer operationCustomizer() {
213 	return (operation, handleMethod) -> {
214 
215 	    if (StringUtils.isNotBlank(operation.getDescription()) || StringUtils.isNotBlank(operation.getSummary())) {
216 		return operation;
217 	    }
218 
219 	    final var method = handleMethod.getMethod();
220 
221 	    final var classEntity = getClassRequestMapping(method);
222 
223 	    final var listAnnotationTypes = Stream.of(method.getAnnotations()) //
224 			    .map(a -> a.annotationType()) //
225 			    .toList();
226 
227 	    if (listAnnotationTypes.contains(GetMapping.class)) {
228 
229 		final var methodEntity = getMethodRequestMapping(method, GetMapping.class);
230 
231 		operation.description("Get " + classEntity + methodEntity);
232 		operation.operationId("getItem");
233 		operation.summary("Get items bla bla");
234 		operation.addExtension("x-operationWeight", "200");
235 
236 		return operation;
237 	    }
238 
239 	    if (listAnnotationTypes.contains(PostMapping.class)) {
240 
241 		final var methodEntity = getMethodRequestMapping(method, GetMapping.class);
242 
243 		operation.description("Create " + classEntity + methodEntity);
244 		operation.operationId("createItem");
245 		operation.summary("Create items bla bla");
246 		operation.addExtension("x-operationWeight", "300");
247 
248 		return operation;
249 	    }
250 
251 	    if (listAnnotationTypes.contains(PutMapping.class)) {
252 
253 		final var methodEntity = getMethodRequestMapping(method, PutMapping.class);
254 
255 		operation.description("Update " + classEntity + methodEntity);
256 		operation.operationId("updateItem");
257 		operation.summary("Update items bla bla");
258 		operation.addExtension("x-operationWeight", "400");
259 
260 		return operation;
261 	    }
262 
263 	    if (listAnnotationTypes.contains(DeleteMapping.class)) {
264 
265 		final var methodEntity = getMethodRequestMapping(method, PutMapping.class);
266 
267 		operation.description("Delete " + classEntity + methodEntity);
268 		operation.operationId("deleteItem");
269 		operation.summary("Delete items bla bla");
270 		operation.addExtension("x-operationWeight", "500");
271 
272 		return operation;
273 	    }
274 
275 	    return operation;
276 	};
277     }
278 
279     private String getClassRequestMapping(final Method method) {
280 
281 	final var requestMappingAnnotationClass = Stream.of(method.getDeclaringClass().getAnnotations()) //
282 			.filter(p -> Objects.equals(p.annotationType(), RequestMapping.class)) //
283 			.findFirst() //
284 			.orElse(null);
285 
286 	if (Objects.nonNull(requestMappingAnnotationClass)) {
287 
288 	    final var values = (String[]) AnnotationUtils.getValue(requestMappingAnnotationClass, "value");
289 
290 	    if (!ArrayUtils.isEmpty(values)) {
291 
292 		return Stream.of(StringUtils.split(values[0], "/")) //
293 				.filter(s -> !StringUtils.containsAny(s, "{}")) //
294 				.collect(Collectors.joining("'s"));
295 	    }
296 	}
297 
298 	return StringUtils.EMPTY;
299     }
300 
301     private String getMethodRequestMapping(final Method method, final Class<? extends Annotation> clazz) {
302 
303 	final var requestMappingAnnotationClass = Stream.of(method.getDeclaringClass().getAnnotations()) //
304 			.filter(p -> Objects.equals(p.annotationType(), clazz)) //
305 			.findFirst() //
306 			.orElse(null);
307 
308 	if (Objects.nonNull(requestMappingAnnotationClass)) {
309 
310 	    final var values = (String[]) AnnotationUtils.getValue(requestMappingAnnotationClass, "value");
311 
312 	    if (!ArrayUtils.isEmpty(values)) {
313 
314 		return Stream.of(StringUtils.split(values[0], "/")) //
315 				.filter(s -> !StringUtils.containsAny(s, "{}")) //
316 				.reduce((first, second) -> second) //
317 				.map(m -> "'s " + m) //
318 				.orElse(StringUtils.EMPTY);
319 	    }
320 	}
321 
322 	return StringUtils.EMPTY;
323     }
324 }