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

com.rgi.geopackage.features.GeoPackageFeatures 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.features;

import com.rgi.common.BoundingBox;
import com.rgi.common.Pair;
import com.rgi.common.util.functional.ThrowingFunction;
import com.rgi.common.util.jdbc.JdbcUtility;
import com.rgi.geopackage.core.GeoPackageCore;
import com.rgi.geopackage.core.SpatialReferenceSystem;
import com.rgi.geopackage.features.geometry.Geometry;
import com.rgi.geopackage.features.geometry.GeometryFactory;
import com.rgi.geopackage.utility.DatabaseUtility;
import com.rgi.geopackage.verification.VerificationIssue;
import com.rgi.geopackage.verification.VerificationLevel;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.sql.Connection;
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.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author Luke Lambert
 *
 */
public class GeoPackageFeatures
{
    /**
     * Constructor
     *
     * @param databaseConnection
     *             The open connection to the database that contains a GeoPackage
     * @param core
     *             Access to GeoPackage's "core" methods
     */
    public GeoPackageFeatures(final Connection databaseConnection, final GeoPackageCore core)
    {
        this.databaseConnection = databaseConnection;
        this.core               = core;
    }

    /**
     * @param verificationLevel
     *             Controls the level of verification testing performed
     * @return the Feature GeoPackage requirements this GeoPackage fails to conform to
     * @throws SQLException
     *             If there is a database error
     */
    public Collection getVerificationIssues(final VerificationLevel verificationLevel) throws SQLException
    {
        return new FeaturesVerifier(this.databaseConnection, verificationLevel).getVerificationIssues();
    }

    /**
     * Creates a user defined features table, and adds a corresponding entry to
     * the content table
     *
     * @param tableName
     *             The name of the features table. The table name must begin
     *             with a letter (A..Z, a..z) or an underscore (_) and may only
     *             be followed by letters, underscores, or numbers, and may not
     *             begin with the prefix "gpkg_"
     * @param identifier
     *             A human-readable identifier (e.g. short name) for the
     *             tableName content
     * @param description
     *             A human-readable description for the tableName content
     * @param boundingBox
     *             Bounding box for all content in tableName
     * @param spatialReferenceSystem
     *             Spatial Reference System (SRS)
     * @param primaryKeyColumnName
     *             Column name for the primary key. The column name must begin
     *             with a letter (A..Z, a..z) or an underscore (_) and may
     *             only be followed by letters, underscores, or numbers
     * @param geometryColumn
     *             Geometry column definition
     * @param columnDefinitions
     *             Definitions of non-geometry columns
     * @return Returns a newly created user defined features table
     * @throws SQLException
     *             throws if the method {@link #getFeatureSet(String)
     *             getFeatureSet} or the method {@link
     *             DatabaseUtility#tableOrViewExists(Connection, String)
     *             tableOrViewExists} or if the database cannot roll back the
     *             changes after a different exception throws will throw an
     *             SQLException
     */
    public FeatureSet addFeatureSet(final String                   tableName,
                                    final String                   identifier,
                                    final String                   description,
                                    final BoundingBox              boundingBox,
                                    final SpatialReferenceSystem   spatialReferenceSystem,
                                    final String                   primaryKeyColumnName,
                                    final GeometryColumnDefinition geometryColumn,
                                    final ColumnDefinition...      columnDefinitions) throws SQLException
    {
        return this.addFeatureSet(tableName,
                                  identifier,
                                  description,
                                  boundingBox,
                                  spatialReferenceSystem,
                                  primaryKeyColumnName,
                                  geometryColumn,
                                  Arrays.asList(columnDefinitions));
    }

