« mooc.fi

Sisällysluettelo

Tehtävät

Osa 7

Kurssin seitsemäs osio alkaa laajemmalla kertaavalla tehtävällä, missä rakennetaan kolmannen osapuolen vitsejä tarjoavaan palveluun arviointitoiminnallisuus. Tämän jälkeen tutustutaan reaktiiviseen ohjelmointiin sekä web socket-teknologiaan. Viimeisen osion tehtävät ovat kaikki testittömiä: osassa toivotaan, että päätät itse lisättävän toiminnallisuuden, osassa taas toivottu toiminnallisuus on kuvattu.

Kertaustehtävässä on toteutettuna kolmannen osapuolen palvelusta vitsejä noutava sovellus. Vitseille on lisätty arviointimahdollisuus (tykkää / en tykkää). Muokkaa sovellusta siten, että käyttäjälle tarjotaan linkki eniten tykättyyn vitsiin -- linkkiä klikkaamalla kyseisen vitsin pääsee näkemään ja arvioimaan. Hyödynnä arvioita eniten tykätyn vitsin päättelyyn: jos arviot omat samat, voit päättää miten toimit.

Tyylitiedostot

Olet ehkäpä huomannut, että tähän mennessä tekemämme web-sovellukset eivät ole kovin kaunista katsottavaa. Kurssilla pääpaino on palvelinpään toiminnallisuuden toteuttamisessa, joten emme jatkossakaan keskity sivustojen ulkoasuun. Sivujen ulkoasun muokkaaminen on kuitenkin melko suoraviivaista. Verkosta löytyy iso kasa oppaita sivun ulkoasun määrittelyyn -- tässä yksi.

Ulkoasun määrittelyssä käytetään usein apuna valmista Twitter Bootstrap -kirjastoa. Ulkoasun määrittely tapahtuu lisäämällä sivun head-osioon oleelliset kirjastot -- tässä kirjastot haetaan https://www.bootstrapcdn.com/-palvelusta, joka tarjoaa kirjastojen ylläpito- ja latauspalvelun, jonka lisäksi elementteihin voi lisätä luokkamäärittelyjä, jotka kertovat niiden tyyleistä.

Alla on esimerkki HTML-sivusta, jossa Twitter Bootstrap on otettu käyttöön. Sivulla on lisäksi määritelty body-elementin luokaksi (class) "container", mikä tekee sivusta päätelaitteen leveyteen reagoivan. Elementillä table oleva luokka "table" lisää elementtiin tyylittelyn. Erilaisiin Twitter Bootstrapin tyyleihin voi tutustua tarkemmin täällä.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Blank</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"/>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"/>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    </head>

    <body class="container">

        <table class="table">
            <tr>
                <th>An</th>
                <th>important</th>
                <th>header</th>
            </tr>
            <tr>
                <td>More</td>
                <td>important</td>
                <td>text</td>
            </tr>
            <tr>
                <td>More</td>
                <td>important</td>
                <td>text</td>
            </tr>
            <tr>
                <td>More</td>
                <td>important</td>
                <td>text</td>
            </tr>
            <tr>
                <td>More</td>
                <td>important</td>
                <td>text</td>
            </tr>
        </table>

    </body>
</html>

Tässä tehtävässä tavoitteena on lähinnä kokeilla sovelluksessa olevaa sivua ilman tyylitiedostoja sekä tyylitiedostojen kanssa. Käynnistä palvelin ja katso miltä juuripolussa toimiva sovellus näyttää.

Sammuta tämän jälkeen palvelin ja muokkaa sovellukseen liittyvää index.html-tiedostoa siten, että poistat kommenttimerkit head-elementissä olevien Twitter Bootstrap -kirjaston linkkien ympäriltä. Käynnistä tämän jälkeen palvelin uudestaan ja katso miltä sivu tämän jälkeen näyttää. Oleellista tässä on se, että sivun ulkoasun muuttamiseen tarvittiin käytännössä vain tyylitiedostojen lisääminen.

Tehtävässä ei ole testejä -- voit palauttaa sen kun olet kokeillut ylläolevaa muutosta.

Reaktiivisen sovelluksen ohjelmointi

Tarkastellaan seuraavaksi lyhyesti reaktiivisten sovellusten ohjelmointia. Tutustumme ensin pikaisesti funktionaaliseen ohjelmointiin sekä reaktiiviseen ohjelmointiin, jonka jälkeen nämä yhdistetään. Lopuksi katsotaan erästä tapaa lisätä palvelimen ja selaimen välistä vuorovaikutusta.

Funktionaalinen ohjelmointi

Funktionaalisen ohjelmoinnin ydinajatuksena on ohjelmakoodin suorituksesta johtuvien sivuvaikutusten minimointi. Sivuvaikutuksilla tarkoitetaan ohjelman tai ympäristön tilaan vaikuttavia epätoivottuja muutoksia. Sivuvaikutuksia ovat esimerkiksi muuttujan arvon muuttuminen, tiedon tallentaminen tietokantaan tai esimerkiksi käyttöliittymän näkymän muuttaminen.

