Tehtävät
Ensimmäisen osan tavoitteet

Tuntee pinnallisesti Spring Boot -sovelluskehyksen ja osaa luoda pyyntöihin reagoivan web-sovelluksen. Osaa muodostaa Thymeleaf-kirjastoa käyttäen template-sivuja, joita käytetään näkymien luontiin. Ymmärtää käsitteet polku, kontrolleri, pyynnön parametri ja polkumuuttuja. Tuntee POST/Redirect/GET-suunnittelumallin, ja osaa luoda em. suunnittelumallia seuraavan sovelluksen. Osaa käsitellä olioita ja kokoelmia osana sovellusta.

Johdanto web-sovelluksiin

Web-sovellukset koostuvat selain- ja palvelinpuolesta. Käyttäjän koneella toimii selainohjelmisto (esim. Google Chrome), jonka kautta käyttäjä tekee pyyntöjä verkossa sijaitsevalle palvelimelle. Kun palvelin vastaanottaa pyynnön, se käsittelee pyynnön ja rakentaa vastauksen. Vastaus voi sisältää esimerkiksi web-sivun HTML-koodia tai jossain muussa muodossa olevaa tietoa.

Web-sovellusten käyttäminen: (1) käyttäjä klikkaa linkkiä, (2) selain tekee pyynnön palvelimelle, (3) palvelin käsittelee pyynnön ja rakentaa vastauksen, (4) selaimen tekemään pyyntöön palautetaan vastaus, (5) vastauksen näyttäminen käyttäjälle -- ei tässä kuvassa.

Selainohjelmointiin ja käyttöliittymäpuoleen keskityttäessä painotetaan rakenteen, ulkoasun ja toiminnallisuuden erottamista toisistaan. Karkeasti voidaan sanoa, että selaimessa näkyvän sivun sisältö ja rakenne määritellään HTML-tiedostoilla, ulkoasu CSS-tiedostoilla ja toiminnallisuus JavaScript-tiedostoilla.

Palvelinpuolen toiminnallisuutta toteutettaessa keskitytään tyypillisesti selainohjelmiston tarvitsevan "APIn" suunnitteluun ja toteutukseen, sivujen muodostamiseen selainohjelmistoa varten, datan tallentamiseen ja käsittelyyn, sekä sellaisten laskentaoperaatioiden toteuttamiseen, joita selainohjelmistossa ei kannata tai voida tehdä.

Ensimmäinen palvelinohjelmisto

Käytämme kurssilla Spring-sovellusperheen Spring Boot projektia web-sovellusten tekemiseen. Merkittävä osa web-sovellusten rakentamisesta perustuu valmiiden kirjastometodien käyttöön. Niiden avulla määritellään (1) mihin osoitteeseen tulevat pyynnöt käsitellään ja (2) mitä pyynnölle tulee tehdä.

Maven

Käytämme tällä kurssilla Mavenia valmiiden kirjastojen noutamiseen sekä projektien hallintaan. Tämän takia projektimme on luotu Maven-projekteina -- uuden Maven-projektin luominen onnistuu NetBeansissa valitsemalla File -> New Project -> Maven -> Java Application.

Maven-projektin riippuvuudet määritellään projektiin liittyvässä (Project Files) pom.xml-tiedostossa olevassa dependencies-osiossa.

Spring-projektin käynnistämisen voi tehdä komentoriviltä komennolla mvn spring-boot:run. Tällöin sovellus käynnistyy muutosten yhteydessä automaattisesti uudestaan (kun käytämme spring-boot-devtools liitännäistä sekä Spring-sovelluskehyksen maven-tukea) -- tämä nopeuttaa hieman ohjelmistokehitysprosessia. Lisää Spring Devtools-projektista löytyy täältä.

Spring -sovelluskehystä käyttävien web-sovellusten kehityksessä käytettävät osat saa käyttöön lisäämällä projektiin riippuvuuden Spring Boot -projektiin (spring-boot-starter-parent) sekä web-projektiin (spring-boot-starter-web). Käytössämme on tällä hetkellä (30.10.) Spring Bootin version 2 ns. milestonejulkaisu. Tehtäväpohjat sisältävät oikeat määrittelyt.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.M5</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Kun riippuvuudet on lisätty projektiin ja projektista pääsee käsiksi Spring-sovelluskehyksen metodeihin ja luokkiin, voimme luoda ensimmäisen palvelinohjelmistomme.

package heimaailma;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@SpringBootApplication
@Controller
public class HeiMaailmaController {

    @GetMapping("*")
    @ResponseBody
    public String home() {
        return "Hei Maailma!";
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(HeiMaailmaController.class, args);
    }
}

Yllä olevassa esimerkissä luodaan pyyntöjä vastaanottava luokka. Pyyntöjä vastaanottavat luokat merkitään @Controller-annotaatiolla. Tämän perusteella Spring-sovelluskehys tietää, että luokan metodit saattavat käsitellä selaimesta tehtyjä pyyntöjä.

