Tehtävät
Viidennen osan tavoitteet

Ymmärtää REST-arkkitehtuurimallin perusperiaatteet. Osaa toteuttaa palvelun, joka tarjoaa dataa REST-muotoisen rajapinnan yli. Osaa toteuttaa palvelun, joka hyödyntää REST-rajapintaa. Tietää, että Javascript-koodia voi suorittaa selaimessa käyttäjän koneella. Osaa tehdä selainohjelmistosta Javascript-pyynnön palvelimelle. Osaa päivittää näkymää Javascript-pyynnön vastauksen perusteella.

REST-Arkkitehtuurimalli

REST (representational state transfer) on ohjelmointirajapintojen toteuttamiseen tarkoitettu arkkitehtuurimalli (tai tyyli). REST-malli määrittelee sovellukset tietoa käsittelevien osien (komponentit), tietokohteiden (resurssit), sekä näiden yhteyksien kautta.

Tietoa käsittelevät osat ovat selainohjelmisto, palvelinohjelmisto, ym. Resurssit ovat sovelluksen käsitteitä (henkilöt, kirjat, laskentaprosessit, laskentatulokset -- käytännössä mikä tahansa voi 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 esitysmuoto (esimerkiksi HTML, JSON tai XML); dataa voi tyypillisesti 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.

Principled Design of the Modern Web Architecture

Tutustu Roy T. Fieldingin ja Richard N. Taylorin artikkeliin "Principled Design of the Modern Web Architecture", jossa REST määritellään sekä Fieldingin väitöskirjan viidenteen lukuun. Vaikka emme tässä kappaleessa täytä kaikkia REST-rajapintoihin liittyviä vaatimuksia -- ainakaan aluksi -- on Roy Fielding sitä mieltä, että oleellista on mahdollisuus resurssien välillä navigointiin.

"A truly RESTful API looks like hypertext. Every addressable unit of information carries an address, either explicitly (e.g., link and id attributes) or implicitly (e.g., derived from the media type definition and representation structure). Query results are represented by a list of links with summary information, not by arrays of object representations (query is not a substitute for identification of resources)."

REST-rajapinnat web-sovelluksissa

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

  • Juuriosoite resurssien käsittelyyn (esimerkiksi /books)
  • Resurssien esitysmuodon määrittelevä mediatyyppi (esimerkiksi HTML, JSON, ...), joka kertoo asiakkaalle miten resurssiin liittyvä data tulee käsitellä.
  • Resursseja voidaan käsitellä HTTP-protokollan metodeilla (GET, POST, DELETE, ..)

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

  • GET osoitteeseen /books palauttaa kaikkien kirjojen tiedot.
  • GET osoitteeseen /books/{id}, missä {id} on yksittäisen kirjan yksilöivä tunniste, palauttaa kyseisen kirjan tiedot.
  • PUT osoitteeseen /books/{id}, missä {id} on yksittäisen kirjan yksilöivä tunniste, muokkaa kyseisen kirjan tietoja. Kirjan uudet tiedot lähetetään osana pyyntöä.
  • DELETE osoitteeseen /books/{id} poistaa kirjan tietyllä tunnuksella.
  • POST osoitteeseen /books luo uuden kirjan pyynnössä lähetettävän datan pohjalta.

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, ja GET-tyyppisellä pyynnöllä haetaan tietoja.

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:

// pakkaus ja importit 

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Book extends AbstractPersistable<Long> {
    private String 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 (esimerkiksi) 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 muuntaminen olioksi tapahtuu annotaation @RequestBody avulla. Annotaatio @RequestBody edeltää kontrollerimetodin parametrina olevaa oliota, johon JSON-muotoisen datan arvot halutaan asettaa.

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

@GetMapping("/books/{id}")
@ResponseBody
public Book getBook(@PathVariable Long id) {
    return bookRepository.getOne(id);
}

Alla oleva esimerkki sekä tallentaa olion tietokantaan, että palauttaa tietokantaan tallennetun olion pääavaimineen.

@PostMapping("/books")
@ResponseBody
public Book postBook(@RequestBody Book book) {
    return bookRepository.save(book);
}

Palvelulle voi nyt lähettää JSON-muotoista dataa; vastaus on myös JSON-muotoinen, mutta luotavaan kirjaan on liitetty tietokantaan tallennuksen yhteydessä saatava kirjan yksilöivä tunnus.

Voimme lisätä annotaatioille @GetMapping, @PostMapping, jne lisätietoa metodin käsittelemästä 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.

@PostMapping(path="/books", consumes="application/json", produces="application/json")
@ResponseBody
public Book postBook(@RequestBody Book book) {
    return bookStorage.create(book);
}

Jos toteutat omaa REST-rajapintaa, kannattanee 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
public class BookController {

    @Autowired
    private BookRepository bookRepository;

    @GetMapping("/books")
    public List<Book> getBooks() {
        return bookRepository.findAll();
    }

    @GetMapping("/books/{id}")
    public Book getBook(@PathVariable Long id) {
        return bookRepository.getOne(id);
    }

    @DeleteMapping("/books/{id}")
    public Book deleteBook(@PathVariable Long id) {
        return bookRepository.deleteById(id);
    }    

    @PostMapping("/books")
    public Book postBook(@RequestBody Book book) {
        return bookRepository.save(book);
    }
}
Apuvälineitä rajapinnan tarjoavan sovelluksen testaamiseen

Avoimen rajapinnan kolmannen osapuolen ohjelmistoille tarjoavat palvelinohjelmistot eivät aina sisällä erillistä käyttöliittymää. Niiden testaaminen tapahtuu sekä automaattisilla testeillä, että erilaisilla selainohjelmistoilla. Yksi hyvin hyödyllinen apuväline on Postman, jonka saa työpöytäsovelluksena ja Chromen liitännäisenä.

Postmanin hyödyntäminen on erittäin suositeltavaa -- kannattaa katsoa sen johdatusvideo, joka löytyy Postmanin sivulta. Katso myös RESTiä käsittelevä Youtube-video, missä Postmania käytetään hieman.

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
}

Alla on kuvattuna RestTemplaten käyttö tiedon hakemiseen, päivittämiseen, poistaamiseen ja lisäämiseen. Esimerkeissä merkkijono osoite vastaa palvelimen osoitetta, esimerkiksi http://www.google.com.

  • GET osoitteeseen /books palauttaa kaikkien kirjojen tiedot tai osajoukon kirjojen tiedoista -- riippuen toteutuksesta.
  • // kirjojen hakeminen
    List<Book> books = restTemplate.getForObject("osoite/books", List.class);
    
  • GET osoitteeseen /books/{id}, missä {id} on yksittäisen kirjan yksilöivä tunniste, palauttaa kyseisen kirjan tiedot.
  • // tunnuksella 5 määritellyn kirjan hakeminen
    Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5);
    
  • PUT osoitteeseen /books/{id}, missä {id} on yksittäisen kirjan yksilöivä tunniste, muokkaa kyseisen kirjan tietoja tai lisää kirjan kyseiselle tunnukselle (toteutuksesta riippuen, lisäystä ei aina toteutettu). Kirjan tiedot lähetetään pyynnön rungossa.
  • // tunnuksella 5 määritellyn kirjan hakeminen
    Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5);
    book.setName(book.getName() + " - DO NOT BUY!");
    
    // kirjan tietojen muokkaaminen
    restTemplate.put("osoite/books/{id}", book, 5);
    
  • DELETE osoitteeseen /books/{id} poistaa kirjan tietyllä tunnuksella.
  •  // tunnuksella 32 määritellyn kirjan poistaminen
    restTemplate.delete("osoite/books/{id}", 32);
    
  • POST osoitteeseen /books luo uuden kirjan pyynnön rungossa lähetettävän datan pohjalta. Palvelun vastuulla on päättää kirjalle tunnus.
  • Book book = new Book();
    book.setName("Harry Potter and the Goblet of Fire");
    
    // uuden kirjan lisääminen
    book = restTemplate.postForObject("osoite/books", book, Book.class);
    

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ää ScoreService-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 ScoreService kuvausta tämän tehtävän toteutuksessa.

