// 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);
}
}
}