Skip to content

Commit

Permalink
Throw dedicated exception if bundle content is not watchable
Browse files Browse the repository at this point in the history
This also adds a FailureAnalyzer which prints a helpful message how to
fix that problem.

Closes spring-projectsgh-38903
  • Loading branch information
mhalbritter committed Apr 29, 2024
1 parent 0757857 commit 8a3b0cd
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 org.springframework.boot.autoconfigure.ssl;

/**
* Thrown when a bundle content location is not watchable.
*
* @author Moritz Halbritter
*/
class BundleContentNotWatchableException extends RuntimeException {

private final BundleContentProperty property;

BundleContentNotWatchableException(BundleContentProperty property) {
super("The content of '%s' is not watchable. Only 'file:' resources are watchable, but '%s' has been set"
.formatted(property.name(), property.value()));
this.property = property;
}

private BundleContentNotWatchableException(String bundleName, BundleContentProperty property, Throwable cause) {
super("The content of '%s' from bundle '%s' is not watchable'. Only 'file:' resources are watchable, but '%s' has been set"
.formatted(property.name(), bundleName, property.value()), cause);
this.property = property;
}

BundleContentNotWatchableException withBundleName(String bundleName) {
return new BundleContentNotWatchableException(bundleName, this.property, this);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 org.springframework.boot.autoconfigure.ssl;

import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;

/**
* An {@link AbstractFailureAnalyzer} that performs analysis of non-watchable bundle
* content failures caused by {@link BundleContentNotWatchableException}.
*
* @author Moritz Halbritter
*/
class BundleContentNotWatchableFailureAnalyzer extends AbstractFailureAnalyzer<BundleContentNotWatchableException> {

@Override
protected FailureAnalysis analyze(Throwable rootFailure, BundleContentNotWatchableException cause) {
return new FailureAnalysis(cause.getMessage(), "Update your application to correct the invalid configuration:\n"
+ "Either use a watchable resource, or disable bundle reloading by setting reload-on-update = false on the bundle.",
cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* @param name the configuration property name (excluding any prefix)
* @param value the configuration property value
* @author Phillip Webb
* @author Moritz Halbritter
*/
record BundleContentProperty(String name, String value) {

Expand All @@ -52,16 +53,17 @@ boolean hasValue() {
}

Path toWatchPath() {
return toPath();
}

private Path toPath() {
try {
URL url = toUrl();
Assert.state(isFileUrl(url), () -> "Value '%s' is not a file URL".formatted(url));
if (!isFileUrl(url)) {
throw new BundleContentNotWatchableException(this);
}
return Path.of(url.toURI()).toAbsolutePath();
}
catch (Exception ex) {
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
throw bundleContentNotWatchableException;
}
throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name),
ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ public void registerBundles(SslBundleRegistry registry) {
}

private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
Function<P, SslBundle> bundleFactory, Function<P, Set<Path>> watchedPaths) {
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) {
properties.forEach((bundleName, bundleProperties) -> {
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
try {
registry.registerBundle(bundleName, bundleSupplier.get());
if (bundleProperties.isReloadOnUpdate()) {
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths.apply(bundleProperties);
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths
.apply(new Bundle<>(bundleName, bundleProperties));
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
}
}
Expand All @@ -80,27 +81,40 @@ private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supp
}
}

private Set<Path> watchedJksPaths(JksSslBundleProperties properties) {
private Set<Path> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
List<BundleContentProperty> watched = new ArrayList<>();
watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation()));
watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation()));
return watchedPaths(watched);
watched.add(new BundleContentProperty("keystore.location", bundle.properties().getKeystore().getLocation()));
watched
.add(new BundleContentProperty("truststore.location", bundle.properties().getTruststore().getLocation()));
return watchedPaths(bundle.name(), watched);
}

private Set<Path> watchedPemPaths(PemSslBundleProperties properties) {
private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
List<BundleContentProperty> watched = new ArrayList<>();
watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey()));
watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate()));
watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey()));
watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate()));
return watchedPaths(watched);
watched
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
watched
.add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate()));
watched.add(new BundleContentProperty("truststore.private-key",
bundle.properties().getTruststore().getPrivateKey()));
watched.add(new BundleContentProperty("truststore.certificate",
bundle.properties().getTruststore().getCertificate()));
return watchedPaths(bundle.name(), watched);
}

private Set<Path> watchedPaths(List<BundleContentProperty> properties) {
return properties.stream()
.filter(BundleContentProperty::hasValue)
.map(BundleContentProperty::toWatchPath)
.collect(Collectors.toSet());
private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
try {
return properties.stream()
.filter(BundleContentProperty::hasValue)
.map(BundleContentProperty::toWatchPath)
.collect(Collectors.toSet());
}
catch (BundleContentNotWatchableException ex) {
throw ex.withBundleName(bundleName);
}
}

private record Bundle<P>(String name, P properties) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ org.springframework.boot.autoconfigure.jooq.NoDslContextBeanFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.MissingR2dbcPoolDependencyFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.MultipleConnectionPoolConfigurationsFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.NoConnectionFactoryBeanFailureAnalyzer
org.springframework.boot.autoconfigure.r2dbc.NoConnectionFactoryBeanFailureAnalyzer,\
org.springframework.boot.autoconfigure.ssl.BundleContentNotWatchableFailureAnalyzer

# Template Availability Providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 org.springframework.boot.autoconfigure.ssl;

import org.junit.jupiter.api.Test;

import org.springframework.boot.diagnostics.FailureAnalysis;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link BundleContentNotWatchableFailureAnalyzer}.
*
* @author Moritz Halbritter
*/
class BundleContentNotWatchableFailureAnalyzerTests {

@Test
void shouldAnalyze() {
FailureAnalysis failureAnalysis = performAnalysis(null);
assertThat(failureAnalysis.getDescription()).isEqualTo(
"The content of 'name' is not watchable. Only 'file:' resources are watchable, but 'classpath:resource.pem' has been set");
assertThat(failureAnalysis.getAction())
.isEqualTo("Update your application to correct the invalid configuration:\n"
+ "Either use a watchable resource, or disable bundle reloading by setting reload-on-update = false on the bundle.");
}

@Test
void shouldAnalyzeWithBundle() {
FailureAnalysis failureAnalysis = performAnalysis("bundle-1");
assertThat(failureAnalysis.getDescription()).isEqualTo(
"The content of 'name' from bundle 'bundle-1' is not watchable'. Only 'file:' resources are watchable, but 'classpath:resource.pem' has been set");
}

private FailureAnalysis performAnalysis(String bundle) {
BundleContentNotWatchableException failure = new BundleContentNotWatchableException(
new BundleContentProperty("name", "classpath:resource.pem"));
if (bundle != null) {
failure = failure.withBundleName(bundle);
}
return new BundleContentNotWatchableFailureAnalyzer().analyze(failure);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.junit.jupiter.api.io.TempDir;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

/**
Expand Down Expand Up @@ -84,4 +85,11 @@ void toWatchPathWhenPathReturnsPath() {
assertThat(property.toWatchPath()).isEqualTo(file);
}

@Test
void shouldThrowBundleContentNotWatchableExceptionIfContentIsNotWatchable() {
BundleContentProperty property = new BundleContentProperty("name", "https://example.com/");
assertThatExceptionOfType(BundleContentNotWatchableException.class).isThrownBy(property::toWatchPath)
.withMessageContaining("Only 'file:' resources are watchable");
}

}

0 comments on commit 8a3b0cd

Please sign in to comment.