    /**
     * Creates a user defined features table, and adds a corresponding entry to
     * the content table
     *
     * @param tableName
     *             The name of the features table. The table name must begin
     *             with a letter (A..Z, a..z) or an underscore (_) and may only
     *             be followed by letters, underscores, or numbers, and may not
     *             begin with the prefix "gpkg_"
     * @param identifier
     *             A human-readable identifier (e.g. short name) for the
     *             tableName content
     * @param description
     *             A human-readable description for the tableName content
     * @param boundingBox
     *             Bounding box for all content in tableName
     * @param spatialReferenceSystem
     *             Spatial Reference System (SRS)
     * @param primaryKeyColumnName
     *             Column name for the primary key. The column name must begin
     *             with a letter (A..Z, a..z) or an underscore (_) and may
     *             only be followed by letters, underscores, or numbers
     * @param geometryColumn
     *             Geometry column definition
     * @param columnDefinitions
     *             Definitions of non-geometry columns
     * @return Returns a newly created user defined features table
     * @throws SQLException
     *             throws if the method {@link #getFeatureSet(String)
     *             getFeatureSet} or the method {@link
     *             DatabaseUtility#tableOrViewExists(Connection, String)
     *             tableOrViewExists} or if the database cannot roll back the
     *             changes after a different exception throws will throw an
     *             SQLException
     */
    public FeatureSet addFeatureSet(final String                       tableName,
                                    final String                       identifier,
                                    final String                       description,
                                    final BoundingBox                  boundingBox,
                                    final SpatialReferenceSystem       spatialReferenceSystem,
                                    final String                       primaryKeyColumnName,
                                    final GeometryColumnDefinition     geometryColumn,
                                    final Collection columnDefinitions) throws SQLException
    {
        DatabaseUtility.validateTableName(tableName);

        if(geometryColumn == null)
        {
            throw new IllegalArgumentException("Geometry column definition name may not be null");
        }

        if(columnDefinitions == null || columnDefinitions.contains(null))
        {
            throw new IllegalArgumentException("Column definitions may not be null");
        }

        if(DatabaseUtility.tableOrViewExists(this.databaseConnection, tableName))
        {
            throw new IllegalArgumentException("A table already exists with this feature set's table name");
        }

        try
        {
            // Create the feature set table
            this.addFeatureTableNoCommit(tableName,
                                         primaryKeyColumnName,
                                         geometryColumn,
                                         columnDefinitions);

            // Add feature set to the content table
            this.core.addContent(tableName,
                                 FeatureSet.FeatureContentType,
                                 identifier,
                                 description,
                                 boundingBox,
                                 spatialReferenceSystem);

            this.addGeometryColumnNoCommit(tableName,
                                           geometryColumn,
                                           spatialReferenceSystem.getIdentifier());

            this.databaseConnection.commit();

            return this.getFeatureSet(tableName);   // TODO this is a lazy way of doing things (carried on from the tiles implementation). There should be a method in core to query for the only information that can't already be obtained in this function - the last_change
        }
        catch(final Throwable th)
        {
            this.databaseConnection.rollback();
            throw th;
        }
    }

    /**
     * Gets the geometry column definition for a specified {@link FeatureSet}
     *
     * @param featureSet
     *             Target feature set
     * @return a {@link GeometryColumn}
     * @throws SQLException
     *             When there is a database error
     */
    public GeometryColumn getGeometryColumn(final FeatureSet featureSet) throws SQLException
    {
        if(featureSet == null)
        {
            throw new IllegalArgumentException("Feature set may not be null");
        }

        final String geometryColumnQuery = String.format("SELECT %s, %s, %s, %s, %s FROM %s WHERE %s = ?",
                                                         "column_name",
                                                         "geometry_type_name",
                                                         "srs_id",
                                                         "z",
                                                         "m",
                                                         GeoPackageFeatures.GeometryColumnsTableName,
                                                         "table_name");

        return JdbcUtility.selectOne(this.databaseConnection,
                                     geometryColumnQuery,
                                     preparedStatement -> preparedStatement.setString(1, featureSet.getTableName()),
                                     resultSet -> new GeometryColumn(featureSet.getTableName(),
                                                                     resultSet.getString(1),                          // geometry column name
                                                                     resultSet.getString(2),                          // geometry column type
                                                                     resultSet.getInt   (3),                          // geometry column srs id
                                                                     ValueRequirement.fromInt(resultSet.getInt(4)),   // z value requirement
                                                                     ValueRequirement.fromInt(resultSet.getInt(5)))); // m value requirement
    }

