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

com.rgi.geopackage.verification.Verifier 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.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