// 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.Globalization; using System.IO; using System.Linq; using Core.Common.IO.Exceptions; using Core.Common.IO.Readers; using Core.Common.Utils; using Core.Common.Utils.Builders; using Ringtoets.Common.IO.Properties; using CoreCommonUtilsResources = Core.Common.Utils.Properties.Resources; namespace Ringtoets.Common.IO.Structures { /// /// File reader for a plain text file in comma-separated values format (*.csv) containing /// data specifying characteristics of height structures. /// public class StructuresCharacteristicsCsvReader : IDisposable { private const char separator = ';'; private readonly string filePath; private readonly string[] requiredHeaderColumns = { "identificatie", "kunstwerken.identificatie", "alphanumeriekewaarde", "numeriekewaarde", "standarddeviatie.variance", "boolean" }; private int locationIdIndex, parameterIdIndex, alphanumericValueIndex, numericValueIndex, varianceValueIndex, varianceTypeIndex; private int headerLength; private int lineNumber; private StreamReader fileReader; /// /// Creates a new instance of /// and opens a given file path. /// /// The path to the file to be read. /// Thrown when is invalid. public StructuresCharacteristicsCsvReader(string path) { FileUtils.ValidateFilePath(path); filePath = path; } /// /// Counts the number of parameter definitions found in the file. /// /// An integer greater than or equal to 0, being the number of parameter rows. /// File/directory cannot be found or /// some other I/O related problem occurred or the header is not in the required format. public int GetLineCount() { using (var reader = StreamReaderHelper.InitializeStreamReader(filePath)) { lineNumber = 1; ValidateHeader(reader); return CountNonEmptyLines(reader, 2); } } /// /// Reads the next structure parameter from file. /// /// The next based on the read file, /// or null when at the end of the file. /// /// Thrown when either: /// /// The file or directory cannot be found. /// The file is empty. /// Some I/O related problem occurred. /// The header is not in the required format. /// /// Thrown when either: /// /// The line does not contain the separator character. /// Location id field is empty or consists out of only white spaces. /// Parameter id field is empty or consists out of only white spaces. /// Numeric value field is not a number or too large/small to be represented as . /// Variance value field is not a number or too large/small to be represented as . /// Boolean field is not a valid value. /// public StructuresParameterRow ReadLine() { if (fileReader == null) { fileReader = StreamReaderHelper.InitializeStreamReader(filePath); lineNumber = 1; IndexFile(fileReader); lineNumber++; } string readText = ReadNextNonEmptyLine(fileReader); if (readText != null) { try { return CreateStructuresParameterRow(readText); } finally { lineNumber++; } } return null; } public void Dispose() { if (fileReader != null) { fileReader.Dispose(); fileReader = null; } } /// /// Validates the header of the file. /// /// The reader, which is currently at the header row. /// The header is not in the required format. private void ValidateHeader(TextReader reader) { string[] tokenizedHeader = GetTokenizedHeader(reader); const int uninitializedValue = -999; int[] requiredHeaderColumnIndices = GetRequiredHeaderColumnIndices(uninitializedValue, tokenizedHeader); ValidateRequiredColumnIndices(requiredHeaderColumnIndices, uninitializedValue); } /// /// Counts the remaining non-empty lines. /// /// The reader at the row from which counting should start. /// The current line, used for error messaging. /// An integer greater than or equal to 0, being the number of parameter rows. /// An I/O exception occurred. private int CountNonEmptyLines(TextReader reader, int currentLine) { int count = 0, lineNumberForMessage = currentLine; string line; while ((line = ReadLineAndHandleIOExceptions(reader, lineNumberForMessage)) != null) { if (!string.IsNullOrWhiteSpace(line)) { count++; } lineNumberForMessage++; } return count; } /// /// Reads the next line and handles I/O exceptions. /// /// The opened text file reader. /// Row number for error messaging. /// The read line, or null when at the end of the file. /// An critical I/O exception occurred. private string ReadLineAndHandleIOExceptions(TextReader reader, int currentLine) { try { return reader.ReadLine(); } catch (OutOfMemoryException e) { throw CreateCriticalFileReadException(currentLine, CoreCommonUtilsResources.Error_Line_too_big_for_RAM, e); } catch (IOException e) { string errorMessage = string.Format(CoreCommonUtilsResources.Error_General_IO_ErrorMessage_0_, e.Message); var fullErrorMessage = new FileReaderErrorMessageBuilder(filePath).Build(errorMessage); throw new CriticalFileReadException(fullErrorMessage, e); } } /// /// Reads the header and sets the internal indices of the required header columns. /// /// The reader used to read the file. /// The file is empty or some I/O exception /// occurred or the header is not in the required format. private void IndexFile(TextReader reader) { string[] tokenizedHeader = GetTokenizedHeader(reader); headerLength = tokenizedHeader.Length; const int uninitializedValue = -999; int[] requiredHeaderColumnIndices = GetRequiredHeaderColumnIndices(uninitializedValue, tokenizedHeader); ValidateRequiredColumnIndices(requiredHeaderColumnIndices, uninitializedValue); SetColumnIndices(requiredHeaderColumnIndices); } /// /// Tokenizes the file header. /// /// The reader used to read the file. /// The header split based on . /// The file is empty or some I/O exception /// occurred. private string[] GetTokenizedHeader(TextReader reader) { var header = ReadLineAndHandleIOExceptions(reader, lineNumber); if (header == null) { throw CreateCriticalFileReadException(lineNumber, CoreCommonUtilsResources.Error_File_empty); } return header.Split(separator) .Select(s => s.Trim().ToLowerInvariant()) .ToArray(); } private int[] GetRequiredHeaderColumnIndices(int initialColumnIndexValue, string[] tokenizedHeader) { int[] requiredHeaderColumnIndices = Enumerable.Repeat(initialColumnIndexValue, requiredHeaderColumns.Length) .ToArray(); for (int columnIndex = 0; columnIndex < tokenizedHeader.Length; columnIndex++) { string columnName = tokenizedHeader[columnIndex]; int index = Array.IndexOf(requiredHeaderColumns, columnName); if (index != -1) { if (requiredHeaderColumnIndices[index] == initialColumnIndexValue) { requiredHeaderColumnIndices[index] = columnIndex; } else { string message = string.Format(Resources.StructuresCharacteristicsCsvReader_Column_0_must_be_defined_only_once, columnName); throw CreateCriticalFileReadException(lineNumber, message); } } } return requiredHeaderColumnIndices; } /// /// Checks if all required header columns have been matched. /// /// The array of matched column indices. /// The initial index value put in . /// The header is not in the required format. private void ValidateRequiredColumnIndices(int[] requiredHeaderColumnIndices, int uninitializedValue) { if (requiredHeaderColumnIndices.Any(i => i == uninitializedValue)) { string message = string.Format(Resources.StructuresCharacteristicsCsvReader_ValidateRequiredColumnIndices_Invalid_header_Must_have_columns_0_, string.Join(", ", requiredHeaderColumns)); throw CreateCriticalFileReadException(lineNumber, message); } } private void SetColumnIndices(int[] requiredHeaderColumnIndices) { locationIdIndex = requiredHeaderColumnIndices[0]; parameterIdIndex = requiredHeaderColumnIndices[1]; alphanumericValueIndex = requiredHeaderColumnIndices[2]; numericValueIndex = requiredHeaderColumnIndices[3]; varianceValueIndex = requiredHeaderColumnIndices[4]; varianceTypeIndex = requiredHeaderColumnIndices[5]; } /// /// Reads lines from file until the first non-white line is hit. /// /// The next line which is not a white line, or null when no non-white /// line could be found before the end of file. /// An critical I/O exception occurred. private string ReadNextNonEmptyLine(StreamReader reader) { string readText; while ((readText = ReadLineAndHandleIOExceptions(reader, lineNumber)) != null) { if (string.IsNullOrWhiteSpace(readText)) { lineNumber++; } else { break; } } return readText; } /// /// Creates the structures parameter row. /// /// The read text. /// /// Thrown when either: /// /// does not contain the separator character. /// Location id field is empty or consists out of only white spaces. /// Parameter id field is empty or consists out of only white spaces. /// Numeric value field is not a number or too large/small to be represented as . /// Variance value field is not a number or too large/small to be represented as . /// Boolean field is not a valid value. /// private StructuresParameterRow CreateStructuresParameterRow(string readText) { string[] tokenizedText = TokenizeString(readText); if (tokenizedText.Length != headerLength) { string message = string.Format(Resources.StructuresCharacteristicsCsvReader_CreateStructuresParameterRow_Line_should_have_NumberOfExpectedElements_0_but_has_ActualNumberOfElements_1_, headerLength, tokenizedText.Length); throw CreateLineParseException(lineNumber, message); } string locationId = ParseLocationId(tokenizedText); string parameterId = ParseParameterId(tokenizedText); string alphanumericValue = ParseAlphanumericValue(tokenizedText); double numbericValue = ParseNumericValue(tokenizedText); double varianceValue = ParseVarianceValue(tokenizedText); VarianceType varianceType = ParseVarianceType(tokenizedText); return new StructuresParameterRow { LocationId = locationId, ParameterId = parameterId, AlphanumericValue = alphanumericValue, NumericalValue = numbericValue, VarianceValue = varianceValue, VarianceType = varianceType, LineNumber = lineNumber }; } /// /// Tokenizes a string using a separator character up to the first empty field. /// /// The text. /// The tokenized parts. /// lacks separator character. private string[] TokenizeString(string readText) { if (!readText.Contains(separator)) { string message = string.Format(Resources.StructuresCharacteristicsCsvReader_TokenizeString_Line_lacks_SeparatorCharacter_0_, separator); throw CreateLineParseException(lineNumber, message); } return readText.Split(separator) .ToArray(); } /// /// Parses the location identifier from the read text. /// /// The tokenized text. /// The location ID. /// Location ID field is empty or only has whitespaces. private string ParseLocationId(string[] tokenizedText) { string locationId = tokenizedText[locationIdIndex]; return ParseIdString(locationId, "Identificatie"); } /// /// Parses the parameter identifier from the read text. /// /// The tokenized text. /// /// The parameter ID. /// Parameter ID field is empty or only has whitespaces. private string ParseParameterId(string[] tokenizedText) { string parameterId = tokenizedText[parameterIdIndex]; return ParseIdString(parameterId, "Kunstwerken.identificatie"); } private string ParseIdString(string parameterTextValue, string parameterName) { if (string.IsNullOrWhiteSpace(parameterTextValue)) { string message = string.Format(Resources.StructuresCharacteristicsCsvReader_ParseIdString_ParameterName_0_cannot_be_empty, parameterName); throw CreateLineParseException(lineNumber, message); } return parameterTextValue; } private string ParseAlphanumericValue(string[] tokenizedText) { return tokenizedText[alphanumericValueIndex]; } /// /// Parses the numeric value. /// /// The tokenized text. /// The numeric value (can be ). /// When the numeric value field is not a number /// or when it's too large or too small to be represented as . private double ParseNumericValue(string[] tokenizedText) { string numericValueText = tokenizedText[numericValueIndex]; return ParseDoubleValue(numericValueText, "Nummerieke waarde"); } /// /// Parses the standard deviation or coefficient of variation value. /// /// The tokenized text. /// The standard deviation or coefficient of variation value (can be ). /// When the standard deviation or coefficient /// of variation value field is not a number or when it's too large or too small /// to be represented as . private double ParseVarianceValue(string[] tokenizedText) { string varianceValueText = tokenizedText[varianceValueIndex]; return ParseDoubleValue(varianceValueText, "Variantie waarde"); } /// /// Parses the double value. /// /// The value text to be parsed. /// Name of the parameter. /// when is null /// or only whitespaces; otherwise the parsed number. /// When is /// not a number or when it's too large or too small to be represented as . private double ParseDoubleValue(string doubleValueText, string parameterName) { if (string.IsNullOrWhiteSpace(doubleValueText)) { return double.NaN; } try { return double.Parse(doubleValueText, CultureInfo.InvariantCulture); } catch (FormatException e) { throw CreateLineParseException(lineNumber, string.Format(Resources.StructuresCharacteristicsCsvReader_ParseDoubleValue_ParameterName_0_not_number, parameterName), e); } catch (OverflowException e) { throw CreateLineParseException(lineNumber, string.Format(Resources.StructuresCharacteristicsCsvReader_ParseDoubleValue_ParameterName_0_overflow_error, parameterName), e); } } /// /// Parses the value that indicates how the variance field should be interpreted. /// /// The tokenized text. /// The based on the text in the file. /// When the 'boolean' field is not a valid value. private VarianceType ParseVarianceType(string[] tokenizedText) { string varianceTypeText = tokenizedText[varianceTypeIndex]; if (string.IsNullOrWhiteSpace(varianceTypeText)) { return VarianceType.NotSpecified; } try { int typeValue = int.Parse(varianceTypeText, CultureInfo.InvariantCulture); if (typeValue == 0) { return VarianceType.CoefficientOfVariation; } if (typeValue == 1) { return VarianceType.StandardDeviation; } throw CreateLineParseException(lineNumber, Resources.StructuresCharacteristicsCsvReader_ParseVarianceType_Column_only_allows_certain_values); } catch (FormatException e) { throw CreateLineParseException(lineNumber, Resources.StructuresCharacteristicsCsvReader_ParseVarianceType_Column_only_allows_certain_values, e); } catch (OverflowException e) { throw CreateLineParseException(lineNumber, Resources.StructuresCharacteristicsCsvReader_ParseVarianceType_Column_only_allows_certain_values, e); } } /// /// Throws a configured instance of . /// /// The line number being read. /// The critical error message. /// Optional: exception that caused this exception to be thrown. /// New with message set. private LineParseException CreateLineParseException(int currentLine, string lineParseErrorMessage, Exception innerException = null) { string locationDescription = string.Format(CoreCommonUtilsResources.TextFile_On_LineNumber_0_, currentLine); var message = new FileReaderErrorMessageBuilder(filePath).WithLocation(locationDescription) .Build(lineParseErrorMessage); return new LineParseException(message, innerException); } /// /// Throws a configured instance of . /// /// The line number being read. /// The critical error message. /// Optional: exception that caused this exception to be thrown. /// New with message and inner exception set. private CriticalFileReadException CreateCriticalFileReadException(int currentLine, string criticalErrorMessage, Exception innerException = null) { string locationDescription = string.Format(CoreCommonUtilsResources.TextFile_On_LineNumber_0_, currentLine); var message = new FileReaderErrorMessageBuilder(filePath).WithLocation(locationDescription) .Build(criticalErrorMessage); return new CriticalFileReadException(message, innerException); } } }