Hlavní navigace

Leiningen: nástroj pro správu projektů napsaných v Clojure (5)

Pavel Tišnovský 12. 3. 2015

I v páté části článku o projektovém správci Leiningen určeném pro správu projektů vytvořených v programovacím jazyku Clojure se budeme zabývat tvorbou jednoduchých webových aplikací s využitím knihovny Ring. Ukážeme se použití tří typů middleware nazvaných wrap-params, wrap-session a wrap-cookies.

Obsah

1. Leiningen: nástroj pro správu projektů napsaných v Clojure (5)

2. První demonstrační příklad – webová aplikace s jednoduchým handlerem

3. Zpracování parametrů požadavku

4. Druhý demonstrační příklad – použití ring.middleware.params/wrap-params

5. Práce se session

6. Třetí demonstrační příklad – použití ring.middleware.session/wrap-session

7. Implementace čítače uloženého v session

8. Čtvrtý demonstrační příklad – uložení hodnoty čítače do session

9. Pátý demonstrační příklad – uložení hodnoty čítače do session (zjednodušená varianta)

10. Šestý demonstrační příklad – zobrazení hodnoty čítače na WWW stránce

11. Základy práce s cookies

12. Sedmý demonstrační příklad – použití ring.middleware.cookies/wrap-cookies

13. Přečtení hodnoty z cookie a nastavení nové hodnoty

14. Odkazy na Internetu

1. Leiningen: nástroj pro správu projektů napsaných v Clojure (5)

Ve čtvrté části článku o nástroji Leiningen jsme si popsali některé základní koncepty, na níž je postavena knihovna Clojure Ring, kterou je možné využít pro tvorbu jednodušších i složitějších webových aplikací v programovacím jazyku Clojure. Připomeňme si, že tato knihovna je abstrakcí mezi webovým kontejnerem (kterým je typicky Jetty či Tomcat) a aplikační logikou. Základem knihovny Clojure Ring je takzvaný handler, což je funkce zavolaná ve chvíli příchodu HTTP požadavku od klienta. Handler musí zajistit vytvoření HTTP odpovědi, přičemž je Ring postaven do značné míry na základních principech funkcionálního programování: handler dostane jako parametr neměnnou (immutable) mapu představující požadavek klienta (request) a výsledkem jeho práce je jiná neměnná mapa reprezentující odpověď serveru (response). Handler lze tedy snadno testovat, rozšiřovat apod.

Webová aplikace využívající knihovnu Ring může být v nejjednodušším případě složena pouze ze tří vrstev:

  1. Aplikační logika
  2. Ring Adapter
  3. Webový server/servlet kontejner/vestavěný kontejner

Navíc se ještě v knihovně Clojure Ring objevuje další zajímavý koncept. Jedná se o takzvané middleware, což jsou v pojetí Ringu makra a funkce vkládané mezi Ring a vlastní implementaci webové aplikace. Tyto funkce a makra tedy například mohou z původní mapy s požadavkem vytvořit novou mapu, v níž již budou předpřipraveny parametry požadavku, cookies, objekty uložené na session atd. My si v dnešním článku popíšeme trojici již připravených middleware:

ring.middleware.params/wrap-params
ring.middleware.session/wrap-session
ring.middleware.cookies/wrap-cookies

2. První demonstrační příklad – webová aplikace s jednoduchým handlerem

Dnešní první demonstrační příklad vznikl nepatrnou úpravou příkladů, které jsme si již popsali v předchozí části tohoto článku. Tento příklad uvádím zejména z toho důvodu, že na jeho zdrojovém kódu bude postaveno i šest ostatních demonstračních příkladů, takže si v dalších kapitolách již nebudeme popisovat tvorbu základní kostry webové aplikace, její spuštění apod. Vytvoření nového projektu nám zajistí notoricky známý příkaz:

lein new app webapp5

Následně upravíme soubor project.clj takovým způsobem, aby se projekt odkazoval na všechny potřebné moduly knihovny Clojure Ring (zvýrazněné řádky):

(defproject webapp3 "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [ring/ring-core "1.3.2"]
                 [ring/ring-jetty-adapter "1.3.2"]]
  :main ^:skip-aot webapp3.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

Změní se samozřejmě i soubor src/webapp5/core.clj, a to následujícím způsobem:

(ns webapp6.core
    (:gen-class))
 
