[SPR-8541] Documented DelegatingSmartContextLoader.

This commit is contained in:
Sam Brannen 2011-08-20 17:41:29 +00:00
parent 0a88d4bae1
commit 466c3b4b2a
2 changed files with 95 additions and 30 deletions

View File

@ -22,12 +22,12 @@ import org.springframework.context.ApplicationContext;
* <p>Strategy interface for loading an {@link ApplicationContext application context} * <p>Strategy interface for loading an {@link ApplicationContext application context}
* for an integration test managed by the Spring TestContext Framework. * for an integration test managed by the Spring TestContext Framework.
* *
* <p>The {@code SmartContextLoader} SPI supersedes the {@link ContextLoader} * <p>The {@code SmartContextLoader} SPI supersedes the {@link ContextLoader} SPI
* SPI introduced in Spring 2.5: a {@code SmartContextLoader} can process both * introduced in Spring 2.5: a {@code SmartContextLoader} can choose to process
* resource locations and configuration classes. Furthermore, a {@code SmartContextLoader} * either resource locations or configuration classes. Furthermore, a
* can set active bean definition profiles in the context that it loads (see * {@code SmartContextLoader} can set active bean definition profiles in the
* {@link MergedContextConfiguration#getActiveProfiles()} and * context that it loads (see {@link MergedContextConfiguration#getActiveProfiles()}
* {@link #loadContext(MergedContextConfiguration)}). * and {@link #loadContext(MergedContextConfiguration)}).
* *
* <p>Clients of a {@code SmartContextLoader} should call * <p>Clients of a {@code SmartContextLoader} should call
* {@link #processContextConfiguration(ContextConfigurationAttributes) * {@link #processContextConfiguration(ContextConfigurationAttributes)
@ -80,7 +80,6 @@ public interface SmartContextLoader extends ContextLoader {
* <code>locations</code> or <code>classes</code> property empty signals that * <code>locations</code> or <code>classes</code> property empty signals that
* this {@code SmartContextLoader} was not able to generate or detect defaults. * this {@code SmartContextLoader} was not able to generate or detect defaults.
* @param configAttributes the context configuration attributes to process * @param configAttributes the context configuration attributes to process
* @see #generatesDefaults()
*/ */
void processContextConfiguration(ContextConfigurationAttributes configAttributes); void processContextConfiguration(ContextConfigurationAttributes configAttributes);

View File

@ -22,6 +22,8 @@ import java.util.List;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextLoader; import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
@ -30,7 +32,23 @@ import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* TODO Document DelegatingSmartContextLoader. * {@code DelegatingSmartContextLoader} is an implementation of the {@link SmartContextLoader}
* SPI that delegates to a set of <em>candidate</em> SmartContextLoaders (i.e.,
* {@link GenericXmlContextLoader} and {@link AnnotationConfigContextLoader}) to
* determine which context loader is appropriate for a given test classÕs configuration.
* Each candidate is given a chance to {@link #processContextConfiguration process} the
* {@link ContextConfigurationAttributes} for each class in the test class hierarchy that
* is annotated with {@link ContextConfiguration @ContextConfiguration}, and the candidate
* that supports the merged, processed configuration will be used to actually
* {@link #loadContext load} the context.
*
* <p>Placing an empty {@code @ContextConfiguration} annotation on
* a test class signals that default resource locations (i.e., XML configuration files)
* or default {@link Configuration configuration classes} should be detected. Furthermore,
* if a specific {@link ContextLoader} or {@link SmartContextLoader} is not explicitly
* declared via {@code @ContextConfiguration}, {@code DelegatingSmartContextLoader} will
* be used as the default loader, thus providing automatic support for either XML
* configuration files or configuration classes, but not both simultaneously.
* *
* @author Sam Brannen * @author Sam Brannen
* @since 3.1 * @since 3.1
@ -43,7 +61,7 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
private static final Log logger = LogFactory.getLog(DelegatingSmartContextLoader.class); private static final Log logger = LogFactory.getLog(DelegatingSmartContextLoader.class);
private final SmartContextLoader xmlLoader = new GenericXmlContextLoader(); private final SmartContextLoader xmlLoader = new GenericXmlContextLoader();
private final SmartContextLoader annotationLoader = new AnnotationConfigContextLoader(); private final SmartContextLoader annotationConfigLoader = new AnnotationConfigContextLoader();
// --- SmartContextLoader -------------------------------------------------- // --- SmartContextLoader --------------------------------------------------
@ -70,24 +88,53 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
} }
/** /**
* TODO Document processContextConfiguration() implementation. * Delegates to candidate {@code SmartContextLoaders} to process the supplied
* {@link ContextConfigurationAttributes}.
*
* <p>Delegation is based on explicit knowledge of the implementations of
* {@link GenericXmlContextLoader} and {@link AnnotationConfigContextLoader}.
* Specifically, the delegation algorithm is as follows:
*
* <ul>
* <li>If the resource locations or configuration classes in the supplied
* {@code ContextConfigurationAttributes} are not empty, the appropriate
* candidate loader will be allowed to process the configuration <em>as is</em>,
* without any checks for detection of defaults.</li>
* <li>Otherwise, {@code GenericXmlContextLoader} will be allowed to process
* the configuration in order to detect default resource locations. If
* {@code GenericXmlContextLoader} detects default resource locations,
* an {@code info} message will be logged.</li>
* <li>Subsequently, {@code AnnotationConfigContextLoader} will be allowed to
* process the configuration in order to detect default configuration classes.
* If {@code AnnotationConfigContextLoader} detects default configuration
* classes, an {@code info} message will be logged.</li>
* </ul>
*
* @param configAttributes the context configuration attributes to process
* @throws IllegalArgumentException if the supplied configuration attributes are
* <code>null</code>, or if the supplied configuration attributes include both
* resource locations and configuration classes
* @throws IllegalStateException if {@code GenericXmlContextLoader} detects default
* configuration classes; if {@code AnnotationConfigContextLoader} detects default
* resource locations; if neither candidate loader detects defaults for the supplied
* context configuration; or if both candidate loaders detect defaults for the
* supplied context configuration
*/ */
public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) { public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) {
if (configAttributes.hasLocations() && configAttributes.hasClasses()) { Assert.notNull(configAttributes, "configAttributes must not be null");
throw new IllegalStateException(String.format( Assert.isTrue(configAttributes.hasLocations() && configAttributes.hasClasses(), String.format(
"Cannot process locations AND configuration classes for context " "Cannot process locations AND configuration classes for context "
+ "configuration %s; configure one or the other, but not both.", configAttributes)); + "configuration %s; configure one or the other, but not both.", configAttributes));
}
// If the original locations or classes were not empty, there's no // If the original locations or classes were not empty, there's no
// need to bother with default detection checks; just let the respective // need to bother with default detection checks; just let the
// loader process the configuration. // appropriate loader process the configuration.
if (configAttributes.hasLocations()) { if (configAttributes.hasLocations()) {
delegateProcessing(xmlLoader, configAttributes); delegateProcessing(xmlLoader, configAttributes);
} }
else if (configAttributes.hasClasses()) { else if (configAttributes.hasClasses()) {
delegateProcessing(annotationLoader, configAttributes); delegateProcessing(annotationConfigLoader, configAttributes);
} }
else { else {
// Else attempt to detect defaults... // Else attempt to detect defaults...
@ -109,28 +156,28 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
name(xmlLoader), configAttributes)); name(xmlLoader), configAttributes));
} }
// Now let the annotation loader process the configuration. // Now let the annotation config loader process the configuration.
delegateProcessing(annotationLoader, configAttributes); delegateProcessing(annotationConfigLoader, configAttributes);
if (configAttributes.hasClasses()) { if (configAttributes.hasClasses()) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info(String.format( logger.info(String.format(
"%s detected default configuration classes for context configuration %s.", "%s detected default configuration classes for context configuration %s.",
name(annotationLoader), configAttributes)); name(annotationConfigLoader), configAttributes));
} }
} }
if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) { if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"%s should NOT have detected default locations for context configuration %s.", "%s should NOT have detected default locations for context configuration %s.",
name(annotationLoader), configAttributes)); name(annotationConfigLoader), configAttributes));
} }
// If neither loader detected defaults, throw an exception. // If neither loader detected defaults, throw an exception.
if (!configAttributes.hasResources()) { if (!configAttributes.hasResources()) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"Neither %s nor %s was able to detect defaults for context configuration %s.", name(xmlLoader), "Neither %s nor %s was able to detect defaults for context configuration %s.", name(xmlLoader),
name(annotationLoader), configAttributes)); name(annotationConfigLoader), configAttributes));
} }
if (configAttributes.hasLocations() && configAttributes.hasClasses()) { if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
@ -145,16 +192,35 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
} }
/** /**
* TODO Document loadContext(MergedContextConfiguration) implementation. * Delegates to an appropriate candidate {@code SmartContextLoader} to load
* an {@link ApplicationContext}.
*
* <p>Delegation is based on explicit knowledge of the implementations of
* {@link GenericXmlContextLoader} and {@link AnnotationConfigContextLoader}.
* Specifically, the delegation algorithm is as follows:
*
* <ul>
* <li>If the resource locations in the supplied {@code MergedContextConfiguration}
* are not empty and the configuration classes are empty,
* {@code GenericXmlContextLoader} will load the {@code ApplicationContext}.</li>
* <li>If the configuration classes in the supplied {@code MergedContextConfiguration}
* are not empty and the resource locations are empty,
* {@code AnnotationConfigContextLoader} will load the {@code ApplicationContext}.</li>
* </ul>
*
* @param mergedConfig the merged context configuration to use to load the application context
* @throws IllegalArgumentException if the supplied merged configuration is <code>null</code>
* @throws IllegalStateException if neither candidate loader is capable of loading an
* {@code ApplicationContext} from the supplied merged context configuration
*/ */
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
Assert.notNull(mergedConfig, "mergedConfig must not be null"); Assert.notNull(mergedConfig, "mergedConfig must not be null");
List<SmartContextLoader> candidates = Arrays.asList(xmlLoader, annotationLoader); List<SmartContextLoader> candidates = Arrays.asList(xmlLoader, annotationConfigLoader);
// Determine if each loader can load a context from the mergedConfig. If
// it can, let it; otherwise, keep iterating.
for (SmartContextLoader loader : candidates) { for (SmartContextLoader loader : candidates) {
// Determine if each loader can load a context from the
// mergedConfig. If it can, let it; otherwise, keep iterating.
if (supports(loader, mergedConfig)) { if (supports(loader, mergedConfig)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig)); logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig));
@ -165,7 +231,7 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"Neither %s nor %s was able to load an ApplicationContext from %s.", name(xmlLoader), "Neither %s nor %s was able to load an ApplicationContext from %s.", name(xmlLoader),
name(annotationLoader), mergedConfig)); name(annotationConfigLoader), mergedConfig));
} }
// --- ContextLoader ------------------------------------------------------- // --- ContextLoader -------------------------------------------------------
@ -177,7 +243,7 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
* @throws UnsupportedOperationException * @throws UnsupportedOperationException
*/ */
public String[] processLocations(Class<?> clazz, String... locations) { public String[] processLocations(Class<?> clazz, String... locations) {
throw new UnsupportedOperationException("DelegatingSmartContextLoader does not support the ContextLoader API. " throw new UnsupportedOperationException("DelegatingSmartContextLoader does not support the ContextLoader SPI. "
+ "Call processContextConfiguration(ContextConfigurationAttributes) instead."); + "Call processContextConfiguration(ContextConfigurationAttributes) instead.");
} }
@ -188,7 +254,7 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
* @throws UnsupportedOperationException * @throws UnsupportedOperationException
*/ */
public ApplicationContext loadContext(String... locations) throws Exception { public ApplicationContext loadContext(String... locations) throws Exception {
throw new UnsupportedOperationException("DelegatingSmartContextLoader does not support the ContextLoader API. " throw new UnsupportedOperationException("DelegatingSmartContextLoader does not support the ContextLoader SPI. "
+ "Call loadContext(MergedContextConfiguration) instead."); + "Call loadContext(MergedContextConfiguration) instead.");
} }