Skip to content

Commit

Permalink
Support 'required properties' precondition
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cbeams committed May 11, 2011
1 parent 3622c6f commit 404f798
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,10 @@ protected void prepareRefresh() {

// 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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ public <T> T getRequiredProperty(String key, Class<T> targetType) throws Illegal
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
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;
Expand All @@ -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;
}
Expand All @@ -65,6 +70,24 @@ public <T> T getProperty(String key, Class<T> targetType, T defaultValue) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ else if (value instanceof Class) {
return null;
}


@SuppressWarnings("serial")
static class ClassConversionException extends ConversionException {
public ClassConversionException(Class<?> actual, Class<?> expected) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,40 @@ public void getPropertyAsClass_withMismatchedRealClassForValue() {
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 { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -60,6 +61,7 @@
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;
Expand Down Expand Up @@ -580,6 +582,32 @@ public void xmlPortletApplicationContext() {
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();
Expand Down

0 comments on commit 404f798

Please sign in to comment.