/*
 * Copyright 2002-2015 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.test;

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

/**
 * This class is only used within tests in the spring-orm module.
 *
 * <p>Abstract JUnit 3.8 test class that holds and exposes a single Spring
 * {@link org.springframework.context.ApplicationContext ApplicationContext}.
 *
 * <p>This class will cache contexts based on a <i>context key</i>: normally the
 * config locations String array describing the Spring resource descriptors
 * making up the context. Unless the {@link #setDirty()} method is called by a
 * test, the context will not be reloaded, even across different subclasses of
 * this test. This is particularly beneficial if your context is slow to
 * construct, for example if you are using Hibernate and the time taken to load
 * the mappings is an issue.
 *
 * <p>For such standard usage, simply override the {@link #getConfigLocations()}
 * method and provide the desired config files. For alternative configuration
 * options, see {@link #getConfigPaths()}.
 *
 * <p><b>WARNING:</b> When doing integration tests from within Eclipse, only use
 * classpath resource URLs. Else, you may see misleading failures when changing
 * context locations.
 *
 * @author Juergen Hoeller
 * @author Rod Johnson
 * @author Sam Brannen
 * @since 2.0
 * @see #getConfigLocations()
 * @see #getApplicationContext()
 * @deprecated as of Spring 3.0, in favor of using the listener-based test context framework
 * ({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests})
 */
@Deprecated
abstract class AbstractSingleSpringContextTests extends AbstractSpringContextTests {

	private static final String SLASH = "/";

	/** Application context this test will run against */
	protected ConfigurableApplicationContext applicationContext;

	/**
	 * This implementation is final. Override {@code onSetUp} for custom behavior.
	 * @see #onSetUp()
	 */
	@Override
	protected final void setUp() throws Exception {
		// lazy load, in case getApplicationContext() has not yet been called.
		if (this.applicationContext == null) {
			this.applicationContext = getContext(getConfigLocations());
		}
		prepareTestInstance();
		onSetUp();
	}

	/**
	 * Prepare this test instance, for example populating its fields.
	 * The context has already been loaded at the time of this callback.
	 * <p>The default implementation does nothing.
	 * @throws Exception in case of preparation failure
	 */
	protected void prepareTestInstance() throws Exception {
	}

	/**
	 * Subclasses can override this method in place of the {@code setUp()}
	 * method, which is final in this class.
	 * <p>The default implementation does nothing.
	 * @throws Exception simply let any exception propagate
	 */
	protected void onSetUp() throws Exception {
	}

	/**
	 * This implementation is final. Override {@code onTearDown} for
	 * custom behavior.
	 * @see #onTearDown()
	 */
	@Override
	protected final void tearDown() throws Exception {
		onTearDown();
	}

	/**
	 * Subclasses can override this to add custom behavior on teardown.
	 * @throws Exception simply let any exception propagate
	 */
	protected void onTearDown() throws Exception {
	}

	/**
	 * Load a Spring ApplicationContext from the given config locations.
	 * <p>The default implementation creates a standard
	 * {@link #createApplicationContext GenericApplicationContext}, allowing
	 * for customizing the internal bean factory through
	 * {@link #customizeBeanFactory}.
	 * @param locations the config locations (as Spring resource locations,
	 * e.g. full classpath locations or any kind of URL)
	 * @return the corresponding ApplicationContext instance (potentially cached)
	 * @throws Exception if context loading failed
	 * @see #createApplicationContext(String[])
	 */
	@Override
	protected ConfigurableApplicationContext loadContext(String... locations) throws Exception {
		if (this.logger.isInfoEnabled()) {
			this.logger.info("Loading context for locations: " + StringUtils.arrayToCommaDelimitedString(locations));
		}
		return createApplicationContext(locations);
	}

	/**
	 * Create a Spring {@link ConfigurableApplicationContext} for use by this test.
	 * <p>The default implementation creates a standard {@link GenericApplicationContext}
	 * instance, calls the {@link #prepareApplicationContext} prepareApplicationContext}
	 * method and the {@link #customizeBeanFactory customizeBeanFactory} method to allow
	 * for customizing the context and its DefaultListableBeanFactory, populates the
	 * context from the specified config {@code locations} through the configured
	 * {@link #createBeanDefinitionReader(GenericApplicationContext) BeanDefinitionReader},
	 * and finally {@link ConfigurableApplicationContext#refresh() refreshes} the context.
	 * @param locations the config locations (as Spring resource locations,
	 * e.g. full classpath locations or any kind of URL)
	 * @return the GenericApplicationContext instance
	 * @see #loadContext(String...)
	 * @see #customizeBeanFactory(DefaultListableBeanFactory)
	 * @see #createBeanDefinitionReader(GenericApplicationContext)
	 */
	private ConfigurableApplicationContext createApplicationContext(String... locations) {
		GenericApplicationContext context = new GenericApplicationContext();
		new XmlBeanDefinitionReader(context).loadBeanDefinitions(locations);
		AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
		context.refresh();
		return context;
	}

	/**
	 * Subclasses can override this method to return the locations of their
	 * config files.
	 * <p>A plain path will be treated as class path location, e.g.:
	 * "org/springframework/whatever/foo.xml". Note however that you may prefix
	 * path locations with standard Spring resource prefixes. Therefore, a
	 * config location path prefixed with "classpath:" with behave the same as a
	 * plain path, but a config location such as
	 * "file:/some/path/path/location/appContext.xml" will be treated as a
	 * filesystem location.
	 * <p>The default implementation builds config locations for the config paths
	 * specified through {@link #getConfigPaths()}.
	 * @return an array of config locations
	 * @see #getConfigPaths()
	 * @see org.springframework.core.io.ResourceLoader#getResource(String)
	 */
	protected final String[] getConfigLocations() {
		String[] paths = getConfigPaths();
		String[] convertedPaths = new String[paths.length];
		for (int i = 0; i < paths.length; i++) {
			String path = paths[i];
			if (path.startsWith(SLASH)) {
				convertedPaths[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path;
			}
			else if (!ResourcePatternUtils.isUrl(path)) {
				convertedPaths[i] = ResourceUtils.CLASSPATH_URL_PREFIX + SLASH
						+ StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(getClass()) + SLASH + path);
			}
			else {
				convertedPaths[i] = StringUtils.cleanPath(path);
			}
		}
		return convertedPaths;
	}

	/**
	 * Subclasses must override this method to return paths to their config
	 * files, relative to the concrete test class.
	 * <p>A plain path, e.g. "context.xml", will be loaded as classpath resource
	 * from the same package that the concrete test class is defined in. A path
	 * starting with a slash is treated as fully qualified class path location,
	 * e.g.: "/org/springframework/whatever/foo.xml".
	 * <p>The default implementation returns an empty array.
	 * @return an array of config locations
	 * @see java.lang.Class#getResource(String)
	 */
	protected abstract String[] getConfigPaths();

	/**
	 * Return the ApplicationContext that this base class manages; may be
	 * {@code null}.
	 */
	protected final ConfigurableApplicationContext getApplicationContext() {
		// lazy load, in case setUp() has not yet been called.
		if (this.applicationContext == null) {
			try {
				this.applicationContext = getContext(getConfigLocations());
			}
			catch (Exception e) {
				// log and continue...
				if (this.logger.isDebugEnabled()) {
					this.logger.debug("Caught exception while retrieving the ApplicationContext for test [" +
							getClass().getName() + "." + getName() + "].", e);
				}
			}
		}

		return this.applicationContext;
	}

}
