Skip to content

Commit 5abaf40

Browse files
author
Rafael J. Staib
committed
Add dynamic compression
1 parent 12b0d42 commit 5abaf40

31 files changed

+634
-1
lines changed
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace JSteps.Web.Mvc
7+
{
8+
public class AcceptEncodingCollection
9+
: AcceptRequestHeaderCollectionBase<AcceptRequestHeaderItem>
10+
{
11+
public AcceptEncodingCollection(string header)
12+
: base(header)
13+
{
14+
}
15+
16+
public override bool AcceptAsterisk
17+
{
18+
get { return true; }
19+
}
20+
21+
public override bool AcceptEmptyHeader
22+
{
23+
get { return true; }
24+
}
25+
26+
public override bool AcceptEmptyValue
27+
{
28+
get { return true; }
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Collections.ObjectModel;
6+
using System.Collections;
7+
8+
namespace JSteps.Web.Mvc
9+
{
10+
/// <summary>
11+
/// Provides a read-only collection base for http accept request headers.
12+
/// </summary>
13+
/// <remarks>
14+
/// accept-encoding spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
15+
/// Note: The list is ordered by quality.
16+
/// </remarks>
17+
public abstract class AcceptRequestHeaderCollectionBase<T>
18+
: IList<T>
19+
where T : AcceptRequestHeaderItem
20+
{
21+
private static char _delimiters = ',';
22+
private static char[] _parameterDelimiters = { ';', '=' };
23+
24+
private readonly List<T> _sortedItems;
25+
26+
protected AcceptRequestHeaderCollectionBase(string header)
27+
{
28+
ArgumentValidation.ValidateArgument("header", header);
29+
30+
if (!AcceptEmptyHeader)
31+
{
32+
throw new ArgumentException("Empty accept request header is not allowed.", "header");
33+
}
34+
35+
_sortedItems = (from i in Parse(header) where (!i.IsAsterisk || AcceptAsterisk) &&
36+
(!i.IsEmpty || AcceptEmptyValue) orderby i.Quality descending select i).ToList();
37+
}
38+
39+
#region Helper Methods
40+
41+
private List<T> Parse(string header)
42+
{
43+
var items = new List<T>();
44+
45+
var values = header.Split(_delimiters);
46+
foreach (var value in values)
47+
{
48+
var parameters = value.Split(_parameterDelimiters);
49+
var keyValuePairs = new Dictionary<string, string>();
50+
51+
if (parameters.Count() > 1)
52+
{
53+
var extensions = parameters.Skip(1).ToArray();
54+
for (var i = 0; i < extensions.Count(); i = i + 2)
55+
{
56+
keyValuePairs.Add(extensions[i].Trim(), extensions[i + 1].Trim());
57+
}
58+
}
59+
60+
var item = CreateItem(parameters[0].Trim(), keyValuePairs);
61+
if (!item.IsEmpty || AcceptEmptyValue)
62+
{
63+
items.Add(item);
64+
}
65+
}
66+
67+
return items;
68+
}
69+
70+
private static T CreateItem(string value, IDictionary<string, string> parameters)
71+
{
72+
var constructor = typeof(T).GetConstructor(new[] { value.GetType(), parameters.GetType() });
73+
return (T)constructor.Invoke(new object[] { value, parameters });
74+
}
75+
76+
#endregion
77+
78+
#region IList<T> Members
79+
80+
public T this[int index]
81+
{
82+
get
83+
{
84+
return _sortedItems[index];
85+
}
86+
set
87+
{
88+
throw new NotSupportedException();
89+
}
90+
}
91+
92+
public void Add(T item)
93+
{
94+
throw new NotSupportedException();
95+
}
96+
97+
public void Clear()
98+
{
99+
throw new NotSupportedException();
100+
}
101+
102+
public bool Contains(T item)
103+
{
104+
return _sortedItems.Contains(item);
105+
}
106+
107+
public void CopyTo(T[] array, int arrayIndex)
108+
{
109+
_sortedItems.CopyTo(array, arrayIndex);
110+
}
111+
112+
public int Count
113+
{
114+
get { return _sortedItems.Count(); }
115+
}
116+
117+
public IEnumerator<T> GetEnumerator()
118+
{
119+
return _sortedItems.GetEnumerator();
120+
}
121+
122+
IEnumerator IEnumerable.GetEnumerator()
123+
{
124+
return _sortedItems.GetEnumerator();
125+
}
126+
127+
public int IndexOf(T item)
128+
{
129+
return _sortedItems.IndexOf(item);
130+
}
131+
132+
public void Insert(int index, T item)
133+
{
134+
throw new NotSupportedException();
135+
}
136+
137+
public bool IsReadOnly
138+
{
139+
get { return true; }
140+
}
141+
142+
public bool Remove(T item)
143+
{
144+
throw new NotSupportedException();
145+
}
146+
147+
public void RemoveAt(int index)
148+
{
149+
throw new NotSupportedException();
150+
}
151+
152+
#endregion
153+
154+
#region Methods & Properties
155+
156+
/// <summary>
157+
/// Whether or not the asterisk (e.g. "*" or "*/*") encoding is available and allowed
158+
/// </summary>
159+
public abstract bool AcceptAsterisk { get; }
160+
161+
/// <summary>
162+
/// Whether or not empty header is allowed
163+
/// </summary>
164+
public abstract bool AcceptEmptyHeader { get; }
165+
166+
/// <summary>
167+
/// Whether or not empty value is allowed
168+
/// </summary>
169+
public abstract bool AcceptEmptyValue { get; }
170+
171+
/// <summary>
172+
/// Searches for an element that matches the conditions defined by the specified predicate,
173+
/// and returns the first occurrence within the entire collection.
174+
/// </summary>
175+
/// <param name="match">The <see cref="Predicate<T>"/> delegate that defines the conditions
176+
/// of the element to search for.</param>
177+
/// <returns>The first element that matches the conditions defined by the specified predicate,
178+
/// if found; otherwise, the default value for type T.</returns>
179+
public T Find(Predicate<T> match)
180+
{
181+
return _sortedItems.Find(match);
182+
}
183+
184+
/// <summary>
185+
/// Returns the first match found from the given canidates that is accepted
186+
/// </summary>
187+
/// <param name="canidates">The list of names to find</param>
188+
/// <returns>The first <see cref="T"/> match to be found</returns>
189+
public T FindAccepted(IEnumerable<string> canidates)
190+
{
191+
Predicate<T> predicate = (T i) =>
192+
{
193+
return (canidates.Any(a => a.Equals(i.Value, StringComparison.OrdinalIgnoreCase)) ||
194+
i.IsAsterisk) && i.Accepted;
195+
};
196+
197+
return Find(predicate);
198+
}
199+
200+
#endregion
201+
}
202+
}
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace JSteps.Web.Mvc
7+
{
8+
public class AcceptRequestHeaderItem
9+
{
10+
private const string _qualityParameterKey = "q";
11+
private static float _defaultQuality = 1;
12+
13+
private bool _isAsterisk;
14+
15+
private AcceptRequestHeaderItem()
16+
{
17+
}
18+
19+
public AcceptRequestHeaderItem(string value, IDictionary<string, string> parameters)
20+
{
21+
Value = value;
22+
IsEmpty = string.IsNullOrWhiteSpace(value);
23+
_isAsterisk = value.Equals("*", StringComparison.OrdinalIgnoreCase);
24+
25+
float quality;
26+
if (parameters.ContainsKey(_qualityParameterKey) &&
27+
float.TryParse(parameters[_qualityParameterKey], out quality))
28+
{
29+
Quality = quality;
30+
}
31+
else
32+
{
33+
Quality = _defaultQuality;
34+
}
35+
Accepted = Quality > 0;
36+
37+
ExtensionParameters = parameters.Where(p => p.Key != _qualityParameterKey)
38+
.ToDictionary(k => k.Key, e => e.Value);
39+
}
40+
41+
/// <summary>
42+
/// Whether or not the value is acceptable. Acceptable means Quality is non-zero an positive.
43+
/// </summary>
44+
public bool Accepted { get; private set; }
45+
46+
/// <summary>
47+
/// Gets a collection of parameters that are not represented by an specific property.
48+
/// </summary>
49+
public IDictionary<string, string> ExtensionParameters { get; private set; }
50+
51+
/// <summary>
52+
/// Whether or not the value is empty.
53+
/// </summary>
54+
public bool IsEmpty { get; private set; }
55+
56+
/// <summary>
57+
/// Whether or not the value is a asterisk.
58+
/// </summary>
59+
public virtual bool IsAsterisk
60+
{
61+
get
62+
{
63+
return _isAsterisk;
64+
}
65+
}
66+
67+
/// <summary>
68+
/// Gets the weighting (or qvalue, quality value) of the encoding
69+
/// </summary>
70+
public float Quality { get; private set; }
71+
72+
/// <summary>
73+
/// Gets the value of one accept item (e.g. "gzip", "de-DE", "text/html").
74+
/// </summary>
75+
public string Value { get; private set; }
76+
}
77+
}

JSteps.Web.Mvc/ArgumentValidation.cs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
3+
namespace JSteps.Web.Mvc
4+
{
5+
public static class ArgumentValidation
6+
{
7+
public static void ValidateArgument(string argumentName, object argumentValue)
8+
{
9+
if (argumentValue == null)
10+
{
11+
throw new ArgumentNullException(argumentName, "The value cannot be null.");
12+
}
13+
}
14+
15+
public static void ValidateStringArgument(string argumentName, string argumentValue)
16+
{
17+
if (string.IsNullOrEmpty(argumentValue))
18+
{
19+
throw new ArgumentException("The value cannot be null or empty.", argumentName);
20+
}
21+
}
22+
}
23+
}

JSteps.Web.Mvc/CompressAttribute.cs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO.Compression;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Web.Mvc;
7+
8+
namespace JSteps.Web.Mvc
9+
{
10+
public class CompressAttribute
11+
: ActionFilterAttribute
12+
{
13+
public override void OnActionExecuting(ActionExecutingContext filterContext)
14+
{
15+
var request = filterContext.HttpContext.Request;
16+
var response = filterContext.HttpContext.Response;
17+
18+
var acceptEncodingHeader = request.Headers["Accept-Encoding"];
19+
if (response.Filter != null && acceptEncodingHeader != null)
20+
{
21+
//NOTE: Values of supported have to be lowerd
22+
var supported = new string[] { "gzip", "deflate" };
23+
var items = new AcceptEncodingCollection(acceptEncodingHeader);
24+
var acceptedItem = items.FindAccepted(supported);
25+
26+
switch (acceptedItem.Value)
27+
{
28+
case "*":
29+
case "gzip":
30+
response.AppendHeader("Content-Encoding", "gzip");
31+
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
32+
break;
33+
34+
case "deflate":
35+
response.AppendHeader("Content-Encoding", "deflate");
36+
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
37+
break;
38+
39+
default:
40+
break;
41+
}
42+
}
43+
44+
base.OnActionExecuting(filterContext);
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)