Keskiössä ovat puhtaat ja epäpuhtaat funktiot. Puhtaat funktiot noudattavat seuraavia periaatteita: (1) funktio ei muuta ohjelman sisäistä tilaa ja sen ainoa tuotos on funktion palauttama arvo, (2) funktion palauttama arvo määräytyy funktiolle parametrina annettavien arvojen perusteella, eikä samat parametrien arvot voi johtaa eri palautettaviin arvoihin, ja (3) funktiolle parametrina annettavat arvot on määritelty ennen funktion arvon palauttamista.

Epäpuhtaat funktiot taas voivat palauttaa arvoja, joihin vaikuttavat myös muutkin asiat kuin funktiolle annettavat parametrit, jonka lisäksi epäpuhtaat funktiot voivat muuttaa ohjelman tilaa. Tällaisia ovat esimerkiksi tietokantaa käyttävät funktiot, joiden toiminta vaikuttaa myös tietokannan sisältöön tai jotka hakevat tietokannasta tietoa.

Funktionaaliset ohjelmointikielet tarjoavat välineitä ja käytänteitä jotka "pakottavat" ohjelmistokehittäjää ohjelmoimaan funktionaalisen ohjelmoinnin periaatteita noudattaen. Tällaisia kieliä ovat esimerkiksi Haskell, joka on puhdas funktionaalinen ohjelmointikieli eli siinä ei ole mahdollista toteuttaa epäpuhtaita funktioita. Toinen esimerkki on Clojure, jossa on mahdollista toteuttaa myös epäpuhtaita funktiota -- Clojureen löytyy myös erillinen Helsingin yliopiston tarjoama MOOC (Functional programming with Clojure).

Funktionaalisen ohjelmoinnin hyötyihin liittyy muunmuassa testattavuus. Alla on annettuna esimerkki metodista, joka palauttaa nykyisenä ajanhetkenä tietyllä kanavalla näkyvän ohjelman.

public TvOhjelma annaTvOhjelma(Opas opas, Kanava kanava) {
    Aikataulu aikataulu = opas.annaAikataulu(kanava);
    return aikataulu.annaTvOhjelma(new Date());
}

Ylläolevan metodin palauttamaan arvoon vaikuttaa aika, eli sen arvo ei määräydy vain annettujen parametrien perusteella. Metodin testaaminen on vaikeaa, sillä aika muuttuu jatkuvasti. Jos määrittelemme myös ajna metodin parametriksi, paranee testattavuus huomattavasti.

public TvOhjelma annaTvOhjelma(Opas opas, Kanava kanava, Date aika) {
    Aikataulu aikataulu = opas.annaAikataulu(kanava);
    return aikataulu.annaTvOhjelma(aika);
}

Funktionaalisessa ohjelmoinnissa käytetään alkioiden käsittelyyn työvälineitä kuten map ja filter, joista ensimmäistä käytetään arvon muuntamiseen ja jälkimmäistä arvojen rajaamiseen. Alla olevassa esimerkissä käydään läpi henkilölista ja valitaan sieltä vain Maija-nimiset henkilöt. Lopulta heiltä valitaan iät ja ne tulostetaan.

List<Henkilo> henkilot = // .. henkilo-lista saatu muualta

henkilot.stream()
        .filter(h -> h.getNimi().equals("Maija"))
        .map(h -> h.getIka())
        .forEach(System.out::println);

Ylläolevassa esimerkissä henkilot-listan sisältö ei muutu ohjelmakoodin suorituksen aikana. Periaatteessa -- jos useampi sovellus haluaisi listaan liittyvät tiedot -- kutsun System.out::println voisi vaihtaa esimerkiksi tiedon lähettämiseen liittyvällä kutsulla.

Reaktiivinen ohjelmointi

Reaktiivisella ohjelmoinnilla tarkoitetaan ohjelmointiparadigmaa, missä ohjelman tila voidaan nähdä verkkona, missä muutokset muuttujiin vaikuttavat myös kaikkiin niistä riippuviin muuttujiin. Perinteisessä imperatiivisessa ohjelmoinnissa alla olevan ohjelman tulostus on 5.

int a = 3;
int b = 2;
int c = a + b;
a = 7;

System.out.println(c);

Reaktiivisessa ohjelmoinnissa asia ei kuitenkaan ole näin, vaan ohjelman tulostus olisi 9. Lauseke int c = a + b; määrittelee muuttujan c arvon riippuvaiseksi muuttujista a ja b, jolloin kaikki muutokset muuttujiin a tai b vaikuttavat myös muuttujan c arvoon.

Reaktiivista ohjelmointia hyödynnetään esimerkiksi taulukkolaskentaohjelmistoissa, missä muutokset yhteen soluun voivat vaikuttaa myös muiden solujen sisältöihin, mitkä taas mahdollisesti päivittävät muita soluja jne. Yleisemmin ajatellen reaktiivinen ohjelmointiparadigma on kätevä tapahtumaohjatussa ohjelmoinnissa; käyttöliittymässä tehtyjen toimintojen aiheuttamat muutokset johtavat myös käyttöliittymässä näkyvän tiedon päivittymisen.

