packages feed

stomp-queue-0.5.0: Network/Mom/Stompl/Client/Stream.hs

module Network.Mom.Stompl.Client.Stream
where

  import qualified Data.Conduit as C
  import           Data.Conduit ((.|))

  import           Data.Conduit.Network (AppData)  
  import           Data.Conduit.Network  as N

  import qualified Data.ByteString.Char8 as B
  import qualified Data.ByteString.UTF8  as U

  import           Network.Mom.Stompl.Parser (stompParser)
  import qualified Network.Mom.Stompl.Frame as F
  import           Network.Mom.Stompl.Client.Exception

  import           Control.Monad (forever)
  import           Control.Monad.Trans (liftIO)
  import           Control.Monad.IO.Class (MonadIO)
  import           Control.Concurrent

  import qualified Data.Attoparsec.ByteString as A 

  ------------------------------------------------------------------------
  -- Error Handler
  ------------------------------------------------------------------------
  type EH = StomplException -> IO ()

  ------------------------------------------------------------------------
  -- A TCP/IP fragment read by the Conduit Client has 4096 bytes.
  -- We allow 1000 fragments = 1024 * 4096 Bytes = 4MB
  ------------------------------------------------------------------------
  maxStep :: Int
  maxStep = 1024

  ------------------------------------------------------------------------
  -- Sender thread: get a Frame from a pipe, convert it into a ByteString
  --                and send it through a socket 
  ------------------------------------------------------------------------
  sender :: AppData -> Chan F.Frame -> IO ()
  sender ad ip =  C.runConduitRes (pipeSource ip .| stream .| N.appSink ad)

  ------------------------------------------------------------------------
  -- Receiver thread: get a ByteStream through a socket,
  --                  parse it to a Frame and send it through a pipe
  ------------------------------------------------------------------------
  receiver :: AppData -> Chan F.Frame -> EH -> IO ()
  receiver ad ip eh = C.runConduitRes (appSource ad .| parseC eh .| pipeSink ip) 

  ------------------------------------------------------------------------
  -- Put a frame into a pipe (a channel)
  ------------------------------------------------------------------------
  pipeSink :: MonadIO m => Chan F.Frame -> C.ConduitT F.Frame C.Void m ()
  pipeSink ch = C.awaitForever (liftIO . writeChan ch)

  ------------------------------------------------------------------------
  -- Read a frame from a pipe (a channel)
  ------------------------------------------------------------------------
  pipeSource :: MonadIO m => Chan F.Frame -> C.ConduitT () F.Frame m ()
  pipeSource ch = forever (liftIO (readChan ch) >>= C.yield)

  ------------------------------------------------------------------------
  -- Convert a frame to a ByteString
  ------------------------------------------------------------------------
  stream :: MonadIO m => C.ConduitT F.Frame B.ByteString m () 
  stream = C.awaitForever (C.yield . F.putFrame)

  ------------------------------------------------------------------------
  -- Parse a Frame from a ByteString
  ------------------------------------------------------------------------
  parseC :: MonadIO m => EH -> C.ConduitT B.ByteString F.Frame m ()
  parseC eh = goOn
    where goOn = go (A.parse stompParser) 0 -- start with a clean parser
          go prs step = do
            mbNew <- C.await
            case mbNew of 
              Nothing -> return () -- socket was closed
              Just s  -> case parseAll prs s of
                           -- parse error: call the error handler ---------
                           Left e -> liftIO (eh $ ProtocolException e)
                                     >> goOn
                           -- we got a result -----------------------------
                           Right (prs', fs) -> do
                             -- Do we have (at least) 1 frame to send? ----
                             step' <- if null fs then return (step+1) 
                                                 else mapM_ C.yield fs >>
                                                      return 0
                             -- Too many fragments ------------------------
                             if step' > maxStep 
                               then liftIO (eh $ ProtocolException 
                                                 "Message too long!") 

                             -- Continue with the current parser ----------
                               else go prs' step'

  ------------------------------------------------------------------------
  -- A parser is something that converts a ByteString into a Frame
  ------------------------------------------------------------------------
  type Parser = B.ByteString -> A.Result F.Frame

  ------------------------------------------------------------------------
  -- Continue parsing until we have a complete frame
  ------------------------------------------------------------------------
  parseAll :: Parser -> B.ByteString -> 
              Either String (Parser, [F.Frame])
  parseAll prs s = case prs s of
                     -- We failed ----------------------------------------
                     A.Fail _ _   e  -> Left $ U.toString s ++ ": " ++ e

                     -- We have a partial result and continue -------------
                     --    feeding this partial result --------------------
                     r@(A.Partial _) -> Right (A.feed r, [])

                     -- We are done ---------------------------------------
                     A.Done s' f     -> 
                       if B.null s' 
                         then Right (A.parse stompParser, [f])
                         -- but there may be a leftover -------------------
                         else case parseAll (A.parse stompParser) s' of
                                Left e           -> Left e
                                Right (prs', fs) -> Right (prs',f:fs)