Browse Source

* Improved router implemented

git-svn-id: trunk@35292 -
michael 8 years ago
parent
commit
a33812f88c

+ 10 - 0
.gitattributes

@@ -3145,6 +3145,11 @@ packages/fcl-web/examples/jsonrpc/extdirect/extdemo.lpr svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/extdirect.in svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/wmext.lfm svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/wmext.pp svneol=native#text/plain
+packages/fcl-web/examples/routing/README svneol=native#text/plain
+packages/fcl-web/examples/routing/demorouting.lpi svneol=native#text/plain
+packages/fcl-web/examples/routing/demorouting.lpr svneol=native#text/plain
+packages/fcl-web/examples/routing/routes.pp svneol=native#text/plain
+packages/fcl-web/examples/routing/sample.ini svneol=native#text/plain
 packages/fcl-web/examples/session/sessiondemo.lpi svneol=native#text/plain
 packages/fcl-web/examples/session/sessiondemo.lpr svneol=native#text/plain
 packages/fcl-web/examples/session/wmsession.lfm svneol=native#text/plain
@@ -3246,9 +3251,11 @@ packages/fcl-web/src/base/fpwebclient.pp svneol=native#text/plain
 packages/fcl-web/src/base/fpwebfile.pp svneol=native#text/plain
 packages/fcl-web/src/base/httpdefs.pp svneol=native#text/plain
 packages/fcl-web/src/base/httpprotocol.pp svneol=native#text/plain
+packages/fcl-web/src/base/httproute.pp svneol=native#text/plain
 packages/fcl-web/src/base/iniwebsession.pp svneol=native#text/plain
 packages/fcl-web/src/base/restbase.pp svneol=native#text/plain
 packages/fcl-web/src/base/restcodegen.pp svneol=native#text/plain
+packages/fcl-web/src/base/tcwebmodule.pp svneol=native#text/plain
 packages/fcl-web/src/base/webpage.pp svneol=native#text/plain
 packages/fcl-web/src/base/websession.pp svneol=native#text/plain
 packages/fcl-web/src/base/webutil.pp svneol=native#text/plain
@@ -3275,8 +3282,11 @@ packages/fcl-web/tests/cgigateway.lpi svneol=native#text/plain
 packages/fcl-web/tests/cgigateway.pp svneol=native#text/plain
 packages/fcl-web/tests/fpcunithpack.lpi svneol=native#text/plain
 packages/fcl-web/tests/fpcunithpack.lpr svneol=native#text/plain
+packages/fcl-web/tests/tchttproute.pp svneol=native#text/plain
 packages/fcl-web/tests/testcgiapp.lpi svneol=native#text/plain
 packages/fcl-web/tests/testcgiapp.pp svneol=native#text/plain
+packages/fcl-web/tests/testfpweb.lpi svneol=native#text/plain
+packages/fcl-web/tests/testfpweb.lpr svneol=native#text/plain
 packages/fcl-web/tests/uhpacktest1.pas svneol=native#text/plain
 packages/fcl-xml/Makefile svneol=native#text/plain
 packages/fcl-xml/Makefile.fpc svneol=native#text/plain

+ 3 - 11
packages/fcl-web/examples/echo/cgi/echo.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -31,7 +31,6 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="\usr\bin\xterm -T 'Lazarus Run Output' -e $(LazarusDir)\tools\runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <RequiredPackages Count="1">
@@ -43,29 +42,22 @@
       <Unit0>
         <Filename Value="echo.lpr"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="echo"/>
       </Unit0>
       <Unit1>
         <Filename Value="..\webmodule\wmecho.pas"/>
         <IsPartOfProject Value="True"/>
         <ComponentName Value="EchoModule"/>
         <ResourceBaseClass Value="DataModule"/>
-        <UnitName Value="wmecho"/>
       </Unit1>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <PathDelim Value="\"/>
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="..\webmodule"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

BIN
packages/fcl-web/examples/echo/cgi/echo.res


+ 22 - 172
packages/fcl-web/examples/httpapp/testhttp.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -6,13 +6,10 @@
       <Flags>
         <MainUnitHasCreateFormStatements Value="False"/>
         <MainUnitHasTitleStatement Value="False"/>
-        <UseDefaultCompilerOptions Value="True"/>
       </Flags>
       <MainUnit Value="0"/>
       <ResourceType Value="res"/>
       <UseXPManifest Value="True"/>
-      <Icon Value="0"/>
-      <ActiveWindowIndexAtStart Value="0"/>
     </General>
     <i18n>
       <EnableI18N LFM="False"/>
@@ -31,269 +28,122 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="12">
       <Unit0>
         <Filename Value="testhttp.pp"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="testhttp"/>
-        <EditorIndex Value="0"/>
-        <WindowIndex Value="0"/>
-        <TopLine Value="1"/>
-        <CursorPos X="16" Y="5"/>
+        <IsVisibleTab Value="True"/>
+        <CursorPos X="41" Y="6"/>
         <UsageCount Value="20"/>
         <Loaded Value="True"/>
       </Unit0>
       <Unit1>
         <Filename Value="fpwebfile.pp"/>
-        <UnitName Value="fpwebfile"/>
-        <EditorIndex Value="10"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="8"/>
         <CursorPos X="22" Y="14"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit1>
       <Unit2>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttp.pp"/>
-        <UnitName Value="fphttp"/>
-        <EditorIndex Value="11"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="188"/>
-        <CursorPos X="1" Y="196"/>
+        <CursorPos Y="196"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit2>
       <Unit3>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpapp.pp"/>
-        <UnitName Value="fphttpapp"/>
-        <EditorIndex Value="6"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="14"/>
         <CursorPos X="31" Y="20"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit3>
       <Unit4>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/custhttpapp.pp"/>
-        <UnitName Value="custhttpapp"/>
-        <EditorIndex Value="7"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="35"/>
         <CursorPos X="30" Y="39"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit4>
       <Unit5>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpserver.pp"/>
-        <UnitName Value="fphttpserver"/>
-        <EditorIndex Value="8"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="18"/>
         <CursorPos X="24" Y="39"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit5>
       <Unit6>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
         <UnitName Value="HTTPDefs"/>
-        <EditorIndex Value="9"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="1005"/>
         <CursorPos X="42" Y="1038"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit6>
       <Unit7>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <UnitName Value="reglazwebextra"/>
-        <IsVisibleTab Value="True"/>
-        <EditorIndex Value="1"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="218"/>
         <CursorPos X="29" Y="235"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit7>
       <Unit8>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/weblazideintf.pp"/>
         <UnitName Value="WebLazIDEIntf"/>
-        <EditorIndex Value="5"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="623"/>
-        <CursorPos X="1" Y="642"/>
+        <CursorPos Y="642"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit8>
       <Unit9>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
         <ComponentName Value="NewHTTPApplicationForm"/>
         <ResourceBaseClass Value="Form"/>
-        <UnitName Value="frmnewhttpapp"/>
-        <EditorIndex Value="3"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="25"/>
         <CursorPos X="34" Y="104"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
         <LoadedDesigner Value="True"/>
       </Unit9>
       <Unit10>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/fpwebstrconsts.pas"/>
         <UnitName Value="fpWebStrConsts"/>
-        <EditorIndex Value="4"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="92"/>
         <CursorPos X="22" Y="121"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit10>
       <Unit11>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <UnitName Value="fpwebdata"/>
-        <EditorIndex Value="2"/>
-        <WindowIndex Value="0"/>
-        <TopLine Value="1"/>
+        <EditorIndex Value="-1"/>
         <CursorPos X="14" Y="15"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit11>
     </Units>
-    <JumpHistory Count="30" HistoryIndex="29">
+    <JumpHistory Count="3" HistoryIndex="2">
       <Position1>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="23" Column="3" TopLine="1"/>
+        <Caret Line="23" Column="3"/>
       </Position1>
       <Position2>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="8" Column="44" TopLine="1"/>
+        <Caret Line="8" Column="44"/>
       </Position2>
       <Position3>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="1" Column="1" TopLine="1"/>
-      </Position3>
-      <Position4>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="29" Column="44" TopLine="1"/>
-      </Position4>
-      <Position5>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="63" Column="22" TopLine="48"/>
-      </Position5>
-      <Position6>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/custhttpapp.pp"/>
-        <Caret Line="39" Column="30" TopLine="35"/>
-      </Position6>
-      <Position7>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpserver.pp"/>
-        <Caret Line="35" Column="38" TopLine="18"/>
-      </Position7>
-      <Position8>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="623" Column="33" TopLine="613"/>
-      </Position8>
-      <Position9>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="1012" Column="7" TopLine="1009"/>
-      </Position9>
-      <Position10>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="281" Column="71" TopLine="263"/>
-      </Position10>
-      <Position11>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="1014" Column="21" TopLine="1010"/>
-      </Position11>
-      <Position12>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="660" Column="21" TopLine="627"/>
-      </Position12>
-      <Position13>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="77" Column="60" TopLine="48"/>
-      </Position13>
-      <Position14>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="86" Column="29" TopLine="58"/>
-      </Position14>
-      <Position15>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/weblazideintf.pp"/>
-        <Caret Line="549" Column="14" TopLine="547"/>
-      </Position15>
-      <Position16>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="54" Column="17" TopLine="36"/>
-      </Position16>
-      <Position17>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="41" Column="20" TopLine="41"/>
-      </Position17>
-      <Position18>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="27" Column="29" TopLine="1"/>
-      </Position18>
-      <Position19>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="93" Column="28" TopLine="76"/>
-      </Position19>
-      <Position20>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="39" Column="43" TopLine="21"/>
-      </Position20>
-      <Position21>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="97" Column="29" TopLine="68"/>
-      </Position21>
-      <Position22>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <Caret Line="1" Column="1" TopLine="1"/>
-      </Position22>
-      <Position23>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <Caret Line="15" Column="14" TopLine="1"/>
-      </Position23>
-      <Position24>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="203" Column="23" TopLine="184"/>
-      </Position24>
-      <Position25>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="66" Column="10" TopLine="59"/>
-      </Position25>
-      <Position26>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="71" Column="24" TopLine="39"/>
-      </Position26>
-      <Position27>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="75" Column="18" TopLine="58"/>
-      </Position27>
-      <Position28>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="111" Column="66" TopLine="95"/>
-      </Position28>
-      <Position29>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="186" Column="15" TopLine="160"/>
-      </Position29>
-      <Position30>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="200" Column="3" TopLine="184"/>
-      </Position30>
+        <Caret Line="29" Column="44"/>
+      </Position3>
     </JumpHistory>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <Parsing>
       <SyntaxOptions>
         <UseAnsiStrings Value="False"/>
       </SyntaxOptions>
     </Parsing>
-    <Other>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 1 - 1
packages/fcl-web/examples/httpapp/testhttp.pp

@@ -3,7 +3,7 @@
 program testhttp;
 
 uses
-  SysUtils, fphttpapp, fpwebfile;
+  SysUtils, fphttpapp, fpwebfile, wmecho;
 
 Procedure Usage;
 

+ 1 - 9
packages/fcl-web/examples/httpclient/httpget.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -32,14 +32,12 @@
       <local>
         <FormatVersion Value="1"/>
         <CommandLineParams Value="http://home/~michael/redirect.cgi out"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="1">
       <Unit0>
         <Filename Value="httpget.pas"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="httpget"/>
       </Unit0>
     </Units>
   </ProjectOptions>
@@ -48,12 +46,6 @@
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 2 - 11
packages/fcl-web/examples/httpserver/simplehttpserver.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -13,7 +13,6 @@
       <Title Value="Simple HTTP server demo"/>
       <ResourceType Value="res"/>
       <UseXPManifest Value="True"/>
-      <Icon Value="0"/>
     </General>
     <i18n>
       <EnableI18N LFM="False"/>
