com.rgi.geopackage.verification.Verifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of swagd Show documentation
Show all versions of swagd Show documentation
SWAGD: Software to Aggregate Geospatial Data
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.verification;
import com.rgi.common.Pair;
import com.rgi.common.util.jdbc.JdbcUtility;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.rgi.geopackage.verification.Assert.assertTrue;
import static java.util.stream.Collectors.toMap;
/**
* @author Luke Lambert
* @author Jenifer Cochran
*/
public class Verifier
{
/**
* Constructor
*
* @param verificationLevel
* Controls the level of verification testing performed
* @param sqliteConnection JDBC connection to the SQLite database
*
*/
public Verifier(final Connection sqliteConnection, final VerificationLevel verificationLevel)
{
if(sqliteConnection == null)
{
throw new IllegalArgumentException("SQLite connection cannot be null");
}
this.sqliteConnection = sqliteConnection;
this.verificationLevel = verificationLevel;
}
/**
* Checks a GeoPackage (via it's {@link Connection}) for violations of the requirements outlined in the standard.
*
* @return Returns the definition for all failed requirements
*/
public Collection getVerificationIssues()
{
return this.getRequirements()
.map(requirementTestMethod -> { try
{
requirementTestMethod.invoke(this);
return null;
}
catch(final InvocationTargetException ex)
{
final Requirement requirement = requirementTestMethod.getAnnotation(Requirement.class);
final Throwable cause = ex.getCause();
if(cause instanceof AssertionError)
{
@SuppressWarnings("CastToConcreteClass")
final AssertionError assertionError = (AssertionError)cause;
return assertionError.getSeverity() == Severity.Skipped ? null
: new VerificationIssue(assertionError.getMessage(),
requirement,
assertionError.getSeverity());
}
return new VerificationIssue(String.format("Unexpected exception thrown when testing requirement %s for GeoPackage verification: %s",
requirement.reference(),
ex.getMessage()),
requirement);
}
catch(final IllegalAccessException ex)
{
// TODO
ex.printStackTrace();
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* @param dataType
* Data type type string
* @return Returns true if dataType is one of the known SQL types or
* matches one of the formatted TEXT or BLOB types
*/
protected static boolean checkDataType(final String dataType)
{
return Verifier.AllowedSqlTypes.contains(dataType) ||
dataType.matches("TEXT\\([0-9]+\\)") ||
dataType.matches("BLOB\\([0-9]+\\)");
}
/**
* @return Returns a stream of methods that are annotated with @Requirement
*/
protected Stream getRequirements()
{
return Stream.of(this.getClass().getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(Requirement.class))
.sorted((method1, method2) -> method1.getAnnotation(Requirement.class)
.reference()
.compareTo(method2.getAnnotation(Requirement.class)
.reference()));
}
/**
* @param table
* Table definition to verify
* @throws AssertionError
* @throws SQLException
*/
protected void verifyTable(final TableDefinition table) throws AssertionError, SQLException
{
this.verifyTable(table.getName(),
table.getColumns(),
table.getForeignKeys(),
table.getGroupUniques());
}
protected void verifyTable(final String tableName,
final Map expectedColumns,
final Set expectedForeinKeys,
final Iterable expectedGroupUniques) throws AssertionError, SQLException
{
this.verifyTableDefinition(tableName);
final Set uniques = this.getUniques(tableName);
this.verifyColumns(tableName,
expectedColumns,
uniques);
this.verifyForeignKeys(tableName,
expectedForeinKeys);
verifyGroupUniques(tableName,
expectedGroupUniques,
uniques);
}
protected void verifyTableDefinition(final String tableName) throws SQLException, AssertionError
{
try(final PreparedStatement statement = this.sqliteConnection.prepareStatement("SELECT sql FROM sqlite_master WHERE (type = 'table' OR type = 'view') AND tbl_name = ?;"))
{
statement.setString(1, tableName);
try(ResultSet gpkgContents = statement.executeQuery())
{
final String sql = gpkgContents.getString("sql");
assertTrue(String.format("The `sql` field must include the %s table SQL Definition.",
tableName),
sql != null,
Severity.Error);
}
}
}
protected void verifyColumns(final String tableName, final Map requiredColumns, final Collection uniques) throws SQLException, AssertionError
{
final Map foundColumns = this.getColumnDefinitions(tableName, uniques);
final Collection errors = new LinkedList<>();
// Make sure the required fields exist in the table
for(final Map.Entry column : requiredColumns.entrySet())
{
if(!foundColumns.containsKey(column.getKey()))
{
errors.add(String.format("required column: %s.%s is missing", tableName, column.getKey()));
continue;
}
final ColumnDefinition columnDefinition = foundColumns.get(column.getKey());
if(columnDefinition != null)
{
// .equals() for ColumnDefinition skips comparing default
// values. It's better to check for functional equivalence
// rather than exact string equality. This avoids issues with
// difference in white space as well as other trivial
// annoyances
if(!columnDefinition.equals(column.getValue()) ||
!this.checkExpressionEquivalence(columnDefinition.getDefaultValue(),
column.getValue().getDefaultValue()))
{
errors.add(String.format("Required column %s is defined as:\n%s\nbut should be:\n%s",
column.getKey(),
columnDefinition.toString(),
column.getValue().toString()));
}
}
}
assertTrue(String.format("Table %s doesn't match the expected table definition in the following ways:\n%s",
tableName,
String.join("\n", errors)),
errors.isEmpty(),
Severity.Error);
}
protected void verifyForeignKeys(final String tableName, final Set requiredForeignKeys) throws AssertionError, SQLException
{
try(final Statement statement = this.sqliteConnection.createStatement())
{
try(final ResultSet fkInfo = statement.executeQuery(String.format("PRAGMA foreign_key_list(%s);", tableName)))
{
final List foundForeignKeys = JdbcUtility.map(fkInfo,
resultSet -> new ForeignKeyDefinition(resultSet.getString("table"),
resultSet.getString("from"),
resultSet.getString("to")));
final Collection missingKeys = new HashSet<>(requiredForeignKeys);
missingKeys.removeAll(foundForeignKeys);
final Collection extraneousKeys = new HashSet<>(foundForeignKeys);
extraneousKeys.removeAll(requiredForeignKeys);
final StringBuilder error = new StringBuilder();
if(!missingKeys.isEmpty())
{
error.append(String.format("The table %s is missing the foreign key constraint(s): \n", tableName));
for(final ForeignKeyDefinition key : missingKeys)
{
error.append(String.format("%s.%s -> %s.%s\n",
tableName,
key.getFromColumnName(),
key.getReferenceTableName(),
key.getToColumnName()));
}
}
if(!extraneousKeys.isEmpty())
{
error.append(String.format("The table %s has extraneous foreign key constraint(s): \n", tableName));
for(final ForeignKeyDefinition key : extraneousKeys)
{
error.append(String.format("%s.%s -> %s.%s\n",
tableName,
key.getFromColumnName(),
key.getReferenceTableName(),
key.getToColumnName()));
}
}
assertTrue(error.toString(),
error.length() == 0,
Severity.Error);
}
catch(final SQLException ignored)
{
// If a table has no foreign keys, executing the query
// PRAGMA foreign_key_list() will throw an
// exception complaining that result set is empty.
// The issue has been posted about it here:
// https://bitbucket.org/xerial/sqlite-jdbc/issue/162/
// If the result set is empty (no foreign keys), there's no
// work to be done. Unfortunately .executeQuery() may throw an
// SQLException for other reasons that may require some
// attention.
}
}
}
protected static void verifyGroupUniques(final String tableName,
final Iterable requiredGroupUniques,
final Collection uniques) throws AssertionError
{
for(final UniqueDefinition groupUnique : requiredGroupUniques)
{
assertTrue(String.format("The table %s is missing the column group unique constraint: (%s)",
tableName,
String.join(", ", groupUnique.getColumnNames())),
uniques.contains(groupUnique),
Severity.Error);
}
}
protected Set getUniques(final String tableName) throws SQLException
{
final Set uniqueDefinitions = new HashSet<>();
final Collection indexNames = JdbcUtility.filterSelect(this.sqliteConnection,
String.format("PRAGMA index_list(%s);", tableName),
null,
resultSet -> resultSet.getBoolean("unique"),
resultSet -> resultSet.getString("name"));
for(final String indexName : indexNames)
{
uniqueDefinitions.add(new UniqueDefinition(JdbcUtility.select(this.sqliteConnection,
String.format("PRAGMA index_info(%s);", indexName),
null,
resultSet -> resultSet.getString("name"))));
}
return uniqueDefinitions;
}
/**
* @return The SQLite connection
*/
protected Connection getSqliteConnection()
{
return this.sqliteConnection;
}
/**
* @return The list of allowed SQL types
*/
protected static List getAllowedSqlTypes()
{
return Collections.unmodifiableList(Verifier.AllowedSqlTypes);
}
private Map getColumnDefinitions(final String tableName, final Collection uniques) throws SQLException
{
return JdbcUtility.select(this.sqliteConnection,
String.format("PRAGMA table_info(%s);", tableName),
null,
resultSet -> { final String columnName = resultSet.getString("name");
return Pair.of(columnName,
new ColumnDefinition(resultSet.getString ("type"),
resultSet.getBoolean("notnull"),
resultSet.getBoolean("pk"),
uniques.stream().anyMatch(unique -> unique.equals(columnName)),
resultSet.getString ("dflt_value")));
})
.stream()
.collect(toMap(Pair::getLeft,
Pair::getRight));
}
private boolean checkExpressionEquivalence(final String expression1,
final String expression2) throws SQLException
{
if((expression1 == null) || (expression2 == null))
{
return (expression1 == null) && (expression2 == null);
}
try(final Statement statement = this.sqliteConnection.createStatement())
{
final String query = String.format("SELECT (%s) = (%s);",
expression1,
expression2);
try(final ResultSet results = statement.executeQuery(query))
{
return results.next() && results.getBoolean(1);
}
}
}
private final Connection sqliteConnection;
protected final VerificationLevel verificationLevel;
private static final List AllowedSqlTypes = Arrays.asList("BOOLEAN", "TINYINT", "SMALLINT", "MEDIUMINT",
"INT", "FLOAT", "DOUBLE", "REAL",
"TEXT", "BLOB", "DATE", "DATETIME",
"GEOMETRY", "POINT", "LINESTRING", "POLYGON",
"MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION",
"INTEGER");
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy