package org.marketcetera.strategyagent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.*;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.junit.BeforeClass;
import org.junit.Test;
import org.marketcetera.core.LoggerConfiguration;
import org.marketcetera.core.Pair;
import org.marketcetera.module.ExpectedFailure;
import org.marketcetera.util.except.I18NException;
import org.marketcetera.util.file.Deleter;
/* $License$ */
/**
* Tests {@link JarClassLoader}
*
* @author anshul@marketcetera.com
*/
public class JarClassLoaderTest
{
/**
* Cleans up the jars in the testing directory.
* This method can only be run before the test as attempts to delete
* these files after the test fails as the classloader has these
* files opened and locked. See the
* <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5041014">
* bug report</a> for details on this issue.
*
* @throws I18NException if there were errors.
*/
@BeforeClass
public static void cleanup() throws Exception {
LoggerConfiguration.logSetup();
cleanDir(JAR_DIR);
cleanDir(CONF_DIR);
}
/**
* Tests jar classloader with invalid directories and sub-directories.
*
* @throws Exception if there were unexpected errors.
*/
@Test
public void invalidDirectory()
throws Exception
{
final StrategyAgentApplicationInfoProvider infoProvider = new StaticStrategyAgentApplicationInfoProvider() {
@Override
public File getModulesDir()
{
return new File("/doesnotexist");
}
};
final File f = infoProvider.getModulesDir();
//Invalid module directory
new ExpectedFailure<FileNotFoundException>(Messages.JAR_DIR_DOES_NOT_EXIST.getText(f.getAbsolutePath())){
@SuppressWarnings("resource")
protected void run()
throws Exception
{
new JarClassLoader(infoProvider,
getClass().getClassLoader());
}
};
//Invalid jars directory
final StrategyAgentApplicationInfoProvider jarsInfo = new StaticStrategyAgentApplicationInfoProvider() {
/* (non-Javadoc)
* @see org.marketcetera.strategyagent.StaticStrategyAgentApplicationInfoProvider#getModulesDir()
*/
@Override
public File getModulesDir()
{
return JAR_DIR;
}
};
new ExpectedFailure<FileNotFoundException>(Messages.JAR_DIR_DOES_NOT_EXIST.getText(new File(JAR_DIR,
"jars").getAbsolutePath())) {
@SuppressWarnings("resource")
protected void run()
throws Exception
{
new JarClassLoader(jarsInfo,
getClass().getClassLoader());
}
};
//Create a temporary jars dir in JAR_DIR to get an error for
//conf sub-directory
File jarsDir = new File(JAR_DIR,
"jars");
jarsDir.mkdirs();
assertTrue(jarsDir.isDirectory());
new ExpectedFailure<FileNotFoundException>(Messages.JAR_DIR_DOES_NOT_EXIST.getText(new File(JAR_DIR,
"conf").getAbsolutePath())) {
@SuppressWarnings("resource")
protected void run()
throws Exception
{
new JarClassLoader(jarsInfo,
getClass().getClassLoader());
}
};
//cleanup
Deleter.apply(jarsDir);
}
/**
* Tests class loader with an empty directory.
*
* @throws Exception if there were unexpected errors.
*/
@Test
public void emptyDir() throws Exception {
JarClassLoader loader = createLoader();
//try finding a random resource and verify it fails
assertNull(loader.findResource("random"));
}
/**
* Tests loading of properties files within the conf sub-directory.
*
* @throws Exception if there were unexpected errors.
*/
@Test
public void loadConfResources() throws Exception {
//Prepare a properties file
final String firstRes = "ek.properties";
final Properties firstProp = createProperties("ek", "value");
writePropertiesToFile(firstRes, firstProp);
//Prepare another property but do not write it.
final String secondRes = "do.properties";
final Properties secondProp = createProperties("do", "value");
JarClassLoader loader = createLoader();
//verify that we can read it.
assertEquals(firstProp, loadProperty(firstRes, loader));
//verify the hitherto unwritten property cannot be read.
assertNull(loader.getResourceAsStream(secondRes));
//now write the second properties, verify it can be read without refresh
writePropertiesToFile(secondRes, secondProp);
assertEquals(secondProp, loadProperty(secondRes, loader));
//verify first one is still there
assertEquals(firstProp, loadProperty(firstRes, loader));
//Prepare a third set of property and verify that it can be
//read with refresh
final String thirdRes = "teen.properties";
final Properties thirdProp = createProperties("teen", "value");
writePropertiesToFile(thirdRes, thirdProp);
loader.refresh();
assertEquals(thirdProp, loadProperty(thirdRes, loader));
//verify other two are still there
assertEquals(secondProp, loadProperty(secondRes, loader));
assertEquals(firstProp, loadProperty(firstRes, loader));
}
/**
* Tests loading of jar resources from the jars subdirectory.
*
* @throws Exception if there were unexpected errors.
*/
@Test
public void loadJarResources() throws Exception {
//Create a jar containing some random properties.
final String firstRes = "first.properties";
File jar1 = createJar("first.jar", firstRes,
createProperties("first", "value"));
//Create another jar but with the wrong suffix.
final String secondRes = "second.properties";
createJar("second.mar",
secondRes,
createProperties("second",
"value"));
//Create a third jar for fun
final String thirdRes = "third.properties";
final Properties thirdProp = createProperties("third", "value");
File jar3 = createJar("third.jar", thirdRes,
thirdProp);
//To test sorting, create more jar files with same property files
final Properties firstFoundProp = createProperties("first", "actualValue");
File jar1a = createJar("FIRST1.jar", firstRes, firstFoundProp);
File jar3a = createJar("zhird.jar", thirdRes,
createProperties("third", "notfound"));
//Create the loader and verify that we can load the properties files
JarClassLoader loader = createLoader();
//The 2nd version of the first property file can be loaded
assertEquals(firstFoundProp,loadProperty(firstRes, loader));
//The second one cannot be as the jar was not included.
assertNull(loader.getResourceAsStream(secondRes));
//The third property file can be loaded.
assertEquals(thirdProp,loadProperty(thirdRes, loader));
assertJars(loader, jar1,jar3, jar1a, jar3a);
//Prep a fourth properties file to add to the mix
final String fourthRes = "fourth.properties";
final String fifthRes = "fifth.properties";
final Properties fifthProp = createProperties("fifth", "value");
//Invoke refresh and verify nothing changes
loader.refresh();
assertEquals(firstFoundProp,loadProperty(firstRes, loader));
assertNull(loader.getResourceAsStream(secondRes));
assertEquals(thirdProp,loadProperty(thirdRes, loader));
//verify that the new files cannot be loaded
assertNull(loader.getResourceAsStream(fourthRes));
assertNull(loader.getResourceAsStream(fifthRes));
//Now create jar files for the new properties.
File jar4 = createJar("fourth.jar", fourthRes,
createProperties("fourth", "value"));
//Chose a wrong suffix
createJar("fifth.mar",
fifthRes,
fifthProp);
//Create jars to test ordering behavior
//Cannot override the jars already there before refresh
File jar1b = createJar("A.jar", firstRes,
createProperties("first","notfound"));
//Can override a jar in the same refresh as this one
final Properties fourthFoundProp = createProperties("fourth", "value");
File jar4a = createJar("4ourth.jar", fourthRes,
fourthFoundProp);
//now refresh
loader.refresh();
//verify that the new property can be loaded
assertEquals(fourthFoundProp,loadProperty(fourthRes, loader));
//verify the one with wrong suffix cannot be loaded
assertNull(loader.getResourceAsStream(fifthRes));
//verify the old ones maintain status-quo
assertEquals(firstFoundProp,loadProperty(firstRes, loader));
assertNull(loader.getResourceAsStream(secondRes));
assertEquals(thirdProp,loadProperty(thirdRes, loader));
assertJars(loader, jar1,jar3, jar1a, jar1b, jar3a, jar4, jar4a);
}
/**
* Deletes all the contents of the supplied directory.
*
* @param inDir the directory to be cleaned.
*
* @throws Exception if there were unexpected errors.
*/
private static void cleanDir(File inDir) throws Exception {
assertTrue(inDir.exists());
File [] contents = inDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
//Only delete select files to make sure
//that we do not delete svn files.
return (name.endsWith("jar") ||
name.endsWith("mar") ||
name.endsWith("properties"));
}
});
if(contents != null) {
for(File f: contents) {
Deleter.apply(f);
}
}
}
/**
* Verifies that the supplied class loader has the expected set of files
* in it.
*
* @param inLoader the classloader instance to test.
*
* @param inFiles the expected set of files.
* @throws Exception if there were unexpected errors.
*/
private static void assertJars(JarClassLoader inLoader, File... inFiles)
throws Exception {
HashSet<URL> loaderURLs = new HashSet<URL>(Arrays.asList(
inLoader.getURLs()));
HashSet<URL> expected = new HashSet<URL>();
for(File f: inFiles) {
expected.add(f.toURI().toURL());
}
//add the conf dir to expected
expected.add(CONF_DIR.toURI().toURL());
assertEquals(expected, loaderURLs);
}
/**
* Loads the properties file from the supplied classloader.
*
* @param inRes the properties file name.
* @param inLoader the classloader instance.
*
* @return the loaded properties instance.
*
* @throws IOException if there were unexpected errors.
*/
private Properties loadProperty(String inRes,
JarClassLoader inLoader)
throws IOException {
Properties p = new Properties();
p.load(inLoader.getResourceAsStream(inRes));
return p;
}
/**
* Creates a properties instance with supplied key value pair
* and some other key value pairs.
*
* @param inKey the key
* @param inValue the value
*
* @return the initialized properties instance
*/
private static Properties createProperties(String inKey, String inValue) {
Properties p = new Properties();
p.put(inKey, inValue);
//throw in some extra values for fun
p.put("cat","mouse");
p.put("keyboard","mouse");
return p;
}
/**
* Write the supplied properties instance to the named properties file
* within the conf subdirectory.
*
* @param inFileName the file name
* @param inContents the contents of the file.
*
* @return the file object pointing to the newly created file.
*
* @throws IOException if there were unexpected errors
*/
private static File writePropertiesToFile(
String inFileName, Properties inContents) throws IOException {
final File file = new File(CONF_DIR, inFileName);
FileOutputStream fos = new FileOutputStream(file);
inContents.store(fos,"");
fos.close();
file.deleteOnExit();
return file;
}
/**
* Creates a loader instance rooted at the testing modules dir.
*
* @return a new loader instance
*
* @throws IOException if there were unexpected errors.
*/
private JarClassLoader createLoader()
throws IOException
{
StrategyAgentApplicationInfoProvider provider = new StaticStrategyAgentApplicationInfoProvider() {
/* (non-Javadoc)
* @see org.marketcetera.strategyagent.StaticStrategyAgentApplicationInfoProvider#getModulesDir()
*/
@Override
public File getModulesDir()
{
return MODULE_DIR;
}
};
return new JarClassLoader(provider,
getClass().getClassLoader());
}
/**
* Creates a jar file in the testing directory, with the given name,
* containing a single properties file having the specified with the
* supplied contents.
*
* @param inJarName the name of the jar file.
* @param inFileName the name of the properties file within the jar
* @param inFileContents the contents of the properties file
*
* @return the file object pointing to the newly created jar file.
*
* @throws IOException if there were errors creating the file.
*/
private static File createJar(String inJarName,
String inFileName,
Properties inFileContents)
throws IOException {
//Write the properties contents to a temporary byte array.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
inFileContents.store(baos,"");
baos.close();
return createJar(inJarName,new JarContents[]{
new JarContents(inFileName, baos.toByteArray())});
}
/**
* Instances of this class specify contents that need to be stuffed
* into a jar file.
*
* @see JarClassLoaderTest#createJar(String, String, Properties)
*/
static class JarContents extends Pair<String, byte[]> {
/**
* Creates a new instance.
*
* @param o1 the path of the file to be added to the jar.
* @param o2 the contents of the file to be added.
*/
public JarContents(String o1, byte[] o2) {
super(o1, o2);
}
}
/**
* Creates a new jar in the jars sub-directory with the specified name
* and the contents.
*
* @param inJarName the name of the jar file.
* @param inContents the contents of the jar file.
*
* @return the file object pointing to the newly created jar.
*
* @throws IOException if there were unexpected errors.
*/
static File createJar(String inJarName,
JarContents[] inContents)
throws IOException {
File jar = new File(JAR_DIR, inJarName);
FileOutputStream fos = new FileOutputStream(jar);
JarOutputStream jos = new JarOutputStream(fos);
for(JarContents jc: inContents) {
JarEntry entry = new JarEntry(jc.getFirstMember());
jos.putNextEntry(entry);
jos.write(jc.getSecondMember());
}
jos.close();
fos.close();
jar.deleteOnExit();
return jar;
}
/**
* The directory where all the sample data is kept.
*/
static final File SAMPLE_DATA_DIR = new File("src" + File.separator +
"test", "sample_data");
/**
* The directory where all the module files are kept.
*/
static final File MODULE_DIR = new File(SAMPLE_DATA_DIR, "modules");
/**
* The directory where all the jars are kept.
*/
static final File JAR_DIR = new File(MODULE_DIR, "jars");
/**
* The directory where all the module configuration files are kept.
*/
static final File CONF_DIR = new File(MODULE_DIR, "conf");
}