// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.Dtf.WindowsInstaller
{
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Text;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
///
/// Contains a collection of key-value pairs suitable for passing between
/// immediate and deferred/rollback/commit custom actions.
///
///
/// Call the method to get a string
/// suitable for storing in a property and reconstructing the custom action data later.
///
///
///
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
public sealed class CustomActionData : IDictionary
{
///
/// "CustomActionData" literal property name.
///
public const string PropertyName = "CustomActionData";
private const char DataSeparator = ';';
private const char KeyValueSeparator = '=';
private IDictionary data;
///
/// Creates a new empty custom action data object.
///
public CustomActionData() : this(null)
{
}
///
/// Reconstructs a custom action data object from data that was previously
/// persisted in a string.
///
/// Previous output from .
public CustomActionData(string keyValueList)
{
this.data = new Dictionary();
if (keyValueList != null)
{
this.Parse(keyValueList);
}
}
///
/// Adds a key and value to the data collection.
///
/// Case-sensitive data key.
/// Data value (may be null).
/// the key does not consist solely of letters,
/// numbers, and the period, underscore, and space characters.
public void Add(string key, string value)
{
CustomActionData.ValidateKey(key);
this.data.Add(key, value);
}
///
/// Adds a value to the data collection, using XML serialization to persist the object as a string.
///
/// Case-sensitive data key.
/// Data value (may be null).
/// the key does not consist solely of letters,
/// numbers, and the period, underscore, and space characters.
/// The value type does not support XML serialization.
/// The value could not be serialized.
public void AddObject(string key, T value)
{
if (value == null)
{
this.Add(key, null);
}
else if (typeof(T) == typeof(string) ||
typeof(T) == typeof(CustomActionData)) // Serialize nested CustomActionData
{
this.Add(key, value.ToString());
}
else
{
string valueString = CustomActionData.Serialize(value);
this.Add(key, valueString);
}
}
///
/// Gets a value from the data collection, using XML serialization to load the object from a string.
///
/// Case-sensitive data key.
/// The value could not be deserialized.
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
public T GetObject(string key)
{
string value = this[key];
if (value == null)
{
return default(T);
}
else if (typeof(T) == typeof(string))
{
// Funny casting because the compiler doesn't know T is string here.
return (T) (object) value;
}
else if (typeof(T) == typeof(CustomActionData))
{
// Deserialize nested CustomActionData.
return (T) (object) new CustomActionData(value);
}
else if (value.Length == 0)
{
return default(T);
}
else
{
return CustomActionData.Deserialize(value);
}
}
///
/// Determines whether the data contains an item with the specified key.
///
/// Case-sensitive data key.
/// true if the data contains an item with the key; otherwise, false
public bool ContainsKey(string key)
{
return this.data.ContainsKey(key);
}
///
/// Gets a collection object containing all the keys of the data.
///
public ICollection Keys
{
get
{
return this.data.Keys;
}
}
///
/// Removes the item with the specified key from the data.
///
/// Case-sensitive data key.
/// true if the item was successfully removed from the data;
/// false if an item with the specified key was not found
public bool Remove(string key)
{
return this.data.Remove(key);
}
///
/// Gets the value with the specified key.
///
/// Case-sensitive data key.
/// Value associated with the specified key, or
/// null if an item with the specified key was not found
/// true if the data contains an item with the specified key; otherwise, false.
public bool TryGetValue(string key, out string value)
{
return this.data.TryGetValue(key, out value);
}
///
/// Gets a collection containing all the values of the data.
///
public ICollection Values
{
get
{
return this.data.Values;
}
}
///
/// Gets or sets a data value with a specified key.
///
/// Case-sensitive data key.
/// the key does not consist solely of letters,
/// numbers, and the period, underscore, and space characters.
public string this[string key]
{
get
{
return this.data[key];
}
set
{
CustomActionData.ValidateKey(key);
this.data[key] = value;
}
}
///
/// Adds an item with key and value to the data collection.
///
/// Case-sensitive data key, with a data value that may be null.
/// the key does not consist solely of letters,
/// numbers, and the period, underscore, and space characters.
public void Add(KeyValuePair item)
{
CustomActionData.ValidateKey(item.Key);
this.data.Add(item);
}
///
/// Removes all items from the data.
///
public void Clear()
{
if (this.data.Count > 0)
{
this.data.Clear();
}
}
///
/// Determines whether the data contains a specified item.
///
/// The data item to locate.
/// true if the data contains the item; otherwise, false
public bool Contains(KeyValuePair item)
{
return this.data.Contains(item);
}
///
/// Copies the data to an array, starting at a particular array index.
///
/// Destination array.
/// Index in the array at which copying begins.
public void CopyTo(KeyValuePair[] array, int arrayIndex)
{
this.data.CopyTo(array, arrayIndex);
}
///
/// Gets the number of items in the data.
///
public int Count
{
get
{
return this.data.Count;
}
}
///
/// Gets a value indicating whether the data is read-only.
///
public bool IsReadOnly
{
get
{
return false;
}
}
///
/// Removes an item from the data.
///
/// The item to remove.
/// true if the item was successfully removed from the data;
/// false if the item was not found
public bool Remove(KeyValuePair item)
{
return this.data.Remove(item);
}
///
/// Returns an enumerator that iterates through the collection.
///
/// An enumerator that can be used to iterate through the collection.
public IEnumerator> GetEnumerator()
{
return this.data.GetEnumerator();
}
///
/// Returns an enumerator that iterates through the collection.
///
/// An enumerator that can be used to iterate through the collection.
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((System.Collections.IEnumerable) this.data).GetEnumerator();
}
///
/// Gets a string representation of the data suitable for persisting in a property.
///
/// Data string in the form "Key1=Value1;Key2=Value2"
public override string ToString()
{
StringBuilder buf = new StringBuilder();
foreach (KeyValuePair item in this.data)
{
if (buf.Length > 0)
{
buf.Append(CustomActionData.DataSeparator);
}
buf.Append(item.Key);
if (item.Value != null)
{
buf.Append(CustomActionData.KeyValueSeparator);
buf.Append(CustomActionData.Escape(item.Value));
}
}
return buf.ToString();
}
///
/// Ensures that a key contains valid characters.
///
/// key to be validated
/// the key does not consist solely of letters,
/// numbers, and the period, underscore, and space characters.
private static void ValidateKey(string key)
{
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
for (int i = 0; i < key.Length; i++)
{
char c = key[i];
if (!Char.IsLetterOrDigit(c) && c != '_' && c != '.' &&
!(i > 0 && i < key.Length - 1 && c == ' '))
{
throw new ArgumentOutOfRangeException("key");
}
}
}
///
/// Serializes a value into an XML string.
///
/// Type of the value.
/// Value to be serialized.
/// Serialized value data as a string.
private static string Serialize(T value)
{
XmlWriterSettings xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
using (XmlWriter xw = XmlWriter.Create(sw, xws))
{
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add(string.Empty, String.Empty); // Prevent output of any namespaces
XmlSerializer ser = new XmlSerializer(typeof(T));
ser.Serialize(xw, value, ns);
return sw.ToString();
}
}
///
/// Deserializes a value from an XML string.
///
/// Expected type of the value.
/// Serialized value data.
/// Deserialized value object.
private static T Deserialize(string value)
{
StringReader sr = new StringReader(value);
using (XmlReader xr = XmlReader.Create(sr))
{
XmlSerializer ser = new XmlSerializer(typeof(T));
return (T) ser.Deserialize(xr);
}
}
///
/// Escapes a value string by doubling any data-separator (semicolon) characters.
///
///
/// Escaped value string
private static string Escape(string value)
{
value = value.Replace(String.Empty + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator);
return value;
}
///
/// Unescapes a value string by undoubling any doubled data-separator (semicolon) characters.
///
///
/// Unescaped value string
private static string Unescape(string value)
{
value = value.Replace(String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator);
return value;
}
///
/// Loads key-value pairs from a string into the data collection.
///
/// key-value pair list of the form returned by
private void Parse(string keyValueList)
{
int itemStart = 0;
while (itemStart < keyValueList.Length)
{
// Find the next non-escaped data separator.
int semi = itemStart - 2;
do
{
semi = keyValueList.IndexOf(CustomActionData.DataSeparator, semi + 2);
}
while (semi >= 0 && semi < keyValueList.Length - 1 && keyValueList[semi + 1] == CustomActionData.DataSeparator);
if (semi < 0)
{
semi = keyValueList.Length;
}
// Find the next non-escaped key-value separator.
int equals = itemStart - 2;
do
{
equals = keyValueList.IndexOf(CustomActionData.KeyValueSeparator, equals + 2);
}
while (equals >= 0 && equals < keyValueList.Length - 1 && keyValueList[equals + 1] == CustomActionData.KeyValueSeparator);
if (equals < 0 || equals > semi)
{
equals = semi;
}
string key = keyValueList.Substring(itemStart, equals - itemStart);
string value = null;
// If there's a key-value separator before the next data separator, then the item has a value.
if (equals < semi)
{
value = keyValueList.Substring(equals + 1, semi - (equals + 1));
value = CustomActionData.Unescape(value);
}
// Add non-duplicate items to the collection.
if (key.Length > 0 && !this.data.ContainsKey(key))
{
this.data.Add(key, value);
}
// Move past the data separator to the next item.
itemStart = semi + 1;
}
}
}
}