// 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.IO; using System.Linq; using Core.Common.IO.Exceptions; using Core.Common.IO.Readers; using Core.Common.Utils; using Core.Common.Utils.Builders; 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", "numeriekeWaarde", "standarddeviatie.variance", "boolean" }; private int locationIdIndex, parameterIdIndex, numericValueIndex, varianceValueIndex, varianceTypeIndex; 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)) { ValidateHeader(reader, 1); 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. /// Row index used in error messaging. /// The header is not in the required format. private void ValidateHeader(TextReader reader, int currentLine) { string[] tokenizedHeader = GetTokenizedHeader(reader); const int uninitializedValue = -999; int[] requiredHeaderColumnIndices = GetRequiredHeaderColumnIndices(uninitializedValue, tokenizedHeader); ValidateRequiredColumnIndices(currentLine, 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((string)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); const int uninitializedValue = -999; int[] requiredHeaderColumnIndices = GetRequiredHeaderColumnIndices(uninitializedValue, tokenizedHeader); ValidateRequiredColumnIndices(lineNumber, 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) { // TODO: same column multiple times! requiredHeaderColumnIndices[index] = columnIndex; } } return requiredHeaderColumnIndices; } /// /// /// /// /// /// /// The header is not in the required format. private void ValidateRequiredColumnIndices(int currentLine, int[] requiredHeaderColumnIndices, int uninitializedValue) { if (requiredHeaderColumnIndices.Any(i => i == uninitializedValue)) { throw CreateCriticalFileReadException(currentLine, string.Format("Het bestand is niet geschikt om kunstwerken parameters uit te lezen (Verwachte koptekst moet de volgende kolommen bevatten: {0}.", string.Join(", ", requiredHeaderColumns))); } } private void SetColumnIndices(int[] requiredHeaderColumnIndices) { locationIdIndex = requiredHeaderColumnIndices[0]; parameterIdIndex = requiredHeaderColumnIndices[1]; numericValueIndex = requiredHeaderColumnIndices[2]; varianceValueIndex = requiredHeaderColumnIndices[3]; varianceTypeIndex = requiredHeaderColumnIndices[4]; } /// /// 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); // TODO: tokenizedText.Length smaller than largest index value => LineParseException string locationId = ParseLocationId(tokenizedText); string parameterId = ParseParameterId(tokenizedText); double numbericValue = ParseNumericValue(tokenizedText); double varianceValue = ParseVarianceValue(tokenizedText); VarianceType varianceType = ParseVarianceType(tokenizedText); return new StructuresParameterRow { LocationId = locationId, ParameterId = parameterId, NumericalValue = numbericValue, VarianceValue = varianceValue, VarianceType = varianceType }; } /// /// 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)) { throw CreateLineParseException(lineNumber, string.Format("Regel ontbreekt het verwachte scheidingsteken (het karakter: {0}).", separator)); } return readText.Split(separator) .TakeWhile(text => !string.IsNullOrEmpty(text)) .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]; if (string.IsNullOrWhiteSpace(locationId)) { throw CreateLineParseException(lineNumber, "'Identificatie' kolom mag geen lege waardes bevatten."); } return locationId; } /// /// 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]; if (string.IsNullOrWhiteSpace(parameterId)) { throw CreateLineParseException(lineNumber, "'Kunstwerken.identificatie' kolom mag geen lege waardes bevatten."); } return parameterId; } /// /// 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); } catch (FormatException e) { throw CreateLineParseException(lineNumber, string.Format("{0} kan niet worden omgezet naar een getal.", parameterName), e); } catch (OverflowException e) { throw CreateLineParseException(lineNumber, string.Format("{0} is te groot of te klein om ingelezen te worden.", 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); if (typeValue == 0) { return VarianceType.CoefficientOfVariation; } if (typeValue == 1) { return VarianceType.StandardDeviation; } throw CreateLineParseException(lineNumber, "De 'Boolean' kolom mag uitsluitend de waardes '0' of '1' bevatten, of mag leeg zijn."); } catch (FormatException e) { throw CreateLineParseException(lineNumber, "De 'Boolean' kolom mag uitsluitend de waardes '0' of '1' bevatten, of mag leeg zijn.", e); } catch (OverflowException e) { throw CreateLineParseException(lineNumber, "De 'Boolean' kolom mag uitsluitend de waardes '0' of '1' bevatten, of mag leeg zijn.", 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); } } }