« mooc.fi

Sisällysluettelo

Tehtävät

Osa 4

Neljäs osio alkaa kertaustehtävällä, jossa täydennetään varauskalenterin toiminnallisuutta. Tämän jälkeen keskitymme web-ohjelmistoille tyypilliseen ohjelmistokehitysprosessiin sekä tähän liittyviin oleellisiin työvälineisiin. Lopulta tutustumme REST-arkkitehtuurimalliin.

Tässä tehtävässä tehtävänäsi on täydentää kesken jäänyttä varaussovellusta siten, että kaikki käyttäjät näkevät varaukset, mutta vain kirjautuneet käyttäjät pääsevät lisäämään varauksia.

Kun käyttäjä tekee pyynnön sovelluksen juuripolkuun /reservations, tulee hänen nähdä varaussivu. Allaolevassa esimerkissä tietokannassa ei ole varauksia, mutta jos niitä on, tulee ne listata kohdan Current reservations alla.

Jos kirjautumaton käyttäjä yrittää tehdä varauksen, hänet ohjataan kirjautumissivulle.

Kun kirjautuminen onnistuu, voi käyttäjä tehdä varauksia.

Sovelluksen tulee kirjautumis- ja varaustoiminnallisuuden lisäksi myös varmistaa, että varaukset eivät mene päällekkäin.

Luokassa DefaultController luodaan muutamia testikäyttäjiä, joita voi (esimerkiksi) käyttää sovelluksen testauksessa. Tarvitset ainakin:

  • Palvelun käyttäjän tunnistautumiseen (esim. CustomUserDetailsService, kts. tehtävä 35), jolla täydennät luokkaa SecurityConfiguration
  • Tavan aikaleimojen käsittelyyn (kts. esim. tehtävä 30)
  • Kontrollerin varausten käsittelyyn ja tekemiseen

Käyttäjät ja oikeudet

Käyttäjillä on usein erilaisia oikeuksia sovelluksessa. Verkkokaupassa kaikki voivat listata tuotteita sekä lisätä tuotteita ostoskoriin, mutta vain tunnistautuneet käyttäjät voivat tehdä tilauksia. Tunnistautuneista käyttäjistä vain osa, esimerkiksi kaupan työntekijät, voivat tehdä muokkauksia tuotteisiin.

Tällaisen toiminnan toteuttamiseen käytetään oikeuksia, joiden lisääminen vaatii muutamia muokkauksia aiempaan kirjautumistoiminnallisuuteemme. Aiemmin näkemässämme luokassa CustomUserDetailsService noudettiin käyttäjä seuraavasti:

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException("No such user: " + username);
        }

        return new org.springframework.security.core.userdetails.User(
                account.getUsername(),
                account.getPassword(),
                true,
                true,
                true,
                true,
                Arrays.asList(new SimpleGrantedAuthority("USER")));
    }

Palautettavan User-olion luomiseen liittyy lista oikeuksia. Yllä käyttäjälle on määritelty oikeus USER, mutta oikeuksia voisi olla myös useampi. Seuraava esimerkki palauttaa käyttäjän "USER" ja "ADMIN" -oikeuksilla.

        return new org.springframework.security.core.userdetails.User(
                account.getUsername(),
                account.getPassword(),
                true,
                true,
                true,
                true,
                Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN")));

Oikeuksia käytetään käytettävissä olevien polkujen rajaamisessa. Voimme rajata luokassa SecurityConfiguration osan poluista esimerkiksi vain käyttäjille, joilla on ADMIN-oikeus. Alla olevassa esimerkissä kaikki käyttäjät saavat tehdä GET-pyynnön sovelluksen juuripolkuun. Vain ADMIN-käyttäjät pääsevät polkuun /clients, jonka lisäksi muille sivuille tarvitaan kirjautuminen (mikä tahansa oikeus). Kuka tahansa pääsee kirjautumislomakkeeseen käsiksi.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.GET, "/").permitAll()
                .antMatchers("/clients").hasAnyAuthority("ADMIN")
                .anyRequest().authenticated();
        http.formLogin()
                .permitAll();
    }

Oikeuksia varten määritellään tyypillisesti erillinen tietokantataulu, ja käyttäjällä voi olla useampia oikeuksia.

