package org.sigmah.server.inject; /* * #%L * Sigmah * %% * Copyright (C) 2010 - 2016 URD * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.InitializationError; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.persist.PersistService; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.junit.runner.notification.RunNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; /** * <p> * Guice JUnit runner class. * </p> * <p> * Use this class as following : * </p> * * <pre> * @RunWith(GuiceJUnitRunner.class) * @GuiceModules({ ComponentsTestModule.class, ServicesTestModule.class }) * public class ServiceTest { * *     @Inject *     private IService service; *   *     @Test *     public void testApp() { *         Assert.assertEquals("Hello World!", service.doSomething()); *     } * } * </pre> * * @author Denis Colliot (dcolliot@ideia.fr) */ public final class GuiceJUnitRunner extends BlockJUnit4ClassRunner { /** * Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(GuiceJUnitRunner.class); /** * Annotation used to specify modules to load for JUnit tests. * * @author Denis Colliot (dcolliot@ideia.fr) */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public static @interface GuiceModules { /** * Module(s) classe(s) to initialize into injector. * * @return An array of the classes to use to initialize the injector. */ Class<?>[] value(); } /** * The injector. */ private final Injector injector; /** * State of the database. * <p> * <code>true</code> if the database is required by the test class but * no local database was found. * <code>false</code> otherwise. * <p> * If <code>true</code> the whole test class will be skipped. */ private final boolean databaseRequiredButUnavailable; /** * {@inheritDoc} */ @Override public Object createTest() throws Exception { final Object obj = super.createTest(); injector.injectMembers(obj); return obj; } /** * Guice JUnit runner initialization. * * @param klass * The class defining injector modules to initialize. * @throws InitializationError * If an error occurs during injector creation. * @throws ParserConfigurationException * If a required feature of the XML parser is not available. * @throws SAXException * If the database configuration file could not be parsed. * @throws IOException * If an error occurs while reading the database configuration file. * @throws ClassNotFoundException * If the SQL driver was not found. */ public GuiceJUnitRunner(final Class<?> klass) throws InitializationError, ParserConfigurationException, SAXException, IOException, ClassNotFoundException { super(klass); final List<Class<?>> classes = getModulesFor(klass); this.injector = createInjectorFor(classes); boolean databaseIsRequiredAndUnavailable = false; if (classes.contains(PersistenceModule.class)) { if (isDatabaseAvailable()) { startPersistUnit(); } else { databaseIsRequiredAndUnavailable = true; } } this.databaseRequiredButUnavailable = databaseIsRequiredAndUnavailable; } /** * {@inheritDoc} */ @Override public void run(RunNotifier notifier) { if (databaseRequiredButUnavailable) { notifier.fireTestIgnored(getDescription()); } else { super.run(notifier); } } /** * Creates the {@link Injector} instance for the given modules {@code classes}. * * @param classes * The modules classes. * @return the created {@link Injector} instance. * @throws InitializationError * If an error occurs during module(s) instantiation. */ private static Injector createInjectorFor(final List<Class<?>> classes) throws InitializationError { final Module[] modules = new Module[classes.size()]; int index = 0; for (final Class<?> klass : classes) { try { modules[index++] = (Module) (klass).newInstance(); } catch (final InstantiationException | IllegalAccessException e) { throw new InitializationError(e); } } return Guice.createInjector(modules); } /** * Returns the modules classes defined in given {@code klass} {@link GuiceModules} annotation. * * @param klass * The class defining a {@code GuiceModules} annotation. * @return The modules classes defined in given {@code klass} {@link GuiceModules} annotation. * @throws InitializationError * If the {@code klass} does not define a {@link GuiceModules} annotation. */ private static List<Class<?>> getModulesFor(final Class<?> klass) throws InitializationError { final GuiceModules annotation = klass.getAnnotation(GuiceModules.class); if (annotation == null) { throw new InitializationError("Missing @GuiceModules annotation for unit test '" + klass.getName() + "'"); } return Arrays.asList(annotation.value()); } /** * Starts the {@link PersistService}. * <b>Should be done only once.</b> */ private void startPersistUnit() { injector.getInstance(PersistService.class).start(); } /** * Parse the <code>META-INF/persistence.xml</code> file and try to connect to * the database. * * @return <code>true</code> if the connection was successful, * <code>false</code> otherwise. */ private boolean isDatabaseAvailable() throws ParserConfigurationException, SAXException, IOException, ClassNotFoundException { final PersistenceXmlHandler configuration = parsePersistenceXml(); Class.forName(configuration.getDriverClass()); boolean available; try { final Connection testConnection = DriverManager.getConnection( configuration.getConnectionUrl(), configuration.getUsername(), configuration.getPassword()); testConnection.close(); available = true; } catch (SQLException ex) { LOGGER.trace("Unable to open a connection to the test database.", ex); available = false; } return available; } /** * Parse the <code>META-INF/persistence.xml</code> file and return its * configuration. * * @return an instance of <code>PersistenceXmlHandler</code> containing the * test database connection properties. * * @throws ParserConfigurationException * If a required feature is not available. * @throws SAXException * If the XML file could not be parsed. * @throws IOException * If an error occurs while reading the file. */ private PersistenceXmlHandler parsePersistenceXml() throws ParserConfigurationException, SAXException, IOException { final SAXParserFactory factory = SAXParserFactory.newInstance(); // Disable validation to avoid fetching the schema for each test. factory.setValidating(false); factory.setFeature("http://xml.org/sax/features/validation", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); final SAXParser parser = factory.newSAXParser(); final PersistenceXmlHandler persistenceXmlHandler = new PersistenceXmlHandler(); parser.parse(getClass().getResourceAsStream("/META-INF/persistence.xml"), persistenceXmlHandler); return persistenceXmlHandler; } }