packages feed

happs-hsp-template-0.2: src/HAppS/Template/HSP/Handle.hs

module HAppS.Template.HSP.Handle
    ( HSPState
    , Store
    , newStore
    , destroyStore
    , runSimpleHSPHandle
    , runSimpleHSPHandleT
    , runHSPHandle
    , runHSPHandleT
    , execTemplate
    , addParam
    , addJsonParam
    ) where

import Text.RJson
import HSP hiding (Request)
import Control.Monad.State
import Data.IORef
import System.Plugins
import System.Plugins.Env
import System.INotify
import System.FilePath
import System.Directory

import qualified Data.ByteString.Char8 as P
import qualified Data.ByteString.Lazy.Char8 as L

import qualified Data.Map as Map
import HAppS.Server.SimpleHTTP hiding (Web,path)
--import HAppS.Server.SimpleHTTP.HTTP.Types

import HAppS.Template.HSP

hspArgs =
        [ "-fglasgow-exts"
        , "-fallow-overlapping-instances"
        , "-fallow-undecidable-instances"
        , "-F", "-pgmFtrhsx"
        , "-fno-warn-overlapping-patterns"
        ]


data HSPState
    = HSPState
    { hspStore :: Store
    , hspLocal :: Map.Map String JsonData
    , hspGlobal :: Request -> IO JsonData
    , hspPrefix :: FilePath
    , hspObjDir :: FilePath
    }

type Store = IORef (Map.Map FilePath (Either Errors (Module, Web XML)))

newStore :: IO Store
newStore = newIORef Map.empty

destroyStore :: Store -> IO ()
destroyStore store
    = do map <- atomicModifyIORef store (\ref -> (Map.empty, ref))
         mapM_ unloadAll [ m | Right (m,_) <- Map.elems map ]

instance ToMessage XML where
    toContentType _ = P.pack "text/html"
    toMessage = L.pack . renderXML

instance ToMessage JsonData where
    toContentType _ = P.pack "application/json"
    toMessage = L.pack . show

type HSPHandleT m a = ServerPartT (StateT HSPState m) a
type HSPWebT m = WebT (StateT HSPState m)

type HSPHandle a = HSPHandleT IO a
type HSPWeb = HSPWebT IO

runSimpleHSPHandle :: ToMessage a => Store -> HSPHandle a -> ServerPart a
runSimpleHSPHandle = runSimpleHSPHandleT

runSimpleHSPHandleT :: ToMessage a => Store -> HSPHandle a -> ServerPart a
runSimpleHSPHandleT = runHSPHandleT "." "."

runHSPHandle :: ToMessage a => FilePath -> FilePath -> Store -> HSPHandle a -> ServerPart a
runHSPHandle = runHSPHandleT

runHSPHandleT :: ToMessage a => FilePath -> FilePath -> Store -> HSPHandle a -> ServerPart a
runHSPHandleT prefix objDir store (ServerPartT h)
    = let hspState rq = HSPState{ hspStore = store
                                , hspGlobal = mkGlobal self rq
                                , hspLocal = Map.empty
                                , hspPrefix = prefix
                                , hspObjDir = objDir }
          self = ServerPartT $ \rq -> WebT $ evalStateT (unWebT (h rq)) (hspState rq)
      in self

mkGlobal :: ToMessage a => ServerPart a -> Request -> Request -> IO JsonData
mkGlobal sp oldRequest newRequest
    = do let Left mr = applyRequest [sp] request
         resp <- mr
         case fmap P.unpack $ getHeader "content-type" resp of
           Just "application/json" -> return $ case parseJsonByteString (rsBody resp) of
                                                 Left err -> JDString err
                                                 Right val-> val
           Just contentType        -> return $ JDString $ "Incorrect content-type: " ++ contentType
           Nothing                 -> return $ JDString $ "Missing content-type"
    where request = newRequest { rqCookies = rqCookies newRequest ++ rqCookies oldRequest
                               , rqVersion = rqVersion oldRequest
                               , rqHeaders = rqHeaders oldRequest
                               , rqPeer = rqPeer oldRequest }

execTemplate :: FilePath -> HSPWeb Response
execTemplate tmpl =
    do state <- get
       let webState = WebState { queryGlobal = hspGlobal state
                               , queryLocal = hspLocal state }
       compResult <- liftIO $ requestPage state tmpl
       case compResult of
         Left errs -> return $ toResponse $ unlines errs
         Right (_,web) -> liftM toResponse $ liftIO $ evalHSP (runWebXML webState web)

requestPage state tmpl
    = do let file = (hspPrefix state) </> tmpl
         store <- readIORef (hspStore state)
         case Map.lookup file store of
           Just x -> return x
           Nothing -> do compResult <- compileHSP (hspPrefix state) (hspObjDir state) file
                         case compResult of
                           Right (mod,_) -> do atomicModifyIORef' (hspStore state)
                                                  $ Map.insert file compResult
                                               reloadNotification state file mod mod
                           _ -> return ()
                         return compResult

replacePath newPrefix oldPrefix path
    = worker (splitDirectories oldPrefix) (splitDirectories path)
    where worker (x:xs) (y:ys) | y==x = worker xs ys
          worker _ ys = newPrefix </> joinPath ys

reloadNotification state topFile topMod mod
    = do ino <- initINotify
         let realPath = replacePath (hspPrefix state) (hspObjDir state) (path mod)
         mbFile <- findFile knownExtensions realPath
         case mbFile of
           Nothing -> return undefined
           Just file -> addWatch ino [Modify,Move,Delete] file $ \event ->
                          do putStrLn $ "Recompiling: " ++ topFile ++ " because of " ++ show event
                             doUnload <- atomicModifyIORef (hspStore state)
                                         $ \ref -> (Map.insert topFile (Left ["Compiling..."]) ref
                                                   , case Map.lookup topFile ref of
                                                       Just (Right _) -> True
                                                       _              -> False )
                             when doUnload $ unloadAll topMod
                             compResult <- compileHSP (hspPrefix state) (hspObjDir state) topFile
                             atomicModifyIORef' (hspStore state) $ Map.insert topFile compResult
         deps <- getModuleDeps mod
         mapM_ (reloadNotification state topFile topMod) deps

findFile :: [String] -> FilePath -> IO (Maybe FilePath)
findFile [] _  = return Nothing
findFile (ext:exts) file
    = do let l = replaceExtension file ext
         b <- doesFileExist l
         if b then return $ Just l
              else findFile exts file

knownExtensions = [".hs",".lhs",".hsp"]

atomicModifyIORef' ref fn = atomicModifyIORef ref (\val -> (fn val, ()))

compileHSP :: FilePath -> FilePath -> FilePath -> IO (Either Errors (Module, Web XML))
compileHSP prefix objPath file
    = do createDirectoryIfMissing True objPath
         let extraArgs = ["-hidir",objPath, "-odir",objPath,"-i"++prefix]
         mkStatus <- liftIO $ makeAll file (extraArgs ++ hspArgs)
         case mkStatus of
           MakeFailure errs  -> return $ Left errs
           MakeSuccess mkcode obj -> do
             ldStatus <- liftIO $ load obj [objPath] [] "page"
             case ldStatus of
               LoadFailure errs -> return $ Left errs
               LoadSuccess mod page -> return $ Right (mod,page)



addParam :: ToJson json => String -> json -> HSPWeb ()
addParam key val
    = addJsonParam key (toJson val)

addJsonParam :: String -> JsonData -> HSPWeb ()
addJsonParam key val
    = modify $ \st -> st{hspLocal = Map.insert key val (hspLocal st) }