/// implements MVC patterns over mORMot's ORM/SOA and SynMustache
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotMVC;

{
    This file is part of Synopse mORMot framework.

    Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - achechulin

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

}

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

uses
  {$ifdef MSWINDOWS}
  Windows,
  {$else}
  {$ifdef FPC}
  SynFPCLinux,
  {$endif}
  {$ifdef KYLIX3}
  Types,
  LibC,
  SynKylix,
  {$endif}
  {$endif}
  SysUtils,
  Classes,
  {$ifndef LVCL}
  Contnrs,
  {$endif}
  Variants,
  SynCommons,
  SynLog,
  SynCrypto,
  SynMustache,
  mORMot,
  mORMotWrappers;

{ ====== Views ====== }

type
  /// TMVCView.Flags rendering context
  TMVCViewFlags = set of (viewHasGenerationTimeTag);

  /// define a particular rendered View
  // - as rendered by TMVCViewsAbstract.Render() method
  TMVCView = record
    /// the low-level content of this View
    Content: RawByteString;
    /// the MIME content type of this View
    ContentType: RawUTF8;
    /// some additional rendering information about this View
    Flags: TMVCViewFlags;
  end;

  /// an abstract class able to implement Views
  TMVCViewsAbstract = class
  protected
    fFactory: TInterfaceFactory;
    fLogClass: TSynLogClass;
    fViewTemplateFolder, fViewStaticFolder: TFileName;
    fFactoryErrorIndex: integer;
    fViewFlags: TMVCViewFlags;
    fViewGenerationTimeTag: RawUTF8;
    procedure SetViewTemplateFolder(const aFolder: TFileName);
    /// overriden implementations should return the rendered content
    procedure Render(methodIndex: Integer; const Context: variant;
      var View: TMVCView); virtual; abstract;
    /// return the static file contents - from fViewStaticFolder by default
    // - called if cacheStatic has been defined
    function GetStaticFile(const aFileName: TFileName): RawByteString; virtual;
  public
    /// initialize the class
    constructor Create(aInterface: PTypeInfo; aLogClass: TSynLogClass);
    /// read-only access to the associated factory for the implementation class
    property Factory: TInterfaceFactory read fFactory;
    /// read-only access to the local folder containing the Mustache views
    property ViewTemplateFolder: TFileName read fViewTemplateFolder write SetViewTemplateFolder;
    /// retrieve the .static local folder name
    property ViewStaticFolder: TFileName read fViewStaticFolder;
    /// any occurence of this tag in a rendered view will be converted
    // into the rendering time in microseconds
    // - equals '[[GENERATION_TIME_TAG]]' by default
    property ViewGenerationTimeTag: RawUTF8 read fViewGenerationTimeTag write fViewGenerationTimeTag;
  end;

  /// general parameters defining the Mustache Views process
  // - used as a separate value so that we would be able to store the
  // settings in a file, e.g. encoded as a JSON object
  TMVCViewsMustacheParameters = record
    /// where the mustache template files are stored
    // - if not set, will search in a 'Views' folder under the current executable
    Folder: TFileName;
    /// the file extensions to search in the given Folder, specified as CSV
    // - if not set, will search for 'html,json,css'
    CSVExtensions: TFileName;
    /// defines if the view files should be checked for modification
    // - any value would automatically update the rendering template, if the file
    // changed after a given number of seconds - default is 5 seconds
    // - setting 0 would be slightly faster, since content would never be checked
    FileTimestampMonitorAfterSeconds: cardinal;
    /// file extension (e.g. '.html') to be used to create void templates
    // - default '' will create no void template file in the given Folder
    ExtensionForNotExistingTemplate: TFileName;
    /// set of block helpers to be registered to TSynMustache
    // - default will use TSynMustache.HelpersGetStandardList definition
    Helpers: TSynMustacheHelpers;
  end;

  /// a class able to implement Views using Mustache templates
  TMVCViewsMustache = class(TMVCViewsAbstract)
  protected
    fViewTemplateFileTimestampMonitor: cardinal;
    fViewPartials: TSynMustachePartials;
    fViewHelpers: TSynMustacheHelpers;
    fViews: array of record // follows fFactory.Methods[]
      Mustache: TSynMustache;
      Template: RawUTF8;
      MethodName: TFileName;
      SearchPattern: TFileName;
      FileName: TFileName;
      ShortFileName: TFileName;
      FileExt: TFileName;
      ContentType: RawUTF8;
      Locker: IAutoLocker;
      FileAgeLast: PtrUInt;
      FileAgeCheckTick: Int64;
      Flags: TMVCViewFlags;
    end;
    function GetRenderer(methodIndex: integer; var view: TMVCView): TSynMustache;
    /// search for template files in ViewTemplateFolder
    function FindTemplates(const Mask: TFileName): TFileNameDynArray; virtual;
    /// return the template file contents
    function GetTemplate(const aFileName: TFileName): RawUTF8; virtual;
    /// return the template file date and time
    function GetTemplateAge(const aFileName: TFileName): PtrUInt; virtual;
    /// overriden implementations should return the rendered content
    procedure Render(methodIndex: Integer; const Context: variant; var View: TMVCView); override;
    // some helpers defined here to avoid SynCrypto link in SynMustache
    class procedure md5(const Value: variant; out result: variant);
    class procedure sha1(const Value: variant; out result: variant);
    class procedure sha256(const Value: variant; out result: variant);
  public
    /// create an instance of this ViewModel implementation class
    // - define the associated REST instance, the interface definition and the
    // local folder where the mustache template files are stored
    // - will search and parse the matching views (and associated *.partial)
    constructor Create(aInterface: PTypeInfo;
      const aParameters: TMVCViewsMustacheParameters;
      aLogClass: TSynLogClass=nil); reintroduce; overload; virtual;
    /// create an instance of this ViewModel implementation class
    // - this overloaded version will use default parameters (i.e. search for
    // html+json+css in the "Views" sub-folder under the executable)
    // - will search and parse the matching views (and associated *.partial),
    // optionally creating void templates for any missing view
    constructor Create(aInterface: PTypeInfo; aLogClass: TSynLogClass=nil;
      aExtensionForNotExistingTemplate: TFileName=''); overload;
    /// define the supplied Expression Helpers definition
    // - returns self so that may be called in a fluent interface
    function RegisterExpressionHelpers(const aNames: array of RawUTF8;
      const aEvents: array of TSynMustacheHelperEvent): TMVCViewsMustache;
    /// define Expression Helpers for some ORM tables
    // - e.g. to read a TSQLMyRecord from its ID value and put its fields
    // in the current rendering data context, you can write:
    // ! aView.RegisterExpressionHelpersForTables(aServer,[TSQLMyRecord]);
    // then use the following Mustache tag
    // ! {{#TSQLMyRecord MyRecordID}} ... {{/TSQLMyRecord MyRecordID}}
    // - returns self so that may be called in a fluent interface
    function RegisterExpressionHelpersForTables(aRest: TSQLRest;
      const aTables: array of TSQLRecordClass): TMVCViewsMustache; overload;
    /// define Expression Helpers for all ORM tables of the supplied model
    // - e.g. to read a TSQLMyRecord from its ID value and put its fields
    // in the current rendering data context, you can write:
    // ! aView.RegisterExpressionHelpersForTables(aServer);
    // then use the following Mustache tag
    // ! {{#TSQLMyRecord MyRecordID}} ... {{/TSQLMyRecord MyRecordID}}
    // - returns self so that may be called in a fluent interface
    function RegisterExpressionHelpersForTables(
      aRest: TSQLRest): TMVCViewsMustache; overload;
    /// define some Expression Helpers for hashing
    // - i.e. md5, sha1 and sha256 hashing
    // - would allow e.g. to compute a Gravatar URI via:
    // ! <img src=http://www.gravatar.com/avatar/{{md5 email}}?s=200></img>
    // - returns self so that may be called in a fluent interface
    function RegisterExpressionHelpersForCrypto: TMVCViewsMustache;

    /// finalize the instance
    destructor Destroy; override;
  end;


{ ====== Sessions ====== }

type
  /// an abstract class able to implement ViewModel/Controller sessions
  // - see TMVCSessionWithCookies to implement cookie-based sessions
  // - this kind of ViewModel will implement client side storage of sessions,
  // storing any (simple) record content on the browser client side
  // - at login, a record containing session-related information (session ID,
  // display and login name, preferences, rights...) can be computed only once
  // on the server side from the Model, then stored on the client side (typically
  // in a cookie): later on, session information can be retrieved by the server
  // logic (via CheckAndRetrieve - note that any security attribute should be
  // verified against the Model), then the renderer (CheckAndRetrieveInfo
  // returning the record as TDocVariant in the data context "Session" field) -
  // such a pattern is very efficient and allows good scaling
  // - session are expected to be tied to the TMVCSessionAbstract instance
  // lifetime, so are lost after server restart, unless they are persisted
  // via LoadContext/SaveContext methods
  TMVCSessionAbstract = class
  public
    /// create an instance of this ViewModel implementation class
    constructor Create; virtual;
    /// will create a new session
    // - setting an optional record data, and returning the internal session ID
    // - you can supply a time period, after which the session will expire -
    // default is 1 hour - note that overriden methods may not implement it
    function Initialize(PRecordData: pointer=nil; PRecordTypeInfo: pointer=nil;
      SessionTimeOutMinutes: cardinal=60): integer; virtual; abstract;
    /// fast check if there is a session associated to the current context
    function Exists: boolean; virtual; abstract;
    /// retrieve the current session ID
    // - can optionally retrieve the associated record Data parameter
    function CheckAndRetrieve(PRecordData: pointer=nil; PRecordTypeInfo: pointer=nil;
      PExpires: PCardinal=nil): integer; virtual; abstract;
    /// retrieve the session information as a JSON object
    // - returned as a TDocVariant, including any associated record Data
    // - will call CheckAndRetrieve() then RecordSaveJSON() and _JsonFast()
    function CheckAndRetrieveInfo(PRecordDataTypeInfo: pointer): variant; virtual;
    /// clear the session
    procedure Finalize; virtual; abstract;
    /// return all session generation information as ready-to-be stored string
    // - to be retrieved via LoadContext, e.g. after restart
    function SaveContext: RawUTF8; virtual; abstract;
    /// restore session generation information from SaveContext format
    // - returns TRUE on success
    function LoadContext(const Saved: RawUTF8): boolean; virtual; abstract;
  end;

  /// information used by TMVCSessionWithCookies for cookie generation
  // - i.e. the session ID, cookie name, encryption and HMAC secret keys
  // - this data can be persisted so that the very same cookie information
  // are available after server restart
  TMVCSessionWithCookiesContext = packed record
    /// the cookie name, used for storage on the client side
    CookieName: RawUTF8;
    /// an increasing counter, to implement unique session ID
    SessionSequence: integer;
    /// secret information, used for HMAC digital signature of cookie content
    Secret: THMAC_CRC32C;
    /// random IV used as CTR on Crypt[] secret key
    CryptNonce: Cardinal;
    /// secret information, used for encryption of the cookie content
    Crypt: array[byte] of byte;
  end;

  /// a class able to implement ViewModel/Controller sessions with cookies
  // - this kind of ViewModel will implement cookie-based sessions, able to
  // store any (simple) record content in the cookie, on the browser client side
  // - those cookies have the same feature set than JWT, but with a lower
  // payload (thanks to binary serialization), and cookie safety (not accessible
  // from JavaScript): they are digitally signed (with HMAC-CRC32C and a
  // temporary secret key), they include an unique session identifier (like
  // "jti" claim), issue and expiration dates (like "iat" and "exp" claims),
  // and they are encrypted with a temporary key - this secret keys is tied to
  // the TMVCSessionWithCookies instance lifetime, so new cookies are generated
  // after server restart, unless they are persisted via LoadContext/SaveContext
  // - signature and encryption are weak, but very fast, to avoid DDOS attacks
  TMVCSessionWithCookies = class(TMVCSessionAbstract)
  protected
    fContext: TMVCSessionWithCookiesContext;
    // overriden e.g. in TMVCSessionWithRestServer using ServiceContext threadvar
    function GetCookie: RawUTF8; virtual; abstract;
    procedure SetCookie(const cookie: RawUTF8); virtual; abstract;
    procedure Crypt(P: PAnsiChar; bytes: integer);
    function CheckAndRetrieveFromCookie(const cookie: RawUTF8;
      PRecordData, PRecordTypeInfo: pointer; PExpires: PCardinal): integer;
  public
    /// create an instance of this ViewModel implementation class
    constructor Create; override;
    /// will initialize the session cookie
    // - setting an optional record data, which will be stored Base64-encoded
    // - will return the 32-bit internal session ID
    // - you can supply a time period, after which the session will expire -
    // default is 1 hour, and could go up to
    function Initialize(PRecordData: pointer=nil; PRecordTypeInfo: pointer=nil;
      SessionTimeOutMinutes: cardinal=60): integer; override;
    /// fast check if there is a cookie session associated to the current context
    function Exists: boolean; override;
    /// retrieve the session ID from the current cookie
    // - can optionally retrieve the record Data parameter stored in the cookie
    // - will return the 32-bit internal session ID, or 0 if the cookie is invalid
    function CheckAndRetrieve(PRecordData: pointer=nil; PRecordTypeInfo: pointer=nil;
      PExpires: PCardinal=nil): integer; override;
    /// clear the session
    // - by deleting the cookie on the client side
    procedure Finalize; override;
    /// return all cookie generation information as base64 encoded text
    // - to be retrieved via LoadContext
    function SaveContext: RawUTF8; override;
    /// restore cookie generation information from SaveContext text format
    // - returns TRUE after checking the crc and unserializing the supplied data
    // - WARNING: if the unerlying record type structure changed (i.e. any
    // field is modified or added), restoration will lead to data corruption of
    // low-level binary content, then trigger unexpected GPF: if you change the
    // record type definition, do NOT use LoadContext - and reset all cookies
    function LoadContext(const Saved: RawUTF8): boolean; override;
    /// direct access to the low-level information used for cookies generation
    // - use SaveContext and LoadContext methods to persist this information
    // before server shutdown, so that the cookies can be re-used after restart
    property Context: TMVCSessionWithCookiesContext read fContext write fContext;
    /// you can customize the cookie name
    // - default is 'mORMot', and cookie is restricted to Path=/RestRoot
    property CookieName: RawUTF8 read fContext.CookieName write fContext.CookieName;
  end;

  /// implement a ViewModel/Controller sessions in a TSQLRestServer instance
  // - will use ServiceContext.Request threadvar to access the client cookies
  TMVCSessionWithRestServer = class(TMVCSessionWithCookies)
  protected
    function GetCookie: RawUTF8; override;
    procedure SetCookie(const cookie: RawUTF8); override;
  end;

  /// implement a single ViewModel/Controller in-memory session
  // - this kind of session could be used in-process, e.g. for a VCL/FMX GUI
  // - do NOT use it with multiple clients, e.g. from HTTP remote access
  TMVCSessionSingle = class(TMVCSessionWithCookies)
  protected
    fSingleCookie: RawUTF8;
    function GetCookie: RawUTF8; override;
    procedure SetCookie(const cookie: RawUTF8); override;
  end;


  { ====== Application Run ====== }

  /// record type to define commands e.g. to redirect to another URI
  // - do NOT access those record property directly, but rather use
  // TMVCApplication.GotoView/GotoError/GotoDefault methods, e.g.
  // !  function TBlogApplication.Logout: TMVCAction;
  // !  begin
  // !    CurrentSession.Finalize;
  // !    GotoDefault(result);
  // !  end;
  // - this record type should match exactly TServiceCustomAnswer layout,
  // so that TServiceMethod.InternalExecute() would handle it directly
  TMVCAction = record
    /// the method name to be executed
    RedirectToMethodName: RawUTF8;
    /// may contain a JSON object which will be used to specify parameters
    // to the specified method
    RedirectToMethodParameters: RawUTF8;
    /// which HTTP Status code should be returned
    // - if RedirectMethodName is set, will return 307 HTTP_TEMPORARYREDIRECT
    // by default, but you can set here the expected HTTP Status code, e.g.
    // 201 HTTP_CREATED or 404 HTTP_NOTFOUND
    ReturnedStatus: cardinal;
  end;

  TMVCApplication = class;

  /// abstract MVC rendering execution context
  // - you shoud not execute this abstract class, but any of the inherited class
  // - one instance inherited from this class would be allocated for each event
  // - may return some data (when inheriting from TMVCRendererReturningData), or
  // even simply display the value in a VCL/FMX GUI, without any output
  TMVCRendererAbstract = class
  protected
    fApplication: TMVCApplication;
    fMethodIndex: integer;
    fMethodReturnsAction: boolean;
    fInput: RawUTF8;
    procedure Renders(var outContext: variant; status: cardinal;
      forcesError: boolean); virtual; abstract;
    function Redirects(const action: TMVCAction): boolean; virtual;
    procedure CommandError(const ErrorName: RawUTF8; const ErrorValue: variant;
      ErrorCode: Integer); virtual;
  public
    /// initialize a rendering process for a given MVC Application/ViewModel
    constructor Create(aApplication: TMVCApplication); reintroduce;
    /// main execution method of the rendering process
    // - Input should have been set with the incoming execution context
    procedure ExecuteCommand(aMethodIndex: integer); virtual;
    /// incoming execution context, to be processed via ExecuteCommand() method
    // - should be specified as a raw JSON object
    property Input: RawUTF8 read fInput write fInput;
  end;

  /// how TMVCRendererReturningData should cache its content
  TMVCRendererCachePolicy = (
    cacheNone,
    cacheRootIgnoringSession, cacheRootIfSession, cacheRootIfNoSession,
    cacheRootWithSession,
    cacheWithParametersIgnoringSession, cacheWithParametersIfSession,
    cacheWithParametersIfNoSession);

  TMVCRunWithViews = class;

  /// abstract MVC rendering execution context, returning some content
  // - the Output property would contain the content to be returned
  // - can be used to return e.g. some rendered HTML or some raw JSON,
  // or even some server-side generated report as PDF, using our mORMotReport.pas
  TMVCRendererReturningData = class(TMVCRendererAbstract)
  protected
    fRun: TMVCRunWithViews;
    fOutput: TServiceCustomAnswer;
    fOutputFlags: TMVCViewFlags;
    fCacheEnabled: boolean;
    fCacheCurrent: (noCache, rootCache, inputCache);
    fCacheCurrentSec: cardinal;
    fCacheCurrentInputValueKey: RawUTF8;
    function Redirects(const action: TMVCAction): boolean; override;
  public
    /// initialize a rendering process for a given MVC Application/ViewModel
    // - you need to specify a MVC Views engine, e.g. TMVCViewsMustache instance
    constructor Create(aRun: TMVCRunWithViews); reintroduce; virtual;
    /// main execution method of the rendering process
    // - this overriden method would handle proper caching as defined by
    // TMVCRunWithViews.SetCache()
    procedure ExecuteCommand(aMethodIndex: integer); override;
    /// caller should retrieve this value after ExecuteCommand method execution
    property Output: TServiceCustomAnswer read fOutput;
  end;

  TMVCRendererReturningDataClass = class of TMVCRendererReturningData;

  /// MVC rendering execution context, returning some rendered View content
  // - will use an associated Views templates system, e.g. a Mustache renderer
  TMVCRendererFromViews = class(TMVCRendererReturningData)
  protected
    // Renders() will fill Output using the corresponding View, to be sent back
    procedure Renders(var outContext: variant; status: cardinal; forcesError: boolean); override;
  public
    /// initialize a rendering process for a given MVC Application/ViewModel
    // - this overriden constructor will ensure that cache is enabled
    constructor Create(aRun: TMVCRunWithViews); override;
  end;

  /// MVC rendering execution context, returning some un-rendered JSON content
  // - may be used e.g. for debugging purpose
  // - for instance, TMVCRunOnRestServer will return such context with the
  // supplied URI ends with '/json' (e.g. for any /root/method/json request)
  TMVCRendererJson = class(TMVCRendererReturningData)
  protected
    // Renders() will fill Output with the outgoing JSON, to be sent back
    procedure Renders(var outContext: variant; status: cardinal; forcesError: boolean); override;
  end;

  /// abstract class used by TMVCApplication to run
  // - a single TMVCApplication logic may handle several TMVCRun instances
  TMVCRun = class
  protected
    fApplication: TMVCApplication;
  public
    /// link this runner class to a specified MVC application
    // - will also reset the associated Application.Session instance
    constructor Create(aApplication: TMVCApplication); reintroduce;
    /// method called to flush the caching mechanism for all MVC commands
    procedure NotifyContentChanged; virtual;
    /// you may call this method to flush any caching mechanism for a MVC command
    procedure NotifyContentChangedForMethod(aMethodIndex: integer); overload; virtual;
    /// you may call this method to flush any caching mechanism for a MVC command
    procedure NotifyContentChangedForMethod(const aMethodName: RawUTF8); overload;
    /// read-write access to the associated MVC Application/ViewModel instance
    property Application: TMVCApplication read fApplication write fApplication;
  end;

  /// abstract class used by TMVCApplication to run TMVCViews-based process
  // - this inherited class will host a MVC Views instance, and handle
  // an optional simple in-memory cache
  TMVCRunWithViews = class(TMVCRun)
  protected
    fViews: TMVCViewsAbstract;
    fCacheLocker: IAutoLocker;
    fCache: array of record // fCache[MethodIndex]
      Policy: TMVCRendererCachePolicy;
      TimeOutSeconds: cardinal;
      RootValue: RawUTF8;
      RootValueExpirationTime: cardinal;
      InputValues: TSynNameValue;
    end;
  public
    /// link this runner class to a specified MVC application
    constructor Create(aApplication: TMVCApplication;
      aViews: TMVCViewsAbstract=nil); reintroduce;
    /// method called to flush the caching mechanism for a MVC command
    procedure NotifyContentChangedForMethod(aMethodIndex: integer); override;
    /// defines the caching policy for a given MVC command
    // - a time expiration period (up to 5 minutes) can also be defined per
    // MVC command - leaving default 0 will set to 5 minutes expiration delay
    // - function calls can be chained to create some fluent definition interface
    // like in TAnyBLogapplication.Create:
    // ! fMainRunner := TMVCRunWithViews.Create(self).SetCache('default',cacheRoot);
    function SetCache(const aMethodName: RawUTF8; aPolicy: TMVCRendererCachePolicy;
      aTimeOutSeconds: cardinal=0): TMVCRunWithViews; virtual;
    /// finalize this instance
    destructor Destroy; override;
    /// read-write access to the associated MVC Views instance
    property Views: TMVCViewsAbstract read fViews;
  end;

  /// the kinds of optional content which may be published
  // - publishMvcInfo will define a /root/[aSubURI/]mvc-info HTML page,
  // which is pretty convenient when working with views
  // - publishStatic will define a /root/[aSubURI/].static sub-folder,
  // ready to serve any file available in the Views\.static local folder,
  // via an in-memory cache (if cacheStatic is also defined)
  // - cacheStatic enables an in-memory cache of publishStatic files; if not set,
  // TSQLRestServerURIContext.ReturnFile is called to avoid buffering, which may
  // be a better solution on http.sys or if NGINX's X-Accel-Redirect header is set
  // - registerORMTableAsExpressions will register Mustache Expression Helpers
  // for every TSQLRecord table of the Server data model
  // - by default, TSQLRestServer authentication would be by-passed for all
  // MVC routes, unless bypassAuthentication option is undefined
  TMVCPublishOption = (publishMvcInfo, publishStatic, cacheStatic,
    registerORMTableAsExpressions, bypassAuthentication);

  /// which kind of optional content should be publish
  TMVCPublishOptions = set of TMVCPublishOption;

  /// run TMVCApplication directly within a TSQLRestServer method-based service
  // - this is the easiest way to host and publish a MVC Application, optionally
  // in conjunction with REST/AJAX client access
  TMVCRunOnRestServer = class(TMVCRunWithViews)
  protected
    fRestServer: TSQLRestServer;
    fPublishOptions: TMVCPublishOptions;
    fMvcInfoCache: RawUTF8;
    fStaticCache: TSynNameValue;
    fStaticCacheControlMaxAge: integer;
    /// callback used for the rendering on the TSQLRestServer
    procedure RunOnRestServerRoot(Ctxt: TSQLRestServerURIContext);
    procedure RunOnRestServerSub(Ctxt: TSQLRestServerURIContext);
    procedure InternalRunOnRestServer(Ctxt: TSQLRestServerURIContext;
      const MethodName: RawUTF8);
  public
    /// this constructor will publish some views to a TSQLRestServer instance
    // - the associated RestModel can match the supplied TSQLRestServer, or be
    // another instance (if the data model is not part of the publishing server)
    // - all TMVCApplication methods would be registered to the TSQLRestServer,
    // as /root/methodName if aSubURI is '', or as /root/aSubURI/methodName
    // - if aApplication has no Views instance associated, this constructor will
    // initialize a Mustache renderer in its default folder, with '.html' void
    // template generation
    // - will also create a TMVCSessionWithRestServer for simple cookie sessions
    // - aPublishOptions could be used to specify integration with the server
    constructor Create(aApplication: TMVCApplication;
      aRestServer: TSQLRestServer=nil; const aSubURI: RawUTF8='';
      aViews: TMVCViewsAbstract=nil; aPublishOptions: TMVCPublishOptions=
        [low(TMVCPublishOption)..high(TMVCPublishOption)]); reintroduce;
    /// define some content for a static file
    // - only used if cacheStatic has been defined
    function AddStaticCache(const aFileName: TFileName;
      const aFileContent: RawByteString): RawByteString;
    /// current publishing options, as specify to the constructor
    property PublishOptions: TMVCPublishOptions read fPublishOptions write fPublishOptions;
    /// optional "Cache-Control: max-age=###" header value for static content
    property StaticCacheControlMaxAge: integer
      read fStaticCacheControlMaxAge write fStaticCacheControlMaxAge;
  end;


  { ====== Application / ViewModel ====== }

  /// Exception class triggerred by mORMot MVC/MVVM applications internally
  // - those error are internal fatal errors of the server side process
  EMVCException = class(ESynException);

  /// Exception class triggerred by mORMot MVC/MVVM applications externally
  // - those error are external errors which should be notified to the client
  // - can be used to change the default view, e.g. on application error
  EMVCApplication = class(ESynException)
  protected
    fAction: TMVCAction;
  public
    /// same as calling TMVCApplication.GotoView()
    // - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
    constructor CreateGotoView(const aMethod: RawUTF8;
      const aParametersNameValuePairs: array of const;
      aStatus: cardinal=HTTP_TEMPORARYREDIRECT);
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(const aErrorMessage: string;
      aErrorCode: integer=HTTP_BADREQUEST); overload;
    /// same as calling TMVCApplication.GotoError()
    constructor CreateGotoError(aHtmlErrorCode: integer); overload;
    /// same as calling TMVCApplication.GotoDefault
    // - HTTP_TEMPORARYREDIRECT will change the URI, but HTTP_SUCCESS won't
    constructor CreateDefault(aStatus: cardinal=HTTP_TEMPORARYREDIRECT);
  end;

  /// defines the main and error pages for the ViewModel of one application
  IMVCApplication = interface(IInvokable)
    ['{C48718BF-861B-448A-B593-8012DB51E15D}']
    /// the default main page
    // - whole data context is retrieved and returned as a TDocVariant
    procedure Default(var Scope: variant);
    /// the error page
    // - in addition to the error message, a whole data context is retrieved
    // and returned as a TDocVariant
    procedure Error(var Msg: RawUTF8; var Scope: variant);
  end;

  /// parent class to implement a MVC/MVVM application
  // - you should inherit from this class, then implement an interface inheriting
  // from IMVCApplication to define the various commands of the application
  // - here the Model would be a TSQLRest instance, Views will be defined by
  // TMVCViewsAbstract (e.g. TMVCViewsMustache), and the ViewModel/Controller
  // will be implemented with IMVCApplication methods of the inherited class
  // - inherits from TInjectableObject, so that you could resolve dependencies
  // via services or stubs, following the IoC pattern
  TMVCApplication = class(TInjectableObject)
  protected
    fFactory: TInterfaceFactory;
    fFactoryEntry: pointer;
    fFactoryErrorIndex: integer;
    fSession: TMVCSessionAbstract;
    fRestModel: TSQLRest;
    fRestServer: TSQLRestServer;
    fLocker: IAutoLocker;
    // if any TMVCRun instance is store here, will be freed by Destroy
    // but note that a single TMVCApplication logic may handle several TMVCRun
    fMainRunner: TMVCRun;
    procedure SetSession(Value: TMVCSessionAbstract);
    /// to be called when the data model did change to force content re-creation
    // - this default implementation will call fMainRunner.NotifyContentChanged
    procedure FlushAnyCache; virtual;
    /// generic IMVCApplication.Error method implementation
    procedure Error(var Msg: RawUTF8; var Scope: variant); virtual;
    /// every view will have this data context transmitted as "main":...
    procedure GetViewInfo(MethodIndex: integer; out info: variant); virtual;
    /// compute the data context e.g. for the /mvc-info URI
    procedure GetMvcInfo(out info: variant); virtual;
    /// wrappers to redirect to IMVCApplication standard methods
    // - if status is HTTP_TEMPORARYREDIRECT, it will change the URI
    // whereas HTTP_SUCCESS would just render the view for the current URI
    class procedure GotoView(var Action: TMVCAction; const MethodName: RawUTF8;
      const ParametersNameValuePairs: array of const;
      Status: cardinal=HTTP_TEMPORARYREDIRECT);
    class procedure GotoError(var Action: TMVCAction; const Msg: string;
      ErrorCode: integer=HTTP_BADREQUEST); overload;
    class procedure GotoError(var Action: TMVCAction; ErrorCode: integer); overload;
    class procedure GotoDefault(var Action: TMVCAction;
      Status: cardinal=HTTP_TEMPORARYREDIRECT);
  public
    /// initialize the instance of the MVC/MVVM application
    // - define the associated REST instance, and the interface definition for
    // application commands
    // - is not defined as constructor, since this TInjectableObject may
    // expect injection using the CreateInjected() constructor
    procedure Start(aRestModel: TSQLRest; aInterface: PTypeInfo); virtual;
    /// finalize the application
    // - and release any associated CurrentSession, Views, and fMainRunner
    destructor Destroy; override;

    /// read-only access to the associated mORMot REST instance implementing the
    // MVC data Model of the application
    // - is a TSQLRestServer instance e.g. for TMVCRunOnRestServer
    property RestModel: TSQLRest read fRestModel;
    /// read-only access to the associated factory for IMVCApplication interface
    property Factory: TInterfaceFactory read fFactory;
    /// read-write access to the associated Session instance
    property CurrentSession: TMVCSessionAbstract read fSession write SetSession;
    /// global mutex which may be used to protect ViewModel/Controller code
    // - you may call Locker.ProtectMethod in any implementation method to
    // ensure that no other thread would access the same data
    // - for store some cache data among methods, you may consider defining a
    // ILockedDocVariant private field, and use it to store values safely
    // - note that regular RestModel CRUD operations are already thread safe, so
    // it is not necessary to use this Locker with ORM or SOA methods
    property Locker: IAutoLocker read fLocker;
    /// read-write access to the main associated TMVCRun instance
    // - if any TMVCRun instance is stored here, will be freed by Destroy
    // - but note that a single TMVCApplication logic may handle several TMVCRun
    property MainRunner: TMVCRun read fMainRunner;
  end;