(require '[ring.adapter.jetty     :as jetty])
(require '[ring.util.response     :as response])
 
(def html-page
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
")
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (-> (response/response html-page)
        (response/content-type "text/html; charset=utf-8")))
 
(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Ve zdrojovém textu vidíme definice čtyř symbolů:

# Symbol Význam
1 html-page navázaný na řetězec představující HTML stránku poslanou klientovi
2 handler navázaný na funkci reprezentující handler pro zpracování požadavku
3 app datová struktura představující kostru webové aplikace
4 -main navázaný na funkci spuštěnou nástrojem Leiningen

Aplikaci si můžeme jednoduše otestovat:

lein run

Následně se k aplikaci připojíme z CLI klienta:

curl -v localhost:8080
* Rebuilt URL to: localhost:8080/
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 10 Mar 2015 17:34:31 GMT
< Content-Type: text/html;charset=UTF-8
< Content-Length: 174
* Server Jetty(7.6.13.v20130916) is not blacklisted
< Server: Jetty(7.6.13.v20130916)
<
    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
* Connection #0 to host localhost left intact

3. Zpracování parametrů požadavku

Jednou ze základních funkcí prakticky každého webového serveru je rozpoznání a zpracování parametrů požadavků, které server přijme od klienta. Připomeňme si, že protokol HTTP rozlišuje několik možností, jak pametry serveru přidat: buď je možné parametry zakódovat přímo do URL (metoda GET) nebo je alternativně možné parametry předat v těle požadavku (metoda POST). Aby bylo možné parametry získat a zpracovat nezávisle na použité metodě, lze v případě knihovny Clojure Ring použít middleware ring.middleware.params/wrap-params. Tento middleware zajistí vytvoření mapy obsahující původní požadavek (request) i již zpracované parametry. Datová struktura, kterou je definována kostra webové aplikace, bude vypadat následovně:

(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        ring.middleware.params/wrap-params))

Makro -> zajišťuje především lepší čitelnost s menším množstvím závorek. Namísto tohoto makra by bylo možné provést i tento zápis:

(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (ring.middleware.params/wrap-params handler))

To se však se zvyšujícím se počtem middleware stává méně srozumitelné.

4. Druhý demonstrační příklad – použití ring.middleware.params/wrap-params

Podívejme se nyní na postup úpravy dnešního prvního demontračního příkladu takovým způsobem, aby se mohly jednoduše zpracovávat parametry požadavku (requestu). První úprava spočívá v odlišné deklaraci app, což již bylo naznačeno v předchozí kapitole (pouze se použije zkrácené jméno díky importu a vytvoření aliasu):

(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        http-params/wrap-params))

Dále se v handleru přečtou parametry uložené pod klíčem :params. Následně se celá struktura nesoucí informace o všech parametrech jednoduše vytiskne na standardní výstup:

(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params (:params request)]
        (println "Params: " params))
    (-> (response/response html-page)
        (response/content-type "text/html; charset=utf-8")))

Následuje výpis zdrojového kódu celého dnešního druhého demonstračního příkladu:

(ns webapp7.core
    (:gen-class))
 
