Skip to content

Commit 6cf811b

Browse files
committed
Changes report: Ease group declaration through code or properties with actuators. Fixes #1651.
1 parent 64d369f commit 6cf811b

File tree

14 files changed

+1993
-107
lines changed

14 files changed

+1993
-107
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@
5656
import org.springdoc.core.customizers.ActuatorOperationCustomizer;
5757
import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer;
5858
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
59+
import org.springdoc.core.customizers.GlobalOperationCustomizer;
5960
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
60-
import org.springdoc.core.customizers.OpenApiCustomizer;
61-
import org.springdoc.core.customizers.OperationCustomizer;
6261
import org.springdoc.core.customizers.PropertyCustomizer;
6362
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
6463
import org.springdoc.core.models.GroupedOpenApi;
@@ -470,7 +469,7 @@ static BeanFactoryPostProcessor springdocBeanFactoryPostProcessor3(List<GroupedO
470469
@Bean
471470
@Lazy(false)
472471
@ConditionalOnManagementPort(ManagementPortType.SAME)
473-
OperationCustomizer actuatorCustomizer() {
472+
GlobalOperationCustomizer actuatorCustomizer() {
474473
return new ActuatorOperationCustomizer();
475474
}
476475

@@ -483,7 +482,7 @@ OperationCustomizer actuatorCustomizer() {
483482
@Bean
484483
@Lazy(false)
485484
@ConditionalOnManagementPort(ManagementPortType.SAME)
486-
OpenApiCustomizer actuatorOpenApiCustomizer(WebEndpointProperties webEndpointProperties) {
485+
GlobalOpenApiCustomizer actuatorOpenApiCustomizer(WebEndpointProperties webEndpointProperties) {
487486
return new ActuatorOpenApiCustomizer(webEndpointProperties);
488487
}
489488

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configurer/SpringdocActuatorBeanFactoryConfigurer.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
8080
GroupedOpenApi actuatorGroup = GroupedOpenApi.builder().group(ACTUATOR_DEFAULT_GROUP)
8181
.pathsToMatch(webEndpointProperties.getBasePath() + ALL_PATTERN)
8282
.pathsToExclude(webEndpointProperties.getBasePath() + HEALTH_PATTERN)
83-
.addOperationCustomizer(actuatorCustomizer)
84-
.addOpenApiCustomizer(actuatorOpenApiCustomizer)
8583
.build();
8684
// Add the actuator group
8785
newGroups.add(actuatorGroup);

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/ActuatorOpenApiCustomizer.java

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@
2424

2525
package org.springdoc.core.customizers;
2626

27+
import java.util.Comparator;
28+
import java.util.HashSet;
2729
import java.util.List;
30+
import java.util.Map.Entry;
2831
import java.util.Optional;
32+
import java.util.Set;
2933
import java.util.regex.Matcher;
3034
import java.util.regex.Pattern;
35+
import java.util.stream.Stream;
3136

3237
import io.swagger.v3.oas.models.OpenAPI;
3338
import io.swagger.v3.oas.models.PathItem;
39+
import io.swagger.v3.oas.models.Paths;
3440
import io.swagger.v3.oas.models.media.StringSchema;
3541
import io.swagger.v3.oas.models.parameters.Parameter;
3642
import io.swagger.v3.oas.models.parameters.PathParameter;
@@ -44,13 +50,16 @@
4450
* The type Actuator open api customiser.
4551
* @author bnasslahsen
4652
*/
47-
public class ActuatorOpenApiCustomizer implements OpenApiCustomizer {
53+
public class ActuatorOpenApiCustomizer implements GlobalOpenApiCustomizer {
4854

4955
/**
5056
* The Path pathern.
5157
*/
5258
private final Pattern pathPathern = Pattern.compile("\\{(.*?)}");
5359

60+
/**
61+
* The Web endpoint properties.
62+
*/
5463
private final WebEndpointProperties webEndpointProperties;
5564

5665
/**
@@ -62,26 +71,54 @@ public ActuatorOpenApiCustomizer(WebEndpointProperties webEndpointProperties) {
6271
this.webEndpointProperties = webEndpointProperties;
6372
}
6473

65-
@Override
66-
public void customise(OpenAPI openApi) {
67-
if (!CollectionUtils.isEmpty(openApi.getPaths()))
68-
openApi.getPaths().entrySet().stream()
69-
.filter(stringPathItemEntry -> stringPathItemEntry.getKey().startsWith(webEndpointProperties.getBasePath() + DEFAULT_PATH_SEPARATOR))
70-
.forEach(stringPathItemEntry -> {
71-
String path = stringPathItemEntry.getKey();
72-
Matcher matcher = pathPathern.matcher(path);
73-
while (matcher.find()) {
74-
String pathParam = matcher.group(1);
75-
PathItem pathItem = stringPathItemEntry.getValue();
76-
pathItem.readOperations().forEach(operation -> {
77-
List<Parameter> existingParameters = operation.getParameters();
78-
Optional<Parameter> existingParam = Optional.empty();
79-
if (!CollectionUtils.isEmpty(existingParameters))
80-
existingParam = existingParameters.stream().filter(p -> pathParam.equals(p.getName())).findAny();
81-
if (!existingParam.isPresent())
82-
operation.addParametersItem(new PathParameter().name(pathParam).schema(new StringSchema()));
83-
});
74+
private Stream<Entry<String, PathItem>> actuatorPathEntryStream(OpenAPI openApi, String relativeSubPath) {
75+
String pathPrefix = webEndpointProperties.getBasePath() + Optional.ofNullable(relativeSubPath).orElse("");
76+
return Optional.ofNullable(openApi.getPaths())
77+
.map(Paths::entrySet)
78+
.map(Set::stream)
79+
.map(s -> s.filter(entry -> entry.getKey().startsWith(pathPrefix)))
80+
.orElse(Stream.empty());
81+
}
82+
83+
private void handleActuatorPathParam(OpenAPI openApi) {
84+
actuatorPathEntryStream(openApi, DEFAULT_PATH_SEPARATOR).forEach(stringPathItemEntry -> {
85+
String path = stringPathItemEntry.getKey();
86+
Matcher matcher = pathPathern.matcher(path);
87+
while (matcher.find()) {
88+
String pathParam = matcher.group(1);
89+
PathItem pathItem = stringPathItemEntry.getValue();
90+
pathItem.readOperations().forEach(operation -> {
91+
List<Parameter> existingParameters = operation.getParameters();
92+
Optional<Parameter> existingParam = Optional.empty();
93+
if (!CollectionUtils.isEmpty(existingParameters))
94+
existingParam = existingParameters.stream().filter(p -> pathParam.equals(p.getName())).findAny();
95+
if (!existingParam.isPresent())
96+
operation.addParametersItem(new PathParameter().name(pathParam).schema(new StringSchema()));
97+
});
98+
}
99+
});
100+
}
101+
102+
private void handleActuatorOperationIdUniqueness(OpenAPI openApi) {
103+
Set<String> usedOperationIds = new HashSet<>();
104+
actuatorPathEntryStream(openApi, null)
105+
.sorted(Comparator.comparing(Entry::getKey))
106+
.forEachOrdered(stringPathItemEntry -> {
107+
stringPathItemEntry.getValue().readOperations().forEach(operation -> {
108+
String initialOperationId = operation.getOperationId();
109+
String uniqueOperationId = operation.getOperationId();
110+
int counter = 1;
111+
while (!usedOperationIds.add(uniqueOperationId)) {
112+
uniqueOperationId = initialOperationId + "_" + ++counter;
84113
}
114+
operation.setOperationId(uniqueOperationId);
85115
});
116+
});
117+
}
118+
119+
@Override
120+
public void customise(OpenAPI openApi) {
121+
handleActuatorPathParam(openApi);
122+
handleActuatorOperationIdUniqueness(openApi);
86123
}
87124
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/ActuatorOperationCustomizer.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
import java.lang.reflect.Field;
2828
import java.lang.reflect.Parameter;
29-
import java.util.HashMap;
3029
import java.util.regex.Matcher;
3130
import java.util.regex.Pattern;
3231

@@ -47,24 +46,27 @@
4746
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
4847
import org.springframework.web.method.HandlerMethod;
4948

50-
import static org.apache.commons.lang3.math.NumberUtils.INTEGER_ONE;
5149
import static org.springdoc.core.providers.ActuatorProvider.getTag;
5250

5351
/**
5452
* The type Actuator operation customizer.
5553
* @author bnasslahsen
5654
*/
57-
public class ActuatorOperationCustomizer implements OperationCustomizer {
55+
public class ActuatorOperationCustomizer implements GlobalOperationCustomizer {
5856

5957
/**
60-
* The Method count.
58+
* The constant OPERATION.
6159
*/
62-
private final HashMap<String, Integer> methodCountMap = new HashMap<>();
63-
6460
private static final String OPERATION = "operation";
6561

62+
/**
63+
* The constant PARAMETER.
64+
*/
6665
private static final String PARAMETER = "parameter";
6766

67+
/**
68+
* The constant LOGGER.
69+
*/
6870
private static final Logger LOGGER = LoggerFactory.getLogger(ActuatorOperationCustomizer.class);
6971

7072
/**
@@ -106,14 +108,6 @@ public Operation customize(Operation operation, HandlerMethod handlerMethod) {
106108
while (matcher.find()) {
107109
operationId = matcher.group(1);
108110
}
109-
if (methodCountMap.containsKey(operationId)) {
110-
Integer methodCount = methodCountMap.get(operationId) + 1;
111-
methodCountMap.put(operationId, methodCount);
112-
operationId = operationId + "_" + methodCount;
113-
}
114-
else
115-
methodCountMap.put(operationId, INTEGER_ONE);
116-
117111
if (!summary.contains("$"))
118112
operation.setSummary(summary);
119113
operation.setOperationId(operationId);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.app186;
26+
27+
28+
import org.junit.jupiter.api.Test;
29+
import org.springdoc.core.customizers.OpenApiCustomizer;
30+
import org.springdoc.core.customizers.OperationCustomizer;
31+
import org.springdoc.core.models.GroupedOpenApi;
32+
import org.springdoc.core.utils.Constants;
33+
import test.org.springdoc.api.AbstractCommonTest;
34+
35+
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
36+
import org.springframework.boot.autoconfigure.SpringBootApplication;
37+
import org.springframework.boot.test.context.SpringBootTest;
38+
import org.springframework.context.annotation.Bean;
39+
import org.springframework.context.annotation.ComponentScan;
40+
import org.springframework.test.context.TestPropertySource;
41+
42+
import static org.springdoc.core.utils.Constants.ALL_PATTERN;
43+
44+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
45+
@TestPropertySource(properties={ "springdoc.show-actuator=true",
46+
"springdoc.group-configs[0].group=group-actuator-as-properties",
47+
"springdoc.group-configs[0].paths-to-match=${management.endpoints.web.base-path:/actuator}/**",
48+
"management.endpoints.enabled-by-default=true",
49+
"management.endpoints.web.exposure.include=*",
50+
"management.endpoints.web.exposure.exclude=functions, shutdown"})
51+
public class SpringDocApp186Test extends AbstractCommonTest {
52+
53+
@SpringBootApplication
54+
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.app186" })
55+
static class SpringDocTestApp {
56+
57+
@Bean
58+
public GroupedOpenApi asCodeCheckBackwardsCompatibility(OpenApiCustomizer actuatorOpenApiCustomiser,
59+
OperationCustomizer actuatorCustomizer, WebEndpointProperties endpointProperties) {
60+
return GroupedOpenApi.builder()
61+
.group("group-actuator-as-code-check-backwards-compatibility")
62+
.pathsToMatch(endpointProperties.getBasePath()+ ALL_PATTERN)
63+
.addOpenApiCustomizer(actuatorOpenApiCustomiser)
64+
.addOperationCustomizer(actuatorCustomizer)
65+
.build();
66+
}
67+
68+
@Bean
69+
public GroupedOpenApi asCode(WebEndpointProperties endpointProperties) {
70+
return GroupedOpenApi.builder()
71+
.group("group-actuator-as-code")
72+
.pathsToMatch(endpointProperties.getBasePath()+ ALL_PATTERN)
73+
.build();
74+
}
75+
}
76+
77+
@Test
78+
public void testApp() throws Exception {
79+
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL).exchange()
80+
.expectStatus().isOk()
81+
.expectBody().json(getContent("results/app186.json"), true);
82+
}
83+
84+
@Test
85+
public void testGroupActuatorAsCodeCheckBackwardsCompatibility() throws Exception {
86+
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-code-check-backwards-compatibility").exchange()
87+
.expectStatus().isOk()
88+
.expectBody().json(getContent("results/app186.json"), true);
89+
}
90+
91+
@Test
92+
public void testGroupActuatorAsCode() throws Exception {
93+
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-code").exchange()
94+
.expectStatus().isOk()
95+
.expectBody().json(getContent("results/app186.json"), true);
96+
}
97+
98+
@Test
99+
public void testGroupActuatorAsProperties() throws Exception {
100+
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-properties").exchange()
101+
.expectStatus().isOk()
102+
.expectBody().json(getContent("results/app186.json"), true);
103+
}
104+
}
105+

0 commit comments

Comments
 (0)