const
  /// the pseudo-method name for the MVC information html page
  MVCINFO_URI = 'mvc-info';

  /// the pseudo-method name for any static content for Views
  STATIC_URI = '.static';


implementation


{ TMVCViewsAbstract }

constructor TMVCViewsAbstract.Create(aInterface: PTypeInfo; aLogClass: TSynLogClass);
begin
  inherited Create;
  fFactory := TInterfaceFactory.Get(aInterface);
  fFactoryErrorIndex := fFactory.FindMethodIndex('Error');
  if aLogClass=nil then
    fLogClass := TSQLLog else
    fLogClass := aLogClass;
  fViewGenerationTimeTag := '[[GENERATION_TIME_TAG]]';
end;

procedure TMVCViewsAbstract.SetViewTemplateFolder(const aFolder: TFileName);
begin
  fViewTemplateFolder := IncludeTrailingPathDelimiter(aFolder);
  fViewStaticFolder := IncludeTrailingPathDelimiter(fViewTemplateFolder+STATIC_URI);
end;

function TMVCViewsAbstract.GetStaticFile(const aFileName: TFileName): RawByteString;
begin
  result := StringFromFile(fViewStaticFolder + aFileName);
end;


{ TMVCViewsMustache }

const
  MUSTACHE_METHODPARTIAL =
  '{{<method}}{{verb}} {{methodName}}{{#hasInParams}}({{#args}}{{^dirResult}}'+
  '{{dirName}} {{argName}}: {{typeDelphi}}{{commaArg}}{{/dirResult}}{{/args}})'+
  '{{/hasInParams}}{{#args}}{{#dirResult}}: {{typeDelphi}}{{/dirResult}}'+
  '{{/args}};{{/method}}';

  MUSTACHE_VOIDVIEW = MUSTACHE_METHODPARTIAL+
  '<<! void template created for the {{interfaceName}}.{{methodName}} View:'#13#10+
  ' defined as'#13#10'   {{>method}}'#13#10' with the following data context:'#13#10+
  '   * Main: variant'#13#10'{{#args}}{{#dirOutput}}   * {{argName}}:'+
  ' {{typePascal}}'#13#10'{{/dirOutput}}{{/args}}>>'#13#10;

  MUSTACHE_MVCINFO = MUSTACHE_METHODPARTIAL+
  '{{<url}}/{{root}}/{{methodName}}{{#hasInParams}}?'+
  '{{#args}}{{#dirInput}}{{argName}}=</b>..[{{typePascal}}]..<b>'+
  '{{#commaInSingle}}&{{/commaInSingle}}{{/dirInput}}{{/args}}{{/hasInParams}}{{/url}}'+
  '{{<mustache}}<b>&#123;{Main&#125;}</b>: variant{{#args}}{{#dirOutput}}<br><b>{&#123;'+
  '{{argName}}&#125;}</b>: {{typePascal}}{{/dirOutput}}{{/args}}{{/mustache}}'+
  '<html><head><title>{{Name}} Information</title></head><body '+
  'style="font-family:Verdana;"><h1>{{Name}} mORMotMVC Information</h1>'+
  '<p><strong>Generated by a <i>mORMot</i> {{mORMot}} server</strong><br>'+
  '<small>&copy;Synopse Informatique - <a href=https://synopse.info>'+
  'https://synopse.info</a></small></p><h2>Controller Definition</h2>'+
  '<p>Registered interface is:</p><pre>'#13#10+
  '  I{{name}} = interface(IInvokable)'#13#10'{{#methods}}'#13#10+
  '    {{>method}}'#13#10'{{/methods}}'#13#10'  end;'#13#10+
  '</pre><p>Use this page as reference when writing your <a href=http://blog.synopse.info'+
  '/post/2014/04/28/Mustache-Logic-less-templates-for-Delphi-part-1>Mustache</a> Views.</p>'+
  '<h2>Available Commands</h2><p>You can access the following commands:</p>'+
  '<ul>{{#methods}}<li><b>{{>url}}</b>{{/methods}}</ul><p>Any missing parameter '+
  'would be replaced by its default value.</p><h2>Available Views</h2>'+
  '<p>The following views are defined, with expected data context:</p><ul>'+
  '{{#methods}}{{^resultIsServiceCustomAnswer}}<li><b>{{>url}}</b><p>{{>mustache}}'+
  '</p></li>{{/resultIsServiceCustomAnswer}}{{/methods}}</ul><p>'+
  'Currently, all views are located in the <code>{{viewsFolder}}</code> folder.</p>';

  MUSTACHE_DEFAULTERROR =
  '<html><head><title>mORMotMVC Error</title></head><body style='+
  '"font-family:Verdana;"><h1>mORMotMVC Default Error Page</h1><p>A <code>'+
  '{{exceptionName}}</code> exception did raise during {{className}} process '+
  'with the following message:</p><pre>{{exceptionMessage}}</pre><p>'+
  'Triggered with the following context:</p><pre>{{originalErrorContext}}</pre>';


