-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRNGSessionDataFile.cs
More file actions
477 lines (429 loc) · 18.8 KB
/
Copy pathRNGSessionDataFile.cs
File metadata and controls
477 lines (429 loc) · 18.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
//*********************************************************************************************************************
// File Name: RNGSessionDataFile.cs
// Description: Interface to a Random Number Generator file
//
// Copyright (c) 2023-2024 Mike Pullen
// Licensed under the MIT License. See LICENSE in the repository root.
//
// Revision History:
//====================================================================================================================
// 2023/12/04 - Mike Pullen - Original implementation.
// 2026/08/31 - Mike Pullen - Report anything that needs raising about a loaded file through LastError
// 2026/09/08 - Mike Pullen - Appended to a file that already holds readings rather than writing over it, so
// recording a second session into the same file keeps the first
// 2026/09/09 - Mike Pullen - Said what the check on the file actually asks, which is whether anything has
// been written to it rather than whether it holds readings
//*********************************************************************************************************************
using System;
using System.IO;
namespace RandomNumberGenerator
{
/// <summary>
/// Interface for the RNG session file writer
/// </summary>
public interface IRNGSessionFileWriter
{
string FilePath { get; set; }
bool WriteDataPoint(IXMLDataPoint dataPoint);
bool WriteSessionEnd();
bool WriteSessionStart(bool bSimulated, int iTargetValue);
bool PrepareForAppend(string sFilePath);
}
/// <summary>
/// Interface for the RNG session file reader
/// </summary>
public interface IRNGSessionFileReader
{
string FilePath { get; set; }
string LastError { get; }
bool LoadFile(IRNGSessionData sessionData, uint uBatchSize = 1000);
void Close();
}
/// <summary>
/// Interface for the Random Number Generator session data file
/// </summary>
public interface IRNGSessionDataFile
{
string FilePath { get; set; }
string LastError { get; }
bool SessionInProgress { get; set; }
bool EndSession();
bool IsValid();
bool LoadSession(IRNGSessionData sessionData, string sFilePath);
bool StartSession(IRNGSessionData sessionData);
bool WriteDataPoint(IXMLDataPoint dataPoint);
}
/// <summary>
/// Represents a Random Number Generator session file
/// </summary>
public class RNGSessionDataFile : IRNGSessionDataFile
{
#region Constructors
/// <summary>
/// Construct with the writer and parent
/// </summary>
/// <param name="writer">IN - The XML writer object to use to for the file (cannot be null)</param>
public RNGSessionDataFile(IRNGSessionFileWriter writer)
{
// Writer object provided cannot be null
if (null == writer)
{
throw new ArgumentNullException(nameof(writer), "Specified writer object cannot be null");
}
m_Writer = writer;
}
/// <summary>
/// Construct with the writer and reader
/// </summary>
/// <param name="writer">IN - The XML writer object to use for the file (cannot be null)</param>
/// <param name="reader">IN - The XML reader object to use for the file (can be null)</param>
public RNGSessionDataFile(IRNGSessionFileWriter writer, IRNGSessionFileReader reader)
{
// Writer object provided cannot be null
if (null == writer)
{
throw new ArgumentNullException(nameof(writer), "Specified writer object cannot be null");
}
m_Writer = writer;
m_Reader = reader;
}
/// <summary>
/// Destructor. Ensures any pending session is ended.
/// </summary>
~RNGSessionDataFile()
{
// Closing the session writes to the file, which can fail. An exception escaping a finalizer
// brings the process down, so a failure here is given up on rather than reported, leaving the
// file to be recovered when it is next opened.
try
{
if (m_bSessionInProgress)
{
EndSession();
}
}
catch (Exception)
{
// Nothing can be done about it at this point
}
}
#endregion
#region Methods
/// <summary>
/// Starts a new session in the data file
/// </summary>
/// <param name="sessionData">IN - The data for the new session (cannot be null)</param>
/// <returns>true if successful; otherwise, false</returns>
public bool StartSession(IRNGSessionData sessionData)
{
// If a session is already in progress, there is nothing to do
if (true == m_bSessionInProgress)
{
return true;
}
// Session data object provided cannot be null
if (null == sessionData)
{
throw new ArgumentNullException(nameof(sessionData), "Specified data object cannot be null");
}
// Default the status to failure
bool bStatus = false;
try
{
// Check if the writer is valid
bool bValid = IsValid();
if (bValid)
{
// A file that has anything in it already is continued rather than started over. Starting
// a session opens the file for writing from the beginning, so doing that to a file with a
// session already in it would throw what it holds away without asking. That could not
// happen while ending a session also cleared the chosen file, because the only way back
// to a file was to load it, and loading prepares it for appending; now that the file
// stays chosen, pressing Start again has to be safe on its own.
bool bHasContent = FileHasContent(FilePath);
if (true == bHasContent)
{
// Reopens the file after whatever is already in it
PrepareWriterForAppend(FilePath);
bStatus = true;
}
else
{
// Write the session start, recording the session as in progress only once it is, so a
// write that reports failure does not leave the file looking like it holds a session
bStatus = m_Writer.WriteSessionStart(sessionData.Simulated, sessionData.TargetValue);
m_bSessionInProgress = bStatus;
}
}
else
{
// Invalid writer - likely no file path set
throw new InvalidOperationException("No data file selected. Please select a file before starting a session.");
}
}
catch (InvalidOperationException)
{
// Re-throw InvalidOperationException to be handled by calling code
m_bSessionInProgress = false;
throw;
}
catch (UnauthorizedAccessException)
{
// Re-throw UnauthorizedAccessException to be handled by calling code
m_bSessionInProgress = false;
throw;
}
catch (System.IO.IOException)
{
// Re-throw IO exceptions to be handled by calling code
m_bSessionInProgress = false;
throw;
}
catch (Exception)
{
// Re-throw other exceptions to be handled by calling code
m_bSessionInProgress = false;
throw;
}
return bStatus;
}
/// <summary>
/// Writes a data point to the file
/// </summary>
/// <param name="dataPoint">IN - The data point to write (cannot be null)</param>
/// <returns>true if successful; otherwise, false</returns>
public bool WriteDataPoint(IXMLDataPoint dataPoint)
{
// Data point object provided cannot be null
if (null == dataPoint)
{
throw new ArgumentNullException(nameof(dataPoint), "Specified data point object cannot be null");
}
// Default the status to failure
bool bStatus = false;
// Check if the writer is valid and a session is in progress
bool bValid = IsValid();
if (bValid && m_bSessionInProgress)
{
// Write the data point
bStatus = m_Writer.WriteDataPoint(dataPoint);
}
return bStatus;
}
/// <summary>
/// Loads session data from an existing XML file
/// </summary>
/// <param name="sessionData">INOUT - The session data object to load into (cannot be null)</param>
/// <param name="sFilePath">IN - Path to the session file to load (cannot be null or empty)</param>
/// <returns>true if successful; otherwise, false</returns>
public bool LoadSession(IRNGSessionData sessionData, string sFilePath)
{
// Validate parameters
if (null == sessionData)
{
throw new ArgumentNullException(nameof(sessionData), "Specified session data object cannot be null");
}
if (string.IsNullOrEmpty(sFilePath))
{
throw new ArgumentNullException(nameof(sFilePath), "Specified file path cannot be null or empty");
}
bool bStatus = false;
// Check if we have a reader available
if (null != m_Reader)
{
try
{
// Set the file path and load the session
m_Reader.FilePath = sFilePath;
bStatus = m_Reader.LoadFile(sessionData);
// If loading failed, throw an exception with the detailed error message
if (false == bStatus)
{
string sErrorMessage = m_Reader.LastError;
if (string.IsNullOrEmpty(sErrorMessage))
{
sErrorMessage = $"Unknown error loading file '{Path.GetFileName(sFilePath)}'.";
}
throw new InvalidDataException(sErrorMessage);
}
// Update our file path to match the loaded session
FilePath = sFilePath;
// Prepare the writer for appending to the loaded file
PrepareWriterForAppend(sFilePath);
}
catch (IOException ioException)
{
// Re-throw IO exceptions to be handled by calling code
throw new IOException($"File I/O error accessing '{Path.GetFileName(sFilePath)}': {ioException.Message}", ioException);
}
catch (InvalidDataException)
{
// Re-throw InvalidDataException (from XML reader errors) to be handled by calling code
throw;
}
catch (InvalidOperationException)
{
// Re-throw InvalidOperationException, which already describes the problem for the user,
// rather than wrapping it in a general message
throw;
}
catch (Exception generalException)
{
// Wrap other exceptions with file context
throw new Exception($"Unexpected error loading file '{Path.GetFileName(sFilePath)}': {generalException.Message}", generalException);
}
}
else
{
// No reader available - cannot load sessions
throw new InvalidOperationException("No XML reader available for loading session data. Use constructor with reader parameter.");
}
return bStatus;
}
/// <summary>
/// Whether anything has been written to a file already, which decides whether starting a session
/// continues it or begins it. A file that does not exist yet, or that exists with nothing in it, is
/// begun.
/// NOTE: This asks whether the file has anything in it, not whether any readings were recorded. A
/// session that was started and stopped without recording anything leaves a session element and no
/// data, and that file has to be continued rather than written over: the session element is already
/// there, and PrepareForAppend reopens it. Reading far enough to count the readings would mean
/// opening a file that grows by roughly 2MB an hour to answer a question whose answer does not
/// change what happens.
/// </summary>
/// <param name="sFilePath">IN - Path to the session file</param>
/// <returns>true if the file exists and has something in it; otherwise, false</returns>
private static bool FileHasContent(string sFilePath)
{
if (string.IsNullOrEmpty(sFilePath))
{
return false;
}
FileInfo sessionFile = new FileInfo(sFilePath);
return (sessionFile.Exists && (0 < sessionFile.Length));
}
/// <summary>
/// Prepares the writer for appending data to an existing loaded file
/// </summary>
/// <param name="sFilePath">IN - Path to the file to prepare for appending</param>
private void PrepareWriterForAppend(string sFilePath)
{
// Configure the writer for append mode with the specific file path
bool bPrepared = false;
if (null != m_Writer)
{
bPrepared = m_Writer.PrepareForAppend(sFilePath);
}
// Preparing the file has to succeed before the session can be recorded as in progress. Reporting
// the session as in progress when it is not would leave data being written through a writer that
// was never set up, and reporting it as not in progress would let the next session start
// overwrite the file that has just been loaded.
if (false == bPrepared)
{
throw new InvalidOperationException($"Unable to prepare '{Path.GetFileName(sFilePath)}' for" +
$" appending, so the session cannot be continued.");
}
// The file is already open and contains a session start tag
m_bSessionInProgress = true;
}
/// <summary>
/// Writes the session end and closes the file
/// </summary>
/// <returns>true if successful; otherwise, false</returns>
public bool EndSession()
{
// Default the status to failure
bool bStatus = false;
// Check if the writer is valid
bool bValid = IsValid();
if (bValid)
{
// Write the session end. The session is only recorded as closed once the writer confirms it,
// so a failure leaves it open to be closed again rather than leaving the file unterminated
// with nothing left that will try to finish it.
bStatus = m_Writer.WriteSessionEnd();
m_bSessionInProgress = (false == bStatus);
}
return bStatus;
}
/// <summary>
/// Checks that a valid file path has been set
/// </summary>
public bool IsValid()
{
// Default to invalid until proven otherwise
bool bValid = false;
// Check if the writer exists and has a valid file path
if (null != m_Writer)
{
// Check if a file path is set
if (!string.IsNullOrEmpty(m_Writer.FilePath))
{
// Only validate the file once
if (false == m_bFileValidated)
{
// Check if a file info object can be created from the writer's file property
try
{
// Creating the file info is what validates the path; the object itself is not
// needed, so it is discarded rather than left looking like it goes unused
_ = new FileInfo(m_Writer.FilePath);
bValid = true;
m_bFileValidated = true;
}
catch
{
// File path is not valid
bValid = false;
}
}
else
{
// File was previously validated
bValid = true;
}
}
// No file path set
else
{
bValid = false;
}
}
return bValid;
}
#endregion
#region Properties
/// <summary>
/// File path for this file
/// </summary>
public string FilePath
{
get => (null == m_Writer) ? "" : m_Writer.FilePath;
set
{
if (null != m_Writer)
{
// Set the new path and reset the file validation
m_Writer.FilePath = value;
m_bFileValidated = false;
}
}
}
/// <summary>
/// Description of the last problem encountered reading the file, which is also set when a file was
/// loaded but something about it needs to be reported. Empty when there is nothing to report.
/// </summary>
public string LastError { get => (null == m_Reader) ? string.Empty : m_Reader.LastError; }
/// <summary>
/// Specifies if a session is in progress (start has been written but not end)
/// </summary>
public bool SessionInProgress { get => m_bSessionInProgress; set => m_bSessionInProgress = value; }
#endregion
#region Data Members
private IRNGSessionFileWriter m_Writer = null;
private IRNGSessionFileReader m_Reader = null;
private bool m_bSessionInProgress = false;
private bool m_bFileValidated = false;
#endregion
}
}