diff --git a/CHANGELOG.md b/CHANGELOG.md index e235e6f..4d34d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - CsvWriter for creating CSV files + Settings + - AppendText method for IcdFile + - IcdStreamWriter, a wrapper for a StreamWriter + - New XML conversion framework for performance improvements +### Changed + - XmlUtils is now using the improved XML conversion framework ## [5.0.0] - 2018-09-14 ### Added diff --git a/ICD.Common.Utils.Tests/Xml/XmlUtilsTest.cs b/ICD.Common.Utils.Tests/Xml/XmlUtilsTest.cs index 027455d..9b0b95a 100644 --- a/ICD.Common.Utils.Tests/Xml/XmlUtilsTest.cs +++ b/ICD.Common.Utils.Tests/Xml/XmlUtilsTest.cs @@ -90,6 +90,7 @@ namespace ICD.Common.Utils.Tests.Xml { paths.Add(args.Path); nodes.Add(args.Outer); + return true; } ); diff --git a/ICD.Common.Utils/Csv/CsvWriter.cs b/ICD.Common.Utils/Csv/CsvWriter.cs new file mode 100644 index 0000000..0911129 --- /dev/null +++ b/ICD.Common.Utils/Csv/CsvWriter.cs @@ -0,0 +1,135 @@ +using System; +using System.Linq; +using ICD.Common.Properties; +using ICD.Common.Utils.IO; + +namespace ICD.Common.Utils.Csv +{ + public sealed class CsvWriter : IDisposable + { + private const string QUOTATION_MARK = "\""; + private const string DOUBLE_QUOTE_MARK = "\"\""; + + private readonly IcdTextWriter m_Writer; + + private readonly string m_Seperator; + private readonly string m_LineTerminator; + private readonly bool m_AlwaysEscape; + + private bool m_NewLine; + + /// + /// Constructor. + /// + public CsvWriter(IcdTextWriter writer, bool spaceAfterComma, bool alwaysEscape, string newline, params string[] header) + { + m_NewLine = true; + m_Writer = writer; + m_Seperator = spaceAfterComma ? ", " : ","; + m_AlwaysEscape = alwaysEscape; + m_LineTerminator = newline; + + if(header.Any()) + AppendRow(header); + } + + ~CsvWriter() + { + Dispose(); + } + + /// + /// Calls ToString() for each item and adds the row to the builder. + /// + /// + [PublicAPI] + public void AppendRow(params object[] row) + { + foreach (object value in row) + AppendValue(value); + AppendNewline(); + } + + /// + /// Adds the row to the builder. + /// + /// + [PublicAPI] + public void AppendRow(params string[] row) + { + foreach (string value in row) + AppendValue(value); + AppendNewline(); + } + + /// + /// Calls ToString() on the item and adds it to the builder. + /// + /// + [PublicAPI] + public void AppendValue(object value) + { + AppendValue(string.Format("{0}", value)); + } + + /// + /// Adds a value to the builder. + /// + /// + [PublicAPI] + public void AppendValue(string value) + { + if (!m_NewLine) + m_Writer.WrappedTextWriter.Write(m_Seperator); + + if (m_AlwaysEscape || value.Contains(",")) + { + value = value.Replace(QUOTATION_MARK, DOUBLE_QUOTE_MARK); + + // Append the value, surrounded by quotes + m_Writer.WrappedTextWriter.Write(QUOTATION_MARK); + m_Writer.WrappedTextWriter.Write(value); + m_Writer.WrappedTextWriter.Write(QUOTATION_MARK); + } + else + { + m_Writer.WrappedTextWriter.Write(value); + } + + m_NewLine = false; + } + + /// + /// Adds a New Line To the Builder + /// + [PublicAPI] + public void AppendNewline() + { + m_Writer.WrappedTextWriter.Write(m_LineTerminator); + + m_NewLine = true; + } + + public void Dispose() + { + m_Writer.Dispose(); + } + + /// + /// Instantiates a new CsvWriter with the properties given in the CsvWriterSettings. + /// + /// + /// + /// + /// + [PublicAPI] + public static CsvWriter Create(IcdTextWriter writer, CsvWriterSettings settings, params string[] header) + { + return new CsvWriter(writer, + settings.InsertSpaceAfterComma, + settings.AlwaysEscapeEveryValue, + settings.NewLineSequence, + header); + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Csv/CsvWriterSettings.cs b/ICD.Common.Utils/Csv/CsvWriterSettings.cs new file mode 100644 index 0000000..249abdf --- /dev/null +++ b/ICD.Common.Utils/Csv/CsvWriterSettings.cs @@ -0,0 +1,46 @@ +using ICD.Common.Properties; + +namespace ICD.Common.Utils.Csv +{ + public sealed class CsvWriterSettings + { + private bool m_InsertSpaceAfterComma = true; + private bool m_AlwaysEscapeEveryValue = true; + private string m_NewLineSequence = IcdEnvironment.NewLine; + + /// + /// Gets/Sets whether to insert a space between elements, after the comma + /// Defaults to true. + /// + [PublicAPI] + public bool InsertSpaceAfterComma + { + get { return m_InsertSpaceAfterComma; } + set { m_InsertSpaceAfterComma = value; } + } + + /// + /// Gets/Sets whether to always escape the values. + /// If true, values are recorded surrounded by quotes, regardless of if they contain a comma or not. Quotes are escaped. + /// If false, values are recorded as the value without quotes, unless escaping is required. + /// Defaults to true. + /// + [PublicAPI] + public bool AlwaysEscapeEveryValue + { + get { return m_AlwaysEscapeEveryValue; } + set { m_AlwaysEscapeEveryValue = value; } + } + + /// + /// Gets/Sets the newline character or characters to deliniate records. + /// Defaults to System.NewLine. + /// + [PublicAPI] + public string NewLineSequence + { + get { return m_NewLineSequence; } + set { m_NewLineSequence = value; } + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj index 289cef7..ba191bc 100644 --- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj +++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj @@ -85,6 +85,8 @@ + + @@ -109,6 +111,7 @@ + @@ -195,12 +198,17 @@ + + + + + diff --git a/ICD.Common.Utils/IO/IcdFile.cs b/ICD.Common.Utils/IO/IcdFile.cs index 7e01e98..c31696e 100644 --- a/ICD.Common.Utils/IO/IcdFile.cs +++ b/ICD.Common.Utils/IO/IcdFile.cs @@ -94,5 +94,11 @@ namespace ICD.Common.Utils.IO { return new IcdFileStream(File.Create(path)); } + + [PublicAPI] + public static IcdStreamWriter AppendText(string path) + { + return new IcdStreamWriter(File.AppendText(path)); + } } } diff --git a/ICD.Common.Utils/IO/IcdStreamWriter.cs b/ICD.Common.Utils/IO/IcdStreamWriter.cs new file mode 100644 index 0000000..4d5604a --- /dev/null +++ b/ICD.Common.Utils/IO/IcdStreamWriter.cs @@ -0,0 +1,22 @@ +using System; +#if SIMPLSHARP +using Crestron.SimplSharp.CrestronIO; +#elif STANDARD +using System.IO; +#endif + +namespace ICD.Common.Utils.IO +{ + public sealed class IcdStreamWriter : IcdTextWriter + { + public StreamWriter WrappedStreamWriter { get { return WrappedTextWriter as StreamWriter; } } + + /// + /// Constructor. + /// + /// + public IcdStreamWriter(StreamWriter baseStreamWriter) : base(baseStreamWriter) + { + } + } +} \ No newline at end of file diff --git a/ICD.Common.Utils/Xml/AbstractGenericXmlConverter.cs b/ICD.Common.Utils/Xml/AbstractGenericXmlConverter.cs new file mode 100644 index 0000000..e8fafcf --- /dev/null +++ b/ICD.Common.Utils/Xml/AbstractGenericXmlConverter.cs @@ -0,0 +1,181 @@ +using System; +#if SIMPLSHARP +using Crestron.SimplSharp.CrestronXml; +#else +using System.Xml; +#endif +using ICD.Common.Properties; + +namespace ICD.Common.Utils.Xml +{ + public abstract class AbstractGenericXmlConverter : AbstractXmlConverter + { + /// + /// Creates a new instance of T. + /// + /// + protected abstract T Instantiate(); + + /// + /// Writes the XML representation of the object. + /// + /// + /// + /// The value. + public sealed override void WriteXml(IcdXmlTextWriter writer, string elementName, object value) + { + if (writer == null) + throw new ArgumentNullException("writer"); + + if (value == null) + { + writer.WriteElementString(elementName, null); + return; + } + + WriteXml(writer, elementName, (T)value); + } + + /// + /// Writes the XML representation of the object. + /// + /// + /// + /// The value. + [PublicAPI] + public void WriteXml(IcdXmlTextWriter writer, string elementName, T value) + { + if (writer == null) + throw new ArgumentNullException("writer"); + + if (value == null) + { + writer.WriteElementString(elementName, null); + return; + } + + writer.WriteStartElement(elementName); + { + WriteAttributes(writer, value); + WriteElements(writer, value); + } + writer.WriteEndElement(); + } + + /// + /// Override to write attributes to the root element. + /// + /// + /// + protected virtual void WriteAttributes(IcdXmlTextWriter writer, T value) + { + } + + /// + /// Override to write elements to the writer. + /// + /// + /// + protected virtual void WriteElements(IcdXmlTextWriter writer, T value) + { + } + + /// + /// Reads the XML representation of the object. + /// + /// The XmlReader to read from. + /// + /// The object value. + /// + public sealed override object ReadXml(IcdXmlReader reader) + { + return ReadXmlTyped(reader); + } + + /// + /// Reads the XML representation of the object. + /// + /// The XmlReader to read from. + /// + /// The object value. + /// + [PublicAPI] + public virtual T ReadXmlTyped(IcdXmlReader reader) + { + // Read into the first node + if (reader.NodeType != XmlNodeType.Element && !reader.ReadToNextElement()) + throw new FormatException(); + + T output = default(T); + bool instantiated = false; + + while (true) + { + if (!instantiated) + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + if (reader.IsEmptyElement) + return default(T); + + output = Instantiate(); + instantiated = true; + + // Read the root attributes + while (reader.MoveToNextAttribute()) + ReadAttribute(reader, output); + + // Read out of the root element + if (!reader.Read()) + throw new FormatException(); + continue; + + default: + // Keep reading until we reach the root element. + if (!reader.Read()) + throw new FormatException(); + continue; + } + } + + switch (reader.NodeType) + { + case XmlNodeType.Element: + ReadElement(reader, output); + continue; + + case XmlNodeType.EndElement: + // Read out of the end element + reader.Read(); + return output; + + default: + if (!reader.Read()) + return output; + break; + } + } + } + + /// + /// Override to handle the current attribute. + /// + /// + /// + protected virtual void ReadAttribute(IcdXmlReader reader, T instance) + { + } + + /// + /// Override to handle the current element. + /// + /// + /// + protected virtual void ReadElement(IcdXmlReader reader, T instance) + { + // Skip the element + reader.Skip(); + } + } +} diff --git a/ICD.Common.Utils/Xml/AbstractXmlConverter.cs b/ICD.Common.Utils/Xml/AbstractXmlConverter.cs new file mode 100644 index 0000000..23a818c --- /dev/null +++ b/ICD.Common.Utils/Xml/AbstractXmlConverter.cs @@ -0,0 +1,25 @@ +using ICD.Common.Properties; + +namespace ICD.Common.Utils.Xml +{ + public abstract class AbstractXmlConverter : IXmlConverter + { + /// + /// Writes the XML representation of the object. + /// + /// + /// + /// The value. + public abstract void WriteXml(IcdXmlTextWriter writer, string elementName, object value); + + /// + /// Reads the XML representation of the object. + /// + /// The XmlReader to read from. + /// + /// The object value. + /// + [PublicAPI] + public abstract object ReadXml(IcdXmlReader reader); + } +} diff --git a/ICD.Common.Utils/Xml/DefaultXmlConverter.cs b/ICD.Common.Utils/Xml/DefaultXmlConverter.cs new file mode 100644 index 0000000..da72eea --- /dev/null +++ b/ICD.Common.Utils/Xml/DefaultXmlConverter.cs @@ -0,0 +1,32 @@ +using System; + +namespace ICD.Common.Utils.Xml +{ + public sealed class DefaultXmlConverter : AbstractXmlConverter + { + /// + /// Writes the XML representation of the object. + /// + /// + /// + /// The value. + public override void WriteXml(IcdXmlTextWriter writer, string elementName, object value) + { + string elementString = IcdXmlConvert.ToString(value); + + writer.WriteElementString(elementName, elementString); + } + + /// + /// Reads the XML representation of the object. + /// + /// The XmlReader to read from. + /// + /// The object value. + /// + public override object ReadXml(IcdXmlReader reader) + { + throw new NotSupportedException(); + } + } +} diff --git a/ICD.Common.Utils/Xml/IXmlConverter.cs b/ICD.Common.Utils/Xml/IXmlConverter.cs new file mode 100644 index 0000000..e695040 --- /dev/null +++ b/ICD.Common.Utils/Xml/IXmlConverter.cs @@ -0,0 +1,22 @@ +namespace ICD.Common.Utils.Xml +{ + public interface IXmlConverter + { + /// + /// Writes the XML representation of the object. + /// + /// + /// + /// The value. + void WriteXml(IcdXmlTextWriter writer, string elementName, object value); + + /// + /// Reads the XML representation of the object. + /// + /// The XmlReader to read from. + /// + /// The object value. + /// + object ReadXml(IcdXmlReader reader); + } +} diff --git a/ICD.Common.Utils/Xml/IcdXmlConvert.cs b/ICD.Common.Utils/Xml/IcdXmlConvert.cs index e9ed1a5..eda85b7 100644 --- a/ICD.Common.Utils/Xml/IcdXmlConvert.cs +++ b/ICD.Common.Utils/Xml/IcdXmlConvert.cs @@ -1,4 +1,6 @@ using System; +using System.Text; +using ICD.Common.Utils.IO; #if SIMPLSHARP using Crestron.SimplSharp.CrestronXml; #else @@ -9,6 +11,89 @@ namespace ICD.Common.Utils.Xml { public static class IcdXmlConvert { + /// + /// Serializes the given instance to an xml string. + /// + /// + /// + /// + public static string SerializeObject(string elementName, object value) + { + if (value == null) + return ToString(null); + + IXmlConverter converter = XmlConverterAttribute.GetConverterForInstance(value); + + StringBuilder builder = new StringBuilder(); + + using (IcdStringWriter stringWriter = new IcdStringWriter(builder)) + { + using (IcdXmlTextWriter writer = new IcdXmlTextWriter(stringWriter)) + converter.WriteXml(writer, elementName, value); + } + + return builder.ToString(); + } + + /// + /// Deserializes the given xml to an instance of the given type. + /// + /// + /// + /// + public static T DeserializeObject(string xml) + { + return (T)DeserializeObject(typeof(T), xml); + } + + /// + /// Deserializes the given xml to an instance of the given type. + /// + /// + /// + /// + private static object DeserializeObject(Type type, string xml) + { + if (type == null) + throw new ArgumentNullException("type"); + + using (IcdXmlReader reader = new IcdXmlReader(xml)) + return DeserializeObject(type, reader); + } + + /// + /// Deserializes the current node to an instance of the given type. + /// + /// + /// + /// + public static T DeserializeObject(IcdXmlReader reader) + { + if (reader == null) + throw new ArgumentNullException("reader"); + + return (T)DeserializeObject(typeof(T), reader); + } + + /// + /// Deserializes the current node to an instance of the given type. + /// + /// + /// + /// + public static object DeserializeObject(Type type, IcdXmlReader reader) + { + if (type == null) + throw new ArgumentNullException("type"); + + if (reader == null) + throw new ArgumentNullException("reader"); + + IXmlConverter converter = XmlConverterAttribute.GetConverterForType(type); + + return converter.ReadXml(reader); + } + public static string ToString(int value) { return XmlConvert.ToString(value); diff --git a/ICD.Common.Utils/Xml/XmlConverterAttribute.cs b/ICD.Common.Utils/Xml/XmlConverterAttribute.cs new file mode 100644 index 0000000..af24969 --- /dev/null +++ b/ICD.Common.Utils/Xml/XmlConverterAttribute.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using ICD.Common.Utils.Attributes; + +namespace ICD.Common.Utils.Xml +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public sealed class XmlConverterAttribute : AbstractIcdAttribute + { + private static readonly Dictionary s_InstanceTypeToConverter; + private static readonly Dictionary s_ConverterTypeToConverter; + + private readonly Type m_ConverterType; + + /// + /// Gets the converter type. + /// + public Type ConverterType { get { return m_ConverterType; } } + + /// + /// Static constructor. + /// + static XmlConverterAttribute() + { + s_InstanceTypeToConverter = new Dictionary(); + s_ConverterTypeToConverter = new Dictionary(); + } + + /// + /// Constructor. + /// + /// + public XmlConverterAttribute(Type converterType) + { + m_ConverterType = converterType; + } + + /// + /// Gets the XML converter for the given instance. + /// + /// + /// + public static IXmlConverter GetConverterForInstance(object value) + { + return value == null ? LazyLoadConverter(typeof(DefaultXmlConverter)) : GetConverterForType(value.GetType()); + } + + /// + /// Gets the XML converter for the given type. + /// + /// + /// + public static IXmlConverter GetConverterForType(Type type) + { + if (type == null) + throw new ArgumentNullException("type"); + + IXmlConverter converter; + if (!s_InstanceTypeToConverter.TryGetValue(type, out converter)) + { + XmlConverterAttribute attribute = AttributeUtils.GetClassAttribute(type); + Type converterType = attribute == null ? typeof(DefaultXmlConverter) : attribute.ConverterType; + + converter = LazyLoadConverter(converterType); + s_InstanceTypeToConverter[type] = converter; + } + + return converter; + } + + /// + /// Lazy-loads the converter of the given type. + /// + /// + /// + private static IXmlConverter LazyLoadConverter(Type converterType) + { + if (converterType == null) + throw new ArgumentNullException("converterType"); + + IXmlConverter converter; + if (!s_ConverterTypeToConverter.TryGetValue(converterType, out converter)) + { + converter = ReflectionUtils.CreateInstance(converterType) as IXmlConverter; + s_ConverterTypeToConverter[converterType] = converter; + } + + return converter; + } + } +} diff --git a/ICD.Common.Utils/Xml/XmlReaderExtensions.cs b/ICD.Common.Utils/Xml/XmlReaderExtensions.cs index 613a9c6..e7bd60f 100644 --- a/ICD.Common.Utils/Xml/XmlReaderExtensions.cs +++ b/ICD.Common.Utils/Xml/XmlReaderExtensions.cs @@ -115,7 +115,7 @@ namespace ICD.Common.Utils.Xml /// /// [PublicAPI] - public static void Recurse(this IcdXmlReader extends, Action callback) + public static void Recurse(this IcdXmlReader extends, Func callback) { if (extends == null) throw new ArgumentNullException("extends"); diff --git a/ICD.Common.Utils/Xml/XmlUtils.cs b/ICD.Common.Utils/Xml/XmlUtils.cs index 226bc1a..0f559f4 100644 --- a/ICD.Common.Utils/Xml/XmlUtils.cs +++ b/ICD.Common.Utils/Xml/XmlUtils.cs @@ -138,7 +138,7 @@ namespace ICD.Common.Utils.Xml /// /// [PublicAPI] - public static void Recurse(string xml, Action callback) + public static void Recurse(string xml, Func callback) { if (callback == null) throw new ArgumentNullException("callback"); @@ -152,7 +152,7 @@ namespace ICD.Common.Utils.Xml /// /// /// - private static void Recurse(string xml, Stack path, Action callback) + private static void Recurse(string xml, Stack path, Func callback) { if (path == null) throw new ArgumentNullException("path"); @@ -168,10 +168,11 @@ namespace ICD.Common.Utils.Xml path.Push(childReader.Name); string[] pathOutput = path.Reverse().ToArray(path.Count); - callback(new XmlRecursionEventArgs(xml, pathOutput)); - - foreach (string child in childReader.GetChildElementsAsString()) - Recurse(child, path, callback); + if (callback(new XmlRecursionEventArgs(xml, pathOutput))) + { + foreach (string child in childReader.GetChildElementsAsString()) + Recurse(child, path, callback); + } path.Pop(); }