Huom! Valmis ScoreService-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ä.

Ja asiaan..

Tee luokka wad.service.GameRestClient, joka toteuttaa rajapinnan GameService. Luokan tulee käyttää ScoreService-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 ScoreService-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 ScoreService-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

Jos osoitteessa http://wepa-scoreservice-heroku.herokuapp.com/games olevan palvelun tietokanta on "täynnä", voi tietokannan tyhjentää tekemällä kutsun osoitteeseen http://wepa-scoreservice-heroku.herokuapp.com/games.

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

Roy Fielding kokee vain tason 3 sovelluksen 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.

Usein sovelluksemme kuitenkin toimivat jo palvelun juuripalvelussa, ja haluemme 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.

Käytännössä sovelluksen kehittäjä ei kuitenkaan 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();
}
Avoin data

Verkko on täynnä avoimia (ja osittain avoimia) ohjelmointirajapintoja, jotka odottavat niiden hyödyntämistä. Tällaisia kokoelmia löytyy muunmuassa osoitteista https://www.avoindata.fi/fi, https://data.europa.eu/euodp/en/home, https://index.okfn.org/dataset/, https://github.com/toddmotto/public-apis, jne.

CORS: Rajoitettu pääsy resursseihin

Palvelinohjelmiston tarjoamiin tietoihin kuten kuviin ja videoihin pääsee käsiksi lähes mistä tahansa palvelusta. Palvelinohjelmiston toiminnallisuus voi rakentua toisen palvelun päälle. On myös mahdollista toteuttaa sovelluksia siten, että ne koostuvat pääosin selainpuolen kirjastoista, jotka hakevat tietoa palvelimilta.