Sovelluksessa on toteutettuna käyttäjienhallinta tällä hetkellä siten, että käyttäjillä ei ole erillisiä oikeuksia. Muokkaa sovellusta ja lisää sovellukseen käyttäjäkohtaiset oikeudet. Suojaa tämän jälkeen sovelluksen polut seuraavasti:

  • Kuka tahansa saa nähdä polusta /happypath palautetun tiedon
  • Vain USER tai ADMIN -käyttäjät saavat nähdä polusta /secretpath palautetun tiedon
  • Vain ADMIN-käyttäjät saavat nähdä polusta /adminpath palautetun tiedon

Lisää sovellukseen myös seuraavat käyttäjät:

Käyttäjätunnus Salasana Oikeudet
larry larry USER
moe moe USER ja ADMIN
curly curly ADMIN

Tyypillinen ohjelmistokehitysprosessi

Ohjelmiston elinkaareen kuuluu vaatimusmäärittely, suunnittelu, toteutus, testaus, sekä ylläpito ja jatkokehitys. Vaatimusmäärittelyyn kuuluu ohjelmistoon liittyvien toiveiden ja vaatimusten kartoitus, jota seuraa suunnittelu, missä pohditaan miten vaatimukset toteutetaan. Toteutusvaihe sisältää ohjelmointia sekä sovelluksen elinympäristöön liittyvien komponenttien yhteensovittamista. Testaukseen kuuluu sovelluksen testaus niin automaattisesti kuin manuaalisesti. Kun ohjelmisto tai sen osa on toiminnassa, tulee elinkaaren osaksi myös käytössä olevasta ohjelmistosta löytyvien virheiden korjaaminen sekä uusien ominaisuuksien kehittäminen.

Ohjelmointiin ja ohjelmistojen kehitykseen liittyy jatkuva etsiminen ja kokeileminen. Ongelmat pyritään ratkaisemaan kokeilemalla vaihtoehtoja kunnes ongelmaan löytyy sopiva ratkaisu. Jos ongelma on osittain tuttu, on tarkasteltavia vaihtoehtoja vähemmän, ja jos ongelma on tuttu, on siihen tyypillisesti ainakin yksi valmis ratkaisumalli. Tämän hetken suosituimmat ohjelmistokehitysmenetelmät (ketterät menetelmät kuten Scrum ja Kanban) ohjaavat työn läpinäkyvyyteen, oman työskentelyn kehittämiseen sekä siihen, että muiden osallistuminen ohjelmistokehitykseen on helppoa.

Ohjelmistoon liittyvät toiveet ja vaatimukset

Ohjelmistoon liittyvistä toiveista ja vaatimuksista keskustellaan asiakkaan ja käyttäjien kanssa, ja ne kirjataan muistiin. Vaatimukset kirjataan usein lyhyessä tarinamuodossa, joka kerrotaan uutta toiminnallisuutta toivovan henkilön näkökulmasta: "As a (käyttäjän tyyppi) I want (tavoite) so that (syy)." -- esimerkiksi "As a user I want to be able to view the messages so that I can see what others have written". Vaatimuksia kirjattaessa saadaan kuva ohjelmistolta toivotusta toiminnallisuudesta, jonka jälkeen toiminnallisuuksia voidaan järjestää tärkeysjärjestykseen.

Toiminnallisuuksien tärkeysjärjestykseen asettaminen tapahtuu yhdessä asiakkaan ja käyttäjien kanssa. Kun toiminnallisuudet ovat kutakuinkin tärkeysjärjestyksessä, valitaan niistä muutama kerrallaan työstettäväksi. Samalla varmistetaan asiakkaan kanssa, että ohjelmistokehittäjät ja asiakas ymmärtävät toiveen samalla tavalla. Kun toiminnallisuus on valmis, toiminnallisuus näytetään asiakkaalle ja asiakas pääsee kertomaan uusia toiminnallisuustoiveita sekä mahdollisesti uudelleenjärjestelemään vaatimusten tärkeysjärjestystä.

Vaatimuksia ja toiveita, sekä niiden kulkemista projektin eri vaiheissa voidaan käsitellä esimerkiksi Trello:n avulla. Ohje Trellon käyttöön.

Versionhallinta

