Dryad/JobBrowser/Tools/tools.cs

4329 lines
151 KiB
C#

/*
Copyright (c) Microsoft Corporation
All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License
at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF
TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions and
limitations under the License.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing; // for color
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Security.Cryptography;
// Implement here generally-useful tools.
namespace Microsoft.Research.Tools
{
/// <summary>
/// An error handling function.
/// </summary>
/// <param name="message">Message to display.</param>
/// <param name="messageKind">Kind of message.</param>
public delegate void StatusReporter(string message, StatusKind messageKind);
/// <summary>
/// Kind of status displayed.
/// </summary>
public enum StatusKind
{
/// <summary>
/// Everything is fine.
/// </summary>
OK,
/// <summary>
/// Some error occurred.
/// </summary>
Error,
/// <summary>
/// A new long-term operation is initiated.
/// </summary>
LongOp,
};
/// <summary>
/// Communication management with background activities.
/// </summary>
public struct CommManager
{
/// <summary>
/// Used to report status.
/// </summary>
public StatusReporter Status;
/// <summary>
/// Used to report progress.
/// </summary>
public Action<int> Progress;
/// <summary>
/// Used to cancel activities.
/// </summary>
public CancellationToken Token;
/// <summary>
/// Create a communication manager.
/// </summary>
/// <param name="status">Status to report errors.</param>
/// <param name="progress">Action to report progress.</param>
/// <param name="token">Token to cancel computations.</param>
public CommManager(StatusReporter status, Action<int> progress, CancellationToken token)
{
this.Status = status;
this.Progress = progress;
this.Token = token;
}
}
/// <summary>
/// Untyped version of work item.
/// </summary>
public interface IBackgroundWorkItem
{
/// <summary>
/// Description of the work item.
/// </summary>
string Description { get; }
/// <summary>
/// Perform the background work.
/// </summary>
/// <param name="queue">Queue for work.</param>
/// <param name="reporter">Delegate used to report errors.</param>
/// <param name="progressReporter">Delegate used to report progress.</param>
/// <param name="cancel">If true for an item in the queue, cancel it.</param>
void Queue(BackgroundWorkQueue queue, StatusReporter reporter, Action<int> progressReporter, Func<IBackgroundWorkItem, bool> cancel);
/// <summary>
/// True if the item has been cancelled.
/// </summary>
bool Cancelled { get; }
/// <summary>
/// Cancel this item.
/// </summary>
void Cancel();
/// <summary>
/// Run the computation (called on a background thread).
/// </summary>
void Run();
/// <summary>
/// Run the continuation (called on the foreground thread).
/// <param name="ex">Exception that occurred during background work (or null).</param>
/// </summary>
void RunContinuation(Exception ex);
/// <summary>
/// Can be used to cancel this work item.
/// </summary>
CancellationTokenSource TokenSource { get; }
}
/// <summary>
/// A piece of work to be performed in the background.
/// </summary>
/// <typeparam name="T">Type of result from computation.</typeparam>
public class BackgroundWorkItem<T> : IBackgroundWorkItem
{
/// <summary>
/// Computation to invoke. If the computation is not cancelled the result is passed as the second argument to the continuation.
/// </summary>
public Func<CommManager, T> Computation { get; protected set; }
/// <summary>
/// Function to call when the work is completed. The first argument is 'true' if the computation was not cancelled. The second argument is the result of the computation.
/// </summary>
public Action<bool, T> Continuation { get; protected set; }
/// <summary>
/// Structure used to signal to the Computation.
/// </summary>
StatusReporter reporter;
/// <summary>
/// Progress reporter.
/// </summary>
private Action<int> progress;
/// <summary>
/// Result of background computation.
/// </summary>
T Result;
/// <summary>
/// Description of the background work.
/// </summary>
public string Description { get; protected set; }
/// <summary>
/// True if item has been cancelled.
/// </summary>
public bool Cancelled { get; protected set; }
/// <summary>
/// Queue containing item.
/// </summary>
private BackgroundWorkQueue queue;
/// <summary>
/// Source for cancellation token.
/// </summary>
public CancellationTokenSource TokenSource { get; protected set; }
// ReSharper disable ConvertToConstant.Local
bool TraceAsync =
// ReSharper restore ConvertToConstant.Local
#if DEBUG_WORKQUEUE
#else
false;
#endif
// ReSharper disable once StaticFieldInGenericType
private static int crtid;
/// <summary>
/// Create a background work item.
/// </summary>
/// <param name="computation">Computation to perform on a background thread. Ideally this should always be a static method.</param>
/// <param name="continuation">Continuation to invoke on the foreground thread when work is done.</param>
/// <param name="description">Description of the background work.</param>
public BackgroundWorkItem(Func<CommManager, T> computation, Action<bool, T> continuation, string description)
{
this.Description = description;
this.Computation = computation;
this.Continuation = continuation;
this.reporter = null;
this.queue = null;
this.Id = crtid++;
this.TokenSource = new CancellationTokenSource();
}
/// <summary>
/// Perform the background work.
/// </summary>
/// <param name="q">Worker which does the work.</param>
/// <param name="rep">Delegate used to report errors.</param>
/// <param name="progressReporter">Delegate used to report progress.</param>
/// <param name="cancel">If true for an item, cancel it.</param>
public void Queue(BackgroundWorkQueue q, StatusReporter rep, Action<int> progressReporter, Func<IBackgroundWorkItem, bool> cancel)
{
if (TraceAsync)
Console.WriteLine("{0} Queueing {1}", Utilities.PreciseTime, this.Description);
this.queue = q;
this.reporter = rep;
this.progress = progressReporter;
this.queue.CancelMatching(cancel);
this.queue.Enqueue(this);
}
/// <summary>
/// Run the computation; called on the background thread.
/// </summary>
public void Run()
{
DateTime startTime = DateTime.Now;
if (TraceAsync)
Console.WriteLine("{0} Running function {1}", Utilities.PreciseTime, this.Description);
try
{
CommManager manager = new CommManager(this.reporter, this.progress, this.TokenSource.Token);
this.Result = this.Computation(manager);
}
catch (Exception ex)
{
this.reporter(this.Description + " failed with " + ex.Message, StatusKind.Error);
Console.WriteLine(ex);
this.Cancelled = true;
}
DateTime endTime = DateTime.Now;
TimeSpan duration = endTime - startTime;
Console.WriteLine("Operation <" + this.Description + "> took " + duration);
}
/// <summary>
/// Run the continuation; called on the foreground thread.
/// </summary>
/// <param name="ex">Exception that occurred during background work.</param>
public void RunContinuation(Exception ex)
{
if (ex != null)
this.reporter(this.Id + ": Exception during background work: " + ex.Message, StatusKind.Error);
if (TraceAsync)
Console.WriteLine("{0} Running continuation of {1}", Utilities.PreciseTime, this.Description);
this.Continuation(this.Cancelled, this.Result);
}
/// <summary>
/// Cancel the work.
/// </summary>
public void Cancel()
{
if (TraceAsync)
Console.WriteLine("{1}/{0}: Cancelling", this.Description, this.Id);
this.Cancelled = true;
this.TokenSource.Cancel();
this.queue.CancelMe(this);
}
/// <summary>
/// Unique id of this work item.
/// </summary>
public int Id
{
get;
protected set;
}
}
/// <summary>
/// Maintains a queue of tasks for a background worker.
/// </summary>
public class BackgroundWorkQueue
{
/// <summary>
/// Worker performing the work.
/// </summary>
public BackgroundWorker BackgroundWorker { get; protected set; }
/// <summary>
/// Queue of items waiting for worker.
/// </summary>
readonly List<IBackgroundWorkItem> queue;
/// <summary>
/// Currently executing work item.
/// </summary>
IBackgroundWorkItem current;
private ToolStripStatusLabel currentItemLabel, queueSizeLabel;
/// <summary>
/// Create a background work queue servicing a specified worker.
/// </summary>
/// <param name="worker">Worker to use.</param>
/// <param name="current">Label where the current work is displayed.</param>
/// <param name="queue">Label where the queue size is displayed.</param>
public BackgroundWorkQueue(BackgroundWorker worker, ToolStripStatusLabel current, ToolStripStatusLabel queue)
{
this.currentItemLabel = current;
this.queueSizeLabel = queue;
if (worker == null)
throw new ArgumentNullException("worker");
this.BackgroundWorker = worker;
this.BackgroundWorker.WorkerSupportsCancellation = true;
this.BackgroundWorker.RunWorkerCompleted += this.worker_RunWorkerCompleted;
this.BackgroundWorker.DoWork += this.worker_DoWork;
this.queue = new List<IBackgroundWorkItem>();
this.current = null;
this.stopped = false;
}
/// <summary>
/// Called on background thread to do the work specified by the 'current' item.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="e">Unused.</param>
void worker_DoWork(object sender, DoWorkEventArgs e)
{
if (this.stopped || this.current == null)
return;
#if DEBUG_WORKQUEUE
#endif
if (!this.current.Cancelled)
{
this.current.Run();
}
else
{
#if DEBUG_WORKQUEUE
#endif
}
if (this.BackgroundWorker.CancellationPending)
e.Cancel = true;
}
private bool stopped;
/// <summary>
/// Called when the worker is completed.
/// </summary>
/// <param name="sender">Unused.</param>
/// <param name="e">Event describing completion.</param>
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (this.current != null)
{
#if DEBUG_WORKQUEUE
#endif
IBackgroundWorkItem crt = this.current;
this.current = null;
if (this.currentItemLabel != null)
this.currentItemLabel.Text = "";
crt.RunContinuation(e.Error);
}
this.Kick();
}
/// <summary>
/// Add an item to the work queue.
/// </summary>
/// <param name="item">Item to add.</param>
internal void Enqueue(IBackgroundWorkItem item)
{
this.queue.Add(item);
this.Kick();
}
/// <summary>
/// Try to run one more item from the queue.
/// </summary>
public void Kick()
{
if (this.BackgroundWorker.IsBusy)
return;
if (this.queue.Count == 0)
return;
if (this.current != null)
throw new Exception("current is not null");
this.current = this.queue[0];
if (this.currentItemLabel != null)
this.currentItemLabel.Text = "Doing " + this.current.Description;
this.queue.RemoveAt(0);
if (this.queueSizeLabel != null)
this.queueSizeLabel.Text = "Pending " + this.queue.Count + " items";
this.Start();
}
/// <summary>
/// Start execution of the current item.
/// </summary>
private void Start()
{
if (this.BackgroundWorker.IsBusy)
throw new Exception("Worker is busy");
this.BackgroundWorker.RunWorkerAsync();
}
/// <summary>
/// Cancel everything matching the filter from the queue.
/// </summary>
/// <param name="filter">Filter: the items where the filter is true will be cancelled.</param>
internal void CancelMatching(Func<IBackgroundWorkItem, bool> filter)
{
if (filter == null)
return;
foreach (IBackgroundWorkItem item in this.queue)
{
if (filter(item))
{
#if DEBUG_WORKQUEUE
#endif
item.Cancel();
}
}
if (this.current != null && filter(this.current))
{
this.Cancel(this.current);
}
}
/// <summary>
/// Cancel the specified item.
/// </summary>
/// <param name="backgroundWorkItem">Item to cancel.</param>
internal void Cancel(IBackgroundWorkItem backgroundWorkItem)
{
if (this.current == backgroundWorkItem)
{
this.BackgroundWorker.CancelAsync();
this.current.Cancel();
}
}
/// <summary>
/// An item asks to be cancelled.
/// </summary>
/// <param name="backgroundWorkItem">Item to cancel.</param>
internal void CancelMe(IBackgroundWorkItem backgroundWorkItem)
{
if (this.current == backgroundWorkItem)
{
this.BackgroundWorker.CancelAsync();
}
}
/// <summary>
/// Stop the queue.
/// </summary>
public void Stop()
{
this.stopped = true;
this.CancelCurrentWork();
}
/// <summary>
/// Cancel the currently running work.
/// </summary>
public void CancelCurrentWork()
{
if (this.current == null) return;
this.current.Cancel();
}
}
/// <summary>
/// Useful static methods for all applications.
/// </summary>
public static class Utilities
{
private static DateTime firstTime = DateTime.MinValue;
/// <summary>
/// Current time precise enough for logging.
/// </summary>
public static string PreciseTime
{
get
{
if (Utilities.firstTime == DateTime.MinValue)
{
Utilities.firstTime = DateTime.Now;
}
TimeSpan elapsed = (DateTime.Now - Utilities.firstTime);
return elapsed + "\t";
}
}
/// <summary>
/// Read an unquoted word from the given line starting at index 'currentIndex'.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <param name="currentIndex">Start index of word.</param>
/// <param name="word">Found word.</param>
/// <returns>Index of separator after word (or of end of line).</returns>
private static int ParseUnquotedWord(string line, int currentIndex, out string word)
{
int separatorIndex = line.IndexOf(',', currentIndex);
if (separatorIndex == -1)
{
word = line.Substring(currentIndex);
return line.Length;
}
else
{
word = line.Substring(currentIndex, separatorIndex - currentIndex);
return separatorIndex;
}
}
/// <summary>
/// Read a quoted word from the given line starting at index 'currentIndex'.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <param name="currentIndex">Start index of word.</param>
/// <param name="word">Found word.</param>
/// <returns>Index of separator after word (or of end of line). Returns -1 if the end quote is missing.</returns>
private static int ParseQuotedWord(string line, int currentIndex, out string word)
{
int endIndex = currentIndex + 1;
while (true)
{
endIndex = line.IndexOf('\"', endIndex);
if (endIndex == -1)
{
word = "";
return -1;
}
if (endIndex == line.Length - 1)
{
// last word on line
break;
}
else if (line[endIndex + 1] == '\"')
{
// quoted quote, continue
endIndex += 2;
continue;
}
else
{
// end of quoted word
break;
}
}
word = line.Substring(currentIndex + 1, endIndex - currentIndex - 1); // drop the start and end quotes
word = word.Replace("\"\"", "\""); // fix quoted quotes
return endIndex + 1;
}
/// <summary>
/// Split a comma-separated value line into fields. Properly parses quoted fields.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <returns>A list of the fields parsed.</returns>
public static List<string> SplitCSVLine(string line)
{
List<string> results = new List<string>();
int currentIndex = 0;
while (currentIndex < line.Length)
{
string word;
if (line[currentIndex] == '\"')
{
currentIndex = ParseQuotedWord(line, currentIndex, out word);
if (currentIndex < 0)
// end-of-line in quoted word; need more data
return null;
}
else
{
currentIndex = ParseUnquotedWord(line, currentIndex, out word);
}
results.Add(word);
if (currentIndex < line.Length)
{
// expect a separator
if (line[currentIndex] != ',')
throw new ArgumentException("Not found expected separator character after quoted field");
currentIndex++;
}
}
return results;
}
/// <summary>
/// Regular expression matching a GUID.
/// </summary>
public static readonly Regex GuidRegex = new Regex(@"([0-9A-F]{8})-([0-9A-F]{4})-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", RegexOptions.Compiled);
/// <summary>
/// Format string to use to represent datetimes in high precision as a string.
/// </summary>
public static string HighPrecisionDateFormat = "MM/dd/yyyy HH:mm:ss.fff tt";
static MD5 MD5Cached;
/// <summary>
/// A string encoding of the MD5 checksum of the input string.
/// </summary>
/// <param name="s">String to encode using MD5.</param>
/// <returns>A string containing the MD5 encoding.</returns>
public static string MD5(string s)
{
if (MD5Cached == null)
MD5Cached = System.Security.Cryptography.MD5.Create();
byte [] code = MD5Cached.ComputeHash(Encoding.UTF8.GetBytes(s));
StringBuilder result = new StringBuilder();
foreach (byte b in code)
result.Append(b.ToString("X2"));
return result.ToString();
}
/// <summary>
/// Move or rename a file.
/// </summary>
/// <param name="lpExistingFileName">Existing file.</param>
/// <param name="lpNewFileName">New file.</param>
/// <returns>True on success.</returns>
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);
/// <summary>
/// Move or rename a file. Throws an exception on failure.
/// </summary>
/// <param name="from">Existing file.</param>
/// <param name="to">New file.</param>
public static void Move(string from, string to)
{
bool success = MoveFile(from, to);
if (success)
return;
throw new Win32Exception();
}
/// <summary>
/// Given a string of the form [k=v](,[k=v]*), parse it into a dictionary.
/// Keys and values cannot contain commas or equal signs.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <returns>A dictionary mapping the keys to values.</returns>
public static Dictionary<string, string> ParseCommaSeparatedKeyValuePair(string line)
{
Dictionary<string, string> result = new Dictionary<string, string>();
if (line.Length == 0)
return result;
string[] pieces = line.Split(',');
foreach (string piece in pieces)
{
if (piece.Length == 0) continue;
string[] parts = piece.Split('=');
if (parts.Length != 2)
throw new ArgumentException("Element `" + piece + "' not in k=v form");
result.Add(parts[0].Trim(), parts[1].Trim());
}
return result;
}
/// <summary>
/// True if this file name seems to indicate a text file.
/// </summary>
/// <param name="filename">File whose name is tested.</param>
/// <returns>True if this name indicates a text file.</returns>
public static bool FileNameIndicatesTextFile(string filename)
{
string[] textSuffixes = {
".txt", ".bat", ".cmd", ".log", ".config", ".xml", ".html"
};
foreach (string suffix in textSuffixes)
{
if (filename.EndsWith(suffix))
return true;
}
return false;
}
/// <summary>
/// Create the directory if it does not exist; it retries a few times.
/// </summary>
/// <param name="filename">Path for file; directory name is extracted and created.</param>
public static void EnsureDirectoryExistsForFile(string filename)
{
string dir = Path.GetDirectoryName(filename);
bool ok = false;
int count = 0;
while (!ok && count < 5)
{
try
{
if (dir != null && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
ok = true;
}
catch (IOException)
{
Thread.Sleep(200);
}
count++;
}
}
/// <summary>
/// Add an item in front of a list; the list is mutated.
/// </summary>
/// <param name="list">List to add item to.</param>
/// <param name="item">Item to add in front.</param>
/// <param name="maxlen">Maximum list lenght.</param>
/// <returns>The new list.</returns>
public static IList<T> AddItemInFront<T>(IList<T> list, int maxlen, T item)
{
if (list.Contains(item))
// move to front
list.Remove(item);
else if (list.Count >= maxlen)
// drop last
list.RemoveAt(list.Count - 1);
list.Insert(0, item);
return list;
}
/// <summary>
/// The file name may contain illegal characters; replace them with something legal.
/// Does not guarantee that the file name will be unique.
/// </summary>
/// <param name="filename">Filename to legalize.</param>
/// <returns>A file name which is legal.</returns>
public static string LegalizeFileName(string filename)
{
HashSet<char> illegal = new HashSet<char>(Path.GetInvalidFileNameChars());
StringBuilder result = new StringBuilder();
foreach (char c in filename)
{
if (illegal.Contains(c))
// replace illegal characters with a dash
result.Append('-');
else
result.Append(c);
}
return result.ToString();
}
/// <summary>
/// Truncate x to a given number of decimals after decimal point.
/// </summary>
/// <param name="x">Value to truncate.</param>
/// <param name="decimals">Number of decimal values.</param>
/// <returns>The input with at most the indicated number of decimal places.</returns>
public static string Round(double x, int decimals)
{
if (decimals < 0)
decimals = 0;
double rounded = Math.Round(x, decimals);
return rounded.ToString();
}
private static Random jitterRandom = new Random();
/// <summary>
/// Generate a random value in the interval -max .. max with a distribution skewed towards the center
/// </summary>
/// <param name="max">Maximum amount of jitter.</param>
/// <returns>The jitter value.</returns>
public static double Jitter(double max)
{
double rand = 100 * jitterRandom.NextDouble() - 50;
return rand * max / 50;
}
/// <summary>
/// Check if two strings represent the same machine.
/// </summary>
/// <param name="m1">First machine.</param>
/// <param name="m2">Second machine.</param>
/// <returns>'true' if the two names point to the same machine really.</returns>
public static bool SameMachine(string m1, string m2)
{
return m1.ToLower() == m2.ToLower();
}
/// <summary>
/// Extract the executable embedded in the assembly, then store it in the disk.
/// </summary>
/// <param name="filename">The name of the file in the assembly</param>
/// <returns>the path to the instantiated file on disk(a local path)</returns>
public static string InstantiateExecutableFromRes(string filename)
{
if (File.Exists(filename))
return (new FileInfo(filename)).FullName;
// Get Current Assembly refrence
Assembly currentAssembly = Assembly.GetExecutingAssembly();
// Get all embedded resources
string[] arrResources = currentAssembly.GetManifestResourceNames();
foreach (string resourceName in arrResources)
{
if (resourceName.EndsWith(filename))
{ //or other extension desired
//Name of the file saved on disk
string saveAsName = filename;
//just save in the current directory
FileInfo fileInfoOutputFile = new FileInfo(saveAsName);
if (fileInfoOutputFile.Exists)
{
//overwrite if desired (depending on your needs)
fileInfoOutputFile.Delete();
}
FileStream streamToOutputFile = fileInfoOutputFile.OpenWrite();
Stream streamToResourceFile = currentAssembly.GetManifestResourceStream(resourceName);
const int size = 4096;
byte[] bytes = new byte[4096];
int numBytes;
while ((numBytes = streamToResourceFile.Read(bytes, 0, size)) > 0)
{
streamToOutputFile.Write(bytes, 0, numBytes);
}
streamToOutputFile.Close();
streamToResourceFile.Close();
return fileInfoOutputFile.FullName;
}
}
return null;
}
/// <summary>
/// Copy the source directory to the target directory.
/// </summary>
/// <param name="sourceDirectory">The path to the source directory.</param>
/// <param name="targetDirectory">The path to the target directory.</param>
/// <param name="pattern">Only filenames and subdirectories matching the pattern will be copied.</param>
public static void CopyDirectory(string sourceDirectory, string targetDirectory, string pattern)
{
DirectoryInfo diSource = new DirectoryInfo(sourceDirectory);
DirectoryInfo diTarget = new DirectoryInfo(targetDirectory);
CopyAll(diSource, diTarget, pattern);
}
/// <summary>
/// Copy all the stuff from source dir to target dir. Just a private version of CopyDirectory
/// </summary>
/// <param name="source">The directory information of the source directory.</param>
/// <param name="target">The directory information of thetarget directory.</param>
/// <param name="pattern">Only files and subdirectories matching the pattern will be copied.</param>
private static void CopyAll(DirectoryInfo source, DirectoryInfo target, string pattern)
{
// Check if the target directory exists, if not, create it.
if (Directory.Exists(target.FullName) == false)
{
Directory.CreateDirectory(target.FullName);
}
// Copy each file into it's new directory.
foreach (FileInfo fi in source.GetFiles(pattern))
{
//Trace.TraceInformation(@"Copying {0}\{1}", target.FullName, fi.Name);
fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true);
}
// Copy each subdirectory using recursion.
foreach (DirectoryInfo diSourceSubDir in source.GetDirectories(pattern))
{
DirectoryInfo nextTargetSubDir =
target.CreateSubdirectory(diSourceSubDir.Name);
CopyAll(diSourceSubDir, nextTargetSubDir, pattern);
}
}
/// <summary>
/// Convert a time value printed by the dryad job manager into a datetime object.
/// </summary>
/// <param name="tzone">Configuration describing the local time zone.</param>
/// <param name="cosmosTime">CsTime value (A Cosmos time implementation).</param>
/// <returns>The absolute time represented by the value.</returns>
public static DateTime Convert64time(TimeZoneInfo tzone, string cosmosTime)
{
DateTime time = new DateTime(Convert.ToInt64(cosmosTime), DateTimeKind.Unspecified).AddYears(1600);
time = System.TimeZoneInfo.ConvertTimeFromUtc(time, tzone);
return time;
}
/// <summary>
/// Add one more key-value pair to a stringbuilder in the form k=v. If the builder is not empty, add a comma in front too.
/// </summary>
/// <param name="builder">Stringbuilder where the string is built.</param>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
public static void AddKVP(StringBuilder builder, string key, object value)
{
if (builder.Length > 0)
builder.Append(",");
// remove commas from key and value, otherwise it won't work
key = key.Replace(',', '-');
builder.Append(key);
builder.Append("=");
string val = value.ToString().Replace(',', '-');
builder.Append(val);
}
/// <summary>
/// CreateHardLink will utilize PInvoke to call the Win32 function CreateHardLink.
/// </summary>
/// <param name="lpFileName">Source FileName</param>
/// <param name="lpExistingFileName">Destination FileName</param>
/// <param name="lpSecurityAttributes">Security Attributes - Should be IntPtr.Zero</param>
/// <returns>True if the function succeeds.</returns>
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern public bool CreateHardLink(string lpFileName, string lpExistingFileName,
IntPtr lpSecurityAttributes);
/// <summary>
/// Create a hard link, and throw an exception if this fails.
/// </summary>
/// <param name="to">Destination file.</param>
/// <param name="from">Source file.</param>
public static void CreateHardLink(string to, string from)
{
bool status = CreateHardLink(to, from, new IntPtr(0));
if (!status)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
/// <summary>
/// Copy a file, but try creating a hard link first.
/// </summary>
/// <param name="destination">Destination file.</param>
/// <param name="source">Source file.</param>
/// <returns>False if the copy failed. May throw exceptions.</returns>
public static bool LinkOrCopy(string destination, string source)
{
if (!File.Exists(source))
return false;
if (File.Exists(destination))
{
// check if the source and destination may be the same file
FileInfo si = new FileInfo(source);
FileInfo di = new FileInfo(destination);
if (si.LastWriteTime == di.LastWriteTime && si.Length == di.Length)
return true;
File.Delete(destination);
}
// no exceptions thrown here
bool success = Utilities.CreateHardLink(destination, source, new IntPtr(0));
if (success) return true;
File.Copy(source, destination);
return true;
}
/// <summary>
/// Run a process, return exit code if waiting for completion.
/// </summary>
/// <param name="process">Command-line to invoke.</param>
/// <param name="workDirectory">If not null, used to set the work directory of the process.</param>
/// <param name="quote">If true, add quotes around parameters.</param>
/// <param name="wait">If true, wait for completion.</param>
/// <param name="useshell">Do we use shell for executing?</param>
/// <param name="runAsAdmin">Do we need admin privileges? (if true, usually also requires useshell to be true)</param>
/// <param name="arguments">Arguments to pass.</param>
/// <returns>Exit code of the process if waiting for the process, zero otherwise.</returns>
// ReSharper disable once UnusedMethodReturnValue.Global
public static int RunProcess(string process, string workDirectory, bool quote, bool wait, bool useshell, bool runAsAdmin, params string[] arguments)
{
StringBuilder args = new StringBuilder();
string q = quote ? @"""" : "";
foreach (string arg in arguments)
{
args.Append(@" " + q + arg + q);
}
Trace.TraceInformation("Running: " + process + " " + args);
ProcessStartInfo processStartInfo = new ProcessStartInfo(process, args.ToString());
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = useshell;
processStartInfo.RedirectStandardOutput = !useshell;
processStartInfo.RedirectStandardError = !useshell;
if (workDirectory != null)
{
processStartInfo.WorkingDirectory = workDirectory;
}
if (runAsAdmin)
{
processStartInfo.Verb = "runas";
}
Process p = new Process();
p.StartInfo = processStartInfo;
int exitcode;
try
{
p.Start();
if (!wait)
// no waiting
return 0;
if (!useshell)
{
StreamReader procout = p.StandardOutput;
if (!p.HasExited)
{
string outLine;
while ((outLine = procout.ReadLine()) != null)
{
Trace.TraceInformation(outLine);
}
}
}
p.WaitForExit();
exitcode = p.ExitCode;
if (exitcode != 0)
Trace.TraceInformation("Process has exited with exitcode " + exitcode);
}
catch (InvalidOperationException e)
{
exitcode = -1;
Trace.TraceInformation("Could not start process " + process + " exception " + e);
}
catch (Win32Exception e)
{
exitcode = -1;
Trace.TraceInformation("Could not start process " + process + " exception " + e);
}
return exitcode;
}
/// <summary>
/// Private representation wrapping process and delegate to invoke on exit.
/// </summary>
class BackgroundProcessHandle
{
Process process;
Action<int> onExit;
/// <summary>
/// Create a handle for a process to run in the background.
/// </summary>
/// <param name="p">Process to run.</param>
public BackgroundProcessHandle(Process p)
{
this.process = p;
}
/// <summary>
/// Run the process and invoke this delegate on exit.
/// </summary>
/// <param name="oe">Delegate to invoke on process exit; arg is process exit code.</param>
/// <returns>True if launching the process succeeds.</returns>
public bool Run(Action<int> oe)
{
this.onExit = oe;
try
{
this.process.EnableRaisingEvents = true;
this.process.Exited += this.processExited;
this.process.Start();
}
catch (InvalidOperationException e)
{
Trace.TraceInformation("Could not start process " + this.process.ProcessName + " exception " + e);
return false;
}
catch (Win32Exception e)
{
Trace.TraceInformation("Could not start process " + this.process.ProcessName + " exception " + e);
return false;
}
return true;
}
/// <summary>
/// Event handler when the process exits.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void processExited(object sender, EventArgs e)
{
if (this.onExit != null)
this.onExit(this.process.ExitCode);
}
}
/// <summary>
/// Run a process, return exit code if waiting for completion.
/// </summary>
/// <param name="process">Command-line to invoke.</param>
/// <param name="workDirectory">If not null, used to set the work directory of the process.</param>
/// <param name="onExit">Delegate to invoke on exit; passed exit code.</param>
/// <param name="arguments">Arguments to pass.</param>
/// <returns>True if running succeeded.</returns>
public static bool RunProcessAsync(string process, string workDirectory, Action<int> onExit, params string[] arguments)
{
string args = string.Join(" ", arguments);
Trace.TraceInformation("Running: " + process + " " + args);
ProcessStartInfo processStartInfo = new ProcessStartInfo(process, args);
processStartInfo.CreateNoWindow = true;
if (workDirectory != null)
{
processStartInfo.WorkingDirectory = workDirectory;
}
Process p = new Process();
p.StartInfo = processStartInfo;
BackgroundProcessHandle bph = new BackgroundProcessHandle(p);
bool started = bph.Run(onExit);
return started;
}
/// <summary>
/// Save an object as xml.
/// </summary>
/// <typeparam name="T">Type of object to save.</typeparam>
/// <param name="filename">File to save object description to.</param>
/// <param name="obj">Object to save.</param>
public static void SaveAsXml<T>(string filename, T obj)
{
using (StreamWriter sw = new StreamWriter(filename))
{
XmlSerializer s = new XmlSerializer(typeof(T));
s.Serialize(sw, obj);
}
}
/// <summary>
/// Load the description of an object from a file.
/// </summary>
/// <typeparam name="T">Type of object to read from file.</typeparam>
/// <param name="filename">File containing the description.</param>
public static T LoadXml<T>(string filename)
{
using (StreamReader sr = new StreamReader(filename))
{
XmlSerializer s = new XmlSerializer(typeof(T));
Object o = s.Deserialize(sr);
return (T)o;
}
}
/// <summary>
/// Convert a hsv color value to a rgb color value.
/// </summary>
/// <param name="h">Hue, between 0 and 1.</param>
/// <param name="s">Saturation, between 0 and 1.</param>
/// <param name="v">Value, between 0 and 1.</param>
/// <param name="r">Red component, between 0 and 1.</param>
/// <param name="g">Green component, between 0 and 1.</param>
/// <param name="b">Blue component, between 0 and 1.</param>
public static void HSVtoRGB(double h, double s, double v, out double r, out double g, out double b)
{
h *= 6;
int i = (int)Math.Floor(h);
double f = h - i;
if ((i & 1) == 0) f = 1 - f; // if i is even
double m = v * (1 - s);
double n = v * (1 - s * f);
r = 0;
g = 0;
b = 0;
switch (i)
{
case 6: throw new ArgumentOutOfRangeException("h");
case 0: r = v; g = n; b = m; break;
case 1: r = n; g = v; b = m; break;
case 2: r = m; g = v; b = n; break;
case 3: r = m; g = n; b = v; break;
case 4: r = n; g = m; b = v; break;
case 5: r = v; g = m; b = n; break;
}
}
/// <summary>
/// If the xpath expression matches return the first Xml node matching.
/// Else return null.
/// </summary>
/// <param name="node">Node where matching starts.</param>
/// <param name="xpath">Xpath expression to match.</param>
/// <returns>First matching node or null.</returns>
public static XmlNode FirstIfAnyXmlMatch(XmlNode node, string xpath)
{
XmlNodeList matching = node.SelectNodes(xpath);
if (matching.Count == 0)
return null;
return matching[0];
}
/// <summary>
/// Get the single expected matching node in an XML document.
/// </summary>
/// <param name="node">Search starting from this node.</param>
/// <param name="xpath">Xpath expression to search for nodes.</param>
/// <returns>The single matching node.</returns>
public static XmlNode SingleXmlMatch(XmlNode node, string xpath)
{
XmlNode child = Utilities.FirstIfAnyXmlMatch(node, xpath);
if (child == null)
throw new XmlException("Node " + node.Name + " does not have exactly 1 child with path " + xpath + " in xml document");
return child;
}
/// <summary>
/// Read the web page with the specified URL.
/// </summary>
/// <param name="url">Url to reach.</param>
/// <returns>A streamreader that returns the loaded web page.</returns>
/// <param name="credentials">Credentials to use.</param>
public static Stream Navigate(string url, ICredentials credentials)
{
CookieContainer cookiejar = new CookieContainer();
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.CookieContainer = cookiejar;
req.ServicePoint.ConnectionLimit = 1;
if (credentials == null)
req.UseDefaultCredentials = true;
else
req.Credentials = credentials;
req.PreAuthenticate = false;
req.Method = "GET";
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
System.Diagnostics.Trace.Assert(resp.StatusCode == HttpStatusCode.OK);
Trace.TraceInformation("Received response");
return resp.GetResponseStream();
}
/// <summary>
/// Convert Hexadecimal number to integer.
/// </summary>
/// <param name="hexString">A string representing a hex number.</param>
/// <returns>The equivalent integer value.</returns>
public static int HexToInt(string hexString)
{
return int.Parse(hexString, System.Globalization.NumberStyles.HexNumber, null);
}
/// <summary>
/// Convert integer to hexadecimal.
/// </summary>
/// <param name="number">Number to convert.</param>
/// <returns>A string which is the hexadecimal representation.</returns>
public static string IntToHex(int number)
{
return String.Format("{0:x}", number);
}
/// <summary>
/// This is like Path.Combine, but with multiple arguments.
/// </summary>
/// <param name="paths">Paths to combine.</param>
/// <returns>A single path composed of all segments.</returns>
public static string PathCombine(params string[] paths)
{
if (paths.Length == 0)
return "";
string result = paths[0];
for (int i = 1; i < paths.Length; i++)
result = Path.Combine(result, paths[i]);
return result;
}
/// <summary>
/// Compute the average of a set of colors.
/// </summary>
/// <param name="colors">Colors to average.</param>
/// <returns>A new color, which is the average of all other colors.</returns>
public static Color AverageColor(IEnumerable<Color> colors)
{
int count = 0;
int totalA = 0, totalR = 0, totalG = 0, totalB = 0;
foreach (Color c in colors)
{
totalA += c.A;
totalR += c.R;
totalG += c.G;
totalB += c.B;
count++;
}
if (count == 0)
return Color.Black;
return Color.FromArgb(totalA / count, totalR / count, totalG / count, totalB / count);
}
/// <summary>
/// Find a color contrasting with the given one (e.g., to use as foreground when the other is background).
/// </summary>
/// <param name="color">Color to contrast with.</param>
/// <returns>A contrasting color.</returns>
public static Color VisibleColor(Color color)
{
if (color.GetBrightness() < 0.5)
return Color.White;
else
return Color.Black;
}
/// <summary>
/// Copy a file which may be already opened for writing.
/// </summary>
/// <param name="from">Original name.</param>
/// <param name="to">New name.</param>
public static void CopyFile(string from, string to)
{
Stream rd = new FileStream(from, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
Stream wr = new FileStream(to, FileMode.Create, FileAccess.Write);
byte[] buf = new byte[1 << 13];
for (; ; )
{
int len = rd.Read(buf, 0, buf.Length);
if (len == 0) break;
wr.Write(buf, 0, len);
}
rd.Close();
wr.Close();
}
static char[] FolderSeparators = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
/// <summary>
/// Split a pathname into a list of directories.
/// </summary>
/// <param name="path">Path to split.</param>
/// <returns>The complete list of directories on the path.</returns>
public static string[] SplitPathname(string path)
{
string[] subpaths = path.Split(FolderSeparators, StringSplitOptions.RemoveEmptyEntries);
return subpaths;
}
/// <summary>
/// Check whether a given string could be the current user.
/// </summary>
/// <param name="username">Name of user.</param>
/// <returns>True if it matches the login name.</returns>
public static bool IsThisUser(string username)
{
System.Security.Principal.WindowsIdentity id = System.Security.Principal.WindowsIdentity.GetCurrent();
return (id.Name == username);
}
/// <summary>
/// Given a file with records serialized by DryadLINQ create the metadata to morph it into a single-partition file.
/// </summary>
/// <param name="file">File containing DryadLINQ-serialized records.</param>
/// <returns>A uri pointing to to partitioned file table whose body is the specified file.</returns>
public static string TransformToPartitionedTable(UNCPathname file)
{
string prefix = file.DirectoryAndFilename;
PartitionedFileMetadata md = new PartitionedFileMetadata();
PartitionedFileMetadata.Partition part = new PartitionedFileMetadata.Partition(0, 0, file.Machine, prefix);
string partFilename = part.Replica(0).ToString();
md.Add(part);
// partitions have to have a very stylized name
Utilities.CreateHardLink(partFilename, file.ToString());
// Create the metadata for the file we are returning
UNCPathname metadatafile = file;
metadatafile.Filename = "metadata-" + file.Filename;
string uri = md.CreateMetadataFile(metadatafile);
return uri;
}
/// <summary>
/// Generate a regular expression corresponding to a search pattern.
/// This replaces ? with .? and * with .*
/// </summary>
/// <param name="match"></param>
/// <returns></returns>
public static Regex RegexFromSearchPattern(string match)
{
if (string.IsNullOrEmpty(match))
return new Regex("");
match = match.Replace("?", ".?");
match = match.Replace("*", ".*");
return new Regex(match);
}
/// <summary>
/// Parse a line of the form k=v,k=v. Values may be quoted.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <returns>A dictionary with all key=value parts, or null if parsing fails because of an end-of-line in quoted value.</returns>
public static Dictionary<string, string> ParseCSVKVP(string line)
{
Dictionary<string, string> result = new Dictionary<string, string>();
while (!string.IsNullOrWhiteSpace(line))
{
int eq = line.IndexOf('=');
if (eq < 0)
throw new ArgumentException("Could not find equal sign in " + line);
string key = line.Substring(0, eq).Trim();
string value;
int cont;
if (line.Length > eq && line[eq+1] == '"')
{
cont = ParseQuotedWord(line, eq+1, out value);
}
else
{
cont = ParseUnquotedWord(line, eq+1, out value);
}
if (cont < 0) return null; // end of line in quoted value
if (cont >= line.Length)
line = "";
else
line = line.Substring(cont + 1); // skip next comma
result.Add(key, value);
}
return result;
}
}
/// <summary>
/// A binding list implementation which permits sorting.
/// </summary>
/// <typeparam name="T">Type of elements in list.</typeparam>
public class BindingListSortable<T> : BindingList<T>
{
private bool isSorted;
private ListSortDirection direction = ListSortDirection.Ascending;
private PropertyDescriptor sortProperty;
/// <summary>
/// Create an empty binding list.
/// </summary>
public BindingListSortable()
{
this.isSorted = false;
}
/// <summary>
/// Initialize a sortable binding list with a set of values.
/// </summary>
/// <param name="values">Values to insert in list.</param>
public BindingListSortable(IList<T> values)
: base(values)
{
this.isSorted = false;
}
/// <summary>
/// What is the list sorted on?
/// </summary>
public string SortedOn { get; set; }
/// <summary>
/// Sort the list on the indicated property.
/// </summary>
/// <param name="property">Property of T to sort on.</param>
public void Sort(string property)
{
this.sortProperty = this.FindPropertyDescriptor(property);
this.ApplySortCore(sortProperty, direction);
}
/// <summary>
/// Redo the last sorting operation.
/// <returns>True if there was a last sorting operation.</returns>
/// </summary>
public bool RedoSort()
{
if (this.SortedOn == null)
return false;
this.Sort(this.SortedOn);
return true;
}
/// <summary>
/// Override indicating that the list supports sorting.
/// </summary>
protected override bool SupportsSortingCore
{
get { return true; }
}
/// <summary>
/// Apply the sorting operation.
/// </summary>
/// <param name="property">Property to sort on.</param>
/// <param name="dir">Direction of sort.</param>
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection dir)
{
this.SortedOn = property.Name;
this.direction = dir;
List<T> items = this.Items as List<T>;
if (null != items)
{
PropertyComparer<T> pc = new PropertyComparer<T>(property, dir);
items.Sort(pc);
/* Set sorted */
this.isSorted = true;
}
else
{
/* Set sorted */
this.isSorted = false;
}
}
/// <summary>
/// Is the list sorted?
/// </summary>
protected override bool IsSortedCore
{
get { return this.isSorted; }
}
/// <summary>
/// "Unsort" the list.
/// </summary>
protected override void RemoveSortCore()
{
this.isSorted = false;
}
private PropertyDescriptor FindPropertyDescriptor(string property)
{
PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(typeof(T));
PropertyDescriptor prop = pdc.Find(property, true);
return prop;
}
internal class PropertyComparer<TKey> : System.Collections.Generic.IComparer<TKey>
{
private PropertyDescriptor property;
private ListSortDirection direction;
public PropertyComparer(PropertyDescriptor property, ListSortDirection direction)
{
this.property = property;
this.direction = direction;
}
public int Compare(TKey xVal, TKey yVal)
{
/* Get property values */
object xValue;
object yValue;
if (property != null)
{
xValue = GetPropertyValue(xVal, property.Name);
yValue = GetPropertyValue(yVal, property.Name);
}
else
{
xValue = xVal;
yValue = yVal;
}
/* Determine sort order */
int sort = CompareAscending(xValue, yValue);
if (direction == ListSortDirection.Descending)
{
sort = -sort;
}
return sort;
}
public bool Equals(TKey xVal, TKey yVal)
{
return xVal.Equals(yVal);
}
public int GetHashCode(TKey obj)
{
return obj.GetHashCode();
}
/* Compare two property values of any type */
private int CompareAscending(object xValue, object yValue)
{
int result;
/* If values implement IComparer */
var value = xValue as IComparable;
if (value != null)
{
result = value.CompareTo(yValue);
}
else throw new ArgumentException("values are not comparable");
return result;
}
private object GetPropertyValue(TKey value, string prop)
{
/* Get property */
PropertyInfo propertyInfo = value.GetType().GetProperty(prop);
/* Return value */
return propertyInfo.GetValue(value, null);
}
}
}
/// <summary>
/// A legend maps a color to an explanation.
/// </summary>
public class Legend
{
/// <summary>
/// The name of the entity which was used to construct the legend information.
/// </summary>
public string LegendSourceName { get; internal set; }
/// <summary>
/// Explanation for the choice of a color.
/// </summary>
public class ColorLegend
{
/// <summary>
/// Explanation for the color.
/// </summary>
public string label;
/// <summary>
/// Colors will be ordered on this index when showing the legend.
/// </summary>
public double orderingIndex;
/// <summary>
/// String representation of the color legend.
/// </summary>
/// <returns>A string representation.</returns>
public override string ToString()
{
return label;
}
};
/// <summary>
/// Minimum color index represented.
/// </summary>
public double MinIndex { get; protected set; }
/// <summary>
/// Maximum color index represented.
/// </summary>
public double MaxIndex { get; protected set; }
/// <summary>
/// Color with minimum index.
/// </summary>
public Color MinIndexColor { get; protected set; }
/// <summary>
/// Color with maximum index.
/// </summary>
public Color MaxIndexColor { get; protected set; }
/// <summary>
/// Map from color to explanation.
/// </summary>
protected readonly Dictionary<Color, ColorLegend> explanations;
/// <summary>
/// Create an empty legend.
/// </summary>
/// <param name="sourcename">Name of entity used to construct the legend.</param>
public Legend(string sourcename)
{
this.LegendSourceName = sourcename;
this.explanations = new Dictionary<Color, ColorLegend>();
}
/// <summary>
/// The number of colors in the legend.
/// </summary>
public int Size { get { return this.explanations.Count; } }
/// <summary>
/// Explanation for color c, if any.
/// </summary>
/// <param name="c">Color whose explanation is sought.</param>
/// <returns>The meaning of color c, or null.</returns>
public ColorLegend this[Color c]
{
get
{
if (this.explanations.ContainsKey(c))
return this.explanations[c];
else
return null;
}
}
/// <summary>
/// Add a new explanation to the legend.
/// </summary>
/// <param name="c">Color to explain.</param>
/// <param name="explanation">Explanation for the color.</param>
/// <param name="index">Ordering index of color.</param>
public void Add(Color c, string explanation, double index)
{
if (explanation != null && !this.explanations.ContainsKey(c))
{
if (this.explanations.Count == 0)
{
// first element added
this.MinIndex = this.MaxIndex = index;
this.MinIndexColor = this.MaxIndexColor = c;
}
else
{
if (this.MinIndex > index)
{
this.MinIndex = index;
this.MinIndexColor = c;
}
if (this.MaxIndex < index)
{
this.MaxIndex = index;
this.MaxIndexColor = c;
}
}
this.explanations.Add(c, new ColorLegend { label = explanation, orderingIndex = index });
}
}
/// <summary>
/// Select only the specified colors from the legend.
/// </summary>
/// <param name="restrictTo">Restrict to these colors.</param>
/// <returns>A new legend.</returns>
public Legend Select(IEnumerable<Color> restrictTo)
{
Legend result = new Legend(this.LegendSourceName);
foreach (Color c in restrictTo)
{
ColorLegend legend = this[c];
if (legend == null) continue;
result.Add(c, legend.label, legend.orderingIndex);
}
return result;
}
/// <summary>
/// The list of all colors in the legend.
/// </summary>
/// <returns>The list of all colors represented in the legend.</returns>
public IEnumerable<Color> GetAllColors()
{
return this.explanations.Keys;
}
/// <summary>
/// Add a new explanation for a color.
/// </summary>
/// <param name="col">Color to explain.</param>
/// <param name="colorLegend">Explanation to add.</param>
public void Add(Color col, ColorLegend colorLegend)
{
if (colorLegend == null)
return;
if (!this.explanations.ContainsKey(col))
this.explanations.Add(col, colorLegend);
}
}
/// <summary>
/// Point on a two-dimensional surface, with double coordinates.
/// </summary>
public struct Point2D
{
/// <summary>
/// Point coordinates.
/// </summary>
double x, y;
/// <summary>
/// Create a point on a 2-D plane surface.
/// </summary>
/// <param name="x">X coordinate.</param>
/// <param name="y">Y coordinate.</param>
public Point2D(double x, double y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// Create a point at the specified coordinates from the origin.
/// </summary>
/// <param name="size">Size to encode as a point.</param>
public Point2D(SizeF size)
{
this.x = size.Width;
this.y = size.Height;
}
/// <summary>
/// X coordinate of point.
/// </summary>
public double X { get { return this.x; } }
/// <summary>
/// Y Coordinate of point.
/// </summary>
public double Y { get { return this.y; } }
/// <summary>
/// Return a point having the max coordinates from two points.
/// </summary>
/// <param name="other">Point to compare against.</param>
/// <returns>A new point having X = max(this.x, other.x) (similar for Y)</returns>
public Point2D Max(Point2D other)
{
return new Point2D(Math.Max(this.X, other.X), Math.Max(this.Y, other.Y));
}
/// <summary>
/// Return a point having the min coordinates from two points.
/// </summary>
/// <param name="other">Point to compare against.</param>
/// <returns>A new point having X = min(this.x, other.x) (similar for Y)</returns>
public Point2D Min(Point2D other)
{
return new Point2D(Math.Min(this.X, other.X), Math.Min(this.Y, other.Y));
}
/// <summary>
/// True if a point has coordinates smaller than another one.
/// </summary>
/// <param name="left">Left point.</param>
/// <param name="right">Right point.</param>
/// <returns>True if the left point has both coordinates smaller.</returns>
public static bool operator <(Point2D left, Point2D right)
{
return left.X < right.X && left.Y < right.Y;
}
/// <summary>
/// True if a point has coordinates bigger than another one.
/// </summary>
/// <param name="left">Left point.</param>
/// <param name="right">Right point.</param>
/// <returns>True if the left point has both coordinates bigger.</returns>
public static bool operator >(Point2D left, Point2D right)
{
return left.X > right.X && left.Y > right.Y;
}
/// <summary>
/// True if a point has coordinates smaller or equal than another one.
/// </summary>
/// <param name="left">Left point.</param>
/// <param name="right">Right point.</param>
/// <returns>True if the left point has both coordinates smaller or equal.</returns>
public static bool operator <=(Point2D left, Point2D right)
{
return left.X <= right.X && left.Y <= right.Y;
}
/// <summary>
/// True if a point has coordinates greater or equal than another one.
/// </summary>
/// <param name="left">Left point.</param>
/// <param name="right">Right point.</param>
/// <returns>True if the left point has both coordinates greater or equal.</returns>
public static bool operator >=(Point2D left, Point2D right)
{
return left.X >= right.X && left.Y >= right.Y;
}
/// <summary>
/// The second point is a displacement: compute new endpoint.
/// </summary>
/// <param name="left">Original.</param>
/// <param name="right">Displacement.</param>
/// <returns>A new point, at the given displacement from the original one.</returns>
public static Point2D operator +(Point2D left, Point2D right)
{
return new Point2D(left.X + right.X, left.Y + right.Y);
}
/// <summary>
/// Translate a point by a given amount.
/// </summary>
/// <param name="xp">Amount to translate in X direction.</param>
/// <param name="yp">Amount to translate in Y direction.</param>
/// <returns>A new point.</returns>
public Point2D Translate(double xp, double yp)
{
return new Point2D(this.X + xp, this.Y + yp);
}
/// <summary>
/// String representation of the point.
/// </summary>
/// <returns>A string describing the point.</returns>
public override string ToString()
{
return "(" + this.X + "," + this.Y + ")";
}
}
/// <summary>
/// Class describing a rectangle on a 2D plane with edges parallel to the axes.
/// </summary>
public class Rectangle2D
{
/// <summary>
/// Two opposite corners of the rectangle.
/// </summary>
Point2D one, two;
/// <summary>
/// True if one is less than two.
/// </summary>
bool normalized;
/// <summary>
/// Create a rectangle given 4 coordinates. Note that there is no requirement to have points in either order.
/// </summary>
/// <param name="lx">Left X coordinate.</param>
/// <param name="ly">Left Y coordinate.</param>
/// <param name="rx">Right X coordinate.</param>
/// <param name="ry">Right Y coordinate.</param>
public Rectangle2D(double lx, double ly, double rx, double ry)
{
one = new Point2D(lx, ly);
two = new Point2D(rx, ry);
normalized = one < two;
}
/// <summary>
/// Create a rectangle from two points.
/// </summary>
/// <param name="one">One corner.</param>
/// <param name="two">Second corner.</param>
public Rectangle2D(Point2D one, Point2D two)
{
this.one = one;
this.two = two;
normalized = one < two;
}
/// <summary>
/// Make the first corner to be lower and to the left of the second corner.
/// </summary>
/// <returns>A new rectangle.</returns>
public Rectangle2D Normalize()
{
if (normalized)
return this;
bool swapx = one.X > two.X;
bool swapy = one.Y > two.Y;
return new Rectangle2D(
swapx ? two.X : one.X,
swapy ? two.Y : one.Y,
swapx ? one.X : two.X,
swapy ? one.Y : two.Y);
}
/// <summary>
/// True if rectangle has null width or height.
/// </summary>
/// <returns>True if rectangle has zero area.</returns>
public bool Degenerate()
{
// ReSharper disable CompareOfFloatsByEqualityOperator
return (one.X == two.X) || (one.Y == two.Y);
// ReSharper restore CompareOfFloatsByEqualityOperator
}
/// <summary>
/// First corner.
/// </summary>
public Point2D Corner1 { get { return one; } }
/// <summary>
/// Second corner.
/// </summary>
public Point2D Corner2 { get { return two; } }
/// <summary>
/// Center of the rectangle.
/// </summary>
public Point2D Center
{
get
{
return new Point2D(one.X + this.Width / 2, one.Y + this.Height / 2);
}
}
/// <summary>
/// Compute the intersection of two rectangles.
/// </summary>
/// <param name="with">Rectangle to intersect with.</param>
/// <returns>A new rectangle. The result may be degenerate if the intersection is empty.</returns>
public Rectangle2D Intersect(Rectangle2D with)
{
Point2D ul = this.one.Max(with.one);
Point2D lr = this.two.Min(with.two);
if (ul.X > lr.X ||
ul.Y > lr.Y)
return new Rectangle2D(0, 0, 0, 0);
// We expect this is normalized
return new Rectangle2D(ul, lr);
}
/// <summary>
/// Rectangle area.
/// </summary>
/// <returns>The area of the rectangle.</returns>
public double Area()
{
return Math.Abs((one.X - two.X) * (one.Y - two.Y));
}
/// <summary>
/// If a rectangle is degenerate, stretch it a little.
/// </summary>
/// <returns>Always a non-degenerate, normalized rectangle.</returns>
public Rectangle2D FixDegeneracy()
{
const double epsilon = 0.1;
Rectangle2D norm = this.Normalize();
// if the size is 0 make it slightly larger
if (norm.Degenerate())
{
double xmin = norm.Corner1.X;
double xmax = norm.Corner2.X;
double ymin = norm.Corner1.Y;
double ymax = norm.Corner2.Y;
if (xmax <= xmin)
xmin -= xmax * 0.1 + epsilon;
if (ymax <= ymin)
ymin -= ymax * 0.1 + epsilon;
norm = new Rectangle2D(xmin, ymin, xmax, ymax);
}
return norm;
}
/// <summary>
/// Height of rectangle.
/// </summary>
public double Height { get { return Math.Abs(one.Y - two.Y); } }
/// <summary>
/// Width of rectangle.
/// </summary>
public double Width { get { return Math.Abs(one.X - two.X); } }
/// <summary>
/// Generate a new rectangle, with the smallest integer coordinates which contains this one.
/// </summary>
/// <returns>A new rectangle.</returns>
public Rectangle2D ExpandToIntegerCoordinates()
{
Rectangle2D norm = this.Normalize();
return new Rectangle2D(Math.Floor(norm.one.X), Math.Floor(norm.one.Y), Math.Ceiling(norm.two.X), Math.Ceiling(norm.two.Y));
}
/// <summary>
/// Create a rectangle given a corner and size.
/// </summary>
/// <param name="xl">Left X coordinate.</param>
/// <param name="yl">Left Y coordinate.</param>
/// <param name="width">Width of rectangle (may be negative).</param>
/// <param name="height">Height of rectangle (may be negative).</param>
/// <returns></returns>
public static Rectangle2D MakeRectangle(double xl, double yl, double width, double height)
{
return new Rectangle2D(xl, yl, xl + width, yl + height);
}
/// <summary>
/// Check whether a point is inside a rectangle.
/// </summary>
/// <param name="point">Point to check.</param>
/// <returns>True if the point is inside the rectangle.</returns>
public bool Inside(Point2D point)
{
Rectangle2D norm = this.Normalize();
return norm.Corner1 <= point && point <= norm.Corner2;
}
/// <summary>
/// Check whether a rectangle includes another one.
/// </summary>
/// <param name="other">The rectangle that should be inside.</param>
/// <returns>True if other is inside this.</returns>
public bool Includes(Rectangle2D other)
{
return Inside(other.Corner1) && Inside(other.Corner2);
}
/// <summary>
/// Move a rectangle.
/// </summary>
/// <param name="x">Amount to move in x direction.</param>
/// <param name="y">Amount to move in y direction.</param>
/// <returns>A new rectangle.</returns>
public Rectangle2D Translate(double x, double y)
{
return new Rectangle2D(this.Corner1.Translate(x, y), this.Corner2.Translate(x, y));
}
/// <summary>
/// Size of rectangle.
/// </summary>
public Point2D Size
{
get
{
return new Point2D(this.Width, this.Height);
}
}
}
/// <summary>
/// Represents a UNC pathname: machine, directory, file.
/// If 'machine' is null, this is a directory on localhost.
/// if 'file' is null, this represents a directory.
/// The directory cannot be null or empty.
/// </summary>
[Serializable]
public class UNCPathname
{
/// <summary>
/// Machine hosting the path; if null, it represents 'localhost'.
/// </summary>
public string Machine { get; set; }
/// <summary>
/// Directory; may not be null; includes the share name.
/// </summary>
public string Directory { get; set; }
/// <summary>
/// Filename; may be null.
/// </summary>
public string Filename { get; set; }
/// <summary>
/// Create a UNC pathname from a machine, directory and file.
/// </summary>
/// <param name="machine">Machine hosting the path; if null or empty, it represents 'localhost'.</param>
/// <param name="directory">Directory; may not be null.</param>
/// <param name="file">Filename; if null or empty, the pathname represents a directory.</param>
public UNCPathname(string machine, string directory, string file)
{
if (machine == "")
machine = null;
this.Machine = machine;
if (string.IsNullOrEmpty(directory))
throw new ArgumentException("The directory of a UNC pathname cannot be null");
this.Directory = directory;
if (file == "")
file = null;
this.Filename = file;
}
/// <summary>
/// The name of the share.
/// </summary>
public string Sharename
{
get
{
if (string.IsNullOrEmpty(this.DirectoryAndFilename))
return "";
string[] subpaths = Utilities.SplitPathname(this.DirectoryAndFilename);
return subpaths[0];
}
}
/// <summary>
/// Path without the share.
/// </summary>
public string DirectoryAndFilenameNoShare
{
get
{
if (string.IsNullOrEmpty(this.DirectoryAndFilename))
return "";
string[] subpaths = Utilities.SplitPathname(this.DirectoryAndFilename);
string result = string.Join(Path.DirectorySeparatorChar.ToString(), subpaths, 1, subpaths.Length - 1);
return result;
}
}
/// <summary>
/// Create a new uncpathname holding the same information as the other.
/// </summary>
/// <param name="other">UNCPathname to copy.</param>
public UNCPathname(UNCPathname other)
{
this.Machine = other.Machine;
this.Directory = other.Directory;
this.Filename = other.Filename;
}
/// <summary>
/// Create a UNC pathname from a string.
/// </summary>
/// <param name="path">Path to break into pieces.</param>
public UNCPathname(string path)
{
// First extract machine name
if (path.StartsWith(@"\\") ||
path.StartsWith(@"//"))
{
int slash = path.IndexOf("/", 2);
int bkslash = path.IndexOf("\\", 2);
if (bkslash < slash || slash < 0)
slash = bkslash;
if (slash >= 0)
{
this.Machine = path.Substring(2, slash - 2); // length - 2
path = path.Substring(slash + 1);
}
else
{
// there is just a machine
this.Machine = path.Substring(2);
path = "";
}
}
else
this.Machine = null;
// extract the directory and file
{
int slash = path.LastIndexOf('/');
int bkslash = path.LastIndexOf('\\');
if (bkslash > slash)
slash = bkslash;
if (slash >= 0)
{
this.Filename = path.Substring(slash + 1);
this.Directory = path.Substring(0, slash);
}
else
{
this.Filename = path;
this.Directory = null;
throw new ArgumentException("Pathname cannot contain an empty directory");
}
}
}
/// <summary>
/// Create a pathname from a machine and a file name.
/// </summary>
/// <param name="machine">Machine name.</param>
/// <param name="dirandfile">Pathname on local machine.</param>
public UNCPathname(string machine, string dirandfile)
: this(machine, Path.GetDirectoryName(dirandfile), Path.GetFileName(dirandfile))
{
}
/// <summary>
/// Pathname represented as a UNC pathname suitable for passing to File/Directory functions.
/// </summary>
/// <returns>The UNC pathname as a string.</returns>
public override string ToString()
{
if (Filename == null)
return this.UNCDirectory;
else
return Path.Combine(this.UNCDirectory, this.Filename);
}
/// <summary>
/// The combined directory and filename path.
/// If filename is null, only the directory is returned.
/// </summary>
public string DirectoryAndFilename
{
get
{
if (this.Filename == null)
return this.Directory.Trim('/', '\\');
return Path.Combine(this.Directory.Trim('/', '\\'), this.Filename.Trim('/', '\\'));
}
}
/// <summary>
/// True if the pathname represents a directory, false if it represents a file.
/// </summary>
public bool IsDirectory
{
get { return this.Filename == null; }
}
/// <summary>
/// True if the pathname is on the local machine (implicitly 'localhost'), false otherwise.
/// </summary>
public bool IsLocal
{
get { return this.Machine == null; }
}
/// <summary>
/// Just the machine and directory, in UNC form.
/// </summary>
public string UNCDirectory
{
get
{
StringBuilder builder = new StringBuilder();
if (Machine != null)
{
builder.Append(@"\\");
builder.Append(this.Machine);
}
if (!Directory.StartsWith(@"\"))
builder.Append(@"\");
builder.Append(this.Directory);
return builder.ToString();
}
}
}
/// <summary>
/// Map from objects to colors.
/// <typeparam name="T">Type of data source providing the information in the color map.</typeparam>
/// </summary>
public class ColorMap<T> : IEnumerable<object>
{
/// <summary>
/// Source of data used to build the color map.
/// </summary>
protected readonly T source;
/// <summary>
/// Map from object (a value in the column) to color.
/// </summary>
protected Dictionary<object, Color> map;
/// <summary>
/// Explanation for the color choices.
/// </summary>
protected readonly Legend legend;
/// <summary>
/// Create an empty color map.
/// </summary>
public ColorMap(T source)
{
this.source = source;
this.map = new Dictionary<object, Color>();
this.legend = new Legend("");
this.DefaultColor = Color.Gray;
this.IsContinuous = false;
}
/// <summary>
/// Set the name of the source which originated the legend.
/// </summary>
/// <param name="legendSource">Name of the source which originated the legend.</param>
public void SetLegendSourceName(string legendSource)
{
this.Legend.LegendSourceName = legendSource;
}
/// <summary>
/// If true this is a map from continuous values, else it is from discrete values.
/// </summary>
public bool IsContinuous { get; protected set; }
/// <summary>
/// Source providing the data for the map.
/// </summary>
public T ColorSource { get { return this.source; } }
/// <summary>
/// Find color associated with a label.
/// </summary>
/// <param name="label">Label.</param>
/// <returns>Color value.</returns>
public virtual Color this[object label]
{
get
{
if (this.map.ContainsKey(label))
return this.map[label];
else
return this.DefaultColor;
}
set
{
if (this.map.ContainsKey(label))
this.map[label] = value;
else
this.map.Add(label, value);
}
}
/// <summary>
/// The default color, when color is not in the map.
/// </summary>
public Color DefaultColor { get; set; }
/// <summary>
/// Color scale 0..1 => 0..255
/// </summary>
protected static int CS(double c)
{
return (int)(c * 255);
}
/// <summary>
/// Copy all data in a color map.
/// </summary>
/// <returns>A new color map which has the same information.</returns>
public ColorMap<T> Clone()
{
ColorMap<T> retval = new ColorMap<T>(this.source);
retval.DefaultColor = this.DefaultColor;
retval.map = new Dictionary<object, Color>(this.map);
return retval;
}
/// <summary>
/// An enumerator over the map objects.
/// </summary>
/// <returns>All the objects stored in the map.</returns>
public IEnumerator<object> GetEnumerator()
{
return this.map.Keys.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <summary>
/// The legend associated with this color map.
/// </summary>
public Legend Legend { get { return this.legend; } }
/// <summary>
/// Explanation for a given color.
/// </summary>
/// <param name="c">Color selected.</param>
/// <returns>The legend explaining the color, if any.</returns>
public Legend.ColorLegend Explanation(Color c)
{
return this.Legend[c];
}
/// <summary>
/// Explanation for the color of a label.
/// </summary>
/// <param name="label">Label whose color is explained.</param>
/// <returns>A Legend.ColorLegend explaining the color.</returns>
public Legend.ColorLegend Explanation(object label)
{
return this.Legend[this[label]];
}
}
/// <summary>
/// Color map representing discrete data.
/// </summary>
/// <typeparam name="T">Type of data source for color map.</typeparam>
public abstract class DiscreteColorMap<T> : ColorMap<T>
{
/// <summary>
/// Total number of colors represented by this colormap.
/// </summary>
protected readonly int totalcolors;
/// <summary>
/// Generate a new colormap for the given number of colors.
/// </summary>
/// <param name="colors">Number of distinct colors expected.</param>
/// <param name="source">Source of data.</param>
protected DiscreteColorMap(T source, int colors)
: base(source)
{
this.totalcolors = colors;
this.IsContinuous = false;
}
/// <summary>
/// Generate a color for color #index.
/// </summary>
/// <param name="index">Color number to generate [0..total).</param>
/// <returns>A nice color, such that all total colors are distinguishable.</returns>
protected abstract Color BucketColor(int index);
/// <summary>
/// Add a set of labels, all at the given index.
/// </summary>
/// <param name="labels">Labels to add.</param>
/// <param name="index">Index between 0..totalcolors.</param>
/// <param name="legd">Explanation for this class.</param>
public void AddLabelClass(IEnumerable<object> labels, int index, string legd)
{
if (index < 0 || index >= this.totalcolors)
throw new System.ArgumentException("Color index " + index + " out of range 0.." + totalcolors);
Color c = this.BucketColor(index);
int added = 0;
foreach (object label in labels)
{
added++;
this[label] = c;
}
if (added > 0 && legd != null)
{
this.legend.Add(c, legd, index);
}
}
}
/// <summary>
/// A colormap containing some pre-computed colors.
/// This works up to 32 colors only.
/// </summary>
/// <typeparam name="T">Type of data source for color map.</typeparam>
public class PrecomputedColorMap<T> : DiscreteColorMap<T>
{
// ReSharper disable once StaticFieldInGenericType
static Color[] preAssigned;
static PrecomputedColorMap()
{
preAssigned = new Color[] {
Color.Red, Color.Blue, Color.Yellow,
Color.Green, Color.Magenta, Color.Brown,
Color.DarkRed, Color.Indigo, Color.Orange,
Color.DarkGreen, Color.DarkViolet, Color.Chocolate,
Color.Tomato, Color.Aqua, Color.YellowGreen,
Color.SeaGreen, Color.Plum, Color.Wheat,
Color.Coral, Color.Firebrick, Color.Pink,
Color.Olive, Color.PaleGreen,
Color.SaddleBrown,Color.Lime, Color.Sienna,
Color.Goldenrod, Color.PaleTurquoise, Color.Khaki,
Color.DarkKhaki, Color.Purple, Color.Peru
};
}
/// <summary>
/// Maximum number of colors that can be represented by this color map.
/// </summary>
public static int MaxColors { get { return preAssigned.Length; } }
/// <summary>
/// Allocate a pre-computed color map.
/// </summary>
/// <param name="source">Data source for this color map.</param>
/// <param name="colors">Number of distinct colors.</param>
public PrecomputedColorMap(T source, int colors)
: base(source, colors)
{
if (colors > preAssigned.Length)
throw new ArgumentOutOfRangeException("The pre-assigned color map does not support more than " + preAssigned.Length + " colors");
}
/// <summary>
/// The color associated to a bucket.
/// </summary>
/// <param name="index">Bucket index.</param>
/// <returns>The color.</returns>
protected override Color BucketColor(int index)
{
int ix = index; // (index * preAssigned.Length) / this.totalcolors;
return preAssigned[ix];
}
}
/// <summary>
/// Map from objects to colors computing automatically colors for many objects mapped into a small number of buckets.
/// </summary>
/// <typeparam name="T">Type of data source for color map.</typeparam>
public class HSVColorMap<T> : DiscreteColorMap<T>
{
/// <summary>
/// Generate a new colormap for the given number of colors.
/// </summary>
/// <param name="colors">Number of distinct colors expected.</param>
/// <param name="source">Source of data.</param>
public HSVColorMap(T source, int colors)
: base(source, colors)
{
}
/// <summary>
/// Generate a color for color #index.
/// </summary>
/// <param name="index">Color number to generate [0..total).</param>
/// <returns>A nice color, such that all total colors are distinguishable.</returns>
protected override Color BucketColor(int index)
{
return HSVColorMap<T>.OneCircleColor(index, this.totalcolors);
}
/// <summary>
/// Generate a color for color #index, out of 'total' colors.
/// </summary>
/// <param name="index">Color number to generate [0..total).</param>
/// <param name="total">Total number of colors expected.</param>
/// <returns>A nice color, such that all total colors are distinguishable.</returns>
private static Color OneCircleColor(int index, int total)
{
int samplesPerCircle = total;
const int startOffset = 1; // rotate around circle
const double fractionOfCircle = 0.8; // leave a gap to distinguish start from end
int pointno = index + startOffset;
double h = pointno * fractionOfCircle / samplesPerCircle;
const double s = 1;
double r, g, b;
Utilities.HSVtoRGB(h, s, s, out r, out g, out b);
Color c = Color.FromArgb(CS(r), CS(g), CS(b));
return c;
}
}
/// <summary>
/// This class is used to turn selected properties of an object into something suitable for a databinding.
/// </summary>
public class PropertyEnumerator<T>
{
/// <summary>
/// Skip these properties when enumerating.
/// </summary>
HashSet<string> skip;
/// <summary>
/// Expand these properties (which are probably classes themselves) into sub-fields.
/// </summary>
HashSet<string> expand;
/// <summary>
/// A property and its value.
/// </summary>
public class PropertyValue : IName
{
/// <summary>
/// Name of property.
/// </summary>
public string ObjectName { get; private set; }
/// <summary>
/// Value of property.
/// </summary>
public string Value { get; private set; }
/// <summary>
/// Create a property value with a given name and value.
/// </summary>
/// <param name="name">Name of property.</param>
/// <param name="value">Value of property.</param>
public PropertyValue(string name, string value)
{
this.ObjectName = name ?? "<null>";
this.Value = value ?? "<null>";
}
/// <summary>
/// String representation of the property value.
/// </summary>
/// <returns>A string representing the property value.</returns>
public override string ToString()
{
return this.ObjectName + "=" + this.Value;
}
}
/// <summary>
/// Type of the data item.
/// </summary>
// ReSharper disable once StaticFieldInGenericType
static Type dataType = typeof(T);
/// <summary>
/// Common initialization code.
/// </summary>
private void Initialize()
{
this.skip = new HashSet<string>();
this.expand = new HashSet<string>();
}
/// <summary>
/// Create a propertyenumerator which looks at the properties of an
/// </summary>
/// <param name="data">Object whose properties should be enumerated.</param>
public PropertyEnumerator(T data)
{
this.Initialize();
this.Data = data;
}
/// <summary>
/// Create an empty property enumerator, with no object bound yet.
/// </summary>
public PropertyEnumerator()
{
this.Initialize();
this.Data = default(T);
this.SetExpandAllNonPrimitive = false;
this.ValueFormatter = null;
}
/// <summary>
/// Item whose properties are listed.
/// </summary>
public T Data { get; set; }
/// <summary>
/// In not null this function is used to format the values returned as strings.
/// </summary>
public Func<object, string> ValueFormatter { get; set; }
/// <summary>
/// Formats the value depending on the type.
/// </summary>
/// <param name="propertyValue">Value to format.</param>
/// <returns>The formatted value.</returns>
string FormattedValue(object propertyValue)
{
if (this.ValueFormatter != null)
return this.ValueFormatter(propertyValue);
return propertyValue.ToString();
}
/// <summary>
/// If set all non-primitive properties (structs) are expanded by default.
/// </summary>
public bool SetExpandAllNonPrimitive
{
set
{
if (value)
{
IEnumerable<PropertyInfo> properties = PropertyEnumerator<T>.dataType.GetProperties();
foreach (var prop in properties)
{
if (prop.PropertyType == typeof(string)) continue;
IEnumerable<PropertyInfo> recursiveproperties = prop.PropertyType.GetProperties();
if (recursiveproperties.Count() > 1)
this.expand.Add(prop.Name);
}
}
}
}
/// <summary>
/// Extract all properties whose values are computed.
/// </summary>
public IEnumerable<string> AllPropertyNames()
{
IEnumerable<string> properties = PropertyEnumerator<T>.dataType.GetProperties().
Where(prop => !this.skip.Contains(prop.Name)).
SelectMany(this.ExtracRecursivetProperties);
return properties;
}
/// <summary>
/// If the property has to be expanded recursively extract its properties.
/// </summary>
/// <param name="prop">Property to expand on.</param>
/// <returns>The list of properties of this property.</returns>
private IEnumerable<string> ExtracRecursivetProperties(PropertyInfo prop)
{
if (!this.expand.Contains(prop.Name))
return new string [] { prop.Name };
IEnumerable<PropertyInfo> retval = prop.GetType().GetProperties();
return retval.Select(p => prop.Name + "." + p.Name);
}
/// <summary>
/// Extract the value of a property; if the property has to be expanded, it will be a list of property values.
/// </summary>
/// <param name="prop">Property whose value is extracted.</param>
/// <returns>A collection of property values with one or more elements.</returns>
private IEnumerable<PropertyValue> ExtractPropertyValue(PropertyInfo prop)
{
object propvalue = prop.GetValue(this.Data, null);
if (this.expand.Contains(prop.Name))
{
IEnumerable<PropertyValue> retval =
propvalue.GetType().GetProperties().Select(p =>
new PropertyValue(prop.Name + "." + p.Name, this.FormattedValue(p.GetValue(propvalue, null))));
foreach (PropertyValue p in retval)
yield return p;
}
else
{
yield return new PropertyValue(prop.Name, this.FormattedValue(propvalue));
}
}
/// <summary>
/// Get the list of all interesting properties of this object.
/// <param name="retval">Populate this list with the property values.</param>
/// </summary>
public void PopulateWithProperties(IList<PropertyEnumerator<T>.PropertyValue> retval)
{
IEnumerable<PropertyValue> properties = PropertyEnumerator<T>.dataType.GetProperties().
Where(prop => !this.skip.Contains(prop.Name)).
SelectMany(this.ExtractPropertyValue);
foreach (PropertyValue v in properties)
retval.Add(v);
}
/// <summary>
/// Do not display these properties.
/// </summary>
/// <param name="properties">Properties to skip.</param>
public void Skip(params string[] properties)
{
foreach (string p in properties)
this.skip.Add(p);
}
/// <summary>
/// Do not display these properties.
/// </summary>
/// <param name="properties">Properties to skip.</param>
public void Skip(IEnumerable<string> properties)
{
foreach (string p in properties)
this.skip.Add(p);
}
/// <summary>
/// Expand the fields of these properties.
/// </summary>
/// <param name="properties">Properties to expand; each subproperty will become one result.</param>
public void Expand(params string[] properties)
{
foreach (string p in properties)
this.expand.Add(p);
}
}
/// <summary>
/// Base implementation of file streamer.
/// </summary>
public abstract class BaseFileStreamer
{
/// <summary>
/// True if the file is expected to contain a header.
/// </summary>
protected bool HasHeader { get; set; }
/// <summary>
/// File header.
/// </summary>
protected string[] header;
/// <summary>
/// Used to read the file.
/// </summary>
protected ISharedStreamReader reader;
/// <summary>
/// Used to write the file.
/// </summary>
protected StreamWriter writer;
/// <summary>
/// Number of fields on each line.
/// </summary>
protected int fields;
/// <summary>
/// Current line number in file.
/// </summary>
protected long currentLine;
/// <summary>
/// If true, all lines are expected to have the same length.
/// </summary>
public bool AllLinesSameLength { get; set; }
/// <summary>
/// File to access.
/// </summary>
protected readonly string filename;
/// <summary>
/// Mode the stream operates.
/// </summary>
FileMode mode;
/// <summary>
/// Delegate used to report errors.
/// </summary>
protected readonly StatusReporter statusReporter;
/// <summary>
/// Create a base streamer.
/// </summary>
/// <param name="filename">File to access.</param>
/// <param name="mode">File mode.</param>
/// <param name="statusReporter">Delegate used to report errors.</param>
/// <param name="keepNewlines">If true keep newlines.</param>
protected BaseFileStreamer(string filename, FileMode mode, bool keepNewlines, StatusReporter statusReporter)
{
this.reader = null;
this.writer = null;
this.filename = filename;
this.mode = mode;
this.statusReporter = statusReporter;
// ReSharper disable once DoNotCallOverridableMethodsInConstructor
this.Reset(keepNewlines);
}
/// <summary>
/// Go to the beginning.
/// </summary>
public virtual void Reset(bool keepNewlines)
{
this.Close();
switch (this.mode)
{
case FileMode.Open:
this.reader = new FileSharedStreamReader(filename, keepNewlines);
if (this.reader.Exception != null)
throw this.reader.Exception;
this.writer = null;
break;
case FileMode.Create:
this.writer = new StreamWriter(filename);
this.reader = null;
break;
default:
this.Error("no support for file mode " + this.mode);
break;
}
}
/// <summary>
/// We are done reading/writing to the csv file.
/// </summary>
public virtual void Close()
{
if (this.reader != null)
this.reader.Close();
else if (this.writer != null)
this.writer.Close();
}
/// <summary>
/// Read one line from the file. If the file has a header, the header should be read first.
/// </summary>
/// <returns>The line read.</returns>
public virtual string[] ReadLine()
{
if (this.HasHeader && this.header == null)
{
this.Error("Attempt to read a line from a file before reading the header");
}
return this.ReadLineInternal();
}
/// <summary>
/// Returns the header of the file; if the file does not have a header, this will trigger an exception.
/// Must be called before ReadLine().
/// </summary>
/// <returns>The file header.</returns>
public virtual string[] ReadHeader()
{
if (!this.HasHeader)
this.Error("Attempt to read header from a file without a header");
this.header = ReadLineInternal();
return this.header;
}
/// <summary>
/// Line being parsed.
/// </summary>
public virtual long CurrentLineNumber { get { return this.currentLine; } }
/// <summary>
/// Must be called before WriteLine. Writes the file header.
/// </summary>
/// <param name="hdr">File header.</param>
public virtual void WriteHeader(IEnumerable<string> hdr)
{
if (!this.HasHeader)
this.Error("Attempt to add a header to a file without header");
this.header = hdr.ToArray();
this.WriteLineInternal(this.header.ToList());
}
/// <summary>
/// Writes a line in the file; if the file has a header, must be called after WriteHeader.
/// </summary>
/// <param name="line">Line to write to the file.</param>
public virtual void WriteLine(IEnumerable<string> line)
{
if (this.HasHeader && this.header == null)
this.Error("Attempt to write a line in file before a header");
this.WriteLineInternal(line.ToList());
}
/// <summary>
/// The contents of the file, except the header.
/// Closes the file at the end.
/// </summary>
/// <returns>An iterator over the contents.</returns>
public IEnumerable<string[]> ReadFile()
{
string[] line;
if (this.HasHeader)
{
line = this.ReadHeader();
if (line == null)
goto done;
}
while (true)
{
line = this.ReadLine();
if (line == null)
{
goto done;
}
yield return line;
}
done:
this.reader.Close();
this.reader = null;
yield break;
}
/// <summary>
/// Internal implementation for line reading.
/// </summary>
/// <returns>The line read as a sequence of strings. Returns null when there is nothing to read.</returns>
protected abstract string[] ReadLineInternal();
/// <summary>
/// Internal implementation of writing.
/// </summary>
/// <param name="line">Strings to be written to one file line.</param>
protected abstract void WriteLineInternal(List<string> line);
/// <summary>
/// Signal an error.
/// </summary>
/// <param name="message">Error message.</param>
protected abstract void Error(string message);
}
/// <summary>
/// Reads a file as a set of key-value pairs.
/// Each record is a complete line.
/// TODO: Change this to properly parse the fields.
/// </summary>
public class KVPFileStreamer : BaseFileStreamer
{
private const string separator = ",";
/// <summary>
/// Create a file streamer which knows how to operate on a KVP file.
/// It always reads and writes complete lines.
/// </summary>
/// <param name="filename">File to operate on.</param>
/// <param name="mode">Mode of access.</param>
/// <param name="statusReporter">Delegate used to report errors.</param>
public KVPFileStreamer(string filename, FileMode mode, StatusReporter statusReporter)
: base(filename, mode, false, statusReporter)
{
}
/// <summary>
/// Internal read implementation.
/// </summary>
/// <returns>A line read from the file.</returns>
protected override string[] ReadLineInternal()
{
string line = this.reader.ReadLine();
if (line == null)
{
return null;
}
this.currentLine++;
return new string[] { line };
}
/// <summary>
/// Internal write implementation.
/// </summary>
/// <param name="line">Line to write.</param>
protected override void WriteLineInternal(List<string> line)
{
StringBuilder builder = new StringBuilder();
bool first = true;
foreach (string w in line)
{
if (!first)
builder.Append(separator);
builder.Append(w);
first = false;
}
this.writer.WriteLine(builder.ToString());
this.currentLine++;
}
/// <summary>
/// Signal an error.
/// </summary>
/// <param name="message">Error message.</param>
protected override void Error(string message)
{
this.statusReporter("KVP file `" + this.filename + "' format error on line " + this.currentLine + ": " + message, StatusKind.Error);
}
}
/// <summary>
/// A comma-separated list of values in a file; may be used to read or write the file (but not both at the same time).
/// (The separator may be something else besides comma too).
/// Values may be quoted.
/// </summary>
public class CSVFileStreamer : BaseFileStreamer
{
/// <summary>
/// Create a CSV file.
/// </summary>
/// <param name="filename">File to create.</param>
/// <param name="mode">Read or write?</param>
/// <param name="hasHeader">True if the file contains a header.</param>
/// <param name="statusReporter">Delegate used to report errors.</param>
public CSVFileStreamer(string filename, FileMode mode, bool hasHeader, StatusReporter statusReporter)
: base(filename, mode, false, statusReporter)
{
this.HasHeader = hasHeader;
this.header = null;
this.fields = -1; // not known yet
this.Separator = ',';
this.currentLine = 0;
this.AllLinesSameLength = true;
}
/// <summary>
/// Field separator; by default it's comma.
/// </summary>
private char separator;
/// <summary>
/// The field separator.
/// </summary>
public char Separator
{
set
{
if (this.currentLine != 0)
this.Error("You cannot change the file separator in the middle of the file");
if (value == '\"')
throw new ArgumentException("You cannot use the quote as a field separator in a CSV file");
this.separator = value;
}
get
{
return this.separator;
}
}
private void Check(IEnumerable<string> line)
{
int linelen = line.Count();
if (this.fields == -1)
this.fields = linelen;
else if (this.AllLinesSameLength && (this.fields != linelen))
this.Error("Line has " + linelen + " instead of " + this.fields + " fields.");
}
/// <summary>
/// Internal line reading from CSV files.
/// </summary>
/// <returns>A set of tokens read from a line.</returns>
/// <remarks>True when done reading.</remarks>
protected override string[] ReadLineInternal()
{
if (this.reader.EndOfStream)
{
return null;
}
string line = this.reader.ReadLine();
if (!line.Contains("\""))
{
// quick case
string[] retval = line.Split(this.Separator);
Check(retval);
this.currentLine++;
return retval;
}
tryToSplit:
List<string> results = new List<string>();
int currentIndex = 0;
while (currentIndex < line.Length)
{
string word;
if (line[currentIndex] == '\"')
{
currentIndex = this.ParseQuotedWord(line, currentIndex, out word);
}
else
{
currentIndex = this.ParseUnquotedWord(line, currentIndex, out word);
}
if (currentIndex == -1)
{
string nextOne = this.reader.ReadLine();
line += nextOne;
this.currentLine++;
goto tryToSplit; // retry parsing together with the next line
}
results.Add(word);
if (currentIndex < line.Length)
{
// expect a separator
if (line[currentIndex] != this.separator)
this.Error("Not found expected separator character");
currentIndex++;
}
}
this.currentLine++;
this.Check(results);
return results.ToArray();
}
/// <summary>
/// Read an unquoted word from the given line starting at index 'currentIndex'.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <param name="currentIndex">Start index of word.</param>
/// <param name="word">Found word.</param>
/// <returns>Index of separator after word (or of end of line).</returns>
private int ParseUnquotedWord(string line, int currentIndex, out string word)
{
int separatorIndex = line.IndexOf(this.separator, currentIndex);
if (separatorIndex == -1)
{
word = line.Substring(currentIndex);
return line.Length;
}
else
{
word = line.Substring(currentIndex, separatorIndex - currentIndex);
return separatorIndex;
}
}
/// <summary>
/// Read a quoted word from the given line starting at index 'currentIndex'.
/// </summary>
/// <param name="line">Line to parse.</param>
/// <param name="currentIndex">Start index of word.</param>
/// <param name="word">Found word.</param>
/// <returns>Index of separator after word (or of end of line). Returns -1 if the end quote is missing.</returns>
private int ParseQuotedWord(string line, int currentIndex, out string word)
{
int endIndex = currentIndex + 1;
while (true)
{
endIndex = line.IndexOf('\"', endIndex);
if (endIndex == -1)
{
this.Error("Newline in quoted string");
word = "";
return -1;
}
if (endIndex == line.Length - 1)
{
// last word on line
break;
}
else if (line[endIndex + 1] == '\"')
{
// quoted quote, continue
endIndex += 2;
continue;
}
else
{
// end of quoted word
break;
}
}
word = line.Substring(currentIndex + 1, endIndex - currentIndex - 1); // drop the start and end quotes
word = word.Replace("\"\"", "\""); // fix quoted quotes
return endIndex + 1;
}
/// <summary>
/// Signal an error that occurred during file reading.
/// </summary>
/// <param name="message">Message describing the error.</param>
protected override void Error(string message)
{
this.statusReporter("CSV file `" + Path.GetFileName(this.filename) + "' format error on line " + this.currentLine + ": " + message, StatusKind.Error);
}
/// <summary>
/// Add a line to the file.
/// </summary>
/// <param name="line">Line to add.</param>
protected override void WriteLineInternal(List<string> line)
{
Check(line);
bool first = true;
foreach (string word in line)
{
string wordToWrite = Quote(word);
if (!first)
this.writer.Write("{0}", this.separator);
else
first = false;
this.writer.Write("{0}", wordToWrite);
}
this.writer.WriteLine();
this.currentLine++;
}
private string Quote(string word)
{
if (word.Contains('\"'))
{
// double the quotes
word = word.Replace("\"", "\"\"");
return "\"" + word + "\"";
}
// quote the whole word if it contains spaces
if (word.Contains(this.Separator))
return "\"" + word + "\"";
else
return word;
}
}
/// <summary>
/// The objects in this class have each a unique id (in the class).
/// </summary>
public interface IUniqueId
{
/// <summary>
/// Get the object's unique id.
/// </summary>
int Id { get; }
}
/// <summary>
/// Interface for objects which have a name.
/// </summary>
public interface IName
{
/// <summary>
/// Get the object's name. Cannot use just 'name' since this property is often already defined.
/// </summary>
string ObjectName { get; }
}
/// <summary>
/// A stripped-down stream reader.
/// Never throws an exception; rather, stores the state in the 'Exception' field.
/// </summary>
public interface ISharedStreamReader : IDisposable
{
/// <summary>
/// Exception that occurred while opening the stream.
/// </summary>
Exception Exception { get; }
/// <summary>
/// True if the reader has reached the end of stream.
/// </summary>
bool EndOfStream { get; }
/// <summary>
/// Read one line from the stream.
/// </summary>
/// <returns></returns>
string ReadLine();
/// <summary>
/// Close the actual stream reader.
/// </summary>
void Close();
/// <summary>
/// Read the stream to the end from the current position.
/// </summary>
/// <returns>The contents of the stream.</returns>
/// <param name="token">Can be used to cancel the reading.</param>
string ReadToEnd(CancellationToken token);
/// <summary>
/// Read all the lines remaining in the stream.
/// </summary>
/// <returns>An iterator over all remaining lines.</returns>
IEnumerable<string> ReadAllLines();
}
/// <summary>
/// Common functionality too all ISharedStreamReaders.
/// </summary>
public abstract class BaseSharedStreamReader : ISharedStreamReader
{
/// <summary>
/// Exception that occurred while opening the stream.
/// </summary>
public Exception Exception { get; protected set; }
/// <summary>
/// An exception occurred while trying to build this stream.
/// </summary>
/// <param name="ex">Exception that occurred.</param>
protected BaseSharedStreamReader(Exception ex)
{
this.Exception = ex;
}
/// <summary>
/// Basic shared stream reader.
/// </summary>
protected BaseSharedStreamReader()
{
this.Exception = null;
}
/// <summary>
/// True if we have reached the end of the stream.
/// </summary>
public abstract bool EndOfStream { get; }
/// <summary>
/// Read one line from the stream.
/// </summary>
/// <returns></returns>
public abstract string ReadLine();
/// <summary>
/// Dispose of the stream.
/// </summary>
public abstract void Dispose();
/// <summary>
/// Read the whole stream to the end.
/// </summary>
/// <returns>A string containing the whole contents of the stream.</returns>
/// <param name="token">Can be used to cancel the reading.</param>
public virtual string ReadToEnd(CancellationToken token)
{
StringBuilder result = new StringBuilder();
foreach (string s in this.ReadAllLines())
{
token.ThrowIfCancellationRequested();
result.AppendLine(s);
}
return result.ToString();
}
/// <summary>
/// Close the stream.
/// </summary>
public abstract void Close();
/// <summary>
/// Read all the lines remaining in the stream.
/// </summary>
/// <returns>An iterator over all remaining lines.</returns>
public IEnumerable<string> ReadAllLines()
{
while (! this.EndOfStream)
{
yield return this.ReadLine();
}
this.Close();
}
}
/// <summary>
/// SharedStreamReader reading from another stream.
/// </summary>
public class SharedStreamReader : BaseSharedStreamReader
{
/// <summary>
/// If not null use this stream to write to the cache.
/// </summary>
StreamWriter cacheWriter;
/// <summary>
/// Delegate to call when stream is closed if caching.
/// </summary>
Action onClose;
/// <summary>
/// If true the reader keeps all newlines.
/// </summary>
private bool readerKeepsNewlines;
/// <summary>
/// A shared stream reader representing an exception.
/// </summary>
/// <param name="ex">Exception represented by this stream reader.</param>
public SharedStreamReader(Exception ex)
: base(ex)
{
this.cacheWriter = null;
}
/// <summary>
/// Actual stream where the data is being read from.
/// </summary>
protected ISimpleStreamReader actualReader;
/// <summary>
/// Create a stream reader for the specified stream.
/// </summary>
/// <param name="reader">Stream to read.</param>
/// <param name="keepNewlines">Keep newlines in the input stream.</param>
public SharedStreamReader(ISimpleStreamReader reader, bool keepNewlines)
{
this.actualReader = reader;
this.cacheWriter = null;
this.readerKeepsNewlines = keepNewlines;
this.readerKeepsNewlines = false;
}
/// <summary>
/// Create a stream reader for the specified stream, cache the file in the specified stream.
/// </summary>
/// <param name="reader">Stream to read.</param>
/// <param name="cacheWriter">Use this stream to copy the file to a cache.</param>
/// <param name="onClose">Delegate to call when stream is completely read.</param>
/// <param name="readerKeepsNewlines">If true the reader keeps all newlines.</param>
public SharedStreamReader(ISimpleStreamReader reader, StreamWriter cacheWriter, bool readerKeepsNewlines, Action onClose)
{
this.readerKeepsNewlines = readerKeepsNewlines;
this.actualReader = reader;
this.cacheWriter = cacheWriter;
this.onClose = onClose;
}
/// <summary>
/// Set the cache writer stream.
/// </summary>
/// <param name="cw">Stream used to write to the cache.</param>
/// <param name="onCl">Action to invoke on close.</param>
/// <param name="keepNewlines">If true keep newlines.</param>
public void SetCacheWriter(StreamWriter cw, bool keepNewlines, Action onCl)
{
this.readerKeepsNewlines = keepNewlines;
this.cacheWriter = cw;
this.onClose = onCl;
}
/// <summary>
/// SharedStreamReader reading from a null stream.
/// </summary>
public SharedStreamReader()
{
this.actualReader = new WrapperSimpleStreamReader(StreamReader.Null);
this.cacheWriter = null;
this.readerKeepsNewlines = false;
}
/// <summary>
/// True if the reader has reached the end of stream.
/// </summary>
public override bool EndOfStream
{
get
{
return this.actualReader.EndOfStream;
}
}
/// <summary>
/// Read one line from the stream.
/// </summary>
/// <returns>The read line.</returns>
public override string ReadLine()
{
string line = this.actualReader.ReadLine();
if (this.cacheWriter != null)
{
if (this.readerKeepsNewlines)
this.cacheWriter.Write(line);
else
this.cacheWriter.WriteLine(line);
}
return line;
}
/// <summary>
/// Close the actual stream reader.
/// </summary>
public override void Close()
{
this.actualReader.Close();
if (this.cacheWriter != null)
this.cacheWriter.Close();
if (this.onClose != null)
this.onClose();
}
/// <summary>
/// Done with the stream.
/// </summary>
public override void Dispose()
{
this.actualReader.Dispose();
}
/// <summary>
/// Read the stream to the end from the current position.
/// </summary>
/// <returns>The contents of the stream.</returns>
/// <param name="token">Can be used to cancel the reading.</param>
public override string ReadToEnd(CancellationToken token)
{
token.ThrowIfCancellationRequested();
string result = this.actualReader.ReadToEnd(token);
if (this.cacheWriter != null)
{
this.cacheWriter.Write(result);
this.cacheWriter.Close();
if (this.onClose != null)
this.onClose();
}
return result;
}
}
/// <summary>
/// SharedStreamReader reading from another stream.
/// </summary>
public class FileSharedStreamReader : SharedStreamReader
{
/// <summary>
/// File that is being read.
/// </summary>
string file;
/// <summary>
/// Create a file stream reader representing an exception.
/// </summary>
/// <param name="ex">Exception.</param>
public FileSharedStreamReader(Exception ex)
: base(ex)
{ }
/// <summary>
/// Create a stream reader for the specified file.
/// </summary>
/// <param name="file">File to read.</param>
/// <param name="keepNewline">If true keep newlines.</param>
public FileSharedStreamReader(string file, bool keepNewline)
{
try
{
this.file = file;
Stream rd = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
this.actualReader = new SimpleStreamReader(rd, keepNewline);
}
catch (Exception ex)
{
// I don't know how to handle other exceptions.
this.file = "Exception: " + ex.Message;
this.Exception = ex;
return;
}
}
/// <summary>
/// Create a file shared stream reader backed-up by a cache.
/// </summary>
/// <param name="file">File to read from.</param>
/// <param name="cache">Cache here the contents read from the file.</param>
/// <param name="onClose">Action to invoke when file is closed.</param>
/// <param name="keepNewlines">If true keep newlines.</param>
public FileSharedStreamReader(string file, StreamWriter cache, bool keepNewlines, Action onClose)
{
try
{
Stream rd = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
this.actualReader = new SimpleStreamReader(rd, true);
this.SetCacheWriter(cache, keepNewlines, onClose);
}
catch (Exception ex)
{
// I don't know how to handle other exceptions.
this.Exception = ex;
return;
}
}
/// <summary>
/// String representation of the File reader.
/// </summary>
/// <returns>A string representing the stream reader.</returns>
public override string ToString()
{
return this.file;
}
}
/// <summary>
/// A shared stream reader reading from a string collection.
/// </summary>
public class StringIteratorStreamReader : BaseSharedStreamReader
{
/// <summary>
/// Read from this iterator.
/// </summary>
IEnumerator<string> contents;
/// <summary>
/// True if we have reached the end of the stream.
/// </summary>
bool endOfStream;
private bool keepNewlines;
/// <summary>
/// End of the stream.
/// </summary>
public override bool EndOfStream
{
get { return this.endOfStream; }
}
/// <summary>
/// Create a stream reader representing an exception.
/// </summary>
/// <param name="ex">Exception.</param>
public StringIteratorStreamReader(Exception ex)
: base(ex)
{
this.endOfStream = true;
}
/// <summary>
/// A string iterator reading from this data.
/// </summary>
/// <param name="data">Strings to read from; each is a "line".</param>
/// <param name="keepNewlines">If true keep the newlines.</param>
public StringIteratorStreamReader(IEnumerable<string> data, bool keepNewlines)
{
this.keepNewlines = keepNewlines;
this.contents = data.GetEnumerator();
this.endOfStream = !this.contents.MoveNext();
}
/// <summary>
/// Read one line from the stream.
/// </summary>
/// <returns>One line from the set of strings.</returns>
public override string ReadLine()
{
string line = this.contents.Current;
this.endOfStream = !this.contents.MoveNext();
return line;
}
/// <summary>
/// Get rid of the stream.
/// </summary>
public override void Dispose()
{
}
/// <summary>
/// Finish reading the stream.
/// </summary>
public override void Close()
{
this.endOfStream = true;
this.contents = null;
}
}
/// <summary>
/// A completely stripped-down StreamReader; only 3 public methods.
/// </summary>
public interface ISimpleStreamReader
: IDisposable
{
/// <summary>
/// Read one line of text.
/// </summary>
/// <returns>The read line.</returns>
string ReadLine();
/// <summary>
/// True if we have reached the end of stream.
/// </summary>
bool EndOfStream { get; }
/// <summary>
/// Read all the data from the stream.
/// </summary>
/// <param name="token">Cancellation token.</param>
/// <returns>All the data in the stream.</returns>
string ReadToEnd(CancellationToken token);
/// <summary>
/// Close the stream.
/// </summary>
void Close();
}
/// <summary>
/// A simple stream reader which wraps a true StreamReader.
/// This class is only here for compatibility purposes, it should be unused.
/// </summary>
public class WrapperSimpleStreamReader : ISimpleStreamReader
{
private StreamReader reader;
/// <summary>
/// Create a Wrapper around a stream reader.
/// </summary>
/// <param name="reader">Stream reader to wrap.</param>
public WrapperSimpleStreamReader(StreamReader reader)
{
this.reader = reader;
}
/// <summary>
/// Read one line of text.
/// </summary>
/// <returns>The read line.</returns>
public string ReadLine()
{
return this.reader.ReadLine();
}
/// <summary>
/// True if we have reached the end of stream.
/// </summary>
public bool EndOfStream
{
get { return this.reader.EndOfStream; }
}
/// <summary>
/// Read all the data from the stream.
/// </summary>
/// <param name="token">Cancellation token.</param>
/// <returns>All the data in the stream.</returns>
public string ReadToEnd(CancellationToken token)
{
token.ThrowIfCancellationRequested();
return this.reader.ReadToEnd();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <filterpriority>2</filterpriority>
public void Dispose()
{
this.reader.Dispose();
}
/// <summary>
/// Close the stream.
/// </summary>
public void Close()
{
this.reader.Close();
}
}
/// <summary>
/// A simple stream reader. This is almost like a standard StreamReader, but
/// has the option not to strip the end-of-line characters in ReadLine.
/// </summary>
public class SimpleStreamReader : ISimpleStreamReader
{
/// <summary>
/// Stream we are reading from.
/// </summary>
private Stream stream;
/// <summary>
/// If true do not strip the End of line characters.
/// </summary>
private bool keepEndOfLine;
/// <summary>
/// FIFO buffer.
/// </summary>
/// <typeparam name="T">Type of data stored in buffer.</typeparam>
private class Buffer<T>
{
/// <summary>
/// Data in buffer.
/// </summary>
public T[] Data;
/// <summary>
/// Last index of data available in the buffer.
/// </summary>
public int EndOfData;
/// <summary>
/// Current position for reading from buffer.
/// </summary>
public int CurrentPosition;
private const int minBufferSize = 4096;
/// <summary>
/// Allocate a buffer.
/// </summary>
/// <param name="size">Buffer size.</param>
public Buffer(int size)
{
if (size < minBufferSize)
size = minBufferSize;
this.Data = new T[size];
this.EndOfData = 0;
this.CurrentPosition = 0;
}
/// <summary>
/// Actual size of allocated buffer.
/// </summary>
public int Size
{
get
{
return this.Data.Length;
}
}
/// <summary>
/// True if the buffer is empty.
/// </summary>
/// <returns>A boolean indicating whether there is any data in the buffer.</returns>
public bool IsEmpty()
{
return this.CurrentPosition == this.EndOfData;
}
/// <summary>
/// Items available in the buffer.
/// </summary>
public int ItemsAvailable
{
get { return this.EndOfData - this.CurrentPosition; }
}
/// <summary>
/// Value at specified index in buffer.
/// </summary>
/// <param name="index">Index of value in buffer.</param>
/// <returns>The value in the buffer.</returns>
public T this[int index]
{
get
{
if (index < 0 || index > this.EndOfData)
throw new ArgumentOutOfRangeException("index", "must be between 0 and " + this.EndOfData);
return this.Data[index];
}
}
/// <summary>
/// Delete first items from the buffer.
/// </summary>
/// <param name="items">Number of items to delete.</param>
public void DeletePreamble(int items)
{
if (this.EndOfData < items)
throw new ArgumentException("Cannot delete " + items + " from buffer since only " + this.EndOfData + " are present");
Buffer.BlockCopy(this.Data, items, this.Data, 0, this.EndOfData - items);
this.EndOfData -= items;
}
}
/// <summary>
/// Buffer for read data.
/// </summary>
private Buffer<byte> byteBuffer;
/// <summary>
/// Buffer for decoded data.
/// </summary>
private Buffer<char> charBuffer;
private bool checkPreamble;
/// <summary>
/// Files may have a preamble which indicates the data encoding.
/// </summary>
private byte[] preamble;
/// <summary>
/// If true encoding must be detected.
/// </summary>
private bool encodingUnknown;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private Encoding dataEncoding;
private Decoder dataDecoder;
/// <summary>
/// Create a SimpleStream reader to read from a stream.
/// </summary>
/// <param name="Stream">Stream to read from.</param>
/// <param name="keepEndOfLine">If true do not strip the end of line characters.</param>
public SimpleStreamReader(Stream Stream, bool keepEndOfLine = false)
: this(Stream, keepEndOfLine, Encoding.UTF8, true)
{
}
/// <summary>
/// Create a SimpleStream reader reading from a stream.
/// </summary>
/// <param name="stream">Stream to read from.</param>
/// <param name="keepEndOfLine">If true do not strip the end of line characters.</param>
/// <param name="bufferSize">Size of buffer to use when reading from the underlying stream.</param>
/// <param name="encoding">Character encoding to use.</param>
/// <param name="detectEncoding">If true detect the encoding.</param>
public SimpleStreamReader(Stream stream, bool keepEndOfLine, Encoding encoding, bool detectEncoding, int bufferSize = 4096)
{
if (stream == null)
throw new ArgumentNullException("stream");
if (!stream.CanRead)
throw new ArgumentException("StreamNotReadable");
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException("bufferSize");
this.dataEncoding = encoding;
this.dataDecoder = this.dataEncoding.GetDecoder();
this.stream = stream;
this.keepEndOfLine = keepEndOfLine;
this.byteBuffer = new Buffer<byte>(bufferSize);
int charSize = encoding.GetMaxCharCount(this.byteBuffer.Size);
this.charBuffer = new Buffer<char>(charSize);
this.encodingUnknown = detectEncoding;
this.preamble = encoding.GetPreamble();
this.checkPreamble = (this.preamble.Length > 0);
}
/// <summary>
/// Read one line of text.
/// </summary>
/// <returns>The read line.</returns>
public string ReadLine()
{
if (this.stream == null)
throw new InvalidOperationException("Reader closed");
if (this.charBuffer.IsEmpty())
{
if (this.ReadAndConvert() == 0) return null;
}
StringBuilder sb = new StringBuilder();
do
{
int i = this.charBuffer.CurrentPosition;
do
{
char ch = this.charBuffer[i];
if (ch == '\r' || ch == '\n')
{
int offset = this.keepEndOfLine ? 1 : 0;
sb.Append(this.charBuffer.Data, this.charBuffer.CurrentPosition, i + offset - this.charBuffer.CurrentPosition);
this.charBuffer.CurrentPosition = i + 1;
if (ch == '\r')
{
// check for \r\n
if (this.charBuffer.CurrentPosition < this.charBuffer.EndOfData || // one more character available
this.ReadAndConvert() > 0) // read next batch
{
if (this.charBuffer[this.charBuffer.CurrentPosition] == '\n')
this.charBuffer.CurrentPosition++;
if (this.keepEndOfLine)
sb.Append('\n');
}
}
return sb.ToString();
}
i++;
} while (i < this.charBuffer.EndOfData);
sb.Append(this.charBuffer.Data, this.charBuffer.CurrentPosition, this.charBuffer.ItemsAvailable);
}
while (this.ReadAndConvert() > 0);
return sb.ToString();
}
/// <summary>
/// True if we have reached the end of stream.
/// </summary>
public bool EndOfStream
{
get
{
if (this.stream == null)
throw new InvalidOperationException("Reader closed");
if (!this.charBuffer.IsEmpty())
return false;
int numRead = this.ReadAndConvert();
return numRead == 0;
}
}
private bool CheckForPreamble()
{
if (this.checkPreamble)
{
int len = (this.byteBuffer.EndOfData >= (this.preamble.Length)) ?
(this.preamble.Length - this.byteBuffer.CurrentPosition) : this.byteBuffer.ItemsAvailable;
for (int i = 0; i < len; i++)
{
if (this.byteBuffer.Data[this.byteBuffer.CurrentPosition + i] != this.preamble[this.byteBuffer.CurrentPosition + i])
{
this.checkPreamble = false;
return false;
}
}
// preamble found
this.byteBuffer.DeletePreamble(this.preamble.Length);
this.byteBuffer.CurrentPosition = 0;
this.checkPreamble = false;
this.encodingUnknown = false;
}
return this.checkPreamble;
}
/// <summary>
/// Read one buffer of bytes, and transfer them to a byte of chars.
/// Return number of chars transferred.
/// </summary>
/// <returns>The number of chars read.</returns>
private int ReadAndConvert()
{
this.charBuffer.EndOfData = this.charBuffer.CurrentPosition = 0;
if (!this.checkPreamble)
this.byteBuffer.EndOfData = 0;
do
{
if (this.checkPreamble)
{
int len = stream.Read(this.byteBuffer.Data, this.byteBuffer.CurrentPosition, this.byteBuffer.Size - this.byteBuffer.CurrentPosition);
if (len == 0)
{
if (this.byteBuffer.EndOfData > 0)
{
this.charBuffer.EndOfData += this.dataDecoder.GetChars(this.byteBuffer.Data, 0, this.byteBuffer.EndOfData, this.charBuffer.Data, this.charBuffer.EndOfData);
this.byteBuffer.CurrentPosition = this.byteBuffer.EndOfData = 0;
}
return this.charBuffer.EndOfData;
}
this.byteBuffer.EndOfData += len;
}
else
{
this.byteBuffer.EndOfData = stream.Read(this.byteBuffer.Data, 0, this.byteBuffer.Size);
if (this.byteBuffer.EndOfData == 0) // EOF
return this.charBuffer.EndOfData;
}
if (this.CheckForPreamble())
continue;
if (this.encodingUnknown && this.byteBuffer.EndOfData >= 2)
this.DetectEncoding();
this.charBuffer.EndOfData += this.dataDecoder.GetChars(this.byteBuffer.Data, 0, this.byteBuffer.EndOfData, this.charBuffer.Data, this.charBuffer.EndOfData);
} while (this.charBuffer.EndOfData == 0);
return this.charBuffer.EndOfData;
}
private void DetectEncoding()
{
// first few bytes in the buffer, starting at 0
int len = this.byteBuffer.EndOfData;
if (len < 2)
return;
this.encodingUnknown = false;
bool changed = false;
if (this.byteBuffer[0] == 0xFE && this.byteBuffer[1] == 0xFF)
{
// Big Endian Unicode
this.dataEncoding = new UnicodeEncoding(true, true);
this.byteBuffer.DeletePreamble(2);
changed = true;
}
else if (this.byteBuffer[0] == 0xFF && this.byteBuffer[1] == 0xFE)
{
if (len < 4 || this.byteBuffer[2] != 0 || this.byteBuffer[3] != 0)
{
this.dataEncoding = new UnicodeEncoding(false, true);
this.byteBuffer.DeletePreamble(2);
changed = true;
}
else
{
this.dataEncoding = new UTF32Encoding(false, true);
this.byteBuffer.DeletePreamble(4);
changed = true;
}
}
else if (len >= 3 && this.byteBuffer[0] == 0xEF && this.byteBuffer[1] == 0xBB && this.byteBuffer[2] == 0xBF)
{
this.dataEncoding = Encoding.UTF8;
this.byteBuffer.DeletePreamble(3);
changed = true;
}
else if (len >= 4 && this.byteBuffer[0] == 0 && this.byteBuffer[1] == 0 &&
this.byteBuffer[2] == 0xFE && this.byteBuffer[3] == 0xFF)
{
this.dataEncoding = new UTF32Encoding(true, true);
this.byteBuffer.DeletePreamble(4);
changed = true;
}
else if (len == 2)
this.encodingUnknown = true;
if (changed)
{
this.dataDecoder = this.dataEncoding.GetDecoder();
int maxCharsPerBuffer = this.dataEncoding.GetMaxCharCount(byteBuffer.Size);
this.charBuffer = new Buffer<char>(maxCharsPerBuffer);
}
}
/// <summary>
/// Read all the data from the stream.
/// </summary>
/// <param name="token">Cancellation token.</param>
/// <returns>All the data in the stream.</returns>
public string ReadToEnd(CancellationToken token)
{
if (this.stream == null)
throw new InvalidOperationException("Stream closed");
StringBuilder builder = new StringBuilder();
do
{
token.ThrowIfCancellationRequested();
builder.Append(this.charBuffer.Data, this.charBuffer.CurrentPosition, this.charBuffer.ItemsAvailable);
this.charBuffer.CurrentPosition = this.charBuffer.EndOfData;
this.ReadAndConvert();
} while (this.charBuffer.EndOfData > 0);
return builder.ToString();
}
/// <summary>
/// Close the stream.
/// </summary>
public void Close()
{
this.Dispose(true);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <filterpriority>2</filterpriority>
public void Dispose()
{
this.Dispose(false);
}
/// <summary>
/// Internal dispose method.
/// </summary>
/// <param name="disposing"></param>
protected void Dispose(bool disposing)
{
this.stream.Dispose();
this.stream = null;
this.charBuffer = null;
this.byteBuffer = null;
}
}
}