function MethodHasView(const aMethod: TServiceMethod): boolean;
begin // any method returning a TMVCAction do not have any associated view
  result := (aMethod.ArgsResultIndex<0) or
            (aMethod.Args[aMethod.ArgsResultIndex].ValueType<>smvRecord) or
            (aMethod.Args[aMethod.ArgsResultIndex].ArgTypeInfo<>TypeInfo(TMVCAction));
end;

constructor TMVCViewsMustache.Create(aInterface: PTypeInfo;
  const aParameters: TMVCViewsMustacheParameters; aLogClass: TSynLogClass);
var m, i: integer;
    LowerExt: TFileName;
    files: TFileNameDynArray;
    partial: TSynMustache;
    partialName: RawUTF8;
    info: variant;
begin
  inherited Create(aInterface,aLogClass);
  // get views
  fViewTemplateFileTimestampMonitor := aParameters.FileTimestampMonitorAfterSeconds;
  if aParameters.Folder='' then
    ViewTemplateFolder := ExeVersion.ProgramFilePath+'Views' else
    ViewTemplateFolder := aParameters.Folder;
  if (aParameters.ExtensionForNotExistingTemplate<>'') and
     (not DirectoryExists(ViewTemplateFolder)) then
    CreateDir(ViewTemplateFolder);
  if aParameters.CSVExtensions='' then
    LowerExt := ',html,json,css,' else
    LowerExt := ','+SysUtils.LowerCase(aParameters.CSVExtensions)+',';
  SetLength(fViews,fFactory.MethodsCount);
  for m := 0 to fFactory.MethodsCount-1 do
  if MethodHasView(fFactory.Methods[m]) then
  with fViews[m] do begin
    Locker := TAutoLocker.Create;
    MethodName := UTF8ToString(fFactory.Methods[m].URI);
    SearchPattern := MethodName+'.*';
    files := FindTemplates(SearchPattern);
    if length(files)>0 then begin
      for i := 0 to length(files)-1 do begin
        ShortFileName := files[i];
        FileExt := SysUtils.LowerCase(copy(ExtractFileExt(ShortFileName),2,100));
        if Pos(','+FileExt+',',LowerExt)>0 then
          break; // found a template with the right extension
      end;
      FileName := ViewTemplateFolder+ShortFileName;
      ContentType := GetMimeContentType(nil,0,ShortFileName);
    end else begin
      fLogClass.Add.Log(
        sllWarning,'%.Create: Missing View file in %',[self,SearchPattern]);
      if aParameters.ExtensionForNotExistingTemplate<>'' then begin
        ShortFileName := MethodName+aParameters.ExtensionForNotExistingTemplate;
        FileName := ViewTemplateFolder+ShortFileName;
        info := ContextFromMethod(fFactory.Methods[m]);
        info.interfaceName := fFactory.InterfaceTypeInfo^.Name;
        FileFromString(StringReplaceChars(StringReplaceChars(
          TSynMustache.Parse(MUSTACHE_VOIDVIEW).Render(info),'<','{'),'>','}'),
          FileName);
      end;
    end;
  end;
  fViewHelpers := aParameters.Helpers;
  // get partials
  fViewPartials := TSynMustachePartials.Create;
  files := FindTemplates('*.partial');
  for i := 0 to length(files)-1 do begin
    StringToUTF8(GetFileNameWithoutExt(files[i]),partialName);
    try
      partial := fViewPartials.Add(partialName,GetTemplate(files[i]));
      if not(viewHasGenerationTimeTag in fViewFlags) and
         partial.FoundInTemplate(fViewGenerationTimeTag) then
        include(fViewFlags,viewHasGenerationTimeTag);
    except
      on E: Exception do
        fLogClass.Add.Log(
          sllError,'%.Create: Invalid Partial file % - %',[self,files[i],E]);
    end;
  end;
end;

constructor TMVCViewsMustache.Create(aInterface: PTypeInfo;
  aLogClass: TSynLogClass; aExtensionForNotExistingTemplate: TFileName);
var params: TMVCViewsMustacheParameters;
begin
  fillchar(params,sizeof(params),0);
  params.FileTimestampMonitorAfterSeconds := 5;
  params.ExtensionForNotExistingTemplate := aExtensionForNotExistingTemplate;
  params.Helpers := TSynMustache.HelpersGetStandardList;
  Create(aInterface,params,aLogClass);
end;

destructor TMVCViewsMustache.Destroy;
begin
  inherited;
  fViewPartials.Free;
end;

type
  THtmlTableStyleLabel = (labelFalse,labelTrue,labelOff,labelOn,labelValue);
  TExpressionHtmlTableStyle = class
  public
    class procedure StartTable(WR: TTextWriter); virtual;
    class procedure BeforeFieldName(WR: TTextWriter); virtual;
    class procedure BeforeValue(WR: TTextWriter); virtual;
    class procedure AddLabel(WR: TTextWriter; const text: string;
      kind: THtmlTableStyleLabel); virtual;
    class procedure AfterValue(WR: TTextWriter); virtual;
    class procedure EndTable(WR: TTextWriter); virtual;
  end;
  TExpressionHtmlTableStyleBootstrap = class(TExpressionHtmlTableStyle)
  public
    class procedure StartTable(WR: TTextWriter); override;
    class procedure AddLabel(WR: TTextWriter; const text: string;
      kind: THtmlTableStyleLabel); override;
  end;
  TExpressionHtmlTableStyleClass = class of TExpressionHtmlTableStyle;

  TExpressionHelperForTable = class
  public
    Rest: TSQLRest;
    Table: TSQLRecordClass;
    TableProps: TSQLRecordProperties;
    HtmlTableStyle: TExpressionHtmlTableStyleClass;
    constructor Create(aRest: TSQLRest; aTable: TSQLRecordClass;
      var aHelpers: TSynMustacheHelpers);
    procedure ExpressionGet(const Value: variant; out result: variant);
    procedure ExpressionHtmlTable(const Value: variant; out result: variant);
  end;

constructor TExpressionHelperForTable.Create(aRest: TSQLRest;
  aTable: TSQLRecordClass; var aHelpers: TSynMustacheHelpers);
var HelperName: RawUTF8;
begin
  aRest.PrivateGarbageCollector.Add(self);
  Rest := aRest;
  HelperName := RawUTF8(aTable.ClassName);
  Table := aTable;
  TableProps := aTable.RecordProps;
  TSynMustache.HelperAdd(aHelpers,HelperName,ExpressionGet);
  HtmlTableStyle := TExpressionHtmlTableStyleBootstrap;
  TSynMustache.HelperAdd(aHelpers,HelperName+'.HtmlTable',ExpressionHtmlTable);
end;

procedure TExpressionHelperForTable.ExpressionHtmlTable(const Value: variant;
  out result: variant);
var Rec: PDocVariantData;
    f,i,j,int: integer;
    Field: TSQLPropInfo;
    timelog: TTimeLogBits;
    caption: string;
    sets: TStringList;
    utf8: RawUTF8;
    W: TTextWriter;
    tmp: TTextWriterStackBuffer;
const ONOFF: array[boolean] of THtmlTableStyleLabel = (labelOff,labelOn);
      ENUM: array[boolean,boolean] of THtmlTableStyleLabel =
        ((labelValue,labelValue),(labelFalse,labelTrue));
