// Copyright (C) Stichting Deltares 2016. All rights reserved. // // This file is part of Ringtoets. // // Ringtoets is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Data; using System.Data.Entity; using System.IO; using System.Linq; using System.ServiceModel; using Application.Ringtoets.Storage.Create; using Application.Ringtoets.Storage.DbContext; using Application.Ringtoets.Storage.Exceptions; using Application.Ringtoets.Storage.Properties; using Application.Ringtoets.Storage.Read; using Core.Common.Base.Data; using Core.Common.Base.Storage; using Core.Common.Utils; using Core.Common.Utils.Builders; using log4net; using Ringtoets.Integration.Data; using UtilsResources = Core.Common.Utils.Properties.Resources; namespace Application.Ringtoets.Storage { /// /// This class interacts with an SQLite database file using the Entity Framework. /// public class StorageSqLite : IStoreProject { private const int currentDatabaseVersion = 5; private static readonly ILog log = LogManager.GetLogger(typeof(StorageSqLite)); private StagedProject stagedProject; public string FileFilter { get { return Resources.Ringtoets_project_file_filter; } } public bool HasStagedProject { get { return stagedProject != null; } } public void StageProject(IProject project) { var ringtoetsProject = project as RingtoetsProject; if (ringtoetsProject == null) { throw new ArgumentNullException("project"); } var registry = new PersistenceRegistry(); stagedProject = new StagedProject(ringtoetsProject, ringtoetsProject.Create(registry)); } public void UnstageProject() { stagedProject = null; } public void SaveProjectAs(string databaseFilePath) { if (!HasStagedProject) { throw new InvalidOperationException("Call 'StageProject(IProject)' first before calling this method."); } try { BackedUpFileWriter writer = new BackedUpFileWriter(databaseFilePath); writer.Perform(() => { SaveProjectInDatabase(databaseFilePath); }); } catch (IOException e) { throw new StorageException(e.Message, e); } catch (CannotDeleteBackupFileException e) { log.Warn(e.Message, e); } finally { UnstageProject(); } } public IProject LoadProject(string databaseFilePath) { var connectionString = GetConnectionToExistingFile(databaseFilePath); try { RingtoetsProject project; using (var dbContext = new RingtoetsEntities(connectionString)) { ValidateDatabaseVersion(dbContext, databaseFilePath); ProjectEntity projectEntity; try { projectEntity = dbContext.ProjectEntities.Single(); } catch (InvalidOperationException exception) { throw CreateStorageReaderException(databaseFilePath, Resources.StorageSqLite_LoadProject_Invalid_Ringtoets_database_file, exception); } project = projectEntity.Read(new ReadConversionCollector()); } project.Name = Path.GetFileNameWithoutExtension(databaseFilePath); return project; } catch (DataException exception) { throw CreateStorageReaderException(databaseFilePath, Resources.StorageSqLite_LoadProject_Invalid_Ringtoets_database_file, exception); } catch (SystemException exception) { throw CreateStorageReaderException(databaseFilePath, Resources.StorageSqLite_LoadProject_Invalid_Ringtoets_database_file, exception); } } public bool HasStagedProjectChanges(string filePath) { if (!HasStagedProject) { throw new InvalidOperationException("Call 'StageProject(IProject)' first before calling this method."); } if (string.IsNullOrWhiteSpace(filePath)) { return true; } var connectionString = GetConnectionToExistingFile(filePath); try { byte[] originalHash; using (var dbContext = new RingtoetsEntities(connectionString)) { originalHash = dbContext.VersionEntities.Select(v => v.FingerPrint).First(); } byte[] hash = FingerprintHelper.Get(stagedProject.Entity); return !FingerprintHelper.AreEqual(originalHash, hash); } catch (QuotaExceededException e) { throw new StorageException(Resources.StorageSqLite_HasStagedProjectChanges_Project_contains_too_many_objects_to_generate_fingerprint, e); } } private void SaveProjectInDatabase(string databaseFilePath) { var connectionString = GetConnectionToNewFile(databaseFilePath); using (var dbContext = new RingtoetsEntities(connectionString)) { try { dbContext.VersionEntities.Add(new VersionEntity { Version = currentDatabaseVersion, Timestamp = DateTime.Now, FingerPrint = FingerprintHelper.Get(stagedProject.Entity) }); dbContext.ProjectEntities.Add(stagedProject.Entity); dbContext.SaveChanges(); } catch (DataException exception) { throw CreateStorageWriterException(databaseFilePath, Resources.Error_saving_database, exception); } catch (QuotaExceededException exception) { throw CreateStorageWriterException(databaseFilePath, exception.Message, exception); } catch (SystemException exception) { if (exception is InvalidOperationException || exception is NotSupportedException) { throw CreateStorageWriterException(databaseFilePath, Resources.Error_during_connection, exception); } throw; } stagedProject.Model.Name = Path.GetFileNameWithoutExtension(databaseFilePath); } } private static void ValidateDatabaseVersion(RingtoetsEntities ringtoetsEntities, string databaseFilePath) { try { long databaseVersion = ringtoetsEntities.VersionEntities.Select(v => v.Version).Single(); if (databaseVersion <= 0) { string m = string.Format(Resources.StorageSqLite_ValidateDatabaseVersion_DatabaseVersion_0_is_invalid, databaseVersion); var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(m); throw new StorageValidationException(message); } if (databaseVersion > currentDatabaseVersion) { string m = string.Format(Resources.StorageSqLite_ValidateDatabaseVersion_DatabaseVersion_0_higher_then_current_DatabaseVersion_1_, databaseVersion, currentDatabaseVersion); var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(m); throw new StorageValidationException(message); } } catch (InvalidOperationException e) { var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(Resources.StorageSqLite_ValidateDatabaseVersion_Database_must_have_one_VersionEntity_row); throw new StorageValidationException(message, e); } } /// /// Attempts to set the connection to an existing storage file . /// /// Path to database file. /// is invalid. /// Thrown when: /// does not exist /// the database has an invalid schema. /// /// private static string GetConnectionToExistingFile(string databaseFilePath) { FileUtils.ValidateFilePath(databaseFilePath); return GetConnectionToFile(databaseFilePath); } /// /// Sets the connection to a newly created (empty) Ringtoets database file. /// /// Path to database file. /// Thrown when: /// /// is invalid /// points to an existing file /// /// Thrown when: /// executing DatabaseStructure script failed /// /// private static string GetConnectionToNewFile(string databaseFilePath) { FileUtils.ValidateFilePath(databaseFilePath); StorageSqliteCreator.CreateDatabaseStructure(databaseFilePath); return GetConnectionToFile(databaseFilePath); } /// /// Establishes a connection to an existing . /// /// The path of the database file to connect to. /// No file exists at . private static string GetConnectionToFile(string databaseFilePath) { if (!File.Exists(databaseFilePath)) { var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(UtilsResources.Error_File_does_not_exist); throw new CouldNotConnectException(message); } return GetConnectionToStorage(databaseFilePath); } /// /// Sets the connection to the Ringtoets database. /// /// The path of the file, which is used for creating exceptions. /// Thrown when the database does not contain the table version. private static string GetConnectionToStorage(string databaseFilePath) { var connectionString = SqLiteConnectionStringBuilder.BuildSqLiteEntityConnectionString(databaseFilePath); using (var dbContext = new RingtoetsEntities(connectionString)) { try { dbContext.Database.Initialize(true); dbContext.VersionEntities.Load(); } catch (Exception exception) { var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(Resources.StorageSqLite_LoadProject_Invalid_Ringtoets_database_file); throw new StorageValidationException(message, exception); } } return connectionString; } /// /// Throws a configured instance of when writing to the storage file failed. /// /// The path of the file that was attempted to connect with. /// The critical error message. /// Exception that caused this exception to be thrown. /// Returns a new . private static StorageException CreateStorageWriterException(string databaseFilePath, string errorMessage, Exception innerException) { var message = new FileWriterErrorMessageBuilder(databaseFilePath).Build(errorMessage); return new StorageException(message, innerException); } /// /// Throws a configured instance of when reading the storage file failed. /// /// The path of the file that was attempted to connect with. /// The critical error message. /// Exception that caused this exception to be thrown. /// Returns a new . private static StorageException CreateStorageReaderException(string databaseFilePath, string errorMessage, Exception innerException = null) { var message = new FileReaderErrorMessageBuilder(databaseFilePath).Build(errorMessage); return new StorageException(message, innerException); } private class StagedProject { public StagedProject(RingtoetsProject projectModel, ProjectEntity projectEntity) { Model = projectModel; Entity = projectEntity; } public RingtoetsProject Model { get; private set; } public ProjectEntity Entity { get; private set; } } } }