Skip to content

Commit 2e910f4

Browse files
author
bnasslahsen
committed
Added support for dynamic groups from application.yml. Fixes #344
1 parent 90e6524 commit 2e910f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2977
-51
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.springdoc.core;
2+
3+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
4+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6+
import org.springframework.context.annotation.Conditional;
7+
8+
import static org.springdoc.core.Constants.SPRINGDOC_CACHE_DISABLED;
9+
10+
public class CacheOrGroupedOpenApiCondition extends AnyNestedCondition {
11+
12+
CacheOrGroupedOpenApiCondition() {
13+
super(ConfigurationPhase.REGISTER_BEAN);
14+
}
15+
16+
@Conditional(MultipleOpenApiSupportCondition.class)
17+
static class OnMultipleOpenApiSupportCondition {}
18+
19+
@ConditionalOnProperty(name = SPRINGDOC_CACHE_DISABLED)
20+
@ConditionalOnMissingBean(GroupedOpenApi.class)
21+
static class OnCacheDisabled {}
22+
23+
}

springdoc-openapi-common/src/main/java/org/springdoc/core/Constants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
public final class Constants {
2626

27+
public static final String SPRINGDOC_PREFIX="springdoc";
28+
2729
public static final String DEFAULT_API_DOCS_URL = "/v3/api-docs";
2830

2931
public static final String DEFAULT_SERVER_DESCRIPTION = "Generated server url";
@@ -88,6 +90,10 @@ public final class Constants {
8890

8991
public static final String DEFAULT_GROUP_NAME = "springdocDefault";
9092

93+
public static final String GROUP_CONFIG_FIRST_PROPERTY ="springdoc.group-configs[0].group";
94+
95+
public static final String GROUP_NAME_NOT_NULL = "Group name can not be null";
96+
9197
public static final String GET_METHOD = "get";
9298

9399
public static final String POST_METHOD = "post";

springdoc-openapi-common/src/main/java/org/springdoc/core/GroupedOpenApi.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
import org.springframework.util.CollectionUtils;
2929

30+
import static org.springdoc.core.Constants.GROUP_NAME_NOT_NULL;
31+
3032
public class GroupedOpenApi {
3133

3234
private final String group;
@@ -42,7 +44,7 @@ public class GroupedOpenApi {
4244
private final List<String> pathsToExclude;
4345

4446
private GroupedOpenApi(Builder builder) {
45-
this.group = Objects.requireNonNull(builder.group, "group");
47+
this.group = Objects.requireNonNull(builder.group, GROUP_NAME_NOT_NULL);
4648
this.pathsToMatch = builder.pathsToMatch;
4749
this.packagesToScan = builder.packagesToScan;
4850
this.packagesToExclude = builder.packagesToExclude;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.springdoc.core;
2+
3+
4+
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7+
8+
import static org.springdoc.core.Constants.GROUP_CONFIG_FIRST_PROPERTY;
9+
10+
public class MultipleOpenApiSupportCondition extends AnyNestedCondition {
11+
12+
MultipleOpenApiSupportCondition() {
13+
super(ConfigurationPhase.REGISTER_BEAN);
14+
}
15+
16+
@ConditionalOnBean(GroupedOpenApi.class)
17+
static class OnGroupedOpenApiBean {}
18+
19+
@ConditionalOnProperty(name = GROUP_CONFIG_FIRST_PROPERTY)
20+
static class OnGroupConfigProperty {}
21+
22+
}

springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfigProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
3030

3131
@Configuration
32-
@ConfigurationProperties(prefix = "springdoc")
32+
@ConfigurationProperties(prefix = Constants.SPRINGDOC_PREFIX)
3333
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
3434
public class SpringDocConfigProperties {
3535

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

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Optional;
25+
import java.util.stream.Collectors;
2526

2627
import com.fasterxml.jackson.databind.node.ObjectNode;
2728
import groovy.lang.MetaClass;
@@ -39,24 +40,26 @@
3940
import org.springdoc.core.customizers.OpenApiCustomiser;
4041
import org.springdoc.core.customizers.PropertyCustomizer;
4142

42-
import org.springframework.beans.factory.config.BeanDefinition;
4343
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
4444
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
45-
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
46-
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
4745
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
48-
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4946
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
5047
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
48+
import org.springframework.boot.context.properties.bind.BindResult;
49+
import org.springframework.boot.context.properties.bind.Binder;
5150
import org.springframework.context.ApplicationContext;
5251
import org.springframework.context.annotation.Bean;
52+
import org.springframework.context.annotation.Conditional;
5353
import org.springframework.context.annotation.Configuration;
5454
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
55+
import org.springframework.core.env.Environment;
56+
import org.springframework.util.CollectionUtils;
5557

56-
import static org.springdoc.core.Constants.SPRINGDOC_CACHE_DISABLED;
5758
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;
59+
import static org.springdoc.core.Constants.SPRINGDOC_PREFIX;
5860
import static org.springdoc.core.Constants.SPRINGDOC_SCHEMA_RESOLVE_PROPERTIES;
5961
import static org.springdoc.core.SpringDocUtils.getConfig;
62+
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
6063

6164
@Configuration
6265
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
@@ -89,10 +92,11 @@ ResponseSupportConverter responseSupportConverter() {
8992
@ConditionalOnClass(value = { MetaClass.class })
9093
class GroovyProvider {
9194
@Bean
92-
Object ignoreGroovyMetaClass () {
95+
Object ignoreGroovyMetaClass() {
9396
SpringDocUtils.getConfig().addRequestWrapperToIgnore(MetaClass.class);
9497
return null;
9598
}
99+
96100
@Bean
97101
RequestTypeToIgnoreConverter requestTypeToIgnoreConverter() {
98102
return new RequestTypeToIgnoreConverter();
@@ -156,34 +160,30 @@ OpenApiCustomiser propertiesResolverForSchema(PropertyResolverUtils propertyReso
156160
};
157161
}
158162

159-
static class ConditionOnCacheOrGroupedOpenApi extends AnyNestedCondition {
160-
161-
ConditionOnCacheOrGroupedOpenApi() {
162-
super(ConfigurationPhase.REGISTER_BEAN);
163-
}
164-
165-
@Bean
166-
@ConditionalOnBean(GroupedOpenApi.class)
167-
BeanFactoryPostProcessor beanFactoryPostProcessor1() {
168-
return getBeanFactoryPostProcessor();
169-
}
170-
171-
@Bean
172-
@ConditionalOnProperty(name = SPRINGDOC_CACHE_DISABLED)
173-
@ConditionalOnMissingBean(GroupedOpenApi.class)
174-
BeanFactoryPostProcessor beanFactoryPostProcessor2() {
175-
return getBeanFactoryPostProcessor();
176-
}
177-
178-
private BeanFactoryPostProcessor getBeanFactoryPostProcessor() {
179-
return beanFactory -> {
180-
for (String beanName : beanFactory.getBeanNamesForType(OpenAPIBuilder.class)) {
181-
beanFactory.getBeanDefinition(beanName).setScope(BeanDefinition.SCOPE_PROTOTYPE);
182-
}
183-
for (String beanName : beanFactory.getBeanNamesForType(OpenAPI.class)) {
184-
beanFactory.getBeanDefinition(beanName).setScope(BeanDefinition.SCOPE_PROTOTYPE);
185-
}
186-
};
187-
}
163+
@Bean
164+
@Conditional(CacheOrGroupedOpenApiCondition.class)
165+
BeanFactoryPostProcessor springdocBeanFactoryPostProcessor(Environment environment) {
166+
return beanFactory -> {
167+
final BindResult<SpringDocConfigProperties> result = Binder.get(environment)
168+
.bind(SPRINGDOC_PREFIX, SpringDocConfigProperties.class);
169+
if (result.isBound()) {
170+
SpringDocConfigProperties springDocGroupConfig = result.get();
171+
List<GroupedOpenApi> groupedOpenApis = springDocGroupConfig.getGroupConfigs().stream()
172+
.map(elt -> {
173+
GroupedOpenApi.Builder builder = GroupedOpenApi.builder();
174+
if (!CollectionUtils.isEmpty(elt.getPackagesToScan()))
175+
builder.packagesToScan(elt.getPackagesToScan().toArray(new String[0]));
176+
if (!CollectionUtils.isEmpty(elt.getPathsToMatch()))
177+
builder.pathsToMatch(elt.getPathsToMatch().toArray(new String[0]));
178+
return builder.setGroup(elt.getGroup()).build();
179+
})
180+
.collect(Collectors.toList());
181+
groupedOpenApis.forEach(elt -> beanFactory.registerSingleton(elt.getGroup(), elt));
182+
}
183+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPIBuilder.class))
184+
beanFactory.getBeanDefinition(beanName).setScope(SCOPE_PROTOTYPE);
185+
for (String beanName : beanFactory.getBeanNamesForType(OpenAPI.class))
186+
beanFactory.getBeanDefinition(beanName).setScope(SCOPE_PROTOTYPE);
187+
};
188188
}
189189
}

springdoc-openapi-common/src/main/java/org/springdoc/core/SwaggerUiConfigProperties.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.context.annotation.Configuration;
3838
import org.springframework.util.CollectionUtils;
3939

40+
import static org.springdoc.core.Constants.GROUP_NAME_NOT_NULL;
4041
import static org.springdoc.core.Constants.SPRINGDOC_SWAGGER_UI_ENABLED;
4142
import static org.springdoc.core.Constants.SWAGGER_UI_OAUTH_REDIRECT_URL;
4243
import static org.springframework.util.AntPathMatcher.DEFAULT_PATH_SEPARATOR;
@@ -415,11 +416,13 @@ public SwaggerUrl() {
415416
}
416417

417418
public SwaggerUrl(String group, String url) {
419+
Objects.requireNonNull(group, GROUP_NAME_NOT_NULL);
418420
this.url = url;
419421
this.name = group;
420422
}
421423

422424
public SwaggerUrl(String group) {
425+
Objects.requireNonNull(group, GROUP_NAME_NOT_NULL);
423426
this.name = group;
424427
}
425428

springdoc-openapi-webflux-core/src/main/java/org/springdoc/core/MultipleOpenApiWebFluxConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
import org.springdoc.api.MultipleOpenApiResource;
2424

2525
import org.springframework.beans.factory.ObjectFactory;
26-
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2726
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2827
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2928
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
3029
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Conditional;
3131
import org.springframework.context.annotation.Configuration;
3232
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
3333

@@ -36,8 +36,8 @@
3636

3737
@Configuration
3838
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
39-
@ConditionalOnBean(GroupedOpenApi.class)
4039
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, matchIfMissing = true)
40+
@Conditional(MultipleOpenApiSupportCondition.class)
4141
public class MultipleOpenApiWebFluxConfiguration {
4242

4343
@Bean
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app68;
20+
21+
import io.swagger.v3.oas.models.Components;
22+
import io.swagger.v3.oas.models.OpenAPI;
23+
import io.swagger.v3.oas.models.info.Info;
24+
import io.swagger.v3.oas.models.info.License;
25+
import io.swagger.v3.oas.models.security.SecurityScheme;
26+
import test.org.springdoc.api.AbstractSpringDocTest;
27+
28+
import org.springframework.boot.autoconfigure.SpringBootApplication;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.ComponentScan;
31+
import org.springframework.test.context.TestPropertySource;
32+
33+
@TestPropertySource(properties = {
34+
"springdoc.group-configs[0].group=stream",
35+
"springdoc.group-configs[0].pathsToMatch=/stream/**"
36+
})
37+
public class SpringDocApp68Test extends AbstractSpringDocTest {
38+
39+
public SpringDocApp68Test() {
40+
this.groupName = "/stream";
41+
}
42+
43+
44+
@SpringBootApplication
45+
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.app68" })
46+
static class SpringDocTestApp {
47+
@Bean
48+
public OpenAPI customOpenAPI() {
49+
return new OpenAPI()
50+
.components(new Components().addSecuritySchemes("basicScheme",
51+
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")))
52+
.info(new Info().title("Tweet API").version("v0")
53+
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app68.controller;
20+
21+
import test.org.springdoc.api.app68.exception.TweetConflictException;
22+
import test.org.springdoc.api.app68.exception.TweetNotFoundException;
23+
import test.org.springdoc.api.app68.payload.ErrorResponse;
24+
25+
import org.springframework.http.HttpStatus;
26+
import org.springframework.http.ResponseEntity;
27+
import org.springframework.web.bind.annotation.ExceptionHandler;
28+
import org.springframework.web.bind.annotation.ResponseStatus;
29+
import org.springframework.web.bind.annotation.RestControllerAdvice;
30+
31+
@RestControllerAdvice
32+
public class ExceptionTranslator {
33+
34+
35+
@SuppressWarnings("rawtypes")
36+
@ExceptionHandler(TweetConflictException.class)
37+
@ResponseStatus(HttpStatus.CONFLICT)
38+
public ResponseEntity handleDuplicateKeyException(TweetConflictException ex) {
39+
return ResponseEntity.status(HttpStatus.CONFLICT)
40+
.body(new ErrorResponse("A Tweet with the same text already exists"));
41+
}
42+
43+
@SuppressWarnings("rawtypes")
44+
@ExceptionHandler(TweetNotFoundException.class)
45+
@ResponseStatus(HttpStatus.NOT_FOUND)
46+
public ResponseEntity handleTweetNotFoundException(TweetNotFoundException ex) {
47+
return ResponseEntity.notFound().build();
48+
}
49+
50+
}

0 commit comments

Comments
 (0)