Termi reaktiivinen ohjelmointi (reactive programming) on kuormittunut, ja sillä on myös toinen yleisesti käytössä oleva merkitys. Reaktiivisella ohjelmoinnilla tarkoitetaan myös reaktiivisten sovellusten kehittämistä.

Funktionaalinen reaktiivinen ohjelmointi

Funktionaalinen reaktiivinen ohjelmointi on funktionaalista ohjelmointia ja reaktiivista ohjelmointia yhdistävä ohjelmointiparadigma. Järjestelmiä on karkeasti jakaen kahta tyyppiä, joista toinen perustuu viestien lähettämiseen (viestejä välitetään verkon läpi kunnes tulos saavutettu) ja toinen viestien odottamiseen (odotetaan kunnes tulokselle on tarvetta, ja tuotetaan tulos).

Materiaalin edellisessä osassa olleessa verkkokauppojen hintavertailuohjelmassa (tehtävä LowestPrices) käytettiin esimerkiksi seuraavaa lähdekoodia kaikkien verkkokauppojen läpikäyntiin:

// services on lista hintatietoja tarjoavia palveluita
// ja taskExecutor on Javan AsyncTaskExecutor-luokan ilmentymä
BaseService bestService = services.stream().parallel()
                .map(s -> taskExecutor.submit(() -> {
                            s.getLowestPrice(item);
                            return s;
                        })
                ).map(f -> {
                    try {
                        return f.get();
                    } catch (Throwable t) {
                        // käsittele virhe
                        return null;
                    }
                })
                .min((s1, s2) -> Double.compare(s1.getLowestPrice(item), s2.getLowestPrice(item)))
                .get();

Esimerkissä etsitään edullisin vaihtoehto kaikista vaihtoehdoista, jolle tehdään lopuksi jotain. Esimerkissä on kuitenkin ongelma: metodin get-kutsuminen Future-rajapinnan toteuttavalle oliolle jää odottamaan tuloksen valmistumista. Samalla myös pyyntöä käsittelevä säie on odotustilassa.

Ohjelman voisi rakentaa fiksummin. Entä jos sovellus lähettäisikin vastauksen selaimelle kun laskenta on valmis, mutta ei pakottaisi säiettä odottamaan vastausta? Eräs mahdollisuus on CompletableFuture-luokan käyttö, jonka avulla työn alla oleville tehtäville voidaan kertoa mitä pitää tehdä sitten kun laskenta on valmis. Tutustu aiheeseen tarkemmin osoitteessa http://www.deadcoderising.com/java8-writing-asynchronous-code-with-completablefuture/. Springin dokumentaatiossa löytyy myös aiheeseen liittyvää sisältöä.

Web Socketit

WebSocketit ovat tapa toteuttaa palvelimen ja selaimen välinen kommunikointi siten, että selain rekisteröityy palveluun, jonka jälkeen palvelin voi lähettää selaimelle dataa ilman uutta pyyntöä selaimelta. Rekisteröityminen tapahtuu sivulla olevan Javascriptin avulla, jonka jälkeen Javascriptiä käytetään myös palvelimelta tulevan tiedon käsittelyyn.

Spring tarjoaa komponentit Websockettien käyttöön. Määritellään ensin riippuvuudet projekteihin spring-boot-starter-websocket ja spring-messaging, jonka jälkeen luodaan tarvittavat konfiguraatiotiedostot.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
</dependency>
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // osoite, johon selain ottaa yhteyttä rekisteröityäkseen
        registry.addEndpoint("/register").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // osoite, jonka alla websocket-kommunikaatio tapahtuu
        registry.setApplicationDestinationPrefixes("/ws");
        // osoite, jonka kautta viestit kuljetetaan
        registry.enableSimpleBroker("/channel");
    }
}

Ylläolevan konfiguraation oleellisimmat osat ovat annotaatio @EnableWebSocketMessageBroker, joka mahdollistaa websocketien käytön sekä luo viestinvälittäjän. Konfiguraation osa registry.enableSimpleBroker("/channel"); luo polun, jota pitkin vastaukset lähetetään käyttäjälle ja registry.setApplicationDestinationPrefixes("/ws"); kertoo että sovelluksen polkuun /ws päätyvät viestit ohjataan viestinvälittäjälle. Näiden lisäksi rivi registry.addEndpoint("/register").withSockJS(); määrittelee STOMP-protokollalle osoitteen, mitä kautta palveluun voi rekisteröityä. Tässä lisäksi määritellään SockJS fallback-tuki, jota käytetään jos käyttäjän selain ei tue Websocketteja.

Selainpuolella käyttäjä tarvitsee sekä SockJS- että StompJS-kirjastot. Hyödynnämme CDN-verkostoja, jotka tarjoavat staattista sisältöä sivuille ilman tarvetta niiden omalla palvelimella säilyttämiselle. Esimerkiksi CDNJS-palvelu ehdottaa edellämainituille kirjastoille olemassaolevia osoitteita -- kirjastot voi luononllisesti pitää myös osana omaa sovellusta.

