delphimvcframework/sources/MVCFramework.Router.pas

417 lines
13 KiB
ObjectPascal
Raw Normal View History

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2017 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
// Collaborators on this file: Ezequiel Juliano M<>ller (ezequieljuliano@gmail.com)
//
// ***************************************************************************
//
// 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.
//
// *************************************************************************** }
2015-12-22 12:38:17 +01:00
2013-10-30 00:48:23 +01:00
unit MVCFramework.Router;
{$I dmvcframework.inc}
2013-10-30 00:48:23 +01:00
interface
uses
System.Rtti,
System.SysUtils,
System.Generics.Collections,
System.RegularExpressions,
System.AnsiStrings,
2013-10-30 00:48:23 +01:00
MVCFramework,
MVCFramework.Commons,
IdURI;
2013-10-30 00:48:23 +01:00
type
2013-10-30 00:48:23 +01:00
TMVCRouter = class
private
FRttiContext: TRttiContext;
FConfig: TMVCConfig;
FMethodToCall: TRttiMethod;
FControllerClazz: TMVCControllerClazz;
FControllerCreateAction: TMVCControllerCreateAction;
function GetAttribute<T: TCustomAttribute>(const AAttributes: TArray<TCustomAttribute>): T;
function GetFirstMediaType(const AContentType: string): string;
function IsHTTPContentTypeCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AContentType: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsHTTPAcceptCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AAccept: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsHTTPMethodCompatible(
const AMethodType: TMVCHTTPMethodType;
const AAttributes: TArray<TCustomAttribute>): Boolean;
function IsCompatiblePath(
const AMVCPath: string;
const APath: string;
var AParams: TMVCRequestParamsTable): Boolean;
2013-10-30 00:48:23 +01:00
protected
{ protected declarations }
public
class function StringMethodToHTTPMetod(const AValue: string): TMVCHTTPMethodType; static;
2013-10-30 00:48:23 +01:00
public
constructor Create(const AConfig: TMVCConfig);
destructor Destroy; override;
function ExecuteRouting(
const ARequestPathInfo: string;
const ARequestMethodType: TMVCHTTPMethodType;
const ARequestContentType: string;
const ARequestAccept: string;
const AControllers: TObjectList<TMVCControllerDelegate>;
const ADefaultContentType: string;
const ADefaultContentCharset: string;
var ARequestParams: TMVCRequestParamsTable;
out AResponseContentType: string;
out AResponseContentEncoding: string): Boolean;
property MethodToCall: TRttiMethod read FMethodToCall;
property ControllerClazz: TMVCControllerClazz read FControllerClazz;
property ControllerCreateAction: TMVCControllerCreateAction read FControllerCreateAction;
2013-10-30 00:48:23 +01:00
end;
implementation
{ TMVCRouter }
constructor TMVCRouter.Create(const AConfig: TMVCConfig);
2013-10-30 00:48:23 +01:00
begin
inherited Create;
FRttiContext := TRttiContext.Create;
FConfig := AConfig;
FMethodToCall := nil;
FControllerClazz := nil;
FControllerCreateAction := nil;
end;
destructor TMVCRouter.Destroy;
begin
FRttiContext.Free;
inherited Destroy;
2013-10-30 00:48:23 +01:00
end;
function TMVCRouter.ExecuteRouting(const ARequestPathInfo: string;
const ARequestMethodType: TMVCHTTPMethodType;
const ARequestContentType, ARequestAccept: string;
const AControllers: TObjectList<TMVCControllerDelegate>;
const ADefaultContentType: string;
const ADefaultContentCharset: string;
var ARequestParams: TMVCRequestParamsTable;
out AResponseContentType: string;
2014-06-27 15:30:39 +02:00
out AResponseContentEncoding: string): Boolean;
2013-10-30 00:48:23 +01:00
var
LRequestPathInfo: string;
LRequestAccept: string;
LRequestContentType: string;
LControllerMappedPath: string;
LControllerDelegate: TMVCControllerDelegate;
LAttributes: TArray<TCustomAttribute>;
LAtt: TCustomAttribute;
LRttiType: TRttiType;
LMethods: TArray<TRTTIMethod>;
LMethod: TRTTIMethod;
LFound: Boolean;
LMethodPath: string;
LProduceAttribute: MVCProducesAttribute;
2013-10-30 00:48:23 +01:00
begin
Result := False;
2013-10-30 00:48:23 +01:00
FMethodToCall := nil;
FControllerClazz := nil;
FControllerCreateAction := nil;
LRequestAccept := ARequestAccept;
LRequestContentType := ARequestContentType;
LRequestPathInfo := ARequestPathInfo;
if (Trim(LRequestPathInfo) = EmptyStr) then
LRequestPathInfo := '/'
2013-10-30 00:48:23 +01:00
else
begin
if LRequestPathInfo[1] <> '/' then
LRequestPathInfo := '/' + LRequestPathInfo;
2013-10-30 00:48:23 +01:00
end;
LRequestPathInfo := TIdURI.PathEncode(LRequestPathInfo);
2013-10-30 00:48:23 +01:00
2014-02-24 10:20:34 +01:00
{ ISAPI CHANGE THE REQUEST PATH INFO START }
if IsLibrary then
begin
if string(LRequestPathInfo).StartsWith(FConfig.Value[TMVCConfigKey.ISAPIPath]) then
LRequestPathInfo := LRequestPathInfo.Remove(0, FConfig.Value[TMVCConfigKey.ISAPIPath].Length);
if Length(LRequestPathInfo) = 0 then
LRequestPathInfo := '/';
2014-02-24 10:20:34 +01:00
end;
{ ISAPI CHANGE THE REQUEST PATH INFO END }
TMonitor.Enter(Lock);
2014-04-01 00:02:31 +02:00
try
LControllerMappedPath := EmptyStr;
for LControllerDelegate in AControllers do
2014-04-01 00:02:31 +02:00
begin
SetLength(LAttributes, 0);
LRttiType := FRttiContext.GetType(LControllerDelegate.Clazz.ClassInfo);
LAttributes := LRttiType.GetAttributes;
if (LAttributes = nil) then
2014-04-01 00:02:31 +02:00
Continue;
2013-10-30 00:48:23 +01:00
LFound := False;
for LAtt in LAttributes do
if LAtt is MVCPathAttribute then
2014-04-01 00:02:31 +02:00
begin
LFound := True;
LControllerMappedPath := MVCPathAttribute(LAtt).Path;
2014-04-01 00:02:31 +02:00
Break;
end;
2013-10-30 00:48:23 +01:00
if not LFound then
raise EMVCException.CreateFmt('Controller %s does not have MVCPath attribute', [LRttiType.Name]);
2013-10-30 00:48:23 +01:00
if (LControllerMappedPath = '/') then
LControllerMappedPath := '';
2013-10-30 00:48:23 +01:00
if (not LControllerMappedPath.IsEmpty) and (Pos(LControllerMappedPath, LRequestPathInfo) <> 1) then
2014-04-01 00:02:31 +02:00
Continue;
LMethods := LRttiType.GetMethods;
for LMethod in LMethods do
2013-10-30 00:48:23 +01:00
begin
LAttributes := LMethod.GetAttributes;
for LAtt in LAttributes do
if LAtt is MVCPathAttribute then
if IsHTTPMethodCompatible(ARequestMethodType, LAttributes) and
IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes) and
IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes) then
2013-10-30 00:48:23 +01:00
begin
LMethodPath := MVCPathAttribute(LAtt).Path;
if IsCompatiblePath(LControllerMappedPath + LMethodPath, LRequestPathInfo, ARequestParams) then
2013-11-08 23:10:25 +01:00
begin
FMethodToCall := LMethod;
FControllerClazz := LControllerDelegate.Clazz;
FControllerCreateAction := LControllerDelegate.CreateAction;
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
if Assigned(LProduceAttribute) then
2014-04-01 00:02:31 +02:00
begin
AResponseContentType := LProduceAttribute.Value;
AResponseContentEncoding := LProduceAttribute.Encoding;
2014-04-01 00:02:31 +02:00
end
else
begin
AResponseContentType := ADefaultContentType;
2014-04-10 13:56:23 +02:00
AResponseContentEncoding := ADefaultContentCharset;
2014-04-01 00:02:31 +02:00
end;
2014-10-26 20:48:52 +01:00
Exit(True);
end;
end;
end;
end;
2014-04-01 00:02:31 +02:00
finally
TMonitor.Exit(Lock);
2013-10-30 00:48:23 +01:00
end;
end;
function TMVCRouter.GetAttribute<T>(const AAttributes: TArray<TCustomAttribute>): T;
2013-10-30 00:48:23 +01:00
var
Att: TCustomAttribute;
2013-10-30 00:48:23 +01:00
begin
Result := nil;
for Att in AAttributes do
if Att is T then
Exit(T(Att));
2013-10-30 00:48:23 +01:00
end;
function TMVCRouter.GetFirstMediaType(const AContentType: string): string;
2014-06-27 15:30:39 +02:00
begin
Result := AContentType;
while Pos(',', Result) > 0 do
Result := Copy(Result, 1, Pos(',', Result) - 1);
while Pos(';', Result) > 0 do
2014-10-26 20:48:52 +01:00
Result := Copy(Result, 1, Pos(';', Result) - 1);
2014-06-27 15:30:39 +02:00
end;
function TMVCRouter.IsCompatiblePath(
const AMVCPath: string;
const APath: string;
2013-10-30 00:48:23 +01:00
var AParams: TMVCRequestParamsTable): Boolean;
2013-10-30 00:48:23 +01:00
function ToPattern(const V: string; Names: TList<string>): string;
var
S: string;
2013-10-30 00:48:23 +01:00
begin
Result := V;
for S in Names do
Result := StringReplace(Result, '($' + S + ')', '([ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>@\.\_\,%\w\d\x2D\x3A]*)', [rfReplaceAll]);
2013-10-30 00:48:23 +01:00
end;
function GetParametersNames(const V: string): TList<string>;
var
S: string;
Matches: TMatchCollection;
M: TMatch;
I: Integer;
2013-10-30 00:48:23 +01:00
begin
Result := TList<string>.Create;
S := '\(\$([A-Za-z0-9\_]+)\)';
Matches := TRegEx.Matches(V, S, [roIgnoreCase, roCompiled, roSingleLine]);
for M in Matches do
for I := 0 to M.Groups.Count - 1 do
2013-10-30 00:48:23 +01:00
begin
S := M.Groups[I].Value;
if (Length(S) > 0) and (S[1] <> '(') then
2013-10-30 00:48:23 +01:00
begin
Result.Add(S);
2013-10-30 00:48:23 +01:00
Break;
end;
end;
end;
var
RegEx: TRegEx;
Macth: TMatch;
Pattern: string;
I: Integer;
2013-10-30 00:48:23 +01:00
Names: TList<string>;
begin
Names := GetParametersNames(AMVCPath);
try
Pattern := ToPattern(AMVCPath, Names);
if (APath = AMVCPath) then
2014-10-26 20:48:52 +01:00
Exit(True)
2013-10-30 00:48:23 +01:00
else
begin
RegEx := TRegEx.Create('^' + Pattern + '$', [roIgnoreCase, roCompiled, roSingleLine]);
Macth := RegEx.match(APath);
Result := Macth.Success;
2013-10-30 00:48:23 +01:00
if Result then
for I := 1 to pred(Macth.Groups.Count) do
AParams.Add(Names[I - 1], TIdURI.URLDecode(Macth.Groups[I].Value));
2013-10-30 00:48:23 +01:00
end;
finally
Names.Free;
end;
end;
function TMVCRouter.IsHTTPAcceptCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AAccept: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
var
I: Integer;
2014-04-01 00:02:31 +02:00
MethodAccept: string;
FoundOneAttProduces: Boolean;
2014-04-01 00:02:31 +02:00
begin
Result := False;
2014-06-27 15:30:39 +02:00
FoundOneAttProduces := False;
for I := 0 to High(AAttributes) do
if AAttributes[I] is MVCProducesAttribute then
begin
FoundOneAttProduces := True;
MethodAccept := MVCProducesAttribute(AAttributes[I]).Value;
AAccept := GetFirstMediaType(AAccept);
2014-04-01 00:02:31 +02:00
Result := SameText(AAccept, MethodAccept, loInvariantLocale);
if Result then
Break;
end;
Result := (not FoundOneAttProduces) or (FoundOneAttProduces and Result);
2014-04-01 00:02:31 +02:00
end;
function TMVCRouter.IsHTTPContentTypeCompatible(
const ARequestMethodType: TMVCHTTPMethodType;
var AContentType: string;
const AAttributes: TArray<TCustomAttribute>): Boolean;
2014-04-01 00:02:31 +02:00
var
I: Integer;
2014-04-01 00:02:31 +02:00
MethodContentType: string;
FoundOneAttConsumes: Boolean;
begin
if ARequestMethodType in [httpGET, httpDELETE, httpHEAD, httpOPTIONS] then
2014-10-26 20:48:52 +01:00
Exit(True);
2014-03-24 17:37:08 +01:00
Result := False;
FoundOneAttConsumes := False;
for I := 0 to High(AAttributes) do
if AAttributes[I] is MVCConsumesAttribute then
begin
FoundOneAttConsumes := True;
MethodContentType := MVCConsumesAttribute(AAttributes[I]).Value;
AContentType := GetFirstMediaType(AContentType);
2014-04-01 00:02:31 +02:00
Result := SameText(AContentType, MethodContentType, loInvariantLocale);
if Result then
Break;
end;
Result := (not FoundOneAttConsumes) or (FoundOneAttConsumes and Result);
end;
function TMVCRouter.IsHTTPMethodCompatible(
const AMethodType: TMVCHTTPMethodType;
const AAttributes: TArray<TCustomAttribute>): Boolean;
2013-10-30 00:48:23 +01:00
var
I: Integer;
2013-10-30 00:48:23 +01:00
MustBeCompatible: Boolean;
CompatibleMethods: TMVCHTTPMethods;
begin
Result := False;
2013-10-30 00:48:23 +01:00
MustBeCompatible := False;
for I := 0 to High(AAttributes) do
if AAttributes[I] is MVCHTTPMethodAttribute then
2013-10-30 00:48:23 +01:00
begin
2014-10-26 20:48:52 +01:00
MustBeCompatible := True;
CompatibleMethods := MVCHTTPMethodAttribute(AAttributes[I]).MVCHTTPMethods;
2013-10-30 00:48:23 +01:00
Result := (AMethodType in CompatibleMethods);
end;
Result := (not MustBeCompatible) or (MustBeCompatible and Result);
2013-10-30 00:48:23 +01:00
end;
class function TMVCRouter.StringMethodToHTTPMetod(const AValue: string): TMVCHTTPMethodType;
2013-10-30 00:48:23 +01:00
begin
if AValue = 'GET' then
2013-10-30 00:48:23 +01:00
Exit(httpGET);
if AValue = 'POST' then
2013-10-30 00:48:23 +01:00
Exit(httpPOST);
if AValue = 'DELETE' then
2013-10-30 00:48:23 +01:00
Exit(httpDELETE);
if AValue = 'PUT' then
2013-10-30 00:48:23 +01:00
Exit(httpPUT);
if AValue = 'HEAD' then
2013-10-30 00:48:23 +01:00
Exit(httpHEAD);
if AValue = 'OPTIONS' then
2013-10-30 00:48:23 +01:00
Exit(httpOPTIONS);
if AValue = 'PATCH' then
2014-03-24 17:37:08 +01:00
Exit(httpPATCH);
if AValue = 'TRACE' then
2014-03-24 17:37:08 +01:00
Exit(httpTRACE);
raise EMVCException.CreateFmt('Unknown HTTP method [%s]', [AValue]);
2013-10-30 00:48:23 +01:00
end;
end.