Luokalle on määritelty lisäksi metodi home, jolla on kaksi annotaatiota: @GetMapping ja @ResponseBody. Annotaation @GetMapping avulla määritellään kuunneltava polku -- tässä kaikki "*". Annotaatio @ResponseBody kertoo sovelluskehykselle, että metodin vastaus tulee näyttää vastauksena sellaisenaan.

Eriytämme pyyntöjä vastaanottavat luokat ja sovelluksen käynnistämiseen käytettävän luokan jatkossa toisistaan.

package heimaailma;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HeiMaailmaApplication {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(HeiMaailmaApplication.class, args);
    }
}
package heimaailma;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HeiMaailmaController {

    @GetMapping("*")
    @ResponseBody
    public String home() {
        return "Hei Maailma!";
    }
}
Tehtävien tekeminen

Tästä eteenpäin materiaalissa on myös ohjelmointitehtäviä. Tehtävien tekeminen ja palautus tapahtuu NetBeans-ympäristössä Test My Code-liitännäisen avulla. Test My Code lataa tehtäväpohjat sinulle valmiiksi.

Kuten huomattava osa ohjelmointikursseista, tämäkin kurssi alkaa tehtävällä, jossa toteutettava ohjelma kirjoittaa tekstin Hello World!.

Toteuta tehtäväpohjan pakkauksessa wad.helloworld olevaan HelloWorldController luokkaan toiminnallisuus, joka kuuntelee kaikkia pyyntöjä. Kun palvelin vastaanottaa pyynnön, tulee palvelimen palauttaa merkkijono "Hello World!".

Käynnistä palvelin painamalla NetBeansin play-nappia, suorittamalla HelloWorldApplication-luokan main-metodi, tai kirjoittamalla projektin juuripolussa mvn spring-boot:run. Avaa nettiselain, mene osoitteeseen http://localhost:8080 ja näet selaimessasi tekstin "Hello World!".

Palvelin sammutetaan NetBeansissa punaista nappia painamalla -- vain yksi sovellus voi olla kerrallaan päällä samassa osoitteessa. Palauta tehtävä lopuksi Test My Code:n submit-napilla.

Apua! Palvelimeni ei suostu sammumaan!

Palvelimen sammuttaminen tapahtuu NetBeansissa punaista neliötä klikkaamalla, joka sammuttaa suoritettavan ohjelman. Joissakin käyttöjärjestelmissä tämä on kuitenkin bugista, jolloin palvelin tulee sammuttaa komentoriviltä.

Saat portissa 8080 käynnissä olevan prosessin tunnuksen tietoon terminaalissa komennolla lsof -i :8080. Etsi komennon palauttamasta tulosteesta prosessin tunnus, jonka jälkeen voit sammuttaa prosessin komennolla kill -9 prosessin-tunnus.

Esimerkiksi:

> lsof -i :8080
COMMAND  PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    9916 kayttaja   51u  IPv6 0x65802ef6be5c6f29      0t0  TCP *:tram (LISTEN)
>
  

Yllä prosessin tunnus (PID) on 9916. Tämän jälkeen prosessi sammutetaan komennolla kill -9 9916.

> lsof -i :8080
COMMAND  PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    9916 kayttaja   51u  IPv6 0x65802ef6be5c6f29      0t0  TCP *:tram (LISTEN)
> kill -9 9916
  

Jos käynnistät sovelluksen komentoriviltä (komento mvn spring-boot:run sovelluksen juurikansiossa), ei tätä ongelmaa pitäisi olla.

Palvelinohjelmiston polut

Sovellus kuuntelee kaikkia palvelinohjelmistoon tulevia pyyntöjä jos pyyntöjen käsittelyyn tehty metodi on annotoitu @GetMapping-annotaatiolla, jolle on asetettu parametriksi "*". Käytännössä @GetMapping-annotaation parametrilla määritellään polku, johon palvelimelle tulevat pyynnöt voidaan ohjata. Tähdellä ilmoitetaan, että kyseinen metodi käsittelee kaikki pyynnöt. Muiden polkujen määrittely on luonnollisesti myös mahdollista.

Antamalla @GetMapping-annotaation poluksi merkkijono "/salaisuus", kaikki web-palvelimen osoitteeseen /salaisuus tehtävät pyynnöt ohjautuvat metodille, jolla kyseinen annotaatio on. Allaolevassa esimerkissä määritellään polku /salaisuus ja kerrotaan, että polkuun tehtävät pyynnöt palauttavat merkkijonon "Kryptos".

@GetMapping("/salaisuus")
@ResponseBody
public String home() {
    return "Kryptos";
}

Yhteen ohjelmaan voi määritellä useampia polkuja. Jokainen polku käsitellään omassa metodissaan. Alla olevassa esimerkissä pyyntöjä vastaanottavaan luokkaan on määritelty kolme erillistä polkua, joista jokainen palauttaa käyttäjälle merkkijonon.

package polut;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PolkuController {

    @GetMapping("/path")
    @ResponseBody
    public String path() {
        return "Polku (path)";
    }

    @GetMapping("/route")
    @ResponseBody
    public String route() {
        return "Polku (route)";
    }

    @GetMapping("/trail")
    @ResponseBody
    public String trail() {
        return "Polku (trail)";
    }
}

