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
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
70
71
72
73
74
75
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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 }