{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} module Main where import Control.Applicative ((<$>), optional) import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Lazy (unpack) import Happstack.Lite import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label) import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value) import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A -- | To start the app we call the `serve` function. The first argument -- is an optional configuration parameter. The second argument is our -- web application: main :: IO () main = serve Nothing myApp -- | The top-level of our application is just a bunch of routes mappings to handlers. -- -- `dir` is guard used to match on static path components. For -- example, `dir "echo"`, will match on http://localhost:8000/echo. To -- match on `"/foo/bar"` you would write, `dir "foo" $ dir "bar" $ -- handler`. -- -- Each of the routes is tried until one successfully returns a -- value. In this case, a `Response`. -- -- We convert the list of handlers in a single handler using `msum`. -- -- The last handler, `homePage` is not guarded by anything, so it will -- always be called if none of the other handlers were successful. myApp :: ServerPart Response myApp = msum [ dir "echo" $ echo , dir "query" $ queryParams , dir "form" $ formPage , dir "fortune" $ fortune , dir "files" $ fileServing , dir "upload" $ upload , homePage ] -- | Since this is a web application, we are going to want to create -- some HTML pages. We will do that using blaze-html. A blaze tutorial -- can be found here: -- -- http://jaspervdj.be/blaze/tutorial.html -- -- I like to make a template function which captures common elements -- of pages in my web app, such as importing style sheets, external -- javascript files, menus, etc. For this tutorial we have a very -- simple template: template :: Text -> Html -> Response template title body = toResponse $ H.html $ do H.head $ do H.title (toHtml title) H.body $ do body p $ a ! href "/" $ "back home" -- | `ok` tells the server to return the page with the HTTP response -- code '200 OK'. There are other helper functions like `notFound` and -- `seeOther` for other response codes. Or use `setResponseCode` to -- specify a response code by number. homePage :: ServerPart Response homePage = ok $ template "home page" $ do H.h1 "Hello!" H.p "Writing applications with happstack-lite is fast and simple!" H.p "Check out these killer apps." H.p $ a ! href "/echo/secret%20message" $ "echo" H.p $ a ! href "/query?foo=bar" $ "query parameters" H.p $ a ! href "/form" $ "form processing" H.p $ a ! href "/fortune" $ "(fortune) cookies" H.p $ a ! href "/files" $ "file serving" H.p $ a ! href "/upload" $ "file uploads" -- | The `dir` function only matches on static path segments. If we have -- a dynamic path segment, we can use the `path` function to capture -- the value and optionally convert it to another type such as -- Integer. In this example we just echo the captured path segment in -- the html. For example, trying visiting: -- -- http://localhost:8000/echo/fantastic echo :: ServerPart Response echo = path $ \(msg :: String) -> ok $ template "echo" $ do p $ "echo says: " >> toHtml msg p "Change the url to echo something else." -- We can also extract values from the query string part of the URL. The -- query string is the part that looks like "?foo=bar". Trying visiting: -- -- http://localhost:8000/query?foo=bar -- -- `lookText` will normally fail (by calling `mzero`) if the parameter -- is not found. In this example we used `optional` from -- `Control.Applicative` so that it will return a `Maybe` value -- instead. queryParams :: ServerPart Response queryParams = do mFoo <- optional $ lookText "foo" ok $ template "query params" $ do p $ "foo is set to: " >> toHtml (show mFoo) p $ "change the url to set it to something else." -- | We can use `lookText` (and friends) to extract values from forms -- as well. -- -- We use the same `lookText` function from the previous section to -- look up values in in form data. -- -- You will also note that we use the `method` function to select -- between a `GET` request and a `POST` request. -- -- When the user first views the form, the browser will request -- "/form" using the `GET` method. In the form tag, we see that the -- form will also be submitted to "/form" when the user presses the -- submit button. But in the form tag we have set the method attribute -- to `POST`. formPage :: ServerPart Response formPage = msum [ viewForm, processForm ] where viewForm :: ServerPart Response viewForm = do method GET ok $ template "form" $ form ! action "/form" ! enctype "multipart/form-data" ! A.method "POST" $ do label ! A.for "msg" $ "Say something clever" input ! type_ "text" ! A.id "msg" ! name "msg" input ! type_ "submit" ! value "Say it!" processForm :: ServerPart Response processForm = do method POST msg <- lookText "msg" ok $ template "form" $ do H.p "You said:" H.p (toHtml msg) -- | This example extends the form example to save the message in a -- cookie. That means you can navigate away from the page and when you -- come back later it will remember the message you saved. -- -- There are only a few new things in this example compared to the form example. -- -- 1. `lookCookieValue` works just like `lookText`, except it looks -- for the value in the cookies instead of request paramaters or form -- data. -- -- 2. `addCookies` sends cookies to the browser. addCookies has the -- type: `addCookies :: [(CookieLife, Cookie)] -> ServerPart ()` -- -- 3. `CookieLife` specifies how long the cookie is valid. `Session` -- means it is only valid until the browser window is closed. -- -- 4. `mkCookie` takes the cookie name and the cookie value and makes -- a `Cookie`. -- -- 5. `seeOther` (aka, 303 redirect) tells the browser to do a new -- GET request on "/fortune". fortune :: ServerPart Response fortune = msum [ viewFortune, updateFortune ] where viewFortune :: ServerPart Response viewFortune = do method GET mMemory <- optional $ lookCookieValue "fortune" let memory = fromMaybe "Your future will be filled with web programming." mMemory ok $ template "fortune" $ do H.p "The message in your (fortune) cookie says:" H.p (toHtml memory) form ! action "/fortune" ! enctype "multipart/form-data" ! A.method "POST" $ do label ! A.for "fortune" $ "Change your fortune: " input ! type_ "text" ! A.id "fortune" ! name "new_fortune" input ! type_ "submit" ! value "Say it!" updateFortune :: ServerPart Response updateFortune = do method POST fortune <- lookText "new_fortune" addCookies [(Session, mkCookie "fortune" (unpack fortune))] seeOther ("/fortune" :: String) (toResponse ()) -- | In most web applications, we will want to serve static files from the -- disk such as images, stylesheets, javascript, etc. We can do -- that using the `serveDirectory` function. -- -- The first argument specifies whether `serveDirectory` should create -- directory listings or not. -- -- The second argument is a list of index files. If the user requests -- a directory and the directory contains a index file (in this -- example "index.html"), then the server will display that index file -- instead of a directory listing. -- -- The third argument is the path to the directory we want to serve -- files from. Here we serve files from the current directory. -- -- On support platforms (Linux, OS X, Windows), the `serveDirectory` -- function will automatically use sendfile() to serve the -- files. sendfile() uses low-level kernel operations to transfer -- files directly from the disk to the network with minimal CPU usage -- and maximal bandwidth usage. fileServing :: ServerPart Response fileServing = serveDirectory EnableBrowsing ["index.html"] "." -- | Handling file uploads is very straight forward. We create a form, -- just as before. Except instead of `lookText` we use `lookFile`. -- -- When a file is uploaded, we store it in a temporary location. The -- temporary file will automatically be deleted after the server has -- sent the response. That ensures that unused files don't clutter up -- the disk. -- -- In most cases, you don't want a user to upload a file just to have -- it deleted. Normally the upload handler would use `moveFile` or -- `copyFile` to move (or copy) the temporary file to a permanent -- location. upload :: ServerPart Response upload = msum [ uploadForm , handleUpload ] where uploadForm :: ServerPart Response uploadForm = do method GET ok $ template "upload form" $ do form ! enctype "multipart/form-data" ! A.method "POST" ! action "/upload" $ do input ! type_ "file" ! name "file_upload" ! size "40" input ! type_ "submit" ! value "upload" handleUpload :: ServerPart Response handleUpload = do (tmpFile, uploadName, contentType) <- lookFile "file_upload" ok $ template "file uploaded" $ do p (toHtml $ "temporary file: " ++ tmpFile) p (toHtml $ "uploaded name: " ++ uploadName) p (toHtml $ "content-type: " ++ show contentType)