Toteuta pakkauksessa wad.hellopaths olevaan luokkaan HelloPathsController seuraava toiminnallisuus:

  • Pyyntö polkuun /hello palauttaa käyttäjälle merkkijonon "Hello"
  • Pyyntö polkuun /paths palauttaa käyttäjälle merkkijonon "Paths"

Alla olevassa kuvassa on esimerkki tilanteesta, missä selaimella on tehty pyyntö polkuun /hello

Palauta tehtävä TMC:lle kun olet valmis.

Pyynnön parametrit

Palvelimelle voi lähettää tietoa pyynnön parametreina. Tutustutaan ensin tapaan, missä pyynnön parametrit lisätään osoitteeseen. Esimerkiksi pyynnössä http://localhost:8080/salaisuus?onko=nauris on parametri nimeltä onko, jonka arvoksi on määritelty arvo nauris.

Parametrien lisääminen pyyntöön tapahtuu lisäämällä osoitteen perään kysymysmerkki, jota seuraa parametrin nimi, yhtäsuuruusmerkki ja parametrille annettava arvo. Pyynnössä tuleviin parametreihin pääsee käsiksi @RequestParam-annotaation avulla.

Allaolevan esimerkin sovellus tervehtii kaikkia pyynnön tekijöitä. Ohjelma käsittelee polkuun /hei tulevia pyyntöjä ja palauttaa niihin vastauksena tervehdyksen. Tervehdykseen liitetään pyynnössä tulevan nimi-parametrin arvo.

package parametrit;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TervehtijaController {

    @GetMapping("/hei")
    @ResponseBody
    public String tervehdi(@RequestParam String nimi) {
        return "Hei " + nimi + ", mitä kuuluu?";
    }
}

Nyt esimerkiksi osoitteeseen http://localhost:8080/hei?nimi=Ada tehtävä pyyntö saa vastaukseksi merkkijonon Hei Ada, mitä kuuluu?.

Jos parametreja on useampia, erotellaan ne toisistaan &-merkillä. Seuraavassa esimerkissä pyynnössä on kolme parametria, eka, toka ja kolmas, joiden arvot ovat 1, 2 ja 3 vastaavasti.

http://localhost:8080/salaisuus?eka=1&toka=2&kolmas=3

Kaikki pyynnössä olevat parametrit saa pyyntöä käsittelevät metodin käyttöön @RequestParam-annotaatiolla, mitä seuraa Map-tietorakenne, johon parametrit ja niiden arvot asetetaan. Allaolevassa esimerkissä pyynnön parametrit asetetaan Map-tietorakenteeseen, jonka jälkeen kaikki pyynnön arvojen avaimet palautetaan kysyjälle.

package parametrit;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PyyntoParametrienNimetController {

    @GetMapping("/nimet")
    @ResponseBody
    public String nimet(@RequestParam Map<String, String> parametrit) {
        return parametrit.keySet().toString();
    }
}

Toteuta pakkauksessa wad.hellorequestparams olevaan luokkaan HelloRequestParamsController seuraava toiminnallisuus:

  • Pyyntö polkuun /hello palauttaa käyttäjälle merkkijonon "Hello ", johon on liitetty param-nimisen parametrin sisältämä arvo.
  • Pyyntö polkuun /params palauttaa käyttäjälle kaikkien pyynnön mukana tulevien parametrien nimet ja arvot.

Alla olevassa kuvassa on esimerkki tilanteesta, missä selaimella on tehty pyyntö polkuun /params?hello=world&it=works

Palauta tehtävä TMC:lle kun olet valmis.

Toteuta tässä tehtävässä pakkauksessa wad.calculator sijaitsevaan CalculatorController-luokkaan seuraava toiminnallisuus:

  • Pyyntö polkuun /add laskee parametrien first ja second arvot yhteen ja palauttaa vastauksen käyttäjälle. Huomaa että arvot ovat numeroita, ja ne tulee myös käsitellä numeroina.
  • Pyyntö polkuun /multiply kertoo parametrien first ja second arvot ja palauttaa vastauksen käyttäjälle.

Palauta tehtävä TMC:lle kun olet valmis.

Näkymät ja data

Sovelluksemme ovat vastaanottaneet tiettyyn polkuun tulevan pyynnön ja palauttaneet käyttäjälle merkkijonomuodossa olevaa tietoa. Palvelin voi myös luoda käyttäjälle näkymän, jonka selain lopulta näyttää käyttäjälle.

Näkymät luodaan tyypillisesti apukirjastojen avulla siten, että ohjelmoija luo HTML-näkymät ja upottaa HTML-koodiin kirjastospesifejä komentoja. Nämä komennot mahdollistavat mm. tiedon lisäämisen sivuille.

Tällä kurssilla käyttämämme apuväline näkymän luomiseen on Thymeleaf, joka tarjoaa välineitä datan lisäämiseen HTML-sivuille. Käytännössä näkymiä luodessa luodaan ensin HTML-sivu, jonka jälkeen sivulle lisätään komentoja Thymeleafin käsiteltäväksi.

