PageRenderTime 46ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/ClientDependency.Core/CompositeFiles/CompositeDependencyHandler.cs

#
C# | 276 lines | 168 code | 39 blank | 69 comment | 32 complexity | 4bad321d440ced43873bf2d821a65921 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 fileKey;
  37. int version = 0;
  38. if (string.IsNullOrEmpty(context.Request.PathInfo))
  39. {
  40. // querystring format
  41. fileKey = 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. //get path to parse
  56. var path = context.Request.PathInfo.TrimStart('/');
  57. var pathFormat = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.PathBasedUrlFormat;
  58. //parse using the parser
  59. if (!PathBasedUrlFormatter.Parse(pathFormat, path, out fileKey, out type, out version))
  60. {
  61. throw new FormatException("Could not parse the URL path: " + path + " with the format specified: " + pathFormat);
  62. }
  63. }
  64. fileKey = context.Server.UrlDecode(fileKey);
  65. if (string.IsNullOrEmpty(fileKey))
  66. throw new ArgumentException("Must specify a fileset in the request");
  67. byte[] outputBytes = null;
  68. //retry up to 5 times... this is only here due to a bug found in another website that was returning a blank
  69. //result. To date, it can't be replicated in VS, but we'll leave it here for error handling support... can't hurt
  70. for (int i = 0; i < 5; i++)
  71. {
  72. outputBytes = ProcessRequestInternal(contextBase, fileKey, type, version, outputBytes);
  73. if (outputBytes != null && outputBytes.Length > 0)
  74. break;
  75. ClientDependencySettings.Instance.Logger.Error(string.Format("No bytes were returned, this is attempt {0}. Fileset: {1}, Type: {2}, Version: {3}", i, fileKey, type, version), null);
  76. }
  77. if (outputBytes == null || outputBytes.Length == 0)
  78. {
  79. ClientDependencySettings.Instance.Logger.Fatal(string.Format("No bytes were returned after 5 attempts. Fileset: {0}, Type: {1}, Version: {2}", fileKey, type, version), null);
  80. List<CompositeFileDefinition> fDefs;
  81. outputBytes = GetCombinedFiles(contextBase, fileKey, type, out fDefs);
  82. }
  83. context.Response.ContentType = type == ClientDependencyType.Javascript ? "application/x-javascript" : "text/css";
  84. context.Response.OutputStream.Write(outputBytes, 0, outputBytes.Length);
  85. }
  86. internal byte[] ProcessRequestInternal(HttpContextBase context, string fileset, ClientDependencyType type, int version, byte[] outputBytes)
  87. {
  88. //get the compression type supported
  89. var clientCompression = context.GetClientCompression();
  90. var x1 = ClientDependencySettings.Instance;
  91. if (x1 == null) throw new Exception("x1");
  92. var x2 = x1.DefaultFileMapProvider;
  93. if (x2 == null) throw new Exception("x2");
  94. //get the map to the composite file for this file set, if it exists.
  95. var map = ClientDependencySettings.Instance.DefaultFileMapProvider.GetCompositeFile(fileset, version, clientCompression.ToString());
  96. string compositeFileName = "";
  97. if (map != null && map.HasFileBytes)
  98. {
  99. ProcessFromFile(context, map, out compositeFileName, out outputBytes);
  100. }
  101. else
  102. {
  103. lock (Lock)
  104. {
  105. //check again...
  106. if (map != null && map.HasFileBytes)
  107. {
  108. //there's files there now, so process them
  109. ProcessFromFile(context, map, out compositeFileName, out outputBytes);
  110. }
  111. else
  112. {
  113. List<CompositeFileDefinition> fileDefinitions;
  114. byte[] fileBytes;
  115. if (ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.UrlType == CompositeUrlType.MappedId)
  116. {
  117. //need to try to find the map by it's id/version (not compression)
  118. var filePaths = ClientDependencySettings.Instance.DefaultFileMapProvider.GetDependentFiles(fileset, version);
  119. if (filePaths == null)
  120. {
  121. throw new KeyNotFoundException("no map was found for the dependency key: " + fileset +
  122. " ,CompositeUrlType.MappedId requires that a map is found");
  123. }
  124. //combine files and get the definition types of them (internal vs external resources)
  125. fileBytes = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider
  126. .CombineFiles(filePaths.ToArray(), context, type, out fileDefinitions);
  127. }
  128. else
  129. {
  130. //need to do the combining, etc... and save the file map
  131. fileBytes = GetCombinedFiles(context, fileset, type, out fileDefinitions);
  132. }
  133. //compress data
  134. outputBytes = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.CompressBytes(clientCompression, fileBytes);
  135. context.AddCompressionResponseHeader(clientCompression);
  136. //save combined file
  137. var compositeFile = ClientDependencySettings.Instance
  138. .DefaultCompositeFileProcessingProvider
  139. .SaveCompositeFile(outputBytes, type, context.Server);
  140. if (compositeFile != null)
  141. {
  142. compositeFileName = compositeFile.FullName;
  143. if (!string.IsNullOrEmpty(compositeFileName))
  144. {
  145. //Update the XML file map
  146. ClientDependencySettings.Instance.DefaultFileMapProvider.CreateUpdateMap(fileset, clientCompression.ToString(),
  147. fileDefinitions.Select(x => new BasicFile(type) { FilePath = x.Uri }).Cast<IClientDependencyFile>(),
  148. compositeFileName,
  149. ClientDependencySettings.Instance.Version);
  150. }
  151. }
  152. }
  153. }
  154. }
  155. SetCaching(context, compositeFileName, fileset, clientCompression);
  156. return outputBytes;
  157. }
  158. private byte[] GetCombinedFiles(HttpContextBase context, string fileset, ClientDependencyType type, out List<CompositeFileDefinition> fDefs)
  159. {
  160. //get the file list
  161. string[] filePaths = fileset.DecodeFrom64Url().Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
  162. //combine files and get the definition types of them (internal vs external resources)
  163. return ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.CombineFiles(filePaths, context, type, out fDefs);
  164. }
  165. private void ProcessFromFile(HttpContextBase context, CompositeFileMap map, out string compositeFileName, out byte[] outputBytes)
  166. {
  167. //the saved file's bytes are already compressed.
  168. outputBytes = map.GetCompositeFileBytes();
  169. compositeFileName = map.CompositeFileName;
  170. CompressionType cType = (CompressionType)Enum.Parse(typeof(CompressionType), map.CompressionType);
  171. context.AddCompressionResponseHeader(cType);
  172. }
  173. /// <summary>
  174. /// Sets the output cache parameters and also the client side caching parameters
  175. /// </summary>
  176. /// <param name="context"></param>
  177. /// <param name="fileName">The name of the file that has been saved to disk</param>
  178. /// <param name="fileset">The Base64 encoded string supplied in the query string for the handler</param>
  179. /// <param name="compressionType"></param>
  180. private void SetCaching(HttpContextBase context, string fileName, string fileset, CompressionType compressionType)
  181. {
  182. //This ensures OutputCaching is set for this handler and also controls
  183. //client side caching on the browser side. Default is 10 days.
  184. var duration = TimeSpan.FromDays(10);
  185. var cache = context.Response.Cache;
  186. cache.SetCacheability(HttpCacheability.Public);
  187. cache.SetExpires(DateTime.Now.Add(duration));
  188. cache.SetMaxAge(duration);
  189. cache.SetValidUntilExpires(true);
  190. cache.SetLastModified(DateTime.Now);
  191. cache.SetETag("\"" + (fileset + compressionType.ToString()).GenerateHash() + "\"");
  192. //set server OutputCache to vary by our params
  193. /* // proper way to do it is to have
  194. * cache.SetVaryByCustom("cdparms");
  195. *
  196. * // then have this in global.asax
  197. * public override string GetVaryByCustomString(HttpContext context, string arg)
  198. * {
  199. * if (arg == "cdparms")
  200. * {
  201. * if (string.IsNullOrEmpty(context.Request.PathInfo))
  202. * {
  203. * // querystring format
  204. * return context.Request["s"] + "+" + context.Request["t"] + "+" + (context.Request["v"] ?? "0");
  205. * }
  206. * else
  207. * {
  208. * // path format
  209. * return context.Request.PathInfo.Replace('/', '');
  210. * }
  211. * }
  212. * }
  213. *
  214. * // that way, there would be one cache entry for both querystring and path formats.
  215. * // but, it requires a global.asax and I can't find a way to do without it.
  216. */
  217. // in any case, cache already varies by pathInfo (build-in) so for path formats, we do not need anything
  218. // just add params for querystring format, just in case...
  219. cache.VaryByParams["t"] = true;
  220. cache.VaryByParams["s"] = true;
  221. cache.VaryByParams["cdv"] = true;
  222. //ensure the cache is different based on the encoding specified per browser
  223. cache.VaryByContentEncodings["gzip"] = true;
  224. cache.VaryByContentEncodings["deflate"] = true;
  225. //don't allow varying by wildcard
  226. cache.SetOmitVaryStar(true);
  227. //ensure client browser maintains strict caching rules
  228. cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
  229. //This is the only way to set the max-age cachability header in ASP.Net!
  230. //FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
  231. //maxAgeField.SetValue(cache, duration);
  232. //make this output cache dependent on the file if there is one.
  233. if (!string.IsNullOrEmpty(fileName))
  234. context.Response.AddFileDependency(fileName);
  235. }
  236. }
  237. }