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.
Osa 6, kertaus: JokeVotes
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>
Hello CSS
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.
Chat 2010
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.
Users and Messages
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.
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:
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
seuraavanlaiseksi
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}"
).
Profiilit
Lisätään tässä sivulle toiminnallisuus henkilöiden listaamiseen. Palauttaessasi tehtävän vakuutat että toteutuksesi toimii tehtävänannon mukaisesti.
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.
Kun tehtävä on valmis, sivulla näkyy viimeisimmät käyttäjät ja heidän sloganit.
Uusia henkilöitä
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
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.
Kirjautuminen
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.
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:
Kun sivulla klikkaa linkkiä "Sign up now", pääsee osoitetta /signup
kuuntelevan kontrollerin kautta sivulle signup.html
, joka näyttää seuraavalta:
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.
Tykkääminen
Tässä tehtävässä lisätään tykkäystoiminnallisuus viesteihin.
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.
Huom! Jos teet tulevaisuudessa tykkäystoiminnallisuutta, joskus puhdas selainpuolen toteutus on tarpeeksi -- tutustu esimerkiksi SocialiteJS-kirjastoon.
Kaverit
Tässä tehtävässä lisätään mahdollisuus kaverien lisäämiseen. Palauta se taas kokonaisuutena.
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.
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ä.
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!".
Kun nappia painetaan, käyttäjän pitäisi olla toisen käyttäjän kaveri.
Oma feature
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...
the end.