Repository: springdoc/springdoc-openapi
Pull request #1543 (2022-03-09 18:17:15)

Add ServerBaseUrlCustomizer functionality to be able to customize the generated Server URL
ServerBaseUrlCustomiser to allow making changes to the generated server base URL. Also adds a getter method for retrieving the generated server base URL to use in an OpenApiBuilderCustomizer in order to make customizations based on the generated value

skylar-stark: Use case: strip the "context-path" off the generated server URL and instead add it to the beginning of each path, so that Open API generation works behind a reverse proxy

Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIService.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIService.java
index bffc25527..2fecc869e 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIService.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/OpenAPIService.java
@@ -62,7 +62,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
-
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
@@ -120,6 +120,11 @@ public class OpenAPIService implements ApplicationContextAware {
 	 */
 	private final Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers;
 
+	/**
+	 * The server base URL customisers.
+	 */
+	private final Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers;
+
 	/**
 	 * The Spring doc config properties.
 	 */
@@ -188,7 +193,8 @@ public class OpenAPIService implements ApplicationContextAware {
 	 */
 	OpenAPIService(Optional<OpenAPI> openAPI, SecurityService securityParser,
 			SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
-			Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers) {
+			Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
+			Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
 		if (openAPI.isPresent()) {
 			this.openAPI = openAPI.get();
 			if (this.openAPI.getComponents() == null)
@@ -202,6 +208,7 @@ public class OpenAPIService implements ApplicationContextAware {
 		this.securityParser = securityParser;
 		this.springDocConfigProperties = springDocConfigProperties;
 		this.openApiBuilderCustomisers = openApiBuilderCustomisers;
+		this.serverBaseUrlCustomisers = serverBaseUrlCustomisers;
 		if (springDocConfigProperties.isUseFqn())
 			TypeNameResolver.std.setUseFqn(true);
 	}
@@ -466,7 +473,15 @@ public class OpenAPIService implements ApplicationContextAware {
 	 * @param serverBaseUrl the server base url
 	 */
 	public void setServerBaseUrl(String serverBaseUrl) {
-		this.serverBaseUrl = serverBaseUrl;
+		String customServerBaseUrl = serverBaseUrl;
+
+		if (serverBaseUrlCustomisers != null && serverBaseUrlCustomisers.isPresent()) {
+			for (ServerBaseUrlCustomizer customiser : serverBaseUrlCustomisers.get()) {
+				customServerBaseUrl = customiser.customise(customServerBaseUrl);
+			}
+		}
+
+		this.serverBaseUrl = customServerBaseUrl;
 	}
 
 	/**
@@ -808,8 +823,17 @@ public class OpenAPIService implements ApplicationContextAware {
 		return securityParser;
 	}
 
+	/**
+	 * Gets server base URL
+	 * 
+	 * @return the server base URL
+	 */
+	public String getServerBaseUrl() {
+		return serverBaseUrl;
+	}
+
 	@Override
 	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 		this.context = applicationContext;
 	}
-}
+}
\ No newline at end of file
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
index 6a3f89b8e..ba2c0f21f 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/SpringDocConfiguration.java
@@ -53,6 +53,7 @@ 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;
 import org.springdoc.core.providers.CloudFunctionProvider;
 import org.springdoc.core.providers.JavadocProvider;
@@ -225,8 +226,9 @@ public class SpringDocConfiguration {
 	OpenAPIService openAPIBuilder(Optional<OpenAPI> openAPI,
 			SecurityService securityParser,
 			SpringDocConfigProperties springDocConfigProperties,PropertyResolverUtils propertyResolverUtils,
-			Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers) {
-		return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers);
+			Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
+			Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers) {
+		return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers);
 	}
 
 	/**
@@ -579,4 +581,4 @@ public class SpringDocConfiguration {
 			return new SpringCloudFunctionProvider(functionCatalog, genericResponseService, springDocConfigProperties);
 		}
 	}
-}
+}
\ No newline at end of file
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/ServerBaseUrlCustomizer.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/ServerBaseUrlCustomizer.java
new file mode 100644
index 000000000..ad7e04d93
--- /dev/null
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/customizers/ServerBaseUrlCustomizer.java
@@ -0,0 +1,17 @@
+package org.springdoc.core.customizers;
+
+/**
+ * The interface Server Base URL customiser.
+ * @author skylar-stark
+ */
+@FunctionalInterface
+public interface ServerBaseUrlCustomizer {
+
+	/**
+	 * Customise.
+	 * 
+	 * @param serverBaseUrl the serverBaseUrl.
+	 * @return the customised serverBaseUrl
+	 */
+	public String customise(String serverBaseUrl);
+}


Test patch:
diff --git a/springdoc-openapi-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java b/springdoc-openapi-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java
index a3bb0db70..207d9ce45 100644
--- a/springdoc-openapi-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java
+++ b/springdoc-openapi-common/src/test/java/org/springdoc/api/AbstractOpenApiResourceTest.java
@@ -20,6 +20,7 @@
 
 package org.springdoc.api;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -49,6 +50,7 @@ import org.springdoc.core.SpringDocConfigProperties;
 import org.springdoc.core.SpringDocProviders;
 import org.springdoc.core.customizers.OpenApiCustomiser;
 import org.springdoc.core.customizers.OperationCustomizer;
+import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
 import org.springdoc.core.filters.OpenApiMethodFilter;
 import org.springdoc.core.fn.RouterOperation;
 
@@ -111,6 +113,7 @@ class AbstractOpenApiResourceTest {
 		openAPI = new OpenAPI();
 		openAPI.setPaths(new Paths().addPathItem(PATH, new PathItem()));
 		ReflectionTestUtils.setField(openAPIService, "cachedOpenAPI", new HashMap<>());
+		ReflectionTestUtils.setField(openAPIService, "serverBaseUrlCustomisers", Optional.empty());
 
 		when(openAPIService.getCalculatedOpenAPI()).thenReturn(openAPI);
 		when(openAPIService.getContext()).thenReturn(context);
@@ -215,6 +218,71 @@ class AbstractOpenApiResourceTest {
 		assertThat(after.getServers().get(0).getUrl(), is(customUrl));
 	}
 
+	@Test
+	void serverBaseUrlCustomisersTest() throws InterruptedException {
+		when(openAPIService.updateServers(any())).thenCallRealMethod();
+		when(openAPIService.getCachedOpenAPI(any())).thenCallRealMethod();
+		doAnswer(new CallsRealMethods()).when(openAPIService).setServerBaseUrl(any());
+		doAnswer(new CallsRealMethods()).when(openAPIService).setCachedOpenAPI(any(), any());
+
+		SpringDocConfigProperties properties = new SpringDocConfigProperties();
+		properties.setPreLoadingEnabled(true);
+
+		resource = new EmptyPathsOpenApiResource(
+				GROUP_NAME,
+				openAPIBuilderObjectFactory,
+				requestBuilder,
+				responseBuilder,
+				operationParser,
+				Optional.empty(),
+				Optional.empty(),
+				Optional.empty(),
+				properties,
+				springDocProviders
+		);
+
+		// wait for executor to be done
+		Thread.sleep(1_000);
+
+		Locale locale = Locale.US;
+
+		// Test that setting generated URL works fine with no customisers present
+		String generatedUrl = "https://generated-url.com/context-path";
+		openAPIService.setServerBaseUrl(generatedUrl);
+		openAPIService.updateServers(openAPI);
+		OpenAPI after = resource.getOpenApi(locale);
+		assertThat(after.getServers().get(0).getUrl(), is(generatedUrl));
+
+		// Test that adding a serverBaseUrlCustomiser has the desired effect
+		ServerBaseUrlCustomizer serverBaseUrlCustomiser = serverBaseUrl -> serverBaseUrl.replace("/context-path", "");
+		List<ServerBaseUrlCustomizer> serverBaseUrlCustomiserList = new ArrayList<>();
+		serverBaseUrlCustomiserList.add(serverBaseUrlCustomiser);
+
+		ReflectionTestUtils.setField(openAPIService, "serverBaseUrlCustomisers", Optional.of(serverBaseUrlCustomiserList));
+		openAPIService.setServerBaseUrl(generatedUrl);
+		openAPIService.updateServers(openAPI);
+		after = resource.getOpenApi(locale);
+		assertThat(after.getServers().get(0).getUrl(), is("https://generated-url.com"));
+
+		// Test that serverBaseUrlCustomisers are performed in order
+		generatedUrl = "https://generated-url.com/context-path/second-path";
+		ServerBaseUrlCustomizer serverBaseUrlCustomiser2 = serverBaseUrl -> serverBaseUrl.replace("/context-path/second-path", "");
+		serverBaseUrlCustomiserList.add(serverBaseUrlCustomiser2);
+
+		openAPIService.setServerBaseUrl(generatedUrl);
+		openAPIService.updateServers(openAPI);
+		after = resource.getOpenApi(locale);
+		assertThat(after.getServers().get(0).getUrl(), is("https://generated-url.com/second-path"));
+
+		// Test that all serverBaseUrlCustomisers in the List are performed
+		ServerBaseUrlCustomizer serverBaseUrlCustomiser3 = serverBaseUrl -> serverBaseUrl.replace("/second-path", "");
+		serverBaseUrlCustomiserList.add(serverBaseUrlCustomiser3);
+
+		openAPIService.setServerBaseUrl(generatedUrl);
+		openAPIService.updateServers(openAPI);
+		after = resource.getOpenApi(locale);
+		assertThat(after.getServers().get(0).getUrl(), is("https://generated-url.com"));
+	}
 
 	private static class EmptyPathsOpenApiResource extends AbstractOpenApiResource {