begin
  Rec := _Safe(Value);
  if Rec^.Kind=dvObject then begin
    W := TTextWriter.CreateOwnedStream(tmp);
    try
      HtmlTableStyle.StartTable(W);
      for f := 0 to TableProps.Fields.Count-1 do begin
        Field := TableProps.Fields.List[f];
        i := Rec^.GetValueIndex(Field.Name);
        if i<0 then
          continue;
        if not (Field.SQLFieldType in [sftAnsiText,sftUTF8Text,sftInteger,
           sftFloat,sftCurrency,sftTimeLog,sftModTime,sftCreateTime,sftDateTime,
           sftDateTimeMS,sftUnixTime,sftUnixMSTime,sftBoolean,sftEnumerate,sftSet]) then
          continue;
        HtmlTableStyle.BeforeFieldName(W);
        GetCaptionFromPCharLen(TrimLeftLowerCase(Field.Name),caption);
        W.AddHtmlEscapeString(caption);
        HtmlTableStyle.BeforeValue(W);
        VariantToUTF8(Rec^.Values[i],utf8);
        case Field.SQLFieldType of
        sftAnsiText,sftUTF8Text,sftInteger,sftFloat,sftCurrency:
          W.AddHtmlEscape(pointer(utf8));
        sftTimeLog,sftModTime,sftCreateTime:
          if VariantToInt64(Rec^.Values[i],timelog.Value) then
            W.AddHtmlEscapeString(timeLog.i18nText);
        sftDateTime, sftDateTimeMS: begin
          timelog.From(utf8);
          W.AddHtmlEscapeString(timeLog.i18nText);
        end;
        sftUnixTime, sftUnixMSTime:
          if VariantToInt64(Rec^.Values[i],timelog.Value) then begin
            if Field.SQLFieldType=sftUnixTime then
              timelog.FromUnixTime(timelog.Value) else
              timelog.FromUnixMSTime(timelog.Value);
            W.AddHtmlEscapeString(timeLog.i18nText);
          end;
        sftBoolean,sftEnumerate:
          if Field.InheritsFrom(TSQLPropInfoRTTIEnum) then begin
            caption := TSQLPropInfoRTTIEnum(Field).GetCaption(utf8,int);
            HtmlTableStyle.AddLabel(W,caption,
              ENUM[Field.SQLFieldType=sftBoolean,int<>0]);
          end;
        sftSet:
          if Field.InheritsFrom(TSQLPropInfoRTTISet) and
             VariantToInteger(Rec^.Values[i],int) then begin
            sets := TStringList.Create;
            try
              TSQLPropInfoRTTISet(Field).SetEnumType^.AddCaptionStrings(sets);
              for j := 0 to sets.Count-1 do begin
                HtmlTableStyle.AddLabel(W,sets[j],ONOFF[GetBit(int,j)]);
                W.AddShort('<br/>');
              end;
            finally
              sets.Free;
            end;
          end;
        end;
        HtmlTableStyle.AfterValue(W);
      end;
      HtmlTableStyle.EndTable(W);
      RawUTF8ToVariant(W.Text,result);
    finally
      W.Free;
    end;
  end else
   result := Value; // not an object -> return input value as is
end;

procedure TExpressionHelperForTable.ExpressionGet(const Value: variant;
  out result: variant);
var Rec: TSQLRecord;
    ID: integer;
begin
  SetVariantNull(result);
  if not VariantToInteger(Value,ID) then
    exit;
  Rec := Table.Create;
  try
    if Rest.Retrieve(ID,Rec) then
      result := Rec.GetSimpleFieldsAsDocVariant(true);
  finally
    Rec.Free;
  end;
end;

class procedure TExpressionHtmlTableStyle.AddLabel(WR: TTextWriter;
  const text: string; kind: THtmlTableStyleLabel);
const SETLABEL: array[THtmlTableStyleLabel] of string[3] = ('','','- ','+ ','');
begin
  WR.AddShort(SETLABEL[kind]);
  WR.AddHtmlEscapeString(text);
  WR.AddShort('&nbsp;');
end;

class procedure TExpressionHtmlTableStyle.AfterValue(WR: TTextWriter);
begin
  WR.AddShort('</td></tr>');
end;

class procedure TExpressionHtmlTableStyle.BeforeFieldName(WR: TTextWriter);
begin
  WR.AddShort('<tr><td>');
end;

class procedure TExpressionHtmlTableStyle.BeforeValue(WR: TTextWriter);
begin
  WR.AddShort('</td><td>');
end;

class procedure TExpressionHtmlTableStyle.EndTable(WR: TTextWriter);
begin
  WR.AddShort('</table>');
end;

class procedure TExpressionHtmlTableStyle.StartTable(WR: TTextWriter);
begin
  WR.AddShort('<table>');
end;

class procedure TExpressionHtmlTableStyleBootstrap.AddLabel(
  WR: TTextWriter; const text: string; kind: THtmlTableStyleLabel);
const SETLABEL: array[THtmlTableStyleLabel] of string[7] = (
  'danger','success','danger','success','primary');
begin
  WR.AddShort('<span class="label label-');
  WR.AddShort(SETLABEL[kind]);
  WR.Add('"','>');
  WR.AddHtmlEscapeString(text);
  WR.AddShort('</span>');
end;

class procedure TExpressionHtmlTableStyleBootstrap.StartTable(WR: TTextWriter);
begin
  WR.AddShort('<table class="table table-striped table-bordered">');
end;

function TMVCViewsMustache.RegisterExpressionHelpers(
  const aNames: array of RawUTF8;
  const aEvents: array of TSynMustacheHelperEvent): TMVCViewsMustache;
begin
  if self<>nil then
    TSynMustache.HelperAdd(fViewHelpers,aNames,aEvents);
  result := self;
end;

function TMVCViewsMustache.RegisterExpressionHelpersForTables(
  aRest: TSQLRest; const aTables: array of TSQLRecordClass): TMVCViewsMustache;
var t: integer;
begin
  if (self<>nil) and (aRest<>nil) then
    for t := 0 to high(aTables) do
      if aRest.Model.GetTableIndex(aTables[t])>=0 then
        TExpressionHelperForTable.Create(aRest,aTables[t],fViewHelpers);
  result := self;
end;

function TMVCViewsMustache.RegisterExpressionHelpersForTables(
  aRest: TSQLRest): TMVCViewsMustache;
var t: integer;
begin
  if (self<>nil) and (aRest<>nil) then
    for t := 0 to aRest.Model.TablesMax do
     TExpressionHelperForTable.Create(aRest,aRest.Model.Tables[t],fViewHelpers);
  result := self;
end;

function TMVCViewsMustache.RegisterExpressionHelpersForCrypto: TMVCViewsMustache;
begin
  result := RegisterExpressionHelpers(['md5','sha1','sha256'],[md5,sha1,sha256]);
end;

class procedure TMVCViewsMustache.md5(const Value: variant;
  out result: variant);
begin
  RawUTF8ToVariant(SynCrypto.MD5(ToUTF8(Value)),result);
end;

class procedure TMVCViewsMustache.sha1(const Value: variant;
  out result: variant);
begin
  RawUTF8ToVariant(SynCrypto.SHA1(ToUTF8(Value)),result);
end;

class procedure TMVCViewsMustache.sha256(const Value: variant;
  out result: variant);
begin
  RawUTF8ToVariant(SynCrypto.SHA256(ToUTF8(Value)),result);
end;

function TMVCViewsMustache.GetRenderer(methodIndex: integer; var view: TMVCView): TSynMustache;
var age: PtrUInt;
begin
  if cardinal(methodIndex)>=fFactory.MethodsCount then
    raise EMVCException.CreateUTF8('%.Render(methodIndex=%)',[self,methodIndex]);
  with fViews[methodIndex], Locker.ProtectMethod do begin
    if MethodName='' then
      raise EMVCException.CreateUTF8('%.Render(''%''): not a View',[self,MethodName]);
    if (Mustache=nil) and (FileName='') then
      raise EMVCException.CreateUTF8('%.Render(''%''): Missing Template in ''%''',
        [self,MethodName,SearchPattern]);
    if (Mustache=nil) or ((fViewTemplateFileTimestampMonitor<>0) and
       (FileAgeCheckTick<GetTickCount64)) then begin
      age := GetTemplateAge(ShortFileName);
      if (Mustache=nil) or (age<>FileAgeLast) then begin
        Mustache := nil; // no Mustache.Free: TSynMustache instances are cached
        FileAgeLast := age;
        Template := GetTemplate(ShortFileName);
        if Template<>'' then
        try
          Mustache := TSynMustache.Parse(Template);
          if Mustache.FoundInTemplate(fViewGenerationTimeTag) then
            include(Flags,viewHasGenerationTimeTag);
        except
          on E: Exception do
            raise EMVCException.CreateUTF8('%.Render(''%''): Invalid Template: % - %',
              [self,ShortFileName,E,E.Message]);
        end else
          raise EMVCException.CreateUTF8('%.Render(''%''): Missing Template in ''%''',
            [self,ShortFileName,SearchPattern]);
        if fViewTemplateFileTimestampMonitor<>0 then
          FileAgeCheckTick := GetTickCount64+
            Int64(fViewTemplateFileTimestampMonitor)*Int64(1000);
      end;
    end;
    view.ContentType := ContentType;
    view.Flags :=  view.Flags+Flags;
    result := Mustache;
  end;
end;

function TMVCViewsMustache.FindTemplates(const Mask: TFileName): TFileNameDynArray;
begin
  result := FindFilesDynArrayToFileNames(
    FindFiles(ViewTemplateFolder,Mask,'',{sorted=}false,{withdir=}false));
end;

function TMVCViewsMustache.GetTemplate(const aFileName: TFileName): RawUTF8;
begin
  result := AnyTextFileToRawUTF8(ViewTemplateFolder+aFileName,true);
end;

{$WARN SYMBOL_DEPRECATED OFF} // we don't need TDateTime, just values to compare
function TMVCViewsMustache.GetTemplateAge(const aFileName: TFileName): PtrUInt;
begin
  result := FileAge(ViewTemplateFolder+aFileName);
end;
{$WARN SYMBOL_DEPRECATED ON}

procedure TMVCViewsMustache.Render(methodIndex: Integer; const Context: variant;
  var View: TMVCView);
begin
  View.Content := GetRenderer(methodIndex,View).
    Render(Context,fViewPartials,fViewHelpers);
  if IsVoid(View.Content) then // rendering failure
    with fViews[methodIndex] do begin
      Locker.Enter;
      try
        Mustache := nil; // force reload view ASAP
      finally
        Locker.Leave;
      end;
      raise EMVCException.CreateUTF8('%.Render(''%''): Void [%] Template - '+
        'please write some content into this file!',[self,ShortFileName,FileName]);
    end;
end;


{ TMVCSessionAbstract }

constructor TMVCSessionAbstract.Create;
begin
  inherited;
end;

function TMVCSessionAbstract.CheckAndRetrieveInfo(PRecordDataTypeInfo: pointer): variant;
var rec: array[byte] of word; // 512 bytes to store locally any kind of record
    recsize: integer;
    sessionID: integer;
  procedure ProcessSession;
  var recJSON: RawUTF8;
  begin
    SaveJSON(rec,PRecordDataTypeInfo,[twoForceJSONExtended,
     twoEnumSetsAsBooleanInRecord,twoTrimLeftEnumSets],recJSON);
    TDocVariantData(result).InitJSONInPlace(pointer(recJSON),JSON_OPTIONS_FAST);
  end;
