AVOIN PALVELU- JA KOHDERAJAPINTA KANTA-HÄMEESEEN
Ammattikorkeakoulututkinnon opinnäytetyö Riihimäki, tieto- ja viestintätekniikka
Kevät, 2019
Yuxiu Guo
Tieto- ja viestintätekniikan koulutusohjelma Riihimäki
Tekijä Yuxiu Guo Vuosi 2019
Työn nimi Avoin palvelu- ja kohderajapinta Kanta-Hämeeseen Työn ohjaaja Toni Laitinen
TIIVISTELMÄ
Opinnäytetyössä tavoitteena oli tehdä paikkatietokanta Kanta-Hämeeseen.
Hankkeen taustalla oli nykyiset ongelmat paikkatietojen kanssa. Tietoja oli pal- jon, mutta ne olivat hajallaan. Niiden löydettävyys oli huono ja niihin oli vaikea päästä käsiksi. Tietojen löytäminen oli myös vaikeaa. Paikkatiedoilla tässä opin- näytetyössä ovat kahvilat, nähtävyydet, ulkoilureitit, jne. Niihin liittyy yleensä au- kioloajat ja tavoitteena oli että lähellä olevia avoinna olevia kohteita voisi hel- posti hakea.
Työn tilaaja on Hämeen ammattikorkeakoulu Oy, Älykkäät palvelut -tutkimusyk- sikkö. Mukana oli myös Hämeenlinnan kaupungin edustajia. Tilaajan tavoitteena oli saada helppokäyttöinen ja avoin paikkatietojärjestelmä, johon olisi mahdol- lista koostaa kaikki Kanta-Hämeen paikat. Erityisesti tärkeää oli, että paikkatie- toja voisi hakea helposti erilaisilla laitteilla. Tärkeimpänä oli avoin rajapinta, jonka avulla kannan tietojen ylläpitäminen onnistuisi helposti ja jonka päälle olisi mahdollista tehdä helposti erilaisia käyttöliittymiä.
Keskeiseksi tavoitteeksi tässä työssä tuli avoimet RESTful-palvelut. Lisäksi halut- tiin toteuttaa niiden päälle perus ylläpitokäyttöliittymät, joilla voidaan hallin- noida tietoja. Yksinkertainen karttakäyttöliittymä haluttiin, jotta tiedot saatiin vi- sualisoitua.
Tuloksena syntyi kaikki yllämainitut toiminnallisuudet. Varsinaista toteutustyötä helpotti asiakkaan antamat viitekehykset. Ohjelmointikieleksi haluttiin PHP ja tietokannaksi PostgreSQL. Arkkitehtuuriksi valittiin MVC-malli, joka helpotti koo- din jäsentämistä ja hyödynnettävyyttä.
Yhteenvetona voisi todeta, että opinnäytetyön tavoitteet saavutettiin. Sovellusta on kuitenkin tarkoitus jatkokehittää, jotta siitä saadaan vielä toimivampi. Selkeitä kehittämisehdotuksia ovat käyttöoikeushallinta ja mobiilisovellus.
Avainsanat REST, RESTful-palvelu, Paikkatieto
Sivut 49 sivua, joista liitteitä 24 sivua
Information and Communication Technology Riihimäki
Author Yuxiu Guo Year 2019
Subject Open Service and Destination interface to Kanta-Häme Supervisors Toni Laitinen
ABSTRACT
The goal of the thesis project was to create a spatial database in the Kanta-Häme area. At the background of the project there were problems with the current spatial data. There was a lot of information, but it was scattered. It was difficult to find and access this information. Spatial data in this project included cafes, tourist sights, outdoor tours, etc. These locations are usually associated with opening hours and customers want to easily find nearby open attractions.
The work was commissioned by Häme University of Applied Sciences, the Intelli- gent Services Research Unit. There were also representatives from the city of Hämeenlinna. The commissioner wanted to get an open and user-friendly loca- tion information system, a system that would contain all the interesting locations in Kanta-Häme. It was especially important, that the location data could be easily retrieved using different devices. The most important issue was the open inter- face, which would make possible to easily create many different user interfaces.
The key objective of this work was open RESTful services. In addition, the com- missioner wanted to acquire basic maintenance user interfaces to manage the data. A simple map interface was required to visualize the data.
As a result of the project, all the above functionalities were created. The actual implementation work was facilitated by the customer reference frameworks.
The programming language was PHP and PostgreSQL was used as the database.
An MVC model was chosen as the architecture here which facilitated code struc- turing and usability.
In summary, the objectives of the thesis were achieved. However, the application is intended to be further developed to make it even more functional. Clear de- velopment suggestions include user management and developing a mobile ap- plication.
Keywords REST, RESTful service, Spatial database.
Pages 49 pages including appendices 24 pages
1 JOHDANTO ... 1
2 PAIKKATIETOKANNAN SUUNNITTELU ... 2
2.1 Käytetty tietokanta ja sen laajennukset ... 2
2.1.1 PostgreSQL ... 2
2.1.2 PostGIS-laajennus ... 2
2.2 Tietokannan tietomallin suunnittelu ... 2
3 ARKKITEHTUURIN SUUNNITTELU ... 3
3.1 Operatiivinen arkkitehtuuri ... 3
3.1.1 Käytetty sovelluspalvelin ... 3
3.1.2 Tietokantapalvelin ... 4
3.2 Sovellusarkkitehtuuri ... 4
3.2.1 MVC ... 6
4 TIETOKANTAKERROS (ENGLANNIKSI DATA ACCESS LAYER) ... 7
4.1 Käytetyt valmiit kirjastot ... 7
4.1.1 PHP-DS (englanniksi Data Structures) ... 7
4.1.2 Doctrine ORM ... 8
4.2 Malli luokat (englanniksi Model) ... 8
4.3 DAO-luokat ... 9
5 LIIKETOIMINTAKERROS (ENGLANNIKSI BUSINESS LAYER)... 10
5.1 RESTFul API ... 11
5.2 Käytetyt valmiit kirjastot ... 13
5.2.1 PHP Composer ... 13
5.2.2 PHP-DS ... 13
5.2.3 GeoPHP ... 14
5.2.4 JSON SCHEMA ... 14
5.3 Paikkaapi.php ... 15
5.4 Controller-luokka... 15
5.5 API-luokat ... 16
5.6 Skeemat ... 16
6 REST API KÄYTTÖ ... 17
6.1 Osoitteet ... 17
6.2 Haku... 18
6.2.1 Kaikkien tietojen haku ... 18
6.2.2 Suodatettu haku ... 18
6.2.3 Haku id:llä ... 19
6.2.4 Haku paikan id:llä ... 19
6.2.5 Muut APIt... 19
6.3 Lisäys ... 19
6.4 Päivitys ... 20
7 KÄYTTÖLIITTYMÄKERROS (ENGLANNIKSI PRESENTATION LAYER) ... 20
7.1 Arkkitehtuuri ... 20
7.2 Käytetyt valmiit kirjastot ... 21
7.2.1 jQuery ... 21
7.2.2 jQuery UI ... 21
7.2.3 jsGrid ... 22
7.2.4 Serialize ... 22
7.2.5 GoogleApi ... 22
7.2.6 Leaflet ... 22
7.2.7 OpenStreeMap ... 22
7.3 Tietojen syöttäminen ... 23
7.4 Kartta ... 23
8 YHTEENVETO ... 24
LÄHTEET ... 25
Liitteet
Liite 1 TERMIT
Liite 2 RESTAPI LÄHDEKOODIT
Liite 3 YLLÄPITOKÄYTTÖLIITTYMÄN LÄHDEKOODIT
Liite 4 KARTTAKÄYTTÖLIITTYMÄN LÄHDEKOODIT
1 JOHDANTO
Tämän opinnäytetyön tarkoituksena oli toteuttaa Kanta-Hämeeseen pal- velu- ja kohdetietokanta, sekä avoin rajapinta siihen. Lisäksi opinnäyte- työssä oli tarkoitus toteuttaa yksinkertainen karttakäyttöliittymä ja tieto- jen ylläpitoon tarvittavat peruskäyttöliittymät. Tarve opinnäytetyölle oli selkeä. Kanta-Hämeessä oli paljon paikkatietoa, mutta se oli hajallaan. Sen saatavuus ja löytäminen oli vaikeaa. Tämän vuoksi haluttiin yksi paikka, mistä tarvittavat tiedot saisi helposti ulos.
Opinnäytetyö alkaa tietokannan suunnittelulla. Tietokantaan haluttiin tal- lentaa Kanta-Hämeen palvelut ja kiinnostavat kohteet. Niistä haluttiin tal- lentaa perustietojen lisäksi myös sijaintitiedot, jotta niitä voitaisiin näyttää kartalla. Perustietoina haluttiin tallentaa kohteen luokittelu, nimi, kuvaus ja osoite. Kohteeseen haluttiin lisäksi liittää kuvia, yhteyshenkilöitä, auki- olotiedot ja www-linkit. Luokittelun avulla voidaan karttaa määritellä ha- luttaessa omia tasoja, joita voi näyttää tai piilotta. Näiden pohjatietojen perusteella valittiin sopiva tietokanta ja tehtiin tietomalli.
Tämän jälkeen suunniteltiin järjestelmän kokonaisarkkitehtuuri. Sovellus- palvelimeksi valittiin Apache 2.4 HTTP -palvelin ja palvelinpuolen ohjel- mointikieleksi valittiin PHP-ohjelmointikieli. Käyttöliittymä halutiin isoloida palvelimesta. Tämän vuoksi otettiin käyttöön MVC-arkkitehtuurimalli (eng- lanniksi Model-View-Controller).
MVC-mallin mallikerros toteutettiin palvelinpuolella PHP- ohjelmointikielellä. MVC-mallin osista lähinnä tietokantaa on malli (englan- niksi Model). Jäljempänä siitä käytetään myös nimitystä Data Access Layer eli tietokantakerros. Tietokantakerros toteutettiin Doctrine ORM -kirjaston avulla. Jokaista taulua varten toteutettiin oma Model-luokka ja Doctrine- kirjaston avulla se sidottiin tietokantatauluun ja sen kenttiin.
MVC-mallin ohjainkerros (englanniksi Controller) toteutettiin palvelin puo- lella PHP-ohjelmointikielellä. Käytännössä siinä toteutettiin avoimet REST- rajapinnat tietokannan tietojen päivittämiseen. Tiedonvälitykseen valittiin JSON-formaatti. Ohjainkerros käyttää tietokantakerrosta tietokannan ope- roimiseen. Ohjainkerrokseen rakennettiin oma reititysmekanismi REST- kutsujen välittämiseen oikealla API-luokalle. API-luokkiin toteutettiin si- säänpäin tulevien tietojen validointi skeemaa vastaan ennen varsinaista operaatiota.
MVC-mallin näkymäkerros eli käyttöliittymäkerros toteutettiin HTML5, CSS
ja Javascript -tekniikoilla. Peruskäyttöliittymien toteutuksessa hyödynnet-
tiin useita valmiita javascript-kirjastoja. Peruskirjastoksi valittiin jQuery ja
sitä laajennettiin jQuery UI ja jsGrid -kirjastoilla. jQuery-kirjaston avulla hoi-
detaan REST-kutsut palvelimelle. jQuery UI -kirjastoa käytetään joihinkin
käyttöliittymä toiminnallisuuksiin. jsGrid-kirjastoa puolestaan käytetään syöttölomakkeilla tietojen näyttämiseen, päivittämiseen ja poistamiseen.
Käyttöliittymäkerrokseen tehtiin kaksi erillistä yhden sivun HTML- sovellusta. Ylläpitokäyttöliittymän avulla voidaan hallinnoida tietokannan tietoja. Karttakäyttöliittymä, jonka avulla voidaan visuaalisesti nähdä tieto- kannan kohteet kartalla ja hakea niitä. Myös karttakäyttöliittymän toteu- tuksessa käytettiin valmiskirjastoja. Leaflet-kirjaston avulla haetaan kartan tiilet OpenStreetMap-palvelusta ja näytetään kohteet kartalla. GoogleApia käytetään osoitehakuihin.
Opinnäytetyössä keskityttiin MVC-mallin M- ja C-kerrosten ja niistä muo- dostuvan REST APIn kehittämiseen ja dokumentointiin. Niiden päälle ra- kennettiin peruskäyttöliittymät ja kartta, mutta vain perustoiminnallisuuk- sien osalta.
2 PAIKKATIETOKANNAN SUUNNITTELU
2.1 Käytetty tietokanta ja sen laajennukset
2.1.1 PostgreSQL
Tietokannaksi valittiin PostgreSQL, koska PostGIS-laajennuksen avulla se tukee hyvin paikkatietojen tallennusta. Se mahdollistaa spatiaalisten ja maantieteellisten kohteiden tallentamisen PostgreSQL-tietokantaan.
PostgreSQL on avoimeen lähdekoodiin perustuva relaatiotietokanta. Se kuitenkin poikkeaa hiukan perinteisestä relaatiotietokannasta, koska siinä tietokannan riveistä säilytetään useita versioita. Perinteisessä relaatiokan- nassa versiotietoja ei säilytetä.
2.1.2 PostGIS-laajennus
PostGIS-laajennus sisältää geometry-tietotyypin, johon voidaan tallentaa erityyppisiä geometrioita. Se mahdollistaa esim. yksittäisten pisteiden, vektoreiden tai monikulmioiden tallentamisen tietokantaan.
PostGIS-laajennuksessa on myös paljon geometrioiden käsittelyyn tarkoi- tettuja tietokantafunktioita, mutta tässä sovelluksessa niitä ei tarvita.
2.2 Tietokannan tietomallin suunnittelu
Tietokanta mallinnettiin käyttämällä Microsoft Visio Professional -tuotetta.
Aluksi käytiin asiakkaan kanssa keskustelu, jossa selvisi käsitemalli. Sen pe- rusteella päädyttiin alla olevan kuvan mukaiseen tietomalliin. Jokaisella paikalla on luokittelu (hotelli, kahvila, majoitus) ja luokittelulla on kategoria (palvelu tai kohde). Paikalla puolestaan voi olla useita paikkatietoja (sijain- teja), yhteystietoja, kuvia, linkkejä ja aukioloaikoja. Aukioloaikatyyppi ker- too onko kyseessä erikoispäivä, maanantai, tiistai, jne.
Kuva 1. Tietokannan ER-malli
3 ARKKITEHTUURIN SUUNNITTELU
3.1 Operatiivinen arkkitehtuuri
3.1.1 Käytetty sovelluspalvelin
Sovellus tarvitsee ajoympäristöksi Linux-palvelimen, joko fyysisen palveli- men tai virtuaalipalvelimen.
Itse sovellus rakennettiin Apache2 www-palvelimen päälle, koska siinä on hyvä tuki PHP-ohjelmointikielelle. Apache2 perusasennuksen lisäksi palve- lin koneelle asennettiin myös PHP-tulkki ja PostgreSQL-klientti.
Asennuskomennot:
apt-get update (1)
apt-get -y upgrade (2)
apt-get -y install apache2 curl apache2-utils cron php li-
bapache2-mod-php php-mysql php-pgsql (3)
apt-get install -y php-cli (4)
apt-get -y install postgresql-client-10
a2enmod proxy rewrite proxy_http proxy_wstunnel headers proxy_balancer proxy_html deflate rewrite authz_groupfile
(5) (6)
3.1.2 Tietokantapalvelin
Tietokantapalvelin tarvitsee myös ajoympäristöksi Linux-palvelimen. Vaih- toehtoisesti myös tietokanta voidaan ostaa pilvipalveluna, kunhan PostGIS- laajennus on tuettuna. Ainakin Amazon RDS tukee PostGIS-laajennusta.
Tietokantana PostgreSQL, johon lisättynä PostGIS-laajennus.
Asennuskomennot:
apt-get update (1)
apt-get install -y postgresql-10 postgresql-client-10 post-
gresql-contrib-10 (2)
apt-get install -y postgresql-10-postgis-2.4 (3) Tietokannan luonti palvelimella psql-komennolla tai työasemalta pgAdmin työkalulla.
3.2 Sovellusarkkitehtuuri
Sovellusarkkitehtuurissa päädyttiin hyödyntämään MVC-mallia. Siinä so- vellus on jaettu kolmeen kerrokseen: malli (englanniksi model), ohjain (englanniksi controller) ja näkymä (englanniksi view). Näkymä vastaa käyt- töliittymäkerrosta, ohjain liiketoimintakerrosta ja malli tietokantakerrosta.
Alla olevassa kuvassa on esitetty opinnäytetyössä käytetty MVC-mallin mu-
kainen arkkitehtuuri.
Kuva 2. MVC-mallin mukainen arkkitehtuuri
Alla olevassa kuvassa on kuvattu tietojen välittyminen opinnäytetyön MVC-mallin mukaisessa toteutuksessa.
Kuva 3. Toteutuksen sekvenssikaavio
3.2.1 MVC
MVC-malli keksittiin 1970-80-luvun taitteessa. Ajatus lähti Trygve Reens- kaugin vuonna 1978 kirjoittamasta muistiosta (Trygve Mikkjel Heyerdahl Reenskaug, 2003). Muistiossa kuvattiin mallin kolme kerrosta. Alkuperäi- sen idean mukaan Malli-kerros (englanniksi Model) edustaa tietoa ja malli voi olla yksittäinen olio tai olioiden hierarkia. Näkymät (englanniksi View) kuvattiin alkuperäisessä dokumentissa mallin visuaaliseksi kuvaukseksi.
Näkymä sidotaan malliin ja se saa näytettävän tiedon kysymällä ne mallilta.
Ohjain (englanniksi Controller) on alkuperäisessä muistiossa kuvattu käyt- täjän ja järjestelmän yhdistäväksi linkiksi. Alkuperäisessä ajatuksessa mu- kana oli myös muokkaaja kerros (englanniksi Editors). Sen tarkoituksena on laajentaa ohjainta, siten että käyttäjä voi muuttaa näkymässä olevaa tie- toa. (Reenskaug, 1979)
Ensimmäinen julkinen toteutus tehtiin vuonna 80-luvun alussa Smalltalk- 80 luokkakirjastoon. Alkuvaiheessa MVC:stä ei kuitenkaan ollut mitään jul- kista dokumentaatiota. Ensimmäinen julkaisu oli “A Cookbook for Using the Model-View-Controller User Interface Paradigm in Smalltalk -80" ja se julkaistiin vasta 1988. (Wiki.c2, 2014)
MVC-mallin läpimurto alkoi 2000-luvulla, kun se tuli Java, Python ja Ruby ympäristöihin. Vuonna Ruby ja Python versiot mahdollistivat MVC-mallin hyödyntämisen myös pienissä sovelluksissa, koska kyseisten työvälineiden avulla voitiin kehittää nopeasti. (Wikipedia, 2019a)
Kuva 4. MVC-mallin kerrokset ja niiden vuorovaikutus, (Wikipedia, 2019a)
Ylläolevassa kuvassa on esitetty mallin kerrokset ja niiden keskinäinen vuo-
rovaikutus. Käyttäjän syöte menee ohjainkerrokseen. Ohjainkerros käyttää
mallikerrosta ja mallikerros päivittää näkymäkerrosta. Lopulta käyttäjä nä-
kee päivitetyn näkymän. (Wikipedia, 2019a)
MVC on nykyisin suosittu, koska sen avulla koodi jäsentyy paremmin. Koodi organisoidaan tehtävien mukaan ja jokaisella kerroksella on selkeä oma tehtävä. (codecademy, n.d.)
4 TIETOKANTAKERROS (ENGLANNIKSI DATA ACCESS LAYER)
Tietokanta kerros toteutettiin hyödyntämällä Doctrine ORM -kirjastoa. Jo- kaista tietokannan taulua varten tehtiin oma malliluokka ja oma dao- luokka. Jokainen malliluokka periytyy basemodel-luokasta, jossa on perus- toiminnallisuudet. Samoin jokainen dao-luokka periytyy basedao-luokasta, jossa on dao-luokkien perustoiminnallisuudet.
Kuva 5. Tietokantakerroksen luokkakaavio
4.1 Käytetyt valmiit kirjastot
4.1.1 PHP-DS (englanniksi Data Structures)
Tämä kirjasto sisältää sovelluksen tarvitsemia tietorakenteita. PHP:n perus
array ei ollut sovelluksen kannalta riittävä, vaan sovellus tarvitsee DS\Map
tietorakennetta. Sen avulla voidaan tallentaa avain-arvo pareja ja tarvitta-
essa hakea arvoa avaimella.
Esimerkki DS\Map käytöstä
$map = new \Ds\Map();
$map->put('a', 1);
$map->put('b', 2);
$map->get('b');
4.1.2 Doctrine ORM
Doctrine-kirjaston avulla tietokanta voidaan sitoa PHP-luokkiin. Käytän- nössä malliluokan alkuun määritellään annotaatioilla käytettävä tietokan- tataulu ja jokaisen kentän kommenttiin määritellään tietokannan kentän tietotyyppi. Tämän jälkeen kirjaston avulla voidaan, käyttämällä DQL:ää (Doctrine Query Language), hakea tietokannan tiedot suoraan PHP- malliluokan olioihin. Myös päivitys onnistuu helposti. Tarvitsee vain vaih- taa malli olion sisältöä ja kutsua käskeä Doctrine-kirjastoa persistoimaan kyseinen olio.
Toiminnot menevät EntityManager-luokan kautta. Ensin pitää luoda Enti- tyManager-olio:
$isDevMode = true;
$ormconfig = Setup:: createAnnotationMetadataConfigu- ration (array( __DIR__ ."/src"), $isDevMode);
$ormconn = array(
'driver' => 'pdo_pgsql', 'user' => 'paikkatieto', 'password' => 'paikkatieto', 'host' => '127.0.0.1',
'port' => '5435',
'dbname' => 'paikkatieto'
$this->entityManager = EntityManager:: ); cre- ate ($ormconn, $ormconfig);
4.2 Malli luokat (englanniksi Model)
Jokaisesta tietokantataulusta tehtiin oma malliluokka. Jokaisesta tietokan- tataulun kentästä tehtiin malliluokan muuttuja ja jokaiselle muuttujalle tehtiin getter- ja setter -funktiot. Annotaatioilla määriteltiin tietokantatau- lun nimi ja tietokantakenttien tietotyypit.
Malliluokissa on funktio fromJson, jonka avulla JSON-olio voidaan lukea
malliluokan kenttiin. Malliluokka periytyy BaseModel-luokasta. BaseMo-
del-luokka toteuttaa JsonSerializable rajapinnan eli käytännössä jsonSe-
rialize funktion. Kyseinen funktio lukee kaikki model-luokan muuttujat ja
palauttaa ne, jotta kyseinen malli voidaan muuntaa JSON-muotoon. Base- Model-luokka sisältää myös getGeometry funktion, joka muuntaa GeoJSON muotoisen olion merkkijonoksi, joka voidaan tallentaa tietokan- nan geometry tietotyyppiin.
Esimerkkimalli alla:
/** * @Entity @Table(name="paikkatieto.linkki") **/
class Linkki extends BaseModel {
/** @Id @Column(type="integer") @GeneratedValue
**/ protected $linkki_id;
/** @Column(type="integer") **/
protected $paikka_id;
/** @Column(type="string") **/
protected $linkki;
function fromJson($json) { $this->linkki_id=$this->get- Value($json,'linkki_id');
$this->paikka_id=$this->get- Value($json,'paikka_id');
$this->linkki=$this->getValue($json,'link- ki');
} /**
* @return mixed */
public function getLinkki_id() {
return $this->linkki_id;
… } }
4.3 DAO-luokat
Jokaista tietokantataulua varten tehtiin myös oma DAO-luokka. DAO- luokkien avulla tietokantataulusta voidaan hakea, päivittää, lisätä tai pois- taa tietoja. DAO-luokat kytkeytyvät kantaan Doctrine-kirjaston Entity- Manager luokan avulla.
Alla esimerkkejä Doctrine-kirjaston käytöstä:
// kuvan haku
public function haeKuva($id) {
$kuvaEntity = $this->getEntityManager()-
>find("Kuva", $id);
return $kuvaEntity;
}
// kuvan luonti
public function luoKuva($kuva){
$kuvaEntity = new Kuva();
$kuvaEntity->fromJson($kuva);
$this->getEntityManager()->per- sist($kuvaEntity);
$this->getEntityManager()->flush();
return $kuvaEntity->getKuva_id();
}
// paikan kuvien haku
public function haePaikanKuvat($paikka_id) {
$query = $this->getEntityManager()->create- Query("SELECT k FROM Kuva k WHERE k.paikka_id = " .
$paikka_id);
$kuvat = $query->getResult();
return $kuvat;
}
5 LIIKETOIMINTAKERROS (ENGLANNIKSI BUSINESS LAYER)
Liiketoimintakerros sisältää varsinaisen rajapinnan toteutuksen. Liiketoi- mintakerros tarjoaa ulospäin REST API-rajapinnan. Tietojen siirtoon palve- limen ja asiakkaiden välillä käytetään JSON-formaattia.
Kaikki API-kutsut ohjataan yhden PHP-sivun (paikkaapi.php) kautta. Siinä päätellään mihin API-luokkaan (paikka, yhteystieto, aukiolo, kuva, linkki) kutsu välitetään. Tämä on tarkemmin kuvattu luvussa 5.3.
API-luokat on periytetty Controller-luokasta, joka sisältää REST-kutsujen reitityksen oikeille API-luokan funktioille. Controller-luokka on tarkemmin kuvattu luvussa 5.4.
Varsinaisen API-luokan funktiot kutsuvat tietokantakerrosta. Päivityksen ja
lisäyksen yhteydessä tarkistetaan lisäksi, että pyynnössä tuleva JSON-olio
on skeeman mukainen ja siinä on kaikki tarvittavat tiedot.
Kuva 6. Liiketoimintakerroksen luokkakaavio
5.1 RESTFul API
REST lyhenne tulee sanoista REpresentional State Transfer. Se on Roy Fiel- dingin kehittämä HTTP-protokollaan perustuva arkkitehtuuri tyyli. Hän ke- hitti sitä rinnakkain HTTP 1.1 protokollan kanssa ja se julkaistiin hänen väi- töskirjassaan vuonna 2000. (Wikipedia, 2019b). REST on arkkitehtuurinen tyyli hajautettujen www-sovelluksien tekoon.
REST määrittelee kuusi arkkitehtuurista rajoitetta. Järjestelmän on nouda- tettava näitä rajoitteita, jotta se täyttää määritelmän RESTful-järjestelmä tai API. Arkkitehtuuriset rajoitteet ovat: asiakas-palvelin-sovellus, tilaton, välimuistikelpoinen, kerrostettu järjestelmä, yhtenäinen rajapinta ja valin- naisesti koodin siirtäminen käyttöliittymälle. (RESTfulAPI.net, REST Architectural Constraints, 2017b)
Asiakas-palvelin sovelluksessa käyttöliittymä ja tietokanta eristetään toisis-
taan. Tällä mahdollistetaan helpompi siirrettävyys uusille käyttöliittymä-
alustoille. Web-sovelluksille suurin hyöty on kuitenkin, se että komponent-
teja voidaan kehittää samanaikaisesti ja toisistaan riippumatta. Samalla
palvelinkomponentit tulevat yksinkertaisimmiksi ja järjestelmästä tulee pa- remmin skaalattava. (R. Fielding, 2014)
Tilattomuus tarkoittaa, että palvelimella ei säilytetä asiakkaan tiloja, vaan että tarvittavat tilasidonnaiset tiedot pitää välittää jokaisen pyynnön mu- kana. Istunto tiedot säilötään asiakassovelluksen puolella. Tämä parantaa palvelin puolen suorituskykyä, koska palvelimen ei tarvitse säilöä tilatietoja pyyntöjen välillä, vaan ne voidaan tuhota heti pyynnön päätyttyä. Järjes- telmän valvonta helpottuu myös huomattavasti, koska pyyntö sisältää kaikki tiedot. Haittapuolena on kasvanut verkkoliikenne ja palvelimen riip- puvuus useista asiakassovellus versioista. (R. Fielding, 2014)
Välimuistikelpoisuudella tarkoitetaan, että palvelin voi merkitä vastauksen sellaiseksi, että asiakassovellus voi käyttää sitä vastauksena samanlaisiin pyyntöihin. Tällöin asiakassovelluksen ei tarvitse kysyä sitä palvelimelta vaan se voi käyttää paikallisessa muistissa olevaa vastausta. Välimuistira- joitteet vähentävät palvelimelle tulevaa kuormaa ja siten lisäävät suoritus- kykyä ja skaalautuvuutta. Haittapuolena välimuisti voi huonontaa luotetta- vuutta, koska välimuistin tieto voi olla vanhentunutta. (R. Fielding, 2014) Kerrostettu järjestelmä voi koostua useasta hierarkkisesta tasosta. Kom- ponentit eivät kuitenkaan voi nähdä välittömän kerroksen ulkopuolella.
(RESTfulAPI.net, What is REST, 2017a). Kerrostetussa järjestelmässä toi- minnot voidaan jakaa useille palvelimille. APIt voidaan laittaa yhdelle pal- velimelle, tietokanta toiselle ja autentikointi omalle palvelimelleen.
(RESTfulAPI.net, REST Architectural Constraints, 2017b). Asiakas ei tiedä kytkeytyykö se loppupalvelimelle vai välityspalvelimelle. Välityspalvelimien avulla järjestelmää voidaan skaalata ja tehdä yhteisiä välimuisteja.
(Wikipedia, 2019b).
RESTful web -palvelut tarjoavat yhtenäisen rajapinnan, jota on helppo käyt- tää. Järjestelmässä olevalla resurssilla tulisi olla vain yksi URI. Yksittäinen resurssi ei saisi olla liian suuri. Resurssi voi sisältää linkkejä, joista saadaan siihen liittyviä tietoja. Resurssien esitysten tulee noudattaa sovittuja oh- jeita, kuten nimeämiskäytännöt ja formaatti. Resurssien käyttö ja muutta- minen tapahtuu yhtenäisen tavan avulla. (RESTfulAPI.net, REST Architectural Constraints, 2017b)
On demand -koodin avulla voidaan laajentaa asiakassovelluksen toiminnal- lisuutta. Tämä tekee asiakassovelluksista yksinkertaisempia.
(RESTfulAPI.net, What is REST, 2017a). RESTful-palvelu voi palauttaa asia-
kassovellukselle käännettyjä komponentteja kuten Java appletteja tai Ja-
vaScriptiä. (Wikipedia, 2019b).
5.2 Käytetyt valmiit kirjastot
Palvelin puolella on käytetty koodin selkeyttämisen vuoksi valmiita avoi- men lähdekoodin kirjastoja. Kirjastot on asennettu PHP composer -työka- lun avulla.
5.2.1 PHP Composer
Composer on työväline, jolla voi hallinnoida sovelluksen tarvitsemia kirjas- toja. Se asentaa tarvittavat kirjastot vendor-hakemistoon ja tekee sinne au- toload.php tiedoston. Autoload.php tiedosto lisätään sovellukseen re- quire_once funktion avulla ja tämän jälkeen kaikki composerilla ladatut kir- jastot ovat sovelluksen käytössä. Composer määritykset tehdään com- poser.json tiedostoon.
Esimerkki määrityksistä:
{ "autoload" : { "psr-4" : {
"Hamk\\Paikkatietoapi\\" : "src"
},
"psr-0" : {
"\"php-ds/php-ds\" : \"~1.2\"" : ""
} },
"config" : {
"vendor-dir" : "src/vendor"
},
"name" : "hamk/paikkatietoapi", "require" : {
"php" : ">=7.3",
"php-ds/php-ds" : "^1.2", "doctrine/orm" : "^2.6.2", "symfony/yaml" : "2.*",
"justinrainbow/json-schema" : "~5.2"
},
"type" : "rest api",
"description" : "paikkatieto rest api"
}
Tämän jälkeen ajetaan ”composer update”-komento.
5.2.2 PHP-DS
PHP-ohjelmointikielessä on vakiona array-tietorakenne, mutta se ei ollut
sovelluksen kannalta riittävä. Sen takia käyttöön otettiin PHP Data Structu-
res -kirjasto, joka tarjoaa monipuolisemmat ja tietorakenteen sovelluksen
käyttöön.
Kirjastosta käytetään Map-luokkaa, jonka avulla voidaan säilö avain/arvo pareja. Se helpottaa käsittelyä, koska Map:stä voidaan hakea haluttu tietue suoraan avaimella. Sovellus käyttää myös Sequence-luokkaa, joka järjestää arvot yhteen lineaariseen ulottuvuuteen.
5.2.3 GeoPHP
GeoPHP-kirjastoa käytetään GeoJSON muotoisten olioiden käsittelyyn. Tie- tokannassa paikkatiedot ovat geometry-tyyppisessä tietotyypissä. Geo- metry tietotyyppi ei sovellu käyttöliittymäkerrokselle ja se on muutettava toiseen muotoon. Muodoksi valittiin GeoJSON, koska kartat tukevat GeoJSON-olioita. Lisäksi kartan pisteistä voidaan helposti luoda GeoJSON- olioita, jotka voidaan lähettää tietokantaan tallennettaviksi. Lisäksi GeoJSON-olion sisällä voidaan välittää properties-kentässä sovelluskohtai- sia tietoja (esim paikan nimi, paikan kuvaus, jne).
Esimerkki kartan pisteestä:
{ "type": "Feature", "geometry": { "type": "Point",
"coordinates": [125.6, 10.1]
},
"properties": {
"nimi": "Dinagat Islands"
} }
5.2.4 JSON SCHEMA
JSON Schema on PHP-kirjasto, jonka avulla voidaan validoida JSON-olioita skeemaa vasten. Kirjaston avulla voidaan helposti tarkistaa, että sisäänpäin tulevassa datassa on kaikki tarvittavat kentät ja ne ovat oikean muotoisia.
Kirjaston käyttö:
function validoi($schemajson,$jsonobject) { $validator = new Validator;
$realpath = real-
path ( __DIR__ .'/schema/'.$schemajson);
$schema = 'file://'.$realpath;
$validator->validate(
$jsonobject,
(object)['$ref' => $schema],
Constraint:: CHECK_MODE_APPLY_DEFAULTS );
if (!$validator->isValid()) {
header ('Content-type: application/json;
charset=iso-8859-1');
echo json_encode ($validator->getErrors());
return false;
}
return true;
}
5.3 Paikkaapi.php
Kaikki REST API -rajapinnan kutsut ohjataan paikkaapi.php tiedoston kautta. Siinä pilkotaan pyynnön uri osiin ’/’-merkin kohdalta ja sijoitetaan osat array-olioon. Elementtien avulla päätellään kutsuttava API (paikka, linkki, jne), haluttu alitoiminto ja id. Päättely riippuu elementtien lukumäärästä.
− Jos taulukossa on 2 elementtiä niin o toinen elementti on api
o Esim. GET /paikkaapi.php/paikka o Hakee kaikki paikat
− Jos taulukossa on 3 elementtiä niin o toinen elementti on api o kolmas elementti on id
o Esim. GET /paikkaapi.php/paikka/15 o Hakee paikan, jonka id on 15
− Jos taulukossa on 4 elementtiä niin o toinen elementti on alitoiminto o kolmas elementti on id
o neljäs elementti on haluttu api
o Esim. GET /paikkaapi.php/paikka/15/yhteystieto
o Hakee yhteystiedot, jotka liittyvät paikkaan, jonka id on 15.
5.4 Controller-luokka
Jokainen API-luokka periytyy Controller-luokasta. Controller-luokan teh-
tävä on tarkistaa minkä tyyppinen REST-kutsu on kyseessä (GET, POST,
PUT, DELETE, PATCH) ja välittää se oikealle funktiolle. Oletustoteutuksena
kaikki kutsut palauttavat virheviestin ja ne toimivat vasta kun ne on toteu-
tettu varsinaisessa API-luokassa.
Kutsut menevät seuraavasti:
Taulukko 1. API-kutsut
HTTP VERB Funktio Parametrit Selitys
GET haeKaikki - Hakee kaikki rivit
GET haePaikantiedot paikka_id Hakee yksittäiseen paikkaan liittyvät ri- vit.
GET haeAvaimella id Hakee avaimella yh-
den rivin.
GET getjson -
Hakee kaikki rivit, mutta palauttaa paikkatiedon
GeoJSON muodossa.
POST luo - Tekee uuden rivin.
PUT paivita - Päivittää olemassa
olevan rivin (kaikki tiedot)
DELETE poista id Poistaa olemassa
olevan rivin.
PATCH Tätä ei toteutettu, koska ei tarvetta
5.5 API-luokat
API-luokat periytyvät Controller-luokasta ja ne ovat varsinainen työ suorit- taja. Ne kuormittavat halutut funktiot ja toteuttavat ne halutulla tavalla.
Ne alustavat tarvittavat DAO-luokat ja käyttävät niitä tietojen hakemiseen, päivittämiseen ja poistamiseen. Lisäksi niissä toteutetaan tietojen validointi ennen tietokantaan vientiä.
5.6 Skeemat
Skeemat määrittävät millaisia JSON-olioita REST API -rajapinnat voivat ot-
taa vastaan. JSON-oliot validoidaan tarvittaessa skeemoja vastaan ja jos
olio on puutteellinen, niin toimintoa ei jatketa. Skeemassa määritellään tie-
torakenteen, niiden tietotyypit, pakollisuudet ja rajoitukset, esim. pituu-
det, muodot, jne.
Alla on esimerkki skeemasta:
{ "$schema": "http://json-schema.org/draft- 04/schema#",
"title": "linkki",
"description": "paikkaan liityvat www linkit", "type": "array",
"properties": { "linkki_id": {
"description": "Linkin uniikki id", "type": "integer"
},
"paikka_id": {
"type": "integer"
}, "linkki": {
"description": "Linkin kuvaus", "type": "string",
"minLength": 12, "maxLength": 255 }
},
"required": ["paikka_id", "linkki"]
}
6 REST API KÄYTTÖ
6.1 Osoitteet
Osoite muodostuu URLin alkuosasta /api/paikkaapi.php ja URLin loppu-
osasta, joka on alla olevassa taulukossa. Loppuosa kuvaa käytettävän APIn.
Taulukko 2. RESTful-palveluiden osoitteet
URL Api Selitys Tuetut toimin-
not /paikka/ paikka api Paikka ja paikkatie-
totaulujen hallin- nointiin käytetty api
lisäys, päivitys, poisto, haku ja GeoJSON haku
/kuva/ kuva api Paikkojen kuvien hallinnointiin käy- tetty api.
lisäys, poisto, haku
binaarihaku (image) /linkki/ linkki api Paikkojen www-link-
kien hallinnointiin käytetty api.
lisäys, päivitys, poisto ja haku /luokittelu/ luokittelu
api Luokittelujen ha-
kuun käytetty api. haku
/yhteystieto/ yhteystieto api
Paikkojen yhteys- henkilötietojen hal- linnointiin käytetty api.
lisäys, päivitys, poisto, ja haku
/aukiolo/ aukioloaika api
Paikkojen aukioloai- kojen hallinnointiin käytetty api
lisäys, päivitys, poisto ja haku
6.2 Haku
Kaikki ylläkuvatut APIt tukevat hakua id:llä. Paikka API tukee kaikkien paik- katietojen hakua ja suodatettua hakua, joka palauttaa hakukriteerit täyttä- vät paikat. Luokittelu API puolestaan tukee ainoastaan kaikkien kannassa olevien luokittelutietojen hakua. Muut APIt tukevat yksittäisen paikan tietojen hakua.
6.2.1 Kaikkien tietojen haku
Toimii nykyisellään vain paikka APIn kanssa. Urlin loppuosaan laitetaan sana ”kaikki”. Tällöin palautetaan kaikki paikkatiedot. Kaikki paikkatiedot voidaan hakea myös geojson muotoisena, jolloin urlin loppuosaan laite- taan sana ”geojson”.
6.2.2 Suodatettu haku
Suodatettu haku toimii nykyisellään vain paikka APIn kanssa. Halutut haku-
kriteerit laitetaan urlin loppuun. esim. /paikka/?osoite=Hämeenl. Tieto-
kannasta haetaan LIKE tai sama kuin-haulla hakukriteerejä vastaavat rivit
ja palautetaan ne kutsujalle. Sallitut hakukriteerit ovat alla olevassa taulu- kossa:
Taulukko 3. Sallitut hakukriteerit
Parametri Tietotyyppi Selitys
paikka_id numero Hakee paikka taulusta paikka_id kentän perusteella eli palauttaa yhden rivin id:llä haettuna.
luokittelu_id numero Palauttaa ne rivit, joilla luokit- telu_id on sama kuin parametrin arvot.
nimi merkkijono Paikka taulun nimi kentän mu- kaan tapahtuva LIKE-haku.
kuvaus merkkijono Paikka taulun kuvaus kentän mukaan tapahtuva LIKE-haku.
paikkatieto[osoite] merkkijono Paikkatietotaulun osoite kentän mukaan tapahtuva LIKE-haku.
6.2.3 Haku id:llä
Id:llä hakeminen onnistuu, kun URLin loppuun lisätään halutun rivin id. Eli esim. /linkki/15 palauttaa tietokannassa linkki-taulussa olevan rivin, jonka id on 15.
6.2.4 Haku paikan id:llä
Tällä haulla voidaan hakea paikkaan liittyviä yhteystietoja, linkkejä, kuvia ja aukioloja. URL muodostuu seuraavasti: paikka/<id>/<api>. Missä <id> on paikan id ja <api> on halutun APIn nimi (yhteystieto, kuva, linkki, aukiolo).
6.2.5 Muut APIt
Luokittelu APIn avulla voidaan hakea tietokannassa olevat luokittelu. Se tu- kee vain hakua ja urlin loppuun on laitettava ”kaikki”.
6.3 Lisäys
Lisäykset menevät normaalisti paikkaapi.php kautta ja kutsu urlissa on ha-
luttu API. HTTP-verbi on tällöin POST ja JSON data tulee pyynnön request-
payload:ssa.
6.4 Päivitys
Päivitykset menevät normaalisti paikkaapi.php kautta ja kutsu urlissa on haluttu API. HTTP-verbi on tällöin PUT ja JSON data tulee pyynnön request- payload:ssa.
6.5 Poisto
Päivitykset menevät normaalisti paikkaapi.php kautta ja kutsu urlissa on haluttu API. HTTP-verbi on tällöin DELETE ja poistettavan tietueen id on ur- lin loppuosassa.
7 KÄYTTÖLIITTYMÄKERROS (ENGLANNIKSI PRESENTATION LAYER)
7.1 Arkkitehtuuri
Käyttöliittymäkerrokseen toteutettiin kaksi yhden html-sivun sovellusta:
karttasovellus ja ylläpitosovellus. Käyttöliittymäpuolelle otettiin mukaan kuvassa näkyvät avoimeen lähdekoodiin perustuvat kirjastot. Kirjastojen käyttötarkoitus on selitetty luvussa 7.2.
Kuva 7. Käyttöliittymäkerroksen kirjastot
7.2 Käytetyt valmiit kirjastot
7.2.1 jQuery
jQuery-kirjasto on nopea ja pieni JavaScript-kirjasto, jonka helpottaa HTML dokumenttien (englanniksi view) käsittelyä ja Ajax-kutsujen tekemistä. Se toimii useimmissa markkinoilla olevissa selaimissa.
Se on hyvä pohjaratkaisu REST-palvelukutsujen tekemiseen. Sen avulla voi- daan helposti tehdä tarvittavat REST-kutsut palvelimelle. Lisäksi siihen on saatavilla useita kirjastoja jotka laajentavat sen toimintaa.
Alla on esimerkki REST-palvelun kutsumisesta jQuery-kirjastolla:
return $.ajax({
type: "POST",
url: "../api/paikkaapi.php/paikka, contentType: 'application/json', dataType: "JSON",
data: JSON .stringify(item) });
7.2.2 jQuery UI
jQuery UI on jQuery-kirjaston päälle rakennettu kevyt käyttöliittymäkir- jasto. Se sisältää useita käteviä käyttöliittymäkomponentteja. Tässä työssä kuitenkin käytetään vain Dialog-komponenttia, jonka avulla voidaan hel- posti avata HTML div omaan modaaliin ikkunaan. Dialogin avaus onnistuu alla olevalla JavaScript-koodilla:
$( "#lisaakuvaDialog" ).dialog();
Tämä avaa uuden ikkunan, jonka sisällä on div, jonka id on ”lisaaKuva- Dialog”. Eli alla oleva div:
<div class="hiddenform" id="lisaakuvaDialog"
title="Lisää kuva">
<form id="lisaaKuvaForm">
<fieldset>
<input type="hidden" name="paikka_id"
id="kuva_paikka_id" value="15"/>
<label class="label" for="kuva">Tallenna kuva</label>
<input type="file" name="kuva" id="kuva">
<input class="button" type="button" on- click=" submitLisaaKuva ();" value="Lisää" />
</fieldset>
</form>
</div>
7.2.3 jsGrid
jsGrid-kirjasto on myös jQuery-kirjaston päälle kirjoitettu kirjasto. Sen avulla voidaan käyttöliittymän puolella näyttää tietoja taulukkomuodossa.
Se mahdollistaa myös tietojen muokkaamisen, suodattamisen, poistami- sen, sivutuksen ja järjestämisen.
Tässä työssä sitä käytetään näytöillä, joissa pitää näyttää useita tietokan- nan rivejä. Taulukoissa näytettävät tiedot haetaan palvelimelta REST API kautta. Myös päivitykset, lisäykset ja poistot tehdään taulukko komponen- tin ja REST APIn avulla.
Taulukko komponentin ja palvelimen väliin on tehty oma JavaScript luokka (RestController), jonka kautta kaikki taulukko komponentin palvelukutsut kulkevat. RestController sisältää funktiokutsut kaikille operaatiolle: tieto- jen haku, lisäys, poisto, päivitys.
7.2.4 Serialize
Tämän kirjaston avulla HTML lomake voidaan muuttaa JSON-muotoon. Se helpottaa paljon ajax-kutsujen tekemistä, koska lomaketta ei tarvitse itse käydä kenttä kentältä läpi, vaan kirjasto tekee siitä JSON-olion.
7.2.5 GoogleApi
GoogleApi:sta käytetään Googlen geokoodaus-palvelua. Sen avulla voi- daan selvittää karttapisteen koordinaattien avulla katuosoite. Osoitteen selvittämistä tarvitaan ylläpitokäyttöliittymissä. Paikan osoittavaa ikonia halutaan siirtää kartalla ja uuden paikan osoite halutaan tallentaa tietokan- taan.
7.2.6 Leaflet
LeafLet-kirjaston avulla ladataan karttapohja OpenStreetMap-palvelusta.
Lisäksi sen avulla näytetään geojson muotoiset karttakohteet omana taso- naan kartalla. Kirjastoa käytetään sekä ylläpitokäyttöliittymissä, että kart- takäyttöliittymässä. Alun perin ajatuksena oli käyttää OpenLayers-kirjas- toa, mutta LeafLet korvasi sen. OpenLayers olisi ollut turhan raskas ja Leaf- Let:ssä oli kaikki tarvittavat toiminnallisuudet.
7.2.7 OpenStreeMap
Pohjakartta haetaan OpenStreetMap-palvelusta. Alun perin ajateltiin käyt-
tää GoogleMaps-palvelun kartta-aineistoja, mutta ilmaisessa OpenStreet-
Map:ssa oli tarvittavat ominaisuudet, joten se valittiin. Karttapohjan vaih-
taminen on tosin helppoa koska ne ladataan LeafLet-kirjaston kautta.
7.3 Tietojen syöttäminen
Ylläpitokäyttöliittymä toteutettiin välilehtiratkaisuna. Näytöllä on viisi väli- lehteä, yksi kutakin syötettävää tietoa kohden. Ensimmäisellä välilehdellä on lisäksi kartta, jonka avulla karttakohdetta voi visuaalisesti siirtää oike- aan kohtaan. Tietojen syöttäminen hoidetaan taulukon avulla.
Kuva 8. Ylläpitokäyttöliittymä
7.4 Kartta
Karttakäyttöliittymä yksinkertaisuudessaan näyttää kohteet kartalta.
Kohdetta klikattaessa käyttäjälle näytetään tarkemmat tiedot kohteesta.
Kuva 9. Karttakäyttöliittymä
8 YHTEENVETO
Opinnäytetyön tarkoituksena oli toteuttaa paikkatietokanta Kanta-Hämee- seen. Tuloksena syntyi avoin rajapinta ja tarvittavat peruskäyttöliittymät.
Toteutuksessa käytettiin paljon avoimen lähdekoodin kirjastoja ja niiden yhteensovittaminen oli välillä haastavaa.
Opinnäytetyön aikana osa suunnitelluista teknologioista vaihdettiin.
GoogleMaps vaihdettiin OpenStreetMap-palveluun, koska se oli ilmainen ja käyttötarkoitukseen riittävä. Alun perin ajateltiin käyttää GeoServer-pal- velua karttatasojen esittämiseen. Sovelluksessa ei kuitenkaan ollut varsi- naisia karttatasoja, esim. omia pohjakarttoja, vaan vain pistemäisiä koh- teita. Pistemäisten kohteiden näyttämiseen GeoServer olisi ollut turhan raskas vaihtoehto. Tämän vuoksi sovelluksessa päädyttiin käyttämään LeafLet-kirjaston geojson tasoja. GeoJSON on muutenkin yleistymässä ja hyvin tuettuna mobiililaitteissa ja se voi sisältää myös omia tietoja, kuten tässä sovelluksessa haluttiin.
MVC-malli ja RESTful-arkkitehtuurimallit helpottivat tekemistä. Ne saneli- vat yhtenäisen tavan tehdä asioita. Myös jatkokehitys on helpompaa, kun asiat on tehty samalla tavalla. Aluksi tietokanta operaatiot tehtiin suoraan PHP:n tietokantaluokkien avulla. Tämä kuitenkin osoittautui työlääksi ja sen vuoksi käyttöön otettiin Doctrine-kirjasto. Se helpotti tietokantaohjel- mointia huomattavasti.
Lopputuloksena syntyi jatkokehitettävä paikkatietokanta, johon on mah-
dollista säilöä paljon hyödyllistä tietoa. Avoimet rajapinnat mahdollistavat
uusien käyttöliittymien tekemisen. Käyttäjähallinta ja mobiilikäyttöliittymä
ovat varmasti seuraavia jatkokehityskohteita tälle sovellukselle.
LÄHTEET
codecademy. (ei pvm). MVC: Model, View, Controller. Haettu 25. 03 2019 osoitteesta https://www.codecademy.com/articles/mvc
R. Fielding, J. R. (2014). Hypertext Transfer Protocol (HTTP/1.1): Semantics
and Content. Haettu 08. 04 2019 osoitteesta
https://tools.ietf.org/html/rfc7231#section-4.2.3
Reenskaug, T. (1979). MODELS - VIEWS - CONTROLLERS. Haettu 25. 03 2019 osoitteesta http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf RESTfulAPI.net, REST Architectural Constraints. (2017b). REST Architectural Constraints. Haettu 07. 04 2019 osoitteesta https://restfulapi.net/rest- architectural-constraints/
RESTfulAPI.net, What is REST. (2017a). What is REST. Haettu 07. 04 2019 osoitteesta https://restfulapi.net/
Trygve Mikkjel Heyerdahl Reenskaug. (2003). MVC: Model-View-
Controller. Haettu 20. 03 2019 osoitteesta
http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html
Wiki.c2. (2014). Model View Controller History. Haettu 02. 04 2019 osoitteesta http://wiki.c2.com/?ModelViewControllerHistory
Wikipedia. (2019a). Model–view–controller. Haettu 06. 04 2019 osoitteesta
https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93control ler
Wikipedia. (2019b). Representational state transfer. Haettu 06. 04 2019 osoitteesta
https://en.wikipedia.org/wiki/Representational_state_transfer
Liite 1 TERMIT
HTTP
HyperText Transfer Protocol. Selaimen ja palvelimen väliseen tiedonsiir- toon käytettävä protokolla.
JAVASCRIPT
Ohjelmointikieli, jota käytetään lisäämään dynaamista toiminnallisuutta www-sivuille.
JSON
JavaScript Object Notation. Yksikertainen tiedonvälitykseen käytetty for- maatti.
MVC MVC-arkkitehtuurimalli on ohjelmistoarkkitehtuurimalli, joka jakaa ohjel- miston kolmeen osaan: malli (englanniksi model), näkymä (englanniksi view) ja ohjain (englanniksi controller).
ORM Object Relational Mapping. Tekniikka, jota käytetään olio-ohjelmointikie- lissä tiedon muuntamiseen yhteensopimattomien järjestelmien kanssa.
PHP PHP on palvelinpuolen ohjelmointikieli web kehitykseen.
Liite 2 RESTAPI LÄHDEKOODIT
./api/api/aukioloapi.class.php
<?php
include_once 'controller.class.php';
class AukioloApi extends Controller { private $aukiolodao;
function __construct() {
$this->aukiolodao = new AukioloDAO();
}
function luo($aukiolo) {
$aukiolo_id = $this->aukiolodao->luoAukiolo($aukiolo);
error_log ("aukioloid1 " . $aukiolo_id);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['maanantai'], 1);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['tiistai'], 2);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['keskiviikko'], 3);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['torstai'], 4);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['perjantai'], 5);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['lauantai'], 6);
$id = $this->tallennaPaiva($aukiolo_id, $aukiolo, $aukiolo['sunnuntai'], 7);
if($id != null) {
return $this->haeAvaimella($id);
} return null;
}
function tallennaPaiva($aukiolo_id, $aukiolo, $paiva, $tyyppi_id) { if(!isset($paiva['alkuaika']) || !isset($paiva['loppuaika'])) return -1;
error_log ("aukioloid " . $aukiolo_id);
$paiva['alkupaiva'] = $aukiolo['alkupaiva'];
$paiva['loppupaiva'] = $aukiolo['loppupaiva'];
$paiva['paikka_id'] = $aukiolo['paikka_id'];
$paiva['tyyppi_id'] = $tyyppi_id;
$paiva['aukiolo_id'] = $aukiolo_id;
$id = $this->aukiolodao->luoAukioloaika($paiva);
}
function haePaikantiedot($paikka_id) {
$linki = $this->aukiolodao->haePaikanAukiolot($paikka_id);
return $linki;
}
function haeAvaimella($id) {
$aukiolo = $this->aukiolodao->haeAukiolo($id);
return $aukiolo;
}
function poista($id) {
return $this->aukiolodao->poistaAukiolo($id);
} }
?>
./api/api/controller.class.php
<?php
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
class Controller {
private function getJsonObject() {
$contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
if(strcasecmp($contentType, 'application/json') != 0){
throw new Exception('Content type must be: application/json');
}
$content = trim(file_get_contents("php://input"));
$jsonobject = json_decode($content, true);
return $jsonobject;
}
public function dispatch($subAction,$id) { if ($this->customdispatch($subAction,$id)) { return;
}
$httpverb = strtoupper($_SERVER['REQUEST_METHOD']);
$result = null;
switch($httpverb) { case 'GET':
if(isset($subAction)) { if($subAction === "paikka") {
$result = $this->haePaikantiedot($id);
if(!isset($result)) {
return $this->virhe("no data found for id ".$id);
} } }
if(isset($id) && $result === null) { if($id === "kaikki") { $result = $this->haeKaikki();
} else if($id === "geojson") { $result = $this->geojson();
} else {
$result = $this->haeAvaimella($id);
} } else {
if($result === null) { $result = $this->haeKaikki();
} } break;
case 'POST':
$newobject = $this->getJsonObject();
$result = $this->luo($newobject);
break;
case 'PUT':
$newobject = $this->getJsonObject();
$result = $this->paivita($newobject);
break;
case 'DELETE':
if(isset($id)) { $this->poista($id);
} else {
throw new Exception("please give id!");
}
break;
case 'PATCH':
throw new Exception("not supported by this api. contact joni....");
break;
default:
$result = "{ \"error\": \"not implemented\"";
break;
}
if($result != null) {
header('Content-type: application/json; charset=iso-8859-1');
echo json_encode($result);
return;
} }
function customdispatch($subAction,$id) { return false;
}
function virhe($teksti) {
header('Content-type: application/json; charset=iso-8859-1');
echo "{ \"error\": \"" .$teksti. "\" }";
}
function geojson() {
return "{ \"error\": \"not implemented\"";
}
function haePaikantiedot($paikka_id) {
return "{ \"error\": \"haePaikantiedot not implemented\"";
}
function haeKaikki() {
return "{ \"error\": \"haeKaikki not implemented\"";
}
function luo($newobject) {
return "{ \"error\": \"luo not implemented\"";
}
function paivita($newobject) {
return "{ \"error\": \"paivita not implemented\"";
}
function haeAvaimella($id) {
return "{ \"error\": \"haeAvaimella not implemented\"";
}
function poista($id) {
return "{ \"error\": \"poista not implemented\"";
}
function validoi($schemajson,$newobject) { $validator = new Validator;
$realpath = realpath(__DIR__.'/schema/'.$schemajson);
$schema = 'file://'.$realpath;
$validator->validate(
$newobject,
(object)['$ref' => $schema], Constraint::CHECK_MODE_APPLY_DEFAULTS );
if (!$validator->isValid()) {
header('Content-type: application/json; charset=iso-8859-1');
echo json_encode($validator->getErrors());
return false;
} return true;
} }
?>
./api/api/kuvaapi.class.php
<?php
class KuvaApi extends Controller { private $kuvadao;
function customdispatch($subAction,$id) { if($subAction === "getimage") { $kuva = $this->getImage($id);
header('Content-type: ' . $kuva->getTyyppi());
$binary = base64_decode(substr($kuva->getKuva(),37));
echo $binary;
return true;
}
return false;
}
function __construct() { $this->kuvadao = new KuvaDAO();
}
function getImage($id) {
$kuva = $this->kuvadao->haeKuva($id);
return $kuva;
}
function haeAvaimella($id) {
$kuva = $this->kuvadao->haeKuva($id);
return $kuva;
}
function luo($newobject) {
$kuva_id = $this->kuvadao->luoKuva($newobject);
if($kuva_id != null) {
return $this->haeAvaimella($kuva_id);
} return null;
}
function haePaikantiedot($paikka_id) {
return $this->kuvadao->haePaikanKuvat($paikka_id);
}
function poista($id) {
$this->kuvadao->poistaKuva($id);
} }
?>
./api/api/linkkiapi.class.php
<?php
include_once 'controller.class.php';
class LinkkiApi extends Controller { private $linkkidao;
function __construct() {
$this->linkkidao = new LinkkiDAO();
}
function luo($newobject) {
if ($this->validoi("linkkischema.json", $newobject)) { $id = $this->linkkidao->luoLinkki($newobject);
if($id != null) {
return $this->haeAvaimella($id);
} }
return null;
}
function haePaikantiedot($paikka_id) {
$linki = $this->linkkidao->haePaikanLinkit($paikka_id);
return $linki;
}
function haeAvaimella($id) {
$linki = $this->linkkidao->haelinkki($id);
return $linki;
}
function poista($id) {
$linki = $this->linkkidao->poisLinkki($id);
return $linki;
}
function paivita($newobject) {
if ($this->validoi("linkkischema.json", $newobject)) { $linki = $this->linkkidao->paivitaLinkki($newobject);
return $linki;
} return null;
} }
?>
./api/api/luokitteluapi.class.php
<?php
class LuokitteluApi extends Controller { private $luokitteludao;
function __construct() {
$this->luokitteludao = new LuokitteluDAO();
}
function __destruct() { }
function haeKaikki() {
$luokittelut = $this->luokitteludao->haeLuokittelut();
return $luokittelut;
} } ?>
./api/api/paikkaapi.class.php
<?php
include_once 'controller.class.php';
class PaikkaApi extends Controller { private $paikkadao;
private $aukiolodao;
private $yhteystietodao;
private $kuvadao;
private $linkkidao;
function __construct() {
$this->paikkadao = new PaikkaDAO();
$this->aukiolodao = new AukioloDAO();
$this->yhteystietodao = new YhteystietoDAO();
$this->kuvadao = new KuvaDAO();
$this->linkkidao = new LinkkiDAO();
}
function __destruct() { }
function geojson() {
return $this->paikkadao->haeGeot()->values();
}
function haeKaikki() { $whereclause = "";
foreach($_GET as $key=>$val) { if(isset($val) && $val != null) { $condition = "";
if($key != "luokittelu_id" && $key != "paikkatieto") { $condition = "p." . $key . " like '" . $val . "%'";
} else if($key === "luokittelu_id" && $val != 0) { $condition = "p." . $key . " = " . $val;
} else if($key === "paikkatieto" && $val['osoite'] != null) { $condition = "t.osoite like '" . $val['osoite'] . "%'";
}
if($condition != "") {
$whereclause = $whereclause . " AND " . $condition;
} } }
$paikat = $this->paikkadao->haePaikat($whereclause);
return $paikat;
}
function luo($newobject) {
$id = $this->paikkadao->luoPaikka($newobject);
if($id != null) {
return $this->haeAvaimella($id);
} return null;
}
function paivita($newobject) {
return $this->paikkadao->paivitaPaikka($newobject);
}
function haeAvaimella($id) {
$paikka = $this->paikkadao->haePaikka($id);
return $paikka;
}
function poista($paikka_id) {
$this->yhteystietodao->poistaPaikanYhteystiedot($paikka_id);
$this->linkkidao->poistaPaikanLinkit($paikka_id);
$this->kuvadao->poistaPaikanKuvat($paikka_id);
$this->aukiolodao->poistaPaikanAukiolot($paikka_id);
return $this->paikkadao->poistaPaikka($paikka_id);
} }
?>
./api/api/schema/linkkischema.json
{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "linkki",
"description": "paikkaan liityvat www linkit", "type": "array",
"properties": { "linki_id": {
"description": "The unique identifier for a link", "type": "integer"
},
"paikka_id": { "type": "integer"
}, "linki": {
"description": "Name of the product", "type": "string",
"minLength": 12, "maxLength": 255 }
},
"required": ["paikka_id", "linki"]
}
./api/api/yhteystietoapi.class.php
<?php
include_once 'controller.class.php';
class YhteystietoApi extends Controller { private $yhteystietodao;
function __construct() {
$this->yhteystietodao = new YhteystietoDAO();
}
function luo($newobject) {
$id = $this->yhteystietodao->luoYhteystieto($newobject);
if($id != null) {
return $this->haeAvaimella($id);
} return null;
}
function haeAvaimella($id) {
$yhteystieto = $this->yhteystietodao->haeyhteystieto($id);
return $yhteystieto;
}
function poista($id) {
return $this->yhteystietodao->poistaYhteystieto($id);
}
function haePaikantiedot($paikka_id) {
return $this->yhteystietodao->haePaikanYhteystiedot($paikka_id);
}
function paivita($yhteystieto) {
return $this->yhteystietodao->paivitaYhteystieto($yhteystieto);
} }
?>
./api/dao/aukiolodao.class.php
<?php
class AukioloDAO extends BaseDao{
public function luoAukioloAika($aukioloaika){
$aukioloaikaEntity = new Aukioloaika(); //make new model $aukioloaikaEntity->fromJson($aukioloaika);
error_log ("aukioloid " . $aukioloaikaEntity->getAukioloId());
$this->getEntityManager()->persist($aukioloaikaEntity);
$this->getEntityManager()->flush();
return $aukioloaikaEntity->getAukioloaika_id();
}
public function luoAukiolo($aukiolo){
$aukiolo_id = $this->getSequenceNextValue(null, "paikkatieto.aukiolo_aukiolo_id_seq");
$aukioloEntity = new Aukiolo(); //make new model $aukioloEntity->fromJson($aukiolo);
$aukioloEntity->setAukioloId($aukiolo_id);
$this->getEntityManager()->persist($aukioloEntity);
$this->getEntityManager()->flush();
return $aukiolo_id;
}
public function haeAukiolo() { $aukiolo = new Aukiolo();
return $aukiolo;
}
public function poistaPaikanAukiolot($paikka_id) {
$query = $this->getEntityManager()->createQuery("SELECT a FROM aukioloaika a WHERE a.paikka_id = " .
$paikka_id);
$aukioloajat = $query->getResult();
foreach($aukioloajat as $aukioloaika) {
$this->getEntityManager()->remove($aukioloaika);
}
$query = $this->getEntityManager()->createQuery("SELECT a FROM aukiolo a WHERE a.paikka_id = " . $paikka_id);
$aukiolot = $query->getResult();
foreach($aukiolot as $aukiolo) {
$this->getEntityManager()->remove($aukiolo);
}
$this->getEntityManager()->flush();
return "success";
}
public function haePaikanAukiolot($paikka_id) {
$query = $this->getEntityManager()->createQuery("SELECT a FROM aukiolo a WHERE a.paikka_id = " . $paikka_id);
$aukiolot = $query->getResult();
return $aukiolot;
}
public function poistaAukiolo($id) { $conn = $this->getConnection();
$poistaPaikkaQuery = "delete from paikkatieto.aukioloaika where aukiolo_id = " . $id . ";";
$result = pg_query($conn, $poistaPaikkaQuery);
$paikkaEntity = $this->getEntityManager()->find("aukiolo", $id);
$this->getEntityManager()->remove($paikkaEntity);
$this->getEntityManager()->flush();
return "success";
} } ?>
./api/dao/basedao.class.php
<?php
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
class BaseDao { private $geojson;
public $FAILED = "failed";
public $SUCCESS = "success";
private $entityManager;
function __construct() { $this->geojson = new GeoJSON();
}
public function getConnection() {