Thymeleaf-sivut ("templatet") sijaitsevat projektin kansiossa src/main/resources/templates tai sen alla olevissa kansioissa. NetBeansissa kansio löytyy kun klikataan "Other Sources"-kansiota.

Thymeleafin käyttöönotto

Thymeleafin käyttöönotto vaatii pom.xml-tiedostossa olevien riippuvuuksien muokkaamista. Web-sovellusten perustoiminnallisuus saatiin käyttöön lisäämällä org.springframework.boot-ryhmän komponentti spring-boot-starter-web pom.xml-tiedoston dependencies-osioon. Kun vaihdamme riippuvuuden muotoon spring-boot-starter-thymeleaf, pääsemme käyttämään Thymeleafia.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

Jos edellämainittu riippuvuus ei ole aiemmin ladattuna koneelle, tulee se myös hakea. Tämä onnistuu joko kirjoittamalla komentorivillä projektin juuressa komento mvn dependency:resolve tai valitsemalle NetBeansissa projektiin liittyvä kansio Dependencies oikealla hiirennapilla ja painamalla Download Declared Dependencies.

Thymeleaf vaatii myös, että jokaisen HTML-sivun html-elementin määrittelyssä tulee olla seuraavat määrittelyt.

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

Alla olevassa esimerkissä luodaan juuripolkua / kuunteleva sovellus. Kun sovellukseen tehdään pyyntö, palautetaan HTML-sivu, jonka Thymeleaf käsittelee. Spring päättelee käsiteltävän ja palautettavan sivun merkkijonon perusteella. Alla metodi palauttaa merkkijonon "index", jolloin Spring etsii kansiosta src/main/resources/templates/ sivua index.html. Kun sivu löytyy, se annetaan Thymeleafin käsiteltäväksi, jonka jälkeen sivu palautetaan käyttäjälle.

package thymeleaf;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ThymeleafController {

    @GetMapping("/")
    public String home() {
        return "index";
    }
}

Pyyntöjä käsittelevällä metodilla ei ole annotaatiota @ResponseBody. Emme siis tässä halua, että metodin palauttama arvo näytetään suoraan käyttäjälle, vaan haluamme, että käyttäjälle näytetään merkkijonon osoittama näkymä. Näkymä luodaan Thymeleafin avulla.

Toteuta tässä tehtävässä pakkauksessa wad.hellothymeleaf sijaitsevaan HelloThymeleafController-luokkaan seuraava toiminnallisuus:

  • Pyyntö juuripolkuun / palauttaa käyttäjälle Thymeleafin avulla kansiossa src/main/resources/templates/ olevan index.html-tiedoston.
  • Pyyntö polkuun /video palauttaa käyttäjälle Thymeleafin avulla kansiossa src/main/resources/templates/ olevan video.html-tiedoston.

Alla on esimerkki ohjelman toiminnasta, kun selaimella on tehty pyyntö sovelluksen juuripolkuun.

Palauta tehtävä TMC:lle kun olet valmis.

HTML

Jos mietit mistä ihmeestä tuossa HTML-lyhenteessä on kyse tai haluat verestää HTML-osaamistasi, nyt on hyvä hetki käydä lukemassa osoitteessa http://www.w3schools.com/html/default.asp oleva HTML-opas.

Model ja datan lisääminen näkymään

Palvelinohjelmistossa luodun tai haetun datan lisääminen näkymään tapahtuu Model-olion avulla. Kun lisäämme Model-olion pyyntöjä käsittelevän metodin parametriksi, lisää Spring-sovelluskehys sen automaattisesti käyttöömme.

package thymeleafdata;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

public class ThymeleafJaDataController {

    @GetMapping("/")
    public String home(Model model) {
        return "index";
    }
}

Model on Spring-sovelluskehyksen käyttämä hajautustaulun toimintaa jäljittelevä olio. Alla olevassa esimerkissä määrittelemme pyyntöjä käsittelevälle metodille Model-olion, jonka jälkeen lisäämme lokeroon nimeltä teksti arvon "Hei mualima!".

package thymeleafdata;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ThymeleafJaDataController {

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("teksti", "Hei mualima!");
        return "index";
    }
}

Kun käyttäjä tekee pyynnön, joka ohjautuu ylläolevaan metodiin, ohjautuu pyyntö return-komennon jälkeen Thymeleafille, joka saa käyttöönsä myös Model-olion ja siihen lisätyt arvot.

Sivun käsittely Thymeleafissa

Oletetaan, että käytössämme olevan index.html-sivun lähdekoodi on seuraavanlainen:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Otsikko</title>
    </head>

    <body>
        <h1>Hei maailma!</h1>

        <h2 th:text="${teksti}">testi</h2>
    </body>
</html>

Kun Thymeleaf käsittelee HTML-sivun, se etsii sieltä elementtejä, joilla on th:-alkuisia attribuutteja. Ylläolevasta sivusta Thymeleaf löytää h2-elementin, jolla on attribuutti th:text -- <h2 th:text="${teksti}">testi</h2>. Attribuutti th:text kertoo Thymeleafille, että elementin tekstiarvo (tässä "testi") tulee korvata attribuutin arvon ilmaisemalla muuttujalla. Attribuutin th:text arvona on ${teksti}, jolloin Thymeleaf etsii model-oliosta arvoa avaimella "teksti".

