All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rgi.geopackage.extensions.ExtensionsVerifier Maven / Gradle / Ivy

The newest version!
/* The MIT License (MIT)
 *
 * Copyright (c) 2015 Reinventing Geospatial, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.rgi.geopackage.extensions;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import com.rgi.common.util.jdbc.JdbcUtility;
import com.rgi.common.util.jdbc.ResultSetStream;
import com.rgi.geopackage.utility.DatabaseUtility;
import com.rgi.geopackage.verification.Assert;
import com.rgi.geopackage.verification.AssertionError;
import com.rgi.geopackage.verification.ColumnDefinition;
import com.rgi.geopackage.verification.Requirement;
import com.rgi.geopackage.verification.Severity;
import com.rgi.geopackage.verification.TableDefinition;
import com.rgi.geopackage.verification.UniqueDefinition;
import com.rgi.geopackage.verification.VerificationLevel;
import com.rgi.geopackage.verification.Verifier;

/**
 *
 * @author Jenifer Cochran
 *
 */
public class ExtensionsVerifier extends Verifier
{
    private class ExtensionData
    {
        private ExtensionData(final String tableName,
                              final String columnName,
                              final String extensionName)
        {
            this.tableName     = tableName;
            this.columnName    = columnName;
            this.extensionName = extensionName;
        }

        private final String tableName;
        private final String columnName;
        private final String extensionName;
    }

    private final boolean hasGpkgExtensionsTable;

    private List gpkgExtensionsDataAndColumnName;

    /**
     * Constructor
     *
     * @param verificationLevel
     *             Controls the level of verification testing performed
     * @param sqliteConnection
     *             A connection handle to the database
     * @throws SQLException
     *             if test initialization fails to get information from the
     *             database
     */
    public ExtensionsVerifier(final Connection sqliteConnection, final VerificationLevel verificationLevel) throws SQLException
    {
        super(sqliteConnection, verificationLevel);

        this.hasGpkgExtensionsTable = DatabaseUtility.tableOrViewExists(this.getSqliteConnection(), GeoPackageExtensions.ExtensionsTableName);

        if(this.hasGpkgExtensionsTable)
        {
            final String query = String.format("SELECT table_name, column_name, extension_name FROM %s;", GeoPackageExtensions.ExtensionsTableName);

            try(Statement statement             = this.getSqliteConnection().createStatement();
                ResultSet tableNameColumnNameRS = statement.executeQuery(query))
            {
                this.gpkgExtensionsDataAndColumnName = JdbcUtility.map(tableNameColumnNameRS,
                                                                       resultSet -> new ExtensionData(tableNameColumnNameRS.getString("table_name"),
                                                                                                      tableNameColumnNameRS.getString("column_name"),
                                                                                                      tableNameColumnNameRS.getString("extension_name")));
            }
        }
    }