Ohjelmiston lähdekoodin ja dokumentaatio tallennetaan keskitetysti versionhallintaan, mistä kuka tahansa voi hakea ohjelmistosta uusimman version sekä lähettää sinne uudemman päivitetyn version. Käytännössä jokaisella ohjelmistokehittäjällä on oma hiekkalaatikko, jossa ohjelmistoon voi tehdä muutoksia vaikuttamatta muiden tekemään työhön. Jokaisella ohjelmistokehittäjällä on yleensä samat tai samankaltaiset työkalut (ohjelmointiympäristö, ...), mikä helpottaa muiden kehittäjien auttamista.

Kun ohjelmistokehittäjä valitsee vaatimuksen työstettäväksi, hän tyypillisesti hakee projektin versionhallinnasta projektin uusimman version, sekä lähtee toteuttamaan uutta vaatimusta. Kun vaatimukseen liittyvä osa tai komponentti on valmis sekä testattu paikallisesti (automaattiset testit on olemassa, toimii ohjelmistokehittäjän koneella), lähetetään uusi versio versionhallintapalvelimelle.

Versionhallintapalvelin sisältää myös mahdollisesti useampia versioita projektista. Esimerkiksi git-mahdollistaa ns. branchien käyttämisen, jolloin uusia ominaisuuksia voidaan toteuttaa erillään "päähaarasta". Kun uusi ominaisuus on valmis, voidaan se lisätä päähaaraan. Versionhallinnassa olevia koodeja voidaan myös tägätä julkaisuversioiksi.

Yleisin versionhallintatyökalu on Git, joka on käytössä Githubissa. Ensiaskeleet Githubin käyttöön.

Jatkuva integraatio

Versionhallintapalvelin on tyypillisesti kytketty integraatiopalvelimeen, jonka tehtävänä on suorittaa ohjelmistoon liittyvät testit jokaisen muutoksen yhteydessä sekä tuottaa niistä mahdollisesti erilaisia raportteja. Integraatiopalvelin kuuntelee käytännössä versionhallintajärjestelmässä tapahtuvia muutoksia, ja hakee uusimman lähdekoodiversion muutoksen yhteydessä.

Kun testit ajetaan sekä paikallisella kehityskoneella että erillisellä integraatiokoneella ohjelmistosta huomataan virheitä, jotka eivät tule esille muutoksen tehneen kehittäjän paikallisella koneella (esimerkiksi erilainen käyttöjärjestelmä, selain, ...). On myös mahdollista että ohjelmistosta ei noudeta kaikkia sen osia -- ohjelmisto voi koostua useista komponenteista -- jolloin kaikkien vaikutusten testaaminen paikallisesti on mahdotonta. Jos testit eivät mene läpi integraatiokoneella, korjataan muutokset mahdollisimman nopeasti.

Työkaluja automaattiseen kääntämiseen ja jatkuvaan integrointiin ovat esimerkiksi Travis ja Coveralls. Travis varmistaa että viimeisin lähdekoodiversio kääntyy ja että testit menevät läpi, ja Coveralls tarjoaa välineitä testikattavuuden ja projektin historian tarkasteluun -- tässä hyödyksi on esimerkiksi Cobertura. Kummatkin ovat ilmaisia käyttää kun projektin lähdekoodi on avointa -- kumpikin tarjoaa myös suoran Github-tuen.

Travisin käyttöönottoon vaaditaan käytännössä se, että projekti on esimerkiksi Githubissa ja että sen juurikansiossa on travisin konfiguraatiotiedosto .travis.yml. Yksinkertaisimmillaan konfiguraatiotiedosto sisältää vain käytetyn ohjelmointikielen -- travis osaa esimerkiksi päätellä projektin tyypin pom.xml-tiedoston pohjalta. Ohje Traviksen käyttöönottoon.

Nopeasti näytille

Kun uusi vaatimus tai sen osa on saatu valmiiksi, kannattaa viedä palvelimelle palautteen saamista varten. On tyypillistä, että ohjelmistolle on ainakin Staging- ja Tuotanto-palvelimet. Staging-palvelin on lähes identtinen ympäristö tuotantoympäristöön verrattuna. Staging (usein myös QA)-ympäristöön kopioidaan ajoittain tuotantoympäristön data, ja se toimii viimeisenä testaus- ja validointipaikkana (Quality assurance) ennen tuotantoon siirtoa. QA-ympäristöä käytetään myös demo- ja harjoitteluympäristönä. Kun QA-ympäristössä oleva sovellus on päätetty toimivaksi, siirretään sovellus tuotantoympäristöön.