    /**
     * Gets the non-identifier (primary key) and non-geometry columns
     *
     * @param featureSet
     *             Handle to a feature table
     * @return a collection of {@link Column}s that describe the attributes the feature set contains
     * @throws SQLException
     *             If there is a database error
     */
    public List getAttributeColumns(final FeatureSet featureSet) throws SQLException
    {
        if(featureSet == null)
        {
            throw new IllegalArgumentException("Feature set may not be null");
        }

        try(final Statement statement = GeoPackageFeatures.this.databaseConnection.createStatement())
        {
            //noinspection JDBCExecuteWithNonConstantString
            try(final ResultSet tableInfo = statement.executeQuery(String.format("PRAGMA table_info(%s)", featureSet.getTableName())))
            {
                final List columns = new ArrayList<>();

                while(tableInfo.next())
                {
                    final String name = tableInfo.getString("name");

                    if(!tableInfo.getBoolean("pk") &&                               // We don't want the primary key column
                       !name.equalsIgnoreCase(featureSet.getGeometryColumnName()))  // We also don't want the geometry column
                    {
                            final String type         = tableInfo.getString("type");
                            final String defaultValue = tableInfo.getString("dflt_value");

                            final EnumSet flags = EnumSet.noneOf(ColumnFlag.class);

                            if(tableInfo.getBoolean("notnull"))
                            {
                                flags.add(ColumnFlag.NotNull);
                            }

                            // TODO there are other ColumnFlags that need to be checked here: AutoIncrement, Unique
                            // TODO neither those flags aren't directly available in table_info

                            columns.add(new Column(name,
                                                   type,
                                                   flags,
                                                   null,
                                                   defaultValue));
                    }
                }

                return columns;
            }
        }
    }

