Hlavní navigace

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

5. 3. 2015
Doba čtení: 21 minut

Sdílet

Ve čtvrté části článku o nástroji Leiningen určeného pro správu projektů vytvořených v programovacím jazyku Clojure si ukážeme tvorbu jednoduchých webových aplikací s využitím knihovny Ring. Tato knihovna je napsána takovým způsobem, že nad ní lze vytvořit i velmi komplexní webové frameworky.

Obsah

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

2. Knihovna Ring

3. První demonstrační příklad – kostra webové aplikace

4. Instalace všech závislých knihoven

5. Spuštění a otestování prvního demonstračního příkladu

6. Druhý demonstrační příklad – výpis datové struktury popisující požadavek od klienta

7. Spuštění druhého demonstračního příkladu

8. Třetí demonstrační příklad – implementace handleru pro vygenerování HTML stránky

9. Čtvrtý demonstrační příklad – jednoduchá kalkulačka

10. Získání hodnot zadaných do formuláře

11. Vytvoření odpovědi serveru

12. Odkazy na Internetu

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

V již čtvrté části článku o nástroji Leiningen si ukážeme tvorbu (zpočátku velmi jednoduchých) webových aplikací. Použijeme přitom, což je pochopitelné, programovací jazyk Clojure a taktéž knihovnu nazvanou Clojure Ring, zkráceně jen Ring. Nejprve si řekneme, jakým způsobem je možné vytvořit kostru primitivního webového projektu, který bude pro všechny dotazy vracet neměnnou webovou stránku (resp. neměnný plaintext) a posléze přejdeme k poněkud zajímavějšímu tématu – reakci na data poslaná uživatelem resp. prohlížečem na server. Všechny čtyři dnes ukazované demonstrační příklady sice budou velmi jednoduché, ovšem my si na nich – právě díky jejich jednoduchosti – ukážeme některé základní principy, na nichž je knihovna Clojure Ring postavená a které jsou využívány složitějšími webovými frameworky (je ostatně typické, že většina webových frameworků určených pro programovací jazyk Clojure je založena právě na knihovně Ring).

2. Knihovna Ring

Knihovna Clojure Ring slouží k usnadnění tvorby webových aplikací s využitím funkcionálního paradigmatu. Je navržena modulárním způsobem, což například znamená, že programátoři mají poměrně velkou volnost při konfiguraci aplikace či v tvorbě a použití takzvaného middleware, což jsou v pojetí Ringu makra a funkce vkládaná mezi Ring a vlastní implementaci webové aplikace (tímto tématem se budeme podrobněji zabývat v navazující části tohoto článku). 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

Aplikační logika se skládá z takzvaného handleru volaného při příchodu každého požadavku od klienta a popř. také z již zmíněného middleware. Základní funkce handleru je navržena čistě funkcionálně – handler je běžná funkce, které se při příchodu požadavku předá datová struktura typu mapa obsahující předzpracované informace o požadavku (request), návratovou hodnotou handleru je opět datová struktura typu mapa představující odpověď serveru (response). To je vše – na rozdíl od CGI skriptů se nemusí řešit zpracování standardního vstupu či proměnné prostředí QUERY_STRING, neprovádí se ani ruční formátování výstupu, jako je tomu u JSP stránek (či servletů). Speciálním případem je stav, kdy návratová hodnota handleru (response) je nil, což knihovna Ring „přeloží“ do známého HTTP kódu 404. Díky této vlastnosti se může implementace handleru v některých případech zjednodušit.

Zajímavý je Ring Adapter. Jedná se o konfigurovatelnou mezivrstvu mezi aplikací a webovým serverem. Existují tři základní možnosti konfigurace adaptéru:

  1. Použije se interní server, v současnosti Jetty, který je součástí aplikace a běží ve stejné JVM (tuto nejjednodušší možnost použijeme i v dnešních demonstračních příkladech)
  2. Použijí se servlety provozované například na Tomcatu
  3. Použije se server běžící mimo vlastní JVM, pro komunikaci se serverem lze zvolit protokol (tímto způsobem je možné například realizovat load balancing, oddělení vyvinuté webové aplikace od Internetu atd.)

Ve skutečnosti však webová aplikace využívající Ring může používat a většinou taktéž používá i další knihovny. Pro generování HTML stránek se využívají knihovny Hiccup či Enlive, pro dispatching (resp. zjednodušení dispatchingu) pak Moustache či (pravděpodobně častěji) Compojure. Podrobnosti o těchto knihovnách si opět řekneme příště.