Käytännössä Thymeleaf etsii -- koska sivulla olevasta elementistä löytyy attribuutti th:text="${teksti}" -- Model-oliosta lokeron nimeltä teksti ja asettaa siinä olevan arvon elementin tekstiarvoksi. Tässä tapauksessa teksti testi korvataan Model-olion lokerosta teksti löytyvällä arvolla, eli tekstillä Hei mualima!.

Tehtäväpohjan mukana tulevaan HTML-tiedostoon on toteutettu tarina, joka tarvitsee otsikon ja päähenkilön. Toteuta pakkauksessa wad.hellomodel sijaitsevaan HelloModelController-luokkaan toiminnallisuus, joka käsittelee juuripolkuun tulevia pyyntöjä ja käyttää pyynnössä tulevia parametreja tarinan täydentämiseen. Voit olettaa, että pyynnön mukana tulevien parametrien nimet ovat title ja person.

Lisää pyynnön mukana tulevien parametrien arvot Thymeleafille annettavaan HashMappiin. Otsikon avaimen tulee olla "title" ja henkilön avaimen tulee olla "person". Palautettava sivu on index.html.

Alla on esimerkki ohjelman toiminnasta, kun juuripolkuun tehdyssä pyynnössä on annettuna otsikoksi Mökkielämää ja henkilöksi Leena.

Palauta tehtävä TMC:lle kun olet valmis.

Tiedon lähettäminen palvelimelle

HTML-sivuille voi määritellä lomakkeita (form), joiden avulla käyttäjä voi lähettää tietoa palvelimelle. Lomakkeen määrittely tapahtuu form-elementin avulla, jolle kerrotaan polku, mihin lomake lähetetään (action), sekä pyynnön tyyppi (method). Pidämme pyynnön tyypin toistaiseksi POST-tyyppisenä.

Lomakkeeseen voidaan määritellä mm. tekstikenttiä (<input type="text"...) sekä painike, jolla lomake lähetetään (<input type="submit"...). Alla tekstikentän name-attribuutin arvoksi on asetettu nimi. Tämä tarkoittaa sitä, että kun lomakkeen tiedot lähetetään palvelimelle, tulee pyynnössä nimi-niminen parametri, jonka arvona on tekstikenttään kirjoitettu teksti.

<form th:action="@{/}" method="POST">
    <input type="text" name="nimi"/>
    <input type="submit"/>
</form>

Lomakkeen avulla lähetetty tieto -- jos lähetysmetodiksi on asetettu "POST" -- vastaanotetaan annotaation @PostMapping avulla. Annotaatio on kuin @GetMapping, mutta annotaatiolla merkitään, että polku kuuntelee POST-tyyppisiä pyyntöjä.

HTML-lomakkeen lähetys ja th:action

Polku, johon lomakkeen tiedot lähetetään määritellään form-elementin action-attribuutin avulla. Haluamme vaikuttaa polkuun hieman ja määrittelemme sen thymeleafin kautta th:action attribuutilla. Polku on lisäksi @{polku} @-merkin sekä aaltosulkujen sisällä -- @{polku}.

Tämän avulla varaudumme tilanteeseen, missä palvelimella voi sijaita useampia sovelluksia. Ohjelmoimamme sovellus voi sijaita esimerkiksi polussa http://osoite.com/sovellus1/, ja Thymeleaf päättelee automaattisesti lomakkeen poluksi osoitteen http://osoite.com/sovellus1/polku.

Post/Redirect/Get -suunnittelumalli

Kun palvelimelle lähetetään tietoa POST-tyyppisessä pyynnössä, pyynnön parametrit kulkevat pyynnön rungossa -- palaamme pyynnön erilaisiin muotoihin myöhemmin kurssilla.

Oikeastaan kaikki pyynnöt, joissa lähetetään tietoa palvelimelle, ovat ongelmallisia jos pyynnön vastauksena palautetaan näytettävä sivu. Tällöin käyttäjä voi sivun uudelleenlatauksen (esim. painamalla F5) yhteydessä lähettää aiemmin lähettämänsä datan vahingossa uudelleen.

Lomakkeen dataa vastaanottava toiminnallisuus tulee toteuttaa siten, että lähetetyn tiedon käsittelyn jälkeen käyttäjälle palautetaan vastauksena uudelleenohjauspyyntö. Tämän jälkeen käyttäjän selain tekee uuden pyynnön uudelleenohjauspyynnön mukana annettuun osoitteeseen. Tätä toteutustapaa kutsutaan Post/Redirect/Get-suunnittelumalliksi ja sillä mm. estetään lomakkeiden uudelleenlähetys, jonka lisäksi vähennetään toiminnallisuuden toisteisuutta.

POST-pyynnön kuuntelu ja uudelleenohjaus

