diff --git a/flow-tests/vaadin-spring-tests/pom.xml b/flow-tests/vaadin-spring-tests/pom.xml index d9db694125d..6d73ceaca51 100644 --- a/flow-tests/vaadin-spring-tests/pom.xml +++ b/flow-tests/vaadin-spring-tests/pom.xml @@ -335,6 +335,7 @@ test-spring-boot-only-prepare test-spring-white-list + test-spring-filter-packages diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml new file mode 100644 index 00000000000..4bfc2a17e1a --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + vaadin-test-spring-filter-packages + com.vaadin + 24.5-SNAPSHOT + + vaadin-test-spring-filter-packages-lib-allowed + Library with vaadin.allowed-packages property + + + jar + + true + + + + + com.vaadin + flow-server + ${project.version} + + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java new file mode 100644 index 00000000000..42753d36dea --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/BlockedRoute.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.allowed; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +/** + * Blocked route in a jar package. + */ +@Route("blocked-route-in-jar") +public class BlockedRoute extends Div { + + public BlockedRoute() { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java new file mode 100644 index 00000000000..42bf8c6cced --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/CustomVaadinServiceInitListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.allowed.startup; + +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.VaadinServiceInitListener; + +public class CustomVaadinServiceInitListener + implements VaadinServiceInitListener { + + @Override + public void serviceInit(ServiceInitEvent event) { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java new file mode 100644 index 00000000000..5b265fe6147 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/java/com/vaadin/flow/spring/test/allowed/startup/vaadin/AllowedRoute.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.allowed.startup.vaadin; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route(value = "allowed-route-in-jar") +public class AllowedRoute extends Div { + + public AllowedRoute() { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties new file mode 100644 index 00000000000..27b339a7c3b --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed/src/main/resources/META-INF/VAADIN/package.properties @@ -0,0 +1 @@ +vaadin.allowed-packages=com/vaadin/flow/spring/test/allowed/startup diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml new file mode 100644 index 00000000000..15a6afbb362 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + vaadin-test-spring-filter-packages + com.vaadin + 24.5-SNAPSHOT + + vaadin-test-spring-filter-packages-lib-blocked + Library with vaadin.blocked-packages property + + + jar + + true + + + + + com.vaadin + flow-server + ${project.version} + + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java new file mode 100644 index 00000000000..10d49a1a14c --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/ScannedAllowedRoute.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.blocked; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("allowed-route-in-another-jar") +public class ScannedAllowedRoute extends Div { + + public ScannedAllowedRoute() { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java new file mode 100644 index 00000000000..35fc7c8bcb2 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/BlockedCustomVaadinServiceInitListener.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.blocked.startup; + +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.VaadinServiceInitListener; + +public class BlockedCustomVaadinServiceInitListener + implements VaadinServiceInitListener { + + @Override + public void serviceInit(ServiceInitEvent event) { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java new file mode 100644 index 00000000000..f7f46d1bcf3 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/java/com/vaadin/flow/spring/test/blocked/startup/vaadin/ScannedBlockedRoute.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.blocked.startup.vaadin; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route(value = "blocked-route-in-scanned-jar") +public class ScannedBlockedRoute extends Div { + + public ScannedBlockedRoute() { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties new file mode 100644 index 00000000000..7bb9c099f12 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked/src/main/resources/META-INF/VAADIN/package.properties @@ -0,0 +1 @@ +vaadin.blocked-packages=com/vaadin/flow/spring/test/blocked/startup diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml new file mode 100644 index 00000000000..71fb3f7a0ce --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + vaadin-test-spring-filter-packages + com.vaadin + 24.5-SNAPSHOT + + vaadin-test-spring-filter-packages-lib-exclude + Library with vaadin.blocked-jar property + + + jar + + true + + + + + com.vaadin + flow-server + ${project.version} + + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml new file mode 100644 index 00000000000..0a1b762ffbc --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + vaadin-spring-tests + com.vaadin + 24.5-SNAPSHOT + + vaadin-test-spring-filter-packages + The main module for a Spring boot package filter tests + + + pom + + true + + + + lib-allowed + lib-blocked + lib-exclude + ui + + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml new file mode 100644 index 00000000000..cf88cab2bc7 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/pom.xml @@ -0,0 +1,134 @@ + + + + + 4.0.0 + + vaadin-test-spring-filter-packages + com.vaadin + 24.5-SNAPSHOT + + vaadin-test-spring-filter-packages-ui + jar + + + true + + + + + com.vaadin + vaadin-spring + ${project.version} + + + + com.vaadin + vaadin-dev-server + ${project.version} + + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.apache.logging.log4j + log4j-api + + + + + + org.springframework.boot + spring-boot-starter-jetty + + + + com.vaadin + vaadin-test-spring-filter-packages-lib-allowed + ${project.version} + + + + com.vaadin + vaadin-test-spring-filter-packages-lib-blocked + ${project.version} + + + + + + + + com.vaadin + flow-maven-plugin + ${project.version} + + + + prepare-frontend + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + 100 + 2500 + 9009 + + -Xdebug + -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18888 + + + + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + + + diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java new file mode 100644 index 00000000000..15930edffee --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/ClassScannerView.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.filtering; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route("") +public class ClassScannerView extends Div { + + public static Set> classes = Collections.emptySet(); + public static final String SCANNED_CLASSES = "scanned-classes"; + + public ClassScannerView() { + Span scannedClasses = new Span(classes.stream() + .map(Class::getSimpleName).collect(Collectors.joining(","))); + scannedClasses.setId(SCANNED_CLASSES); + add(scannedClasses); + } + +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java new file mode 100644 index 00000000000..4639a0371a7 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/TestServletInitializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.filtering; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.vaadin.flow.spring.VaadinServletContextInitializer; + +/** + * The entry point of the Spring Boot application. + */ +@SpringBootApplication +@Configuration +public class TestServletInitializer { + + public static void main(String[] args) { + SpringApplication.run(TestServletInitializer.class, args); + } + + @Bean + public VaadinServletContextInitializer vaadinServletContextInitializer( + ApplicationContext context) { + + return new VaadinServletContextInitializer(context) { + @Override + protected Set> findClassesForDevMode( + Set basePackages, + List> annotations, + List> superTypes) { + ClassScannerView.classes = super.findClassesForDevMode( + basePackages, annotations, superTypes); + return ClassScannerView.classes; + } + }; + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java new file mode 100644 index 00000000000..2345e312883 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/java/com/vaadin/flow/spring/test/filtering/blocked/BlockedView.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.filtering.blocked; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("blocked") +public class BlockedView extends Div { + + public BlockedView() { + } +} diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties new file mode 100644 index 00000000000..a9214ff1f4f --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/main/resources/application.properties @@ -0,0 +1,2 @@ +server.port=8888 +vaadin.blocked-packages=blocked,com/vaadin/flow/spring/test/filtering/blocked diff --git a/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java new file mode 100644 index 00000000000..7ba72c0a4f6 --- /dev/null +++ b/flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui/src/test/java/com/vaadin/flow/spring/test/filtering/ClassScannerIT.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.test.filtering; + +import java.util.List; +import java.util.stream.Stream; + +import com.vaadin.flow.spring.test.allowed.startup.CustomVaadinServiceInitListener; +import com.vaadin.flow.spring.test.allowed.startup.vaadin.AllowedRoute; +import com.vaadin.flow.spring.test.allowed.BlockedRoute; +import com.vaadin.flow.spring.test.blocked.ScannedAllowedRoute; +import com.vaadin.flow.spring.test.blocked.startup.BlockedCustomVaadinServiceInitListener; +import com.vaadin.flow.spring.test.blocked.startup.vaadin.ScannedBlockedRoute; +import com.vaadin.flow.spring.test.filtering.blocked.BlockedView; +import com.vaadin.flow.testutil.ChromeBrowserTest; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; + +/** + * Primary target of this IT is class scanning of DevModeServletContextListener + * in {@link com.vaadin.flow.spring.VaadinServletContextInitializer} and + * especially usage of {@code vaadin.blocked-packages} and + * {@code vaadin.allowed-packages} in a multi-module Maven project with jar + * packaged dependencies. + */ +public class ClassScannerIT extends ChromeBrowserTest { + + @Test + public void uiModule_withBlockedPackages() { + open(); + assertClassAllowed(ClassScannerView.class.getSimpleName()); + assertClassBlocked(BlockedView.class.getSimpleName()); + } + + @Test + public void libAllowedModule_withAllowedPackagesJar() { + open(); + assertClassAllowed(AllowedRoute.class.getSimpleName()); + assertClassAllowed( + CustomVaadinServiceInitListener.class.getSimpleName()); + assertClassBlocked(BlockedRoute.class.getSimpleName()); + } + + @Test + public void libBlockedModule_withBlockedPackagesJar() { + open(); + assertClassBlocked(ScannedBlockedRoute.class.getSimpleName()); + assertClassBlocked( + BlockedCustomVaadinServiceInitListener.class.getSimpleName()); + assertClassAllowed(ScannedAllowedRoute.class.getSimpleName()); + } + + private void assertClassAllowed(String className) { + Assert.assertTrue(className + " should be allowed.", + getScannedClasses().contains(className)); + } + + private void assertClassBlocked(String className) { + Assert.assertFalse(className + " should be blocked.", + getScannedClasses().contains(className)); + } + + private List getScannedClasses() { + return Stream.of(findElement(By.id(ClassScannerView.SCANNED_CLASSES)) + .getText().split(",")).map(String::trim).toList(); + } + + @Override + protected String getTestPath() { + return "/"; + } +} diff --git a/scripts/computeMatrix.js b/scripts/computeMatrix.js index 7040642ceb0..a713f6ed112 100755 --- a/scripts/computeMatrix.js +++ b/scripts/computeMatrix.js @@ -90,6 +90,10 @@ const moduleWeights = { 'flow-tests/vaadin-spring-tests/test-spring-boot-scan': { pos: 4, weight: 4 }, 'flow-tests/vaadin-spring-tests/test-spring-boot-contextpath': { pos: 4, weight: 4 }, 'flow-tests/vaadin-spring-tests/test-spring-boot-reverseproxy': { pos: 4, weight: 4 }, + 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/ui': { pos: 4, weight: 4 }, + 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-allowed': { pos: 4, weight: 4 }, + 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-blocked': { pos: 4, weight: 4 }, + 'flow-tests/vaadin-spring-tests/test-spring-filter-packages/lib-exclude': { pos: 4, weight: 4 }, 'flow-tests/vaadin-spring-tests/test-spring-white-list': { pos: 4, weight: 3 }, 'flow-tests/vaadin-spring-tests/test-spring-common': { pos: 4, weight: 2 }, 'flow-tests/vaadin-spring-tests/test-spring-helpers': { pos: 4, weight: 1 }, diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java index d201b142cd0..7b6380aae31 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/VaadinServletContextInitializer.java @@ -37,6 +37,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -85,6 +86,7 @@ import com.vaadin.flow.server.startup.WebComponentExporterAwareValidator; import com.vaadin.flow.server.webcomponent.WebComponentConfigurationRegistry; import com.vaadin.flow.spring.VaadinScanPackagesRegistrar.VaadinScanPackages; +import com.vaadin.flow.spring.io.FilterableResourceResolver; import com.vaadin.flow.theme.Theme; /** @@ -523,9 +525,8 @@ public void failFastContextInitialized(ServletContextEvent event) collectHandleTypes(devModeHandlerManager.getHandlesTypes(), annotations, superTypes); - Set> classes = findByAnnotationOrSuperType(basePackages, - customLoader, annotations, superTypes) - .collect(Collectors.toSet()); + Set> classes = findClassesForDevMode(basePackages, + annotations, superTypes); if (devModeCachingEnabled) { classes.addAll(ReloadCache.jarClasses); @@ -592,6 +593,13 @@ private boolean isScanOnlySet() { } + protected Set> findClassesForDevMode(Set basePackages, + List> annotations, + List> superTypes) { + return findByAnnotationOrSuperType(basePackages, customLoader, + annotations, superTypes).collect(Collectors.toSet()); + } + private class WebComponentServletContextListener implements FailFastServletContextListener { @@ -801,7 +809,7 @@ private CompositeServletContextListener createCompositeListener( private Stream> findByAnnotation(Collection packages, Class... annotations) { - return findByAnnotation(packages, appContext, annotations); + return findByAnnotation(packages, customLoader, annotations); } private Stream> findByAnnotation(Collection packages, @@ -812,7 +820,7 @@ private Stream> findByAnnotation(Collection packages, Stream> findBySuperType(Collection packages, Class type) { - return findBySuperType(packages, appContext, type); + return findBySuperType(packages, customLoader, type); } private Stream> findBySuperType(Collection packages, @@ -921,7 +929,7 @@ private static void collectHandleTypes(Class[] handleTypes, * with atmosphere we skip known packaged from our resources collection. */ private static class CustomResourceLoader - extends PathMatchingResourcePatternResolver { + extends FilterableResourceResolver { private final PrefixTree scanNever = new PrefixTree(DEFAULT_SCAN_NEVER); @@ -1013,22 +1021,25 @@ private Resource[] collectResources(String locationPattern) .replace(".class", ""); ReloadCache.jarClassNames.add(className); } - if (shouldPathBeScanned(relativePath)) { + if (shouldPathBeScanned(relativePath, + path.substring(0, index))) { resourcesList.add(resource); } } else { List parents = rootPaths.stream() - .filter(path::startsWith) - .collect(Collectors.toList()); + .filter(path::startsWith).toList(); if (parents.isEmpty()) { throw new IllegalStateException(String.format( "Parent resource of [%s] not found in the resources!", path)); } - + AtomicBoolean parentIsAllowedByPackageProperties = new AtomicBoolean( + true); if (parents.stream() .anyMatch(parent -> shouldPathBeScanned( - path.substring(parent.length())))) { + path.substring(parent.length()), + parent, + parentIsAllowedByPackageProperties))) { resourcesList.add(resource); } } @@ -1047,9 +1058,67 @@ private Resource[] collectResources(String locationPattern) return resourcesList.toArray(new Resource[0]); } + /** + * Checks if the given path should be scanned. + * + * @param path + * the relative path to check + * @return {@code true} if the path should be scanned, {@code false} + * otherwise + */ private boolean shouldPathBeScanned(String path) { return scanAlways.hasPrefix(path) || !scanNever.hasPrefix(path); } + + /** + * Checks if the given path should be scanned. Checks + * package.properties. + * + * @param path + * the relative path to check + * @param rootPath + * the root path of the resource. Also, a key for cached + * properties. + * @return {@code true} if the path should be scanned, {@code false} + * otherwise + */ + private boolean shouldPathBeScanned(String path, String rootPath) { + return shouldPathBeScanned(path, rootPath, null); + } + + /** + * Checks if the given path should be scanned. Checks + * package.properties. + * + * @param path + * the relative path to check + * @param rootPath + * the root path of the resource. Also, a key for cached + * properties. + * @param parentIsAllowedByPackageProperties + * This value is used as a default value for the + * package.properties check. Value of the object may be + * changed, if result changes. null defaults to true. + * @return {@code true} if the path should be scanned, {@code false} + * otherwise + */ + private boolean shouldPathBeScanned(String path, String rootPath, + AtomicBoolean parentIsAllowedByPackageProperties) { + if (shouldPathBeScanned(path)) { + // The given parentIsAllowedByPackageProperties ensures that + // result from the previous check follows up here as a default + // value. + boolean defaultValue = parentIsAllowedByPackageProperties == null + || parentIsAllowedByPackageProperties.get(); + boolean allowed = isAllowedByPackageProperties(rootPath, path, + defaultValue); + if (parentIsAllowedByPackageProperties != null) { + parentIsAllowedByPackageProperties.set(allowed); + } + return allowed; + } + return false; + } } private static Logger getLogger() { diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java new file mode 100644 index 00000000000..10b143860a8 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/io/FilterableResourceResolver.java @@ -0,0 +1,388 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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 + * + * http://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 com.vaadin.flow.spring.io; + +import java.io.IOException; +import java.io.Serializable; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.ResourceUtils; + +/** + * A {@link PathMatchingResourcePatternResolver} that allows filtering resources + * by package properties. The resolver reads META-INF/VAADIN/package.properties + * from JAR files and directories. The properties file can contain a list of the + * allowed or blocked packages. If it contains both, the allowed packages take + * precedence. Allowed packages are mapped with the key + * "vaadin.allowed-packages". Blocked packages are mapped with the key + * "vaadin.blocked-packages". + * + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + */ +public class FilterableResourceResolver + extends PathMatchingResourcePatternResolver implements Serializable { + + private static final String JAR_PROTOCOL = "jar:"; + private static final String JAR_KEY = ".jar!/"; + private static final String PACKAGE_PROPERTIES_PATH = "META-INF/VAADIN/package.properties"; + + /** + * The property key for allowed packages. + */ + public static final String ALLOWED_PACKAGES_PROPERTY = "vaadin.allowed-packages"; + /** + * The property key for blocked packages. + */ + public static final String BLOCKED_PACKAGES_PROPERTY = "vaadin.blocked-packages"; + + private final Map propertiesCache = new HashMap<>(); + + private record PackageInfo(Set allowedPackages, + Set blockedPackages) implements Serializable { + } + + /** + * Creates a new instance of the resolver. + * + * @param resourceLoader + * the resource loader to use + */ + public FilterableResourceResolver(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(FilterableResourceResolver.class); + } + + private String toJarPath(String path) { + return path.substring(0, path.lastIndexOf(JAR_KEY) + JAR_KEY.length()); + } + + private String pathToKey(String path) { + String key = path.substring(0, path.lastIndexOf(JAR_KEY)); + if (key.startsWith(JAR_PROTOCOL)) { + // clear the jar: prefix + key = key.substring(4); + } + return key; + } + + /** + * Checks if the given path is a JAR file. + * + * @param path + * the path to check. Not null. + * @return {@code true} if the path is a JAR file, {@code false} otherwise + */ + protected boolean isJar(String path) { + return path.lastIndexOf(JAR_KEY) != -1; + } + + private Resource doResolveRootDirResource(Resource original) + throws IOException { + String rootDirPath = original.getURI().getPath(); + if (rootDirPath != null) { + int index = rootDirPath.lastIndexOf(JAR_KEY); + if (index != -1) { + String jarPath = rootDirPath.substring(0, + index + JAR_KEY.length()); + return new UrlResource(jarPath); + } + } + return super.resolveRootDirResource(original); + } + + /** + * Find all resources in jar files that match the given location pattern via + * the Ant-style PathMatcher. Supports additional filtering based on allowed + * or blocked packages in package.properties. + * + * @param rootDirResource + * the root directory as Resource + * @param rootDirUrl + * the pre-resolved root directory URL + * @param subPattern + * the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException + * in case of I/O error + */ + @Override + protected Set doFindPathMatchingJarResources( + Resource rootDirResource, URL rootDirUrl, String subPattern) + throws IOException { + String path = rootDirResource.getURI().toString(); + cachePackageProperties(path, rootDirResource, rootDirUrl); + + if (isBlockedJar(rootDirResource)) { + return Collections.emptySet(); + } + return super.doFindPathMatchingJarResources(rootDirResource, rootDirUrl, + subPattern); + } + + /** + * Find all class path resources with the given path via the configured + * ClassLoader. Called by findAllClassPathResources(String). Supports + * additional filtering based on allowed or blocked packages in + * package.properties. + * + * @param path + * the absolute path within the class path (never a leading + * slash) + * @return a mutable Set of matching Resource instances + * @throws IOException + * in case of I/O errors + */ + @Override + protected Set doFindAllClassPathResources(String path) + throws IOException { + var result = super.doFindAllClassPathResources(path); + result.removeIf(res -> { + cachePackageProperties(res); + return isBlockedJar(res); + }); + return result; + } + + private void cachePackageProperties(String path, Resource rootDirResource, + URL rootDirUrl) throws IOException { + if (!propertiesCache.containsKey(path)) { + if (isJar(path)) { + String jarPath = pathToKey(path); + propertiesCache.put(jarPath, readPackageProperties(rootDirUrl, + path, doResolveRootDirResource(rootDirResource))); + getLogger().trace("Caching package.properties of JAR {}", path); + } else { + Resource resource = doFindPathMatchingFileResources( + rootDirResource, PACKAGE_PROPERTIES_PATH).stream() + .findFirst().orElse(null); + Properties properties = resource != null + ? PropertiesLoaderUtils.loadProperties(resource) + : null; + propertiesCache.put(path, createPackageInfo(properties)); + getLogger().trace("Caching package.properties of directory {}", + path); + } + } + } + + private void cachePackageProperties(Resource res) { + try { + Resource rootDirResource = convertClassLoaderURL(res.getURL()); + String rootDirPath = rootDirResource.getURI().toString(); + String rootPath = rootDirResource.getURI().getPath(); + if (rootPath != null && isJar(rootDirPath)) { + String jarPath = toJarPath(rootDirPath); + String key = pathToKey(rootPath); + if (!propertiesCache.containsKey(key)) { + propertiesCache.put(key, readPackageProperties(null, + jarPath, rootDirResource)); + getLogger().trace("Caching package.properties of JAR {}", + rootPath); + } + } else if (!propertiesCache.containsKey(rootPath)) { + Resource resource = doFindPathMatchingFileResources( + rootDirResource, PACKAGE_PROPERTIES_PATH).stream() + .findFirst().orElse(null); + Properties properties = resource != null + ? PropertiesLoaderUtils.loadProperties(resource) + : null; + propertiesCache.put(rootPath, createPackageInfo(properties)); + getLogger().trace("Caching package.properties of directory {}", + rootPath); + } + + } catch (IOException e) { + getLogger().warn("Failed to find {} for path {}", + PACKAGE_PROPERTIES_PATH, res, e); + } + } + + /** + * Returns whether the given resource is a blocked jar and shouldn't be + * included. + * + * @param resource + * the resource to check + * @return {@code true} if the resource is a blocked jar, {@code false} + * otherwise + */ + protected boolean isBlockedJar(Resource resource) { + // placeholder to handle case of package.properties with + // vaadin.blocked-jar=true + return false; + } + + /** + * See {@link super#doFindPathMatchingJarResources(Resource, URL, String)}. + * This method is slightly adjusted from the origin to just read + * META-INF/VAADIN/package.properties and transform it to Properties object. + */ + private PackageInfo readPackageProperties(URL jarPathURL, String jarPath, + Resource rootDirResource) throws IOException { + URLConnection con = null; + JarFile jarFile; + String jarFileUrl; + String urlFile = null; + boolean closeJarFile; + if (jarPathURL != null) { + con = jarPathURL.openConnection(); + urlFile = jarPathURL.getPath(); + } + if (con instanceof JarURLConnection jarCon) { + jarFile = jarCon.getJarFile(); + closeJarFile = !jarCon.getUseCaches(); + } else { + // No JarURLConnection -> need to resort to URL file parsing. + // We'll assume URLs of the format "jar:path!/entry", with the + // protocol + // being arbitrary as long as following the entry format. + // We'll also handle paths with and without leading "file:" + // prefix. + urlFile = urlFile != null ? urlFile : jarPath; + try { + int separatorIndex = urlFile + .indexOf(ResourceUtils.WAR_URL_SEPARATOR); + if (separatorIndex == -1) { + separatorIndex = urlFile + .indexOf(ResourceUtils.JAR_URL_SEPARATOR); + } + if (separatorIndex != -1) { + jarFileUrl = urlFile.substring(0, separatorIndex); + jarFile = getJarFile(jarFileUrl); + } else { + jarFile = new JarFile(urlFile); + } + closeJarFile = true; + } catch (ZipException ex) { + if (getLogger().isDebugEnabled()) { + getLogger().debug("Skipping invalid jar class path entry [" + + urlFile + "]"); + } + return null; + } + } + + try { + if (getLogger().isTraceEnabled()) { + getLogger().trace("Looking for package.properties in jar file [" + + rootDirResource + "]"); + } + for (Enumeration entries = jarFile.entries(); entries + .hasMoreElements();) { + JarEntry entry = entries.nextElement(); + String entryPath = entry.getName(); + if (entryPath.endsWith(PACKAGE_PROPERTIES_PATH)) { + Resource resource = doFindPathMatchingFileResources( + rootDirResource, PACKAGE_PROPERTIES_PATH).stream() + .findFirst().orElseGet(() -> { + try { + return rootDirResource.createRelative( + PACKAGE_PROPERTIES_PATH); + } catch (IOException e) { + getLogger().warn( + "Could not read package.properties", + e); + return null; + } + }); + Properties prop = resource != null + ? PropertiesLoaderUtils.loadProperties(resource) + : null; + if (getLogger().isTraceEnabled()) { + getLogger().trace("Read package.properties: [{}]", + prop); + } + return prop != null ? createPackageInfo(prop) : null; + } + } + return null; + } finally { + if (closeJarFile) { + jarFile.close(); + } + } + } + + /** + * Check if the target path is allowed by the package properties. + * + * @param rootPath + * Root path as a key for the cached properties + * @param targetPath + * relative path to check + * @param defaultValue + * default value to return if the properties are not found + * @return {@code true} if the target path is allowed by the package + * properties, + */ + protected boolean isAllowedByPackageProperties(String rootPath, + String targetPath, boolean defaultValue) { + PackageInfo packageInfo = propertiesCache.get(rootPath); + if (packageInfo == null) { + return defaultValue; + } + + if (!packageInfo.allowedPackages().isEmpty()) { + return packageInfo.allowedPackages().stream() + .anyMatch(targetPath::startsWith); + } else if (!packageInfo.blockedPackages().isEmpty()) { + return packageInfo.blockedPackages().stream() + .noneMatch(targetPath::startsWith); + } + return defaultValue; + } + + private PackageInfo createPackageInfo(Properties properties) { + if (properties == null) { + return null; + } + Set allowedPackages = Stream + .of(properties.getProperty(ALLOWED_PACKAGES_PROPERTY, "") + .split(",")) + .filter(pkg -> !pkg.isBlank()).map(String::trim) + .map(pkg -> pkg.replace(".", "/")).collect(Collectors.toSet()); + Set blockedPackages = Stream + .of(properties.getProperty(BLOCKED_PACKAGES_PROPERTY, "") + .split(",")) + .filter(pkg -> !pkg.isBlank()).map(String::trim) + .map(pkg -> pkg.replace(".", "/")).collect(Collectors.toSet()); + return new PackageInfo(allowedPackages, blockedPackages); + } +}