diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilter.java new file mode 100644 index 0000000000..fb07223dba --- /dev/null +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2024 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 io.spring.initializr.generator.project; + +import java.util.Arrays; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Allows to filter {@link ProjectGenerationConfiguration}. + * + * @author Simon Zambrovski + * @author Moritz Halbritter + */ +public interface ProjectGenerationConfigurationTypeFilter extends Predicate> { + + /** + * Creates a {@link ProjectGenerationConfigurationTypeFilter} which includes the given + * types. + * @param types the types to include + * @return a {@link ProjectGenerationConfigurationTypeFilter} + */ + static ProjectGenerationConfigurationTypeFilter include(Class... types) { + Set> classes = Set.of(types); + return classes::contains; + } + + /** + * Creates a {@link ProjectGenerationConfigurationTypeFilter} which excludes the given + * types. + * @param types the types to exclude + * @return a {@link ProjectGenerationConfigurationTypeFilter} + */ + static ProjectGenerationConfigurationTypeFilter exclude(Class... types) { + Set> classes = Set.of(types); + return (clazz) -> !classes.contains(clazz); + } + + /** + * Creates a {@link ProjectGenerationConfigurationTypeFilter} from multiple filters + * which must all match. + * @param filters the filters + * @return a combined {@link ProjectGenerationConfigurationTypeFilter} + */ + static ProjectGenerationConfigurationTypeFilter allMatch(ProjectGenerationConfigurationTypeFilter... filters) { + return allMatch(Arrays.asList(filters)); + } + + /** + * Creates a {@link ProjectGenerationConfigurationTypeFilter} from multiple filters + * which must all match. + * @param filters the filters + * @return a combined {@link ProjectGenerationConfigurationTypeFilter} + */ + static ProjectGenerationConfigurationTypeFilter allMatch( + Iterable filters) { + return (clazz) -> { + boolean match = true; + for (ProjectGenerationConfigurationTypeFilter filter : filters) { + match &= filter.test(clazz); + } + return match; + }; + } + +} diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerator.java b/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerator.java index 59d111662d..4b316e0bc4 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerator.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/project/ProjectGenerator.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; /** * Main entry point for project generation that processes a {@link ProjectDescription} by @@ -34,6 +35,8 @@ * * @author Andy Wilkinson * @author Stephane Nicoll + * @author Simon Zambrovski + * @author Moritz Halbritter */ public class ProjectGenerator { @@ -116,17 +119,45 @@ public T generate(ProjectDescription description, ProjectAssetGenerator p /** * Return the {@link ProjectGenerationConfiguration} class names that should be - * considered. By default this method will load candidates using - * {@link SpringFactoriesLoader} with {@link ProjectGenerationConfiguration}. + * considered. By default, this method will load candidates using + * {@link SpringFactoriesLoader} with {@link ProjectGenerationConfiguration} and + * exclude those which are not matched by the + * {@link ProjectGenerationConfigurationTypeFilter}, also loaded by + * {@link SpringFactoriesLoader}. * @param description the description of the project to generate * @return a list of candidate configurations */ - @SuppressWarnings("deprecation") + protected List getCandidateProjectGenerationConfigurations(ProjectDescription description) { + List candidates = getProjectGenerationConfigurationFactoryNames(); + ProjectGenerationConfigurationTypeFilter filter = getProjectGenerationConfigurationExclusionFilter(); + return candidates.stream().filter((candidate) -> { + Class type = resolveClass(candidate); + return type != null && filter.test(type); + }).toList(); + } + + private Class resolveClass(String candidate) { + try { + return ClassUtils.forName(candidate, getClass().getClassLoader()); + } + catch (ClassNotFoundException ex) { + return null; + } + } + + @SuppressWarnings("deprecation") + List getProjectGenerationConfigurationFactoryNames() { return SpringFactoriesLoader.loadFactoryNames(ProjectGenerationConfiguration.class, getClass().getClassLoader()); } + ProjectGenerationConfigurationTypeFilter getProjectGenerationConfigurationExclusionFilter() { + List filters = SpringFactoriesLoader + .loadFactories(ProjectGenerationConfigurationTypeFilter.class, getClass().getClassLoader()); + return ProjectGenerationConfigurationTypeFilter.allMatch(filters); + } + private void registerProjectDescription(ProjectGenerationContext context, ProjectDescription description) { context.registerBean(ProjectDescription.class, resolve(description, context)); } diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilterTests.java new file mode 100644 index 0000000000..8a35b6e06a --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGenerationConfigurationTypeFilterTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2024 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 io.spring.initializr.generator.project; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProjectGenerationConfigurationTypeFilter}. + * + * @author Moritz Halbritter + */ +class ProjectGenerationConfigurationTypeFilterTests { + + @Test + void include() { + ProjectGenerationConfigurationTypeFilter filter = ProjectGenerationConfigurationTypeFilter.include(A.class, + B.class); + assertThat(filter).accepts(A.class, B.class); + assertThat(filter).rejects(C.class); + } + + @Test + void exclude() { + ProjectGenerationConfigurationTypeFilter filter = ProjectGenerationConfigurationTypeFilter.exclude(A.class, + B.class); + assertThat(filter).rejects(A.class, B.class); + assertThat(filter).accepts(C.class); + } + + @Test + void allMatch() { + ProjectGenerationConfigurationTypeFilter filterA = (clazz) -> clazz.equals(A.class); + ProjectGenerationConfigurationTypeFilter filterAorB = (clazz) -> clazz.equals(A.class) || clazz.equals(B.class); + ProjectGenerationConfigurationTypeFilter filterNotC = (clazz) -> !clazz.equals(C.class); + ProjectGenerationConfigurationTypeFilter combined = ProjectGenerationConfigurationTypeFilter.allMatch(filterA, + filterAorB, filterNotC); + assertThat(combined).accepts(A.class); + assertThat(combined).rejects(B.class, C.class); + } + + private static final class A { + + } + + private static final class B { + + } + + private static final class C { + + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGeneratorTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGeneratorTests.java index abf738f5d9..75f35412b5 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGeneratorTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/project/ProjectGeneratorTests.java @@ -25,6 +25,8 @@ import io.spring.initializr.generator.buildsystem.maven.MavenBuildSystem; import io.spring.initializr.generator.project.contributor.TestProjectGenerationConfiguration; +import io.spring.initializr.generator.project.contributor.TestProjectGenerationConfiguration2; +import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.mockito.InOrder; @@ -181,8 +183,9 @@ void generateCanBeExtendedToFilterProjectContributors(@TempDir Path projectDir) given(description.getBuildSystem()).willReturn(new MavenBuildSystem()); ProjectGenerator generator = new ProjectGenerator(mockContextInitializr()) { @Override - protected List getCandidateProjectGenerationConfigurations(ProjectDescription description) { - assertThat(description).isSameAs(description); + protected List getCandidateProjectGenerationConfigurations( + ProjectDescription generatorDescription) { + assertThat(description).isSameAs(generatorDescription); return Collections.singletonList(TestProjectGenerationConfiguration.class.getName()); } }; @@ -195,6 +198,37 @@ protected List getCandidateProjectGenerationConfigurations(ProjectDescri verify(description).getBuildSystem(); } + @Test + void loadAndConstructProjectGenerationTypeExclusionFilter() { + ProjectGenerator generator = new ProjectGenerator(mockContextInitializr()); + ProjectGenerationConfigurationTypeFilter filter = generator.getProjectGenerationConfigurationExclusionFilter(); + assertThat(filter).isNotNull(); + assertThat(filter.test(TestProjectGenerationConfiguration.class)).isFalse(); + assertThat(filter.test(TestProjectGenerationConfiguration2.class)).isFalse(); + assertThat(filter.test(Integer.class)).isTrue(); + } + + @Test + void filterProjectContributorsCorrectly() { + ProjectDescription description = mock(ProjectDescription.class); + given(description.getArtifactId()).willReturn("test-custom-contributor"); + given(description.getBuildSystem()).willReturn(new MavenBuildSystem()); + ProjectGenerator generator = new ProjectGenerator(mockContextInitializr()) { + @Override + List getProjectGenerationConfigurationFactoryNames() { + return Lists.list(TestProjectGenerationConfiguration.class.getName(), + TestProjectGenerationConfiguration2.class.getName()); + } + + @Override + ProjectGenerationConfigurationTypeFilter getProjectGenerationConfigurationExclusionFilter() { + return (clazz) -> !TestProjectGenerationConfiguration2.class.equals(clazz); + } + }; + List candidates = generator.getCandidateProjectGenerationConfigurations(description); + assertThat(candidates).containsOnly(TestProjectGenerationConfiguration.class.getCanonicalName()); + } + @SuppressWarnings("unchecked") private Consumer mockContextInitializr() { return mock(Consumer.class); diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2.java b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2.java new file mode 100644 index 0000000000..b23ffe6621 --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-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 io.spring.initializr.generator.project.contributor; + +import io.spring.initializr.generator.project.ProjectGenerationConfiguration; + +/** + * Test contributor. + */ +@ProjectGenerationConfiguration +public class TestProjectGenerationConfiguration2 { + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2ExcludingTypeFilter.java b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2ExcludingTypeFilter.java new file mode 100644 index 0000000000..5ec647461f --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfiguration2ExcludingTypeFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-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 io.spring.initializr.generator.project.contributor; + +import io.spring.initializr.generator.project.ProjectGenerationConfigurationTypeFilter; + +public class TestProjectGenerationConfiguration2ExcludingTypeFilter + implements ProjectGenerationConfigurationTypeFilter { + + private final ProjectGenerationConfigurationTypeFilter delegate = ProjectGenerationConfigurationTypeFilter + .exclude(TestProjectGenerationConfiguration2.class); + + @Override + public boolean test(Class type) { + return this.delegate.test(type); + } + +} diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfigurationExcludingTypeFilter.java b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfigurationExcludingTypeFilter.java new file mode 100644 index 0000000000..5d048a785f --- /dev/null +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/project/contributor/TestProjectGenerationConfigurationExcludingTypeFilter.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-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 io.spring.initializr.generator.project.contributor; + +import io.spring.initializr.generator.project.ProjectGenerationConfigurationTypeFilter; + +public class TestProjectGenerationConfigurationExcludingTypeFilter implements ProjectGenerationConfigurationTypeFilter { + + private final ProjectGenerationConfigurationTypeFilter delegate = ProjectGenerationConfigurationTypeFilter + .exclude(TestProjectGenerationConfiguration.class); + + @Override + public boolean test(Class type) { + return this.delegate.test(type); + } + +} diff --git a/initializr-generator/src/test/resources/META-INF/spring.factories b/initializr-generator/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000000..20d001923c --- /dev/null +++ b/initializr-generator/src/test/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +io.spring.initializr.generator.project.ProjectGenerationConfigurationTypeFilter=\ + io.spring.initializr.generator.project.contributor.TestProjectGenerationConfigurationExcludingTypeFilter,\ + io.spring.initializr.generator.project.contributor.TestProjectGenerationConfiguration2ExcludingTypeFilter +