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

com.rgi.geopackage.utility.DatabaseUtility 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.utility;

import com.rgi.common.util.jdbc.JdbcUtility;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * @author Luke Lambert
 *
 */
public class DatabaseUtility
{
    /**
     * @param sqliteFile
     *            the GeoPackag file
     * @return the String representation of the sqliteVersion
     * @throws IOException
     *             throws when the FileDoes not exist or unable to seek in the
     *             File to read the SQLite Version
     */
    public static DatabaseVersion getSqliteVersion(final File sqliteFile) throws IOException
    {
        if(sqliteFile == null)
        {
            throw new IllegalArgumentException("File cannot be null");
        }

        if(!sqliteFile.exists())
        {
            throw new FileNotFoundException("File does not exist: " + sqliteFile.getPath());
        }

        if(sqliteFile.length() < 100)
        {
            throw new IllegalArgumentException("File must be at least 100 bytes to be an SQLite file.");
        }

        try(RandomAccessFile randomAccessFile = new RandomAccessFile(sqliteFile, "r"))
        {
            // https://www.sqlite.org/fileformat2.html
            // Bytes 96 -> 100 are an int representing the sqlite version
            randomAccessFile.seek(96);
            final int version = randomAccessFile.readInt();

            // Major/minor/revision, https://www.sqlite.org/fileformat2.html
            final int major    = version/1000000;
            final int minor    = (version - (major*1000000))/1000;
            final int revision = version - ((major*1000000) + (minor*1000));

            return new DatabaseVersion(major, minor, revision);
        }
    }

    /**
     * @param connection
     *            the connection to the database
     * @param name
     *            the name of the table
     * @return true if the table or view exists in the database; otherwise
     *         returns false
     * @throws SQLException
     *             throws if unable to connect to the database or other various
     *             SQLExceptions
     */
    public static boolean tableOrViewExists(final Connection connection, final String name) throws SQLException
    {
        DatabaseUtility.verify(connection);

        if(name == null || name.isEmpty())
        {
        	throw new IllegalArgumentException("Table/view name cannot be null or empty");
        }

        return JdbcUtility.selectOne(connection,
                                     "SELECT COUNT(*) FROM sqlite_master WHERE (type = 'table' OR type = 'view') AND name = ? LIMIT 1;",
                                     preparedStatement -> preparedStatement.setString(1, name),
                                     resultSet -> resultSet.getInt(1)) > 0;
    }

    /**
     * @param connection
     *            the connection to the database
     * @param names
     *            the names of the tables
     * @return true if All the tables or views exists in the database; otherwise
     *         returns false
     * @throws SQLException
     *             throws if unable to connect to the database or other various
     *             SQLExceptions
     */
    public static boolean tablesOrViewsExists(final Connection connection, final String... names) throws SQLException
    {
        DatabaseUtility.verify(connection);

        final Set uniqueNames = new HashSet<>(Arrays.asList(names));

        return JdbcUtility.selectOne(connection,
                                     String.format("SELECT COUNT(*) AS count FROM sqlite_master WHERE (type = 'table' OR type = 'view') AND name IN (%s);",
                                                   String.join(", ", Collections.nCopies(uniqueNames.size(), "?"))),
                                     preparedStatement -> { int index = 1;

                                                            for(final String name : uniqueNames)
                                                            {
                                                                preparedStatement.setString(index++, name);
                                                            }
                                                          },
                                     resultSet -> resultSet.getInt("count")) == uniqueNames.size();
    }

    /**
     * @param connection
     *            connection to the database
     * @param applicationId
     *            the int Application ID to be set
     * @throws SQLException
     *             throws if various SQLExceptions occur
     */
    public static void setApplicationId(final Connection connection, final int applicationId) throws SQLException
    {
        DatabaseUtility.verify(connection);

        JdbcUtility.update(connection, String.format("PRAGMA application_id = %d;", applicationId));
    }