(require '[ring.adapter.jetty     :as jetty])
(require '[ring.middleware.params :as http-params])
(require '[ring.util.response     :as response])
 
(def html-page
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
")
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params (:params request)]
        (println "Params: " params))
    (-> (response/response html-page)
        (response/content-type "text/html; charset=utf-8")))
 
(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Opět můžeme provést několik testů. V následujících výpisech je nejdříve zobrazen požadavek od CLI klienta a na dalších řádcích pak text vypsaný serverem/webovou aplikací (klient se samozřejmě spouští z jiného terminálu):

curl "localhost:8080"
 
Params:  {}
curl "localhost:8080?param1=Hello&param2=World"
 
Params:  {param1 Hello, param2 World}
curl localhost:8080?message="Message%20with%20spaces"
 
Params:  {message Message with spaces}

5. Práce se session

Zpracování parametrů požadavku je poměrně triviální, ovšem zajímavější je již práce se session. Knihovna Clojure Ring samozřejmě obsahuje možnost ukládat stav (libovolné objekty) na session a při dalším požadavku tyto objekty opět získat, ovšem musíme mít na paměti, že mapa představující session je neměnná (immutable), což může být především zpočátku poněkud matoucí. Pokud potřebujeme pracovat se session, lze použít middleware ring.middleware.session/wrap-session, který upraví původní mapu s daty požadavku takovým způsobem, že nová mapa bude obsahovat i klíč :session, na nějž je navázána mapa s objekty uloženými v sezení. Nově upravený handler může vypadat následovně:

(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        ring.middleware.session/wrap-session
        ring.middleware.params/wrap-params))

(zde se již ukazuje přednost použití makra ->)

6. Třetí demonstrační příklad – použití ring.middleware.session/wrap-session

V dnešním třetím demonstračním příkladu je ukázán základní způsob práce se session. Datová struktura app je upravena způsobem popsaným v předchozí kapitole, takže si ji již nemusíme znovu uvádět. Pokud je skutečně aplikován middleware ring.middleware.session/wrap-session, obsahuje nově vytvořená mapa request i klíč :session navázaný na další mapu, ve které mohou (ale nemusí) být uloženy libovolné objekty. Následuje výpis úplného zdrojového kódu dnešního třetího demonstračního příkladu:

(ns webapp8.core
    (:gen-class))
 
(require '[ring.adapter.jetty      :as jetty])
(require '[ring.middleware.params  :as http-params])
(require '[ring.middleware.session :as http-session])
(require '[ring.util.response      :as response])
 
(def html-page
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
")
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params  (:params request)
          session (:session request)]
        (println "Params:  " params)
        (println "Session: " session))
    (-> (response/response html-page)
        (response/content-type "text/html; charset=utf-8")))
 
(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        http-session/wrap-session
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Aplikaci si můžeme otestovat:

lein run
 
2015-03-10 19:07:15.369:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-03-10 19:07:15.405:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
wget localhost:8080
 
Params:   {}
Session:  {}

7. Implementace čítače uloženého v session

Nyní se již konečně dostáváme k praktickému příkladu. Bude se jednat o jednoduchou webovou aplikaci, která bude mít na session uložený čítač. Pokud klient bude aplikaci posílat více požadavků, bude se čítač zvyšovat o jedničku. Jak lze však tohoto chování dosáhnout? Základem je, že v odpovědi (response), což je běžná mapa, musí být obsažen i klíč :session navázaný na další pod-mapu, v níž jsou uloženy jednotlivé objekty, které se mají na session zachovat pro další požadavek. Stačí tedy upravit odpověď (vracenou handlerem) následujícím způsobem:

    (-> (response/response html-page)
           (response/content-type "text/html; charset=utf-8")
           (assoc :session new-session)))))

Připomeňme si, že funkce assoc vezme svůj první parametr (což musí být mapa) a vrátí novou mapu, která obsahuje jak mapu původní, tak i novou dvojici klíč-hodnota (druhý a třetí parametr). Z výše uvedeného zápisu by se mohlo zdát, že assoc voláme jen se dvěma parametry, ve skutečnosti to však není pravda, protože je celé volání uzavřeno v makru → (je skutečně dobré si chování tohoto makra nastudovat a důsledně ho používat :-). Výše uvedený zápis tedy vede k pouhému přidání nové dvojice :session+new_session do mapy reprezentující odpověď serveru (response).

Poznámka: dokumentace k funkci assoc:

user=> (doc assoc)
-------------------------
clojure.core/assoc
([map key val] [map key val & kvs])
  assoc[iate]. When applied to a map, returns a new map of the
    same (hashed/sorted) type, that contains the mapping of key(s) to
    val(s). When applied to a vector, returns a new vector that
    contains val at index. Note - index must be <= (count vector).

8. Čtvrtý demonstrační příklad – uložení hodnoty čítače do session

Se znalostí toho, jak se čte původní session i jak se nová session přidává do odpovědi serveru (response), již můžeme v handleru realizovat jednoduchý čítač:

(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params      (:params  request)
          old-session (:session request)
          counter     (:counter old-session 0)]
        (println "Params:      " params)
        (println "Old session: " old-session)
        (println "Counter:     " counter)
        (let [new-session (assoc old-session :counter (inc counter))]
            (println "New session: " new-session)
            (-> (response/response html-page)
                (response/content-type "text/html; charset=utf-8")
                (assoc :session new-session)))))

