Changed license to Apache License 2.0

This commit is contained in:
Markus Ewald 2024-06-13 18:36:21 +02:00 committed by cygon
parent d3bf0be9d7
commit 9f36d71529
144 changed files with 32422 additions and 32544 deletions

View file

@ -1,319 +1,318 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Nuclex.Support.Parsing;
namespace Nuclex.Support.Settings {
partial class ConfigurationFileStore {
#region class ParserState
/// <summary>Remembers the target store and current category of the parser</summary>
private class ParserState {
/// <summary>Store to which the parsed categories and options will be added</summary>
public ConfigurationFileStore Store;
/// <summary>Current category options belong to</summary>
public Category Category;
}
#endregion // class ParserState
/// <summary>Parses a configuration file from the specified text reader</summary>
/// <param name="reader">Reader the configuration file will be parsed from</param>
/// <returns>The configuration file parsed from the specified reader</returns>
public static ConfigurationFileStore Parse(TextReader reader) {
var store = new ConfigurationFileStore();
var state = new ParserState() {
Store = store,
Category = store.rootCategory
};
for(; ; ) {
string line = reader.ReadLine();
if(line == null) {
break;
}
parseLine(state, line);
}
return store;
}
/// <summary>Incrementally parses a line read from a configuration file</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line that has been read</param>
private static void parseLine(ParserState state, string line) {
// If the line is empty, ignore it
int length = line.Length;
if(length == 0) {
return;
}
// Skip all spaces at the beginning of the line
int firstCharacterIndex = 0;
ParserHelper.SkipSpaces(line, ref firstCharacterIndex);
// If the line contained nothing but spaces, ignore it
if(firstCharacterIndex == length) {
return;
}
// If the line is a comment, ignore it
if((line[firstCharacterIndex] == '#') || (line[firstCharacterIndex] == ';')) {
return;
}
// Now the line is either a category definition or some attempt to set an option
if(line[firstCharacterIndex] == '[') {
parseCategory(state, line, firstCharacterIndex);
} else {
parseOption(state, line, firstCharacterIndex);
}
state.Category.Lines.Add(line);
}
/// <summary>Parses a category definition encountered on a line</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line containing the category definition</param>
/// <param name="firstCharacterIndex">Index of the definition's first character</param>
private static void parseCategory(
ParserState state, string line, int firstCharacterIndex
) {
Debug.Assert(line[firstCharacterIndex] == '[');
int nameStartIndex = firstCharacterIndex + 1;
ParserHelper.SkipSpaces(line, ref nameStartIndex);
int lastCharacterIndex = line.Length - 1;
if(nameStartIndex >= lastCharacterIndex) {
return; // No space left for closing brace
}
int nameEndIndex = line.IndexOf(']', nameStartIndex);
if(nameEndIndex == -1) {
return; // No closing brace in line
}
// Skip any whitespaces between the last character and the closing brace
do {
--nameEndIndex;
} while(char.IsWhiteSpace(line, nameEndIndex));
// Now we know that the line holds a category definition and where exactly in
// the line the category name is located. Create the category.
state.Category = new Category() {
CategoryName = new StringSegment(
line, nameStartIndex, nameEndIndex - nameStartIndex + 1
),
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category);
}
/// <summary>Parses an option definition encountered on a line</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line containing the option definition</param>
/// <param name="firstCharacterIndex">Index of the definition's first character</param>
private static void parseOption(
ParserState state, string line, int firstCharacterIndex
) {
int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1);
if(assignmentIndex == -1) {
return; // No assignment took place
}
// Cut off any whitespaces between the option name and the assignment
int nameEndIndex = assignmentIndex;
do {
--nameEndIndex;
} while(char.IsWhiteSpace(line, nameEndIndex));
// We have enough information to know that this is an assignment of some kind
Option option = new Option() {
LineIndex = state.Category.Lines.Count,
OptionName = new StringSegment(
line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1
)
};
parseOptionValue(option, line, assignmentIndex + 1);
// We've got the option assignment, either with an empty or proper value
state.Store.options.Add(option);
state.Category.OptionLookup.Add(option.OptionName.ToString(), option);
}
/// <summary>Parses the value assigned to an option</summary>
/// <param name="option">Option to which a value is being assigned</param>
/// <param name="line">Line containing the option assignment</param>
/// <param name="assignmentEndIndex">Index one after the assignment character</param>
private static void parseOptionValue(Option option, string line, int assignmentEndIndex) {
int firstCharacterIndex = assignmentEndIndex;
ParserHelper.SkipSpaces(line, ref firstCharacterIndex);
// Just for beauty, when the option value is empty but padded with spaces,
// leave one space between the equals sign and the value.
if(firstCharacterIndex > assignmentEndIndex) {
++assignmentEndIndex;
}
// If the line consists of only whitespace, create an empty value
if(firstCharacterIndex == line.Length) {
option.OptionValue = new StringSegment(line, assignmentEndIndex, 0);
return;
}
char firstCharacter = line[firstCharacterIndex];
// Values can be quoted to allow for comments characters appearing in them
int lastCharacterIndex;
if(firstCharacter == '"') {
lastCharacterIndex = line.LastIndexOf('"');
} else {
lastCharacterIndex = firstCharacterIndex;
}
int commentStartIndex = line.IndexOf(';', lastCharacterIndex);
if(commentStartIndex == -1) {
commentStartIndex = line.IndexOf('#', lastCharacterIndex);
}
if(commentStartIndex == -1) {
lastCharacterIndex = line.Length - 1;
} else {
lastCharacterIndex = commentStartIndex - 1;
}
while(lastCharacterIndex > firstCharacterIndex) {
if(char.IsWhiteSpace(line, lastCharacterIndex)) {
--lastCharacterIndex;
} else {
break;
}
}
option.OptionValue = new StringSegment(
line, firstCharacterIndex, lastCharacterIndex - firstCharacterIndex + 1
);
}
/// <summary>Determines the best matching type for an option value</summary>
/// <param name="value">Value for which the best matching type will be found</param>
/// <returns>The best matching type for the specified option value</returns>
private static Type getBestMatchingType(ref StringSegment value) {
if(value.Count == 0) {
return typeof(string);
}
// If there are at least two characters, it may be an integer with
// a sign in front of it
if(value.Count >= 2) {
int index = value.Offset;
if(ParserHelper.SkipInteger(value.Text, ref index)) {
if(index >= value.Offset + value.Count) {
return typeof(int);
}
if(value.Text[index] == '.') {
return typeof(float);
}
}
} else { // If it's just a single character, it may be a number
if(char.IsNumber(value.Text, value.Offset)) {
return typeof(int);
}
}
// If it parses as a boolean literal, then it must be a boolean
if(parseBooleanLiteral(ref value) != null) {
return typeof(bool);
}
return typeof(string);
}
/// <summary>Tries to parse a boolean literal</summary>
/// <param name="value">Value that will be parsed as a boolean literal</param>
/// <returns>
/// True or false if the value was a boolean literal, null if it wasn't
/// </returns>
private static bool? parseBooleanLiteral(ref StringSegment value) {
switch(value.Count) {
// If the string spells 'no', it is considered a boolean
case 2: {
bool isSpellingNo =
((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) &&
((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O'));
return isSpellingNo ? new Nullable<bool>(false) : null;
}
// If the string spells 'yes', it is considered a boolean
case 3: {
bool isSpellingYes =
((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) &&
((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) &&
((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S'));
return isSpellingYes ? new Nullable<bool>(true) : null;
}
// If the string spells 'true', it is considered a boolean
case 4: {
bool isSpellingTrue =
((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) &&
((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) &&
((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) &&
((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E'));
return isSpellingTrue ? new Nullable<bool>(true) : null;
}
// If the string spells 'false', it is considered a boolean
case 5: {
bool isSpellingFalse =
((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) &&
((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) &&
((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) &&
((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) &&
((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E'));
return isSpellingFalse ? new Nullable<bool>(false) : null;
}
// Anything else is not considered a boolean
default: {
return null;
}
}
}
}
} // namespace Nuclex.Support.Configuration
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Nuclex.Support.Parsing;
namespace Nuclex.Support.Settings {
partial class ConfigurationFileStore {
#region class ParserState
/// <summary>Remembers the target store and current category of the parser</summary>
private class ParserState {
/// <summary>Store to which the parsed categories and options will be added</summary>
public ConfigurationFileStore Store;
/// <summary>Current category options belong to</summary>
public Category Category;
}
#endregion // class ParserState
/// <summary>Parses a configuration file from the specified text reader</summary>
/// <param name="reader">Reader the configuration file will be parsed from</param>
/// <returns>The configuration file parsed from the specified reader</returns>
public static ConfigurationFileStore Parse(TextReader reader) {
var store = new ConfigurationFileStore();
var state = new ParserState() {
Store = store,
Category = store.rootCategory
};
for(; ; ) {
string line = reader.ReadLine();
if(line == null) {
break;
}
parseLine(state, line);
}
return store;
}
/// <summary>Incrementally parses a line read from a configuration file</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line that has been read</param>
private static void parseLine(ParserState state, string line) {
// If the line is empty, ignore it
int length = line.Length;
if(length == 0) {
return;
}
// Skip all spaces at the beginning of the line
int firstCharacterIndex = 0;
ParserHelper.SkipSpaces(line, ref firstCharacterIndex);
// If the line contained nothing but spaces, ignore it
if(firstCharacterIndex == length) {
return;
}
// If the line is a comment, ignore it
if((line[firstCharacterIndex] == '#') || (line[firstCharacterIndex] == ';')) {
return;
}
// Now the line is either a category definition or some attempt to set an option
if(line[firstCharacterIndex] == '[') {
parseCategory(state, line, firstCharacterIndex);
} else {
parseOption(state, line, firstCharacterIndex);
}
state.Category.Lines.Add(line);
}
/// <summary>Parses a category definition encountered on a line</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line containing the category definition</param>
/// <param name="firstCharacterIndex">Index of the definition's first character</param>
private static void parseCategory(
ParserState state, string line, int firstCharacterIndex
) {
Debug.Assert(line[firstCharacterIndex] == '[');
int nameStartIndex = firstCharacterIndex + 1;
ParserHelper.SkipSpaces(line, ref nameStartIndex);
int lastCharacterIndex = line.Length - 1;
if(nameStartIndex >= lastCharacterIndex) {
return; // No space left for closing brace
}
int nameEndIndex = line.IndexOf(']', nameStartIndex);
if(nameEndIndex == -1) {
return; // No closing brace in line
}
// Skip any whitespaces between the last character and the closing brace
do {
--nameEndIndex;
} while(char.IsWhiteSpace(line, nameEndIndex));
// Now we know that the line holds a category definition and where exactly in
// the line the category name is located. Create the category.
state.Category = new Category() {
CategoryName = new StringSegment(
line, nameStartIndex, nameEndIndex - nameStartIndex + 1
),
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category);
}
/// <summary>Parses an option definition encountered on a line</summary>
/// <param name="state">Current parser state</param>
/// <param name="line">Line containing the option definition</param>
/// <param name="firstCharacterIndex">Index of the definition's first character</param>
private static void parseOption(
ParserState state, string line, int firstCharacterIndex
) {
int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1);
if(assignmentIndex == -1) {
return; // No assignment took place
}
// Cut off any whitespaces between the option name and the assignment
int nameEndIndex = assignmentIndex;
do {
--nameEndIndex;
} while(char.IsWhiteSpace(line, nameEndIndex));
// We have enough information to know that this is an assignment of some kind
Option option = new Option() {
LineIndex = state.Category.Lines.Count,
OptionName = new StringSegment(
line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1
)
};
parseOptionValue(option, line, assignmentIndex + 1);
// We've got the option assignment, either with an empty or proper value
state.Store.options.Add(option);
state.Category.OptionLookup.Add(option.OptionName.ToString(), option);
}
/// <summary>Parses the value assigned to an option</summary>
/// <param name="option">Option to which a value is being assigned</param>
/// <param name="line">Line containing the option assignment</param>
/// <param name="assignmentEndIndex">Index one after the assignment character</param>
private static void parseOptionValue(Option option, string line, int assignmentEndIndex) {
int firstCharacterIndex = assignmentEndIndex;
ParserHelper.SkipSpaces(line, ref firstCharacterIndex);
// Just for beauty, when the option value is empty but padded with spaces,
// leave one space between the equals sign and the value.
if(firstCharacterIndex > assignmentEndIndex) {
++assignmentEndIndex;
}
// If the line consists of only whitespace, create an empty value
if(firstCharacterIndex == line.Length) {
option.OptionValue = new StringSegment(line, assignmentEndIndex, 0);
return;
}
char firstCharacter = line[firstCharacterIndex];
// Values can be quoted to allow for comments characters appearing in them
int lastCharacterIndex;
if(firstCharacter == '"') {
lastCharacterIndex = line.LastIndexOf('"');
} else {
lastCharacterIndex = firstCharacterIndex;
}
int commentStartIndex = line.IndexOf(';', lastCharacterIndex);
if(commentStartIndex == -1) {
commentStartIndex = line.IndexOf('#', lastCharacterIndex);
}
if(commentStartIndex == -1) {
lastCharacterIndex = line.Length - 1;
} else {
lastCharacterIndex = commentStartIndex - 1;
}
while(lastCharacterIndex > firstCharacterIndex) {
if(char.IsWhiteSpace(line, lastCharacterIndex)) {
--lastCharacterIndex;
} else {
break;
}
}
option.OptionValue = new StringSegment(
line, firstCharacterIndex, lastCharacterIndex - firstCharacterIndex + 1
);
}
/// <summary>Determines the best matching type for an option value</summary>
/// <param name="value">Value for which the best matching type will be found</param>
/// <returns>The best matching type for the specified option value</returns>
private static Type getBestMatchingType(ref StringSegment value) {
if(value.Count == 0) {
return typeof(string);
}
// If there are at least two characters, it may be an integer with
// a sign in front of it
if(value.Count >= 2) {
int index = value.Offset;
if(ParserHelper.SkipInteger(value.Text, ref index)) {
if(index >= value.Offset + value.Count) {
return typeof(int);
}
if(value.Text[index] == '.') {
return typeof(float);
}
}
} else { // If it's just a single character, it may be a number
if(char.IsNumber(value.Text, value.Offset)) {
return typeof(int);
}
}
// If it parses as a boolean literal, then it must be a boolean
if(parseBooleanLiteral(ref value) != null) {
return typeof(bool);
}
return typeof(string);
}
/// <summary>Tries to parse a boolean literal</summary>
/// <param name="value">Value that will be parsed as a boolean literal</param>
/// <returns>
/// True or false if the value was a boolean literal, null if it wasn't
/// </returns>
private static bool? parseBooleanLiteral(ref StringSegment value) {
switch(value.Count) {
// If the string spells 'no', it is considered a boolean
case 2: {
bool isSpellingNo =
((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) &&
((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O'));
return isSpellingNo ? new Nullable<bool>(false) : null;
}
// If the string spells 'yes', it is considered a boolean
case 3: {
bool isSpellingYes =
((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) &&
((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) &&
((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S'));
return isSpellingYes ? new Nullable<bool>(true) : null;
}
// If the string spells 'true', it is considered a boolean
case 4: {
bool isSpellingTrue =
((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) &&
((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) &&
((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) &&
((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E'));
return isSpellingTrue ? new Nullable<bool>(true) : null;
}
// If the string spells 'false', it is considered a boolean
case 5: {
bool isSpellingFalse =
((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) &&
((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) &&
((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) &&
((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) &&
((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E'));
return isSpellingFalse ? new Nullable<bool>(false) : null;
}
// Anything else is not considered a boolean
default: {
return null;
}
}
}
}
} // namespace Nuclex.Support.Configuration

View file

@ -1,467 +1,466 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the configuration file store</summary>
[TestFixture]
internal class ConfigurationFileStoreTest {
/// <summary>
/// Verifies that loading an empty file doesn't lead to an exception
/// </summary>
[Test]
public void CanParseEmptyFile() {
Assert.That(() => load(string.Empty), Throws.Nothing);
}
/// <summary>
/// Verifies that categories can be parsed from a configuration file
/// </summary>
[Test]
public void CanParseCategories() {
string[] categoryNames = new string[] { "Category1", "Category 2" };
string fileContents =
"[" + categoryNames[0] + "]\r\n" +
" [ " + categoryNames[1] + " ] \r\n";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.EquivalentTo(categoryNames));
}
/// <summary>
/// Verifies that malformed categories can be handled by the parser
/// </summary>
[Test]
public void MalformedCategoriesAreIgnored() {
string fileContents =
"[ Not a category\r\n" +
" [";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that empty lines in the configuration file have no meaning
/// </summary>
[Test]
public void EmptyLinesAreSkipped() {
string fileContents =
"\r\n" +
" ";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that category definitions after a comment sign are ignored
/// </summary>
[Test]
public void CommentedOutCategoriesAreIgnored() {
string fileContents =
"#[NotACategory]\r\n" +
"; [ Also Not A Category ]\r\n";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that assignments without an option name are ignored by the parser
/// </summary>
[Test]
public void NamelessAssignmentsAreIgnored() {
string fileContents =
"=\r\n" +
" = \r\n" +
" = hello";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
Assert.That(configurationFile.EnumerateOptions(), Is.Empty);
}
/// <summary>
/// Verifies that assignments without an option name are ignored by the parser
/// </summary>
[Test]
public void OptionsCanHaveEmptyValues() {
string fileContents =
"a =\r\n" +
"b = \r\n" +
"c = ; hello";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
var options = new List<OptionInfo>(configurationFile.EnumerateOptions());
Assert.That(options.Count, Is.EqualTo(3));
for(int index = 0; index < options.Count; ++index) {
Assert.That(
configurationFile.Get<string>(null, options[index].Name), Is.Null.Or.Empty
);
}
}
/// <summary>
/// Verifies that values assigned to options can contain space charcters
/// </summary>
[Test]
public void OptionValuesCanContainSpaces() {
string fileContents =
"test = hello world";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.Get<string>(null, "test"), Is.EqualTo("hello world"));
}
/// <summary>
/// Verifies that values enclosed in quotes can embed comment characters
/// </summary>
[Test]
public void OptionValuesWithQuotesCanEmbedComments() {
string fileContents =
"test = \"This ; is # not a comment\" # but this is";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"This ; is # not a comment\"")
);
}
/// <summary>
/// Verifies that values can end on a quote without causing trouble
/// </summary>
[Test]
public void CommentsCanEndWithAQuote() {
string fileContents =
"test = \"This value ends with a quote\"";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"This value ends with a quote\"")
);
}
/// <summary>
/// Verifies that values can forget the closing quote without causing trouble
/// </summary>
[Test]
public void ClosingQuoteCanBeOmmitted() {
string fileContents =
"test = \"No closing quote";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"No closing quote")
);
}
/// <summary>
/// Verifies that text placed after the closing quote will also be part of
/// an option's value
/// </summary>
[Test]
public void TextAfterClosingQuoteBecomesPartOfValue() {
string fileContents =
"test = \"Begins here\" end ends here";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"Begins here\" end ends here")
);
}
/// <summary>
/// Verifies that text placed after the closing quote will also be part of
/// an option's value
/// </summary>
[Test]
public void OptionValuesCanBeChanged() {
string fileContents = "test = 123 ; comment";
ConfigurationFileStore configurationFile = load(fileContents);
configurationFile.Set(null, "test", "hello world");
Assert.That(
save(configurationFile),
Contains.Substring("hello world").And.ContainsSubstring("comment")
);
}
/// <summary>
/// Verifies that options can be added to the configuration file
/// </summary>
[Test]
public void OptionsCanBeAdded() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", "123");
Assert.That(configurationFile.Get<string>(null, "test"), Is.EqualTo("123"));
}
/// <summary>
/// Verifies that options can be added to the configuration file
/// </summary>
[Test]
public void CategoriesCanBeAdded() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>("general", "sol", "42");
Assert.That(
configurationFile.EnumerateCategories(), Is.EquivalentTo(new string[] { "general" })
);
Assert.That(save(configurationFile), Contains.Substring("[general]"));
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
var configurationFile = new ConfigurationFileStore();
Assert.That(
() => configurationFile.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", "123");
Assert.That(
() => configurationFile.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that it's possible to enumerate a category that doesn't exist
/// </summary>
[Test]
public void NonExistingCategoryCanBeEnumerated() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.EnumerateOptions("doesn't exist"), Is.Empty);
}
/// <summary>
/// Verifies that it's possible to create an option without a value
/// </summary>
[Test]
public void ValuelessOptionsCanBeCreated() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Get<string>(null, "test"), Is.Null.Or.Empty);
}
/// <summary>
/// Verifies that it's possible to assign an empty value to an option
/// </summary>
[Test]
public void OptionValueCanBeCleared() {
string fileContents = "test = 123 ; comment";
ConfigurationFileStore configurationFile = load(fileContents);
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Get<string>(null, "test"), Is.Null.Or.Empty);
}
/// <summary>
/// Verifies that it's possible to remove options from the configuration file
/// </summary>
[Test]
public void OptionsCanBeRemoved() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Remove(null, "test"), Is.True);
string value;
Assert.That(configurationFile.TryGet<string>(null, "test", out value), Is.False);
}
/// <summary>
/// Verifies that options are removed from the configuration file correctly
/// </summary>
[Test]
public void RemovingOptionShiftsFollowingOptionsUp() {
string fileContents =
"first = 1\r\n" +
"second = 2";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.Remove(null, "first"), Is.True);
configurationFile.Set<string>(null, "second", "yay! first!");
Assert.That(save(configurationFile), Has.No.ContainsSubstring("1"));
Assert.That(save(configurationFile), Contains.Substring("second"));
Assert.That(save(configurationFile), Contains.Substring("yay! first!"));
}
/// <summary>
/// Verifies that it's not an error to remove an option from a non-existing category
/// </summary>
[Test]
public void CanRemoveOptionFromNonExistingCategory() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.Remove("nothing", "first"), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove a non-existing option
/// </summary>
[Test]
public void CanRemoveNonExistingOption() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.Remove(null, "first"), Is.False);
}
/// <summary>
/// Verifies that the configuration file store can identify various types of values
/// </summary>
[
Test,
TestCase("nothing=", typeof(string)),
TestCase("text = world", typeof(string)),
TestCase("short=9", typeof(int)),
TestCase("integer = 123", typeof(int)),
TestCase("integer = 123 ", typeof(int)),
TestCase("string=x", typeof(string)),
TestCase("string = 123s", typeof(string)),
TestCase("float = 123.45", typeof(float)),
TestCase("float = 123.45 ", typeof(float)),
TestCase("boolean = true", typeof(bool)),
TestCase("boolean = false", typeof(bool)),
TestCase("boolean = yes", typeof(bool)),
TestCase("boolean = no", typeof(bool))
]
public void OptionTypeCanBeIdentified(string assignment, Type expectedType) {
ConfigurationFileStore configurationFile = load(assignment);
OptionInfo info;
using(
IEnumerator<OptionInfo> enumerator = configurationFile.EnumerateOptions().GetEnumerator()
) {
Assert.That(enumerator.MoveNext(), Is.True);
info = enumerator.Current;
Assert.That(enumerator.MoveNext(), Is.False);
}
Assert.That(info.OptionType, Is.EqualTo(expectedType));
}
/// <summary>
/// Verifies that configuration files containing duplicate option names can not
/// be used with the configuration file store
/// </summary>
[Test]
public void FilesWithDuplicateOptionNamesCannotBeProcessed() {
string fileContents =
"duplicate name = 1\r\n" +
"duplicate name = 2";
Assert.That(() => load(fileContents), Throws.Exception);
}
/// <summary>
/// Verifies that attempting to cast a value to an incompatible data type causes
/// a FormatException to be thrown
/// </summary>
[Test]
public void ImpossibleCastCausesFormatException() {
string fileContents = "fail = yesnomaybe";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
() => configurationFile.Get<bool>(null, "fail"),
Throws.Exception.AssignableTo<FormatException>()
);
}
/// <summary>
/// Verifies that configuration files containing duplicate option names can not
/// be used with the configuration file store
/// </summary>
[
Test,
TestCase("value = yes", true),
TestCase("value = true", true),
TestCase("value = no", false),
TestCase("value = false", false)
]
public void BooleanLiteralsAreUnderstood(string fileContents, bool expectedValue) {
ConfigurationFileStore configurationFile = load(fileContents);
if(expectedValue) {
Assert.That(configurationFile.Get<bool>(null, "value"), Is.True);
} else {
Assert.That(configurationFile.Get<bool>(null, "value"), Is.False);
}
}
/// <summary>Loads a configuration file from a string</summary>
/// <param name="fileContents">Contents of the configuration file</param>
/// <returns>The configuration file loaded from the string</returns>
private static ConfigurationFileStore load(string fileContents) {
using(var reader = new StringReader(fileContents)) {
return ConfigurationFileStore.Parse(reader);
}
}
/// <summary>Saves a configuration file into a string</summary>
/// <param name="configurationFile">Configuration file that will be saved</param>
/// <returns>Contents of the configuration file</returns>
private static string save(ConfigurationFileStore configurationFile) {
var builder = new StringBuilder();
using(var writer = new StringWriter(builder)) {
configurationFile.Save(writer);
writer.Flush();
}
return builder.ToString();
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the configuration file store</summary>
[TestFixture]
internal class ConfigurationFileStoreTest {
/// <summary>
/// Verifies that loading an empty file doesn't lead to an exception
/// </summary>
[Test]
public void CanParseEmptyFile() {
Assert.That(() => load(string.Empty), Throws.Nothing);
}
/// <summary>
/// Verifies that categories can be parsed from a configuration file
/// </summary>
[Test]
public void CanParseCategories() {
string[] categoryNames = new string[] { "Category1", "Category 2" };
string fileContents =
"[" + categoryNames[0] + "]\r\n" +
" [ " + categoryNames[1] + " ] \r\n";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.EquivalentTo(categoryNames));
}
/// <summary>
/// Verifies that malformed categories can be handled by the parser
/// </summary>
[Test]
public void MalformedCategoriesAreIgnored() {
string fileContents =
"[ Not a category\r\n" +
" [";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that empty lines in the configuration file have no meaning
/// </summary>
[Test]
public void EmptyLinesAreSkipped() {
string fileContents =
"\r\n" +
" ";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that category definitions after a comment sign are ignored
/// </summary>
[Test]
public void CommentedOutCategoriesAreIgnored() {
string fileContents =
"#[NotACategory]\r\n" +
"; [ Also Not A Category ]\r\n";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that assignments without an option name are ignored by the parser
/// </summary>
[Test]
public void NamelessAssignmentsAreIgnored() {
string fileContents =
"=\r\n" +
" = \r\n" +
" = hello";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
Assert.That(configurationFile.EnumerateOptions(), Is.Empty);
}
/// <summary>
/// Verifies that assignments without an option name are ignored by the parser
/// </summary>
[Test]
public void OptionsCanHaveEmptyValues() {
string fileContents =
"a =\r\n" +
"b = \r\n" +
"c = ; hello";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.EnumerateCategories(), Is.Empty);
var options = new List<OptionInfo>(configurationFile.EnumerateOptions());
Assert.That(options.Count, Is.EqualTo(3));
for(int index = 0; index < options.Count; ++index) {
Assert.That(
configurationFile.Get<string>(null, options[index].Name), Is.Null.Or.Empty
);
}
}
/// <summary>
/// Verifies that values assigned to options can contain space charcters
/// </summary>
[Test]
public void OptionValuesCanContainSpaces() {
string fileContents =
"test = hello world";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.Get<string>(null, "test"), Is.EqualTo("hello world"));
}
/// <summary>
/// Verifies that values enclosed in quotes can embed comment characters
/// </summary>
[Test]
public void OptionValuesWithQuotesCanEmbedComments() {
string fileContents =
"test = \"This ; is # not a comment\" # but this is";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"This ; is # not a comment\"")
);
}
/// <summary>
/// Verifies that values can end on a quote without causing trouble
/// </summary>
[Test]
public void CommentsCanEndWithAQuote() {
string fileContents =
"test = \"This value ends with a quote\"";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"This value ends with a quote\"")
);
}
/// <summary>
/// Verifies that values can forget the closing quote without causing trouble
/// </summary>
[Test]
public void ClosingQuoteCanBeOmmitted() {
string fileContents =
"test = \"No closing quote";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"No closing quote")
);
}
/// <summary>
/// Verifies that text placed after the closing quote will also be part of
/// an option's value
/// </summary>
[Test]
public void TextAfterClosingQuoteBecomesPartOfValue() {
string fileContents =
"test = \"Begins here\" end ends here";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
configurationFile.Get<string>(null, "test"),
Is.EqualTo("\"Begins here\" end ends here")
);
}
/// <summary>
/// Verifies that text placed after the closing quote will also be part of
/// an option's value
/// </summary>
[Test]
public void OptionValuesCanBeChanged() {
string fileContents = "test = 123 ; comment";
ConfigurationFileStore configurationFile = load(fileContents);
configurationFile.Set(null, "test", "hello world");
Assert.That(
save(configurationFile),
Contains.Substring("hello world").And.ContainsSubstring("comment")
);
}
/// <summary>
/// Verifies that options can be added to the configuration file
/// </summary>
[Test]
public void OptionsCanBeAdded() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", "123");
Assert.That(configurationFile.Get<string>(null, "test"), Is.EqualTo("123"));
}
/// <summary>
/// Verifies that options can be added to the configuration file
/// </summary>
[Test]
public void CategoriesCanBeAdded() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>("general", "sol", "42");
Assert.That(
configurationFile.EnumerateCategories(), Is.EquivalentTo(new string[] { "general" })
);
Assert.That(save(configurationFile), Contains.Substring("[general]"));
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
var configurationFile = new ConfigurationFileStore();
Assert.That(
() => configurationFile.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", "123");
Assert.That(
() => configurationFile.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that it's possible to enumerate a category that doesn't exist
/// </summary>
[Test]
public void NonExistingCategoryCanBeEnumerated() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.EnumerateOptions("doesn't exist"), Is.Empty);
}
/// <summary>
/// Verifies that it's possible to create an option without a value
/// </summary>
[Test]
public void ValuelessOptionsCanBeCreated() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Get<string>(null, "test"), Is.Null.Or.Empty);
}
/// <summary>
/// Verifies that it's possible to assign an empty value to an option
/// </summary>
[Test]
public void OptionValueCanBeCleared() {
string fileContents = "test = 123 ; comment";
ConfigurationFileStore configurationFile = load(fileContents);
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Get<string>(null, "test"), Is.Null.Or.Empty);
}
/// <summary>
/// Verifies that it's possible to remove options from the configuration file
/// </summary>
[Test]
public void OptionsCanBeRemoved() {
var configurationFile = new ConfigurationFileStore();
configurationFile.Set<string>(null, "test", null);
Assert.That(configurationFile.Remove(null, "test"), Is.True);
string value;
Assert.That(configurationFile.TryGet<string>(null, "test", out value), Is.False);
}
/// <summary>
/// Verifies that options are removed from the configuration file correctly
/// </summary>
[Test]
public void RemovingOptionShiftsFollowingOptionsUp() {
string fileContents =
"first = 1\r\n" +
"second = 2";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(configurationFile.Remove(null, "first"), Is.True);
configurationFile.Set<string>(null, "second", "yay! first!");
Assert.That(save(configurationFile), Has.No.ContainsSubstring("1"));
Assert.That(save(configurationFile), Contains.Substring("second"));
Assert.That(save(configurationFile), Contains.Substring("yay! first!"));
}
/// <summary>
/// Verifies that it's not an error to remove an option from a non-existing category
/// </summary>
[Test]
public void CanRemoveOptionFromNonExistingCategory() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.Remove("nothing", "first"), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove a non-existing option
/// </summary>
[Test]
public void CanRemoveNonExistingOption() {
var configurationFile = new ConfigurationFileStore();
Assert.That(configurationFile.Remove(null, "first"), Is.False);
}
/// <summary>
/// Verifies that the configuration file store can identify various types of values
/// </summary>
[
Test,
TestCase("nothing=", typeof(string)),
TestCase("text = world", typeof(string)),
TestCase("short=9", typeof(int)),
TestCase("integer = 123", typeof(int)),
TestCase("integer = 123 ", typeof(int)),
TestCase("string=x", typeof(string)),
TestCase("string = 123s", typeof(string)),
TestCase("float = 123.45", typeof(float)),
TestCase("float = 123.45 ", typeof(float)),
TestCase("boolean = true", typeof(bool)),
TestCase("boolean = false", typeof(bool)),
TestCase("boolean = yes", typeof(bool)),
TestCase("boolean = no", typeof(bool))
]
public void OptionTypeCanBeIdentified(string assignment, Type expectedType) {
ConfigurationFileStore configurationFile = load(assignment);
OptionInfo info;
using(
IEnumerator<OptionInfo> enumerator = configurationFile.EnumerateOptions().GetEnumerator()
) {
Assert.That(enumerator.MoveNext(), Is.True);
info = enumerator.Current;
Assert.That(enumerator.MoveNext(), Is.False);
}
Assert.That(info.OptionType, Is.EqualTo(expectedType));
}
/// <summary>
/// Verifies that configuration files containing duplicate option names can not
/// be used with the configuration file store
/// </summary>
[Test]
public void FilesWithDuplicateOptionNamesCannotBeProcessed() {
string fileContents =
"duplicate name = 1\r\n" +
"duplicate name = 2";
Assert.That(() => load(fileContents), Throws.Exception);
}
/// <summary>
/// Verifies that attempting to cast a value to an incompatible data type causes
/// a FormatException to be thrown
/// </summary>
[Test]
public void ImpossibleCastCausesFormatException() {
string fileContents = "fail = yesnomaybe";
ConfigurationFileStore configurationFile = load(fileContents);
Assert.That(
() => configurationFile.Get<bool>(null, "fail"),
Throws.Exception.AssignableTo<FormatException>()
);
}
/// <summary>
/// Verifies that configuration files containing duplicate option names can not
/// be used with the configuration file store
/// </summary>
[
Test,
TestCase("value = yes", true),
TestCase("value = true", true),
TestCase("value = no", false),
TestCase("value = false", false)
]
public void BooleanLiteralsAreUnderstood(string fileContents, bool expectedValue) {
ConfigurationFileStore configurationFile = load(fileContents);
if(expectedValue) {
Assert.That(configurationFile.Get<bool>(null, "value"), Is.True);
} else {
Assert.That(configurationFile.Get<bool>(null, "value"), Is.False);
}
}
/// <summary>Loads a configuration file from a string</summary>
/// <param name="fileContents">Contents of the configuration file</param>
/// <returns>The configuration file loaded from the string</returns>
private static ConfigurationFileStore load(string fileContents) {
using(var reader = new StringReader(fileContents)) {
return ConfigurationFileStore.Parse(reader);
}
}
/// <summary>Saves a configuration file into a string</summary>
/// <param name="configurationFile">Configuration file that will be saved</param>
/// <returns>Contents of the configuration file</returns>
private static string save(ConfigurationFileStore configurationFile) {
var builder = new StringBuilder();
using(var writer = new StringWriter(builder)) {
configurationFile.Save(writer);
writer.Flush();
}
return builder.ToString();
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST

View file

@ -1,431 +1,430 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
namespace Nuclex.Support.Settings {
/// <summary>Represents an ini- or cfg-like configuration file</summary>
/// <remarks>
/// <para>
/// This class tries its best to preserve the formatting of configuration files.
/// Changing a value will keep the line it appears in intact. The parser also takes
/// as much data from a line as it can - anything to the left of an equals sign
/// becomes the name, anything to the right (excluding comments) becomes the value.
/// </para>
/// <para>
/// To access the contents of a configuration file, simply parse it and use it like
/// you would any other settings store:
/// </para>
/// <example>
/// <code>
/// // # Settings.ini
/// // message = hello world ; the usual...
/// // show message = true
/// ISettingsStore settings;
/// using(var reader = new StreamReader("settings.ini")) {
/// settings = ConfigurationFile.Parse(reader);
/// }
///
/// if(settings.Get&lt;bool&gt;(null, "show message")) {
/// Console.WriteLine(settings.Get&lt;string&gt;(null, "message"));
/// }
/// </code>
/// </example>
/// <para>
/// It's usually a good idea to keep an application and all of its required files
/// together, whether it's code or data, but platforms often have their own conventions:
/// </para>
/// <list type="bullet">
/// <listheader>
/// <term>Operating System</term>
/// <description>Convention</description>
/// </listheader>
/// <item>
/// <term>Linux</term>
/// <description>
/// System-wide configuration goes into /etc/&lt;appname&gt;/, user-specific
/// configuration goes into ~/.&lt;appname&gt;/ while static configuration that is
/// known at build time resides with the application in /opt/&lt;appname&gt;/
/// </description>
/// </item>
/// <item>
/// <term>Windows</term>
/// <description>
/// System-wide configuration goes into %ProgramData%, user-specific configuration
/// has no real place (try %AppData%/&lt;appname&gt;/ if you want to hide it from
/// the user, %UserProfile%/Documents/&lt;appname&gt; if the user should see it)
/// and static configuration resides with your application
/// in %ProgramFiles%/&lt;appname&gt;/.
/// </description>
/// </item>
/// <item>
/// <term>MacOS</term>
/// <description>
/// System-wide configuration goes into /etc/&lt;appname&gt;/, user-specific
/// configuration goes into /Users/&lt;username&gt;/.&lt;appname&gt;/ while static
/// configuration resides with the application in /Applications/&lt;appname&gt;/
/// </description>
/// </item>
/// </list>
/// </remarks>
public partial class ConfigurationFileStore : ISettingsStore {
#region class Category
/// <summary>Stores informations about a category found in the configuration file</summary>
private class Category {
/// <summary>Name of the category as a string</summary>
public StringSegment CategoryName;
/// <summary>Lookup table for the options in this category</summary>
public IDictionary<string, Option> OptionLookup;
/// <summary>Lines this category and its options consist of</summary>
public IList<string> Lines;
}
#endregion // class Category
#region class Option
/// <summary>Stores informations about an option found in the configuration file</summary>
private class Option {
/// <summary>Index of the line the option is defined in</summary>
public int LineIndex;
/// <summary>Name of the option as a string</summary>
public StringSegment OptionName;
/// <summary>Value of the option as a string</summary>
public StringSegment OptionValue;
}
#endregion // class Option
/// <summary>Initializes a new, empty configuration file</summary>
public ConfigurationFileStore() {
this.options = new List<Option>();
this.categoryLookup = new Dictionary<string, Category>();
this.rootCategory = new Category() {
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
}
/// <summary>Saves the configuration file into the specified writer</summary>
/// <param name="writer">Writer the configuration file will be saved into</param>
public void Save(TextWriter writer) {
for(int index = 0; index < this.rootCategory.Lines.Count; ++index) {
writer.WriteLine(this.rootCategory.Lines[index]);
}
foreach(Category category in this.categoryLookup.Values) {
for(int index = 0; index < category.Lines.Count; ++index) {
writer.WriteLine(category.Lines[index]);
}
}
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.categoryLookup.Keys;
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
Category enumeratedCategory = getCategoryByName(category);
if(enumeratedCategory == null) {
yield break;
}
foreach(Option option in this.rootCategory.OptionLookup.Values) {
OptionInfo optionInfo = new OptionInfo() {
Name = option.OptionName.ToString(),
OptionType = getBestMatchingType(ref option.OptionValue)
};
yield return optionInfo;
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the configuration file"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the configuration file"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
Category containingCategory = getCategoryByName(category);
if(containingCategory != null) {
Option option;
if(containingCategory.OptionLookup.TryGetValue(optionName, out option)) {
if(typeof(TValue) == typeof(bool)) {
bool? boolean = parseBooleanLiteral(ref option.OptionValue);
if(boolean.HasValue) {
value = (TValue)(object)boolean.Value;
return true;
} else {
throw new FormatException(
"The value '" + option.OptionValue.ToString() + "' is not a boolean"
);
}
} else {
value = (TValue)Convert.ChangeType(option.OptionValue.ToString(), typeof(TValue));
return true;
}
}
}
value = default(TValue);
return false;
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
string valueAsString = (string)Convert.ChangeType(
value, typeof(string), CultureInfo.InvariantCulture
);
Category targetCategory;
if(string.IsNullOrEmpty(category)) {
targetCategory = this.rootCategory;
} else if(!this.categoryLookup.TryGetValue(category, out targetCategory)) {
targetCategory = createCategory(category);
createOption(targetCategory, optionName, valueAsString);
return;
}
Option targetOption;
if(targetCategory.OptionLookup.TryGetValue(optionName, out targetOption)) {
changeOption(targetCategory, targetOption, valueAsString);
} else {
createOption(targetCategory, optionName, valueAsString);
}
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
Category sourceCategory = getCategoryByName(category);
if(sourceCategory == null) {
return false;
}
Option option;
if(!sourceCategory.OptionLookup.TryGetValue(optionName, out option)) {
return false;
}
sourceCategory.Lines.RemoveAt(option.LineIndex);
sourceCategory.OptionLookup.Remove(optionName);
foreach(Option shiftedOption in sourceCategory.OptionLookup.Values) {
if(shiftedOption.LineIndex > option.LineIndex) {
--shiftedOption.LineIndex;
}
}
return true;
}
/// <summary>Looks a category up by its name</summary>
/// <param name="categoryName">
/// Name of the category. Can be null for the root category
/// </param>
/// <returns>The category with the specified name</returns>
private Category getCategoryByName(string categoryName) {
Category category;
if(string.IsNullOrEmpty(categoryName)) {
category = this.rootCategory;
} else if(!this.categoryLookup.TryGetValue(categoryName, out category)) {
return null;
}
return category;
}
/// <summary>Creates a new option</summary>
/// <param name="category">Category the option will be added to</param>
/// <param name="name">Name of the option</param>
/// <param name="value">Value that will be assigned to the option</param>
private void createOption(Category category, string name, string value) {
int valueLength;
if(value == null) {
valueLength = 0;
} else {
valueLength = value.Length;
}
// Build the complete line containing the option assignment
string line;
{
StringBuilder builder = new StringBuilder(name.Length + 3 + valueLength);
builder.Append(name);
builder.Append(" = ");
if(valueLength > 0) {
builder.Append(value);
}
line = builder.ToString();
}
Option newOption = new Option() {
OptionName = new StringSegment(line, 0, name.Length),
OptionValue = new StringSegment(line, name.Length + 3, valueLength)
};
// Figure out which line the new option should be put in
int lastLineIndex = category.Lines.Count - 1;
if((lastLineIndex > 0) && (category.Lines[lastLineIndex].Length == 0)) {
newOption.LineIndex = lastLineIndex;
category.Lines.Insert(lastLineIndex, line);
} else {
newOption.LineIndex = category.Lines.Count;
category.Lines.Add(line);
category.Lines.Add(string.Empty);
}
category.OptionLookup.Add(name, newOption);
}
/// <summary>Changes the value of an option</summary>
/// <param name="category">Category that holds the option</param>
/// <param name="option">Option whose value will be changed</param>
/// <param name="newValue">New value that will be assigned to the option</param>
private void changeOption(Category category, Option option, string newValue) {
int newValueLength;
if(newValue == null) {
newValueLength = 0;
} else {
newValueLength = newValue.Length;
}
// Form the new line
string line = option.OptionValue.Text;
{
StringBuilder builder = new StringBuilder(
line.Length - option.OptionValue.Count + newValueLength
);
// Stuff before the value
if(option.OptionValue.Offset > 0) {
builder.Append(line, 0, option.OptionValue.Offset);
}
// The value itself
if(newValueLength > 0) {
builder.Append(newValue);
}
// Stuff after the value
int endIndex = option.OptionValue.Offset + option.OptionValue.Count;
if(endIndex < line.Length) {
builder.Append(line, endIndex, line.Length - endIndex);
}
line = builder.ToString();
}
option.OptionValue = new StringSegment(line, option.OptionValue.Offset, newValueLength);
category.Lines[option.LineIndex] = line;
}
/// <summary>Creates a new category in the configuration file</summary>
/// <param name="category">Name of the new category</param>
/// <returns>The category that was created</returns>
private Category createCategory(string category) {
string categoryDefinition;
{
StringBuilder builder = new StringBuilder(category.Length + 2);
builder.Append('[');
builder.Append(category);
builder.Append(']');
categoryDefinition = builder.ToString();
}
Category newCategory = new Category() {
CategoryName = new StringSegment(categoryDefinition, 1, category.Length),
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
newCategory.Lines.Add(categoryDefinition);
newCategory.Lines.Add(string.Empty);
this.categoryLookup.Add(category, newCategory);
return newCategory;
}
/// <summary>Records where options are stored in the configuration file</summary>
private IList<Option> options;
/// <summary>Root category where options above any category definition go</summary>
private Category rootCategory;
/// <summary>Lookup table for all categories by their name</summary>
private IDictionary<string, Category> categoryLookup;
}
} // namespace Nuclex.Support.Settings
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
namespace Nuclex.Support.Settings {
/// <summary>Represents an ini- or cfg-like configuration file</summary>
/// <remarks>
/// <para>
/// This class tries its best to preserve the formatting of configuration files.
/// Changing a value will keep the line it appears in intact. The parser also takes
/// as much data from a line as it can - anything to the left of an equals sign
/// becomes the name, anything to the right (excluding comments) becomes the value.
/// </para>
/// <para>
/// To access the contents of a configuration file, simply parse it and use it like
/// you would any other settings store:
/// </para>
/// <example>
/// <code>
/// // # Settings.ini
/// // message = hello world ; the usual...
/// // show message = true
/// ISettingsStore settings;
/// using(var reader = new StreamReader("settings.ini")) {
/// settings = ConfigurationFile.Parse(reader);
/// }
///
/// if(settings.Get&lt;bool&gt;(null, "show message")) {
/// Console.WriteLine(settings.Get&lt;string&gt;(null, "message"));
/// }
/// </code>
/// </example>
/// <para>
/// It's usually a good idea to keep an application and all of its required files
/// together, whether it's code or data, but platforms often have their own conventions:
/// </para>
/// <list type="bullet">
/// <listheader>
/// <term>Operating System</term>
/// <description>Convention</description>
/// </listheader>
/// <item>
/// <term>Linux</term>
/// <description>
/// System-wide configuration goes into /etc/&lt;appname&gt;/, user-specific
/// configuration goes into ~/.&lt;appname&gt;/ while static configuration that is
/// known at build time resides with the application in /opt/&lt;appname&gt;/
/// </description>
/// </item>
/// <item>
/// <term>Windows</term>
/// <description>
/// System-wide configuration goes into %ProgramData%, user-specific configuration
/// has no real place (try %AppData%/&lt;appname&gt;/ if you want to hide it from
/// the user, %UserProfile%/Documents/&lt;appname&gt; if the user should see it)
/// and static configuration resides with your application
/// in %ProgramFiles%/&lt;appname&gt;/.
/// </description>
/// </item>
/// <item>
/// <term>MacOS</term>
/// <description>
/// System-wide configuration goes into /etc/&lt;appname&gt;/, user-specific
/// configuration goes into /Users/&lt;username&gt;/.&lt;appname&gt;/ while static
/// configuration resides with the application in /Applications/&lt;appname&gt;/
/// </description>
/// </item>
/// </list>
/// </remarks>
public partial class ConfigurationFileStore : ISettingsStore {
#region class Category
/// <summary>Stores informations about a category found in the configuration file</summary>
private class Category {
/// <summary>Name of the category as a string</summary>
public StringSegment CategoryName;
/// <summary>Lookup table for the options in this category</summary>
public IDictionary<string, Option> OptionLookup;
/// <summary>Lines this category and its options consist of</summary>
public IList<string> Lines;
}
#endregion // class Category
#region class Option
/// <summary>Stores informations about an option found in the configuration file</summary>
private class Option {
/// <summary>Index of the line the option is defined in</summary>
public int LineIndex;
/// <summary>Name of the option as a string</summary>
public StringSegment OptionName;
/// <summary>Value of the option as a string</summary>
public StringSegment OptionValue;
}
#endregion // class Option
/// <summary>Initializes a new, empty configuration file</summary>
public ConfigurationFileStore() {
this.options = new List<Option>();
this.categoryLookup = new Dictionary<string, Category>();
this.rootCategory = new Category() {
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
}
/// <summary>Saves the configuration file into the specified writer</summary>
/// <param name="writer">Writer the configuration file will be saved into</param>
public void Save(TextWriter writer) {
for(int index = 0; index < this.rootCategory.Lines.Count; ++index) {
writer.WriteLine(this.rootCategory.Lines[index]);
}
foreach(Category category in this.categoryLookup.Values) {
for(int index = 0; index < category.Lines.Count; ++index) {
writer.WriteLine(category.Lines[index]);
}
}
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.categoryLookup.Keys;
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
Category enumeratedCategory = getCategoryByName(category);
if(enumeratedCategory == null) {
yield break;
}
foreach(Option option in this.rootCategory.OptionLookup.Values) {
OptionInfo optionInfo = new OptionInfo() {
Name = option.OptionName.ToString(),
OptionType = getBestMatchingType(ref option.OptionValue)
};
yield return optionInfo;
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the configuration file"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the configuration file"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
Category containingCategory = getCategoryByName(category);
if(containingCategory != null) {
Option option;
if(containingCategory.OptionLookup.TryGetValue(optionName, out option)) {
if(typeof(TValue) == typeof(bool)) {
bool? boolean = parseBooleanLiteral(ref option.OptionValue);
if(boolean.HasValue) {
value = (TValue)(object)boolean.Value;
return true;
} else {
throw new FormatException(
"The value '" + option.OptionValue.ToString() + "' is not a boolean"
);
}
} else {
value = (TValue)Convert.ChangeType(option.OptionValue.ToString(), typeof(TValue));
return true;
}
}
}
value = default(TValue);
return false;
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
string valueAsString = (string)Convert.ChangeType(
value, typeof(string), CultureInfo.InvariantCulture
);
Category targetCategory;
if(string.IsNullOrEmpty(category)) {
targetCategory = this.rootCategory;
} else if(!this.categoryLookup.TryGetValue(category, out targetCategory)) {
targetCategory = createCategory(category);
createOption(targetCategory, optionName, valueAsString);
return;
}
Option targetOption;
if(targetCategory.OptionLookup.TryGetValue(optionName, out targetOption)) {
changeOption(targetCategory, targetOption, valueAsString);
} else {
createOption(targetCategory, optionName, valueAsString);
}
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
Category sourceCategory = getCategoryByName(category);
if(sourceCategory == null) {
return false;
}
Option option;
if(!sourceCategory.OptionLookup.TryGetValue(optionName, out option)) {
return false;
}
sourceCategory.Lines.RemoveAt(option.LineIndex);
sourceCategory.OptionLookup.Remove(optionName);
foreach(Option shiftedOption in sourceCategory.OptionLookup.Values) {
if(shiftedOption.LineIndex > option.LineIndex) {
--shiftedOption.LineIndex;
}
}
return true;
}
/// <summary>Looks a category up by its name</summary>
/// <param name="categoryName">
/// Name of the category. Can be null for the root category
/// </param>
/// <returns>The category with the specified name</returns>
private Category getCategoryByName(string categoryName) {
Category category;
if(string.IsNullOrEmpty(categoryName)) {
category = this.rootCategory;
} else if(!this.categoryLookup.TryGetValue(categoryName, out category)) {
return null;
}
return category;
}
/// <summary>Creates a new option</summary>
/// <param name="category">Category the option will be added to</param>
/// <param name="name">Name of the option</param>
/// <param name="value">Value that will be assigned to the option</param>
private void createOption(Category category, string name, string value) {
int valueLength;
if(value == null) {
valueLength = 0;
} else {
valueLength = value.Length;
}
// Build the complete line containing the option assignment
string line;
{
StringBuilder builder = new StringBuilder(name.Length + 3 + valueLength);
builder.Append(name);
builder.Append(" = ");
if(valueLength > 0) {
builder.Append(value);
}
line = builder.ToString();
}
Option newOption = new Option() {
OptionName = new StringSegment(line, 0, name.Length),
OptionValue = new StringSegment(line, name.Length + 3, valueLength)
};
// Figure out which line the new option should be put in
int lastLineIndex = category.Lines.Count - 1;
if((lastLineIndex > 0) && (category.Lines[lastLineIndex].Length == 0)) {
newOption.LineIndex = lastLineIndex;
category.Lines.Insert(lastLineIndex, line);
} else {
newOption.LineIndex = category.Lines.Count;
category.Lines.Add(line);
category.Lines.Add(string.Empty);
}
category.OptionLookup.Add(name, newOption);
}
/// <summary>Changes the value of an option</summary>
/// <param name="category">Category that holds the option</param>
/// <param name="option">Option whose value will be changed</param>
/// <param name="newValue">New value that will be assigned to the option</param>
private void changeOption(Category category, Option option, string newValue) {
int newValueLength;
if(newValue == null) {
newValueLength = 0;
} else {
newValueLength = newValue.Length;
}
// Form the new line
string line = option.OptionValue.Text;
{
StringBuilder builder = new StringBuilder(
line.Length - option.OptionValue.Count + newValueLength
);
// Stuff before the value
if(option.OptionValue.Offset > 0) {
builder.Append(line, 0, option.OptionValue.Offset);
}
// The value itself
if(newValueLength > 0) {
builder.Append(newValue);
}
// Stuff after the value
int endIndex = option.OptionValue.Offset + option.OptionValue.Count;
if(endIndex < line.Length) {
builder.Append(line, endIndex, line.Length - endIndex);
}
line = builder.ToString();
}
option.OptionValue = new StringSegment(line, option.OptionValue.Offset, newValueLength);
category.Lines[option.LineIndex] = line;
}
/// <summary>Creates a new category in the configuration file</summary>
/// <param name="category">Name of the new category</param>
/// <returns>The category that was created</returns>
private Category createCategory(string category) {
string categoryDefinition;
{
StringBuilder builder = new StringBuilder(category.Length + 2);
builder.Append('[');
builder.Append(category);
builder.Append(']');
categoryDefinition = builder.ToString();
}
Category newCategory = new Category() {
CategoryName = new StringSegment(categoryDefinition, 1, category.Length),
OptionLookup = new Dictionary<string, Option>(),
Lines = new List<string>()
};
newCategory.Lines.Add(categoryDefinition);
newCategory.Lines.Add(string.Empty);
this.categoryLookup.Add(category, newCategory);
return newCategory;
}
/// <summary>Records where options are stored in the configuration file</summary>
private IList<Option> options;
/// <summary>Root category where options above any category definition go</summary>
private Category rootCategory;
/// <summary>Lookup table for all categories by their name</summary>
private IDictionary<string, Category> categoryLookup;
}
} // namespace Nuclex.Support.Settings

View file

@ -1,101 +1,100 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
namespace Nuclex.Support.Settings {
/// <summary>Interface by which settings and configuration data can be accessed</summary>
/// <remarks>
/// <para>
/// The intended usage pattern for options is for your application to simply read and
/// write whatever options it needs using the type it expects them to be.
/// </para>
/// <para>
/// When you enumerate the options appearing under a category, the settings store will
/// try to guess the likely type of an option, but this is not always accurate. For
/// example, assigning the text 'true' to an option in a .cfg or .ini file could mean
/// that the option is a boolean or it could simply be a coincidence. When you read
/// this value as a boolean, the settings store will correctly convert it to a boolean,
/// when you read it as a string, you will get back "true" in plain text.
/// </para>
/// <para>
/// Which types of values a settings store can accept can also change between different
/// settings store implementations. The windows registry supports string and byte
/// arrays whereas configuration files have no standardized way of holding these.
/// Any property store must support a minimal subset including booleans, integers,
/// floating point values and strings.
/// </para>
/// </remarks>
public interface ISettingsStore {
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
IEnumerable<string> EnumerateCategories();
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
IEnumerable<OptionInfo> EnumerateOptions(string category = null);
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
TValue Get<TValue>(string category, string optionName);
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
bool TryGet<TValue>(string category, string optionName, out TValue value);
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
void Set<TValue>(string category, string optionName, TValue value);
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
bool Remove(string category, string optionName);
}
} // namespace Nuclex.Support.Settings
#if WANT_TO_SUPPORT_MESSED_UP_CONFIGURATION_FILES
/// <remarks>
/// Some settings stores allow multiple options with the same name to exist.
/// If you request a collection of values (IEnumerable, ICollection, IList or their
/// generic variants), you will be given a collection of all values appearing
/// in the scope you specified.
/// </remarks>
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Generic;
namespace Nuclex.Support.Settings {
/// <summary>Interface by which settings and configuration data can be accessed</summary>
/// <remarks>
/// <para>
/// The intended usage pattern for options is for your application to simply read and
/// write whatever options it needs using the type it expects them to be.
/// </para>
/// <para>
/// When you enumerate the options appearing under a category, the settings store will
/// try to guess the likely type of an option, but this is not always accurate. For
/// example, assigning the text 'true' to an option in a .cfg or .ini file could mean
/// that the option is a boolean or it could simply be a coincidence. When you read
/// this value as a boolean, the settings store will correctly convert it to a boolean,
/// when you read it as a string, you will get back "true" in plain text.
/// </para>
/// <para>
/// Which types of values a settings store can accept can also change between different
/// settings store implementations. The windows registry supports string and byte
/// arrays whereas configuration files have no standardized way of holding these.
/// Any property store must support a minimal subset including booleans, integers,
/// floating point values and strings.
/// </para>
/// </remarks>
public interface ISettingsStore {
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
IEnumerable<string> EnumerateCategories();
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
IEnumerable<OptionInfo> EnumerateOptions(string category = null);
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
TValue Get<TValue>(string category, string optionName);
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
bool TryGet<TValue>(string category, string optionName, out TValue value);
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
void Set<TValue>(string category, string optionName, TValue value);
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
bool Remove(string category, string optionName);
}
} // namespace Nuclex.Support.Settings
#if WANT_TO_SUPPORT_MESSED_UP_CONFIGURATION_FILES
/// <remarks>
/// Some settings stores allow multiple options with the same name to exist.
/// If you request a collection of values (IEnumerable, ICollection, IList or their
/// generic variants), you will be given a collection of all values appearing
/// in the scope you specified.
/// </remarks>
#endif

View file

@ -1,165 +1,164 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the memory settings store</summary>
[TestFixture]
internal class MemoryStoreTest {
/// <summary>Verifies that constructed a memory store throws an exception</summary>
[Test]
public void CanBeCreated() {
Assert.That(() => new MemoryStore(), Throws.Nothing);
}
/// <summary>
/// Verifies that it's possible to enumerate the options in a non-existing category
/// </summary>
[Test]
public void NonExistingCategoriesCanBeEnumerated() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.EnumerateOptions("doesn't exist"), Is.Empty);
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
var memoryStore = new MemoryStore();
Assert.That(
() => memoryStore.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "test", "123");
Assert.That(
() => memoryStore.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that settings can be stored in the memory store
/// </summary>
[Test]
public void SettingsCanBeAssignedAndRetrieved() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>("general", "sol", "42");
Assert.That(memoryStore.Get<string>("general", "sol"), Is.EqualTo("42"));
}
/// <summary>
/// Verifies that it's possible to remove options from the memory store
/// </summary>
[Test]
public void OptionsCanBeRemoved() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "test", null);
Assert.That(memoryStore.Remove(null, "test"), Is.True);
string value;
Assert.That(memoryStore.TryGet<string>(null, "test", out value), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove an option from a non-existing category
/// </summary>
[Test]
public void CanRemoveOptionFromNonExistingCategory() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.Remove("nothing", "first"), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove a non-existing option
/// </summary>
[Test]
public void CanRemoveNonExistingOption() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.Remove(null, "first"), Is.False);
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void RootCategoryIsNotEnumerated() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void OptionsInRootCategoryCanBeEnumerated() {
var memoryStore = new MemoryStore();
string[] optionNames = new string[] { "first", "second" };
memoryStore.Set<int>(null, optionNames[0], 1);
memoryStore.Set<int>(null, optionNames[1], 2);
var optionInfos = new List<OptionInfo>(memoryStore.EnumerateOptions());
Assert.That(optionInfos.Count, Is.EqualTo(2));
var enumeratedOptionNames = new List<string>() {
optionInfos[0].Name, optionInfos[1].Name
};
Assert.That(enumeratedOptionNames, Is.EquivalentTo(optionNames));
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void CategoriesCanBeCreated() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "not", "used");
memoryStore.Set<string>("test", "message", "hello world");
Assert.That(memoryStore.EnumerateCategories(), Is.EquivalentTo(new string[] { "test" }));
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the memory settings store</summary>
[TestFixture]
internal class MemoryStoreTest {
/// <summary>Verifies that constructed a memory store throws an exception</summary>
[Test]
public void CanBeCreated() {
Assert.That(() => new MemoryStore(), Throws.Nothing);
}
/// <summary>
/// Verifies that it's possible to enumerate the options in a non-existing category
/// </summary>
[Test]
public void NonExistingCategoriesCanBeEnumerated() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.EnumerateOptions("doesn't exist"), Is.Empty);
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
var memoryStore = new MemoryStore();
Assert.That(
() => memoryStore.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "test", "123");
Assert.That(
() => memoryStore.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
/// <summary>
/// Verifies that settings can be stored in the memory store
/// </summary>
[Test]
public void SettingsCanBeAssignedAndRetrieved() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>("general", "sol", "42");
Assert.That(memoryStore.Get<string>("general", "sol"), Is.EqualTo("42"));
}
/// <summary>
/// Verifies that it's possible to remove options from the memory store
/// </summary>
[Test]
public void OptionsCanBeRemoved() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "test", null);
Assert.That(memoryStore.Remove(null, "test"), Is.True);
string value;
Assert.That(memoryStore.TryGet<string>(null, "test", out value), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove an option from a non-existing category
/// </summary>
[Test]
public void CanRemoveOptionFromNonExistingCategory() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.Remove("nothing", "first"), Is.False);
}
/// <summary>
/// Verifies that it's not an error to remove a non-existing option
/// </summary>
[Test]
public void CanRemoveNonExistingOption() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.Remove(null, "first"), Is.False);
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void RootCategoryIsNotEnumerated() {
var memoryStore = new MemoryStore();
Assert.That(memoryStore.EnumerateCategories(), Is.Empty);
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void OptionsInRootCategoryCanBeEnumerated() {
var memoryStore = new MemoryStore();
string[] optionNames = new string[] { "first", "second" };
memoryStore.Set<int>(null, optionNames[0], 1);
memoryStore.Set<int>(null, optionNames[1], 2);
var optionInfos = new List<OptionInfo>(memoryStore.EnumerateOptions());
Assert.That(optionInfos.Count, Is.EqualTo(2));
var enumeratedOptionNames = new List<string>() {
optionInfos[0].Name, optionInfos[1].Name
};
Assert.That(enumeratedOptionNames, Is.EquivalentTo(optionNames));
}
/// <summary>
/// Verifies that the root category is not part of the enumerated categories
/// </summary>
[Test]
public void CategoriesCanBeCreated() {
var memoryStore = new MemoryStore();
memoryStore.Set<string>(null, "not", "used");
memoryStore.Set<string>("test", "message", "hello world");
Assert.That(memoryStore.EnumerateCategories(), Is.EquivalentTo(new string[] { "test" }));
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST

View file

@ -1,162 +1,161 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
namespace Nuclex.Support.Settings {
/// <summary>Stores settings in memory</summary>
public class MemoryStore : ISettingsStore {
/// <summary>Initializes a new settings store managing settings in memory</summary>
public MemoryStore() {
this.options = new Dictionary<string, IDictionary<string, object>>();
this.rootOptions = new Dictionary<string, object>();
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.options.Keys;
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
IDictionary<string, object> categoryOptions;
if(string.IsNullOrEmpty(category)) {
categoryOptions = this.rootOptions;
} else if(!this.options.TryGetValue(category, out categoryOptions)) {
yield break;
}
foreach(KeyValuePair<string, object> option in categoryOptions) {
OptionInfo optionInfo = new OptionInfo() {
Name = option.Key,
OptionType = option.Value.GetType()
};
yield return optionInfo;
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the settings"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the settings"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
IDictionary<string, object> categoryOptions = getCategoryByName(category);
if(categoryOptions != null) {
object valueAsObject;
if(categoryOptions.TryGetValue(optionName, out valueAsObject)) {
value = (TValue)Convert.ChangeType(valueAsObject, typeof(TValue));
return true;
}
}
value = default(TValue);
return false;
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
IDictionary<string, object> targetCategory;
if(string.IsNullOrEmpty(category)) {
targetCategory = this.rootOptions;
} else if(!this.options.TryGetValue(category, out targetCategory)) {
targetCategory = new Dictionary<string, object>();
this.options.Add(category, targetCategory);
targetCategory.Add(optionName, value);
return;
}
targetCategory[optionName] = value;
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
IDictionary<string, object> targetCategory = getCategoryByName(category);
if(targetCategory == null) {
return false;
}
return targetCategory.Remove(optionName);
}
/// <summary>Looks up a category by its name</summary>
/// <param name="name">Name of the category that will be looked up</param>
/// <returns>The category with the specified name if found, null otherwise</returns>
private IDictionary<string, object> getCategoryByName(string name) {
IDictionary<string, object> category;
if(string.IsNullOrEmpty(name)) {
category = this.rootOptions;
} else if(!this.options.TryGetValue(name, out category)) {
return null;
}
return category;
}
/// <summary>Categories and the options stored in them</summary>
private IDictionary<string, IDictionary<string, object>> options;
/// <summary>Options stored at the root level</summary>
private IDictionary<string, object> rootOptions;
}
} // namespace Nuclex.Support.Settings
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Generic;
namespace Nuclex.Support.Settings {
/// <summary>Stores settings in memory</summary>
public class MemoryStore : ISettingsStore {
/// <summary>Initializes a new settings store managing settings in memory</summary>
public MemoryStore() {
this.options = new Dictionary<string, IDictionary<string, object>>();
this.rootOptions = new Dictionary<string, object>();
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.options.Keys;
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
IDictionary<string, object> categoryOptions;
if(string.IsNullOrEmpty(category)) {
categoryOptions = this.rootOptions;
} else if(!this.options.TryGetValue(category, out categoryOptions)) {
yield break;
}
foreach(KeyValuePair<string, object> option in categoryOptions) {
OptionInfo optionInfo = new OptionInfo() {
Name = option.Key,
OptionType = option.Value.GetType()
};
yield return optionInfo;
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the settings"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the settings"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
IDictionary<string, object> categoryOptions = getCategoryByName(category);
if(categoryOptions != null) {
object valueAsObject;
if(categoryOptions.TryGetValue(optionName, out valueAsObject)) {
value = (TValue)Convert.ChangeType(valueAsObject, typeof(TValue));
return true;
}
}
value = default(TValue);
return false;
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
IDictionary<string, object> targetCategory;
if(string.IsNullOrEmpty(category)) {
targetCategory = this.rootOptions;
} else if(!this.options.TryGetValue(category, out targetCategory)) {
targetCategory = new Dictionary<string, object>();
this.options.Add(category, targetCategory);
targetCategory.Add(optionName, value);
return;
}
targetCategory[optionName] = value;
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
IDictionary<string, object> targetCategory = getCategoryByName(category);
if(targetCategory == null) {
return false;
}
return targetCategory.Remove(optionName);
}
/// <summary>Looks up a category by its name</summary>
/// <param name="name">Name of the category that will be looked up</param>
/// <returns>The category with the specified name if found, null otherwise</returns>
private IDictionary<string, object> getCategoryByName(string name) {
IDictionary<string, object> category;
if(string.IsNullOrEmpty(name)) {
category = this.rootOptions;
} else if(!this.options.TryGetValue(name, out category)) {
return null;
}
return category;
}
/// <summary>Categories and the options stored in them</summary>
private IDictionary<string, IDictionary<string, object>> options;
/// <summary>Options stored at the root level</summary>
private IDictionary<string, object> rootOptions;
}
} // namespace Nuclex.Support.Settings

View file

@ -1,35 +1,34 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
namespace Nuclex.Support.Settings {
/// <summary>Informations about an option stored in a settings container</summary>
public struct OptionInfo {
/// <summary>Name of the option</summary>
public string Name;
/// <summary>Data type of the option</summary>
public Type OptionType;
}
} // namespace Nuclex.Support.Settings
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
namespace Nuclex.Support.Settings {
/// <summary>Informations about an option stored in a settings container</summary>
public struct OptionInfo {
/// <summary>Name of the option</summary>
public string Name;
/// <summary>Data type of the option</summary>
public Type OptionType;
}
} // namespace Nuclex.Support.Settings

View file

@ -1,366 +1,365 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST && WINDOWS
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Win32;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the windows registry settings store</summary>
[TestFixture]
internal class WindowsRegistryStoreTest {
#region class TestContext
/// <summary>Sets up a temporary registry key for the unit test</summary>
private class TestContext : IDisposable {
/// <summary>Initializes a new test context</summary>
public TestContext() {
this.keyName = Guid.NewGuid().ToString();
this.registryKey = Registry.CurrentUser.CreateSubKey(this.keyName);
this.store = new WindowsRegistryStore(this.registryKey, writable: true);
}
/// <summary>Immediately frees all resources owned by the test context</summary>
public void Dispose() {
if(this.store != null) {
this.store.Dispose();
this.store = null;
this.registryKey = null;
} else if(this.registryKey != null) {
this.registryKey.Dispose();
this.registryKey = null;
}
if(this.keyName != null) {
Registry.CurrentUser.DeleteSubKeyTree(this.keyName);
this.keyName = null;
}
}
/// <summary>Store created on a temporary registry key</summary>
public WindowsRegistryStore Store {
get { return this.store; }
}
/// <summary>Name of the temporary registry key</summary>
private string keyName;
/// <summary>Registry key (ownership transfered to the store)</summary>
private RegistryKey registryKey;
/// <summary>Store that is accessing the registry key</summary>
private WindowsRegistryStore store;
}
#endregion // class TestContext
/// <summary>Verifies that new instances of the registry store can be created</summary>
[Test]
public void CanBeCreated() {
Assert.That(
() => { using(var context = new TestContext()) { } }, Throws.Nothing
);
}
/// <summary>Verifies that new instances of the registry store can be created</summary>
[Test]
public void RegistryHivesCanBeOpened() {
Assert.That(
() => {
using(
var store = new WindowsRegistryStore(
RegistryHive.CurrentUser, "", writable: false
)
) { }
},
Throws.Nothing
);
}
/// <summary>Verifies that booleans can be stored in the registry</summary>
[Test]
public void BooleansCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", true);
Assert.That(context.Store.Get<bool>(null, "test"), Is.True);
context.Store.Set(null, "test", false);
Assert.That(context.Store.Get<bool>(null, "test"), Is.False);
}
}
/// <summary>Verifies that integers can be stored in the registry</summary>
[Test]
public void IntegersCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", 123);
Assert.That(context.Store.Get<int>(null, "test"), Is.EqualTo(123));
context.Store.Set(null, "test", 456);
Assert.That(context.Store.Get<int>(null, "test"), Is.EqualTo(456));
}
}
/// <summary>Verifies that floats can be stored in the registry</summary>
[Test]
public void FloatsCanBeStored() {
float testValue = float.Parse("123.456", CultureInfo.InvariantCulture);
using(var context = new TestContext()) {
context.Store.Set(null, "test", testValue);
Assert.That(context.Store.Get<float>(null, "test"), Is.EqualTo(testValue));
testValue = float.Parse("654.321", CultureInfo.InvariantCulture);
context.Store.Set(null, "test", testValue);
Assert.That(context.Store.Get<float>(null, "test"), Is.EqualTo(testValue));
}
}
/// <summary>Verifies that strings can be stored in the registry</summary>
[Test]
public void StringsCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", "hello world");
Assert.That(context.Store.Get<string>(null, "test"), Is.EqualTo("hello world"));
context.Store.Set(null, "test", "world hello");
Assert.That(context.Store.Get<string>(null, "test"), Is.EqualTo("world hello"));
}
}
/// <summary>Verifies that long integers can be stored in the registry</summary>
[Test]
public void LongIntegersCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", long.MaxValue);
Assert.That(context.Store.Get<long>(null, "test"), Is.EqualTo(long.MaxValue));
context.Store.Set(null, "test", long.MinValue);
Assert.That(context.Store.Get<long>(null, "test"), Is.EqualTo(long.MinValue));
}
}
/// <summary>Verifies that string arrays can be stored in the registry</summary>
[Test]
public void StringArraysCanBeStored() {
string[] english = new string[] { "one", "two", "three" };
string[] german = new string[] { "eins", "zwei", "drei" };
using(var context = new TestContext()) {
context.Store.Set(null, "test", english);
Assert.That(context.Store.Get<string[]>(null, "test"), Is.EquivalentTo(english));
context.Store.Set(null, "test", german);
Assert.That(context.Store.Get<string[]>(null, "test"), Is.EquivalentTo(german));
}
}
/// <summary>
/// Verifies that it's possible to enumerate a category that doesn't exist
/// </summary>
[Test]
public void NonExistingCategoryCanBeEnumerated() {
using(var context = new TestContext()) {
Assert.That(context.Store.EnumerateOptions("doesn't exist"), Is.Empty);
}
}
/// <summary>Verifies that byte arrays can be stored in the registry</summary>
[Test]
public void ByteArraysCanBeStored() {
byte[] ascending = new byte[] { 1, 2, 3 };
byte[] descending = new byte[] { 9, 8, 7 };
using(var context = new TestContext()) {
context.Store.Set(null, "test", ascending);
Assert.That(context.Store.Get<byte[]>(null, "test"), Is.EquivalentTo(ascending));
context.Store.Set(null, "test", descending);
Assert.That(context.Store.Get<byte[]>(null, "test"), Is.EquivalentTo(descending));
}
}
/// <summary>Verifies that strings can be stored in the registry</summary>
[Test]
public void ValuesCanBeStoredInCategories() {
using(var context = new TestContext()) {
context.Store.Set("main", "test", "hello world");
string value;
Assert.That(context.Store.TryGet<string>(null, "test", out value), Is.False);
Assert.That(context.Store.Get<string>("main", "test"), Is.EqualTo("hello world"));
}
}
/// <summary>Verifies that the subkeys of a registry key can be enumerated</summary>
[Test]
public void CategoriesCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set(names[0], "sol", 21);
context.Store.Set(names[1], "sol", 42);
context.Store.Set(names[2], "sol", 84);
Assert.That(context.Store.EnumerateCategories(), Is.EquivalentTo(names));
}
}
/// <summary>Verifies that the values under a registry subkey can be enumerated</summary>
[Test]
public void OptionsInCategoryCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set("test", names[0], 1);
context.Store.Set("test", names[1], 2);
context.Store.Set("test", names[2], 3);
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions("test"));
Assert.That(optionInfos.Count, Is.EqualTo(3));
}
}
/// <summary>Verifies that the values under a registry key can be enumerated</summary>
[Test]
public void RootOptionsCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set(null, names[0], 1);
context.Store.Set(null, names[1], 2);
context.Store.Set(null, names[2], 3);
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions(null));
Assert.That(optionInfos.Count, Is.EqualTo(3));
string[] actualNames = new string[] {
optionInfos[0].Name, optionInfos[1].Name, optionInfos[2].Name
};
Assert.That(actualNames, Is.EquivalentTo(names));
}
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
using(var context = new TestContext()) {
Assert.That(
() => context.Store.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
using(var context = new TestContext()) {
Assert.That(
() => context.Store.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
}
/// <summary>
/// Verifies that values can be removed from a registry key
/// </summary>
[Test]
public void ValuesCanBeRemovedFromRoot() {
using(var context = new TestContext()) {
context.Store.Set(null, "nothing", "short-lived");
Assert.That(context.Store.Remove(null, "nothing"), Is.True);
Assert.That(context.Store.Remove(null, "nothing"), Is.False);
Assert.That(context.Store.EnumerateOptions(), Is.Empty);
}
}
/// <summary>
/// Verifies that values can be removed from the subkey of a registry key
/// </summary>
[Test]
public void ValuesCanBeRemovedFromCategory() {
using(var context = new TestContext()) {
context.Store.Set("limbo", "nothing", "short-lived");
Assert.That(context.Store.Remove("limbo", "nothing"), Is.True);
Assert.That(context.Store.Remove("limbo", "nothing"), Is.False);
Assert.That(context.Store.EnumerateOptions("limbo"), Is.Empty);
}
}
/// <summary>
/// Verifies that values can be removed from a non-existing subkey without
/// causing an error
/// </summary>
[Test]
public void RemovingValueFromNonExistingCategoryCanBeHandled() {
using(var context = new TestContext()) {
Assert.That(context.Store.Remove("empty", "nothing"), Is.False);
}
}
/// <summary>
/// Verifies that the store identifies the types of values stored in
/// a registry when they are enumerated
/// </summary>
[Test]
public void ValueTypesAreIdentifiedWhenEnumerating() {
Type[] types = new Type[] {
typeof(int),
typeof(long),
typeof(byte[]),
typeof(string),
typeof(string[])
};
using(var context = new TestContext()) {
context.Store.Set<int>(null, "0", 123);
context.Store.Set<long>(null, "1", 456L);
context.Store.Set<byte[]>(null, "2", new byte[] { 7, 8, 9 });
context.Store.Set<string>(null, "3", "text");
context.Store.Set<string[]>(null, "4", new string[] { "many", "words" });
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions());
for(int index = 0; index < optionInfos.Count; ++index) {
int typeIndex = int.Parse(optionInfos[index].Name);
Assert.That(optionInfos[index].OptionType, Is.EqualTo(types[typeIndex]));
}
}
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST && WINDOWS
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Win32;
using NUnit.Framework;
namespace Nuclex.Support.Settings {
/// <summary>Unit tests for the windows registry settings store</summary>
[TestFixture]
internal class WindowsRegistryStoreTest {
#region class TestContext
/// <summary>Sets up a temporary registry key for the unit test</summary>
private class TestContext : IDisposable {
/// <summary>Initializes a new test context</summary>
public TestContext() {
this.keyName = Guid.NewGuid().ToString();
this.registryKey = Registry.CurrentUser.CreateSubKey(this.keyName);
this.store = new WindowsRegistryStore(this.registryKey, writable: true);
}
/// <summary>Immediately frees all resources owned by the test context</summary>
public void Dispose() {
if(this.store != null) {
this.store.Dispose();
this.store = null;
this.registryKey = null;
} else if(this.registryKey != null) {
this.registryKey.Dispose();
this.registryKey = null;
}
if(this.keyName != null) {
Registry.CurrentUser.DeleteSubKeyTree(this.keyName);
this.keyName = null;
}
}
/// <summary>Store created on a temporary registry key</summary>
public WindowsRegistryStore Store {
get { return this.store; }
}
/// <summary>Name of the temporary registry key</summary>
private string keyName;
/// <summary>Registry key (ownership transfered to the store)</summary>
private RegistryKey registryKey;
/// <summary>Store that is accessing the registry key</summary>
private WindowsRegistryStore store;
}
#endregion // class TestContext
/// <summary>Verifies that new instances of the registry store can be created</summary>
[Test]
public void CanBeCreated() {
Assert.That(
() => { using(var context = new TestContext()) { } }, Throws.Nothing
);
}
/// <summary>Verifies that new instances of the registry store can be created</summary>
[Test]
public void RegistryHivesCanBeOpened() {
Assert.That(
() => {
using(
var store = new WindowsRegistryStore(
RegistryHive.CurrentUser, "", writable: false
)
) { }
},
Throws.Nothing
);
}
/// <summary>Verifies that booleans can be stored in the registry</summary>
[Test]
public void BooleansCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", true);
Assert.That(context.Store.Get<bool>(null, "test"), Is.True);
context.Store.Set(null, "test", false);
Assert.That(context.Store.Get<bool>(null, "test"), Is.False);
}
}
/// <summary>Verifies that integers can be stored in the registry</summary>
[Test]
public void IntegersCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", 123);
Assert.That(context.Store.Get<int>(null, "test"), Is.EqualTo(123));
context.Store.Set(null, "test", 456);
Assert.That(context.Store.Get<int>(null, "test"), Is.EqualTo(456));
}
}
/// <summary>Verifies that floats can be stored in the registry</summary>
[Test]
public void FloatsCanBeStored() {
float testValue = float.Parse("123.456", CultureInfo.InvariantCulture);
using(var context = new TestContext()) {
context.Store.Set(null, "test", testValue);
Assert.That(context.Store.Get<float>(null, "test"), Is.EqualTo(testValue));
testValue = float.Parse("654.321", CultureInfo.InvariantCulture);
context.Store.Set(null, "test", testValue);
Assert.That(context.Store.Get<float>(null, "test"), Is.EqualTo(testValue));
}
}
/// <summary>Verifies that strings can be stored in the registry</summary>
[Test]
public void StringsCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", "hello world");
Assert.That(context.Store.Get<string>(null, "test"), Is.EqualTo("hello world"));
context.Store.Set(null, "test", "world hello");
Assert.That(context.Store.Get<string>(null, "test"), Is.EqualTo("world hello"));
}
}
/// <summary>Verifies that long integers can be stored in the registry</summary>
[Test]
public void LongIntegersCanBeStored() {
using(var context = new TestContext()) {
context.Store.Set(null, "test", long.MaxValue);
Assert.That(context.Store.Get<long>(null, "test"), Is.EqualTo(long.MaxValue));
context.Store.Set(null, "test", long.MinValue);
Assert.That(context.Store.Get<long>(null, "test"), Is.EqualTo(long.MinValue));
}
}
/// <summary>Verifies that string arrays can be stored in the registry</summary>
[Test]
public void StringArraysCanBeStored() {
string[] english = new string[] { "one", "two", "three" };
string[] german = new string[] { "eins", "zwei", "drei" };
using(var context = new TestContext()) {
context.Store.Set(null, "test", english);
Assert.That(context.Store.Get<string[]>(null, "test"), Is.EquivalentTo(english));
context.Store.Set(null, "test", german);
Assert.That(context.Store.Get<string[]>(null, "test"), Is.EquivalentTo(german));
}
}
/// <summary>
/// Verifies that it's possible to enumerate a category that doesn't exist
/// </summary>
[Test]
public void NonExistingCategoryCanBeEnumerated() {
using(var context = new TestContext()) {
Assert.That(context.Store.EnumerateOptions("doesn't exist"), Is.Empty);
}
}
/// <summary>Verifies that byte arrays can be stored in the registry</summary>
[Test]
public void ByteArraysCanBeStored() {
byte[] ascending = new byte[] { 1, 2, 3 };
byte[] descending = new byte[] { 9, 8, 7 };
using(var context = new TestContext()) {
context.Store.Set(null, "test", ascending);
Assert.That(context.Store.Get<byte[]>(null, "test"), Is.EquivalentTo(ascending));
context.Store.Set(null, "test", descending);
Assert.That(context.Store.Get<byte[]>(null, "test"), Is.EquivalentTo(descending));
}
}
/// <summary>Verifies that strings can be stored in the registry</summary>
[Test]
public void ValuesCanBeStoredInCategories() {
using(var context = new TestContext()) {
context.Store.Set("main", "test", "hello world");
string value;
Assert.That(context.Store.TryGet<string>(null, "test", out value), Is.False);
Assert.That(context.Store.Get<string>("main", "test"), Is.EqualTo("hello world"));
}
}
/// <summary>Verifies that the subkeys of a registry key can be enumerated</summary>
[Test]
public void CategoriesCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set(names[0], "sol", 21);
context.Store.Set(names[1], "sol", 42);
context.Store.Set(names[2], "sol", 84);
Assert.That(context.Store.EnumerateCategories(), Is.EquivalentTo(names));
}
}
/// <summary>Verifies that the values under a registry subkey can be enumerated</summary>
[Test]
public void OptionsInCategoryCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set("test", names[0], 1);
context.Store.Set("test", names[1], 2);
context.Store.Set("test", names[2], 3);
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions("test"));
Assert.That(optionInfos.Count, Is.EqualTo(3));
}
}
/// <summary>Verifies that the values under a registry key can be enumerated</summary>
[Test]
public void RootOptionsCanBeEnumerated() {
string[] names = new string[] { "one", "two", "three" };
using(var context = new TestContext()) {
context.Store.Set(null, names[0], 1);
context.Store.Set(null, names[1], 2);
context.Store.Set(null, names[2], 3);
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions(null));
Assert.That(optionInfos.Count, Is.EqualTo(3));
string[] actualNames = new string[] {
optionInfos[0].Name, optionInfos[1].Name, optionInfos[2].Name
};
Assert.That(actualNames, Is.EquivalentTo(names));
}
}
/// <summary>
/// Verifies that accessing an option that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingOptionThrowsException() {
using(var context = new TestContext()) {
Assert.That(
() => context.Store.Get<string>(null, "doesn't exist"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
}
/// <summary>
/// Verifies that accessing a category that doesn't exist throws an exception
/// </summary>
[Test]
public void AccessingNonExistingCategoryThrowsException() {
using(var context = new TestContext()) {
Assert.That(
() => context.Store.Get<string>("doesn't exist", "test"),
Throws.Exception.AssignableTo<KeyNotFoundException>()
);
}
}
/// <summary>
/// Verifies that values can be removed from a registry key
/// </summary>
[Test]
public void ValuesCanBeRemovedFromRoot() {
using(var context = new TestContext()) {
context.Store.Set(null, "nothing", "short-lived");
Assert.That(context.Store.Remove(null, "nothing"), Is.True);
Assert.That(context.Store.Remove(null, "nothing"), Is.False);
Assert.That(context.Store.EnumerateOptions(), Is.Empty);
}
}
/// <summary>
/// Verifies that values can be removed from the subkey of a registry key
/// </summary>
[Test]
public void ValuesCanBeRemovedFromCategory() {
using(var context = new TestContext()) {
context.Store.Set("limbo", "nothing", "short-lived");
Assert.That(context.Store.Remove("limbo", "nothing"), Is.True);
Assert.That(context.Store.Remove("limbo", "nothing"), Is.False);
Assert.That(context.Store.EnumerateOptions("limbo"), Is.Empty);
}
}
/// <summary>
/// Verifies that values can be removed from a non-existing subkey without
/// causing an error
/// </summary>
[Test]
public void RemovingValueFromNonExistingCategoryCanBeHandled() {
using(var context = new TestContext()) {
Assert.That(context.Store.Remove("empty", "nothing"), Is.False);
}
}
/// <summary>
/// Verifies that the store identifies the types of values stored in
/// a registry when they are enumerated
/// </summary>
[Test]
public void ValueTypesAreIdentifiedWhenEnumerating() {
Type[] types = new Type[] {
typeof(int),
typeof(long),
typeof(byte[]),
typeof(string),
typeof(string[])
};
using(var context = new TestContext()) {
context.Store.Set<int>(null, "0", 123);
context.Store.Set<long>(null, "1", 456L);
context.Store.Set<byte[]>(null, "2", new byte[] { 7, 8, 9 });
context.Store.Set<string>(null, "3", "text");
context.Store.Set<string[]>(null, "4", new string[] { "many", "words" });
var optionInfos = new List<OptionInfo>(context.Store.EnumerateOptions());
for(int index = 0; index < optionInfos.Count; ++index) {
int typeIndex = int.Parse(optionInfos[index].Name);
Assert.That(optionInfos[index].OptionType, Is.EqualTo(types[typeIndex]));
}
}
}
}
} // namespace Nuclex.Support.Settings
#endif // UNITTEST

View file

@ -1,280 +1,279 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2017 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if WINDOWS
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Win32;
namespace Nuclex.Support.Settings {
/// <summary>Stores settings in the registry on Windows operating systems</summary>
/// <remarks>
/// <para>
/// For the cases when you must use the Windows registry, the windows registry store
/// lets you map a registry key as a settings store. Its direct subkeys will become
/// categories and all registry values are made available as options.
/// </para>
/// <para>
/// Use of the registry is strongly discouraged. It binds you to Microsoft's silly
/// technology stack and fragments your application by storing some of its data in
/// the file system while storing other data in an opaque, globally-shared settings
/// manager that is filled with megabytes on unrelated things. Xcopy deployment
/// and portability will be out of the question when relying on the registry.
/// </para>
/// <para>
/// Instead of using this, consider querying for the platform's appropriate location
/// to store settings in.
/// </para>
/// </remarks>
public class WindowsRegistryStore : ISettingsStore, IDisposable {
/// <summary>Initializes a new settings store on the specified registry path</summary>
/// <param name="hive">Hive in which to look</param>
/// <param name="directory">Base path of the settings in the specified hive</param>
/// <param name="writable">Whether to open the registry in writable mode</param>
public WindowsRegistryStore(RegistryHive hive, string directory, bool writable = true) {
using(RegistryKey hiveKey = RegistryKey.OpenBaseKey(hive, RegistryView.Default)) {
this.rootKey = hiveKey.OpenSubKey(directory, writable);
}
this.writable = writable;
}
/// <summary>Initializes a new settings store on the specified registry key</summary>
/// <param name="rootKey">Registry key the settings are stored under</param>
/// <param name="writable">Whether the registry was opened in writable mode</param>
/// <remarks>
/// This constructor takes ownership of the registry key. It will be disposed when
/// the settings store is disposed.
/// </remarks>
public WindowsRegistryStore(RegistryKey rootKey, bool writable = true) {
this.rootKey = rootKey;
this.writable = writable;
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() {
if(this.rootKey != null) {
this.rootKey.Dispose();
this.rootKey = null;
}
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.rootKey.GetSubKeyNames();
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
if(string.IsNullOrEmpty(category)) {
string[] valueNames = this.rootKey.GetValueNames();
for(int index = 0; index < valueNames.Length; ++index) {
yield return new OptionInfo() {
Name = valueNames[index],
OptionType = getBestMatchingType(this.rootKey, valueNames[index])
};
}
} else {
using(RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable)) {
if(categoryKey == null) {
yield break;
}
string[] valueNames = categoryKey.GetValueNames();
for(int index = 0; index < valueNames.Length; ++index) {
yield return new OptionInfo() {
Name = valueNames[index],
OptionType = getBestMatchingType(categoryKey, valueNames[index])
};
}
}
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the registry"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the registry"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
if(string.IsNullOrEmpty(category)) {
return tryGetValueFromKey(this.rootKey, optionName, out value);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
value = default(TValue);
return false;
}
using(categoryKey) {
return tryGetValueFromKey(categoryKey, optionName, out value);
}
}
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
if(string.IsNullOrEmpty(category)) {
setValue(this.rootKey, optionName, value);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
categoryKey = this.rootKey.CreateSubKey(category);
}
using(categoryKey) {
setValue(categoryKey, optionName, value);
}
}
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
if(string.IsNullOrEmpty(category)) {
object value = this.rootKey.GetValue(optionName);
this.rootKey.DeleteValue(optionName, throwOnMissingValue: false);
return (value != null);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
return false;
}
using(categoryKey) {
object value = categoryKey.GetValue(optionName);
categoryKey.DeleteValue(optionName, throwOnMissingValue: false);
return (value != null);
}
}
}
/// <summary>Writes a setting to the registry</summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="registryKey"></param>
/// <param name="optionName"></param>
/// <param name="value"></param>
private void setValue<TValue>(RegistryKey registryKey, string optionName, TValue value) {
if(typeof(TValue) == typeof(int)) {
registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
} else if(typeof(TValue) == typeof(long)) {
registryKey.SetValue(optionName, value, RegistryValueKind.QWord);
} else if(typeof(TValue) == typeof(bool)) {
registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
} else if(typeof(TValue) == typeof(string)) {
registryKey.SetValue(optionName, value, RegistryValueKind.String);
} else if(typeof(TValue) == typeof(string[])) {
registryKey.SetValue(optionName, value, RegistryValueKind.MultiString);
} else if(typeof(TValue) == typeof(byte[])) {
registryKey.SetValue(optionName, value, RegistryValueKind.Binary);
} else {
string valueAsString = (string)Convert.ChangeType(
value, typeof(string), CultureInfo.InvariantCulture
);
registryKey.SetValue(optionName, valueAsString, RegistryValueKind.String);
}
}
/// <summary>Tries to retrieve the value of a registry key if it exists</summary>
/// <typeparam name="TValue">Type of value the registry key is expected to have</typeparam>
/// <param name="categoryKey">Registry key the value is stored under</param>
/// <param name="optionName">Name of the option in the registry</param>
/// <param name="value">Will receive the value read from the registry</param>
/// <returns>True if the value was found, false otherwise</returns>
private bool tryGetValueFromKey<TValue>(
RegistryKey categoryKey, string optionName, out TValue value
) {
object valueAsObject = categoryKey.GetValue(optionName);
if(valueAsObject == null) {
value = default(TValue);
return false;
} else {
value = (TValue)Convert.ChangeType(
valueAsObject, typeof(TValue), CultureInfo.InvariantCulture
);
return true;
}
}
/// <summary>Figures out which .NET type best matches the registry value</summary>
/// <param name="categoryKey">Registry key the key is stored in</param>
/// <param name="optionName">Name of the option that will be retrieved</param>
/// <returns>The best matching .NET type for the registry key's value</returns>
private static Type getBestMatchingType(RegistryKey categoryKey, string optionName) {
RegistryValueKind valueKind = categoryKey.GetValueKind(optionName);
switch(valueKind) {
case RegistryValueKind.Binary: { return typeof(byte[]); }
case RegistryValueKind.DWord: { return typeof(int); }
case RegistryValueKind.QWord: { return typeof(long); }
case RegistryValueKind.MultiString: { return typeof(string[]); }
case RegistryValueKind.ExpandString:
case RegistryValueKind.String:
case RegistryValueKind.Unknown:
case RegistryValueKind.None:
default: { return typeof(string); }
}
}
/// <summary>Key on which the registry store is operating</summary>
private RegistryKey rootKey;
/// <summary>Whether the user can write to the registry key</summary>
private bool writable;
}
} // namespace Nuclex.Support.Settings
#endif // WINDOWS
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if WINDOWS
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Win32;
namespace Nuclex.Support.Settings {
/// <summary>Stores settings in the registry on Windows operating systems</summary>
/// <remarks>
/// <para>
/// For the cases when you must use the Windows registry, the windows registry store
/// lets you map a registry key as a settings store. Its direct subkeys will become
/// categories and all registry values are made available as options.
/// </para>
/// <para>
/// Use of the registry is strongly discouraged. It binds you to Microsoft's silly
/// technology stack and fragments your application by storing some of its data in
/// the file system while storing other data in an opaque, globally-shared settings
/// manager that is filled with megabytes on unrelated things. Xcopy deployment
/// and portability will be out of the question when relying on the registry.
/// </para>
/// <para>
/// Instead of using this, consider querying for the platform's appropriate location
/// to store settings in.
/// </para>
/// </remarks>
public class WindowsRegistryStore : ISettingsStore, IDisposable {
/// <summary>Initializes a new settings store on the specified registry path</summary>
/// <param name="hive">Hive in which to look</param>
/// <param name="directory">Base path of the settings in the specified hive</param>
/// <param name="writable">Whether to open the registry in writable mode</param>
public WindowsRegistryStore(RegistryHive hive, string directory, bool writable = true) {
using(RegistryKey hiveKey = RegistryKey.OpenBaseKey(hive, RegistryView.Default)) {
this.rootKey = hiveKey.OpenSubKey(directory, writable);
}
this.writable = writable;
}
/// <summary>Initializes a new settings store on the specified registry key</summary>
/// <param name="rootKey">Registry key the settings are stored under</param>
/// <param name="writable">Whether the registry was opened in writable mode</param>
/// <remarks>
/// This constructor takes ownership of the registry key. It will be disposed when
/// the settings store is disposed.
/// </remarks>
public WindowsRegistryStore(RegistryKey rootKey, bool writable = true) {
this.rootKey = rootKey;
this.writable = writable;
}
/// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() {
if(this.rootKey != null) {
this.rootKey.Dispose();
this.rootKey = null;
}
}
/// <summary>Enumerates the categories defined in the configuration</summary>
/// <returns>An enumerable list of all used categories</returns>
public IEnumerable<string> EnumerateCategories() {
return this.rootKey.GetSubKeyNames();
}
/// <summary>Enumerates the options stored under the specified category</summary>
/// <param name="category">Category whose options will be enumerated</param>
/// <returns>An enumerable list of all options in the category</returns>
public IEnumerable<OptionInfo> EnumerateOptions(string category = null) {
if(string.IsNullOrEmpty(category)) {
string[] valueNames = this.rootKey.GetValueNames();
for(int index = 0; index < valueNames.Length; ++index) {
yield return new OptionInfo() {
Name = valueNames[index],
OptionType = getBestMatchingType(this.rootKey, valueNames[index])
};
}
} else {
using(RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable)) {
if(categoryKey == null) {
yield break;
}
string[] valueNames = categoryKey.GetValueNames();
for(int index = 0; index < valueNames.Length; ++index) {
yield return new OptionInfo() {
Name = valueNames[index],
OptionType = getBestMatchingType(categoryKey, valueNames[index])
};
}
}
}
}
/// <summary>Retrieves the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <returns>The value of the option with the specified name</returns>
public TValue Get<TValue>(string category, string optionName) {
TValue value;
if(TryGet<TValue>(category, optionName, out value)) {
return value;
} else {
if(string.IsNullOrEmpty(category)) {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' in the registry"
);
} else {
throw new KeyNotFoundException(
"There is no option named '" + optionName + "' under the category '" +
category + "' in the registry"
);
}
}
}
/// <summary>Tries to retrieve the value of the specified option</summary>
/// <typeparam name="TValue">Type the option will be converted to</typeparam>
/// <param name="category">Category the option can be found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be looked up</param>
/// <param name="value">Will receive the value of the option, if found</param>
/// <returns>
/// True if the option existed and its value was written into the <paramref name="value" />
/// parameter, false otherwise
/// </returns>
public bool TryGet<TValue>(string category, string optionName, out TValue value) {
if(string.IsNullOrEmpty(category)) {
return tryGetValueFromKey(this.rootKey, optionName, out value);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
value = default(TValue);
return false;
}
using(categoryKey) {
return tryGetValueFromKey(categoryKey, optionName, out value);
}
}
}
/// <summary>Saves an option in the settings store</summary>
/// <typeparam name="TValue">Type of value that will be saved</typeparam>
/// <param name="category">Category the option will be placed in. Can be null.</param>
/// <param name="optionName">Name of the option that will be saved</param>
/// <param name="value">The value under which the option will be saved</param>
public void Set<TValue>(string category, string optionName, TValue value) {
if(string.IsNullOrEmpty(category)) {
setValue(this.rootKey, optionName, value);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
categoryKey = this.rootKey.CreateSubKey(category);
}
using(categoryKey) {
setValue(categoryKey, optionName, value);
}
}
}
/// <summary>Removes the option with the specified name</summary>
/// <param name="category">Category the option is found in. Can be null.</param>
/// <param name="optionName">Name of the option that will be removed</param>
/// <returns>True if the option was found and removed</returns>
public bool Remove(string category, string optionName) {
if(string.IsNullOrEmpty(category)) {
object value = this.rootKey.GetValue(optionName);
this.rootKey.DeleteValue(optionName, throwOnMissingValue: false);
return (value != null);
} else {
RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
if(categoryKey == null) {
return false;
}
using(categoryKey) {
object value = categoryKey.GetValue(optionName);
categoryKey.DeleteValue(optionName, throwOnMissingValue: false);
return (value != null);
}
}
}
/// <summary>Writes a setting to the registry</summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="registryKey"></param>
/// <param name="optionName"></param>
/// <param name="value"></param>
private void setValue<TValue>(RegistryKey registryKey, string optionName, TValue value) {
if(typeof(TValue) == typeof(int)) {
registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
} else if(typeof(TValue) == typeof(long)) {
registryKey.SetValue(optionName, value, RegistryValueKind.QWord);
} else if(typeof(TValue) == typeof(bool)) {
registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
} else if(typeof(TValue) == typeof(string)) {
registryKey.SetValue(optionName, value, RegistryValueKind.String);
} else if(typeof(TValue) == typeof(string[])) {
registryKey.SetValue(optionName, value, RegistryValueKind.MultiString);
} else if(typeof(TValue) == typeof(byte[])) {
registryKey.SetValue(optionName, value, RegistryValueKind.Binary);
} else {
string valueAsString = (string)Convert.ChangeType(
value, typeof(string), CultureInfo.InvariantCulture
);
registryKey.SetValue(optionName, valueAsString, RegistryValueKind.String);
}
}
/// <summary>Tries to retrieve the value of a registry key if it exists</summary>
/// <typeparam name="TValue">Type of value the registry key is expected to have</typeparam>
/// <param name="categoryKey">Registry key the value is stored under</param>
/// <param name="optionName">Name of the option in the registry</param>
/// <param name="value">Will receive the value read from the registry</param>
/// <returns>True if the value was found, false otherwise</returns>
private bool tryGetValueFromKey<TValue>(
RegistryKey categoryKey, string optionName, out TValue value
) {
object valueAsObject = categoryKey.GetValue(optionName);
if(valueAsObject == null) {
value = default(TValue);
return false;
} else {
value = (TValue)Convert.ChangeType(
valueAsObject, typeof(TValue), CultureInfo.InvariantCulture
);
return true;
}
}
/// <summary>Figures out which .NET type best matches the registry value</summary>
/// <param name="categoryKey">Registry key the key is stored in</param>
/// <param name="optionName">Name of the option that will be retrieved</param>
/// <returns>The best matching .NET type for the registry key's value</returns>
private static Type getBestMatchingType(RegistryKey categoryKey, string optionName) {
RegistryValueKind valueKind = categoryKey.GetValueKind(optionName);
switch(valueKind) {
case RegistryValueKind.Binary: { return typeof(byte[]); }
case RegistryValueKind.DWord: { return typeof(int); }
case RegistryValueKind.QWord: { return typeof(long); }
case RegistryValueKind.MultiString: { return typeof(string[]); }
case RegistryValueKind.ExpandString:
case RegistryValueKind.String:
case RegistryValueKind.Unknown:
case RegistryValueKind.None:
default: { return typeof(string); }
}
}
/// <summary>Key on which the registry store is operating</summary>
private RegistryKey rootKey;
/// <summary>Whether the user can write to the registry key</summary>
private bool writable;
}
} // namespace Nuclex.Support.Settings
#endif // WINDOWS