Kokonaisuudessaan selainpuolen toiminnallisuus on esimerkiksi seuraava -- alla body-elementin sisältö:

<p><input type="button" onclick="send();" value="Say Wut!"/></p>

<script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.1/sockjs.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

<script>
// luodaan asiakasohjelmisto, joka rekisteröityy osoitteeseen "/register"
var client = Stomp.over(new SockJS('/register'));

// kun käyttäjä painaa sivulla olevaa nappia, lähetetään osoitteeseen
// "/ws/messages" viesti "hello world!"
function send() {
    client.send("/ws/messages", {}, JSON.stringify({'message': 'hello world!'}));
}

// otetaan yhteys sovellukseen ja kuunnellaan "channel"-nimistä kanavaa
// -- jos kanavalta tulee viesti, näytetään JavaScript-alert ikkuna
client.connect({}, function (frame) {    
    client.subscribe('channel', function (response) {
        alert(response);
    });
});

// kun sivu suljetaan, yritetään sulkea yhteys palvelimelle
window.onbeforeunload = function () {
    client.disconnect();
}
</script>

Palvelinpuolella ohjelmistomme toimii esimerkiksi seuraavasti. Kuunnellaan osoitteeseen /ws/messages-tulevia pyyntöjä -- allaoleva esimerkki yrittää muuntaa automaattisesti pyynnössä tulevan JSON-datan Message-olioksi. Toisin kuin aiemmin, käytämme nyt @MessageMapping-annotaatiota, joka on viestinvälityksen vastine @RequestMapping-annotaatiolle.

@Controller
public class MessageController {
    
    // koska konfiguraatiossa määritelty "/ws"-juuripoluksi, vastaanottaa
    // tämä kontrollerimetodi osoitteeseen /ws/messages tulevat viestit
    @MessageMapping("/messages")
    public void handleMessage(Message message) throws Exception {
        // tee jotain
    }
}

Voimme lisäksi toteuttaa esimerkiksi palvelun, joka lähettää viestejä kaikille tiettyyn polkuun rekisteröityneille käyttäjille. Allaoleva palvelu lähettää viestin kerran kymmenessä sekunnissa.

@Service
public class MessageService {

    @Autowired
    private SimpMessagingTemplate template;

    @Scheduled(fixedDelay = 10000)
    public void addMessage() {
        Message message = new Message();
        // aseta viestin sisältö
        this.template.convertAndSend("/vastauskanava", message);
    }
}
            

Yllä käytetty SimpMessagingTemplate on hieman kuin aiemmin käyttämämme RestTemplate, mutta eri käyttötarkoitukseen.

Tutustu osoitteessa http://spring.io/guides/gs/messaging-stomp-websocket/ olevaan oppaaseen.

Tehtävässä on hahmoteltu chat-palvelua, jossa käyttäjä voi kirjautuessaan valita kanavan, mihin hän kirjoittaa viestejä. Tehtävään on hahmoteltu yksinkertainen kirjautuminen sekä käyttöliittymä, missä on toiminnallisuus yhteyden ottamiseen palvelimelle. Oletuskanavalla on myös jo yksi käyttäjä, joka lähettelee kanavalle viestejä.

Tutustu sovelluksen toimintaan, ja toteuta sovellukseen yksi uusi haluamasi toiminnallisuus. Se voi olla vaikkapa uusi viestejä lähettävä palvelu (helpohko), tai chatissa olevien henkilöiden listaaminen (haastava). Kerro tehtävän palautuksen yhteydessä kuvaus toteuttamastasi toiminnallisuudesta.

Kertausta

Kertauksen teemana on hieman isomman sovelluksen toteuttaminen alusta lähtien. Sovelluksen teemana on miniyhteisöpalvelu, missä käyttäjät näkevät kaveriensa viestejä; kavereiden viesteistä voidaan myös tykätä. Käytämme ulkoasupohjana netistä valmiiksi löytyvää ulkoasua. Tässä tapauksessa ulkoasumme on w3layouts-palvelun tarjoama Creative Commons Attribution 3.0 Unported-lisenssillä varustettu Cyan Flat UI KIT Responsive mobile web template. Käytännössä pohja tarjoaa nipun komponentteja, joita voimme hyödyntää osana sivumme rakentamista. Pohja käyttää myös valmiita Javascript- ja CSS-komponentteja, kuten jQuery ja Twitter Bootstrap. Termi "responsive" tarkoittaa sitä, että käyttöliittymä mukautuu sitä käyttävän laitteen näytön kokoon.

Ensimmäinen askel käyttöliittymän käyttöönotossa on valmiin paketin purkaminen siten, että sivun katsominen onnistuu kun palvelin käynnistetään. Ensimmäisessä tehtäväpohjassa tämä on tehty valmiiksi -- sivuun liittyvät erilaiset resurssit kuten Javascript-tiedostot ja kuvat on siirretty kansion static-alle, näkymän linkit on korjattu osoittamaan oikeisiin sijainteihin, ja näkymästä on luotu erillinen tiedosto template.html. Tämän lisäksi palvelinohjelmiston oletuskontrolleri ohjaa käyttäjän näkymään, missä tiedoston template.html sisältö näytetään.