begin
  SetVariantNull(result);
  recsize := RecordTypeInfoSize(PRecordDataTypeInfo);
  if recsize>SizeOf(rec) then // =0 if PRecordDataTypeInfo=nil (sessionID only)
    raise EMVCException.CreateUTF8('%.CheckAndRetrieveInfo: recsize=%',[self,recsize]);
  FillCharFast(rec,recsize,0);
  try
    sessionID := CheckAndRetrieve(@rec,PRecordDataTypeInfo);
    if sessionID<>0 then begin
      if recsize>0 then
        ProcessSession;
      _ObjAddProps(['id',sessionID],result);
    end;
  finally
    if recsize>0 then // manual finalization of managed fields
      RecordClear(rec,PRecordDataTypeInfo);
  end;
end;


{ TMVCSessionWithCookies }

constructor TMVCSessionWithCookies.Create;
var rnd: THash512;
begin
  inherited Create;
  fContext.CookieName := 'mORMot';
  // temporary secret for encryption
  fContext.CryptNonce := Random32gsl;
  TAESPRNG.Main.FillRandom(@fContext.Crypt,sizeof(fContext.Crypt));
  // temporary secret for HMAC-CRC32C
  TAESPRNG.Main.FillRandom(@rnd,sizeof(rnd));
  fContext.Secret.Init(@rnd,sizeof(rnd));
end;

procedure XorMemoryCTR(data: PCardinal; key256bytes: PCardinalArray;
  size: PtrUInt; ctr: cardinal);
begin
  while size>=sizeof(Cardinal) do begin
    dec(size,sizeof(Cardinal));
    data^ := data^ xor key256bytes[ctr and $3f] xor ctr;
    inc(data);
    ctr := ((ctr xor (ctr shr 15))*2246822519); // prime-number ctr diffusion
    ctr := ((ctr xor (ctr shr 13))*3266489917);
    ctr := ctr xor (ctr shr 16);
  end;
  if size=0 then
    exit; // no padding
  repeat
    dec(size);
    PByteArray(data)[size] := PByteArray(data)[size] xor ctr;
    ctr := ctr shr 8; // 1..3 pending iterations
  until size=0;
end;

procedure TMVCSessionWithCookies.Crypt(P: PAnsiChar; bytes: integer);
begin
  XorMemoryCTR(@P[4],@fContext.Crypt,bytes-4,xxHash32(fContext.CryptNonce,P,4));
end;

function TMVCSessionWithCookies.Exists: boolean;
begin
  result := GetCookie<>'';
end;

type // map the binary layout of our base-64 serialized cookies
  TCookieContent = packed record
    head: packed record
      cryptnonce: cardinal; // ctr=hash32(cryptnonce)
      hmac: cardinal;       // = signature
      session: integer;     // = jti claim
      issued: cardinal;     // = iat claim (from UnixTimeUTC - Y2106)
      expires: cardinal;    // = exp claim
    end;
    data: array[0..2047] of byte; // binary serialization of record value
  end;
  PCookieContent = ^TCookieContent;

function TMVCSessionWithCookies.CheckAndRetrieve(PRecordData: pointer;
  PRecordTypeInfo: pointer; PExpires: PCardinal): integer;
var cookie: RawUTF8;
begin
  cookie := GetCookie;
  if cookie='' then
    result := 0 else // no cookie -> no session
    result := CheckAndRetrieveFromCookie(cookie,PRecordData,PRecordTypeInfo,PExpires);
end;

function TMVCSessionWithCookies.CheckAndRetrieveFromCookie(const cookie: RawUTF8;
  PRecordData, PRecordTypeInfo: pointer; PExpires: PCardinal): integer;
var clen, len: integer;
    now: cardinal;
    ccend: PAnsiChar;
    cc: TCookieContent;
begin
  result := 0; // parsing error
  if cookie='' then
    exit;
  clen := length(cookie);
  len := Base64uriToBinLength(clen);
  if (len>=sizeof(cc.head)) and (len<=sizeof(cc)) and
     Base64uriDecode(pointer(cookie),@cc,clen) then begin
    Crypt(@cc,len);
    if (cardinal(cc.head.session)<=cardinal(fContext.SessionSequence)) then begin
      if PExpires<>nil then
        PExpires^ := cc.head.expires;
      now := UnixTimeUTC;
      if (cc.head.issued<=now) and (cc.head.expires>=now) and
         (fContext.Secret.Compute(@cc.head.session,len-8)=cc.head.hmac) then
      if PRecordData=nil then
        result := cc.head.session else
        if (PRecordTypeInfo<>nil) and (len>sizeof(cc.head)) then begin
          ccend := PAnsiChar(@cc)+len;
          if RecordLoad(PRecordData^,@cc.data,PRecordTypeInfo,nil,ccend)=ccend then
            result := cc.head.session;
        end;
    end;
  end;
  if result=0 then
    Finalize; // delete any invalid/expired cookie on server side
end;

function TMVCSessionWithCookies.Initialize(PRecordData: pointer;
  PRecordTypeInfo: pointer; SessionTimeOutMinutes: cardinal): integer;
var len: integer;
    cc: TCookieContent;
begin
  if (PRecordData<>nil) and (PRecordTypeInfo<>nil) then
    len := RecordSaveLength(PRecordData^,PRecordTypeInfo) else
    len := 0;
  if len>sizeof(cc.data) then // all cookies storage should be < 4K
    raise EMVCApplication.CreateGotoError('Big Fat Cookie');
  result := InterlockedIncrement(fContext.SessionSequence);
  if result=MaxInt-1024 then
    fContext.SessionSequence := 0; // thread-safe overflow rounding
  cc.head.cryptnonce := Random32gsl;
  cc.head.session := result;
  cc.head.issued := UnixTimeUTC;
  if SessionTimeOutMinutes=0 then
    SessionTimeOutMinutes := 31*24*60; // 1 month is a reasonable high value
  cc.head.expires := cc.head.issued+SessionTimeOutMinutes*60;
  if len>0 then
    RecordSave(PRecordData^,@cc.data,PRecordTypeInfo);
  inc(len,sizeof(cc.head));
  cc.head.hmac := fContext.Secret.Compute(@cc.head.session,len-8);
  Crypt(@cc,len);
  SetCookie(BinToBase64URI(@cc,len));
end;

procedure TMVCSessionWithCookies.Finalize;
begin
  SetCookie(COOKIE_EXPIRED);
end;

function TMVCSessionWithCookies.LoadContext(const Saved: RawUTF8): boolean;
begin
  result := RecordLoadBase64(pointer(Saved),length(Saved),
    fContext,TypeInfo(TMVCSessionWithCookiesContext));
end;

function TMVCSessionWithCookies.SaveContext: RawUTF8;
begin
  result := RecordSaveBase64(fContext,TypeInfo(TMVCSessionWithCookiesContext));
end;


{ TMVCSessionWithRestServer }

function TMVCSessionWithRestServer.GetCookie: RawUTF8;
begin
  result := ServiceContext.Request.InCookie[fContext.CookieName];
end;

procedure TMVCSessionWithRestServer.SetCookie(const cookie: RawUTF8);
var ctxt: TSQLRestServerURIContext;
begin
  ctxt := ServiceContext.Request;
  ctxt.OutSetCookie := fContext.CookieName+'='+cookie;
  ctxt.InCookie[CookieName] := cookie;
end;


{ TMVCSessionSingle }

function TMVCSessionSingle.GetCookie: RawUTF8;
begin
  result := fSingleCookie;
end;

procedure TMVCSessionSingle.SetCookie(const cookie: RawUTF8);
begin
  fSingleCookie := cookie;
end;


{ EMVCApplication }

constructor EMVCApplication.CreateDefault(aStatus: cardinal);
begin
  inherited CreateFmt('CreateDefault(%d)',[aStatus]);
  TMVCApplication.GotoDefault(fAction,aStatus);
end;

constructor EMVCApplication.CreateGotoError(const aErrorMessage: string;
  aErrorCode: integer);
begin
  inherited CreateFmt('Error #%d: %s',[aErrorCode,aErrorMessage]);
  TMVCApplication.GotoError(fAction,aErrorMessage,aErrorCode);
end;

constructor EMVCApplication.CreateGotoError(aHtmlErrorCode: integer);
begin
  inherited CreateFmt('Error=%d',[aHtmlErrorCode]);
  TMVCApplication.GotoError(fAction,aHtmlErrorCode);
end;

constructor EMVCApplication.CreateGotoView(const aMethod: RawUTF8;
  const aParametersNameValuePairs: array of const; aStatus: cardinal);
begin
  inherited CreateFmt('GotoView(''%s'',%d)',[aMethod,aStatus]);
  TMVCApplication.GotoView(fAction,aMethod,aParametersNameValuePairs,aStatus);
end;


{ TMVCApplication }

procedure TMVCApplication.Start(aRestModel: TSQLRest; aInterface: PTypeInfo);
var m: integer;
    entry: PInterfaceEntry;
begin
  fLocker := TAutoLocker.Create;
  fRestModel := aRestModel;
  fFactory := TInterfaceFactory.Get(aInterface);
  fFactoryErrorIndex := fFactory.FindMethodIndex('Error');
  if fFactoryErrorIndex<0 then
    raise EMVCException.CreateUTF8(
      '% does not implement the IMVCApplication.Error() method',[aInterface.Name]);
  entry := GetInterfaceEntry(fFactory.InterfaceIID);
  if entry=nil then
    raise EMVCException.CreateUTF8('%.Start(%): this class should implement %',
      [self,aRestModel,fFactory.InterfaceTypeInfo^.Name]);
  fFactoryEntry := PAnsiChar(self)+entry^.IOffset;
  for m := 0 to fFactory.MethodsCount-1 do
    if not MethodHasView(fFactory.Methods[m]) then
    with fFactory.Methods[m] do
      if ArgsOutFirst<>ArgsResultIndex then
        raise EMVCException.CreateUTF8(
          '%.Start(%): %.% var/out parameters not allowed with TMVCAction result',
          [self,aRestModel,fFactory.InterfaceTypeInfo^.Name,URI]) else
        // TServiceCustomAnswer maps TMVCAction in TMVCApplication.RunOnRestServer
        ArgsResultIsServiceCustomAnswer := true;
  FlushAnyCache;
end;

destructor TMVCApplication.Destroy;
begin
  inherited;
  fMainRunner.Free;
  fSession.Free;
end;

procedure TMVCApplication.Error(var Msg: RawUTF8; var Scope: variant);
begin // do nothing: just pass input error Msg and data Scope to the view
end;

class procedure TMVCApplication.GotoView(var Action: TMVCAction; const MethodName: RawUTF8;
  const ParametersNameValuePairs: array of const; status: cardinal);
begin
  Action.ReturnedStatus := status;
  Action.RedirectToMethodName := MethodName;
  if high(ParametersNameValuePairs)<1 then
    Action.RedirectToMethodParameters := '' else
    Action.RedirectToMethodParameters := JSONEncode(ParametersNameValuePairs);
end;

class procedure TMVCApplication.GotoError(var Action: TMVCAction;
  const Msg: string; ErrorCode: integer);
begin
  GotoView(Action,'Error',['Msg',Msg],ErrorCode);
end;

class procedure TMVCApplication.GotoError(var Action: TMVCAction;
  ErrorCode: integer);
