delphimvcframework/sources/MVCFramework.Controllers.CacheController.pas

252 lines
8.1 KiB
ObjectPascal
Raw Normal View History

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2017 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
// ***************************************************************************
//
// 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.
//
// *************************************************************************** }
2016-09-25 16:17:37 +02:00
unit MVCFramework.Controllers.CacheController;
interface
//NOTE: To use this controller you need DelphiRedisClient
// To use DelphiRedisClient just open a command prompt, go to where you
// usually put your Delphi libraries and run the following command (requires git)
// git clone --recursive https://github.com/danieleteti/delphiredisclient
2016-09-25 16:17:37 +02:00
uses
MVCFramework, Redis.Client, Redis.Commons, MVCFramework.Commons;
2016-09-25 16:17:37 +02:00
type
EMVCCacheException = class(EMVCException)
end;
2016-09-25 16:17:37 +02:00
TMVCCacheController = class(TMVCController)
private
FRedis: IRedisClient;
FCacheEnabled: Boolean;
FExposeCache: Boolean;
FCurrentCacheKey: string;
2016-09-25 16:17:37 +02:00
procedure SetCacheEnabled(const Value: Boolean);
procedure SetExposeCache(const Value: Boolean);
procedure CheckCacheKey;
function RedisClient: IRedisClient;
2016-09-25 16:17:37 +02:00
protected
procedure OnBeforeAction(Context: TWebContext; const AActionNAme: string;
var Handled: Boolean); override;
procedure OnAfterAction(Context: TWebContext; const AActionNAme: string); override;
/// <summary>
/// Put in cache an arbitrary string using an arbitraty key (FragmentKey)
/// </summary>
procedure SetCacheFragment(const FragmentKey: string; const Value: string;
2016-09-25 16:17:37 +02:00
const ExpireInSeconds: UInt64);
/// <summary>
/// Get a previously cached string present at FragmentKey key
/// </summary>
function GetFromCacheFragment(const FragmentKey: string; out Value: string): Boolean;
procedure SetCache(const ExpireInSeconds: UInt64);
/// <summary>
/// Sets the cache key that will be used by the subsequent "GetFromCache" and "SetCache" methods
/// </summary>
procedure SetCacheKey(const Key: string);
/// <summary>
/// Returns true if the cache is available and automatically fills
/// the response using the cache contents
/// </summary>
function CacheAvailable: Boolean;
2016-09-25 16:17:37 +02:00
/// <summary>
/// If set to true the cache is enabled for this controller instance
/// </summary>
property CacheEnabled: Boolean read FCacheEnabled write SetCacheEnabled;
/// <summary>
/// If set to true the response will contains "X-CACHE-HIT=1" if the content has been
/// retrived from cache
/// </summary>
property ExposeCache: Boolean read FExposeCache write SetExposeCache;
end;
implementation
uses
System.SysUtils, System.Classes, Redis.Values;
2016-09-25 16:17:37 +02:00
function TMVCCacheController.CacheAvailable: Boolean;
2016-09-25 16:17:37 +02:00
var
lOutput: TRedisArray;
lStatusPieces: TArray<string>;
2016-09-25 16:17:37 +02:00
begin
if not FCacheEnabled then
Exit(False); // ignore and return false
CheckCacheKey;
2016-09-25 16:17:37 +02:00
// check if the redis key is present
lOutput := RedisClient.HMGET(FCurrentCacheKey, ['contenttype', 'headers', 'body', 'type',
'status']);
Result := lOutput.Items[0].HasValue and lOutput.Items[1].HasValue and
lOutput.Items[2].HasValue and lOutput.Items[3].HasValue and lOutput.Items[4].HasValue;
2016-09-25 16:17:37 +02:00
if Result then
begin
// if contents is cached, serve it from cache
Context.Response.CustomHeaders.Clear;
Context.Response.CustomHeaders.AddStrings(lOutput.Items[1].Value.Split([sLineBreak]));
2016-09-25 16:17:37 +02:00
if FExposeCache then
begin
Context.Response.CustomHeaders.AddPair('X-CACHE-HIT', '1');
end;
ContentType := lOutput.Items[0];
if lOutput.Items[3] = 'text' then
begin
ResponseStream.Append(lOutput.Items[2]);
RenderResponseStream;
end
else
begin
Render(TBytesStream.Create(TEncoding.ANSI.GetBytes(lOutput.Items[2])), True);
ContentCharset := '';
end;
lStatusPieces := string(lOutput.Items[4]).Split([':']);
ResponseStatusCode(StrToInt(lStatusPieces[0]), lStatusPieces[1]);
2016-09-25 16:17:37 +02:00
end;
end;
procedure TMVCCacheController.CheckCacheKey;
begin
if FCurrentCacheKey = '' then
raise EMVCCacheException.Create
('Cache key not set [Hint: Call "SetCacheKey" before "CacheAvailable" or "SetCache"]');
end;
function TMVCCacheController.GetFromCacheFragment(const FragmentKey: string;
out Value: string): Boolean;
var
lValue: TRedisString;
2016-09-25 16:17:37 +02:00
begin
if not FCacheEnabled then
Exit(False); // ignore and return false
// check if the redis key is present
lValue := RedisClient.GET(FragmentKey);
Result := lValue.HasValue;
if Result then
Value := lValue;
2016-09-25 16:17:37 +02:00
end;
procedure TMVCCacheController.OnAfterAction(Context: TWebContext; const AActionNAme: string);
begin
inherited;
end;
procedure TMVCCacheController.OnBeforeAction(Context: TWebContext; const AActionNAme: string;
var Handled: Boolean);
begin
inherited;
FCacheEnabled := True;
FExposeCache := True;
end;
function TMVCCacheController.RedisClient: IRedisClient;
var
lConnection: string;
lPieces: TArray<string>;
begin
if FRedis = nil then
begin
lConnection := self.Config['redis_connection_string'];
if lConnection.Trim.IsEmpty then
raise ERedisException.Create
('"redis_connection_string" config key is not defined (format is <host>:<port>)')
2016-09-25 16:17:37 +02:00
else
begin
lPieces := lConnection.Split([':']);
if Length(lPieces) <> 2 then
raise ERedisException.Create('Invalid "redis_connection_string" (format is <host>:<port>)');
FRedis := NewRedisClient(lPieces[0], StrToInt(lPieces[1]));
end;
end;
Result := FRedis;
end;
procedure TMVCCacheController.SetCache(const ExpireInSeconds: UInt64);
2016-09-25 16:17:37 +02:00
begin
if not FCacheEnabled then
Exit; // ignore
CheckCacheKey;
2016-09-25 16:17:37 +02:00
if FCacheEnabled then
RedisClient.MULTI(
procedure(const R: IRedisClient)
var
SS: TStringStream;
2016-09-25 16:17:37 +02:00
begin
if Context.Response.RawWebResponse.ContentStream = nil then
R.HMSET(FCurrentCacheKey, ['contenttype', 'headers', 'body', 'type', 'status'], [
ContentType,
Context.Response.CustomHeaders.Text,
Context.Response.RawWebResponse.Content,
'text', Context.Response.StatusCode.ToString + ':' + Context.Response.ReasonString])
else
begin
Context.Response.RawWebResponse.ContentStream.Position := 0;
SS := TStringStream.Create('', TEncoding.ANSI);
try
SS.CopyFrom(Context.Response.RawWebResponse.ContentStream, 0);
R.HMSET(FCurrentCacheKey, ['contenttype', 'headers', 'body', 'type', 'status'],
[ContentType,
Context.Response.CustomHeaders.Text,
SS.DataString,
'stream', Context.Response.StatusCode.ToString + ':' +
Context.Response.ReasonString]);
Context.Response.RawWebResponse.ContentStream.Position := 0;
finally
SS.Free;
end;
end;
R.EXPIRE(FCurrentCacheKey, ExpireInSeconds);
2016-09-25 16:17:37 +02:00
end);
end;
procedure TMVCCacheController.SetCacheEnabled(const Value: Boolean);
begin
FCacheEnabled := Value;
FCurrentCacheKey := '';
2016-09-25 16:17:37 +02:00
end;
procedure TMVCCacheController.SetCacheFragment(const FragmentKey: string; const Value: string;
2016-09-25 16:17:37 +02:00
const ExpireInSeconds: UInt64);
begin
if FCacheEnabled then
begin
RedisClient.&SET(FragmentKey, TEncoding.Default.GetBytes(Value), ExpireInSeconds);
2016-09-25 16:17:37 +02:00
end;
end;
procedure TMVCCacheController.SetCacheKey(const Key: string);
begin
FCurrentCacheKey := Key;
end;
2016-09-25 16:17:37 +02:00
procedure TMVCCacheController.SetExposeCache(const Value: Boolean);
begin
FExposeCache := Value;
end;
end.