PageRenderTime 60ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/ASP.Net Client Dependency/CompositeFiles/CompositeDependencyHandler.cs

#
C# | 296 lines | 188 code | 40 blank | 68 comment | 37 complexity | 13812e59597e9390e17a63212c8de604 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Web;
  4. using System.Reflection;
  5. using System.IO;
  6. using System.Linq;
  7. using ClientDependency.Core.CompositeFiles.Providers;
  8. using ClientDependency.Core.Config;
  9. using System.Text;
  10. using System.Web.Security;
  11. namespace ClientDependency.Core.CompositeFiles
  12. {
  13. public class CompositeDependencyHandler : IHttpHandler
  14. {
  15. private readonly static object Lock = new object();
  16. /// <summary>
  17. /// When building composite includes, it creates a Base64 encoded string of all of the combined dependency file paths
  18. /// for a given composite group. If this group contains too many files, then the file path with the query string will be very long.
  19. /// This is the maximum allowed number of characters that there is allowed, otherwise an exception is thrown.
  20. /// </summary>
  21. /// <remarks>
  22. /// If this handler path needs to change, it can be changed by setting it in the global.asax on application start
  23. /// </remarks>
  24. public static int MaxHandlerUrlLength = 2048;
  25. bool IHttpHandler.IsReusable
  26. {
  27. get
  28. {
  29. return true;
  30. }
  31. }
  32. void IHttpHandler.ProcessRequest(HttpContext context)
  33. {
  34. var contextBase = new HttpContextWrapper(context);
  35. ClientDependencyType type;
  36. string fileset;
  37. int version = 0;
  38. if (string.IsNullOrEmpty(context.Request.PathInfo))
  39. {
  40. // querystring format
  41. fileset = context.Request["s"];
  42. if (!string.IsNullOrEmpty(context.Request["cdv"]) && !Int32.TryParse(context.Request["cdv"], out version))
  43. throw new ArgumentException("Could not parse the version in the request");
  44. try
  45. {
  46. type = (ClientDependencyType)Enum.Parse(typeof(ClientDependencyType), context.Request["t"], true);
  47. }
  48. catch
  49. {
  50. throw new ArgumentException("Could not parse the type set in the request");
  51. }
  52. }
  53. else
  54. {
  55. // path format
  56. var segs = context.Request.PathInfo.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  57. fileset = "";
  58. int i = 0;
  59. while (i < segs.Length - 1)
  60. fileset += segs[i++];
  61. int pos;
  62. pos = segs[i].IndexOf('.');
  63. if (pos < 0)
  64. throw new ArgumentException("Could not parse the type set in the request");
  65. fileset += segs[i].Substring(0, pos);
  66. string ext = segs[i].Substring(pos + 1);
  67. pos = ext.IndexOf('.');
  68. if (pos > 0)
  69. {
  70. if (!Int32.TryParse(ext.Substring(0, pos), out version))
  71. throw new ArgumentException("Could not parse the version in the request");
  72. ext = ext.Substring(pos + 1);
  73. }
  74. ext = ext.ToLower();
  75. if (ext == "js")
  76. type = ClientDependencyType.Javascript;
  77. else if (ext == "css")
  78. type = ClientDependencyType.Css;
  79. else
  80. throw new ArgumentException("Could not parse the type set in the request");
  81. }
  82. fileset = context.Server.UrlDecode(fileset);
  83. if (string.IsNullOrEmpty(fileset))
  84. throw new ArgumentException("Must specify a fileset in the request");
  85. byte[] outputBytes = null;
  86. //retry up to 5 times... this is only here due to a bug found in another website that was returning a blank
  87. //result. To date, it can't be replicated in VS, but we'll leave it here for error handling support... can't hurt
  88. for (int i = 0; i < 5; i++)
  89. {
  90. outputBytes = ProcessRequestInternal(contextBase, fileset, type, version, outputBytes);
  91. if (outputBytes != null && outputBytes.Length > 0)
  92. break;
  93. ClientDependencySettings.Instance.Logger.Error(string.Format("No bytes were returned, this is attempt {0}. Fileset: {1}, Type: {2}, Version: {3}", i, fileset, type, version), null);
  94. }
  95. if (outputBytes == null || outputBytes.Length == 0)
  96. {
  97. ClientDependencySettings.Instance.Logger.Fatal(string.Format("No bytes were returned after 5 attempts. Fileset: {0}, Type: {1}, Version: {2}", fileset, type, version), null);
  98. List<CompositeFileDefinition> fDefs;
  99. outputBytes = GetCombinedFiles(contextBase, fileset, type, out fDefs);
  100. }
  101. context.Response.ContentType = type == ClientDependencyType.Javascript ? "application/x-javascript" : "text/css";
  102. context.Response.OutputStream.Write(outputBytes, 0, outputBytes.Length);
  103. }
  104. internal byte[] ProcessRequestInternal(HttpContextBase context, string fileset, ClientDependencyType type, int version, byte[] outputBytes)
  105. {
  106. //get the compression type supported
  107. var clientCompression = context.GetClientCompression();
  108. //get the map to the composite file for this file set, if it exists.
  109. var map = ClientDependencySettings.Instance.DefaultFileMapProvider.GetCompositeFile(fileset, version, clientCompression.ToString());
  110. string compositeFileName = "";
  111. if (map != null && map.HasFileBytes)
  112. {
  113. ProcessFromFile(context, map, out compositeFileName, out outputBytes);
  114. }
  115. else
  116. {
  117. lock (Lock)
  118. {
  119. //check again...
  120. if (map != null && map.HasFileBytes)
  121. {
  122. //there's files there now, so process them
  123. ProcessFromFile(context, map, out compositeFileName, out outputBytes);
  124. }
  125. else
  126. {
  127. List<CompositeFileDefinition> fileDefinitions;
  128. byte[] fileBytes;
  129. if (ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.UrlType == CompositeUrlType.MappedId)
  130. {
  131. //need to try to find the map by it's id/version (not compression)
  132. var filePaths = ClientDependencySettings.Instance.DefaultFileMapProvider.GetDependentFiles(fileset, version);
  133. if (filePaths == null)
  134. {
  135. throw new KeyNotFoundException("no map was found for the dependency key: " + fileset +
  136. " ,CompositeUrlType.MappedId requires that a map is found");
  137. }
  138. //combine files and get the definition types of them (internal vs external resources)
  139. fileBytes = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider
  140. .CombineFiles(filePaths.ToArray(), context, type, out fileDefinitions);
  141. }
  142. else
  143. {
  144. //need to do the combining, etc... and save the file map
  145. fileBytes = GetCombinedFiles(context, fileset, type, out fileDefinitions);
  146. }
  147. //compress data
  148. outputBytes = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.CompressBytes(clientCompression, fileBytes);
  149. context.AddCompressionResponseHeader(clientCompression);
  150. //save combined file
  151. var compositeFile = ClientDependencySettings.Instance
  152. .DefaultCompositeFileProcessingProvider
  153. .SaveCompositeFile(outputBytes, type, context.Server);
  154. if (compositeFile != null)
  155. {
  156. compositeFileName = compositeFile.FullName;
  157. if (!string.IsNullOrEmpty(compositeFileName))
  158. {
  159. //Update the XML file map
  160. ClientDependencySettings.Instance.DefaultFileMapProvider.CreateUpdateMap(fileset, clientCompression.ToString(),
  161. fileDefinitions.Select(x => new BasicFile(type) { FilePath = x.Uri }).Cast<IClientDependencyFile>(),
  162. compositeFileName,
  163. ClientDependencySettings.Instance.Version);
  164. }
  165. }
  166. }
  167. }
  168. }
  169. SetCaching(context, compositeFileName, fileset, clientCompression);
  170. return outputBytes;
  171. }
  172. private byte[] GetCombinedFiles(HttpContextBase context, string fileset, ClientDependencyType type, out List<CompositeFileDefinition> fDefs)
  173. {
  174. //get the file list
  175. string[] filePaths = fileset.DecodeFrom64Url().Split(';');
  176. //combine files and get the definition types of them (internal vs external resources)
  177. return ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.CombineFiles(filePaths, context, type, out fDefs);
  178. }
  179. private void ProcessFromFile(HttpContextBase context, CompositeFileMap map, out string compositeFileName, out byte[] outputBytes)
  180. {
  181. //the saved file's bytes are already compressed.
  182. outputBytes = map.GetCompositeFileBytes();
  183. compositeFileName = map.CompositeFileName;
  184. CompressionType cType = (CompressionType)Enum.Parse(typeof(CompressionType), map.CompressionType);
  185. context.AddCompressionResponseHeader(cType);
  186. }
  187. /// <summary>
  188. /// Sets the output cache parameters and also the client side caching parameters
  189. /// </summary>
  190. /// <param name="context"></param>
  191. /// <param name="fileName">The name of the file that has been saved to disk</param>
  192. /// <param name="fileset">The Base64 encoded string supplied in the query string for the handler</param>
  193. /// <param name="compressionType"></param>
  194. private void SetCaching(HttpContextBase context, string fileName, string fileset, CompressionType compressionType)
  195. {
  196. if (string.IsNullOrEmpty(fileName))
  197. {
  198. ClientDependencySettings.Instance.Logger.Error("ClientDependency handler path is null", new Exception());
  199. return;
  200. }
  201. //This ensures OutputCaching is set for this handler and also controls
  202. //client side caching on the browser side. Default is 10 days.
  203. var duration = TimeSpan.FromDays(10);
  204. var cache = context.Response.Cache;
  205. cache.SetCacheability(HttpCacheability.Public);
  206. cache.SetExpires(DateTime.Now.Add(duration));
  207. cache.SetMaxAge(duration);
  208. cache.SetValidUntilExpires(true);
  209. cache.SetLastModified(DateTime.Now);
  210. cache.SetETag("\"" + FormsAuthentication.HashPasswordForStoringInConfigFile(fileset + compressionType.ToString(), "MD5") + "\"");
  211. //set server OutputCache to vary by our params
  212. /* // proper way to do it is to have
  213. * cache.SetVaryByCustom("cdparms");
  214. *
  215. * // then have this in global.asax
  216. * public override string GetVaryByCustomString(HttpContext context, string arg)
  217. * {
  218. * if (arg == "cdparms")
  219. * {
  220. * if (string.IsNullOrEmpty(context.Request.PathInfo))
  221. * {
  222. * // querystring format
  223. * return context.Request["s"] + "+" + context.Request["t"] + "+" + (context.Request["v"] ?? "0");
  224. * }
  225. * else
  226. * {
  227. * // path format
  228. * return context.Request.PathInfo.Replace('/', '');
  229. * }
  230. * }
  231. * }
  232. *
  233. * // that way, there would be one cache entry for both querystring and path formats.
  234. * // but, it requires a global.asax and I can't find a way to do without it.
  235. */
  236. // in any case, cache already varies by pathInfo (build-in) so for path formats, we do not need anything
  237. // just add params for querystring format, just in case...
  238. cache.VaryByParams["t"] = true;
  239. cache.VaryByParams["s"] = true;
  240. cache.VaryByParams["cdv"] = true;
  241. //ensure the cache is different based on the encoding specified per browser
  242. cache.VaryByContentEncodings["gzip"] = true;
  243. cache.VaryByContentEncodings["deflate"] = true;
  244. //don't allow varying by wildcard
  245. cache.SetOmitVaryStar(true);
  246. //ensure client browser maintains strict caching rules
  247. cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
  248. //This is the only way to set the max-age cachability header in ASP.Net!
  249. //FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
  250. //maxAgeField.SetValue(cache, duration);
  251. //make this output cache dependent on the file if there is one.
  252. if (!string.IsNullOrEmpty(fileName))
  253. context.Response.AddFileDependency(fileName);
  254. }
  255. }
  256. }