Alla on toteutettu POST-tyyppistä pyyntöä kuunteleva polku sekä siihen liittyvä toiminnallisuus. POST-tyyppinen pyyntö määritellään annotaation @PostMapping avulla. Palauttamalla pyyntöä käsittelevästä metodista merkkijono redirect:/ kerrotaan, että pyynnölle tulee lähettää vastauksena uudelleenohjauspyyntö polkuun "/". Kun selain vastaanottaa uudelleenohjauspyynnön, tekee se GET-tyyppisen pyynnön uudelleenohjauspyynnössä annettuun osoitteeseen.

package uudelleenohjaus;

import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class RedirectOnPostController {

    private String message;
  
    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("message", message);
        return "index";
    }

    @PostMapping("/")
    public String post(@RequestParam String content) {
        this.message = content;
        return "redirect:/";
    }
}

Tehtäväpohjassa on toiminnallisuus, jonka avulla sivulla voi näyttää tietoa, ja jonka avulla sivulta lähetetty tieto voidaan myös käsitellä. Tiedon lähettämiseen tarvitaan sivulle kuitenkin lomake.

Toteuta tehtäväpohjan kansiossa src/main/resources/templates olevaan index.html-tiedostoon lomake. Lomakkeessa tulee olla tekstikenttä, jonka nimen tulee olla content. Tämän lisäksi, lomakkeessa tulee olla myös nappi, jolla lomakkeen voi lähettää. Lomakkeen tiedot tulee lähettää juuriosoitteeseen POST-tyyppisellä pyynnöllä.

Kun sovellus toimii oikein, voit vaihtaa sivulla näkyvää otsikkoa lomakkeen avulla.

Tehtäväpohjassa on sekä muistilappujen listaamistoiminnallisuus, että lomake, jonka avulla voidaan lähettää POST-tyyppisiä pyyntöjä palvelimelle. Toteuta sovellukseen toiminnallisuus, missä palvelin kuuntelee POST-tyyppisiä pyyntöjä, lisää pyynnön yhteydessä tulevan tiedon sovelluksessa olevaan listaan ja uudelleenohjaa käyttäjän tekemään GET-tyyppisen pyynnön juuriosoitteeseen.

Tehtävässä ei ole testejä. Palauta tehtävä kun ohjelma toimii halutulla tavalla.

Listojen käsittely

Thymeleafille annettavalle Model-oliolle voi asettaa tekstin lisäksi myös arvokokoelmia. Alla luomme "pääohjelmassa" listan, joka asetetaan Thymeleafin käsiteltäväksi menevään Model-olioon jokaisen juuripolkuun tehtävän pyynnön yhteydessä. Jos juuripolkuun lähetetään parametri nimeltä "content", lisätään se myös listaan.

package thymeleafdata;

import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class ListaController {
    private List<String> lista;

    public ListaController() {
        this.lista = new ArrayList<>();
        this.lista.add("Hello world!");
    }

    @GetMapping(value = "/")
    public String home(Model model) {
        model.addAttribute("list", lista);
        return "index";
    }
  
    @PostMapping(value = "/")
    public String post(Model model, String content) {
        this.lista.add(content);
        return "redirect:/";
    }
}

Listan läpikäynti Thymeleafissa tapahtuu attribuutin th:each avulla. Sen määrittely saa muuttujan nimen, johon kokoelmasta otettava alkio kullakin iteraatiolla tallennetaan, sekä läpikäytävän kokoelman. Perussyntaksiltaan th:each on seuraavanlainen.

<p th:each="alkio : ${lista}">
    <span th:text="${alkio}">hello world!</span>
</p>

Yllä käytämme attribuuttia nimeltä lista ja luomme jokaiselle sen sisältämälle alkiolle p-elementin, jonka sisällä on span-elementti, jonka tekstinä on alkion arvo. Attribuutin th:each voi asettaa käytännössä mille tahansa toistettavalle elementille. Esimerkiksi HTML-listan voisi tehdä seuraavalla tavalla.

<ul>
    <li th:each="alkio : ${lista}">
        <span th:text="${alkio}">hello world!</span>
    </li>
</ul>

Huom! Eräs klassinen virhe on määritellä iteroitava joukko merkkijonona th:each="alkio : lista". Tämä ei luonnollisesti toimi.

Tehtäväpohjassa on palvelinpuolen toiminnallisuus, jossa käsitellään juuripolkuun tuleva pyyntö, sekä lisätään lista Thymeleafille sivun käsittelyyn. Tehtäväpohjaan liittyvä HTML-sivu ei kuitenkaan sisällä juurikaan toiminnallisuutta.

Lisää HTML-sivulle (1) listalla olevien arvojen tulostaminen th:each-komennon avulla ja (2) lomake, jonka avulla palvelimelle voidaan lähettää uusia arvoja.

Toteuta tehtäväpohjan pakkauksessa wad.notebook olevaan NotebookController-luokkaan muistio-ohjelma, jolle voi lisätä muistiinpanoja. Tee ohjelmastasi sellainen, että jos muistiinpanoja on yli 10, se muistaa ja näyttää niistä vain viimeisimmät 10.

Hyödynnä muistion tekemiseen tehtäväpohjassa valmiina mukana tulevaa HTML-sivua. Huomaa, että lomakkeen metodi on POST ja että lomakkeen tekstikentän nimi on content.