Selainpuolella Javascriptin avulla tehdyt pyynnöt ovat oletuksena rajoitettuja. Jos palvelimelle ei määritellä erillistä CORS-tukea, eivät sovelluksen osoitteen ulkopuolelta tehdyt Javascript pyynnöt onnistu. Käytännössä siis, jos käyttäjä on sivulla hs.fi, selaimessa toimiva Javascript-ohjelmisto voi tehdä pyyntöjä vain osoitteeseen hs.fi.

Palvelinohjelmistot määrittelevät voiko niihin tehdä pyyntöjä myös palvelimen osoitteen ulkopuolelta ("Cross-Origin Resource Sharing"-tuki). Yksinkertaisimmillaan CORS-tuen saa lisättyä palvelinohjelmistoon lisäämällä kontrollerimetodille annotaatio @CrossOrigin. Annotaatiolle määritellään osoitteet, joissa sijaitsevista osoitteista pyyntöjä saa tehdä.

@CrossOrigin(origins = "/**")
@GetMapping("/books")
@ResponseBody
public List<Book> getBooks() {
    return bookRepository.findAll();
}

Koko sovelluksen tasolla vastaavan määrittelyn voi tehdä erillisen konfiguraatiotiedoston avulla.

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
	registry.addMapping("/**");
    }
}

Nyt sovellukseen voi tehdä Javascript-pyynnön missä tahansa sijaitsevasta sovelluksesta.

Selainohjelmistot

Tutustutaan seuraavaksi selainpuolen toiminnallisuuden peruspalasiin.

Web-sivujen rakenne

Web-sivut määritellään HTML-kielen avulla. Yksittäinen HTML-dokumentti koostuu sisäkkäin ja peräkkäin olevista elementeistä, jotka määrittelevät sivun rakenteen sekä sivun sisältävän tekstin. Rakenteen määrittelevät elementit erotellaan pienempi kuin (<) ja suurempi kuin (>) -merkeillä. Elementti avataan elementin nimen sisältävällä pienempi kuin -merkillä alkavalla ja suurempi kuin -merkkiin loppuvalla merkkijonolla, esim. <html>, ja suljetaan merkkijonolla jossa elementin pienempi kuin -merkin jälkeen on vinoviiva, esim </html>. Yksittäisen elementin sisälle voi laittaa muita elementtejä.

Tyypillisen HTML-dokumentin runko näyttää seuraavalta. Kun klikkaat allaolevassa iframe-elementissä Result-tekstiä, näet HTML-sivun, ja kun painat HTML-tekstiä, näet HTML-koodin. Klikkaamalla elementin oikeassa ylälaidassa olevasta Edit in JSFiddle-linkistä, pääset muokkaamaan elementtiä suoraan JSFiddlessä.

 

Yllä olevassa HTML-dokumentissa on dokumentin tyypin kertova erikoiselementti <!DOCTYPE html>, joka kertoo dokumentin olevan HTML-sivu. Tätä seuraa elementti <html>, joka aloittaa HTML-dokumentin. Elementti <html> sisältää yleensä kaksi elementtiä, elementit <head> ja <body>. Elementti <head> sisältää sivun otsaketiedot, eli esimerkiksi sivun käyttämän merkistön <meta charset="utf-8" /> ja otsikon <title>. Elementti <body> sisältää selaimessa näytettävän sivun rungon. Ylläolevalla sivulla on ensimmäisen tason otsake-elementti h1 (header 1) ja tekstielementti p (paragraph).

Elementit voivat sisältää tekstisolmun. Esimerkiksi yllä olevat elementit title, h1 ja p kukin sisältävät tekstisolmun eli tekstiä. Tekstisolmulle ei ole erillistä elementtiä tai määrettä, vaan se näkyy käyttäjälle sivulla olevana tekstinä.

Puhe tekstisolmuista antaa viitettä jonkinlaisesta puurakenteesta. HTML-dokumentit ovat rakenteellisia dokumentteja, joiden rakenne on usein helppo ymmärtää puumaisena kaaviona. Ylläolevan web-sivun voi esittää esimerkiksi seuraavanlaisena puuna.

                   html

               /          \

             /              \

          head              body

        /       \         /      \

     meta       title     h1      p

                 :        :       :

              tekstiä  tekstiä tekstiä

Koska HTML-dokumentti on rakenteellinen dokumentti, on elementtien sulkemisjärjestyksellä väliä. Elementit tulee sulkea samassa järjestyksessä kuin ne on avattu. Esimerkiksi, järjestys <body><p>whoa, minttutee!</body></p> on väärä, kun taas järjestys <body><p>whoa, minttutee!</p></body> on oikea.

Kaikki elementit eivät kuitenkaan sisällä tekstisolmua, eikä niitä suljeta erikseen. Yksi näistä poikkeuksista on link-elementti.

