From b7ddce2728d64be122d4c097de34c425ef0fb542 Mon Sep 17 00:00:00 2001 From: Dirk Stenger Date: Fri, 18 Oct 2024 13:29:25 +0200 Subject: [PATCH] #62 - Include updated ets-common and refactorings --- pom.xml | 247 ++--- src/{main => }/docker/Dockerfile | 0 .../cite/gpkg12/nsg/TestNGController.java | 259 +++-- .../NSG_SpatialReferenceSystemsTests.java | 752 +++++++------- .../gpkg12/nsg/metadata/MetadataTests.java | 236 ++--- .../cite/gpkg12/nsg/tiles/NSG_TileTests.java | 961 +++++++++--------- .../opengis/cite/gpkg12/nsg/util/CrsList.java | 125 ++- .../cite/gpkg12/nsg/util/CrsListingUtils.java | 97 +- .../gpkg12/nsg/VerifyTestNGController.java | 158 +-- .../nsg/metadata/MetadataTestsTest.java | 160 ++- .../gpkg12/nsg/util/CrsListingUtilsTest.java | 18 +- 11 files changed, 1467 insertions(+), 1546 deletions(-) rename src/{main => }/docker/Dockerfile (100%) diff --git a/pom.xml b/pom.xml index 8e6fbf5..9c89dae 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,5 @@ - + org.opengis.cite ets-common @@ -17,13 +18,13 @@ Apache License, Version 2.0 - http://opensource.org/licenses/Apache-2.0 + https://opensource.org/licenses/Apache-2.0 Open Geospatial Consortium - http://www.opengeospatial.org/ + https://www.ogc.org/ scm:git:https://github.com/opengeospatial/ets-gpkg12-nsg.git @@ -122,10 +123,10 @@ xerces xercesImpl - - xml-apis - xml-apis - + + xml-apis + xml-apis + @@ -140,79 +141,74 @@ org.opengis.cite.gpkg12.nsg.TestNGController - - ${basedir}/src/assembly/deps.xml - ${basedir}/src/assembly/ctl-scripts.xml - ${basedir}/src/assembly/aio.xml - - - - - package - - single - - - - - - maven-surefire-plugin - - - maven-compiler-plugin - - 17 - 17 - - - - maven-jar-plugin - - - maven-release-plugin - 3.0.1 - - true - @{project.version} - release - - - - maven-site-plugin - - true - - org.codehaus.mojo - buildnumber-maven-plugin - org.jacoco jacoco-maven-plugin - 0.8.11 - - - - prepare-agent - - - - report - - report - - - maven-scm-publish-plugin - 3.2.1 - - gh-pages - + + + + + io.fabric8 + docker-maven-plugin + + + + + + ${project.version}-teamengine-${docker.teamengine.version} + + + + + 8081:8080 + + + + http://localhost:8081/teamengine + + + + + + + + + + maven-dependency-plugin + + + + org.opengis.cite.teamengine + teamengine-web + ${docker.teamengine.version} + war + + + org.opengis.cite.teamengine + teamengine-web + ${docker.teamengine.version} + common-libs + zip + + + org.opengis.cite.teamengine + teamengine-console + ${docker.teamengine.version} + base + zip + + + + + + @@ -223,127 +219,28 @@ io.fabric8 docker-maven-plugin - 0.43.4 - - - - opengeospatial/${project.artifactId} - - ${project.basedir}/src/main/docker - - ${project.version}-teamengine-${docker.teamengine.version} - - - - - - ${project.build.directory} - . - - dependency/*teamengine-*.war - dependency/*teamengine-*.zip - *ets-*.zip - - - - - - - - - 8081:8080 - - - - http://localhost:8081/teamengine - - - - - - - - - start - pre-integration-test - - build - start - - - - stop - post-integration-test - - stop - remove - - build build - - - - maven-dependency-plugin - 3.6.1 - + push - copy-dependencies + push - - - - - org.opengis.cite.teamengine - teamengine-web - ${docker.teamengine.version} - war - - - org.opengis.cite.teamengine - teamengine-web - ${docker.teamengine.version} - common-libs - zip - - - org.opengis.cite.teamengine - teamengine-console - ${docker.teamengine.version} - base - zip - - - - - release - - - maven-gpg-plugin - 1.6 + maven-dependency-plugin - sign-artifacts - verify - sign + copy - - - --pinentry-mode - loopback - - @@ -353,10 +250,6 @@ - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - site scm:git:git@github.com:opengeospatial/ets-gpkg12-nsg.git diff --git a/src/main/docker/Dockerfile b/src/docker/Dockerfile similarity index 100% rename from src/main/docker/Dockerfile rename to src/docker/Dockerfile diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/TestNGController.java b/src/main/java/org/opengis/cite/gpkg12/nsg/TestNGController.java index 689202d..9562fa3 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/TestNGController.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/TestNGController.java @@ -35,135 +35,132 @@ */ public class TestNGController implements TestSuiteController { - private TestRunExecutor executor; - - private Properties etsProperties = new Properties(); - - /** - * A convenience method for running the test suite using a command-line interface. The default values of the test - * run arguments are as follows: - *
    - *
  • XML properties file: ${user.home}/test-run-props.xml
  • - *
  • outputDir: ${user.home}
  • - *
- *

- * Synopsis - *

