using System; using System.Data; using System.Data.SQLite; using System.IO; using System.Xml; using Ringtoets.Piping.Data; using Ringtoets.Piping.IO.Builders; using Ringtoets.Piping.IO.Exceptions; using Ringtoets.Piping.IO.Properties; namespace Ringtoets.Piping.IO { /// /// This class reads a SqLite database file and constructs instances from this database. /// public class PipingSoilProfileReader : IDisposable { private const string databaseRequiredVersion = "15.0.5.0"; private const string pipingMechanismName = "Piping"; private const string profileCountColumn = "ProfileCount"; private const string dimensionColumn = "Dimension"; private const string isAquiferColumn = "IsAquifer"; private const string profileNameColumn = "ProfileName"; private const string intersectionXColumn = "IntersectionX"; private const string bottomColumn = "Bottom"; private const string topColumn = "Top"; private const string layerGeometryColumn = "LayerGeometry"; private const string abovePhreaticLevelColumn = "AbovePhreaticLevel"; private const string belowPhreaticLevelColumn = "BelowPhreaticLevel"; private const string dryUnitWeightColumn = "DryUnitWeight"; private const string layerCountColumn = "LayerCount"; private const string mechanismParameterName = "mechanism"; private readonly string databaseFileName; private SQLiteConnection connection; private SQLiteDataReader dataReader; /// /// Creates a new instance of which will use the /// as its source. The reader will not point to any record at the start. Use to start reading /// profiles. /// /// The path of the database file to open. /// Thrown when the given is invalid. /// Thrown when the given didn't point to an existing file. public PipingSoilProfileReader(string databaseFilePath) { FileUtils.ValidateFilePath(databaseFilePath); if (!File.Exists(databaseFilePath)) { throw new FileNotFoundException(String.Format(Resources.Error_File_0_does_not_exist, databaseFilePath)); } databaseFileName = Path.GetFileName(databaseFilePath); OpenConnection(databaseFilePath); SetReaderToFirstRecord(); } /// /// Gets the total number of profiles that can be read from the database. /// public int Count { get; private set; } /// /// Gets the value true if profiles can be read using the . /// false otherwise. /// public bool HasNext { get; private set; } /// /// Reads the information for the next profile from the database and creates a instance /// of the information. /// /// The next from the database, or null if no more profiles can be read. /// Thrown when reading the profile in the database contained a non-parsable geometry. /// Thrown when the database returned incorrect values for required properties. public PipingSoilProfile ReadProfile() { if (!HasNext) { return null; } var dimensionValue = Read(dimensionColumn); return dimensionValue == 1 ? ReadPipingProfile1D() : ReadPipingProfile2D(); } public void Dispose() { if (dataReader != null) { dataReader.Dispose(); } connection.Close(); connection.Dispose(); } /// /// Moves the reader to the next record in the database. /// private void MoveNext() { HasNext = dataReader.Read() || (dataReader.NextResult() && dataReader.Read()); } private PipingSoilProfile ReadPipingProfile1D() { var profileName = Read(profileNameColumn); var layerCount = Read(layerCountColumn); double bottom; try { bottom = Read(bottomColumn); } catch (InvalidCastException e) { SkipRecords((int)layerCount); var message = string.Format(Resources.PipingSoilProfileReader_Profile_0_has_invalid_value_on_column_1_, profileName, intersectionXColumn); throw new PipingSoilProfileReadException(message, e); } var soilProfileBuilder = new SoilProfileBuilder1D(profileName, bottom); for (var i = 1; i <= layerCount; i++) { soilProfileBuilder.Add(ReadPipingSoilLayer().AsPipingSoilLayer()); MoveNext(); } return soilProfileBuilder.Build(); } /// /// Reads information for a profile from the database and creates a based on the information. /// /// A new with information from the database. /// Thrown when /// /// a layer's geometry could not be parsed as XML; /// the parsed geometry did not contain loops; /// after reading the layers, no layers were added to be build; /// unexpected values were encountered for layer properties /// /// /// Thrown when no valid point to obtain a one-dimensional /// intersection from was read from the database. private PipingSoilProfile ReadPipingProfile2D() { var profileName = Read(profileNameColumn); var layerCount = Read(layerCountColumn); try { double intersectionX; try { intersectionX = TryRead(intersectionXColumn); } catch (InvalidCastException e) { SkipRecords((int)layerCount); var message = string.Format(Resources.PipingSoilProfileReader_Profile_0_has_invalid_value_on_column_1_, profileName, intersectionXColumn); throw new PipingSoilProfileReadException(message, e); } var soilProfileBuilder = new SoilProfileBuilder2D(profileName, intersectionX); for (int i = 1; i <= layerCount; i++) { try { soilProfileBuilder.Add(ReadPiping2DSoilLayer()); } catch (XmlException e) { SkipRecords((int)layerCount + 1 - i); var format = string.Format(Resources.PipingSoilProfileReader_CouldNotParseGeometryOfLayer_0_InProfile_1_, i, profileName); throw new PipingSoilProfileReadException( format, e); } catch (SoilProfileBuilderException e) { SkipRecords((int) layerCount + 1 - i); var format = string.Format(Resources.PipingSoilProfileReader_CouldNotParseGeometryOfLayer_0_InProfile_1_, i, profileName); throw new PipingSoilProfileReadException( format, e); } catch (ArgumentException e) { SkipRecords((int) layerCount + 1 - i); var format = string.Format(Resources.PipingSoilProfileReader_CouldNotParseGeometryOfLayer_0_InProfile_1_, i, profileName); throw new PipingSoilProfileReadException( format, e); } catch (InvalidCastException e) { SkipRecords((int)layerCount + 1 - i); var message = string.Format(Resources.PipingSoilProfileReader_Profile_0_has_invalid_value_on_column_1_, profileName, intersectionXColumn); throw new PipingSoilProfileReadException(message, e); } MoveNext(); } try { return soilProfileBuilder.Build(); } catch (SoilProfileBuilderException e) { SkipRecords((int) 0 + 1 - 1); var exception = new PipingSoilProfileReadException( string.Format(Resources.PipingSoilProfileReader_CouldNotParseGeometryOfLayer_0_InProfile_1_, 1, profileName), e); throw exception; } } catch (ArgumentException e) { HandleCriticalBuildException(profileName, e); } return null; } private static void HandleCriticalBuildException(string profileName, Exception e) { var message = string.Format(Resources.PipingSoilProfileReader_Could_not_build_profile_0_from_layer_definitions, profileName); throw new CriticalFileReadException(message, e); } private void SkipRecords(int count) { for(int i = 0; i < count; i++) { MoveNext(); } } /// /// Reads a value at column from the database. /// /// The expected type of value in the column with name . /// The name of the column to read from. /// The read value from the column with name . /// Thrown when the value in the column was not of type . private T TryRead(string columnName) { var dbValue = dataReader[columnName]; var isNullable = typeof(T).IsGenericType && typeof(Nullable<>).IsAssignableFrom(typeof(T).GetGenericTypeDefinition()); if (isNullable && dbValue == DBNull.Value) { return default(T); } return (T)dbValue; } /// /// Reads a value at column from the database. /// /// The expected type of value in the column with name . /// The name of the column to read from. /// The read value from the column with name . /// Thrown when the value in the column was not of type . private T Read(string columnName) { try { return (T)dataReader[columnName]; } catch (InvalidCastException) { throw new CriticalFileReadException(string.Format(Resources.PipingSoilProfileReader_Critical_Unexpected_value_on_column_0_, columnName)); } } private SoilLayer1D ReadPipingSoilLayer() { var topValue = TryRead(topColumn); var isAquiferValue = TryRead(isAquiferColumn); var belowPhreaticLevelValue = TryRead(belowPhreaticLevelColumn); var abovePhreaticLevelValue = TryRead(abovePhreaticLevelColumn); var dryUnitWeightValue = TryRead(dryUnitWeightColumn); var pipingSoilLayer = new SoilLayer1D(topValue) { IsAquifer = isAquiferValue, BelowPhreaticLevel = belowPhreaticLevelValue, AbovePhreaticLevel = abovePhreaticLevelValue, DryUnitWeight = dryUnitWeightValue }; return pipingSoilLayer; } /// /// Reads a soil layer from a 2d profile /// /// A new instance, based on the information read from the database. /// Thrown when a column did not contain a value of the expected type. /// Thrown when the read geometry does not contain segments that form form /// a loop for either the inner or outer loop. private SoilLayer2D ReadPiping2DSoilLayer() { var geometryValue = TryRead(layerGeometryColumn); var isAquiferValue = TryRead(isAquiferColumn); var belowPhreaticLevelValue = TryRead(belowPhreaticLevelColumn); var abovePhreaticLevelValue = TryRead(abovePhreaticLevelColumn); var dryUnitWeightValue = TryRead(dryUnitWeightColumn); SoilLayer2D pipingSoilLayer = new PipingSoilLayer2DReader(geometryValue).Read(); pipingSoilLayer.IsAquifer = isAquiferValue; pipingSoilLayer.BelowPhreaticLevel = belowPhreaticLevelValue; pipingSoilLayer.AbovePhreaticLevel = abovePhreaticLevelValue; pipingSoilLayer.DryUnitWeight = dryUnitWeightValue; return pipingSoilLayer; } private void SetReaderToFirstRecord() { InitializeDataReader(); MoveNext(); } private void OpenConnection(string dbFile) { var connectionStringBuilder = new SQLiteConnectionStringBuilder { FailIfMissing = true, DataSource = dbFile, ReadOnly = true, ForeignKeys = true }; connection = new SQLiteConnection(connectionStringBuilder.ConnectionString); Connect(); } private void Connect() { try { connection.Open(); } catch (SQLiteException) { connection.Dispose(); } } /// /// Prepares the two queries required for obtaining all the SoilProfile1D and SoilProfile2D with an x defined /// to take an intersection from. Since two separate queries are used, the will /// have two result sets which the method takes into account. /// private void InitializeDataReader() { string versionQuery = string.Format( "SELECT Value FROM _Metadata WHERE Key = 'VERSION' AND Value = '{0}';", databaseRequiredVersion ); string countQuery = string.Format(string.Join( " ", "SELECT", "(SELECT COUNT(*)", "FROM Mechanism as m", "JOIN MechanismPointLocation as mpl ON mpl.ME_ID = m.ME_ID", "JOIN SoilProfile2D as p2 ON p2.SP2D_ID = mpl.SP2D_ID", "WHERE m.ME_Name = @{0})", " + ", "(SELECT COUNT(*)", "FROM SoilProfile1D) as {1};"), mechanismParameterName, profileCountColumn); string materialPropertiesQuery = string.Format( string.Join(" ", "(SELECT", "m.MA_ID,", "sum(case when pn.PN_Name = 'AbovePhreaticLevel' then pv.PV_Value end) {0},", "sum(case when pn.PN_Name = 'BelowPhreaticLevel' then pv.PV_Value end) {1},", "sum(case when pn.PN_Name = 'DryUnitWeight' then pv.PV_Value end) {2}", "FROM ParameterNames as pn", "JOIN ParameterValues as pv ON pn.PN_ID = pv.PN_ID", "JOIN Materials as m ON m.MA_ID = pv.MA_ID", "GROUP BY m.MA_ID) as mat ON l.MA_ID = mat.MA_ID"), abovePhreaticLevelColumn, belowPhreaticLevelColumn, dryUnitWeightColumn); string layer1DCountQuery = string.Format( string.Join(" ", "(SELECT SP1D_ID, COUNT(*) as {0}", "FROM SoilLayer1D", "GROUP BY SP1D_ID) lc ON lc.SP1D_ID = p.SP1D_ID"), layerCountColumn); string layer2DCountQuery = string.Format( string.Join(" ", "(SELECT SP2D_ID, COUNT(*) as {0}", "FROM SoilLayer2D", "GROUP BY SP2D_ID) lc ON lc.SP2D_ID = p.SP2D_ID"), layerCountColumn); string layer1DPropertiesQuery = string.Format( string.Join(" ", "(SELECT", "pv.SL1D_ID,", "sum(case when pn.PN_Name = 'IsAquifer' then pv.PV_Value end) {0}", "FROM ParameterNames as pn", "JOIN LayerParameterValues as pv ON pn.PN_ID = pv.PN_ID", "GROUP BY pv.SL1D_ID) as lpv ON lpv.SL1D_ID = l.SL1D_ID" ), isAquiferColumn); string layer2DPropertiesQuery = string.Format( string.Join(" ", "(SELECT", "pv.SL2D_ID,", "sum(case when pn.PN_Name = 'IsAquifer' then pv.PV_Value end) {0}", "FROM ParameterNames as pn", "JOIN LayerParameterValues as pv ON pn.PN_ID = pv.PN_ID", "GROUP BY pv.SL2D_ID) as lpv ON lpv.SL2D_ID = l.SL2D_ID" ), isAquiferColumn); var query1D = string.Format( string.Join(" ", "SELECT", "1 as {0},", "p.SP1D_Name as {1},", "lc.{2},", "p.BottomLevel as {3},", "l.TopLevel as {4},", "{5},", "{6},", "{7},", "{8}", "FROM SoilProfile1D as p", "JOIN {9}", "JOIN SoilLayer1D as l ON l.SP1D_ID = p.SP1D_ID", "LEFT JOIN {10}", "LEFT JOIN {11}", "ORDER BY ProfileName;"), dimensionColumn, profileNameColumn, layerCountColumn, bottomColumn, topColumn, abovePhreaticLevelColumn, belowPhreaticLevelColumn, dryUnitWeightColumn, isAquiferColumn, layer1DCountQuery, materialPropertiesQuery, layer1DPropertiesQuery); var query2D = string.Format( string.Join(" ", "SELECT", "2 as {0},", "p.SP2D_Name as {1},", "lc.{2},", "l.GeometrySurface as {3}, ", "mpl.X as {4},", "{5},", "{6},", "{7},", "{8}", "FROM Mechanism as m", "JOIN MechanismPointLocation as mpl ON mpl.ME_ID = m.ME_ID", "JOIN SoilProfile2D as p ON p.SP2D_ID = mpl.SP2D_ID", "JOIN {9}", "JOIN SoilLayer2D as l ON l.SP2D_ID = p.SP2D_ID", "LEFT JOIN {10}", "LEFT JOIN {11}", "WHERE m.ME_Name = @{12}", "ORDER BY ProfileName;"), dimensionColumn, profileNameColumn, layerCountColumn, layerGeometryColumn, intersectionXColumn, abovePhreaticLevelColumn, belowPhreaticLevelColumn, dryUnitWeightColumn, isAquiferColumn, layer2DCountQuery, materialPropertiesQuery, layer2DPropertiesQuery, mechanismParameterName); CreateDataReader(versionQuery + countQuery + query2D + query1D, new SQLiteParameter { DbType = DbType.String, Value = pipingMechanismName, ParameterName = mechanismParameterName }); } /// /// Creates a new data reader to use in this class. /// /// Thrown when /// /// Version of the database doesn't match the required version. /// Version of the database could not be read. /// Amount of profiles in database could not be read. /// A query could not be executed on the database schema. /// /// private void CreateDataReader(string queryString, params SQLiteParameter[] parameters) { using (var query = new SQLiteCommand(connection) { CommandText = queryString }) { query.Parameters.AddRange(parameters); try { dataReader = query.ExecuteReader(); CheckVersion(); GetCount(); } catch (SQLiteException e) { Dispose(); var exception = new PipingSoilProfileReadException(string.Format(Resources.Error_SoilProfile_read_from_database, databaseFileName), e); throw exception; } } } /// /// Checks the version read from the metadata table against the . /// /// Thrown when versions don't match. private void CheckVersion() { if (!dataReader.HasRows) { Dispose(); throw new PipingSoilProfileReadException(string.Format( Resources.PipingSoilProfileReader_Database_file_0_incorrect_version_requires_1, databaseFileName, databaseRequiredVersion)); } dataReader.NextResult(); } private void GetCount() { dataReader.Read(); Count = (int)Read(profileCountColumn); dataReader.NextResult(); } } }