Kun selaimet lataavat HTML-dokumenttia ja muodostavat sen perusteella muistissa säilytettävää puuta, ne käyvät sen läpi ylhäältä alas, vasemmalta oikealle. Kun selain kohtaa elementin, se luo sille uuden solmun. Seuraavista elementeistä luodut solmut menevät aiemmin luodun solmun alle kunnes aiemmin kohdattu elementti suljetaan. Aina kun elementti suljetaan, puussa palataan ylöspäin edelliselle tasolle.

Elementit, attribuutit, nimet ja luokat

Elementit voivat sisältää attribuutteja, joilla voi olla yksi tai useampi arvo. Edellä nähdyssä HTML-dokumentissa elementille meta on määritelty erillinen attribuutti charset, joka kertoo dokumentissa käytettävän merkistön: "utf-8". Vastaavasti tiedon syöttämiseen käytettävien lomakkeiden input ym. kentissä käyttämämme attribuutti name määrittelee nimen, jota käytetään palvelimelle lähetettävän kentän sisällön tunnistamisessa.

Muita yleisesti käytettäviä attribuuttityyppejä ovat id, joka määrittelee elementille uniikin tunnisteen sekä class, jonka avulla elementille voidaan määritellä tyyppiluokitus. Uudehkossa HTML5-määritelmässä elementit voivat sisältää myös data-attribuutteja, joiden toiminnallisuutta ei ole ennalta määritelty, ja joita käytetään tyypillisesti sovelluksen toiminnallisuuden takaamiseksi.

Kun elementtejä haetaan id-attribuutin perusteella, vastaukseksi pitäisi tulla tyypillisesti vain yksi elementti, mutta class-attribuutin perusteella hakuvastauksia voi olla useampi.

Lista attribuuteista

W3Schools-sivusto sisältää hyvän yhteenvedon käytössä olevista attribuuteista: http://www.w3schools.com/tags/ref_attributes.asp. Lisätietoa data-attribuuteista löytyy osoitteesta http://www.w3schools.com/tags/att_global_data.asp.

Javascript

Siinä missä HTML on kieli web-sivujen rakenteen ja sisällön luomiseen, JavaScript on kieli dynaamisen toiminnan lisäämiselle. JavaScript on ohjelmakoodia, jota suoritetaan komento kerrallaan -- ylhäältä alas, vasemmalta oikealle.

JavaScript-koodi suoritetaan käyttäjän omassa selaimessa. Samalla on hyvä kuitenkin mainita, että nykyään myös palvelinohjelmistoja ohjelmoidaan Javascriptillä -- tästä esimerkkinä NodeJs.

JavaScript-tiedoston pääte on yleensä .js ja siihen viitataan elementillä script. Elementillä script on attribuutti src, jolla kerrotaan lähdekooditiedoston sijainti. Kun lisäämme Javascript-koodia web-projektiimme, lisätään se tyypillisesti kansion src/main/resources/public/javascript/ alle. Kansiossa public olevat tiedostot siirtyvät suoraan näkyville web-maailmaan, joten niitä ei tarvitse käsitellä erikseen esimerkiksi Thymeleaf-moottorin toimesta.

Jos lähdekoodi on kansiossa javascript olevassa tiedostossa code.js, käytetään script-elementtiä seuraavasti: <script th:src="@{/javascript/code.js}"></script>.

Yleinen käytänne JavaScript-lähdekoodien sivulle lisäämiseen on lisätä ne sivun loppuun juuri ennen body-elementin sulkemista. Tämä johtuu mm. siitä, että selain lähtee hakemaan JavaScript-tiedostoa kun se kohtaa sen määrittelyn HTML-dokumentissa, jolloin kaikki muut toiminnot odottavat latausta. Jos lähdekooditiedosto ladataan vasta sivun lopussa, käyttäjälle näytetään sivun sisältöä jo ennen Javascript-lähdekoodin latautumista, sillä selaimet usein näyttävät sivua käyttäjälle sitä mukaa kun se latautuu. Tällä luodaan tunne nopeammin reagoivista ja latautuvista sivuista.

Määre defer siirtää lataamisen sivun loppuun

Nykyään script-elementille voi lisätä määreen defer, jonka olemassaolo kertoo että elementin src-attribuutin määrittelemä tiedosto tulee suorittaa vasta kun html-sivu on käsitelty.

...
<script th:src="@{/javascript/code.js}" defer></script>
...

Defer-määre on kuitenkin uudehko lisä, eikä se toimi kaikissa selaimissa. Lisätietoa täältä...

Luodaan kansioon javascript lähdekooditiedosto code.js. Tiedostossa code.js on funktio sayHello. Funktio luo ponnahdusikkunan, missä on teksti "hello there".

function sayHello() {
    alert("hello there");
}