Stručný popis funkce handleru:

  1. Získají se parametry požadavku a uloží se do lokální proměnné params.
  2. Získá se session požadavku a uloží se do lokální proměnné old-session.
  3. Získá se hodnota čítače (pokud neexistuje tak 0) ze session a uloží se do proměnné counter.
  4. Všechny tři údaje se vytisknou na standardní výstup.
  5. Vytvoří se proměnná new-session obsahující v prvku :counter novou hodnotu čítače.
  6. Nová session se vytiskne na standardní výstup.
  7. Vygeneruje se mapa obsahující odpověď serveru (response).

Následuje výpis celého zdrojového kódu demonstračního příkladu:

(ns webapp9.core
    (:gen-class))
 
(require '[ring.adapter.jetty      :as jetty])
(require '[ring.middleware.params  :as http-params])
(require '[ring.middleware.session :as http-session])
(require '[ring.util.response      :as response])
 
(def html-page
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
")
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params      (:params  request)
          old-session (:session request)
          counter     (:counter old-session 0)]
        (println "Params:      " params)
        (println "Old session: " old-session)
        (println "Counter:     " counter)
        (let [new-session (assoc old-session :counter (inc counter))]
            (println "New session: " new-session)
            (-> (response/response html-page)
                (response/content-type "text/html; charset=utf-8")
                (assoc :session new-session)))))
 
(def app
    "Datova struktura predstavujici kostru webove aplikace."
    (-> handler
        http-session/wrap-session
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Opět si můžeme aplikaci otestovat a to otevřením stránky z adresy localhost:8080 v prohlížeči a několikerým stlačením F5/Reload. Webová aplikace by měla na standardní výstup vypsat následující zprávy:

lein run
 
2015-03-10 19:33:34.809:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-03-10 19:33:34.845:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
Params:       {}
Old session:  {}
Counter:      0
New session:  {:counter 1}
 
Params:       {}
Old session:  {:counter 1}
Counter:      1
New session:  {:counter 2}
 
Params:       {}
Old session:  {:counter 2}
Counter:      2
New session:  {:counter 3}
 
Params:       {}
Old session:  {:counter 3}
Counter:      3
New session:  {:counter 4}
 
Params:       {}
Old session:  {:counter 4}
Counter:      4
New session:  {:counter 5}
 
Params:       {}
Old session:  {:counter 5}
Counter:      5
New session:  {:counter 6}

Vidíme, že skutečně vše pracuje korektně.

9. Pátý demonstrační příklad – uložení hodnoty čítače do session (zjednodušená varianta)

V předchozím demonstračním příkladu jsme vytvářeli novou mapu se session, která se ukládala do nové lokální proměnné (přesněji řečeno se navázala na lokální symbol). To však ve skutečnosti není nutné, protože novou session lze jednoduše vytvořit ze session staré na jediném řádku, což je patrné z následujícího úryvku kódu:

...
...
...
(-> (response/response html-page)
    (response/content-type "text/html; charset=utf-8")
    (assoc :session {:counter (inc counter)}))))

Přičemž hodnotu navázanou na lokální symbol counter přečteme z původní session (a když hodnota neexistuje, inicializuje se čítač na nulu):

(let [session     (:session request)
      counter     (:counter session 0)]
...
...
...

Úplný zdrojový kód dnešního pátého demonstračního příkladu je nepatrně kratší než kód předchozí:

(ns webapp10.core
    (:gen-class))
 
(require '[ring.adapter.jetty      :as jetty])
(require '[ring.middleware.params  :as http-params])
(require '[ring.middleware.session :as http-session])
(require '[ring.util.response      :as response])
 
(def html-page
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
         </body>
     </html>
")
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params      (:params  request)
          session     (:session request)
          counter     (:counter session 0)]
        (println "Params:  " params)
        (println "Session: " session)
        (println "Counter: " counter)
        (-> (response/response html-page)
            (response/content-type "text/html; charset=utf-8")
            (assoc :session {:counter (inc counter)}))))
 
(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        http-session/wrap-session
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Kontrola korektnosti aplikace otevřením stránky localhost:8080 a několikerým stlačením F5/Reload:

lein run
 
2015-03-10 19:58:00.456:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-03-10 19:58:00.488:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
Params:   {}
Session:  {}
Counter:  0
 
Params:   {}
Session:  {:counter 1}
Counter:  1
 
Params:   {}
Session:  {:counter 2}
Counter:  2
 
Params:   {}
Session:  {:counter 3}
Counter:  3
 
Params:   {}
Session:  {:counter 4}
Counter:  4

(výstup je nepatrně odlišný, protože již nemáme k dispozici původní i novou mapu session).

10. Šestý demonstrační příklad – zobrazení hodnoty čítače na WWW stránce

V šestém (již předposledním) demonstračním příkladu je ukázáno, jak lze hodnotu čítače jednoduše zobrazit přímo na HTML stránce. Úprava předchozího demonstračního příkladu je vlastně pouze minimální – namísto řetězce navázaného na symbol html-page se použije funkce create-html-page, které se předá hodnota čítače. Povšimněte si použití univerzální funkce str, která relativně čitelným způsobem dokáže spojit řetězcové literály s hodnotou čítače:

(defn create-html-page
    "Vygenerovani HTML stranky."
    [counter]
    (str
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
             <p>Counter: " counter "</p>
         </body>
     </html>
"))

Samotný handler je upraven pouze na jediném (zvýrazněném) řádku:

(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params      (:params  request)
          session     (:session request)
          counter     (:counter session 0)]
        (println "Params:  " params)
        (println "Session: " session)
        (println "Counter: " counter)
        (-> (response/response (create-html-page counter))
            (response/content-type "text/html; charset=utf-8")
            (assoc :session {:counter (inc counter)}))))

Opět si uveďme úplný zdrojový kód šestého demonstračního příkladu:

(ns webapp11.core
    (:gen-class))
 
(require '[ring.adapter.jetty      :as jetty])
(require '[ring.middleware.params  :as http-params])
(require '[ring.middleware.session :as http-session])
(require '[ring.util.response      :as response])
 
(defn create-html-page
    "Vygenerovani HTML stranky."
    [counter]
    (str
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
             <p>Counter: " counter "</p>
         </body>
     </html>
"))
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params      (:params  request)
          session     (:session request)
          counter     (:counter session 0)]
        (println "Params:  " params)
        (println "Session: " session)
        (println "Counter: " counter)
        (-> (response/response (create-html-page counter))
            (response/content-type "text/html; charset=utf-8")
            (assoc :session {:counter (inc counter)}))))
 
(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        http-session/wrap-session
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Obrázek 1: Webová aplikace s čítačem.

11. Základy práce s cookies

Kromě sezení (session) lze v knihovně Clojure Ring přímo pracovat i s cookies, ostatně session bývá obvykle realizováno právě přes cookies. K tomu, aby se k mapě představující původní požadavek přidaly i informace o cookies, lze použít middleware se jménem ring.middleware.cookies/wrap-cookies, které k mapě požadavku přidá další dvojici klíč-hodnota, kde klíč má (podle očekávání) jméno :cookies. Kostra webové aplikace může vypadat následovně:

(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        ring.middleware.cookies/wrap-cookies
        ring.middleware.params/wrap-params))

Pokud se mají zpracovat parametry požadavku, session i cookies, dostaneme následující strukturu:

(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        ring.middleware.cookies/wrap-cookies
        ring.middleware.session/wrap-session
        ring.middleware.params/wrap-params))

U třech middleware aplikovaných za sebou a postupně obalujících původní mapu s informacemi o požadavku se již v plné kráse ukazují možnosti makra ->.

To však není vše – kromě přečtení cookies je nutné umět i vytvořit nové cookie. Nejjednodušším způsobem je předat další parametry funkci ring.util.response, například:

(ring.util.response/set-cookie :user-name "Pavel")

Popř. při požadavku na nastavení dalších vlastností cookie (zde konkrétně životnosti):

(ring.util.response/set-cookie :user-name "Pavel" {:max-age 36000000})

12. Sedmý demonstrační příklad – použití ring.middleware.cookies/wrap-cookies

Dnešní sedmý a současně i poslední demonstrační příklad je (alespoň prozatím) nejsložitější, ovšem můžeme si na něm ukázat jak přečtení parametrů požadavku, tak i základy práce se session. Tato webová aplikace zobrazí na straně klienta formulář, v němž se nachází textové políčko pro zadání jména a tlačítko nadepsané „Remember me“ pro odeslání formuláře na server. O tuto část se stará funkce nazvaná create-html-page. Za povšimnutí stojí fakt, že se formulář správně zobrazí i za předpokladu, že parametr user-name bude mít hodnotu nil:

(defn create-html-page
    "Vygenerovani HTML stranky."
    [user-name]
    (str
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
             <form method='post' action='/'>
                 User name: <input type='text' name='user-name' value='" user-name "'/><br />
                            <input type='submit' value='Remember me' />
             </form>
         </body>
     </html>
"))

Vzhledem k tomu, že potřebujeme číst parametry požadavku a současně i pracovat s cookies, musí kostra aplikace vypadat takto:

(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        cookies/wrap-cookies
        http-params/wrap-params))

Handler musí přečíst parametr se jménem user-name i stejně pojmenovaný objekt z cookies. Ani jeden z těchto objektů nemusí být nalezen, typicky při prvním přístupu na stránku webové aplikace, což však nevadí, protože or dokáže bez problémů zpracovat i hodnoty nil (žádné NullPointerException nenastane :-). Pokud je na formuláři zadáno nové jméno, tak or vrátí právě toto jméno, pokud však formulář žádné jméno neobsahuje (nově spuštěný prohlížeč), přečte a vrátí se jméno z cookie:

(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params        (:params  request)
          cookies       (:cookies request)
          new-user-name (get params "user-name")
          old-user-name (get (get cookies "user-name") :value)
          user-name     (or new-user-name old-user-name)]
        (println "New user name " new-user-name)
        (println "Old user name " old-user-name)
        (println "Incoming cookies: " cookies)
        (let [response (generate-response user-name)]
              (println "Outgoing cookies: " (get response :cookies))
              response)))

Odpověď serveru je vytvořena v nové funkci, a to především z toho důvodu, že potřebujeme na standardní výstup vypsat jak starou, tak i novou hodnotu cookie(s):

(defn generate-response
    "Vytvoreni odpovedi."
    [user-name]
     (let [html-output (create-html-page user-name)]
        (if user-name
            (-> (http-response/response html-output)
                (http-response/set-cookie :user-name user-name {:max-age 36000000})
                (http-response/content-type "text/html"))
            (-> (http-response/response html-output)
                (http-response/content-type "text/html")))))

Opět se podívejme na úplný zdrojový kód aplikace:

(ns webapp12.core
    (:gen-class))
 
(require '[ring.adapter.jetty      :as jetty])
(require '[ring.middleware.params  :as http-params])
(require '[ring.util.response      :as http-response])
(require '[ring.middleware.cookies :as cookies])
 
(defn create-html-page
    "Vygenerovani HTML stranky."
    [user-name]
    (str
"    <html>
         <head>
             <title>Powered by Ring!</title>
         </head>
         <body>
             <h1>Powered by Ring!</h1>
             <form method='post' action='/'>
                 User name: <input type='text' name='user-name' value='" user-name "'/><br />
                            <input type='submit' value='Remember me' />
             </form>
         </body>
     </html>
"))
 
(defn generate-response
    "Vytvoreni odpovedi."
    [user-name]
     (let [html-output (create-html-page user-name)]
        (if user-name
            (-> (http-response/response html-output)
                (http-response/set-cookie :user-name user-name {:max-age 36000000})
                (http-response/content-type "text/html"))
            (-> (http-response/response html-output)
                (http-response/content-type "text/html")))))
 
(defn handler
    "Zpracovani pozadavku."
    [request]
    (let [params        (:params  request)
          cookies       (:cookies request)
          new-user-name (get params "user-name")
          old-user-name (get (get cookies "user-name") :value)
          user-name     (or new-user-name old-user-name)]
        (println "New user name " new-user-name)
        (println "Old user name " old-user-name)
        (println "Incoming cookies: " cookies)
        (let [response (generate-response user-name)]
              (println "Outgoing cookies: " (get response :cookies))
              response)))
 
(def app
    "Datova struktra predstavujici kostru webove aplikace."
    (-> handler
        cookies/wrap-cookies
        http-params/wrap-params))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

13. Přečtení hodnoty z cookie a nastavení nové hodnoty

Podívejme se, jak se bude aplikace chovat při testování. Předpokládejme, že v prohlížeči se nejdříve zobrazí prázdný formulář, posléze se zadá jméno „Pavel“, prohlížeč se vypne, zapne a jméno se změní na „tisnik“:

lein run
 
2015-03-10 20:29:49.330:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-03-10 20:29:49.382:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
První zobrazení formuláře:
New user name  nil
Old user name  nil
Incoming cookies:  {}
Outgoing cookies:  nil
 
Zadání jména a odeslání formuláře:
New user name  Pavel
Old user name  nil
Incoming cookies:  {}
Outgoing cookies:  {:user-name {:max-age 36000000, :value Pavel}}
 
Reload:
New user name  Pavel
Old user name  Pavel
Incoming cookies:  {user-name {:value Pavel}}
Outgoing cookies:  {:user-name {:max-age 36000000, :value Pavel}}
 
Zadání nového jména:
New user name  tisnik
Old user name  Pavel
Incoming cookies:  {user-name {:value Pavel}}
Outgoing cookies:  {:user-name {:max-age 36000000, :value tisnik}}

Obrázek 2: Formulář zobrazený ve webovém prohlížeči.

Obrázek 3: Aplikace skutečně vytvořila cookie.

Obrázek 4: Informace o cookie: jméno, hodnota, doba platnosti.

14. Odkazy na Internetu

  1. Clojure Ring na GitHubu
    https://github.com/ring-clojure/ring
  2. A brief overview of the Clojure web stack
    https://brehaut.net/blog/2011/rin­g_introduction
  3. Getting Started with Ring
    http://www.learningclojure­.com/2013/01/getting-started-with-ring.html
  4. Getting Started with Ring and Compojure – Clojure Web Programming
    http://www.myclojureadven­ture.com/2011/03/getting-started-with-ring-and-compojure.html
  5. Leiningen: nástroj pro správu projektů napsaných v Clojure
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/
  6. Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/
  7. Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/
  8. Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/
  9. Unit Testing in Clojure
    http://nakkaya.com/2009/11/18/unit-testing-in-clojure/
  10. Testing in Clojure (Part-1: Unit testing)
    http://blog.knoldus.com/2014/03/22/tes­ting-in-clojure-part-1-unit-testing/
  11. API for clojure.test – Clojure v1.6 (stable)
    https://clojure.github.io/clo­jure/clojure.test-api.html
  12. Leiningen: úvodní stránka
    http://leiningen.org/
  13. Leiningen: Git repository
    https://github.com/techno­mancy/leiningen
  14. leiningen-win-installer
    http://leiningen-win-installer.djpowell.net/
  15. Clojure 1: Úvod
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/
  16. Clojure 2: Symboly, kolekce atd.
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/
  17. Clojure 3: Funkcionální programování
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/
  18. Clojure 4: Kolekce, sekvence a lazy sekvence
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/
  19. Clojure 5: Sekvence, lazy sekvence a paralelní programy
    http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/
  20. Clojure 6: Podpora pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/
  21. Clojure 7: Další funkce pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/
  22. Clojure 8: Identity, stavy, neměnné hodnoty a reference
    http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/
  23. Clojure 9: Validátory, pozorovatelé a kooperace s Javou
    http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/
  24. Clojure 10: Kooperace mezi Clojure a Javou
    http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/
  25. Clojure 11: Generátorová notace seznamu/list comprehension
    http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/
  26. Clojure 12: Překlad programů z Clojure do bajtkódu JVM I
    http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/
  27. Clojure 13: Překlad programů z Clojure do bajtkódu JVM II
    2) http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/
  28. Clojure 14: Základy práce se systémem maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/
  29. Clojure 15: Tvorba uživatelských maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/
  30. Clojure 16: Složitější uživatelská makra
    http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/
  31. Clojure 17: Využití standardních maker v praxi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/
  32. Clojure 18: Základní techniky optimalizace aplikací
    http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/
  33. Clojure 19: Vývojová prostředí pro Clojure
    http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/
  34. Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/
  35. Clojure 21: ClojureScript aneb překlad Clojure do JS
    http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/
Našli jste v článku chybu?
Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

DigiZone.cz: Česká televize mění schéma ČT :D

Česká televize mění schéma ČT :D

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: Vládu obejde, kvůli EET rovnou do sněmovny

Vládu obejde, kvůli EET rovnou do sněmovny

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Lupa.cz: UX přestává pro firmy být magie

UX přestává pro firmy být magie

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Vitalia.cz: Chtějí si léčit kvasinky. Lék je jen v Německu

Chtějí si léčit kvasinky. Lék je jen v Německu

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Měšec.cz: Finančním poradcům hrozí vracení provizí

Finančním poradcům hrozí vracení provizí

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

120na80.cz: Bojíte se encefalitidy?

Bojíte se encefalitidy?

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu