forked from Shazwazza/ClientDependency
/
CompositeDependencyHandler.cs
221 lines (196 loc) · 7.69 KB
/
CompositeDependencyHandler.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Reflection;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Linq;
using ClientDependency.Core.Config;
namespace ClientDependency.Core
{
public class CompositeDependencyHandler : IHttpHandler
{
static CompositeDependencyHandler()
{
HandlerFileName = DefaultHandlerFileName;
//MaxHandlerUrlLength = DefaultMaxHandlerUrlLength;
}
private static readonly string _versionNo = string.Empty;
private const string DefaultHandlerFileName = "DependencyHandler.axd";
//private const int DefaultMaxHandlerUrlLength = 2000;
private object m_Lock = new object();
/// <summary>
/// The handler file name, by default this is DependencyHandler.axd.
/// </summary>
/// <remarks>
/// If this handler path needs to change, it can be change by setting it in the global.asax on application start
/// TODO: Update this so that it searches the web.config handlers for the dependency handler type to get the path
/// </remarks>
public static string HandlerFileName { get; set; }
/// <summary>
/// When building composite includes, it creates a Base64 encoded string of all of the combined dependency file paths
/// for a given composite group. If this group contains too many files, then the file path with the query string will be very long.
/// This is the maximum allowed number of characters that there is allowed, otherwise an exception is thrown.
/// </summary>
/// <remarks>
/// If this handler path needs to change, it can be change by setting it in the global.asax on application start
/// </remarks>
public static int MaxHandlerUrlLength { get; set; }
bool IHttpHandler.IsReusable
{
get
{
return true;
}
}
void IHttpHandler.ProcessRequest(HttpContext context)
{
HttpResponse response = context.Response;
string fileset = context.Server.UrlDecode(context.Request["s"]);
ClientDependencyType type;
try
{
type = (ClientDependencyType)Enum.Parse(typeof(ClientDependencyType), context.Request["t"], true);
}
catch
{
throw new ArgumentException("Could not parse the type set in the request");
}
if (string.IsNullOrEmpty(fileset))
throw new ArgumentException("Must specify a fileset in the request");
string compositeFileName = "";
byte[] outputBytes = null;
//get the map to the composite file for this file set, if it exists.
CompositeFileMap map = CompositeFileXmlMapper.Instance.GetCompositeFile(fileset);
if (map != null && map.HasFileBytes)
{
ProcessFromFile(context, map, out compositeFileName, out outputBytes);
}
else
{
bool fromFile = false;
lock (m_Lock)
{
//check again...
if (map == null || !map.HasFileBytes)
{
//need to do the combining, etc... and save the file map
//get the file list
string[] strFiles = DecodeFrom64(fileset).Split(';');
//combine files and get the definition types of them (internal vs external resources)
List<CompositeFileDefinition> fDefs;
byte[] fileBytes = CompositeFileProcessor.CombineFiles(strFiles, context, type, out fDefs);
//compress data
CompressionType cType = GetCompression(context);
outputBytes = CompositeFileProcessor.CompressBytes(cType, fileBytes);
SetContentEncodingHeaders(context, cType);
//save combined file
compositeFileName = CompositeFileProcessor.SaveCompositeFile(outputBytes, type);
if (!string.IsNullOrEmpty(compositeFileName))
{
//Update the XML file map
CompositeFileXmlMapper.Instance.CreateMap(fileset, cType.ToString(),
fDefs
.Where(f => f.IsLocalFile)
.Select(x => new FileInfo(context.Server.MapPath(x.Uri))).ToList(), compositeFileName);
}
}
else
{
//files are there now, process from file.
fromFile = true;
}
}
if (fromFile)
{
ProcessFromFile(context, map, out compositeFileName, out outputBytes);
}
}
SetCaching(context, compositeFileName);
context.Response.ContentType = type == ClientDependencyType.Javascript ? "text/javascript" : "text/css";
context.Response.OutputStream.Write(outputBytes, 0, outputBytes.Length);
}
private void ProcessFromFile(HttpContext context, CompositeFileMap map, out string compositeFileName, out byte[] outputBytes)
{
//the saved file's bytes are already compressed.
outputBytes = map.GetCompositeFileBytes();
compositeFileName = map.CompositeFileName;
CompressionType cType = (CompressionType)Enum.Parse(typeof(CompressionType), map.CompressionType);
SetContentEncodingHeaders(context, cType);
}
/// <summary>
/// Sets the output cache parameters and also the client side caching parameters
/// </summary>
/// <param name="context"></param>
private void SetCaching(HttpContext context, string fileName)
{
//This ensures OutputCaching is set for this handler and also controls
//client side caching on the browser side. Default is 10 days.
TimeSpan duration = TimeSpan.FromDays(10);
HttpCachePolicy cache = context.Response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetMaxAge(duration);
cache.SetValidUntilExpires(true);
cache.SetLastModified(DateTime.Now);
cache.SetETag(Guid.NewGuid().ToString());
//set server OutputCache to vary by our params
cache.VaryByParams["t"] = true;
cache.VaryByParams["s"] = true;
//don't allow varying by wildcard
cache.SetOmitVaryStar(true);
//ensure client browser maintains strict caching rules
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
//This is the only way to set the max-age cachability header in ASP.Net!
FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, duration);
//make this output cache dependent on the file if there is one.
if (!string.IsNullOrEmpty(fileName))
context.Response.AddFileDependency(fileName);
}
/// <summary>
/// Sets the content encoding headers based on compressions
/// </summary>
/// <param name="context"></param>
/// <param name="type"></param>
private void SetContentEncodingHeaders(HttpContext context, CompressionType type)
{
if (type == CompressionType.deflate)
{
context.Response.AddHeader("Content-encoding", "deflate");
}
else if (type == CompressionType.gzip)
{
context.Response.AddHeader("Content-encoding", "gzip");
}
}
/// <summary>
/// Check what kind of compression to use
/// </summary>
private CompressionType GetCompression(HttpContext context)
{
CompressionType type = CompressionType.none;
string acceptEncoding = context.Request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(acceptEncoding))
{
//deflate is faster in .Net according to Mads Kristensen (blogengine.net)
if (acceptEncoding.Contains("deflate"))
{
type = CompressionType.deflate;
}
else if (acceptEncoding.Contains("gzip"))
{
type = CompressionType.gzip;
}
}
return type;
}
private string DecodeFrom64(string toDecode)
{
byte[] toDecodeAsBytes = System.Convert.FromBase64String(toDecode);
return System.Text.ASCIIEncoding.ASCII.GetString(toDecodeAsBytes);
}
}
}