001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.test.context.support; 018 019import org.apache.commons.logging.Log; 020import org.apache.commons.logging.LogFactory; 021 022import org.springframework.context.ApplicationContext; 023import org.springframework.lang.Nullable; 024import org.springframework.test.context.ContextConfiguration; 025import org.springframework.test.context.ContextConfigurationAttributes; 026import org.springframework.test.context.ContextLoader; 027import org.springframework.test.context.MergedContextConfiguration; 028import org.springframework.test.context.SmartContextLoader; 029import org.springframework.util.Assert; 030 031/** 032 * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class 033 * for implementations of the {@link SmartContextLoader} SPI that delegate to a 034 * set of <em>candidate</em> SmartContextLoaders (i.e., one that supports XML 035 * configuration files or Groovy scripts and one that supports annotated classes) 036 * to determine which context loader is appropriate for a given test class's 037 * configuration. Each candidate is given a chance to 038 * {@linkplain #processContextConfiguration process} the 039 * {@link ContextConfigurationAttributes} for each class in the test class hierarchy 040 * that is annotated with {@link ContextConfiguration @ContextConfiguration}, and 041 * the candidate that supports the merged, processed configuration will be used to 042 * actually {@linkplain #loadContext load} the context. 043 * 044 * <p>Any reference to an <em>XML-based loader</em> can be interpreted to mean 045 * a context loader that supports only XML configuration files or one that 046 * supports both XML configuration files and Groovy scripts simultaneously. 047 * 048 * <p>Placing an empty {@code @ContextConfiguration} annotation on a test class signals 049 * that default resource locations (e.g., XML configuration files or Groovy scripts) 050 * or default 051 * {@linkplain org.springframework.context.annotation.Configuration configuration classes} 052 * should be detected. Furthermore, if a specific {@link ContextLoader} or 053 * {@link SmartContextLoader} is not explicitly declared via 054 * {@code @ContextConfiguration}, a concrete subclass of 055 * {@code AbstractDelegatingSmartContextLoader} will be used as the default loader, 056 * thus providing automatic support for either path-based resource locations 057 * (e.g., XML configuration files and Groovy scripts) or annotated classes, 058 * but not both simultaneously. 059 * 060 * <p>As of Spring 3.2, a test class may optionally declare neither path-based 061 * resource locations nor annotated classes and instead declare only {@linkplain 062 * ContextConfiguration#initializers application context initializers}. In such 063 * cases, an attempt will still be made to detect defaults, but their absence will 064 * not result in an exception. 065 * 066 * @author Sam Brannen 067 * @author Phillip Webb 068 * @since 3.2 069 * @see SmartContextLoader 070 */ 071public abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader { 072 073 private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class); 074 075 076 /** 077 * Get the delegate {@code SmartContextLoader} that supports XML configuration 078 * files and/or Groovy scripts. 079 */ 080 protected abstract SmartContextLoader getXmlLoader(); 081 082 /** 083 * Get the delegate {@code SmartContextLoader} that supports annotated classes. 084 */ 085 protected abstract SmartContextLoader getAnnotationConfigLoader(); 086 087 088 // ContextLoader 089 090 /** 091 * {@code AbstractDelegatingSmartContextLoader} does not support the 092 * {@link ContextLoader#processLocations(Class, String...)} method. Call 093 * {@link #processContextConfiguration(ContextConfigurationAttributes)} instead. 094 * @throws UnsupportedOperationException in this implementation 095 */ 096 @Override 097 public final String[] processLocations(Class<?> clazz, @Nullable String... locations) { 098 throw new UnsupportedOperationException( 099 "DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + 100 "Call processContextConfiguration(ContextConfigurationAttributes) instead."); 101 } 102 103 /** 104 * {@code AbstractDelegatingSmartContextLoader} does not support the 105 * {@link ContextLoader#loadContext(String...) } method. Call 106 * {@link #loadContext(MergedContextConfiguration)} instead. 107 * @throws UnsupportedOperationException in this implementation 108 */ 109 @Override 110 public final ApplicationContext loadContext(String... locations) throws Exception { 111 throw new UnsupportedOperationException( 112 "DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + 113 "Call loadContext(MergedContextConfiguration) instead."); 114 } 115 116 117 // SmartContextLoader 118 119 /** 120 * Delegates to candidate {@code SmartContextLoaders} to process the supplied 121 * {@link ContextConfigurationAttributes}. 122 * <p>Delegation is based on explicit knowledge of the implementations of the 123 * default loaders for {@linkplain #getXmlLoader() XML configuration files and 124 * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}. 125 * Specifically, the delegation algorithm is as follows: 126 * <ul> 127 * <li>If the resource locations or annotated classes in the supplied 128 * {@code ContextConfigurationAttributes} are not empty, the appropriate 129 * candidate loader will be allowed to process the configuration <em>as is</em>, 130 * without any checks for detection of defaults.</li> 131 * <li>Otherwise, the XML-based loader will be allowed to process 132 * the configuration in order to detect default resource locations. If 133 * the XML-based loader detects default resource locations, 134 * an {@code info} message will be logged.</li> 135 * <li>Subsequently, the annotation-based loader will be allowed to 136 * process the configuration in order to detect default configuration classes. 137 * If the annotation-based loader detects default configuration 138 * classes, an {@code info} message will be logged.</li> 139 * </ul> 140 * @param configAttributes the context configuration attributes to process 141 * @throws IllegalArgumentException if the supplied configuration attributes are 142 * {@code null}, or if the supplied configuration attributes include both 143 * resource locations and annotated classes 144 * @throws IllegalStateException if the XML-based loader detects default 145 * configuration classes; if the annotation-based loader detects default 146 * resource locations; if neither candidate loader detects defaults for the supplied 147 * context configuration; or if both candidate loaders detect defaults for the 148 * supplied context configuration 149 */ 150 @Override 151 public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) { 152 Assert.notNull(configAttributes, "configAttributes must not be null"); 153 Assert.isTrue(!(configAttributes.hasLocations() && configAttributes.hasClasses()), 154 () -> String.format("Cannot process locations AND classes for context configuration %s: " + 155 "configure one or the other, but not both.", configAttributes)); 156 157 // If the original locations or classes were not empty, there's no 158 // need to bother with default detection checks; just let the 159 // appropriate loader process the configuration. 160 if (configAttributes.hasLocations()) { 161 delegateProcessing(getXmlLoader(), configAttributes); 162 } 163 else if (configAttributes.hasClasses()) { 164 delegateProcessing(getAnnotationConfigLoader(), configAttributes); 165 } 166 else { 167 // Else attempt to detect defaults... 168 169 // Let the XML loader process the configuration. 170 delegateProcessing(getXmlLoader(), configAttributes); 171 boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations(); 172 173 if (xmlLoaderDetectedDefaults) { 174 if (logger.isInfoEnabled()) { 175 logger.info(String.format("%s detected default locations for context configuration %s.", 176 name(getXmlLoader()), configAttributes)); 177 } 178 } 179 180 Assert.state(!configAttributes.hasClasses(), () -> String.format( 181 "%s should NOT have detected default configuration classes for context configuration %s.", 182 name(getXmlLoader()), configAttributes)); 183 184 // Now let the annotation config loader process the configuration. 185 delegateProcessing(getAnnotationConfigLoader(), configAttributes); 186 187 if (configAttributes.hasClasses()) { 188 if (logger.isInfoEnabled()) { 189 logger.info(String.format("%s detected default configuration classes for context configuration %s.", 190 name(getAnnotationConfigLoader()), configAttributes)); 191 } 192 } 193 194 Assert.state(xmlLoaderDetectedDefaults || !configAttributes.hasLocations(), () -> String.format( 195 "%s should NOT have detected default locations for context configuration %s.", 196 name(getAnnotationConfigLoader()), configAttributes)); 197 198 if (configAttributes.hasLocations() && configAttributes.hasClasses()) { 199 String msg = String.format( 200 "Configuration error: both default locations AND default configuration classes " + 201 "were detected for context configuration %s; configure one or the other, but not both.", 202 configAttributes); 203 logger.error(msg); 204 throw new IllegalStateException(msg); 205 } 206 } 207 } 208 209 /** 210 * Delegates to an appropriate candidate {@code SmartContextLoader} to load 211 * an {@link ApplicationContext}. 212 * <p>Delegation is based on explicit knowledge of the implementations of the 213 * default loaders for {@linkplain #getXmlLoader() XML configuration files and 214 * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}. 215 * Specifically, the delegation algorithm is as follows: 216 * <ul> 217 * <li>If the resource locations in the supplied {@code MergedContextConfiguration} 218 * are not empty and the annotated classes are empty, 219 * the XML-based loader will load the {@code ApplicationContext}.</li> 220 * <li>If the annotated classes in the supplied {@code MergedContextConfiguration} 221 * are not empty and the resource locations are empty, 222 * the annotation-based loader will load the {@code ApplicationContext}.</li> 223 * </ul> 224 * @param mergedConfig the merged context configuration to use to load the application context 225 * @throws IllegalArgumentException if the supplied merged configuration is {@code null} 226 * @throws IllegalStateException if neither candidate loader is capable of loading an 227 * {@code ApplicationContext} from the supplied merged context configuration 228 */ 229 @Override 230 public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { 231 Assert.notNull(mergedConfig, "MergedContextConfiguration must not be null"); 232 233 Assert.state(!(mergedConfig.hasLocations() && mergedConfig.hasClasses()), () -> String.format( 234 "Neither %s nor %s supports loading an ApplicationContext from %s: " + 235 "declare either 'locations' or 'classes' but not both.", name(getXmlLoader()), 236 name(getAnnotationConfigLoader()), mergedConfig)); 237 238 SmartContextLoader[] candidates = {getXmlLoader(), getAnnotationConfigLoader()}; 239 for (SmartContextLoader loader : candidates) { 240 // Determine if each loader can load a context from the mergedConfig. If it 241 // can, let it; otherwise, keep iterating. 242 if (supports(loader, mergedConfig)) { 243 return delegateLoading(loader, mergedConfig); 244 } 245 } 246 247 // If neither of the candidates supports the mergedConfig based on resources but 248 // ACIs or customizers were declared, then delegate to the annotation config 249 // loader. 250 if (!mergedConfig.getContextInitializerClasses().isEmpty() || !mergedConfig.getContextCustomizers().isEmpty()) { 251 return delegateLoading(getAnnotationConfigLoader(), mergedConfig); 252 } 253 254 // else... 255 throw new IllegalStateException(String.format( 256 "Neither %s nor %s was able to load an ApplicationContext from %s.", 257 name(getXmlLoader()), name(getAnnotationConfigLoader()), mergedConfig)); 258 } 259 260 261 private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) { 262 if (logger.isDebugEnabled()) { 263 logger.debug(String.format("Delegating to %s to process context configuration %s.", 264 name(loader), configAttributes)); 265 } 266 loader.processContextConfiguration(configAttributes); 267 } 268 269 private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig) 270 throws Exception { 271 272 if (logger.isDebugEnabled()) { 273 logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig)); 274 } 275 return loader.loadContext(mergedConfig); 276 } 277 278 private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) { 279 if (loader == getAnnotationConfigLoader()) { 280 return (mergedConfig.hasClasses() && !mergedConfig.hasLocations()); 281 } 282 else { 283 return (mergedConfig.hasLocations() && !mergedConfig.hasClasses()); 284 } 285 } 286 287 private static String name(SmartContextLoader loader) { 288 return loader.getClass().getSimpleName(); 289 } 290 291}