    /**
     * Gets a feature set object based on its table name
     *
     * @param featureSetTableName
     *             Name of a feature set table
     * @return Returns a {@link FeatureSet} or null if there isn't with the
     *             supplied table name
     * @throws SQLException
     *             If there is a database error
     */
    @SuppressWarnings("JDBCExecuteWithNonConstantString")
    public FeatureSet getFeatureSet(final String featureSetTableName) throws SQLException
    {
        if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageFeatures.GeometryColumnsTableName))
        {
            return null;
        }

        final String geometryColumnQuery = String.format("SELECT %s FROM %s WHERE %s = ?",
                                                         "column_name",
                                                         GeoPackageFeatures.GeometryColumnsTableName,
                                                         "table_name");

        final String geometryColumnName = JdbcUtility.selectOne(this.databaseConnection,
                                                                geometryColumnQuery,
                                                                preparedStatement -> preparedStatement.setString(1, featureSetTableName),
                                                                resultSet -> resultSet.getString(1)); // geometry column name

        if(geometryColumnName == null) // If the table exists, but isn't an entry in the geometry column table...
        {
            return null;
        }

        try(final Statement statement = GeoPackageFeatures.this.databaseConnection.createStatement())
        {
            try(final ResultSet tableInfo = statement.executeQuery(String.format("PRAGMA table_info(%s)", featureSetTableName)))
            {
                String primaryKeyColumnName = null;

                final Collection attributeColumnNames = new ArrayList<>();

                while(tableInfo.next())
                {
                    final String name = tableInfo.getString("name");

                    if(!name.equalsIgnoreCase(geometryColumnName))
                    {
                        if(tableInfo.getBoolean("pk"))
                        {
                            primaryKeyColumnName = name;
                        }
                        else // non-primary key columns (there can only be one primary key column according to the spec)
                        {
                            attributeColumnNames.add(name);
                        }
                    }
                }

                final String finalPrimaryKeyColumnName = primaryKeyColumnName;

                return this.core.getContent(featureSetTableName,
                                            (tableName,
                                             dataType,
                                             identifier,
                                             description,
                                             lastChange,
                                             minimumX,
                                             minimumY,
                                             maximumX,
                                             maximumY,
                                             spatialReferenceSystem) -> new FeatureSet(tableName,
                                                                                       identifier,
                                                                                       description,
                                                                                       lastChange,
                                                                                       minimumX,
                                                                                       minimumY,
                                                                                       maximumX,
                                                                                       maximumY,
                                                                                       spatialReferenceSystem,
                                                                                       finalPrimaryKeyColumnName,
                                                                                       geometryColumnName,
                                                                                       attributeColumnNames));
            }
        }
    }

    /**
     * Gets all entries in the GeoPackage's contents table with the "features"
     * data_type
     *
     * @return Returns a collection of {@link FeatureSet}s
     * @throws SQLException
     *             throws if the method
     *             {@link #getFeatureSets(SpatialReferenceSystem) getFeatureSets}
     *             throws
     */
    public Collection getFeatureSets() throws SQLException
    {
        return this.getFeatureSets(null);
    }

    /**
     * Gets all entries in the GeoPackage's contents table with the "features"
     * data_type that also match the supplied spatial reference system
     *
     * @param matchingSpatialReferenceSystem
     *            Spatial reference system that returned {@link FeatureSet}s
     *            much refer to
     * @return Returns a collection of {@link FeatureSet}s
     * @throws SQLException
     *             if there's an SQL error
     */
    public Collection getFeatureSets(final SpatialReferenceSystem matchingSpatialReferenceSystem) throws SQLException
    {
        return this.core
                   .getContentTableNames(FeatureSet.FeatureContentType,
                                         matchingSpatialReferenceSystem)
                   .stream()
                   .map((ThrowingFunction)(this::getFeatureSet))
                   .collect(Collectors.toList());
    }

    /**
     * Returns a list of all features that correspond to the given feature set.
     * If a large set of features is anticipated use {@link #visitFeatures} to
     * avoid memory issues
     *
     * @param featureSet
     *             Handle to a feature table
     * @return List of features
     * @throws SQLException
     *             if there is a database error
     * @throws WellKnownBinaryFormatException
     *             Handle to a feature table
     */
    public List getFeatures(final FeatureSet featureSet) throws SQLException, WellKnownBinaryFormatException
    {
        if(featureSet == null)
        {
            throw new IllegalArgumentException("Feature set may not be null");
        }

        final String featureQuery = String.format("SELECT %s, %s%s FROM %s",
                                                  featureSet.getPrimaryKeyColumnName(),
                                                  featureSet.getGeometryColumnName(),
                                                  featureSet.getAttributeColumnNames().isEmpty() ? ""
                                                                                                 : ", " + String.join(", ", featureSet.getAttributeColumnNames()),
                                                  featureSet.getTableName());

        try(final Statement statement = this.databaseConnection.createStatement())
        {
            //noinspection JDBCExecuteWithNonConstantString
            try(final ResultSet resultSet = statement.executeQuery(featureQuery))
            {
                final List results = new ArrayList<>();

                while(resultSet.next())
                {
                    final Map attributes = new HashMap<>();

                    for(final String columnName : featureSet.getAttributeColumnNames())
                    {
                        attributes.put(columnName, resultSet.getObject(columnName));
                    }

                    results.add(new Feature(resultSet.getInt(featureSet.getPrimaryKeyColumnName()),
                                            this.createGeometry(resultSet.getBytes(featureSet.getGeometryColumnName())),
                                            attributes));
                }

                return results;
            }
        }
    }

    /**
     * Gets a {@link Feature} given a geometry column and feature identifier
     *
     * @param featureSet
     *             Feature set containing the requested feature
     * @param featureIdentifier
     *             Identifier for a feature
     * @return a {@link Feature}
     * @throws SQLException
     *             if there is a database error
     * @throws WellKnownBinaryFormatException
     *             if any of the features contain malformed Well Known Binary data
     */
    public Feature getFeature(final FeatureSet featureSet,
                              final int        featureIdentifier) throws SQLException, WellKnownBinaryFormatException
    {
        if(featureSet == null)
        {
            throw new IllegalArgumentException("Feature set may not be null");
        }

        final String featureQuery = String.format("SELECT %s%s FROM %s WHERE %s = ?",
                                                  featureSet.getGeometryColumnName(),
                                                  featureSet.getAttributeColumnNames().isEmpty() ? ""
                                                                                                 : ", " + String.join(", ", featureSet.getAttributeColumnNames()),
                                                  featureSet.getTableName(),
                                                  featureSet.getPrimaryKeyColumnName());

        final Pair> feature = JdbcUtility.selectOne(this.databaseConnection,
                                                                                featureQuery,
                                                                                preparedStatement -> preparedStatement.setInt(1, featureIdentifier),
                                                                                resultSet -> { final Map attributes = new HashMap<>();

                                                                                               for(final String columnName : featureSet.getAttributeColumnNames())
                                                                                               {
                                                                                                   attributes.put(columnName, resultSet.getObject(columnName));
                                                                                               }

                                                                                               return Pair.of(resultSet.getBytes(featureSet.getGeometryColumnName()),
                                                                                                              attributes);
                                                                                             });
        if(feature == null)
        {
            return null;
        }

        return new Feature(featureIdentifier,
                           this.createGeometry(feature.getLeft()),
                           feature.getRight());
    }

    /**
     * Applies a consumer to every feature in a feature set
     *
     * @param featureSet
     *             Handle to a feature table
     * @param featureConsumer
     *             Callback that operates on a single feature
     * @throws SQLException
     *             if there is a database error
     * @throws WellKnownBinaryFormatException
     *             Handle to a feature table
     */
    public void visitFeatures(final FeatureSet        featureSet,
                              final Consumer featureConsumer) throws SQLException, WellKnownBinaryFormatException
    {
        if(featureSet == null)
        {
            throw new IllegalArgumentException("Geometry column may not be null");
        }

        if(featureConsumer == null)
        {
            throw new IllegalArgumentException("Feature consumer may not be null");
        }

        final String featureQuery = String.format("SELECT %s, %s%s FROM %s",
                                                  featureSet.getPrimaryKeyColumnName(),
                                                  featureSet.getGeometryColumnName(),
                                                  featureSet.getAttributeColumnNames().isEmpty() ? ""
                                                                                                 : ", " + String.join(", ", featureSet.getAttributeColumnNames()),
                                                  featureSet.getTableName());

        try(final Statement statement = this.databaseConnection.createStatement())
        {
            //noinspection JDBCExecuteWithNonConstantString
            try(final ResultSet resultSet = statement.executeQuery(featureQuery))
            {
                while(resultSet.next())
                {
                    final Map attributes = new HashMap<>();

                    for(final String columnName : featureSet.getAttributeColumnNames())
                    {
                        attributes.put(columnName, resultSet.getObject(columnName));
                    }

                    featureConsumer.accept(new Feature(resultSet.getInt(featureSet.getPrimaryKeyColumnName()),
                                                       this.createGeometry(resultSet.getBytes(featureSet.getGeometryColumnName())),
                                                       attributes));
                }
            }
        }
    }

    /**
     * Adds a feature to a feature set
     *
     * @param geometryColumn
     *             Geometry column of a feature set
     * @param geometry
     *             Geometry of a feature
     * @param attributeColumnNames
     *             List of attribute column names, specified in the same order as the supplied values
     * @param attributeValues
     *             List of attribute values, specified in the same order as the supplied column names
     * @return a handle to the newly created {@link Feature} object
     * @throws SQLException
     *             if there is a database error
     */
    public Feature addFeature(final GeometryColumn geometryColumn,
                              final Geometry       geometry,
                              final List   attributeColumnNames,
                              final List   attributeValues) throws SQLException
    {
        if(geometryColumn == null)
        {
            throw new IllegalArgumentException("Geometry column may not be null");
        }

        if(geometry == null)
        {
            throw new IllegalArgumentException("Geometry may not be null");
        }

        if(attributeColumnNames == null)
        {
            throw new IllegalArgumentException("Attribute column names may not be null");
        }

        if(attributeValues == null)
        {
            throw new IllegalArgumentException("Attribute values may not be null");
        }

        if(attributeColumnNames.size() != attributeValues.size())
        {
            throw new IllegalArgumentException("The number of attribute column names must match the number of attribute values");
        }

        if(!geometryColumn.getGeometryType()
                          .toUpperCase()
                          .equals(geometry.getGeometryTypeName()))
        {
            throw new IllegalArgumentException("Geometry column may only contain geometries of type " + geometryColumn.getGeometryType().toUpperCase());
        }

        verifyValueRequirements(geometryColumn, geometry);

        final List columnNames = new LinkedList<>(attributeColumnNames);

        columnNames.add(0, geometryColumn.getColumnName());

        final String insertFeatureSql = String.format("INSERT INTO %s (%s) VALUES (%s)",
                                                      geometryColumn.getTableName(),
                                                      String.join(", ", columnNames),
                                                      String.join(", ", Collections.nCopies(columnNames.size(), "?")));

        final int identifier = JdbcUtility.update(this.databaseConnection,
                                                  insertFeatureSql,
                                                  preparedStatement -> { int parameterIndex = 1;

                                                                         final byte[] bytes = createBlob(geometry, geometryColumn.getSpatialReferenceSystemIdentifier());
                                                                         preparedStatement.setBytes(parameterIndex++, bytes);

                                                                         columnNames.remove(0);    // Skip the geometry column

                                                                         for(final Object attributeValue : attributeValues)
                                                                         {
                                                                             preparedStatement.setObject(parameterIndex++, attributeValue);
                                                                         }
                                                                        },
                                                  resultSet -> resultSet.getInt(1));    // New feature identifier

        this.databaseConnection.commit();

        final Map attributes = new HashMap<>(attributeColumnNames.size());

        for(int x = 0; x < attributeColumnNames.size(); ++x)
        {
            attributes.put(attributeColumnNames.get(x),
                           attributeValues     .get(x));
        }

        return new Feature(identifier,
                           geometry,
                           attributes);
    }

    /**
     * Add multiple features to a feature set
     *
     * @param geometryColumn
     *             Geometry column of the target feature set
     * @param attributeColumnNames
     *             A list of columns for which the attribute values are being provided
     * @param features
     *             A collection of geometry/attribute collection pairs. The
     *             attribute collection must have the same number and order for
     *             attributes as specified by the attributeColumns parameter.
     * @throws SQLException
     *             if there is a database error
     */
    public void addFeatures(final GeometryColumn                         geometryColumn,
                            final List                           attributeColumnNames,
                            final Iterable>> features) throws SQLException
    {
        if(geometryColumn == null)
        {
            throw new IllegalArgumentException("Geometry column may not be null");
        }

        if(attributeColumnNames == null)
        {
            throw new IllegalArgumentException("Columns may not be null");
        }

        if(features == null)
        {
            throw new IllegalArgumentException("Values may not be null");
        }

        features.forEach(feature -> { if(feature == null)
                                      {
                                          throw new IllegalArgumentException("Features collection may not contain null features");
                                      }

                                      final Geometry geometry = feature.getLeft();

                                      if(geometry == null)
                                      {
                                          throw new IllegalArgumentException("Features collection may not contain null geometries");
                                      }

                                      final List attributes = feature.getRight();

                                      if(attributes == null)
                                      {
                                          throw new IllegalArgumentException("Feature collection may not have a null set of attributes");
                                      }

                                      if(attributes.size() != attributeColumnNames.size())
                                      {
                                          throw new IllegalArgumentException("Feature attribute collections must match the size of the attribute column name collection");
                                      }

                                      if(!geometryColumn.getGeometryType()
                                                        .toUpperCase()
                                                        .equals(geometry.getGeometryTypeName()))
                                      {
                                          throw new IllegalArgumentException("Geometry column may only contain geometries of type " + geometryColumn.getGeometryType().toUpperCase());
                                      }



                                      verifyValueRequirements(geometryColumn, geometry);
                                    });

        final List columnNames = new LinkedList<>(attributeColumnNames);

        columnNames.add(0, geometryColumn.getColumnName());

        final int columnCount = columnNames.size();

        final String insertFeatureSql = String.format("INSERT INTO %s (%s) VALUES (%s)",
                                                      geometryColumn.getTableName(),
                                                      String.join(", ", columnNames),
                                                      String.join(", ", Collections.nCopies(columnNames.size(), "?")));

        JdbcUtility.update(this.databaseConnection,
                           insertFeatureSql,
                           features,
                           (preparedStatement, feature) -> { final Geometry     geometry   = feature.getLeft();
                                                             final List attributes = feature.getRight();

                                                             preparedStatement.setBytes(1, createBlob(geometry, geometryColumn.getSpatialReferenceSystemIdentifier()));

                                                             for(int parameterIndex = 2; parameterIndex <= columnCount; ++parameterIndex)
                                                             {
                                                                 preparedStatement.setObject(parameterIndex, attributes.get(parameterIndex-2));
                                                             }
                                                           });

        this.databaseConnection.commit();
    }

    /**
     * Associate a geometry factory with a specific geometry type code.
     *
     * @param geometryTypeCode
     *             Code representation of the geometry type. Must be in the
     *             range 0 and 2^32 - 1 (range of a 32 bit unsigned integer)
     *             inclusive
     * @param geometryFactory
     *             Callback that creates a geometry that corresponds to the
     *             geometry type code
     */
    public void registerGeometryFactory(final long            geometryTypeCode,
                                        final GeometryFactory geometryFactory)
    {
        this.wellKnownBinaryFactory.registerGeometryFactory(geometryTypeCode, geometryFactory);
    }

    /**
     * Creates the Geometry Column 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 roll back as a single transaction. * * @throws SQLException * if there is a database error */ protected void createGeometryColumnTableNoCommit() throws SQLException { // Create the geometry column table or view if(!DatabaseUtility.tableOrViewExists(this.databaseConnection, GeoPackageFeatures.GeometryColumnsTableName)) { JdbcUtility.update(this.databaseConnection, getGeometryColumnsCreationSql()); } } /** * Creates an entry in the gpkg_geometry_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 roll back as a single transaction. * * @param tableName * The name of the features table * @param geometryColumn * Definition of a geometry column to be added * @param spatialReferenceSystemIdentifier * Spatial Reference System (SRS) * @throws SQLException * if there is a database error */ protected void addGeometryColumnNoCommit(final String tableName, final GeometryColumnDefinition geometryColumn, final int spatialReferenceSystemIdentifier) throws SQLException { this.createGeometryColumnTableNoCommit(); // Create the feature metadata table final String insertTileMatrix = String.format("INSERT INTO %s (%s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?)", GeoPackageFeatures.GeometryColumnsTableName, "table_name", "column_name", "geometry_type_name", "srs_id", "z", "m"); JdbcUtility.update(this.databaseConnection, insertTileMatrix, preparedStatement -> { preparedStatement.setString(1, tableName); preparedStatement.setString(2, geometryColumn.getName()); preparedStatement.setString(3, geometryColumn.getType()); preparedStatement.setInt (4, spatialReferenceSystemIdentifier); preparedStatement.setInt (5, geometryColumn.getZRequirement().getValue()); preparedStatement.setInt (6, geometryColumn.getMRequirement().getValue()); }); } protected static String getGeometryColumnsCreationSql() { // http://www.geopackage.org/spec/#gpkg_geometry_columns_cols // http://www.geopackage.org/spec/#gpkg_geometry_columns_sql return "CREATE TABLE " + GeoPackageFeatures.GeometryColumnsTableName + '\n' + "(table_name TEXT NOT NULL, -- Name of the table containing the geometry column\n" + " column_name TEXT NOT NULL, -- Name of a column in the feature table that is a Geometry Column\n" + " geometry_type_name TEXT NOT NULL, -- Name from Geometry Type Codes (Core) or Geometry Type Codes (Extension) in Geometry Types (Normative)\n" + " srs_id INTEGER NOT NULL, -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id\n" + " z TINYINT NOT NULL, -- 0: z values prohibited; 1: z values mandatory; 2: z values optional\n" + " m TINYINT NOT NULL, -- 0: m values prohibited; 1: m values mandatory; 2: m values optional\n" + " CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name)," + " CONSTRAINT uk_gc_table_name UNIQUE (table_name)," + " CONSTRAINT fk_gc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents (table_name)," + " CONSTRAINT fk_gc_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id));"; } private Geometry createGeometry(final byte[] geoPackageBinaryBlob) throws WellKnownBinaryFormatException { final BinaryHeader binaryHeader = new BinaryHeader(geoPackageBinaryBlob); // This will throw if the array length is too short to contain a header (or if it's not long enough to contain the envelope type specified) if(binaryHeader.getBinaryType() == BinaryType.Standard) { final int headerByteLength = binaryHeader.getByteSize(); return this.wellKnownBinaryFactory .createGeometry(ByteBuffer.wrap(geoPackageBinaryBlob, headerByteLength, geoPackageBinaryBlob.length - headerByteLength) .asReadOnlyBuffer()); // Minor insurance that geometry extension implementations can't change the buffer } // else, this is an extended binary type. The next 4 bytes are the "extension_code" // http://www.geopackage.org/spec/#_requirement-70 // "... This extension_code SHOULD identify the implementer of the // extension and/or the particular geometry type extension, and SHOULD // be unique." // TODO: read the 4 byte extension code, and look it up in a mapping of known extensions codes/parsers (to be registered by extension implementors) throw new WellKnownBinaryFormatException("Extensions of GeoPackageBinary geometry encoding are not currently supported"); } private static byte[] createBlob(final Geometry geometry, final int spatialReferenceSystemIdentifier) { try(final ByteOutputStream byteOutputStream = new ByteOutputStream()) { // TODO HEADER USES OPTIONS: // FORCE ENVELOPE // FORCE ENDIANNESS BinaryHeader.writeBytes(byteOutputStream, geometry, spatialReferenceSystemIdentifier); byteOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); // TODO make this an option (?) geometry.writeWellKnownBinary(byteOutputStream); return byteOutputStream.array(); } } private static void verifyValueRequirements(final GeometryColumn geometryColumn, final Geometry geometry) { final ValueRequirement zRequirement = geometryColumn.getZRequirement(); final ValueRequirement mRequirement = geometryColumn.getMRequirement(); final boolean hasZ = geometry.hasZ(); final boolean hasM = geometry.hasM(); if((zRequirement == ValueRequirement.Prohibited && hasZ) || (zRequirement == ValueRequirement.Mandatory && !hasZ) || (mRequirement == ValueRequirement.Prohibited && hasM) || (mRequirement == ValueRequirement.Mandatory && !hasM)) { throw new IllegalArgumentException(String.format("Geometry is incompatible with the requirements Z %s, M %s", zRequirement.toString().toLowerCase(), mRequirement.toString().toLowerCase())); } } private void addFeatureTableNoCommit(final String featureTableName, final String primaryKeyColumnName, final GeometryColumnDefinition geometryColumn, final Collection columnDefinitions) throws SQLException { // http://www.geopackage.org/spec/#feature_user_tables // http://www.geopackage.org/spec/#example_feature_table_sql final List columns = new LinkedList<>(columnDefinitions); columns.add(0, new PrimaryKeyColumnDefinition(primaryKeyColumnName)); columns.add(1, geometryColumn); // TODO Move the table-building functionality to the table definition class? final StringBuilder createTableSql = new StringBuilder(); createTableSql.append("CREATE TABLE "); createTableSql.append(featureTableName); createTableSql.append("\n("); for(int columnIndex = 0; columnIndex < columns.size(); ++columnIndex) { final AbstractColumnDefinition column = columns.get(columnIndex); final String comment = column.getComment(); final String defaultValue = column.getDefaultValue().equals(ColumnDefault.None) ? "" : " DEFAULT " + column.getDefaultValue().sqlLiteral(); createTableSql.append(System.lineSeparator()); createTableSql.append(column.getName()); // Verified by AbstractColumnDefinition to not contain SQL injection createTableSql.append(' '); createTableSql.append(column.getType()); // Verified by AbstractColumnDefinition to not contain SQL injection createTableSql.append(column.hasFlag(ColumnFlag.PrimaryKey) ? " PRIMARY KEY" : ""); createTableSql.append(column.hasFlag(ColumnFlag.AutoIncrement) ? " AUTOINCREMENT" : ""); createTableSql.append(column.hasFlag(ColumnFlag.NotNull) ? " NOT NULL" : ""); createTableSql.append(column.hasFlag(ColumnFlag.Unique) ? " UNIQUE" : ""); createTableSql.append(defaultValue); createTableSql.append(columnIndex == columns.size()-1 ? "" : ","); createTableSql.append(comment == null ? "" : " -- " + comment); // Verified by AbstractColumnDefinition to not contain newlines, which I think is the only way injection could work here } createTableSql.append("\n);"); JdbcUtility.update(this.databaseConnection, createTableSql.toString()); } /** * Standard table name of the GeoPackage Geometry Columns table */ public static final String GeometryColumnsTableName = "gpkg_geometry_columns"; private final Connection databaseConnection; private final GeoPackageCore core; private final WellKnownBinaryFactory wellKnownBinaryFactory = new WellKnownBinaryFactory(); }