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

com.rgi.geopackage.schema.GeoPackageSchema 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.schema;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.activation.MimeType;

import com.rgi.common.util.jdbc.ResultSetStream;
import com.rgi.geopackage.core.Content;
import com.rgi.geopackage.utility.DatabaseUtility;
import com.rgi.geopackage.utility.SelectBuilder;
import com.rgi.geopackage.verification.VerificationIssue;
import com.rgi.geopackage.verification.VerificationLevel;

/**
 * @author Luke Lambert
 *
 */
public class GeoPackageSchema
{
    /**
     * Constructor
     *
     * @param databaseConnection
     *             The open connection to the database that contains a GeoPackage
     */
    public GeoPackageSchema(final Connection databaseConnection)
    {
        this.databaseConnection = databaseConnection;
    }

    /**
     * Schema requirements this GeoPackage failed to meet
     *
     * @param verificationLevel
     *             Controls the level of verification testing performed
     * @return The schema GeoPackage requirements this GeoPackage fails to conform to
     * @throws SQLException throws if the method {@link SchemaVerifier#SchemaVerifier} throws an SQLException
     */
    public Collection getVerificationIssues(final VerificationLevel verificationLevel) throws SQLException
    {
        return new SchemaVerifier(this.databaseConnection, verificationLevel).getVerificationIssues();
    }

    /**
     * Add an entry to the geopackage_data_columns table
     *
     * @param table
     *            Content table
     * @param columnName
     *            Name of the table column
     * @param name
     *            A human-readable identifier (e.g. short name) for the
     *            columnName content
     * @param title
     *            A human-readable formal title for the columnName content
     * @param description
     *            A human-readable description for the tableName content
     * @param mimeType
     *            MIME type of columnName if BLOB type, or NULL for other
     *            types
     * @param constraintName
     *            Case sensitive column value constraint name specified by
     *            reference to gpkg_data_column_constraints.constraint name
     * @return Returns the newly added {@link DataColumn} object
     * @throws SQLException
     *             throws if unable to create the Data Columns Table and unable
     *             to insert values into the Data Columns Table along with other
     *             various SQLExceptions
     */
    public DataColumn addDataColumn(final Content  table,
                                    final String   columnName,
                                    final String   name,
                                    final String   title,
                                    final String   description,
                                    final MimeType mimeType,
                                    final String   constraintName) throws SQLException
    {
        if(table == null)
        {
            throw new IllegalArgumentException("Table may not be null");
        }

        if(columnName == null || columnName.isEmpty())
        {
            throw new IllegalArgumentException("Column name may not be null or empty");
        }

        // TODO check to make sure the table exists and that the column belongs to that table

        final DataColumn existingDataColumn = this.getDataColumn(table, columnName);

        if(existingDataColumn != null)
        {
            return existingDataColumn;
        }

        try
        {
            this.createDataColumnsTableNoCommit();  // Create the data columns table if it doesn't exist

            final String insertDataColumnSql = String.format("INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?, ?)",
                                                             GeoPackageSchema.DataColumnsTableName,
                                                             "table_name",
                                                             "column_name",
                                                             "name",
                                                             "title",
                                                             "description",
                                                             "mime_type",
                                                             "constraint_name");

            try(PreparedStatement preparedStatement = this.databaseConnection.prepareStatement(insertDataColumnSql))
            {
                preparedStatement.setString(1, table.getTableName());
                preparedStatement.setString(2, columnName);
                preparedStatement.setString(3, name);
                preparedStatement.setString(4, title);
                preparedStatement.setString(5, description);
                preparedStatement.setObject(6, mimeType.toString());
                preparedStatement.setObject(7, constraintName);

                preparedStatement.executeUpdate();
            }

            final DataColumn dataColumn = new DataColumn(table.getTableName(),
                                                         columnName,
                                                         name,
                                                         title,
                                                         description,
                                                         mimeType.toString(),
                                                         constraintName);
            this.databaseConnection.commit();

            return dataColumn;
        }
        catch(final Exception ex)
        {
            this.databaseConnection.rollback();
            throw ex;
        }

    }