3. První demonstrační příklad – kostra webové aplikace

Ukažme si nyní, jakým způsobem je možné vytvořit kostru prozatím velmi jednoduché webové aplikace. Základ nového projektu se vygeneruje naprosto stejně, jako tomu bylo i ve všech příkladech, které jsme si popisovali v předchozích třech částech tohoto článku:

lein new app webapp1

Po zadání tohoto příkazu se v aktuálním adresáři vytvoří nový podadresář obsahující základ nového projektu (což již také známe):

.
├── doc
│   └── intro.md
├── LICENSE
├── project.clj
├── README.md
├── resources
├── src
│   └── webapp1
│       └── core.clj
└── test
    └── webapp1
        └── core_test.clj
 
6 directories, 6 files

Nyní je nutné upravit soubor project.clj takovým způsobem, aby nově vytvořený projekt mohl využívat vybrané moduly knihovny Ring. Prozatím nám budou postačovat dva moduly nazvané ring-corering-jetty-adapter, takže tyto dva moduly přidáme do vektoru uloženého pod klíčem :dependencies. Dva nově vytvořené řádky v souboru project.clj jsou zvýrazněny (nezapomeňte přitom na správné umístění pravé uzavírací závorky vektoru):

(defproject webapp1 "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 webapp1.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

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

(ns webapp1.core
    (:gen-class))
 
(require '[ring.adapter.jetty :as jetty])
 
(defn app
    "Funkce predstavujici kostru webove aplikace."
    [request]
    {:status 200
     :headers {"Content-Type" "text/plain"}
     :body "Hello World"})
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Ve funkci -main spouštěné nástrojem Leiningen se volá funkce jetty/run-jetty, které se předá handler webové aplikace zpracovávající všechny požadavky od klientů a taktéž mapa, v níž je možné předávat další konfigurační volby, zde konkrétně číslo portu, na němž bude webová aplikace očekávat požadavky. Výchozí port je nastaven na 3000, my však použijeme přece jen známější číslo 8080, samozřejmě za předpokladu, že tento port již není obsazen žádným jiným serverem. Další důležitou funkcí je app, tj. samotný handler webové aplikace. Tomuto handleru se při každém zavolání předá struktura představující požadavek klienta a výsledkem má být mapa obsahující odpověď, která má být (po zpracování knihovnou Ring) poslána zpět klientovi. Odpověď je v tomto případě velmi jednoduchá – obsahuje stavový kód 200 (OK), hlavičku s MIME typem a vlastní tělo odpovědi, což je jednořádkový řetězec „Hello World“. Knihovna Ring tyto údaje z mapy přečte a poskládá z nich korektní HTTP odpověď (response).

4. Instalace všech závislých knihoven

V souboru project.clj byly specifikovány pouze dva nové moduly, na nichž projekt závisí: ring/ring-core verze 1.3.2 a ring/ring-jetty-adapter taktéž verze 1.3.2. Ve skutečnosti však tyto moduly potřebují ke svému použití i mnoho dalších knihoven, takže nyní nastává okamžik automatického stažení všech těchto knihoven. K tomu slouží nám již známý příkaz lein deps, který je nutné spustit v adresáři s projektem, přesněji řečeno v adresáři, kde se nachází soubor project.clj:

lein deps

Po zadání tohoto příkazu by se měly vyhodnotit všechny knihovny, na nichž závisí správná činnost výše zmíněných dvou modulů. V mém případě – systém Linux Mint s čerstvě nainstalovaným nástrojem Leiningen – vypadalo spuštění tohoto příkazu následovně:

Retrieving ring/ring-core/1.3.2/ring-core-1.3.2.pom from clojars
Retrieving org/clojure/tools.reader/0.8.1/tools.reader-0.8.1.pom from central
Retrieving ring/ring-codec/1.0.0/ring-codec-1.0.0.pom from clojars
Retrieving commons-codec/commons-codec/1.6/commons-codec-1.6.pom from central
Retrieving commons-fileupload/commons-fileupload/1.3/commons-fileupload-1.3.pom from central
Retrieving org/apache/commons/commons-parent/28/commons-parent-28.pom from central
Retrieving org/apache/apache/13/apache-13.pom from central
Retrieving commons-io/commons-io/2.2/commons-io-2.2.pom from central
Retrieving org/apache/commons/commons-parent/28/commons-parent-28.pom from central
Retrieving org/apache/apache/13/apache-13.pom from central
Retrieving commons-io/commons-io/2.2/commons-io-2.2.pom from central
Retrieving org/apache/commons/commons-parent/24/commons-parent-24.pom from central
Retrieving clj-time/clj-time/0.6.0/clj-time-0.6.0.pom from clojars
Retrieving joda-time/joda-time/2.2/joda-time-2.2.pom from central
Retrieving crypto-random/crypto-random/1.2.0/crypto-random-1.2.0.pom from clojars
Retrieving org/clojure/clojure/1.2.1/clojure-1.2.1.pom from central
Retrieving crypto-equality/crypto-equality/1.0.0/crypto-equality-1.0.0.pom from clojars
Retrieving ring/ring-jetty-adapter/1.3.2/ring-jetty-adapter-1.3.2.pom from clojars
Retrieving ring/ring-servlet/1.3.2/ring-servlet-1.3.2.pom from clojars
Retrieving org/eclipse/jetty/jetty-server/7.6.13.v20130916/jetty-server-7.6.13.v20130916.pom from central
Retrieving org/eclipse/jetty/jetty-project/7.6.13.v20130916/jetty-project-7.6.13.v20130916.pom from central
Retrieving org/eclipse/jetty/jetty-parent/20/jetty-parent-20.pom from central
Retrieving org/eclipse/jetty/orbit/javax.servlet/2.5.0.v201103041518/javax.servlet-2.5.0.v201103041518.pom from central
Retrieving org/eclipse/jetty/orbit/jetty-orbit/1/jetty-orbit-1.pom from central
Retrieving org/eclipse/jetty/jetty-parent/18/jetty-parent-18.pom from central
Retrieving org/eclipse/jetty/jetty-continuation/7.6.13.v20130916/jetty-continuation-7.6.13.v20130916.pom from central
Retrieving org/eclipse/jetty/jetty-http/7.6.13.v20130916/jetty-http-7.6.13.v20130916.pom from central
Retrieving org/eclipse/jetty/jetty-io/7.6.13.v20130916/jetty-io-7.6.13.v20130916.pom from central
Retrieving org/eclipse/jetty/jetty-util/7.6.13.v20130916/jetty-util-7.6.13.v20130916.pom from central
Retrieving org/clojure/tools.reader/0.8.1/tools.reader-0.8.1.jar from central
Retrieving commons-codec/commons-codec/1.6/commons-codec-1.6.jar from central
Retrieving commons-fileupload/commons-fileupload/1.3/commons-fileupload-1.3.jar from central
Retrieving joda-time/joda-time/2.2/joda-time-2.2.jar from central
Retrieving org/eclipse/jetty/jetty-continuation/7.6.13.v20130916/jetty-continuation-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/jetty-server/7.6.13.v20130916/jetty-server-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/orbit/javax.servlet/2.5.0.v201103041518/javax.servlet-2.5.0.v201103041518.jar from central
Retrieving org/eclipse/jetty/jetty-io/7.6.13.v20130916/jetty-io-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/jetty-io/7.6.13.v20130916/jetty-io-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/jetty-http/7.6.13.v20130916/jetty-http-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/jetty-util/7.6.13.v20130916/jetty-util-7.6.13.v20130916.jar from central
Retrieving ring/ring-core/1.3.2/ring-core-1.3.2.jar from clojars
Retrieving crypto-equality/crypto-equality/1.0.0/crypto-equality-1.0.0.jar from clojars
Retrieving ring/ring-codec/1.0.0/ring-codec-1.0.0.jar from clojars
Retrieving crypto-random/crypto-random/1.2.0/crypto-random-1.2.0.jar from clojars
Retrieving clj-time/clj-time/0.6.0/clj-time-0.6.0.jar from clojars
Retrieving ring/ring-jetty-adapter/1.3.2/ring-jetty-adapter-1.3.2.jar from clojars
Retrieving ring/ring-servlet/1.3.2/ring-servlet-1.3.2.jar from clojars
Retrieving org/eclipse/jetty/jetty-http/7.6.13.v20130916/jetty-http-7.6.13.v20130916.jar from central
Retrieving org/eclipse/jetty/jetty-util/7.6.13.v20130916/jetty-util-7.6.13.v20130916.jar from central
Retrieving ring/ring-core/1.3.2/ring-core-1.3.2.jar from clojars
Retrieving crypto-equality/crypto-equality/1.0.0/crypto-equality-1.0.0.jar from clojars
Retrieving ring/ring-codec/1.0.0/ring-codec-1.0.0.jar from clojars
Retrieving crypto-random/crypto-random/1.2.0/crypto-random-1.2.0.jar from clojars
Retrieving clj-time/clj-time/0.6.0/clj-time-0.6.0.jar from clojars
Retrieving ring/ring-jetty-adapter/1.3.2/ring-jetty-adapter-1.3.2.jar from clojars
Retrieving ring/ring-servlet/1.3.2/ring-servlet-1.3.2.jar from clojars

Všechny stažené knihovny by se měly uložit do podadresáře .m2 vytvořeného v domovském adresáři uživatele.

5. Spuštění a otestování prvního demonstračního příkladu

Nyní by již mělo být vše připravené pro spuštění naší demonstrační webové aplikace, které se v případě použití Leiningenu nijak neliší od spuštění jakékoli jiné aplikace:

lein run

Za několik sekund by mělo dojít k inicializaci webového serveru a na standardní výstup by se mělo vypsat hlášení o tom, na kterém portu byl server spuštěn (v horším případě se vypíše stack trace s chybou :-):

2015-02-21 22:01:04.514:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-02-21 22:01:04.546:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080

Běžící server/aplikaci lze otestovat jak běžným webovým prohlížečem s textovým či grafickým uživatelským rozhraním, tak i například nástroji typu wget či curl. Adresa je ve všech případech stejná – „localhost“ a číslo portu:

curl localhost:8080

Po spuštění tohoto příkazu by se na standardní výstup měl vypsat řetězec vrácený právě vytvořeným a spuštěným webovým serverem:

Hello World

Můžeme se samozřejmě podívat i na podrobnější výpis komunikace mezi klientem (curl) a webovým serverem:

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, 03 Mar 2015 14:44:31 GMT
< Content-Type: text/plain;charset=ISO-8859-1
< Content-Length: 11
* Server Jetty(7.6.13.v20130916) is not blacklisted
< Server: Jetty(7.6.13.v20130916)
<
{ [data not shown]
* Connection #0 to host localhost left intact
Hello World

6. Druhý demonstrační příklad – výpis datové struktury popisující požadavek od klienta

Handler webové aplikace, který byl v prvním demonstračním příkladu představovaný funkcí app, je zavolán po přijetí požadavku (reguest) od klienta. Tento požadavek je nejdříve zpracován knihovnou Ring a následně v podobě mapy předán právě handleru. Protože je reakce na požadavky klienta ústřední částí většiny webových serverů, ukážeme si ve druhém demonstračním příkladu, jak lze zobrazit celou strukturu a obsah mapy předávané do handleru app. Vytvoříme si proto nový projekt s názvem webapp2:

lein new app webapp2

Úprava souboru project.clj je stejná, jako tomu bylo i v prvním demonstračním příkladu:

(defproject webapp2 "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 webapp2.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

Lišit se samozřejmě bude soubor src/webapp2/core.clj. Zde se v handleru pojmenovaném app volá nová funkce nazvaná render-response-body, která vrátí řetězec představující textovou podobu mapy s daty požadavku (request). Aby byl výpis obsahu mapy čitelný, je použita funkce clojure.pprint/pprint, která však provádí výpis na standardní výstup. My naopak potřebujeme, aby se výpis provedl do řetězce, proto je volání funkce clojure.pprint/pprint „obaleno“ do velmi užitečného makra with-out-str, které lokálně mění obsah *out* a konvertuje veškerý výstup do řetězce:

(ns webapp2.core
    (:gen-class))
 
(require '[ring.adapter.jetty :as jetty])
(require '[clojure.pprint     :as pprint])
 
(defn render-response-body
    [request]
    (with-out-str (pprint/pprint request)))
 
(defn app
    "Funkce predstavujici kostru webove aplikace."
    [request]
    {:status 200
     :headers {"Content-Type" "text/plain"}
     :body (render-response-body request)})
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

7. Spuštění druhého demonstračního příkladu

Po spuštění dnešního druhého demonstračního příkladu příkazem:

lein run

…je možné otestovat chování webové aplikace, a to jak při použití webového prohlížeče s GUI, tak i při použití nástroje curl.

Poslání požadavku z webového prohlížeče, konkrétně z Firefoxu:

{:ssl-client-cert nil,
 :remote-addr "127.0.0.1",
 :headers
 {"accept-encoding" "gzip, deflate",
  "user-agent"
  "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0",
  "connection" "keep-alive",
  "accept-language" "en-US,en;q=0.5",
  "accept"
  "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  "host" "localhost:8080"},
 :server-port 8080,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body #<HttpInput org.eclipse.jetty.server.HttpInput@39520844>,
 :scheme :http,
 :request-method :get}

Použití nástroje curl:

curl localhost:8080
{:ssl-client-cert nil,
 :remote-addr "127.0.0.1",
 :headers
 {"user-agent" "curl/7.35.0", "accept" "*/*", "host" "localhost:8080"},
 :server-port 8080,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body #<HttpInput org.eclipse.jetty.server.HttpInput@71e80e4d>,
 :scheme :http,
 :request-method :get}

Použití nástroje curl, odlišná URL:

curl localhost:8080/foo/bar
{:ssl-client-cert nil,
 :remote-addr "127.0.0.1",
 :headers
 {"user-agent" "curl/7.35.0", "accept" "*/*", "host" "localhost:8080"},
 :server-port 8080,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/foo/bar",
 :server-name "localhost",
 :query-string nil,
 :body #<HttpInput org.eclipse.jetty.server.HttpInput@18e16a6b>,
 :scheme :http,
 :request-method :get}

Použití nástroje curl, předání parametrů v URL:

curl "localhost:8080?param1=foo&param2=bar"
{:ssl-client-cert nil,
 :remote-addr "127.0.0.1",
 :headers
 {"user-agent" "curl/7.35.0", "accept" "*/*", "host" "localhost:8080"},
 :server-port 8080,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string "param1=foo&param2=bar",
 :body #<HttpInput org.eclipse.jetty.server.HttpInput@517d009c>,
 :scheme :http,
 :request-method :get}

V posledních třech příkladech si povšimněte především hodnot uložených pod klíči :uri a :query-string. Tyto hodnoty sice mohou být zpracovávány přímo, je to ovšem zbytečně pracné. Z tohoto důvodu nabízí knihovna Ring několik možností, jak tyto hodnoty zpracovávat jednodušším způsobem, což si ukážeme v následujících kapitolách i v další části tohoto článku.

8. Třetí demonstrační příklad – implementace handleru pro vygenerování HTML stránky

Ve třetím demonstračním příkladu si ukážeme nejjednodušší (ale z hlediska návrhu aplikace vlastně i zdaleka nejhorší :-) způsob vygenerování HTML stránky. Navíc bude v této aplikaci použito i velmi užitečné makro ->, které se často používá pro zprehlednění kódu, především ve chvíli, kdy dochází ke zřetězení volání funkcí, přičemž jedna funkce předává svůj výsledek další funkci. Třetí příklad se bude jmenovat webapp3 a vytvoří se známým příkazem:

lein new app webapp3

V souboru project.clj se provedou stejné změny, jaké byly provedeny i v předchozích třech příkladech:

(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}})

Zajímavější je již soubor core.clj, kde se nachází definice globálního symbolu pojmenovaného html-page a navázaného na řetězec představující celou HTML stránku. Tento symbol je použit v handleru, který však již vypadá jinak, než tomu bylo v předchozích příkladech. Můžeme zde vidět použití dvou pomocných funkcí implementovaných ve jmenném prostoru ring.util.response. Díky použití makra -> je možné zřetězit volání dvou middleware funkcí bez nutnosti přílišného závorkování :-). Následující zápis se postará o vytvoření mapy response, kterou jsme dříve vytvářeli ručně:

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

I implementace funkce app je nepatrně odlišná (navíc se již nejedná o funkci :-), opět kvůli použití makra -> a především pak middleware implementovaného ve jmenném prostoru ring.middleware.params. Tento middleware se postará o takové zpracování requestu, aby bylo jednodušší přečíst jednotlivé parametry poslané uživatelem:

(def app
    "Funkce predstavujici kostru webove aplikace."
    (-> handler
        (params/wrap-params)))
(ns webapp3.core
    (:gen-class))
 
(require '[ring.adapter.jetty     :as jetty])
(require '[ring.middleware.params :as 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
    [request]
    (-> (response/response html-page)
        (response/content-type "text/html; charset=utf-8")))
 
(def app
    "Funkce predstavujici kostru webove aplikace."
    (-> handler
        (params/wrap-params)))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

Pro zajímavost – zde je popis makra použitého ve třetím a čtvrtém příkladu:

    user=> (doc ->)
    -------------------------
    clojure.core/->
([x] [x form] [x form & more])
Macro
  Threads the expr through the forms. Inserts x as the
  second item in the first form, making a list of it if it is not a
  list already. If there are more forms, inserts the first form as the
  second item in second form, etc.
nil

9. Čtvrtý demonstrační příklad – jednoduchá kalkulačka

Předchozí demonstrační příklad byl pouze přípravou na vytvoření příkladu čtvrtého a současně i (alespoň dnes) posledního. Ve čtvrtém příkladu je implementována primitivní kalkulačka. Jedná se o webovou stránku s formulářem, do kterého je možné zadat dvě číselné hodnoty a následně se na straně serveru provede součet hodnot a výpis výsledku. Oproti jiným řešením (PHP a spol.) je zde jedno nepatrné vylepšení – číselné hodnoty mohou mít prakticky libovolný rozsah, nejsme tedy limitováni například rozsahem a přesností typu double. Nejdříve si uvedeme celý zdrojový kód čtvrtého demonstračního příkladu, přičemž jednotlivé detaily budou vysvětleny v dalších dvou kapitolách:

(ns webapp4.core
    (:gen-class))
 
(require '[ring.adapter.jetty     :as jetty])
(require '[ring.middleware.params :as params])
(require '[ring.util.response     :as response])
 
(defn render-html-page
    [x y result]
    (str
    "<html>
        <head>
            <title>Ultimate calculator:</title>
        </head>
        <body>
            <h1>Ultimate calculator:</h1>
            <form method='get' action='/'>
                <input type='text' name='x' size='10' value='" x "'/> ×
                <input type='text' name='y' size='10' value='" y "'/> =
                <input type='text' name='result' size='20' value='" result "' readonly='readonly'/>
                <input type='submit' value='Calculate' />
            </form>
        </body>
     </html>"))
 
(defn param->number
    "Prevod parametru specifikovaneho v param-name na cislo typu BigDecimal."
    [params param-name]
    (let [param (get params param-name)]
        (try
            (bigdec param)             ; pokus o prevod na BigDecimal
            (catch Exception e nil)))) ; pokud se prevod nepovede, vraci se nil
 
(defn compute-result
    [x y]
    (if (and x y) (* x y))) ; vetev else neni uvedena -> nil
 
(defn handler
    [request]
    (let [params (:params request)
          x      (param->number params "x")
          y      (param->number params "y")
          result (compute-result x y)]
    (println params x y result)
    (-> (response/response (render-html-page x y result))
        (response/content-type "text/html; charset=utf-8"))))
 
(def app
    "Funkce predstavujici kostru webove aplikace."
    (-> handler
        (params/wrap-params)))
 
(defn -main
    "Spusteni webove aplikace na portu 8080."
    [& args]
    (jetty/run-jetty app {:port 8080}))

10. Získání hodnot zadaných do formuláře

Pojďme si nyní vysvětlit a popsat jednotlivé části implementované webové aplikace. Při každém příchodu požadavku od klienta se nejprve zpracují vstupní data, „obalí“ se parametry (viz další text) a zavolá se uživatelsky definovaný handler. Toto chování nalezneme zde:

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

Samotný handler musí nejprve získat jednotlivé parametry z datové struktury předané v parametru nazvaném request. To je vlastně velmi jednoduché, protože request je mapou a pod klíčem :params je uložena další mapa s názvy a hodnotami jednotlivých parametrů. Vzhledem k tomu, že klíč :params a k němu navázaná hodnota vždy existují, lze pro jeho získání použít zápis:

(:params request)

popř. pokud je někdo psavec a preferuje delší kód, může použít méně idiomatický zápis:

(get request :params)

V mapě, která je získána jedním z předchozích zápisů, jsou uloženy názvy a hodnoty parametrů, a to opět ve formě mapy. Nyní jsou však klíče reprezentovány řetězci a nikoli keywordy, takže například pro přečtení hodnoty parametrů pojmenovaných „x“ a „y“ se použije tento fragment kódu:

(defn handler
    [request]
    (let [params (:params request)
          x      (get params "x")
          y      (get params "y")]
    (println params x y result)
    ...
    ...
    ...
))

Pokud parametry daného jména neexistují, vrátí funkce get hodnotu nil.

Úkolem webové aplikace je provést součet dvou čísel, proto je nutné hodnoty obou parametrů (což jsou řetězce) převést na numerické hodnoty. K tomu nám pomůže tato funkce, která v případě neúspěchu vrátí nil:

(defn param->number
    "Prevod parametru specifikovaneho v param-name na cislo typu BigDecimal."
    [params param-name]
    (let [param (get params param-name)]
        (try
            (bigdec param)             ; pokus o prevod na BigDecimal
            (catch Exception e nil)))) ; pokud se prevod nepovede, vraci se nil

Při sčítání je zapotřebí dát pozor na stav, kdy alespoň jeden z parametrů není číslem, což zajistí speciální forma if a makro and:

CS24_early

(defn compute-result
    [x y]
    (if (and x y) (* x y))) ; vetev else neni uvedena -> nil

Celý handler vypadá následovně:

(defn handler
    [request]
    (let [params (:params request)
          x      (param->number params "x")
          y      (param->number params "y")
          result (compute-result x y)]
    (println params x y result)
    (-> (response/response (render-html-page x y result))
        (response/content-type "text/html; charset=utf-8"))))

11. Vytvoření odpovědi serveru

Handler vytváří mapu response s využitím middleware responce/responseresponse/content-type. Vlastní vytvoření HTML stránky vracené klientovi je implementováno ve funkci pojmenované render-html-page, které se předají hodnoty parametrů „x“ a „y“ i vypočteného výsledku. Při prvním dotazu budou všechny tři předávané hodnoty nastaveny na nil, což nám ovšem nevadí – nil v Clojure totiž v žádném případě není tak špatně použitelné jako zdánlivě podobné null v Javě :-), což je ostatně patrné z kódu, kde se vytváří výsledný řetězec pomocí funkce str (této funkci vůbec nevadí, že některý z jejích parametrů je nil):

(defn render-html-page
    [x y result]
    (str
    "<html>
        <head>
            <title>Ultimate calculator:</title>
        </head>
        <body>
            <h1>Ultimate calculator:</h1>
            <form method='get' action='/'>
                <input type='text' name='x' size='10' value='" x "'/> ×
                <input type='text' name='y' size='10' value='" y "'/> =
                <input type='text' name='result' size='20' value='" result "' readonly='readonly'/>
                <input type='submit' value='Calculate' />
            </form>
        </body>
     </html>"))

12. 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. Unit Testing in Clojure
    http://nakkaya.com/2009/11/18/unit-testing-in-clojure/
  9. Testing in Clojure (Part-1: Unit testing)
    http://blog.knoldus.com/2014/03/22/tes­ting-in-clojure-part-1-unit-testing/
  10. API for clojure.test – Clojure v1.6 (stable)
    https://clojure.github.io/clo­jure/clojure.test-api.html
  11. Leiningen: úvodní stránka
    http://leiningen.org/
  12. Leiningen: Git repository
    https://github.com/techno­mancy/leiningen
  13. leiningen-win-installer
    http://leiningen-win-installer.djpowell.net/
  14. Clojure 1: Úvod
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/
  15. Clojure 2: Symboly, kolekce atd.
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/
  16. 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/
  17. 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/
  18. 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/
  19. Clojure 6: Podpora pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/
  20. Clojure 7: Další funkce pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/
  21. 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/
  22. 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/
  23. Clojure 10: Kooperace mezi Clojure a Javou
    http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/
  24. Clojure 11: Generátorová notace seznamu/list comprehension
    http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/
  25. 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/
  26. 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/
  27. Clojure 14: Základy práce se systémem maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/
  28. Clojure 15: Tvorba uživatelských maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/
  29. Clojure 16: Složitější uživatelská makra
    http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/
  30. Clojure 17: Využití standardních maker v praxi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/
  31. Clojure 18: Základní techniky optimalizace aplikací
    http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/
  32. Clojure 19: Vývojová prostředí pro Clojure
    http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/
  33. 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/
  34. Clojure 21: ClojureScript aneb překlad Clojure do JS
    http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.