Tuotantoympäristö voi olla yksittäinen palvelin, tai se saattaa olla joukko palvelimia, joihin uusin muutos viedään hiljalleen. Tuotantoympäristö on tyypillisesti erillään muista ympäristöistä mahdollisten virheiden minimoimiseksi.

Käytännössä versioiden päivitys tuotantoon tapahtuu usein automaattisesti. Esimerkiksi ohjelmistoon liittyvä Travis-konfiguraatio voidaan määritellä niin, että jos kaikki testit menevät läpi integraatiopalvelimella, siirretään ohjelmisto automaattisesti tuotantoon. Esimerkiksi Herokussa sijaitsevaan sovellukseen muutokset voidaan hakea automaattisesti Githubista (ohje).

REST-Arkkitehtuurimalli

REST (representational state transfer) on ohjelmointirajapintojen toteuttamiseen tarkoitettu arkkitehtuurimalli (tai "tyyli"), joka määrittelee sovellukset tietoa käsittelevien osien (komponentit), tietokohteiden (resurssit), sekä näitä yhdistävien yhteyksien kautta.

Tietoa käsittelevät osat ovat selainohjelmisto, palvelinohjelmisto, ym. Resurssit ovat sovelluksen käsitteitä (henkilöt, kirjat, laskentaprosessit, laskentatulokset -- mikä tahansa voi käytännössä olla resurssi) sekä niitä yksilöiviä osoitteita. Resurssikokoelmat ovat löydettävissä ja navigoitavissa: resurssikokoelma voi löytyä esimerkiksi osoitteesta /persons, /books, /processes tai /results. Yksittäisille resursseille määritellään uniikit osoitteet (esimerkiksi /persons/1), ja niillä on myös määritelty esitysmuoto (esimerkiksi HTML, JSON tai XML); dataa voi lähettää ja vastaanottaa samassa muodossa. Resursseja ja tietoa käsittelevien osien yhteys perustuu tyypillisesti asiakas-palvelin -malliin, missä asiakas tekee pyynnön ja palvelin kuuntelee ja käsittelee vastaanottamiaan pyyntöjä sekä vastaa niihin.

REST-rajapinnat ja Web-sovellukset

HTTP-protokollan yli käsiteltävillä REST-rajapinnoilla on tyypillisesti seuraavat ominaisuudet:

Kirjojen käsittelyyn ja muokkaamiseen määriteltävä rajapinta voisi olla esimerkiksi seuraavanlainen:

Osoitteissa käytetään tyypillisesti substantiivejä -- ei books?id={id} vaan /books/{id}. HTTP-pyynnön tyyppi määrittelee operaation. DELETE-tyyppisellä pyynnöllä poistetaan, POST-tyyppisellä pyynnöllä lisätään, PUT-tyyppisellä pyynnöllä päivitetään tietoja, ja GET-tyyppisellä pyynnöllä haetaan.

Datan muoto on toteuttajan päätettävissä. Tällä hetkellä eräs suosituista datamuodoista on JSON, sillä sen käyttäminen osana selainohjelmistoja on suoraviivaista JavaScriptin kautta. Myös palvelinohjelmistot tukevat olioiden muuttamista JSON-muotoon.

Oletetaan että edelläkuvattu kirjojen käsittelyyn tarkoitettu rajapinta käsittelee JSON-muotoista dataa. Kirjaa kuvaava luokka on seuraavanlainen:

package wad;

public class Book {
    private Long id;
    private String name;

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String setName(String name) {
        this.name = name;
    }
}

Kun luokasta on tehty olio, jonka id-muuttujan arvo on 2 ja nimi "Harry Potter and the Chamber of Secrets", on sen JSON-esitys seuraavanlainen:

{
  "id":2,
  "name":"Harry Potter and the Chamber of Secrets"
}

JSON-notaatio määrittelee olion alkavalla aaltosululla {, jota seuraa oliomuuttujien nimet ja niiden arvot. Lopulta olio päätetään sulkevaan aaltosulkuun }. Oliomuuttujien nimet ovat hipsuissa " sillä ne käsitellään merkkijonoina. Muuttujien arvot ovat arvon tyypistä riippuen hipsuissa. Tarkempi kuvaus JSON-notaatiosta löytyy sivulta json.org.

