// 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
}
}