    /**
     * Requirement 79
     *
     * 
A GeoPackage MAY contain a table or update table view named * gpkg_extensions. If present this table SHALL be defined per clause * 2.5.2.1.1 Table * Definition, GeoPackage * Extensions Table or View Definition (Table or View Name: * gpkg_extensions) and * gpkg_extensions Table Definition SQL. *
* * @throws SQLException throws when various SQLExceptions occur * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 79", text = "A GeoPackage MAY contain a table or updateable view named gpkg_extensions." + " If present this table SHALL be defined per clause 2.5.2.1.1 Table Definition, " + "GeoPackage Extensions Table or View Definition (Table or View Name: gpkg_extensions) " + "and gpkg_extensions Table Definition SQL. ") public void Requirement79() throws AssertionError, SQLException { if(this.hasGpkgExtensionsTable) { this.verifyTable(ExtensionsVerifier.ExtensionsTableDefinition); } } /** * Requirement 80 * *
* Every extension of a GeoPackage SHALL be registered in a corresponding * row in the gpkg_extensions table. The absence of a gpkg_extensions table * or the absence of rows in gpkg_extnsions table SHALL both indicate the * absence of extensions to a GeoPackage. *
*/ @Requirement(reference = "Requirement 80", text = "Every extension of a GeoPackage SHALL be registered in a corresponding row " + "in the gpkg_extensions table. The absence of a gpkg_extensions table or " + "the absence of rows in gpkg_extnsions table SHALL both indicate the absence " + "of extensions to a GeoPackage.") public void Requirement80() { // TODO implement this requirement // Check if it has geometry_columns table // if it does check geometry_type_name, // if in Annex E // if it is not in the extensions table under extension_name = gpkg_geo_, throw assertion Error // else not in annex e // extension name does not begin with gpkg and extension name ends with geom // check master table for rtree% table // check if extension name has gpkg_rtree_index fail if doesn't // check master table for fgti_% // fail if extension_name != gpkg_srs_id_trigger // use Severity.Warning } /** * Requirement 81 * *
Values of the gpkg_extensions table_name * column SHALL reference values in the gpkg_contents * table_name column or be NULL. They SHALL NOT be NULL for * rows where the column_name value is not NULL. *
* * @throws SQLException throws when various SQLExceptions occur * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 81", text = "Values of the gpkg_extensions table_name column SHALL reference values in the " + "gpkg_contents table_name column or be NULL. They SHALL NOT be NULL for rows" + " where the column_name value is not NULL. ") public void Requirement81() throws SQLException, AssertionError { if(this.hasGpkgExtensionsTable) { for(final ExtensionData extensionData : this.gpkgExtensionsDataAndColumnName) { final String columnName = extensionData.columnName; final boolean validEntry = extensionData.tableName == null ? columnName == null : true; // If table name is null then so must column name Assert.assertTrue("The value in table_name can only be null if column_name is also null.", validEntry, Severity.Warning); } // Check that the table_name in GeoPackage Extensions references a table in sqlite master final String query = String.format("SELECT table_name as extensionsTableName "+ "FROM %s "+ "WHERE extensionsTableName NOT IN"+ "(SELECT tbl_name "+ "FROM sqlite_master "+ "WHERE tbl_name = extensionsTableName);", GeoPackageExtensions.ExtensionsTableName); try(Statement stmt2 = this.getSqliteConnection().createStatement(); ResultSet tablesNotInSM = stmt2.executeQuery(query)) { final List nonExistantExtensionsTable = ResultSetStream.getStream(tablesNotInSM) .map(resultSet -> { try { return resultSet.getString("extensionsTableName"); } catch(final SQLException ex) { return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); Assert.assertTrue(String.format("The following table(s) does not exist in the sqlite master table. " + "Either create table following table(s) or delete this entry in %s.\n %s", GeoPackageExtensions.ExtensionsTableName, nonExistantExtensionsTable.stream() .map(table-> String.format("\t%s", table)) .collect(Collectors.joining("\n"))), nonExistantExtensionsTable.isEmpty(), Severity.Warning); } } } /** * Requirement 82
The * column_name column value in a gpkg_extensions * row SHALL be the name of a column in the table specified by the * table_name column value for that row, or be NULL. *
* * @throws SQLException throws when various SQLExceptions occur * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 82", text = "The column_name column value in a gpkg_extensions row SHALL be the name of a column in the table specified by the table_name column value for that row, or be NULL.") public void Requirement82() throws SQLException, AssertionError { if(this.hasGpkgExtensionsTable && !this.gpkgExtensionsDataAndColumnName.isEmpty()) { for(final ExtensionData extensionData : this.gpkgExtensionsDataAndColumnName) { final String columnName = extensionData.columnName; if(extensionData.tableName != null && columnName != null) { final String query = String.format("PRAGMA table_info(%s);", extensionData.tableName); try(final PreparedStatement statement = this.getSqliteConnection().prepareStatement(query); final ResultSet tableInfo = statement.executeQuery()) { final boolean columnExists = ResultSetStream.getStream(tableInfo) .anyMatch(resultSet -> { try { return resultSet.getString("name").equals(columnName); } catch(final SQLException ex) { return false; } }); Assert.assertTrue(String.format("The column %s does not exist in the table %s. Please either add this column to this table or delete the record in %s.", columnName, extensionData.tableName, GeoPackageExtensions.ExtensionsTableName), columnExists, Severity.Warning); } } } } } /** * Requirement 83
Each * extension_name column value in a * gpkg_extensions row SHALL be a unique case sensitive value * of the form <author>_<extension_name> where <author> * indicates the person or organization that developed and maintains the * extension. The valid character set for SHALL be [a-zA-Z0-9]. The * valid character set for <extension_name> SHALL be [a-zA-Z0-9_]. An * extension_name for the "gpkg" author name SHALL be one of * those defined in this encoding standard or in an OGC Best Practices * Document that extends it.
* * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 83", text = "Each extension_name column value in a gpkg_extensions row SHALL be a " + "unique case sensitive value of the form _ " + "where indicates the person or organization that developed " + "and maintains the extension. The valid character set for " + "SHALL be [a-zA-Z0-9]. The valid character set for " + "SHALL be [a-zA-Z0-9_]. An extension_name for the gpkg author name " + "SHALL be one of those defined in this encoding standard or in an OGC " + "Best Practices Document that extends it.") public void Requirement83() throws AssertionError { if(this.hasGpkgExtensionsTable) { final Set invalidExtensionNames = this.gpkgExtensionsDataAndColumnName.stream() .map(extensionData -> extensionData.extensionName) .filter(name -> { if(name == null) { return true; } final String author[] = name.split("_", 2); return author.length != 2 || (author[0].matches("gpkg") && !isRegisteredExtension(name)) || !author[0].matches("[a-zA-Z0-9]+") || !author[1].matches("[a-zA-Z0-9_]+"); }) .collect(Collectors.toSet()); Assert.assertTrue(String.format("The following extension_name(s) are invalid: \n%s", invalidExtensionNames.stream() .map(extensionName -> { if(extensionName.isEmpty()) { return "\t"; } return String.format("\t%s", extensionName); }) .filter(Objects::nonNull) .collect(Collectors.joining(", "))), invalidExtensionNames.isEmpty(), Severity.Warning); } } /** * Requirement 84
The definition * column value in a gpkg_extensions row SHALL contain or * reference the text that results from documenting an extension by filling * out the GeoPackage Extension Template in GeoPackage * Extension Template (Normative).
* * @throws SQLException throws when various SQLExceptions occur * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 84", text = "The definition column value in a gpkg_extensions row SHALL " + "contain or reference the text that results from documenting " + "an extension by filling out the GeoPackage Extension Template " + "in GeoPackage Extension Template (Normative).") public void Requirement84() throws SQLException, AssertionError { if(this.hasGpkgExtensionsTable) { final String query = String.format("SELECT table_name " + "FROM %s " + "WHERE definition NOT LIKE '%s' " + "AND definition NOT LIKE '%s' " + "AND definition NOT LIKE '%s' " + "AND definition NOT LIKE '%s';", GeoPackageExtensions.ExtensionsTableName, "Annex%", "http%", "mailto%", "Extension Title%"); try(Statement statement = this.getSqliteConnection().createStatement(); ResultSet invalidDefinitionValues = statement.executeQuery(query)) { final List invalidDefinitions = ResultSetStream.getStream(invalidDefinitionValues) .map(resultSet -> { try { return resultSet.getString("table_name"); } catch(final SQLException ex) { return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); Assert.assertTrue(String.format("The following table_name values in %s table have invalid values for the definition column: %s.", GeoPackageExtensions.ExtensionsTableName, invalidDefinitions.stream() .collect(Collectors.joining(", "))), invalidDefinitions.isEmpty(), Severity.Warning); } } } /** * Requirement 85 * *
* The scope column value in a gpkg_extensions row SHALL be * lowercase "read-write" for an extension that affects both readers and * writers, or "write-only" for an extension that affects only writers. *
* @throws SQLException throws when various SQLExceptions occur * @throws AssertionError throws when the GeoPackage Fails to meet this requirement */ @Requirement(reference = "Requirement 85", text = "The scope column value in a gpkg_extensions row SHALL be lowercase \"read-write\" for an extension that affects both readers and writers, or \"write-only\" for an extension that affects only writers.") public void Requirement85() throws SQLException, AssertionError { if(this.hasGpkgExtensionsTable) { final String query = String.format("SELECT scope FROM %s WHERE scope != 'read-write' AND scope != 'write-only'", GeoPackageExtensions.ExtensionsTableName); try(Statement statement = this.getSqliteConnection().createStatement(); ResultSet invalidScopeValues = statement.executeQuery(query)) { final List invalidScope = ResultSetStream.getStream(invalidScopeValues) .map(resultSet -> { try { return resultSet.getString("scope"); } catch(final SQLException ex) { return null; } }) .filter(Objects::nonNull) .collect(Collectors.toList()); Assert.assertTrue(String.format("There is(are) value(s) in the column scope in %s table that is not 'read-write' or 'write-only' in all lowercase letters. The following values are incorrect: %s", GeoPackageExtensions.ExtensionsTableName, invalidScope.stream() .collect(Collectors.joining(", "))), invalidScope.isEmpty(), Severity.Warning); } } } private static boolean isRegisteredExtension(final String extensionName) { return RegisteredExtensions.contains(extensionName); } private static final TableDefinition ExtensionsTableDefinition; private static final List RegisteredExtensions; static { final Map extensionsTableColumns = new HashMap<>(); extensionsTableColumns.put("table_name", new ColumnDefinition("TEXT", false, false, false, null)); extensionsTableColumns.put("column_name", new ColumnDefinition("TEXT", false, false, false, null)); extensionsTableColumns.put("extension_name", new ColumnDefinition("TEXT", true, false, false, null)); extensionsTableColumns.put("definition", new ColumnDefinition("TEXT", true, false, false, null)); extensionsTableColumns.put("scope", new ColumnDefinition("TEXT", true, false, false, null)); ExtensionsTableDefinition = new TableDefinition(GeoPackageExtensions.ExtensionsTableName, extensionsTableColumns, Collections.emptySet(), new HashSet<>(Arrays.asList(new UniqueDefinition("table_name", "column_name", "extension_name")))); RegisteredExtensions = Arrays.asList("gpkg_zoom_other","gpkg_webp", "gpkg_geometry_columns", "gpkg_rtree_index","gpkg_geometry_type_trigger", "gpkg_srs_id_trigger"); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy