delphimvcframework/sources/MVCFramework.Router.pas

393 lines
13 KiB
ObjectPascal
Raw Normal View History

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2016 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.
//
// *************************************************************************** }
2015-12-22 12:38:17 +01:00
2013-10-30 00:48:23 +01:00
unit MVCFramework.Router;
interface
uses
Web.HTTPApp,
RTTIUtilsU,
MVCFramework.Commons,
System.RTTI,
MVCFramework,
System.Generics.Collections;
type
TMVCRouter = class
private
2014-04-01 00:02:31 +02:00
FCTX: TRttiContext;
2013-10-30 00:48:23 +01:00
FMethodToCall: TRTTIMethod;
FMVCControllerClass: TMVCControllerClass;
FMVCControllerDelegate: TMVCControllerDelegate;
2013-10-30 00:48:23 +01:00
FMVCConfig: TMVCConfig;
2015-04-01 17:01:23 +02:00
function IsHTTPContentTypeCompatible(AWebRequestMethodType: TMVCHTTPMethodType;
AContentType: String; AAttributes: TArray<TCustomAttribute>): Boolean;
function IsHTTPAcceptCompatible(AWebRequestMethodType: TMVCHTTPMethodType; AAccept: string;
AAttributes: TArray<TCustomAttribute>): Boolean;
2014-06-27 15:30:39 +02:00
function GetFirstMimeType(const AContentType: string): string;
2013-10-30 00:48:23 +01:00
protected
function IsHTTPMethodCompatible(AMethodType: TMVCHTTPMethodType;
AAttributes: TArray<TCustomAttribute>): Boolean; virtual;
2015-04-01 17:01:23 +02:00
function IsCompatiblePath(AMVCPath: string; APath: string; var AParams: TMVCRequestParamsTable)
: Boolean; virtual;
function GetAttribute<T: TCustomAttribute>(AAttributes: TArray<TCustomAttribute>): T;
2013-10-30 00:48:23 +01:00
public
2015-04-01 17:01:23 +02:00
class function StringMethodToHTTPMetod(const Value: AnsiString): TMVCHTTPMethodType;
2013-10-30 00:48:23 +01:00
constructor Create(AMVCConfig: TMVCConfig);
function ExecuteRouting(AWebRequestPathInfo: AnsiString;
2015-04-01 17:01:23 +02:00
AWebRequestMethodType: TMVCHTTPMethodType; AWebRequestContentType: AnsiString;
AWebRequestAccept: AnsiString; AMVCControllers: TObjectList<TMVCControllerRoutable>;
2015-04-01 17:01:23 +02:00
ADefaultContentType: string; ADefaultContentCharset: string;
var AMVCRequestParams: TMVCRequestParamsTable; out AResponseContentType: string;
out AResponseContentEncoding: string): Boolean; overload;
2013-10-30 00:48:23 +01:00
property MethodToCall: TRTTIMethod read FMethodToCall;
property MVCControllerClass: TMVCControllerClass read FMVCControllerClass;
property MVCControllerDelegate: TMVCControllerDelegate read FMVCControllerDelegate;
2013-10-30 00:48:23 +01:00
end;
implementation
uses
2015-04-01 17:01:23 +02:00
System.AnsiStrings,
2013-10-30 00:48:23 +01:00
System.StrUtils,
System.RegularExpressions,
System.SysUtils,
idURI;
{ TMVCRouter }
constructor TMVCRouter.Create(AMVCConfig: TMVCConfig);
begin
inherited Create;
FMVCConfig := AMVCConfig;
end;
function TMVCRouter.ExecuteRouting(AWebRequestPathInfo: AnsiString;
2014-10-26 20:48:52 +01:00
AWebRequestMethodType: TMVCHTTPMethodType; AWebRequestContentType: AnsiString;
AWebRequestAccept: AnsiString; AMVCControllers: TObjectList<TMVCControllerRoutable>;
2014-06-27 15:30:39 +02:00
ADefaultContentType, ADefaultContentCharset: string;
2015-04-01 17:01:23 +02:00
var AMVCRequestParams: 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
controllerRoutable: TMVCControllerRoutable;
2013-10-30 00:48:23 +01:00
_type: TRttiType;
_methods: TArray<TRTTIMethod>;
_method: TRTTIMethod;
_attribute: TCustomAttribute;
2014-04-01 00:02:31 +02:00
_attributes: TArray<TCustomAttribute>;
2013-10-30 00:48:23 +01:00
i: Integer;
ControllerMappedPath: string;
MethodPathAttribute: string;
MVCProduceAttr: MVCProducesAttribute;
2014-10-26 20:48:52 +01:00
Found: Boolean;
2015-04-01 17:01:23 +02:00
LWebRequestPathInfo: string;
LWebRequestAccept: String;
2013-10-30 00:48:23 +01:00
begin
FMethodToCall := nil;
FMVCControllerClass := nil;
FMVCControllerDelegate := nil;
2015-04-01 17:01:23 +02:00
LWebRequestAccept := String(AWebRequestAccept);
2013-10-30 00:48:23 +01:00
2015-04-01 17:01:23 +02:00
LWebRequestPathInfo := string(AWebRequestPathInfo);
if Trim(LWebRequestPathInfo) = EmptyStr then
LWebRequestPathInfo := '/'
2013-10-30 00:48:23 +01:00
else
begin
2015-04-01 17:01:23 +02:00
if LWebRequestPathInfo[1] <> '/' then
LWebRequestPathInfo := '/' + LWebRequestPathInfo;
2013-10-30 00:48:23 +01:00
end;
// FIX https://github.com/danieleteti/delphimvcframework/issues/17
LWebRequestPathInfo := TIdURI.PathEncode(LWebRequestPathInfo);
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
2015-04-01 17:01:23 +02:00
if string(LWebRequestPathInfo).StartsWith(FMVCConfig.Value[TMVCConfigKey.ISAPIPath]) then
LWebRequestPathInfo := LWebRequestPathInfo.Remove(0,
FMVCConfig.Value[TMVCConfigKey.ISAPIPath].Length);
if Length(LWebRequestPathInfo) = 0 then
LWebRequestPathInfo := '/';
2014-02-24 10:20:34 +01:00
end;
{ ISAPI CHANGE THE REQUEST PATH INFO END }
2014-04-01 00:02:31 +02:00
TMonitor.Enter(Lock); // start of lock
try
2013-10-30 00:48:23 +01:00
2014-04-01 00:02:31 +02:00
Result := False;
ControllerMappedPath := '';
for controllerRoutable in AMVCControllers do
2014-04-01 00:02:31 +02:00
begin
SetLength(_attributes, 0);
_type := FCTX.GetType(controllerRoutable.&Class.ClassInfo);
2014-04-01 00:02:31 +02:00
_attributes := _type.GetAttributes;
2015-04-01 17:01:23 +02:00
if _attributes = nil then
2014-04-01 00:02:31 +02:00
Continue;
2013-10-30 00:48:23 +01:00
2014-10-26 20:48:52 +01:00
Found := False;
2014-04-01 00:02:31 +02:00
for _attribute in _attributes do
if _attribute is MVCPathAttribute then
begin
2014-10-26 20:48:52 +01:00
Found := True;
2014-04-01 00:02:31 +02:00
ControllerMappedPath := MVCPathAttribute(_attribute).Path;
Break;
end;
2013-10-30 00:48:23 +01:00
2014-10-26 20:48:52 +01:00
if not Found then
2015-04-01 17:01:23 +02:00
raise EMVCException.Create('Controller ' + _type.Name + ' doesn''t have MVCPath attribute');
2013-10-30 00:48:23 +01:00
2014-04-01 00:02:31 +02:00
if ControllerMappedPath = '/' then // WE WANT TO AVOID '//' AS MVCPATH
ControllerMappedPath := '';
2013-10-30 00:48:23 +01:00
2015-04-01 17:01:23 +02:00
if (not ControllerMappedPath.IsEmpty) and (Pos(ControllerMappedPath, LWebRequestPathInfo) <> 1)
then
2014-04-01 00:02:31 +02:00
Continue;
_methods := _type.GetMethods;
for _method in _methods do
2013-10-30 00:48:23 +01:00
begin
2014-04-01 00:02:31 +02:00
_attributes := _method.GetAttributes;
for i := 0 to Length(_attributes) - 1 do
2013-10-30 00:48:23 +01:00
begin
2014-04-01 00:02:31 +02:00
_attribute := _attributes[i];
if _attribute is MVCPathAttribute then
2013-10-30 00:48:23 +01:00
begin
2014-04-01 00:02:31 +02:00
if IsHTTPMethodCompatible(AWebRequestMethodType, _attributes) and
2015-04-01 17:01:23 +02:00
IsHTTPContentTypeCompatible(AWebRequestMethodType, String(AWebRequestContentType),
_attributes) and IsHTTPAcceptCompatible(AWebRequestMethodType, LWebRequestAccept,
2014-10-26 20:48:52 +01:00
_attributes) then
2013-10-30 00:48:23 +01:00
begin
2014-04-01 00:02:31 +02:00
MethodPathAttribute := MVCPathAttribute(_attribute).Path;
2015-04-01 17:01:23 +02:00
if IsCompatiblePath(ControllerMappedPath + MethodPathAttribute, LWebRequestPathInfo,
AMVCRequestParams) then
2013-11-08 23:10:25 +01:00
begin
2014-04-01 00:02:31 +02:00
FMethodToCall := _method;
FMVCControllerClass := controllerRoutable.&Class;
FMVCControllerDelegate := controllerRoutable.Delegate;
2014-04-01 00:02:31 +02:00
// getting the default contenttype using MVCProduceAttribute
2015-04-01 17:01:23 +02:00
MVCProduceAttr := GetAttribute<MVCProducesAttribute>(_attributes);
if MVCProduceAttr <> nil then
2014-04-01 00:02:31 +02:00
begin
AResponseContentType := MVCProduceAttr.Value;
AResponseContentEncoding := MVCProduceAttr.ProduceEncoding;
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);
2014-04-01 00:02:31 +02:00
end; // if is compatible path
end; // if is compatible method, contenttype and accept
end; // if attribute is mvcpath
end; // for each attributes on method
end; // for each methods
end; // for each controllers
finally
TMonitor.Exit(Lock);
2013-10-30 00:48:23 +01:00
end;
end;
function TMVCRouter.GetAttribute<T>(AAttributes: TArray<TCustomAttribute>): T;
var
a: TCustomAttribute;
begin
Result := nil;
for a in AAttributes do
if a is T then
Exit(T(a));
end;
2014-10-26 20:48:52 +01:00
function TMVCRouter.GetFirstMimeType(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);
// application/json;charset=UTF-8 {daniele}
2014-06-27 15:30:39 +02:00
end;
2013-10-30 00:48:23 +01:00
function TMVCRouter.IsCompatiblePath(AMVCPath: string; APath: string;
var AParams: TMVCRequestParamsTable): Boolean;
function ToPattern(const V: string; Names: TList<string>): string;
var
s: string;
begin
Result := V;
for s in Names do
Result := StringReplace(Result, '($' + s + ')', '([ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>@\.\_\,%\w\d\x2D\x3A]*)',
2015-04-01 17:01:23 +02:00
[rfReplaceAll]);
2013-10-30 00:48:23 +01:00
end;
function GetParametersNames(const V: string): TList<string>;
var
s: string;
matches: TMatchCollection;
match: TMatch;
i: Integer;
begin
Result := TList<string>.Create;
s := '\(\$([A-Za-z0-9]+)\)';
matches := TRegEx.matches(V, s, [roIgnoreCase, roCompiled, roSingleLine]);
for match in matches do
for i := 0 to match.Groups.Count - 1 do
begin
s := match.Groups[i].Value;
if (Length(s) > 0) and (s[1] <> '(') then
begin
Result.Add(s);
Break;
end;
end;
end;
var
re: TRegEx;
m: TMatch;
pattern: string;
i: Integer;
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
2015-04-01 17:01:23 +02:00
re := TRegEx.Create('^' + pattern + '$', [roIgnoreCase, roCompiled, roSingleLine]);
2013-10-30 00:48:23 +01:00
m := re.match(APath);
Result := m.Success;
if Result then
for i := 1 to pred(m.Groups.Count) do
AParams.Add(Names[i - 1], TIdURI.URLDecode(m.Groups[i].Value));
end;
finally
Names.Free;
end;
end;
2015-04-01 17:01:23 +02:00
function TMVCRouter.IsHTTPAcceptCompatible(AWebRequestMethodType: TMVCHTTPMethodType;
AAccept: string; AAttributes: TArray<TCustomAttribute>): Boolean;
var
i: Integer;
2014-04-01 00:02:31 +02:00
MethodAccept: string;
FoundOneAttribProduces: Boolean;
begin
Result := False;
FoundOneAttribProduces := False;
for i := 0 to high(AAttributes) do
begin
if AAttributes[i] is MVCProducesAttribute then
begin
2014-10-26 20:48:52 +01:00
FoundOneAttribProduces := True;
2014-04-01 00:02:31 +02:00
MethodAccept := MVCProducesAttribute(AAttributes[i]).Value;
2014-06-27 15:30:39 +02:00
AAccept := GetFirstMimeType(AAccept);
2014-06-30 17:00:46 +02:00
// while Pos(',', AAccept) > 0 do
// AAccept := Copy(AAccept, 1, Pos(',', AAccept) - 1);
2014-06-27 15:30:39 +02:00
2014-04-01 00:02:31 +02:00
Result := SameText(AAccept, MethodAccept, loInvariantLocale);
if Result then
Break;
end;
end;
Result := (not FoundOneAttribProduces) or (FoundOneAttribProduces and Result);
end;
2015-04-01 17:01:23 +02:00
function TMVCRouter.IsHTTPContentTypeCompatible(AWebRequestMethodType: TMVCHTTPMethodType;
AContentType: String; AAttributes: TArray<TCustomAttribute>): Boolean;
2014-04-01 00:02:31 +02:00
var
i: Integer;
MethodContentType: string;
FoundOneAttribConsumes: Boolean;
begin
2014-03-24 17:37:08 +01:00
// content type is applicable only for PUT, POST and PATCH
if AWebRequestMethodType 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;
FoundOneAttribConsumes := False;
for i := 0 to high(AAttributes) do
begin
if AAttributes[i] is MVCConsumesAttribute then
begin
2014-10-26 20:48:52 +01:00
FoundOneAttribConsumes := True;
2014-04-01 00:02:31 +02:00
MethodContentType := MVCConsumesAttribute(AAttributes[i]).Value;
2014-06-27 15:30:39 +02:00
AContentType := GetFirstMimeType(AContentType);
2014-04-01 00:02:31 +02:00
Result := SameText(AContentType, MethodContentType, loInvariantLocale);
if Result then
Break;
end;
end;
Result := (not FoundOneAttribConsumes) or (FoundOneAttribConsumes and Result);
end;
2013-10-30 00:48:23 +01:00
function TMVCRouter.IsHTTPMethodCompatible(AMethodType: TMVCHTTPMethodType;
AAttributes: TArray<TCustomAttribute>): Boolean;
var
i: Integer;
MustBeCompatible: Boolean;
CompatibleMethods: TMVCHTTPMethods;
begin
Result := False;
// if there aren't MVCHTTPMethod attributes defined, the action is compatibile with all methods
2013-10-30 00:48:23 +01:00
MustBeCompatible := False;
for i := 0 to high(AAttributes) do
begin
if AAttributes[i] is MVCHTTPMethodAttribute then
begin
2014-10-26 20:48:52 +01:00
MustBeCompatible := True;
2015-04-01 17:01:23 +02:00
CompatibleMethods := MVCHTTPMethodAttribute(AAttributes[i]).MVCHTTPMethods;
2013-10-30 00:48:23 +01:00
Result := (AMethodType in CompatibleMethods);
end;
end;
Result := (not MustBeCompatible) or (MustBeCompatible and Result);
2013-10-30 00:48:23 +01:00
end;
2015-04-01 17:01:23 +02:00
class function TMVCRouter.StringMethodToHTTPMetod(const Value: AnsiString): TMVCHTTPMethodType;
2013-10-30 00:48:23 +01:00
begin
if Value = 'GET' then
Exit(httpGET);
if Value = 'POST' then
Exit(httpPOST);
if Value = 'DELETE' then
Exit(httpDELETE);
if Value = 'PUT' then
Exit(httpPUT);
if Value = 'HEAD' then
Exit(httpHEAD);
if Value = 'OPTIONS' then
Exit(httpOPTIONS);
2014-03-24 17:37:08 +01:00
if Value = 'PATCH' then
Exit(httpPATCH);
if Value = 'TRACE' then
Exit(httpTRACE);
2013-10-30 00:48:23 +01:00
raise EMVCException.CreateFmt('Unknown HTTP method [%s]', [Value]);
end;
end.