package atg.test;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import junit.framework.TestCase;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import atg.nucleus.GenericService;
import atg.nucleus.Nucleus;
import atg.nucleus.logging.ClassLoggingFactory;
import atg.nucleus.logging.ConsoleLogListener;
import atg.test.configuration.BasicConfiguration;
import atg.test.configuration.RepositoryConfiguration;
import atg.test.util.FileUtil;
import atg.test.util.RepositoryManager;
/**
* Replacement base class for {@link AtgDustTestCase}. Extend this class and use
* the following 'pattern' whenever you want to junit test some atg components:
* <ul>
* <li><b>Copy</b> all needed configuration and repository mapping files to a
* staging location outside of your source tree using<b>
* {@link AtgDustCase#copyConfigurationFiles(String[], String, String...)}</b>.
* The staging directory will automatically be used as the configuration
* directory. Copying all needed priorities to a location outside of the source
* tree is the preferred method, because this frameworks creates properties on
* the fly and that could pollute your current source tree.</li>
* <!--
* <li><b>
*
* <i>Or: </i></b>tell {@link AtgDustCase} class where the configuration
* location is by using <b>{@link AtgDustCase#setConfigurationLocation(String)}
* </b>, but be aware that the location will also be used for properties file
* generation.</li>
* -->
* </ul>
*
* <!-- p> <b>Rule of thumb:</b> When running repository tests, copy everything
* outside of your source tree (or when you use maven, use the target directory
* ). If you run basic component/formhandler tests, pointing it to your existing
* configuration directory might be sufficient.
*
* </p-->
*
* Repository based tests are depended on one of the two steps previously
* described plus:
* <ul>
* <li><b>{@link AtgDustCase#prepareRepository(String, String...)}</b> for
* testing against an default in-memory hsql database or <b>
* {@link AtgDustCase#prepareRepository(String, Properties, boolean, String...)}
* </b> for testing against an existing database.</li>
* </ul>
*
* If you need to generate some components "on the fly":
* <ul>
* <li><b>{@link AtgDustCase#createPropertyFile(String, String, Class)}</b></li>
* </ul>
*
* <p>
* Example usage can be found in test.SongsRepositoryTest.
* </p>
*
* <p>
* This class overrides Junit 3 and not Junit 4 because currently Junit 4 has
* some test runner/eclipse related bugs which makes it impossible for me to use
* it.
* </p>
*
* @author robert
*/
@SuppressWarnings("unchecked")
public class AtgDustCase extends TestCase {
private static final Logger log = Logger.getLogger(AtgDustCase.class);
private RepositoryManager repositoryManager = new RepositoryManager();
private final BasicConfiguration basicConfiguration = new BasicConfiguration();
private File configurationLocation;
private Nucleus nucleus;
private boolean isDebug;
private String atgConfigPath;
private String environment;
private String localConfig;
private List<String> configDstsDir;
private static Map<String, Long> CONFIG_FILES_TIMESTAMPS,
CONFIG_FILES_GLOBAL_FORCE = null;
private static Class<?> perflib;
public static final File TIMESTAMP_SER = new File(System
.getProperty("java.io.tmpdir")
+ File.separator + "atg-dust-tstamp-rh.ser"),
GLOBAL_FORCE_SER = new File(System.getProperty("java.io.tmpdir")
+ File.separator + "atg-dust-gforce-rh.ser");
private static long SERIAL_TTL = 43200000L;
/**
* Every *.properties file copied using this method will have it's scope (if
* one is available) set to global.
*
* @param srcDirs
* One or more directories containing needed configuration files.
* @param dstDir
* where to copy the above files to. This will also be the
* configuration location.
* @param excludes
* One or more directories not to include during the copy
* process. Use this one to speeds up the test cycle
* considerably. You can also call it with an empty
* {@link String[]} or <code>null</code> if nothing should be
* excluded
* @throws IOException
* Whenever some file related error's occur.
*/
protected final void copyConfigurationFiles(final String[] srcDirs,
final String dstDir, final String... excludes) throws IOException {
setConfigurationLocation(dstDir);
if (log.isDebugEnabled()) {
log.debug("Copying configuration files and "
+ "forcing global scope on all configs");
}
preCopyingOfConfigurationFiles(srcDirs, excludes);
for (final String srcs : srcDirs) {
FileUtil.copyDirectory(srcs, dstDir, Arrays
.asList(excludes == null ? new String[] {} : excludes));
}
forceGlobalScopeOnAllConfigs(dstDir);
if (FileUtil.isDirty()) {
FileUtil.serialize(GLOBAL_FORCE_SER, FileUtil
.getConfigFilesTimestamps());
}
}
/**
* Donated by Remi Dupuis
*
* @param properties
* @throws IOException
*/
protected final void manageConfigurationFiles(Properties properties)
throws IOException {
String atgConfigPath = properties.getProperty("atgConfigsJars").replace("/", File.separator);
String[] configs = properties.getProperty("configs").split(",");
String environment = properties.getProperty("environment");
String localConfig = properties.getProperty("localConfig");
String[] excludes = properties.getProperty("excludes").split(",");
String rootConfigDir = properties.getProperty("rootConfigDir").replace(
"/", File.separator);
int i = 0;
for (String conf : configs) {
String src = conf.split(" to ")[0];
String dst = conf.split(" to ")[1];
configs[i] = (rootConfigDir + "/" + src.trim() + " to "
+ rootConfigDir + "/" + dst.trim()).replace("/",
File.separator);
i++;
}
i = 0;
for (String dir : excludes) {
excludes[i] = dir.trim();
i++;
}
final List<String> srcsAsList = new ArrayList<String>();
final List<String> distsAsList = new ArrayList<String>();
for (String config : configs) {
srcsAsList.add(config.split(" to ")[0]);
distsAsList.add(config.split(" to ")[1]);
}
this.atgConfigPath = atgConfigPath;
this.environment = environment;
this.localConfig = localConfig;
// The Last dstdir is used for Configuration location
setConfigurationLocation(distsAsList.get(distsAsList.size() - 1));
if (log.isDebugEnabled()) {
log.debug("Copying configuration files and "
+ "forcing global scope on all configs");
}
preCopyingOfConfigurationFiles(srcsAsList.toArray(new String[] {}),
excludes);
log.info("Copying configuration files and "
+ "forcing global scope on all configs");
// copy all files to it's destination
for (String config : configs) {
FileUtil.copyDirectory(config.split(" to ")[0], config
.split(" to ")[1], Arrays
.asList(excludes == null ? new String[] {} : excludes));
log.debug(config);
log.debug(config.split(" to ")[0]);
log.debug(config.split(" to ")[1]);
}
// forcing global scope on all configurations
for (String config : configs) {
String dstDir = config.split(" to ")[1];
// forcing global scope on all property files
forceGlobalScopeOnAllConfigs(dstDir);
}
this.configDstsDir = distsAsList;
}
/**
* @param configurationStagingLocation
* The location where the property file should be created. This
* will also set the {@link AtgDustCase#configurationLocation}.
*
* @param nucleusComponentPath
* Nucleus component path (e.g /Some/Service/Impl).
*
* @param clazz
* The {@link Class} implementing the nucleus component specified
* in previous argument.
*
* @throws IOException
* If we have some File related errors
*/
protected final void createPropertyFile(
final String configurationStagingLocation,
final String nucleusComponentPath, final Class<?> clazz)
throws IOException {
this.configurationLocation = new File(configurationStagingLocation);
FileUtil.createPropertyFile(nucleusComponentPath,
configurationLocation, clazz.getClass(),
new HashMap<String, String>());
}
/**
* Prepares a test against an default in-memory hsql database.
*
* @param repoPath
* the nucleus component path of the repository to be tested.
*
* @param definitionFiles
* one or more repository definition files.
* @throws IOException
* The moment we have some properties/configuration related
* error
* @throws SQLException
* Whenever there is a database related error
*
*/
protected final void prepareRepository(final String repoPath,
final String... definitionFiles) throws SQLException, IOException {
final Properties properties = new Properties();
properties.put("driver", "org.hsqldb.jdbcDriver");
properties.put("url", "jdbc:hsqldb:mem:testDb");
properties.put("user", "sa");
properties.put("password", "");
prepareRepository(repoPath, properties, true, true, definitionFiles);
}
/**
* Prepares a test against an existing database.
*
* @param repositoryPath
* The the repository to be tested, specified as nucleus
* component path.
* @param connectionProperties
* A {@link Properties} instance with the following values (in
* this example the properties are geared towards an mysql
* database):
*
* <pre>
* final Properties properties = new Properties();
* properties.put("driver", "com.mysql.jdbc.Driver");
* properties.put("url", "jdbc:mysql://localhost:3306/someDb");
* properties.put("user", "someUserName");
* properties.put("password", "somePassword");
* </pre>
*
*
* @param dropTables
* If <code>true</code> then existing tables will be dropped and
* re-created, if set to <code>false</code> the existing tables
* will be used.
*
* @param createTables
* if set to <code>true</code> all non existing tables needed for
* the current test run will be created, if set to
* <code>false</code> this class expects all needed tables for
* this test run to be already created
*
* @param definitionFiles
* One or more needed repository definition files.
* @throws IOException
* The moment we have some properties/configuration related
* error
* @throws SQLException
* Whenever there is a database related error
*
*/
protected final void prepareRepository(final String repositoryPath,
final Properties connectionProperties,
final boolean dropTables,
final boolean createTables,
final String... definitionFiles)
throws SQLException, IOException {
final Map<String, String> connectionSettings = new HashMap<String, String>();
for (final Iterator<Entry<Object, Object>> it = connectionProperties
.entrySet().iterator(); it.hasNext();) {
final Entry<Object, Object> entry = it.next();
connectionSettings.put((String) entry.getKey(), (String) entry
.getValue());
}
final RepositoryConfiguration repositoryConfiguration = new RepositoryConfiguration();
repositoryConfiguration.setDebug(isDebug);
repositoryConfiguration.createPropertiesByConfigurationLocation(configurationLocation);
repositoryConfiguration.createFakeXADataSource(configurationLocation, connectionSettings);
repositoryConfiguration.createRepositoryConfiguration(
configurationLocation, repositoryPath, dropTables,
createTables, definitionFiles);
repositoryManager.initializeMinimalRepositoryConfiguration(
configurationLocation, repositoryPath, connectionSettings,
dropTables, isDebug, definitionFiles);
}
/**
* Method for retrieving a fully injected atg component
*
* @param nucleusComponentPath
* Path to a nucleus component (e.g. /Some/Service/Impl).
* @return Fully injected instance of the component registered under
* previous argument or <code>null</code> if there is an error.
* @throws IOException
*/
protected Object resolveNucleusComponent(final String nucleusComponentPath)
throws IOException {
startNucleus(configurationLocation);
return enableLoggingOnGenericService(nucleus .resolveName(nucleusComponentPath));
}
/**
* Call this method to set the configuration location.
*
* @param configurationLocation
* The configuration location to set. Most of the time this
* location is a directory containing all repository definition
* files and component property files which are needed for the
* test.
*/
protected final void setConfigurationLocation(
final String configurationLocation) {
this.configurationLocation = new File(configurationLocation);
if (log.isDebugEnabled()) {
log.debug("Using configuration location: "
+ this.configurationLocation.getPath());
}
}
/**
* Always make sure to call this because it will do necessary clean up
* actions (shutting down in-memory database (if it was used) and the
* nucleus) so the next test can run safely.
*/
@Override
protected void tearDown() throws Exception {
log.debug("tearDown(): begin");
super.tearDown();
if (nucleus != null) {
log.debug("tearDown(): shutting down nucleus");
nucleus.doStopService();
nucleus.stopService();
nucleus.destroy();
}
if (repositoryManager != null) {
log.debug("tearDown(): shutting down in memory db");
repositoryManager.shutdownInMemoryDbAndCloseConnections();
}
}
/**
* Enables or disables the debug level of nucleus components.
*
* @param isDebug
* Setting this to <code>true</code> will enable debug on all
* (currently only on repository related) components, setting it
* to <code>false</code> turn's the debug off again.
*/
protected void setDebug(boolean isDebug) {
this.isDebug = isDebug;
}
/**
*
* @param configpath
* @return
* @throws IOException
*/
private void startNucleus(final File configpath) throws IOException {
if (nucleus == null || !nucleus.isRunning()) {
ClassLoggingFactory.getFactory();
basicConfiguration.setDebug(isDebug);
basicConfiguration.createPropertiesByConfigurationLocation(configpath);
System.setProperty("atg.dynamo.license.read", "true");
System.setProperty("atg.license.read", "true");
// TODO: Can I safely keep this one disabled?
// NucleusServlet.addNamingFactoriesAndProtocolHandlers();
if (environment != null && !environment.equals("")) {
for (String property : environment.split(";")) {
String[] keyvalue = property.split("=");
System.setProperty(keyvalue[0], keyvalue[1]);
log.info(keyvalue[0] + "=" + keyvalue[1]);
}
}
String fullConfigPath = "";
if (atgConfigPath != null && !atgConfigPath.equals("")) {
fullConfigPath = atgConfigPath + ";" + fullConfigPath;
}
if (configDstsDir != null && configDstsDir.size() > 0) {
for (String dst : configDstsDir) {
fullConfigPath = fullConfigPath + dst + ";";
}
} else {
fullConfigPath = configpath.getAbsolutePath();
}
if (atgConfigPath != null && !atgConfigPath.equals("")) {
fullConfigPath = fullConfigPath + localConfig.replace("/", File.separator);
}
log.info("The full config path used to start nucleus: " + fullConfigPath);
System.setProperty("atg.configpath", new File(fullConfigPath).getAbsolutePath());
nucleus = Nucleus.startNucleus(new String[] { fullConfigPath });
}
}
/**
* Will enable logging on the object/service that was passed in (as a method
* argument) if it's an instance of {@link GenericService}. This method is
* automatically called from
* {@link AtgDustCase#resolveNucleusComponent(String)}. Debug level is
* enabled the moment {@link AtgDustCase#setDebug(boolean)} was called with
* <code>true</code>.
*
* @param service
* an instance of GenericService
*
* @return the GenericService instance that was passed in with all log
* levels enabled, if it's a {@link GenericService}
*/
private Object enableLoggingOnGenericService(final Object service) {
if (service instanceof GenericService) {
((GenericService) service).setLoggingDebug(isDebug);
((GenericService) service).setLoggingInfo(true);
((GenericService) service).setLoggingWarning(true);
((GenericService) service).setLoggingError(true);
((GenericService) service)
.removeLogListener(new ConsoleLogListener());
((GenericService) service).addLogListener(new ConsoleLogListener());
}
return service;
}
private void preCopyingOfConfigurationFiles(final String[] srcDirs,
final String excludes[]) throws IOException {
boolean isDirty = false;
for (final String src : srcDirs) {
for (final File file : (List<File>) FileUtils.listFiles(new File(
src), null, true)) {
if (!Arrays.asList(
excludes == null ? new String[] {} : excludes)
.contains(file.getName())
&& !file.getPath().contains(".svn") && file.isFile()) {
if (CONFIG_FILES_TIMESTAMPS.get(file.getPath()) != null
&& file.lastModified() == CONFIG_FILES_TIMESTAMPS
.get(file.getPath())) {
} else {
CONFIG_FILES_TIMESTAMPS.put(file.getPath(), file
.lastModified());
isDirty = true;
}
}
}
}
if (isDirty) {
if (log.isDebugEnabled()) {
log
.debug("Config files timestamps map is dirty an will be re serialized");
}
FileUtil.serialize(TIMESTAMP_SER, CONFIG_FILES_TIMESTAMPS);
}
FileUtil.setConfigFilesTimestamps(CONFIG_FILES_TIMESTAMPS);
FileUtil.setConfigFilesGlobalForce(CONFIG_FILES_GLOBAL_FORCE);
}
private void forceGlobalScopeOnAllConfigs(final String dstDir)
throws IOException {
if (perflib == null) {
for (final File file : (List<File>) FileUtils.listFiles(new File(
dstDir), new String[] { "properties" }, true)) {
new FileUtil().searchAndReplace("$scope=", "$scope=global\n",
file);
}
} else {
try {
List<File> payload = (List<File>) FileUtils.listFiles(new File(
dstDir), new String[] { "properties" }, true);
Method schedule = perflib.getMethod("schedule", new Class[] {
int.class, List.class, Class.class, String.class,
Class[].class, List.class });
List<Object> list = new ArrayList<Object>();
list.add("$scope=");
list.add("$scope=global\n");
schedule.invoke(perflib.newInstance(), 4, payload,
FileUtil.class, "searchAndReplace", new Class[] {
String.class, String.class, File.class }, list);
} catch (Exception e) {
log.error("Error: ", e);
}
}
}
static {
final String s = System.getProperty("SERIAL_TTL");
if (log.isDebugEnabled()) {
log.debug(s == null ? "SERIAL_TTL has not been set "
+ "using default value of: " + SERIAL_TTL
+ " m/s or start VM with -DSERIAL_TTL=some_number_value"
: "SERIAL_TTL is set to:" + s);
}
try {
SERIAL_TTL = s != null ? Long.parseLong(s) * 1000 : SERIAL_TTL;
} catch (NumberFormatException e) {
log.error("Error using the -DSERIAL_TTL value: ", e);
}
CONFIG_FILES_TIMESTAMPS = FileUtil.deserialize(TIMESTAMP_SER,
SERIAL_TTL);
CONFIG_FILES_GLOBAL_FORCE = FileUtil.deserialize(GLOBAL_FORCE_SER,
SERIAL_TTL);
try {
perflib = Class
.forName("com.bsdroot.util.concurrent.SchedulerService");
} catch (ClassNotFoundException e) {
log
.debug("com.bsdroot.util.concurrent experimantal performance library not found, continuing normally");
}
}
}