    /**
     * Adds an entry to the GeoPackage's data column constraints table
     *
     * @param constraintName
     *            Case sensitive name of constraint
     * @param constraintType
     *            Constraint type
     * @param value
     *            Specified case sensitive value for enum or glob or
     *            null for range constraintType
     * @param minimum
     *            Minimum value for "range" or null for "enum" or
     *            "glob" constraintType
     * @param minimumIsInclusive
     *            false if minimum value is exclusive,
     *            true if minimum value is inclusive, or
     *            null for "enum" or "glob" constraintType
     * @param maximum
     *            Maximum value for "range" or null for "enum" or
     *            "glob" constraintType
     * @param maximumIsInclusive
     *            false if maximum value is exclusive,
     *            true if maximum value is inclusive, or
     *            null for "enum" or "glob" constraintType
     * @param description
     *            For ranges and globs, describes the constraint; for enums,
     *            describes the enum value.
     * @return Returns the newly added {@link DataColumnConstraint} object
     * @throws SQLException
     *             throws if unable to create the Data Columns Constraints Table
     *             and unable to insert values into the Data Columns Constraints
     *             Table along with other various SQLExceptions
     */
    public DataColumnConstraint addDataColumnConstraint(final String  constraintName,
                                                        final Type    constraintType,
                                                        final String  value,
                                                        final Number  minimum,
                                                        final Boolean minimumIsInclusive,
                                                        final Number  maximum,
                                                        final Boolean maximumIsInclusive,
                                                        final String  description) throws SQLException
    {
        if(constraintName == null || constraintName.isEmpty())
        {
            throw new IllegalArgumentException("Constraint name may not be null or empty");
        }

        if(constraintType == null)
        {
            throw new IllegalArgumentException("Constraint type may not be null");
        }

        if(constraintType == Type.Range && value != null)
        {
            throw new IllegalArgumentException("Value must be null for range constraints types");   // Requirement 63
        }

        if(constraintType == Type.Range &&
           (minimum == null ||
            maximum == null ||
            minimum.doubleValue() >= maximum.doubleValue()))
        {
            throw new IllegalArgumentException("Minimum and maximum may not be null, and minimum must be strictly less than maximum for range constraints types");   // Requirement 64
        }

        if(constraintType == Type.Range &&
           (minimumIsInclusive == null ||
            maximumIsInclusive == null))
        {
            throw new IllegalArgumentException("Inclusivity parameters may not be null, for range constraints types");   // Requirement 65
        }

        if((constraintType == Type.Enum ||
            constraintType == Type.Glob) &&
           (minimum            != null ||
            maximum            != null ||
            minimumIsInclusive != null ||
            maximumIsInclusive != null))
        {
            throw new IllegalArgumentException("Minimum, maximum and inclusivity parameters must be null for enum and glob constraint types");  // Requirement 66
        }

        if((constraintType == Type.Enum ||
            constraintType == Type.Glob) &&
           value == null)
        {
            throw new IllegalArgumentException("Value may not be null for enum and glob constraint types");  // Requirement 67
        }

        final DataColumnConstraint existingDataColumnConstraint = this.getDataColumnConstraint(constraintName,
                                                                                               constraintType,
                                                                                               value);

        if(existingDataColumnConstraint != null)
        {
            return existingDataColumnConstraint;
        }

        try
        {
            this.createDataColumnConstraintsTableNoCommit();  // Create the data column constraints table if it doesn't exist

            final String insertDataColumnSql = String.format("INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                                                             GeoPackageSchema.DataColumnConstraintsTableName,
                                                             "constraint_name",
                                                             "constraint_type",
                                                             "value",
                                                             "min",
                                                             "minIsInclusive",
                                                             "max",
                                                             "maxIsInclusive",
                                                             "description");

            try(PreparedStatement preparedStatement = this.databaseConnection.prepareStatement(insertDataColumnSql))
            {
                preparedStatement.setString (1, constraintName);
                preparedStatement.setString (2, constraintType.toString());
                preparedStatement.setString (3, value);
                preparedStatement.setObject (4, minimum);
                preparedStatement.setObject (5, minimumIsInclusive);
                preparedStatement.setObject (6, maximum);
                preparedStatement.setObject (7, maximumIsInclusive);
                preparedStatement.setObject (8, description);

                preparedStatement.executeUpdate();
            }

            final DataColumnConstraint dataColumnConstraint = new DataColumnConstraint(constraintName,
                                                                                       constraintType.toString(),
                                                                                       value,
                                                                                       minimum,
                                                                                       minimumIsInclusive,
                                                                                       maximum,
                                                                                       maximumIsInclusive,
                                                                                       description);
            this.databaseConnection.commit();

            return dataColumnConstraint;
        }
        catch(final Exception ex)
        {
            this.databaseConnection.rollback();
            throw ex;
        }
    }