- * - *
-     * ets-*-aio.jar [-o|--outputDir $TMPDIR] [test-run-props.xml]
-     * 
- * - * @param args - * Test run arguments (optional). - * @throws Exception - * If the test run cannot be executed (usually due to unsatisfied pre-conditions). - */ - public static void main( String[] args ) - throws Exception { - TestRunArguments testRunArgs = new TestRunArguments(); - JCommander cmd = new JCommander( testRunArgs ); - try { - cmd.parse( args ); - } catch ( ParameterException px ) { - System.out.println( px.getMessage() ); - cmd.usage(); - } - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = dbf.newDocumentBuilder(); - File xmlArgs = testRunArgs.getPropertiesFile(); - Document testRunProps = db.parse( xmlArgs ); - TestNGController controller = new TestNGController( testRunArgs.getOutputDir() ); - Source testResults = controller.doTestRun( testRunProps ); - System.out.println( "Test results: " + testResults.getSystemId() ); - } - - /** - * Default constructor uses the location given by the "user.home" system property as the root output directory. - */ - public TestNGController() { - this( new File( FilenameUtils.normalize( System.getProperty( "user.home" ) )).toURI().toString() ); - } - - - - - /** - * Construct a controller that writes results to the given output directory. - * - * @param outputDir - * The location of the directory in which test results will be written; it will be created if it does not - * exist. - */ - public TestNGController( String outputDir ) { - try (InputStream is = getClass().getResourceAsStream( "ets.properties" )) { - this.etsProperties.load( is ); - } catch ( IOException ex ) { - TestSuiteLogger.log( Level.WARNING, "Unable to load ets.properties. " + ex.getMessage() ); - } - URL tngSuite = TestNGController.class.getResource( "testng.xml" ); - File resultsDir = null; - if ( null == outputDir || outputDir.isEmpty() ) { - - resultsDir = new File( FilenameUtils.normalize( System.getProperty( "user.home" )) ); - - } else if ( outputDir.startsWith( "file:" ) ) { - resultsDir = new File( URI.create( outputDir ) ); - } else { - resultsDir = new File( outputDir ); - } - TestSuiteLogger.log( Level.CONFIG, "Using TestNG config: " + tngSuite ); - TestSuiteLogger.log( Level.CONFIG, "Using outputDirPath: " + resultsDir.getAbsolutePath() ); - // NOTE: setting third argument to 'true' enables the default listeners - this.executor = new TestNGExecutor( tngSuite.toString(), resultsDir.getAbsolutePath(), false ); - } - - @Override - public String getCode() { - return etsProperties.getProperty( "ets-code" ); - } - - @Override - public String getVersion() { - return etsProperties.getProperty( "ets-version" ); - } - - @Override - public String getTitle() { - return etsProperties.getProperty( "ets-title" ); - } - - @Override - public Source doTestRun( Document testRunArgs ) - throws Exception { - validateTestRunArgs( testRunArgs ); - return executor.execute( testRunArgs ); - } - - /** - * Validates the test run arguments. The test run is aborted if any of these checks fail. - * - * @param testRunArgs - * A DOM Document containing a set of XML properties (key-value pairs). - * @throws IllegalArgumentException - * If any arguments are missing or invalid for some reason. - */ - void validateTestRunArgs( Document testRunArgs ) { - if ( null == testRunArgs || !testRunArgs.getDocumentElement().getNodeName().equals( "properties" ) ) { - throw new IllegalArgumentException( "Input is not an XML properties document." ); - } - NodeList entries = testRunArgs.getDocumentElement().getElementsByTagName( "entry" ); - if ( entries.getLength() == 0 ) { - throw new IllegalArgumentException( "No test run arguments found." ); - } - Map args = new HashMap(); - for ( int i = 0; i < entries.getLength(); i++ ) { - Element entry = (Element) entries.item( i ); - args.put( entry.getAttribute( "key" ), entry.getTextContent() ); - } - if ( !args.containsKey( TestRunArg.IUT.toString() ) ) { - throw new IllegalArgumentException( String.format( "Missing argument: '%s' must be present.", - TestRunArg.IUT ) ); - } - } + private TestRunExecutor executor; + + private Properties etsProperties = new Properties(); + + /** + * A convenience method for running the test suite using a command-line interface. The + * default values of the test run arguments are as follows: + *
    + *
  • XML properties file: ${user.home}/test-run-props.xml
  • + *
  • outputDir: ${user.home}
  • + *
+ *

+ * Synopsis + *

+ * + *
+	 * ets-*-aio.jar [-o|--outputDir $TMPDIR] [test-run-props.xml]
+	 * 
+ * @param args Test run arguments (optional). + * @throws Exception If the test run cannot be executed (usually due to unsatisfied + * pre-conditions). + */ + public static void main(String[] args) throws Exception { + TestRunArguments testRunArgs = new TestRunArguments(); + JCommander cmd = new JCommander(testRunArgs); + try { + cmd.parse(args); + } + catch (ParameterException px) { + System.out.println(px.getMessage()); + cmd.usage(); + } + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + File xmlArgs = testRunArgs.getPropertiesFile(); + Document testRunProps = db.parse(xmlArgs); + TestNGController controller = new TestNGController(testRunArgs.getOutputDir()); + Source testResults = controller.doTestRun(testRunProps); + System.out.println("Test results: " + testResults.getSystemId()); + } + + /** + * Default constructor uses the location given by the "user.home" system property as + * the root output directory. + */ + public TestNGController() { + this(new File(FilenameUtils.normalize(System.getProperty("user.home"))).toURI().toString()); + } + + /** + * Construct a controller that writes results to the given output directory. + * @param outputDir The location of the directory in which test results will be + * written; it will be created if it does not exist. + */ + public TestNGController(String outputDir) { + try (InputStream is = getClass().getResourceAsStream("ets.properties")) { + this.etsProperties.load(is); + } + catch (IOException ex) { + TestSuiteLogger.log(Level.WARNING, "Unable to load ets.properties. " + ex.getMessage()); + } + URL tngSuite = TestNGController.class.getResource("testng.xml"); + File resultsDir = null; + if (null == outputDir || outputDir.isEmpty()) { + + resultsDir = new File(FilenameUtils.normalize(System.getProperty("user.home"))); + + } + else if (outputDir.startsWith("file:")) { + resultsDir = new File(URI.create(outputDir)); + } + else { + resultsDir = new File(outputDir); + } + TestSuiteLogger.log(Level.CONFIG, "Using TestNG config: " + tngSuite); + TestSuiteLogger.log(Level.CONFIG, "Using outputDirPath: " + resultsDir.getAbsolutePath()); + // NOTE: setting third argument to 'true' enables the default listeners + this.executor = new TestNGExecutor(tngSuite.toString(), resultsDir.getAbsolutePath(), false); + } + + @Override + public String getCode() { + return etsProperties.getProperty("ets-code"); + } + + @Override + public String getVersion() { + return etsProperties.getProperty("ets-version"); + } + + @Override + public String getTitle() { + return etsProperties.getProperty("ets-title"); + } + + @Override + public Source doTestRun(Document testRunArgs) throws Exception { + validateTestRunArgs(testRunArgs); + return executor.execute(testRunArgs); + } + + /** + * Validates the test run arguments. The test run is aborted if any of these checks + * fail. + * @param testRunArgs A DOM Document containing a set of XML properties (key-value + * pairs). + * @throws IllegalArgumentException If any arguments are missing or invalid for some + * reason. + */ + void validateTestRunArgs(Document testRunArgs) { + if (null == testRunArgs || !testRunArgs.getDocumentElement().getNodeName().equals("properties")) { + throw new IllegalArgumentException("Input is not an XML properties document."); + } + NodeList entries = testRunArgs.getDocumentElement().getElementsByTagName("entry"); + if (entries.getLength() == 0) { + throw new IllegalArgumentException("No test run arguments found."); + } + Map args = new HashMap(); + for (int i = 0; i < entries.getLength(); i++) { + Element entry = (Element) entries.item(i); + args.put(entry.getAttribute("key"), entry.getTextContent()); + } + if (!args.containsKey(TestRunArg.IUT.toString())) { + throw new IllegalArgumentException( + String.format("Missing argument: '%s' must be present.", TestRunArg.IUT)); + } + } + } diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/core/NSG_SpatialReferenceSystemsTests.java b/src/main/java/org/opengis/cite/gpkg12/nsg/core/NSG_SpatialReferenceSystemsTests.java index 4282568..d08d9be 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/core/NSG_SpatialReferenceSystemsTests.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/core/NSG_SpatialReferenceSystemsTests.java @@ -33,377 +33,385 @@ public class NSG_SpatialReferenceSystemsTests extends CommonFixture { - private final static Logger LOG = Logger.getLogger( NSG_SpatialReferenceSystemsTests.class.getName() ); - - private static final String ANNEX_C_3395_TABLE = "Annex_C_3395_Table.txt"; - - private static final String ANNEX_E_4326_TABLE = "Annex_E_4326_Table.txt"; - - private static final double TOLERANCE = 1.0e-10; - - private CrsList crsListing; - - /** - * Factory to create coordinate reference systems we are calling it out specifically here due to conflicts with - * other packages. - */ - private CRSFactory crsFactory = new org.apache.sis.referencing.factory.GeodeticObjectFactory(); - - @BeforeClass - public void parseCrsListing() { - crsListing = CrsListingUtils.parseCrsListing(); - } - - /** - * NSG Req 3: The CRSs listed in Table 4, Table 5, and Table 6 SHALL be the only CRSs used by raster tile pyramid - * and vector feature data tables in a GeoPackage. - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 3 (identified CRSs)") - public void crsTest() - throws SQLException { - if ( crsListing == null ) - throw new SkipException( "No designated CRS Lookup Table available" ); - - String queryStr = "SELECT srs_id, organization_coordsys_id FROM gpkg_spatial_ref_sys"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - final Collection invalidSrsIds = new LinkedList<>(); - final Collection invalidOrgIds = new LinkedList<>(); - - while ( resultSet.next() ) { - String srsID = resultSet.getString( "srs_id" ).trim(); - String orgID = resultSet.getString( "organization_coordsys_id" ).trim(); - - if ( srsID.equals( "0" ) || orgID.equals( "0" ) ) { - continue; - } - if ( srsID.equals( "-1" ) || orgID.equals( "-1" ) ) { - continue; - } - - String crsOrgID = crsListing.getOrganizationCoordsysIdBySrsId( srsID ); - if ( crsOrgID == null ) { - invalidSrsIds.add( srsID ); - } else { - if ( !crsOrgID.equals( orgID ) ) { - invalidOrgIds.add( orgID ); - } - } - } - resultSet.close(); - statement.close(); - - assertTrue( invalidSrsIds.isEmpty(), - MessageFormat.format( "The gpkg_spatial_ref_sys table contains invalid srs_id values {0}", - invalidSrsIds.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - assertTrue( invalidOrgIds.isEmpty(), - MessageFormat.format( "The gpkg_spatial_ref_sys table contains invalid organization_coordsys_id values {0}", - invalidOrgIds.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - } - } - - /** - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 4 & 5 (match Annex table)") - public void matchAnnexTableTest() - throws SQLException { - // --- original intent was to implement here; but may make more sense to - // implement in NSG_TileTests - final Collection invalidMatrixEntries = new LinkedList<>(); - - // In the case there is no gpkg_tile_matrix ... say this geopackage has no tiles, skip this test - final boolean hasTileMatrixTable = DatabaseUtility.doesTableOrViewExist( this.databaseConnection, - "gpkg_tile_matrix" ); - - if ( hasTileMatrixTable ) { - - String queryStr = "SELECT tm.table_name AS tabName, sel.data_type AS dataTyp, sel.crs_id AS crsID, tm.zoom_level AS zoomLvl, tm.matrix_width AS matrixW, tm.matrix_height AS matrixH, tm.tile_width AS tileW, tm.tile_height AS tileH, tm.pixel_x_size AS pixelSzX, tm.pixel_y_size AS pixelSzY " - + "FROM gpkg_tile_matrix tm " - + "INNER JOIN (SELECT gc.table_name, gc.data_type, gs.organization_coordsys_id as crs_id from gpkg_contents gc inner join gpkg_spatial_ref_sys gs where gc.srs_id=gs.srs_id) AS sel " - + "ON tm.table_name=sel.table_name " + "WHERE crsID IN (3395, 4326) ORDER BY zoomLvl;"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - List annexC_3395 = populateAnnex( ANNEX_C_3395_TABLE, "Annex C (EPSG:3395)" ); - List annexE_4326 = populateAnnex( ANNEX_E_4326_TABLE, "Annex E (EPSG:4326)" ); - - while ( resultSet.next() ) { - String tabNam = resultSet.getString( "tabName" ).trim(); - String srsID = resultSet.getString( "crsID" ).trim(); - int zoomLvl = resultSet.getInt( "zoomLvl" ); - - long matrixW = resultSet.getLong( "matrixW" ); - long matrixH = resultSet.getLong( "matrixH" ); - double pixelSzX = resultSet.getDouble( "pixelSzX" ); - double pixelSzY = resultSet.getDouble( "pixelSzY" ); - - List annexTable = selectAnnexBySrsId( srsID, annexC_3395, annexE_4326 ); - for ( Object[] obj : annexTable ) { - if ( zoomLvl == (int) obj[0] ) { - long imW = (long) obj[3]; - long imH = (long) obj[4]; - double pX = (double) obj[2]; - double pY = (double) obj[2]; - - if ( Math.abs( pX - pixelSzX ) > TOLERANCE ) { - invalidMatrixEntries.add( tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " - + "Pixel Size X: " + pixelSzX + "; but expected " + pX ); - } else if ( Math.abs( pY - pixelSzY ) > TOLERANCE ) { - invalidMatrixEntries.add( tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " - + "Pixel Size Y: " + pixelSzY + "; but expected " + pY ); - } else if ( imW != matrixW ) { - invalidMatrixEntries.add( tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " - + "Matrix Width: " + matrixW + "; but expected " + imW ); - } else if ( imH != matrixH ) { - invalidMatrixEntries.add( tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " - + "Matrix Height: " + matrixH + "; but expected " + imH ); - } - } - } - } - assertTrue( invalidMatrixEntries.isEmpty(), - MessageFormat.format( "The gpkg_tile_matrix table contains invalid Pixels Size or Matrix Size values for tables: {0}", - invalidMatrixEntries.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - } - } - } - - /* - * TODO: Implement Test for Requirement 6 - * - * --- NSG Req 6: The WGS 84 Geographic 2D CRS SHALL be used for 2D vector features. WGS 84 Geographic 2D - * GeoPackages SHALL follow the technical guidance provided in Annex E: Implementation Guide for EPSG::4326 Tiles. - * - * @Test(groups = { "NSG" }, description = "NSG Req 6 (match Annex table)") - */ - - /** - * NSG Req 8: The CRS definitions in Table 7 through Table 19 below SHALL be used to specify the CRS used for tiles - * and vector feature user data tables containing NSG data in a GeoPackage. - * - * NSG Req 9: Other CRS definitions SHALL NOT be specified for GeoPackage SQL tables containing NSG data. - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 8 & 9 (CRS definitions)") - public void crsDefinitionsTest() - throws SQLException { - if ( crsListing == null ) - throw new SkipException( "No designated CRS Lookup Table available" ); - - String queryStr = "SELECT srs_id,definition FROM gpkg_spatial_ref_sys"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - final Collection invalidSrsDefs = new LinkedList<>(); - - while ( resultSet.next() ) { - String srsID = resultSet.getString( "srs_id" ).trim(); - if ( srsID.equals( "0" ) ) { - continue; - } - if ( srsID.equals( "-1" ) ) { - continue; - } - - // Get the current CRS definition and get from our saved specification, - // the CRS definition that we expect. Both of these are WKT. - final String defin = resultSet.getString( "definition" ); - final String specin = crsListing.getDefinitionBySrsId( srsID ); - - String specWKT = specin; - String defWKT = defin; - - try { - // Parse WKT - this one is from the specification file - CoordinateReferenceSystem specCRS = crsFactory.createFromWKT(specin); - specWKT = specCRS.toWKT(); - - // Parse WKT - this one is from the geopackage - CoordinateReferenceSystem testCRS = crsFactory.createFromWKT(defin); - defWKT = testCRS.toWKT(); - - } catch (FactoryException e) { - // Normalization failed - } - - // The WKTs may be normalized now. However, there still may be spaces - // in there that could cause a difference. Hence, we'll compare using a function that - // strips spaces and moves all content to lower case. - // This is still an extremely incomplete test and the CRSs may still be the same. In part, - // this is due to different variations of WKT content. - // We have found no WKT comparison utilities that will work. - - boolean crsEquivalent = compareDefintion( defWKT, specWKT ); - - // At this point we know the names from testCRS.getName() may still be different, - // and the identifiers from testCRS.getIdentifiers() may be different, but these - // may still be "the same" CRS. There is not much we can do about it given the available - // utilities. - - - if ( !crsEquivalent ) { - final String issueRpt = String.format( "srs_id: %s : GeoPackage WKT (normalized): %s : Specification WKT (normalized): %s ", - srsID, defin, specin ); - invalidSrsDefs.add( issueRpt ); - Assert.fail( MessageFormat.format( "The CRS for this srs_id is not exactly equivalent to the specification. {0}", - invalidSrsDefs.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - } - - } - } - } - - /** - * NSG Req 19: Data validity SHALL be assessed against data value constraints specified in Table 26 below using a - * test suite. Data validity MAY be enforced by SQL triggers. - * - * 19-A: Addresses Table 26 Rows 1-2 (regarding table "gpkg_spatial_ref_sys") - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 19-A (Data Validity: gpkg_spatial_ref_sys)") - public void dataValidity_gpkg_spatial_ref_sys() - throws SQLException { - if ( crsListing == null ) - throw new SkipException( "No designated CRS Lookup Table available" ); - - String queryStr = "SELECT srs_id,organization,description FROM gpkg_spatial_ref_sys;"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - final Collection invalidOrgs = new LinkedList<>(); - final Collection invalidDesc = new LinkedList<>(); - - while ( resultSet.next() ) { - String srsID = resultSet.getString( "srs_id" ).trim(); - if ( "0".equals( srsID ) || "-1".equals( srsID ) ) { - continue; - } - - // --- test for: Table 26; Row 1 - String srsOrg = resultSet.getString( "organization" ); - validateCrsOrganistion( srsID, srsOrg, invalidOrgs ); - - // --- test for: Table 26; Row 2 - String description = resultSet.getString( "description" ); - validateCrsDescription( srsID, description, invalidDesc ); - } - - assertTrue( invalidOrgs.isEmpty(), - MessageFormat.format( "The gpkg_spatial_ref_sys table contains invalid organization values for IDs: {0}, should be \'EPSG\' or \'NGA\'", - invalidOrgs.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - assertTrue( invalidDesc.isEmpty(), - MessageFormat.format( "The gpkg_spatial_ref_sys table contains invalid descriptions for IDs: {0}", - invalidDesc.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - } - } - - private void validateCrsOrganistion( String srsID, String srsOrg, Collection invalidOrgs ) { - if ( srsOrg == null ) { - invalidOrgs.add( srsID + ": null (expected 'EPSG' or 'NGA')" ); - } else { - srsOrg = srsOrg.trim(); - if ( !"EPSG".equalsIgnoreCase( srsOrg ) && !"NGA".equalsIgnoreCase( srsOrg ) ) { - invalidOrgs.add( srsID + ": " + srsOrg + " (expected 'EPSG' or 'NGA')" ); - } - } - } - - private void validateCrsDescription( String srsID, String description, Collection invalidDesc ) { - if ( description == null ) { - invalidDesc.add( srsID + " (expected not to be null)" ); - return; - } - if ( !isCrsDescriptionValid( description ) ) { - invalidDesc.add( srsID - + ": '" - + description - + "' (expected not to be an empty string, not all whitespace, not “unknown” (any case), not “tbd” (any case))" ); - return; - } - String expectedDescription = crsListing.getDescriptionBySrsId( srsID ); - if ( expectedDescription != null && !isDescriptionAsExpected( description, expectedDescription ) ) { - invalidDesc.add( srsID + " : '" + description + "' (expected: '" + expectedDescription + "')" ); - } - } - - private boolean isDescriptionAsExpected( String descriptionToTest, String expectedDescription ) { - descriptionToTest = removeWhitespaces( descriptionToTest ); - expectedDescription = removeWhitespaces( expectedDescription ); - if ( descriptionToTest.endsWith( "." ) ) - descriptionToTest = descriptionToTest.substring( 0, descriptionToTest.length() - 1 ); - if ( expectedDescription.endsWith( "." ) ) - expectedDescription = expectedDescription.substring( 0, expectedDescription.length() - 1 ); - return descriptionToTest.equalsIgnoreCase( expectedDescription ); - } - - private boolean compareDefintion( String definitionToTest, String expectedDefinition ) { - definitionToTest = removeWhitespaces( definitionToTest ); - expectedDefinition = removeWhitespaces( expectedDefinition ); - return definitionToTest.equalsIgnoreCase( expectedDefinition ); - } - - private List selectAnnexBySrsId( String srsID, List annexC_3395, List annexE_4326 ) { - if ( srsID.equals( "3395" ) ) { - return annexC_3395; - } else if ( srsID.equals( "4326" ) ) { - return annexE_4326; - } else if ( ( srsID.equals( "5041" ) ) || ( srsID.equals( "5042" ) ) ) { - return Collections.emptyList(); - } - return Collections.emptyList(); - } - - private List populateAnnex( String annexTableName, String annexName ) { - InputStream resourceToRead = this.getClass().getResourceAsStream( annexTableName ); - try (BufferedReader br = new BufferedReader( new InputStreamReader( resourceToRead, "UTF-8" ) )) { - List annexEntries = new ArrayList<>(); - String line; - while ( ( line = br.readLine() ) != null ) { - List items = Arrays.asList( line.split( "\\s*,\\s*" ) ); - if ( !items.isEmpty() && ( items.size() == 5 ) ) { - addNewEntry( annexEntries, items.get( 0 ), items.get( 1 ), items.get( 2 ), items.get( 3 ), - items.get( 4 ) ); - } else { - throw new SkipException( annexName + " Table is corrupt " ); - } - } - return annexEntries; - } catch ( IOException e ) { - throw new SkipException( annexName + " Table not available" ); - } - } - - private void addNewEntry( List table, String zoom, String scale, String pixelSz, String matrixWidth, - String matrixHeight ) { - int zoomAsInt = Integer.parseInt( zoom ); - double scaleAsDouble = Double.parseDouble( scale ); - double pixelSizeAsDouble = Double.parseDouble( pixelSz ); - long matrixWidthAsLong = Long.parseLong( matrixWidth ); - long matrixHeightAsLong = Long.parseLong( matrixHeight ); - Object[] row = { zoomAsInt, scaleAsDouble, pixelSizeAsDouble, matrixWidthAsLong, matrixHeightAsLong }; - table.add( row ); - } - - private String removeWhitespaces( String description ) { - if ( description != null ) - return description.trim().replaceAll( "\\s+", "" ); - return description; - } - - private boolean isCrsDescriptionValid( String srsDesc ) { - return srsDesc.length() > 0 && srsDesc.trim().length() > 0 && ( !srsDesc.equalsIgnoreCase( "NULL" ) ) - && ( !srsDesc.equalsIgnoreCase( "UNK" ) ) && ( !srsDesc.equalsIgnoreCase( "UNKNOWN" ) ) - && ( !srsDesc.equalsIgnoreCase( "TBD" ) ); - } + private final static Logger LOG = Logger.getLogger(NSG_SpatialReferenceSystemsTests.class.getName()); + + private static final String ANNEX_C_3395_TABLE = "Annex_C_3395_Table.txt"; + + private static final String ANNEX_E_4326_TABLE = "Annex_E_4326_Table.txt"; + + private static final double TOLERANCE = 1.0e-10; + + private CrsList crsListing; + + /** + * Factory to create coordinate reference systems we are calling it out specifically + * here due to conflicts with other packages. + */ + private CRSFactory crsFactory = new org.apache.sis.referencing.factory.GeodeticObjectFactory(); + + @BeforeClass + public void parseCrsListing() { + crsListing = CrsListingUtils.parseCrsListing(); + } + + /** + * NSG Req 3: The CRSs listed in Table 4, Table 5, and Table 6 SHALL be the only CRSs + * used by raster tile pyramid and vector feature data tables in a GeoPackage. + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 3 (identified CRSs)") + public void crsTest() throws SQLException { + if (crsListing == null) + throw new SkipException("No designated CRS Lookup Table available"); + + String queryStr = "SELECT srs_id, organization_coordsys_id FROM gpkg_spatial_ref_sys"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + final Collection invalidSrsIds = new LinkedList<>(); + final Collection invalidOrgIds = new LinkedList<>(); + + while (resultSet.next()) { + String srsID = resultSet.getString("srs_id").trim(); + String orgID = resultSet.getString("organization_coordsys_id").trim(); + + if (srsID.equals("0") || orgID.equals("0")) { + continue; + } + if (srsID.equals("-1") || orgID.equals("-1")) { + continue; + } + + String crsOrgID = crsListing.getOrganizationCoordsysIdBySrsId(srsID); + if (crsOrgID == null) { + invalidSrsIds.add(srsID); + } + else { + if (!crsOrgID.equals(orgID)) { + invalidOrgIds.add(orgID); + } + } + } + resultSet.close(); + statement.close(); + + assertTrue(invalidSrsIds.isEmpty(), + MessageFormat.format("The gpkg_spatial_ref_sys table contains invalid srs_id values {0}", + invalidSrsIds.stream().map(Object::toString).collect(Collectors.joining(", ")))); + assertTrue(invalidOrgIds.isEmpty(), + MessageFormat.format( + "The gpkg_spatial_ref_sys table contains invalid organization_coordsys_id values {0}", + invalidOrgIds.stream().map(Object::toString).collect(Collectors.joining(", ")))); + } + } + + /** + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 4 & 5 (match Annex table)") + public void matchAnnexTableTest() throws SQLException { + // --- original intent was to implement here; but may make more sense to + // implement in NSG_TileTests + final Collection invalidMatrixEntries = new LinkedList<>(); + + // In the case there is no gpkg_tile_matrix ... say this geopackage has no tiles, + // skip this test + final boolean hasTileMatrixTable = DatabaseUtility.doesTableOrViewExist(this.databaseConnection, + "gpkg_tile_matrix"); + + if (hasTileMatrixTable) { + + String queryStr = "SELECT tm.table_name AS tabName, sel.data_type AS dataTyp, sel.crs_id AS crsID, tm.zoom_level AS zoomLvl, tm.matrix_width AS matrixW, tm.matrix_height AS matrixH, tm.tile_width AS tileW, tm.tile_height AS tileH, tm.pixel_x_size AS pixelSzX, tm.pixel_y_size AS pixelSzY " + + "FROM gpkg_tile_matrix tm " + + "INNER JOIN (SELECT gc.table_name, gc.data_type, gs.organization_coordsys_id as crs_id from gpkg_contents gc inner join gpkg_spatial_ref_sys gs where gc.srs_id=gs.srs_id) AS sel " + + "ON tm.table_name=sel.table_name " + "WHERE crsID IN (3395, 4326) ORDER BY zoomLvl;"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + List annexC_3395 = populateAnnex(ANNEX_C_3395_TABLE, "Annex C (EPSG:3395)"); + List annexE_4326 = populateAnnex(ANNEX_E_4326_TABLE, "Annex E (EPSG:4326)"); + + while (resultSet.next()) { + String tabNam = resultSet.getString("tabName").trim(); + String srsID = resultSet.getString("crsID").trim(); + int zoomLvl = resultSet.getInt("zoomLvl"); + + long matrixW = resultSet.getLong("matrixW"); + long matrixH = resultSet.getLong("matrixH"); + double pixelSzX = resultSet.getDouble("pixelSzX"); + double pixelSzY = resultSet.getDouble("pixelSzY"); + + List annexTable = selectAnnexBySrsId(srsID, annexC_3395, annexE_4326); + for (Object[] obj : annexTable) { + if (zoomLvl == (int) obj[0]) { + long imW = (long) obj[3]; + long imH = (long) obj[4]; + double pX = (double) obj[2]; + double pY = (double) obj[2]; + + if (Math.abs(pX - pixelSzX) > TOLERANCE) { + invalidMatrixEntries.add(tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " + + "Pixel Size X: " + pixelSzX + "; but expected " + pX); + } + else if (Math.abs(pY - pixelSzY) > TOLERANCE) { + invalidMatrixEntries.add(tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " + + "Pixel Size Y: " + pixelSzY + "; but expected " + pY); + } + else if (imW != matrixW) { + invalidMatrixEntries.add(tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " + + "Matrix Width: " + matrixW + "; but expected " + imW); + } + else if (imH != matrixH) { + invalidMatrixEntries.add(tabNam + " (" + srsID + ", Zoom Level: " + zoomLvl + "): " + + "Matrix Height: " + matrixH + "; but expected " + imH); + } + } + } + } + assertTrue(invalidMatrixEntries.isEmpty(), MessageFormat.format( + "The gpkg_tile_matrix table contains invalid Pixels Size or Matrix Size values for tables: {0}", + invalidMatrixEntries.stream().map(Object::toString).collect(Collectors.joining(", ")))); + } + } + } + + /* + * TODO: Implement Test for Requirement 6 + * + * --- NSG Req 6: The WGS 84 Geographic 2D CRS SHALL be used for 2D vector features. + * WGS 84 Geographic 2D GeoPackages SHALL follow the technical guidance provided in + * Annex E: Implementation Guide for EPSG::4326 Tiles. + * + * @Test(groups = { "NSG" }, description = "NSG Req 6 (match Annex table)") + */ + + /** + * NSG Req 8: The CRS definitions in Table 7 through Table 19 below SHALL be used to + * specify the CRS used for tiles and vector feature user data tables containing NSG + * data in a GeoPackage. + * + * NSG Req 9: Other CRS definitions SHALL NOT be specified for GeoPackage SQL tables + * containing NSG data. + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 8 & 9 (CRS definitions)") + public void crsDefinitionsTest() throws SQLException { + if (crsListing == null) + throw new SkipException("No designated CRS Lookup Table available"); + + String queryStr = "SELECT srs_id,definition FROM gpkg_spatial_ref_sys"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + final Collection invalidSrsDefs = new LinkedList<>(); + + while (resultSet.next()) { + String srsID = resultSet.getString("srs_id").trim(); + if (srsID.equals("0")) { + continue; + } + if (srsID.equals("-1")) { + continue; + } + + // Get the current CRS definition and get from our saved specification, + // the CRS definition that we expect. Both of these are WKT. + final String defin = resultSet.getString("definition"); + final String specin = crsListing.getDefinitionBySrsId(srsID); + + String specWKT = specin; + String defWKT = defin; + + try { + // Parse WKT - this one is from the specification file + CoordinateReferenceSystem specCRS = crsFactory.createFromWKT(specin); + specWKT = specCRS.toWKT(); + + // Parse WKT - this one is from the geopackage + CoordinateReferenceSystem testCRS = crsFactory.createFromWKT(defin); + defWKT = testCRS.toWKT(); + + } + catch (FactoryException e) { + // Normalization failed + } + + // The WKTs may be normalized now. However, there still may be spaces + // in there that could cause a difference. Hence, we'll compare using a + // function that + // strips spaces and moves all content to lower case. + // This is still an extremely incomplete test and the CRSs may still be + // the same. In part, + // this is due to different variations of WKT content. + // We have found no WKT comparison utilities that will work. + + boolean crsEquivalent = compareDefintion(defWKT, specWKT); + + // At this point we know the names from testCRS.getName() may still be + // different, + // and the identifiers from testCRS.getIdentifiers() may be different, but + // these + // may still be "the same" CRS. There is not much we can do about it given + // the available + // utilities. + + if (!crsEquivalent) { + final String issueRpt = String.format( + "srs_id: %s : GeoPackage WKT (normalized): %s : Specification WKT (normalized): %s ", srsID, + defin, specin); + invalidSrsDefs.add(issueRpt); + Assert.fail(MessageFormat.format( + "The CRS for this srs_id is not exactly equivalent to the specification. {0}", + invalidSrsDefs.stream().map(Object::toString).collect(Collectors.joining(", ")))); + } + + } + } + } + + /** + * NSG Req 19: Data validity SHALL be assessed against data value constraints + * specified in Table 26 below using a test suite. Data validity MAY be enforced by + * SQL triggers. + * + * 19-A: Addresses Table 26 Rows 1-2 (regarding table "gpkg_spatial_ref_sys") + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 19-A (Data Validity: gpkg_spatial_ref_sys)") + public void dataValidity_gpkg_spatial_ref_sys() throws SQLException { + if (crsListing == null) + throw new SkipException("No designated CRS Lookup Table available"); + + String queryStr = "SELECT srs_id,organization,description FROM gpkg_spatial_ref_sys;"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + final Collection invalidOrgs = new LinkedList<>(); + final Collection invalidDesc = new LinkedList<>(); + + while (resultSet.next()) { + String srsID = resultSet.getString("srs_id").trim(); + if ("0".equals(srsID) || "-1".equals(srsID)) { + continue; + } + + // --- test for: Table 26; Row 1 + String srsOrg = resultSet.getString("organization"); + validateCrsOrganistion(srsID, srsOrg, invalidOrgs); + + // --- test for: Table 26; Row 2 + String description = resultSet.getString("description"); + validateCrsDescription(srsID, description, invalidDesc); + } + + assertTrue(invalidOrgs.isEmpty(), MessageFormat.format( + "The gpkg_spatial_ref_sys table contains invalid organization values for IDs: {0}, should be \'EPSG\' or \'NGA\'", + invalidOrgs.stream().map(Object::toString).collect(Collectors.joining(", ")))); + assertTrue(invalidDesc.isEmpty(), + MessageFormat.format("The gpkg_spatial_ref_sys table contains invalid descriptions for IDs: {0}", + invalidDesc.stream().map(Object::toString).collect(Collectors.joining(", ")))); + } + } + + private void validateCrsOrganistion(String srsID, String srsOrg, Collection invalidOrgs) { + if (srsOrg == null) { + invalidOrgs.add(srsID + ": null (expected 'EPSG' or 'NGA')"); + } + else { + srsOrg = srsOrg.trim(); + if (!"EPSG".equalsIgnoreCase(srsOrg) && !"NGA".equalsIgnoreCase(srsOrg)) { + invalidOrgs.add(srsID + ": " + srsOrg + " (expected 'EPSG' or 'NGA')"); + } + } + } + + private void validateCrsDescription(String srsID, String description, Collection invalidDesc) { + if (description == null) { + invalidDesc.add(srsID + " (expected not to be null)"); + return; + } + if (!isCrsDescriptionValid(description)) { + invalidDesc.add(srsID + ": '" + description + + "' (expected not to be an empty string, not all whitespace, not “unknown” (any case), not “tbd” (any case))"); + return; + } + String expectedDescription = crsListing.getDescriptionBySrsId(srsID); + if (expectedDescription != null && !isDescriptionAsExpected(description, expectedDescription)) { + invalidDesc.add(srsID + " : '" + description + "' (expected: '" + expectedDescription + "')"); + } + } + + private boolean isDescriptionAsExpected(String descriptionToTest, String expectedDescription) { + descriptionToTest = removeWhitespaces(descriptionToTest); + expectedDescription = removeWhitespaces(expectedDescription); + if (descriptionToTest.endsWith(".")) + descriptionToTest = descriptionToTest.substring(0, descriptionToTest.length() - 1); + if (expectedDescription.endsWith(".")) + expectedDescription = expectedDescription.substring(0, expectedDescription.length() - 1); + return descriptionToTest.equalsIgnoreCase(expectedDescription); + } + + private boolean compareDefintion(String definitionToTest, String expectedDefinition) { + definitionToTest = removeWhitespaces(definitionToTest); + expectedDefinition = removeWhitespaces(expectedDefinition); + return definitionToTest.equalsIgnoreCase(expectedDefinition); + } + + private List selectAnnexBySrsId(String srsID, List annexC_3395, List annexE_4326) { + if (srsID.equals("3395")) { + return annexC_3395; + } + else if (srsID.equals("4326")) { + return annexE_4326; + } + else if ((srsID.equals("5041")) || (srsID.equals("5042"))) { + return Collections.emptyList(); + } + return Collections.emptyList(); + } + + private List populateAnnex(String annexTableName, String annexName) { + InputStream resourceToRead = this.getClass().getResourceAsStream(annexTableName); + try (BufferedReader br = new BufferedReader(new InputStreamReader(resourceToRead, "UTF-8"))) { + List annexEntries = new ArrayList<>(); + String line; + while ((line = br.readLine()) != null) { + List items = Arrays.asList(line.split("\\s*,\\s*")); + if (!items.isEmpty() && (items.size() == 5)) { + addNewEntry(annexEntries, items.get(0), items.get(1), items.get(2), items.get(3), items.get(4)); + } + else { + throw new SkipException(annexName + " Table is corrupt "); + } + } + return annexEntries; + } + catch (IOException e) { + throw new SkipException(annexName + " Table not available"); + } + } + + private void addNewEntry(List table, String zoom, String scale, String pixelSz, String matrixWidth, + String matrixHeight) { + int zoomAsInt = Integer.parseInt(zoom); + double scaleAsDouble = Double.parseDouble(scale); + double pixelSizeAsDouble = Double.parseDouble(pixelSz); + long matrixWidthAsLong = Long.parseLong(matrixWidth); + long matrixHeightAsLong = Long.parseLong(matrixHeight); + Object[] row = { zoomAsInt, scaleAsDouble, pixelSizeAsDouble, matrixWidthAsLong, matrixHeightAsLong }; + table.add(row); + } + + private String removeWhitespaces(String description) { + if (description != null) + return description.trim().replaceAll("\\s+", ""); + return description; + } + + private boolean isCrsDescriptionValid(String srsDesc) { + return srsDesc.length() > 0 && srsDesc.trim().length() > 0 && (!srsDesc.equalsIgnoreCase("NULL")) + && (!srsDesc.equalsIgnoreCase("UNK")) && (!srsDesc.equalsIgnoreCase("UNKNOWN")) + && (!srsDesc.equalsIgnoreCase("TBD")); + } } \ No newline at end of file diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTests.java b/src/main/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTests.java index 3f0773e..a9f1099 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTests.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTests.java @@ -35,122 +35,124 @@ public class MetadataTests extends CommonFixture { - private final static Logger LOG = Logger.getLogger( MetadataTests.class.getName() ); - - private Schema schema; - - private DocumentBuilder builder; - - @BeforeClass - public void initSchema() - throws SAXException { - SchemaFactory schemaFactory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ); - URL resource = getClass().getClassLoader().getResource( "org/opengis/cite/gpkg12/nsg/metadata/NMIS_v2.X_Schema/nas/nmis.xsd" ); - this.schema = schemaFactory.newSchema( resource ); - } - - @BeforeClass - public void initDocumentBuilder() - throws ParserConfigurationException { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setValidating( false ); - this.builder = factory.newDocumentBuilder(); - } - - /** - * Validate metadata against NMIS xsd https://nsgreg.nga.mil/doc/view?i=2491 - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(description = "Validate against NMIS schema") - public void metadataSchemaValidation() - throws SQLException { - if ( this.schema == null ) - throw new SkipException( "Schema required for validation could not be loaded." ); - - if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_metadata")) { - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( "SELECT metadata FROM gpkg_metadata;" )) { - List xmlEntries = findXmlEntries( resultSet ); - validateXmlEntries( xmlEntries ); - } - } else { - throw new SkipException( "Table gpkg_metadata required to evaluate metadata." ); - } - } - - void validateXmlEntries( List xmlEntries ) { - if ( xmlEntries.isEmpty() ) - throw new AssertionError( "GeoPackage does not contain a NMIS v2.2 metadata as XML." ); - if ( xmlEntries.size() == 1 ) { - validateXmlEntry( xmlEntries.get( 0 ) ); - } else { - boolean hasValidMetadataEntry = hasValidEntry( xmlEntries ); - if ( !hasValidMetadataEntry ) - throw new AssertionError( "GeoPackage does not contain at least one valid NMIS v2.2 metadata as XML." ); - } - } - - private void validateXmlEntry( String xmlEntry ) { - try { - ValidationErrorHandler validationResult = validate( xmlEntry ); - if ( validationResult.errorsDetected() ) { - String errors = validationResult.getErrors().stream().map( Object::toString ).collect( Collectors.joining( "," ) ); - throw new AssertionError( - "GeoPackage does not contain a valid NMIS v2.2 metadata as XML. Results of the schema validation: " - + errors ); - } - } catch ( IOException | SAXException e ) { - LOG.log( WARNING, "An error occurred during validation.", e ); - throw new AssertionError( "Validation of the NMIS v2.2 metadata as XML failed: " + e.getMessage() ); - } - } - - List findXmlEntries( ResultSet resultSet ) - throws SQLException { - List xmlEntries = new ArrayList<>(); - while ( resultSet.next() ) { - String metadataEntry = resultSet.getString( "metadata" ); - try (StringReader reader = new StringReader( metadataEntry )) { - InputSource inputSource = new InputSource( reader ); - this.builder.parse( inputSource ); - xmlEntries.add( metadataEntry ); - } catch ( SAXException | IOException e ) { - } - } - return xmlEntries; - } - - private boolean hasValidEntry( List metadataEntries ) { - for ( String metadataEntry : metadataEntries ) { - if ( isEntryValid( metadataEntry ) ) - return true; - } - return false; - } - - private boolean isEntryValid( String metadataEntry ) { - try { - ValidationErrorHandler validationResult = validate( metadataEntry ); - if ( !validationResult.errorsDetected() ) { - return true; - } - } catch ( IOException | SAXException e ) { - LOG.log( WARNING, "An error occurred during validation.", e ); - } - return false; - } - - private ValidationErrorHandler validate( String metadataEntry ) - throws IOException, SAXException { - Validator validator = schema.newValidator(); - ValidationErrorHandler errHandler = new ValidationErrorHandler(); - validator.setErrorHandler( errHandler ); - Source source = new StreamSource( new ByteArrayInputStream( metadataEntry.getBytes() ) ); - validator.validate( source ); - return errHandler; - } + private final static Logger LOG = Logger.getLogger(MetadataTests.class.getName()); + + private Schema schema; + + private DocumentBuilder builder; + + @BeforeClass + public void initSchema() throws SAXException { + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + URL resource = getClass().getClassLoader() + .getResource("org/opengis/cite/gpkg12/nsg/metadata/NMIS_v2.X_Schema/nas/nmis.xsd"); + this.schema = schemaFactory.newSchema(resource); + } + + @BeforeClass + public void initDocumentBuilder() throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + this.builder = factory.newDocumentBuilder(); + } + + /** + * Validate metadata against NMIS xsd https://nsgreg.nga.mil/doc/view?i=2491 + * @throws SQLException if access to gpkg failed + */ + @Test(description = "Validate against NMIS schema") + public void metadataSchemaValidation() throws SQLException { + if (this.schema == null) + throw new SkipException("Schema required for validation could not be loaded."); + + if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_metadata")) { + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT metadata FROM gpkg_metadata;")) { + List xmlEntries = findXmlEntries(resultSet); + validateXmlEntries(xmlEntries); + } + } + else { + throw new SkipException("Table gpkg_metadata required to evaluate metadata."); + } + } + + void validateXmlEntries(List xmlEntries) { + if (xmlEntries.isEmpty()) + throw new AssertionError("GeoPackage does not contain a NMIS v2.2 metadata as XML."); + if (xmlEntries.size() == 1) { + validateXmlEntry(xmlEntries.get(0)); + } + else { + boolean hasValidMetadataEntry = hasValidEntry(xmlEntries); + if (!hasValidMetadataEntry) + throw new AssertionError("GeoPackage does not contain at least one valid NMIS v2.2 metadata as XML."); + } + } + + private void validateXmlEntry(String xmlEntry) { + try { + ValidationErrorHandler validationResult = validate(xmlEntry); + if (validationResult.errorsDetected()) { + String errors = validationResult.getErrors() + .stream() + .map(Object::toString) + .collect(Collectors.joining(",")); + throw new AssertionError( + "GeoPackage does not contain a valid NMIS v2.2 metadata as XML. Results of the schema validation: " + + errors); + } + } + catch (IOException | SAXException e) { + LOG.log(WARNING, "An error occurred during validation.", e); + throw new AssertionError("Validation of the NMIS v2.2 metadata as XML failed: " + e.getMessage()); + } + } + + List findXmlEntries(ResultSet resultSet) throws SQLException { + List xmlEntries = new ArrayList<>(); + while (resultSet.next()) { + String metadataEntry = resultSet.getString("metadata"); + try (StringReader reader = new StringReader(metadataEntry)) { + InputSource inputSource = new InputSource(reader); + this.builder.parse(inputSource); + xmlEntries.add(metadataEntry); + } + catch (SAXException | IOException e) { + } + } + return xmlEntries; + } + + private boolean hasValidEntry(List metadataEntries) { + for (String metadataEntry : metadataEntries) { + if (isEntryValid(metadataEntry)) + return true; + } + return false; + } + + private boolean isEntryValid(String metadataEntry) { + try { + ValidationErrorHandler validationResult = validate(metadataEntry); + if (!validationResult.errorsDetected()) { + return true; + } + } + catch (IOException | SAXException e) { + LOG.log(WARNING, "An error occurred during validation.", e); + } + return false; + } + + private ValidationErrorHandler validate(String metadataEntry) throws IOException, SAXException { + Validator validator = schema.newValidator(); + ValidationErrorHandler errHandler = new ValidationErrorHandler(); + validator.setErrorHandler(errHandler); + Source source = new StreamSource(new ByteArrayInputStream(metadataEntry.getBytes())); + validator.validate(source); + return errHandler; + } } diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/tiles/NSG_TileTests.java b/src/main/java/org/opengis/cite/gpkg12/nsg/tiles/NSG_TileTests.java index 08c2450..caea5d0 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/tiles/NSG_TileTests.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/tiles/NSG_TileTests.java @@ -42,492 +42,487 @@ public class NSG_TileTests extends CommonFixture { - private static final int MIN_ZOOM = 0; - - private static final int MAX_ZOOM = 24; - - private static final double TOLERANCE = 1.0e-10; - - - /** - * Validate a string value to ensure it contains no illegal characters or content - * - * @param inputString The string to validate - * @return validated string - * @throws IllegalArgumentException if the input is found to be invalid - */ - public static String ValidateStringInput( String inputString ) throws IllegalArgumentException { - - StringBuilder sb = new StringBuilder(50); // initial size is 50. This is expected to be sufficient for most table and field names. This is NOT a limit. - for (int ii = 0; ii < inputString.length(); ++ii) { - final char cleanedchar = cleanChar(inputString.charAt(ii)); - if (cleanedchar == '^') { // This is an illegal character indicator - throw new IllegalArgumentException(String.format("Illegal parameter provided within SQL statement. Error in %s at character %c",inputString, inputString.charAt(ii))); - } - else { - sb.append(cleanedchar); - } - } - return sb.toString(); - } - - /** - * Validate and clean a character of a string. - * - * @param inputChar A character of a string, for which we will check validity, replacing any illegal characters with % - * @return a validated character - */ - private static char cleanChar(char inputChar) { - // 0 - 9 - for (int i = 48; i < 58; ++i) { - if (inputChar == i) return (char) i; - } - - // 'A' - 'Z' - for (int i = 65; i < 91; ++i) { - if (inputChar == i) return (char) i; - } - - // 'a' - 'z' - for (int i = 97; i < 123; ++i) { - if (inputChar == i) return (char) i; - } - - // other valid characters - switch (inputChar) { - case '.': - return '.'; - case '-': - return '-'; - case '_': - return '_'; - case ' ': - return ' '; - } - return '^'; - } - - - /** - * NSG Req 19: Data validity SHALL be assessed against data value constraints specified in Table 26 below using a - * test suite. Data validity MAY be enforced by SQL triggers. - * - * 19-D: Addresses Table 26 Rows 12-17 (regarding table "gpkg_tile_matrix") - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 19-D (Data Validity: gpkg_tile_matrix)") - public void dataValidity_gpkg_tile_matrix() - throws SQLException { - // test for: Table 26; Row 17 - String queryStr = "SELECT table_name FROM gpkg_contents WHERE data_type=\'tiles\';"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - while ( resultSet.next() ) { - String tableName = ValidateStringInput(resultSet.getString( "table_name" ).trim()); - int firstZoom = 999; - int lastZoom = -1; - - // test for: Table 26; Row 12 & 17 - try (final Statement tileStatement = this.databaseConnection.createStatement(); - final ResultSet tileResultSet = tileStatement.executeQuery( "SELECT DISTINCT zoom_level FROM " + tableName + " ORDER BY zoom_level;" )) { - while ( tileResultSet.next() ) { - int zoom = tileResultSet.getInt( "zoom_level" ); - firstZoom = Math.min( firstZoom, zoom ); - lastZoom = Math.max( lastZoom, zoom ); - } - } - - assertTrue( ( firstZoom >= MIN_ZOOM ), - MessageFormat.format( "The " - + tableName - + " table contains an invalid minimum zoom_level: {0}, should be: {1}", - Integer.toString( firstZoom ), Integer.toString( this.MIN_ZOOM ) ) ); - assertTrue( ( lastZoom <= MAX_ZOOM ), - MessageFormat.format( "The " - + tableName - + " table contains an invalid maximum zoom_level: {0}, should be: {1}", - Integer.toString( firstZoom ), Integer.toString( this.MAX_ZOOM ) ) ); - - - try (final Statement tileStatement = this.databaseConnection.createStatement(); - final ResultSet tileResultSet = tileStatement.executeQuery( "SELECT zoom_level, tile_width, tile_height, pixel_x_size, pixel_y_size FROM gpkg_tile_matrix WHERE table_name=\'" - + tableName + "\' ORDER BY zoom_level;" )) { - boolean firstFound = false; - boolean lastFound = false; - double pixelSzX = 0.0D; - double pixelSzY = 0.0D; - - while ( tileResultSet.next() ) { - int zoom = tileResultSet.getInt( "zoom_level" ); - if ( zoom >= firstZoom && zoom <= lastZoom ) { - int tileWidth = tileResultSet.getInt( "tile_width" ); - int tileHeight = tileResultSet.getInt( "tile_height" ); - - double lastPixelSzX = pixelSzX; - double lastPixelSzY = pixelSzY; - pixelSzX = tileResultSet.getDouble( "pixel_x_size" ); - pixelSzY = tileResultSet.getDouble( "pixel_y_size" ); - - // test for: Table 26; Row 12 (again) - assertTrue( ( 0 <= zoom && zoom <= lastZoom ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid zoom_level: {0} for {1}, should be between {2} and {3}", - Integer.toString( zoom ), tableName, - Integer.toString( firstZoom ), - Integer.toString( lastZoom ) ) ); - - if ( !firstFound ) { - firstFound = ( zoom == firstZoom ); - } - - if ( !lastFound ) { - lastFound = ( zoom == lastZoom ); - } - - // test for: Table 26; Row 13 - assertTrue( ( tileWidth == 256 ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid tile_width: {0} for {1}, should be 256", - Integer.toString( tileWidth ), tableName ) ); - - // test for: Table 26; Row 14 - assertTrue( ( tileHeight == 256 ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid tile_height: {0} for {1}, should be 256", - Integer.toString( tileHeight ), tableName ) ); - - // test for: Table 26; Row 15 - double deltaX = Math.abs( ( pixelSzX * 2.0D ) - lastPixelSzX ); - assertTrue( ( ( zoom == firstZoom ) || ( deltaX < TOLERANCE ) ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid pixel_x_size: {0} for {1}", - String.format( "%.10f", pixelSzX ), tableName ) ); - - // test for: Table 26; Row 16 - double deltaY = Math.abs( ( pixelSzY * 2.0D ) - lastPixelSzY ); - assertTrue( ( ( zoom == firstZoom ) || ( deltaY < TOLERANCE ) ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid pixel_y_size: {0} for {1}", - String.format( "%.10f", pixelSzY ), tableName ) ); - } - } - - // test for: Table 26; Row 12 & 17 (again) - assertTrue( firstFound, - MessageFormat.format( "The gpkg_tile_matrix contains an invalid zoom_level: no zoom level 0 for {0}", - tableName ) ); - assertTrue( lastFound, - MessageFormat.format( "The gpkg_tile_matrix contains an invalid zoom_level: no max zoom level for {0}", - tableName ) ); - } - } - } - } - - /** - * NSG Req 20: The gpkg_tile_matrix table SHALL contain tile_width and tile_height column values of 256 for every - * table_name tile pyramid data table. - * - * NSG Req 21: Every tile_data tile in every table_name tile pyramid data table shall have a width and height of 256 - * pixels. - * - * @throws SQLException - * if access to gpkg failed - * @throws IOException - * if tile data coul not be read - */ - @Test(groups = { "NSG" }, description = "NSG Req 20 & 21 (Tile widths and heights)") - public void tileSizeTests() - throws SQLException, IOException { - - if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix")) { - - // test Req 20 - Collection tableNameAndExtents = collectTableNameAndExtents(); - assertTrue( tableNameAndExtents.isEmpty(), - MessageFormat.format( "The gpkg_tile_matrix table contains invalid tile width/height values for tables: {0}", - tableNameAndExtents.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - - // test Req 21 - String tableNameQuery = "SELECT DISTINCT table_name FROM gpkg_tile_matrix;"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( tableNameQuery )) { - while ( resultSet.next() ) { - String tableName = ValidateStringInput(resultSet.getString( "table_name" ).trim()); - - try (final Statement subStatement = this.databaseConnection.createStatement(); - final ResultSet subResultSet = subStatement.executeQuery( "SELECT zoom_level, tile_column, tile_row, tile_data FROM " + tableName )) { - while ( subResultSet.next() ) { - byte[] image = subResultSet.getBytes( "tile_data" ); - ImageInputStream iis = ImageIO.createImageInputStream( new ByteArrayInputStream( image ) ); - Iterator readers = ImageIO.getImageReaders( iis ); - - while ( readers.hasNext() ) { - ImageReader read = (ImageReader) readers.next(); - read.setInput( iis, true ); - int width = read.getWidth( 0 ); - int height = read.getHeight( 0 ); - - assertTrue( ( width == 256 ), - MessageFormat.format( "The pyramid data table (for {0}) contains a tile image (at zoom_level: {1}, (col,row): {2},{3}) with an invalid tile_width: {4}", - tableName, - Integer.toString( subResultSet.getInt( "zoom_level" ) ), - Integer.toString( subResultSet.getInt( "tile_column" ) ), - Integer.toString( subResultSet.getInt( "tile_row" ) ), - Integer.toString( width ) ) ); - assertTrue( ( height == 256 ), - MessageFormat.format( "The pyramid data table (for {0}) contains a tile image (at zoom_level: {1}, (col,row): {2},{3}) with an invalid tile_height: {4}", - tableName, - Integer.toString( subResultSet.getInt( "zoom_level" ) ), - Integer.toString( subResultSet.getInt( "tile_column" ) ), - Integer.toString( subResultSet.getInt( "tile_row" ) ), - Integer.toString( height ) ) ); - } - } - } - } - } - } else { - throw new SkipException( "Table gpkg_tile_matrix required to perform this test." ); - } - } - - /** - * NSG Req 22: The gpkg_tile_matrix table SHALL contain pixel_x_size and pixel_y_size column values that differ by a - * factor of 2 between all adjacent zoom levels for each tile pyramid data table per OGC GeoPackage Clause 2.2.3. It - * SHALL NOT contain pixel sizes that vary by irregular intervals or by regular intervals other than a factor of 2 - * between adjacent zoom levels per OGC GeoPackage Clause 3.2.1. - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 22 (pixels sizes factor of 2)") - public void pixelsSizeTests() - throws SQLException { - String queryStr = "SELECT table_name FROM gpkg_contents WHERE data_type=\'tiles\';"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - while ( resultSet.next() ) { - String tableName = ValidateStringInput(resultSet.getString( "table_name" ).trim()); - - String subQueryStr = "SELECT pixel_x_size, pixel_y_size FROM gpkg_tile_matrix " + "WHERE table_name=\'" - + tableName + "\' ORDER BY zoom_level;"; - - try (final Statement subStatement = this.databaseConnection.createStatement(); - final ResultSet subResultSet = subStatement.executeQuery( subQueryStr )) { - double pixelSzX = -1.0D; - double pixelSzY = -1.0D; - - while ( subResultSet.next() ) { - double lastPixelSzX = pixelSzX; - double lastPixelSzY = pixelSzY; - pixelSzX = subResultSet.getDouble( "pixel_x_size" ); - pixelSzY = subResultSet.getDouble( "pixel_y_size" ); - - if ( lastPixelSzX < 0.0D ) { - lastPixelSzX = pixelSzX * 2.0D; - lastPixelSzY = pixelSzY * 2.0D; - } - - double deltaX = Math.abs( ( pixelSzX * 2.0D ) - lastPixelSzX ); - assertTrue( ( deltaX < this.TOLERANCE ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid pixel_x_size: {0} for {1}", - String.format( "%.10f", pixelSzX ), tableName ) ); - double deltaY = Math.abs( ( pixelSzY * 2.0D ) - lastPixelSzY ); - assertTrue( ( deltaY < this.TOLERANCE ), - MessageFormat.format( "The gpkg_tile_matrix contains an invalid pixel_y_size: {0} for {1}", - String.format( "%.10f", pixelSzY ), tableName ) ); - } - } - } - } - } - - /** - * NSG Req 23: The (min_x, min_y, max_x, max_y) values in the gpkg_tile_matrix_set table SHALL be the maximum bounds - * of the CRS specified for the tile pyramid data table and SHALL be used to determine the geographic position of - * each tile in the tile pyramid data table. - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 23 (bounding box in gpkg_tile_matrix_set)") - public void boundingBoxTests() - throws SQLException { - - if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix_set")) { - - String queryStr = "SELECT table_name, srs_id, min_x, min_y, max_x, max_y FROM gpkg_tile_matrix_set;"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - while ( resultSet.next() ) { - String srsID = resultSet.getString( "srs_id" ).trim(); - String tableName = resultSet.getString( "table_name" ).trim(); - double[] mbr = findMbrBySrsId( srsID ); - - assertTrue( ( mbr != null ), - MessageFormat.format( "The gpkg_tile_matrix_set contains an invalid CRS definition: {0} for table {1}", - srsID, tableName ) ); - - double minX = resultSet.getDouble( "min_x" ); - assertTrue( ( minX == mbr[0] ), - MessageFormat.format( "The gpkg_tile_matrix_set contains an invalid min_x value: {0} for table {1} (should be {2})", - Double.valueOf( minX ), tableName, Double.valueOf( mbr[0] ) ) ); - - double minY = resultSet.getDouble( "min_y" ); - assertTrue( ( minY == mbr[1] ), - MessageFormat.format( "The gpkg_tile_matrix_set contains an invalid min_y value: {0} for table {1} (should be {2})", - Double.valueOf( minY ), tableName, Double.valueOf( mbr[1] ) ) ); - - double maxX = resultSet.getDouble( "max_x" ); - assertTrue( ( maxX == mbr[2] ), - MessageFormat.format( "The gpkg_tile_matrix_set contains an invalid max_x value: {0} for table {1} (should be {2})", - Double.valueOf( maxX ), tableName, Double.valueOf( mbr[2] ) ) ); - - double maxY = resultSet.getDouble( "max_y" ); - assertTrue( ( maxY == mbr[3] ), - MessageFormat.format( "The gpkg_tile_matrix_set contains an invalid max_y value: {0} for table {1} (should be {2})", - Double.valueOf( maxY ), tableName, Double.valueOf( mbr[3] ) ) ); - } - } - } else { - throw new SkipException( "Table gpkg_tile_matrix_set required to perform this test." ); - } - } - - private Collection collectTableNameAndExtents() - throws SQLException { - - final Collection tableNamesAndExtents = new LinkedList<>(); - if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix_set")) { - - String tableNameAndExtentsQuery = "SELECT table_name, zoom_level, tile_width, tile_height FROM gpkg_tile_matrix " - + "WHERE NOT ((tile_width=256) AND (tile_height=256));"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( tableNameAndExtentsQuery )) { - - - while ( resultSet.next() ) { - String tableName = resultSet.getString( "table_name" ); - int tileWidth = resultSet.getInt( "tile_width" ); - int tileHeight = resultSet.getInt( "tile_height" ); - tableNamesAndExtents.add( tableName + ": (tile_width)" + tileWidth + ", (tile_height)" + tileHeight ); - } - - } - } else { - throw new SkipException( "Table gpkg_tile_matrix_set required to perform this test." ); - } - return tableNamesAndExtents; - } - - private double[] findMbrBySrsId( String srsID ) { - if ( srsID.equals( "3395" ) ) { - return new double[] { -20037508.342789244D, -20037508.342789244D, 20037508.342789244D, 20037508.342789244D }; - } else if ( srsID.equals( "5041" ) ) { - return new double[] { -14440759.350252D, -14440759.350252D, 18440759.350252D, 18440759.350252D }; - } else if ( srsID.equals( "4326" ) ) { - return new double[] { -180.0D, -90.0D, 180.0D, 90.0D }; - } - return null; - } - - - /** - * --- NSG Req 19: Data validity SHALL be assessed against data value constraints specified in Table 26 below using - * a test suite. Data validity MAY be enforced by SQL triggers. - * - * --- 19-B: Addresses Table 26 Rows 3-7 (regarding table "gpkg_contents") - * - * @throws SQLException - * if access to gpkg failed - */ - @Test(groups = { "NSG" }, description = "NSG Req 19-B (Data Validity: gpkg_contents, tiles)") - public void dataValidity_gpkg_contents() - throws SQLException { - String queryStr = "SELECT srs_id,table_name,min_x,min_y,max_x,max_y FROM \'gpkg_contents\' WHERE (data_type=\'tiles\');"; - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( queryStr )) { - - final Collection invalidMinX = new LinkedList<>(); - final Collection invalidMinY = new LinkedList<>(); - final Collection invalidMaxX = new LinkedList<>(); - final Collection invalidMaxY = new LinkedList<>(); - - while ( resultSet.next() ) { - final String srsTabNam = resultSet.getString("table_name"); - // test for: Table 26; Row 4 - collectInvalidMinValues( resultSet, invalidMinX, srsTabNam, "min_x" ); - // test for: Table 26; Row 5 - collectInvalidMinValues( resultSet, invalidMinY, srsTabNam, "min_y" ); - // test for: Table 26; Row 6 - collectInvalidMaxValues( resultSet, invalidMaxX, srsTabNam, "max_x" ); - // test for: Table 26; Row 7 - collectInvalidMaxValues( resultSet, invalidMaxY, srsTabNam, "max_y" ); - } - - assertTrue( invalidMinX.isEmpty(), - MessageFormat.format( "The gpkg_contents table contains invalid minimum X bounds values for tables: {0}", - invalidMinX.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - assertTrue( invalidMinY.isEmpty(), - MessageFormat.format( "The gpkg_contents table contains invalid minimum Y bounds values for tables: {0}", - invalidMinY.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - assertTrue( invalidMaxX.isEmpty(), - MessageFormat.format( "The gpkg_contents table contains invalid maximum X bounds values for tables: {0}", - invalidMaxX.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - assertTrue( invalidMaxY.isEmpty(), - MessageFormat.format( "The gpkg_contents table contains invalid maximum Y bounds values for tables: {0}", - invalidMaxY.stream().map( Object::toString ).collect( Collectors.joining( ", " ) ) ) ); - } - - } - - private void collectInvalidMaxValues( ResultSet resultSet, Collection invalidValue, String srsTabNam, - String testBoundsColumn ) throws SQLException { - if ( resultSet.getString( testBoundsColumn ) != null ) { - double val = resultSet.getDouble( testBoundsColumn ); - - double bnd = this.checkTileBounds( ValidateStringInput(srsTabNam), testBoundsColumn ); - if ( val > bnd ) { - invalidValue.add( srsTabNam + ":" + val + ", should be: " + bnd ); - } - } - } - - private void collectInvalidMinValues( ResultSet resultSet, Collection invalidValue, String srsTabNam, - String testBoundsColumn ) - throws SQLException { - if ( resultSet.getString( testBoundsColumn ) != null ) { - double val = resultSet.getDouble( testBoundsColumn ); - - double bnd = this.checkTileBounds( ValidateStringInput(srsTabNam), testBoundsColumn ); - if ( val < bnd ) { - invalidValue.add( srsTabNam + ":" + val + ", should be: " + bnd ); - } - } - } - - - - /* - * convenience routine to consistently return specific bounds column as double - */ - private double checkTileBounds( String tableName, String boundsColumn ) - throws SQLException { - - try (final Statement statement = this.databaseConnection.createStatement(); - final ResultSet resultSet = statement.executeQuery( "SELECT " + boundsColumn + " FROM gpkg_tile_matrix_set WHERE table_name = \'" + tableName + "\';" )) { - assertTrue( resultSet.next(), ErrorMessage.format( ErrorMessageKeys.BAD_TILE_MATRIX_SET_TABLE_DEFINITION ) ); - - return resultSet.getDouble( boundsColumn ); - } + private static final int MIN_ZOOM = 0; + + private static final int MAX_ZOOM = 24; + + private static final double TOLERANCE = 1.0e-10; + + /** + * Validate a string value to ensure it contains no illegal characters or content + * @param inputString The string to validate + * @return validated string + * @throws IllegalArgumentException if the input is found to be invalid + */ + public static String ValidateStringInput(String inputString) throws IllegalArgumentException { + + StringBuilder sb = new StringBuilder(50); // initial size is 50. This is expected + // to be sufficient for most table and + // field names. This is NOT a limit. + for (int ii = 0; ii < inputString.length(); ++ii) { + final char cleanedchar = cleanChar(inputString.charAt(ii)); + if (cleanedchar == '^') { // This is an illegal character indicator + throw new IllegalArgumentException( + String.format("Illegal parameter provided within SQL statement. Error in %s at character %c", + inputString, inputString.charAt(ii))); + } + else { + sb.append(cleanedchar); + } + } + return sb.toString(); } + /** + * Validate and clean a character of a string. + * @param inputChar A character of a string, for which we will check validity, + * replacing any illegal characters with % + * @return a validated character + */ + private static char cleanChar(char inputChar) { + // 0 - 9 + for (int i = 48; i < 58; ++i) { + if (inputChar == i) + return (char) i; + } + + // 'A' - 'Z' + for (int i = 65; i < 91; ++i) { + if (inputChar == i) + return (char) i; + } + + // 'a' - 'z' + for (int i = 97; i < 123; ++i) { + if (inputChar == i) + return (char) i; + } + + // other valid characters + switch (inputChar) { + case '.': + return '.'; + case '-': + return '-'; + case '_': + return '_'; + case ' ': + return ' '; + } + return '^'; + } + + /** + * NSG Req 19: Data validity SHALL be assessed against data value constraints + * specified in Table 26 below using a test suite. Data validity MAY be enforced by + * SQL triggers. + * + * 19-D: Addresses Table 26 Rows 12-17 (regarding table "gpkg_tile_matrix") + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 19-D (Data Validity: gpkg_tile_matrix)") + public void dataValidity_gpkg_tile_matrix() throws SQLException { + // test for: Table 26; Row 17 + String queryStr = "SELECT table_name FROM gpkg_contents WHERE data_type=\'tiles\';"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + while (resultSet.next()) { + String tableName = ValidateStringInput(resultSet.getString("table_name").trim()); + int firstZoom = 999; + int lastZoom = -1; + + // test for: Table 26; Row 12 & 17 + try (final Statement tileStatement = this.databaseConnection.createStatement(); + final ResultSet tileResultSet = tileStatement + .executeQuery("SELECT DISTINCT zoom_level FROM " + tableName + " ORDER BY zoom_level;")) { + while (tileResultSet.next()) { + int zoom = tileResultSet.getInt("zoom_level"); + firstZoom = Math.min(firstZoom, zoom); + lastZoom = Math.max(lastZoom, zoom); + } + } + + assertTrue((firstZoom >= MIN_ZOOM), + MessageFormat.format( + "The " + tableName + + " table contains an invalid minimum zoom_level: {0}, should be: {1}", + Integer.toString(firstZoom), Integer.toString(this.MIN_ZOOM))); + assertTrue((lastZoom <= MAX_ZOOM), + MessageFormat.format( + "The " + tableName + + " table contains an invalid maximum zoom_level: {0}, should be: {1}", + Integer.toString(firstZoom), Integer.toString(this.MAX_ZOOM))); + + try (final Statement tileStatement = this.databaseConnection.createStatement(); + final ResultSet tileResultSet = tileStatement.executeQuery( + "SELECT zoom_level, tile_width, tile_height, pixel_x_size, pixel_y_size FROM gpkg_tile_matrix WHERE table_name=\'" + + tableName + "\' ORDER BY zoom_level;")) { + boolean firstFound = false; + boolean lastFound = false; + double pixelSzX = 0.0D; + double pixelSzY = 0.0D; + + while (tileResultSet.next()) { + int zoom = tileResultSet.getInt("zoom_level"); + if (zoom >= firstZoom && zoom <= lastZoom) { + int tileWidth = tileResultSet.getInt("tile_width"); + int tileHeight = tileResultSet.getInt("tile_height"); + + double lastPixelSzX = pixelSzX; + double lastPixelSzY = pixelSzY; + pixelSzX = tileResultSet.getDouble("pixel_x_size"); + pixelSzY = tileResultSet.getDouble("pixel_y_size"); + + // test for: Table 26; Row 12 (again) + assertTrue((0 <= zoom && zoom <= lastZoom), MessageFormat.format( + "The gpkg_tile_matrix contains an invalid zoom_level: {0} for {1}, should be between {2} and {3}", + Integer.toString(zoom), tableName, Integer.toString(firstZoom), + Integer.toString(lastZoom))); + + if (!firstFound) { + firstFound = (zoom == firstZoom); + } + + if (!lastFound) { + lastFound = (zoom == lastZoom); + } + + // test for: Table 26; Row 13 + assertTrue((tileWidth == 256), MessageFormat.format( + "The gpkg_tile_matrix contains an invalid tile_width: {0} for {1}, should be 256", + Integer.toString(tileWidth), tableName)); + + // test for: Table 26; Row 14 + assertTrue((tileHeight == 256), MessageFormat.format( + "The gpkg_tile_matrix contains an invalid tile_height: {0} for {1}, should be 256", + Integer.toString(tileHeight), tableName)); + + // test for: Table 26; Row 15 + double deltaX = Math.abs((pixelSzX * 2.0D) - lastPixelSzX); + assertTrue(((zoom == firstZoom) || (deltaX < TOLERANCE)), + MessageFormat.format( + "The gpkg_tile_matrix contains an invalid pixel_x_size: {0} for {1}", + String.format("%.10f", pixelSzX), tableName)); + + // test for: Table 26; Row 16 + double deltaY = Math.abs((pixelSzY * 2.0D) - lastPixelSzY); + assertTrue(((zoom == firstZoom) || (deltaY < TOLERANCE)), + MessageFormat.format( + "The gpkg_tile_matrix contains an invalid pixel_y_size: {0} for {1}", + String.format("%.10f", pixelSzY), tableName)); + } + } + + // test for: Table 26; Row 12 & 17 (again) + assertTrue(firstFound, MessageFormat.format( + "The gpkg_tile_matrix contains an invalid zoom_level: no zoom level 0 for {0}", tableName)); + assertTrue(lastFound, + MessageFormat.format( + "The gpkg_tile_matrix contains an invalid zoom_level: no max zoom level for {0}", + tableName)); + } + } + } + } + + /** + * NSG Req 20: The gpkg_tile_matrix table SHALL contain tile_width and tile_height + * column values of 256 for every table_name tile pyramid data table. + * + * NSG Req 21: Every tile_data tile in every table_name tile pyramid data table shall + * have a width and height of 256 pixels. + * @throws SQLException if access to gpkg failed + * @throws IOException if tile data coul not be read + */ + @Test(groups = { "NSG" }, description = "NSG Req 20 & 21 (Tile widths and heights)") + public void tileSizeTests() throws SQLException, IOException { + + if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix")) { + + // test Req 20 + Collection tableNameAndExtents = collectTableNameAndExtents(); + assertTrue(tableNameAndExtents.isEmpty(), + MessageFormat.format( + "The gpkg_tile_matrix table contains invalid tile width/height values for tables: {0}", + tableNameAndExtents.stream().map(Object::toString).collect(Collectors.joining(", ")))); + + // test Req 21 + String tableNameQuery = "SELECT DISTINCT table_name FROM gpkg_tile_matrix;"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(tableNameQuery)) { + while (resultSet.next()) { + String tableName = ValidateStringInput(resultSet.getString("table_name").trim()); + + try (final Statement subStatement = this.databaseConnection.createStatement(); + final ResultSet subResultSet = subStatement.executeQuery( + "SELECT zoom_level, tile_column, tile_row, tile_data FROM " + tableName)) { + while (subResultSet.next()) { + byte[] image = subResultSet.getBytes("tile_data"); + ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(image)); + Iterator readers = ImageIO.getImageReaders(iis); + + while (readers.hasNext()) { + ImageReader read = (ImageReader) readers.next(); + read.setInput(iis, true); + int width = read.getWidth(0); + int height = read.getHeight(0); + + assertTrue((width == 256), MessageFormat.format( + "The pyramid data table (for {0}) contains a tile image (at zoom_level: {1}, (col,row): {2},{3}) with an invalid tile_width: {4}", + tableName, Integer.toString(subResultSet.getInt("zoom_level")), + Integer.toString(subResultSet.getInt("tile_column")), + Integer.toString(subResultSet.getInt("tile_row")), Integer.toString(width))); + assertTrue((height == 256), MessageFormat.format( + "The pyramid data table (for {0}) contains a tile image (at zoom_level: {1}, (col,row): {2},{3}) with an invalid tile_height: {4}", + tableName, Integer.toString(subResultSet.getInt("zoom_level")), + Integer.toString(subResultSet.getInt("tile_column")), + Integer.toString(subResultSet.getInt("tile_row")), Integer.toString(height))); + } + } + } + } + } + } + else { + throw new SkipException("Table gpkg_tile_matrix required to perform this test."); + } + } + + /** + * NSG Req 22: The gpkg_tile_matrix table SHALL contain pixel_x_size and pixel_y_size + * column values that differ by a factor of 2 between all adjacent zoom levels for + * each tile pyramid data table per OGC GeoPackage Clause 2.2.3. It SHALL NOT contain + * pixel sizes that vary by irregular intervals or by regular intervals other than a + * factor of 2 between adjacent zoom levels per OGC GeoPackage Clause 3.2.1. + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 22 (pixels sizes factor of 2)") + public void pixelsSizeTests() throws SQLException { + String queryStr = "SELECT table_name FROM gpkg_contents WHERE data_type=\'tiles\';"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + while (resultSet.next()) { + String tableName = ValidateStringInput(resultSet.getString("table_name").trim()); + + String subQueryStr = "SELECT pixel_x_size, pixel_y_size FROM gpkg_tile_matrix " + "WHERE table_name=\'" + + tableName + "\' ORDER BY zoom_level;"; + + try (final Statement subStatement = this.databaseConnection.createStatement(); + final ResultSet subResultSet = subStatement.executeQuery(subQueryStr)) { + double pixelSzX = -1.0D; + double pixelSzY = -1.0D; + + while (subResultSet.next()) { + double lastPixelSzX = pixelSzX; + double lastPixelSzY = pixelSzY; + pixelSzX = subResultSet.getDouble("pixel_x_size"); + pixelSzY = subResultSet.getDouble("pixel_y_size"); + + if (lastPixelSzX < 0.0D) { + lastPixelSzX = pixelSzX * 2.0D; + lastPixelSzY = pixelSzY * 2.0D; + } + + double deltaX = Math.abs((pixelSzX * 2.0D) - lastPixelSzX); + assertTrue((deltaX < this.TOLERANCE), + MessageFormat.format( + "The gpkg_tile_matrix contains an invalid pixel_x_size: {0} for {1}", + String.format("%.10f", pixelSzX), tableName)); + double deltaY = Math.abs((pixelSzY * 2.0D) - lastPixelSzY); + assertTrue((deltaY < this.TOLERANCE), + MessageFormat.format( + "The gpkg_tile_matrix contains an invalid pixel_y_size: {0} for {1}", + String.format("%.10f", pixelSzY), tableName)); + } + } + } + } + } - + /** + * NSG Req 23: The (min_x, min_y, max_x, max_y) values in the gpkg_tile_matrix_set + * table SHALL be the maximum bounds of the CRS specified for the tile pyramid data + * table and SHALL be used to determine the geographic position of each tile in the + * tile pyramid data table. + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 23 (bounding box in gpkg_tile_matrix_set)") + public void boundingBoxTests() throws SQLException { + + if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix_set")) { + + String queryStr = "SELECT table_name, srs_id, min_x, min_y, max_x, max_y FROM gpkg_tile_matrix_set;"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + while (resultSet.next()) { + String srsID = resultSet.getString("srs_id").trim(); + String tableName = resultSet.getString("table_name").trim(); + double[] mbr = findMbrBySrsId(srsID); + + assertTrue((mbr != null), + MessageFormat.format( + "The gpkg_tile_matrix_set contains an invalid CRS definition: {0} for table {1}", + srsID, tableName)); + + double minX = resultSet.getDouble("min_x"); + assertTrue((minX == mbr[0]), MessageFormat.format( + "The gpkg_tile_matrix_set contains an invalid min_x value: {0} for table {1} (should be {2})", + Double.valueOf(minX), tableName, Double.valueOf(mbr[0]))); + + double minY = resultSet.getDouble("min_y"); + assertTrue((minY == mbr[1]), MessageFormat.format( + "The gpkg_tile_matrix_set contains an invalid min_y value: {0} for table {1} (should be {2})", + Double.valueOf(minY), tableName, Double.valueOf(mbr[1]))); + + double maxX = resultSet.getDouble("max_x"); + assertTrue((maxX == mbr[2]), MessageFormat.format( + "The gpkg_tile_matrix_set contains an invalid max_x value: {0} for table {1} (should be {2})", + Double.valueOf(maxX), tableName, Double.valueOf(mbr[2]))); + + double maxY = resultSet.getDouble("max_y"); + assertTrue((maxY == mbr[3]), MessageFormat.format( + "The gpkg_tile_matrix_set contains an invalid max_y value: {0} for table {1} (should be {2})", + Double.valueOf(maxY), tableName, Double.valueOf(mbr[3]))); + } + } + } + else { + throw new SkipException("Table gpkg_tile_matrix_set required to perform this test."); + } + } + + private Collection collectTableNameAndExtents() throws SQLException { + + final Collection tableNamesAndExtents = new LinkedList<>(); + if (DatabaseUtility.doesTableOrViewExist(this.databaseConnection, "gpkg_tile_matrix_set")) { + + String tableNameAndExtentsQuery = "SELECT table_name, zoom_level, tile_width, tile_height FROM gpkg_tile_matrix " + + "WHERE NOT ((tile_width=256) AND (tile_height=256));"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(tableNameAndExtentsQuery)) { + + while (resultSet.next()) { + String tableName = resultSet.getString("table_name"); + int tileWidth = resultSet.getInt("tile_width"); + int tileHeight = resultSet.getInt("tile_height"); + tableNamesAndExtents.add(tableName + ": (tile_width)" + tileWidth + ", (tile_height)" + tileHeight); + } + + } + } + else { + throw new SkipException("Table gpkg_tile_matrix_set required to perform this test."); + } + return tableNamesAndExtents; + } + private double[] findMbrBySrsId(String srsID) { + if (srsID.equals("3395")) { + return new double[] { -20037508.342789244D, -20037508.342789244D, 20037508.342789244D, + 20037508.342789244D }; + } + else if (srsID.equals("5041")) { + return new double[] { -14440759.350252D, -14440759.350252D, 18440759.350252D, 18440759.350252D }; + } + else if (srsID.equals("4326")) { + return new double[] { -180.0D, -90.0D, 180.0D, 90.0D }; + } + return null; + } + + /** + * --- NSG Req 19: Data validity SHALL be assessed against data value constraints + * specified in Table 26 below using a test suite. Data validity MAY be enforced by + * SQL triggers. + * + * --- 19-B: Addresses Table 26 Rows 3-7 (regarding table "gpkg_contents") + * @throws SQLException if access to gpkg failed + */ + @Test(groups = { "NSG" }, description = "NSG Req 19-B (Data Validity: gpkg_contents, tiles)") + public void dataValidity_gpkg_contents() throws SQLException { + String queryStr = "SELECT srs_id,table_name,min_x,min_y,max_x,max_y FROM \'gpkg_contents\' WHERE (data_type=\'tiles\');"; + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery(queryStr)) { + + final Collection invalidMinX = new LinkedList<>(); + final Collection invalidMinY = new LinkedList<>(); + final Collection invalidMaxX = new LinkedList<>(); + final Collection invalidMaxY = new LinkedList<>(); + + while (resultSet.next()) { + final String srsTabNam = resultSet.getString("table_name"); + // test for: Table 26; Row 4 + collectInvalidMinValues(resultSet, invalidMinX, srsTabNam, "min_x"); + // test for: Table 26; Row 5 + collectInvalidMinValues(resultSet, invalidMinY, srsTabNam, "min_y"); + // test for: Table 26; Row 6 + collectInvalidMaxValues(resultSet, invalidMaxX, srsTabNam, "max_x"); + // test for: Table 26; Row 7 + collectInvalidMaxValues(resultSet, invalidMaxY, srsTabNam, "max_y"); + } + + assertTrue(invalidMinX.isEmpty(), + MessageFormat.format( + "The gpkg_contents table contains invalid minimum X bounds values for tables: {0}", + invalidMinX.stream().map(Object::toString).collect(Collectors.joining(", ")))); + assertTrue(invalidMinY.isEmpty(), + MessageFormat.format( + "The gpkg_contents table contains invalid minimum Y bounds values for tables: {0}", + invalidMinY.stream().map(Object::toString).collect(Collectors.joining(", ")))); + assertTrue(invalidMaxX.isEmpty(), + MessageFormat.format( + "The gpkg_contents table contains invalid maximum X bounds values for tables: {0}", + invalidMaxX.stream().map(Object::toString).collect(Collectors.joining(", ")))); + assertTrue(invalidMaxY.isEmpty(), + MessageFormat.format( + "The gpkg_contents table contains invalid maximum Y bounds values for tables: {0}", + invalidMaxY.stream().map(Object::toString).collect(Collectors.joining(", ")))); + } + + } + + private void collectInvalidMaxValues(ResultSet resultSet, Collection invalidValue, String srsTabNam, + String testBoundsColumn) throws SQLException { + if (resultSet.getString(testBoundsColumn) != null) { + double val = resultSet.getDouble(testBoundsColumn); + + double bnd = this.checkTileBounds(ValidateStringInput(srsTabNam), testBoundsColumn); + if (val > bnd) { + invalidValue.add(srsTabNam + ":" + val + ", should be: " + bnd); + } + } + } + + private void collectInvalidMinValues(ResultSet resultSet, Collection invalidValue, String srsTabNam, + String testBoundsColumn) throws SQLException { + if (resultSet.getString(testBoundsColumn) != null) { + double val = resultSet.getDouble(testBoundsColumn); + + double bnd = this.checkTileBounds(ValidateStringInput(srsTabNam), testBoundsColumn); + if (val < bnd) { + invalidValue.add(srsTabNam + ":" + val + ", should be: " + bnd); + } + } + } + + /* + * convenience routine to consistently return specific bounds column as double + */ + private double checkTileBounds(String tableName, String boundsColumn) throws SQLException { + + try (final Statement statement = this.databaseConnection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT " + boundsColumn + + " FROM gpkg_tile_matrix_set WHERE table_name = \'" + tableName + "\';")) { + assertTrue(resultSet.next(), ErrorMessage.format(ErrorMessageKeys.BAD_TILE_MATRIX_SET_TABLE_DEFINITION)); + + return resultSet.getDouble(boundsColumn); + } + } } \ No newline at end of file diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsList.java b/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsList.java index 46c90cd..01e360d 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsList.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsList.java @@ -8,78 +8,75 @@ */ public class CrsList { - private List crsListingList = new ArrayList<>(); + private List crsListingList = new ArrayList<>(); - /** - * Adds a new CrsListing to the list. - * - * @param id - * the srs_id - * @param definition - * the definition - * @param organization_coordsys_id - * the organization_coordsys_id - * @param description - * the description - */ - public void addListing( String id, String definition, String organization_coordsys_id, String description ) { - crsListingList.add( new CrsListing( id, definition, organization_coordsys_id, description ) ); - } + /** + * Adds a new CrsListing to the list. + * @param id the srs_id + * @param definition the definition + * @param organization_coordsys_id the organization_coordsys_id + * @param description the description + */ + public void addListing(String id, String definition, String organization_coordsys_id, String description) { + crsListingList.add(new CrsListing(id, definition, organization_coordsys_id, description)); + } - /** - * @param srsID - * the srsId of the CrsListing to identify, never null - * @return the organization_coordsys_id of the entry with the passed srs_id, null if not available - */ - public String getOrganizationCoordsysIdBySrsId( String srsID ) { - for ( CrsListing crsListing : crsListingList ) { - if ( srsID.equals( crsListing.id ) ) - return crsListing.organization_coordsys_id; - } - return null; - } + /** + * @param srsID the srsId of the CrsListing to identify, never null + * @return the organization_coordsys_id of the entry with the passed srs_id, + * null if not available + */ + public String getOrganizationCoordsysIdBySrsId(String srsID) { + for (CrsListing crsListing : crsListingList) { + if (srsID.equals(crsListing.id)) + return crsListing.organization_coordsys_id; + } + return null; + } - /** - * @param srsID - * the srsId of the CrsListing to identify, never null - * @return the definition of the entry with the passed srs_id, null if not available - */ - public String getDefinitionBySrsId( String srsID ) { - for ( CrsListing crsListing : crsListingList ) { - if ( srsID.equals( crsListing.id ) ) - return crsListing.definition; - } - return null; - } + /** + * @param srsID the srsId of the CrsListing to identify, never null + * @return the definition of the entry with the passed srs_id, null if + * not available + */ + public String getDefinitionBySrsId(String srsID) { + for (CrsListing crsListing : crsListingList) { + if (srsID.equals(crsListing.id)) + return crsListing.definition; + } + return null; + } - /** - * @param srsID - * the srsId of the CrsListing to identify, never null - * @return the description of the entry with the passed srs_id, null if not available - */ - public String getDescriptionBySrsId( String srsID ) { - for ( CrsListing crsListing : crsListingList ) { - if ( srsID.equals( crsListing.id ) ) - return crsListing.description; - } - return null; - } + /** + * @param srsID the srsId of the CrsListing to identify, never null + * @return the description of the entry with the passed srs_id, null if + * not available + */ + public String getDescriptionBySrsId(String srsID) { + for (CrsListing crsListing : crsListingList) { + if (srsID.equals(crsListing.id)) + return crsListing.description; + } + return null; + } - private class CrsListing { - private String id; + private class CrsListing { - private String definition; + private String id; - private String organization_coordsys_id; + private String definition; - private String description; + private String organization_coordsys_id; - public CrsListing( String id, String definition, String organization_coordsys_id, String description ) { - this.id = id; - this.definition = definition; - this.organization_coordsys_id = organization_coordsys_id; - this.description = description; - } - } + private String description; + + public CrsListing(String id, String definition, String organization_coordsys_id, String description) { + this.id = id; + this.definition = definition; + this.organization_coordsys_id = organization_coordsys_id; + this.description = description; + } + + } } diff --git a/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtils.java b/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtils.java index 232d478..e114f9d 100644 --- a/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtils.java +++ b/src/main/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtils.java @@ -17,61 +17,62 @@ */ public class CrsListingUtils { - private static final String NSG_CRS_LISTING = "/org/opengis/cite/gpkg12/nsg/core/NSG_CRS_WKT.xml"; + private static final String NSG_CRS_LISTING = "/org/opengis/cite/gpkg12/nsg/core/NSG_CRS_WKT.xml"; - private CrsListingUtils() { - } + private CrsListingUtils() { + } - /** - * Parses the CRS list. - * - * @return the CrsList with all available rows, may be empty but never null. - */ - public static CrsList parseCrsListing() { - CrsList crsList = new CrsList(); - NodeList rows = openCrsListing(); + /** + * Parses the CRS list. + * @return the CrsList with all available rows, may be empty but never + * null. + */ + public static CrsList parseCrsListing() { + CrsList crsList = new CrsList(); + NodeList rows = openCrsListing(); - for ( int rowIndex = 0; rowIndex < rows.getLength(); rowIndex++ ) { - Element row = (Element) rows.item( rowIndex ); - String id = getXMLElementTextValue( row, "srs_id" ); - String definition = getXMLElementTextValue( row, "definition" ); - String organization_coordsys_id = getXMLElementTextValue( row, "organization_coordsys_id" ); - String description = getXMLElementTextValue( row, "description" ); - crsList.addListing( id, definition, organization_coordsys_id, description ); - } - return crsList; + for (int rowIndex = 0; rowIndex < rows.getLength(); rowIndex++) { + Element row = (Element) rows.item(rowIndex); + String id = getXMLElementTextValue(row, "srs_id"); + String definition = getXMLElementTextValue(row, "definition"); + String organization_coordsys_id = getXMLElementTextValue(row, "organization_coordsys_id"); + String description = getXMLElementTextValue(row, "description"); + crsList.addListing(id, definition, organization_coordsys_id, description); + } + return crsList; - } + } - private static NodeList openCrsListing() { - InputStream crsListing = CrsListingUtils.class.getResourceAsStream( NSG_CRS_LISTING ); - String rootName = "Row"; - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + private static NodeList openCrsListing() { + InputStream crsListing = CrsListingUtils.class.getResourceAsStream(NSG_CRS_LISTING); + String rootName = "Row"; + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - try { - DocumentBuilder ioe = dbf.newDocumentBuilder(); - Document dom = ioe.parse( crsListing ); - if ( dom != null ) { - Element docElems = dom.getDocumentElement(); - if ( docElems != null ) { - return docElems.getElementsByTagName( rootName ); - } - } - } catch ( ParserConfigurationException | SAXException | IOException e ) { - // TODO - // LOG.log( Level.SEVERE, "Could not open CRS listing.", e ); - } + try { + DocumentBuilder ioe = dbf.newDocumentBuilder(); + Document dom = ioe.parse(crsListing); + if (dom != null) { + Element docElems = dom.getDocumentElement(); + if (docElems != null) { + return docElems.getElementsByTagName(rootName); + } + } + } + catch (ParserConfigurationException | SAXException | IOException e) { + // TODO + // LOG.log( Level.SEVERE, "Could not open CRS listing.", e ); + } - return null; - } + return null; + } - private static String getXMLElementTextValue( Element element, String tagName ) { - NodeList nl = element.getElementsByTagName( tagName ); - if ( nl != null && nl.getLength() > 0 ) { - Element el = (Element) nl.item( 0 ); - return el.getFirstChild().getNodeValue(); - } - return null; - } + private static String getXMLElementTextValue(Element element, String tagName) { + NodeList nl = element.getElementsByTagName(tagName); + if (nl != null && nl.getLength() > 0) { + Element el = (Element) nl.item(0); + return el.getFirstChild().getNodeValue(); + } + return null; + } } diff --git a/src/test/java/org/opengis/cite/gpkg12/nsg/VerifyTestNGController.java b/src/test/java/org/opengis/cite/gpkg12/nsg/VerifyTestNGController.java index 7909c4f..5f485b1 100644 --- a/src/test/java/org/opengis/cite/gpkg12/nsg/VerifyTestNGController.java +++ b/src/test/java/org/opengis/cite/gpkg12/nsg/VerifyTestNGController.java @@ -27,68 +27,104 @@ /** * Verifies the results of executing a test run using the main controller * (TestNGController). - * + * */ public class VerifyTestNGController { - private static DocumentBuilder docBuilder; - private Properties testRunProps; - - @BeforeClass - public static void initParser() throws ParserConfigurationException { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setNamespaceAware(true); - dbf.setValidating(false); - dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - docBuilder = dbf.newDocumentBuilder(); - } - - @Before - public void loadDefaultTestRunProperties() throws InvalidPropertiesFormatException, IOException { - this.testRunProps = new Properties(); - this.testRunProps.loadFromXML(getClass().getResourceAsStream("/test-run-props.xml")); - } - - @Test - public void cleanTestRun() throws Exception { - - runTests(ClassLoader.getSystemResource("gpkg/gpkg-test-5208.gpkg"), 0); - runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_spi_nonlinear_webp_elevation.gpkg"), 5); - runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_spatial_index_extension.gpkg"), 4); - runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_no_extensions.gpkg"), 4); - runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_no_extensions_with_gpkg_ogr_contents.gpkg"), 4); - runTests(ClassLoader.getSystemResource("gpkg/empty.gpkg"), 2); - runTests(ClassLoader.getSystemResource("gpkg/states10.gpkg"), 2); - runTests(ClassLoader.getSystemResource("gpkg/bluemarble.gpkg"), 4); - runTests(ClassLoader.getSystemResource("gpkg/gdal_sample.gpkg"), 0); - runTests(ClassLoader.getSystemResource("gpkg/elevation.gpkg"), 5); // These two are the id notnull thing - runTests(ClassLoader.getSystemResource("gpkg/coastline-polyline-hydro-115mil-and-smaller.gpkg"), 3); - runTests(ClassLoader.getSystemResource("gpkg/v12_bad_attributes.gpkg"), 4); // R119 - runTests(ClassLoader.getSystemResource("gpkg/sample1_0.gpkg"), 5); // R77 - runTests(ClassLoader.getSystemResource("gpkg/simple_sewer_features.gpkg"), 5); // This is an invalid 1.0 or 1.1 GPKG - it has an invalid metadata table (md_standard_URI instead of md_standard_uri) - runTests(ClassLoader.getSystemResource("gpkg/sample1_1.gpkg"), 5); // R77 - runTests(ClassLoader.getSystemResource("gpkg/sample1_2.gpkg"), 5); // R77 - runTests(ClassLoader.getSystemResource("gpkg/sample1_2F10.gpkg"), 5); // Default "undefined", R77 - runTests(ClassLoader.getSystemResource("gpkg/geonames_belgium.gpkg"), 6); // lower case data types, R77 - runTests(ClassLoader.getSystemResource("gpkg/haiti-vectors-split.gpkg"), 5); // lower case data types, R77 - runTests(ClassLoader.getSystemResource("gpkg/bentiu_southsudan-osm-20170213.gpkg"), 5); //R5, R29 - } - private void runTests(URL testSubject, int fails) throws Exception { - this.testRunProps.setProperty(TestRunArg.IUT.toString(), testSubject.toURI().toString()); - ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024); - this.testRunProps.storeToXML(outStream, "Integration test"); - - Document testRunArgs = docBuilder.parse(new ByteArrayInputStream(outStream.toByteArray())); - - // set up the test controller and run the tests - TestNGController controller = new TestNGController(); - Source results = controller.doTestRun(testRunArgs); - String xpath = "/testng-results/@failed"; - XdmValue failed = XMLUtils.evaluateXPath2(results, xpath, null); - int numFailed = Integer.parseInt(failed.getUnderlyingValue().getStringValue()); - if (fails != numFailed){ - // Extraneous if allows you to set a breakpoint... - assertEquals(MessageFormat.format("Unexpected number of fail verdicts for file {0}.\nSee {1} for details.", testSubject.toString(), results.getSystemId()), fails, numFailed); - } - } + private static DocumentBuilder docBuilder; + + private Properties testRunProps; + + @BeforeClass + public static void initParser() throws ParserConfigurationException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setValidating(false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + docBuilder = dbf.newDocumentBuilder(); + } + + @Before + public void loadDefaultTestRunProperties() throws InvalidPropertiesFormatException, IOException { + this.testRunProps = new Properties(); + this.testRunProps.loadFromXML(getClass().getResourceAsStream("/test-run-props.xml")); + } + + @Test + public void cleanTestRun() throws Exception { + + runTests(ClassLoader.getSystemResource("gpkg/gpkg-test-5208.gpkg"), 0); + runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_spi_nonlinear_webp_elevation.gpkg"), 5); + runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_spatial_index_extension.gpkg"), 4); + runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_no_extensions.gpkg"), 4); + runTests(ClassLoader.getSystemResource("gpkg/gdal_sample_v1.2_no_extensions_with_gpkg_ogr_contents.gpkg"), 4); + runTests(ClassLoader.getSystemResource("gpkg/empty.gpkg"), 2); + runTests(ClassLoader.getSystemResource("gpkg/states10.gpkg"), 2); + runTests(ClassLoader.getSystemResource("gpkg/bluemarble.gpkg"), 4); + runTests(ClassLoader.getSystemResource("gpkg/gdal_sample.gpkg"), 0); + runTests(ClassLoader.getSystemResource("gpkg/elevation.gpkg"), 5); // These two + // are the id + // notnull + // thing + runTests(ClassLoader.getSystemResource("gpkg/coastline-polyline-hydro-115mil-and-smaller.gpkg"), 3); + runTests(ClassLoader.getSystemResource("gpkg/v12_bad_attributes.gpkg"), 4); // R119 + runTests(ClassLoader.getSystemResource("gpkg/sample1_0.gpkg"), 5); // R77 + runTests(ClassLoader.getSystemResource("gpkg/simple_sewer_features.gpkg"), 5); // This + // is + // an + // invalid + // 1.0 + // or + // 1.1 + // GPKG + // - + // it + // has + // an + // invalid + // metadata + // table + // (md_standard_URI + // instead + // of + // md_standard_uri) + runTests(ClassLoader.getSystemResource("gpkg/sample1_1.gpkg"), 5); // R77 + runTests(ClassLoader.getSystemResource("gpkg/sample1_2.gpkg"), 5); // R77 + runTests(ClassLoader.getSystemResource("gpkg/sample1_2F10.gpkg"), 5); // Default + // "undefined", + // R77 + runTests(ClassLoader.getSystemResource("gpkg/geonames_belgium.gpkg"), 6); // lower + // case + // data + // types, + // R77 + runTests(ClassLoader.getSystemResource("gpkg/haiti-vectors-split.gpkg"), 5); // lower + // case + // data + // types, + // R77 + runTests(ClassLoader.getSystemResource("gpkg/bentiu_southsudan-osm-20170213.gpkg"), 5); // R5, + // R29 + } + + private void runTests(URL testSubject, int fails) throws Exception { + this.testRunProps.setProperty(TestRunArg.IUT.toString(), testSubject.toURI().toString()); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024); + this.testRunProps.storeToXML(outStream, "Integration test"); + + Document testRunArgs = docBuilder.parse(new ByteArrayInputStream(outStream.toByteArray())); + + // set up the test controller and run the tests + TestNGController controller = new TestNGController(); + Source results = controller.doTestRun(testRunArgs); + String xpath = "/testng-results/@failed"; + XdmValue failed = XMLUtils.evaluateXPath2(results, xpath, null); + int numFailed = Integer.parseInt(failed.getUnderlyingValue().getStringValue()); + if (fails != numFailed) { + // Extraneous if allows you to set a breakpoint... + assertEquals(MessageFormat.format("Unexpected number of fail verdicts for file {0}.\nSee {1} for details.", + testSubject.toString(), results.getSystemId()), fails, numFailed); + } + } + } \ No newline at end of file diff --git a/src/test/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTestsTest.java b/src/test/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTestsTest.java index ff24bdb..fc870bc 100644 --- a/src/test/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTestsTest.java +++ b/src/test/java/org/opengis/cite/gpkg12/nsg/metadata/MetadataTestsTest.java @@ -25,89 +25,81 @@ */ public class MetadataTestsTest { - private MetadataTests metadataTests; - - @Before - public void initMetadataTests() - throws SAXException, ParserConfigurationException { - this.metadataTests = new MetadataTests(); - metadataTests.initSchema(); - metadataTests.initDocumentBuilder(); - } - - @Test(expected = AssertionError.class) - public void testWithOneInvalidEntry() - throws Exception { - ResultSet resultSet = mockResultSetWithOneStringEntry(); - List xmlEntries = metadataTests.findXmlEntries( resultSet ); - assertThat( xmlEntries.size(), is( 0 ) ); - - metadataTests.validateXmlEntries( xmlEntries ); - } - - @Test - public void testWithOneValidEntry() - throws Exception { - ResultSet resultSet = mockResultSetWithOneValidEntry(); - List xmlEntries = metadataTests.findXmlEntries( resultSet ); - assertThat( xmlEntries.size(), is( 1 ) ); - - metadataTests.validateXmlEntries( xmlEntries ); - } - - @Test - public void testWithOneInvalidAndOneValidEntry() - throws Exception { - ResultSet resultSet = mockResultSetWithOneInvalidAndOneValidEntry(); - List xmlEntries = metadataTests.findXmlEntries( resultSet ); - assertThat( xmlEntries.size(), is( 1 ) ); - - metadataTests.validateXmlEntries( xmlEntries ); - } - - private ResultSet mockResultSetWithOneStringEntry() - throws SQLException { - ResultSet mock = mock( ResultSet.class ); - when( mock.next() ).thenReturn( true, false ); - when( mock.getString( "metadata" ) ).thenReturn( simpleStringMetadata() ); - return mock; - } - - private ResultSet mockResultSetWithOneValidEntry() - throws SQLException, IOException { - ResultSet mock = mock( ResultSet.class ); - when( mock.next() ).thenReturn( true, false ); - when( mock.getString( "metadata" ) ).thenReturn( validMetadata() ); - return mock; - } - - private ResultSet mockResultSetWithOneInvalidAndOneValidEntry() - throws SQLException, IOException { - ResultSet mock = mock( ResultSet.class ); - when( mock.next() ).thenReturn( true, true, false ); - when( mock.getString( "metadata" ) ).thenReturn( emptyMetadata(), validMetadata() ); - return mock; - } - - private String validMetadata() - throws IOException { - InputStream metadata = getClass().getResourceAsStream( "nmisExampleInstanceShort_ISM6.xml" ); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ( ( nRead = metadata.read( data, 0, data.length ) ) != -1 ) { - buffer.write( data, 0, nRead ); - } - buffer.flush(); - return new String( buffer.toByteArray(), StandardCharsets.UTF_8 ); - } - - private String simpleStringMetadata() { - return "invalid"; - } - - private String emptyMetadata() { - return ""; - } + private MetadataTests metadataTests; + + @Before + public void initMetadataTests() throws SAXException, ParserConfigurationException { + this.metadataTests = new MetadataTests(); + metadataTests.initSchema(); + metadataTests.initDocumentBuilder(); + } + + @Test(expected = AssertionError.class) + public void testWithOneInvalidEntry() throws Exception { + ResultSet resultSet = mockResultSetWithOneStringEntry(); + List xmlEntries = metadataTests.findXmlEntries(resultSet); + assertThat(xmlEntries.size(), is(0)); + + metadataTests.validateXmlEntries(xmlEntries); + } + + @Test + public void testWithOneValidEntry() throws Exception { + ResultSet resultSet = mockResultSetWithOneValidEntry(); + List xmlEntries = metadataTests.findXmlEntries(resultSet); + assertThat(xmlEntries.size(), is(1)); + + metadataTests.validateXmlEntries(xmlEntries); + } + + @Test + public void testWithOneInvalidAndOneValidEntry() throws Exception { + ResultSet resultSet = mockResultSetWithOneInvalidAndOneValidEntry(); + List xmlEntries = metadataTests.findXmlEntries(resultSet); + assertThat(xmlEntries.size(), is(1)); + + metadataTests.validateXmlEntries(xmlEntries); + } + + private ResultSet mockResultSetWithOneStringEntry() throws SQLException { + ResultSet mock = mock(ResultSet.class); + when(mock.next()).thenReturn(true, false); + when(mock.getString("metadata")).thenReturn(simpleStringMetadata()); + return mock; + } + + private ResultSet mockResultSetWithOneValidEntry() throws SQLException, IOException { + ResultSet mock = mock(ResultSet.class); + when(mock.next()).thenReturn(true, false); + when(mock.getString("metadata")).thenReturn(validMetadata()); + return mock; + } + + private ResultSet mockResultSetWithOneInvalidAndOneValidEntry() throws SQLException, IOException { + ResultSet mock = mock(ResultSet.class); + when(mock.next()).thenReturn(true, true, false); + when(mock.getString("metadata")).thenReturn(emptyMetadata(), validMetadata()); + return mock; + } + + private String validMetadata() throws IOException { + InputStream metadata = getClass().getResourceAsStream("nmisExampleInstanceShort_ISM6.xml"); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[1024]; + while ((nRead = metadata.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + return new String(buffer.toByteArray(), StandardCharsets.UTF_8); + } + + private String simpleStringMetadata() { + return "invalid"; + } + + private String emptyMetadata() { + return ""; + } } \ No newline at end of file diff --git a/src/test/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtilsTest.java b/src/test/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtilsTest.java index a294eaf..af0d224 100644 --- a/src/test/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtilsTest.java +++ b/src/test/java/org/opengis/cite/gpkg12/nsg/util/CrsListingUtilsTest.java @@ -12,15 +12,15 @@ */ public class CrsListingUtilsTest { - @Test - public void testParseCrsLIsting() { - CrsList crsList = CrsListingUtils.parseCrsListing(); + @Test + public void testParseCrsLIsting() { + CrsList crsList = CrsListingUtils.parseCrsListing(); - assertThat( crsList, notNullValue() ); - assertThat( crsList.getDefinitionBySrsId( "5041" ).trim(), startsWith( "PROJCRS[\"WGS 84 / UPS North (E,N)" ) ); - assertThat( crsList.getOrganizationCoordsysIdBySrsId( "5042" ), is( "5042" ) ); - assertThat( crsList.getDescriptionBySrsId( "5773" ), - is( "Height surface resulting from the application of the EGM96 geoid model to the WGS 84 ellipsoid. Replaces EGM84 geoid height (CRS code 5798). Replaced by EGM2008 geoid height (CRS code 3855)." ) ); - } + assertThat(crsList, notNullValue()); + assertThat(crsList.getDefinitionBySrsId("5041").trim(), startsWith("PROJCRS[\"WGS 84 / UPS North (E,N)")); + assertThat(crsList.getOrganizationCoordsysIdBySrsId("5042"), is("5042")); + assertThat(crsList.getDescriptionBySrsId("5773"), is( + "Height surface resulting from the application of the EGM96 geoid model to the WGS 84 ellipsoid. Replaces EGM84 geoid height (CRS code 5798). Replaced by EGM2008 geoid height (CRS code 3855).")); + } } \ No newline at end of file