Pyynnön rungossa lähetettävän JSON-muotoisen datan muuttaminen olioksi tapahtuu annotaation @RequestBody avulla. Annotaatio @RequestBody edeltää kontrollerimetodin parametrina olevaa oliota, johon sovelluskehittäjä pyytää Spring-sovelluskehystä asettamaan JSON-muotoisen datan arvot.

    @RequestMapping(method=RequestMethod.POST)
    public String postBook(@RequestBody Book book) {
        bookRepository.save(book);
        return "redirect:/books";
    }

Vastauksen saa lähetettyä käyttäjälle JSON-muodossa lisäämällä pyyntöä käsittelevään metodiin annotaatio @ResponseBody. Annotaatio @ResponseBody pyytää Spring-sovelluskehystä asettamaan palvelimen tuottaman datan selaimelle lähetettävän vastauksen runkoon. Jos vastaus on olio, muutetaan se (oletuksena) automaattisesti JSON-muotoiseksi vastaukseksi.

    @RequestMapping(method=RequestMethod.GET)
    @ResponseBody
    public Book getBook() {
        Book book = new Book();
        book.setName("Spring API");
        return book;
    }

Edellä mainitut annotaatiot voi myös yhdistää. Oletetaan, että käytössä on bookRepository-niminen olio, jonka metodi save lisää kirjalle yksilöivän tunnuksen ja varastoi sen myöhempää käyttöä varten. Metodi myös palauttaa viitteen uuteen kirja-olioon. Uuden kirjan lisääminen tapahtuisi tällöin seuraavasti.

    @RequestMapping(method=RequestMethod.POST)
    @ResponseBody
    public Book postBook(@RequestBody Book book) {
        return bookRepository.save(book);
    }

Nyt palvelulle voi lähettää JSON-muotoista dataa; vastaus on myös JSON-muotoinen, mutta luotavaan kirjaan on liitetty sen yksilöivä tunnus.

Voimme lisätä annotaatioon @RequestMapping lisätietoa metodin tuottamasta datasta. Attribuutti consumes kertoo minkälaista dataa metodin kuuntelema osoite hyväksyy. Metodi voidaan rajoittaa vastaanottamaan JSON-muotoista dataa merkkijonolla "application/json". Vastaavasti metodille voidaan lisätä tietoa datasta, jota se tuottaa. Attribuutti produces kertoo tuotettavan datatyypin. Alla määritelty metodi sekä vastaanottaa että tuottaa JSON-muotoista dataa.

    @RequestMapping(method=RequestMethod.POST, 
                      consumes="application/json", produces="application/json")
    @ResponseBody
    public Book postBook(@RequestBody Book book) {
        return bookStorage.create(book);
    }

Jos on toteuttamassa omaa REST-rajapintaa, kannattanee joko käyttää Spring Data REST -komponenttia (palaamme tähän hieman myöhemmin) tai määritellä kontrolleriluokan annotaatioksi @RestController. Tämä asettaa jokaisen luokan metodiin annotaation @ResponseBody sekä sopivan datatyypin -- tässä tapauksessa "application/json".

Toteutetaan seuraavaksi kaikki tarvitut metodit kirjojen tallentamiseen. Kontrolleri hyödyntää erillistä luokkaa, joka tallentaa kirjaolioita tietokantaan ja tarjoaa tuen aiemmin määrittelemiemme books-osoitteiden ja pyyntöjen käsittelyyn -- PUT-metodi on jätetty omaa kokeilua varten.

// importit

@RestController
@RequestMapping("books")
public class BookController {

    @Autowired
    private BookRepository bookRepository;

    @RequestMapping(method=RequestMethod.GET)
    public List<Book> getBooks() {
        return bookRepository.findAll();
    }

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public Book getBook(@PathVariable Integer id) {
        return bookRepository.findOne(id);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.DELETE)
    public Book deleteBook(@PathVariable Integer id) {
        return bookRepository.delete(id);
    }    

    @RequestMapping(method=RequestMethod.POST)
    public Book postBook(@RequestBody Book book) {
        return bookRepository.save(book);
    }
}

Tässä tehtävässä toteutetaan pelitulospalvelu, joka tarjoaa REST-rajapinnan pelien ja tuloksien käsittelyyn. Huom! Kaikki syötteet ja vasteet ovat JSON-muotoisia olioita. Tehtäväpohjassa on toteutettu valmiiksi luokat Game ja Score sekä käytännölliset Repository-rajapinnat.

GameController

Pelejä käsitellään luokan Game avulla.

