sourceafBedSheet::ClientAsset.fan

using afIoc::Inject
using concurrent::AtomicBool
using concurrent::AtomicRef

** (Response Object) - 
** An asset that is uniquely identified by a client URL.
** 
** A 'ClientAsset' corresponds to a client URL that may be used by clients (e.g. internet browsers) to retrieve the asset.
** 
** Generally 'ClientAssets' are acquired from the 'FileHander' and 'PodHander' services and used to embed client URLs in web pages.
** 
**   syntax: fantom
**   urlStr := fileHandler.fromLocalUrl(`/images/fanny.jpg`).clientUrl.encode
** 
** The URLs generated by 'ClientAssets' may be automatically transformed by asset caching strategies such as 
** [Cold Feet]`pod:afColdFeet`. 
** As such, 'ClientAsset' instances are cached and automatically updated should the underlying asset be modified.
** To prevent needless polling of the file system, assets are checked for modification every 2 minutes in production 
** or 2 seconds otherwise.
** 
** Custom Client Assets
** ====================
** If you want to serve up assets from a database or other source, subclass 'ClientAsset' to create your own custom implementation. 
** Custom 'ClientAsset' instances should created by a `ClientAssetProducer` and contributed to the 'ClientAssetProducers' service. 
** This ensures your custom assets will automatically adopt any asset caching strategy set by Cold Feet.
const abstract class ClientAsset : Asset {

    @Inject
    private const ClientAssetCache? _assetCache 
    @Inject
    private const BedSheetServer?   _bedServer
    private const AtomicRef         _clientUrlRef       := AtomicRef()

    ** Autobuild 'ClientAsset' instances with IoC.
    @NoDoc
    protected new make(|This|? in) { in?.call(this) }

    ** The URL relative to the 'BedSheet' [WebMod]`web::WebMod` that corresponds to the asset resource. 
    ** If your application is the ROOT WebMod then this will be the same as 'clientUrl'; bar any asset caching. 
    ** If in doubt, use the 'clientUrl' instead.
    **  
    ** Returns 'null' if asset doesn't exist.
    abstract Uri? localUrl()

    ** The URL that clients (e.g. web browsers) should use to access the asset resource. 
    ** The 'clientUrl' contains any extra 'WebMod' path segments required to reach the 'BedSheet WebMod'.
    ** It also contains path segments as provided by any asset caching strategies, such as [Cold Feet]`pod:afColdFeet`.
    ** 
    ** Client URLs are designed to be used / embedded in your HTML and therefore are relative to the host and start with a '/'. 
    ** 
    ** Returns 'null' if asset doesn't exist.
    ** 
    ** Subclasses should override 'clientUrl()' if they **do not** wish the client URL to be transformed by asset caching strategies like [Cold Feet]`http://eggbox.fantomfactory.org/pods/afColdFeet`. 
    virtual Uri? clientUrl() {
        if (_assetCache == null)    // assetCache is nullable for FileAsset legacy code
            throw Err("${this.typeof.qname} needs to be built via IoC")
        localUrl := localUrl
        if (localUrl == null)
            return null
        if (_clientUrlRef.val == null)
            _clientUrlRef.val = _assetCache.toClientUrl(localUrl, this)
        return _clientUrlRef.val
    }
    
    ** Returns an absolute URL (for example, one that starts with 'http://...') using [BedSheetServer.toAbsoluteUrl()]`BedSheetServer.toAbsoluteUrl`.
    ** 
    ** Returns 'null' if asset doesn't exist.
    virtual Uri? clientUrlAbs() {
        _bedServer.toAbsoluteUrl(clientUrl)
    }
    
    @NoDoc
    override Int hash() {
        localUrl ?: super.hash
    }
    
    @NoDoc
    override Bool equals(Obj? obj) {
        localUrl != null
            ? localUrl == (obj as ClientAsset)?.localUrl
            : super.equals(obj)
    }

    ** Returns 'clientUrl.encode()' so it may be printed in HTML. Returns the string 'null' if the asset doesn't exist.
    override Str toStr() {
        clientUrl?.encode ?: super.toStr
    }
    
    private const AtomicBool    _lastIsModifiedRef  := AtomicBool(false)
    private const AtomicRef     _lastCheckedRef     := AtomicRef(Duration.now - 1day)

    @NoDoc
    virtual Bool isModified(Duration timeout) {
        // cache false responses for X secs to avoid hammering the file system
        if (_lastIsModifiedRef.val == false) {
            now     := Duration.now.floor(1sec)
            oldNow  := (Duration) _lastCheckedRef.val
            if ((now - oldNow) < timeout)
                return _lastIsModifiedRef.val
            
            // don't update old now until timeout expires
            _lastCheckedRef.val = now
        }

        newModifiedTs   := actualModified       // ?.floor(1sec) - lets not mess with the *actual* value
        oldModifiedTs   := modified
        isModified      := newModifiedTs == null || newModifiedTs > oldModifiedTs
        _lastIsModifiedRef.val = isModified

        // no need to cache the new "modified" value, because the Cache deletes us when this isModified() returns true
        // plus it'd be difficult to make a nice API around it 
        
        // reset our cache timeout
        if (isModified == false)
             _lastCheckedRef.val = Duration.now.floor(1sec)

        return isModified
    }
    
    ** If the asset contents are liable to change behind the scenes, 
    ** like the contents of a file may, then this should return the latest
    ** calculated modified date. 
    ** 
    ** May return 'null' if not known.
    @NoDoc  // used by isModified()
    virtual DateTime? actualModified() {
        modified
    }
}