Seuraavat tehtäväpohjat sisältävät aina edellisen tehtävän ratkaisun, jolloin voit hypätä joidenkin kohtien yli tarvittaessa. Tehtävissä ei ole testejä; palauta tehtävä aina kun saat sen onnistuneesti valmiiksi.

Lisätään tässä sivulle toiminnallisuus viestien listaamiseen ja lähettämiseen. Henkilöiden luomista tai kirjautumista ei vielä toteuteta. Palauttaessasi tehtävän vakuutat että toteutuksesi toimii tehtävänannon mukaisesti.

[Person|name (String)]1-*[Post|date (Date);title (String);content (String)]

Luo tehtävään ylläolevassa UML-kaaviossa kuvatut entiteetit Person ja Post, sekä niille sopivat Repository-rajapinnat. Lisää tämän jälkeen profiiliin DevProfile koodi, jolla saat sovellukseen sovelluksen käynnistyessä muutaman käyttäjän sekä heille viestejä (@PostConstruct-annotaatiosta on tässä hyötyä!).

Kun testidata on käytössä, luo template.html-tiedostosta kopio nimeltä index.html. Muokkaa luokkaa DefaultController siten, että pyyntö mihin tahansa osoitteeseen näyttää sivun index.html; lisää modeliin myös 10 uusinta viestiä, uusin ensimmäisenä.

Muokkaa tämän jälkeen sivua index.html siten, että viestit näkyvät siinä. Löytänet oikean alueen hakemalla tekstiä "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dapibus dui id libero auctor cursus.". Kun saat toteutuksen toimimaan, sinun pitäisi nähdä templaten sijaan lisäämäsi tekstit; viestialue voi näyttää esimerkiksi seuraavalta:

 

Kolme viestiä listattuna. Ensimmäisessä Anonymous -henkilö sanoo 'asdsada', toisessa Anonymous -henkilö ei sano mitään, ja kolmannessa Jack Reacher sanoo 'I'm not a vagrant. I'm a hobo. Big difference'. Viesteissä näkyy myös kellonajat.

 

Lisää tämän jälkeen erillinen kontrolleri, joka kuuntelee osoitteeseen /posts tulevia POST-tyyppisiä pyyntöjä. Kun kontrolleri vastaanottaa pyynnön, se tallentaa viestin ja ohjaa käyttäjän oletusnäkymään (missä haetaan kymmenen uusinta viestiä). Voit hakea esimerkiksi tietokannasta satunnaisen henkilön viestin lähettäjäksi.

Kun POST-pyyntöjä osoitteeseen /posts kuunteleva kontrolleri toimii, muokkaa index.html-tiedostossa olevaa viestikenttää seuraavanlaisesta

 

Lomake, missä otsikkona 'GET IN TOUCH', ja kenttinä nimi (Name), sähköposti (Email), puhelinnumero (Phone) ja viesti (Message). Lomakkeen lähetysnapissa teksti 'Send Message'.

 

seuraavanlaiseksi

 

Lomake, missä otsikkona 'WHAT'S GOING ON?', jonka lisäksi näkyy iso tekstialue, johon voi kirjoittaa viestin. Lomakkeen lähetysnapissa teksti 'Let 'Em Know!'.

 

Kun laatikkoon kirjoitetaan viesti ja "Let 'Em Know!"-nappia painetaan, tulee viesti lähettää POST-tyyppisellä pyynnöllä osoitteeseen /posts. Huomaa että tarvitset lomakkeen tietojen lähettämiseen -- jos palvelin valittaa jotain CORS-teemaista, varmista, että osa lomakkeesta tehdään Thymeleafin avulla (esim th:action="@{/posts}").

Lisätään tässä sivulle toiminnallisuus henkilöiden listaamiseen. Palauttaessasi tehtävän vakuutat että toteutuksesi toimii tehtävänannon mukaisesti.

[Person|name (String);slogan (String); lastUpdated (Date)]1-*[Post|date (Date);title (String);content (String)]

Lisää luokalle Person kentät lastUpdated ja slogan. Lastupdated pitää kirjaa viimeisimmästä muutoksesta; slogan taas on -- noh -- henkilön slogan.

Muokkaa tämän jälkeen oletuskontrolleria siten, että se lisää 10 viimeksi tietojaan päivittänyttä käyttäjää modeliin. Muokkaa sivua index.html siten, että sivulla oletuksena ollut henkilö "Zach Dunes" vaihtuu käyttäjälistaan.

 

Kuva, missä näkyy yksittäinen Henkilö 'Zach Dunes', teksti 'Lorem ipsum dolor sit amet, (jne)' ja nappi 'Profile'

 

Kun tehtävä on valmis, sivulla näkyy viimeisimmät käyttäjät ja heidän sloganit.

 

Kuva, missä kaksi henkilöä listattuna. Ensimmäisenä 'Jack Reacher', sloganina 'I know I'm smarter than an armadillo'. Toisena 'Jack Bauer', sloganina 'I'm federal agent Jack Bauer. This is the longest day of my life' Kummallekin on myös nappi, jossa lukee 'Profile'.

 