Toteuta pakkaukseen wad.controller luokka GameController, joka tarjoaa REST-rajapinnan pelien käsittelyyn:

  • POST /games luo uuden pelin sille annetun pelin tiedoilla ja palauttaa luodun pelin tiedot. (Huom. vieläkin! Pyynnön rungossa oleva data on aina JSON-muotoista. Vastaukset tulee myös palauttaa JSON-muotoisina.)
  • GET /games listaa kaikki talletetut pelit.
  • GET /games/{name} palauttaa yksittäisen pelin tiedot pelin nimen perusteella.
  • DELETE /games/{name} poistaa nimen mukaisen pelin. Palauttaa poistetun pelin tiedot.

ScoreController

Jokaiselle pelille voidaan tallettaa pelikohtaisia tuloksia (luokka Score). Jokainen pistetulos kuuluu tietylle pelille, ja tulokseen liittyy aina pistetulos points numerona sekä pelaajan nimimerkki nickname.

Toteuta luokka wad.controller.ScoreController, joka tarjoaa REST-rajapinnan tuloksien käsittelyyn:

  • POST /games/{name}/scores luo uuden tuloksen pelille name ja asettaa tulokseen pelin tiedot. Tuloksen tiedot lähetetään kyselyn rungossa.
  • GET /games/{name}/scores listaa pelin name tulokset.
  • GET /games/{name}/scores/{id} palauttaa tunnuksella id löytyvän tuloksen name-nimiselle pelille.
  • DELETE /games/{name}/scores/{id} poistaa avaimen id mukaisen tuloksen peliltä name (pelin tietoja ei tule pyynnön rungossa). Palauttaa poistetun tuloksen tiedot.

Valmiin palvelun käyttäminen

Toisen sovelluksen tarjoamaan REST-rajapintaan pääsee kätevästi käsiksi RestTemplate-luokan avulla. Voimme luoda oman komponentin kirjojen hakemiseen.

// importit

@Service
public class BookService {

    private RestTemplate restTemplate;
    
    public BookService() {
        this.restTemplate = new RestTemplate();
    }

    // tänne luokan tarjoamat palvelut
}

Usein sovellukset hyödyntävät kolmannen osapuolen tarjoamaa palvelua omien toiminnallisuuksiensa toteuttamiseen. Harjoitellaan tätä seuraavaksi.

Palvelu GameRater lisää aiempaan tulospalveluun mahdollisuuden arvostella yksittäisiä pelejä antamalla niille numeroarvosanan 0-5. Arvostelu tehdään kuitenkin erilliseen palveluun, emmekä siis laajenna edellistä palvelua suoraan.

GameRater-palvelun tulee käyttää Tulospalvelu-palvelun REST-rajapintaa, jonka avulla se tarjoaa samanlaisen rajapinnan pelien ja tulosten käsittelyyn. Ainoastaan pelien arvostelut käsitellään ja talletetaan tässä palvelussa! Arvosteluihin käytettävä entiteetti Rating ja siihen liittyvät palveluluokat on valmiina tehtäväpohjassa.

Huom! Joudut tutkimaan tehtäväpohjassa annettua koodia, jotta voit hyödyntää sitä. Joudut myös lukemaan tehtävän Tulospalvelu kuvausta tämän tehtävän toteutuksessa.

Huom! Valmis Tulospalvelu-palvelu löytyy osoitteesta http://wepa-scoreservice-heroku.herokuapp.com/games, joten voit tehdä tämän tehtävän täysin riippumatta tulospalvelu-tehtävästä.

GameRestClient ja GameController

Tee luokka wad.service.GameRestClient, joka toteuttaa rajapinnan GameService. Luokan tulee käyttää Tulospalvelu-palvelua kaikissa rajapinnan määrittelemissä toiminnoissa. REST-rajapinnan käyttö onnistuu Springin RestTemplate-luokan avulla.

Huom! GameRestClient-luokan setUri-metodi ottaa parametriksi yllä annetun URL-osoitteen valmiiseen Tulospalvelu-palveluun.

Luo luokka wad.controller.GameController, joka tarjoaa täsmälleen samanlaisen JSON/REST-rajapinnan kuin Tulospalvelu-palvelun GameController, mutta siten, että jokainen toiminto käyttää valmista Tulospalvelu-palvelua rajapinnan GameService kautta.

