Lucene.Net.Support.DictionaryExtensions: Added Load and Store methods from Apache Harmony, so an IDictionary<string, string> can be used the same way the Properties class is used in Java (saving and loading the same format).
Project: http://git-wip-us.apache.org/repos/asf/lucenenet/repo Commit: http://git-wip-us.apache.org/repos/asf/lucenenet/commit/cd2d3514 Tree: http://git-wip-us.apache.org/repos/asf/lucenenet/tree/cd2d3514 Diff: http://git-wip-us.apache.org/repos/asf/lucenenet/diff/cd2d3514 Branch: refs/heads/master Commit: cd2d3514e3fcda42ec1168667e91657f64aac5f6 Parents: dc67a55 Author: Shad Storhaug <[email protected]> Authored: Mon Jul 31 06:55:19 2017 +0700 Committer: Shad Storhaug <[email protected]> Committed: Wed Aug 2 09:53:09 2017 +0700 ---------------------------------------------------------------------- src/Lucene.Net.Tests/Lucene.Net.Tests.csproj | 2 + .../Support/TestDictionaryExtensions.cs | 411 +++++++++++++++++++ .../Support/hyts_PropertiesTest.properties | 29 ++ src/Lucene.Net/Support/DictionaryExtensions.cs | 409 +++++++++++++++++- 4 files changed, 850 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/lucenenet/blob/cd2d3514/src/Lucene.Net.Tests/Lucene.Net.Tests.csproj ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Tests/Lucene.Net.Tests.csproj b/src/Lucene.Net.Tests/Lucene.Net.Tests.csproj index 7631329..13282e1 100644 --- a/src/Lucene.Net.Tests/Lucene.Net.Tests.csproj +++ b/src/Lucene.Net.Tests/Lucene.Net.Tests.csproj @@ -118,6 +118,7 @@ <None Include="Lucene.Net.snk" /> <None Include="Lucene.Net.Tests.project.json" /> <EmbeddedResource Include="Store\LUCENENET521.zip" /> + <EmbeddedResource Include="Support\hyts_PropertiesTest.properties" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Lucene.Net.Analysis.Common\Lucene.Net.Analysis.Common.csproj"> @@ -522,6 +523,7 @@ <Compile Include="Support\IO\TestReadOnlyHeapByteBuffer.cs" /> <Compile Include="Support\IO\TestStreamTokenizer.cs" /> <Compile Include="Support\SmallObject.cs" /> + <Compile Include="Support\TestDictionaryExtensions.cs" /> <Compile Include="Support\TestPriorityQueue.cs" /> <Compile Include="Support\Threading\TestCloseableThreadLocal.cs" /> <Compile Include="Support\TestCollections.cs" /> http://git-wip-us.apache.org/repos/asf/lucenenet/blob/cd2d3514/src/Lucene.Net.Tests/Support/TestDictionaryExtensions.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Tests/Support/TestDictionaryExtensions.cs b/src/Lucene.Net.Tests/Support/TestDictionaryExtensions.cs new file mode 100644 index 0000000..540d964 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/TestDictionaryExtensions.cs @@ -0,0 +1,411 @@ +using Lucene.Net.Support; +using Lucene.Net.Util; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Lucene.Net.Tests.Support +{ + public class TestDictionaryExtensions : LuceneTestCase + { + [Test] + public void Test_loadLSystem_IO_Stream_ArgumentNullException() + { + Dictionary<string, string> p = new Dictionary<string, string>(); + try + { + p.Load((Stream)null); + fail("should throw NullPointerException"); + } +#pragma warning disable 168 + catch (ArgumentNullException e) +#pragma warning restore 168 + { + // Expected + } + } + + /** + * @tests java.util.Properties#load(java.io.InputStream) + */ + [Test] + public void Test_loadLSystem_IO_Stream() + { + Dictionary<string, string> prop = new Dictionary<string, string>(); + using (Stream @is = new MemoryStream(writeProperties())) + { + prop.Load(@is); + } + assertEquals("Failed to load correct properties", "harmony.tests", prop.get("test.pkg")); + assertNull("Load failed to parse incorrectly", prop + .get("commented.entry")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("=".getBytes())); + assertEquals("Failed to add empty key", "", prop.get("")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream(" = ".getBytes())); + assertEquals("Failed to add empty key2", "", prop.get("")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream(" a= b".getBytes())); + assertEquals("Failed to ignore whitespace", "b", prop.get("a")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream(" a b".getBytes())); + assertEquals("Failed to interpret whitespace as =", "b", prop.get("a")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("#comment\na=value" + .getBytes("UTF-8"))); + assertEquals("value", prop.get("a")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("#\u008d\u00d2\na=\u008d\u00d3" + .getBytes("ISO-8859-1"))); + assertEquals("Failed to parse chars >= 0x80", "\u008d\u00d3", prop + .get("a")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream( + "#properties file\r\nfred=1\r\n#last comment" + .getBytes("ISO-8859-1"))); + assertEquals("Failed to load when last line contains a comment", "1", + prop.get("fred")); + + // Regression tests for HARMONY-5414 + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("a=\\u1234z".getBytes())); + + prop = new Dictionary<string, string>(); + try + { + prop.Load(new MemoryStream("a=\\u123".getBytes())); + fail("should throw IllegalArgumentException"); + } +#pragma warning disable 168 + catch (ArgumentException e) +#pragma warning restore 168 + { + // Expected + } + + prop = new Dictionary<string, string>(); + try + { + prop.Load(new MemoryStream("a=\\u123z".getBytes())); + fail("should throw IllegalArgumentException"); + } + catch (ArgumentException /*expected*/) + { + // Expected + } + + prop = new Dictionary<string, string>(); + Dictionary<string, string> expected = new Dictionary<string, string>(); + expected.Put("a", "\u0000"); + prop.Load(new MemoryStream("a=\\".getBytes())); + assertEquals("Failed to read trailing slash value", expected, prop); + + prop = new Dictionary<string, string>(); + expected = new Dictionary<string, string>(); + expected.Put("a", "\u1234\u0000"); + prop.Load(new MemoryStream("a=\\u1234\\".getBytes())); + assertEquals("Failed to read trailing slash value #2", expected, prop); + + prop = new Dictionary<string, string>(); + expected = new Dictionary<string, string>(); + expected.Put("a", "q"); + prop.Load(new MemoryStream("a=\\q".getBytes())); + assertEquals("Failed to read slash value #3", expected, prop); + } + + /** + * @tests java.util.Properties#load(java.io.InputStream) + */ + [Test] + public void Test_loadLSystem_IO_Stream_Special() + { + // Test for method void java.util.Properties.load(java.io.InputStream) + Dictionary<string, string> prop = null; + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("=".getBytes())); + assertTrue("Failed to add empty key", prop.get("").equals("")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("=\r\n".getBytes())); + assertTrue("Failed to add empty key", prop.get("").equals("")); + + prop = new Dictionary<string, string>(); + prop.Load(new MemoryStream("=\n\r".getBytes())); + assertTrue("Failed to add empty key", prop.get("").equals("")); + } + + /** + * @tests java.util.Properties#load(java.io.InputStream) + */ + [Test] + public void Test_loadLSystem_IO_Stream_subtest0() + { + Dictionary<string, string> props = new Dictionary<string, string>(); + using (Stream input = GetType().getResourceAsStream("hyts_PropertiesTest.properties")) + props.Load(input); + + assertEquals("1", "\n \t \f", props.getProperty(" \r")); + assertEquals("2", "a", props.getProperty("a")); + assertEquals("3", "bb as,dn ", props.getProperty("b")); + assertEquals("4", ":: cu", props.getProperty("c\r \t\nu")); + assertEquals("5", "bu", props.getProperty("bu")); + assertEquals("6", "d\r\ne=e", props.getProperty("d")); + assertEquals("7", "fff", props.getProperty("f")); + assertEquals("8", "g", props.getProperty("g")); + assertEquals("9", "", props.getProperty("h h")); + assertEquals("10", "i=i", props.getProperty(" ")); + assertEquals("11", " j", props.getProperty("j")); + assertEquals("12", " c", props.getProperty("space")); + assertEquals("13", "\\", props.getProperty("dblbackslash")); + } + + /** + * @tests java.util.Properties#store(java.io.OutputStream, java.lang.String) + */ + [Test] + public void Test_storeLSystem_IO_StreamLSystem_String() + { + Dictionary<string, string> myProps = new Dictionary<string, string>(); + myProps.Put("Property A", " aye\\\f\t\n\r\b"); + myProps.Put("Property B", "b ee#!=:"); + myProps.Put("Property C", "see"); + + MemoryStream @out = new MemoryStream(); + myProps.Store(@out, "A Header"); + @out.Dispose(); + + MemoryStream @in = new MemoryStream(@out.ToArray()); + Dictionary<string, string> myProps2 = new Dictionary<string, string>(); + myProps2.Load(@in); + @in.Dispose(); + + using (var e = myProps.Keys.GetEnumerator()) + { + String nextKey; + while (e.MoveNext()) + { + nextKey = e.Current; + assertTrue("Stored property list not equal to original", myProps2 + .getProperty(nextKey).equals(myProps.getProperty(nextKey))); + } + } + } + + /** + * if loading from single line like "hello" without "\n\r" neither "=", it + * should be same as loading from "hello=" + */ + [Test] + public void TestLoadSingleLine() + { + Dictionary<string, string> props = new Dictionary<string, string>(); + Stream sr = new MemoryStream("hello".getBytes()); + props.Load(sr); + assertEquals(1, props.size()); + } + + private String comment1 = "comment1"; + + private String comment2 = "comment2"; + + private void validateOutput(String[] expectStrings, byte[] output) + { + MemoryStream bais = new MemoryStream(output); + TextReader br = new StreamReader(bais, + Encoding.GetEncoding("ISO-8859-1")); + foreach (String expectString in expectStrings) + { + assertEquals(expectString, br.ReadLine()); + } + br.ReadLine(); + assertNull(br.ReadLine()); + br.Dispose(); + } + + [Test] + public void TestStore_scenario0() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario1() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario2() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + '\n' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario3() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + '\r' + comment2); + validateOutput(new String[] { "#comment1", "#", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario4() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + '#' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario5() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + '!' + comment2); + validateOutput(new String[] { "#comment1", "!comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario6() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + '#' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario7() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + '!' + comment2); + validateOutput(new String[] { "#comment1", "!comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario8() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + '\n' + '#' + comment2); + validateOutput(new String[] { "#comment1", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario9() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + '\r' + '#' + comment2); + validateOutput(new String[] { "#comment1", "#", "#comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario10() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\r' + '\n' + '!' + comment2); + validateOutput(new String[] { "#comment1", "!comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + [Test] + public void TestStore_scenario11() + { + MemoryStream baos = new MemoryStream(); + Dictionary<string, string> props = new Dictionary<string, string>(); + props.Store(baos, comment1 + '\n' + '\r' + '!' + comment2); + validateOutput(new String[] { "#comment1", "#", "!comment2" }, + baos.ToArray()); + baos.Dispose(); + } + + + + protected byte[] writeProperties() + { + MemoryStream bout = new MemoryStream(); + TextWriter ps = new StreamWriter(bout); + ps.WriteLine("#commented.entry=Bogus"); + ps.WriteLine("test.pkg=harmony.tests"); + ps.WriteLine("test.proj=Automated Tests"); + ps.Dispose(); + return bout.ToArray(); + } + + } + + public static class Extensions + { + public static byte[] getBytes(this string input) + { + return Encoding.UTF8.GetBytes(input); + } + + public static byte[] getBytes(this string input, string encoding) + { + return Encoding.GetEncoding(encoding).GetBytes(input); + } + + public static string get(this IDictionary<string, string> dict, string key) + { + string result; + dict.TryGetValue(key, out result); + return result; + } + + public static string getProperty(this IDictionary<string, string> dict, string key) + { + string result; + dict.TryGetValue(key, out result); + return result; + } + } +} http://git-wip-us.apache.org/repos/asf/lucenenet/blob/cd2d3514/src/Lucene.Net.Tests/Support/hyts_PropertiesTest.properties ---------------------------------------------------------------------- diff --git a/src/Lucene.Net.Tests/Support/hyts_PropertiesTest.properties b/src/Lucene.Net.Tests/Support/hyts_PropertiesTest.properties new file mode 100644 index 0000000..6c1b950 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/hyts_PropertiesTest.properties @@ -0,0 +1,29 @@ + + + + + \ \r \n \t \f + + +! dshfjklahfjkldashgjl;as + #jdfagdfjagkdjfghksdajfd + +!!properties + +a=a +b bb as,dn +c\r\ \t\nu =:: cu +bu= b\ + u +d=d\r\ne=e +f :f\ +f\ + f +g g +h\u0020h +\ i=i +j=\ j +space=\ c + +dblbackslash=\\ + \ No newline at end of file http://git-wip-us.apache.org/repos/asf/lucenenet/blob/cd2d3514/src/Lucene.Net/Support/DictionaryExtensions.cs ---------------------------------------------------------------------- diff --git a/src/Lucene.Net/Support/DictionaryExtensions.cs b/src/Lucene.Net/Support/DictionaryExtensions.cs index 18acd6a..0f616cb 100644 --- a/src/Lucene.Net/Support/DictionaryExtensions.cs +++ b/src/Lucene.Net/Support/DictionaryExtensions.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; namespace Lucene.Net.Support { @@ -50,5 +53,409 @@ namespace Lucene.Net.Support dict[key] = value; return oldValue; } + + private static readonly int NONE = 0, SLASH = 1, UNICODE = 2, CONTINUE = 3, + KEY_DONE = 4, IGNORE = 5; + private static string lineSeparator = Environment.NewLine; + + + // LUCENENET NOTE: Sourced from Apache Harmony: + + /// <summary> + /// Loads properties from the specified <see cref="Stream"/>. The encoding is + /// ISO8859-1. + /// </summary> + /// <remarks> + /// The Properties file is interpreted according to the + /// following rules: + /// <list type="bullet"> + /// <item><description> + /// Empty lines are ignored. + /// </description></item> + /// <item><description> + /// Lines starting with either a "#" or a "!" are comment lines and are + /// ignored. + /// </description></item> + /// <item><description> + /// A backslash at the end of the line escapes the following newline + /// character ("\r", "\n", "\r\n"). If there's a whitespace after the + /// backslash it will just escape that whitespace instead of concatenating + /// the lines. This does not apply to comment lines. + /// </description></item> + /// <item><description> + /// A property line consists of the key, the space between the key and + /// the value, and the value. The key goes up to the first whitespace, "=" or + /// ":" that is not escaped. The space between the key and the value contains + /// either one whitespace, one "=" or one ":" and any number of additional + /// whitespaces before and after that character. The value starts with the + /// first character after the space between the key and the value. + /// </description></item> + /// <item><description> + /// Following escape sequences are recognized: "\ ", "\\", "\r", "\n", + /// "\!", "\#", "\t", "\b", "\f", and "\uXXXX" (unicode character). + /// </description></item> + /// </list> + /// <para/> + /// This method is to mimic and interoperate with the Properties class in Java, which + /// is essentially a string dictionary that natively supports importing and exporting to this format. + /// </remarks> + /// <param name="dict">This dictionary.</param> + /// <param name="input">The <see cref="Stream"/>.</param> + /// <exception cref="IOException">If error occurs during reading from the <see cref="Stream"/>.</exception> + public static void Load(this IDictionary<string, string> dict, Stream input) + { + if (input == null) + { + throw new ArgumentNullException("input"); + } + lock (dict) + { + int mode = NONE, unicode = 0, count = 0; + char nextChar; + char[] buf = new char[40]; + int offset = 0, keyLength = -1, intVal; + bool firstChar = true; + Stream bis = input; + + while (true) + { + intVal = bis.ReadByte(); + if (intVal == -1) + { + // if mode is UNICODE but has less than 4 hex digits, should + // throw an IllegalArgumentException + // luni.08=Invalid Unicode sequence: expected format \\uxxxx + if (mode == UNICODE && count < 4) + { + throw new ArgumentException("Invalid Unicode sequence: expected format \\uxxxx"); //$NON-NLS-1$ + } + // if mode is SLASH and no data is read, should append '\u0000' + // to buf + if (mode == SLASH) + { + buf[offset++] = '\u0000'; + } + break; + } + nextChar = (char)(intVal & 0xff); + + if (offset == buf.Length) + { + char[] newBuf = new char[buf.Length * 2]; + System.Array.Copy(buf, 0, newBuf, 0, offset); + buf = newBuf; + } + if (mode == UNICODE) + { + int digit = Character.Digit(nextChar, 16); + if (digit >= 0) + { + unicode = (unicode << 4) + digit; + if (++count < 4) + { + continue; + } + } + else if (count <= 4) + { + // luni.09=Invalid Unicode sequence: illegal character + throw new ArgumentException("Invalid Unicode sequence: illegal character"); //$NON-NLS-1$ + } + mode = NONE; + buf[offset++] = (char)unicode; + if (nextChar != '\n') + { + continue; + } + } + if (mode == SLASH) + { + mode = NONE; + switch (nextChar) + { + case '\r': + mode = CONTINUE; // Look for a following \n + continue; + case '\n': + mode = IGNORE; // Ignore whitespace on the next line + continue; + case 'b': + nextChar = '\b'; + break; + case 'f': + nextChar = '\f'; + break; + case 'n': + nextChar = '\n'; + break; + case 'r': + nextChar = '\r'; + break; + case 't': + nextChar = '\t'; + break; + case 'u': + mode = UNICODE; + unicode = count = 0; + continue; + } + } + else + { + switch (nextChar) + { + case '#': + case '!': + if (firstChar) + { + while (true) + { + intVal = bis.ReadByte(); + if (intVal == -1) + { + break; + } + // & 0xff not required + nextChar = (char)intVal; + if (nextChar == '\r' || nextChar == '\n') + { + break; + } + } + continue; + } + break; + case '\n': + if (mode == CONTINUE) + { // Part of a \r\n sequence + mode = IGNORE; // Ignore whitespace on the next line + continue; + } + // fall into the next case + mode = NONE; + firstChar = true; + if (offset > 0 || (offset == 0 && keyLength == 0)) + { + if (keyLength == -1) + { + keyLength = offset; + } + string temp = new string(buf, 0, offset); + dict.Put(temp.Substring(0, keyLength), temp + .Substring(keyLength)); + } + keyLength = -1; + offset = 0; + continue; + case '\r': + mode = NONE; + firstChar = true; + if (offset > 0 || (offset == 0 && keyLength == 0)) + { + if (keyLength == -1) + { + keyLength = offset; + } + string temp = new string(buf, 0, offset); + dict.Put(temp.Substring(0, keyLength), temp + .Substring(keyLength)); + } + keyLength = -1; + offset = 0; + continue; + case '\\': + if (mode == KEY_DONE) + { + keyLength = offset; + } + mode = SLASH; + continue; + case ':': + case '=': + if (keyLength == -1) + { // if parsing the key + mode = NONE; + keyLength = offset; + continue; + } + break; + } + if (nextChar < 256 && char.IsWhiteSpace(nextChar)) + { + if (mode == CONTINUE) + { + mode = IGNORE; + } + // if key length == 0 or value length == 0 + if (offset == 0 || offset == keyLength || mode == IGNORE) + { + continue; + } + if (keyLength == -1) + { // if parsing the key + mode = KEY_DONE; + continue; + } + } + if (mode == IGNORE || mode == CONTINUE) + { + mode = NONE; + } + } + firstChar = false; + if (mode == KEY_DONE) + { + keyLength = offset; + mode = NONE; + } + buf[offset++] = nextChar; + } + if (keyLength == -1 && offset > 0) + { + keyLength = offset; + } + if (keyLength >= 0) + { + string temp = new string(buf, 0, offset); + dict.Put(temp.Substring(0, keyLength), temp.Substring(keyLength)); + } + } + } + + /// <summary> + /// Stores the mappings in this Properties to the specified + /// <see cref="Stream"/>, putting the specified comment at the beginning. The + /// output from this method is suitable for being read by the + /// <see cref="Load(IDictionary{string, string}, Stream)"/> method. + /// </summary> + /// <param name="dict">This dictionary.</param> + /// <param name="output">The output <see cref="Stream"/> to write to.</param> + /// <param name="comments">The comments to put at the beginning.</param> + /// <exception cref="IOException">If an error occurs during the write to the <see cref="Stream"/>.</exception> + /// <exception cref="InvalidCastException">If the key or value of a mapping is not a <see cref="string"/>.</exception> + public static void Store(this IDictionary<string, string> dict, Stream output, string comments) + { + lock (dict) + { + StreamWriter writer = new StreamWriter(output, Encoding.GetEncoding("iso-8859-1")); //$NON-NLS-1$ + if (comments != null) + { + WriteComments(writer, comments); + } + writer.Write('#'); + writer.Write(new DateTime().ToString("yyyy-MM-dd")); + writer.Write(lineSeparator); + + StringBuilder buffer = new StringBuilder(200); + foreach (var entry in dict) + { + string key = entry.Key; + DumpString(buffer, key, true); + buffer.Append('='); + DumpString(buffer, entry.Value, false); + buffer.Append(lineSeparator); + writer.Write(buffer.ToString()); + buffer.Length = 0; + } + writer.Flush(); + } + } + + private static void WriteComments(TextWriter writer, string comments) + { + writer.Write('#'); + char[] chars = comments.ToCharArray(); + for (int index = 0; index < chars.Length; index++) + { + if (chars[index] == '\r' || chars[index] == '\n') + { + int indexPlusOne = index + 1; + if (chars[index] == '\r' && indexPlusOne < chars.Length + && chars[indexPlusOne] == '\n') + { + // "\r\n" + continue; + } + writer.Write(lineSeparator); + if (indexPlusOne < chars.Length + && (chars[indexPlusOne] == '#' || chars[indexPlusOne] == '!')) + { + // return char with either '#' or '!' afterward + continue; + } + writer.Write('#'); + } + else + { + writer.Write(chars[index]); + } + } + writer.Write(lineSeparator); + } + + private static void DumpString(StringBuilder buffer, string str, bool isKey) + { + int index = 0, length = str.Length; + if (!isKey && index < length && str[index] == ' ') + { + buffer.Append("\\ "); //$NON-NLS-1$ + index++; + } + + for (; index < length; index++) + { + char ch = str[index]; + switch (ch) + { + case '\t': + buffer.Append("\\t"); //$NON-NLS-1$ + break; + case '\n': + buffer.Append("\\n"); //$NON-NLS-1$ + break; + case '\f': + buffer.Append("\\f"); //$NON-NLS-1$ + break; + case '\r': + buffer.Append("\\r"); //$NON-NLS-1$ + break; + default: + if ("\\#!=:".IndexOf(ch) >= 0 || (isKey && ch == ' ')) + { + buffer.Append('\\'); + } + if (ch >= ' ' && ch <= '~') + { + buffer.Append(ch); + } + else + { + buffer.Append(ToHexaDecimal(ch)); + } + break; + } + } + } + + private static char[] ToHexaDecimal(int ch) + { + char[] hexChars = { '\\', 'u', '0', '0', '0', '0' }; + int hexChar, index = hexChars.Length, copyOfCh = ch; + do + { + hexChar = copyOfCh & 15; + if (hexChar > 9) + { + hexChar = hexChar - 10 + 'A'; + } + else + { + hexChar += '0'; + } + hexChars[--index] = (char)hexChar; + //} while ((copyOfCh >>>= 4) != 0); + } while ((copyOfCh = (int)((uint)copyOfCh >> 4)) != 0); + return hexChars; + } } } \ No newline at end of file