Tässä tehtävässä toteutetaan ensimmäinen versio uusien henkilöiden lisäämisestä.

Muokkaa tiedostoa index.html siten, että kopioit nykyisen kirjautumislomakkeen ja luot siitä erillisen "Sign up"-lomakkeen, minkä avulla voi luoda uuden käyttäjän. Lomake voi näyttää esimerkiksi seuraavanlaiselta

 

Kirjautumislomake. Otsikkona 'Not a member? Sign Up Now', jota seuraa kaksi tekstikenttää -- yksi nimelle ja yksi sloganille. Näitä seuraa nappi 'Sign up'.

 

Lisää tämän jälkeen sovellukseen erillinen kontrolleri, joka kuuntelee POST-tyyppisiä pyyntöjä osoitteeseen /persons; pyynnön pohjalta luodaan uusi käyttäjä. Muokkaa kontrolleria ja juuri luomaasi lomaketta siten, että niiden avulla voidaan luoda uusia käyttäjiä. Uuden käyttäjän lisäämisen tulee onnistua kirjoittamalla käyttäjän nimi ja slogan; käyttäjien lisäämiseen erikoistunut kontrolleri tallentaa käyttäjän ja ohjaa pyynnön oletuskontrollerille.

 

Kirjautumislomake. Otsikkona 'Not a member? Sign Up Now', jota seuraa kaksi tekstikenttää -- yksi nimelle ja yksi sloganille. Näitä seuraa nappi 'Sign up'. Tällä kertaa nimi-kenttään on täytetty esimerkin vuoksi nimi 'Horst von der Goltz' ja slogan 'Oldies but goodies!'

 

 

Tässä kuvalla vinkataan, että Horst olisi luotu järjestelmään. Edellisestä tehtävästä tuttu listaus henkilöitä, mutta tässä yksi henkilöistä 'Horst von der Goltz'.

 

Tässä tehtävässä sovellukseen lisätään autorisointi- ja autentikointitoiminnallisuus. Tämän lisäksi kirjautumissivu ja uuden käyttäjän luomiseen tarkoitettu sivu muokataan erillisiksi sivuiksi, sekä asetetaan viestien lähetys niin, että niillä on oikeat kirjoittajat.

[Person| name (String); slogan (String); lastUpdated (Date); username (String); password (String); salt (String)]1-*[Post|date (Date);title (String);content (String)]

Ennen alkua, lisää Person-entiteetille kentät username, password ja salt. Käyttäjätunnuksen tulee olla uniikki.

Sivujen eriyttäminen

Kopioi index.html-tiedostosta kaksi uutta versiota, toisen nimeksi tulee login.html ja toisen nimeksi signup.html. Luo tämän jälkeen oletuskontrolleriin kaksi kontrollerimetodia. Pyyntö osoitteeseen /login näyttää sivun login.html. Pyyntö osoitteeseen /signup näyttää sivun signup.html.

Muokkaa sivuja siten, että login.html näyttää seuraavalta:

 

Kirjautumislomake. Otsikkona 'Sign in', jonka lisäksi lomakkeessa kaksi kenttää: 'username' ja 'password'. Näiden lisäksi lomakkeeseen liittyvä nappi 'Sign in'. Lomakkeen alapuolella on teksti 'Sign Up Now ->', joka on linkki.

 

Kun sivulla klikkaa linkkiä "Sign up now", pääsee osoitetta /signup kuuntelevan kontrollerin kautta sivulle signup.html, joka näyttää seuraavalta:

 

Rekisteröitymislomake. Otsikkona 'Sign Up Form', jonka lisäksi lomakkeessa neljä kenttää: 'name', 'slogan', 'username' ja 'password'. Näiden lisäksi lomakkeeseen liittyvä nappi 'Sign up'.

 

Kirjautumissivun tulee vieläkin lähettää lomakkeelle syötettävät tiedot palvelimelle.

Tietoturvakonffit

Lue tässä välissä seuraavat sivut http://www.javacodegeeks.com/2012/08/bcrypt-salt-its-bare-minimum.html ja http://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/crypto/bcrypt/BCrypt.html.

Muokkaa Person-luokan setPassword-metodi seuraavanlaiseksi:

    public void setPassword(String password) {
        this.salt = BCrypt.gensalt();
        this.password = BCrypt.hashpw(password, this.salt);
    }

Jos et muista mitään salt-termistä tai miksi ylläoleva tehdään, lue esimerksi Wikipedian artikkeli Salt (cryptography).

Lisää tämän jälkeen projektiin Spring Security-riippuvuus, ja konfiguroi siihen liittyvä SecurityConfiguration-komponentti seuraavasti:

  • Kuka tahansa saa tehdä pyynnön osoitteisiin /login, /signup ja /static (sekä kaikkiin sen alla oleviin resursseihin). Kuka tahansa saa myös lähettää POST-tyyppisen pyynnön osoitteeseen /persons.
  • Sovelluksessa on kirjautumislomake, joka on osoitteessa /login -- kun kirjautuminen onnistuu, käyttäjä ohjataan oletuskontrollerin kuuntelemaan osoitteeseen.
  • Sovelluksessa on logout-toiminnallisuus, joka on osoitteessa /logout -- kun kirjautuminen onnistuu, käyttäjä ohjataan /login-osoitteeseen.
  • Kirjautumiseen käytetään alla annettua JpaAuthenticationProvider-luokkaa.
