Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ease group declaration through code or properties with actuators #1651

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -52,9 +52,8 @@
import org.springdoc.core.customizers.DataRestDelegatingMethodParameterCustomizer;
import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.providers.ActuatorProvider;
Expand Down Expand Up @@ -462,7 +461,7 @@ static BeanFactoryPostProcessor springdocBeanFactoryPostProcessor3(List<GroupedO
@Bean
@Lazy(false)
@ConditionalOnManagementPort(ManagementPortType.SAME)
OperationCustomizer actuatorCustomizer() {
GlobalOperationCustomizer actuatorCustomizer() {
return new ActuatorOperationCustomizer();
}

Expand All @@ -475,7 +474,7 @@ OperationCustomizer actuatorCustomizer() {
@Bean
@Lazy(false)
@ConditionalOnManagementPort(ManagementPortType.SAME)
OpenApiCustomiser actuatorOpenApiCustomiser(WebEndpointProperties webEndpointProperties) {
GlobalOpenApiCustomizer actuatorOpenApiCustomiser(WebEndpointProperties webEndpointProperties) {
return new ActuatorOpenApiCustomizer(webEndpointProperties);
}

Expand Down
Expand Up @@ -77,8 +77,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
GroupedOpenApi actuatorGroup = GroupedOpenApi.builder().group(ACTUATOR_DEFAULT_GROUP)
.pathsToMatch(webEndpointProperties.getBasePath() + ALL_PATTERN)
.pathsToExclude(webEndpointProperties.getBasePath() + HEALTH_PATTERN)
.addOperationCustomizer(actuatorCustomizer)
.addOpenApiCustomiser(actuatorOpenApiCustomiser)
.build();
// Add the actuator group
newGroups.add(actuatorGroup);
Expand Down
Expand Up @@ -22,13 +22,19 @@

package org.springdoc.core.customizers;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.PathParameter;
Expand All @@ -42,7 +48,7 @@
* The type Actuator open api customiser.
* @author bnasslahsen
*/
public class ActuatorOpenApiCustomizer implements OpenApiCustomiser {
public class ActuatorOpenApiCustomizer implements GlobalOpenApiCustomizer {

/**
* The Path pathern.
Expand All @@ -63,26 +69,54 @@ public ActuatorOpenApiCustomizer(WebEndpointProperties webEndpointProperties) {
this.webEndpointProperties = webEndpointProperties;
}

private Stream<Entry<String, PathItem>> actuatorPathEntryStream(OpenAPI openApi, String relativeSubPath) {
String pathPrefix = webEndpointProperties.getBasePath() + Optional.ofNullable(relativeSubPath).orElse("");
return Optional.ofNullable(openApi.getPaths())
.map(Paths::entrySet)
.map(Set::stream)
.map(s -> s.filter(entry -> entry.getKey().startsWith(pathPrefix)))
.orElse(Stream.empty());
}

private void handleActuatorPathParam(OpenAPI openApi) {
actuatorPathEntryStream(openApi, DEFAULT_PATH_SEPARATOR).forEach(stringPathItemEntry -> {
String path = stringPathItemEntry.getKey();
Matcher matcher = pathPathern.matcher(path);
while (matcher.find()) {
String pathParam = matcher.group(1);
PathItem pathItem = stringPathItemEntry.getValue();
pathItem.readOperations().forEach(operation -> {
List<Parameter> existingParameters = operation.getParameters();
Optional<Parameter> existingParam = Optional.empty();
if (!CollectionUtils.isEmpty(existingParameters))
existingParam = existingParameters.stream().filter(p -> pathParam.equals(p.getName())).findAny();
if (!existingParam.isPresent())
operation.addParametersItem(new PathParameter().name(pathParam).schema(new StringSchema()));
});
}
});
}

private void handleActuatorOperationIdUniqueness(OpenAPI openApi) {
Set<String> usedOperationIds = new HashSet<>();
actuatorPathEntryStream(openApi, null)
.sorted(Comparator.comparing(Entry::getKey))
.forEachOrdered(stringPathItemEntry -> {
stringPathItemEntry.getValue().readOperations().forEach(operation -> {
String initialOperationId = operation.getOperationId();
String uniqueOperationId = operation.getOperationId();
int counter = 1;
while (!usedOperationIds.add(uniqueOperationId)) {
uniqueOperationId = initialOperationId + "_" + ++counter;
}
operation.setOperationId(uniqueOperationId);
});
});
}

@Override
public void customise(OpenAPI openApi) {
if (!CollectionUtils.isEmpty(openApi.getPaths()))
openApi.getPaths().entrySet().stream()
.filter(stringPathItemEntry -> stringPathItemEntry.getKey().startsWith(webEndpointProperties.getBasePath() + DEFAULT_PATH_SEPARATOR))
.forEach(stringPathItemEntry -> {
String path = stringPathItemEntry.getKey();
Matcher matcher = pathPathern.matcher(path);
while (matcher.find()) {
String pathParam = matcher.group(1);
PathItem pathItem = stringPathItemEntry.getValue();
pathItem.readOperations().forEach(operation -> {
List<Parameter> existingParameters = operation.getParameters();
Optional<Parameter> existingParam = Optional.empty();
if (!CollectionUtils.isEmpty(existingParameters))
existingParam = existingParameters.stream().filter(p -> pathParam.equals(p.getName())).findAny();
if (!existingParam.isPresent())
operation.addParametersItem(new PathParameter().name(pathParam).schema(new StringSchema()));
});
}
});
handleActuatorPathParam(openApi);
handleActuatorOperationIdUniqueness(openApi);
}
}
Expand Up @@ -24,7 +24,6 @@

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -45,19 +44,13 @@
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.web.method.HandlerMethod;

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

/**
* The type Actuator operation customizer.
* @author bnasslahsen
*/
public class ActuatorOperationCustomizer implements OperationCustomizer {

/**
* The Method count.
*/
private final HashMap<String, Integer> methodCountMap = new HashMap<>();
public class ActuatorOperationCustomizer implements GlobalOperationCustomizer {

/**
* The constant OPERATION.
Expand Down Expand Up @@ -113,14 +106,6 @@ public Operation customize(Operation operation, HandlerMethod handlerMethod) {
while (matcher.find()) {
operationId = matcher.group(1);
}
if (methodCountMap.containsKey(operationId)) {
Integer methodCount = methodCountMap.get(operationId) + 1;
methodCountMap.put(operationId, methodCount);
operationId = operationId + "_" + methodCount;
}
else
methodCountMap.put(operationId, INTEGER_ONE);

if (!summary.contains("$"))
operation.setSummary(summary);
operation.setOperationId(operationId);
Expand Down
@@ -0,0 +1,100 @@
/*
*
* *
* * * Copyright 2019-2020 the original author or authors.
* * *
* * * Licensed under the Apache License, Version 2.0 (the "License");
* * * you may not use this file except in compliance with the License.
* * * You may obtain a copy of the License at
* * *
* * * https://www.apache.org/licenses/LICENSE-2.0
* * *
* * * Unless required by applicable law or agreed to in writing, software
* * * distributed under the License is distributed on an "AS IS" BASIS,
* * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * See the License for the specific language governing permissions and
* * * limitations under the License.
* *
*
*/

package test.org.springdoc.api.app186;

import static org.springdoc.core.Constants.ALL_PATTERN;

import org.junit.jupiter.api.Test;
import org.springdoc.core.Constants;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;

import test.org.springdoc.api.AbstractCommonTest;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties={ "springdoc.show-actuator=true",
"springdoc.group-configs[0].group=group-actuator-as-properties",
"springdoc.group-configs[0].paths-to-match=${management.endpoints.web.base-path:/actuator}/**",
"management.endpoints.enabled-by-default=true",
"management.endpoints.web.exposure.include=*",
"management.endpoints.web.exposure.exclude=functions, shutdown"})
public class SpringDocApp186Test extends AbstractCommonTest {

@SpringBootApplication
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.app186" })
static class SpringDocTestApp {

@Bean
public GroupedOpenApi asCodeCheckBackwardsCompatibility(OpenApiCustomiser actuatorOpenApiCustomiser,
OperationCustomizer actuatorCustomizer, WebEndpointProperties endpointProperties) {
return GroupedOpenApi.builder()
.group("group-actuator-as-code-check-backwards-compatibility")
.pathsToMatch(endpointProperties.getBasePath()+ ALL_PATTERN)
.addOpenApiCustomiser(actuatorOpenApiCustomiser)
.addOperationCustomizer(actuatorCustomizer)
.build();
}

@Bean
public GroupedOpenApi asCode(WebEndpointProperties endpointProperties) {
return GroupedOpenApi.builder()
.group("group-actuator-as-code")
.pathsToMatch(endpointProperties.getBasePath()+ ALL_PATTERN)
.build();
}
}

@Test
public void testApp() throws Exception {
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL).exchange()
.expectStatus().isOk()
.expectBody().json(getContent("results/app186.json"), true);
}

@Test
public void testGroupActuatorAsCodeCheckBackwardsCompatibility() throws Exception {
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-code-check-backwards-compatibility").exchange()
.expectStatus().isOk()
.expectBody().json(getContent("results/app186.json"), true);
}

@Test
public void testGroupActuatorAsCode() throws Exception {
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-code").exchange()
.expectStatus().isOk()
.expectBody().json(getContent("results/app186.json"), true);
}

@Test
public void testGroupActuatorAsProperties() throws Exception {
webTestClient.get().uri(Constants.DEFAULT_API_DOCS_URL + "/group-actuator-as-properties").exchange()
.expectStatus().isOk()
.expectBody().json(getContent("results/app186.json"), true);
}

}