Olioiden käsittely

Modeliin voi lisätä myös muunlaisia olioita. Oletetaan, että käytössämme on henkilöä kuvaava luokka.

public class Henkilo {
    private String nimi;

    public Henkilo(String nimi) {
        this.nimi = nimi;
    }

    public String getNimi() {
        return this.nimi;
    }

    public void setNimi(String nimi) {
        this.nimi = nimi;
    }
}

Henkilo-olion lisääminen on suoraviivaista:

@GetMapping("/")
public String home(Model model) {
    model.addAttribute("henkilo", new Henkilo("Le Pigeon"));
    return "index";
}

Kun sivua luodaan, henkilöön päästään käsiksi modeliin asetetun avaimen perusteella. Edellä luotu "Le Pigeon"-henkilö on tallessa avaimella "henkilo". Kuten aiemminkin, avaimella pääsee olioon käsiksi.

<h2 th:text="${henkilo}">Henkilön nimi</h2>

Ylläolevaa henkilön tulostusta kokeillessamme saamme näkyville (esim.) merkkijonon Henkilo@29453f44 -- ei ihan mitä toivoimme. Käytännössä Thymeleaf kutsuu edellisessä tapauksessa olioon liittyvää toString-metodia, jota emme ole määritelleet. Pääsemme oliomuuttujiin käsiksi olemassaolevien getMuuttuja-metodien kautta. Jos haluamme tulostaa Henkilo-olioon liittyvän nimen, kutsumme metodia getNimi. Thymeleafin käyttämässä notaatiossa kutsu muuntuu muotoon henkilo.nimi. Saamme siis halutun tulostuksen seuraavalla tavalla:

<h2 th:text="${henkilo.nimi}">Henkilön nimi</h2>

Olioita listalla

Listan läpikäynti Thymeleafissa tapahtuu attribuutin th:each avulla. Sen määrittely saa muuttujan nimen, johon kokoelmasta otettava alkio kullakin iteraatiolla tallennetaan, sekä läpikäytävän kokoelman. Perussyntaksiltaan th:each on jo tullut aiemmin tutuksi.

<p th:each="alkio : ${lista}">
    <span th:text="${alkio}">hello world!</span>
</p>

Iteroitavan joukon alkioiden ominaisuuksiin pääsee käsiksi aivan samalla tavalla kuin muiden olioiden ominaisuuksiin. Tutkitaan seuraavaa esimerkkiä, jossa listaan lisätään kaksi henkilöä, lista lisätään pyyntöön ja lopulta luodaan näkymä Thymeleafin avulla.

package henkilot;

import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HenkiloController {
    private List<Henkilo> henkilot;

    public HenkiloController() {
        this.henkilot = new ArrayList<>();
        this.henkilot.add(new Henkilo("James Gosling"));
        this.henkilot.add(new Henkilo("Martin Odersky"));
    }

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("list", henkilot);
        return "index";
    }
}
<p>Ja huomenna puheet pitävät:</p>
<ol>
    <li th:each="henkilo : ${list}">
        <span th:text="${henkilo.nimi}">Esimerkkihenkilö</span>
    </li>
</ol>

Käyttäjälle lähetettävä sivu näyttää palvelimella tapahtuneen prosessoinnin jälkeen seuraavalta.

<p>Ja huomenna puheet pitävät:</p>
<ol>
    <li><span>James Gosling</span></li>
    <li><span>Martin Odersky</span></li>
</ol>

Tehtäväpohjassa on sovellus, jossa käsitellään Item-tyyppisiä olioita. Tehtävänäsi on lisätä sovellukseen lisätoiminnallisuutta:

  • Kun käyttäjä avaa selaimella sovelluksen juuripolun, tulee hänen lomakkeen lisäksi nähdä lista esineistä. Jokaisesta esineestä tulee tulla ilmi sen nimi (name) ja tyyppi (type).
  • Kun käyttäjä lähettää lomakkeella uuden esineen palvelimelle, tulee palvelimen säilöä esine listalle seuraavaa näyttämistä varten. Huomaa, että lomake lähettää tiedot POST-pyynnöllä sovelluksen juureen. Kun esine on säilötty, uudelleenohjaa käyttäjän pyyntö siten, että käyttäjän selain tekee GET-tyyppisen pyynnön sovelluksen juuripolkuun.

Alla olevassa esimerkissä sovellukseen on lisätty olemassaolevan taikurin hatun lisäksi Party hat, eli bilehattu.

Kokoelmien käsittely ja polkumuuttujat

Polkuja käytetään erilaisten resurssien tunnistamiseen ja yksilöintiin. Usein kuitenkin vastaan tulee tilanne, missä luodut resurssit ovat uniikkeja, emmekä tiedä niiden tietoja ennen sovelluksen käynnistymistä. Jos haluaisimme näyttää tietyn resurssin tiedot, voisimme lisätä pyyntöön parametrin -- esim esineet?tunnus=3, minkä arvo olisi haetun resurssin tunnus.

