2016-11-07 12:24:10 +01:00
|
|
|
// ***************************************************************************
|
|
|
|
//
|
|
|
|
// Delphi MVC Framework
|
|
|
|
//
|
2019-01-08 12:48:27 +01:00
|
|
|
// Copyright (c) 2010-2019 Daniele Teti and the DMVCFramework Team
|
2016-11-07 12:24:10 +01:00
|
|
|
//
|
|
|
|
// 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;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
{$I dmvcframework.inc}
|
|
|
|
|
2016-09-25 16:17:37 +02:00
|
|
|
interface
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
// NOTE: To use this controller you need DelphiRedisClient
|
2016-11-07 12:24:10 +01:00
|
|
|
// 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
|
2017-03-23 18:51:25 +01:00
|
|
|
System.SysUtils,
|
|
|
|
System.Classes,
|
|
|
|
MVCFramework,
|
|
|
|
MVCFramework.Commons,
|
|
|
|
Redis.Client,
|
|
|
|
Redis.Commons,
|
|
|
|
Redis.Values;
|
2016-09-25 16:17:37 +02:00
|
|
|
|
|
|
|
type
|
2016-11-07 12:24:10 +01:00
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
EMVCCacheException = class(EMVCException)
|
|
|
|
private
|
|
|
|
{ private declarations }
|
|
|
|
protected
|
|
|
|
{ protected declarations }
|
|
|
|
public
|
|
|
|
{ public declarations }
|
2016-11-07 12:24:10 +01:00
|
|
|
end;
|
|
|
|
|
2016-09-25 16:17:37 +02:00
|
|
|
TMVCCacheController = class(TMVCController)
|
|
|
|
private
|
|
|
|
FRedis: IRedisClient;
|
|
|
|
FCacheEnabled: Boolean;
|
|
|
|
FExposeCache: Boolean;
|
2016-11-07 12:24:10 +01:00
|
|
|
FCurrentCacheKey: string;
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure SetCacheEnabled(const AValue: Boolean);
|
|
|
|
procedure SetExposeCache(const AValue: Boolean);
|
2016-11-07 12:24:10 +01:00
|
|
|
procedure CheckCacheKey;
|
|
|
|
function RedisClient: IRedisClient;
|
2016-09-25 16:17:37 +02:00
|
|
|
protected
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure OnBeforeAction(AContext: TWebContext; const AActionNAme: string; var AHandled: Boolean); override;
|
|
|
|
procedure OnAfterAction(AContext: TWebContext; const AActionNAme: string); override;
|
2016-11-07 12:24:10 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Put in cache an arbitrary string using an arbitraty key (FragmentKey)
|
|
|
|
/// </summary>
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure SetCacheFragment(const AFragmentKey: string; const AValue: string; const AExpireInSeconds: UInt64);
|
2016-11-07 12:24:10 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Get a previously cached string present at FragmentKey key
|
|
|
|
/// </summary>
|
2017-03-23 18:51:25 +01:00
|
|
|
function GetFromCacheFragment(const AFragmentKey: string; out AValue: string): Boolean;
|
|
|
|
procedure SetCache(const AExpireInSeconds: UInt64);
|
2016-11-07 12:24:10 +01:00
|
|
|
/// <summary>
|
|
|
|
/// Sets the cache key that will be used by the subsequent "GetFromCache" and "SetCache" methods
|
|
|
|
/// </summary>
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure SetCacheKey(const AKey: string);
|
2016-11-07 12:24:10 +01:00
|
|
|
/// <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
|
|
|
|
|
2016-11-07 12:24:10 +01:00
|
|
|
function TMVCCacheController.CacheAvailable: Boolean;
|
2016-09-25 16:17:37 +02:00
|
|
|
var
|
2016-11-07 12:24:10 +01:00
|
|
|
lOutput: TRedisArray;
|
|
|
|
lStatusPieces: TArray<string>;
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
|
|
|
if not FCacheEnabled then
|
|
|
|
Exit(False); // ignore and return false
|
2017-03-23 18:51:25 +01:00
|
|
|
|
2016-11-07 12:24:10 +01:00
|
|
|
CheckCacheKey;
|
2016-09-25 16:17:37 +02:00
|
|
|
|
|
|
|
// check if the redis key is present
|
2017-03-23 18:51:25 +01:00
|
|
|
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;
|
2016-11-07 12:24:10 +01:00
|
|
|
Context.Response.CustomHeaders.AddStrings(lOutput.Items[1].Value.Split([sLineBreak]));
|
2017-03-23 18:51:25 +01:00
|
|
|
|
2016-09-25 16:17:37 +02:00
|
|
|
if FExposeCache then
|
|
|
|
Context.Response.CustomHeaders.AddPair('X-CACHE-HIT', '1');
|
2017-03-23 18:51:25 +01:00
|
|
|
|
2016-11-07 12:24:10 +01:00
|
|
|
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);
|
|
|
|
end;
|
|
|
|
lStatusPieces := string(lOutput.Items[4]).Split([':']);
|
2017-03-20 19:08:01 +01:00
|
|
|
ResponseStatus(StrToInt(lStatusPieces[0]), lStatusPieces[1]);
|
2016-09-25 16:17:37 +02:00
|
|
|
end;
|
|
|
|
end;
|
|
|
|
|
2016-11-07 12:24:10 +01:00
|
|
|
procedure TMVCCacheController.CheckCacheKey;
|
|
|
|
begin
|
2017-03-23 18:51:25 +01:00
|
|
|
if (FCurrentCacheKey = EmptyStr) then
|
|
|
|
raise EMVCCacheException.Create('Cache key not set [Hint: Call "SetCacheKey" before "CacheAvailable" or "SetCache"]');
|
2016-11-07 12:24:10 +01:00
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
function TMVCCacheController.GetFromCacheFragment(const AFragmentKey: string;
|
|
|
|
out AValue: string): Boolean;
|
2016-11-07 12:24:10 +01:00
|
|
|
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
|
2017-03-23 18:51:25 +01:00
|
|
|
lValue := RedisClient.GET(AFragmentKey);
|
2016-11-07 12:24:10 +01:00
|
|
|
Result := lValue.HasValue;
|
|
|
|
if Result then
|
2017-03-23 18:51:25 +01:00
|
|
|
AValue := lValue;
|
2016-09-25 16:17:37 +02:00
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.OnAfterAction(AContext: TWebContext; const AActionNAme: string);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.OnBeforeAction(AContext: TWebContext; const AActionNAme: string; var AHandled: Boolean);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
|
|
|
inherited;
|
|
|
|
FCacheEnabled := True;
|
|
|
|
FExposeCache := True;
|
|
|
|
end;
|
|
|
|
|
|
|
|
function TMVCCacheController.RedisClient: IRedisClient;
|
|
|
|
var
|
|
|
|
lConnection: string;
|
|
|
|
lPieces: TArray<string>;
|
|
|
|
begin
|
2017-03-23 18:51:25 +01:00
|
|
|
if (FRedis = nil) then
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
|
|
|
lConnection := self.Config['redis_connection_string'];
|
|
|
|
if lConnection.Trim.IsEmpty then
|
2017-03-23 18:51:25 +01:00
|
|
|
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;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.SetCache(const AExpireInSeconds: UInt64);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
2016-11-07 12:24:10 +01:00
|
|
|
if not FCacheEnabled then
|
|
|
|
Exit; // ignore
|
2017-03-23 18:51:25 +01:00
|
|
|
|
2016-11-07 12:24:10 +01:00
|
|
|
CheckCacheKey;
|
2017-03-23 18:51:25 +01:00
|
|
|
|
2016-09-25 16:17:37 +02:00
|
|
|
if FCacheEnabled then
|
|
|
|
RedisClient.MULTI(
|
2016-11-07 12:24:10 +01:00
|
|
|
procedure(const R: IRedisClient)
|
|
|
|
var
|
|
|
|
SS: TStringStream;
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
2016-11-07 12:24:10 +01:00
|
|
|
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;
|
2017-03-23 18:51:25 +01:00
|
|
|
R.EXPIRE(FCurrentCacheKey, AExpireInSeconds);
|
2016-09-25 16:17:37 +02:00
|
|
|
end);
|
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.SetCacheEnabled(const AValue: Boolean);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
2017-03-23 18:51:25 +01:00
|
|
|
FCacheEnabled := AValue;
|
2016-11-07 12:24:10 +01:00
|
|
|
FCurrentCacheKey := '';
|
2016-09-25 16:17:37 +02:00
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.SetCacheFragment(const AFragmentKey: string; const AValue: string;
|
|
|
|
const AExpireInSeconds: UInt64);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
|
|
|
if FCacheEnabled then
|
2017-03-23 18:51:25 +01:00
|
|
|
RedisClient.&SET(AFragmentKey, TEncoding.Default.GetBytes(AValue), AExpireInSeconds);
|
2016-09-25 16:17:37 +02:00
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.SetCacheKey(const AKey: string);
|
2016-11-07 12:24:10 +01:00
|
|
|
begin
|
2017-03-23 18:51:25 +01:00
|
|
|
FCurrentCacheKey := AKey;
|
2016-11-07 12:24:10 +01:00
|
|
|
end;
|
|
|
|
|
2017-03-23 18:51:25 +01:00
|
|
|
procedure TMVCCacheController.SetExposeCache(const AValue: Boolean);
|
2016-09-25 16:17:37 +02:00
|
|
|
begin
|
2017-03-23 18:51:25 +01:00
|
|
|
FExposeCache := AValue;
|
2016-09-25 16:17:37 +02:00
|
|
|
end;
|
|
|
|
|
|
|
|
end.
|