/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wms; import static junit.framework.TestCase.fail; import static org.geoserver.data.test.MockData.WORLD; import static org.junit.Assert.*; import java.awt.Color; import java.awt.Frame; import java.awt.Graphics; import java.awt.Panel; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import javax.imageio.ImageIO; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.dom.DOMSource; import org.custommonkey.xmlunit.SimpleNamespaceContext; import org.custommonkey.xmlunit.XMLUnit; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.LayerGroupInfo.Mode; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; import org.geoserver.data.test.TestData; import org.geoserver.ows.Request; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.test.GeoServerSystemTestSupport; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.data.FeatureSource; import org.geotools.map.FeatureLayer; import org.geotools.map.GridCoverageLayer; import org.geotools.map.Layer; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.styling.Style; import org.geotools.xml.Configuration; import org.geotools.xml.Parser; import org.geotools.xml.transform.TransformerBase; import org.opengis.feature.Feature; import org.opengis.feature.type.FeatureType; import org.w3c.dom.Document; import org.xml.sax.InputSource; import org.xml.sax.SAXParseException; import org.springframework.mock.web.MockHttpServletResponse; import com.vividsolutions.jts.geom.Envelope; /** * Base support class for wms tests. * <p> * Deriving from this test class provides the test case with preconfigured geoserver and wms * objects. * </p> * * @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org * */ public abstract class WMSTestSupport extends GeoServerSystemTestSupport { protected static final String NATURE_GROUP = "nature"; protected static final String CONTAINER_GROUP = "containerGroup"; protected static final String OPAQUE_GROUP = "opaqueGroup"; protected static final int SHOW_TIMEOUT = 2000; protected static final boolean INTERACTIVE = false; protected static final Color BG_COLOR = Color.white; protected static final Color COLOR_PLACES_GRAY = new Color(170, 170, 170); protected static final Color COLOR_LAKES_BLUE = new Color(64, 64, 192); /** * @return The global wms singleton from the application context. */ protected WMS getWMS() { WMS wms = (WMS) applicationContext.getBean("wms"); // WMS wms = new WMS(getGeoServer()); // wms.setApplicationContext(applicationContext); return wms; } /** * @return The global web map service singleton from the application context. */ protected WebMapService getWebMapService() { return (WebMapService) applicationContext.getBean("webMapService"); } @Override protected void setUpTestData(SystemTestData testData) throws Exception { super.setUpTestData(testData); Map<String, String> namespaces = new HashMap<String, String>(); namespaces.put("xlink", "http://www.w3.org/1999/xlink"); namespaces.put("xsi", "http://www.w3.org/2001/XMLSchema-instance"); namespaces.put("wfs", "http://www.opengis.net/wfs"); namespaces.put("wcs", "http://www.opengis.net/wcs/1.1.1"); namespaces.put("gml", "http://www.opengis.net/gml"); namespaces.put("sf", "http://cite.opengeospatial.org/gmlsf"); namespaces.put("kml", "http://www.opengis.net/kml/2.2"); testData.registerNamespaces(namespaces); registerNamespaces(namespaces); XMLUnit.setXpathNamespaceContext(new SimpleNamespaceContext(namespaces)); //Add a raster layer testData.setUpRasterLayer(WORLD, "world.tiff", null, null, TestData.class); } @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); // setup a layer group Catalog catalog = getCatalog(); LayerGroupInfo group = catalog.getFactory().createLayerGroup(); LayerInfo lakes = catalog.getLayerByName(getLayerId(MockData.LAKES)); LayerInfo forests = catalog.getLayerByName(getLayerId(MockData.FORESTS)); if(lakes != null && forests != null) { group.setName(NATURE_GROUP); group.getLayers().add(lakes); group.getLayers().add(forests); CatalogBuilder cb = new CatalogBuilder(catalog); cb.calculateLayerGroupBounds(group); catalog.add(group); } testData.addStyle("default", "Default.sld",MockData.class, catalog); //"default", MockData.class.getResource("Default.sld") // create a group containing the other group LayerGroupInfo containerGroup = catalog.getFactory().createLayerGroup(); LayerGroupInfo nature = catalog.getLayerGroupByName(NATURE_GROUP); if (nature != null) { containerGroup.setName(CONTAINER_GROUP); containerGroup.setMode(Mode.CONTAINER); containerGroup.getLayers().add(nature); CatalogBuilder cb = new CatalogBuilder(catalog); cb.calculateLayerGroupBounds(containerGroup); catalog.add(containerGroup); } } protected void setupOpaqueGroup(Catalog catalog) throws Exception { // setup an opaque group too LayerGroupInfo opaqueGroup = catalog.getFactory().createLayerGroup(); LayerInfo roadSegments = catalog.getLayerByName(getLayerId(MockData.ROAD_SEGMENTS)); LayerInfo neatline = catalog.getLayerByName(getLayerId(MockData.MAP_NEATLINE)); if(roadSegments != null && neatline != null) { opaqueGroup.setName(OPAQUE_GROUP); opaqueGroup.setMode(Mode.OPAQUE_CONTAINER);; opaqueGroup.getLayers().add(roadSegments); opaqueGroup.getLayers().add(neatline); CatalogBuilder cb = new CatalogBuilder(catalog); cb.calculateLayerGroupBounds(opaqueGroup); catalog.add(opaqueGroup); } } /** * subclass hook to register additional namespaces. */ protected void registerNamespaces(Map<String, String> namespaces) { } /** * Convenience method for subclasses to create a map layer from a layer name. * <p> * The map layer is created with the default style for the layer. * </p> * * @param layerName * The name of the layer. * * @return A new map layer. */ protected Layer createMapLayer(QName layerName) throws IOException { return createMapLayer(layerName, null); } /** * Convenience method for subclasses to create a map layer from a layer name and a style name. * <p> * The map layer is created with the default style for the layer. * </p> * * @param layerName * The name of the layer. * @param a * style in the catalog (or null if you want to use the default style) * * @return A new map layer. */ protected Layer createMapLayer(QName layerName, String styleName) throws IOException { Catalog catalog = getCatalog(); LayerInfo layerInfo = catalog.getLayerByName(layerName.getLocalPart()); Style style = layerInfo.getDefaultStyle().getStyle(); if (styleName != null) { style = catalog.getStyleByName(styleName).getStyle(); } FeatureTypeInfo info = catalog.getFeatureTypeByName( layerName.getNamespaceURI(), layerName.getLocalPart()); Layer layer = null; if (info != null) { FeatureSource<? extends FeatureType, ? extends Feature> featureSource; featureSource = info.getFeatureSource(null, null); layer = new FeatureLayer(featureSource, style); } else { //try a coverage CoverageInfo cinfo = catalog.getCoverageByName(layerName.getNamespaceURI(), layerName.getLocalPart()); GridCoverage2D cov = (GridCoverage2D) cinfo.getGridCoverage(null, null); layer = new GridCoverageLayer(cov, style); } if (layer == null) { throw new IllegalArgumentException("Could not find layer for " + layerName); } layer.setTitle(layerInfo.getTitle()); return layer; } /** * Calls through to {@link #createGetMapRequest(QName[])}. * */ protected GetMapRequest createGetMapRequest(QName layerName) { return createGetMapRequest(new QName[] { layerName }); } /** * Convenience method for subclasses to create a new GetMapRequest object. * <p> * The returned object has the following properties: * <ul> * <li>styles set to default styles for layers specified * <li>bbox set to (-180,-90,180,180 ) * <li>crs set to epsg:4326 * </ul> * Caller must set additional parameters of request as need be. * </p> * * @param The * layer names of the request. * * @return A new GetMapRequest object. */ protected GetMapRequest createGetMapRequest(QName[] layerNames) { GetMapRequest request = new GetMapRequest(); request.setBaseUrl("http://localhost:8080/geoserver"); List<MapLayerInfo> layers = new ArrayList<MapLayerInfo>(layerNames.length); List styles = new ArrayList(); for (int i = 0; i < layerNames.length; i++) { LayerInfo layerInfo = getCatalog().getLayerByName(layerNames[i].getLocalPart()); try { styles.add(layerInfo.getDefaultStyle().getStyle()); } catch (IOException e) { throw new RuntimeException(e); } layers.add(new MapLayerInfo(layerInfo)); } request.setLayers(layers); request.setStyles(styles); request.setBbox(new Envelope(-180, -90, 180, 90)); request.setCrs(DefaultGeographicCRS.WGS84); request.setSRS("EPSG:4326"); request.setRawKvp(new HashMap()); return request; } /** * Asserts that the image is not blank, in the sense that there must be pixels different from * the passed background color. * * @param testName * the name of the test to throw meaningfull messages if something goes wrong * @param image * the imgage to check it is not "blank" * @param bgColor * the background color for which differing pixels are looked for */ protected void assertNotBlank(String testName, BufferedImage image, Color bgColor) { int pixelsDiffer = countNonBlankPixels(testName, image, bgColor); assertTrue(testName + " image is comlpetely blank", 0 < pixelsDiffer); } /** * Asserts that the image is blank, in the sense that all pixels will be equal to the background * color * * @param testName the name of the test to throw meaningful messages if something goes wrong * @param image the image to check it is not "blank" * @param bgColor the background color for which differing pixels are looked for */ protected void assertBlank(String testName, BufferedImage image, Color bgColor) { int pixelsDiffer = countNonBlankPixels(testName, image, bgColor); assertEquals(testName + " image is completely blank", 0, pixelsDiffer); } /** * Counts the number of non black pixels * * @param testName * @param image * @param bgColor * */ protected int countNonBlankPixels(String testName, BufferedImage image, Color bgColor) { int pixelsDiffer = 0; for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { if (image.getRGB(x, y) != bgColor.getRGB()) { ++pixelsDiffer; } } } LOGGER.fine(testName + ": pixel count=" + (image.getWidth() * image.getHeight()) + " non bg pixels: " + pixelsDiffer); return pixelsDiffer; } /** * Utility method to run the transformation on tr with the provided request and returns the * result as a DOM. * <p> * Parsing the response is done in a namespace aware way. * </p> * * @param req * , the Object to run the xml transformation against with {@code tr}, usually an * instance of a {@link Request} subclass * @param tr * , the transformer to run the transformation with and produce the result as a DOM */ public static Document transform(Object req, TransformerBase tr) throws Exception { return transform(req, tr, true); } /** * Utility method to run the transformation on tr with the provided request and returns the * result as a DOM * * @param req * , the Object to run the xml transformation against with {@code tr}, usually an * instance of a {@link Request} subclass * @param tr * , the transformer to run the transformation with and produce the result as a DOM * @param namespaceAware * whether to use a namespace aware parser for the response or not */ public static Document transform(Object req, TransformerBase tr, boolean namespaceAware) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); tr.transform(req, out); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(namespaceAware); DocumentBuilder db = dbf.newDocumentBuilder(); /** * Resolves everything to an empty xml document, useful for skipping errors due to missing * dtds and the like * * @author Andrea Aime - TOPP */ class EmptyResolver implements org.xml.sax.EntityResolver { public InputSource resolveEntity(String publicId, String systemId) throws org.xml.sax.SAXException, IOException { StringReader reader = new StringReader("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); InputSource source = new InputSource(reader); source.setPublicId(publicId); source.setSystemId(systemId); return source; } } db.setEntityResolver(new EmptyResolver()); // System.out.println(out.toString()); Document doc = db.parse(new ByteArrayInputStream(out.toByteArray())); return doc; } /** * Checks that the image generated by the map producer is not blank. * * @param testName * @param producer */ protected void assertNotBlank(String testName, BufferedImage image) { assertNotBlank(testName, image, BG_COLOR); showImage(testName, image); } /** * Checks that the image generated by the map producer is not blank. * * @param testName * @param producer */ protected void assertBlank(String testName, BufferedImage image) { assertBlank(testName, image, BG_COLOR); showImage(testName, image); } public static void showImage(String frameName, final BufferedImage image) { showImage(frameName, SHOW_TIMEOUT, image); } /** * Shows <code>image</code> in a Frame. * * @param frameName * @param timeOut * @param image */ public static void showImage(String frameName, long timeOut, final BufferedImage image) { int width = image.getWidth(); int height = image.getHeight(); if (((System.getProperty("java.awt.headless") == null) || !System.getProperty( "java.awt.headless").equals("true")) && INTERACTIVE) { Frame frame = new Frame(frameName); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { e.getWindow().dispose(); } }); Panel p = new Panel(null) { // no layout manager so it respects // setSize public void paint(Graphics g) { g.drawImage(image, 0, 0, this); } }; frame.add(p); p.setSize(width, height); frame.pack(); frame.setVisible(true); try { Thread.sleep(timeOut); } catch (InterruptedException e) { e.printStackTrace(); } frame.dispose(); } } /** * Performs some checks on an image response assuming the image is a png. * @see #checkImage(MockHttpServletResponse, String) */ protected void checkImage(MockHttpServletResponse response) { checkImage(response, "image/png", -1, -1); } /** * Performs some checks on an image response such as the mime type and attempts to read the * actual image into a buffered image. * */ protected void checkImage(MockHttpServletResponse response, String mimeType, int width, int height) { assertEquals(mimeType, response.getContentType()); try { BufferedImage image = ImageIO.read(getBinaryInputStream(response)); assertNotNull(image); if(width > 0) { assertEquals(width, image.getWidth()); } if(height > 0) { assertEquals(height, image.getHeight()); } } catch (Throwable t) { t.printStackTrace(); fail("Could not read image returned from GetMap:" + t.getLocalizedMessage()); } } /** * Checks the pixel i/j has the specified color * @param image * @param i * @param j * @param color */ protected void assertPixel(BufferedImage image, int i, int j, Color color) { Color actual = getPixelColor(image, i, j); assertEquals(color, actual); } /** * Checks the pixel i/j is fully transparent * @param image * @param i * @param j */ protected void assertPixelIsTransparent(BufferedImage image, int i, int j) { int pixel = image.getRGB(i,j); assertEquals(true, (pixel>>24) == 0x00); } /** * Gets a specific pixel color from the specified buffered image * @param image * @param i * @param j * @param color * */ protected Color getPixelColor(BufferedImage image, int i, int j) { ColorModel cm = image.getColorModel(); Raster raster = image.getRaster(); Object pixel = raster.getDataElements(i, j, null); Color actual; if(cm.hasAlpha()) { actual = new Color(cm.getRed(pixel), cm.getGreen(pixel), cm.getBlue(pixel), cm.getAlpha(pixel)); } else { actual = new Color(cm.getRed(pixel), cm.getGreen(pixel), cm.getBlue(pixel), 255); } return actual; } /** * Sets up a template in a feature type directory. * * @param featureTypeName The name of the feature type. * @param template The name of the template. * @param body The content of the template. * * @throws IOException */ protected void setupTemplate(QName featureTypeName,String template,String body) throws IOException { ResourceInfo info = getCatalog().getResourceByName(toName(featureTypeName), ResourceInfo.class); getDataDirectory().copyToResourceDir(info, new ByteArrayInputStream(body.getBytes()),template); } protected LayerGroupInfo createLakesPlacesLayerGroup(Catalog catalog, LayerGroupInfo.Mode mode, LayerInfo rootLayer) throws Exception { return createLakesPlacesLayerGroup(catalog, "lakes_and_places", mode, rootLayer); } protected LayerGroupInfo createLakesPlacesLayerGroup(Catalog catalog, String name, LayerGroupInfo.Mode mode, LayerInfo rootLayer) throws Exception { LayerInfo lakes = catalog.getLayerByName(getLayerId(MockData.LAKES)); LayerInfo places = catalog.getLayerByName(getLayerId(MockData.NAMED_PLACES)); LayerGroupInfo group = catalog.getFactory().createLayerGroup(); group.setName(name); group.setMode(mode); if (rootLayer != null) { group.setRootLayer(rootLayer); group.setRootLayerStyle(rootLayer.getDefaultStyle()); } group.getLayers().add(lakes); group.getLayers().add(places); CatalogBuilder cb = new CatalogBuilder(catalog); cb.calculateLayerGroupBounds(group); catalog.add(group); return group; } protected int getRawTopLayerCount() { Catalog rawCatalog = (Catalog) GeoServerExtensions.bean("rawCatalog"); List<LayerInfo> layers = new ArrayList<LayerInfo>(rawCatalog.getLayers()); for (ListIterator<LayerInfo> it = layers.listIterator(); it.hasNext();) { LayerInfo next = it.next(); if (!next.enabled() || next.getName().equals(MockData.GEOMETRYLESS.getLocalPart())) { it.remove(); } } List<LayerGroupInfo> groups = rawCatalog.getLayerGroups(); int opaqueDelta = groups.stream().anyMatch(lg -> OPAQUE_GROUP.equals(lg.getName())) ? 2 : 0; int expectedLayerCount = layers.size() + groups.size() - 1 /* nested layer group */ - opaqueDelta; return expectedLayerCount; } /** * Validates a document against the * * @param dom * @param configuration */ @SuppressWarnings("rawtypes") protected void checkWms13ValidationErrors(Document dom) throws Exception { Parser p = new Parser((Configuration) Class.forName("org.geotools.wms.v1_3.WMSConfiguration").newInstance()); p.setValidating(true); p.parse(new DOMSource(dom)); if (!p.getValidationErrors().isEmpty()) { for (Iterator e = p.getValidationErrors().iterator(); e.hasNext();) { SAXParseException ex = (SAXParseException) e.next(); System.out.println(ex.getLineNumber() + "," + ex.getColumnNumber() + " -" + ex.toString()); } fail("Document did not validate."); } } }