    /**
     * @param connection
     *            connection to the database
     * @return the application Id of the database
     * @throws SQLException
     *             throws if various SQLExceptions occur
     */
    public static int getApplicationId(final Connection connection) throws SQLException
    {
        DatabaseUtility.verify(connection);

        return JdbcUtility.selectOne(connection,
                                     "PRAGMA application_id;",
                                     null,
                                     resultSet -> resultSet.getInt("application_id"));
    }

    /**
     * @param connection
     *            connection to the database
     * @param state
     *            true or false whether you want foreign_keys to be set on or
     *            off
     * @throws SQLException
     *             throws if various SQLExceptions occur
     */
    public static void setPragmaForeignKeys(final Connection connection, final boolean state) throws SQLException
    {
        DatabaseUtility.verify(connection);

        JdbcUtility.update(connection, String.format("PRAGMA foreign_keys = %d;", (state ? 1 : 0)));
    }

    /**
     * @param connection
     *               connection to the database
     * @throws SQLException
     *                 throws if various SQLExceptions occur
     */
    public static void setPragmaJournalModeMemory(final Connection connection) throws SQLException
    {
        DatabaseUtility.verify(connection);

        JdbcUtility.update(connection, "PRAGMA journal_mode = MEMORY;");
    }

    /**
     * @param connection
     *               connection to the database
     * @throws SQLException
     *                 throws if various SQLExceptions occur
     */
    public static void setPragmaSynchronousOff(final Connection connection) throws SQLException
    {
        DatabaseUtility.verify(connection);

        JdbcUtility.update(connection, "PRAGMA synchronous = OFF;");
    }

    /**
     * Get the smallest value for a table and column that does not yet exist
     * 
     *
     * @param connection
     *             connection to the database
     * @param tableName
     *             table name
     * @param columnName
     *             column name
     * @return the smallest value for a table and column that does not yet exist
     * @throws SQLException
     *             if there's a database error
     */
    @SuppressWarnings("unchecked")
    public static  T nextValue(final Connection connection, final String tableName, final String columnName) throws SQLException
    {
        final String smallestNonexistentValue = String.format("SELECT (table1.%1$s + 1) " +
                                                              "FROM %2$s AS table1 LEFT JOIN %2$s table2 on table2.%1$s = (table1.%1$s + 1) " +
                                                              "WHERE table2.%1$s IS NULL " +
                                                              "ORDER BY table1.%1$s " +
                                                              "LIMIT 1",
                                                              columnName,
                                                              tableName);
        return JdbcUtility.selectOne(connection,
                                     smallestNonexistentValue,
                                     null,
                                     resultSet -> (T)resultSet.getObject(1));
    }

    public static List getColumnNames(final Connection connection, final String tableOrViewName) throws SQLException
    {
        verify(connection);

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

        return JdbcUtility.select(connection,
                                  String.format("PRAGMA table_info('%s')", tableOrViewName),
                                  null,
                                  resultSet -> resultSet.getString("name"));
    }

    public static void validateTableName(final String tableName)
    {
        if(tableName == null || tableName.isEmpty())
        {
            throw new IllegalArgumentException("The table name may not be null or empty");
        }

        if(sqlKeywords.stream()
                      .anyMatch(keyword -> keyword.equalsIgnoreCase(tableName)))
        {
            throw new IllegalArgumentException(String.format("The table name may not be an SQL keyword: %s", String.join(", ", sqlKeywords)));
        }

        if(!identifierPattern.matcher(tableName).matches())
        {
            throw new IllegalArgumentException("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");
        }

        if(tableName.startsWith("gpkg_"))
        {
            throw new IllegalArgumentException("The table name may not start with the reserved prefix 'gpkg_'");
        }
    }