Huom! Muista asettaa GameService-rajapinnan kautta URL-osoite valmiiseen http://wepa-scoreservice-heroku.herokuapp.com/games-osoitteeseen ohjelman käynnistyessä, esimerkiksi controller-luokan @PostConstruct-metodissa.

RatingController

Jokaiselle pelille voidaan tallettaa pelikohtaisia arvosteluja entiteetin Rating avulla. Arvosteluun liittyy numeroarvosana rating (0-5).

Arvostelut liittyvät peleihin, jotka on talletettu eri palveluun, joten entiteetin Rating viittaus peliin täytyy tallettaa suoraan avaimena. Koska peleihin viitataan REST-rajapinnassa pelin nimellä, talletetaan jokaiseen Rating-entiteettiin pelin nimi attribuuttiin gameName. Tämän attribuutin avulla voidaan siis löytää arvosteluja pelin nimen perusteella.

Toteuta luokka wad.controller.RatingController, joka tarjoaa REST-rajapinnan arvostelujen käsittelyyn:

  • POST /games/{name}/ratings luo uuden arvostelun pelille name - ainoa vastaanotettava attribuutti on rating
  • GET /games/{name}/ratings listaa talletetut arvostelut pelille name
  • GET /games/{name}/ratings/{id} palauttaa yksittäisen arvostelun tiedot pelin nimen name ja avaimen id perusteella
  • DELETE /games/{name}/ratings/{id} poistaa avaimen id mukaisen arvostelun

REST-palvelun kypsyystasot

Martin Fowler käsittelee artikkelissaan Richardson Maturity Model REST-rajapintojen kypsyyttä. Richardson Maturity Model (RMM) jaottelee REST-toteutuksen kolmeen tasoon, joista kukin tarkentaa toteutusta.

Aloituspiste on tason 0 palvelut, joita ei pidetä REST-palveluina. Näissä palveluissa HTTP-protokollaa käytetään lähinnä väylänä viestien lähettämiseen ja vastaanottamiseen, ja HTTP-protokollan käyttötapaan ei juurikaan oteta kantaa. Esimerkki tason 0 palvelusta on yksittäinen kontrollerimetodi, joka päättelee toteutettavan toiminnallisuuden pyynnössä olevan sisällön perusteella.

Tason 1 palvelut käsittelevät palveluita resursseina. Resurssit kuvataan palvelun osoitteena (esimerkiksi /books-resurssi sisältää kirjoja), ja resursseja voidaan hakea tunnisteiden perusteella (esim. /books/nimi). Edelliseen tasoon verrattuna käytössä on nyt konkreettisia resursseja; olio-ohjelmoijan kannalta näitä voidaan pitää myös olioina joilla on tila.

Tasolla 2 resurssien käsittelyyn käytetään kuvaavia HTTP-pyyntötyyppejä. Esimerkiksi resurssin pyyntö tapahtuu GET-metodilla, ja resurssin tilan muokkaaminen esimerkiksi PUT, POST, tai DELETE-metodilla. Näiden lisäksi palvelun vastaukset kuvaavat tapahtuneita toimintoja. Esimerkiksi jos palvelu luo resurssin, vastauksen tulee olla statuskoodi 201, joka viestittää selaimelle resurssin luomisen onnistumisesta. Oleellista tällä tasolla on pyyntötyyppien erottaminen sen perusteella että muokkaavatko ne palvelimen dataa vai ei (GET vs. muut).

Kolmas taso sisältää tasot 1 ja 2, mutta lisää käyttäjälle mahdollisuuden ymmärtää palvelun tarjoama toiminnallisuus palvelimen vastausten perusteella. Webissä huomiota herättänyt termi HATEOAS käytännössä määrittelee miten web-resursseja tulisi löytää webistä.

RESTin isä, Roy Fielding, pitää vain tason 3 sovellusta oikeana REST-sovelluksena. Ohjelmistosuunnittelun näkökulmasta jokainen taso parantaa sovelluksen ylläpidettävyyttä -- Level 1 tackles the question of handling complexity by using divide and conquer, breaking a large service endpoint down into multiple resources; Level 2 introduces a standard set of verbs so that we handle similar situations in the same way, removing unnecessary variation; Level 3 introduces discoverability, providing a way of making a protocol more self-documenting. (lähde)