@Component
public class JpaAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private PersonRepository personRepository;

    @Override
    public Authentication authenticate(Authentication a) throws AuthenticationException {
        String username = a.getPrincipal().toString();
        String password = a.getCredentials().toString();

        Person person = personRepository.findByUsername(username);

        if (person == null) {
            throw new AuthenticationException("Unable to authenticate user " + username) {
            };
        }

        if (!BCrypt.hashpw(password, person.getSalt()).equals(person.getPassword())) {
            throw new AuthenticationException("Unable to authenticate user " + username) {
            };
        }

        List<GrantedAuthority> grantedAuths = new ArrayList<>();
        grantedAuths.add(new SimpleGrantedAuthority("USER"));

        return new UsernamePasswordAuthenticationToken(person.getUsername(), password, grantedAuths);
    }

    @Override
    public boolean supports(Class<?> type) {
        return true;
    }
}

Nyt sekä kirjautuminen käyttäjänä että käyttäjän luomisen pitäisi toimia.

Uloskirjautuminen

Spring Security olettaa, että uloskirjautumispyynnön mukana annetaan edellisen pyynnön vastaukseen generoitu satunnainen tunnus. Pelkkä GET-pyyntö osoitteeseen /logout ei toimi, sillä haluamme estää mahdollisia CSRF-hyökkäyksiä.

Haluamme kuitenkin että logout-nappi on linkki, joten teemme erillisen piilossa olevan lomakkeen, joka lähetetään linkkiä painamalla.

Muokkaa sivun oikeassa ylälaidassa olevaa logout-nappia siten, että se lähettää piilossa generoidun lomakkeen palvelimelle. Allaolevasta koodista lienee sinulle hyötyä.

<ul class="logout list-unstyled">
    <li><a href="#" onclick="document.getElementById('logout-form').submit();"><span> </span></a></li>
</ul>
<form style="visibility: hidden" id ="logout-form" method="post" action="#" th:action="@{/logout}"><input type="submit" value="Logout"/></form>

Nyt myös uloskirjautumisen pitäisi toimia.

Viestien lähetys oikealla nimellä

Kun käyttäjä on sivulla, hän on kirjautunut. Käyttäjätunnukseen pääsee käsiksi SecurityContextHolder-luokan kautta, joka pitää kirjaa kirjautuneesta käyttäjästä. Luokkaa käytetään seuraavasti:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication.getName()); // tulostaa käyttäjätunnuksen

Toteuta palvelu, jonka voi injektoida @Autowired-annotaation avulla mihin tahansa sovellukseen, ja jolta saa tällä hetkellä kirjautuneen käyttäjän tiedot Person-oliona.

Lisää tämän jälkeen osoitteeseen /posts-tehtäviä pyyntöjä kuuntelevaan kontrolleriin toiminnallisuus, missä uutta viestiä lisättäessä sille lisätään myös oikea kirjoittaja.

Nyt kun kirjaudut sovellukseen, voit kirjoittaa viestin siten, että viestin kirjoittajana näkyy käyttäjäsi nimi.

Tässä tehtävässä lisätään tykkäystoiminnallisuus viesteihin.

[Person]-*[Post]
[Person]-*[Like]
[Like]*-[Post]

Muokkaa ensin Post-entiteettiä siten, että sille luodaan satunnaisesti generoitu merkkijonotunnus. Luo tämän jälkeen entiteetti Like (kannattanee nimetä se jotenkin ettei tietokanta hermostu :)), joka kuvaa resurssiin liittyvää tykkäystä. Pidä resurssina merkkijonoa, joka viittaa esimerkiksi postin tunnukseen; jokainen tykkäys liittyy myös käyttäjään.

Tee tämän jälkeen kontrolleri, joka kuuntelee POST-tyyppisiä pyyntöjä osoitteeseen /likes. Pyynnön mukana tulee parametri resourceId, joka viittaa tykättävään resurssiin -- lisää myös jokaiseen tykkäykseen nykyinen käyttäjä, jonka saat edellisessä tehtävässä toteutetusta palvelusta. Uudelleenohjaa käyttäjä oletuskontrollerille tykkäyksen lisäämisen jälkeen.

Muokkaa tämän jälkeen oletuskontrolleria siten, että tietokannasta haetaan sivulla näytettäviin viesteihin liittyvät tykkäykset. Lisää tykkäykset modelin attribuutiksi nimeltä likes -- tykkäysten tulee olla Map, missä avain on resurssin tunnus ja arvo tykkäysten lukumäärä.

Toteuta tämän jälkeen käyttöliittymään viestikohtainen tykkäyslinkki -- alla olevasta koodista lienee hyötyä.

<form style="visibility: hidden" th:id="@{like-{id}(id=${post.id})}" method="post" action="#" th:action="@{/likes}">
    <input type="text" name="resourceId" th:value="${post.id}"></input>
    <input type="submit" value="Like"/>