Toinen vaihtoehto on ajatella polkua haettavan resurssin tunnistajana. Annotaatiolle @GetMapping määriteltävään polkuun voidaan määritellä polkumuuttuja aaltosulkujen avulla. Esimerkiksi polku "/{arvo}" ottaisi vastaan minkä tahansa juuripolun alle tulevan kyselyn ja tallentaisi arvon myöhempää käyttöä varten. Tällöin jos käyttäjä tekee pyynnön esimerkiksi osoitteeseen http://localhost:8080/kirja, tallentuu arvo "kirja" myöhempää käyttöä varten. Polkumuuttujiin pääsee käsiksi pyyntöä käsittelevälle metodille määriteltävän annotaation @PathVariable avulla.

Yksittäisen henkilön näyttäminen onnistuisi esimerkiksi seuravavasti:

package henkilot;

import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HenkiloController {
    private List<Henkilo> henkilot;

    public ListaController() {
        this.henkilot = new ArrayList<>();
        this.henkilot.add(new Henkilo("James Gosling"));
        this.henkilot.add(new Henkilo("Martin Odersky"));
    }

    @GetMapping("/")
    public String home(Model model) {
        model.addAttribute("list", henkilot);
        return "index";
    }

    @GetMapping("/{id}")
    public String getOne(Model model, @PathVariable Integer id) {
        if(id < 0 || id >= this.henkilot.size()) {
            return home(model);
        }

        model.addAttribute("henkilo", henkilot.get(id));
        return "henkilo";
    }
}

Tehtäväpohjassa on sovellus, jossa käsitellään taas edellisestä tehtävästä tuttuja Item-tyyppisiä olioita. Tällä kertaa esineet kuitenkin kuvastavat hattuja. Kun sovelluksen juureen tehdään pyyntö, käyttäjälle näytetään oletushattu ("default"). Lisää sovellukseen toiminnallisuus, minkä avulla tiettyyn polkuun tehtävä kysely palauttaa sivun, jossa näkyy tietyn hatun tiedot -- huomaa, että voit asettaa polkumuuttujan tyypiksi myös Stringin.

Sovelluksen juuripolkuun tehtävä pyyntö näyttää seuraavanlaisen sivun:

Muihin osoitteisiin tehtävät pyynnöt taas palauttavat tehtäväpohjassa olevasta items-hajautustaulusta polkuun sopivan hatun. Esimerkiksi pyyntö polkuun /ascot näyttää seuraavanlaisen sivun:

FluentLenium

Seuraavat kaksi tehtävää käyttävät FluentLenium-nimistä kirjastoa testeihin. Klikkaa projektien kohdalla Dependencies -> Download declared dependencies, niin kirjastot latautuvat käyttöön.

Edellisessä tehtävässä käytössämme oli vain yksi sivu. Olisi kuitenkin hienoa, jos jokaiselle hatulle olisi oma sivu -- ainakin sovelluksen käyttäjän näkökulmasta.

Tehtäväpohjassa on valmiina sovellus, joka listaa olemassaolevat hatut ja näyttää ne käyttäjälle. Jokaisen hatun yhteydessä on linkki, jota klikkaamalla pitäisi päästä hatun omalle sivulle.

Toteuta sekä html-sivu (single.html), että sopiva metodi, joka ohjaa pyynnön sivulle.

Pyyntö sovelluksen juureen luo seuraavanlaisen sivun.

Jos sivulta klikkaa hattua, pääsee tietyn hatun tiedot sisältävälle sivulle. Alla olevassa esimerkissä on klikattu taikurin hattuun liittyvää linkkiä.

Tässä tehtävässä tulee rakentaa tehtävien hallintaan tarkoitettu sovellus. Sovelluksen käyttämät sivut ovat valmiina näkyvissä, itse sovelluksen pääset toteuttamaan itse.

Sovelluksen tulee sisältää seuraavat toiminnallisuudet:

  • Kaikkien tehtävien listaaminen. Kun käyttäjä tekee pyynnön sovelluksen juuripolkuun, tulee hänelle näyttää sivu, missä tehtävät on listattuna. Sivulla on myös lomake tehtävien lisäämiseen.
  • Yksittäisen tehtävän lisääminen. Kun käyttäjä täyttää lomakkeen sivulla ja lähettää tiedot palvelimelle, tulee sovelluksen lisätä tehtävä näytettävään listaan.
  • Yksittäisen tehtävän poistaminen. Kun käyttäjä painaa tehtävään liittyvää Done!-nappia, tulee tehtävä poistaa listalta. Toteuta tämä niin, että metodin tyyppi on DELETE:
    @DeleteMapping("/{item}")
  • Yksittäisen tehtävän näyttäminen. Kun käyttäjä klikkaa tehtävään liittyvää linkkiä, tulee käyttäjälle näyttää tehtäväsivu. Huom! Tehtävään liittyvien tarkistusten määrä tulee kasvaa aina yhdellä kun sivulla vieraillaan.

Alla kuva tehtävien listauksesta:

Kun tehtävää klikkaa, näytetään erillinen tehtäväsivu:

Kun sivu avataan toisen kerran, kasvaa tehtävien tarkistukseen liittyvä laskuri:

Sisällysluettelo