-- |Maintains a persistent HTTP connection to a CouchDB database server. -- CouchDB enjoys closing the connection if there is an error (document -- not found, etc.) In such cases, 'CouchMonad' will automatically -- reestablish the connection. {-# LANGUAGE CPP #-} module Database.CouchDB.HTTP ( request , RequestMethod (..) , CouchMonad , Response (..) , runCouchDB , runCouchDB' , runCouchDBURI , CouchConn() , createCouchConn , createCouchConnFromURI , runCouchDBWith , closeCouchConn ) where import Data.IORef import Control.Concurrent import Network.TCP import Network.HTTP import Network.URI import Control.Exception (bracket) import Control.Monad (liftM) import Control.Monad.Trans (MonadIO (..)) import Data.Maybe (fromJust) import qualified Data.ByteString as BS (ByteString, length) import qualified Data.ByteString.UTF8 as UTF8 (fromString, toString) import Network.HTTP.Auth import Control.Applicative import Control.Monad (ap) -- |Describes a connection to a CouchDB database. This type is -- encapsulated by 'CouchMonad'. data CouchConn = CouchConn { ccConn :: IORef (HandleStream BS.ByteString) , ccURI :: URI , ccHostname :: String , ccPort :: Int , ccAuth :: Maybe Authority -- ^login credentials, if needed. } -- |A computation that interacts with a CouchDB database. This monad -- encapsulates the 'IO' monad, a persistent HTTP connnection to a -- CouchDB database and enough information to re-open the connection -- if it is closed. data CouchMonad a = CouchMonad (CouchConn -> IO (a,CouchConn)) instance Applicative CouchMonad where pure = return (<*>) = ap instance Functor CouchMonad where fmap = liftM instance Monad CouchMonad where return a = CouchMonad $ \conn -> return (a,conn) (CouchMonad m) >>= k = CouchMonad $ \conn -> do (a,conn') <- m conn let (CouchMonad m') = k a m' conn' #if MIN_VERSION_base(4,13,0) instance MonadFail CouchMonad where #endif fail msg = CouchMonad $ \conn -> do fail $ "internal error: " ++ msg instance MonadIO CouchMonad where liftIO m = CouchMonad $ \conn -> m >>= \a -> return (a,conn) makeURL :: String -- ^path -> [(String,String)] -> CouchMonad URI makeURL path query = CouchMonad $ \conn -> do return ( (ccURI conn) { uriPath = '/':path , uriQuery = '?':(urlEncodeVars query) } ,conn ) getConn :: CouchMonad (HandleStream BS.ByteString) getConn = CouchMonad $ \conn -> do r <- readIORef (ccConn conn) return (r,conn) getConnAuth :: CouchMonad (Maybe Authority) getConnAuth = CouchMonad $ \conn -> return ((ccAuth conn),conn) reopenConnection :: CouchMonad () reopenConnection = CouchMonad $ \conn -> do c <- liftIO $ readIORef (ccConn conn) >>= close connection <- liftIO $ openTCPConnection (ccHostname conn) (ccPort conn) writeIORef (ccConn conn) connection return ((), conn) makeHeaders bodyLen = [ Header HdrContentType "application/json" , Header HdrConnection "keep-alive" , Header HdrContentLength (show bodyLen) ] -- |Send a request to the database. If the connection is closed, it is -- reopened and the request is resent. On other errors, we raise an -- exception. request :: String -- ^path of the request -> [(String,String)] -- ^dictionary of GET parameters -> RequestMethod -> [Header] -> String -- ^body of the request -> CouchMonad (Response String) request path query method headers body = do let body' = UTF8.fromString body url <- makeURL path query let allHeaders = (makeHeaders (BS.length body')) ++ headers conn <- getConn auth <- getConnAuth let req' = Request url method allHeaders body' let req = maybe req' (fillAuth req') auth let retry 0 = do fail $ "server error: " ++ show req retry n = do response <- liftIO $ sendHTTP conn req case response of Left err -> do reopenConnection retry (n-1) Right val -> return (unUTF8 val) retry 2 where unUTF8 :: Response BS.ByteString -> Response String unUTF8 (Response c r h b) = Response c r h (UTF8.toString b) fillAuth :: Request a -> Authority -> Request a fillAuth req auth = req { rqHeaders = new : rqHeaders req } where new = Header HdrAuthorization (withAuthority auth req) runCouchDBURI :: URI -- ^URI to connect -> CouchMonad a -> IO a runCouchDBURI uri act = bracket (createCouchConnFromURI uri) closeCouchConn (flip runCouchDBWith act) runCouchDB :: String -- ^hostname -> Int -- ^port -> CouchMonad a -> IO a runCouchDB hostname port act = bracket (createCouchConn hostname port) closeCouchConn (flip runCouchDBWith act) -- |Connects to the CouchDB server at localhost:5984. runCouchDB' :: CouchMonad a -> IO a runCouchDB' = runCouchDB "127.0.0.1" 5984 -- |Run a CouchDB computation with an existing CouchDB connection. runCouchDBWith :: CouchConn -> CouchMonad a -> IO a runCouchDBWith conn (CouchMonad f) = fmap fst $ f conn -- |Create a CouchDB connection for use with runCouchDBWith. createCouchConn :: String -- ^hostname -> Int -- ^port -> IO (CouchConn) createCouchConn hostname port = createCouchAuthConn hostname port Nothing -- |Create a CouchDB connection with password authentication for use -- with runCouchDBWith. createCouchAuthConn :: String -- ^hostname -> Int -- ^port -> Maybe Authority -- ^Login credentials -> IO (CouchConn) createCouchAuthConn hostname port auth = do let uriAuth = URIAuth "" hostname (':':(show port)) let baseURI = URI "http:" (Just uriAuth) "" "" "" c <- openTCPConnection hostname port conn <- newIORef c return (CouchConn conn baseURI hostname port auth) -- |Create a CouchDB from an URI connection for use with runCouchDBWith. createCouchConnFromURI :: URI -- ^URI with possible login credentials -> IO (CouchConn) createCouchConnFromURI baseURI = do createCouchAuthConn hostname port auth where ua = fromJust $ uriAuthority baseURI hostname = uriRegName ua port = uriAuthPort (Just baseURI) ua ua2 = (fromJust.parseURIAuthority.uriToAuthorityString) baseURI auth = (Just AuthBasic) `ap` (return "") `ap` (user ua2) `ap` (password ua2) `ap` (return baseURI) -- |Closes an open CouchDB connection closeCouchConn :: CouchConn -> IO () closeCouchConn (CouchConn conn _ _ _ _ ) = readIORef conn >>= close