begin
  if ErrorCode<=HTTP_CONTINUE then
    ErrorCode := HTTP_BADREQUEST;
  GotoView(Action,'Error',['Msg',StatusCodeToErrorMsg(ErrorCode)],ErrorCode);
end;

class procedure TMVCApplication.GotoDefault(var Action: TMVCAction; Status: cardinal);
begin
  Action.ReturnedStatus := Status;
  Action.RedirectToMethodName := 'Default';
  Action.RedirectToMethodParameters := '';
end;

procedure TMVCApplication.SetSession(Value: TMVCSessionAbstract);
begin
  FreeAndNil(fSession);
  fSession := Value;
end;

procedure TMVCApplication.GetViewInfo(MethodIndex: integer; out info: variant);
begin
  if MethodIndex>=0 then
    info := _ObjFast(['pageName',fFactory.Methods[MethodIndex].URI]) else
    info := _ObjFast([]);
end;

procedure TMVCApplication.GetMvcInfo(out info: variant);
begin
  info := _ObjFast(['name',fFactory.InterfaceTypeInfo^.Name,
    'mORMot',SYNOPSE_FRAMEWORK_VERSION,'root',RestModel.Model.Root,
    'methods',ContextFromMethods(fFactory)]);
end;

procedure TMVCApplication.FlushAnyCache;
begin
  if fMainRunner<>nil then
    fMainRunner.NotifyContentChanged;
end;


{ TMVCRendererAbstract }

constructor TMVCRendererAbstract.Create(aApplication: TMVCApplication);
begin
  fApplication := aApplication;
end;

procedure TMVCRendererAbstract.CommandError(const ErrorName: RawUTF8;
  const ErrorValue: variant; ErrorCode: Integer);
var info, renderContext: variant;
begin
  fApplication.GetViewInfo(fMethodIndex,info);
  renderContext := _ObjFast(['main',info, 'msg',StatusCodeToErrorMsg(ErrorCode),
    'errorCode',ErrorCode, ErrorName,ErrorValue]);
  renderContext.originalErrorContext := JSONReformat(ToUTF8(renderContext));
  Renders(renderContext,ErrorCode,true);
end;

procedure TMVCRendererAbstract.ExecuteCommand(aMethodIndex: integer);
var action: TMVCAction;
    exec: TServiceMethodExecute;
    isAction: boolean;
    WR: TTextWriter;
    methodOutput: RawUTF8;
    renderContext, info: variant;
    err: shortstring;
    tmp: TTextWriterStackBuffer;
begin
  action.ReturnedStatus := HTTP_SUCCESS;
  fMethodIndex := aMethodIndex;
  try
    if fMethodIndex>=0 then begin
      repeat
        try
          isAction := fApplication.fFactory.Methods[fMethodIndex].ArgsResultIsServiceCustomAnswer;
          WR := TJSONSerializer.CreateOwnedStream(tmp);
          try
            WR.Add('{');
            exec := TServiceMethodExecute.Create(@fApplication.fFactory.Methods[fMethodIndex]);
            try
              exec.Options := [optVariantCopiedByReference];
              exec.ServiceCustomAnswerStatus := action.ReturnedStatus;
              err := '';
              if not exec.ExecuteJson([fApplication.fFactoryEntry],pointer(fInput),WR,@err,true) then
                if err<>'' then
                  raise EMVCException.CreateUTF8('%.CommandRunMethod: %',[self,err]) else
                with fApplication.fFactory do
                  raise EMVCException.CreateUTF8('%.CommandRunMethod: %.%() execution error',
                    [self,InterfaceTypeInfo^.Name,Methods[fMethodIndex].URI]);
              action.RedirectToMethodName := exec.ServiceCustomAnswerHead;
              action.ReturnedStatus := exec.ServiceCustomAnswerStatus;
            finally
              exec.Free;
            end;
            if not isAction then
              WR.Add('}');
            WR.SetText(methodOutput);
          finally
            WR.Free;
          end;
          if isAction then
            // was a TMVCAction mapped in a TServiceCustomAnswer record
            action.RedirectToMethodParameters := methodOutput else begin
            // rendering, e.g. with fast Mustache {{template}}
            _Json(methodOutput,renderContext,JSON_OPTIONS_FAST);
            fApplication.GetViewInfo(fMethodIndex,info);
            _Safe(renderContext)^.AddValue('main',info);
            if fMethodIndex=fApplication.fFactoryErrorIndex then
              _ObjAddProps(['errorCode',action.ReturnedStatus,
                'originalErrorContext',JSONReformat(ToUTF8(renderContext))],
                renderContext);
            Renders(renderContext,action.ReturnedStatus,false);
            exit; // success
          end;
        except
          on E: EMVCApplication do
            action := E.fAction;
        end; // lower level exceptions will be handled below
        fInput := action.RedirectToMethodParameters;
        fMethodIndex := fApplication.fFactory.FindMethodIndex(action.RedirectToMethodName);
        if action.ReturnedStatus=0 then
          action.ReturnedStatus := HTTP_SUCCESS else
        if (action.ReturnedStatus=HTTP_TEMPORARYREDIRECT) or
           (action.ReturnedStatus=HTTP_FOUND) or
           (action.ReturnedStatus=HTTP_SEEOTHER) or
           (action.ReturnedStatus=HTTP_MOVEDPERMANENTLY) then
          if Redirects(action) then // if redirection is implemented
            exit else
            action.ReturnedStatus := HTTP_SUCCESS; // fallback is to handle here
      until fMethodIndex<0; // loop to handle redirection
    end;
    // if we reached here, there was a wrong URI -> render the 404 error page
    CommandError('notfound',true,HTTP_NOTFOUND);
  except
    on E: Exception do
      CommandError('exception',
        ObjectToVariantDebug(E,'%.ExecuteCommand',[self]),HTTP_SERVERERROR);
  end;
end;

function TMVCRendererAbstract.Redirects(const action: TMVCAction): boolean;
begin
  result := false;
end; // indicates redirection did not happen -> caller should do it manually


{ TMVCRendererFromViews }

constructor TMVCRendererFromViews.Create(aRun: TMVCRunWithViews);
begin
  inherited Create(aRun);
  fCacheEnabled := true;
end;

procedure TMVCRendererFromViews.Renders(var outContext: variant;
  status: cardinal; forcesError: boolean);
var view: TMVCView;
begin
  view.Flags := fRun.fViews.fViewFlags;
  if forcesError or (fMethodIndex=fRun.fViews.fFactoryErrorIndex) then
    try // last change rendering of the error page
      fRun.fViews.Render(fRun.fViews.fFactoryErrorIndex,outContext,view);
    except // fallback to default HTML error template, if current did not work
      on E: Exception do begin
        _ObjAddProps(['exceptionName',E.ClassName,
          'exceptionMessage',E.Message,'className',ClassName],outContext);
        view.Content := TSynMustache.Parse(MUSTACHE_DEFAULTERROR).Render(outContext);
        view.ContentType := HTML_CONTENT_TYPE;
      end;
    end else
      fRun.fViews.Render(fMethodIndex,outContext,view);
  fOutput.Content := view.Content;
  fOutput.Header := HEADER_CONTENT_TYPE+view.ContentType;
  fOutput.Status := status;
  fOutputFlags := view.Flags;
end;


{ TMVCRendererJson }

procedure TMVCRendererJson.Renders(var outContext: variant;
  status: cardinal; forcesError: boolean);
begin
  fOutput.Content := JSONReformat(ToUTF8(outContext));
  fOutput.Header := JSON_CONTENT_TYPE_HEADER_VAR;
  fOutput.Status := status;
end;


{ TMVCRun }

constructor TMVCRun.Create(aApplication: TMVCApplication);
begin
  fApplication := aApplication;
  fApplication.SetSession(nil);
end;

procedure TMVCRun.NotifyContentChangedForMethod(aMethodIndex: integer);
begin // do nothing at this abstract level
end;

procedure TMVCRun.NotifyContentChanged;
var m: integer;
begin
  for m := 0 to fApplication.fFactory.MethodsCount-1 do
    NotifyContentChangedForMethod(m)
end;

procedure TMVCRun.NotifyContentChangedForMethod(const aMethodName: RawUTF8);
begin
  NotifyContentChangedForMethod(fApplication.fFactory.FindMethodIndex(aMethodName));
end;


{ TMVCRunWithViews }

constructor TMVCRunWithViews.Create(aApplication: TMVCApplication;
  aViews: TMVCViewsAbstract);
begin
  inherited Create(aApplication);
  fViews := aViews;
  fCacheLocker := TAutoLocker.Create;
end;

function TMVCRunWithViews.SetCache(const aMethodName: RawUTF8;
  aPolicy: TMVCRendererCachePolicy; aTimeOutSeconds: cardinal): TMVCRunWithViews;
const MAX_CACHE_TIMEOUT = 60*15; // 15 minutes
var aMethodIndex: integer;
begin
  with fCacheLocker.ProtectMethod do begin
    aMethodIndex := fApplication.fFactory.CheckMethodIndex(aMethodName);
    if fCache=nil then
      SetLength(fCache,fApplication.fFactory.MethodsCount);
    with fCache[aMethodIndex] do begin
      Policy := aPolicy;
      if aTimeOutSeconds-1>=MAX_CACHE_TIMEOUT then
        TimeOutSeconds := MAX_CACHE_TIMEOUT else
        TimeOutSeconds := aTimeOutSeconds;
      NotifyContentChangedForMethod(aMethodIndex);
    end;
  end;
  result := self;
end;

destructor TMVCRunWithViews.Destroy;
begin
  fViews.Free;
  inherited;
end;

procedure TMVCRunWithViews.NotifyContentChangedForMethod(aMethodIndex: integer);
begin
  inherited;
  with fCacheLocker.ProtectMethod do
  if cardinal(aMethodIndex)<cardinal(Length(fCache)) then
  with fCache[aMethodIndex] do
    case Policy of
    cacheRootIgnoringSession,cacheRootIfSession,cacheRootIfNoSession:
      RootValue := '';
    cacheRootWithSession,
    cacheWithParametersIgnoringSession,cacheWithParametersIfSession,
    cacheWithParametersIfNoSession:
      InputValues.Init(false);
    end;
end;


{ TMVCRunOnRestServer }

constructor TMVCRunOnRestServer.Create(aApplication: TMVCApplication;
  aRestServer: TSQLRestServer; const aSubURI: RawUTF8;
  aViews: TMVCViewsAbstract; aPublishOptions: TMVCPublishOptions);
var m: integer;
    bypass: boolean;
    method: RawUTF8;
