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