namespace CSharpVitamins { using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; /// /// Represents a weighted value (or quality value) from an http header e.g. gzip=0.9; deflate; x-gzip=0.5; /// /// /// accept-encoding spec: /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html /// /// /// Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 /// Accept-Encoding: gzip,deflate /// Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 /// Accept-Language: en-us,en;q=0.5 /// [DebuggerDisplay("QValue[{Name}, {Weight}]")] public struct QValue : IComparable { static char[] delimiters = { ';', '=' }; const float defaultWeight = 1; #region Fields string _name; float _weight; int _ordinal; #endregion #region Constructors /// /// Creates a new QValue by parsing the given value /// for name and weight (qvalue) /// /// The value to be parsed e.g. gzip=0.3 public QValue(string value) : this(value, 0) { } /// /// Creates a new QValue by parsing the given value /// for name and weight (qvalue) and assigns the given /// ordinal /// /// The value to be parsed e.g. gzip=0.3 /// The ordinal/index where the item /// was found in the original list. public QValue(string value, int ordinal) { _name = null; _weight = 0; _ordinal = ordinal; ParseInternal(ref this, value); } #endregion #region Properties /// /// The name of the value part /// public string Name { get { return _name; } } /// /// The weighting (or qvalue, quality value) of the encoding /// public float Weight { get { return _weight; } } /// /// Whether the value can be accepted /// i.e. it's weight is greater than zero /// public bool CanAccept { get { return _weight > 0; } } /// /// Whether the value is empty (i.e. has no name) /// public bool IsEmpty { get { return string.IsNullOrEmpty(_name); } } #endregion #region Methods /// /// Parses the given string for name and /// weigth (qvalue) /// /// The string to parse public static QValue Parse(string value) { QValue item = new QValue(); ParseInternal(ref item, value); return item; } /// /// Parses the given string for name and /// weigth (qvalue) /// /// The string to parse /// The order of item in sequence /// public static QValue Parse(string value, int ordinal) { QValue item = Parse(value); item._ordinal = ordinal; return item; } /// /// Parses the given string for name and /// weigth (qvalue) /// /// The string to parse static void ParseInternal(ref QValue target, string value) { string[] parts = value.Split(delimiters, 3); if (parts.Length > 0) { target._name = parts[0].Trim(); target._weight = defaultWeight; } if (parts.Length == 3) { float.TryParse(parts[2], out target._weight); } } #endregion #region IComparable Members /// /// Compares this instance to another QValue by /// comparing first weights, then ordinals. /// /// The QValue to compare /// public int CompareTo(QValue other) { int value = _weight.CompareTo(other._weight); if (value == 0) { int ord = -_ordinal; value = ord.CompareTo(-other._ordinal); } return value; } #endregion #region CompareByWeight /// /// Compares two QValues in ascending order. /// /// The first QValue /// The second QValue /// public static int CompareByWeightAsc(QValue x, QValue y) { return x.CompareTo(y); } /// /// Compares two QValues in descending order. /// /// The first QValue /// The second QValue /// public static int CompareByWeightDesc(QValue x, QValue y) { return -x.CompareTo(y); } #endregion } /// /// Provides a collection for working with qvalue http headers /// /// /// accept-encoding spec: /// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html /// [DebuggerDisplay("QValue[{Count}, {AcceptWildcard}]")] public sealed class QValueList : List { static char[] delimiters = { ',' }; #region Fields bool _acceptWildcard; bool _autoSort; #endregion #region Constructors /// /// Creates a new instance of an QValueList list from /// the given string of comma delimited values /// /// The raw string of qvalues to load public QValueList(string values) : this(null == values ? new string[0] : values.Split(delimiters, StringSplitOptions.RemoveEmptyEntries)) { } /// /// Creates a new instance of an QValueList from /// the given string array of qvalues /// /// The array of qvalue strings /// i.e. name(;q=[0-9\.]+)? /// /// Should AcceptWildcard include */* as well? /// What about other wildcard forms? /// public QValueList(string[] values) { int ordinal = -1; foreach (string value in values) { QValue qvalue = QValue.Parse(value.Trim(), ++ordinal); if (qvalue.Name.Equals("*")) // wildcard _acceptWildcard = qvalue.CanAccept; Add(qvalue); } /// this list should be sorted by weight for /// methods like FindPreferred to work correctly DefaultSort(); _autoSort = true; } #endregion #region Properties /// /// Whether or not the wildcarded encoding is available and allowed /// public bool AcceptWildcard { get { return _acceptWildcard; } } /// /// Whether, after an add operation, the list should be resorted /// public bool AutoSort { get { return _autoSort; } set { _autoSort = value; } } /// /// Synonym for FindPreferred /// /// The preferred order in which to return an encoding /// An QValue based on weight, or null public QValue this[params string[] candidates] { get { return FindPreferred(candidates); } } #endregion #region Add /// /// Adds an item to the list, then applies sorting /// if AutoSort is enabled. /// /// The item to add public new void Add(QValue item) { base.Add(item); applyAutoSort(); } #endregion #region AddRange /// /// Adds a range of items to the list, then applies sorting /// if AutoSort is enabled. /// /// The items to add public new void AddRange(IEnumerable collection) { bool state = _autoSort; _autoSort = false; base.AddRange(collection); _autoSort = state; applyAutoSort(); } #endregion #region Find /// /// Finds the first QValue with the given name (case-insensitive) /// /// The name of the QValue to search for /// public QValue Find(string name) { Predicate criteria = delegate(QValue item) { return item.Name.Equals(name, StringComparison.OrdinalIgnoreCase); }; return Find(criteria); } #endregion #region FindHighestWeight /// /// Returns the first match found from the given candidates /// /// The list of QValue names to find /// The first QValue match to be found /// Loops from the first item in the list to the last and finds /// the first candidate - the list must be sorted for weight prior to /// calling this method. public QValue FindHighestWeight(params string[] candidates) { Predicate criteria = delegate(QValue item) { return isCandidate(item.Name, candidates); }; return Find(criteria); } #endregion #region FindPreferred /// /// Returns the first match found from the given candidates that is accepted /// /// The list of names to find /// The first QValue match to be found /// Loops from the first item in the list to the last and finds the /// first candidate that can be accepted - the list must be sorted for weight /// prior to calling this method. public QValue FindPreferred(params string[] candidates) { Predicate criteria = delegate(QValue item) { return isCandidate(item.Name, candidates) && item.CanAccept; }; return Find(criteria); } #endregion #region DefaultSort /// /// Sorts the list comparing by weight in /// descending order /// public void DefaultSort() { Sort(QValue.CompareByWeightDesc); } #endregion #region applyAutoSort /// /// Applies the default sorting method if /// the autosort field is currently enabled /// void applyAutoSort() { if (_autoSort) DefaultSort(); } #endregion #region isCandidate /// /// Determines if the given item contained within the applied array /// (case-insensitive) /// /// The string to search for /// The array to search in /// static bool isCandidate(string item, params string[] candidates) { foreach (string candidate in candidates) { if (candidate.Equals(item, StringComparison.OrdinalIgnoreCase)) return true; } return false; } #endregion } }