begin
  if aApplication=nil then
    raise EMVCException.CreateUTF8('%.Create(aApplication=nil)',[self]);
  if aRestServer=nil then
    fRestServer := aApplication.RestModel as TSQLRestServer else
    fRestServer := aRestServer;
  if aViews=nil then
    {$ifdef WITHLOG}
    aViews := TMVCViewsMustache.Create(aApplication.fFactory.InterfaceTypeInfo,
      fRestServer.LogClass,'.html') else
    aViews.fLogClass := fRestServer.LogClass;
    {$else}
    aViews := TMVCViewsMustache.Create(aApplication.fFactory.InterfaceTypeInfo,
      TSQLLog,'.html');
    {$endif}
  inherited Create(aApplication,aViews);
  fPublishOptions := aPublishOptions;
  bypass := bypassAuthentication in fPublishOptions;
  if aSubURI<>'' then
    fRestServer.ServiceMethodRegister(aSubURI,RunOnRestServerSub,bypass) else begin
    for m := 0 to fApplication.fFactory.MethodsCount-1 do begin
      method := fApplication.fFactory.Methods[m].URI;
      if method[1]='_' then
        delete(method,1,1); // e.g. IService._Start() -> /service/start
      fRestServer.ServiceMethodRegister(method,RunOnRestServerRoot,bypass);
    end;
    if publishMvcInfo in fPublishOptions then
      fRestServer.ServiceMethodRegister(MVCINFO_URI,RunOnRestServerRoot,bypass);
    if publishStatic in fPublishOptions then
      fRestServer.ServiceMethodRegister(STATIC_URI,RunOnRestServerRoot,bypass);
  end;
  if (registerORMTableAsExpressions in fPublishOptions) and
     aViews.InheritsFrom(TMVCViewsMustache) then
    TMVCViewsMustache(aViews).RegisterExpressionHelpersForTables(fRestServer);
  fStaticCache.Init({casesensitive=}true);
  fApplication.SetSession(TMVCSessionWithRestServer.Create);
end;

function TMVCRunOnRestServer.AddStaticCache(const aFileName: TFileName;
  const aFileContent: RawByteString): RawByteString;
begin
  if aFileContent<>'' then // also cache content-type
    result := GetMimeContentType(
      pointer(aFileContent),length(aFileContent),aFileName)+#10+aFileContent else
    result := '';
  fStaticCache.Add(StringToUTF8(aFileName),result);
end;

procedure TMVCRunOnRestServer.InternalRunOnRestServer(
  Ctxt: TSQLRestServerURIContext; const MethodName: RawUTF8);
var mvcinfo, inputContext: variant;
    rawMethodName,rawFormat,static,body,content: RawUTF8;
    staticFileName: TFileName;
    rendererClass: TMVCRendererReturningDataClass;
    renderer: TMVCRendererReturningData;
    methodIndex: integer;
    method: PServiceMethod;
    timer: TPrecisionTimer;
begin
  Split(MethodName,'/',rawMethodName,rawFormat);
  // 1. implement mvc-info endpoint
  if (publishMvcInfo in fPublishOptions) and
     IdemPropNameU(rawMethodName,MVCINFO_URI) then begin
    if fMvcInfoCache='' then begin
      fApplication.GetMvcInfo(mvcinfo);
      mvcinfo.viewsFolder := fViews.ViewTemplateFolder;
      fMvcInfoCache := TSynMustache.Parse(MUSTACHE_MVCINFO).Render(mvcinfo);
    end;
    Ctxt.Returns(fMvcInfoCache,HTTP_SUCCESS,HTML_CONTENT_TYPE_HEADER,True);
  end else
  // 2. serve static resources, with proper caching
  if (publishStatic in fPublishOptions) and
     IdemPropNameU(rawMethodName,STATIC_URI) then begin
    // code below will use a local in-memory cache, but would do the same as:
    // Ctxt.ReturnFileFromFolder(fViews.ViewStaticFolder);
    fCacheLocker.Enter;
    try
      if cacheStatic in fPublishOptions then
        static := fStaticCache.Value(rawFormat,#0) else
        static := #0;
      if static=#0 then // static='' means HTTP_NOTFOUND
        if PosEx('..',rawFormat)>0 then // avoid injection
          static := '' else begin
          staticFileName := UTF8ToString(StringReplaceChars(rawFormat,'/',PathDelim));
          if cacheStatic in fPublishOptions then begin // retrieve and cache
            static := fViews.GetStaticFile(staticFileName);
            static := AddStaticCache(staticFileName,static);
          end else begin // no cache
            staticFileName := fViews.ViewStaticFolder + staticFileName;
            Ctxt.ReturnFile(staticFileName,{handle304=}true,'','','',fStaticCacheControlMaxAge);
            exit;
          end;
        end;
    finally
      fCacheLocker.Leave;
    end;
    if static='' then
      Ctxt.Error('',HTTP_NOTFOUND,fStaticCacheControlMaxAge) else begin
      Split(static,#10,content,static);
      Ctxt.Returns(static,HTTP_SUCCESS,HEADER_CONTENT_TYPE+content,
        {handle304=}true,false,fStaticCacheControlMaxAge);
    end;
  end else begin
    // 3. render regular page using proper viewer
    timer.Start;
    if IdemPropNameU(rawFormat,'json') then
      rendererClass := TMVCRendererJSON else
      rendererClass := TMVCRendererFromViews;
    renderer := rendererClass.Create(self);
    try
      if Ctxt.Method in [mGET,mPOST] then begin
        methodIndex := fApplication.fFactory.FindMethodIndex(rawMethodName);
        if methodIndex>=0 then begin
          method := @fApplication.fFactory.Methods[methodIndex];
          inputContext := Ctxt.GetInputAsTDocVariant(JSON_OPTIONS_FAST_EXTENDED,method);
          if not VarIsEmpty(inputContext) then
          with _Safe(inputContext)^ do begin
            if (Kind=dvObject) and (Count>0) then
              // try {"p.a1":5,"p.a2":"dfasdfa"} -> {"p":{"a1":5,"a2":"dfasdfa"}}
              if method^.ArgsInputValuesCount=1 then
                FlattenAsNestedObject(RawUTF8(method^.Args[method^.ArgsInFirst].ParamName^));
            renderer.fInput := ToJSON;
          end;
        end;
        renderer.ExecuteCommand(methodIndex);
      end else
        renderer.CommandError('notfound',true,HTTP_NOTFOUND);
      body := renderer.Output.Content;
      if viewHasGenerationTimeTag in renderer.fOutputFlags then
        body := StringReplaceAll(body,fViews.ViewGenerationTimeTag,
          ShortStringToAnsi7String(timer.Stop));
      Ctxt.Returns(body,renderer.Output.Status,
        renderer.Output.Header,{handle304=}true,{noerrorprocess=}true,
        {cachecontrol=}0,{hashwithouttime:}crc32cUTF8ToHex(renderer.Output.Content));
    finally
      renderer.Free;
    end;
  end;
end;

procedure TMVCRunOnRestServer.RunOnRestServerRoot(Ctxt: TSQLRestServerURIContext);
begin
  InternalRunOnRestServer(Ctxt,Ctxt.URI+'/'+Ctxt.URIBlobFieldName);
end;

procedure TMVCRunOnRestServer.RunOnRestServerSub(Ctxt: TSQLRestServerURIContext);
begin
  if Ctxt.URIBlobFieldName='' then
    Ctxt.Redirect(Ctxt.URIWithoutSignature+'/default') else
    InternalRunOnRestServer(Ctxt,Ctxt.URIBlobFieldName);
end;


{ TMVCRendererReturningData }

constructor TMVCRendererReturningData.Create(aRun: TMVCRunWithViews);
begin
  fRun := aRun;
  inherited Create(fRun.Application);
end;

procedure TMVCRendererReturningData.ExecuteCommand(aMethodIndex: integer);
  procedure SetOutputValue(const aValue: RawUTF8);
  begin
    fOutput.Status := HTTP_SUCCESS;
    Split(aValue,#0,fOutput.Header,RawUTF8(fOutput.Content));
  end;
  function RetrievedFromInputValues(const aKey: RawUTF8;
    const aInputValues: TSynNameValue): boolean;
  var i: integer;
  begin
    i := aInputValues.Find(aKey);
    if (i>=0) and (aInputValues.List[i].Value<>'') and
       (fCacheCurrentSec<cardinal(aInputValues.List[i].Tag)) then begin
      SetOutputValue(aInputValues.List[i].Value);
      result := true;
    end else begin
      fCacheCurrent := inputCache;
      fCacheCurrentInputValueKey := aKey;
      result := false;
    end;
  end;
var sessionID: integer;
label doRoot,doInput;
begin
  // first check if content can be retrieved from cache
  if not fCacheEnabled then begin
    inherited ExecuteCommand(aMethodIndex);
    exit;
  end;
  fCacheCurrent := noCache;
  fCacheCurrentSec := GetTickCount64 div 1000;
  fRun.fCacheLocker.Enter;
  try
    if cardinal(aMethodIndex)<cardinal(Length(fRun.fCache)) then
    with fRun.fCache[aMethodIndex] do begin
      case Policy of
      cacheRootIgnoringSession:
        if fInput='' then
   doRoot:if (RootValue<>'') and (fCacheCurrentSec<RootValueExpirationTime) then begin
            SetOutputValue(RootValue);
            exit;
          end else
            fCacheCurrent := rootCache;
      cacheRootIfSession:
        if (fInput='') and fApplication.CurrentSession.Exists then
          goto doRoot;
      cacheRootIfNoSession:
        if (fInput='') and not fApplication.CurrentSession.Exists then
          goto doRoot;
      cacheRootWithSession:
        if fInput='' then begin
          sessionID := fApplication.CurrentSession.CheckAndRetrieve;
          if sessionID=0 then
            goto doRoot else
          if RetrievedFromInputValues(UInt32ToUtf8(sessionID),InputValues) then
            exit;
        end;
      cacheWithParametersIgnoringSession:
  doInput:if fInput='' then
            goto doRoot else
          if RetrievedFromInputValues(fInput,InputValues) then
            exit;
      cacheWithParametersIfSession:
        if fApplication.CurrentSession.Exists then
          goto doInput;
      cacheWithParametersIfNoSession:
        if not fApplication.CurrentSession.Exists then
          goto doInput;
      end;
    end;
  finally
    fRun.fCacheLocker.Leave; // ExecuteCommand() process should not be locked
  end;
  // compute the context and render the page using the corresponding View
  inherited ExecuteCommand(aMethodIndex);
  // update cache
  if fCacheCurrent<>noCache then
  try
    fRun.fCacheLocker.Enter;
    with fRun.fCache[aMethodIndex] do begin
      inc(fCacheCurrentSec,TimeOutSeconds);
      case fCacheCurrent of
      rootCache:
        if fOutput.Status=HTTP_SUCCESS then begin
          RootValue := fOutput.Header+#0+fOutput.Content;
          RootValueExpirationTime := fCacheCurrentSec;
        end else
          RootValue := '';
      inputCache:
        if fOutput.Status=HTTP_SUCCESS then
          InputValues.Add(fCacheCurrentInputValueKey,fOutput.Header+#0+fOutput.Content,fCacheCurrentSec) else
          InputValues.Add(fCacheCurrentInputValueKey,'');
      end;
    end;
  finally
    fRun.fCacheLocker.Leave;
  end;
end;

function TMVCRendererReturningData.Redirects(const action: TMVCAction): boolean;
begin
  fOutput.Header := 'Location: '+UrlEncodeJsonObject(action.RedirectToMethodName,
    action.RedirectToMethodParameters,['main']);
  fOutput.Status := action.ReturnedStatus;
  result := true;
end;


initialization
  assert(sizeof(TMVCAction)=sizeof(TServiceCustomAnswer));
end.

