diff --git a/initializr-metadata/src/main/java/io/spring/initializr/metadata/BillOfMaterials.java b/initializr-metadata/src/main/java/io/spring/initializr/metadata/BillOfMaterials.java index d5b8777e..93d9046f 100644 --- a/initializr-metadata/src/main/java/io/spring/initializr/metadata/BillOfMaterials.java +++ b/initializr-metadata/src/main/java/io/spring/initializr/metadata/BillOfMaterials.java @@ -19,6 +19,7 @@ package io.spring.initializr.metadata; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; @@ -185,10 +186,25 @@ public class BillOfMaterials { * additional BOMs to use, if any. * @param bootVersion the Spring Boot version * @return the bill of materials + * @throws IllegalStateException if bom has mappings, none of which match the + * requested version */ public BillOfMaterials resolve(Version bootVersion) { + return this.resolveSafe(bootVersion).orElseThrow(() -> new IllegalStateException( + "No suitable mapping was found for " + this + " and version " + bootVersion)); + } + + /** + * Resolve this instance according to the specified Spring Boot {@link Version}. + * Return a {@link BillOfMaterials} instance that holds the version, repositories and + * additional BOMs to use, if any. + * @param bootVersion the Spring Boot version + * @return the bill of materials or empty if bom has mappings, none of which match the + * requested version + */ + public Optional resolveSafe(Version bootVersion) { if (this.mappings.isEmpty()) { - return this; + return Optional.of(this); } for (Mapping mapping : this.mappings) { @@ -202,10 +218,10 @@ public class BillOfMaterials { .addAll(!mapping.repositories.isEmpty() ? mapping.repositories : this.repositories); resolvedBom.additionalBoms .addAll(!mapping.additionalBoms.isEmpty() ? mapping.additionalBoms : this.additionalBoms); - return resolvedBom; + return Optional.of(resolvedBom); } } - throw new IllegalStateException("No suitable mapping was found for " + this + " and version " + bootVersion); + return Optional.empty(); } @Override diff --git a/initializr-metadata/src/test/java/io/spring/initializr/metadata/BillOfMaterialsTests.java b/initializr-metadata/src/test/java/io/spring/initializr/metadata/BillOfMaterialsTests.java index 659eb9ea..1f297f01 100755 --- a/initializr-metadata/src/test/java/io/spring/initializr/metadata/BillOfMaterialsTests.java +++ b/initializr-metadata/src/test/java/io/spring/initializr/metadata/BillOfMaterialsTests.java @@ -123,6 +123,15 @@ class BillOfMaterialsTests { .withMessageContaining("1.4.1.RELEASE"); } + @Test + void noRangeAvailableSafe() { + BillOfMaterials bom = BillOfMaterials.create("com.example", "bom"); + bom.getMappings().add(Mapping.create("[1.2.0.RELEASE,1.3.0.M1)", "1.1.0")); + bom.getMappings().add(Mapping.create("[1.3.0.M1, 1.4.0.M1)", "1.2.0")); + bom.validate(); + assertThat(bom.resolveSafe(Version.parse("1.4.1.RELEASE"))).isEmpty(); + } + @Test void resolveRangeWithVariablePatch() { BillOfMaterials bom = BillOfMaterials.create("com.example", "bom", "1.0.0"); diff --git a/initializr-web/src/main/java/io/spring/initializr/web/controller/InvalidMetadataRequestException.java b/initializr-web/src/main/java/io/spring/initializr/web/controller/InvalidMetadataRequestException.java new file mode 100644 index 00000000..ddcd2661 --- /dev/null +++ b/initializr-web/src/main/java/io/spring/initializr/web/controller/InvalidMetadataRequestException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2019 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.web.controller; + +/** + * Thrown when a metadata request is invalid. + * + * @author Chris Bono + */ +@SuppressWarnings("serial") +public class InvalidMetadataRequestException extends RuntimeException { + + public InvalidMetadataRequestException(String message) { + super(message); + } + +} diff --git a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java index 98a58db7..83eeeecd 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/controller/ProjectMetadataController.java @@ -16,8 +16,11 @@ package io.spring.initializr.web.controller; +import java.io.IOException; import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletResponse; + import io.spring.initializr.generator.version.Version; import io.spring.initializr.metadata.DependencyMetadata; import io.spring.initializr.metadata.DependencyMetadataProvider; @@ -30,9 +33,11 @@ import io.spring.initializr.web.mapper.InitializrMetadataV2JsonMapper; import io.spring.initializr.web.mapper.InitializrMetadataVersion; import org.springframework.http.CacheControl; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -85,6 +90,12 @@ public class ProjectMetadataController extends AbstractMetadataController { return dependenciesFor(InitializrMetadataVersion.V2_1, bootVersion); } + @ExceptionHandler + public void invalidMetadaRequest(HttpServletResponse response, InvalidMetadataRequestException ex) + throws IOException { + response.sendError(HttpStatus.BAD_REQUEST.value(), ex.getMessage()); + } + /** * Return the {@link CacheControl} response headers to use for the specified * {@link InitializrMetadata metadata}. If no cache should be applied diff --git a/initializr-web/src/main/java/io/spring/initializr/web/support/DefaultDependencyMetadataProvider.java b/initializr-web/src/main/java/io/spring/initializr/web/support/DefaultDependencyMetadataProvider.java index 02ae329a..33102c6e 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/support/DefaultDependencyMetadataProvider.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/support/DefaultDependencyMetadataProvider.java @@ -26,6 +26,7 @@ import io.spring.initializr.metadata.DependencyMetadata; import io.spring.initializr.metadata.DependencyMetadataProvider; import io.spring.initializr.metadata.InitializrMetadata; import io.spring.initializr.metadata.Repository; +import io.spring.initializr.web.controller.InvalidMetadataRequestException; import org.springframework.cache.annotation.Cacheable; @@ -57,8 +58,12 @@ public class DefaultDependencyMetadataProvider implements DependencyMetadataProv Map boms = new LinkedHashMap<>(); for (Dependency dependency : dependencies.values()) { if (dependency.getBom() != null) { - boms.put(dependency.getBom(), - metadata.getConfiguration().getEnv().getBoms().get(dependency.getBom()).resolve(bootVersion)); + BillOfMaterials bom = metadata.getConfiguration().getEnv().getBoms().get(dependency.getBom()) + .resolveSafe(bootVersion) + .orElseThrow(() -> new InvalidMetadataRequestException(String.format( + "Dependency '%s' points to Bom '%s' that is not compatible with requested platform version '%s'.", + dependency.getId(), dependency.getBom(), bootVersion))); + boms.put(dependency.getBom(), bom); } } // Each resolved bom may require additional repositories diff --git a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java index 2b6d5ea2..577acf97 100644 --- a/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/controller/ProjectMetadataControllerIntegrationTests.java @@ -60,6 +60,18 @@ public class ProjectMetadataControllerIntegrationTests extends AbstractInitializ validateMetadata(response, InitializrMetadataVersion.V2.getMediaType(), "2.0.0", JSONCompareMode.STRICT); } + @Test + void metadataWithInvalidBootVersion() { + try { + execute("/dependencies?bootVersion=1.5.17.RELEASE", String.class, "application/vnd.initializr.v2.1+json", + "application/json"); + } + catch (HttpClientErrorException ex) { + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(ex.getResponseBodyAsString().contains("1.5.17.RELEASE")); + } + } + @Test void metadataWithCurrentAcceptHeader() { getRequests().setFields("_links.maven-project", "dependencies.values[0]", "type.values[0]", diff --git a/initializr-web/src/test/java/io/spring/initializr/web/support/DefaultDependencyMetadataProviderTests.java b/initializr-web/src/test/java/io/spring/initializr/web/support/DefaultDependencyMetadataProviderTests.java index a92f6d1b..e79048f9 100755 --- a/initializr-web/src/test/java/io/spring/initializr/web/support/DefaultDependencyMetadataProviderTests.java +++ b/initializr-web/src/test/java/io/spring/initializr/web/support/DefaultDependencyMetadataProviderTests.java @@ -23,9 +23,11 @@ import io.spring.initializr.metadata.Dependency; import io.spring.initializr.metadata.DependencyMetadata; import io.spring.initializr.metadata.DependencyMetadataProvider; import io.spring.initializr.metadata.InitializrMetadata; +import io.spring.initializr.web.controller.InvalidMetadataRequestException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Stephane Nicoll @@ -74,6 +76,21 @@ class DefaultDependencyMetadataProviderTests { assertThat(anotherDependencyMetadata.getDependencies().get("first").getVersion()).isEqualTo("0.2.0.RELEASE"); } + @Test + void resolveBomInvalidBootVersion() { + Dependency first = Dependency.withId("first", "org.foo", "first"); + first.setRepository("repo-foo"); + first.setBom("bom-foo"); + BillOfMaterials bom = BillOfMaterials.create("org.foo", "bom"); + bom.getMappings() + .add(BillOfMaterials.Mapping.create("[1.0.0.RELEASE, 1.1.0.RELEASE)", "2.0.0.RELEASE", "repo-foo")); + InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults().addBom("bom-foo", bom) + .addRepository("repo-foo", "foo", "http://localhost", false).addDependencyGroup("test", first).build(); + assertThatExceptionOfType(InvalidMetadataRequestException.class) + .isThrownBy(() -> this.provider.get(metadata, Version.parse("1.1.5.RELEASE"))) + .withMessageContainingAll(first.getId(), first.getBom(), "1.1.5.RELEASE"); + } + @Test void addRepoAndRemoveDuplicates() { Dependency first = Dependency.withId("first", "org.foo", "first");