Osa 4

Mediatyyppi ja tiedostojen käsittely

Palvelimelle tehtävät pyynnöt ja palvelimelta saatavat vastaukset voivat sisältää erimuotoista tietoa. Pyyntö tai vastaus voi sisältää esimerkiksi tekstidokumentin, kuvatiedoston tai vaikkapa PDF-tiedoston. Palvelin vastaanottaa ja kertoo pyynnön tyypin HTTP-protokollan mukana kulkevalla otsakkeella Content-Type.

Tätä tietoa lähetettävän tai vastaanotettavan datan muodosta kutsutaan mediatyypiksi. Tietoa käsittelevä ohjelmisto päättää mediatyypin perusteella miten data käsitellään. Mediatyyppi sisältää yleensä kaksi osaa; mediatyypin sekä tarkenteen (esim application/json). Kattava lista eri mediatyypeistä löytyy IANA-organisaation ylläpitämästä mediatyyppilistasta.

Tyypillisiä mediatyyppejä ovat erilaiset kuvat image/*, videot video/*, äänet audio/* sekä erilaiset tekstimuodot kuten JSON application/json.

Web-palvelut voivat tarjota käytännössä mitä tahansa näistä tiedostotyypeistä käyttäjälle; käyttäjän sovellusohjelmisto päättelee vastauksessa tulevan mediatyypin mukaan osaako se käsitellä tiedoston.

Yksinkertaisimmillaan mediatiedoston lähetys palvelimelta toimii Springillä seuraavasti. Oletetaan, että käytössämme on levypalvelin ja polussa /media/data/ oleva PNG-kuvatiedosto architecture.png.

@GetMapping(path = "/images/1", produces = "image/png")
public void copyImage(OutputStream out) throws IOException {
    Files.copy(Paths.get("/media/data/architecture.png"), out);
}

Yllä olevassa esimerkissä kerromme että metodi kuuntelee polkua /images/1 ja tuottaa image/png-tyyppistä sisältöä. Spring asettaa kontrollerin metodin parametriksi pyynnön vastaukseen liittyvän OutputStream-olion, johon vastaus voidaan kirjoittaa. Files-luokan tarjoama copy-metodi kopioi kuvan suoraan tiedostosta pyynnön vastaukseksi.

Ylläolevan kontrollerimetodin palauttaman kuvan voi näyttää osana sivua img-elementin avulla. Jos metodi kuuntelee osoitetta /media/image.png, HTML-elementti <img src="/media/image.png" /> hakee kuvan automaattisesti osoitteesta sivun latautuessa.

Huom! Jos kuvat ovat staattisia eikä niitä esimerkiksi lisäillä tai poisteta, tulee niiden olla esimerkiksi projektin kansiossa /src/main/resources/public/img — tällaisille staattisille kuville ei tule määritellä kontrollerimetodia. Kansion public alla olevat tiedostot kopioidaan web-sovelluksen käyttöön, ja niihin pääsee käsiksi web-selaimella ilman tarvetta kontrollerille.

Tiedostojen tallentaminen ja lataaminen

Web-sivuilta voi lähettää tiedostoja palvelimelle määrittelemällä input-elementin type-parametrin arvoksi file. Tämän lisäksi lomakkeelle tulee kertoa, että se voi sisältää myös tiedostoja — tämä tapahtuu form-elementin attribuutilla enctype, jonka arvoksi asetetaan multipart/form-data).

<form th:action="@{/files}" method="POST" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" value="Send!"/>
</form>

Lomake lähettää tiedot palvelimelle, jonka tulee käsitellä pyyntö. Pyynnön käsittely tapahtuu aivan kuten minkä tahansa muunkin pyynnön, mutta tässä tapauksessa pyynnön parametrin tyyppi on MultipartFile, joka sisältää lähetettävän tiedoston tiedot.

Alla oleva kontrollerimetodi vastaanottaa pyynnön, ja tulostaa pyynnössä lähetetyn tiedoston koon ja tyypin. Se ei kuitenkaan tee vielä muuta.

@PostMapping("/files")
public String create(@RequestParam("file") MultipartFile file) {
    System.out.println(file.getSize());
    System.out.println(file.getContentType());

    return "redirect:/files";
}

MultipartFile-olio sisältää viitteen tavutaulukkoon, joka sisältää pyynnössä lähetetyn datan. Tavutaulukon — eli tässä tapauksessa datan — tallennus tietokantaan onnistuu seuraavasti. Alla määritelty entiteetti FileObject kapseloi tavutaulukon ja mahdollistaa sen tallentamisen tietokantaan.

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

// muita sopivia annotaatioita
@Entity
public class FileObject extends AbstractPersistable<Long> {

    @Lob
    private byte[] content;

}
Annotaatiolla @Lob kerrotaan että annotoitu muuttuja tulee tallentaa tietokantaan isona dataobjektina. Tietokantamoottorit tallentavat nämä tyypillisesti erilliseen isommille tiedostoille tarkoitettuun sijaintiin, jolloin tietokannan tehokkuus ei juurikaan kärsi erikokoisten kenttien takia.

Kun entiteetille tekee repository-olion, voi sen ottaa käyttöön myös kontrollerissa. Tietokantaan tallentaminen tapahtuu tällöin seuraavasti:

@PostMapping("/files")
public String save(@RequestParam("file") MultipartFile file) throws IOException {
    FileObject fo = new FileObject();
    fo.setContent(file.getBytes());

    fileObjectRepository.save(fo);

    return "redirect:/files";
}

Tiedoston lähetys kontrollerista onnistuu vastaavasti. Tässä tapauksessa oletamme, että data on muotoa image/png; kontrolleri palauttaa tietokantaoliolta saatavan tavutaulukon pyynnön vastauksen rungossa.

@GetMapping(path = "/files/{id}", produces = "image/png")
@ResponseBody
public byte[] get(@PathVariable Long id) {
    return fileObjectRepository.getOne(id).getContent();
}
Loading

Kun tietokantaan tallennetaan isoja tiedostoja, kannattaa tietokanta suunnitella siten, että tiedostot ladataan vain niitä tarvittaessa. Voimme lisätä olioattribuuteille annotaatiolla @Basic lisämääreen fetch, minkä avulla hakeminen rajoitetaan eksplisiittisiin kutsuihin. Tarkasta tässä vaiheessa edellisen tehtävän mallivastaus — huomaat että sielläkin — vaikka annotaatio @Basic ei ollut käytössä — konkreettinen kuva ladataan hyvin harvoin.

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

// muut annotaatiot
@Entity
public class FileObject extends AbstractPersistable<Long> {

    @Lob
    @Basic(fetch = FetchType.LAZY)
    private byte[] content;

}

Ylläoleva @Basic(fetch = FetchType.LAZY) annotaatio luo annotoidun muuttujan get-metodiin ns. proxymetodin — muuttujaan liittyvä data haetaan tietokannasta vasta kun metodia getContent() kutsutaan.

Yleiskäyttöinen tiedoston tallennus ja lataaminen

Edellisessä esimerkissä määrittelimme kontrollerimetodin palauttaman mediatyypin osaksi @GetMapping annotaatiota. Usein tiedostopalvelimet voivat kuitenkin palauttaa lähes minkätyyppisiä tiedostoja tahansa. Tutustutaan tässä yleisempään tiedoston tallentamiseen ja lataukseen.

Käytämme edellisessä esimerkissä käytettyä FileObject-entiteettiä toteutuksen pohjana.

Jotta voimme kertoa tiedoston mediatyypin, haluamme tallentaa sen tietokantaan. Tallennetaan tietokantaan mediatyypin lisäksi myös tiedoston alkuperäinen nimi sekä tiedoston pituus.

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

// muut annotaatiot
@Entity
public class FileObject extends AbstractPersistable<Long> {

    private String name;
    private String mediaType;
    private Long size;

    @Lob
    @Basic(fetch = FetchType.LAZY)
    private byte[] content;

}

Pääsemme kaikkiin kenttiin käsiksi MultipartFile-olion kautta; muokataan aiemmin näkemäämme kontrolleria siten, että täytämme kaikki yllä määritellyt kentät tietokantaan tallennettavaan olioon.

@PostMapping("/files")
public String save(@RequestParam("file") MultipartFile file) throws IOException {
    FileObject fo = new FileObject();

    fo.setName(file.getOriginalName());
    fo.setMediaType(file.getContentType());
    fo.setSize(file.getSize());
    fo.setContent(file.getBytes());

    fileObjectRepository.save(fo);

    return "redirect:/files";
}

Nyt tietokantaan tallennettu olio tietää myös siihen liittyvän mediatyypin. Haluamme seuraavaksi pystyä myös kertomaan kyseisen mediatyypin tiedostoa hakevalle käyttäjälle.

ResponseEntity-oliota käytetään vastauksen paketointiin; voimme palauttaa kontrollerista ResponseEntity-olion, jonka pohjalta Spring luo vastauksen käyttäjälle. ResponseEntity-oliolle voidaan myös asettaa otsaketietoja, joihin saamme asetettua mediatyypin.

@GetMapping("/files/{id}")
public ResponseEntity<byte[]> viewFile(@PathVariable Long id) {
    FileObject fo = fileObjectRepository.getOne(id);

    final HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.parseMediaType(fo.getMediaType()));
    headers.setContentLength(fo.getSize());

    return new ResponseEntity<>(fo.getContent(), headers, HttpStatus.CREATED);
}

Ylläolevassa esimerkissä vastaanotetaan pyyntö, minkä pohjalta tietokannasta haetaan FileObject-olio. Tämän jälkeen luodaan otsakeolio HttpHeaders ja asetetaan sille palautettavan datan mediatyyppi ja koko. Lopuksi palautetaan ResponseEntity-olio, mihin data, otsaketiedot ja pyyntöön liittyvä statusviesti (tässä tapauksessa CREATED eli tiedosto luotu palvelimelle) liitetään.

Edeltävä esimerkki ei ota kantaa tiedoston nimeen tai siihen, miten se ladataan. Voimme lisätä vastaukseen Content-Disposition-otsakkeen, minkä avulla voidaan ehdottaa tiedoston tallennusnimeä sekä kertoa, että tiedosto on liitetiedosto, jolloin se tulee tallentaa.

@GetMapping("/files/{id}")
public ResponseEntity<byte[]> viewFile(@PathVariable Long id) {
    FileObject fo = fileObjectRepository.getOne(id);

    final HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.parseMediaType(fo.getMediaType()));
    headers.setContentLength(fo.getSize());
    headers.add("Content-Disposition", "attachment; filename=" + fo.getName());

    return new ResponseEntity<>(fo.getContent(), headers, HttpStatus.CREATED);
}
Loading
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan: