/* (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.wcs2_0; import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.fail; import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo; import java.awt.geom.AffineTransform; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.imageio.metadata.IIOMetadataNode; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Session; import javax.mail.internet.MimeMessage; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import org.custommonkey.xmlunit.SimpleNamespaceContext; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.DimensionInfo; import org.geoserver.catalog.DimensionPresentation; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.impl.DimensionInfoImpl; import org.geoserver.config.GeoServer; import org.geoserver.data.test.MockData; import org.geoserver.data.test.SystemTestData; import org.geoserver.test.GeoServerSystemTestSupport; import org.geoserver.wcs.CoverageCleanerCallback; import org.geoserver.wcs.WCSInfo; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffConstants; import org.geotools.data.DataUtilities; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.wcs.v2_0.WCSConfiguration; import org.geotools.xml.Parser; import org.junit.After; import org.opengis.coverage.Coverage; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridGeometry; import org.opengis.referencing.operation.MathTransform; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSInput; import org.w3c.dom.ls.LSResourceResolver; import org.xml.sax.SAXParseException; /** * Base support class for wcs tests. * * @author Andrea Aime, GeoSolutions * */ @SuppressWarnings("serial") public abstract class WCSTestSupport extends GeoServerSystemTestSupport { protected static XpathEngine xpath; protected static final boolean IS_WINDOWS; protected static final Schema WCS20_SCHEMA; List<GridCoverage> coverages = new ArrayList<GridCoverage>(); protected final static String VERSION = WCS20Const.CUR_VERSION; protected static final QName UTM11 = new QName(MockData.WCS_URI, "utm11", MockData.WCS_PREFIX); /** * Small value for comparaison of sample values. Since most grid coverage implementations in * Geotools 2 store geophysics values as {@code float} numbers, this {@code EPS} value must * be of the order of {@code float} relative precision, not {@code double}. */ static final float EPS = 1E-5f; static { final Map<String, String> namespaceMap = new HashMap<String, String>() { { put("http://www.opengis.net/wcs/2.0", "./src/main/resources/schemas/wcs/2.0/"); put("http://www.opengis.net/gmlcov/1.0", "./src/main/resources/schemas/gmlcov/1.0/"); put("http://www.opengis.net/gml/3.2", "./src/main/resources/schemas/gml/3.2.1/"); put("http://www.w3.org/1999/xlink", "./src/test/resources/schemas/xlink/"); put("http://www.w3.org/XML/1998/namespace", "./src/test/resources/schemas/xml/"); put("http://www.isotc211.org/2005/gmd", "./src/main/resources/schemas/iso/19139/20070417/gmd/"); put("http://www.isotc211.org/2005/gco", "./src/main/resources/schemas/iso/19139/20070417/gco/"); put("http://www.isotc211.org/2005/gss", "./src/main/resources/schemas/iso/19139/20070417/gss/"); put("http://www.isotc211.org/2005/gts", "./src/main/resources/schemas/iso/19139/20070417/gts/"); put("http://www.isotc211.org/2005/gsr", "./src/main/resources/schemas/iso/19139/20070417/gsr/"); put("http://www.opengis.net/swe/2.0", "./src/main/resources/schemas/sweCommon/2.0/"); put("http://www.opengis.net/ows/2.0", "./src/main/resources/schemas/ows/2.0/"); } }; try { final SchemaFactory factory = SchemaFactory .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver(new LSResourceResolver() { DOMImplementationLS dom; { try { // ok, this is ugly.. the only way I've found to create an InputLS without // having to really implement every bit of it is to create a DOMImplementationLS DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setNamespaceAware( true ); DocumentBuilder builder = builderFactory.newDocumentBuilder(); // fake xml to parse String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><empty></empty>"; dom = (DOMImplementationLS) builder.parse(new ByteArrayInputStream(xml.getBytes())).getImplementation(); } catch(Exception e) { throw new RuntimeException(e); } } @Override public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { String localPosition = namespaceMap.get(namespaceURI); if (localPosition != null) { try { if (systemId.contains("/")) { systemId = systemId.substring(systemId.lastIndexOf("/") + 1); } File file = new File(localPosition + systemId); if (file.exists()) { URL url = DataUtilities.fileToURL(file); systemId = url.toURI().toASCIIString(); LSInput input = dom.createLSInput(); input.setPublicId(publicId); input.setSystemId(systemId); return input; } } catch (Exception e) { return null; } } return null; } }); WCS20_SCHEMA = factory.newSchema(WCSTestSupport.class.getResource("/schemas/wcs/2.0/wcsAll.xsd")); } catch (Exception e) { throw new RuntimeException("Could not parse the WCS 2.0 schemas", e); } boolean windows = false; try { windows = System.getProperty("os.name").matches(".*Windows.*"); } catch (Exception e) { // no os.name? oh well, never mind } IS_WINDOWS = windows; } /** * @return The global wcs instance from the application context. */ protected WCSInfo getWCS() { return getGeoServer().getService(WCSInfo.class); } /** * Only setup coverages */ protected void setUpTestData(SystemTestData testData) throws Exception { super.setUpTestData(testData); testData.setUpDefaultRasterLayers(); testData.setUpWcs10RasterLayers(); testData.setUpWcs11RasterLayers(); testData.setUpRasterLayer(UTM11, "/utm11-2.tiff", null, null, WCSTestSupport.class); } @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); // init xmlunit Map<String, String> namespaces = new HashMap<String, String>(); namespaces.put("wcs", "http://www.opengis.net/wcs/2.0"); namespaces.put("wcscrs", "http://www.opengis.net/wcs/service-extension/crs/1.0"); namespaces.put("ows", "http://www.opengis.net/ows/2.0"); namespaces.put("xlink", "http://www.w3.org/1999/xlink"); namespaces.put("int", "http://www.opengis.net/WCS_service-extension_interpolation/1.0"); namespaces.put("gmlcov", "http://www.opengis.net/gmlcov/1.0"); namespaces.put("swe", "http://www.opengis.net/swe/2.0"); namespaces.put("gml", "http://www.opengis.net/gml/3.2"); namespaces.put("wcsgs", "http://www.geoserver.org/wcsgs/2.0"); XMLUnit.setXpathNamespaceContext(new SimpleNamespaceContext(namespaces)); xpath = XMLUnit.newXpathEngine(); } @Override protected boolean isMemoryCleanRequired() { return IS_WINDOWS; } /** * Validates a document against the * * @param dom * @param configuration */ @SuppressWarnings("rawtypes") protected void checkValidationErrors(Document dom) throws Exception { Parser p = new Parser(new WCSConfiguration()); 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."); } } /** * Marks the coverage to be cleaned when the test ends * @param coverage */ protected void scheduleForCleaning(GridCoverage coverage) { if(coverage != null) { coverages.add(coverage); } } @After public void cleanCoverages() { for (GridCoverage coverage : coverages) { CoverageCleanerCallback.disposeCoverage(coverage); } } protected void checkFullCapabilitiesDocument(Document dom) throws Exception { checkValidationErrors(dom, WCS20_SCHEMA); // TODO: check all the layers are here, the profiles, and so on // check that we have the crs extension assertXpathEvaluatesTo("1", "count(//ows:ServiceIdentification[ows:Profile='http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs'])", dom); assertXpathEvaluatesTo("1", "count(//wcs:ServiceMetadata/wcs:Extension[wcscrs:crsSupported = 'http://www.opengis.net/def/crs/EPSG/0/4326'])", dom); // check the interpolation extension assertXpathEvaluatesTo("1", "count(//ows:ServiceIdentification[ows:Profile='http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/interpolation'])", dom); assertXpathEvaluatesTo("1", "count(//wcs:ServiceMetadata/wcs:Extension[int:interpolationSupported='http://www.opengis.net/def/interpolation/OGC/1/nearest-neighbor'])", dom); assertXpathEvaluatesTo("1", "count(//wcs:ServiceMetadata/wcs:Extension[int:interpolationSupported='http://www.opengis.net/def/interpolation/OGC/1/linear'])", dom); assertXpathEvaluatesTo("1", "count(//wcs:ServiceMetadata/wcs:Extension[int:interpolationSupported='http://www.opengis.net/def/interpolation/OGC/1/cubic'])", dom); } /** * Gets a TIFFField node with the given tag number. This is done by searching for a TIFFField * with attribute number whose value is the specified tag value. * * @param tag DOCUMENT ME! * * @return DOCUMENT ME! */ protected IIOMetadataNode getTiffField(Node rootNode, final int tag) { Node node = rootNode.getFirstChild(); if (node != null){ node = node.getFirstChild(); for (; node != null; node = node.getNextSibling()) { Node number = node.getAttributes().getNamedItem(GeoTiffConstants.NUMBER_ATTRIBUTE); if (number != null && tag == Integer.parseInt(number.getNodeValue())) { return (IIOMetadataNode) node; } } } return null; } protected void setInputLimit(int kbytes) { GeoServer gs = getGeoServer(); WCSInfo info = gs.getService(WCSInfo.class); info.setMaxInputMemory(kbytes); gs.save(info); } protected void setOutputLimit(int kbytes) { GeoServer gs = getGeoServer(); WCSInfo info = gs.getService(WCSInfo.class); info.setMaxOutputMemory(kbytes); gs.save(info); } /** * Compares the envelopes of two coverages for equality using the smallest * scale factor of their "grid to world" transform as the tolerance. * * @param expected The coverage having the expected envelope. * @param actual The coverage having the actual envelope. */ protected static void assertEnvelopeEquals(Coverage expected, Coverage actual) { final double scaleA = getScale(expected); final double scaleB = getScale(actual); assertEnvelopeEquals((GeneralEnvelope)expected.getEnvelope(),scaleA,(GeneralEnvelope)actual.getEnvelope(),scaleB); } protected static void assertEnvelopeEquals(GeneralEnvelope expected,double scaleExpected, GeneralEnvelope actual,double scaleActual) { final double tolerance; if (scaleExpected <= scaleActual) { tolerance = scaleExpected*1E-1; } else if (!Double.isNaN(scaleActual)) { tolerance = scaleActual*1E-1; } else { tolerance = EPS; } assertTrue(expected.equals(actual, tolerance, false)); } /** * Returns the "Sample to geophysics" transform as an affine transform, or {@code null} * if none. Note that the returned instance may be an immutable one, not necessarly the * default Java2D implementation. * * @param coverage The coverage for which to get the "grid to CRS" affine transform. * @return The "grid to CRS" affine transform of the given coverage, or {@code null} * if none or if the transform is not affine. */ protected static AffineTransform getAffineTransform(final Coverage coverage) { if (coverage instanceof GridCoverage) { final GridGeometry geometry = ((GridCoverage) coverage).getGridGeometry(); if (geometry != null) { final MathTransform gridToCRS; if (geometry instanceof GridGeometry2D) { gridToCRS = ((GridGeometry2D) geometry).getGridToCRS(); } else { gridToCRS = geometry.getGridToCRS(); } if (gridToCRS instanceof AffineTransform) { return (AffineTransform) gridToCRS; } } } return null; } /** * Returns the scale of the "grid to CRS" transform, or {@link Double#NaN} if unknown. * * @param coverage The coverage for which to get the "grid to CRS" scale, or {@code null}. * @return The "grid to CRS" scale, or {@code NaN} if none or if the transform is not affine. */ protected static double getScale(final Coverage coverage) { final AffineTransform gridToCRS = getAffineTransform(coverage); return (gridToCRS != null) ? XAffineTransform.getScale(gridToCRS) : Double.NaN; } /** * Parses a multipart message from the response * @param response * * @throws MessagingException * @throws IOException */ protected Multipart getMultipart(MockHttpServletResponse response) throws MessagingException, IOException { MimeMessage body = new MimeMessage((Session) null, getBinaryInputStream(response)); Multipart multipart = (Multipart) body.getContent(); return multipart; } /** * Configures the specified dimension for a coverage * * @param coverageName * @param metadataKey * @param presentation * @param resolution */ protected void setupRasterDimension(String coverageName, String metadataKey, DimensionPresentation presentation, Double resolution) { CoverageInfo info = getCatalog().getCoverageByName(coverageName); DimensionInfo di = new DimensionInfoImpl(); di.setEnabled(true); di.setPresentation(presentation); if(resolution != null) { di.setResolution(new BigDecimal(resolution)); } info.getMetadata().put(metadataKey, di); getCatalog().save(info); } /** * Configures the specified dimension for a coverage * * @param coverageName * @param metadataKey * @param presentation * @param resolution * @param unitSymbol */ protected void setupRasterDimension(String coverageName, String metadataKey, DimensionPresentation presentation, Double resolution, String unitSymbol) { CoverageInfo info = getCatalog().getCoverageByName(coverageName); DimensionInfo di = new DimensionInfoImpl(); di.setEnabled(true); di.setPresentation(presentation); if(resolution != null) { di.setResolution(new BigDecimal(resolution)); } if(unitSymbol != null) { di.setUnitSymbol(unitSymbol); } info.getMetadata().put(metadataKey, di); getCatalog().save(info); } /** * Clears dimension information from the specified coverage * * @param coverageName * @param metadataKey * @param presentation * @param resolution */ protected void clearDimensions(String coverageName) { CoverageInfo info = getCatalog().getCoverageByName(coverageName); info.getMetadata().remove(ResourceInfo.TIME); info.getMetadata().remove(ResourceInfo.ELEVATION); getCatalog().save(info); } }