Huom! Sovellusta suunniteltaessa ja toteuttaessa ei tule olettaa että RMM-tason 3 sovellus olisi parempi kuin RMM-tason 2 sovellus. Sovellus voi olla huono riippumatta toteutetusta REST-rajapinnan muodosta -- jossain tapauksissa rajapintaa ei oikeasti edes tarvita; asiakkaan tarpeet ja toiveet määräävät mitä sovelluskehittäjän kannattaa tehdä.

Spring Data Rest

Spring-sovelluskehys sisältää projektin Spring Data REST, minkä avulla REST-palveluiden tekeminen helpottuu hieman. Lisäämällä projektin pom.xml-konfiguraatioon riippuvuus spring-boot-starter-data-rest saamme Spring Boot-paketoidun version kyseisestä projektista käyttöömme.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

Nyt Repository-luokkamme tarjoavat automaattisesti REST-rajapinnan, jonka kautta resursseihin pääsee käsiksi. REST-rajapinta luodaan oletuksena sovelluksen juureen, ja tehdään luomalla monikko domain-olioista. Esimerkiksi, jos käytössä on luokka Book, sekä sille määritelty BookRepository, joka perii Spring Data JPA:n rajapinnan, generoidaan rajapinnan /books alle toiminnallisuus kirja-olioiden muokkaamiseen.

Luo rajapinta ItemRepository, joka tarjoaa Item-olioiden tietokantatallennustoiminnallisuuden. Lisää tämän jälkeen Spring Data REST-riippuvuus pom.xml-tiedostoon, ja tarkista REST-rajapintasi toiminta esimerkiksi Postman REST Clientin avulla. Wat is this magic?

Usein käytännössä sovelluksemme kuitenkin toimivat jo palvelun juuripalvelussa, ja haluaisimme esimerkiksi tarjota rajapinnan erillisessä osoitteesssa. Spring Data REST-projektin konfiguraatiota voi muokata erillisen RepositoryRestMvcConfiguration-luokan kautta. Alla olevassa esimerkissä REST-rajapinta luodaan osoitteen /api/v1-alle. Annotaatio @Component kertoo Springille että luokka tulee ladata käyttöön käynnistysvaiheessa; rajapinta kertoo mistä luokasta on kyse.

// pakkaus ja importit

@Component
public class CustomizedRestMvcConfiguration extends RepositoryRestConfigurerAdapter {

    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.setBasePath("/api/v1");
    }
}

Nyt jos sovelluksessa on entiteetti Book sekä siihen sopiva BookRepository, on Spring Data REST-rajapinta osoitteessa /api/v1/books.

Tehtävässä on käytössä nyt jo tutuhko viestien kirjoitus- ja lukemispalvelu. Lisää sovellukseen REST-rajapinta viestien käsittelyyn. GET-pyynnön osoitteeseen /api/messages tulee palauttaa lista viesteistä, POST-pyyntö osoitteeseen /api/messages luo uuden viestin, jne.

Käytännössä sovelluksen kehittäjä ei kuitenkaan tyypillisesti halua kaikkia HTTP-protokollan metodeja kaikkien käyttöön. Käytössä olevien metodien rajaaminen onnistuu käytettävää Repository-rajapintaa muokkaamalla. Alla olevassa esimerkissä BookRepository-rajapinnan olioita ei pysty poistamaan automaattisesti luodun REST-rajapinnan yli.

// pakkaus
import wad.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RestResource;

public interface BookRepository extends JpaRepository<Message, Long> {

    @RestResource(exported = false)
    @Override
    public void delete(Long id);

}

Spring Data REST ja RestTemplate

Spring Data RESTin avulla luotavien rajapintojen hyödyntäminen onnistuu RestTemplaten avulla. Esimerkiksi yllä luotavasta rajapinnasta voidaan hakea Resource-olioita, jotka sisältävät kirjoja. RestTemplaten metodin exchange palauttaa vastausentiteetin, mikä sisältää hakemamme olion tiedot. Kyselyn mukana annettava ParameterizedTypeReference taas kertoo minkälaiseksi olioksi vastaus tulee muuntaa.

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Resource<Book>> response = 
    restTemplate.exchange("osoite/books/1", // osoite
                          HttpMethod.GET, // metodi
                          null, // pyynnön runko; tässä tyhjä
                          new ParameterizedTypeReference<Resource<Book>>() {}); // vastaustyyppi

if (response.getStatusCode() == HttpStatus.OK) {
    Resource<Book> resource = response.getBody();
    Book book = resource.getContent();
}