@@ -32,28 +31,20 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="1">
       <Unit0>
         <Filename Value="simplehttpserver.pas"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="simplehttpserver"/>
       </Unit0>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 22 - 0
packages/fcl-web/examples/routing/README

@@ -0,0 +1,22 @@
+This demo demonstrates the routing mechanism of fpWeb.
+
+It can be run as a CGI or as a HTTP standalone server program.
+
+In order to get a correct set of routes in the demo, demorouting.ini file
+must be configured correctly and placed next to the binary.
+
+There is a different section for each type of binary: (CGI or Standalone)
+
+Each section needs at least the BaseURL key, this is the URL where the
+application can be reached.
+
+Example:
+
+[CGI]
+; Assuming the demo is in cgi-bin
+BaseURL=http://localhost/cgi-bin/demorouting.cgi
+
+[Standalone]
+Port=8080
+; Optional, the following is the default.
+;BaseURL=http://localhost:8080/

+ 69 - 0
packages/fcl-web/examples/routing/demorouting.lpi

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="9"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="demorouting"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <VersionInfo>
+      <StringTable ProductVersion=""/>
+    </VersionInfo>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <FormatVersion Value="1"/>
+      </local>
+    </RunParams>
+    <Units Count="3">
+      <Unit0>
+        <Filename Value="demorouting.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="routes.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="../../src/base/httproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit2>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="demorouting"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../../src/base"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 34 - 0
packages/fcl-web/examples/routing/demorouting.lpr

@@ -0,0 +1,34 @@
+program demorouting;
+
+{$DEFINE STANDALONE}
+
+uses
+  sysutils,
+  routes,
+{$IFDEF STANDALONE}
+  fphttpapp,
+{$ENDIF}
+{$IFDEF CGI}
+  fpcgi,
+{$ENDIF}
+  inifiles;
+
+
+begin
+  With TInifile.Create(ChangeFileExt(ParamStr(0),'.ini')) do
+    try
+      {$IFDEF CGI}
+      BaseURL:=ReadString('CGI','BaseURL','');
+      {$ENDIF CGI}
+      {$IFDEF STANDALONE}
+      Application.Port:=ReadInteger('Standalone','Port',8080);
+      BaseURL:=ReadString('Standalone','BaseURL','http://localhost:'+IntToStr(Application.Port));
+      {$ENDIF STANDALONE}
+    finally
+      Free;
+    end;
+  RegisterRoutes;
+  Application.Initialize;
+  Application.Run;
+end.
+

+ 203 - 0
packages/fcl-web/examples/routing/routes.pp

