Support 'required properties' precondition

Users may now call #setRequiredProperties(String...) against the
Environment (via its ConfigurablePropertyResolver interface) in order
to indicate which properties must be present.

Environment#validateRequiredProperties() is invoked by
AbstractApplicationContext during the refresh() lifecycle to perform
the actual check and a MissingRequiredPropertiesException is thrown
if the precondition is not satisfied.

Issue: SPR-8323
This commit is contained in:
Chris Beams 2011-05-11 07:36:04 +00:00
parent 3622c6f340
commit 404f798048
8 changed files with 170 additions and 1 deletions

View File

@ -488,6 +488,10 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
// Initialize any placeholder property sources in the context environment
initPropertySources();
// Validate that all properties marked as required are resolvable
// see ConfigurablePropertyResolver#setRequiredProperties
this.environment.validateRequiredProperties();
}
/**

View File

@ -216,6 +216,14 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
return this.propertyResolver.getRequiredProperty(key, targetType);
}
public void setRequiredProperties(String... requiredProperties) {
this.propertyResolver.setRequiredProperties(requiredProperties);
}
public void validateRequiredProperties() throws MissingRequiredPropertiesException {
this.propertyResolver.validateRequiredProperties();
}
public String resolvePlaceholders(String text) {
return this.propertyResolver.resolvePlaceholders(text);
}

View File

@ -21,6 +21,9 @@ import static org.springframework.util.SystemPropertyUtils.PLACEHOLDER_PREFIX;
import static org.springframework.util.SystemPropertyUtils.PLACEHOLDER_SUFFIX;
import static org.springframework.util.SystemPropertyUtils.VALUE_SEPARATOR;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.ConversionService;
@ -47,6 +50,8 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
private String placeholderSuffix = PLACEHOLDER_SUFFIX;
private String valueSeparator = VALUE_SEPARATOR;
private final Set<String> requiredProperties = new LinkedHashSet<String>();
public ConversionService getConversionService() {
return this.conversionService;
}
@ -65,6 +70,24 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
return value == null ? defaultValue : value;
}
public void setRequiredProperties(String... requiredProperties) {
for (String key : requiredProperties) {
this.requiredProperties.add(key);
}
}
public void validateRequiredProperties() {
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
ex.addMissingRequiredProperty(key);
}
}
if (!ex.getMissingRequiredProperties().isEmpty()) {
throw ex;
}
}
public String getRequiredProperty(String key) throws IllegalStateException {
String value = getProperty(key);
if (value == null) {

View File

@ -45,6 +45,23 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
void setConversionService(ConversionService conversionService);
void setPlaceholderPrefix(String placeholderPrefix);
void setPlaceholderSuffix(String placeholderSuffix);
void setValueSeparator(String valueSeparator);
/**
* Specify which properties must be present, to be verified by
* {@link #validateRequiredProperties()}.
*/
void setRequiredProperties(String... requiredProperties);
/**
* Validate that each of the properties specified by
* {@link #setRequiredProperties} is present and resolves to a
* non-{@code null} value.
* @throws MissingRequiredPropertiesException if any of the required
* properties are not resolvable.
*/
void validateRequiredProperties() throws MissingRequiredPropertiesException;
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2002-2011 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
*
* 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 org.springframework.core.env;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Exception thrown when required properties are not found.
*
* @author Chris Beams
* @since 3.1
* @see ConfigurablePropertyResolver#setRequiredProperties(String...)
* @see ConfigurablePropertyResolver#validateRequiredProperties()
* @see org.springframework.context.support.AbstractApplicationContext#prepareRefresh()
*/
@SuppressWarnings("serial")
public class MissingRequiredPropertiesException extends IllegalStateException {
private final Set<String> missingRequiredProperties = new LinkedHashSet<String>();
/**
* Return the set of properties marked as required but not present
* upon validation.
* @see ConfigurablePropertyResolver#setRequiredProperties(String...)
* @see ConfigurablePropertyResolver#validateRequiredProperties()
*/
public Set<String> getMissingRequiredProperties() {
return missingRequiredProperties;
}
void addMissingRequiredProperty(String key) {
missingRequiredProperties.add(key);
}
@Override
public String getMessage() {
return String.format(
"The following properties were declared as required but could " +
"not be resolved: %s", this.getMissingRequiredProperties());
}
}

View File

@ -135,7 +135,6 @@ public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {
return null;
}
@SuppressWarnings("serial")
static class ClassConversionException extends ConversionException {
public ClassConversionException(Class<?> actual, Class<?> expected) {

View File

@ -318,6 +318,40 @@ public class PropertySourcesPropertyResolverTests {
resolver.getPropertyAsClass("some.class", SomeType.class);
}
@Test
public void setRequiredProperties_andValidateRequiredProperties() {
// no properties have been marked as required -> validation should pass
propertyResolver.validateRequiredProperties();
// mark which properties are required
propertyResolver.setRequiredProperties("foo", "bar");
// neither foo nor bar properties are present -> validating should throw
try {
propertyResolver.validateRequiredProperties();
fail("expected validation exception");
} catch (MissingRequiredPropertiesException ex) {
assertThat(ex.getMessage(), equalTo(
"The following properties were declared as required " +
"but could not be resolved: [foo, bar]"));
}
// add foo property -> validation should fail only on missing 'bar' property
testProperties.put("foo", "fooValue");
try {
propertyResolver.validateRequiredProperties();
fail("expected validation exception");
} catch (MissingRequiredPropertiesException ex) {
assertThat(ex.getMessage(), equalTo(
"The following properties were declared as required " +
"but could not be resolved: [bar]"));
}
// add bar property -> validation should pass, even with an empty string value
testProperties.put("bar", "");
propertyResolver.validateRequiredProperties();
}
static interface SomeType { }
static class SpecificType implements SomeType { }
}

View File

@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
import static org.springframework.context.ConfigurableApplicationContext.ENVIRONMENT_BEAN_NAME;
import static org.springframework.core.env.EnvironmentIntegrationTests.Constants.DERIVED_DEV_BEAN_NAME;
@ -60,6 +61,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.jca.context.ResourceAdapterApplicationContext;
import org.springframework.jca.support.SimpleBootstrapContext;
import org.springframework.jca.work.SimpleTaskWorkManager;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.mock.web.MockServletContext;
@ -580,6 +582,32 @@ public class EnvironmentIntegrationTests {
assertThat(ctx.containsBean(PROD_BEAN_NAME), is(true));
}
@Test
public void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() {
{
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.refresh();
}
{
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setRequiredProperties("foo", "bar");
try {
ctx.refresh();
fail("expected missing property exception");
} catch (MissingRequiredPropertiesException ex) {
}
}
{
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setRequiredProperties("foo");
ctx.setEnvironment(new MockEnvironment().withProperty("foo", "fooValue"));
ctx.refresh(); // should succeed
}
}
private DefaultListableBeanFactory newBeanFactoryWithEnvironmentAwareBean() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();