----------------------------------------------------------------------------- -- -- Module : MFlow.Forms.Internals -- Copyright : -- License : BSD3 -- -- Maintainer : agocorona@gmail.com -- Stability : experimental -- Portability : -- -- | -- ----------------------------------------------------------------------------- {-# OPTIONS -XDeriveDataTypeable -XExistentialQuantification -XScopedTypeVariables -XFlexibleInstances -XUndecidableInstances -XMultiParamTypeClasses -XGeneralizedNewtypeDeriving -XFlexibleContexts -XOverlappingInstances -XRecordWildCards -XTemplateHaskell #-} module MFlow.Forms.Internals where import MFlow import MFlow.Cookies import Control.Applicative import Data.Monoid import Control.Monad.Trans import Control.Monad.State import qualified Data.ByteString.Lazy.Char8 as B import qualified Data.ByteString.Char8 as SB import Data.Typeable import Data.RefSerialize hiding((<|>)) import Data.TCache import Data.TCache.Memoization import Data.TCache.DefaultPersistence import Data.TCache.Memoization import Data.Dynamic import qualified Data.Map as M import Unsafe.Coerce import Control.Workflow as WF import Control.Monad.Identity import Data.List import System.IO.Unsafe import Control.Concurrent.MVar import qualified Data.Text as T -- ---- for traces -- import Control.Exception as CE import Control.Concurrent import Control.Monad.Loc --import Debug.Trace --(!>) = flip trace data FailBack a = BackPoint a | NoBack a | GoBack deriving (Show,Typeable) instance (Serialize a) => Serialize (FailBack a ) where showp (BackPoint x)= insertString (B.pack iCanFailBack) >> showp x showp (NoBack x) = insertString (B.pack noFailBack) >> showp x showp GoBack = insertString (B.pack repeatPlease) readp = choice [icanFailBackp,repeatPleasep,noFailBackp] where noFailBackp = symbol noFailBack >> readp >>= return . NoBack icanFailBackp = symbol iCanFailBack >> readp >>= return . BackPoint repeatPleasep = symbol repeatPlease >> return GoBack iCanFailBack= "B" repeatPlease= "G" noFailBack= "N" newtype Sup m a = Sup { runSup :: m (FailBack a ) } class MonadState s m => Supervise s m where supBack :: s -> m () supBack = const $ return () supervise :: m (FailBack a) -> m (FailBack a) supervise= id --instance Monad m => Supervise () m where -- handle= const $ return () instance (Supervise s m)=> Monad (Sup m) where fail _ = Sup . return $ GoBack return x = Sup . return $ NoBack x x >>= f = Sup $ loop where loop = do s <- get v <- supervise $ runSup x -- !> "loop" case v of NoBack y -> supervise $ runSup (f y) -- !> "runback" BackPoint y -> do z <- supervise $ runSup (f y) -- !> "BACK" case z of GoBack -> supBack s >> loop -- !> "BACKTRACKING" other -> return other GoBack -> return $ GoBack fromFailBack (NoBack x) = x fromFailBack (BackPoint x)= x toFailBack x= NoBack x -- | the FlowM monad executes the page navigation. It perform Backtracking when necessary to syncronize -- when the user press the back button or when the user enter an arbitrary URL. The instruction pointer -- is moved to the right position within the procedure to handle the request. -- -- However this is transparent to the programmer, who codify in the style of a console application. newtype FlowM v m a= FlowM {runFlowM :: FlowMM v m a} deriving (Monad,MonadIO,Functor,MonadState(MFlowState v)) flowM= FlowM --runFlowM= runView {-# NOINLINE breturn #-} -- | Use this instead of return to return from a computation with ask statements -- -- This way when the user press the back button, the computation will execute back, to -- the returned code, according with the user navigation. breturn :: (Monad m) => a -> FlowM v m a breturn = flowM . Sup . return . BackPoint -- !> "breturn" instance (Supervise s m,MonadIO m) => MonadIO (Sup m) where liftIO f= Sup $ liftIO f >>= \ x -> return $ NoBack x instance (Monad m,Functor m) => Functor (Sup m) where fmap f g= Sup $ do mr <- runSup g case mr of BackPoint x -> return . BackPoint $ f x NoBack x -> return . NoBack $ f x GoBack -> return $ GoBack liftSup f = Sup $ f >>= \x -> return $ NoBack x instance MonadTrans Sup where lift f = Sup $ f >>= \x -> return $ NoBack x instance (Supervise s m,MonadState s m) => MonadState s (Sup m) where get= lift get -- !> "get" put= lift . put type WState view m = StateT (MFlowState view) m type FlowMM view m= Sup (WState view m) data FormElm view a = FormElm [view] (Maybe a) deriving Typeable instance Serialize a => Serialize (FormElm view a) where showp (FormElm _ x)= showp x readp= readp >>= \x -> return $ FormElm [] x -- | @View v m a@ is a widget (formlet) with formatting `v` running the monad `m` (usually `IO`) and which return a value of type `a` -- -- It has 'Applicative', 'Alternative' and 'Monad' instances. -- -- Things to know about these instances: -- -- If the View expression does not validate, ask will present the page again. -- -- /Alternative instance/: Both alternatives are executed. The rest is as usual -- -- /Monad Instance/: -- -- The rendering of each statement is added to the previous. If you want to avoid this, use 'wcallback' -- -- The execution is stopped when the statement has a formlet-widget that does not validate and -- return an invalid response (So it will present the page again if no other widget in the expression validates). -- -- The monadic code is executed from the beginning each time the page is presented or refreshed -- -- use 'pageFlow' if your page has more than one monadic computation with dynamic behaviour -- -- use 'pageFlow' to identify each subflow branch of a conditional -- -- For example: -- -- > pageFlow "myid" $ do -- > r <- formlet1 -- > liftIO $ ioaction1 r -- > s <- formlet2 -- > liftIO $ ioaction2 s -- > case s of -- > True -> pageFlow "idtrue" $ do .... -- > False -> paeFlow "idfalse" $ do ... -- > ... -- -- Here if @formlet2@ do not validate, @ioaction2@ is not executed. But if @formLet1@ validates and the -- page is refreshed two times (because @formlet2@ has failed, see above),then @ioaction1@ is executed two times. -- use 'cachedByKey' if you want to avoid repeated IO executions. newtype View v m a = View { runView :: WState v m (FormElm v a)} instance Monad m => Supervise (MFlowState v) (WState v m) where supBack st= do MFlowState{..} <- get put st{ mfEnv= mfEnv,mfToken=mfToken , mfPath=mfPath,mfPIndex= mfPIndex , mfData=mfData , mfTrace= mfTrace , inSync=False,newAsk=False} --instance CMT.MonadCatchIO (FlowM v IO) where -- catch f hand = FlowM . Sup $ do -- s <- get -- (r,s') <- lift $ execMF f s `CE.catch` \e -> execMF (hand e) s -- -- put s' -- return r -- where -- execMF f s= runStateT (runSup (runFlowM f) ) s instance MonadLoc (FlowM v IO) where withLoc loc f = FlowM . Sup $ do withLoc loc $ do s <- get (r,s') <- lift $ do rs@(r,s') <- runStateT (runSup (runFlowM f) ) s `CE.catch` (handler1 loc s) case mfTrace s' of [] -> return rs trace -> return(r, s'{mfTrace= loc:trace}) put s' return r where handler1 loc s (e :: SomeException)= do case CE.fromException e :: Maybe WFErrors of Just e -> CE.throw e -- !> ("TROWNF=" ++ show e) Nothing -> case CE.fromException e :: Maybe AsyncException of Just e -> CE.throw e -- !> ("TROWN ASYNCF=" ++ show e) Nothing -> return (GoBack, s{mfTrace= [show e]}) --instance (Serialize a,Typeable a, FormInput v) => MonadLoc (FlowM v (Workflow IO)) a where -- withLoc loc f = FlowM . Sup $ -- withLoc loc $ do -- s <- get -- (r,s') <- lift . WF.step $ exec1d "jkkjk" ( runStateT (runSup $ runFlowM f) s) `CMT.catch` (handler1 loc s) -- put s' -- return r -- -- where -- handler1 loc s (e :: SomeException)= -- return (GoBack, s{mfTrace= Just ["exception: " ++show e]}) instance MonadLoc (View v IO) where withLoc loc f = View $ do withLoc loc $ do s <- get (r,s') <- lift $ do rs@(r,s') <- runStateT (runView f) s `CE.catch` (handler1 loc s) case mfTrace s' of [] -> return rs trace -> return(r, s'{mfTrace= loc:trace}) put s' return r where handler1 loc s (e :: SomeException)= do case CE.fromException e :: Maybe WFErrors of Just e -> CE.throw e -- !> ("TROWN=" ++ show e) Nothing -> case CE.fromException e :: Maybe AsyncException of Just e -> CE.throw e -- !> ("TROWN ASYNC=" ++ show e) Nothing -> return (FormElm [] Nothing, s{mfTrace= [show e]}) -- !> loc instance (FormInput v,Serialize a) => Serialize (a,MFlowState v) where showp (x,s)= case mfDebug s of False -> showp x True -> showp(x, mfEnv s) readp= choice[nodebug, debug] where nodebug= readp >>= \x -> return (x, mFlowState0{mfSequence= -1}) debug= do (x,env) <- readp return (x,mFlowState0{mfEnv= env,mfSequence= -1}) instance Functor (FormElm view ) where fmap f (FormElm form x)= FormElm form (fmap f x) instance (Monad m,Functor m) => Functor (View view m) where fmap f x= View $ fmap (fmap f) $ runView x instance (Functor m, Monad m) => Applicative (View view m) where pure a = View $ return (FormElm [] $ Just a) View f <*> View g= View $ f >>= \(FormElm form1 k) -> g >>= \(FormElm form2 x) -> return $ FormElm (form1 ++ form2) (k <*> x) instance (Functor m, Monad m) => Alternative (View view m) where empty= View $ return $ FormElm [] Nothing View f <|> View g= View $ do FormElm form1 k <- f FormElm form2 x <- g return $ FormElm (form1 ++ form2) (k <|> x) instance (Monad m) => Monad (View view m) where View x >>= f = View $ do FormElm form1 mk <- x case mk of Just k -> do st <- get put st{linkMatched = False} FormElm form2 mk <- runView $ f k return $ FormElm (form1 ++ form2) mk Nothing -> return $ FormElm form1 Nothing return = View . return . FormElm [] . Just -- fail msg= View . return $ FormElm [fromStr msg] Nothing instance (Monad m, Functor m, Monoid a) => Monoid (View v m a) where mappend x y = mappend <$> x <*> y -- beware that both operands must validate to generate a sum mempty= return mempty -- | It is a callback in the view monad. The callback rendering substitutes the widget rendering -- when the latter is validated, without afecting the rendering of other widgets. This allow -- the simultaneous execution of different behaviours in different widgets in the -- same page. The inspiration is the callback primitive in the Seaside Web Framework -- that allows similar functionality (See ) -- -- This is the visible difference with 'waction' callbacks, which execute a -- a flow in the FlowM monad that takes complete control of the navigation, while wactions are -- executed whithin the same page. wcallback :: Monad m => View view m a -> (a -> View view m b) -> View view m b wcallback (View x) f = View $ do FormElm form1 mk <- x case mk of Just k -> do modify $ \st -> st{linkMatched= False, needForm=False} runView (f k) Nothing -> return $ FormElm form1 Nothing --clear :: Monad m => View v m () --clear= View $ do -- modify $ \s -> s{mfClear= True} -- return . FormElm [] $ Just () clear :: Monad m => View v m () clear = wcallback (return()) (const $ return()) --incLink :: MonadState (MFlowState view) m => m() --incLink= modify $ \st -> st{mfPIndex= if linkMatched st then mfPIndex st + 1 else mfPIndex st -- ,linkMatched= False} -- !> "<-inc" --incLinkDepth -- :: MFlowState v -> Int --incLinkDepth s= -- -- let depth= mfLinkDepth s -- in if mfLinkSelected s -- then -- depth +1 -- else -- depth --instance (Monad m) => Monad (FlowM view m) where -- --View view m a-> (a -> View view m b) -> View view m b -- FlowM x >>= f = FlowM $ do -- FormElm _ mk <- x -- case mk of -- Just k -> do -- FormElm _ mk <- runFlowM $ f k -- return $ FormElm [] mk -- Nothing -> return $ FormElm [] Nothing -- -- return= FlowM . return . FormElm [] . Just instance MonadTrans (View view) where lift f = View $ (lift f) >>= \x -> return $ FormElm [] $ Just x instance MonadTrans (FlowM view) where lift f = FlowM $ lift (lift f) -- >>= \x -> return x instance (Monad m)=> MonadState (MFlowState view) (View view m) where get = View $ get >>= \x -> return $ FormElm [] $ Just x put st = View $ put st >>= \x -> return $ FormElm [] $ Just x --instance (Monad m)=> MonadState (MFlowState view) (FlowM view m) where -- get = FlowM $ get >>= \x -> return $ FormElm [] $ Just x -- put st = FlowM $ put st >>= \x -> return $ FormElm [] $ Just x instance (MonadIO m) => MonadIO (View view m) where liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO onf the Identity monad -- | Execute the widget in a monad and return the result in another. changeMonad :: (Monad m, Executable m1) => View v m1 a -> View v m a changeMonad w= View . StateT $ \s -> let (r,s')= execute $ runStateT ( runView w) s in mfSequence s' `seq` return (r,s') -- | True if the flow is going back (as a result of the back button pressed in the web browser). -- Usually this check is nos necessary unless conditional code make it necessary -- -- @menu= do -- mop <- getGoStraighTo -- case mop of -- Just goop -> goop -- Nothing -> do -- r \<- `ask` option1 \<|> option2 -- case r of -- op1 -> setGoStraighTo (Just goop1) >> goop1 -- op2 -> setGoStraighTo (Just goop2) >> goop2@ -- -- This pseudocode below would execute the ask of the menu once. But the user will never have -- the possibility to see the menu again. To let him choose other option, the code -- has to be change to -- -- @menu= do -- mop <- getGoStraighTo -- back <- `goingBack` -- case (mop,back) of -- (Just goop,False) -> goop -- _ -> do -- r \<- `ask` option1 \<|> option2 -- case r of -- op1 -> setGoStraighTo (Just goop1) >> goop1 -- op2 -> setGoStraighTo (Just goop2) >> goop2@ -- -- However this is very specialized. Normally the back button detection is not necessary. -- In a persistent flow (with step) even this default entry option would be completely automatic, -- since the process would restart at the last page visited. goingBack :: MonadState (MFlowState view) m => m Bool goingBack = do st <- get return $ not (inSync st) && not (newAsk st) -- | Will prevent the Suprack beyond the point where 'preventGoingBack' is located. -- If the user press the back button beyond that point, the flow parameter is executed, usually -- it is an ask statement with a message. If the flow is not going back, it does nothing. It is a cut in Supracking -- -- It is useful when an undoable transaction has been commited. For example, after a payment. -- -- This example show a message when the user go back and press again to pay -- -- > ask $ wlink () << b << "press here to pay 100000 $ " -- > payIt -- > preventGoingBack . ask $ b << "You paid 10000 $ one time" -- > ++> wlink () << b << " Please press here to complete the proccess" -- > ask $ wlink () << b << "OK, press here to go to the menu or press the back button to verify that you can not pay again" -- > where -- > payIt= liftIO $ print "paying" preventGoingBack :: ( Functor m, MonadIO m, FormInput v) => FlowM v m () -> FlowM v m () preventGoingBack msg= do back <- goingBack if not back then breturn() else do breturn() -- will not go back beyond this clearEnv modify $ \s -> s{newAsk= True} msg type Lang= String data MFlowState view= MFlowState{ mfSequence :: Int, mfCached :: Bool, newAsk :: Bool, inSync :: Bool, mfLang :: Lang, mfEnv :: Params, needForm :: Bool, mfToken :: Token, mfkillTime :: Int, mfSessionTime :: Integer, mfCookies :: [Cookie], mfHttpHeaders :: [(SB.ByteString,SB.ByteString)], mfHeader :: view -> view, mfDebug :: Bool, mfRequirements :: [Requirement], mfData :: M.Map TypeRep Void, mfAjax :: Maybe (M.Map String Void), mfSeqCache :: Int, notSyncInAction :: Bool, -- Link management mfPath :: [String], mfPrefix :: String, mfPIndex :: Int, mfPageIndex :: Maybe Int, linkMatched :: Bool, mfLinks :: M.Map String Int, mfAutorefresh :: Bool, mfTrace :: [String], mfClear :: Bool } deriving Typeable type Void = Char mFlowState0 :: (FormInput view) => MFlowState view mFlowState0 = MFlowState 0 False True True "en" [] False (error "token of mFlowState0 used") 0 0 [] [] stdHeader False [] M.empty Nothing 0 False [] "" 1 Nothing False M.empty False [] False -- | Set user-defined data in the context of the session. -- -- The data is indexed by type in a map. So the user can insert-retrieve different kinds of data -- in the session context. -- -- This example define @addHistory@ and @getHistory@ to maintain a Html log in the session of a Flow: -- -- > newtype History = History ( Html) deriving Typeable -- > setHistory html= setSessionData $ History html -- > getHistory= getSessionData `onNothing` return (History mempty) >>= \(History h) -> return h -- > addHistory html= do -- > html' <- getHistory -- > setHistory $ html' `mappend` html setSessionData :: (Typeable a,MonadState (MFlowState view) m) => a -> m () setSessionData x= modify $ \st -> st{mfData= M.insert (typeOf x ) (unsafeCoerce x) (mfData st)} delSessionData x= modify $ \st -> st{mfData= M.delete (typeOf x ) (mfData st)} -- | Get the session data of the desired type if there is any. getSessionData :: (Typeable a, MonadState (MFlowState view) m) => m (Maybe a) getSessionData = resp where resp= gets mfData >>= \list -> case M.lookup ( typeOf $ typeResp resp ) list of Just x -> return . Just $ unsafeCoerce x Nothing -> return $ Nothing typeResp :: m (Maybe x) -> x typeResp= undefined -- | Return the user language. Now it is fixed to "en" getLang :: MonadState (MFlowState view) m => m String getLang= gets mfLang getToken :: MonadState (MFlowState view) m => m Token getToken= gets mfToken -- get a parameter form the las received response getEnv :: MonadState (MFlowState view) m => m Params getEnv = gets mfEnv stdHeader v = v -- | Set the header-footer that will enclose the widgets. It must be provided in the -- same formatting than them, altrough with normalization to byteStrings any formatting can be used -- -- This header uses XML trough Haskell Server Pages () -- -- @ -- setHeader $ \c -> -- \ -- \ -- \ my title \ -- \) -- \ -- \ -- \<% c %\> -- \ -- \ -- @ -- -- This header uses "Text.XHtml" -- -- @ -- setHeader $ \c -> -- `thehtml` -- << (`header` -- << (`thetitle` << title +++ -- `meta` ! [`name` \"Keywords\",content \"sci-fi\"])) +++ -- `body` ! [`style` \"margin-left:5%;margin-right:5%\"] c -- @ -- -- This header uses both. It uses byteString tags -- -- @ -- setHeader $ \c -> -- `bhtml` [] $ -- `btag` "head" [] $ -- (`toByteString` (thetitle << title) `append` -- `toByteString` ) `append` -- `bbody` [(\"style\", \"margin-left:5%;margin-right:5%\")] c -- @ setHeader :: MonadState (MFlowState view) m => (view -> view) -> m () setHeader header= do fs <- get put fs{mfHeader= header} -- | Return the current header getHeader :: ( Monad m) => FlowM view m (view -> view) getHeader= gets mfHeader -- | Add another header embedded in the previous one addHeader new= do fhtml <- getHeader setHeader $ fhtml . new -- | Set an HTTP cookie setCookie :: MonadState (MFlowState view) m => String -- ^ name -> String -- ^ value -> String -- ^ path -> Maybe Integer -- ^ Max-Age in seconds. Nothing for a session cookie -> m () setCookie n v p me= do modify $ \st -> st{mfCookies= (SB.pack n,SB.pack v,SB.pack p, fmap (SB.pack . show) me):mfCookies st } -- | Set an HTTP Response header setHttpHeader :: MonadState (MFlowState view) m => SB.ByteString -- ^ name -> SB.ByteString -- ^ value -> m () setHttpHeader n v = do modify $ \st -> st{mfHttpHeaders= (n,v):mfHttpHeaders st } -- | Set -- 1) the timeout of the flow execution since the last user interaction. -- Once passed, the flow executes from the begining. -- -- 2) In persistent flows -- it set the session state timeout for the flow, that is persistent. If the -- flow is not persistent, it has no effect. -- -- As the other state primitives, it can be run in the Flow and in the View monad -- -- `transient` flows restart anew. -- persistent flows (that use `step`) restart at the las saved execution point, unless -- the session time has expired for the user. setTimeouts :: ( MonadState (MFlowState v) m) => Int -> Integer -> m () setTimeouts kt st= do fs <- get put fs{ mfkillTime= kt, mfSessionTime= st} getWFName :: MonadState (MFlowState view) m => m String getWFName = do fs <- get return . twfname $ mfToken fs getCurrentUser :: MonadState (MFlowState view) m => m String getCurrentUser = do st<- gets mfToken return $ tuser st type Name= String type Type= String type Value= String type Checked= Bool type OnClick= Maybe String normalize :: (Monad m, FormInput v) => View v m a -> View B.ByteString m a normalize f= View . StateT $ \s ->do (FormElm fs mx, s') <- runStateT ( runView f) $ unsafeCoerce s return (FormElm (map toByteString fs ) mx,unsafeCoerce s') -- | Minimal interface for defining the basic form and link elements. The core of MFlow is agnostic -- about the rendering package used. Every formatting (either HTML or not) used with MFlow must have an -- instance of this class -- -- See "MFlow.Forms.Blaze.Html for the instance for blaze-html. "MFlow.Forms.XHtml" for the instance -- for @Text.XHtml@ and MFlow.Forms.HSP for the instance for Haskell Server Pages. class (Monoid view,Typeable view) => FormInput view where toByteString :: view -> B.ByteString toHttpData :: view -> HttpData fromStr :: String -> view fromStrNoEncode :: String -> view ftag :: String -> view -> view inred :: view -> view flink :: String -> view -> view flink1:: String -> view flink1 verb = flink verb (fromStr verb) finput :: Name -> Type -> Value -> Checked -> OnClick -> view ftextarea :: String -> T.Text -> view fselect :: String -> view -> view foption :: String -> view -> Bool -> view foption1 :: String -> Bool -> view foption1 val msel= foption val (fromStr val) msel formAction :: String -> view -> view attrs :: view -> Attribs -> view --instance (MonadIO m) => MonadIO (FlowM view m) where -- liftIO io= let x= liftIO io in x `seq` lift x -- to force liftIO==unsafePerformIO onf the Identity monad --instance Executable (View v m) where -- execute f = execute $ evalStateT f mFlowState0 --instance (Monad m, Executable m, Monoid view, FormInput view) -- => Executable (StateT (MFlowState view) m) where -- execute f= execute $ evalStateT f mFlowState0 -- | Cached widgets operate with widgets in the Identity monad, but they may perform IO using the execute instance -- of the monad m, which is usually the IO monad. execute basically \"sanctifies\" the use of unsafePerformIO for a transient purpose -- such is caching. This is defined in "Data.TCache.Memoization". The programmer can create his -- own instance for his monad. -- -- With `cachedWidget` it is possible to cache the rendering of a widget as a ByteString (maintaining type safety) --, permanently or for a certain time. this is very useful for complex widgets that present information. Specially it they must access to databases. -- -- @ -- import MFlow.Wai.Blaze.Html.All -- import Some.Time.Library -- addMessageFlows [(noscript, time)] -- main= run 80 waiMessageFlow -- time=do ask $ cachedWidget \"time\" 5 -- $ wlink () b << \"the time is \" ++ show (execute giveTheTime) ++ \" click here\" -- time -- @ -- -- this pseudocode would update the time every 5 seconds. The execution of the IO computation -- giveTheTime must be executed inside the cached widget to avoid unnecesary IO executions. -- -- NOTE: the rendering of cached widgets are shared by all users cachedWidget :: (MonadIO m,Typeable view , FormInput view, Typeable a, Executable m ) => String -- ^ The key of the cached object for the retrieval -> Int -- ^ Timeout of the caching. Zero means the whole server run -> View view Identity a -- ^ The cached widget, in the Identity monad -> View view m a -- ^ The cached result cachedWidget key t mf = View . StateT $ \s -> do let((FormElm form _), sec)= execute $ cachedByKey key t $ proc mf s{mfCached=True} let((FormElm _ mx2), s2) = execute $ runStateT ( runView mf) s{mfSeqCache= sec,mfCached=True} let s''= s{inSync = inSync s2 ,mfRequirements=mfRequirements s2 ,mfPath= mfPath s2 ,needForm= needForm s2 ,mfPIndex= mfPIndex s2 ,mfPageIndex= mfPageIndex s2 ,mfSeqCache= mfSeqCache s + mfSeqCache s2 - sec} return $ (mfSeqCache s'') `seq` ((FormElm form mx2), s'') -- !> ("enter: "++show (mfSeqCache s) ++" exit: "++ show ( mfSeqCache s2)) where proc mf s= runStateT (runView mf) s >>= \(r,_) ->mfSeqCache s `seq` return (r,mfSeqCache s ) -- | A shorter name for `cachedWidget` wcached :: (MonadIO m,Typeable view , FormInput view, Typeable a, Executable m ) => String -- ^ The key of the cached object for the retrieval -> Int -- ^ Timeout of the caching. Zero means sessionwide -> View view Identity a -- ^ The cached widget, in the Identity monad -> View view m a -- ^ The cached result wcached= cachedWidget -- | Unlike `cachedWidget`, which cache the rendering but not the user response, @wfreeze@ -- cache also the user response. This is useful for pseudo-widgets which just show information -- while the controls are in other non freezed widgets. A freezed widget ever return the first user response -- It is faster than `cachedWidget`. -- It is not restricted to the Identity monad. -- -- NOTE: the content of freezed widgets are shared by all users wfreeze :: (MonadIO m,Typeable view , FormInput view, Typeable a, Executable m ) => String -- ^ The key of the cached object for the retrieval -> Int -- ^ Timeout of the caching. Zero means sessionwide -> View view m a -- ^ The cached widget -> View view m a -- ^ The cached result wfreeze key t mf = View . StateT $ \s -> do (FormElm f mx, req,seq,ajax) <- cachedByKey key t $ proc mf s{mfCached=True} return ((FormElm f mx), s{mfRequirements=req ,mfSeqCache= seq,mfAjax=ajax}) where proc mf s= do (r,s) <- runStateT (runView mf) s return (r,mfRequirements s, mfSeqCache s, mfAjax s) {- | Execute the Flow, in the @FlowM view m@ monad. It is used as parameter of `hackMessageFlow` `waiMessageFlow` or `addMessageFlows` The flow is executed in a loop. When the flow is finished, it is started again @main= do addMessageFlows [(\"noscript\",transient $ runFlow mainf)] forkIO . run 80 $ waiMessageFlow adminLoop @ -} runFlow :: (FormInput view, MonadIO m) => FlowM view m () -> Token -> m () runFlow f t= loop (runFlowOnce1 f) t --evalStateT (runSup . runFlowM $ breturn() >> f) mFlowState0{mfToken=t,mfEnv= tenv t} >> return () -- >> return () where loop f t = do t' <- f t let t''= t'{tpath=[twfname t']} liftIO $ do flushRec t'' sendToMF t'' t'' -- !> "SEND" loop f t'' -- !> "LOOPAGAIN" runFlowOnce :: (MonadIO m, FormInput view, Monad m) => FlowM view m () -> Token -> m () runFlowOnce f t= runFlowOnce1 f t >> return () runFlowOnce1 f t = evalStateT (runSup . runFlowM $ do backInit f getToken) mFlowState0{mfToken=t ,mfPath= tpath t ,mfEnv= tenv t} >>= return . fromFailBack where backInit= do s <- get -- !> "BackInit" case mfTrace s of [] -> do modify $ \s -> s{mfEnv=[], newAsk= True} breturn () tr -> do error $ disp tr where disp tr= "TRACE (error in the last line):\n\n" ++(concat $ intersperse "\n" tr) -- to restart the flow in case of going back before the first page of the flow -- | Run a persistent flow inside the current flow. It is identified by the procedure and -- the string identifier. -- unlike the normal flows, that run within infinite loops, runFlowIn executes once. -- In subsequent executions, the flow will get the intermediate responses from te log -- and will return the result -- without asking again. This is useful for asking/storing/retrieving user defined configurations by -- means of web formularies. runFlowIn :: (MonadIO m, FormInput view) => String -> FlowM view (Workflow IO) b -> FlowM view m b runFlowIn wf f= FlowM . Sup $ do st <- get let t = mfToken st (r,st') <- liftIO $ exec1nc wf $ runFlow1 st f t put st{mfPath= mfPath st'} case r of GoBack -> delWF wf () return r where runFlow1 st f t= runStateT (runSup . runFlowM $ f) st --exec1bnc :: String -> Workflow IO a -> IO a --exec1bnc wf f= liftIO $ do -- ei <- getState wf f () -- case ei of -- Left err -> CE.throw err -- Right (name, _, stat) -> do -- let vers = reverse $ dropWhile isback $ reverse $ S.versions stat !> (unpack $ runW $ showp (S.versions stat)) -- stat'= stat{S.versions= vers, S.state= length vers} !> (unpack $ runW $ showp vers) -- atomically $ writeDBRef (S.self stat) stat' -- writeResource stat' -- runWF1 name f stat' False -- `CE.catch` -- (\(e :: CE.SomeException) -> liftIO $ do ---- let name= keyWF wf () -- clearRunningFlag name --`debug` ("exception"++ show e) -- CE.throw e ) -- `finally` -- (liftIO . atomically . -- when(S.recover stat) $ do -- let ref= S.self stat -- s <- readDBRef ref `onNothing` error ("step: not found: "++ S.wfName stat) -- writeDBRef ref s{S.recover= False,S.versions= reverse $ S.versions s}) -- -- where -- -- --isback x= serializedEqual x (pack "G ") -- -- --dropBacks _ []= [] --dropBacks nbacks (x:xs)= -- case isback x of -- True -> dropBacks (nbacks+1) xs -- False -> if nbacks== 0 then x:dropBacks 0 xs -- else dropBacks 0 xs --switchWF -- :: (MonadIO m, -- FormInput view) -- => String -- -> FlowM view (Workflow IO) b -- -> FlowM view m b --switchWF wf f= FlowM . Sup $ do -- st <- get -- let t = mfToken st -- (r,st') <- liftIO $ WF.exec1 wf $ runFlow1 st f t -- put st{mfPath= mfPath st'} -- return r --switchWF wf f= do -- liftIO $ addMessageFlows [(wf,runFlow f)] -- transfer wf -- -- where -- runFlow1 st f t= runStateT (runSup . runFlowM $ f) st -- | transfer control to another flow. (experimental) --transfer :: MonadIO m => String -> FlowM v m () --transfer flowname = do -- t <- gets mfToken -- let t'= t{twfname= flowname} -- liftIO $ do -- (r,_) <- msgScheduler t' -- sendFlush t r -- | to unlift a FlowM computation. useful for executing the configuration generated by runFLowIn -- outside of the web flow (FlowM) monad runFlowConf :: (FormInput view, MonadIO m) => FlowM view m a -> m a runFlowConf f = do q <- liftIO newEmptyMVar -- `debug` (i++w++u) qr <- liftIO newEmptyMVar let t= Token "" "" "" [] [] q qr evalStateT (runSup . runFlowM $ f ) mFlowState0{mfToken=t} >>= return . fromFailBack -- >> return () -- | Clears the environment clearEnv :: MonadState (MFlowState view) m => m () clearEnv= do st <- get put st{ mfEnv= []} -- | stores the result of the flow in a persistent log. When restarted, it get the result -- from the log and it does not execute it again. When no results are in the log, the computation -- is executed. It is equivalent to 'Control.Workflow.step' but in the FlowM monad. step :: (Serialize a, Typeable view, FormInput view, MonadIO m, Typeable a) => FlowM view m a -> FlowM view (Workflow m) a step f= do s <- get flowM $ Sup $ do (r,s') <- lift . WF.step $ runStateT (runSup $ runFlowM f) s -- when recovery of a workflow, the MFlow state is not considered when( mfSequence s' /= -1) $ put s' -- !> (show $ mfSequence s') -- else put s{newAsk=True} return r -- | to execute transient flows as if they were persistent -- it can be used instead of step, but it does not log the response. it ever executes the computation -- -- > transient $ runFlow f === runFlow $ transientNav f transientNav :: (Serialize a, Typeable view, FormInput view, Typeable a) => FlowM view IO a -> FlowM view (Workflow IO) a transientNav= MFlow.Forms.Internals.step --transientNav f= do -- s <- get -- flowM $ Sup $ do -- (r,s') <- lift . unsafeIOtoWF $ runStateT (runSup $ runFlowM f) s -- put s' -- return r --stepWFRef -- :: (Serialize a, -- Typeable view, -- FormInput view, -- MonadIO m, -- Typeable a) => -- FlowM view m a -- -> FlowM view (Workflow m) (WFRef (FailBack a),a) --stepWFRef f= do -- s <- get -- flowM $ Sup $ do -- (r,s') <- lift . WF.stepWFRef $ runStateT (runSup $ runFlowM f) s -- -- when recovery of a workflow, the MFlow state is not considered -- when( mfSequence s' >0) $ put s' -- return r --step f= do -- s <- get -- flowM $ Sup $ do -- (r,s') <- do -- (br,s') <- runStateT (runSup $ runFlowM f) s -- case br of -- NoBack r -> WF.step $ return r -- BackPoint r -> WF.step $ return r -- GoBack -> undoStep -- -- when recovery of a workflow, the MFlow state is not considered -- when( mfSequence s' >0) $ put s' -- return r --stepDebug -- :: (Serialize a, -- Typeable view, -- FormInput view, -- Monoid view, -- MonadIO m, -- Typeable a) => -- FlowM view m a -- -> FlowM view (Workflow m) a --stepDebug f= Sup $ do -- s <- get -- (r, s') <- lift $ do -- (r',stat)<- do -- rec <- isInRecover -- case rec of -- True ->do (r', s'') <- getStep 0 -- return (r',s{mfEnv= mfEnv (s'' `asTypeOf`s)}) -- False -> return (undefined,s) -- (r'', s''') <- WF.stepDebug $ runStateT (runSup f) stat >>= \(r,s)-> return (r, s) -- return $ (r'' `asTypeOf` r', s''' ) -- put s' -- return r data ParamResult v a= NoParam | NotValidated String v | Validated a deriving (Read, Show) valToMaybe (Validated x)= Just x valToMaybe _= Nothing isValidated (Validated x)= True isValidated _= False fromValidated (Validated x)= x fromValidated NoParam= error $ "fromValidated : NoParam" fromValidated (NotValidated s err)= error $ "fromValidated: NotValidated "++ s getParam1 :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v) => String -> Params -> m (ParamResult v a) getParam1 par req = case lookup par req of Just x1 -> readParam x1 Nothing -> return NoParam readParam :: (Monad m, MonadState (MFlowState v) m, Typeable a, Read a, FormInput v) => String -> m (ParamResult v a) readParam x1 = r where r= do modify $ \s -> s{inSync= True} maybeRead x1 getType :: m (ParamResult v a) -> a getType= undefined x= getType r maybeRead str= do let typeofx = typeOf x if typeofx == typeOf ( undefined :: String) then return . Validated $ unsafeCoerce str else if typeofx == typeOf (undefined :: T.Text) then return . Validated . unsafeCoerce $ T.pack str else case readsPrec 0 $ str of [(x,"")] -> return $ Validated x _ -> do let err= inred . fromStr $ "can't read \"" ++ str ++ "\" as type " ++ show (typeOf x) return $ NotValidated str err ---- Requirements -- | Requirements are javascripts, Stylesheets or server processes (or any instance of the 'Requirement' class) that are included in the -- Web page or in the server when a widget specifies this. @requires@ is the -- procedure to be called with the list of requirements. -- Varios widgets in the page can require the same element, MFlow will install it once. requires rs =do st <- get let l = mfRequirements st -- let rs'= map Requirement rs \\ l put st {mfRequirements= l ++ map Requirement rs} data Requirement= forall a.(Show a,Typeable a,Requirements a) => Requirement a deriving Typeable class Requirements a where installRequirements :: (Monad m,FormInput view) => [a] -> m view instance Show Requirement where show (Requirement a)= show a ++ "\n" installAllRequirements :: ( Monad m, FormInput view) => WState view m view installAllRequirements= do rs <- gets mfRequirements installAllRequirements1 mempty rs where installAllRequirements1 v []= return v installAllRequirements1 v rs= do let typehead= case head rs of {Requirement r -> typeOf r} (rs',rs'')= partition1 typehead rs v' <- installRequirements2 rs' installAllRequirements1 (v `mappend` v') rs'' where installRequirements2 []= return $ fromStrNoEncode "" installRequirements2 (Requirement r:rs)= installRequirements $ r:unmap rs unmap []=[] unmap (Requirement r:rs)= unsafeCoerce r:unmap rs partition1 typehead xs = foldr select ([],[]) xs where select x ~(ts,fs)= let typer= case x of Requirement r -> typeOf r in if typer== typehead then ( x:ts,fs) else (ts, x:fs) -- Web requirements --- loadjsfile filename lcallbacks= "var fileref=document.createElement('script');\ \fileref.setAttribute('type','text/javascript');\ \fileref.setAttribute('src',\'" ++ filename ++ "\');\ \document.getElementsByTagName('head')[0].appendChild(fileref);" ++ onload where onload= case lcallbacks of [] -> "" cs -> "fileref.onload = function() {"++ (concat $ nub cs)++"};" loadjs content= content loadcssfile filename= "var fileref=document.createElement('link');\ \fileref.setAttribute('rel', 'stylesheet');\ \fileref.setAttribute('type', 'text/css');\ \fileref.setAttribute('href', \'"++filename++"\');\ \document.getElementsByTagName('head')[0].appendChild(fileref);" loadcss content= "var fileref=document.createElement('link');\ \fileref.setAttribute('rel', 'stylesheet');\ \fileref.setAttribute('type', 'text/css');\ \fileref.innerText=\""++content++"\";\ \document.getElementsByTagName('head')[0].appendChild(fileref);" data WebRequirement= JScriptFile String [String] -- ^ Script URL and the list of scripts to be executed when loaded | CSSFile String -- ^ a CSS file URL | CSS String -- ^ a String with a CSS description | JScript String -- ^ a string with a valid JavaScript | ServerProc (String, Flow) -- ^ a server procedure deriving(Typeable,Eq,Ord,Show) instance Eq (String, Flow) where (x,_) == (y,_)= x == y instance Ord (String, Flow) where compare(x,_) (y,_)= compare x y instance Show (String, Flow) where show (x,_)= show x instance Requirements WebRequirement where installRequirements= installWebRequirements installWebRequirements :: (Monad m,FormInput view) =>[WebRequirement] -> m view installWebRequirements rs= do let s = jsRequirements $ sort rs return $ ftag "script" (fromStrNoEncode s) jsRequirements []= "" jsRequirements (r@(JScriptFile f c) : r'@(JScriptFile f' c'):rs) | f==f' = jsRequirements $ JScriptFile f (nub $ c++c'):rs | otherwise= strRequirement r ++ jsRequirements (r':rs) jsRequirements (r:r':rs) | r== r' = jsRequirements $ r:rs | otherwise= strRequirement r ++ jsRequirements (r':rs) jsRequirements (r:rs)= strRequirement r++jsRequirements rs strRequirement (CSSFile s') = loadcssfile s' strRequirement (CSS s') = loadcss s' strRequirement (JScriptFile s' call) = loadjsfile s' call strRequirement (JScript s') = loadjs s' strRequirement (ServerProc f)= (unsafePerformIO $! addMessageFlows [f]) `seq` "" --- AJAX ---- ajaxScript= "function loadXMLObj()" ++ "{" ++ "var xmlhttp;" ++ "if (window.XMLHttpRequest)" ++ "{"++ " xmlhttp=new XMLHttpRequest();" ++ " }" ++ "else" ++ "{"++ " xmlhttp=new ActiveXObject('Microsoft.XMLHTTP');" ++ " }" ++ "return xmlhttp" ++ "};" ++ " xmlhttp= loadXMLObj();" ++ " noparam= '';"++ ""++ "function doServer (servproc,param,param2){" ++ " xmlhttp.open('GET',servproc+'?ajax='+param+'&val='+param2,true);" ++ " xmlhttp.send();};" ++ ""++ "xmlhttp.onreadystatechange=function()" ++ " {" ++ " if (xmlhttp.readyState== 4 && xmlhttp.status==200)" ++ " {" ++ " eval(xmlhttp.responseText);" ++ " }" ++ " };" ++ "" formPrefix index verb st form anchored= do let path = currentPath False index (mfPath st) verb (anchor,anchorf) <- case anchored of True -> do anchor <- genNewId return ('#':anchor, (ftag "a") mempty `attrs` [("name",anchor)]) False -> return (mempty,mempty) return $ formAction (path ++ anchor ) $ mconcat ( anchorf:form) -- !> anchor -- | insert a form tag if the widget has form input fields. If not, it does nothing insertForm w=View $ do FormElm forms mx <- runView w st <- get cont <- case needForm st of True -> do frm <- formPrefix (mfPIndex st) (twfname $ mfToken st ) st forms False put st{needForm= False} return frm _ -> return $ mconcat forms return $ FormElm [cont] mx currentPath insInBackTracking index lpath verb = (if null lpath then verb else case insInBackTracking of True -> concat $ take index ['/':v | v <- lpath] -- !> ("index= " ++ show index) False -> concat ['/':v| v <- lpath]) -- | Generate a new string. Useful for creating tag identifiers and other attributes. -- -- if the page is refreshed, the identifiers generated are the same. genNewId :: MonadState (MFlowState view) m => m String genNewId= do st <- get case mfCached st of False -> do let n= mfSequence st prefseq= mfPrefix st put $ st{mfSequence= n+1} return $ 'p':show n++prefseq True -> do let n = mfSeqCache st put $ st{mfSeqCache=n+1} return $ 'c' : (show n) -- | get the next ideitifier that will be created by genNewId getNextId :: MonadState (MFlowState view) m => m String getNextId= do st <- get case mfCached st of False -> do let n= mfSequence st prefseq= mfPrefix st return $ 'p':show n++prefseq True -> do let n = mfSeqCache st return $ 'c' : (show n)