HTML-dokumentti, jossa lähdekooditiedosto ladataan, näyttää seuraavalta. Attribuutille onclick määritellään elementin klikkauksen yhteydessä suoritettava koodi.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Sivun otsikko (näkyy selaimen palkissa)</title>
    </head>
    <body>
        <header>
            <h1>Sivulla näkyvä otsikko</h1>
        </header>

        <article>
            <p>Sivuilla näytettävä normaali teksti on p-elementin sisällä. Alla on nappi,
            jota painamalla kutsutaan funktiota "sayHello".</p>
            <input type="button" value="Tervehdi" onclick="sayHello();" />
        </article>

        <!-- ladataan JavaScript-koodit tiedoston lopussa! -->
        <script th:src="@{javascript/code.js}"></script>

    </body>
</html>

Alla sama JSFiddlessä -- siellä kuitenkin code.js samassa kansiossa HTML-tiedoston kanssa:

Javascript-oppaita

Jos Javascript ei ole ennalta tuttu kieli, kannattaa tutustua W3Schools-sivuston tarjoamaan Javascript-oppaaseen sekä kurssin Web-selainohjelmointi (jo hieman hapantuneeseen) materiaaliin.

Sivujen rakenteen muokkaaminen Javascriptin avulla

JavaScriptiä käytetään ennenkaikkea dynaamisen toiminnallisuuden lisäämiseksi web-sivuille. Esimerkiksi web-sivuilla oleviin elementteihin tulee pystyä asettamaan arvoja, ja niitä tulee myös pystyä hakemaan. JavaScriptissä pääsee käsiksi dokumentissa oleviin elementteihin komennolla document.getElementById("tunnus"), joka palauttaa elementin, jonka id-attribuutti on "tunnus". Muita attribuutti- ja elementtityyppejä pääsee käsittelemään esimerkiksi querySelector-metodin avulla.

Alla on tekstikenttä, jonka HTML-koodi on <input type="text" id="tekstikentta"/>. Kentän tunnus on siis tekstikentta. Jos haluamme päästä käsiksi elementtiin, jonka tunnus on "tekstikentta", käytämme komentoa document.getElementById("tekstikentta"). Tekstikenttäelementillä on attribuutti value, joka voidaan tulostaa.

Tekstikentälle voidaan asettaa arvo kuten muillekin muuttujille. Alla olevassa esimerkissä haetaan edellisen esimerkin tekstikenttä, ja asetetaan sille arvo 5.

Tehdään vielä ohjelma, joka kysyy käyttäjältä syötettä, ja asettaa sen yllä olevan tekstikentän arvoksi.

Arvon asettaminen osaksi tekstiä

Yllä tekstikentälle asetettiin arvo sen value-attribuuttiin. Kaikilla elementeillä ei ole value-attribuuttia, vaan joillain näytetään niiden elementin sisällä oleva arvo. Elementin sisälle asetetaan arvo muuttujaan liittyvällä attribuutilla innerHTML.

Alla olevassa esimerkissä sivulla on tekstielementti, jossa ei ole lainkaan sisältöä. Jos tekstielementtiin lisätään sisältöä, tulee se näkyville.

Vastaavasti tekstin keskelle -- sisäelementtiin -- voi asettaa arvoja. Elementti span sopii tähän hyvin.

Case: Laskin

Luodaan laskin. Laskimella on kaksi toiminnallisuutta: pluslasku ja kertolasku. Luodaan ensin laskimelle javascriptkoodi, joka on tiedostossa laskin.js. Javascript-koodissa oletetaan, että on olemassa input-tyyppiset elementit tunnuksilla "eka" ja "toka" sekä span-tyyppinen elementti tunnuksella "tulos". Funktiossa plus haetaan elementtien "eka" ja "toka" arvot, ja asetetaan pluslaskun summa elementin "tulos" arvoksi. Kertolaskussa tehdään lähes sama, mutta tulokseen asetetaan kertolaskun tulos. Koodissa on myös apufunktio, jota käytetään sekä arvojen hakemiseen annetuilla tunnuksilla merkityistä kentistä että näiden haettujen arvojen muuttamiseen numeroiksi.

function haeNumero(tunnus) {
    return parseInt(document.getElementById(tunnus).value);
}

function asetaTulos(tulos) {
    document.getElementById("tulos").innerHTML = tulos;
}

function plus() {
    asetaTulos(haeNumero("eka") + haeNumero("toka"));
}

function kerto() {
    asetaTulos(haeNumero("eka") * haeNumero("toka"));
}

Laskimen käyttämä HTML-dokumentti näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Laskin</title>
    </head>
    <body>
        <header>
            <h1>Plus- ja Kertolaskin</h1>
        </header>

        <section>
            <p>
                <input type="text" id="eka" value="0" />
                <input type="text" id="toka" value="0" />
            </p>

            <p>
                <input type="button" value="+" onclick="plus();" />
                <input type="button" value="*" onclick="kerto();" />
            </p>


            <p>Laskimen antama vastaus: <span id="tulos"></span></p>
        </section>

        <script src="javascript/laskin.js"></script>
    </body>
</html>

Kokonaisuudessaan laskin näyttää seuraavalta:

 

Toteuta edellisen esimerkin perusteella laskin, jossa on plus-, miinus-, kerto- ja jakolaskutoiminnallisuus. Keskity vain selainpuolen toiminnallisuuteen: älä muokkaa palvelinpuolen toiminnallisuutta. Varmista myös, että sivu on käytettävä ilman erillistä ohjetekstiä, eli että käyttämäsi napit ja tekstit kertovat käyttäjälle kaiken oleellisen.

Tehtävään ei ole TMC:ssä testejä -- kun sovellus toimii oikein, lähetä se palvelimelle.

Elementtien valinta

Käytimme getElementById-kutsua tietyn elementin hakemiseen. Kaikki sivun elementit voi taas hakea esimerkiksi getElementsByTagName("*")-kutsulla. Molemmat ovat kuitenkin hieman kömpelöjä jos tiedämme mitä haluamme hakea.

W3C DOM-määrittely sisältää myös paremman ohjelmointirajapinnan elementtien läpikäyntiin. Selectors API sisältää mm. querySelector-kutsun, jolla saadaan CSS-valitsinten kaltainen kyselytoiminnallisuus.

Selector APIn tarjoamien querySelector (yksittäisen osuman haku) ja querySelectorAll (kaikkien osumien haku) -komentojen avulla kyselyn rajoittaminen vain header-elementissä oleviin a-elementteihin on helppoa.

var linkit = document.querySelectorAll("nav a");
// linkit-muuttuja sisältää nyt kaikki a-elementit, jotka ovat nav-elementin sisällä

Vastaavasti header-elementin sisällä olevat linkit voi hakea seuraavanlaisella kyselyllä.

var linkit = document.querySelectorAll("header a");
// linkit-muuttuja sisältää nyt kaikki a-elementit, jotka ovat header-elementin sisällä

Elementtien lisääminen

HTML-dokumenttiin lisätään uusia elementtejä document-olion createElement-metodilla. Esimerkiksi alla luodaan p-elementti (tekstisolmu; createTextNode), joka asetetaan muuttujaan tekstiElementti. Tämän jälkeen luodaan tekstisolmu, joka sisältää tekstin "o-hai". Lopulta tekstisolmun lisätään tekstielementtiin.

var tekstiElementti = document.createElement("p");
var tekstiSolmu = document.createTextNode("o-hai");

tekstiElementti.appendChild(tekstiSolmu);

Ylläoleva esimerkki ei luonnollisesti muuta HTML-dokumentin rakennetta sillä uutta elementtiä ei lisätä osaksi HTML-dokumenttia. Olemassaoleviin elementteihin voidaan lisätä sisältöä elementin appendChild-metodilla. Alla olevan tekstialue sisältää article-elementin, jonka tunnus on dom-esim-3. Voimme lisätä siihen elementtejä elementin appendChild-metodilla.

var tekstiElementti = document.createElement("p");
var tekstiSolmu = document.createTextNode("o-noes!");

tekstiElementti.appendChild(tekstiSolmu);

var alue = document.getElementById("dom-esim-3");
alue.appendChild(tekstiElementti);

Artikkelielementin sekä sen sisältämien tekstielementtien lisääminen onnistuu vastaavasti. Alla olevassa esimerkissä käytössämme on seuraavanlainen section-elementti.

<!-- .. dokumentin alkuosa .. -->
    <section id="osio"></section>
<!-- .. dokumentin loppuosa .. -->

Uusien artikkelien lisääminen onnistuu helposti aiemmin näkemällämme createElement-metodilla.

var artikkeli = document.createElement("article");

var teksti1 = document.createElement("p");
teksti1.appendChild(document.createTextNode("Lorem ipsum... 1"));
artikkeli.appendChild(teksti1);

var teksti2 = document.createElement("p");
teksti2.appendChild(document.createTextNode("Lorem ipsum... 2"));
artikkeli.appendChild(teksti2);

document.getElementById("osio").appendChild(artikkeli);

Alla olevassa esimerkissä elementtejä lisätään yksitellen. Mukana on myös laskuri, joka pitää kirjaa elementtien lukumäärästä.

jQuery

jQuery on JavaScript-kirjasto, jonka tavoitteena on helpottaa selainohjelmistojen toteutusta. Se tarjoaa apuvälineitä mm. DOM-puun muokkaamiseen, tapahtumien käsittelyyn sekä palvelimelle tehtävien kyselyiden toteuttamiseen, ja sen avulla toteutettu toiminnallisuus toimii myös useimmissa selaimissa.

Uusimman jQuery-version saa ladattua täältä. Käytännössä jQuery on JavaScript-tiedosto, joka ladataan sivun latautuessa. Tiedoston voi asettaa esimerkiksi head-elementin sisään, tai ennen omia lähdekooditiedostoja.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
     </head>
    <body>

        <!-- sivun sisältö -->

        <script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
        <script src="javascript/koodi.js"></script>
    </body>
</html>

Valitsimet

Käytimme edellisissä osioissa valmiita JavaScriptin DOM-toiminnallisuuksia. Elementtien etsimiseen on käytetty mm. getElementById-kutsua. JQuery käyttää Sizzle-kirjastoa elementtien valinnan helpottamiseen. Esimerkiksi elementti, jonka attribuutin "id" arvo on "nimi", löytyy seuraavalla komennolla.

var elementti = $("#nimi");

Kyselyt ovat muotoa$("kysely"). Jos elementtia haetaan id-attribuutin perusteella, lisätään kyselyn alkuun risuaita. Jos elementtiä haetaan luokan (class) perusteella, lisätään kyselyn alkuun piste. Jos taas elementtiä halutaan hakea esimerkiksi nimen perusteella, muodostetaan kysely sekä elementin että attribuutin kautta, esim. $("input[name=nimi]") palauttaa kaikki input-tyyppiset elementit, joissa name-attribuutin arvo on nimi.

jQueryn valitsimet

Tarkempi kuvaus jQueryn valitsimista löytyy osoitteesta http://api.jquery.com/category/selectors/.

Elementtien lisääminen

JQuery tekee elementtien lisäämisestä hieman suoraviivaisempaa. Voimme kutsun document.createElement sijaan määritellä elementin tyypin sanomalla $("<article />");. Myös tekstielementin luominen on hieman helpompaa: $("<p/>").text("test");. Aiempi koodimme:

var artikkeli = document.createElement("article");

var teksti1 = document.createElement("p");
teksti1.appendChild(document.createTextNode("Lorem ipsum... 1"));
artikkeli.appendChild(teksti1);

var teksti2 = document.createElement("p");
teksti2.appendChild(document.createTextNode("Lorem ipsum... 2"));
artikkeli.appendChild(teksti2);

document.getElementById("osio").appendChild(artikkeli);

Voidaan kirjoittaa myös hieman suoraviivaisemmin:

var artikkeli = $("<article/>");

var teksti1 = $("<p/>");
teksti1.text("Lorem ipsum... 1");
artikkeli.append(teksti1);

var teksti2 = $("<p/>");
teksti2.text("Lorem ipsum... 2");
artikkeli.append(teksti2);

$("#osio").append(artikkeli);
DOM-puun muokkaus

Tarkempi kuvaus operaatioista DOM-puun muokkaamiseen löytyy osoitteesta http://api.jquery.com/category/Manipulation/.

Tapahtumien käsittely

JQuery rakentaa JavaScriptin valmiiden komponenttien päälle, joten sillä on toiminnallisuus myös tapahtumankäsittelijöiden rekisteröimiseen sivun komponenteille. Eräs hyvin hyödyllinen tapahtumankäsittelijä liittyy sivun latautumiseen: komennolla $(document).ready(function() {}); voidaan määritellä funktion runko, joka suoritetaan kun sivun latautuminen on valmis.

Kun sivun latautuminen on valmis, voimme olla varmoja siitä, että sivulla on kaikki siihen kuuluvat elementit. Tällöin on näppärää tehdä myös kyselyjä palvelimelle. Jos haluaisimme että id-attribuutin arvolla "osio" määriteltyyn elementtiin lisättäisiin kaksi tekstielementtiä sisältävä artikkelielementti kun sivu on latautunut, olisi tarvittava Javascript-koodi seuraavanlainen:

$(document).ready(function() {
  var artikkeli = $("<article/>");

  var teksti1 = $("<p/>");
  teksti1.text("Lorem ipsum... 1");
  artikkeli.append(teksti1);

  var teksti2 = $("<p/>");
  teksti2.text("Lorem ipsum... 2");
  artikkeli.append(teksti2);

  $("#osio").append(artikkeli);
});
JSON

JSON, eli JavaScript Object Notation, on tiedon esitysmuoto. Olion määrittely alkaa aaltosululla {, jota seuraa muuttujan nimi ja sille annettava arvo. Arvon asetus oliomuuttujalle tapahtuu kaksoispisteellä, esimerkiksi nimi: "Arvo". Useampia muuttujia voi määritellä pilkulla eroteltuna. Olion määrittely lopetetaan sulkevaan aaltosulkuun }.

var olio = {nimi: "Arvo", tieto: 2000};

Olion muuttujiin pääsee käsiksi piste-notaatiolla. Esimerkiksi olio-olion muuttuja nimi löytyy komennolla olio.nimi.

var olio = {nimi: "Arvo", tieto: 2000};
alert(olio.nimi);

Myös uusien oliomuuttujien lisääminen on suoraviivaista. Uuden muuttujan lisääminen tapahtuu myös pistenotaatiolla -- harrastuksen lisääminen tapahtuu olio-oliolle sanomalla olio.harrastus = "koodaus";.

var olio = {nimi: "Arvo", tieto: 2000};
alert(olio.nimi);
olio.harrastus = "koodaus";
alert(olio.harrastus);

Olioiden rakennetta ei siis ole lyöty ennalta lukkoon.

Kyselyt palvelimelle

JQuery tarjoaa myös tuen kyselyjen tekemiseen erilliselle palvelinkomponentille.

Kyselyt hoituvat kätevästi JQueryn $.getJSON-funktiolla. Alla olevassa esimerkissä haemme ICNDb.comista oleellista dataa.

Kyselyn palauttama data ohjataan $.getJSON-funktion toisena parametrina määriteltävään funktioon. Alla olevassa esimerkissä kutsumme vain alert-komentoa kaikelle palautettavalle datalle.

$.getJSON("http://api.icndb.com/jokes/random/5",
    function(data) {
        alert(data);
    }
);

Ylläoleva esimerkki tulostaa vastaukset konsoliin -- huomaa, että jQuery muuntaa merkkijonomuotoiset vastaukset automaattisesti JSON-olioksi. Käytetään JQueryn each-komentoa listassa olevien elementtien iterointiin. Komennolle each voi antaa parametrina iteroitavan listan, sekä funktion, jota kutsutaan jokaisella listassa olevalla oliolla.

$.getJSON("http://api.icndb.com/jokes/random/5",
    function(data) {
        $.each(data.value, function(i, item) {
            alert(i);
            alert(item);
            alert("-----");
        });
    }
);

Nyt ylläoleva komento tulostaa vastauksen value-kentässä olevat oliot yksitellen. Oletetaan, että käytössämme on elementti, jonka tunnus on "vitsit". JQuery tarjoaa myös mahdollisuuden nopeaan tekstielementtien luontiin komennolla $("<p/>"). Elementteihin voi asettaa tekstin text-komennolla, ja elementin voi lisätä tietyllä tunnuksella määriteltyyn elementtiin komennolla appendTo("#tunnus").

$.getJSON("http://api.icndb.com/jokes/random/5",
    function(data) {
        $.each(data.value, function(i, item) {
            $("<p/>").text(item.joke).appendTo("#vitsit");
        });
    }
);

Tiedon lähettäminen palvelimelle

Jos tiedämme, että palvelu palauttaa JSON-dataa, voimme käyttää yllä käsiteltyä lähestymistapaa. Esimerkiksi viestien noutaminen Chat-chat -tehtävän viestipalvelimelta onnistuu seuraavalla komennolla. Tässä tapauksessa lisäämme jokaiseen viestiin liittyvän message-attribuutin "vitsit"-tunnuksella määriteltyyn elementtiin. Osoitteessa http://bad.herokuapp.com/app/messages on valmiina viestejä tarjoava sovellus.

$.getJSON("http://bad.herokuapp.com/app/messages", function(data) {
    $.each(data, function(i, item) {
        $("<p/>").text(item.message).appendTo("#vitsit");
    });
});

Yllä oleva komento on lyhenne alla määritellystä komennosta.

$.ajax({
    url: "http://bad.herokuapp.com/app/messages",
    dataType: 'json',
    success: parseMessages
});

function parseMessages(messages) {
    $.each(messages, function(i, item) {
        $("<p/>").text(item.message).appendTo("#vitsit");
    });
}

Komennolle $.ajax voi lisätä myös dataa, mitä lähetetään palvelimelle. Esimerkiksi seuraavalla komennolla lähetetään osoitteeseen http://bad.herokuapp.com/app/in olio, jonka sisällä on attribuutit name ja details. Lähetettävän datan tyyppi asetetaan attribuutilla contentType, alla ilmoitamme että data on json-muotoista, ja että se käyttää utf-8 -merkistöä.

var dataToSend = JSON.stringify({
        name: "bob",
        details: "i'm ted"
    });

$.ajax({
    url: "http://bad.herokuapp.com/app/in",
    dataType: 'json',
    contentType:'application/json; charset=utf-8',
    type: 'post',
    data: dataToSend
});

Pyynnössä voi sekä lähettää että vastaanottaa dataa. Attribuutin success asettaminen ylläolevaan pyyntöön aiheuttaa success-attribuutin arvona olevan funktion kutsun kun pyyntö on onnistunut.

Tehtävään on hahmoteltu tehtävien hallintaan tarkoitetun sovelluksen palvelinpuolen toiminnallisuutta. Lisää sovellukseen selainpuolen toiminnallisuus, joka mahdollistaa tehtävien lisäämisen sivulle Javascriptin avulla. Uusien tehtävien lisäämisen ei siis pidä aiheuttaa sivun uudelleenlatausta, vaan uusi tehtävä tulee lähettää palvelimelle Javascript-pyyntönä.

Kun saat sovelluksen toimimaan, mieti myös sen käytettävyyttä. Sovellukselle ei ole automaattisia testejä.

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.

Sisällysluettelo