packages feed

live-sequencer-0.0.4: http/enable/HTTPServer.hs

module HTTPServer (
    Methods(Methods),
    run,
    getModuleList,
    getModuleContent,
    updateModuleContent,
    separatorLine,
    splitProtected,
    Error, notFound,
    ) where

import qualified HTTPServer.Option as Option
import qualified Module
import qualified IO

import qualified Network.Shed.Httpd as HTTPd
import qualified Network.CGI as CGI
import Network.URI ( uriPath )

import Text.Html((<<), (+++), )
import qualified Text.Html as Html

import qualified Control.Monad.Exception.Synchronous as Exc
import Control.Monad.Exception.Synchronous ( ExceptionalT )

import qualified Text.ParserCombinators.Parsec as Parsec

import qualified Control.Monad.Trans.Class as MT

import qualified Data.List.HT as ListHT
import qualified Data.List as List
import Data.Tuple.HT ( mapPair )
import Control.Functor.HT ( void )


headers :: [(String, String)]
headers =
    [("Content-Type", "text/html; charset=latin1")]

data Error = Error Int String

badRequest :: String -> Error
badRequest = Error 400

notFound :: String -> Error
notFound = Error 404

methodNotAllowed :: String -> Error
methodNotAllowed = Error 405


data Methods =
    Methods {
        getModuleList :: IO [Module.Name],
        getModuleContent ::
            Module.Name -> ExceptionalT Error IO String,
        updateModuleContent ::
            Module.Name -> String ->
            ExceptionalT Error IO (Maybe String, String)
    }

run :: Methods -> Option.Option -> IO ()
run dict opt = 
    case Option.port opt of
        Option.Port port ->
            void $ HTTPd.initServer port $
                handleException . server dict opt

server ::
    Methods ->
    Option.Option ->
    HTTPd.Request ->
    ExceptionalT Error IO HTTPd.Response
server dict opt req =
    case HTTPd.reqMethod req of
        "GET" ->
            case uriPath (HTTPd.reqURI req) of
                "/" ->
                    MT.lift $
                    fmap (HTTPd.Response 200 headers . formatModuleList) $
                    getModuleList dict
                '/':modName -> do
                    modList <- MT.lift $ getModuleList dict
                    modIdent <- parseModuleName modName
                    content <- getModuleContent dict modIdent
                    return $ HTTPd.Response 200 headers $
                        formatModuleContent opt modList modIdent (Nothing, content)
                _ ->
                    Exc.throwT $ badRequest $ "Bad path in URL"
        "POST" ->
            case uriPath $ HTTPd.reqURI req of
                '/':modName -> do
                    modList <- MT.lift $ getModuleList dict
                    modIdent <- parseModuleName modName
                    editable <-
                        case lookup "content" $ CGI.formDecode $ HTTPd.reqBody req of
                            Just str -> return str
                            _ -> Exc.throwT $ badRequest $
                                 "Argument 'content' missing"
                    updatedContent <-
                        updateModuleContent dict modIdent editable
                    return $ HTTPd.Response 200 headers $
                        formatModuleContent opt modList modIdent updatedContent
                _ ->
                    Exc.throwT $ badRequest $ "Bad path in URL"
        method ->
            Exc.throwT $ methodNotAllowed $
                "Method " ++ method ++ " not allowed"


handleException ::
    (Monad m) =>
    ExceptionalT Error m HTTPd.Response -> m HTTPd.Response
handleException =
    Exc.resolveT $ \(Error errorCode msg) ->
        return $
        HTTPd.Response errorCode headers $
        Html.renderHtml $
        Html.header (Html.thetitle <<
            ("Haskell Live Sequencer - Error " ++ show errorCode)) +++
        (Html.body $ Html.concatHtml $
         map (\line -> Html.toHtml line +++ Html.br) $ lines msg)


parseModuleName ::
    (Monad m) =>
    String -> ExceptionalT Error m Module.Name
parseModuleName modName =
    Exc.mapExceptionT
        (badRequest .
         ("syntax error in module name:\n"++) .
         show) $
    Exc.fromEitherT $ return $
    Parsec.parse
        (Parsec.between (return ()) Parsec.eof IO.input)
        "" modName

formatModuleList :: [Module.Name] -> String
formatModuleList list =
    Html.renderHtml $
    Html.header (Html.thetitle << "Haskell Live Sequencer - Module list") +++
    Html.body (htmlFromModuleList list)

formatModuleContent ::
    Option.Option ->
    [Module.Name] -> Module.Name -> (Maybe String, String) -> String
formatModuleContent opt list name (mmsg, content) =
    Html.renderHtml $
    Html.header (Html.thetitle <<
        ("Haskell Live Sequencer - " ++ Module.tellName name)) +++
    (Html.body $
        sideBySide
            (htmlFromModuleList list)
            (Html.h1 << Module.deconsName name +++
             maybe Html.noHtml
                 (\msg -> (Html.! [Html.color Html.red]) $ Html.font $
                     htmlFromMultiline $ "error:\n" ++ msg) mmsg +++
             ((Html.! [Html.action $ Module.deconsName name, Html.method "post",
                       Html.HtmlAttr "accept-charset" "ISO-8859-1"]) $
              Html.form $
                  case splitProtected content of
                      (protected, sepEditable) ->
                          Html.pre << protected
                          +++
                          case sepEditable of
                              Nothing -> Html.noHtml
                              Just (separator, editable) ->
                                  Html.pre << separator
                                  +++
                                  Html.textarea
                                      Html.! [Html.name "content",
                                              Html.rows $ Option.rows opt,
                                              Html.cols $ Option.columns opt]
                                      << editable
                                  +++
                                  Html.br
                                  +++
                                  Html.submit "" "submit")))

splitProtected :: String -> (String, Maybe (String, String))
splitProtected =
    mapPair (unlines,
        \lns ->
            case lns of
                separator:suffix -> Just (separator, unlines suffix)
                [] -> Nothing) .
    ListHT.break (List.isPrefixOf separatorLine) .
    lines

separatorLine :: String
separatorLine = replicate 8 '-'

htmlFromMultiline :: String -> Html.Html
htmlFromMultiline =
    Html.concatHtml . List.intersperse Html.br .
    map Html.toHtml . lines

{-
As far as I can see,
CSS would require me to use an absolute horizontal position
for the module content.
Thus I stick to the much-maligned tables.
-}
sideBySide :: Html.Html -> Html.Html -> Html.Html
sideBySide left right =
    Html.simpleTable [] [Html.valign "top"] [[left, right]]

htmlFromModuleList :: [Module.Name] -> Html.Html
htmlFromModuleList =
    (Html.! [Html.identifier "module-list"]) .
    Html.unordList . map
       (\name -> (Html.anchor << Module.deconsName name) Html.! [Html.href $ Module.deconsName name])