Obsah
1. Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
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
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:
- Aplikační logika
- Ring Adapter
- 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:
- 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)
- Použijí se servlety provozované například na Tomcatu
- 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-core a ring-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¶m2=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¶m2=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:
(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/response a response/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
- Clojure Ring na GitHubu
https://github.com/ring-clojure/ring - A brief overview of the Clojure web stack
https://brehaut.net/blog/2011/ring_introduction - Getting Started with Ring
http://www.learningclojure.com/2013/01/getting-started-with-ring.html - Getting Started with Ring and Compojure – Clojure Web Programming
http://www.myclojureadventure.com/2011/03/getting-started-with-ring-and-compojure.html - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - 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/ - 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/ - Unit Testing in Clojure
http://nakkaya.com/2009/11/18/unit-testing-in-clojure/ - Testing in Clojure (Part-1: Unit testing)
http://blog.knoldus.com/2014/03/22/testing-in-clojure-part-1-unit-testing/ - API for clojure.test – Clojure v1.6 (stable)
https://clojure.github.io/clojure/clojure.test-api.html - Leiningen: úvodní stránka
http://leiningen.org/ - Leiningen: Git repository
https://github.com/technomancy/leiningen - leiningen-win-installer
http://leiningen-win-installer.djpowell.net/ - Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - 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/ - 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/ - 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/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - 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/ - 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/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - 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/ - 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/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - 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/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/