/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.cluster.integration; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.impl.CatalogImpl; import org.geoserver.cluster.client.JMSQueueListener; import org.geoserver.cluster.events.ToggleType; import org.geoserver.cluster.impl.rest.Controller; import org.geoserver.config.GeoServer; import org.geoserver.config.GeoServerDataDirectory; import org.geoserver.config.GeoServerLoader; import org.geoserver.config.GeoServerPersister; import org.geoserver.config.util.XStreamPersister; import org.geoserver.config.util.XStreamPersisterFactory; import org.geoserver.data.test.SystemTestData; import org.geoserver.logging.LoggingUtils; import org.geoserver.platform.ContextLoadedEvent; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.test.DirectoryResourceLoader; import org.geoserver.test.GeoServerTestApplicationContext; import org.geoserver.util.IOUtils; import org.geotools.util.logging.Log4JLoggerFactory; import org.geotools.util.logging.Logging; import org.geotools.xml.XSD; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.WebApplicationContext; import java.io.File; import java.io.IOException; import java.util.UUID; /** * Creates a GeoServer instance that can be used to tests JMS synchronizations. Note that * only the bare minimum required elements will be instance. */ public final class GeoServerInstance { // this test data directory will be used to instantiate all GeoServe instances private static final SystemTestData BASE_TEST_DATA = createTestData(); // cluster name for the interaction tests private static final String CLUSTER_NAME = UUID.randomUUID().toString(); /** * Helper method that creates a system test data directory. */ private static SystemTestData createTestData() { SystemTestData testData; // instantiate a test data directory try { // initiate the test data testData = new SystemTestData(); testData.setUp(); // add layers and styles testData.setUpDefault(); // add GeoServer default styles, this will allow us to have the same ids addDefaultStyles(testData.getDataDirectoryRoot()); } catch (Exception exception) { throw new RuntimeException("Error creating base test directory.", exception); } // add a JVM shutdown for removing the test data directory Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { IOUtils.delete(BASE_TEST_DATA.getDataDirectoryRoot()); } catch (Exception exception) { throw new RuntimeException(String.format( "Error deleting base test data directory '%s'.", BASE_TEST_DATA.getDataDirectoryRoot().getAbsolutePath()), exception); } })); return testData; } /** * Helper method that adds GeoServer default styles to a dt directory. */ private static void addDefaultStyles(File dataDirectory) throws IOException { // prepare all the necessary GeoServer objects File stylesDirectory = new File(dataDirectory, "styles"); GeoServerResourceLoader loader = new GeoServerResourceLoader(dataDirectory); Catalog catalog = new CatalogImpl(); catalog.setResourceLoader(loader); XStreamPersister xstreamPersister = new XStreamPersisterFactory().createXMLPersister(); GeoServerPersister geoserverPersister = new GeoServerPersister(loader, xstreamPersister); catalog.addListener(geoserverPersister); // create default styles createDefaultStyle(catalog, stylesDirectory, "point", "default_point.sld"); createDefaultStyle(catalog, stylesDirectory, "line", "default_line.sld"); createDefaultStyle(catalog, stylesDirectory, "polygon", "default_polygon.sld"); createDefaultStyle(catalog, stylesDirectory, "raster", "default_raster.sld"); createDefaultStyle(catalog, stylesDirectory, "generic", "default_generic.sld"); } /** * Helper method that adds a GeoServer default style to the provided styles directory. */ private static void createDefaultStyle(Catalog catalog, File stylesDirectory, String styleName, String fileName) { // copy style from classpath to styles directory try { IOUtils.copy(GeoServerLoader.class.getResourceAsStream(fileName), new File(stylesDirectory, fileName)); } catch (Exception exception) { throw new RuntimeException(String.format( "Error copying default style '%s' to directory '%s'.", fileName, stylesDirectory.getAbsolutePath()), exception); } // create GeoServer style object StyleInfo style = catalog.getFactory().createStyle(); style.setName(styleName); style.setFilename(fileName); // add the style to the catalog, GeoServer persister will take of writing the style description catalog.add(style); } // this instance data directory private final File dataDirectory; // this instance application context private final GeoServerTestApplicationContext applicationContext; // will be used to set JMS options private final Controller jmsController; // will be used to listen on consumed events private final JMSQueueListener jmsQueueListener; public GeoServerInstance() { this(null); } public GeoServerInstance(String instanceName) { try { // create this instance base data directory by copying the base test data dataDirectory = createTempDirectory(instanceName == null ? "INSTANCE" : instanceName); IOUtils.deepCopy(BASE_TEST_DATA.getDataDirectoryRoot(), dataDirectory); // disable security manager to speed up tests System.setSecurityManager(null); // take control of the logging Logging.ALL.setLoggerFactory(Log4JLoggerFactory.getInstance()); System.setProperty(LoggingUtils.RELINQUISH_LOG4J_CONTROL, "true"); // initialize Spring application context applicationContext = initInstance(); // get some JMS util beans jmsController = applicationContext.getBean(Controller.class); jmsQueueListener = applicationContext.getBean(JMSQueueListener.class); // set integration tests cluster name jmsController.setGroup(CLUSTER_NAME); saveJmsConfiguration(); } catch (Exception exception) { throw new RuntimeException(String.format( "Error instantiating GeoServer instance '%s'.", instanceName), exception); } } /** * Helper method that just creates a temporary directory using the provide prefix. */ private static File createTempDirectory(String prefix) { try { // creates a temporary directory using the provided prefix return IOUtils.createTempDirectory(prefix + "-"); } catch (Exception exception) { throw new RuntimeException("Error creating temporary directory.", exception); } } /** * Instantiates this GeoServer instance, i.e. a resource loader based on this * instance data directory is instantiated, a mocked servlet context is created * and an application context is initiated. */ private GeoServerTestApplicationContext initInstance() throws Exception { // instantiate GeoServer loader GeoServerResourceLoader loader = new GeoServerResourceLoader(dataDirectory); // setting logging level LoggingUtils.configureGeoServerLogging(loader, this.getClass().getResourceAsStream("/TEST_LOGGING.properties"), false, true, null); // create a mocked servlet context and instantiate the application context MockServletContext servletContext = createServletContext(); GeoServerTestApplicationContext applicationContext = new GeoServerTestApplicationContext( new String[]{ "classpath*:/applicationContext.xml", "classpath*:/applicationSecurityContext.xml" }, servletContext); applicationContext.setUseLegacyGeoServerLoader(false); applicationContext.refresh(); applicationContext.publishEvent(new ContextLoadedEvent(applicationContext)); servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, applicationContext); return applicationContext; } /** * Helper method that creates a mocked servlet context. */ private MockServletContext createServletContext() { // set up a fake WEB-INF directory ResourceLoader loader; if (dataDirectory.canWrite()) { // make sure we have a WEB-INF directory new File(dataDirectory, "WEB-INF").mkdirs(); loader = new DirectoryResourceLoader(dataDirectory); } else { // use the default loader loader = new DefaultResourceLoader(); } // create a mocked servlet context and set some options MockServletContext servletContext = new MockServletContext(loader); servletContext.setMinorVersion(4); servletContext.setInitParameter("GEOSERVER_DATA_DIR", dataDirectory.getPath()); return servletContext; } /** * Returns this GeoServer instance catalog. */ public Catalog getCatalog() { return (Catalog) applicationContext.getBean("catalog"); } /** * Returns this GeoServer instance configuration accessor. */ public GeoServer getGeoServer() { return (GeoServer) applicationContext.getBean("geoServer"); } /** * Returns this GeoServer instance resource loader. */ public GeoServerResourceLoader getResourceLoader() { return (GeoServerResourceLoader) applicationContext.getBean("resourceLoader"); } /** * Returns this GeoServer instance data directory. */ public GeoServerDataDirectory getDataDirectory() { return new GeoServerDataDirectory(getResourceLoader()); } /** * This GeoServer instance will stop propagating JMS events. */ public void disableJmsMaster() { jmsController.toggle(false, ToggleType.MASTER); saveJmsConfiguration(); } /** * This GeoServer instance will propagate JMS events. */ public void enableJmsMaster() { jmsController.toggle(true, ToggleType.MASTER); saveJmsConfiguration(); } /** * This GeoServer instance will ignore JMS events. */ public void disableJmsSlave() { jmsController.toggle(false, ToggleType.SLAVE); saveJmsConfiguration(); } /** * This GeoServer instance will consume JMS events. */ public void enableJmsSlave() { jmsController.toggle(true, ToggleType.SLAVE); saveJmsConfiguration(); } /** * Makes this GeoServer instance belong to the default JMS cluster, * propagate JMS events and consume JMS events. */ public void setJmsDefaultConfiguration() { jmsController.setBrokerURL(""); jmsController.setGroup("geoserver-cluster"); enableJmsMaster(); enableJmsSlave(); } /** * Will wait until the expected number of events was consumed or * the timeout is reached. */ public void waitEvents(int number, int timeoutMs) { int loops = timeoutMs / 25; for (int i = 0; i <= loops && jmsQueueListener.getConsumedEvents() < number; i++) { try { // wait for 25 milliseconds Thread.sleep(25); } catch (InterruptedException exception) { // well we got interrupted Thread.currentThread().interrupt(); } } } /** * Returns the total number of JMS events consumed by this GeoServer instance. */ public int getConsumedEventsCount() { return (int) jmsQueueListener.getConsumedEvents(); } /** * Resets the total number of JMS events consumed by this GeoServer instance. */ public void resetConsumedEventsCount() { jmsQueueListener.resetconsumedevents(); } /** * Helper method tht just saves the current JMS configuration */ private void saveJmsConfiguration() { try { jmsController.save(); } catch (Exception exception) { throw new RuntimeException("Error saving JMS configuration.", exception); } } /** * Destroy everything related with this instance. */ public void destroy() { // dispose XSD schema, this is important for WFS schemas applicationContext.getBeansOfType(XSD.class).values().forEach(XSD::dispose); // destroy Spring application context applicationContext.destroy(); // remove the data directory try { IOUtils.delete(dataDirectory); } catch (Exception exception) { throw new RuntimeException(String.format( "Error deleting test directory '%s'.", dataDirectory.getAbsolutePath()), exception); } } }