package mil.nga.geopackage.factory; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import android.util.Log; import androidx.annotation.RequiresApi; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import mil.nga.geopackage.GeoPackage; import mil.nga.geopackage.GeoPackageConstants; import mil.nga.geopackage.GeoPackageException; import mil.nga.geopackage.GeoPackageManager; import mil.nga.geopackage.R; import mil.nga.geopackage.core.contents.Contents; import mil.nga.geopackage.core.srs.SpatialReferenceSystem; import mil.nga.geopackage.db.GeoPackageConnection; import mil.nga.geopackage.db.GeoPackageDatabase; import mil.nga.geopackage.db.GeoPackageTableCreator; import mil.nga.geopackage.db.metadata.GeoPackageMetadata; import mil.nga.geopackage.db.metadata.GeoPackageMetadataDataSource; import mil.nga.geopackage.db.metadata.GeoPackageMetadataDb; import mil.nga.geopackage.io.GeoPackageIOUtils; import mil.nga.geopackage.io.GeoPackageProgress; import mil.nga.geopackage.validate.GeoPackageValidate; import mil.nga.sf.util.ByteReader; /** * GeoPackage Database management implementation * * @author osbornb */ class GeoPackageManagerImpl implements GeoPackageManager { /** * Context */ private final Context context; /** * Validate the database header of an imported database */ private boolean importHeaderValidation; /** * Validate the database integrity of a imported database */ private boolean importIntegrityValidation; /** * Validate the database header when opening a database */ private boolean openHeaderValidation; /** * Validate the database integrity when opening a database */ private boolean openIntegrityValidation; /** * Write ahead logging state for SQLite connections */ private boolean sqliteWriteAheadLogging; /** * Constructor * * @param context */ GeoPackageManagerImpl(Context context) { this.context = context; Resources resources = context.getResources(); importHeaderValidation = resources.getBoolean(R.bool.manager_validation_import_header); importIntegrityValidation = resources.getBoolean(R.bool.manager_validation_import_integrity); openHeaderValidation = resources.getBoolean(R.bool.manager_validation_open_header); openIntegrityValidation = resources.getBoolean(R.bool.manager_validation_open_integrity); sqliteWriteAheadLogging = resources.getBoolean(R.bool.sqlite_write_ahead_logging); } /** * {@inheritDoc} */ @Override public List<String> databases() { Set<String> sortedDatabases = new TreeSet<String>(); addDatabases(sortedDatabases); List<String> databases = new ArrayList<String>(); databases.addAll(sortedDatabases); return databases; } /** * {@inheritDoc} */ @Override public List<String> databasesLike(String like) { List<String> databases = null; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); databases = dataSource.getMetadataWhereNameLike(like, GeoPackageMetadata.COLUMN_NAME); } finally { metadataDb.close(); } databases = deleteMissingDatabases(databases); return databases; } /** * {@inheritDoc} */ @Override public List<String> databasesNotLike(String notLike) { List<String> databases = null; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); databases = dataSource.getMetadataWhereNameNotLike(notLike, GeoPackageMetadata.COLUMN_NAME); } finally { metadataDb.close(); } databases = deleteMissingDatabases(databases); return databases; } /** * Delete all databases that do not exist or the database file does not exist * * @param databases list of databases * @return databases that exist */ private List<String> deleteMissingDatabases(List<String> databases) { List<String> filesExist = new ArrayList<>(); for (String database : databases) { if (exists(database)) { filesExist.add(database); } } return filesExist; } /** * {@inheritDoc} */ @Override public List<String> internalDatabases() { Set<String> sortedDatabases = new TreeSet<String>(); addInternalDatabases(sortedDatabases); List<String> databases = new ArrayList<String>(); databases.addAll(sortedDatabases); return databases; } /** * {@inheritDoc} */ @Override public List<String> externalDatabases() { Set<String> sortedDatabases = new TreeSet<String>(); addExternalDatabases(sortedDatabases); List<String> databases = new ArrayList<String>(); databases.addAll(sortedDatabases); return databases; } /** * {@inheritDoc} */ public int count() { return databaseSet().size(); } /** * {@inheritDoc} */ public int internalCount() { return internalDatabaseSet().size(); } /** * {@inheritDoc} */ public int externalCount() { return externalDatabaseSet().size(); } /** * {@inheritDoc} */ @Override public Set<String> databaseSet() { Set<String> databases = new HashSet<String>(); addDatabases(databases); return databases; } /** * {@inheritDoc} */ @Override public Set<String> internalDatabaseSet() { Set<String> databases = new HashSet<String>(); addInternalDatabases(databases); return databases; } /** * {@inheritDoc} */ @Override public Set<String> externalDatabaseSet() { Set<String> databases = new HashSet<String>(); addExternalDatabases(databases); return databases; } /** * {@inheritDoc} */ @Override public boolean exists(String database) { boolean exists = internalDatabaseSet().contains(database); if (!exists) { GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); GeoPackageMetadata metadata = dataSource.get(database); if (metadata != null) { if (metadata.getExternalPath() != null && !new File(metadata.getExternalPath()).exists()) { delete(database); } else { exists = true; } } } finally { metadataDb.close(); } } return exists; } /** * {@inheritDoc} */ @Override public long size(String database) { File dbFile = getFile(database); long size = dbFile.length(); return size; } /** * {@inheritDoc} */ @Override public boolean isExternal(String database) { boolean external = false; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); external = dataSource.isExternal(database); } finally { metadataDb.close(); } return external; } /** * {@inheritDoc} */ @Override public boolean existsAtExternalFile(File file) { return existsAtExternalPath(file.getAbsolutePath()); } /** * {@inheritDoc} */ @Override public boolean existsAtExternalPath(String path) { GeoPackageMetadata metadata = getGeoPackageMetadataAtExternalPath(path); return metadata != null; } /** * {@inheritDoc} */ @Override public String getPath(String database) { File dbFile = getFile(database); String path = dbFile.getAbsolutePath(); return path; } /** * {@inheritDoc} */ @Override public File getFile(String database) { File dbFile = null; GeoPackageMetadata metadata = getGeoPackageMetadata(database); if (metadata != null && metadata.isExternal()) { dbFile = new File(metadata.getExternalPath()); } else { dbFile = context.getDatabasePath(database); } if (dbFile == null || !dbFile.exists()) { throw new GeoPackageException("GeoPackage does not exist: " + database); } return dbFile; } /** * {@inheritDoc} */ @Override public String getDatabaseAtExternalFile(File file) { return getDatabaseAtExternalPath(file.getAbsolutePath()); } /** * {@inheritDoc} */ @Override public String getDatabaseAtExternalPath(String path) { String database = null; GeoPackageMetadata metadata = getGeoPackageMetadataAtExternalPath(path); if (metadata != null) { database = metadata.getName(); } return database; } /** * {@inheritDoc} */ @Override public String readableSize(String database) { long size = size(database); return GeoPackageIOUtils.formatBytes(size); } /** * {@inheritDoc} */ @Override public boolean delete(String database) { boolean deleted = false; boolean external = isExternal(database); GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); deleted = dataSource.delete(database); } finally { metadataDb.close(); } if (!external) { deleted = context.deleteDatabase(database); } return deleted; } /** * {@inheritDoc} */ @Override public boolean deleteAll() { boolean deleted = true; for (String database : databaseSet()) { deleted = delete(database) && deleted; } return deleted; } /** * {@inheritDoc} */ @Override public boolean deleteAllExternal() { boolean deleted = true; for (String database : externalDatabaseSet()) { deleted = delete(database) && deleted; } return deleted; } /** * {@inheritDoc} */ @Override public boolean deleteAllMissingExternal() { boolean deleted = false; List<GeoPackageMetadata> externalGeoPackages = getExternalGeoPackages(); for (GeoPackageMetadata external : externalGeoPackages) { if (!new File(external.getExternalPath()).exists()) { deleted = delete(external.getName()) || deleted; } } return deleted; } /** * {@inheritDoc} */ @Override public boolean create(String database) { boolean created = false; if (exists(database)) { throw new GeoPackageException("GeoPackage already exists: " + database); } else { GeoPackageDatabase db = new GeoPackageDatabase(context.openOrCreateDatabase(database, Context.MODE_PRIVATE, null)); createAndCloseGeoPackage(db); GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); // Save in metadata GeoPackageMetadata metadata = new GeoPackageMetadata(); metadata.setName(database); dataSource.create(metadata); } finally { metadataDb.close(); } created = true; } return created; } /** * Create the required GeoPackage application id and tables in the newly created and open database connection. Then close the connection. * * @param db */ private void createAndCloseGeoPackage(GeoPackageDatabase db) { GeoPackageConnection connection = new GeoPackageConnection(db); // Set the GeoPackage application id and user version connection.setApplicationId(); connection.setUserVersion(); // Create the minimum required tables GeoPackageTableCreator tableCreator = new GeoPackageTableCreator(connection); tableCreator.createRequired(); connection.close(); } /** * {@inheritDoc} */ @Override public boolean createAtPath(String database, File path) { // Create the absolute file path File file = new File(path, database + "." + GeoPackageConstants.EXTENSION); // Create the GeoPackage boolean created = createFile(database, file); return created; } /** * {@inheritDoc} */ @Override public boolean createFile(File file) { // Get the database name String database = GeoPackageIOUtils.getFileNameWithoutExtension(file); // Create the GeoPackage boolean created = createFile(database, file); return created; } /** * {@inheritDoc} */ @Override public boolean createFile(String database, File file) { boolean created = false; if (exists(database)) { throw new GeoPackageException("GeoPackage already exists: " + database); } else { // Check if the path is an absolute path to the GeoPackage file to create if (!GeoPackageValidate.hasGeoPackageExtension(file)) { // Make sure this isn't a path to another file extension if (GeoPackageIOUtils.getFileExtension(file) != null) { throw new GeoPackageException("File can not have a non GeoPackage extension. Invalid File: " + file.getAbsolutePath()); } // Add the extension file = new File(file.getParentFile(), file.getName() + "." + GeoPackageConstants.EXTENSION); } // Make sure the file does not already exist if (file.exists()) { throw new GeoPackageException("GeoPackage file already exists: " + file.getAbsolutePath()); } // Create the new GeoPackage file GeoPackageDatabase db = new GeoPackageDatabase(SQLiteDatabase.openOrCreateDatabase(file, null)); createAndCloseGeoPackage(db); // Import the GeoPackage created = importGeoPackageAsExternalLink(file, database); } return created; } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(File file) { return importGeoPackage(null, file, false); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(File file, boolean override) { return importGeoPackage(null, file, override); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String database, InputStream stream) { return importGeoPackage(database, stream, false, null); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String database, InputStream stream, GeoPackageProgress progress) { return importGeoPackage(database, stream, false, progress); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String database, InputStream stream, boolean override) { return importGeoPackage(database, stream, override, null); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String database, InputStream stream, boolean override, GeoPackageProgress progress) { if (progress != null) { try { int streamLength = stream.available(); if (streamLength > 0) { progress.setMax(streamLength); } } catch (IOException e) { Log.w(GeoPackageManagerImpl.class.getSimpleName(), "Could not determine stream available size. Database: " + database, e); } } boolean success = importGeoPackage(database, override, stream, progress); return success; } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, File file) { return importGeoPackage(name, file, false); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, File file, boolean override) { // Verify the file has the right extension GeoPackageValidate.validateGeoPackageExtension(file); // Use the provided name or the base file name as the database name String database; if (name != null) { database = name; } else { database = GeoPackageIOUtils.getFileNameWithoutExtension(file); } boolean success = false; try { FileInputStream geoPackageStream = new FileInputStream(file); success = importGeoPackage(database, override, geoPackageStream, null); } catch (FileNotFoundException e) { throw new GeoPackageException( "Failed read or write GeoPackage file '" + file + "' to database: '" + database + "'", e); } return success; } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, URL url) { return importGeoPackage(name, url, false, null); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, URL url, GeoPackageProgress progress) { return importGeoPackage(name, url, false, progress); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, URL url, boolean override) { return importGeoPackage(name, url, override, null); } /** * {@inheritDoc} */ @Override public boolean importGeoPackage(String name, URL url, boolean override, GeoPackageProgress progress) { boolean success = false; HttpURLConnection connection = null; try { connection = (HttpURLConnection) url.openConnection(); connection.connect(); int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { String redirect = connection.getHeaderField("Location"); connection.disconnect(); url = new URL(redirect); connection = (HttpURLConnection) url.openConnection(); connection.connect(); } if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new GeoPackageException("Failed to import GeoPackage " + name + " from URL: '" + url.toString() + "'. HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage()); } int fileLength = connection.getContentLength(); if (fileLength != -1 && progress != null) { progress.setMax(fileLength); } InputStream geoPackageStream = connection.getInputStream(); success = importGeoPackage(name, override, geoPackageStream, progress); } catch (IOException e) { throw new GeoPackageException("Failed to import GeoPackage " + name + " from URL: '" + url.toString() + "'", e); } finally { if (connection != null) { connection.disconnect(); } } return success; } /** * {@inheritDoc} */ @Override public void exportGeoPackage(String database, File directory) { exportGeoPackage(database, database, directory); } /** * {@inheritDoc} */ @Override public void exportGeoPackage(String database, String name, File directory) { File file = new File(directory, name); // Add the extension if not on the name if (!GeoPackageValidate.hasGeoPackageExtension(file)) { name += "." + GeoPackageConstants.EXTENSION; file = new File(directory, name); } // Copy the geopackage database to the new file location File dbFile = getFile(database); try { GeoPackageIOUtils.copyFile(dbFile, file); } catch (IOException e) { throw new GeoPackageException( "Failed read or write GeoPackage database '" + database + "' to file: '" + file, e); } } /** * {@inheritDoc} */ @RequiresApi(api = Build.VERSION_CODES.Q) @Override public void exportGeoPackage(String database, String relativePath, Uri uri) throws IOException { exportGeoPackage(database, database, relativePath, uri); } /** * {@inheritDoc} */ @RequiresApi(api = Build.VERSION_CODES.Q) @Override public void exportGeoPackage(String database, String name, String relativePath, Uri uri) throws IOException { // Add the extension if not on the name name = GeoPackageValidate.addGeoPackageExtension(name); ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, GeoPackageConstants.MEDIA_TYPE); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath); exportGeoPackage(database, uri, contentValues); } /** * {@inheritDoc} */ @Override public void exportGeoPackage(String database, Uri uri, ContentValues contentValues) throws IOException { // Get the GeoPackage database file File dbFile = getFile(database); // Insert the row ContentResolver resolver = context.getContentResolver(); Uri insertUri = resolver.insert(uri, contentValues); // Copy the GeoPackage file OutputStream outputStream = resolver.openOutputStream(insertUri); InputStream inputStream = new FileInputStream(dbFile); GeoPackageIOUtils.copyStream(inputStream, outputStream); } /** * {@inheritDoc} */ @Override public GeoPackage open(String database) { return open(database, true); } /** * {@inheritDoc} */ @Override public GeoPackage open(String database, boolean writable) { GeoPackage db = null; if (exists(database)) { GeoPackageCursorFactory cursorFactory = new GeoPackageCursorFactory(); String path = null; SQLiteDatabase sqlite = null; GeoPackageMetadata metadata = getGeoPackageMetadata(database); if (metadata != null && metadata.isExternal()) { path = metadata.getExternalPath(); if (writable) { try { sqlite = SQLiteDatabase.openDatabase(path, cursorFactory, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.NO_LOCALIZED_COLLATORS); } catch (Exception e) { Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to open database as writable: " + database, e); } } if (sqlite == null) { sqlite = SQLiteDatabase.openDatabase(path, cursorFactory, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS); writable = false; } } else { sqlite = context.openOrCreateDatabase(database, Context.MODE_PRIVATE, cursorFactory); } if (sqliteWriteAheadLogging) { sqlite.enableWriteAheadLogging(); } else { sqlite.disableWriteAheadLogging(); } // Validate the database if validation is enabled validateDatabaseAndCloseOnError(sqlite, openHeaderValidation, openIntegrityValidation); GeoPackageConnection connection = new GeoPackageConnection(new GeoPackageDatabase(sqlite, writable, cursorFactory)); connection.enableForeignKeys(); GeoPackageTableCreator tableCreator = new GeoPackageTableCreator(connection); db = new GeoPackageImpl(context, database, path, connection, cursorFactory, tableCreator, writable); // Validate the GeoPackage has the minimum required tables try { GeoPackageValidate.validateMinimumTables(db); } catch (RuntimeException e) { db.close(); throw e; } } return db; } /** * {@inheritDoc} */ @Override public boolean isImportHeaderValidation() { return importHeaderValidation; } /** * {@inheritDoc} */ @Override public void setImportHeaderValidation(boolean enabled) { this.importHeaderValidation = enabled; } /** * {@inheritDoc} */ @Override public boolean isImportIntegrityValidation() { return importIntegrityValidation; } /** * {@inheritDoc} */ @Override public void setImportIntegrityValidation(boolean enabled) { this.importIntegrityValidation = enabled; } /** * {@inheritDoc} */ @Override public boolean isOpenHeaderValidation() { return openHeaderValidation; } /** * {@inheritDoc} */ @Override public void setOpenHeaderValidation(boolean enabled) { this.openHeaderValidation = enabled; } /** * {@inheritDoc} */ @Override public boolean isOpenIntegrityValidation() { return openIntegrityValidation; } /** * {@inheritDoc} */ @Override public void setOpenIntegrityValidation(boolean enabled) { this.openIntegrityValidation = enabled; } /** * {@inheritDoc} */ @Override public boolean isSqliteWriteAheadLogging() { return sqliteWriteAheadLogging; } /** * {@inheritDoc} */ @Override public void setSqliteWriteAheadLogging(boolean enabled) { this.sqliteWriteAheadLogging = enabled; } /** * {@inheritDoc} */ @Override public boolean validate(String database) { boolean valid = isValid(database, true, true); return valid; } /** * {@inheritDoc} */ @Override public boolean validateHeader(String database) { boolean valid = isValid(database, true, false); return valid; } /** * {@inheritDoc} */ @Override public boolean validateIntegrity(String database) { boolean valid = isValid(database, false, true); return valid; } /** * Validate the GeoPackage database * * @param database * @param validateHeader true to validate the header of the database * @param validateIntegrity true to validate the integrity of the database * @return true if valid */ private boolean isValid(String database, boolean validateHeader, boolean validateIntegrity) { boolean valid = false; if (exists(database)) { GeoPackageCursorFactory cursorFactory = new GeoPackageCursorFactory(); String path = null; SQLiteDatabase sqlite; GeoPackageMetadata metadata = getGeoPackageMetadata(database); if (metadata != null && metadata.isExternal()) { path = metadata.getExternalPath(); try { sqlite = SQLiteDatabase.openDatabase(path, cursorFactory, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.NO_LOCALIZED_COLLATORS); } catch (Exception e) { sqlite = SQLiteDatabase.openDatabase(path, cursorFactory, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS); } } else { path = context.getDatabasePath(database).getAbsolutePath(); sqlite = context.openOrCreateDatabase(database, Context.MODE_PRIVATE, cursorFactory); } try { valid = (!validateHeader || isDatabaseHeaderValid(sqlite)) && (!validateIntegrity || sqlite.isDatabaseIntegrityOk()); } catch (Exception e) { Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to validate database", e); } finally { sqlite.close(); } } return valid; } /** * {@inheritDoc} */ @Override public boolean copy(String database, String databaseCopy) { // Copy the database as a new file File dbFile = getFile(database); File dbCopyFile = context.getDatabasePath(databaseCopy); try { GeoPackageIOUtils.copyFile(dbFile, dbCopyFile); } catch (IOException e) { throw new GeoPackageException( "Failed to copy GeoPackage database '" + database + "' to '" + databaseCopy + "'", e); } return exists(databaseCopy); } /** * {@inheritDoc} */ @Override public boolean rename(String database, String newDatabase) { GeoPackageMetadata metadata = getGeoPackageMetadata(database); if (metadata != null) { GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); dataSource.rename(metadata, newDatabase); } finally { metadataDb.close(); } } if ((metadata == null || !metadata.isExternal()) && copy(database, newDatabase)) { delete(database); } return exists(newDatabase); } /** * {@inheritDoc} */ @Override public boolean importGeoPackageAsExternalLink(File path, String database) { return importGeoPackageAsExternalLink(path, database, false); } /** * {@inheritDoc} */ @Override public boolean importGeoPackageAsExternalLink(File path, String database, boolean override) { return importGeoPackageAsExternalLink(path.getAbsolutePath(), database, override); } /** * {@inheritDoc} */ @Override public boolean importGeoPackageAsExternalLink(String path, String database) { return importGeoPackageAsExternalLink(path, database, false); } /** * {@inheritDoc} */ @Override public boolean importGeoPackageAsExternalLink(String path, String database, boolean override) { if (exists(database)) { if (override) { if (!delete(database)) { throw new GeoPackageException( "Failed to delete existing database: " + database); } } else { throw new GeoPackageException( "GeoPackage database already exists: " + database); } } // Verify the file is a database and can be opened try { SQLiteDatabase sqlite = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS); validateDatabaseAndClose(sqlite, importHeaderValidation, importIntegrityValidation); } catch (SQLiteException e) { throw new GeoPackageException( "Failed to import GeoPackage database as external link: " + database + ", Path: " + path, e); } GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); // Save the external link in metadata GeoPackageMetadata metadata = new GeoPackageMetadata(); metadata.setName(database); metadata.setExternalPath(path); dataSource.create(metadata); GeoPackage geoPackage = open(database, false); if (geoPackage != null) { try { GeoPackageValidate.validateMinimumTables(geoPackage); } catch (RuntimeException e) { dataSource.delete(database); throw e; } finally { geoPackage.close(); } } else { dataSource.delete(database); throw new GeoPackageException( "Unable to open GeoPackage database. Database: " + database); } } finally { metadataDb.close(); } return exists(database); } /** * Validate the database and close when validation fails. Throw an error when not valid. * * @param sqliteDatabase database * @param validateHeader validate the header * @param validateIntegrity validate the integrity */ private void validateDatabaseAndCloseOnError(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity) { validateDatabase(sqliteDatabase, validateHeader, validateIntegrity, false, true); } /** * Validate the database and close it. Throw an error when not valid. * * @param sqliteDatabase database * @param validateHeader validate the header * @param validateIntegrity validate the integrity */ private void validateDatabaseAndClose(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity) { validateDatabase(sqliteDatabase, validateHeader, validateIntegrity, true, true); } /** * Validate the database header and integrity. Throw an error when not valid. * * @param sqliteDatabase database * @param validateHeader validate the header * @param validateIntegrity validate the integrity * @param close close the database after validation * @param closeOnError close the database if validation fails */ private void validateDatabase(SQLiteDatabase sqliteDatabase, boolean validateHeader, boolean validateIntegrity, boolean close, boolean closeOnError) { try { if (validateHeader) { validateDatabaseHeader(sqliteDatabase); } if (validateIntegrity) { validateDatabaseIntegrity(sqliteDatabase); } } catch (Exception e) { if (closeOnError) { sqliteDatabase.close(); } throw e; } if (close) { sqliteDatabase.close(); } } /** * Validate the header of the database file to verify it is a sqlite database * * @param sqliteDatabase database */ private void validateDatabaseHeader(SQLiteDatabase sqliteDatabase) { boolean validHeader = isDatabaseHeaderValid(sqliteDatabase); if (!validHeader) { throw new GeoPackageException( "GeoPackage SQLite header is not valid: " + sqliteDatabase.getPath()); } } /** * Determine if the header of the database file is valid * * @param sqliteDatabase database * @return true if valid */ private boolean isDatabaseHeaderValid(SQLiteDatabase sqliteDatabase) { boolean validHeader = false; FileInputStream fis = null; try { fis = new FileInputStream(sqliteDatabase.getPath()); byte[] headerBytes = new byte[16]; if (fis.read(headerBytes) == 16) { ByteReader byteReader = new ByteReader(headerBytes); String header = byteReader.readString(headerBytes.length); String headerPrefix = header.substring(0, GeoPackageConstants.SQLITE_HEADER_PREFIX.length()); validHeader = headerPrefix.equalsIgnoreCase(GeoPackageConstants.SQLITE_HEADER_PREFIX); } } catch (Exception e) { Log.e(GeoPackageManagerImpl.class.getSimpleName(), "Failed to retrieve database header", e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // eat } } } return validHeader; } /** * Validate the integrity of the database * * @param sqliteDatabase database */ private void validateDatabaseIntegrity(SQLiteDatabase sqliteDatabase) { if (!sqliteDatabase.isDatabaseIntegrityOk()) { throw new GeoPackageException( "GeoPackage SQLite file integrity failed: " + sqliteDatabase.getPath()); } } /** * Add all databases to the collection * * @param databases */ private void addDatabases(Collection<String> databases) { // Add the internal databases addInternalDatabases(databases); // Add the external databases addExternalDatabases(databases); } /** * Add all internal databases to the collection * * @param databases */ private void addInternalDatabases(Collection<String> databases) { String[] databaseArray = context.databaseList(); for (String database : databaseArray) { if (!isTemporary(database) && !database .equalsIgnoreCase(GeoPackageMetadataDb.DATABASE_NAME)) { databases.add(database); } } } /** * Add all external databases to the collection * * @param databases */ private void addExternalDatabases(Collection<String> databases) { // Get the external GeoPackages, adding those where the file exists and // deleting those with missing files List<GeoPackageMetadata> externalGeoPackages = getExternalGeoPackages(); for (GeoPackageMetadata external : externalGeoPackages) { if (new File(external.getExternalPath()).exists()) { databases.add(external.getName()); } else { delete(external.getName()); } } } /** * Import the GeoPackage stream * * @param database * @param override * @param geoPackageStream * @param progress * @return true if imported successfully */ private boolean importGeoPackage(String database, boolean override, InputStream geoPackageStream, GeoPackageProgress progress) { try { if (exists(database)) { if (override) { if (!delete(database)) { throw new GeoPackageException( "Failed to delete existing database: " + database); } } else { throw new GeoPackageException( "GeoPackage database already exists: " + database); } } // Copy the geopackage over as a database File newDbFile = context.getDatabasePath(database); try { SQLiteDatabase db = context.openOrCreateDatabase(database, Context.MODE_PRIVATE, null); db.close(); GeoPackageIOUtils.copyStream(geoPackageStream, newDbFile, progress); } catch (IOException e) { throw new GeoPackageException( "Failed to import GeoPackage database: " + database, e); } } finally { GeoPackageIOUtils.closeQuietly(geoPackageStream); } if (progress == null || progress.isActive()) { // Verify that the database is valid try { SQLiteDatabase sqlite = context.openOrCreateDatabase(database, Context.MODE_PRIVATE, null, new DatabaseErrorHandler() { @Override public void onCorruption(SQLiteDatabase dbObj) { } }); validateDatabaseAndClose(sqlite, importHeaderValidation, importIntegrityValidation); GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); // Save in metadata GeoPackageMetadata metadata = new GeoPackageMetadata(); metadata.setName(database); dataSource.create(metadata); } finally { metadataDb.close(); } } catch (Exception e) { delete(database); throw new GeoPackageException( "Invalid GeoPackage database file", e); } GeoPackage geoPackage = open(database, false); if (geoPackage != null) { try { if (!geoPackage.getSpatialReferenceSystemDao() .isTableExists() || !geoPackage.getContentsDao().isTableExists()) { delete(database); throw new GeoPackageException( "Invalid GeoPackage database file. Does not contain required tables: " + SpatialReferenceSystem.TABLE_NAME + " & " + Contents.TABLE_NAME + ", Database: " + database); } } catch (SQLException e) { delete(database); throw new GeoPackageException( "Invalid GeoPackage database file. Could not verify existence of required tables: " + SpatialReferenceSystem.TABLE_NAME + " & " + Contents.TABLE_NAME + ", Database: " + database); } finally { geoPackage.close(); } } else { delete(database); throw new GeoPackageException( "Unable to open GeoPackage database. Database: " + database); } } return exists(database); } /** * Get all external GeoPackage metadata * * @return */ private List<GeoPackageMetadata> getExternalGeoPackages() { List<GeoPackageMetadata> metadata = null; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); metadata = dataSource.getAllExternal(); } finally { metadataDb.close(); } return metadata; } /** * Get the GeoPackage metadata * * @param database * @return */ private GeoPackageMetadata getGeoPackageMetadata(String database) { GeoPackageMetadata metadata = null; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); metadata = dataSource.get(database); } finally { metadataDb.close(); } return metadata; } /** * Get the GeoPackage metadata of the database at the external path * * @param path external database path * @return metadata or null */ private GeoPackageMetadata getGeoPackageMetadataAtExternalPath(String path) { GeoPackageMetadata metadata = null; GeoPackageMetadataDb metadataDb = new GeoPackageMetadataDb( context); metadataDb.open(); try { GeoPackageMetadataDataSource dataSource = new GeoPackageMetadataDataSource(metadataDb); metadata = dataSource.getExternalAtPath(path); } finally { metadataDb.close(); } return metadata; } /** * Check if the database is temporary (rollback journal) * * @param database * @return */ private boolean isTemporary(String database) { return database.endsWith(context.getString(R.string.geopackage_db_rollback_journal_suffix)) || database.endsWith(context.getString(R.string.geopackage_db_write_ahead_log_suffix)) || database.endsWith(context.getString(R.string.geopackage_db_shared_memory_suffix)); } }