/* * Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute * Copyright [2016-2017] EMBL-European Bioinformatics Institute * * 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.ensembl.healthcheck; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.ensembl.PackageScan; import org.ensembl.healthcheck.testcase.EnsTestCase; /** * <p> * A class for instantiating tests and groups of tests. * </p> * * </p> * Tests can be referred to by the user by their class name or by an * alias. That is why the name can't be passed directly to the the * classloader for instantiation. This class creates a map that maps * the possible names of tests to their classname. This is what * the public "forName" method uses to create instances of tests or * testgroups. * </p> * */ public class TestInstantiator { static final Logger log = Logger.getLogger(TestInstantiator.class.getCanonicalName()); /** * <p> * An array of packages that will be scanned to find testcases. Testcases * are classes which inherit from EnsTestCase. * </p> */ private final String[] packageToScan; /** * <p> * A map that maps the names of testcases to their class names. Testcases * can be known under different names. The names under which they are * known are the ones as which they register themselves. The default * behaviour is to use the classes simple name which is inherited from * EnsTestCase. Any testcase can overwrite that, so to find the names, * each testcase must be instantiated. * </p> */ private final Map<String,String> aliasToClassName; /** * Getter for the aliasToClassName attribute. * */ public Map<String, String> getAliasToClassName() { return aliasToClassName; } public TestInstantiator(String... packageToScan) { this.packageToScan = packageToScan; aliasToClassName = createMap(packageToScan); } /** * <p> * Takes a testName as a parameter and returns the class for which it * stands. * </p> * * <p> * The testName can be the full name of a class. This method tries to load * such a method using Class.forName(testName). If that fails, it checks, * if testName is an alias and tries to instantiate the class to which the * alias maps. If this succeeds, the class is returned, if this fails too, * a RuntimeException is thrown. * </p> * * @param testName * @return Class<EnsTestCase> * */ public Class<?> forName(String testName) { Class<?> testClass = null; try { testClass = (Class<?>) Class.forName(testName); log.fine(testName + " is a class name, was able to instantiate it directly."); } catch (ClassNotFoundException e) { if (!aliasToClassName.containsKey(testName)) { throw new RuntimeException( "Could not find " + testName + "! It is neither a name of" + " a class nor a valid alias.\n" + aliasToClassName ); } String className = aliasToClassName.get(testName); log.fine(testName + " maps to " + className.getClass().getName()); try { testClass = (Class<?>) Class.forName(className); } catch (ClassNotFoundException f) { throw new RuntimeException(f); } } if (testClass == null) { throw new NullPointerException(); } return testClass; } /** * * <p> * Creates a new instance of a class. Catches exceptions and rethrows * them as RuntimeExceptions. * </p> * * @param <T> * @param loadedClass * @return new instance * */ protected <T> T newInstance(Class<T> loadedClass) { T testcase = null; try { testcase = loadedClass.newInstance(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } return testcase; } /** * <p> * Loads a test class denoted by testName and instantiates it. Does no * type checking of the loaded class. If you want type checking, use * </p> * * <code> * instanceByName(String testName, Class<T> expectedType) * </code> * * <p> * instead. * </p> * * @param <T> * @param testName * @return instance of test */ public <T> T instanceByName(String testName) { Class<T> loadedClass = (Class<T>) forName(testName); return newInstance(loadedClass); } /** * <p> * Loads a test class denoted by testName and instantiates it. Checks, * if the loaded class is of the expectedType or a subclass of it. * </p> * * @param <T> * @param testName * @param expectedType * @return instance of test */ public <T> T instanceByName(String testName, Class<T> expectedType) { Class<T> loadedClass = (Class<T>) forName(testName); if (expectedType.isAssignableFrom(loadedClass)) { return (T)instanceByName(testName); } throw new RuntimeException(testName + " is not a test case!"); } /** * <p> * Returns a list of names as which the given class registers. * </p> */ protected static List<String> knownNamesFor(Class<?> s) { List<String> result = new ArrayList<String>(); Object theClassInstance = null; try { boolean isAbstractClass = Modifier.isAbstract(s.getModifiers()); if (isAbstractClass) { return result; } theClassInstance = s.newInstance(); } catch (InstantiationException e) { log.config("Could not instantiate " + s + ", got an InstantiationException!"); } catch (IllegalAccessException e) { log.config("Could not instantiate " + s + ", got an IllegalAccessException!"); } if (theClassInstance == null) { return result; } if (EnsTestCase.class.isAssignableFrom(theClassInstance.getClass())) { result.addAll(knownNamesForEnsTestCase((EnsTestCase) theClassInstance)); } if (GroupOfTests.class.isAssignableFrom(theClassInstance.getClass())) { result.addAll(knownNamesForGroupOfTests((GroupOfTests) theClassInstance)); } return result; } /** * <p> * Groups of tests can be called with their short class names. So this * class returns just that short name. * </p> */ protected static List<String> knownNamesForGroupOfTests(GroupOfTests g) { List<String> result = new ArrayList<String>(); if (g == null) { return result; } result.add(g.getName()); return result; } /** * <p> * Returns a list of names as which this testcase can be called. The names * are found by calling the getShortTestName and the getVeryShortTestName * methods on the test. * </p> */ protected static List<String> knownNamesForEnsTestCase(EnsTestCase etc) { List<String> result = new ArrayList<String>(); if (etc == null) { return result; } result.add(etc.getShortTestName()); boolean veryShortTestNameCanBeCalled = true; try { // The method creates the very short test name by trying to cut // off the substring "TestCase" at the end of the name like this: // // return name.substring(0, name.lastIndexOf("TestCase")); // // If the name of the test does not contain the string "TestCase" // and it usually does not, then lastIndexOf returns -1 and sthe // substring method returns a StringIndexOutOfBoundsException. // etc.getVeryShortTestName(); } catch (StringIndexOutOfBoundsException e) { // // which is caught here. // veryShortTestNameCanBeCalled = false; } if (veryShortTestNameCanBeCalled) { if (!etc.getShortTestName().equals( etc.getVeryShortTestName() )) { result.add(etc.getVeryShortTestName()); } } return result; } /** * <p> * Adds a {@see: keyValuePair} to a map like the aliasToClassName * attribute of this class. * </p> * * <p> * It checks, if the keyValuePair has already been added and if so, if * it contradicts what has already been stored. If so, prints out a * warning, but adds it anyway. * </p> * */ public static void addToMapWithCheck(Map simpleNameToClass, keyValuePair currentKeyValuePair) { boolean aliasAlreadyMappedToOtherClass = // has the alias already been added simpleNameToClass.containsKey(currentKeyValuePair.key) // and && // is the class to which it is mapped different // from what we want to add now ( !currentKeyValuePair.value.equals(simpleNameToClass.get(currentKeyValuePair.key) ) ); // If so, we have an ambiguous short name // if (aliasAlreadyMappedToOtherClass) { log.config( currentKeyValuePair.key + " is an ambiguous alias!\n" + currentKeyValuePair.value + "\n" + simpleNameToClass.get(currentKeyValuePair.key) + "\n" ); } simpleNameToClass.put( currentKeyValuePair.key, currentKeyValuePair.value ); } /** * <p> * Scans an array of packages for classes that are subclasses of * EnsTestCase or GroupsOfTests. * </p> * */ public static Map<String,String> createMap(String[] packagesToScan) { Map<String,String> nameMap = new HashMap<String,String>(); for (String packageToScan : packagesToScan) { log.config("Scanning package " + packageToScan); Map<String,String> currentNameMap = createMap(packageToScan); // Duplicate keys would override each other in the final hash // without warning. // nameMap.putAll(currentNameMap); } return nameMap; } /** * Scans a given package for classes that are subclasses of EnsTestCase. * * @param packageToScan * */ public static Map<String,String> createMap(String packageToScan) { Map<String,String> simpleNameToClass = new HashMap(); List<Class<?>> classesInPackage = null; try { classesInPackage = PackageScan.getClassesForPackage(packageToScan, true); } catch(ClassNotFoundException e) { throw new RuntimeException(e); } if (classesInPackage == null) { throw new NullPointerException(); } for (Class<?> s : classesInPackage) { List<String> testNames = null; testNames = knownNamesFor(s); if (testNames==null) { log.warning("I don't know what " + s.getName() + " is. It does not look like a test or a group, so skipping."); } else { for (String testName : testNames) { addToMapWithCheck( simpleNameToClass, new keyValuePair( testName, s.getName() ) ); } } } return simpleNameToClass; } /** * * Summarises the TestInstantiator. * * @see java.lang.Object#toString() */ public String toString() { StringBuffer scannedPackages = new StringBuffer(); for (String p : packageToScan) { scannedPackages.append(" - " + p + "\n"); } return "Summary of " + TestInstantiator.class.getCanonicalName() + "\n\n" + "Scanned packages: \n" + scannedPackages + "\n\n" + "Aliases\n\n" + aliasToClassNametoString(this.aliasToClassName); } /** * Converts a map like the aliasToClassName instance variable to a string * representation. * * @param aliasToClassName * */ public String aliasToClassNametoString(Map<String,String> aliasToClassName) { StringBuffer result = new StringBuffer(); Iterator<String> i = aliasToClassName.keySet().iterator(); while (i.hasNext()) { String s = i.next(); result.append(" - " + String.format("%1$-40s",s) + " -> " + aliasToClassName.get(s).toString() + "\n"); } return result.toString(); } } class keyValuePair { public keyValuePair(String key, String value) { this.key = key; this.value = value; } public String key; public String value; }