@@ -0,0 +1,203 @@
+unit routes;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ sysutils, classes, httpdefs, httproute;
+
+Var
+  BaseURL : String;
+
+Procedure RegisterRoutes;
+
+implementation
+
+uses webutil, fphttp;
+
+Type
+
+  { TMyModule }
+
+  TMyModule = Class(TCustomHTTPModule)
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+  
+  { TMyIntf }
+
+  TMyIntf = Class(TObject,IRouteInterface)
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+  end;
+
+  { TMyHandler }
+
+  TMyHandler = Class(TRouteObject)
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);override;
+  end;
+
+Var
+  C1,C2 : TComponent;
+  MyIntf : TMyIntf;
+
+
+Procedure DumpRoutes(L : TStrings; AURL : String);
+
+  Function DefaultReps(S : String) : string;
+
+  begin
+    Result:=StringReplace(S,'*path','somepath',[]);
+    Result:=StringReplace(Result,':param1','theparam1',[]);
+    Result:=StringReplace(Result,':param2','theparam2',[]);
+    Result:=StringReplace(Result,':param','theparam',[]);
+    If (Result<>'') and (Result[1]='/') then
+      Delete(Result,1,1);
+  end;
+
+Var
+  I : Integer;
+  P : String;
+
+begin
+  THTTPRouter.SanitizeRoute(AURL);
+  L.Add('<A NAME="routes"/>');
+  L.Add('<H1>Try these routes:</H1>');
+  For I:=0 to HTTPRouter.RouteCount-1 do
+    begin
+    P:=DefaultReps(HTTPRouter[i].URLPattern);
+    L.Add('<A HREF="'+BaseURL+'/'+P+'">'+P+'</a><br>');
+    end;
+end;
+
+Procedure RequestToResponse(ATitle : String; ARequest : TRequest; AResponse : TResponse; RouteParams : Array of String);
+
+Var
+  L : TStrings;
+  S : String;
+
+begin
+  L:=TStringList.Create;
+  try
+    L.Add('<HTML>');
+    L.Add('<HEAD>');
+    L.Add('<TITLE>'+ATitle+'</TITLE>');
+    L.Add('</HEAD>');
+    L.Add('<BODY>');
+    L.Add('<H1>'+ATitle+'</H1>');
+    L.Add('<A HREF="#routes">Jump to routes overview</A>');
+    if (Length(RouteParams)>0) then
+      begin
+      L.Add('<H2>Routing parameters:</H2>');
+      L.Add('<table>');
+      L.Add('<tr><th>Param</th><th>Value</th></tr>');
+      for S in RouteParams do
+        L.Add('<tr><td>'+S+'</th><th>'+ARequest.RouteParams[S]+'</th></tr>');
+      L.Add('</table>');
+      end;
+    DumpRequest(ARequest,L,False);
+    DumpRoutes(L,ARequest.URL);
+    L.Add('</BODY>');
+    L.Add('</HTML>');
+    AResponse.Content:=L.Text;
+    AResponse.SendResponse;
+  finally
+    L.Free;
+  end;
+end;
+
+Procedure RequestToResponse(ATitle : String; ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse(ATitle,ARequest,AResponse,[]);
+end;
+
+Procedure SimpleCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Simple callback',ARequest,AResponse);
+end;
+
+Procedure DefaultCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Default callback (*path)',ARequest,AResponse,['path']);
+end;
+
+Procedure ParamPathMiddle(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Path in the middle (onepath/*path/new)',ARequest,AResponse,['path']);
+end;
+
+Procedure ParamPath(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Parametrized path (onepath/:param)',ARequest,AResponse,['param']);
+end;
+
+Procedure ParamPaths2(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Parametrized path (onepath/:param)',ARequest,AResponse,['param1','param2']);
+end;
+
+Procedure ComponentPath(AData : Pointer; ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Component path (component: '+TComponent(AData).Name+')',ARequest,AResponse);
+end;
+
+
+
+{ TMyModule }
+
+procedure TMyModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Old-fashioned Module',ARequest,AResponse);
+end;
+
+{ TMyHandler }
+
+procedure TMyHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Route object',ARequest,AResponse);
+end;
+
+{ TMyIntf }
+
+procedure TMyIntf.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Interface object',ARequest,AResponse);
+end;
+
+Procedure RegisterRoutes;
+
+begin
+  if (C1=Nil) then
+    begin
+    C1:=TComponent.Create(Nil);
+    C1.Name:='ComponentRoute1';
+    C2:=TComponent.Create(Nil);
+    C2.Name:='ComponentRoute2';
+    MyIntf:=TMyIntf.Create;
+    end;
+  HTTPRouter.RegisterRoute('simple',rmall,@SimpleCallBack);
+  HTTPRouter.RegisterRoute('onepath/:param',rmall,@ParamPath);
+  HTTPRouter.RegisterRoute('twopaths/:param1/:param2',rmall,@ParamPaths2);
+  HTTPRouter.RegisterRoute('onepath/*path/new',rmall,@ParamPathMiddle);
+  RegisterHTTPModule('module',TMyModule,True);
+  HTTPRouter.RegisterRoute('/component/1',C1,rmall,@ComponentPath);
+  HTTPRouter.RegisterRoute('/component/2',C2,rmall,@ComponentPath);
+  HTTPRouter.RegisterRoute('/interfaced',rmall,MyIntf);
+  HTTPRouter.RegisterRoute('/routed/object',rmall,TMyHandler);
+  // This will catch all other paths
+  HTTPRouter.RegisterRoute('*path',rmall,@DefaultCallBack,True);
+end;
+
+begin
+  FreeAndNil(C1);
+  FreeAndNil(C2);
+end.
+

+ 8 - 0
packages/fcl-web/examples/routing/sample.ini

@@ -0,0 +1,8 @@
+[CGI]
+; Assuming the demo is in cgi-bin
+BaseURL=http://localhost/cgi-bin/demorouting.cgi
+
+[Standalone]
+Port=8080
+; Optional, the following is the default.
+;BaseURL=http://localhost:8080/

+ 21 - 3
packages/fcl-web/fpmake.pp

@@ -25,6 +25,7 @@ begin
     P.Dependencies.Add('fcl-json');
     P.Dependencies.Add('fcl-net');
     P.Dependencies.Add('fcl-process');
+    P.Dependencies.Add('fcl-fpcunit');
     P.Dependencies.Add('fastcgi');
     P.Dependencies.Add('httpd22', AllOses - [amiga,aros,morphos]);
     P.Dependencies.Add('httpd24', AllOses - [amiga,aros,morphos]);
@@ -41,6 +42,14 @@ begin
     P.SourcePath.Add('src/base');
     P.SourcePath.Add('src/webdata');
     P.SourcePath.Add('src/jsonrpc');
+    P.SourcePath.Add('src/hpack');
+
+    T:=P.Targets.AddUnit('httpdefs.pp');
+    T.ResourceStrings:=true;
+    T.Dependencies.AddUnit('httpprotocol');
+
+    T:=P.Targets.AddUnit('httproute.pp');
+    T.Dependencies.AddUnit('httpdefs');
 
     T:=P.Targets.AddUnit('cgiapp.pp');
     T.ResourceStrings:=true;
@@ -88,10 +97,7 @@ begin
     T:=P.Targets.AddUnit('httpprotocol.pp');
     T:=P.Targets.AddUnit('cgiprotocol.pp');
 
-    T:=P.Targets.AddUnit('httpdefs.pp');
-    T.Dependencies.AddUnit('httpprotocol');
     
-    T.ResourceStrings:=true;
     T:=P.Targets.AddUnit('iniwebsession.pp');
     T.ResourceStrings:=true;
       with T.Dependencies do
@@ -113,6 +119,7 @@ begin
       begin
         ResourceStrings:=true;
         Dependencies.AddUnit('httpdefs');
+        Dependencies.AddUnit('httproute');
         Dependencies.AddUnit('fphttp');
       end;
     with P.Targets.AddUnit('webpage.pp') do
@@ -251,6 +258,17 @@ begin
     T.Dependencies.AddUnit('fpwebclient');
     T:=P.Targets.AddUnit('restbase.pp');
     T:=P.Targets.AddUnit('restcodegen.pp');
+
+    T:=P.Targets.AddUnit('uhpacktables.pp');
+    T:=P.Targets.AddUnit('uhpackimp.pp');
+    With T.Dependencies do  
+      AddUnit('uhpacktables');
+    T:=P.Targets.AddUnit('uhpack.pp');
+    With T.Dependencies do  
+      begin
+      AddUnit('uhpackimp');
+      end;
+    
 {$ifndef ALLPACKAGES}
     Run;
     end;

+ 75 - 0
packages/fcl-web/src/base/README.txt

@@ -6,6 +6,12 @@ fcl-base. See the fcl-base/texts/fptemplate.txt file.
 
 Architecture:
 
+httpprotocol:
+------------
+
+Mostly standard HTTP header definitions, and some auxiliary routines.
+
+
 httpdefs
 --------
 contains the basic HTTP system definitions: 
@@ -25,6 +31,75 @@ TResponse:
 TCustomSession: 
  Base for all session components.
 
+httproute
+---------
+
+The old Delphi style routing worked with Datamodules only. The pattern was
+strictly /modulename/actionname or through query variables: ?module=xyz&Action=nmo
+
+This old routing is still available by setting the LegacyRouting property of
+webhandler or webapplication (custweb) to true. (the new routing described
+below is then disabled)
+
+The new routing is more flexible in 3 ways.
+
+- It is no longer required to use datamodules, but this is still supported.
+  There are now 4 methods that can be used to register a route:
+
+  - Using a callback procedure
+    TRouteCallback = Procedure(ARequest: TRequest; AResponse);
+ 
+  - Using a callback event:
+    TRouteEvent = Procedure(ARequest: TRequest; AResponse) of object;
+
+  - Using an interface
+    IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
+      Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+    end;
+    Note that this is a CORBA interface, so no reference counting.
+
+  - Using a router object:
+    TRouteObject = Class(TObject,IRouteInterface)
+    Public
+      Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); virtual; abstract;
+    end;
+    TRouteObjectClass = Class of TRouteObject;
+  The object class needs to be registered. The router will instantiate the
+  object and release it once the request was handled.
+ 
+  More methods can be added, if need be.
+  All routes are registered using the HTTPRouter.RegisterRoute method.
+  it is overloaded to accept any of the above parameters.
+
+- The router can now match more complex, parametrized routes.
+
+  A route is the path part of an URL; query parameters are not examined.
+
+  /path1/path2/path3/path4
+
+  In these paths, parameters and wildcards are recognized:
+  :param means that it will match any request with a single part in this location
+  *paramm means that it will match any request with zero or more path parts in this location
+
+  examples:
+
+  /path1  
+  /REST/:Resource/:ID
+  /REST/:Resource
+  /*/something
+  /*path/somethiingelse
+  /*path  
+ 
+  The parameters will be added to TRequest, they are available in the (new) RouteParams array property of TRequest.
+  
+  Paths are matched case sensitively by default, and the first matching pattern is used.
+ 
+  HTTP Modules are registered in the router using classname/* or defaultmodulename/*
+
+- A set of methods can be added to the route registration (default is to  accept all methods). 
+  The router will  match the request method. If the method does not match, it will raise an
+  exception which will result in a 405 HTTP error.
+
 fphttp:
 -------
 Basic web system components/classes

+ 99 - 37
packages/fcl-web/src/base/custweb.pp

@@ -37,6 +37,7 @@ Type
   TWebHandler = class(TComponent)
   private
     FDefaultModuleName: String;
+    FLegacyRouting: Boolean;
     FOnIdle: TNotifyEvent;
     FOnInitModule: TInitModuleEvent;
     FOnUnknownRequestEncoding: TOnUnknownEncodingEvent;
@@ -55,6 +56,9 @@ Type
     FOnTerminate : TNotifyEvent;
     FOnLog : TLogEvent;
     FPreferModuleName : Boolean;
+    procedure DoCallModule(AModule: TCustomHTTPModule; AModuleName: String; ARequest: TRequest; AResponse: TResponse);
+    procedure HandleModuleRequest(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+    procedure OldHandleRequest(ARequest: TRequest; AResponse: TResponse);
   protected
     Class Procedure DoError(Msg : String; AStatusCode : Integer = 0; AStatusText : String = '');
     Class Procedure DoError(Fmt : String; Const Args : Array of const;AStatusCode : Integer = 0; AStatusText : String = '');
@@ -73,6 +77,7 @@ Type
     property Terminated: boolean read FTerminated;
   Public
     constructor Create(AOwner: TComponent); override;
+    Destructor Destroy; override;
     Procedure Run; virtual;
     Procedure Log(EventType : TEventType; Const Msg : String);
     Procedure DoHandleRequest(ARequest : TRequest; AResponse : TResponse);
@@ -94,6 +99,7 @@ Type
     Property OnUnknownRequestEncoding : TOnUnknownEncodingEvent Read FOnUnknownRequestEncoding Write FOnUnknownRequestEncoding;
     Property OnInitModule: TInitModuleEvent Read FOnInitModule write FOnInitModule;
     Property PreferModuleName : Boolean Read FPreferModuleName Write FPreferModuleName;
+    Property LegacyRouting : Boolean Read FLegacyRouting Write FLegacyRouting;
   end;
 
   TCustomWebApplication = Class(TCustomApplication)
@@ -107,6 +113,7 @@ Type
     function GetEmail: String;
     function GetEventLog: TEventLog;
     function GetHandleGetOnPost: Boolean;
+    function GetLegacyRouting: Boolean;
     function GetModuleVar: String;
     function GetOnGetModule: TGetModuleEvent;
     function GetOnShowRequestException: TOnShowRequestException;
@@ -120,6 +127,7 @@ Type
     procedure SetDefaultModuleName(AValue: String);
     procedure SetEmail(const AValue: String);
     procedure SetHandleGetOnPost(const AValue: Boolean);
+    procedure SetLegacyRouting(AValue: Boolean);
     procedure SetModuleVar(const AValue: String);
     procedure SetOnGetModule(const AValue: TGetModuleEvent);
     procedure SetOnShowRequestException(const AValue: TOnShowRequestException);
@@ -155,6 +163,7 @@ Type
     Property OnUnknownRequestEncoding : TOnUnknownEncodingEvent Read GetOnUnknownRequestEncoding Write SetOnUnknownRequestEncoding;
     Property EventLog: TEventLog read GetEventLog;
     Property PreferModuleName : Boolean Read GetPreferModuleName Write SetPreferModuleName;
+    Property LegacyRouting : Boolean Read GetLegacyRouting Write SetLegacyRouting;
   end;
 
   EFPWebError = Class(EFPHTTPError);
@@ -163,10 +172,12 @@ procedure ExceptionToHTML(S: TStrings; const E: Exception; const Title, Email, A
 
 Implementation
 
-{$ifdef CGIDEBUG}
+
 uses
-  dbugintf;
-{$endif}
+  {$ifdef CGIDEBUG}
+  dbugintf,
+  {$endif}
+  httproute;
 
 resourcestring
   SErrNoModuleNameForRequest = 'Could not determine HTTP module name for request';
@@ -302,54 +313,89 @@ begin
   Result := FAdministrator;
 end;
 
-Procedure TWebHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+Procedure TWebHandler.DoCallModule(AModule : TCustomHTTPModule; AModuleName : String ; ARequest: TRequest; AResponse: TResponse);
+
+begin
+  SetBaseURL(AModule,AModuleName,ARequest);
+  if (OnInitModule<>Nil) then
+    OnInitModule(Self,AModule);
+  AModule.DoAfterInitModule(ARequest);
+  if AModule.Kind=wkOneShot then
+    begin
+    try
+      AModule.HandleRequest(ARequest,AResponse);
+    finally
+      AModule.Free;
+    end;
+    end
+  else
+    AModule.HandleRequest(ARequest,AResponse);
+end;
+
+Procedure TWebHandler.HandleModuleRequest(Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse);
+
 Var
   MC : TCustomHTTPModuleClass;
   M  : TCustomHTTPModule;
   MN : String;
-  MI : TModuleItem;
+
+begin
+  MC:=Sender.ModuleClass;
+  MN:=Sender.ModuleName;
+  // Modules expect the path info to contain the action name as the first part. (See getmodulename);
+  ARequest.GetNextPathInfo;
+  if Sender.SkipStreaming then
+    M:=MC.CreateNew(Self)
+  else
+    M:=MC.Create(Self);
+  DoCallModule(M,MN,ARequest,AResponse);
+end;
+
+Procedure TWebHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
 
 begin
   try
-    MC:=Nil;
-    M:=NIL;
-    MI:=Nil;
-    If (OnGetModule<>Nil) then
-      OnGetModule(Self,ARequest,MC);
-    If (MC=Nil) then
-      begin
-      MN:=GetModuleName(ARequest);
-      MI:=ModuleFactory.FindModule(MN);
-      if (MI=Nil) then
-        DoError(SErrNoModuleForRequest,[MN],400,'Not found');
-      MC:=MI.ModuleClass;
-      end;
-    M:=FindModule(MC); // Check if a module exists already
-    If (M=Nil) then
-      if assigned(MI) and Mi.SkipStreaming then
-        M:=MC.CreateNew(Self)
-      else
-        M:=MC.Create(Self);
-    SetBaseURL(M,MN,ARequest);
-    if (OnInitModule<>Nil) then
-      OnInitModule(Self,M);
-    M.DoAfterInitModule(ARequest);
-    if M.Kind=wkOneShot then
-      begin
-      try
-        M.HandleRequest(ARequest,AResponse);
-      finally
-        M.Free;
-      end;
-      end
+    if LegacyRouting then
+      OldHandleRequest(ARequest,AResponse)
     else
-      M.HandleRequest(ARequest,AResponse);
+      HTTPRouter.RouteRequest(ARequest,AResponse);
   except
     On E : Exception do
       ShowRequestException(AResponse,E);
   end;
 end;
 
+Procedure TWebHandler.OldHandleRequest(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  MC : TCustomHTTPModuleClass;
+  M  : TCustomHTTPModule;
+  MN : String;
+  MI : TModuleItem;
+
+begin
+  MC:=Nil;
+  M:=NIL;
+  MI:=Nil;
+  If (OnGetModule<>Nil) then
+    OnGetModule(Self,ARequest,MC);
+  If (MC=Nil) then
+    begin
+    MN:=GetModuleName(ARequest);
+    MI:=ModuleFactory.FindModule(MN);
+    if (MI=Nil) then
+      DoError(SErrNoModuleForRequest,[MN],400,'Not found');
+    MC:=MI.ModuleClass;
+    end;
+  M:=FindModule(MC); // Check if a module exists already
+  If (M=Nil) then
+    if assigned(MI) and Mi.SkipStreaming then
+      M:=MC.CreateNew(Self)
+    else
+      M:=MC.Create(Self);
+   DoCallModule(M,MN,ARequest,AResponse);
+end;
+
 function TWebHandler.GetApplicationURL(ARequest: TRequest): String;
 begin
   Result:=FApplicationURL;
@@ -482,6 +528,12 @@ begin
   FHandleGetOnPost := True;
   FRedirectOnError := False;
   FRedirectOnErrorURL := '';
+  ModuleFactory.OnModuleRequest:=@HandleModuleRequest;
+end;
+
+destructor TWebHandler.Destroy;
+begin
+  ModuleFactory.OnModuleRequest:=@HandleModuleRequest;
 end;
 
 { TCustomWebApplication }
@@ -537,6 +589,11 @@ begin
   result := FWebHandler.HandleGetOnPost;
 end;
 
+function TCustomWebApplication.GetLegacyRouting: Boolean;
+begin
+  Result:=FWebHandler.LegacyRouting;
+end;
+
 function TCustomWebApplication.GetModuleVar: String;
 begin
   result := FWebHandler.ModuleVariable;
@@ -602,6 +659,11 @@ begin
   FWebHandler.HandleGetOnPost := AValue;
 end;
 
+procedure TCustomWebApplication.SetLegacyRouting(AValue: Boolean);
+begin
+  FWebHandler.LegacyRouting:=AValue;
+end;
+
 procedure TCustomWebApplication.SetModuleVar(const AValue: String);
 begin
   FWebHandler.ModuleVariable := AValue;

+ 67 - 20
packages/fcl-web/src/base/fphttp.pp

@@ -17,7 +17,7 @@ unit fphttp;
 
 Interface
 
-uses sysutils,classes,httpdefs;
+uses sysutils,classes,httpdefs, httproute;
 
 Type
 { TODO : Implement wkSession }
@@ -188,28 +188,40 @@ Type
 
   { TModuleItem }
 
-  TModuleItem = Class(TCollectionItem)
+  TModuleItem = Class(TCollectionItem, IRouteInterface)
   private
     FModuleClass: TCustomHTTPModuleClass;
     FModuleName: String;
     FSkipStreaming: Boolean;
+    FRouteID : Integer;
+  Protected
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+    Property RouteID : Integer Read FRouteID;
   Public
+    Destructor Destroy; override;
     Property ModuleClass : TCustomHTTPModuleClass Read FModuleClass Write FModuleClass;
     Property ModuleName : String Read FModuleName Write FModuleName;
     Property SkipStreaming : Boolean Read FSkipStreaming Write FSkipStreaming;
   end;
 
   { TModuleFactory }
+  TOnModuleRequest = Procedure (Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse) of object;
 
   TModuleFactory = Class(TCollection)
   private
+    FOnModuleRequest: TOnModuleRequest;
     function GetModule(Index : Integer): TModuleItem;
     procedure SetModule(Index : Integer; const AValue: TModuleItem);
+  Protected
+    procedure DoHandleRequest(Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse);
   Public
+    Procedure RegisterHTTPModule(Const ModuleName : String; ModuleClass : TCustomHTTPModuleClass; SkipStreaming : Boolean = False);virtual;
+    Procedure RegisterHTTPModule(ModuleClass : TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
     Function FindModule(const AModuleName : String) : TModuleItem;
     Function ModuleByName(const AModuleName : String) : TModuleItem;
     Function IndexOfModule(const AModuleName : String) : Integer;
     Property Modules [Index : Integer]: TModuleItem Read GetModule Write SetModule;default;
+    Property OnModuleRequest : TOnModuleRequest Read FOnModuleRequest Write FOnModuleRequest;
   end;
 
   { EFPHTTPError }
@@ -237,9 +249,9 @@ Resourcestring
 
 Implementation
 
-{$ifdef cgidebug}
-uses dbugintf;
-{$endif}
+
+{$ifdef cgidebug} uses dbugintf; {$endif}
+
 
 Var
   GSM : TSessionFactory;
@@ -256,6 +268,21 @@ begin
   Result:=GSM;
 end;
 
+{ TModuleItem }
+
+procedure TModuleItem.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  if (Collection is TModuleFactory) then
+    (Collection as TModuleFactory).DoHandleRequest(Self,ARequest,AResponse);
+end;
+
+destructor TModuleItem.Destroy;
+begin
+  if (FRouteID>0) then
+    httprouter.DeleteRouteByID(FRouteID-1);
+  inherited Destroy;
+end;
+
 
 { TCustomHTTPModule }
 
@@ -335,6 +362,39 @@ begin
   Items[Index]:=AValue;
 end;
 
+procedure TModuleFactory.DoHandleRequest(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+begin
+  If Assigned(OnModuleRequest) then
+    OnModuleRequest(Sender,ARequest,AResponse)
+  else
+    Raise EFPHTTPError.Create('Cannot handle module request, OnModuleRequest not set');
+end;
+
+procedure TModuleFactory.RegisterHTTPModule(const ModuleName: String; ModuleClass: TCustomHTTPModuleClass; SkipStreaming: Boolean);
+
+Var
+  I : Integer;
+  MI : TModuleItem;
+
+begin
+  I:=IndexOfModule(ModuleName);
+  If (I=-1) then
+    begin
+    MI:=Add as TModuleItem;
+    MI.ModuleName:=ModuleName;
+    MI.FRouteID:=httprouter.RegisterRoute('/'+MI.FModuleName+'/*', MI as IRouteInterface,False).ID+1;
+    end
+  else
+    MI:=ModuleFactory[I];
+  MI.ModuleClass:=ModuleClass;
+  MI.SkipStreaming:=SkipStreaming;
+end;
+
+procedure TModuleFactory.RegisterHTTPModule(ModuleClass: TCustomHTTPModuleClass; SkipStreaming: Boolean);
+begin
+  RegisterHTTPModule(ModuleClass.DefaultModuleName,ModuleClass,SkipStreaming);
+end;
+
 function TModuleFactory.FindModule(const AModuleName: String): TModuleItem;
 
 Var
@@ -366,27 +426,14 @@ end;
 
 procedure RegisterHTTPModule(ModuleClass: TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
 begin
-  RegisterHTTPModule(ModuleClass.ClassName,ModuleClass,SkipStreaming);
+  ModuleFactory.RegisterHTTPModule(ModuleClass,SkipStreaming);
 end;
 
 procedure RegisterHTTPModule(const ModuleName: String;
   ModuleClass: TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
   
-Var
-  I : Integer;
-  MI : TModuleItem;
-  
 begin
-  I:=ModuleFactory.IndexOfModule(ModuleName);
-  If (I=-1) then
-    begin
-    MI:=ModuleFactory.Add as TModuleItem;
-    MI.ModuleName:=ModuleName;
-    end
-  else
-    MI:=ModuleFactory[I];
-  MI.ModuleClass:=ModuleClass;
-  MI.SkipStreaming:=SkipStreaming;
+  ModuleFactory.RegisterHTTPModule(ModuleName,ModuleClass,SkipStreaming);
 end;
 
 { THTTPContentProducer }

+ 21 - 0
packages/fcl-web/src/base/httpdefs.pp

@@ -415,8 +415,11 @@ type
     FServerPort : String;
     FContentRead : Boolean;
     FContent : String;
+    FRouteParams : TStrings;
     function GetLocalPathPrefix: string;
     function GetFirstHeaderLine: String;
+    function GetRP(AParam : String): String;
+    procedure SetRP(AParam : String; AValue: String);
   Protected
     Function AllowReadContent : Boolean; virtual;
     Function CreateUploadedFiles : TUploadedFiles; virtual;
@@ -441,6 +444,7 @@ type
     constructor Create; override;
     destructor destroy; override;
     Function GetNextPathInfo : String;
+    Property RouteParams[AParam : String] : String Read GetRP Write SetRP;
     Property ReturnedPathInfo : String Read FReturnedPathInfo Write FReturnedPathInfo;
     Property LocalPathPrefix : string Read GetLocalPathPrefix;
     Property CommandLine : String Read FCommandLine;
@@ -1453,6 +1457,7 @@ end;
 
 destructor TRequest.destroy;
 begin
+  FreeAndNil(FRouteParams);
   FreeAndNil(FFiles);
   inherited destroy;
 end;
@@ -1534,6 +1539,22 @@ begin
     Result := Result + ' HTTP/' + HttpVersion;
 end;
 
+function TRequest.GetRP(AParam : String): String;
+begin
+  if Assigned(FRouteParams) then
+    Result:=FRouteParams.Values[AParam]
+  else
+    Result:='';
+end;
+
+procedure TRequest.SetRP(AParam : String; AValue: String);
+begin
+  if (AValue<>GetRP(AParam)) And ((AValue<>'')<>Assigned(FRouteParams)) then
+    FRouteParams:=TStringList.Create;
+  if (AValue<>'') and Assigned(FRouteParams) then
+    FRouteParams.Values[AParam]:=AValue;
+end;
+
 function TRequest.AllowReadContent: Boolean;
 begin
   Result:=True;

+ 778 - 0
packages/fcl-web/src/base/httproute.pp

@@ -0,0 +1,778 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2017 by the Free Pascal development team
+
+    HTTPRoute: HTTP request router
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+}
+
+{
+  Note:
+  The MatchPattern routine was taken from Brook Framework's router unit, by Silvio Clecio.
+}
+
+{$mode objfpc}
+{$H+}
+
+unit httproute;
+
+interface
+
+uses
+  Classes, SysUtils, httpdefs;
+
+Type
+  EHTTPRoute = Class(EHTTP);
+
+  // Forward definitions;
+
+  THTTPRouter = Class;
+  THTTPRouterClass = Class of THTTPRouter;
+  // Some common HTTP methods.
+
+  TRouteMethod = (rmUnknown,rmAll,rmGet,rmPost,rmPut,rmDelete,rmOptions,rmHead, rmTrace);
+
+  { THTTPRoute }
+
+  THTTPRoute = Class(TCollectionItem)
+  private
+    FDefault: Boolean;
+    FMethod: TRouteMethod;
+    FURLPattern: String;
+    procedure SetURLPattern(AValue: String);
+  Protected
+    Procedure DoHandleRequest(ARequest : TRequest; AResponse : TResponse); virtual;
+  Public
+    Destructor Destroy; override;
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+    Function Matches(Const APattern : String; AMethod : TRouteMethod) : Boolean;
+    Function MatchPattern(Const Path : String; L : TStrings) : Boolean;
+    Function MatchMethod(Const AMethod : TRouteMethod) : Boolean;
+  Published
+    Property Default : Boolean Read FDefault Write FDefault;
+    Property URLPattern : String Read FURLPattern Write SetURLPattern;
+    Property Method : TRouteMethod Read FMethod Write FMethod;
+  end;
+  THTTPRouteClass = Class of THTTPRoute;
+
+  { THTTPRouteList }
+
+  THTTPRouteList = Class (TCollection)
+  private
+    function GetR(AIndex : Integer): THTTPRoute;
+    procedure SetR(AIndex : Integer; AValue: THTTPRoute);
+  Public
+    Property Routes[AIndex : Integer] : THTTPRoute Read GetR Write SetR; default;
+  end;
+
+  TRouteCallBack = Procedure (ARequest: TRequest; AResponse: TResponse);
+
+  { THTTPRouteCallback }
+
+  THTTPRouteCallback = Class(THTTPRoute)
+  private
+    FCallBack: TRouteCallBack;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property CallBack : TRouteCallBack Read FCallBack Write FCallback;
+  end;
+
+  TRouteCallBackEx = Procedure (AData : Pointer; ARequest: TRequest; AResponse: TResponse);
+
+  { THTTPRouteCallbackex }
+
+  THTTPRouteCallbackEx = Class(THTTPRoute)
+  private
+    FCallBack: TRouteCallBackex;
+    FData: Pointer;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property CallBack : TRouteCallBackex Read FCallBack Write FCallback;
+    Property Data : Pointer Read FData Write FData;
+  end;
+
+  TRouteEvent = Procedure (ARequest: TRequest; AResponse: TResponse) of object;
+
+  { THTTPRouteEvent }
+
+  THTTPRouteEvent = Class(THTTPRoute)
+  private
+    FEvent: TRouteEvent;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property Event : TRouteEvent Read FEvent Write FEvent;
+  end;
+
+{$INTERFACES CORBA}
+  IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+  end;
+
+  { THTTPRouteInterface }
+
+  THTTPRouteInterface = Class(THTTPRoute)
+  private
+    FIntf: IRouteInterface;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property Intf : IRouteInterface Read FIntf Write FIntf;
+  end;
+
+  TRouteObject = Class(TObject,IRouteInterface)
+  Public
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); virtual; abstract;
+  end;
+  TRouteObjectClass = Class of TRouteObject;
+
+  { THTTPRouteObject }
+
+  THTTPRouteObject = Class(THTTPRoute)
+  private
+    FClass: TRouteObjectClass;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property ObjectCLass : TRouteObjectClass Read FClass Write FClass;
+  end;
+
+  THTTPRouteRequestEvent = Procedure (Sender : TObject; ARequest : TRequest; AResponse : TResponse) of object;
+
+  { THTTPRouter }
+
+  THTTPRouter = Class(TComponent)
+  private
+    FAfterRequest: THTTPRouteRequestEvent;
+    FBeforeRequest: THTTPRouteRequestEvent;
+    FRoutes : THTTPRouteList;
+    function GetR(AIndex : Integer): THTTPRoute;
+    Class Procedure DoneService;
+    Class
+      Var FService : THTTPRouter;
+          FServiceClass : THTTPRouterClass;
+    function GetRouteCount: Integer;
+  Protected
+    // Return an instance of given class with Pattern, Method, IsDefault filled in.
+    function CreateHTTPRoute(AClass: THTTPRouteClass; const APattern: String; AMethod: TRouteMethod; IsDefault: Boolean ): THTTPRoute; virtual;
+    // Override this if you want to use another collection class.
+    Function CreateRouteList : THTTPRouteList; virtual;
+    Procedure CheckDuplicate(APattern : String; AMethod : TRouteMethod; isDefault : Boolean);
+    // Actually route request. Override this for customized behaviour.
+    Procedure DoRouteRequest(ARequest : TRequest; AResponse : TResponse); virtual;
+    // Extract route from request. This is PathInfo by default (sanitized);
+    Function GetRequestPath(ARequest : TRequest) : String; virtual;
+  Public
+    Constructor Create(AOwner: TComponent); override;
+    Destructor Destroy; override;
+    // Delete given route by index.
+    Procedure DeleteRoute(AIndex : Integer);
+    // Delete given route by index.
+    Procedure DeleteRouteByID(AID : Integer);
+    // Delete given route by index. The route object will be freed.
+    Procedure DeleteRoute(ARoute : THTTPRoute);
+    // Sanitize route path. Strips of query parameters and makes sure it ends in /
+    class function SanitizeRoute(const Path: String): String;
+    // Global instance.
+    Class Function Service : THTTPRouter;
+    // Class for global instance when it is created;
+    Class Function ServiceClass : THTTPRouterClass;
+    // This will destroy the service
+    Class Procedure SetServiceClass(AClass : THTTPRouterClass);
+    // Convert string to HTTP Route method
+    Class Function StringToRouteMethod(Const S : String) : TRouteMethod;
+    // Register event based route
+    Function RegisterRoute(Const APattern : String; AEvent: TRouteEvent; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; AEvent: TRouteEvent; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Register interface based route. Programmer is responsible for the lifetime of the interface.
+    Function RegisterRoute(Const APattern : String; const AIntf: IRouteInterface; IsDefault : Boolean = False) : THTTPRoute; overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; const AIntf: IRouteInterface; IsDefault : Boolean = False): THTTPRoute; overload;
+    // Object class based route. The router is responsible for the lifetime of the object instance
+    Function RegisterRoute(Const APattern : String; const AObjectClass: TRouteObjectClass; IsDefault : Boolean = False) : THTTPRoute; overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; const AobjectClass: TRouteObjectClass; IsDefault : Boolean = False): THTTPRoute; overload;
+    // Register callback based route
+    Function RegisterRoute(Const APattern : String; AData : Pointer; ACallBack: TRouteCallBackex; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AData : Pointer; AMethod : TRouteMethod; ACallBack: TRouteCallBackEx; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Register callbackEx based route
+    Function RegisterRoute(Const APattern : String; ACallBack: TRouteCallBack; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; ACallBack: TRouteCallBack; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Find route. Matches Path on the various patterns. If a pattern is found, then the method is tested.
+    // Returns the route that matches the pattern and method.
+    function FindHTTPRoute(const Path: String; AMethod: TRouteMethod; Params: TStrings; out MethodMismatch: Boolean): THTTPRoute;
+    function GetHTTPRoute(const Path: String; AMethod: TRouteMethod; Params: TStrings): THTTPRoute;
+    // Do actual routing. Exceptions raised will not be caught. Request must be initialized
+    Procedure RouteRequest(ARequest : TRequest; AResponse : TResponse);
+    // Indexed access to the registered routes.
+    Property Routes [AIndex : Integer]  : THTTPRoute Read GetR; Default;
+    // Number of registered routes.
+    Property RouteCount : Integer Read GetRouteCount;
+    // Called before the request is routed.
+    Property BeforeRequest : THTTPRouteRequestEvent Read FBeforeRequest Write FBeforeRequest;
+    // Called after the request is routed, if no exception was raised during or before the request.
+    Property AfterRequest : THTTPRouteRequestEvent Read FAfterRequest Write FAfterRequest;
+  end;
+
+Function RouteMethodToString (R : TRouteMethod)  : String;
+// Shortcut for THTTPRouter.Service;
+Function HTTPRouter : THTTPRouter;
+
+implementation
+
+uses strutils, typinfo;
+
+Resourcestring
+  EDuplicateRoute = 'Duplicate route pattern: %s and method: %s';
+  EDuplicateDefaultRoute = 'Duplicate default route registered with pattern: %s and method: %s';
+
+function RouteMethodToString(R: TRouteMethod): String;
+
+begin
+  if R=rmUnknown then
+    Result:=''
+  else if R=rmAll then
+    Result:='*'
+  else
+    Result:=GetEnumName(TypeInfo(TRouteMethod),Ord(R));
+end;
+
+function HTTPRouter: THTTPRouter;
+begin
+  Result:=THTTPRouter.Service;
+end;
+
+{ THTTPRouteCallback }
+
+procedure THTTPRouteCallback.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  CallBack(ARequest, AResponse);
+end;
+
+{ THTTPRouteObject }
+
+procedure THTTPRouteObject.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+Var
+  O : TRouteObject;
+
+begin
+  O:=ObjectClass.Create;
+  try
+    O.HandleRequest(ARequest,AResponse);
+  finally
+    O.Free;
+  end;
+end;
+
+{ THTTPRouter }
+
+function THTTPRouter.GetR(AIndex : Integer): THTTPRoute;
+begin
+  Result:=FRoutes[AIndex]
+end;
+
+class procedure THTTPRouter.DoneService;
+begin
+  FreeAndNil(FService);
+end;
+
+function THTTPRouter.GetRouteCount: Integer;
+begin
+  Result:=FRoutes.Count;
+end;
+
+function THTTPRouter.CreateRouteList: THTTPRouteList;
+begin
+  Result:=THTTPRouteList.Create(THTTPRoute);
+end;
+
+procedure THTTPRouter.CheckDuplicate(APattern: String; AMethod: TRouteMethod;
+  isDefault: Boolean);
+Var
+  I,DI : Integer;
+  R : THTTPRoute;
+
+begin
+  DI:=-1;
+  For I:=0 to FRoutes.Count-1 do
+    begin
+    R:=FRoutes[I];
+    if R.Default then
+      DI:=I;
+    if R.Matches(APattern,AMethod) then
+      Raise EHTTPRoute.CreateFmt(EDuplicateRoute,[APattern,RouteMethodToString(AMethod)]);
+    end;
+  if isDefault and (DI<>-1) then
+    Raise EHTTPRoute.CreateFmt(EDuplicateDefaultRoute,[APattern,RouteMethodToString(AMethod)]);
+end;
+
+procedure THTTPRouter.DoRouteRequest(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  APath : String;
+  AMethod : TRouteMethod;
+  R : THTTPRoute;
+  L : TStrings;
+  I : Integer;
+  N,V : string;
+
+begin
+  APath:=GetRequestPath(ARequest);
+  AMethod:=StringToRouteMethod(ARequest.Method);
+  L:=TStringList.Create;
+  try
+    R:=GetHTTPRoute(APath,AMethod,L);
+    For I:=0 to L.Count-1 do
+      begin
+      L.GetNameValue(I,N,V);
+      if (N<>'') then
+        ARequest.RouteParams[N]:=V;
+      end;
+    R.HandleRequest(ARequest,AResponse);
+  finally
+    L.Free;
+  end;
+end;
+
+function THTTPRouter.GetRequestPath(ARequest: TRequest): String;
+begin
+  Result:=SanitizeRoute(ARequest.PathInfo);
+end;
+
+constructor THTTPRouter.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+  froutes:=CreateRouteList;
+end;
+
+destructor THTTPRouter.Destroy;
+begin
+  FreeAndNil(FRoutes);
+  inherited Destroy;
+end;
+
+procedure THTTPRouter.DeleteRoute(AIndex: Integer);
+begin
+  FRoutes.Delete(Aindex)
+end;
+
+procedure THTTPRouter.DeleteRouteByID(AID: Integer);
+begin
+  FRoutes.FindItemID(AID).Free;
+end;
+
+procedure THTTPRouter.DeleteRoute(ARoute: THTTPRoute);
+begin
+  ARoute.Free;
+end;
+
+class function THTTPRouter.Service: THTTPRouter;
+begin
+  if FService=Nil then
+    FService:=ServiceClass.Create(Nil);
+  Result:=FService;
+end;
+
+class function THTTPRouter.ServiceClass: THTTPRouterClass;
+begin
+  If FServiceClass=nil then
+    FServiceClass:=THTTPRouter;
+  Result:=FServiceClass;
+end;
+
+class procedure THTTPRouter.SetServiceClass(AClass: THTTPRouterClass);
+begin
+  if Assigned(FService) then
+    FreeAndNil(FService);
+  FServiceClass:=AClass;
+end;
+
+class function THTTPRouter.StringToRouteMethod(const S: String): TRouteMethod;
+begin
+
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AData : Pointer;
+  ACallBack: TRouteCallBackEx; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,AData,rmAll,ACallBack,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AData : Pointer;
+  AMethod: TRouteMethod; ACallBack: TRouteCallBackEx; IsDefault: Boolean
+  ): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteCallbackex,APattern,AMethod,IsDefault);
+  THTTPRouteCallbackex(Result).CallBack:=ACallBack;
+  THTTPRouteCallbackex(Result).Data:=AData;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; ACallBack: TRouteCallBack; IsDefault: Boolean
+  ): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,rmAll,ACallBack,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AMethod: TRouteMethod; ACallBack: TRouteCallBack;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=CreateHTTPRoute(THTTPRouteCallback,APattern,AMethod,IsDefault);
+  THTTPRouteCallback(Result).CallBack:=ACallBack;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AEvent: TRouteEvent;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,rmAll,AEvent,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;
+  AMethod: TRouteMethod; AEvent: TRouteEvent; IsDefault: Boolean): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteEvent,APattern,AMethod,IsDefault);
+  THTTPRouteEvent(Result).Event:=AEvent;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;
+  const AIntf: IRouteInterface; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=RegisterRoute(APattern,rmAll,AIntf,IsDefault);
+end;
+
+function THTTPRouter.CreateHTTPRoute(AClass : THTTPRouteClass; const APattern: String;AMethod: TRouteMethod; IsDefault: Boolean) : THTTPRoute;
+
+begin
+  CheckDuplicate(APattern,AMethod,isDefault);
+  Result:=AClass.Create(FRoutes);
+  With Result do
+    begin
+    URLPattern:=APattern;
+    Default:=IsDefault;
+    Method:=AMethod;
+    end;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AMethod: TRouteMethod; const AIntf: IRouteInterface; IsDefault: Boolean ): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteInterface,APattern,AMethod,IsDefault);
+  THTTPRouteInterface(Result).Intf:=AIntf;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; const AObjectClass: TRouteObjectClass; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=RegisterRoute(APattern,rmAll,AObjectClass,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AMethod: TRouteMethod; const AobjectClass: TRouteObjectClass;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=CreateHTTPRoute(THTTPRouteObject,APattern,AMethod,IsDefault);
+  THTTPRouteObject(Result).ObjectCLass:=AObjectClass;
+end;
+
+Class function THTTPRouter.SanitizeRoute(const Path: String) : String;
+
+Var
+  APathInfo : String;
+
+begin
+  APathInfo:=Path;
+  Delete(APathInfo,Pos('?', APathInfo), MaxInt);
+  Result:=IncludeHTTPPathDelimiter(APathInfo);
+end;
+
+function THTTPRouter.FindHTTPRoute(const Path: String; AMethod: TRouteMethod; Params : TStrings; Out MethodMismatch : Boolean): THTTPRoute;
+
+Var
+  I : Integer;
+  APathInfo : String;
+
+begin
+  APathInfo:=SanitizeRoute(Path);
+  MethodMisMatch:=False;
+  Result:=Nil;
+  I:=0;
+  While (Result=Nil) and (I<FRoutes.Count) do
+    begin
+    Result:=FRoutes[i];
+    If Not Result.MatchPattern(APathInfo,Params) then
+      Result:=Nil
+    else if Not Result.MatchMethod(AMethod) then
+      begin
+      Result:=Nil;
+      Params.Clear;
+      MethodMisMatch:=True;
+      end;
+    Inc(I);
+    end;
+end;
+
+function THTTPRouter.GetHTTPRoute(const Path: String; AMethod: TRouteMethod; Params : TStrings): THTTPRoute;
+
+Const
+  Status : Array[Boolean] of Integer = (404,405);
+  StatusText :Array[Boolean] of String = ('Not found','Method not allowed');
+
+Var
+  MethodMisMatch : Boolean;
+  E:EHTTPRoute;
+
+begin
+  Result:=FindHTTPRoute(Path,AMethod,Params,MethodMisMatch);
+  if Not Assigned(Result) then
+    begin
+    E:=EHTTPRoute.Create(StatusText[MethodMisMatch]);
+    E.StatusText:=StatusText[MethodMisMatch];
+    E.StatusCode:=Status[MethodMisMatch];
+    Raise E;
+    end;
+end;
+
+procedure THTTPRouter.RouteRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  If Assigned(FBeforeRequest) then
+    FBeforeRequest(Self,ARequest,AResponse);
+  DoRouteRequest(ARequest,AResponse);
+  If Assigned(FAfterRequest) then
+    FAfterRequest(Self,ARequest,AResponse);
+end;
+
+{ THTTPRouteInterface }
+
+procedure THTTPRouteInterface.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Intf.HandleRequest(ARequest, AResponse);
+end;
+
+{ THTTPRouteEvent }
+
+procedure THTTPRouteEvent.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Event(ARequest, AResponse);
+end;
+
+{ THTTPRouteList }
+
+function THTTPRouteList.GetR(AIndex : Integer): THTTPRoute;
+begin
+  Result:=Items[AIndex] as THTTPRoute;
+end;
+
+procedure THTTPRouteList.SetR(AIndex : Integer; AValue: THTTPRoute);
+begin
+  Items[AIndex]:=AValue;
+end;
+
+{ THTTPRoute }
+
+procedure THTTPRoute.SetURLPattern(AValue: String);
+
+Var
+  V : String;
+
+begin
+  V:=IncludeHTTPPathDelimiter(AValue);
+  if (V<>'/') and (V[1]='/') then
+    Delete(V,1,1);
+  if FURLPattern=V then Exit;
+  FURLPattern:=V;
+end;
+
+procedure THTTPRoute.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  // Do nothing
+end;
+
+destructor THTTPRoute.Destroy;
+begin
+
+  inherited Destroy;
+end;
+
+procedure THTTPRoute.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  DoHandleRequest(ARequest,AResponse);
+end;
+
+function THTTPRoute.Matches(const APattern: String; AMethod: TRouteMethod
+  ): Boolean;
+begin
+  Result:=(CompareText(URLPattern,APattern)=0)
+          and ((Method=rmAll) or (AMethod=Method))
+end;
+
+Function THTTPRoute.MatchPattern(Const Path : String; L : TStrings) : Boolean;
+
+  Function StartsWith(C : Char; S : String): Boolean; 
+  
+  begin
+    Result:=(Length(S)>0) and (S[1]=C);
+  end;
+  
+  Function EndsWith(C : Char; S : String): Boolean; 
+  
+  Var
+  L : Integer;
+  
+  begin
+    L:=Length(S);
+    Result:=(L>0) and (S[L]=C);
+  end;
+  
+
+  procedure ExtractNextPathLevel(var ALeft: string;
+    var ALvl: string; var ARight: string; const ADelim: Char = '/');
+  var
+    P: Integer;
+  begin
+    if (ALvl<>ADelim) then
+      begin
+      ALeft:=ALeft+ALvl;
+      if StartsWith(ADelim,ARight) then
+        begin
+        ALeft:=ALeft+ADelim;
+        Delete(ARight,1,1);
+        end;
+      end;
+    P:=Pos(ADelim,ARight);
+    if P=0 then
+      P:=Length(ARight)+1;
+    ALvl:=Copy(ARight,1,P-1);
+    ARight:=Copy(ARight,P,MaxInt);
+  end;
+
+  procedure ExtractPrevPathLevel(var ALeft: string;
+    var ALvl: string; var ARight: string; const ADelim: Char = '/');
+  var
+    P,L: Integer;
+  begin
+    if (ALvl<>ADelim) then
+      begin
+      ARight:=ALvl+ARight;
+      L:=Length(ALeft);
+      if EndsWith(ADelim,ALeft) then
+        begin
+        ARight:=ADelim+ARight;
+        Delete(ALeft,L,1);
+        end;
+      end;
+    P:=RPos(ADelim,ALeft);
+    ALvl:=Copy(ALeft,P+1,MaxInt);
+    ALeft:=Copy(ALeft,1,P);
+  end;
+
+var
+  APathInfo : String;
+  APattern : String;
+  VLeftPat, VRightPat, VLeftVal, VRightVal, VVal, VPat, VName: string;
+
+begin
+  Result:= False;
+  if (URLPattern='') then
+     Exit; // Maybe empty pattern should match any path?
+  APathInfo:=Path;
+  APattern:=URLPattern;
+  Delete(APattern, Pos('?', APattern), MaxInt);
+  Delete(APathInfo, Pos('?', APathInfo), MaxInt);
+  if StartsWith('/',APattern) then
+    Delete(APattern,1,1);
+  if StartsWith('/',APathInfo) then
+    Delete(APathInfo,1,1);
+  VLeftPat := '';
+  VLeftVal := '';
+  VPat := '/'; // init value is '/', not ''
+  VVal := '/'; // init value is '/', not ''
+  VRightPat := APattern;
+  VRightVal := APathInfo;
+  repeat
+    // Extract next part
+    ExtractNextPathLevel(VLeftPat, VPat, VRightPat);
+    ExtractNextPathLevel(VLeftVal, VVal, VRightVal);
+    if StartsWith(':',VPat) then
+      begin
+      L.Values[Copy(VPat,2,Maxint)]:=VVal;
+      end
+    else
+      if StartsWith('*',VPat) then
+        begin
+        // *path
+        VName := Copy(VPat, 2, MaxInt);
+        VLeftPat := VRightPat;
+        VLeftVal := VVal + VRightVal;
+        VPat := '/'; // init value is '/', not ''
+        VVal := '/'; // init value is '/', not ''
+        VRightPat := '';
+        VRightVal := '';
+        // if AutoAddSlash ...
+        if EndsWith('/',VLeftPat) and not EndsWith('/',VLeftVal) then
+          Delete(VLeftPat, Length(VLeftPat), 1);
+        repeat
+          // Extract backwards
+          ExtractPrevPathLevel(VLeftPat, VPat, VRightPat);
+          ExtractPrevPathLevel(VLeftVal, VVal, VRightVal);
+          if StartsWith(':', VPat) then
+            begin
+            // *path/:field
+            L.Values[Copy(VPat,2,Maxint)]:=VVal;
+            end
+          else
+            // *path/const
+            if not ((VPat='') and (VLeftPat='')) and (VPat<>VVal) then
+              Exit;
+          // Check if we already done
+          if (VLeftPat='') or (VLeftVal='') then
+            begin
+            if VLeftPat='' then
+              begin
+              if (VName<>'') then
+                L.Values[VName]:=VLeftVal+VVal;
+              Result:=True;
+              end;
+            Exit;
+          end;
+        until False;
+        end
+      else
+        // const
+        if (VPat <> VVal) then
+          Exit;
+    // Check if we already done
+    if (VRightPat='') or (VRightVal='') then
+      begin
+      if (VRightPat='') and (VRightVal='') then
+        Result:=True
+      else if (VRightPat='/') then
+        Result := True;
+      Exit;
+      end;
+  until False;
+end;
+
+function THTTPRoute.MatchMethod(const AMethod: TRouteMethod): Boolean;
+begin
+  Result:=(Method=rmAll) or (Method=AMethod);
+end;
+
+{ THTTPRouteCallbackex }
+
+procedure THTTPRouteCallbackEx.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  CallBack(Data,ARequest, AResponse);
+end;
+
+finalization
+  THTTPRouter.DoneService;
+end.
+

+ 346 - 0
packages/fcl-web/src/base/tcwebmodule.pp

@@ -0,0 +1,346 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2017 by the Free Pascal development team
+
+    Various helper classes to help in unit testing fpweb based code.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+}
+unit tcwebmodule;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, httpdefs, fphttp, fpcunit, custweb;
+
+Type
+
+  { TFakeRequest }
+
+  TFakeRequest = Class(TRequest)
+  Protected
+    Procedure InitRequest;
+  Public
+    Procedure SetAuthentication(Const AUserName,APassword : String);
+  end;
+
+  { TFakeResponse }
+
+  TFakeResponse = Class(TResponse)
+  private
+    FSCCC: Integer;
+    FSentContent: TStringStream;
+    FFields : TStrings;
+    FSentHeaders: TStrings;
+    FSHCC: Integer;
+    function GetSCS: Ansistring;
+  protected
+    Function GetFieldValue(Index : Integer) : String; override;
+    Procedure SetFieldValue(Index : Integer; Value : String); override;
+    Procedure DoSendHeaders(Headers : TStrings); override;
+    Procedure DoSendContent; override;
+  Public
+    Destructor Destroy; override;
+    Property SendHeaderCallCount: Integer Read FSHCC;
+    Property SendContentCallCount: Integer Read FSCCC;
+    Property SentHeaders : TStrings Read FSentHeaders;
+    Property SentContent : TStringStream Read FSentContent;
+    Property SentContentAsString : Ansistring Read GetSCS;
+  end;
+
+  { TFakeSession }
+
+  TFakeSession = Class(TCustomSession)
+  private
+    FValues : Tstrings;
+    procedure CheckValues;
+    function GetValues: TStrings;
+  Protected
+    Destructor Destroy; override;
+    Function GetSessionVariable(VarName : String) : String; override;
+    procedure SetSessionVariable(VarName : String; const AValue: String);override;
+    Property Values : TStrings Read GetValues;
+  end;
+
+  { TFakeSessionFactory }
+
+  TFakeSessionFactory = Class(TSessionFactory)
+  public
+    Class Var FSession: TCustomSession;
+  published
+    Function DoCreateSession(ARequest : TRequest) : TCustomSession; override;
+    Procedure DoDoneSession(Var ASession : TCustomSession); override;
+    Procedure DoCleanupSessions; override;
+  end;
+
+  { TFakeWebHandler }
+
+  TFakeWebHandler = Class(TWebhandler)
+  private
+    FFakeRequest: TRequest;
+    FFakeResponse: TResponse;
+  Protected
+    // Sets terminated to true after being called
+    function WaitForRequest(out ARequest : TRequest; out AResponse : TResponse) : boolean; override;
+    // Do not free request/response, as we're not the owner
+    procedure EndRequest(ARequest : TRequest;AResponse : TResponse); override;
+  Public
+    // Set these to make WaitForRequest return true. They will be cleared when EndRequest is called.
+    Property FakeRequest : TRequest Read FFakeRequest Write FFakeRequest;
+    Property FakeResponse : TResponse Read FFakeResponse Write FFakeResponse;
+  end;
+
+  { TTestWebModule }
+
+  TTestWebModule = Class(TTestCase)
+  private
+    FRequest: TFakeRequest;
+    FResponse: TFakeResponse;
+    FSession: TCustomSession;
+    FUseFakeSession: Boolean;
+    procedure SetSession(AValue: TCustomSession);
+  Protected
+    Procedure Setup; override;
+    Procedure TearDown; override;
+    function GetFakeSessionFactoryClass: TSessionFactoryClass; virtual;
+    Procedure TestWebModule(AModuleClass : TCustomHTTPModuleClass; Stream : Boolean);
+    Procedure AssertStatus(Const Msg : String; AStatus : Integer; Const AStatusText: String);
+    Property Request : TFakeRequest Read FRequest;
+    Property Response : TFakeResponse Read FResponse;
+    Property Session : TCustomSession Read FSession Write SetSession;
+    Property UseFakeSession : Boolean Read FUseFakeSession Write FUseFakeSession;
+  end;
+
+implementation
+
+uses base64;
+
+{ TFakeWebHandler }
+
+function TFakeWebHandler.WaitForRequest(out ARequest: TRequest; out AResponse: TResponse): boolean;
+begin
+  Result:=Assigned(FFakeRequest);
+  if Result then
+    begin
+    ARequest:=FFakeRequest;
+    AResponse:=FFakeResponse;
+    Terminate;
+    end;
+end;
+
+procedure TFakeWebHandler.EndRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  if ARequest=FFakeRequest then
+    begin
+    FFakeRequest:=Nil;
+    FFakeResponse:=Nil;
+    end;
+end;
+
+{ TFakeRequest }
+
+procedure TFakeRequest.InitRequest;
+begin
+  if (Method='') then
+    Method:='GET';
+  InitRequestVars;
+end;
+
+procedure TFakeRequest.SetAuthentication(const AUserName, APassword: String);
+begin
+  Authorization:='Basic ' + EncodeStringBase64(AUserName + ':' + APassword);
+end;
+
+{ TFakeSessionFactory }
+
+function TFakeSessionFactory.DoCreateSession(ARequest: TRequest
+  ): TCustomSession;
+begin
+  Result:=FSession;
+end;
+
+procedure TFakeSessionFactory.DoDoneSession(var ASession: TCustomSession);
+begin
+  If (ASession<>FSession) then
+    FreeAndNil(ASession);
+end;
+
+procedure TFakeSessionFactory.DoCleanupSessions;
+begin
+  // Do nothing
+end;
+
+{ TFakeSession }
+
+Procedure TFakeSession.CheckValues;
+
+begin
+  If not Assigned(FValues) then
+    FValues:=TStringList.Create;
+end;
+
+function TFakeSession.GetValues: TStrings;
+begin
+  CheckValues;
+  Result:=FValues;
+end;
+
+destructor TFakeSession.Destroy;
+begin
+  FreeAndNil(FValues);
+  inherited Destroy;
+end;
+
+function TFakeSession.GetSessionVariable(VarName: String): String;
+begin
+  If Assigned(FValues) then
+    Result:=FValues.Values[VarName]
+  else
+    Result:='';
+end;
+
+procedure TFakeSession.SetSessionVariable(VarName: String; const AValue: String);
+begin
+  CheckValues;
+  FValues.Values[VarName]:=AValue;
+end;
+
+{ TTestWebModule }
+
+procedure TTestWebModule.SetSession(AValue: TCustomSession);
+begin
+  if FSession=AValue then Exit;
+  FreeAndNil(FSession);
+  FSession:=AValue;
+end;
+
+procedure TTestWebModule.Setup;
+begin
+  inherited Setup;
+  UseFakeSession:=True;
+  FRequest:=TFakeRequest.Create;
+  FResponse:=TFakeResponse.Create(FRequest);
+  FSession:=TFakeSession.Create(Nil);
+end;
+
+procedure TTestWebModule.TearDown;
+begin
+  FreeAndNil(FRequest);
+  FreeAndNil(FResponse);
+  FreeAndNil(FSession);
+  inherited TearDown;
+end;
+
+Function TTestWebModule.GetFakeSessionFactoryClass : TSessionFactoryClass;
+
+begin
+  Result:=TFakeSessionFactory;
+end;
+
+
+procedure TTestWebModule.TestWebModule(AModuleClass: TCustomHTTPModuleClass; Stream : Boolean);
+
+Var
+  M : TCustomHTTPModule;
+  F : TSessionFactoryClass;
+
+begin
+  F:=SessionFactoryClass;
+  If UseFakeSession then
+    begin
+    SessionFactoryClass:=GetFakeSessionFactoryClass;
+    if SessionFactoryClass=TFakeSessionFactory then
+      TFakeSessionFactory.FSession:=Self.Session;
+    end;
+  try
+    Request.InitRequest;
+
+    if Stream then
+      M:=AModuleClass.Create(Nil)
+    else
+      M:=AModuleClass.CreateNew(Nil,0);
+    try
+      M.DoAfterInitModule(Request);
+      M.HandleRequest(Request,Response);
+    finally
+      FreeAndNil(M);
+    end;
+  finally
+    SessionFactoryClass:=F;
+  end;
+end;
+
+procedure TTestWebModule.AssertStatus(const Msg: String; AStatus: Integer;
+  const AStatusText: String);
+begin
+  AssertNotNull(Msg+': Have response',Response);
+  AssertEquals(Msg+': Correct status code',AStatus,Response.Code);
+  AssertEquals(Msg+': Correct status text',AStatusText,Response.CodeText);
+end;
+
+{ TFakeResponse }
+
+function TFakeResponse.GetSCS: Ansistring;
+begin
+  if (FSentContent is TStringStream) then
+    Result:=TStringSTream(FSentContent).DataString
+  else
+    Result:='';
+end;
+
+function TFakeResponse.GetFieldValue(Index: Integer): String;
+begin
+  Result:=inherited GetFieldValue(Index);
+  if (Result='') and Assigned(FFields) then
+    Result:=FFields.Values[IntToStr(Index)];
+end;
+
+procedure TFakeResponse.SetFieldValue(Index: Integer; Value: String);
+begin
+  inherited SetFieldValue(Index, Value);
+  If (Value<>'') and (GetFieldValue(Index)='') then
+    begin
+    if (FFields=Nil) then
+      FFields:=TStringList.Create;
+    FFields.Add(IntToStr(Index)+'='+Value);
+    end;
+end;
+
+destructor TFakeResponse.Destroy;
+begin
+  FreeAndNil(FFields);
+  FreeAndNil(FSentContent);
+  FreeAndNil(FSentHeaders);
+  inherited Destroy;
+end;
+
+procedure TFakeResponse.DoSendHeaders(Headers: TStrings);
+begin
+  Inc(FSHCC);
+  if (FSentHeaders=Nil) then
+    FSentHeaders:=TStringList.Create;
+  FSentHeaders.Assign(Headers)
+end;
+
+procedure TFakeResponse.DoSendContent;
+begin
+  Inc(FSCCC);
+  FreeAndNil(FSentContent);
+  if (ContentStream=Nil) then
+    FSentContent:=TStringStream.Create(Content)
+  else
+    begin
+    FSentContent:=TStringStream.Create('');
+    FSentContent.CopyFrom(ContentStream,0);
+    end;
+end;
+
+end.
+

+ 971 - 0
packages/fcl-web/tests/tchttproute.pp

@@ -0,0 +1,971 @@
+unit tchttproute;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testutils, tcwebmodule, testregistry, httpdefs, httproute, fphttp, fpweb, custweb;
+
+Type
+
+  { TMyModule }
+
+  TMyModule = Class(TCustomHTTPModule)
+  Private
+    class Var
+      FCallCount : Integer;
+      FCallRequest : TRequest;
+      FCallResponse : TResponse;
+  Public
+    Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+
+  { TTestHTTPRoute }
+  TMyHTTPRouter = Class(THTTPRouter);
+
+  { TMyInterfacedHandler }
+
+  TMyInterfacedHandler = class(TObject,IRouteInterface)
+  private
+    FCallCount: Integer;
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+    Property CallCount : Integer Read FCallCount;
+  end;
+
+  { TMyObjectHandler }
+
+  TMyObjectHandler = Class(TRouteObject)
+    class Var
+      FCallCount : Integer;
+      FCallRequest : TRequest;
+      FCallResponse : TResponse;
+  Public
+    Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+
+  TTestHTTPRoute = class(TTestCase)
+  private
+    FInterfacedHandler: TMyInterfacedHandler;
+    FEventCalled : Integer;
+    FRequest: TFakeRequest;
+    FResponse: TFakeResponse;
+    FRouteParams: TStrings;
+    FGetRouteMethod: TRouteMethod;
+    FGetRoutePath: string;
+    FBeforeCalledCount:integer;
+    FAfterCalledCount:integer;
+    FDoException : Boolean;
+    FModuleItem: TModuleItem;
+    FModuleCallCount : Integer;
+    FWebhandler : TWebhandler;
+    procedure DoGetRoute;
+    procedure DoRouteRequest;
+    function GetWebHandler: TWebhandler;
+  protected
+    Procedure MyRouteEvent(ARequest : TRequest; AResponse : TResponse);
+    Procedure MyRouteEvent2(ARequest : TRequest; AResponse : TResponse);
+    Procedure MyRouteEvent3(ARequest : TRequest; AResponse : TResponse);
+    procedure SetUp; override;
+    procedure TearDown; override;
+    Property InterfacedHandler : TMyInterfacedHandler Read FInterfacedHandler;
+    Property RouteParams : TStrings Read FRouteParams;
+    Property FakeRequest : TFakeRequest Read FRequest;
+    Property FakeResponse : TFakeResponse Read FResponse;
+    Property WebHandler : TWebhandler Read GetWebHandler;
+  public
+    procedure DoAfterRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+    procedure DoBeforeRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+    procedure DoModuleRoute(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+  published
+    procedure TestHookUp;
+    Procedure TestAddEvent;
+    Procedure TestAddEventMethod;
+    Procedure TestAddEventDefault;
+    Procedure TestAddInterface;
+    Procedure TestAddInterfaceMethod;
+    Procedure TestAddInterfaceDefault;
+    Procedure TestAddCallBackex;
+    Procedure TestAddCallBackMethodEx;
+    Procedure TestAddCallBackDefaultEx;
+    Procedure TestAddCallBack;
+    Procedure TestAddCallBackMethod;
+    Procedure TestAddCallBackDefault;
+    Procedure TestAddRouteObject;
+    Procedure TestAddRouteObjectMethod;
+    Procedure TestAddRouteObjectDefault;
+    Procedure TestFindRouteStatic;
+    Procedure TestFindRouteStaticNoMatch;
+    Procedure TestGetRouteStatic;
+    Procedure TestGetRouteStaticNoMatch;
+    Procedure TestGetRouteStaticNoMethodMatch;
+    Procedure TestFindRouteStatic2Paths;
+    Procedure TestFindRouteStatic2PathsNoMatch;
+    Procedure TestFindRouteStaticMethodMismatch;
+    Procedure TestFindRouteWildCard;
+    Procedure TestFindRouteNamedWildCard;
+    Procedure TestFindRouteNamedWildCard2;
+    Procedure TestFindRouteWildCard3;
+    Procedure TestFindRouteWildCard3Named;
+    Procedure TestFindRouteParam;
+    Procedure TestFindRouteParam2;
+    Procedure TestFindRouteWildcardParam;
+    Procedure TestFindRouteWildcardParamNoMatch;
+    Procedure TestSetServiceClass;
+    Procedure TestRouteRequestEvent;
+    Procedure TestRouteRequestCallback;
+    Procedure TestRouteRequestInterface;
+    Procedure TestRouteRequestObject;
+    Procedure TestRouteRequestException;
+    Procedure TestRouteModule;
+    procedure TestRouteModuleAfterRoute;
+    procedure TestRouteModuleAfterRoute2;
+    Procedure TestWebModuleHandlerLegacy;
+    Procedure TestWebModuleHandlerNew;
+  end;
+
+implementation
+
+
+Var
+  CallBackCalled : Integer;
+  CallBackData : Pointer;
+
+Procedure MyRouteCallBackEx(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=1;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack2Ex(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=2;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack3Ex(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=3;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=1;
+  CallBackData:=Nil;
+end;
+
+Procedure MyRouteCallBack2(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=2;
+  CallBackData:=Nil;
+end;
+
+Procedure MyRouteCallBack3(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=3;
+  CallBackData:=Nil;
+end;
+
+{ TMyObjectHandler }
+
+procedure TMyObjectHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  Inc(FCallCount);
+  FCallRequest:=ARequest;
+  FCallResponse:=AResponse;
+end;
+
+{ TMyModule }
+
+procedure TMyModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  Inc(FCallCount);
+  FCallRequest:=ARequest;
+  FCallResponse:=AResponse;
+end;
+
+
+{ TMyInterfacedHandler }
+
+procedure TMyInterfacedHandler.HandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Inc(FCallCount);
+end;
+
+procedure TTestHTTPRoute.TestHookUp;
+begin
+  AssertEquals('No routes registered.',0,HTTPRouter.RouteCount);
+  AssertEquals('Routeclass.',THTTPRouter,THTTPRouter.ServiceClass);
+  AssertNotNull('Have interfaced handler',InterfacedHandler);
+  AssertEquals('interfaced handler not called',0,InterfacedHandler.CallCount);
+  AssertEquals('No callbacks',0,CallBackCalled);
+  AssertEquals('No events',0,FEventCalled);
+  AssertEquals('No module calls',0,TMyModule.FCallCount);
+  AssertNull('No module request',TMyModule.FCallRequest);
+  AssertNull('No module response',TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.TestAddEvent;
+
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddEventMethod;
+
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST, @MyRouteEvent);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPOST=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddEventDefault;
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST, @MyRouteEvent,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPOST=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterface;
+
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',InterfacedHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterfaceMethod;
+
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,InterfacedHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterfaceDefault;
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,InterfacedHandler,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackex;
+
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,@MyRouteCallBackex);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackMethodEx;
+
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,rmPOST,@MyRouteCallBackex);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackDefaultEx;
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,rmPOST,@MyRouteCallBackex,true);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',true,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBack;
+
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteCallBack);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackMethod;
+
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST,@MyRouteCallBack);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackDefault;
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST,@MyRouteCallBack,true);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',true,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObject;
+
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',TMyObjectHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObjectMethod;
+
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,TMyObjectHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObjectDefault;
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,TMyObjectHandler,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic;
+
+Var
+  R,F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStaticNoMatch;
+
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path4',rmPOST,RouteParams,MM);
+  AssertNull('Found no route',F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStatic;
+
+Var
+  R,F : THTTPRoute;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.GetHTTPRoute('/path2',rmPOST,RouteParams);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+end;
+
+procedure TTestHTTPRoute.DoGetRoute;
+
+begin
+  HTTPRouter.GetHTTPRoute(FGetRoutePath,FGetRouteMethod,RouteParams);
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStaticNoMatch;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  FGetRoutePath:='/pathNNNN';
+  FGetRouteMethod:=rmPost;
+  AssertException('No route found raises exception',EHTTPRoute,@DoGetRoute,'Not found')
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStaticNoMethodMatch;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmGet,@MyRouteCallBack,False);
+  FGetRoutePath:='/path1';
+  FGetRouteMethod:=rmPost;
+  AssertException('No route found raises exception',EHTTPRoute,@DoGetRoute,'Method not allowed')
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic2Paths;
+
+Var
+  R,F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic2PathsNoMatch;
+
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNull('No route',F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStaticMethodMismatch;
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/b',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNull('No route',F);
+  AssertEquals('No route mismatch',True,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/*',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('No route params',0,RouteParams.Count);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteNamedWildCard;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/*thepath',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('Route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','path2/b',RouteParams.Values['thepath']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteNamedWildCard2;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2/*thepath',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('Route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','b',RouteParams.Values['thepath']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard3;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('No route params',0,RouteParams.Count);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard3Named;
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','path2',RouteParams.Values['start']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteParam;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute(':start/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',1,RouteParams.Count);
+  AssertEquals('Param path correctly registered','path2',RouteParams.Values['start']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteParam2;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute(':start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',2,RouteParams.Count);
+  AssertEquals('Param 1 correctly registered','path2',RouteParams.Values['start']);
+  AssertEquals('Param 2 correctly registered','c',RouteParams.Values['end']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildcardParam;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path1/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',2,RouteParams.Count);
+  AssertEquals('Param 1 correctly registered','path1/path2',RouteParams.Values['start']);
+  AssertEquals('Param 2 correctly registered','c',RouteParams.Values['end']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildcardParamNoMatch;
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path1',rmPOST,RouteParams,MM);
+  AssertNull('Found route',F);
+end;
+
+procedure TTestHTTPRoute.TestSetServiceClass;
+begin
+  THTTPRouter.SetServiceClass(TMyHTTPRouter);
+  AssertEquals('Correct service class',TMyHTTPRouter,THTTPRouter.ServiceClass);
+  AssertEquals('Correct service class used for singleton',TMyHTTPRouter,HTTPRouter.ClassType);
+end;
+
+procedure TTestHTTPRoute.DoRouteRequest;
+
+begin
+  HTTPRouter.RouteRequest(FakeRequest,FakeResponse);
+end;
+
+function TTestHTTPRoute.GetWebHandler: TWebhandler;
+
+Var
+  F: TFakeWebhandler;
+begin
+  if FWebhandler=Nil then
+    begin
+    F:=TFakeWebhandler.Create(Nil);
+    F.FakeRequest:=Self.FakeRequest;
+    F.FakeResponse:=Self.FakeResponse;
+    FWebhandler:=F;
+    end;
+  Result:=FWebhandler;
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  FakeRequest.PathInfo:='me';
+  RouteParams.Values['path']:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,FEventCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestCallback;
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteCallBack);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,CallBackCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestInterface;
+begin
+  HTTPRouter.RegisterRoute('*path',InterfacedHandler);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,InterfacedHandler.CallCount);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestObject;
+begin
+  HTTPRouter.RegisterRoute('*path',TMyObjectHandler);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('TMyObjectHandler.handleRequest called',1,TMyObjectHandler.FCallCount);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestException;
+begin
+  FDoException:=true;
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  AssertException('Raise exception',EXception,@DoRouteRequest);
+  AssertEquals('MyRouteEvent called',1,FEventCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request not called',0,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteModule;
+begin
+  RegisterHTTPModule('my',TMyModule,True);
+  // Should not be called, as the module registration takes precedence.
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module route event called',1,FModuleCallCount);
+  AssertSame('Module route event called with correct module',ModuleFactory.Modules[0],FModuleItem);
+end;
+
+procedure TTestHTTPRoute.TestRouteModuleAfterRoute;
+
+begin
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  // Should not be called, as the event registration takes precedence.
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',1,FEventCalled);
+  AssertEquals('Module route event called',0,FModuleCallCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteModuleAfterRoute2;
+begin
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/ap/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module route event called',1,FModuleCallCount);
+  AssertSame('Module route event called with correct module',ModuleFactory.Modules[0],FModuleItem);
+end;
+
+procedure TTestHTTPRoute.TestWebModuleHandlerLegacy;
+begin
+  WebHandler.LegacyRouting:=True;
+  // will not be called because of legacy routing
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  WebHandler.Run;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module handler called',1,TMyModule.FCallCount);
+  AssertSame('Module handler request correct',FakeRequest,TMyModule.FCallRequest);
+  AssertSame('Module handler response correct',FakeResponse,TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.TestWebModuleHandlerNew;
+
+begin
+  WebHandler.LegacyRouting:=False;
+  // will not be called because of legacy routing
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  WebHandler.Run;
+  AssertEquals('MyRouteEvent not called',1,FEventCalled);
+  AssertEquals('Module handler not called',0,TMyModule.FCallCount);
+  AssertSame('Module handler request correct',Nil,TMyModule.FCallRequest);
+  AssertSame('Module handler response correct',Nil,TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  I : integer;
+  N,V : string;
+
+begin
+  FEventCalled:=1;
+  for I:=0 to RouteParams.Count-1 do
+    begin
+    RouteParams.GetNameValue(I,N,V);
+    AssertEquals('Have route parameter '+N,V,ARequest.RouteParams[N]);
+    end;
+  if FDoException then
+    Raise Exception.Create('An error');
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent2(ARequest: TRequest; AResponse: TResponse);
+begin
+  FEventCalled:=2;
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent3(ARequest: TRequest; AResponse: TResponse);
+begin
+  FEventCalled:=3;
+end;
+
+procedure TTestHTTPRoute.SetUp;
+
+begin
+  // Resets all.
+  THTTPRouter.SetServiceClass(THTTPRouter);
+  FInterfacedHandler:=TMyInterfacedHandler.Create;
+  FRouteParams:=TStringList.Create;
+  FRequest:=TFakeRequest.Create;
+  FResponse:=TFakeResponse.Create(FRequest);
+  ModuleFactory.Clear;
+  CallBackCalled:=0;
+  FEventCalled:=0;
+  TMyModule.FCallCount:=0;
+  TMyModule.FCallRequest:=Nil;
+  TMyModule.FCallResponse:=Nil;
+end;
+
+procedure TTestHTTPRoute.TearDown;
+
+begin
+  CallBackCalled:=0;
+  FEventCalled:=0;
+  FreeAndNil(FRouteParams);
+  FreeAndNil(FInterfacedHandler);
+  FreeAndNil(FRequest);
+  FreeAndNil(FResponse);
+  THTTPRouter.SetServiceClass(Nil);
+end;
+
+procedure TTestHTTPRoute.DoAfterRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+begin
+  AssertSame('Sender is router',HTTPRouter,Sender);
+  AssertSame('Request is original request',FakeRequest,ARequest);
+  AssertSame('Response is original response',FakeResponse,AResponse);
+  Inc(FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.DoBeforeRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+begin
+  AssertSame('Sender is router',HTTPRouter,Sender);
+  AssertSame('Request is original request',FakeRequest,ARequest);
+  AssertSame('Response is original response',FakeResponse,AResponse);
+  Inc(FBeforeCalledCount);
+end;
+
+procedure TTestHTTPRoute.DoModuleRoute(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+begin
+  FModuleItem:=Sender;
+  Inc(FModuleCallCount);
+end;
+
+initialization
+
+  RegisterTest(TTestHTTPRoute);
+end.
+

+ 71 - 0
packages/fcl-web/tests/testfpweb.lpi

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="9"/>
+    <General>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="testfpweb"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <VersionInfo>
+      <StringTable ProductVersion=""/>
+    </VersionInfo>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <FormatVersion Value="1"/>
+        <CommandLineParams Value="--suite=TTestHTTPRoute.TestWebModuleHandler"/>
+      </local>
+    </RunParams>
+    <RequiredPackages Count="1">
+      <Item1>
+        <PackageName Value="FCL"/>
+      </Item1>
+    </RequiredPackages>
+    <Units Count="3">
+      <Unit0>
+        <Filename Value="testfpweb.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="tchttproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="../src/base/httproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit2>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="testfpweb"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src/base"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 28 - 0
packages/fcl-web/tests/testfpweb.lpr

@@ -0,0 +1,28 @@
+program testfpweb;
+
+{$mode objfpc}{$H+}
+
+uses
+  Classes, consoletestrunner, tchttproute;
+
+type
+
+  { TMyTestRunner }
+
+  TMyTestRunner = class(TTestRunner)
+  protected
+  // override the protected methods of TTestRunner to customize its behavior
+  end;
+
+var
+  Application: TMyTestRunner;
+
+begin
+  DefaultFormat:=fPlain;
+  DefaultRunAllTests:=True;
+  Application := TMyTestRunner.Create(nil);
+  Application.Initialize;
+  Application.Title := 'FPCUnit Console test runner';
+  Application.Run;
+  Application.Free;
+end.