« mooc.fi

Sisällysluettelo

Tehtävät

Osa 1

Web-sovellusten alkeet

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 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ä.

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).

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.2.RELEASE</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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@SpringBootApplication
@Controller
public class HeiMaailmaController {

    @RequestMapping("*")
    @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: @RequestMapping ja @ResponseBody. Annotaation @RequestMapping avulla määritellään kuunneltava osoite -- 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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HeiMaailmaController {

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

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 tai suorittamalla HelloWorldApplication-luokan main-metodi. 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.

Palvelinohjelmiston polut

Opimme aiemmin, että voimme kuunnella kaikkia palvelinohjelmistoon tulevia pyyntöjä asettamalla @RequestMapping-annotaation parametriksi "*". Käytännössä tämän parametrin avulla määritellään polku, johon palvelimelle tulevat pyynnöt voidaan ohjata. Tähdellä määritellään, että kaikki pyynnöt päätyvät samalle polulle. Muiden polkujen määrittely on luonnollisesti myös mahdollista.

Antamalla poluksi merkkijonon "/salaisuus", kaikki web-palvelimen osoitteeseen /salaisuus tehtävät pyynnöt ohjautuvat kyseiseen polkuun liitettyyn toiminnallisuuteen. Allaolevassa esimerkissä määritellään polku /salaisuus ja kerrotaan, että polkuun tehtävät pyynnöt palauttavat merkkijonon "Kryptos".

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

Yhteen ohjelmaan voi myös määritellä useampia polkuja sekä niihin liittyviä toiminnallisuuksia. 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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PolkuController {

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

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

    @RequestMapping("/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

Pyynnöissä voi lähettää palvelimelle tietoa. Tutustutaan ensin tapaan, missä pyynnön parametrit lisätään osaksi haettavaa osoitetta. 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.

Allaolevassa esimerkissä on luotu palvelinohjelma, jonka tehtävänä on tervehtiä 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-nimisen parametrin arvo.

package parametrit;

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

@Controller
public class TervehtijaController {

    @RequestMapping("/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 samalla @RequestParam-annotaatiolla. Allaolevassa esimerkissä kaikki 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PyyntoParametrienNimetController {

    @RequestMapping("/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.
  • Pyyntö polkuun /sum laskee kaikkien parametrien arvot yhteen ja palauttaa vastauksen käyttäjälle.

Alla on esimerkki ohjelman toiminnasta, kun pyyntö tehdään /sum-polkuun.

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

Näkymät ja data

Tähän asti luomamme sovellukset ovat vastaanottaneet tiettyyn polkuun tulevan pyynnön ja palauttaneet käyttäjälle merkkijonomuodossa olevaa tietoa. Tämä ei kuitenkaan ole ainoa palvelinohjelmistojen toimintatyyppi, vaan 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 HTML-kielellä siten, että HTML-kielen sekaan on upotettu komentoja, joiden perusteella näkymään lisätään palvelimen tuottamaa tietoa.

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-sivut, jonka jälkeen sivuille lisätään komentoja Thymeleafin käsiteltäväksi.

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

Allaolevassa esimerkissä luodaan juuripolkua / kuunteleva sovellus. Kun sovellukseen tehdään pyyntö, palautetaan HTML-sivu, jonka Thymeleaf käsittelee. Thymeleaf päättelee palautettavan sivun metodin palauttaman merkkijonon perusteella. Alla metodi palauttaa merkkijonon "index", jolloin Thymeleaf etsii kansiosta src/main/resources/templates/ sivua index.html. Kun sivu löytyy, Thymeleaf käsittelee sen ja palauttaa sen käyttäjälle. Palaamme tarkemmin tähän käsittelyyn myöhemmin.

package thymeleaf;

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

@Controller
public class ThymeleafController {

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

Huomaa, että pyyntöjä käsittelevällä metodilla ei enää ole annotaatiota @ResponseBody. Emme siis enää halua, että metodin palauttama arvo näytetään suoraan käyttäjälle, vaan haluamme, että käyttäjälle näytetään merkkijonon ilmaisema 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.

Datan lisääminen näkymään

Palvelinohjelmistossa luodun tai haetun datan lisääminen näkymään tapahtuu Model-tyyppisen 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.RequestMapping;

public class ThymeleafJaDataController {

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

Model on Spring-sovelluskehyksen käyttämä Map-rajapinnan toimintaa muistuttava lokerikko, missä jokaisella lokerolla on nimi, mihin arvon voi asettaa. 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.RequestMapping;

@Controller
public class ThymeleafJaDataController {

    @RequestMapping("/")
    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 (testi) tulee korvata attribuutin arvon ilmaisemalla muuttujalla. Attribuutin th:text arvona on ${teksti}, jolloin Thymeleaf etsii model-oliosta avaimella "teksti" arvoa.

Käytännössä Thymeleaf etsii siis Model-oliosta lokeron nimeltä teksti ja asettaa siinä olevan arvon h2-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 GET-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="GET">
    <input type="text" name="nimi"/>
    <input type="submit"/>
</form>

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 GET-tyyppisellä pyynnöllä.

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

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 -- alla parametri on määritelty ei-pakolliseksi annotaation @RequestParam attribuutilla required = false.

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.RequestMapping;
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!");
    }

    @RequestMapping(value = "/")
    public String home(Model model, 
            @RequestParam(required = false) String content) {
        if(content != null && !content.trim().isEmpty()) {
            this.lista.add(content);
        }

        model.addAttribute("list", lista);
        return "index";
    }
}

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.

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

Yllä käytämme attribuuttia nimeltä lista ja tulostamme yksitellen sen sisältämät alkiot. 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.

Alla on esimerkki ohjelman toiminnasta, kun sivulle on lisätty muutama rivi lomakkeen avulla. Viimeisimpänä on juuri lisätty teksti "Hello?".

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.

Alla on esimerkki muistiosta, kun siihen on lisätty 3 viestiä.

Pyynnön uudelleenohjaus ja POST

Olemme tähän mennessä toteuttaneet palvelinohjelmistoihimme vain kyvyn käsitellä GET-tyyppisiä pyyntöjä. GET-tyyppisiä pyyntöjä käytetään ensisijaisesti tiedon hakemiseen, eikä niitä oikeastaan pitäisi käyttää laisinkaan tiedon muuttamiseen. Toinen tapa lähettää tietoa palvelimelle on POST-tyyppiset pyynnöt, joita käytettäessä 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. Kokeile tätä jossain aiemmassa tehtävässä kun olet lähettänyt lomakkeella tietoa!

On tyylikkäämpää toteuttaa lomakkeen dataa vastaanottava toiminnallisuus 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. Pyynnön tyyppi määritellään annotaation @RequestMapping attribuutiksi (method-attribuutti). Tällöin kuunneltava polku tulee määritellä myös tarkemmin (value-attribuutti). 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.RequestMapping;
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!");
    }

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

    @RequestMapping(value = "/", method = RequestMethod.POST)
    public String post(@RequestParam String content) {
        if(!content.trim().isEmpty()) {
            this.lista.add(content);
        }

        return "redirect:/";
    }
}

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.

Olioita kaikkialla!

Thymeleafille annettavaan Modeliin voi hyvin lisätä myös 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:

    @RequestMapping("/")
    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.

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

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.RequestMapping;

@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"));
    }

    @RequestMapping("/")
    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.

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

Polkumuuttujat

Polkuja käytetään erilaisten resurssien tunnistamiseen ja yksilöintiin. Usein kuitenkin vastaan tulee tilanne, missä luodut resurssit ovat uniikkeja, emmekä 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 @RequestMapping 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.RequestMapping;

@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"));
    }

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

    @RequestMapping("/{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:

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:
    @RequestMapping(value = "/{item}", method = RequestMethod.DELETE)
  • 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:

Tiedon tallentaminen

Sovelluksemme -- vaikka huikeita ovatkin -- ovat melko alkeellisia, sillä sovelluksissa käsiteltävää tietoa ei tallenneta mihinkään. Esimerkiksi lomakkeen avulla sovellukselle lähetettävä data katoaa kun sovellus käynnistetään uudestaan. Tämä ei ole kivaa.

Tietokannat ovat palvelinohjelmistosta erillisiä sovelluksia, joiden ensisijainen tehtävä on varmistaa, että käytettävä tieto ei katoa. Otetaan ensiaskeleet tietokannan käyttöön web-palvelinohjelmistoissa -- tutustumme tietokantoihin tarkemmin myöhemmin kurssilla. Käytämme tietokantatoiminnallisuuden toteuttamisessa Spring Data JPA-komponenttia, johon löytyy myös aloituspaketti käyttämästämme Spring Bootista.

Tietokantaan tallennettavat oliot eli entiteetit

Käytämme ORM-kirjastoa (object relational mapping), jonka tehtävänä on muuntaa oliot tietokantaan tallennettavaan muotoon. Karkeasti ajatellen luokka vastaa tietokantataulua ja oliomuuttujat vastaavat tietokannan sarakkeita. Jokainen taulun rivi vastaa yhtä luokasta tehtyä oliota.

Luokat, joista tehdyt oliot voidaan tallentaa tietokantaan, tulee annotoida @Entity-annotaatiolla. Tämän lisäksi luokille tulee määritellä tunnuskenttä, jonka avulla niihin liittyvät oliot voidaan yksilöidä. Voimme käyttää tunnuskentän luomiseen valmista AbstractPersistable-yliluokkaa, jota perittäessä kerromme uniikin tunnuksen tyypin. Esimerkiksi Henkilo-luokasta voidaan tehdä tietokantaan tallennettava seuraavilla muutoksilla.

package wad.domain;

import javax.persistence.Entity;
import org.springframework.data.jpa.domain.AbstractPersistable;

@Entity
public class Henkilo extends AbstractPersistable<Long> {

    private String nimi;

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

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

Kun käytössämme on tietokantaan tallennettava luokka, voimme luoda tietokannan käsittelyyn käytettävän rajapinnan. Kutsutaan tätä rajapintaoliota nimellä HenkiloRepository.

// pakkaus

import wad.domain.Henkilo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface HenkiloRepository extends JpaRepository<Henkilo, Long> {

}

Rajapinta perii Spring Data-projektin JpaRepository-rajapinnan; samalla kerromme, että tallennettava olio on tyyppiä Henkilo ja että tallennettavan olion tunnus on Long-tyyppiä. Tämä tyyppi on sama kuin aiemmin AbstractPersistable-luokan perinnässä parametriksi asetettu tyyppi. Spring osaa käynnistyessään etsiä mm. JpaRepository-rajapintaluokan periviä luokkia. Jos niitä löytyy, se luo niiden pohjalta tietokannan käsittelyyn sopivan olion sekä asettaa olion ohjelmoijan haluamiin muuttujiin.

Nämä muuttujat tulee määritellä @Autowired-annotaatiolla -- jokaiselle muuttujalle tulee oma annotaatio -- palaamme myöhemmin kurssilla tarkemmin tähän ns. olioiden automaattiseen asettamiseen.

Kun olemme luoneet rajapinnan HenkiloRepository, voimme lisätä sen käyttöömme esimerkiksi kontrolleriluokkaan. Tämä tapahtuu seuraavasti.

// ...

@Controller
public class HenkiloController {

    @Autowired
    private HenkiloRepository henkiloRepository;

    // ...
}

Nyt tietokantaan pääsee käsiksi HenkiloRepository-olion kautta. Osoitteessa http://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html on JpaRepository-rajapinnan API-kuvaus, mistä löytyy rajapinnan tarjoamien metodien kuvauksia. Voimme esimerkiksi toteuttaa tietokannassa olevien olioiden listauksen sekä yksittäisen olion haun seuraavasti:

// ...

@Controller
public class HenkiloController {

    @Autowired
    private HenkiloRepository henkiloRepository;

    @RequestMapping(method = RequestMethod.GET)
    public String list(Model model) {
        model.addAttribute("list", henkiloRepository.findAll());
        return "henkilot"; // erillinen henkilot.html
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String get(Model model, @PathVariable Long id) {
        model.addAttribute("henkilo", henkiloRepository.findOne(id));
        return "henkilo"; // erillinen henkilo.html
    }
}

Tässä tehtävässä on valmiiksi toteutettuna tietokantatoiminnallisuus sekä esineiden noutaminen tietokannasta. Lisää sovellukseen toiminnallisuus, jonka avulla esineiden tallentaminen tietokantaan onnistuu valmiiksi määritellyllä lomakkeella.

Alla esimerkki sovelluksesta kun tietokantaan on lisätty muutama rivi:

Luo tässä TodoApplication-tehtävässä nähty tehtävien hallintaan tarkoitettu toiminnallisuus mutta siten, että tehtävät tallennetaan tietokantaan. Tässä tehtävässä entiteettiluokan nimen tulee olla Item ja avaimen tyypin tulee olla Long:

@Entity
public class Item extends AbstractPersistable<Long> {
...

Noudata lisäksi HTML-sivujen rakennetta ja toiminnallisuutta.