sourceafBedSheet::BedSheetBuilder.fan

using afIoc
using inet::IpAddr
using web::WebMod

** Use to programmatically create and launch BedSheet server instances.
**
**   syntax: fantom 
**   watchdog := true
**   BedSheetBuilder(AppModule#).startWisp(8069, watchdog, "dev")
** 
** Note that BedSheet requires specific IoC config to run. Hence when running BedSheet apps this class should be used in
** preference to 'afIoc::RegistryBuilder'. 
** 
** The [toRegistryBuilder()]`BedSheetBuilder.toRegistryBuilder` method can be used to build a web app, but not start the web app or listen to a port.  
** 
** Note the following options:
** 
**   table:
**   Name                          Type      Value
**   ----------------------------  --------  ------------------------------------------------------
**   wisp.sessionStore             Type      The 'WispSessionStore' implementation to use - note this is built outside of IoC.
**   wisp.sessionStoreProxy        Type      The 'WispSessionStore' implementation to use - this version may be an IoC service, or an IoC autobuilt class. Note because the SessionStore needs to be created before the IoC Registry, a session store proxy is created.
** 
class BedSheetBuilder {
    private static const Log log    := Utils.log
    private IpAddr? _ipAddr
    private Type[]  _moduleTypes    := Type[,]
    private Type[]  _modsToRemove   := Type[,]
    private Obj[][] _pods           := Obj[][,]

    
    ** The HTTP port to run the app on. Defaults to '8069'
    Int port {
        get { options[BsConstants.meta_appPort] }
        set { options[BsConstants.meta_appPort] = it }
    }

    ** Options for IoC 'RegistryBuilder'.
    ** Map is mutable, but this field read only.
    Str:Obj? options := Str:Obj?[:] { it.caseInsensitive = true } {
        private set
    }
    
    private new makeFromNothing() { }
    
    ** Creates a 'BedSheetBuilder'. 
    ** 'modOrPodName' may be a pod name or a qualified 'AppModule' type name. 
    ** 'addPodDependencies' is only used if a pod name is passed in.
    new makeFromName(Str modOrPodName, Bool addPodDependencies := true) {
        port = 80
        _initModules(modOrPodName, addPodDependencies)
        _initBanner()
    }
    
    ** Creates a 'BedSheetBuilder' from the given 'AppModule'.
    new makeFromAppModule(Type appModule) : this.makeFromName(appModule.qname, true) { }
    
    ** Adds an IoC module to the registry. 
    This addModule(Type moduleType) {
        if (_modsToRemove.contains(moduleType).not)
            _moduleTypes.add(moduleType)
        return this
    }
    
    ** Adds many IoC modules to the registry. 
    This addModules(Type[] modules) {
        _moduleTypes.addAll(modules.exclude { _modsToRemove.contains(it) })
        return this
    }
    
    ** Inspects the [pod's meta-data]`docLang::Pods#meta` for the key 'afIoc.module'. This is then 
    ** treated as a CSV list of (qualified) module type names to load.
    ** 
    ** If 'addDependencies' is 'true' then the pod's dependencies are also inspected for IoC 
    ** modules.
    **  
    ** Convenience for 'registryBuilder.addModulesFromPod()'
    This addModulesFromPod(Str podName, Bool addDependencies := true) {
        _pods.add([podName, addDependencies])
        return this     
    }
    
    ** Sets a value in the 'options' map. 
    ** Returns 'this' so it may be used as a builder method.        
    This setOption(Str name, Obj? value) {
        options.set(name, value)
        return this
    }

    ** The application name. 
    ** Returns 'pod.dis' (or 'proj.name'if not found) from the application's pod meta, or the pod name if neither are defined.
    ** Read only.
    Str appName() {
        options[BsConstants.meta_appName]       
    }
    
    ** Sets the local IP address that Wisp should bind to, or set to 'null' for the default.
    ** 
    ** This is useful when deploying your application to [Open Shift]`https://developers.openshift.com/en/diy-overview.html` 
    ** or similar where the local IP address is mandated. 
    ** See the Fantom Forum topic: [IP address for afBedSheet]`http://fantom.org/forum/topic/2399`.
    This setIpAddress(IpAddr? ipAddr) {
        this._ipAddr = ipAddr
        return this
    }

    ** Removes modules of the given type. If a module of the given type is subsequently added, it is silently ignored.
    This removeModule(Type moduleType) {
        _moduleTypes.remove(moduleType)
        
        // prevent it from being added later
        _modsToRemove.add(moduleType)
        return this
    }
    
    ** Suppresses surplus logging by this and IoC's 'RegistryBuilder'.
    This silence() {
        options["afIoc.silenceBuilder"] = true
        return this
    }
    
    ** Copies the content of this builder, including all the BedSheet specific config, into an IoC 'RegistryBuilder' instance.
    RegistryBuilder toRegistryBuilder() {
        bob := RegistryBuilder()
        if (isSilent) bob.suppressLogging = true
        _modsToRemove.each { bob.removeModule(it) }
        _pods.each { bob.addModulesFromPod(it[0], it[1]) }
        _moduleTypes.each { bob.addModule(it) }
        options.each |v, k| { bob.options[k] = v }
        return bob
    }
    
    ** Builds the IoC 'Registry'. 
    ** 
    ** Essentially a convenience method for 'toRegistryBuilder.build()'.
    Registry build() {
        toRegistryBuilder.build
    }

    ** Convenience method to start a Wisp server running 'BedSheetWebMod'.
    Int startWisp(Int port := 8069, Bool watchdog := false, Str? env := null) {
        this.port = port
        if (env != null)
            options["afBedSheet.env"] = env
        appPort := port
        dogPort := port + 1
        if (watchdog) {
            options[BsConstants.meta_watchdog]      = true
            options[BsConstants.meta_watchdogPort]  = dogPort
        }
        bob     := toRegistryBuilder
        mod     := watchdog ? WatchdogMod(this, appPort) : BedSheetBootMod(bob)
        return _runWebMod(bob, mod, watchdog ? dogPort : appPort, _ipAddr)
    }

    ** Enables request logs being setting BedSheet's logging level to debug.
    This enableRequestLogs() {
        this.typeof.pod.log.level = LogLevel.debug
        return this
    }
    
    ** Hook to run a fully configured BedSheet 'WebMod'.
    @NoDoc
    virtual Int runWebMod(WebMod webMod, Int port, IpAddr? ipAddr) {
        _runWebMod(toRegistryBuilder, webMod, port, ipAddr)
    }

    @NoDoc // for serialisation
    Str toStringy() {
        mods := _moduleTypes
        pods := _pods
        opts := options.dup
        rems := _modsToRemove
        opts.remove("afIoc.bannerText")
        
        appPod := (Pod) opts[BsConstants.meta_appPod]
        opts[BsConstants.meta_appPodName] = appPod.name
        opts.remove(BsConstants.meta_appPod)
        
        buf := Buf()
        Zip.gzipOutStream(buf.out).writeObj([mods, pods, opts, rems]).close
        return buf.flip.toBase64.replace("/", "_").replace("+", "-")
    }

    @NoDoc // for serialisation
    static BedSheetBuilder fromStringy(Str str) {
        b64  := str.replace("_", "/").replace("-", "+")
        data := (Obj[]) Zip.gzipInStream(Buf.fromBase64(b64).in).readObj
        
        mods := (Type[])    data[0]
        pods := (Obj[][])   data[1]
        opts := (Str:Obj?)  data[2]
        rems := (Type[])    data[3]
        
        appPodName  := (Str) opts[BsConstants.meta_appPodName]
        opts[BsConstants.meta_appPod] = Pod.find(appPodName, true)
        opts.remove(BsConstants.meta_appPodName)

        // reinstate appPod
        bob := BedSheetBuilder()
        bob._moduleTypes    = mods
        bob._pods           = pods
        bob.options         = opts
        bob._modsToRemove   = rems
        bob._initBanner
        
        return bob
    }
    
    private Int _runWebMod(RegistryBuilder bob, WebMod webMod, Int port, IpAddr? ipAddr) {
        WebModRunner(bob, webMod is WatchdogMod).run(webMod, port, ipAddr)
    }

    private Void _initModules(Str moduleName, Bool transDeps) {
        Pod?  pod
        Type? mod
        
        // Pod name given...
        // lots of start up checks looking for pods and modules... 
        // see https://bitbucket.org/SlimerDude/afbedsheet/issue/1/add-a-warning-when-no-appmodule-is-passed
        if (!moduleName.contains("::")) {
            pod = Pod.find(moduleName, true)
            if (!isSilent) log.info(BsLogMsgs.bedSheetWebMod_foundPod(pod))
            mods := _findModFromPod(pod)
            mod = mods.first
            
            if (!transDeps)
                if (!isSilent) log.info("Suppressing transitive dependencies...")
            addModulesFromPod(pod.name, transDeps)
            mods.each { addModule(it) }
        }

        // AppModule name given...
        if (moduleName.contains("::")) {
            mod = Type.find(moduleName, true)
            if (!isSilent) log.info(BsLogMsgs.bedSheetWebMod_foundType(mod))
            pod = mod.pod
            
            addModule(mod)
        }

        // we're screwed! No module = no web app!
        if (mod == null)
            log.warn(BsLogMsgs.bedSheetWebMod_noModuleFound)
        
        // A simple thing - ensure the BedSheet module is added! 
        // (transitive dependencies are added explicitly via @SubModule)
        addModule(BedSheetModule#)

        options[BsConstants.meta_appName]   = ((pod.meta["pod.dis"] ?: pod.meta["proj.name"]) ?: pod?.name) ?: "Unknown"
        options[BsConstants.meta_appPod]    = pod
        options[BsConstants.meta_appModule] = mod
    }

    ** Looks for an 'AppModule' in the given pod. 
    private Type[] _findModFromPod(Pod pod) {
        mods := Type#.emptyList
        modNames := pod.meta["afIoc.module"]
        if (modNames != null) {
            mods = modNames.split(',').map { Type.find(it, true) }
            if (!isSilent) log.info(BsLogMsgs.bedSheetWebMod_foundType(mods.first))
        } else {
            // we have a pod with no module meta... so lets guess the name 'AppModule'
            mod := pod.type("AppModule", false)
            if (mod != null) {
                mods = [mod]
                if (!isSilent) log.info(BsLogMsgs.bedSheetWebMod_foundType(mod))
                if (!isSilent) log.warn(BsLogMsgs.bedSheetWebMod_addModuleToPodMeta(pod, mod))
            }
        }
        return mods
    }
    
    private Void _initBanner() {
        bannerText := _easterEgg("Alien-Factory BedSheet v${BedSheetWebMod#.pod.version}, IoC v${Registry#.pod.version}")
        options["afIoc.bannerText"] = bannerText        
    }

    private static Str _easterEgg(Str title) {
        quotes := _loadQuotes
        if (quotes.isEmpty || (Int.random(0..8) != 2))
            return title
        return quotes[Int.random(0..<quotes.size)]
    }

    private static Str[] _loadQuotes() {
        BedSheetWebMod#.pod.file(`/res/misc/quotes.txt`).readAllLines.exclude { it.isEmpty || it.startsWith("#")}
    }
    
    private Bool isSilent() {
        options["afIoc.silenceBuilder"] == true
    }
}