diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot-contextpath/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-boot-contextpath/src/main/resources/application.properties index a9d87423d0b..caa1da3100f 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-boot-contextpath/src/main/resources/application.properties +++ b/flow-tests/vaadin-spring-tests/test-spring-boot-contextpath/src/main/resources/application.properties @@ -1,3 +1,4 @@ server.port=8888 server.servlet.contextPath=/context +vaadin.exclude-urls=/swagger-ui/** diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot-only-prepare/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-boot-only-prepare/src/main/resources/application.properties index 3b4370b224f..a643255ccc4 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-boot-only-prepare/src/main/resources/application.properties +++ b/flow-tests/vaadin-spring-tests/test-spring-boot-only-prepare/src/main/resources/application.properties @@ -1,2 +1,3 @@ server.port=8888 vaadin.blacklisted-packages=vaadin-spring/target/test-classes +vaadin.excludeUrls=/swagger-ui/** diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy/src/main/resources/application.properties index a2cd7d0e32f..4ec6c2a7da4 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy/src/main/resources/application.properties +++ b/flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy/src/main/resources/application.properties @@ -1,3 +1,5 @@ server.port=8888 proxy.port=9999 proxy.path=/public/path +vaadin.excludeUrls=/swagger-ui/** +springdoc.swagger-ui.configUrl=/public/path/v3/api-docs/swagger-config diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot-scan/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-boot-scan/src/main/resources/application.properties index c4e658f9e20..9627637e21c 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-boot-scan/src/main/resources/application.properties +++ b/flow-tests/vaadin-spring-tests/test-spring-boot-scan/src/main/resources/application.properties @@ -1 +1,2 @@ server.port=8888 +vaadin.exclude-urls=/swagger-ui/** diff --git a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/resources/application.properties index c4e658f9e20..9627637e21c 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/resources/application.properties +++ b/flow-tests/vaadin-spring-tests/test-spring-boot/src/main/resources/application.properties @@ -1 +1,2 @@ server.port=8888 +vaadin.exclude-urls=/swagger-ui/** diff --git a/flow-tests/vaadin-spring-tests/test-spring-common/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-common/pom.xml index 4a08c90cf19..69c94042397 100644 --- a/flow-tests/vaadin-spring-tests/test-spring-common/pom.xml +++ b/flow-tests/vaadin-spring-tests/test-spring-common/pom.xml @@ -76,6 +76,24 @@ jaxb-impl ${jaxb.version} + + + + org.springdoc + springdoc-openapi-ui + 1.6.11 + + + io.github.classgraph + classgraph + 4.8.149 + + + org.yaml + snakeyaml + 1.33 + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-common/src/test/java/com/vaadin/flow/spring/test/SwaggerIT.java b/flow-tests/vaadin-spring-tests/test-spring-common/src/test/java/com/vaadin/flow/spring/test/SwaggerIT.java new file mode 100644 index 00000000000..fa841872451 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-common/src/test/java/com/vaadin/flow/spring/test/SwaggerIT.java @@ -0,0 +1,19 @@ +package com.vaadin.flow.spring.test; + +import org.junit.Test; + +public class SwaggerIT extends AbstractSpringTest { + + @Override + protected String getTestPath() { + return "/swagger-ui.html"; + } + + @Test + public void swaggerUIShown() { + open(); + waitUntil( + driver -> driver.getPageSource().contains("OpenAPI definition") + || driver.getPageSource().contains("Swagger Petstore")); + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-war/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-war/src/main/resources/application.properties new file mode 100644 index 00000000000..fa25a35731f --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-war/src/main/resources/application.properties @@ -0,0 +1 @@ +vaadin.exclude-urls=/swagger-ui/** diff --git a/flow-tests/vaadin-spring-tests/test-spring/pom.xml b/flow-tests/vaadin-spring-tests/test-spring/pom.xml index 74a95d912ea..0822d3a34d3 100644 --- a/flow-tests/vaadin-spring-tests/test-spring/pom.xml +++ b/flow-tests/vaadin-spring-tests/test-spring/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 com.vaadin @@ -42,6 +41,12 @@ com.vaadin vaadin-test-spring-common ${project.version} + + + org.springframework.boot + spring-boot-autoconfigure + + @@ -103,6 +108,15 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/SwaggerIT.java + + + org.eclipse.jetty jetty-maven-plugin diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinConfigurationProperties.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinConfigurationProperties.java index e1ab085b5fb..5686f664c9c 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinConfigurationProperties.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinConfigurationProperties.java @@ -49,6 +49,22 @@ public static String getUrlMapping(Environment environment) { .map(conf -> conf.getUrlMapping()).orElse(null); } + /** + * Gets the excluded URLs using the given environment. + * + * This is needed only when VaadinConfigurationProperties is not available + * for injection, e.g. when using Spring without Boot. + * + * @param environment + * the application environment + * @return the excluded URLs or null if none is defined + */ + public static List getExcludedUrls(Environment environment) { + return Binder.get(environment) + .bind("vaadin", VaadinConfigurationProperties.class) + .map(conf -> conf.getExcludeUrls()).orElse(null); + } + /** * Base URL mapping of the Vaadin servlet. */ @@ -84,6 +100,12 @@ public static String getUrlMapping(Environment environment) { */ private boolean launchBrowser = false; + /** + * URL patterns that should not be handled by the Vaadin servlet when mapped + * to the context root. + */ + private List excludeUrls; + public static class Pnpm { private boolean enable; @@ -261,4 +283,25 @@ public List getWhitelistedPackages() { public void setWhitelistedPackages(List whitelistedPackages) { this.whitelistedPackages = new ArrayList<>(whitelistedPackages); } + + /** + * Get a list of URL patterns that are not handled by the Vaadin servlet + * when it is mapped to the context root. + * + * @return a list of url patterns to exclude + */ + public List getExcludeUrls() { + return excludeUrls; + } + + /** + * Set a list of URL patterns that are not handled by the Vaadin servlet + * when it is mapped to the context root. + * + * @param excludeUrls + * a list of url patterns to exclude + */ + public void setExcludeUrls(List excludeUrls) { + this.excludeUrls = excludeUrls; + } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletConfiguration.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletConfiguration.java index 3d49cddeb14..fef1f3531a4 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletConfiguration.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletConfiguration.java @@ -15,17 +15,28 @@ */ package com.vaadin.flow.spring; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; +import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.mvc.ServletForwardingController; +import org.springframework.web.util.UrlPathHelper; import com.vaadin.flow.server.VaadinServlet; @@ -47,6 +58,83 @@ public class VaadinServletConfiguration { static final String VAADIN_SERVLET_MAPPING = "/vaadinServlet/*"; + public static final String EXCLUDED_URLS_PROPERTY = "vaadin.excludeUrls"; + + /** + * Gets the excluded URLs in a way compatible with both plain Spring and + * Spring Boot. + * + * @param environment + * the application environment + * @return the excluded URLs or null if none is defined + */ + private static List getExcludedUrls(Environment environment) { + if (SpringUtil.isSpringBoot()) { + try { + return (List) Class.forName( + "com.vaadin.flow.spring.VaadinConfigurationProperties") + .getMethod("getExcludedUrls", Environment.class) + .invoke(null, environment); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException + | SecurityException | ClassNotFoundException e) { + LoggerFactory.getLogger(RootMappedCondition.class).error( + "Unable to find excluded URLs from properties", e); + return null; + } + } else { + String value = environment.getProperty(EXCLUDED_URLS_PROPERTY); + if (value == null || value.isEmpty()) { + return Collections.emptyList(); + } else { + return Arrays.stream(value.split(",")).map(url -> url.trim()) + .collect(Collectors.toList()); + } + } + } + + private static class RootExcludeHandler extends SimpleUrlHandlerMapping { + private List excludeUrls; + private AntPathMatcher matcher; + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + public RootExcludeHandler(List excludeUrls, + Controller vaadinForwardingController) { + this.excludeUrls = excludeUrls; + matcher = new AntPathMatcher(); + + setOrder(Ordered.LOWEST_PRECEDENCE - 1); + + // This is /** and not /* so that it is not interpreted as a + // "default handler" + // and we can override the behavior in getHandlerInternal + setUrlMap(Collections.singletonMap("/**", + vaadinForwardingController)); + } + + @Override + protected Object getHandlerInternal(HttpServletRequest request) + throws Exception { + if (excludeUrls != null && !excludeUrls.isEmpty()) { + String requestPath = urlPathHelper + .getPathWithinApplication(request); + for (String pattern : excludeUrls) { + if (matcher.match(pattern, requestPath)) { + getLogger().debug( + "Ignoring request to {} excluded by {}", + requestPath, pattern); + return null; + } + } + } + return super.getHandlerInternal(request); + } + + protected Logger getLogger() { + return LoggerFactory.getLogger(getClass()); + } + + } /** * Makes an url handler mapping allowing to forward requests from a @@ -56,14 +144,9 @@ public class VaadinServletConfiguration { * servlet */ @Bean - public SimpleUrlHandlerMapping vaadinRootMapping() { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - mapping.setOrder(Ordered.LOWEST_PRECEDENCE - 1); - - mapping.setUrlMap( - Collections.singletonMap("/*", vaadinForwardingController())); - - return mapping; + public RootExcludeHandler vaadinRootMapping(Environment environment) { + return new RootExcludeHandler(getExcludedUrls(environment), + vaadinForwardingController()); } /** diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 3998ec818a2..2658506ac5d 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -79,6 +79,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.DispatcherServletRegistrationBeanConfig", "com\\.vaadin\\.flow\\.spring\\.VaadinApplicationConfiguration", "com\\.vaadin\\.flow\\.spring\\.VaadinServletConfiguration", + "com\\.vaadin\\.flow\\.spring\\.VaadinServletConfiguration\\$RootExcludeHandler", "com\\.vaadin\\.flow\\.spring\\.VaadinScopesConfig", "com\\.vaadin\\.flow\\.spring\\.VaadinSpringSecurity", "com\\.vaadin\\.flow\\.spring\\.SpringBootAutoConfiguration",