    /**
     * Gets an entry in the GeoPackage data column table that matches the
     * supplied criteria
     *
     * @param table
     *            Content table
     * @param columnName
     *            Name of the table column
     * @return Returns the a {@link DataColumn} that matches the supplied
     *         criteria, or null if there isn't a match
     * @throws SQLException
     *             throws if the method
     *             {@link DatabaseUtility#tableOrViewExists(Connection, String)}
     *             or if other various SQLExceptions occur
     */
    public DataColumn getDataColumn(final Content table, final String columnName) throws SQLException
    {
        if(table == null)
        {
            throw new IllegalArgumentException("Content table may not be null");
        }

        if(columnName == null || columnName.isEmpty())
        {
            throw new IllegalArgumentException("Column name may not be null or empty");
        }

        if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnsTableName))
        {
            return null;
        }

        final String dataColumnQuerySql = String.format("SELECT %s, %s, %s, %s, %s FROM %s WHERE %s = ? AND %s = ? LIMIT 1;",
                                                        "name",
                                                        "title",
                                                        "description",
                                                        "mime_type",
                                                        "constraint_name",
                                                        GeoPackageSchema.DataColumnsTableName,
                                                        "table_name",
                                                        "column_name");


        try(PreparedStatement preparedStatement = this.databaseConnection.prepareStatement(dataColumnQuerySql))
        {
            preparedStatement.setString(1, table.getTableName());
            preparedStatement.setString(2, columnName);

            try(ResultSet result = preparedStatement.executeQuery())
            {
                if(result.isBeforeFirst())
                {
                    return new DataColumn(table.getTableName(),
                                          columnName,
                                          result.getString(1),  // name
                                          result.getString(2),  // title
                                          result.getString(3),  // description
                                          result.getString(4),  // mime type
                                          result.getString(5)); // constraint name
                }
            }

            return null;
        }
    }

    /**
     * Gets an entry in the GeoPackage data column constraints table that
     * matches the supplied criteria
     *
     * @param constraintName
     *            Case sensitive name of constraint
     * @param constraintType
     *            Constraint type
     * @param value
     *            Specified case sensitive value for enum or glob or
     *            null for range constraintType
     * @return Returns the a {@link DataColumnConstraint} that matches the
     *         supplied criteria, or null if there isn't a match
     * @throws SQLException
     *             throws if the method
     *             {@link DatabaseUtility#tableOrViewExists(Connection, String)}
     *             or if other various SQLExceptions occur
     */
    public DataColumnConstraint getDataColumnConstraint(final String  constraintName,
                                                        final Type    constraintType,
                                                        final String  value) throws SQLException
    {
        if(constraintName == null || constraintName.isEmpty())
        {
            throw new IllegalArgumentException("Constraint name may not be null or empty");
        }

        if(constraintType == null)
        {
            throw new IllegalArgumentException("Constraint type may not be null");
        }

        if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnConstraintsTableName))
        {
            return null;
        }

        try(final SelectBuilder selectStatement = new SelectBuilder(this.databaseConnection,
                                                                    GeoPackageSchema.DataColumnConstraintsTableName,
                                                                    Arrays.asList("min",
                                                                                  "minIsInclusive",
                                                                                  "max",
                                                                                  "maxIsInclusive",
                                                                                  "description"),
                                                                    Arrays.asList(new AbstractMap.SimpleImmutableEntry<>("constraint_name", constraintName),
                                                                                  new AbstractMap.SimpleImmutableEntry<>("constraint_type", constraintType),
                                                                                  new AbstractMap.SimpleImmutableEntry<>("value",           value)));
            final ResultSet result = selectStatement.executeQuery())
        {
            if(result.isBeforeFirst())
            {
                return new DataColumnConstraint(constraintName,
                                                constraintType.toString(),
                                                value,
                                                (Number)  result.getObject(1),   // minimum
                                                          result.getBoolean(2),  // minimum is inclusive
                                                (Number)  result.getObject(3),   // maximum
                                                          result.getBoolean(4),  // maximum is inclusive
                                                          result.getString(5));  // description
            }
        }

        return null;
    }

    /**
     * Gets every entry in the GeoPackage data columns table
     *
     * @return Returns a collection of {@link DataColumn} objects that
     *         represents all of the entries in the GeoPackage data columns
     *         table
     * @throws SQLException
     *             throws if the method
     *             {@link DatabaseUtility#tableOrViewExists(Connection, String)}
     *             or if other various SQLExceptions occur
     */
    public Collection getDataColumns() throws SQLException
    {
        if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnsTableName))
        {
            return Collections.emptyList();
        }

        final String dataColumnQuerySql = String.format("SELECT %s, %s, %s, %s, %s, %s, %s FROM %s;",
                                                        "table_name",
                                                        "column_name",
                                                        "name",
                                                        "title",
                                                        "description",
                                                        "mime_type",
                                                        "constraint_name",
                                                        GeoPackageSchema.DataColumnsTableName);


        try(PreparedStatement preparedStatement = this.databaseConnection.prepareStatement(dataColumnQuerySql);
            ResultSet         results           = preparedStatement.executeQuery())
        {
            return ResultSetStream.getStream(results)
                                  .map(result -> { try
                                                   {
                                                       return new DataColumn(result.getString(1),  // table name
                                                                             result.getString(2),  // column name
                                                                             result.getString(3),  // name
                                                                             result.getString(4),  // title
                                                                             result.getString(5),  // description
                                                                             result.getString(6),  // mime type
                                                                             result.getString(7)); // constraint name
                                                   }
                                                   catch(final SQLException ex)
                                                   {
                                                       return null;
                                                   }
                                                 })
                                  .filter(Objects::nonNull)
                                  .collect(Collectors.toList());
        }
    }

    /**
     * Gets every entry in the GeoPackage data column constraints table
     *
     * @return Returns a collection of {@link DataColumnConstraint} objects that
     *         represents all of the entries in the GeoPackage data column
     *         constraints table
     * @throws SQLException
     *             throws if the method
     *             {@link DatabaseUtility#tableOrViewExists(Connection, String)}
     *             or if other various SQLExceptions occur
     */
    public Collection getDataColumnConstraints() throws SQLException
    {
        if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnConstraintsTableName))
        {
            return Collections.emptyList();
        }

        final String dataColumnConstraintsQuerySql = String.format("SELECT %s, %s, %s, %s, %s, %s, %s, %s FROM %s;",
                                                                   "constraint_name",
                                                                   "constraint_type",
                                                                   "value",
                                                                   "min",
                                                                   "minIsInclusive",
                                                                   "max",
                                                                   "maxIsInclusive",
                                                                   "description",
                                                                   GeoPackageSchema.DataColumnConstraintsTableName);


        try(PreparedStatement preparedStatement = this.databaseConnection.prepareStatement(dataColumnConstraintsQuerySql);
            ResultSet         results           = preparedStatement.executeQuery())
        {
            return ResultSetStream.getStream(results)
                                  .map(result -> { try
                                                   {
                                                       return new DataColumnConstraint(         result.getString(1),  // constraint name
                                                                                                result.getString(2),  // constraint type
                                                                                                result.getString(3),  // value
                                                                                       (Number) result.getObject(4),  // minimum
                                                                                  cast((Integer)result.getObject(5)), // minimum is inclusive
                                                                                       (Number) result.getObject(6),  // maximum
                                                                                  cast((Integer)result.getObject(7)), // maximum is inclusive
                                                                                                result.getString(8)); // description
                                                   }
                                                   catch(final SQLException ex)
                                                   {
                                                       return null;
                                                   }
                                                 })
                                  .filter(Objects::nonNull)
                                  .collect(Collectors.toList());
        }
    }

    private static Boolean cast(final Integer integer)
    {
        return integer == null ? null : integer != 0;
    }


    /**
     * Creates the GeoPackage data columns table
     * 
*
* **WARNING** this does not do a database commit. It is expected * that this transaction will always be paired with others that need to be * committed or rollback as a single transaction. * * @throws SQLException */ protected void createDataColumnsTableNoCommit() throws SQLException { // Create the data columns table if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnsTableName)) { try(Statement statement = this.databaseConnection.createStatement()) { statement.executeUpdate(this.getDataColumnsTableCreationSql()); } } } /** * Creates the GeoPackage data column constraints table *
*
* **WARNING** this does not do a database commit. It is expected * that this transaction will always be paired with others that need to be * committed or rollback as a single transaction. * * @throws SQLException */ protected void createDataColumnConstraintsTableNoCommit() throws SQLException { // Create the data column constraints table if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageSchema.DataColumnConstraintsTableName)) { try(Statement statement = this.databaseConnection.createStatement()) { statement.executeUpdate(this.getDataColumnConstraintsTableCreationSql()); } } } @SuppressWarnings("static-method") protected String getDataColumnsTableCreationSql() { // http://www.geopackage.org/spec/#gpkg_data_columns_cols // http://www.geopackage.org/spec/#gpkg_data_columns_sql return "CREATE TABLE " + GeoPackageSchema.DataColumnsTableName + "\n" + "(table_name TEXT NOT NULL, -- Name of the tiles or feature table\n" + " column_name TEXT NOT NULL, -- Name of the table column\n" + " name TEXT, -- A human-readable identifier (e.g. short name) for the columnName content\n" + " title TEXT, -- A human-readable formal title for the columnName content\n" + " description TEXT, -- A human-readable description for the tableName content\n" + " mime_type TEXT, -- MIME type of columnName if BLOB type, or NULL for other types\n" + " constraint_name TEXT, -- Case sensitive column value constraint name specified by reference to gpkg_data_column_constraints.constraint name\n" + " CONSTRAINT pk_gdc PRIMARY KEY (table_name, column_name),\n" + " CONSTRAINT fk_gdc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name));"; } @SuppressWarnings("static-method") protected String getDataColumnConstraintsTableCreationSql() { // http://www.geopackage.org/spec/#gpkg_data_column_constraints_cols // http://www.geopackage.org/spec/#gpkg_data_column_constraints_sql return "CREATE TABLE " + GeoPackageSchema.DataColumnConstraintsTableName + "\n" + "(constraint_name TEXT NOT NULL, -- Case sensitive name of constraint\n" + " constraint_type TEXT NOT NULL, -- Lowercase type name of constraint: 'range', 'enum', or 'glob'\n" + " value TEXT, -- Specified case sensitive value for enum or glob or null for range constraintType\n" + " min NUMERIC, -- Minimum value for 'range' or null for 'enum' or 'glob' constraintType\n" + " minIsInclusive BOOLEAN, -- false if minimum value is exclusive, or true if minimum value is inclusive\n" + " max NUMERIC, -- Maximum value for 'range' or null for 'enum' or 'glob' constraintType\n" + " maxIsInclusive BOOLEAN, -- false if maximum value is exclusive, or true if maximum value is inclusive\n" + " description TEXT, -- For ranges and globs, describes the constraint; for enums, describes the enum value.\n" + " CONSTRAINT gdcc_ntv UNIQUE (constraint_name, constraint_type, value));"; } private final Connection databaseConnection; /** * The name of the Data Columns Table "gpkg_data_columns" * http://www.geopackage.org/spec/#_data_columns */ public final static String DataColumnsTableName = "gpkg_data_columns"; /** * The name of the Data Column Constraints Table "gpkg_data_column_constraints" * http://www.geopackage.org/spec/#_data_columns_constraints */ public final static String DataColumnConstraintsTableName = "gpkg_data_column_constraints"; }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy