// The MIT License (MIT) // // Copyright (c) 2015 Dave Transom // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // For background refer to this article by Dave Transom // http://www.singular.co.nz/2008/07/finding-preferred-accept-encoding-header-in-csharp/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; namespace wasSharp.Web { /// /// 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 { private static readonly char[] delimiters = { ';', '=' }; private const float defaultWeight = 1; #region Fields private float _weight; private int _ordinal; #endregion Fields #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 Constructors #region Properties /// /// The name of the value part /// public string Name { get; private set; } /// /// The weighting (or qvalue, quality value) of the encoding /// public float Weight => _weight; /// /// Whether the value can be accepted /// i.e. it's weight is greater than zero /// public bool CanAccept => _weight > 0; /// /// Whether the value is empty (i.e. has no name) /// public bool IsEmpty => string.IsNullOrEmpty(Name); #endregion Properties #region Methods /// /// Parses the given string for name and /// weigth (qvalue) /// /// The string to parse public static QValue Parse(string value) { var 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) { var item = Parse(value); item._ordinal = ordinal; return item; } /// /// Parses the given string for name and /// weigth (qvalue) /// /// The string to parse private static void ParseInternal(ref QValue target, string value) { var 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], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture.NumberFormat, out target._weight); } } #endregion Methods #region IComparable Members /// /// Compares this instance to another QValue by /// comparing first weights, then ordinals. /// /// The QValue to compare /// public int CompareTo(QValue other) { var value = _weight.CompareTo(other._weight); if (value == 0) { var ord = -_ordinal; value = ord.CompareTo(-other._ordinal); } return value; } #endregion IComparable Members #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 CompareByWeight } /// /// 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 { private static readonly char[] delimiters = { ',' }; #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 Add #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) { var state = AutoSort; AutoSort = false; base.AddRange(collection); AutoSort = state; applyAutoSort(); } #endregion AddRange #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 = item => item.Name.Equals(name, StringComparison.OrdinalIgnoreCase); return Find(criteria); } #endregion Find #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 = item => isCandidate(item.Name, candidates); return Find(criteria); } #endregion FindHighestWeight #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 = item => isCandidate(item.Name, candidates) && item.CanAccept; return Find(criteria); } #endregion FindPreferred #region DefaultSort /// /// Sorts the list comparing by weight in /// descending order /// public void DefaultSort() { Sort(QValue.CompareByWeightDesc); } #endregion DefaultSort #region applyAutoSort /// /// Applies the default sorting method if /// the autosort field is currently enabled /// private void applyAutoSort() { if (AutoSort) DefaultSort(); } #endregion applyAutoSort #region isCandidate /// /// Determines if the given item contained within the applied array /// (case-insensitive) /// /// The string to search for /// The array to search in /// private static bool isCandidate(string item, params string[] candidates) { foreach (var candidate in candidates) { if (candidate.Equals(item, StringComparison.OrdinalIgnoreCase)) return true; } return false; } #endregion isCandidate #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) { var ordinal = -1; foreach (var value in values) { var 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 Constructors #region Properties /// /// Whether or not the wildcarded encoding is available and allowed /// public bool AcceptWildcard { get; } /// /// Whether, after an add operation, the list should be resorted /// public bool AutoSort { get; set; } /// /// 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] => FindPreferred(candidates); #endregion Properties } }