    public static void validateColumnName(final String columnName)
    {
        if(columnName == null || columnName.isEmpty())
        {
            throw new IllegalArgumentException("The column name may not be null or empty");
        }

        if(sqlKeywords.stream()
                      .anyMatch(keyword -> keyword.equalsIgnoreCase(columnName)))
        {
            throw new IllegalArgumentException(String.format("The column name may not be an SQL keyword: %s", String.join(", ", sqlKeywords)));
        }

        // TODO
        // By wrapping column names in double quotes, you can get away with
        // a variety of strange names. This is *strongly* discouraged, but I'm
        // not sure it's correct to explicitly forbid it.
        if(!identifierPattern.matcher(columnName).matches())
        {
            // This is just a best guess. SQLite doesn't actually have an EBNF diagram for "column-name"
            throw new IllegalArgumentException(String.format("The column '%s' must begin with a letter (A..Z, a..z) or an underscore (_) and may only be followed by letters, underscores, or numbers.",
                                                             columnName));
        }
    }

    private static void verify(final Connection connection) throws SQLException
    {
        if(connection == null || connection.isClosed())
        {
            throw new IllegalArgumentException("The connection cannot be null or closed.");
        }
    }

    private static final Pattern identifierPattern = Pattern.compile("^[_a-zA-Z]\\w*"); // This is just a best guess. SQLite doesn't actually have an EBNF diagram for "column-name"

    private static final Collection sqlKeywords = Arrays.asList("ABORT",       "ACTION",        "ADD",          "AFTER",        "ALL",
                                                                        "ALTER",       "ANALYZE",       "AND",          "AS",           "ASC",
                                                                        "ATTACH",      "AUTOINCREMENT", "BEFORE",       "BEGIN",        "BETWEEN",
                                                                        "BY",          "CASCADE",       "CASE",         "CAST",         "CHECK",
                                                                        "COLLATE",     "COLUMN",        "COMMIT",       "CONFLICT",     "CONSTRAINT",
                                                                        "CREATE",      "CROSS",         "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
                                                                        "DATABASE",    "DEFAULT",       "DEFERRABLE",   "DEFERRED",     "DELETE",
                                                                        "DESC",        "DETACH",        "DISTINCT",     "DROP",         "EACH",
                                                                        "ELSE",        "END",           "ESCAPE",       "EXCEPT",       "EXCLUSIVE",
                                                                        "EXISTS",      "EXPLAIN",       "FAIL",         "FOR",          "FOREIGN",
                                                                        "FROM",        "FULL",          "GLOB",         "GROUP",        "HAVING",
                                                                        "IF",          "IGNORE",        "IMMEDIATE",    "IN",           "INDEX",
                                                                        "INDEXED",     "INITIALLY",     "INNER",        "INSERT",       "INSTEAD",
                                                                        "INTERSECT",   "INTO",          "IS",           "ISNULL",       "JOIN",
                                                                        "KEY",         "LEFT",          "LIKE",         "LIMIT",        "MATCH",
                                                                        "NATURAL",     "NO",            "NOT",          "NOTNULL",      "NULL",
                                                                        "OF",          "OFFSET",        "ON",           "OR",           "ORDER",
                                                                        "OUTER",       "PLAN",          "PRAGMA",       "PRIMARY",      "QUERY",
                                                                        "RAISE",       "RECURSIVE",     "REFERENCES",   "REGEXP",       "REINDEX",
                                                                        "RELEASE",     "RENAME",        "REPLACE",      "RESTRICT",     "RIGHT",
                                                                        "ROLLBACK",    "ROW",           "SAVEPOINT",    "SELECT",       "SET",
                                                                        "TABLE",       "TEMP",          "TEMPORARY",    "THEN",         "TO",
                                                                        "TRANSACTION", "TRIGGER",       "UNION",        "UNIQUE",       "UPDATE",
                                                                        "USING",       "VACUUM",        "VALUES",       "VIEW",         "VIRTUAL",
                                                                        "WHEN",        "WHERE",         "WITH",         "WITHOUT");
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy