#region License
// Copyright 2009 Buu Nguyen (http://www.buunguyen.net/blog)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// The latest version of this file can be found at http://combres.codeplex.com
#endregion
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Web;
using Fasterflect;
using log4net;
using Yahoo.Yui.Compressor;
namespace Combres
{
internal class RequestHandler
{
private static readonly ILog Log = LogManager.GetLogger(
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private HttpContext Context { get; set; }
private Settings Settings { get; set; }
private String CacheKey { get; set; }
private String DateCacheKey { get; set; }
private bool CanCompress { get; set; }
private ResourceSet ResourceSet { get; set; }
public RequestHandler(HttpContext context, Settings settings,
string setName, string version)
{
Context = context;
Settings = settings;
ResourceSet = Settings[setName];
if (ResourceSet == null)
{
throw new ResourceSetNotFoundException("Resource set not found");
}
CanCompress = ClientAcceptsCompression(Context.Request);
CacheKey = string.Concat(typeof(RequestHandler), setName, "_", version, "_", CanCompress);
DateCacheKey = string.Concat(CacheKey, "_date");
}
public void Execute()
{
if (ResourceSet.DebugEnabled)
{
ProcessAndWrite();
return;
}
if (IsInBrowserCache())
return;
if (WriteFromServerCache())
return;
ProcessAndWrite();
}
private void ProcessAndWrite()
{
using (var memoryStream = new MemoryStream(4096))
{
var combinedContent = GetCombinedContents();
if (!ResourceSet.DebugEnabled)
combinedContent = MinifyContent(combinedContent);
ZipContentToStream(combinedContent, memoryStream);
var responseBytes = memoryStream.ToArray();
if (!ResourceSet.DebugEnabled)
CacheNewResponse(responseBytes);
SendOutputToClient(responseBytes, !ResourceSet.DebugEnabled);
}
}
private string MinifyContent(string combinedContent)
{
if (Log.IsDebugEnabled)
Log.Debug("Minifying combined content...");
combinedContent = ResourceSet.Type == ResourceType.JS
? CompressJs(combinedContent)
: CompressCss(combinedContent);
Settings.FilterTypes.ForEach(filterType =>
{
if (Log.IsDebugEnabled)
Log.Debug("Performing minified content filtering with " + filterType + " for set " + ResourceSet.Name + "...");
combinedContent = filterType.Construct().Invoke<string>("TransformMinifiedContent",
new[] { typeof(Settings), typeof(ResourceSet), typeof(string) },
new object[] { Settings, ResourceSet, combinedContent });
});
return combinedContent;
}
/// <summary>
/// Browsers supporting gzip also supporting deflate and gzip is usually more
/// efficient. Thus, there's no need to handle deflate separately.
/// </summary>
private void ZipContentToStream(string content, Stream stream)
{
using (var writer = CanCompress
? new GZipStream(stream, CompressionMode.Compress)
: stream)
{
var bytes = Encoding.UTF8.GetBytes(content);
writer.Write(bytes, 0, bytes.Length);
}
}
private void CacheNewResponse(byte[] responseBytes)
{
Context.Cache.Insert(CacheKey,
responseBytes,
null /* cache dependencies */,
System.Web.Caching.Cache.NoAbsoluteExpiration,
ResourceSet.DurationInDays);
Context.Cache[DateCacheKey] = DateTime.Now;
var etag = GetEtagFromCache(Context, DateCacheKey);
Context.Response.Cache.SetETag(etag);
}
private bool WriteFromServerCache()
{
var responseBytes = Context.Cache[CacheKey] as byte[];
if (responseBytes == null || responseBytes.Length == 0)
return false;
if (Log.IsDebugEnabled)
Log.Debug("Writing to client from server's cache...");
SendOutputToClient(responseBytes, true);
return true;
}
private string GetCombinedContents()
{
var allResourcesContent = new StringBuilder();
if (Log.IsDebugEnabled)
Log.Debug("Combining resources' content...");
foreach (var resource in ResourceSet)
{
var content = ReadResourceContent(resource);
Settings.FilterTypes.ForEach(filterType =>
{
if (Log.IsDebugEnabled)
Log.Debug("Performing single content filtering with " + filterType + " for " + resource.Path + "...");
content = filterType.Construct().Invoke<string>("TransformSingleContent",
new[] { typeof(Settings), typeof(Resource), typeof(string) },
new object[] { Settings, resource, content });
});
allResourcesContent.Append(content);
}
var allResourcesContentString = allResourcesContent.ToString();
Settings.FilterTypes.ForEach(filterType =>
{
if (Log.IsDebugEnabled)
Log.Debug("Performing combined content filtering with " + filterType + " for set " + ResourceSet.Name + "...");
allResourcesContentString = filterType.Construct().Invoke<string>("TransformCombinedContent",
new[] { typeof(Settings), typeof(ResourceSet), typeof(string) },
new object[] { Settings, ResourceSet, allResourcesContentString });
});
return allResourcesContentString;
}
private string ReadResourceContent(Resource resource)
{
if (Log.IsDebugEnabled)
Log.Debug("Reading content of " + resource.Path + "...");
switch (resource.Mode)
{
case ResourceMode.Remote:
case ResourceMode.LocalDynamic:
var absoluteUrl = resource.Mode == ResourceMode.Remote
? resource.Path
: resource.Path.ToAbsoluteUrl();
if (absoluteUrl == null)
throw new ResourceNotFoundException(resource.Path + " cannot be found");
try
{
var webClient = new WebClient();
webClient.Headers["Cookie"] = Context.Request.Headers["Cookie"];
var stream = webClient.OpenRead(absoluteUrl);
var reader = new StreamReader(stream, Encoding.UTF8);
return reader.ReadToEnd();
}
catch (WebException)
{
throw new ResourceNotFoundException(resource.Path + " cannot be found");
}
default:
var fullPath = Context.Server.MapPath(resource.Path);
if (!File.Exists(fullPath))
throw new ResourceNotFoundException(resource.Path + " cannot be found");
return File.ReadAllText(fullPath);
}
}
private void SendOutputToClient(byte[] bytes, bool insertCacheHeaders)
{
if (Log.IsDebugEnabled)
Log.Debug("Writing content to browser...");
var response = Context.Response;
response.AppendHeader("Content-Length", bytes.Length.ToString());
response.ContentType = ResourceSet.Type == ResourceType.JS ? "application/x-javascript" : "text/css";
response.AppendHeader("Content-Encoding", CanCompress ? "gzip" : "utf-8");
response.ContentEncoding = Encoding.Unicode;
if (insertCacheHeaders)
{
/* Tell proxy to cache different versions depending on Accept-Encoding */
response.Cache.VaryByHeaders["Accept-Encoding"] = true;
response.Cache.SetOmitVaryStar(true);
response.Cache.SetMaxAge(ResourceSet.DurationInDays);
response.Cache.SetLastModified(DateTime.Now);
response.Cache.SetExpires(DateTime.Now.Add(ResourceSet.DurationInDays)); /* For HTTP 1.0 browsers */
response.Cache.SetValidUntilExpires(true);
response.Cache.SetCacheability(HttpCacheability.Public);
response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
}
response.OutputStream.Write(bytes, 0, bytes.Length);
response.Flush();
}
private bool IsInBrowserCache()
{
string etag = GetEtagFromCache(Context, DateCacheKey);
if (etag == null)
return false;
string incomingEtag = Context.Request.Headers["If-None-Match"];
if (String.Equals(incomingEtag, etag, StringComparison.Ordinal))
{
if (Log.IsDebugEnabled)
Log.Debug("ETag matches, ending request...");
Context.Response.Cache.SetETag(etag);
Context.Response.AppendHeader("Content-Length", "0");
Context.Response.StatusCode = (int)HttpStatusCode.NotModified;
Context.Response.End();
return true;
}
return false;
}
private static string CompressCss(string content)
{
return CssCompressor.Compress(content);
}
private static string CompressJs(string content)
{
return JavaScriptCompressor.Compress(content,
false /* isVerboseLogging */,
true /* isObfuscateJavascript */,
false /* preserveAllSemicolons */,
false /* disableOptimizations */,
-1 /* lineBreakPosition */,
Encoding.UTF8,
CultureInfo.CurrentCulture);
}
private static string GetEtagFromCache(HttpContext context, string dateCacheKey)
{
var lastModified = context.Cache[dateCacheKey] as DateTime?;
return lastModified == null
? null
: string.Concat("\"", lastModified.GetHashCode(), "\"");
}
private static bool ClientAcceptsCompression(HttpRequest request)
{
var acceptEncoding = request.Headers["Accept-Encoding"];
return !string.IsNullOrEmpty(acceptEncoding) &&
(acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate"));
}
}
}