</form>
<p>
    <a href="#" th:onclick="@{document.getElementById('like-{id}').submit();(id=${post.id})}">+1 like</a> 
    <span th:if="${likes[post.id] != null}">
        <span th:text="${likes[post.id]}">num</span> like<span th:if="${likes[post.id] > 1}">s</span>
    </span>
</p>

Kun tehtävä toimii, sivuilla näkyy tykkäysnapit sekä tehtyjen tykkäysten määrät.

 

Kuva, missä kaksi viestiä listattuna. Kummankin viestin yhteydessä on nyt linkki '+1 like', jota klikkaamalla viestistä voi tykätä. Tämän lisäksi viestissä, jota on tykätty, lukee tykkäysten lukumäärä (tykkäyslinkin vieressä).

 

Huom! Jos teet tulevaisuudessa tykkäystoiminnallisuutta, joskus puhdas selainpuolen toteutus on tarpeeksi -- tutustu esimerkiksi SocialiteJS-kirjastoon.

Tässä tehtävässä lisätään mahdollisuus kaverien lisäämiseen. Palauta se taas kokonaisuutena.

[Person]-*[Post]
[Person]-*[Like]
[Like]*-[Post]
[FriendshipRequest|status (Status)]-2[Person]
--pyyntöön kuuluu sekä pyynnön lähettäjä (source) ja pyynnön vastaanottaja (target)

Entiteetti ja kaveripyynnön lisäys

Luo entiteetti FriendshipRequest, jossa on Person-olio sekä lähteenä että kohteena (source & target). Tämän lisäksi entiteetillä on status, joka voi saada arvot Requested, Accepted, Rejected.

Kun entiteetti on olemassa, luo uusi kontrolleri, joka kuuntelee POST-tyyppisiä pyyntöjä osoitteeseen /friends. POST-tyyppinen pyyntö saa parametrina kentän muuttujan personId, joka on sen käyttäjän tunnus, kenelle kaveripyyntö tehdään. Tallenna pyyntö tietokantaan jos tietokannassa ei ole jo samaa pyyntöä tai vastaavaa pyyntöä, siten, että lähde ja kohde ovat vaihtuneet päittäin.

Muokkaa vielä käyttöliittymää siten, että käyttäjäprofiili näyttää seuraavalta -- listassa tulee näkyä vain ne käyttäjät, jotka eivät ole nykyisen käyttäjän kavereita.

 

Käyttäjän listaus, sama kuin profiileissa. Nyt käyttäjän 'Profile' -napin sijaan näkyy teksti 'Friend 'Em!', mitä klikkaamalla kirjautunut käyttäjä lähettää kohteelle kaveripyynnön.

 

Allaolevasta koodista lienee hyötyä.

<form method="POST" th:action="@{/friends}">
    <input type="hidden" name="personId" th:value="${user.id}"/>
    <input type="submit" class="p-btn" value="Friend 'em!"/>
</form>

Kaveripyyntöjen näyttäminen

Muokkaa oletuskontrolleria siten, että vastaukseen lisätään niiden kaveripyyntöjen lukumäärä, joiden status on Requested ja kohde nykyinen käyttäjä. Muokkaa tämän jälkeen sivua index.html siten, että sivun oikeassa ylälaidassa näytetään notifikaatio, vain jos käyttäjälle on kaveripyyntöjä.

 

Tässä näkyy (1) kuva pienellä -- nyt joku valmis kuva, ei siis tartte toteuttaa kuvatoiminnallisuutta, (2) notifikaatiokuva 'kello', ja (3) logout-nappi. Enpä oikein osaa selittää tätä paremmin..

 

Muokkaa notifikaatiota siten, että kun notifikaatiota klikataan, niin käyttäjä tekee GET-pyynnön osoitteeseen /friends.

Kaveripyyntöjen hyväksyminen

Muokkaa /friends-osoitetta kuuntelevaa kontrolleria siten, että se palauttaa käyttäjälle tehdyt kaveripyynnöt ja näyttää sivun friends.html

Tee tämän jälkeen sivusta index.html kopio friends.html ja muokkaa sitä siten, että se näyttää vain listan käyttäjiä. Muokkaa käyttäjälistaa siten, että jokaiselle käyttäjälle on nappi "Ok! I Like 'im!".

 

Täsmälleen sama kuva kuin aiemmin.

 

Kun nappia painetaan, käyttäjän pitäisi olla toisen käyttäjän kaveri.

Lisää tässä tehtävässä oma feature sovellukseen -- esimerkiksi mahdollisuus profiilikuvien tai muiden kuvien lisääminen.

Kuten huomaat, yhteisöpalvelumme on vielä kaukana valmiista -- ensiaskeleet on kuitenkin tehty. Ennenpitkää sovellus pilkottaisiin pienempiin palasiin, ja sovelluksen toiminta rakennettaisiin niin, että sivulle toteutettava JavaScript-komponentti hakisi sivun sisällön useammasta eri palvelusta. Palaamme tähän myöhemmin...