ProGradu_Aki_Heikkin..

GRAILS JA LIFT – OHJELMISTOKEHYSTEN
VERTAILU WEB-SOVELLUSTEN EVOLUTIIVISESSA PROTOILUSSA
Aki Heikkinen
28.11.2010
Itä-Suomen yliopisto
Tietojenkäsittelytiede
Pro gradu -tutkielma
TIIVISTELMÄ
Evolutiivisen protoilun ja ohjelmistokehyksien hyödyntäminen web-sovellusprojekteissa
on osoittautunut tehokkaaksi valinnaksi useampiin projekteihin. Evolutiivinen protoilu
tarjoaa nopeaan ohjelmistokehitykseen perustuvan inkrementaalisen kehitysmallin. Tätä
mallia hyödyntäen kussakin kehitysvaiheessa kehitettävästä web-sovelluksesta tuotetaan
prototyyppi, jonka avulla asiakkaalta kerätään palautetta parantaakseen lopullisen toimitustuotteen laatua. Grails ja Lift ovat ohjelmistokehyksiä, joilla voi tuottaa tehokkaasti
nykypäivän web-standardien ja teknologioiden vaatimukset täyttäviä web-sovelluksia.
Molemmilla ohjelmistokehyksillä on kuitenkin toisistaan hyvin erilainen lähestymistapa.
Grails-ohjelmistokehys pohjautuu dynaamiseen Groovy-ohjelmointikieleen ja Liftohjelmistokehys pohjautuu funktionaaliseen Scala-ohjelmointikieleen. Molemmat ohjelmointikielet ovat olio-ohjelmointikieliä, jotka tuottavat puhdasta Java-virtuaalikoneeseen
soveltuvaa tavukoodia.
ACM-luokat (ACM Computing Classification System, 1998 version): D.2.2, D.2.3, D.3.3
Avainsanat: Groovy, Grails, Lift, ohjelmistokehys, evolutiivinen protoilu, prototyyppi, Scala, web-sovellus
SISÄLLYSLUETTELO
1
JOHDANTO ............................................................................................................. 1
1.1 Taustaa .............................................................................................................. 2
1.2 Rakenne............................................................................................................. 3
2
EVOLUTIIVINEN PROTOILU ............................................................................... 3
2.1
2.2
2.3
2.4
3
Yleistä ............................................................................................................... 4
Liiketoiminnan näkökulma evolutiiviseen protoiluun ...................................... 5
Evolutiivisen protoilun edut ja heikkoudet ....................................................... 5
Käyttöliittymän evolutiivinen protoilu ............................................................. 7
OHJELMISTOKEHYS............................................................................................. 9
3.1 Ohjelmistokehyksien taustaa ............................................................................ 9
3.2 Ohjelmistokehyksien rakenne ......................................................................... 11
3.3 Web-sovellusohjelmistokehys ........................................................................ 13
4
GRAILS .................................................................................................................. 14
4.1
4.2
4.3
4.4
Groovy-ohjelmointikieli ................................................................................. 15
Rakenne ja arkkitehtuuri ................................................................................. 17
Grails web-sovelluksien rakenne .................................................................... 18
Grails kehitysalustana ..................................................................................... 21
5
LIFT ........................................................................................................................ 22
5.1
5.2
5.3
5.4
5.5
6
Scala-ohjelmointikieli ..................................................................................... 23
Rakenne ja arkkitehtuuri ................................................................................. 25
Snippet-komponentit ....................................................................................... 27
Toimijamalli.................................................................................................... 28
AJAX- ja Comet-ohjelmointi.......................................................................... 29
VERTAILU............................................................................................................. 31
6.1 Yleiskuva kehitettävästä web-sovelluksesta ................................................... 32
6.2 Ensimmäinen vaihe ......................................................................................... 33
6.2.1
Projektin alustaminen...................................................................... 33
6.2.2
Verkkosivupohjan toteuttaminen .................................................... 34
6.2.3
Sivujen ja linkitysten toteuttaminen................................................ 37
6.3 Toinen vaihe.................................................................................................... 40
6.3.1
Monikielisyystuen toteuttaminen .................................................... 41
6.3.2
Palaute ominaisuus.......................................................................... 43
6.3.3
Sisäänkirjautuminen ja käyttäjän tunnistaminen ............................ 47
6.3.4
Palautteen selailuominaisuus ylläpitäjälle ...................................... 49
6.4 Kolmas vaihe .................................................................................................. 52
6.4.1
Dynaamiset sivut Grails-projektissa ............................................... 52
6.4.2
Dynaamiset sivut Lift-projektissa ................................................... 54
6.4.3
Vertailu dynaamisten sivujen toteuttamisessa ................................ 57
6.5 Toimitusvaihe ................................................................................................. 57
6.6 Yleinen vertailu ............................................................................................... 58
7
YHTEENVETO ...................................................................................................... 61
VIITTEET ....................................................................................................................... 63
LIITE Y1: Java-toteutus esimerkkitehtävästä
LIITE Y2: Groovy-toteutus esimerkkitehtävästä
LIITE Y3: Scala-toteutus esimerkkitehtävästä
LIITE G1: Grails web-sovelluksen banneri-komponentin toteutus
LIITE G2: Grails web-sovelluksen pääsivun toteutus
LIITE G3: Grails web-sovelluksen etusivukomponentin toteutus
LIITE G4: Yhden hallintaluokan toteutus
LIITE G5: Palautteen antamistoimintoa vastaava GSP-sivu
LIITE G6: Ylävalikon GSP-sivutoteutus
LIITE G7: Kielivalikko hallintaluokan toteutus
LIITE G8: Palautteen toimintoalueluokka
LIITE G9: Palautteen hallintaluokka
LIITE G10: Type-toimintoalueluokan toteutus
LIITE G11: Category-toimintoalueluokan toteutus
LIITE G12: Languages-toimintoalueluokan toteutus
LIITE G13: Page-toimintoalueluokan toteutus
LIITE G14: Pagecontent-toimintoalueluokan toteutus
LIITE G15: Dynaamisen luonteen mahdollistava yleinen GSP-sivu
LIITE G16: Dynaamisen hahmottamisen mahdollistava hallintaluokka.
LIITE G17: Apuluokka dynaamisen hahmotuksen toteuttamiseen
LIITE G18: Pagecontent-instanssin muokkaussivu
LIITE G19: Tietokantamääritys DataSource.groovy tiedostossa
LIITE L1: Lift web-sovelluksen staattisen banneri-sivupohjan toteutus
LIITE L2: Lift web-sovelluksen frontpage-sivupohjan toteutus
LIITE L3: Lift web-sovelluksen pääsivun toteutus
LIITE L4: Main pääkategoriaan sisältyvien alakategorialinkkien hahmottaminen
LIITE L5: SiteMap-komponentin toteutus ensimmäisessä vaiheessa.
LIITE L6: Lokalisoinnin määritysfunktio
LIITE L7: Käytettävän lokaalin valintatoiminnallisuus
LIITE L8: Palautetta vastaava toimintoaluemalli
LIITE L9: Tietokantayhteys olion toteutus
LIITE L10: Palautteenantolomake
LIITE L11: Palautteenantolomakkeen käsitelevä Snippet-luokka
LIITE L12: Käyttäjää mallintava toimintoalue luokkatoteutus
LIITE L13: Sivupohja-apuluokan Snippet-komponentti toteutus
LIITE L14: Palautteiden selailuominaisuuden sivupohjatoteutus
LIITE L15: Palautteiden selailuominaisuuden Snippet-komponentti
LIITE L16: Yksittäisten palautteiden näkymä-sivupohja
LIITE L17: Yksittäisten palautteiden näkymän Snippet-komponentti
LIITE L18: Type-toimintoalueluokan toteutus
LIITE L19: Category-toimintoalueluokan toteutus
LIITE L20: Languages-toimintoalueluokan toteutus
LIITE L21: Page-toimintoalueluokan toteutus
LIITE L22: Pagecontent-toimintoalueluokan toteutus
LIITE L23: Dynaamisen sivuluonteen mahdollistava sivupohja
LIITE L24: Pagecontent-instanssin muokkaussivupohja
LIITE L25: Dynaamisen sivupohjaluokan Snippet-komponentti
LIITE L26: Yleinen apuluokka sivujenkäsittelyyn
1 JOHDANTO
Nykypäivän web-sovelluksilta odotetaan paljon ja web-kehitykseen liittyy yleensä useita eri standardeja ja teknologioita, joiden toteuttaminen alusta alkaen olisi hyvin työlästä ja hidasta. Samaan aikaan nykypäivän teollisuuden ja kaupallisuuden liiketoimintamallit vaativat nopeita ratkaisuja ja toteutuksia. Kaisler (2005) ja Sommerville (2007)
mukaan ohjelmistotuotannon vastaus nykypäivän vaatimuksiin on nopean ohjelmistokehityksen (rapid software development) ja ohjelmistokehysten (software frameworks)
hyödyntäminen ohjelmistoprojekteissa.
Sommerville (2007) mukaan nopeasta ohjelmistokehityksestä on tullut yksi tärkeimmistä nykypäivän ohjelmistoprojektien vaatimuksista, koska se voi reagoida ja vastata
nopeasti muuttuvaan liiketoimintaympäristöön. Yksi lähestymistapa nopeaan ohjelmistokehitykseen on hyödyntää evolutiivista protoilua, jossa kehitettävä sovellus on alusta
pitäen elävä ja muuttuva prototyyppi kunnes se saavuttaa valmiuden lopullista julkaisua
varten. Kaisler (2005) mukaan ohjelmistokehykset taas tarjoavat hyviksi todettujen
standardien ja teknologioiden toteutukset sekä laajennettavan pohjan erilaisille ohjelmistoprojekteille. Ohjelmistokehyksiä on hyvin monenlaisia ja ne on tavallisesti tarkoitettu tietynlaisia projekteja varten.
Tässä Pro Gradu-tutkielmassa tutkitaan ja verrataan kahta erilaisella lähestymistavalla
toteutettua web-sovellusohjelmistokehystä keskenään evolutiivisen protoilun näkökulmasta. Tutkittavat web-sovellusohjelmistokehykset ovat Groovy-ohjelmointikieleen
pohjautuva Grails ja Scala-ohjelmointikieleen pohjautuva Lift. Molemmat ohjelmointikielet ovat vaihtoehtoisia Java-virtuaalikone (JVM) kieliä, jotka kääntyvät lähdekoodista puhtaaksi JVM- tavukoodiksi ja niitä voi ajaa samanlaisessa ympäristössä. Grails on
kokonaisvaltainen ohjelmistokehys, joka tarjoaa työkaluja projektien luomisesta toimitukseen. Lift on vastaavasti joustava ja erityisen skaalatutuva ohjelmistokehys, joka
tarjoaa kehittäjille suuren vapauden valita erilaiset ratkaisumallit erilaisiin ongelmiin ja
rohkaisee kehittäjiä suunnittelemaan web-sovellukset funktionaalista ohjelmointiparadigmaa käyttäen (WorldWide Conferencing, LLC., 2010).
1
1.1 Taustaa
Sommerville (2007) mukaan ohjelmistokehittäjille on tarjolla useita eri lähestymistapoja prosessimalleja projektien läpivetämiseksi. Osa prosessimalleista pohjautuu kiinteisiin peräkkäisiin vaiheisiin, joissa vaatimusmäärittely, suunnittelu, implementaatio ja
testaaminen ovat omia itsenäisiä kehitysvaiheita. Tällaiset mallit hyödyntävät kiinteitä
vaatimusmäärittelyjä ja ne soveltuvat erityisen hyvin suuriin ohjelmistoprojekteihin,
jossa järjestelmän arkkitehtuuri tulee suunnitella alusta pitäen tietynoloiseksi. Näiden
mallien suurin heikkous on siinä, ettei niitä ole suunniteltu huomioimaan projektien
aikana ilmeneviä mahdollisia muutoksia. Muutokset ovat tyypillisiä erityisesti tilanteissa joissa kehittäjien ja asiakkaiden ymmärrykset ei täysin kohtaa tai esimerkiksi kun
asiakkaalla on visio, mutta ei täysin tiedä miten se tulisi toteuttaa, jotta se vastaisi tehokkaasti tarkoitustaan. Sommerville (2007) mukaan uudemmat ketterät ohjelmistoprosessit ottavat kantaa muutosten huomioimiseen ja mahdollistamiseen ohjelmistoprojektin aikana. Yksi tällainen ketterä lähestymistapa on evolutiivinen protoilu, jossa kehitettävä sovellus toteutetaan pienissä osissa. Stephens, et al. (2002) mukaan tarkoitus kussakin pienemmässä osassa on löytää kehittäjän ja asiakkaan välinen yhteisymmärrys
siitä mitä asiakas todella haluaa ja tarvitsee visionsa toteuttamiseen. Asiakkaan ja kehittäjän välinen yhteisymmärrys helpottaa lopullisten vaatimusmäärittelyn tuottamista ja
rajausta, jonka johdosta projektin resurssit voidaan pitää helpommin hallittavissa. Erityisesti web-sovelluskehityksessä evolutiivinen protoilu on havaittu tehokkaaksi lähestymistavaksi.
Ohjelmistoprosessimallien lisäksi ohjelmistokehitystä on pyritty helpottamaan ja standardisoimaan erilaisilla tekniikka- ja teknologiapohjaisilla lähestymistavoilla. Kaisler
(2005) mukaan yksi nykypäivän merkittävimmistä lähestymistavoista on hyödyntää
uudelleenkäytettäviä ohjelmistokehyksiä. Ohjelmistokehyksiä on useita erilaisia ja ne
yleensä keskittyvät tuottamaan teknillisen ratkaisumallin tietynoloista ohjelmistoa varten. Web-sovellusohjelmistokehykset ovat yksi ohjelmistokehyksien tyyppi ja näiden
päätavoite on tyypillisesti keventää kehittäjien taakkaa tarjoamalla valmiita de factostandarditoteutuksia ja joustavasti muokattavia ohjelmistokomponentteja web-pohjaisiin sovelluksiin. Tällainen lähestymismalli keventää kehittäjien tarvetta tutustua lukuisiin matalantason www-tekniikkatoteutuksiin, jolloin kehittäjillä on enemmän resursseja käytettävissä itse liiketoimintamallien suunnitteluun ja toteuttamiseen.
2
1.2 Rakenne
Tutkielma rakentuu kahdesta pääosasta: luvussa 2-5 esitetään aiheen teoreettiset osuudet ja kuvataan käytössä olevat tekniikat. Luvussa 6 suoritetaan tutkielman empiirinen
osuus. Aiheen käsittely aloitetaan evolutiivisen protoilun teoriasta ja sen hyödyistä ohjelmistokehityksessä. Luvussa kolme esitellään yleisesti mitä ohjelmistokehykset ovat
ja luvuissa neljä ja viisi perehdytään yksityiskohtaisemmin kahteen verrattavaan Grailsja Lift web-sovellusohjelmistokehykseen. Tarkasteltavia asioita ovat erityisesti molempien ohjelmistokehysten konseptit ja vahvuudet. Luvussa kuusi suoritetaan tutkielman
menetelmävertailu, jossa toteutetaan yksinkertainen, mutta ominaisuustäytteinen websovellus sekä Grails- että Lift-ohjelmistokehyksillä. Web-sovellus toteutetaan vaiheittain, jolla pyritään mallintamaan evolutiivista protoilua. Samassa luvussa keskitytään
löytämään myös web-sovellusohjelmistokehyksen eroja ja kartoittamaan kummankin
vahvuuksia ja heikkouksia. Viimeinen luku sisältää tutkielman yhteenvedon.
2 EVOLUTIIVINEN PROTOILU
Sommerville (2007) mukaan prototyypillä tarkoitetaan alustavaa ohjelman versiota,
jonka tarkoitus on demonstroida ohjelman konseptia, kokeilla erilaisia suunnitteluvaihtoehtoja ja yleisesti tarkentaa vaatimusmäärittelyjä. McConnel (2002) mukaan on olemassa kahdenlaisia prototyyppejä: pois-heitettäviä ja evolutiivisia. Kumpikin prototyyppimalli tarjoaa samat edut, mutta niiden tehokkuus voi vaihdella eri projekteissa.
Pois-heitettävän prototyypin tarkoitus on palvella projektia vain sen alkuvaiheissa, jonka jälkeen se heitetään pois ja virallinen tuote kehitetään alusta alkaen hyödyntäen tietoa, jota saatiin protoilulla. Evolutiivisella prototyypillä taas tarkoitetaan vaiheittain
kehittyvää ohjelmaa, josta lopuksi saadaan toimitettava tuote. Evolutiivista protoilua
voidaan täten pitää synonyymina inkrementaaliselle ohjelmistokehitykselle. Sommerville (2007) mukaan evolutiivinen protoilu on osa nykypäivän nopeaa ohjelmistokehitystä ja siitä on tullut yksi merkittävimmistä ohjelmistokehitysmalleista tuottaa rikkaita
web-sovelluksia.
3
2.1 Yleistä
McConnel (2002) mukaan evolutiivinen protoilu on ohjelmistokehityksen elinkaarimalli, jossa kehitettävän järjestelmän konsepti ja kokonaisuus tarkentuu projektin edetessä.
Erityisesti web-sovelluskehityksessä evolutiivinen protoilu aloitetaan kehittämällä järjestelmän näkyvimmät osat, esimerkiksi käyttöliittymä. Kehitteillä olevaa, mutta osittain valmista ja näkyvää web-sovellusta esitellään ja demotaan asiakkaalle, jonka on
tarkoitus antaa palautetta sovellusta koskien. Palautatte hyödyntäen prototyyppiä kehitetään vaiheittain eteenpäin kunnes sekä kehittäjät että asiakkaat toteavat prototyypin olevan riittävän hyvä. Tämän jälkeen sovellukseen toteutetaan loput puuttuvat osat ja prototyyppi toimitetaan lopullisena tuotteena. McConnel (2002) mukaan evolutiivisen protoilunmalli tuottaa tasaisia ja näkyviä edistyksen merkkejä, joiden avulla asiakkaat pysyvät tietoisina projektin etenemisestä. Kuva 1 esittää tyypillisen evolutiivisen protoiluprosessin.
Kuva 1. Evolutiivinen protoiluprosessi
McConnel (2002) toteaa evolutiivisen protoilumallin olevan tehokas erityisesti projekteissa, joissa vaatimukset voivat muuttua nopeasti eikä asiakas halua sitoutua tiettyyn
spesifiseen vaatimusmäärittelyyn tai jos asiakas ja kehittäjät eivät kumpikaan tunne kehitettävää sovellusaluetta kunnolla. Evolutiivinen protoilumalli voi olla hyödyllinen
myös tapauksissa, joissa kehittäjät ovat epävarmoja parhaan arkkitehtuurin ja algoritmien suhteen. McConnel (2002) mukaan onnistuneen evolutiivisen protoilun tulee kuitenkin sisältää todellinen vaatimusmäärittely, suunnitelma ja ylläpidettävää lähdekoodia.
Ilman näitä tekijöitä evolutiivinen protoilu voi olla pahimmillaan pelkkää resurssien
tuhlaamista ilman päämäärää.
4
2.2 Liiketoiminnan näkökulma evolutiiviseen protoiluun
Sommerville (2007) mukaan tietokoneohjelmisto on osa lähes kaikkea nykypäivän liiketoimintaa, joka toimii globaalissa, nopeasti muuttuvassa ympäristössä. Nykypäivän
liiketoiminnalla on uusia haasteita kyetä vastaamaan uusiin tilaisuuksiin, markkinoihin
sekä muuttuviin ekonomisiin asemiin ja ylläpitää kilpailukykyisiä tuotteita ja palveluita. Tästä syystä myös ohjelmistojen kehityksen tulee kyetä edistymään nopeasti, jotta se
voisi vastata liiketoiminnan nopeasti muuttuvaa maailmaa. Näiden tekijöiden tähden
nopeasta ohjelmistokehityksestä ja toimituksesta on tullut yksi kriittisimmistä vaatimuksista valtaosaan ohjelmistoprojekteja.
Sommerville (2007) mukaan vaatimusmäärittelyjen muutokset ovat väistämättömiä
nykymaailman muuttuvassa ympäristössä. Vaatimusmäärittelyjen muutokset yleensä
johtuvat siitä, ettei asiakas pysty etukäteen ennustamaan kuinka kehitettävä järjestelmä
tulee vaikuttamaan työkäytöntöihin, kuinka järjestelmän tulee olla vuorovaikutuksessa
muiden järjestelmien kanssa ja mitä käyttäjätoimintoja tulisi automatisoida. Joissain
tilanteissa todelliset vaatimukset tulevat ilmi vasta järjestelmän toimituksen jälkeen kun
käyttäjät ovat saaneet kokemusta sen käytöstä. Muutoksien väistämättömyyden takia
ohjelmistokehitysprosessit, jotka pohjautuvat tiukkoihin vaatimusmäärittelyihin ja joustamattomiin kehitysvaiheisiin, eivät sovellu Sommerville (2007) mukaan nopeaan ohjelmistokehitykseen, eivätkä siten nopeasti muuttuvaan liiketoimintaympäristöön. Evolutiivinen protoilu pystyy tarjoamaan joustavan, iteratiivisen ja muutokset mahdollistavan kehitysmallin ohjelmistojen kehitykseen.
2.3 Evolutiivisen protoilun edut ja heikkoudet
Sommerville (2007) mukaan evolutiivisen protoilun soveltuminen nykypäivän ohjelmistokehityksen kriteereihin perustuu siihen, että se pohjautuu sekä inkrementaalisen
ohjelmistokehityksen että nopean ohjelmistokehityksen ideaan. Evolutiivisen protoilun
suurimmat edut ovat Sommerville (2007) mukaan seuraavat:
1. Kehitysprosessin määrittely, suunnittelu ja toteutusvaiheet voidaan hoitaa samanaikaisesti. Tämä mahdollistaa muun muassa mahdollisten muutoksien toteuttamisen siinä vaiheessa kun ne ilmenevät.
5
2. Järjestelmä kehitetään pienissä osissa, eli inkrementeissä, jotka yhdessä muodostavat lopullisen tuotteen. Tällöin kaikki projektiin liittyvät tahot, esimerkiksi
loppukäyttäjät, voidaan ottaa mukaan arvioimaan ja määrittämään kukin inkrementti. Etuna on se, että tahot pystyvät esittämään muutospyynnöt ja uudet vaatimukset aikaisessa vaiheessa, jolloin niihin voidaan ottaa kantaa nopeammin.
3. Nopeampien järjestelmän välitoimitusten ansiosta asiakkaat voivat saada järjestelmästä osittain käytännöllistä hyötyä jo sen aikaisessa kehitysvaiheessa.
4. Asiakkaiden ja loppukäyttäjien osallistuminen projektiin palautteen antajina parantaa todellisten vaatimusten löytämistä projektin aikaisessa vaiheessa. Tämän
ansiosta lopullinen tuote vastaa paremmin asiaakaan todellista tarvetta, joka parantaa tuotteen hyväksymisastetta.
5. Käyttöliittymäsuunnittelussa ja toteutuksessa on yleensä käytössä kehitystyökaluja joiden avulla voi tuottaa nopeasti interaktiivisia käyttöliittymä prototyyppejä. Tällaisten työkalujen käyttäminen vähentää kehityskuluja ja resursseja. Osa
kehitystyökaluilla tuotetuista prototyypeistä voidaan jopa liittää mukaan viralliseen tuotteeseen. Esimerkiksi web-sovellukseen prototyyppinä tuotettua HMTLsivupohjaa ja CSS-tyylitiedostoja on mahdollista hyödyntää virallisessa tuotteessa.
Evolutiivisen protoilu on Sommerville (2007) mukaan vesiputousmallia tehokkaampi
lähestymistapa erityisesti sen inkrementaalisen luonteen ansiosta. Sillä on kuitenkin
kaksi merkittävää heikkoutta projektihallinnan ja ohjelmistosuunnittelun näkökulmasta
(Sommerville, 2007):
1. Projektin edistymistä on vaikea hahmottaa, joka johtuu siitä, ettei projektin
elinkaaressa ole tiettyjä luukoonlyötyjä vaiheita. Ainut tapa projektinjohdolle
hahmottaa mihin vaiheeseen projekti on edennyt, on muodostaa erilaisia dokumentteja ja julkaisuversiota kehitettävästä järjestelmästä. Tämä lähestymistapa
ei kuitenkaan ole kustannustehokas mikäli kehitystä tapahtuu nopeasti.
2. Inkrementaalisella kehityksellä tuotetut järjestelmät voivat olla heikosti rakennettuja kokonaisuudessaan. Syy tähän on se, että eri inkrementeissä toteutetut
toiminnallisuudet voivat muuttua kehityksen eri vaiheissa, joka voi korruptoida
järjestelmän alkuperäistä kiinteää rakennetta. Tämän lisäksi järjestelmän rakenteiden muutokset voivat olla vaikeita ja kalliita toteuttaa.
6
Sommerville (2007) mukaan evolutiivinen protoilu soveltuu parhaiten pieniin ja keskikokoisiin ohjelmistoprojekteihin, joissa lähdekoodin määrä on enintään 500’000 riviä.
Suuremmissa projekteissa, joissa tähdätään erityisesti pitkäikäisiin järjestelmiin, voi
olla suuria haasteita hyödyntää evolutiivista protoilua. Tämä johtuu siitä että suurissa
projekteissa työ yleensä jaetaan pienempiin irrallisiin osiin joihin eri kehitysryhmät
ottavat kantaa. Kun irralliset osat ovat valmiita, ne yhdistetään toisiinsa. Tällaisessa
lähestymistavassa muutosten toteutettavuus on hyvin hankalaa ja pahimmillaan se on
este tuottaa vakaita järjestelmäarkkitehtuureja. Sommerville (2007) kuitenkin toteaa,
että evolutiivista protoilua pystyy hyödyntämään osittain suuremmissakin projekteissa.
Esimerkiksi järjestelmän muuttumattomin arkkitehtuurirakenne voitaisiin toteuttaa kiinteämpiä ohjelmistoprosesseja hyödyntäen, ja vain muutosarimmat osat, kuten esimerkiksi käyttöliittymä toteutettaisiin evolutiivista protoilua hyödyntäen.
2.4 Käyttöliittymän evolutiivinen protoilu
McConnel (2002) mukaan ohjelmistojen käyttöliittymä on yleensä kaikista riskialtein
osa, jonka kehitys kannattaa aloittaa yleensä mahdollisimman aikaisessa vaiheessa.
Käyttöliittymä on yleensä ainoa ohjelmiston näkyvä osa, jonka perusteella loppukäyttäjät arvioivat ohjelman kokonaisuutta ja tekevät päätökset sen käytettävyydestä ja hyväksyttävyydestä (Sommerville, 2007).
Sommerville (2007) mukaan ohjelmistojen käyttöliittymien dynaamisen luonteen takia
niitä ei tavallisesti voida määritellä vaatimusmäärittelyssä, vaan ainut käytännöllinen
tapa käyttöliittymän suunnitteluun ja toteuttamiseen on protoilu. Mahdollisten loppukäyttäjien mukaan ottaminen käyttöliittymän suunnittelu- ja kehitysprosessiin on yksi
onnistumisen välttämättömimmistä tekijöistä käyttäjäkeskeisessä suunnittelussa, kun
kyseessä on interaktiivinen järjestelmä. McConnel (2002) mukaan evolutiivisen käyttöliittymä protoilun suurin etu on se, että se pienentää käyttöliittymän kehittämisen riskejä ja kehittämismenoja. Käyttöliittymä protoilu voidaan jakaa kahteen vaiheeseen
(Sommerville, 2007):
1. Kehitysprosessin alkuvaiheessa käyttöliittymä voidaan esittää paperiversiona.
Tällöin kukin sovelluksen näyttö on oma paperiprototyyppi ja näyttöjen välinen
vuorovaikutus voidaan kuvata asiakkaalle visuaalisesti.
7
2. Myöhemmässä kehitysvaiheessa käyttöliittymä määritellään uudelleen käyttäen
hyödyksi aikaisemmassa vaiheessa saatua palautetta ja niiden pohjalta kehitetään interaktiivinen ja automatisoitu käyttöliittymä prototyyppi. Tällaista prototyyppiä voidaan jälleen testauttaa ja simuloida käyttäjillä.
Paperiprototyypin tuottaminen on edullista ja nopeaa, mutta McConnel (2002) mukaan
käyttöliittymän esittäminen interaktiivisena toteutuksena on kaikista tehokkain tapa
saada käyttäjiltä palautetta. Interaktiivisen käyttöliittymän prototyyppikäyttöön tuottamisessa on kuitenkin omat haasteensa (Sommerville, 2007). Esimerkiksi sovelluksella
tulee olla jonkinlaista toiminnallisuutta, mutta yleensä aikaisessa kehitysvaiheessa sovelluskohtaista toiminnallisuutta tai logiikkaa ei ole mahdollista toteuttaa. Tämän lisäksi interaktiivisen käyttöliittymän toteuttaminen ja toimittaminen voi olla hidasta. Sommerville (2007) mukaan evolutiivinen käyttöliittymäprotoilu soveltuu hyvin web-sovellusten käyttöliittymä protoiluun, sillä kehittäjät voivat helposti hyödyntää valmiita
WWW-selaimia web-sovellusten näyttöpohjina. WWW-selaimia hyödyntäen kehittäjät
voivat tehokkaasti tuottaa alustavat käyttöliittymäprototyypit yksinkertaisina HTMLsivuina tai esimerkiksi Java-ohjelmointikielen tarjoamilla valmiilla käyttöliittymäkomponenteilla.
McConnel (2002) mukaan käyttöliittymän evolutiivinen protoilu soveltuu luultavasti
parhaiten erilaisten liiketoimintasovellusten kehittämisprojekteihin, joissa kehittäjillä
voi olla jatkuva, epävirallinen yhteys loppukäyttäjiin. Valtaosa web-sovellusprojekteista ovat yleensä tämäntapaisia projekteja. Käyttöliittymän evolutiivinen protoilu tukee
nopeaa ohjelmistokehitystä erityisesti seuraavilla tavoilla (McConnel, 2002):
1. Se vähentää käyttöliittymän hyväksyttävyyden riskiä, sillä epätyydyttävät käyttöliittymät ja niiden suunnittelumallit voidaan havaita hyvin aikaisessa vaiheessa ja korjaustoimenpiteet ovat todennäköisesti halvemmat.
2. Kehitettävistä sovelluksista tulee pienempiä, sillä asiakkaat ja loppukäyttäjät
pystyvät itse vaikuttamaan siihen mitä ominaisuuksia järjestelmään halutaan
mukaan ja mitä ei. Tavallisesti ohjelmistokehittäjät kuvittelevat käyttäjien tarvitsevan tietynlaisia ominaisuuksia vaikka se ei pitäisi paikkaansa. Tällaisten
epähaluttujen ominaisuuksien karsiminen projektin alkuvaiheessa vähentää kehittämisaikaa.
8
3. Kehitettävistä sovelluksista tulee yksinkertaisempia, sillä asiakkaat eivät yleensä ole kiinnostuneita ohjelmistokehittäjien tarjoamista monimutkaisista ominaisuuksista. Asiakkaat haluavat mieluummin yksinkertaisempi, mutta laadukkaampia ominaisuuksia. Protoilun avulla voi löytää vastaavat monimutkaiset
ominaisuudet, joista käyttäjät eivät edes välitä.
4. Ryömivien vaatimusten määrä on myös havaittu vähentyvän, sillä asiakkailla on
parempi käsitys järjestelmästä jo projektin alkuvaiheessa. Tällöin asiakkaat tekevät yleensä vähemmän lisäyksiä vaatimusmäärittelyyn.
5. Projektin näkyvyys on myös parempi ja asiakkaat pystyvät konkreettisemmin
suhtautumaan kehitteillä olevaan projektiin.
3 OHJELMISTOKEHYS
Ohjelmistokehys on Kaisler (2005) mukaan uudelleenkäytettävä arkkitehtuuri- ja komponenttipohjainen työkalu, joka tarjoaa yleismallisen rakenteen ja toimintatavan sekä
kontekstin kuinka sitä voi hyödyntää. Näiden lisäksi ohjelmistokehykset pohjautuvat
yleensä rajattuun toiminta-alueeseen, kuten esimerkiksi liiketoimintamallinnus (financial modeling) sovelluksiin ja päätöksentekotuki (decision support) järjestelmiin (Shan,
et al., 2006). Sommerville (2007) mukaan ohjelmistokehykset eivät yleensä ole valmiita
sovelluksia, vaan niitä on tarkoitus käyttää toteuttamaan sovelluskohtainen toiminnallisuus rajatussa toiminta-alueessa. Ohjelmistokehys voidaan siten nähdä arkkitehtuuriohjaksena, joka jakaa suunnittelun abstrakteihin luokkiin ja määrittää näiden abstraktien
luokkien väliset velvollisuudet ja yhteistyötoiminnallisuudet (Kaisler, 2005). Ohjelmistokehyksen käyttö on täten sen tarjoamien abstraktien luokkien perimistä ja toteuttamista soveltumaan tiettyä toiminnallisuutta varten. Ohjelmistokehykset pohjautuvat ajatukseen, jossa niiden tarkoitus on tarjota ohjelmistokehitykselle helppo ja nopea tapa
tuottaa joukko spesifisiä, mutta samankaltaisia järjestelmiä, joilla on tietynlainen toiminta-alue (Kaisler, 2005).
3.1 Ohjelmistokehyksien taustaa
Kaisler (2005) mukaan ohjelmistokehittäjät ovat jo pitkään tienneet, että uusien ohjelmistojen kehittäminen aivan alusta alkaen on resursseja hukkaava menetelmä. Tämän
9
takia ohjelmien uudelleenkäyttäminen on pitkään ollut osana uusien sovellusten kehitystä. Kuitenkin ohjelmistokehittäjät ovat aina myös uskoneet virheestä oppimiseen ja
siihen että samanlaisten ohjelmien toteuttaminen alusta alkaen lisäisi tietotaitoa merkittävästi. Vasta 1990-luvulla uudelleenkäytettävyyden tehokkuus yleistyi ohjelmistokehittäjien keskuudessa kun ymmärrettiin että samankaltaiset sovellukset pystyttiin kehittämään tehokkaasti ja nopeammin käyttäen hyödyksi aikaisemmin opittua tietotaitoa ja
uudelleen käyttämällä aikaisempien sovellusten osia.
Kaisler (2005) mukaan ohjelmistokehys perustuu uudelleenkäytettävyyden ideaan ja
sen edeltäjänä toimi olio-ohjelmoinnin komponentti-paradigma. Tässä paradigmassa
ohjelma jaettiin osiin eli komponentteihin ja näitä toteutettuja komponentteja pystyttiin
jatkossa uudelleen käyttämään uusissa projekteissa. Tämän paradigman ongelma oli
kuitenkin se, että vain kaikista pienimpiä ja yksinkertaisimpia yksittäisiä komponentteja
voitiin uudelleenkäyttää tehokkaasti. Keskikokoisten ja suurempien komponenttien
käyttäminen oli tehokasta vain harvoin. Kaisler (2005) toteaa, että viimeaikoina kiinostunus uudelleenkäytettävyydessä on siirtynyt yksittäiskomponenteista suurempiin laajuuksiin, kuten kokonaisiin sovellusrakenteisiin ja järjestelmiin, jotka voidaan käsittää
ohjelmistokehyksinä. Toisinkuin komponentti-paradigmassa, Kaisler (2005) mukaan
ohjelmistokehykset painottavat erityisesti suunnittelumallien uudelleen käyttämiseen,
lähdekoodin uudelleenkäyttämisen sijasta, mutta käytännössä ohjelmistokehyksiä käyttäessä lähdekoodin uudelleenkäyttäminen korkeammalla abstraktitasolla on väistämätöntä.
Vaikkakaan ohjelmistokehyksien käyttö ei ollut yleistä vasta kuin 1990-luvulla, oli sen
käsitteestä maininta kirjallisuudessa jo aikaisemmin. Kaisler (2005) mukaan ohjelmistokehyksistä tehtiin ensimmäinen maininta kirjallisuudessa 1980-luvulla Smalltalk-80
ohjelmointikielen yhteydessä. Smalltalk oli suosittu MVC (Model-View-Controller)
käyttöliittymäkeskeinen ohjelmistokehys, jolla pystyi tuottamaan graafisia käyttöliittymä-pohjaisia sovelluksia. Apple Computer oli yksi ensimmäisistä kaupallisista tuottajista, joka alkoi hyödyntää ohjelmistokehyksen ajatusta omassa MacApp-ohjelmointiympäristössä. Sen jälkeen erityisesti käyttöliittymä-pohjaisten ohjelmistokehysten yleisyys kasvoi nopeasti suurissa ohjelmistotaloissa ja yrityksissä. Myöhemmin ohjelmistokehyksien käsite yleistyi myös pienemmissä ohjelmistotaloissa, ja erityyppisten ohjelmistokehysten asema vakiintui. Kaisler (2005) mukaan ohjelmistokehyksiä on pyritty
luokittelemaan ryhmiin erilaisten kriteerien mukaan. Luokitteluskeemoja on useita eri10
laista, joista eräät suosituimmista ovat Fayad, et al. (1997) tarjoamat skeemat. Alla esitetään kuitenkin vielä yksi erilainen lähtökohta ohjelmistokehyksien luokitteluun (Shan,
et al., 2006):

Käsitteelliset ohjelmistokehykset – yhdistävät arkkitehtuurimallit, esimerkiksi
Zachman Framework.

Sovellusohjelmistokehykset – tarjoaa runkorakenteen sovelluksille, esimerkiksi
WebWork.

Toimintoalue (domain) ohjelmistokehykset – räätälöity tavallisesti tiettyä liiketoiminta osaa varten, esimerkiksi IBM Information Framework.

Alusta (platform) ohjelmistokehykset – ohjelmointimallit ja ajanaikaiset ympäristöt, esimerkiksi .NET ja Java EE framework.

Komponenttiohjelmistokehykset – sovellusten rakenneosia varten, esimerkiksi
Hibernate, iBatis ja Cayenne.

Palveluohjelmistokehykset – liiketoiminta ja tekniset palvelumallit palvelukeskeisille ohjelmistoille, esimerkiksi Semantic Web Service Framework.

Kehitysohjelmistokehykset – rakennuspohjia tuottaa kehitystyökaluja erityisesti
integroituja kehitysympäristöjä varten.
3.2 Ohjelmistokehyksien rakenne
Shan, et al. (2006) mukaan ohjelmistokehys on rajattu tukirakenne, jonka avulla muut
ohjelmistosovellukset voidaan organisoida ja kehittää. Ohjelmistokehys rakentuu kahdesta elementistä: uudelleenkäytettävästä suunnittelumallista ja rakennusosista kuten
esimerkiksi graafisten käyttöliittymien komponenteista. Ohjelmistokehys voi sisältää
muun muassa apuohjelmia, koodikirjastoja, yleisiä palveluita, rajapintoja tai muita työkaluja kuten esimerkiksi komentosarjakieliä. Näiden hyötykomponenttien avulla kehitettävän sovelluksen osat ja komponentit voidaan toteuttaa ja yhdistää yhdeksi kokonaisvaltaiseksi ohjelmistosovellukseksi.
Shan, et al. (2006) mukaan ohjelmistokehyksien rakenne jakautuu myös kahteen eri
alueeseen: staattisiin ja dynaamisiin alueisiin. Staattiset alueet ovat ohjelmistokehyksissä järjestelmän yleisarkkitehtuuri eli peruskomponentit ja niiden väliset suhteet. Staattiset alueet pysyvät muuttumattomia kaikissa ilmentymissä, jotka on toteutettu samalla
11
ohjelmistokehyksellä. Vastaavasti dynaamiset alueet kuvaava ohjelmistokehyksen osia,
jotka voivat olla yksilöllisiä eri ohjelmistokehyksen ilmentymissä. Dynaamiset alueet
on tavallisesti suunniteltu yleismallisiksi, jotta ne voidaan mukauttaa sopimaan erilaisiin sovelluksen tarpeisiin.
Kaisler (2005) mukaan ohjelmistokehysten arkkitehtuuri ja rakenne voi vaihdella merkittävästi, mutta pääpiirtein ne rakentuvat samanlaisen arkkitehtuuri idean pohjalta.
Alla on esitetty viisi eri ohjelmistokehyksen mahdollista arkkitehtuurikerrosta ja kuvaus
niiden yleisestä tarkoituksesta ja toiminnallisuudesta. Esitetyssä mallissa kukin kerros
muodostaa kaikkien sitä alempien kerrosten tarjoamista ominaisuuksista. Kerrokset on
esitetty järjestyksessä alimmasta ylimpään. Kuvassa 2 on esitetty kokonaiskuvaus kaikista arkkitehtuurin kerroksista ja niiden tärkeimmistä sisällöistä.
Kernel-kerros
Kernel-taso tarkoittaa sovelluksen, käyttöjärjestelmän ja fyysisen tietokoneen välistä
rajapintaa. Kaisler (2005) mukaan Kernel-taso tarjoaa yleiset toiminnallisuudet ja palvelut, esimerkiksi metaolio protokollat, oliovaihdot (object trading), säiliökirjastot,
asiakas-palvelin toteutustasot (client-server middleware), tietovarastoinnit ja käyttöjärjestelmätason toiminnallisuudet. Tässä kerroksessa ei ole yleensä lainkaan toimintoalue
tai sovellus-spesifistä toiminnallisuutta, joten se on helposti uudelleenkäytettävä kerros.
Työpöytäkerros
Kaisler (2005) mukaan työpöytäkerroksessa määritellään sovelluksien yleinen toimintamalli, johon kuuluu muun muassa yleinen arkkitehtuuri sekä interaktiivisen sovelluksen ulkoasu ja tuntuma. Työpöytäkerros varmistaa sovelluksen toiminnallisen ja teknisen yhtenäisyyden ja sitä voidaan hyödyntää interaktiivisen käyttöliittymän suunnittelussa.
Liiketoiminnan toimintoaluekerros
Kaisler (2005) mukaan tässä kerroksessa määritellään ja toteutetaan tietyn toimintoaluemallin ydinkäsite ja toiminnallisuus. Liiketoimintakerros toteuttaa pohjan, jota voi
hyödyntää missä tahansa saman toimintoaluemallin omaavassa sovelluksessa. Tähän
kerrokseen kuuluu muun muassa toimintoalue-spesifiset luokat ja arvotyypit.
12
Liiketoimintalogiikan kerros
Kaisler (2005) mukaan liiketoimintalogiikan kerroksessa laaditaan toteutukset eri liiketoimintayksiköihin. Toimintalogiikan toteuttaminen vaatii yleensä liiketoiminnan toimintoaluekerroksen ja työpöytäkerroksen luokkien laajentamista ja abstraktien metodien toteuttamista. Tässä kerroksessa toteutetut luokat ja työkalut ovat hyvin spesifisiä
liiketoiminnan toimintoaluekohtaisia tehtäviä varten.
Sovelluskerros
Kaisler (2005) mukaan sovelluskerros on ylin arkkitehtuurikerroksista ja se määrittää
itse konkreettisen sovelluksen ja sen konfiguraation. Tässä kerroksessa toteutetaan sovelluksen organisaatiotasoiset vaatimukset asentamalla ja konfiguroimalla organisaatioriippuvaiset tekijät.
Kuva 2. Viisikerroksinen ohjelmistokehysarkkitehtuuri
3.3 Web-sovellusohjelmistokehys
Shan, et al. (2006) mukaan web-sovellusohjelmistokehys on uudelleenkäytettävä, runkomainen, puoli-valmis, modulaarinen alusta. Web-sovellusohjelmistokehyksellä voi
tuottaa erilaisia web-sovelluksia, joita tavallisesti ajetaan web-selainten kautta käyttäen
HTTP-protokollaa. Web-sovellusohjelmistokehys koostuvat yleensä palveluosista ja
13
komponenteista, joista voidaan rakentaa monipuolisia ja toiminnallisia liiketoimintapalveluita ja järjestelmä.
Shan, et al. (2006) mukaan web-sovellusohjelmistokehykset ovat kontekstiltaan sovellusohjelmistokehyksiä, mutta se integroi myös muita kerroksia. Web-sovellusohjelmistokehykset toteuttavat muun muassa yleensä MVC suunnittelumallin, joka tavallisesti pohjautuu Model 2 arkkitehtuuriin. Web-sovellusohjelmistokehykset toteuttavat
yleensä myös joitain perustavanlaatuisia liiketoiminnan palveluita kuten haku, versiointi ja oikeuksienhallinta, joilla voi kohentaa sovelluksen luotettavuutta. Näiden lisäksi
web-sovellusohjelmistokehykset toteuttavat myös toimintoaluekerroksen, joka yleensä
mallintaa yleiskäyttöisiä käsitteitä kuten käyttäjiä, ryhmiä ja näille kuuluvia oikeuksia.
Muita mahdollisia relevantteja kerroksia ovat olio-relaatiokerros ja käyttöliittymä, sekä
käyttöliittymän komponenttikirjastot, jotka on suunniteltu nopeaa kehitystä varten.
Shan, et al. (2006) mukaan web-sovellusohjelmistokehykset toteuttavat yleensä hyviksi
havaittuja standardeja ja tekniikoita, jotka mahdollistavat nopean web-sovellus kehittämisen ilman jyrkkää oppimiskäyrää. Tämän lisäksi web-sovellusohjelmistokehysten
käyttäminen vähentää työmäärää ja resursseja kehittää ja ylläpitää web-sovelluksia ja
ohjelmistokehittäjät pystyvät keskittymään sovelluksen liiketoimintapuoleen infrastruktuurin muun teknologian sijasta.
4 GRAILS
Grails on yksi web-sovellusohjelmistokehys, jonka tarkoituksen Rocher, et al. (2009)
tiivistää seuraavasti: Sen tarkoitus on yksinkertaistaa yritystason Java-pohjaista webkehitystä, viedä web-kehitys seuraavalle abstraktiotasolle, hyödyntää muiden ohjelmointiympäristöjen parhaita ominaisuuksia ja mahdollistaa joustava pääsy ja muokkaamisvara pohjateknologioihin. Korkeammalla abstraktiotasolla Grails-ohjelmistokehys pohjautuu seuraaviin suunnittelumalleihin ja periaatteisiin (Rocher, et al., 2009):

Ohjelmistokehittäjien tarvitsee tehdä merkittäviä päätöksiä ja rajoituksia vain
sovelluksen yksilöivien tekijöiden suhteen (Convention over Configuration).

Tiedon toistamisen vähentämiseen monikerroksisessa arkkitehtuurissa (Don’t
Repeat Yourself).
14

Toimintoalue-ominaiseen ohjelmointiin (Domain-specific languages).

Groovy-ohjelmointikielen tarjoamiin ominaisuuksiin.
4.1 Groovy-ohjelmointikieli
Grails-ohjelmistokehys pohjautuu Groovy-ohjelmointikieleen, joka on ketterä ja dynaaminen olio-ohjelmointikieli (Judd, et al., 2008). Groovy toimii Java-alustalla ja se
on saanut inspiraationsa muista ohjelmointikielistä kuten: Python, Ruby, Pearl, Smalltalk ja Java. Judd, et al. (2008) mukaan Groovy-ohjelmointikielen vahvuuksia on sen
yhteensopivuus Java-virtuaalikoneen kanssa. Groovy-ohjelmointikielellä toteutetut luokat kääntyvät sen omalla kääntäjällä puhtaaksi JVM- tavukoodiksi ja se helpottaa Javaohjelmointikielellä toteutettuja luokkia ja komponentteja integrointia Groovy-ohjelmointikieleen ja toisinpäin. Groovy-ohjelmointikielen kehityspaketti GDK (Groovy
Development Kit) laajentaa Java-ohjelmointikielen ohjelmointirajapintaa ja Groovy on
itsessään Java Community Process (JCP) ja Java Specification Request (JSR) 241:n
hyväksymä standardi, jota isännöidään Codehaus-yhteistyö ympäristössä (Codehaus
Foundation, 2008). Toinen vahvuus Groovy-ohjelmointikielessä on sen suuntautuminen
moderneihin ohjelmointi-ominaisuuksiin kuten sulkeumiin (closures) ja arvotietotyyppeihin (properties).
Kuten Java, myös Groovy on imperatiivipohjainen olio-ohjelmointikieli (Ghosh, et al.,
2009). Imperatiivinen ohjelmointi on vanhin ja suosituin ohjelmointiparadigma ja sen
pääajatus on käsitellä tietokoneohjelmissa tiloja ja hyödyntää erilaisia komentoja tilojen
muuttamiseksi (Kaisler, 2005). Imperatiivinen ohjelmointi pohjautuu von Neumann
arkkitehtuuriin ideaan, jossa tieto esitetään muuttuvana varastona. Tässä arkkitehtuurissa ohjelmien toiminta on hakea tietoa varastosta, suorittaa toimintoja ja tallentaa muuttunut tieto takaisin varastoon. Kaisler (2005) mukaan imperatiivinen ohjelmointi voidaan kuvata peräkkäisinä toimintokäskyinä jotka muokkaavat ohjelman tilaa tyypillisesti erilaisten sijoituslauseiden muodossa. Kukin toimintokäsky ja tila on yksilöllinen
tietokoneen osa, jotka yhdistettynä muodostavat kokonaisen ohjelman.
Groovy-ohjelmointikieli mahdollistaa sekä staattisen että dynaamisen tyypityksen
(Rocher, et al., 2009). Staattisella tyypityksellä tarkoitetaan sitä, että kenttien ja muuttujien tyyppi on määritetty tietyksi, esimerkiksi kokonaisluvuksi, liukuluvuksi tai merkki15
jonoksi, kun taas dynaaminen tyypitys tarkoittaa sitä että vain itse muuttuja määritellään, mutta sillä ei anneta mitään yksilöivää tyyppiä. Köning, et al. (2007) mukaan
staattisen tyypityksen etuina on se, että se tarjoaa enemmän tietoa optimointia varten.
Staattisessa tyypityksessä kääntäjä pystyy paremmin tulkitsemaan muuttujien käyttöä,
joka mahdollistaa paremman integroitujen kehitysympäristöjen (IDE) tuen ja metodien
ylilataamisen (overloading). Dynaaminen tyypitys on Köning, et al. (2007) mukaan
joustavampi malli, koska muuttujan tyyppi pystyy vaihtumaan aivan kokonaan sen eliniän aikana ja se mahdollistaa ankkatyypityksen (duck typing). Ankkatyypityksessä olion metodit ja sen arvotietotyypit määrittävät oliota koskevan semantiikan sen sijaan että
se määräytyisi olion luokkaperimän tai rajapintatoteutuksen mukaan (Köning, et al.,
2007). Mikäli oliolla on tarpeelliset metodit tai arvotyyppitiedot niin se voidaan laskea
samankaltaiseksi olioksi.
Groovy-ohjelmointikielen syntaksi pohjautuu Java-ohjelmointikieleen, mutta sen muodollisuutta on kevennetty merkittävästi ja uusia syntaksiominaisuuksia lisätty parantamaan työtehokkuutta (Judd, et al., 2008). Esimerkiksi tietorakenteiden käsittelystä on
tehty helpompaa ja tehokkaampaa Java-ohjelmointikieleen verrattuna. Groovy- ja Javaohjelmointikielien syntaksierot voi havainnollistaa liitteiden Y1 ja Y2 avulla. Liitteissä
on toteutettu sama ohjelma, jossa aluksi kahteen listatietorakenteisiin lisätään kymmenen satunnaista lukua ja listojen leikkauksesta muodostetaan avain-arvo parillinen karttatietorakenne. Karttatietorakenteessa avain kuvaa leikkauksesta löytynyttä leikkausnumeroa ja arvo kuvaa kuinka monta kertaa kyseinen numero löytyi molemmista listoista. Tämän jälkeen karttatietorakenteen sisältö tulostetaan konsoliin. Java-toteutus
esimerkkitehtävästä on esitetty liitteessä Y1 ja Groovy-ohjelmointikielellä toteutettuna
lähdekoodi on liitteen Y2 mukainen. Groovy-ohjelmointikielellä toteutusta versiota voi
nähdä kevennetyn syntaksin, tietorakenteiden käsittelyn yksinkertaistamisen ja sulkeumien käytön.
Syntaksi muutoksen lisäksi Groovy-ohjelmointikielen kehityskirjastossa on useita hyödyllisiä apuluokkia ja laajennettuja Java ohjelmarajapinnan luokkia (Judd, et al., 2008).
Esimerkiksi JUnit-yksikkötestaus on kiinteä osa Groovy:n kehityskirjastoa laajennettuine luokkineen ja metodeineen. Myös XML-rakenteiden käsittelystä on tehty helpompaa ja tehokkaampaa Groovy:n tarjoamien luokkien ja rajapintojen avulla. Judd, et
al. (2008) mukaan XML-rakenteiden käsittely Groovy-ohjelmointikielellä on yhtä selkeää ja inhimillisen luettavaa kuin itse XML-rakenteiden luonnekin. Groovy16
ohjelmointikielelessä on myös muun muassa GPath-ilmaisukieli, jonka avulla XMLrakenteita pystyy navigoimaan tehokkaasti ja saumattomasti XPath-ilmaisukielen tavoin. Groovy-ohjelmointikieli mahdollistaa myös Meta-olio Protokollan (Meta-Object
Protocol), jonka avulla olemassaolevia luokkia voi laajentaa ja niihin voi lisätä uusia
toiminnallisuuksia, vaikka ne olisivatkin viimeisteltyjä (final). Judd, et al. (2008) mukaan Meta-olio Protokolla yksi tärkeimmistä Groovy-ohjelmointikielen ja Grails-ohjelmistokehyksen käsitteistä, sillä se mahdollistaa Grails:in toimintoalueluokkien ominaisuudet ja joustavan olemassaolon.
4.2 Rakenne ja arkkitehtuuri
Judd, et al. (2008) mukaan Grails-ohjelmistokehys voidaan esittää neljäkerroksisena
arkkitehtuuritoteutuksena, joka on esitetty kuvassa 3. Arkkitehtuurin alin taso on Javavirtuaalikone ja sen yllä on Java- ja Groovy-ohjelmointikielien tuki, joihin Grailsohjelmistokehys perustuu. Näiden kerrosten yllä on itse Grails-ohjelmistokehys ja websovelluskerros, jotka muodostavat Grails web-sovelluksien sydämen. Arkkitehtuurikerrosten lisäksi Grails hyödyntää Gant-teknologiaa, johon muun muassa Grails komentorivi-rajapinta pohjautuu. Judd, et al. (2008) mukaan Gant-teknologia käyttää Groovyohjelmointikieltä Apache Ant-skriptien tuottamiseen.
Teknologiatasolla Grails-ohjelmistokehys sisältää lukuisia avoimeen lähdekoodiin pohjautuvia teknologioita, joista tärkeimmät on listattu ja kuvattu lyhyesti alla (Rocher, et
al., 2009):

Hibernate, josta on tullut de facto standardi Java-ohjelmoinnin olio-relaatiotietokantakartoitukseen (object-relational mapping). Grails-ohjelmistokehyksen
GORM (Grails Object-relational Mapping) laajentaa Hibernate teknologiaa ja
tarjoaa kehittäjille lähes konfiguraatio-vapaan oliotallennusrajapinnan.

HSQLDB, joka on täysin Javalla toteutettu relaatiotietokannan hallintajärjestelmän (Relational Database Management System) toteutus. HSQLDB on valmiiksi asennettu oletustietokanta Grails-ohjelmistokehysprojekteissa, mutta tarpeen mukaan kehittäjät voivat ottaa käyttöön toisen tietokantatoteutuksen.

SiteMesh, joka on vankka ja vakaa sivustopohjan hahmottamiskehys (layoutrendering).
17

Spring, joka on käänteisohjaus (Inversion of Control) säiliö ja kääre Javaohjelmoinnissa. Spring-teknologian käyttö Grails-arkkitehtuurissa mahdollistaa
myös Spring-teknologian muiden ominaisuuksien, kuten tietoturvan käytön erilaisten Grails-laajennuksien avulla.

Jetty, joka on Grails-ohjelmistokehykseen upotettu servlet-säiliö (servlet container). Jetty-säiliö tarjoaa helpon ja nopean tavan ajaa ja testata Grails-ohjelmistokehyksellä tuotettuja web-sovelluksia. Vaikka Jetty-säiliö onkin upotettu
Grails-ohjelmistokehykseen, pystyy Grails web-sovelluksia ajamaan myös
muissa sovelluspalvelimissa ja säiliössä
Kuva 3. Grails-ohjelmistokehyksen arkkitehtuuri
4.3 Grails web-sovelluksien rakenne
Rocher, et al. (2009) mukaan Grails-ohjelmistokehyksellä toteutetut web-sovellukset
rakentuvat erilaisista valmispohjaisista pääelementeistä kuten toimintoalueluokista
(domain classes), hallintaluokista (controllers) ja GSP-näytöistä (Groovy Server Page).
Näiden pääelementtien lisäksi sisältö voi rikastaa erilaisilla Web 2.0 ominaisuuksilla
kuten AJAX-tekniikalla, lokalisoinnilla sekä muilla palveluilla, joihin Grails-ohjelmistokehys tarjoaa rajapinnat erilaisten laajennuksen kautta. Grails-ohjelmistokehystä
käyttäen web-sovelluskehittäjien ei tarvitse aloittaa kehitystä aivan tyhjästä. Sen sijaan
nykypäivän standardit toteuttavan web-sovelluksen voi toteuttaa tehokkaasti yhdistelemällä pääelementtejä ja rikastavia Web 2.0 ominaisuuksia. Kuvassa 4 on esitetty tyypillinen Grails web-sovelluksen ajonaikainen ympäristö.
18
Kuva 4. Grails web-sovellusten tyypillinen ajonaikainen ympäristö
Toimintoalueluokat
Rocher, et al. (2009) mukaan olio-ohjelmointikielillä toteutetuissa sovelluksissa on lähes aina jonkinlainen toimintoaluemalli (domain model), joka kuvaa sovellukseen liittyviä liiketoiminta yksiköitä (business entities). Tavallisesti liiketoiminta yksiköiden
sisältämät tiedot tulee olla pysyviä, jonka johdosta ne tallennetaan oliorelaatiotietokantaan, joka mallintaa kyseistä toimintoaluemallia. Grails-ohjelmistokehyksessä kukin
liiketoiminnan yksikkö voidaan esittää omana toimintoalueluokkana. Toimintoalueluokat ovat tavallisia Groovy-luokkia, jolla voi olla tietokenttiä kuvaamaan yksikön sisältöä, sekä liitoksia toisiin yksiköihin kuvastamaan yksiköiden välistä suhdetta. Grailsohjelmistokehys luo automaattisesti kutakin toimintoalueluokkaa varten oman tietokanta taulun ja yksiköiden sisäisiä tietoja voidaan tallentaa sovelluksissa käyttäen GORM
tekniikkaa. Kehittäjän tarvitsee toteuttaa vain liiketoimintayksikköä vastaava Groovyluokat, määrittää mitä ja minkälaisia tietokenttiä kullakin on sekä mitkä ovat kyseisen
toimintoalueluokan suhteet muihin toimintoalueluokkiin. Tämän jälkeen GORM osaa
automaattisesti hoitaa loput tietokanta- ja olio-kartoitusasetukset ja toteutetut toimintoalueluokat ovat valmiita käyttöä varten.
Hallintaluokat
Rocher, et al. (2009) mukaan hallintaluokkien tarkoitus on toimia web-sovellukseen
tulevien pyyntöjen käsittelijöinä. Kun hallintaluokka vastaanottaa pyynnön, se voi toteuttaa jotain pyyntöön liittyvää toiminnallisuutta, jonka jälkeen hallintaluokkaa päättää
mitä web-sovelluksessa tulisi tapahtua seuraavana. Mahdollisia päätöksiä voisi olla
esimerkiksi uudelleenohjata pyyntö toiselle hallintaluokalle, hahmottaa koko näyttö tai
hahmottaa osa nykyisen näytön tiedoista. Hallintaluokkien elämänkaari on pyyntökohtainen, joka tarkoittaa sitä että kyseisestä hallintaluokasta luodaan uusi ilmentymä jo19
kaista tehtyä pyyntöä kohden. Rocher, et al. (2009) mukaan hallintaluokat ovat Grails
web-sovellusten logiikan ydin, jotka muodostavat keskitetyn tiedonsidonta ja ohjauskoordinaation. Hallintaluokat ovat toimintoalueluokkien tapaan tavallisia Groovyluokkia, jotka sisältävät sille ominaiset tiedot, sulkeumat ja muut metodit, joita voi
hyödyntää web-sovelluslogiikan toteuttamiseen. Kehittäjät voivat laatia useita eri hallintaluokkia ja jakaa täten web-sovelluksen logiikkaa eri osa-alueisiin helpomman ylläpidettävyyden nimessä.
GSP-näytöt
Rocher, et al. (2009) mukaan Java-pohjaisiin web-sovelluksia varten on olemassa lukuisia avoimen lähdekoodiin näyttöteknologioita. Yksi nykypäivän tunnetuimmista
näyttöteknologioista on JSP (Java Server Pages), josta on tullut lähes standardi websovelluskehitykseen. JSP-teknologia mahdollistaa perinteisten merkkikieltä, kuten
HTML:n ja Java-lähdekoodin sekoittamisen keskenään muodostaakseen dynaamisen
näytön. JSP mahdollistaa myös erilaisten tag-kirjastojen käytön, joiden avulla logiikka
voidaan erottaa JSP sivunäkymästä. JSP-teknologian yleisyydestä huolimatta Grailsohjelmistokehys tarjoaa Rocher, et al. (2009) mukaan oman GSP-näyttöteknologian
seuraavista syistä:

Grails web-sovellukset voivat saada täyden hyödyn irti käyttämällä omaa näyttöteknologiaa, joka on yhteensopiva Groovy-ohjelmointikielen ajonaikaiseen
ympäristöön ja siihen liittyvään dynaamiseen metodikutsuntaan.

Groovy-ohjelmointikielen GPath-ilmaisukieli, Groovy papunotaatiot sekä operaatioiden ylikirjoitusmahdollisuus tarjoaa tehokkaamman kehityspohjan.

Groovy-ohjelmointikielen muut ominaisuudet kuten säännöllisten lausekkeiden
(regular expression) tuki, GStrings-merkkijonot sekä tietorakenteiden käyttösyntaksi tukevat hyvin näyttöteknologioiden vaatimuksia.
Rocher, et al. (2009) mukaan GSP-teknologia on hyvin samankaltainen JSP-teknologiaan verrattuna. GSP-teknologia mahdollistaa muun muassa JSP-teknologian tapaisen
mekanismin luoda omia tag-kenttiä, joiden avulla logiikkaa ja dynaamista luonnetta
voidaan tuoda näyttöihin. Tämän lisäksi merkkikielen ja Groovy-ohjelmintikielen yhdistäminen on mahdollista, mutta GSP-teknologia rohkaisee kehittäjiä käyttämään mieluummin tag-kirjastojen kenttiä. GSP-teknologia tukee JSP-teknologian tag-kenttiä,
20
mutta se myös tarjoaa lukuisia sisäänrakennettuja omia tag-kenttiä toteuttamaan monipuolista dynamiikkaa.
Web 2.0 ominaisuudet
Grails-ohjelmistokehys mahdollistaa lukuisten Web 2.0 ominaisuuksien hyödyntämisen
web-sovelluksissa. Näitä ominaisuuksia ovat Rocher, et al. (2009) mukaan muun muassa AJAX-tekniikka (kuvattu tarkemmin luvussa 5.5), sivuston lokalisoiminen viestinippujen muodossa (message bundles) ja tilallisten palveluiden sekä prosessien tuottaminen Web Flow-tekniikalla. Myös lukuisia muita Web 2.0 ominaisuuksia on tarjolla
Grails-ohjelmistokehykseen erilaisten laajennuksien kautta.
4.4 Grails kehitysalustana
Rocher, et al. (2009) mukaan Grails ei ole vain web-sovellusohjelmistokehys, mutta
myös kokonaisvaltainen kehitysympäristö tuottaa ja testata web-sovelluksia koko sen
kehityselämänkaaren ajan. Grails tarjoaa komentorivi-rajapinnan ja joukon erilaisia
komentorivi komentoja, joiden avulla voi suorittaa muun muassa kehitys-, kokoamis- ja
testaamistehtäviä. Grails komentorivi-rajapinnan avulla voi muun muassa aloittaa uuden Grails web-sovellusprojektin, joka luo automaattisesti tarvittavan projektirakenteen
ja pohjan. Tämän lisäksi Grails komentorivi-rajapinnan avulla voi luoda uusia toimintoalue- sekä hallintaluokkia, ajaa yksikkötestejä ja koota nykyisen projektin eri versioksi. Grails kehitysalustan avulla kehittäjien ei tarvitse opetella projektirakennetta, laatia
alustavia perusasetuksia tai edes sisäisiä integraatioita eri komponenttien välille. Näiden sijasta kehittäjät voivat siirtyä suoraan toteuttamaan web-sovelluksia.
Judd, et al. (2008) mukaan Grails komentorivi-rajapinta pohjautuu Gant-teknologiaan.
Gant-teknologia taas pohjautuu Groovy-ohjelmointikieleen, jonka avulla tuotetaan
Apache Ant-skriptejä projektin hallintatehtäviä varten. Tavallisesti Apache Ant-skriptit
tuotetaan XML-rakennekielellä, mutta Judd, et al. (2008) mukaan Gant-teknologia on
tehokkaampi ja helpommin lähestyttävä menetelmä. Gant-teknologian avulla projekteille voi asettaa erilaisia automaattisia tehtäviä kuten projektin käännös-, kokoamis- ja
käyttöönottotehtäviä. Tällaisten tehtävien automatisointi lisää projektin ja sen julkaisujen yhdenmukaisuutta. Gant-teknologia on myös helposti laajennettavissa, joka mahdollistaa laajempienkin projektikohtaisten elämänkaaritoimintojen automatisoinnin.
21
Rocher, et al. (2009) mukaan Grails ei pyri sisällyttämään kaikkea web-sovelluksiin
liittyviä ominaisuuksia ja tekniikoita itsessään. Sen sijaan Grails tarjoaa liitännäisjärjestelmän (plugin system), jonka avulla perusominaisuuksia ja tekniikoita pystyy laajentamaan tarpeen mukaan. Grails-ohjelmistokehyksen ydin onkin toimia liitännäisjärjestelmän ajoympäristönä ja kaikki tärkeimmät sisäänrakennetut ominaisuudet ydinominaisuuksia lukuun ottamatta ovat erilaisia valmiiksi asennettuja laajennuksia. Juuri
asennettuun Grails-ohjelmistokehykseen on pakattu mukaan vain tärkeimmät laajennuksen, mutta mahdollisia muita asennettavia Grails-laajennuksia voivat olla Judd, et
al. (2008) mukaan esimerkiksi tietoturva ja tunnistusominaisuudet, Ajax testaaminen,
haku- tai raportointiominaisuudet ja web-palvelut. Rocher, et al. (2009) toteaa, että liitännäisjärjestelmä on Grails-ohjelmistokehyksen kulmakivi. Julkaistuja Grails-laajennuksia voi selata joko Grails komentorivi-rajapinnan kautta tai Grails-ohjelmistokehyksen virallisilla verkkosivuilla (SpringSource, 2009).
5 LIFT
Lift-ohjelmistokehys on Chen-Becker, et al. (2009) mukaan vaihtoehtoinen ohjelmistokehys monien muiden vastaavien joukossa, mutta se tarjoaa muihin verrattuna uudenlaisen tehokkaan tavan ratkaista web-sovelluskehitykseen liittyviä tilanteita. Liftohjelmistokehys on toteutettu ja koottu tehokkaalle ja vakaalle pohjalle, ottaen parhaita
ideoita muista ohjelmistokehyksistä ja lisäämällä joukkoon uudenlaisia ideoita. ChenBecker, et al. (2009) toteaa myös, että Lift-ohjelmistokehyksen kehittäjät ovat suunnitteluvaiheessa pystyneet huomioon ja välttämään virheitä, jotka ovat vaivanneet aikaisempia ohjelmistokehyksiä.
Ghosh, et al. (2009) mukaan Lift-ohjelmistokehyksen erityisiksi vahvuuksiksi voidaan
laskea Scala-ohjelmointikielen kattava hyödyntäminen ja HTML-kaavarakenteiden
prosessointi. Lift-ohjelmistokehys erottaa kaavarakenteiden esityslogiikan käsittelylogiikasta täysin estämällä HTML tag-kenttien suoran kartoituksen. Chen-Becker, et al.
(2010) toteaa, että Lift-ohjelmistokehyksen luonnissa esitys- ja käsittelylogiikan erottaminen toisistaan oli yksi tärkeimmistä suunnitteluvisioista. Ghosh, et al. (2009) mukaan tag-kenttien kartoituksen sijasta Lift-ohjelmistokehys tarjoaa mahdollisuuden toteuttaa erilaisia hallinta- ja käsittelyosia, joita kutsutaan Lift-ohjelmistokehyksen ter22
meissä Snippet-komponenteiksi. Snippet-komponentit hyödyntävät Scala-ohjelmointikielen sulkeumia sitomaan kaavarakenteiden elementtejä ja tietosisältöjä sopiviin sijainteihin kullekin sivuilla. Kolmas tärkeä vahvuus Lift-ohjelmistokehyksessä on ChenBecker, et al. (2010) mukaan AJAX- ja Comet-tekniikoiden tehokas hyödyntäminen.
Näiden tekniikoiden avulla web-sovelluskehittäjät voivat tuottaa sivuille hyödyllisiä
web 2.0 ominaisuuksia vähällä työvaivalla.
Kuten Grails-ohjelmistokehys, myös Lift-ohjelmistokehys perustuu useampaan eri
suunnittelumalliin ja periaatteeseen, joiden tarkoitus on tehostaa ja helpottaa ohjelmistokehitystä (Chen-Becker, et al., 2010):

Ohjelmistokehittäjien tarvitsee konfiguroida vain hyvin vähän alustavia websovelluksen asetuksia, sillä Lift-ohjelmistokehys tarjoaa valmiit asetukset lähes
kaikkeen. Asetusten muokkaaminen on kuitenkin tehty helpoksi, sillä se vaatii
vain muutaman tiedon muokkaamista XML-pohjaiseen tiedostoon ja loppujen
asetusten kirjoittamista selkeisiin Scala-luokkiin.

Ohjelmistokehittäjiä rohkaistaan toteuttamaan sivut näyttö-ensin (View-First)
mallia käyttäen, koska Lift-ohjelmistokehys erottaa esitys- ja käsittelylogiikan
täysin toisistaan. Kehittäjillä on kuitenkin paljon valinnanvara toteuttaa eri näytöt.

Lift-ohjelmistokehys ei pakota web-sovelluskehittäjiä käyttämään vain yhdenlaista tapaa toteuttaa toiminnallisuuksia, vaan useita se tarjoaa erilaisia menetelmätapoja eri toiminnallisuuksia web-sovelluksiin.
Web-sovelluskehittäjät
voivat valita parhaaksi kokemansa tavan toteuttaa eri asiat.
5.1 Scala-ohjelmointikieli
Lift-ohjelmistokehys pohjautuu Scala-ohjelmointikieleen (Chen-Becker, et al., 2009).
Scala-ohjelmointikieli on Groovy-ohjelmointikielen tapaan vielä hyvin nuori ohjelmointikieli, joka tuottaa Java-virtuaalikoneeseen soveltuvaa tavukoodia. Pollak (2009)
mukaan Scala-ohjelmointikieli on funktionaalispainoteinen olio-ohjelmointikieli, joka
mahdollistaa myös imperatiivipohjaisen lähestymistavan ohjelmointiin. Scala-ohjelmointikieli kuitenkin rohkaisee ohjelmoijia tuottamaan ratkaisut funktionaalispainotteisesti, joka on yksi merkittävä tekijä, joka on nostanut Scala-ohjelmointikielen suureen
23
suosioon (Ghosh, et al., 2009). Erityisesti web-sovelluskehityksessä funktionaalispainotteinen lähestymistapa pystyy tarjoamaan imperatiivipohjaista lähestymistapaa tehokkaamman ja turvallisemman tavan toteuttaa esimerkiksi palvelinpäätyjä. Ghosh, et
al. (2009) mukaan funktionaalispainotteisen lähestymistavan tarjoama erilainen abstraktiotaso tukee paremmin web-sovelluskehityksen vaatimuksia. Esimerkiksi funkionaalinen abstraktiotaso tukee paremmin palvelin- ja asiakaspäätytoteutuksien välistä
pyyntö/vastauselämänkaarien käsittelyä ja kuvausta, erilaisten kaavarakenteiden (form)
sisältöjen kartoitusta lähdekoodissa sekä tiedonesitys- ja sovelluslogiikan erottamista
toisistaan.
Kaisler (2005) mukaan funktionaalisen ohjelmoinnin päätarkoitus on tarjota ohjelmoijalle joukko funktioita ja toiminnallisuuksia ja esittää kaikki tieto mahdollisimman
muuttumattomana. Funktionaalisen ohjelmoinnin paradigmassa ohjelma saa syötteen,
jonka se käsittelee ja palauttaa siitä muodostuneen vastauksen. Funktiot ovat yksittäisiä
ohjelman osia, jotka eivät vaikuta muihin ohjelman osiin tai tiloihin, vaan ne ainoastaan
ottavat vastaan syötteen ja palauttavat vastauksen. Funktionaalisen ohjelmoinnin paradigma voidaan täten nähdä imperatiiviseen ohjelmointiin nähden vähemmän virhealttiina. Kuitekin algoritmien kehittäminen funktionaalista paradigmaa käyttäen on haastavampaa (Odersky, et al., 2008).
Ghosh, et al. (2009) mukaan Scala-ohjelmointikielellä on useita ominaisuuksia, joiden
ansiosta se on yksi vahvimpia valintoja vaihtoehtoisten JVM-pohjaisten ohjelmointikielten joukossa. Scala-ohjelmointikielen vahvuudet Java-ohjelmointikieleen verrattuna
ovat (Ghosh, et al., 2009):

Laajennettavuus, esimerkiksi tuki tietorakenteiden rakentamiseen erilaisten sekoituksien (mixins) ja itsetyypitys-annotaatioiden (self-type annotations) avulla.

Staattinen ankkatyypitys (statically checked duck typing) rakennetyypityksen
avulla, joka toimii kuten dynaamisissa ohjelmointikielissä, mutta on tyyppisuojattu.

Korkeammanasteiset funktiot (higher-order functions), joita voidaan syöttää toisille funktioille parametreina ja joita voidaan vastaanottaa palautustyyppeinä.
Scala-ohjelmointikieli mahdollistaa sulkeumien käytön, mikä helpottaa abstraktien kontrollitoiminnallisuuksien toteuttamista ja toimintoalueominaista ohjelmointia.
24

Muuttumattomat tietorakenteet on sisällytetty osana pääkirjastoa, mikä rohkaisee kehittäjiä suunnittelemaan viittausläpinäkyviä abstrahointeja (referentially
transparent abstractions).

Kehittyneemmät generointirakenteet, esimerkiksi for-silmukkan sisäinen suodatusmekanismin joustava toteutus, mikä tekee koodista selkeämpää ja ytimekkäämpää.

Abstraktien tietorakenteiden mallitäsmäys (pattern matching). Scala-ohjelmointikielessä mallit ovat funktioiden osia, joita ohjelmistokehittäjät voivat laatia
erilaisten yhdistäjien (combinators) avulla ja rakentaa laajennettuja abstrahointeja.

Tapausvetoinen (event-driven) ohjelmointi toimijamallia (actor model) käyttäen. Tällaisen kevyen abstraktion avulla voidaan mallintaa skaalatutuvia vuorovaikutteisia järjestelmiä ilman tarvetta käyttää monimutkaisia käänteisohjausmalleja (inversion of control model), esimerkiksi kuuntelijoita ja takaisinkutsujia (callback).
Ghosh, et al. (2009) kuvaa Scala-ohjelmointikielen syntaksin kevyenä, ilmaisuvoimaisena ja ytimekkäänä sen erilaisten rajapintaratkaisujen ja sulkeumaominaisuuksien ansiosta. Kappaleessa 4.1 esitetty tehtävä olisi Scala-ohjelmointikielellä toteutettuna liitteen Y3 mukainen. Liitteestä Y3 voi havaita Scala-ohjelmointikielen funktionaalispainotteisen piirteen, esimerkiksi siitä, miten listojen satunnainen kokonaislukusisältö toteutetaan ja kuinka listojen läpikäyminen on toteutettu verrattuna Java- ja Groovyohjelmointikieliin.
5.2 Rakenne ja arkkitehtuuri
Chen-Becker, et al. (2009) mukaan Lift-ohjelmistokehys voidaan esittää viisitasoisena
arkkitehtuuritoteutuksena. Arkkitehtuurin kolme alinta tasoa ovat Java-virtuaalikone,
Java Enterprise Edition-ohjelmistokehyksellä (JEE) toteutettu web-säiliö sekä Scalaohjelmointikielen alusta. Näiden tasojen päällä on itse Lift-ohjelmistokehys, jonka päällä toimii sillä toteutetut web-sovellukse kuvan 5 esittämällä tavalla. Lift-ohjelmistokehys sisältää lukuisia komponentteja ja apuluokkia, jotka yhdessä tarjoavat monipuolisen kokonaisuuden rakentaa ja käsitellä erilaisia web-sovelluskohtaisia toiminnalli-
25
suuksia. Lift-ohjelmistokehyksen tärkeimmät komponentit ja niiden lyhyet kuvaukset
on esitetty seuraavana (Chen-Becker, et al., 2009):

LiftCore-komponentti on Lift-ohjelmistokehyksen ydin ja sen tarkoitus on
muun muassa käsitellä web-sovellusten pyyntöjen ja vastausten elinkaaret,
hahmotuskanavointi (rendering pipeline) ja kutsua käyttäjäkohtaisia toimintoja.
Kaikki muut Lift-ohjelmistokehyksen toiminnallisuudet toimivat LiftCorekomponentin kautta.

SiteMap-komponentti sisältää web-sovellusten sivut, sekä valikkokartoituksen
niiden välille. Tämän lisäksi komponentti tarjoaa sivutasoisen pääsyhallinta mekanismin, jonka avulla voi hallita mille sivuille kukin käyttäjä voi päästä. Mekanismi mahdollistaa myös pyynnön uudelleenkirjoittamisen (request rewrite) ja
tilariippuvaisen (state-dependent) laskennan.

LiftRules-komponentin avulla Lift:n yleiset asetukset voidaan määrittää.

LiftSession-komponentin avulla hallitaan web-istuntojen tiloja.

S-komponentti on tilallinen olio, joka kuvaa tilan kontekstia pyyntö/vastauselämänkaaressa. Web-istunnon tila tallennetaan S-olioon muuttujina.

SHtml-komponentti sisältää hyödyllisiä XHTML-elementien käsittelytoimintoja
ja apufunktioita.

Views-komponentin luokilla näytöt voidaan kuvata XML-sisältönä.

LiftResponse-komponentti kuvaa vastauksen abstraktiota, joka välitetään asiakkaalle.

Comet-komponentti kuvaa Comet-toimijakerrosta, joka mahdollistaa asynkronisen sisällön lähettämisen web-palvelimelta asiakkaan selaimeen.

Mapper/Record ORM Framework-komponentti on kevyt oliorelaatiokirjasto.
Mapper-kehys on ollut käytössä Lift-ohjelmistokehyksen aikaisemmassa versiossa (1.0), mutta myöhemmin se on korvattu Recoard-kehyksellä. Tällä kirjastokomponenteilla web-sovellukset voivat hyödyntämään oliorelaatiotietokantoja.

HTTP Authentication-komponentti tarjoaa tavan toteuttaa HTTP-tunnistuksen.

JavaScript API-komponentti tarjoaa abstraktiokerroksen hyödyntää JavaScriptkieltä. Tämä komponentti sisältää Scala-luokkia ja olioita, jotka abstrahoivat JavaScript artefakteja.

Utilities-komponentti sisältää lukuisia apufunktioita.
26
Kuva 5. Lift-ohjelmistokehyksen arkkitehtuuri
5.3 Snippet-komponentit
Chen-Becker, et al. (2010) mukaan Lift-ohjelmistokehyksessä hyödynnetään Apache
Wicket-sivupohjajärjestelmää (templating system), joka perustuu XML-rakenteiden
käsittelyyn. Lift-ohjelmistokehyksen Wicket-sivupohjajärjestelmä hyödyntää kattavasti
Scala-ohjelmointikielen XML-tukea ja mahdollistaa sisäisten sivupohjien sekä Snippetkomponenttien käytön. Snippet-komponentit ovat itsenäisiä logiikaosia, joita lisätään
sivuille tuottamaan erilaisia sivuhahmotuksia ja jotka ovat Lift-ohjelmistokehyksen
selkäranka näyttö-ensin hahmotusarkkitehtuurissa. Ne tuottavat sisään syötetystä XMLtiedoista ulostulevaa XML-dataa muuntamalla ja kartoittamalla tietosisällön. Snippetkomponentteja voidaan hyödyntää esimerkiksi web-sovellusten kaavarakenteiden käsittelyssä ja hahmotuksessa, ja niiden käyttäminen mahdollistaa myös web-sivujen tiedonesityksen ja käsittelylogiikan erottamisen toisistaan. Toteutukseltaan Snippetkomponentit ovat tavallisia Scala-luokkia, joiden sulkeumia voidaan kutsua sivupohjatoteutuksissa.
27
5.4 Toimijamalli
Odersky, et al. (2008) mukaan Scala-ohjelmointikieli tarjoaa vaihtoehtoisen säieohjelmointimallin, joka pohjautuu toimijoihin (Actors) ja se on saanut vaikutteita muun muassa Erlang-nimisestä ohjelmointikielestä. Java-ohjelmointikielen sisäänrakennettuun
säieohjelmointimalliin verrattuna Scala-ohjelmointikielen toimijamalli on Odersky, et
al. (2008) mukaan kevyempi ja helpompi toteuttaa sekä ylläpitää. Java-ohjelmointikielessä monisäikeinen toiminnallisuus tarkoittaa sitä, että olioihin on liitetty looginen
monitorointi tarkastelemaan niiden keskinäistä synkronista tilaa. Ajonaikana Javavirtuaalikone asettaa ja avaa lukkoja tietoihin siten, että vain yksi säie saa kerrallaan
pääsyn synkronoituun tietoon. Tässä mallissa erityinen haaste ohjelmoijilla on ymmärtää, mitä tietoa mikin ohjelmankohta käyttää ja muokkaa, jotta tietojen yhtenäisyys ei
rikkoontuisi eikä mahdollisilta kuolonlukkoja (dead lock) syntyisi. Näiden lisäksi testaaminen on epäluotettavaa, koska säikeiden toimintajärjestys on epädeterministinen
(Odersky, et al., 2008).
Odersky, et al. (2008) mukaan toimijamalli perustuu asynkroniseen viestinsiirtomalliin,
jossa kukin toimija on säikeenoloinen elementti, jolla on oma tila. Kukin toimija toimii
itsenäisenä tilallisena elementtinä, mutta ne voivat myös toimia keskenään lähettämällä
toisilleen asynkronisia viestejä. Toimijoiden tilaa ei kuitenkaan jaeta muiden säikeiden
kesken kuten Java-ohjelmointikielen säiemallissa, vaan toimijan tilaa pystyy muuttamaan vain lähettämällä viestejä sille. Kullakin toimijalla on myös oma viestilaatikko,
jossa ne säilyttävät ja lukevat vastaanotettuja viestejä yksikerrallaan. Viestilaatikon
avulla lähetettyjen viestin saapumisjärjestyksellä ei ole väliä, eikä vastaanotetut viesti
keskeytä toimijaa millään tavalla. Kun toimija lukee viestilaatikostaan saapuneen viestin, se vertaa viestin rakennetta Scala-ohjelmointikielen mahdollistaman mallitäsmäyksen avulla. Mikäli viesti vastaa jotain mallitäsmäyksen tyyppiä, niin toimija käsittelee
viestin ja välittää sen sopivalle funktiolle. Vaikka toimijamalli perustuukin asynkroniseen viestinvälitykseen, Ghosh, et al. (2009) mukaan se kykenee se myös toimimaan
synkronisesti tarpeen mukaan. Scala-ohjelmointikielen toimijat ovat kevyitä tapausolioita (event objects), jotka soveltuvat web-sovelluskehitykseen paremmin kuin Javaohjelmointikielen sisäänrakennettu säiemalli erityisesti niiden keveyden ja skaalatutuvuuden ansiosta. Tästä syystä Lift-ohjelmistokehys hyödyntää Scala-ohjelmointikielen
toimijamallia kattavasti, erityisesti Comet-ohjelmoinnissa.
28
5.5 AJAX- ja Comet-ohjelmointi
Chen-Becker, et al. (2010) mukaan Lift-ohjelmistokehys mahdollistaa sekä AJAX(Asynchronous JavaScript And XML) että Comet-tekniikoden käytön tuottamaan rikkaita Web 2.0 sovellusominaisuuksia. AJAX- ja Comet-tekniikoista on tullut viime
vuosina hyvin suosittu lähestymistapa rikkaiden ja dynaamisten web-sovelluksien toteuttamiseen. AJAX- ja Comet-tekniikat ovat vaihtoehto perinteiselle web-sovellusten
pyyntö/vastauselinkaarimallille. Perinteisessä mallissa web-sovelluksen käyttäjän toiminto, esimerkiksi hiiren klikkaaminen painikkeen päällä, luo pyynnön, joka lähetettään palvelimelle. Palvelin vastaanottaa pyynnön, käsittelee sen ja palauttaa sitä vastaavan vastauksen web-sovellukselle, joka hahmotetaan selaimelle. Tämän kierron jälkeen
pyyntö/vastauselinkaari on päättynyt ja uusi vastaava elinkaari aloitetaan käyttäjän seuraavasta toiminnosta. Kuvassa 6 on esitetty perinteisen pyyntö/vastauselinkaaren sovellusmalli. AJAX- ja Comet-tekniikat laajentavat tätä perinteistä mallia tarjoamalla asynkronisen päivityksen jompaankumpaan suuntaan palvelin-asiakas linjassa. AJAXtekniikassa asynkroninen päivitys on asiakkaalta palvelimelle ja Comet-tekniikassa
päivitys on palvelimelta asiakkaalle. Kummassakin tapauksessa päivitys ei ole sidottu
palvelimelta saatuun vastaukseen, kuten perinteisessä mallissa vaan päivitys tapahtuu
taustalla.
Kuva 6. Perinteinen web-sovellusten pyyntö/vastaus sovellusmalli
Chen-Becker, et al. (2010) mukaan AJAX-tekniikassa käyttäjän toiminto web-sovelluksessa lähetetään taustapyyntönä palvelimelle. Palvelin käsittelee taustapyynnön ja
lähettää JavaScript-sirpaleen (fragment) päivittämään asiakaspäädyn sivuston DOMpuurakennetta (Document Object Model). Tämä saa aikaan web-sovelluksen päivitty-
29
misen nykyisen sivun yksittäisiin kohtiin ilman tarvetta päivittää koko sivua. Kuvassa 7
on esitetty AJAX-tekniikan sovellusmalli.
Kuva 7. AJAX sovellusmalli
Chen-Becker, et al. (2010) mukaan Comet-tekniikka lisää perinteiseen sovellusmalliin
pitkäkestoisen HTTP-pyynnön ylläpitäjän (long-polling HTTP request). HTTP-pyynnön ylläpitäjä toimii taustalla ja sen avulla palvelin voi syöttää asiakaspäätyyn tietoa
ilman, että asiakkaan tulisi erikseen pyytää sitä. Comet-tekniikan avulla palvelin pystyy
syöttämään pävitykset kaikkien samalla sivulla olevien asiakkaiden näkymiin. Kuvassa
8 on esitetty Comet-tekniikan sovellusmalli. Suurin haaste Comet-ohjelmoinnissa on
huomioida skaalautuvuus palvelimen päässä.
Kuva 8. Comet sovellusmalli
30
Chen-Becker, et al. (2010) mukaan Lift-ohjelmistokehykseen sisäänrakennettu Comettekniikka pohjautuu Scala-ohjelmointikielen toimijamalliin. Toimijat pohjautuvat Erlang6-ohjelmointikielen toimijamalliin, jossa kukin toimija on asynkroninen komponentti, joka voi vastaanottaa tai lähettää viestejä vastatakseen vastaanotettuihin viesteihin. Scala-ohjelmointikielen toimijamalli kuitenkin eroaa Erlang6-ohjelmointikielen
konseptista sillä, ettei se ole ohjelmointikieleen rakennettu ominaisuus, vaan kirjastokokonaisuus. Scala-ohjelmointikielen toimijamalli on myös paljon skaalatutuvampi,
sillä toimijoilla ei ole yksi-yhteen suhdetta ohjelmasäikeiden kanssa. Chen-Becker, et
al. (2010) mukaan tästä on hyötyä esimerkiksi siinä ettei säikeet jää lukkoon tilanteissa
joissa toimijat odottavat jotain viestiä toiselta toimijalta. Yksi-yhteen suhteen sijasta
kunkin toimijan runko ja laskenta sisällytetään sulkeumiin, jotka tallennetaan sovelluksen välimuistiin. Kun sopiva viesti ilmestyy toimijalle, se suorittaa sulkeumaan sisälletyn laskennan ja toiminnallisuuden. Tämän ansiosta Lift-ohjelmistokehykseen toteutettu Comet-tekniikka on hyvin skaalautuva.
6 VERTAILU
Grails- ja Lift-ohjelmistokehyksillä on erilainen lähtökohta, mutta molemmat pyrkivät
samaan päämäärään: Vähentämään kehittäjien taakkaa tuottaa rikkaita web-sovelluksia.
Tavoite on myös tehostaa kehitysprosessia, nopeuttamalla eri vaiheita ja mahdollistaa
joustavat muutokset salliva ohjelmistoprosessimalli. Empiirinen Grails- ja Liftohjelmistokehysten vertailu suoritetaan toteuttamalla vaiheittain kehitettävä websovellus
molemmilla
web-sovellusohjelmistokehyksillä.
Molemmat
web-
sovellustoteutukset ovat omia toteutuksia, jotka valmistuttuaan tulee pystyä siirtämään
kehitysympäristöstä viralliseen ajoympäristöön. Tällä lähestymistavalla halutaan mallintaa todellisen ohjelmistokehityksen loppuvaiheessa olevaa tuotantoympäristöön siirtymistä. Tutkielmassa luotava web-sovellusprojekti on nimeltään Luma.
Tässä luvussa oletetaan että kaikki tarpeelliset teknologiat ja ohjelmistokehykset on
asennettu käyttökuntoon. Käytössä on Grails-ohjelmistokehyksen versio 1.35 ja Liftohjelmistokehyksen versio 2.1. Web-sovelluskehysten lisäksi käytössä on useita muita
teknologioita, kuten tietokanta ja servlet-säiliö, jotka on tarkoitus olla molemmilla toteutuksilla samat lopullisessa versiossa. Kuitenkin kehityksen aikana eri ohjelmistoke31
hyksillä voi olla käytössä omat kehitysajan teknologiat nopeuttaakseen evolutiivista
protoilua. Molemmissa sovelluksissa lopullinen tietokanta on MySQL 5.1-pohjainen
oliorelaatio-toteutus ja käytettävä servlet-säiliö on Apache Tomcat versio 6.0.
6.1 Yleiskuva kehitettävästä web-sovelluksesta
Kehitettävän web-sovelluksen on tarkoitus olla dynaamisesti päivitettävä verkkosivu,
jossa on lukuisia eri kategorioita ja alikategorioita kuvastamaan erilaista tietosisältöä.
Vierailevien käyttäjien tulee pystyä selaaman ja lukemaan verkkosivun kaikkia julkisia
sivuja, mutta sivustolla tulee olla myös sisäänkirjautumistoiminnallisuus. Sivustoa ylläpitävien käyttäjien tulee pystyä kirjautumaan järjestelmään sisään, jonka kautta he voivat muokata verkkosivun tietosisältöä. Verkkosivuston tulee myös mahdollistaa monikielisyystuki kahdelle eri kielelle, jotka ovat englanti ja suomi. Näiden perustoiminnallisuuksien lisäksi verkkosivulla tulee olla joitain interaktiivisia ja rikastavia Web 2.0
ominaisuuksia kuten WYSIWYG-editori (What You See Is What You Get), jonka avulla ylläpitävät käyttäjät laativat ja muokkaavat verkkosivujen tietosisältöjä. Tarkemmat
kuvaukset halutuista ominaisuuksista esitetään niiden toteutuksien yhteydessä.
Web-sovelluksen kehitys jaetaan kolmeen vaiheeseen, ja kukin vaihe voi sisältää oman
tarvittavan määrän iteraatiota. Ensimmäisessä vaiheessa web-sovellusprojekti aloitetaan
ja web-sovellukseen laaditaan sivuston pohjarakenne ja ulkoasu sekä jokunen linkki
viemään toiselle sivulle. Tässä vaiheessa kukin sivu on staattinen HTML-sivu, jossa
ainut interaktiivisuus on linkitykset sivujen välillä. Toisessa vaiheessa sivustolle laaditaan jokunen interaktiivinen toiminnallisuus, kuten sisäänkirjautuminen ja yksinkertainen palautteenanto- ja selaustoiminnallisuus. Tässä vaiheessa sivustolle laaditaan myös
monikielisyystuki ja autentikaatiotoiminnallisuus, jonka tarkoitus on rajoittaa vierailevia käyttäjiä pääsemästä suljetuille sivuille. Projektin kolmannessa vaiheessa suunnitellaan ja toteutetaan verkkosivun dynaaminen luonne. Dynaamisuuteen liittyy toiminnallisuus, jossa sisäänkirjautunut käyttäjä voi muokata web-sovellukseen kuuluvien sivujen tietosisältöä. Ainoastaan verkkosivun päärakenne ja kategoriat ovat staattisia. Sivut
ovat muokattavissa hyvin määriteltyjen sääntöjen ja pohjarakenteiden mahdollistamalla
tavalla. Yksittäisten sivujen muokkaamisominaisuuden lisäksi sisäänkirjautuneen käyttäjän tulee pystyä myös dynaamisesti lisäämään kullekin sivulle alisivuja, joiden tietosisältö tulee myös olla muokattavissa.
32
6.2 Ensimmäinen vaihe
Web-sovelluskehityksen ensimmäinen vaihe sisältää staattisen sivun pohjarakenteen ja
ulkoasun toteuttamisen, mutta siihen liittyy myös yleiset projektin aloitustoimenpiteet.
Näitä aloitustoimenpiteitä ovat projektin ja projektirakenteen luominen, sekä ajonaikaisten ympäristöjen käyttökuntoon laitto. Ensimmäisen vaiheen tarkoitus on saada
toteutettu jotain näkyvää ja interaktiivista hyvin nopeasti. Tavallisesti projektin alustaminen on hyvin työlästä, mikäli projektit tulee aloittaa aivan tyhjästä. Edessä on monen
eri teknologian, tekniikan ja kehitysympäristön yhdistäminen. Grails- ja Lift-ohjelmistokehykset tarjoavat yksinkertaisen tavan saada projektit kehitysvaiheeseen hyvin vähällä työmäärällä.
6.2.1 Projektin alustaminen
Grails-ohjelmistokehystä käyttäen projekti ja sen rakenne voidaan alustaa käyttämällä
sen tarjoamaa komentorivi-rajapintaa ja kirjoittamalla komento: grails create-app
luma (Judd,
et al., 2008). Tämä komento luo valmiin projektirakenteen ja sisällyttää
mukaan oletusarvoiset asetukset, sekä alustaa ajonaikaisen ympäristön automaattisesti.
Grails-projekteissa oletusarvoisesti kehitysaikaisena ajoympäristönä toimii jetty-säiliö
ja luodun web-sovelluksen voi käynnistää käyttämällä komentoa: grails run-app
projektin hakemistorakenteen sisällä.
Lift-ohjelmistokehyksessä ei ole Grails:in tapaan sisäänrakennettua komentorivi-rajapintaa, mutta sen sijaan Lift hyödyntää suosittua Apache Maven-teknologiaa projektin
hallintatoiminnoissa (Chen-Becker, et al., 2009). Apache Maven-teknologia tarjoaa
komentorivi-rajapinnan ja lukuisia toimintoja sekä projekti- ja riippuvuushallintamekanismin. Apache Maven-teknologialla voi muun muassa luoda uuden käyttövalmiin Liftprojektipohjan seuraavanlaista komentoa käyttäen:
mvn archetype:generate -U \
-DarchetypeGroupId=net.liftweb \
-DarchetypeArtifactId=lift-archetype-blank \
-DarchetypeVersion=2.0 \
-DarchetypeRepository=http://scala-tools.org/repo-releases \
-DgroupId=fi.luma \
-DartifactId=luma \
-Dversion=1.0-SNAPSHOT
33
Kyseinen Maven-komento suorittaa pitkän toimintosarjan, jossa Lift-ohjelmistokehyksen omasta www- tietosäiliöstä (repository) haetaan tarvittavat riippuvuudet ja muodostetaan ajokuntoinen web-sovellusprojekti, jossa lähes kaikki on oletusarvoisesti asennettu valmiiksi. Luodun projektin voi käynnistää kutsumalla Maven-komentoa: mvn
jetty:run.
Tämä komento alustaa web-sovelluksen käyttämään jetty-säiliötä ajonai-
kaisena ympäristönä.
Molemmat ohjelmistokehykset tarjoavat nopean ja samanlaisen tavan aloittaa websovellusprojektit ja luodut projektirakenteet ovat hyvin järjesteltyjä ja nimettyjä, ja ne
pohjautuvat yleisiin web-sovelluksen rakenne- ja nimeämismenetelmiin. Grailsprojektirakenne on kuitenkin kattavampi alusta lähtien, koska sen tarkoitus on tarjoa
mahdollisimman paljon valmista. Grails-projektirakenne sisältää muun muassa valmiit
kansiot ja konfiguraatiot lokalisointiin, CSS-tyylimäärityksiin, tietokantayhteyksiin ja
testeihin. Lift taas pyrkii luomaan vain kaikista oleellisimmat kansiorakenteet ja konfiguraatiot ja antaa kehittäjien itse päättää miten muut konfiguraatiot ja resurssit järjestetään projektissa. Grails projekteja voidaan täten pitää enemmän yhdenmukaistettuina,
joka on hyvä lähtökohta erityisesti aloittelijoiden kannalta sekä tilanteissa, jossa projektin kehittäjät voivat vaihtuvat sen kehityskaaren aikana. Molempien ohjelmistokehysten
komentorivi-rajapintoja voi hyödyntää samalla tavalla projektinhallinta toimintoihin.
Grails:in komentorivi-rajapinta on kuitenkin aloittelijaystävällisempi ja joustavampi
koska sitä voi laajentaa omien tarpeiden mukaan. Vaikka Grails:in sisäänrakennettu
komentorivi-rajapinta onkin helpompi omaksua ja käyttää, on sen käyttöaste rajoitetumpi, sillä se toimii vain ja ainoastaan Grails-ohjelmistokehysprojekteissa. Apache
Maven komentorivi-rajapinta on yleiskäyttöisempi, sillä sitä voi hyödyntää lähes kaikenlaisissa muissa projekteissa ja kolmannen osapuolen kehittäjät voivat laatia uusia
Lift-arkkityyppimäärityksiä erilaisten Lift-projektin aloittamisen helpottamiseksi.
6.2.2 Verkkosivupohjan toteuttaminen
Toteutettava verkkosivun tulee olla mahdollisimman yksinkertainen ja helposti navigoitava loppukäyttäjälle. Verkkosivun pohjarakenteeksi on valittu kuvan 9 mukainen pohjamalli. Tässä mallissa verkkosivu koostuu kuudesta eri toiminnallisuuden alueesta,
jotka keskitetään selainikkunan keskellä, siten että verkkosivun kummallakin sivulle
jätetään tyhjää. Tärkeimmät verkkosivun osat on selitetty lyhyesti alla:
34
 Ylävalikko on tarkoitettu sisältämään sisäänkirjautumistoiminnallisuudet,
monikielisyystoiminnallisuudet sekä muut yleiset sivunhallinta ominaisuudet.
 Banneri-osio sisältää sivuston yleiskuvastavat tunnukset ja mahdolliset mainokset.
 Päävalikko sisältää linkit kaikkiin sivuston pääkategorianäkymiin. Kullakin
pääkategorianäkymällä on joukko alikategorioita, jotka listataan navigaatiovalikossa.
 Navigaatiovalikko sisältää yksilölliset pääkategorianäkymän alikategoriat.
Näiden linkkien kautta hallitaan verkkosivun tietosisältönäkymää.
 Sisältö-osio koostuu kunkin sivun tietosisällöstä.
 Alatunniste sisältää yleiset verkkosivun tiedot ja muutaman mahdollisen interaktiivisen toiminnallisuuden kuten palautteen antamisen ja selaamisen.
tyhjää
ylävalikko
banneri
päävalikko
navigaatiovalikko
sisältö
linkki 1
linkki 2
linkki 3
alatunniste
tyhjää
Kuva 9. verkkosivun pohjamalli
Valitun pohjamallin tapauksessa ylävalikko, banneri- ja alatunnisteosat ovat staattisia
verkkosivukomponentteja, jotka näyttävä samalta missä tahansa verkkosivun tilassa.
Grails-ohjelmistokehyksessä staattiset verkkosivukomponentit voi toteuttaa yksittäisinä
GSP-sivuina, jotka hahmotetaan verkkosivulle haluttuihin kohtiin käyttämällä
<g:render>
GSP-tag kutsua (Judd, et al., 2008). Banneri-komponenti on toteutettu
omaksi div-elementiksi, jonka sisällä on kaksi verkkosivun logoa, jotka toimivat linkkeinä. Liitteessä G1 on esitetty banneri-komponentin GSP-toteutus. Grails-ohjelmistokehyksessä sivuston pohjarakenne voidaan toteuttaa yhteen GSP-sivuun, jota nimitetään tässä projektissa main.gsp sivuksi. Pääsivuun voidaan liittää irralliset GSP-sivujen
toteutukset sekä asettaa erilaisia dynaamisia GSP-tageja. Dynaamisia GSP-tageja ovat
muun muassa <g:layoutBody/> ja <g:layoutTitle/> ja niitä käytetään Judd, et al.
(2008) mukaan fuusioimaan useita eri HTML-sivutoteutuksia yhdeksi kokonaisuudeksi.
Liitteessä G2 on esitetty pääsivun toteutus, josta voi muun muassa nähdä miten eri
staattisia verkkosivukomponentteja kutsutaan ja kuinka dynaamiset GSP-tagit on ase35
tettu. Verkkosivun sisältö osiosta löytyy vain <g:layoutBody/> kenttä, jonka tehtävä
on fuusioida näytettävien HTML-sivurakenteiden <body> kentän sisältö kyseiseen kohtaan. Tämän avulla muuttuva tiedonsisältö esitetään aina samassa kohtaa verkkosivulla.
Lift-ohjelmistokehyksessä web-sovellusten sivurakenteet ovat XHTML-sivutoteutuksia
(Chen-Becker, et al., 2009). Tämän tekniikan avulla sivuston pohjarakenne on helppo
toteuttaa irrallisina sivupohjina, jotka jälkeenpäin voidaan yhdistää pääsivurakenteeseen. Lift:in sivupohjamekanismi on hyvin samanlainen Grails:iin verrattuna, mutta se
tarjoaa useamman eri lähestymis- ja toteutustavan, joka tekee siitä joustavamman kokonaisuuden. Esimerkiksi kehittäjät voivat laatia sivupohjia joko XML-pohjaisina rakenteina tai ohjelmateitse, jolloin sivupohjan XML-rakenne generoidaan dynaamisesti
ajonaikana. Lift-ohjelmistokehys tarjoaa joukon valmiita tag-kenttiä, joiden avulla sivupohjarakenteen voi toteuttaa, mutta kehittäjät pystyvät myös helposti luomaan omia
tag-kenttiä vastaavia Snippet-komponentteja. Tämän projektin verkkosivupohjan toteuttamisen kannata tärkeimmät Lift-ohjelmistokehyksen sisäänrakennetuista tagkentistä ovat <lift:surround/>, <lift:embed/> ja <lift:bind>. Chen-Becker, et
al. (2009) mukaan <lift:surround/> on ylin sitojakenttä, joka upottaa kaikki sen
sisällä olevat XML-rakenteet voidaan haluttuun kohtaan toista sivupohjarakennetta,
jonne sen vastakenttä <lift:bind> on lisätty. Kolmas tag-kenttä <lift:embed/>
mahdollistaa staattisten sivupohjien upottamisen toisiin sivupohjiin. Esimerkiksi ylävalikko, banneri- ja alatunnisteosat on toteutettu irrallisiksi sivupohjiksi ja ne liitetään
pääsivutoteutukseen <lift:embed/> kenttää käyttäen. Kuvassa 10 on esitetty esimerkki Lift:in sivupohjamekanismin hyödyntämisestä kun käyttäjä navigoi URLosoitteeseen: /luma/main/frontpage. Esimerkissä syötetyn URL-osoitteen takana on
vain itse tietosisältötoteutus, joka liitetään automaattisesti haluttuun kohtaan default.html
nimistä pääsivurakennetta. Tämä lisäksi frontpage.html ja de-
fault.html
pohjarakenteet sisältävät myös upotettuja staattisia sivupohjarakenteita
banner, footer
page.html
ja sidebar.html. Liitteissä L1 – L3 on esitetty banner.html, front-
ja default.html sivupohjarakenteiden toteutukset.
36
Kuva 10. Lift sivupohjamekanismi sisäänrakennetuilla tag-kentillä
Molemmissa ohjelmistokehyksissä verkkosivupohja on hyvin helppo toteuttaa teknisesti. Perusidea on toteuttaa yksi tai useampi pohjamalli, johon upotetaan staattisia tai dynaamisia elementtejä haluttuihin kohtiin. Molemmissa ohjelmistokehyksissä sivupohjat
toteutetaan tavallisesti HTML-tyylisinä rakenteina joissa voi hyödyntää kehittyneitä
tag-kirjastoja luomaan dynaamista sisältöä. Ohjelmistokehyksissä sivupohjien hierarkkinen lähestymistapa on kuitenkin vastakkainen. Grails-ohjelmistokehyksessä sivuston
pääelementti on itse pääsivupohja, johon upotetaan tietosisältösivu ja muut sivupohjat.
Lift-ohjelmistokehyksessä sivuston pääelementti on taas tietosisältösivu, jonka ympärille
rakennetaan
pääsivupohja
ja
muut
upotettavat
elementit.
Grails-
ohjelmistokehyksessä on täten luonnollisempi lähestymistapa sivuston hierarkian sisäistämisessä, mutta Lift-ohjelmistokehyksen vastakkainen lähestymistapa on joustavampi,
mikäli sivustolla halutaan hyödyntää useita eri pääsivupohjia. Tämän lisäksi Liftohjelmistokehys tarjoaa suhteessa enemmän eri vaihtoehtoja sivupohjien toteuttamiseen. Kokeneille kehittäjillä tällainen joustavuus voi olla tärkeää optimoinnin kannalta,
mutta aloittelijat voivat kokea sen sekoittavana tekijänä.
6.2.3 Sivujen ja linkitysten toteuttaminen
Toteutettavassa verkkosivustossa tulee olla lukuisia itsenäisiä sivuja ja näiden välillä
selailu tulee pystyä hoitamaan päävalikon ja navigaatiovalikon avulla. Päävalikkoon
lisätään linkit pääkategorioihin, ja kukin pääkategoria sisältää omat linkit sen alle kuuluviin sivustoihin. Toiminnallisesti käyttäjän tulee ensin valita haluamansa pääkategoria
päävalikosta, jonka jälkeen verkkosivun navigaatiovalikkoon hahmottuu linkit pääkate37
goriaan liittyvistä alikategorioista. Kukin sivu voi sisältää omaa tietosisältöä, joten ne
tulee toteuttaa itsenäisinä sivuina.
Grails-projekti
Grails-ohjelmistokehyksessä sivut ja linkityksen voi toteuttaa helpoiten hallintaluokkien avulla. Hallintaluokkia voi luoda Grails komentorivi-rajapinnan avulla käyttämällä
komentoa: grails create-controller (Judd, et al., 2008). Hallintaluokan luominen
lisää projektiin kolme eri tekijää: itse hallintaluokan, hallintaluokkaa vastaavan testiluokan sekä hallintaluokan nimisen kansion views-kansion sisälle. Tämän jälkeen GSPsivut voidaan toteuttaa luodun hallintaluokkanimisen kansion sisälle, joka sijaitsee
views-kansiossa.
Kukin sivu tulee toteuttaa omana GSP-tiedostona, joista yksi esi-
merkki löytyy liitteestä G3. Toteutustasolla sivuston pohjarakenteen päävalikon linkit
vastaavat omia yksittäisiä hallintaluokkia ja navigaatiovalikon linkit vastaavat hallintaluokkaan kuuluvia GSP-sivuja. Kun hallintaluokan sisään kuuluvat GSP-sivut on toteutettu, niin niitä vastaavat linkit tulee määrittää hallintaluokan sisälle omina sulkeumina,
joiden nimet vastaavat GSP-sivujen nimiä. Hallintaluokkaan luodut sulkeumat voivat
sisältää mahdollista käsittely- ja ohjauslogiikkaa, joiden avulla voidaan määrittää mitä
missäkin linkin painalluksessa tapahtuu. Koska projekti on vielä ensimmäisessä vaiheessa, sulkeumien sisälle ei tarvitse vielä kirjoittaa logiikkaa, jolloin ne toimivat tavallisina linkkeinä GSP-sivujen välillä. Liitteessä G4 on esitetty esimerkki hallintaluokan
toteutuksesta.
Jotta linkkejä voitaisiin hyödyntää verkkosivulla, tulee ne lisätä jonnekin. Linkit voitaisiin toteuttaa irrallisina <g:link> tag-kenttinä GSP-sivuilla, mutta on olemassa myös
valmiita Grails-laajennuksia, jotka mallintavat tarvittavaa selailu-ominaisuutta. Työn
tehostamiseksi käyttöön on valittu valmis Grails Navigation-laajennus, jonka voi ottaa
käyttöön mihin tahansa Grails-projektiin kutsumalla seuraavaa komentoa: grails install-plugin
navigation.
Komennon ajaminen asentaa tarvittavat navigaatio-
laajennuksen riippuvuudet suoraan kehitteillä olevaan projektiin, jonka jälkeen ne ovat
valmiita käyttö varten. Laajennus lisää automaattisen navigaatiototeutuksen, joka voidaan sijoittaa haluttuun paikkaan lisäämällä <nav:render/> tag-kenttä. Vastaavasti
navigaation
alavalikko
<nav:renderSubItems/>
voidaan
sijoittaa
haluttuun
paikaan
käyttämällä
tag-kenttää. Liitteessä G4 on esitetty kuinka navigaatio-
laajennus konfiguroidaan käyttökuntoon ja liitteessä G2 on esitetty miten navigaatio
hahmotetaan sivulle.
38
Lift-projekti
Lift-ohjelmistokehyksessä sivujen ja linkityksen toteuttamisen sydän on SiteMapkomponentissa (Chen-Becker, et al., 2009). Kaikki web-sovelluksen piiriin kuuluvat
sivut joihin halutaan päästä käsiksi ajonaikana, tulee määrittää SiteMap-komponentissa.
Liitteessä L5 on esitetty ensimmäisen vaiheen aikana toteutettu SiteMap-komponentti,
jota kutsutaan Lift-ohjelmistokehyksen alustusluokan bootsrap.liftweb.Boot metodissa boot(). Kukin irrallinen sivu esitetään yksilöllisenä komponenttina, jolla on
uniikki tunnus ja muut taustatiedot kuten esimerkiksi linkin fyysinen osoite, nimi ja
joukko johon linkki kuuluu. Liitteessä L5 on luotu linkit kutakin pääkategorian ja näiden alakategorian sivua varten ja ne on jaettu ryhmiin siten, että kukin pääkategorian
linkki kuuluu samaan ryhmään ja eri pääkategorian alakategoriat kuuluvat samaan
ryhmään. SiteMap-komponentin tarkoitus on keskitetysti määrittää web-sovelluksen
sivut. Kullekin sivulle voi SiteMap-komponentin avulla määrittää tunnistetietojen lisäksi myös pääsy- ja näkyvyysrajoitteet käyttäjätunnistusmenettelyä varten. Sivujen SiteMap-määrityksen lisäksi sivut tulee toteuttaa fyysisesti HTML-rakenteina. Liitteessä L2
on esitetty etusivun HTML-rakenne.
Chen-Becker, et al. (2009) mukaan Lift-ohjelmistokehys tarjoaa sisäänrakennetun tehokkaan tavan esittää sivuston linkit valmiina linkkijoukkoina. Käytössä on
<lift:Menu/>
Snippet-komponentti, joka hoitaa linkkijoukkojen hahmotuksen
XHTML-rakenteiksi. Kehittäjällä on käytössä lukuisia eri tapoja määrittää miten linkkijoukot hahmotetaan, mutta yksinkertaisimmillaan hahmotus tapahtuu lisäämällä
<lift:Menu.builder/>
Snippet tag-kutsu haluttuun kohtaan sivua. Kyseinen tag-
kutsu hahmottaa linkkilistan web-sovelluksen kaikista sivuista, jotka ovat näkyviä.
Koska Luma-projektissa pääkategoriat ja alakategoriat halutaan esittää irrallisina kokonaisuuksina, niin käyttöön on otettu <lift:Menu.group/> Snippet-tag kutsu, joka
muodostaa linkkilistan tietyllä joukkotunnuksella merkityistä linkeistä. Liitteissä L3 ja
L4 on esitetty esimerkki kyseistä Snippet tag-kutsusta. Mikäli osa web-sovelluksen
sivujen linkeistä on tarkoitus olla salaisia, ne voidaan määrittää SiteMapkomponentissa näkymättömiksi asettamalla niille Hidden parametri. Tällöin linkki ei
hahmotu <lift:Menu/> tag-kutsusta, mutta sivu on olemassa ja sinne johtavan linkin
voi luoda liittämällä sivulle <lift:Menu.item/> tag-kenttä.
39
Vertailu
Grails-ohjelmistokehyksen yksi merkittävin vahvuus on sen liitännäisjärjestelmä, jonka
avulla kehittäjät voivat helposti ottaa käyttöön kolmannen osapuolen toteuttamia laajennuksia. Navigation-laajennus on hyvä esimerkki siitä, ettei Grails-projekteissa tarvitse keksiä jokaista pyörää uudestaan, mutta ohjelmistokehyksen ei itse tarvitse sisällyttää mitään valmista työkalua yksinkertaisen toiminnallisuuden toteuttamiseen. Vaikka
Lift-ohjelmistokehyksen sisäänrakennettu linkkijoukkojen hahmotustyökalu on tehokas
ja joustava, niin se ei vedä vertoja Grails:in liitännäisjärjestelmän ideologialle. Sisäänrakennettujen työkalujen heikkoutena on se, etteivät ne sovellu välttämättä aivan kaikkiin tapauksiin. Tämän lisäksi kehittäjät voivat tulla nopeasti sokeiksi käyttämään tiettyä sisäänrakennettua toteutusmallia, mikä ei välttämättä ole hyvä kyseisessä tilanteessa. Grails-ohjelmistokehyksen liitännäisjärjestelmä mahdollistaa sen, että kolmannen
osapuolen kehittäjät voivat toteuttaa tiettyyn tapaukseen soveltuvan toteutusmallin, jota
muut kehittäjät voivat hyödyntää samanlaisissa projekteissa. Grails:in liitännäisjärjestelmän heikkous on kuitenkin se, etteivät kaikki laajennukset ole välttämättä kattavasti
testattuja tai edes yhteensopivia kaikkien Grails versioiden kanssa.
Sivustonhallinnassa Lift:in keskitetty lähestymismalli SiteMap-komponentin avulla on
tehokas tapa havainnollistaa ja ylläpitää sivujen olemassaoloa, näkyvyyttä ja rajoituksia. Kuitenkin mikäli web-sovellus on erittäin massiivinen voi keskitetty lähestymistapa
osoittautua tehottomaksi. Lift kuitenkin mahdollistaa SiteMap-komponentin käytön
usealla eri tavalla jolloin erikokoiset web-sovellukset voidaan toteuttaa parhaaksi toteamalla tavalla. Grails-ohjelmistokehyksen hallintaluokat perivät myös keskitetyn sivustonhallintaidean, mutta vain tiedonkäsittelyn ja ohjauksen näkökulmasta.
6.3 Toinen vaihe
Projektin toisessa vaiheessa web-sovellukseen toteutetaan sisäänkirjautumis- ja autentikaatiotoiminnallisuus, monikielisyystuki sekä yksinkertainen palautteenantamis- ja selailutoiminnallisuus. Palautetoiminnallisuudessa käyttäjä voi syöttää web-sovellukseen
liittyvää vapaamuotoista palautetta ja sivuston ylläpitäjä voi selata ja lukea niitä. Syötetty palaute tallennetaan automaattisesti tietokanta ja ylläpitäjälle toteutettava selailurajapinta mahdollistaa tallennettujen palautteiden hakemisen, lukemisen ja tarpeen mukaan poistamisen. Monikielisyystuki ja palautteenantamistoiminnallisuus eivät vaadi
40
sivustolla käyttäjän tunnistamista, joten ne toteutetaan ensimmäisenä. Monikielisyystuki on rajoitettu kahteen tarvittavaan kieleen: suomeen ja englantiin. Näiden ominaisuuksien jälkeen toteutetaan sisäänkirjautuminen, käyttäjätunnistaminen sekä käyttöliittymä ylläpitäjille selata ja lukea annettua palautetta web-sovelluksen sisällä. Tässä vaiheessa web-sovellukseen liitetään tietokanta ja laaditaan tarpeelliset toimintoaluemallit.
6.3.1 Monikielisyystuen toteuttaminen
Monikielisyystuen toteuttaminen vaatii kahden eri tekstityypin huomioimista: staattisten tekstien ja virallisen tietosisältötekstien. Staattiset tekstit ovat verkkosivulle upotettuja tekstejä, joihin voi luokitella esimerkiksi painikkeiden ja eri linkkien tekstit. Tietosisältötekstit ovat taas käyttäjien ja sivuston ylläpitäjien kirjoittamia tekstejä.
Grails-projekti
Judd, et al. (2008) mukaan Grails:in sisään on rakennettu i18n-teknologiatuki, joka
mahdollistaa monikielisyyden toteuttamisen. Tässä teknologiassa verkkosivun kielivalintaa voidaan muuttaa lisäämällä verkkosivun URL-osoitteeseen lang-parametri, jonka
arvona on joku haluttu kielilokaali, esimerkiksi fi (suomi) tai en (englanti). Kun langparametri on kerran syötetty URL-osoitteessa, niin Grails tallentaa sen automaattisesti
evästeisiin jonka avulla sivusto on tietoinen millä kielellä sisältö tulee esittää. Staattisia
tekstejä varten Grails kehittäjän tulee aluksi luoda lokaalitiedosto kutakin tuettua kieltä
varten. Lokaalitiedostot sisältävät kielikäännökset kaikkiin tarvittaviin staattisiin teksteihin avain-arvo pareina. Lokaalitiedoston luomisen jälkeen kehittäjän tarvitsee vain
korvata kaikki GSP-sivuihin upotetut tekstit <g:message/> tag-kentillä, jolle annetaan
code-attribuuttina
jokin lokaalitiedoston avainta vastaava tunnus. Liitteessä G5 on esi-
tetty <g:message/> tag-kentän käyttöä. Koska toisessa vaiheessa verkkosivut ovat vielä staattisia luonteeltaan, voidaan tietosisältöjen monikielisyys toteuttaa staattisesti
GSP-sivuihin. Kuvassa 11 on esitetty yksi mahdollinen lähestymistapa, jossa GSPsivulle on toteutettu ehdollisrakenne käyttäen <g:if> tag-kenttää. Mikäli URLosoitteessa on mukana lang-parametri ja sen arvo on ”en”, niin käytetään ensimmäisen
lohkon tietosisältö, muussa tapauksessa käytetään toisen lohkon tietosisältöä.
41
...
<g:if test="${params.lang == 'en'}">
<h1>Title</h1>
english version of the text
...
</g:if>
<g:else>
<h1>Otsikko</h1>
suomenkielinen versio tekstistä
...
</g:else>
...
Kuva 11. Tietosisällön monikielisyystuen toteuttaminen staattisena GSP-sivuna
Jotta kielivalintaa olisi helpompi käyttää, eikä sitä tarvitsisi kirjoittaa URL-osoitteeseen
manuaalisesti, tulee web-sovellukseen laatia painike, joka syöttää kielivalinta-parametrin automaattisesti URL-osoitteeseen. Liitteessä G6 on esitetty web-sovelluksen staattisen ylävalikon toteutus, jossa on kielivalinta toiminnallisuus ja liitteessä G7 on esitetty
toiminnallisuutta vastaava hallintaluokka.
Lift-projekti
Chen-Becker, et al. (2010) mukaan myös Lift-ohjelmistokehykseen on sisäänrakennettu
i18n-teknologiatuki. Monikielisyystuki voidaan täten toteuttaa lähes samalla tavalla
kuin Grails-ohjelmistokehyksessä. Ainut ero on se, että Lift-ohjelmistokehys ei tarjoa
valmista sisäänrakennettua lokalisoinnin määritysmekanismia vaan ainoastaan resurssien käsittelymekanismin. Liitteessä L6 on esitetty yksi mahdollinen lokalisoinnin määritysmekanismitoteutus, jossa web-sovellus tarkastaa löytyykö URL-osoitteesta langnimistä parametria tai onko selaimen evästeisiin tallennettu aikaisemmin kyseinen parametri. Kyseinen parametri määrittää mitä lokalisointia web-sovelluksen tulee käyttää.
Staattiset ja upotetut tekstit tulee ilmaista Lift-projektin sivupohjissa tag-kentällä:
<lift:loc/>,
jolle voi syöttää locid-attribuutin. Attribuutin arvo on jokin lokaalitie-
doston avain-arvo, jolle on vastaava kielikäännös. Ohjelmakoodissa staattisia tekstejä
voi kutsua S-komponentin funktiota ? kuten esimerkiksi liitteessä L5 on tehty linkkien
nimien määrityksessä. Chen-Becker, et al. (2010) mukaan Lift-ohjelmistokehyksessä
lokalisointilaajuus ulottuu lokaalitiedostojen ohella myös sivupohjatiedostoihin. Tällöin
kustakin sivupohjasta voi luoda oman kieliriippuvaisen version, joiden avulla tietosisältö tekstien lokalisoinnin voi toteuttaa. Kieliriippuvaiset sivupohjat tulee nimetä päättymään alaviivalla varustettuun kielilokaalin tunnukseen, esimerkiksi frontpage_fi.html
tai frontpage_en.html.
42
Lokaalin valintatoiminnallisuus voidaan toteuttaa Lift-projektissa luomalla kaksi uutta
piilotettua linkkiä SiteMap-komponenttiin kuvan 12 tavalla. Koska linkit ovat piilotettuja, ne eivät näy missään <lift:Menu/> Snippet-tagin generoimassa linkkijoukossa,
mutta niitä pystyy liittämään sivuille <lift:Menu.item/> tag-kentän avulla. Liitteessä
L7 on esitetty topbar.html sivupohjatoteutus, jossa kaksi kielivalinnan linkkiä esitetään. Kun käyttäjä painaa jompaakumpaa linkkiä, ohjautuu hän sivuston pääsivulle ja
kielivalinta muuttuu halutuksi.
Menu(Loc("inFinnish", List("luma", "main", "frontpage?lang=fi"),
S ? "in.finnish", Hidden)),
Menu(Loc("inEnglish", List("luma", "main", "frontpage?lang=en"),
S ? "in.english", Hidden))
Kuva 12. Kielivalinta linkkien määritys SiteMap-komponentissa
Vertailu
Chen-Becker, et al. (2010) mukaan monikielisyystuen mahdollistaminen on yksi tärkeimpiä ominaisuuksia nykypäivän web-sovellusohjelmistokehyksissä. Kummankin
ohjelmistokehyksen kohdalla monikielisyystuki oli helppo ja nopea toteuttaa standardisoitujen mallien avulla. Lift-ohjelmistokehys tarjosi enemmän valinnanvaraa kuinka
monikielisyystuki toteutettaisiin, mutta se vaati myös enemmän työpanosta ja lähdekoodia Grails-ohjelmistokehykseen verrattuna. Lift-ohjelmistokehyksen monikielisyys
toteutuksen merkittävin etu Grails-ohjelmistokehykseen verrattuna on se, että monikielisyystuen laajuus ulottuu oletusarvoisesti myös sivupohjiin. Grails-projektin toisen
vaiheen tietosisältöjen monikielisyystukitoteutus voi osoittautua kömpelöksi ja virhealttiimmaksi, mikäli kielivalintamahdollisuuksia olisi useampia. Grails-ohjelmistokehyksen sisäänrakennettua monikielisyystukea voidaan pitää aloittelijaystävällisempänä, sillä se ei vaadi lainkaan konfigurointia tai lähdekoodintoteutusta, mutta sen hyödyntäminen on myös rajoitetumpaa.
6.3.2 Palaute ominaisuus
Palautteen antamisominaisuuden tarkoitus on mahdollistaa helppo verkkosivua koskevan palautteen keräys ja ylläpito. Palautetta voi antaa kuka tahansa joko nimellisenä tai
nimettömänä ja se tallennetaan tietokantaan. Tärkeimmät vaiheet palautteenkeräämisominaisuutta varten on luoda sitä vastaava toimintoaluemalli ja muuttaa se yhteen43
sopivaksi tietokantaa varten sekä asentaa web-sovellus kommunikoimaan tietokannan
kanssa. Näiden lisäksi web-sovellukseen tulee laatia kaavarakennesivu, johon käyttäjä
voi syöttää palautetta koskevat tiedot. Palautteen antamissivulle voi päästä käsiksi
verkkosivun alatunnisteissa olevan linkin kautta.
Grails-projekti
Judd, et al. (2008) mukaan Grails tarjoaa helppokäyttöisen ja tehokkaan tavan tuottaa
toimintoaluemalleja. Kehittäjä voi luoda toimintoalueluokkia käyttämällä seuraavaa
komentoa: grails create-domain-class. Kyseinen komento luo toimintoalueluokan
ja sitä vastaavan testausluokan. Toimintoalueluokka on tavallinen groovy-luokka, joka
vastaa tietokantataulua ja luokkaan määritellyistä tietokentistä muodostuu tietokantataulun sarakkeet. Liitteessä G8 on esitetty palautetta vastaava toimintoalueluokan toteutus. Judd, et al. (2008) mukaan kun toimintoalueluokka on luotu niin sitä voi hyödyntää
erilaisten apumetodien avulla suoraan tietokantaa vasten. Esimerkiksi luomalla luokasta
uuden ilmentymän ja kutsumalla sen metodia save() tallentuu uusi luotu olio suoraan
tietokantaan ja sen voi hakea toimintoalueluokan staattisten apumetodin avulla. Grails
hyödyntää oletusarvoisesti HSQLDB-tietokantaa, joka tallentaa kaiken oletusarvoisesti
ajonaikaiseen muistiin, mutta sen voi myös asettaa tallentamaan tiedot kovalevylle.
Kehittäjille tämä on suuri apu sillä alkuvaiheen tietokantaintegraatio voi yleensä heikentää työtehoa estämällä projektia etenemästä muilta osin. Judd, et al. (2008) mukaan
toimintoalueluokan voi halutessa asettaa dynaamiseen sivumuodostustilaan (scaffold)
liittämällä sen johonkin hallintaluokkaan. Tällöin Grails luo ajonaikana toimintoalueluokkaa vastaavat CRUD (create-read-update-delete) GSP-sivut, joihin voi päästä
käsiksi hallintaluokan kautta. Toimintoalueluokan jälkeen kehittäjien tarvitsee vielä
toteuttaa palautetta vastaava hallintaluokka ja GSP-sivut. Liitteessä G5 on esitetty palautteen antamiseen rakennekaavasivu ja liitteessä G9 on esitetty palautetta varten luotu
hallintaluokka. Liitteen G8 hallintaluokasta vain kolme ensimmäistä sulkeumaa on toteutettu itse ja loput sulkeumista on Grails:in automaattisesti generoimia. Automaattinen generointi on toteutettu kutsumalla Grails komentorivi-rajapinnan komentoa:
grails generate-views
(Judd, et al., 2008). Kyseinen komento generoi dynaamisen
sivumuodostustilan tavalla CRUD-sivut ja kirjoittaa ne fyysisiksi hallintaluokan GSPsivuiksi Grails-projektiin, jonka jälkeen niitä voi halutessa muokata omien tarpeiden
mukaan. Grails web-sovelluksissa tiedonsidonta kaavarakenteista hallintaluokkiin tapahtuu automaattisesti nimeämällä kukin kaavarakenteen kenttä yksilöivällä tunnuksel-
44
la, joka vastaa jotain toimintoalueluokan kenttää. Tiedonkäsittely tapahtuu keskitetysti
hallintaluokassa, jossa määritetään myös jatkotoiminnot ja ohjaukset.
Lift-projekti
Lift-ohjelmistokehyksessä palautteenanto toiminnallisuuden tietokanta- ja toimintoaluepuolen toteutus tehdään laatimalla palautemallia vastaava luokka, MySQLtietokantayhteysolio sekä konfiguroimalla luodusta palautemalliluokasta tietokantaskeema. Tämän jälkeen voidaan toteuttaa palautelomakkeen sivupohjatoteutus ja rakennekaavan käsittelevä Snippet-luokka. Liitteessä L8 on esitetty palautemallia vastaava
luokkatoteutus. Luokka hyödyntää Lift-ohjelmistokehyksen tarjoaa Mapper-komponenttia, jonka avulla luokka perii kaikki oleellisimmat funktiot tietokantahakuja ja tallennusta varten. Luokka tulee vastamaan yhtä tietokantataulua ja luokan sisältämät kentät vastaavat tietokantataulun sarakkeita. Mapper-komponentin hyödyntäminen mahdollistaa myös erilaisten metodien ylikirjoituksen, joiden avulla kehittäjät voivat tehokkaasti vaikuttaa siihen millainen tietokantamalli kustakin toimintoalueluokasta todellisuudessa syntyy. Toimintoaluemallin jälkeen kehittäjien tulee toteuttaa tietokantayhteyttä vastaava ainokaisolio (singleton), josta on esitetty esimerkki liitteessä L9. Ainokaisolion toteutuksen jälkeen se ja toteutettu toimintoalueluokka tulee konfiguroida
toimimaan web-sovelluksessa. Konfiguraatio tulee toteuttaa Lift-ohjelmistokehyksen
alustusluokan bootsrap.liftweb.Boot metodissa boot() ja Lift tarjoaa valmiit apuluokat näiden toteuttamiseen, mutta mahdollistaa myös muokattujen ratkaisujen toteuttamisen. Käytettävät Lift-apuluokat ovat DB ja Schemifier. DB-apuluokka asentaa automaattisesti tietokantayhteyden aikaisemmin luodun tietokannan ainokaisolion perusteella ja Schemifier-apuluokka muodostaa käytössä olevaan tietokantaan tietokantaskeemat luoduista toimintoaluemallitoteutuksista. Kuvassa 13 on esitetty esimerkki molempien apuluokkien hyödyntäminen alustusluokassa. Näiden toteutuksien jälkeen toimintoaluemallin luokkan ilmentymiä voidaan käsitellä ja tallentaa tietokantaan kutsumalla sopivia CRUD-metodeja kuten create, save ja delete. Käytössä on myös hakumetodeja kuten findAll, jonka avulla tietokannasta voi hakea haluttuja tietoja.
45
DB.defineConnectionManager(DefaultConnectionIdentifier, DBVendor)
...
def schemeLogger (msg : => AnyRef) = {
logger.info(msg)
}
Schemifier.schemify(true, schemeLogger _, User, Feedback)
...
Kuva 13. Tietokanta-apuluokkien hyödyntäminen
Tallennuskerroksen jälkeen Lift-projektissa tulee vielä toteuttaa itse palautteenkeräyslomake ja sen käsittelevä Snippet-komponentti. Liitteessä L10 on esitetty palautteenkeräyslomakkeen kaavarakennetoteutus, joka ei sisällä HTML-pohjaisia kaavarakenteiden
kenttiä
suoranaisesti.
Kaavarakenne
on
toteutettu
hyödyntämällä
Lift-
ohjelmistokehyksen tiedonsidontaa ja itse luotua Snippet-komponenttia nimeltä
<lift:GiveFeedback.add/>,
jonka toteutus on esitetty liitteessä L11. Lomakkeen
kaikki tietokentät on sidottu e-nimiseen sitojaan ja kullakin tietokentällä on oma yksilöivä tunnus, esimerkiksi e:name. Kun lomake syötetään lähetyspainiketta painamalla,
niin Lift kutsuu automaattisesti kaavarakenteen Snippet-komponenttia, joka hoitaa tiedontiedonkäsittelyn ja ohjauksen. Snippet-komponentti vastaanottaa tietokenttien arvot,
tarkastaa, ettei palaute ole tyhjä ja tallentaa sen ilmentymän tietokantaan. Tiedonkäsittelyn jälkeen käyttäjä ohjataan uudelle sopivalle sivulle.
Vertailu
Kahdesta ohjelmistokehyksestä Grails:n tapauksessa palautteenantotoiminnallisuus oli
tehokkaampi toteuttaa ja testata. Tämä johtui siitä, ettei Grails vaatinut tietokantayhteyksien luomista ja kaikki sivupohjat ja tallennustoiminnallisuudet pystyi generoimaan
suoraan toimintoalueluokan pohjalta. Grails-projektin tapauksessa ainut merkittävä asia
oli luoda toimintoalueluokka, generoida valmiit pohjat ja hienosäätää sen ulkoasua.
Lift-ohjelmistokehys vastaavasti antoi paljon joustavuutta ja erilaisia toteutustapoja
optimoida toiminnallisuus, mutta vastapainoksi vaati kehittäjän toteuttavan enemmän
lähdekoodia. Lift-projektin hyvä puoli on kuitenkin sen lähestymismalli, jossa tiedonesitys ja logiikka erotellaan toisistaan. Lift-projektissa lomakkeen kaavarakenteesta tuli
hyvin yksinkertainen ja ymmärrettävä, koska mitään logiikkaa ei tarvinnut kirjoittaa
itse kaavarakenteisiin, vaan kaikki käsittelyt ja logiikat hoidettiin Snippet-komponentin
sisällä.
46
6.3.3 Sisäänkirjautuminen ja käyttäjän tunnistaminen
Sisäänkirjautumis- ja käyttäjän tunnistusominaisuuden tarkoitus on rajoittaa joidenkin
web-sovelluksen sivujen näkyvyyttä ja toiminnallisuutta vieraileville käyttäjille. Tietyt
sivut ja toiminnallisuudet ovat käytettävissä vain valitulle käyttäjäjoukolle ja tämä rajaus tapahtuu web-sovelluksessa sisäänkirjautumisen avulla. Käyttäjällä tulee olla tiedossa yksilöivä käyttäjätunnus ja sitä vastaava salasana, jotka syöttämällä websovellukseen, pääsee käsiksi rajoitettuihin toiminnallisuuksiin ja sivuihin. Kehitettävässä web-sovelluksessa on tarve vain kahdenlaisille käyttäjille: vierailevilla ja ylläpitäjille. Vierailevien käyttäjien ei tarvitse kirjautua web-sovellukseen sisään, vaan voivat
selata web-sovelluksen julkisia vapaasti. Ylläpitokäyttäjät voivat myös selata websovelluksen julkisia sivuja vapaasti, muta mikäli he haluavat päästä suljetuille hallintasivuille, tulee heidän kirjautua web-sovellukseen käyttäjätunnuksillaan.
Grails-projekti
Grails-projektissa käyttäjän tunnistautumisen voi hoitaa monella eri tavalla ja jaossa on
lukuisia valmiita Grails-laajennuksia, jotka toteuttavat erilaisia tietoturvastandardeja ja
hyviksi todettuja menettelytapoja. Tähän projektiin tietoturvalaajennukseksi on valittu
Spring Security:n toteutus nimeltään Acegi. Laajennuksen voi asentaa käyttökuntoon
kutsumalla komentoa: grails install-plugin acegi. Judd, et al. (2008) mukaan
Acegi määrittää kolmenlaiset toimintoalueluokat, joiden avulla käyttäjän tunnistautumista hoidetaan: Person, Authority ja Requestmap. Person-luokka vastaa yksittäistä
käyttäjää ja Authority-luokka vastaa olemassa olevia käyttäjätasoja. Kukin yksittäinen
käyttäjä voi kuulu eri käyttäjätasoihin ja kullakin käyttäjätasolla voi olla omia, vain
sille ominaisia toiminnallisuuksia ja sivunäkyvyyksiä. Käyttäjätasojen sivunäkyvyysrajoitukset tulee määrittää Requestmap-luokkaan osoite-käyttäjätaso avain-arvopareina.
Kuvassa 14 on esitetty esimerkki Requestmap-luokan avain-arvo pareista, jossa tietyt
sivunäkyvyydet rajoitetaan näkymään ROLE_ADMIN käyttäjätason omaaville käyttäjille
new RequestMap(url:"/category/**",configAttribute:"ROLE_ADMIN").save()
new RequestMap(url:"/pagecontent/**",configAttribute:"ROLE_ADMIN").save()
new RequestMap(url:"/page/**",configAttribute:"ROLE_ADMIN").save()
Kuva 14. Esimerkki Requestmap-luokan avain-arvo pareista
Acegi-laajennuksen asentaminen projektiin luo myös hallintaluokkia ja GSP-sivut sisäänkirjautumista varten, sekä lisää lukuisia hyödyllisiä GSP tag-kenttiä, joilla voi määrittää käyttäjän tunnistautumisseen liittyviä toiminnallisuuksia. Liitteessä G6 on esitetty
47
esimerkki Acegi-laajennuksen hallintaluokkien ja GSP tag-kenttien hyödyntämisestä.
Käytettävä tag-kentät ovat <g:isLoggedIn> ja <g:loggedInUserInfo> ja niiden tarkoitus on määrittää eri toiminnallisuutta riippuen siitä onko käyttäjä sisäänkirjautunut
web-sovellukseen vai ei.
Lift-projekti
Lift-projektissa sisäänkirjautuminen ja käyttäjäntunnistus voidaan toteuttaa ottamalla
käyttöön käyttäjää mallintava toimintoalueluokka, joka toteuttaa Lift-ohjelmistokehyksen tarjoaman MetaMegaProtoUser-ominaisuusluokan (trait). Chen-Becker, et al.
(2009) mukaan MetaMegaProtoUser-ominaisuusluokka tarjoaa valmiit toiminnallisuudet sisään- ja uloskirjautumiseen, salasanan palauttamiseen ja uusien käyttäjien luomiseen. Ominaisuusluokka kattaa logiikkatoiminnallisuuden lisäksi myös sivupohjatoteutukset, jotka on upotettu ominaisuusluokan sisälle Template-ilmentymänä. Liitteessä
L12 on esitetty käyttäjää mallintavan toimintoalueluokan toteutus. Jotta MetaMegaProtoUser-ominaisuusluokan
tarjoamat sivut olisivat käytössä, tulee ne lisätä SiteMap-
komponenttiin manuaalisesti. Helpoin tapa on lisätä sivut SiteMap-komponentin sivulistaan, on kutsumalla ominaisuusluokan tarjoamaa sitemap() metodia, joka palauttaa
kaikki ominaisuusluokan toteuttamat sivut. Liitteen L5 lopussa on esimerkki miten kyseistä metodia voidaan hyödyntää. Koska web-sovelluksen pitää pystyä esittämään erilaisia toimintoja riippuen siitä onko käyttäjä sisäänkirjautunut järjestelmään vai ei, tulee
Lift-projektiin laatia uusi Snippet-komponentti, joka tuottaa dynaamisesti sivupohjia.
Esimerkki vastaavanlaisesta Snippet-komponentista on esitetty liitteessä L13 ja sivupohja, jossa kyseistä Snippet-komponenttia hyödynnetään liitteessä L7. Yllämainitulla
menetelmällä voidaan rajoittaa toimintojen näkyvyyttä, mutta Chen-Becker, et al.
(2009) mukaan tärkeää on myös rajoittaa toiminnallisuuksiin pääsy. Lift-ohjelmistokehyksessä sivuja voidaan rajoittaa vain tietylle käyttäjäkunnalle asettamalla SiteMapkomponentin sivuille suodatuksia ja parametreja. Kuvassa 15 on esitetty esimerkki SiteMap-komponenttiin lisättävästä browseFeedback-sivusta, jonne vain sisäänkirjautuneella käyttäjällä on pääsy. Sivulle asetetaan arvo-parametri nimeltään loggedIn, jonka
arvo on määritetty toiminnallisuus Aina kun browseFeedback-sivulle pyritään menemään, loggedIn arvoparametrin toiminnallisuus käydään läpi ja mikäli käyttäjä ei ole
sisäänkirjautunut, ohjataan hänet sivulle pääsyn sijasta sisäänkirjautmissivulle.
48
val loggedIn = If(() => User.loggedIn_?, () =>
RedirectResponse("/user_mgt/login "))
...
Menu(Loc("browseFeedback", List("luma", "feedback", "browse"),
S ? "browse.feedback", loggedIn))
Kuva 15. Esimerkki sivulle pääsyrajoituksesta SiteMap-komponentissa
Vertailu
Grails-ohjelmistokehyksen
liitännäisjärjestelmän
tarjoama
etulyöntiasema
Lift-
ohjelmistokehykseen verrattuna on jälleen havaittavissa. Grails-projektin kehittäjät voivat helposti valita käyttöön haluamansa valmiiksi toteutetun autentikaatio-laajennuksen
ja vain muutamalla konfiguraatiolla saada siitä kaiken hyödyn irti. Chen-Becker, et al.
(2009) mukaan Lift-ohjelmistokehyksen tarjoama MetaMegaProtoUser-ominaisuusluokan tarkoitus on soveltua yksinkertaisten sivujen autentikaatiojärjestelmäksi ja tarjota näkökulmaa kehittäjille siitä miten laajemmille web-sovelluksille kannattaa kehittää
oma autentikaatiojärjestelmä. Lift-ohjelmistokehys tarjoaa täten vain esimerkin mahdollisesta autentikaatiojärjestelmästä ja kehottaa kehittäjiä toteuttamaan oman websovellukseen paremmin soveltuvan autentikointijärjestelmän. Grails-ohjelmistokehystä
voidaan täten pitää tehokkaampana valinta, mutta Lift-ohjelmistokehyksen lähestymistapaa voidaan pitää parempana, mikäli kehitettävän web-sovelluksen autentikaatiojärjestelmä vaatii uniikkia toiminnallisuutta jota valmiiksi toteutetut autentikaatiojärjestelmät eivät voi tarjota.
6.3.4 Palautteen selailuominaisuus ylläpitäjälle
Web-sovellukseen toteutettu palautteen antamisominaisuus tallentaa kaikki annetut
palautteet web-sovelluksen tietokantaan, joten niiden selailuun tarvitaan myös websovellukseen kuuluva käyttäjärajapinta. Ainoastaan web-sovellukseen sisäänkirjautunut
ylläpitäjä voi selata ja lukea annettuja palautteita. Palautteiden tulee myös muistaa tilansa siitä onko palaute joskus luettu vai ei. Ylläpitäjän käyttäjän tulee pystyä muuttamaan luetun palautteen tila takaisin lukemattomaksi sekä tarpeentullen poistamaan annettuja palautteita tietokannasta. Ylläpitäjällä ei kuitenkaan saa olla toiminnallisuutta
muuttaa annettun palautteen sisältöä.
49
Grails-projekti
Grails-projektissa palautteen selailukäyttöliittymän toteuttaminen on hyvin suoraviivainen ja helppo prosessi, koska käytössä on jo kaikki tarvittavat resurssit vain pientä viilaamista vailla. Palautteen antamisominaisuutta toteutettaessa luotiin jo tarvittava toimintoalueluokka, hallintaluokka ja toimintoalueluokkaa vastaavat GSP-sivut CRUDtoiminnallisuutta varten. Tarve on enää rajoittaa palautteiden selailunäkymä ylläpitäjäkäyttäjälle, muokata generoituja GSP-sivuja ja hallintaluokan sulkeumia vastaamaan
haluttua toiminnallisuutta. Selailunäkymän rajoittaminen pelkästään ylläpitäjälle tapahtuu hyödyntäen käyttöön otettua acegi-laajennusta, johon laaditaan uudet sivunäkyvyysrajoitukset kuvan 16 mukaisesti.
new RequestMap(url:"/feedback/list/**”, configAttribute:"ROLE_ADMIN").save()
new RequestMap(url:"/feedback/show/**", configAttribute:"ROLE_ADMIN").save()
new RequestMap(url:"/feedback/edit/**", configAttribute:"ROLE_ADMIN").save()
Kuva 16. Sivunäkyvyysrajoitukset palautteen selailua varten
Seuraavaksi muokataan yleistä palautteiden listaussivua ja yksittäisten palautteiden
näkymäsivua: list.gsp ja show.gsp. Palautteiden listaussivusta piilotetaan kaikki
tarpeettomat kentät kuten tietokanta id-kenttä ja sarakkeet järjestetään uudelleen vastaamaan paremmin selailumallia. Tämän lisäksi lukemattomat ja luetut palautteet esitetään listassa ikoneina totuusarvojen sijasta. Yksittäisten palautteiden näkymäsivulta on
myös poistettu kaikki turhat kentät ja painikkeet joiden avulla pystyy siirtymään palautteiden muokkaussivulle. Käyttöliittymäpainikkeista näkymäsivulle on jätetty vain palautteen poistopainike, sekä kaksi uutta painiketta: palaa takaisin ja merkitse palaute
lukemattomaksi. Seuraava vaihe GSP-sivujen jälkeen on muokata hallintaluokassa tapahtuvaa ohjausta, jotta palautteiden selailu ja poistaminen ohjaisi ylläpitokäyttäjän
oikeaan paikkaan suorittaessaan kyseisen toiminnon.
Lift-projekti
Lift-projektissa palautteen selailuominaisuuden toteuttaminen vaatii kahden sivupohjan
ja Snippet-komponentin toteuttamisen. Toteutettavat sivupohjat ovat browse.html ja
show.html,
joista ensimmäisen tarkoitus on listata kaikki saadut palautteet listana ja
toisen sivupohjan tarkoitus on hahmottaa yksi valittu palaute kokonaisuudessaan näytölle ja tarjota mahdollisia muokkausominaisuuksia. Koska selailuominaisuus on rajoitettu vain sisäänkirjautuneelle käyttäjälle, on toteutetut sivupohjat määritetty SiteMapkomponenttiin pääsyrajoituskriteerillä kuvan 17 mukaisesti. Liitteessä L14 on esitetty
50
selailuominaisuuden listaus-sivupohja ja liitteessä L15 on esitetty sitä vastaava Snippetkomponentti, joka hahmottaa listan kaikista annetuista palautteista. Kunkin listattu palaute sisältää linkin uuteen sivunäkymään, johon valittu palaute hahmotetaan kokonaisuutena. Tämän sivunäkymän toteutus löytyy liitteestä L16 ja sivua vastaava Snippetkomponentin toteutus löytyy liitteestä L17. Yksittäisellä sivunäkymällä kaikki palautteen oleelliset tiedot esitetään tauluna ja käyttäjälle tarjotaan myös kaksi toiminnallisuutta: palautteen poistaminen ja lukemattomaksi merkkaaminen. Kumpikin toiminnallisuus on toteutettu linkkinä, joka käsittelyvaiheessa suorittaa oman palautetta koskevan
toiminnallisuuden ja uudelleenohjaa käyttäjän palautteiden listausnäkymään.
val loggedIn = If(() => User.loggedIn_?, () =>
RedirectResponse("/user_mgt/login"))
...
Menu(Loc("browseFeedback", List("luma", "feedback", "browse"),
S ? "browse.feedback", loggedIn)) ::
Menu(Loc("showFeedback", List("luma", "feedback", "show"),
S ? "show.feedback", loggedIn)) ::
Kuva 17. Palautteen selailuominaisuuden määritys SiteMap-komponentissa
Vertailu
Näiden kahden eri ohjelmistokehystoteutuksen välillä merkittävä ero on se, että Grailsohjelmistokehyksessä lähes kaikki toiminnallisuus ja näkymät tarjottiin valmiina, jonka
johdosta kehitykseen kuluvan ajan pystyi helposti keskittämään toiminnallisuuden hienosäätämiseen ja ulkoasun koristamiseen. Lift-ohjelmistokehyksessä kaikki tuli laatia
tyhjästä, joka hidasti vaiheen toteutusta merkittävästi, eikä toiminnallisuutta tullut hienosäädettyä Grails-projektin tavoin. Kyseessä on kuitenkin ollut vain hyvin yksinkertainen CRUD-toiminnallisuus palautetta koskien, ja mikäli ominaisuus olisi vaatinut
uniikkia logiikkaa ja käsittelyä eivät Grails-ohjelmistokehyksen tarjoamat valmiit ratkaisut olisi riittäneet niiden toteuttamiseen. Lift-ohjelmistokehyksen lähestymistapa
olisi myös parempi tilanteessa jossa kehittäjien tulisi laatia useampi erilainen näkymä ja
käsittelytoteutus palautetta koskien. Tällöin kehittäjät voisivat helposti uudelleenkäyttää ja muokata toteutettua toiminnallisuutta kuhunkin tilanteeseen sopivaksi. Grailsprojektissa automaattisesti luodut sivut sisältävät hyvin paljon upotettua logiikkaa ja
sidontaa, joka tekee sen generoidun lähdekoodin uudelleenkäyttämisen virhealttiimmaksi.
51
6.4 olmas vaihe
Kolmannessa projektin vaiheessa staattiset web-sovelluksen sivut on tarkoitus muuttaa
dynaamisiksi ja helposti muutettaviksi. Web-sovelluksen loppukäyttäjät eivät ole ITalan ammattilaisia, joten verkkosivujen kirjoittaminen ja muuttaminen tulee olla mahdollisimman vähän puhtaan HTML-kielen kirjoittamista. Oleellisia ominaisuuksia onkin toteuttaa verkkosivujen muokkaaminen WYSIWYG-mallisena ratkaisuna. Ylläpitävien käyttäjien tulee pystyä myös luomaan, muokkaamaan ja poistamaan alisivuja, kullekin pääsivulle. Luodut alisivut esitetään linkkilistoina sivuilla, jonne ne on luotu.
Oleellinen rajoite sivujen muokkaamisessa on se, että vain web-sovellukseen sisäänkirjautunut käyttäjä voi muokata sivuja sekä luoda ja poistaa alisivuja. Dynaamisten sivujen tulee myös luonnollisesti tukea monikielisyysominaisuutta.
6.4.1 Dynaamiset sivut Grails-projektissa
Grails-projektissa dynaamisten sivujen mahdollistaminen tarkoittaa uusien toimintoalueluokkien luomista. Toteutettu malli koostuu viidestä toimintoalueluokasta, jotka
ovat Type, Category, Languages, Page ja Pagecontent. Type-luokka vastaa websovelluksen pääkategoriaa ja Category-luokka vastaa web-sovelluksen pääkategorian
alakategorioita. Page-luokka kuvaa sivupohjaa joka yksilöidään pääkategosen rian ja
alakategorian mukaan. Kun käyttäjä selailee web-sovelluksen sivuja ja valitsee pääkategorian sekä sitä vastaavan alikategorian voidaan sivusto hahmottamista hallitsevassa
hallintaluokassa määrittää yksilöllisen Page-luokka. Kun Page-luokan ilmentymä on
määritetty, voidaan määrittää kyseistä sivua vastaava sisältö Pagecontent-luokan avulla.
Pagecontent-luokka sisältää fyysisen sivuston sisällön HTML-toteutuksena ja kullakin
eri kielitoteutuksella sekä alisivulla on oma Pagecontent-luokan ilmentymä. Käytettävä
Pagecontent-ilmentymä voidaan valita kielivalinnan ja URL-osoitteeseen liitettävän
numeraalisen id-tunnuksen perusteella. Alla on esitetty yksi esimerkki mahdollisesti
URL-osoitteessa, joka määrittää näytettävän Pagecontent-ilmentymän:
http://localhost:8080/luma/young/computerscience/1?lang=fi
Judd, et al. (2008) mukaan Grails määrittää käytettävän hallintaluokan ja toiminnon
URL-osoitteesta. Ylläolevassa esimerkissä käytössä on young-hallintaluokka, joka vastaa web-sovelluksen yhtä pääkategoriaa sekä computerscience-toiminto, joka vastaa
52
pääkategorian yhtä alikategoriaa. Näiden lisäksi osoitteessa on myös id-tunnusta vastaava numeroarvo 1 ja kielivalintaa vastaava parametri arvolla fi. Tämän osoitteen perusteella hahmotuksesta vastaava hallintaluokka voi päätellä minkä sivun käyttäjä haluaa nähdä ja millä kielellä. Kuvassa 18 on esitetty Grails-projektiin toteutettu toimintoaluemalli ja liitteissä G10 – G14 on esitetty toimintoalueluokkien groovy-kieliset toteutukset.
Kuva 18. Dynaamisten sivujen toimintoaluemalli
Koska alisivujen määrä ei ole kiinteä, on helpompaa tehdä sivujen hahmottamisesta
oma dynaaminen kokonaisuus. Tätä varten kaikki aiemmin luodut staattiset pää- ja alikategorioiden hallintaluokkakohtaiset GSP-sivut on poistettu ja tilalle on luotu yksi
yleinen GSP-sivu jolle hahmotetaan kaikkien sivujen sisällöt. Tälle GSP-sivulle toteutetaan tietosisällön ja muokkaamistoiminnallisuuksien esityksen lisäksi myös linkkilista
alisivuista. Linkkilistan avulla käyttäjä voi selata kutakin sivua ilman tarvetta kirjoittaa
sivun fyysistä osoitetta URL-osoitteeseen. Tämän GSP-sivun toteutus on esitetty liitteessä G15. GSP-sivun päivityksen lisäksi myös hallintaluokat on päivitettävä tunnistamaan haluttu sivukokonaisuus, jotta se osaisi ohjata dynaamista hahmotusta. Liitteessä G16 on esitetty päivitetty hallintaluokka ja G17 on esitetty oleellinen apuluokka,
jossa määritetään hahmotettava sivukokonaisuus. Nyt sivujen lisäys, poistaminen, hahmotus ja ohjaus ovat valmiita ja jäljellä on vain sivujen muokkaustoiminnallisuuden
toteuttaminen. Koska kehitettävä muokkaussivu on tarkoitettu Pagecontent-toimintoalueluokkaa varten, voidaan se luoda automaattisesti kutsumalla Grails komentorivirajapinnan komentoa: grails generate-views. Komento luo neljä GSP-sivua, josta
vain edit.gsp sivua tarvitsee muokata. Luotu GSP-sivu sisältää muokkausmahdollisuuden kaikkiin toimintoalueen kenttiin, mutta koska muokkaamista halutaan yksinker53
taistaa, voidaan kaikki ylimääräiset kentät piilottaa kaavarakenteissa. Pagecontenttoimintoalueluokan kentistä jätetään vain kolme oleellisinta näkyviin: title, name ja
content. Title-kenttä kuvaa sivun otsikkotietoa ja name kenttä kuvaa sivun lyhyttä nimeä. Content-kenttä vastaavasti kuvaa fyysistä tietosisältöä, joka voidaan esittää
HTML-rakenteina. Koska ylläpitäjiltä ei haluta vaatia HTML-osaamista sivujen muokkaamiseen, otetaan käyttöön WYSIWYG-mallinen ratkaisu content-kentän muokkaamiseen. Käyttöön otetaan Grails-laajennuksista löytyvät WYSIWYG-editoritoteutus
FCK Editor. Laajennuksen voi asentaa projektiin kutsumalla seuraavaa komentoa:
grails install-plugin fckeditor.
Kun asennus on valmis, voidaan FCK-editori
liittää muokkaussivulla lisäämällä seuraava GSP tag-kenttä haluttuun kohtaan: <fckeditor:editor/>.
Päivitetty muokkaussivu löytyy liitteestä G18. Kolmannen vaiheen
lopussa Grails-projektissa toteutettu web-sovellus on kuvan 19 mukainen.
Kuva 19. Grails-ohjelmistokehyksellä toteutettu web-sovellus
6.4.2 Dynaamiset sivut Lift-projektissa
Lift-projektissa sivujen dynaaminen luonne voidaan toteutta lähes samalla tavalla kuin
Grails-projektissa. Aluksi laaditaan dynaamisen ratkaisun mahdollistava toimintoaluemalli ja toteutetaan sitä vastaavat luokat, jonka jälkeen luodaan dynaamisesti hahmottuva sivupohja tietosisällön esittämiseen. Toimintoaluemallin luokkia varten toteutetaan
myös tetokantahakutoiminnallisuudet. Seuraavaksi toteutetaan sivun muokkaustoiminnallisuus, johon kuuluu muokkaussivupohjan sekä tallennusominaisuuksien toteuttaminen toimintoaluemalli varten. Viimeisenä vaiheena on toteuttaa alisivujen lisäämisen ja
käsittelyn toiminnallisuus.
54
Lift-projektissa käytettävä dynaamisten sivujen toimintoaluemalli on sama kuin Grailsprojektissa, joka on esitetty kuvassa 18 ja toteutut toimintoalumallin luokat löytyvät
liitteistä L18 – L22. Grails-projektiin verrattuna Lift-projektissa toimintoaluemallin
luokkien toteuttaminen on paljon työläämpää, sillä luokille tulee toteuttaa tietokenttien
lisäksi joitain apumetodeja kyselyiden helpottamiseksi ja hakutulosten tarkastuksia varten. Sivutasolla pääkategoriat ja niiden alakategoriat vastaavat toimintoaluemallissa
Type ja Category-luokkia, joiden avulla käytettävä Page-ilmentymä voidaan määrittää.
Kukin Page-luokka sisältää yksi-moneen relaatiolla Pagecontent-ilmentymiä, jotka kuvaavat itse sivun tietosisältöä. Käytettävä Pagecontent-ilmentymä voidaan määrittää
Page-ilmentymän sekä URL-osoitteeseen syötetyn sivutunnuksen ja kielivalinnan avulla. Alla on esitetty esimerkki URL-osoitteesta jonka perusteella näytettävä sivu voidaan
määrittää:
http://localhost:8080/luma/young/computerscience/?id=1&?lang=fi
Tietosisällön määritystapa on hyvin samanlainen Grails-projektin toteutukseen verrattuna ja ainut eroavaisuus on sivukohtaisen tunnuksen esittämisessä URL-osoitteessa.
Sivukohtainen tunnus esitetään URL-parametrina osoitepolun sijasta, koska Liftohjelmistokehyksessä URL-parametrien hyödyntäminen vaatii vähemmän konfiguraatiota. Lähdekooditasolla URL-parametreja on helppo hyödyntää kutsumalla Liftohjelmistokehyksen S.param metodia ja syöttämällä haluttu URL-parametrin avainarvo. Mikäli sivutunnus olisi haluttu poimittavan URL-polusta, olisi sitä varten tullut laatia konfiguraatio SiteMap-komponenttiin sekä toteutta apumetodit sen käsittelyä varten.
Toimintoalueluokkien toteutuksen jälkeen toteutetaan dynaaminen tietosisällön hahmotus sekä sivun muokkaamistoiminnallisuus. Liitteessä L23 on esitetty dynaamisen hahmotuksen mahdollista sivupohja ja liitteessä L24 on esitetty sivun muokkaustoiminnallisuuden mahdollistava sivupohjatoteutus. Lift-projektissa sivupohjat ovat hyvin kevyitä ja jatkossa helposti muokattavia toteutuksia. Sekä hahmotuksen että muokkaamisen
sivupohjia varten laadittiin yksi yhteinen Snippet-komponentti, joka on esitetty liitteessä L25 ja yleinen apuluokka, jonka toteutus on esitetty liitteessä L26. Hahmotusta varten kutsutaan Snippet-komponentiin toteutettua display metodia, joka apuluokan avustamana hakeaa tietokannasta halutun Pagecontent-ilmentymän. Ilmentymän tiedot jäsentään ja sidotaan sivupohjaan haluttuihin kohtiin Lift:in sisäänrakennetun bind metodin avulla. Snippet-komponentissa tarkastellaan myös käyttäjän tilaa ja mikäli käyttäjä
55
on sisäänkirjautnut ylläpitäjä, niin hänelle tarjotaan myös muokkaustoiminnallisuudet
kullakin sivulla. Muokkaustoiminnallisuutta varten Snippet-komponentiin on toteutettu
edit
metodi, joka tarkastaa aluksi URL-osoitteesta mitä sivua halutaan muokata ja si-
too sivun nykyiset Pagecontent-ilmentymän tietosisällöt muokkauskaavarakenteen
kenttiin, jotka luodaan dynaamisesti Lift:in SHtml-komponentin avulla.
Tässä vaiheessa Lift-projektia sivun tietosisällön muokkaamiskenttä tulisi asiakkaan
toiveiden mukaisesti toteuttaa WYSIWYG-mallisena ratkaisuna. Lift-ohjelmistokehys
ei kuitenkaan tarjoa valmista WYSIWYG-editoritoteutusta joten kehittäjien tulee itse
laatia jokin vastaava toteutus tai ottaa käyttöön jokin kolmannen osapuolen toteuttama
ratkaisu ja integroida se toimimaan Lift web-sovelluksen kanssa. Mahdollisuuksia on
moni, mutta integraation toteuttaminen on hyvin työläsvaihe ja täydellistä yhteensopivuutta ja saumattomuutta ei voi taata. Tästä syystä WYSIWYG-mallisen muokkauskenttä ratkaisun toteuttaminen Lift-projektissa on jätetty kokonaan pois tämän tutkielman laajuudesta. Muokkauskenttä on toteutettu tavallisena tekstialueena, johon käyttäjän tulee kirjoittaa puhdasta HTML-kieltä.
Tietojen muokkaamismahdollisuus vaatii myös Lift-projektissa matalantason ratkaisun.
Jotta muokkatut tiedot säilyisivät pyyntö/vastauselinkaaren ajan, tulee sidotusta Pagecontent-ilmentymästä luoda RequestVar-luokan laajentava ainokaisolio. Pagecontentilmentymän ainokaisolio sisältää viittaukset muokattuihin tietoihin ja kun käyttäjä painaa muokkausivun tallenna-painiketta, kaikki ainokaisolion tiedot päivitetään sellaisenaan olemassaolevaan tietokannan Pagecontent-tietojoukkoon.
Kun sivujen hahmotus ja muokkaamisominaisuus on valmiina, voidaan web-sovellukseen laatia toiminnallisuus joka mahdollistaa alisivujen luomisen kullekin pääkategorian alakategoriasivulle. Toiminnallisuus voidaan toteuttaa lisäämällä sisäänkirjautuneen käyttäjän sivupohjaan uusi muokkaustoiminnallisuus ja lisämällä Snippetkomponenttiin uutta logiikkaa. Ratkaisuna URL-osoitteeseen lisätään eri parametri ilmaisemaan sitä, onko käyttäjä halukas muokkaamaan nykyistä sivua vai haluaako hän
lisätä uuden alisivun. Mikäli URL-osoitteessa on edit-parametri, web-sovellus tarjoaa
käyttäjälle pelkän muokkaussivupohjan nykyisestä Pagecontent-ilmentymästä. Mikäli
URL-osoitteessa on add-parametri, luodaan uusi Pagecontent-ilmentymä ja ohjataan
käyttäjä muokkaussivupohjalle, jonne sidotaan uusi luotu Pagecontent-ilmentymä.
Muokkausivupohjaan lisätään myös toiminnallisuus poistaa nykyinen alisivu ja tämä
56
toiminnallisuus näytetään vain, mikäli kyseessä on alisivu. Jotta alisivulle nagivointi
olisi mahdollista, tulee dynaamiseen tietosisällön sivupohjaan lisätä myös linkkilistan
esitys. Liitteen L25 Snippet-komponentissa linkkilistan esitys on toteutettu linklistmetodissa. Metodissa tietokannasta haetaan kaikki saman Page- ja Languagesilmentymiin kuuluvat Pagecontent-ilmentymät ja ne listataan HTML-linkkitoteutuksina.
6.4.3 Vertailu dynaamisten sivujen toteuttamisessa
Kummassakin ohjelmistokehyksessä dynaaminen tietosisällön esitys voitiin toteuttaa
hyvin samanlaisella ratkaisulla ja menetelmillä. Lift-projekti vaati kuitenkin paljon
enemmän lähdekoodin tekemistä Grails-projektiin nähden erityisesti toimintoaluemallin
luokkien suhteen sekä mahdollisen WYSIWYG-mallisen sivunmuokkausratkaisun toteuttamiseen. Grails-ohjelmistokehyksen liitännäisjärjestelmän avulla käyttöön saatu
WYSIWYG-editori voitiin ottaa peruskäyttöön lisäämällä vain muutama lähdekoodirivi. Lift-ohjelmistokehystä käyttäen vastaavanlaisen ratkaisun toteuttaminen olisi vaatinut paljon integraatiota liittääkseen sivulle kolmannenosapuolen tarjoama valmis WYSIWYG-editori. Lift-ohjelmistokehyksessä hyvänä puolena voidaan todeta sen skaalautuvuus toteuttaa uutta toiminnallisuutta inkrementaalisesti. Lift-projektissa tiedonesityksen ja logiikan erottaminen toisistaan Snippet-komponentilla mahdollista se, ettei
tiedonesitys sivulle tarvinnut laatia tynkiä. Sen sijaan toiminnallisuutta pystyi lisäämään skaalautuvasti ja toteuttamalla uusia metodeja Snippet-komponenttiin ja muokkaamalla sitä vastaavia sivupohjatoteutuksia. Grails-projektissa vastaavasti helpointa
oli toteuttaa kaikesta toiminnallisuudesta aluksi tynkämallit, ja vaiheittain muokata nämä virallisiksi toteutuksiksi. Grails-ohjelmistokehys kuitenkin tarjosi automaattisen
lähdekoodi generoinnin lähes kaikelle toiminnallisuudelle jonka johdosta kolmas vaihe
eteni Lift-projektiin nähden paljon nopeammin.
6.5 Toimitusvaihe
Kolmannen vaiheen jälkeen käytössä on lopulliset versiot Luma web-sovelluksesta,
joka tulee asentaa käyttöön viralliseen tuotantoympäristöön. Tuotantoympäristöksi on
valittu Apache Tomcat 6.0 servlet-säiliö, jota varten web-sovelluksesta tuee tuottaa
war-tiedosto.
Tietokannaksi on valittu MySQL versio 5.1.52. Grails-projektissa on ollut
57
käytössä tähän saakka HSQLDB-tietokanta ja jotta MySQL-tietokanta voitaan ottaa
käyttöön, tulee Grails-projektin DataSource.groovy tiedostoa muokata käyttämään
MySQL-tietokannan tietoja. Liitteessä G19 on esitetty muokattu DataSource.groovy
tiedosto, jonka tuotanto-ajoympäristön konfiguraatioon on lisätty MySQL-tietokantamääritys. Grails-projektissa web-sovelluksesta saadaan tuotettua war-tiedosto kutsumalla Grails komentorivi-rajapinnan komento grails war. Tuotettu war-tiedosto voidaan kokoamisen jälkeen asentaa sellaisenaan Apache Tomcat 6.0 ympäristöön. Liftprojektissa toimitusvaihe onnistuu yhtä helposti. Koska Lift-projektissa virallinen
MySQL-tietokanta otettiin käyttöön jo vaiheessa kaksi, ei tietokantaa varten tarvitse
tehdä muutoksia konfiguraation. Lift-projektista saa koottua tarvittavan war-tiedoston
kutsumalla Apache Maven rajapinnan komentoa mvn install. Tuotettu war-tiedosto
voidaan asentaa sellaisenaan Apache Tomcat-ympäristöön Grails-ohjelmistokehyksen
tavoin.
Toimitus- ja julkaisuvaihe on molemmissa ohjelmistokehyksissä hyvin samanlainen,
mutta Grails-projektin osalta virallisen tietokannan käyttöönoton kohdalla projektiin
ilmestyi useita virheitä siitä ettei Grails-projektin toimintoalueluokat olleet yhteensopivia MySQL-tietokannan kanssa, vaikka ne olivat yhteensopivia HSQLDB-tietokannan
kanssa. Virheet johtuivat muutamasta toimintoalueluokan kentän nimestä sekä kenttärajoituksesta. Virheet saatiin kuitenkin korjattua muuttamalla kenttien nimiä ja vaihtamalla kenttärajoituksia. Muutokset kenttien nimissä aiheuttivat kuitenkin uusia virheitä
muissa Grails-projektin osa-alueissa, koska vanhannimisiä kenttiä ei enää ollut olemassa. Mikäli Grails-projektissa oli otettu käyttöön MySQL-tietokanta jo projektin alussa,
olisi pystytty välttymään tarpeettomalta lisätyöltä.. Lift-projektissa ei ollut vastaavia
ongelmia koska virallinen tietokanta jouduttiin ottamaan käyttöön jo projektin alkuvaiheessa. Grails-ohjelmistokehyksen lähestymistapa tarjota valmis tietokanta toteutus
osoittautuikin loppujen lopuksi huonoksi ratkaisuksi.
6.6 Yleinen vertailu
Grails- ja Lift ovat täysin erilaisella lähestymistavalla toteutettuja ohjelmistokehyksiä.
Grails pyrkki tarjoaamaan mahdollisimman paljon valmista kun taas Lift pyrkii tarjoamaan mahdollisimman paljon eri vaihtoehtoja ominaisuuksien toteuttamiseen, mutta
kehittäjät joutuvat itse tuottamaan tarvittavat ominaisuudet. Grails-ohjelmistokehyksen
58
yksi tärkeimpiä ideologioita on sen sisäänrakennettu liitännäisjärjestelmä, jonka avulla
Luma-projektin kaikki vaativimmat toiminnallisuudet saatiin käyttökuntoon valmiina
toteutuksina ilman tarvetta integrointiin. Grails:in liitännäisjärjestelmä kehottaa kehittäjiä hyödyntämään mahdollisimman paljon valmista ja standardisoitua. Liitännäisjärjestelmän avulla kehitysresurssit pienevät merkittävästi, mutta sen tarjoamat toteutukset
eivät välttämättä sovellu kaikkiin web-sovellusprojekteihin. Grails-ohjelmistokehyksen
yksi heikkous on sen sitoutuneisuus sisäänrakennettuun hallintamekanismiin, joka rajoittaa kehittäjien luovuutta toteuttaa erilaisiin ympäristöihin soveltuvia käsittelymekanismeja. Lift-ohjelmistokehys vastaavasti asettaa kehittäjät valinnanvaran ja vapauden
eteen. Lift-kehittäjillä on mahdollisuus vapaasti valita lähestymistapa kuhunkin yksittäiseen ongelmaan. Tämä lähestymistapa motivoi kehittäjiä löytämään ja oppimaan
erilaisia ratkaisumenetelmiä web-sovelluskehityksessä, joka kasvattaa kehittäjän yleistä
web-sovellustuntemusta. Lift-ohjelmistokehys tarjoaa kuitenkin jotain valmiiksi toteutettua, joiden avulla yksinkertaiset web-sovellukset voi toteutta hyvinkin nopeasti.
Valmiiksi toteutettujen Lift-komponenttien idea on niiden käytön tarjota kehittäjillä
joitain valmiita esimerkkejä. Näiden esimerkkien avulla kehittäjät voivat toteuttaa samanoloisen toiminnallisuuden, joka soveltuu paremmin omiin kriteereihin. Liftohjelmistokehyksen lähestymistavan ongelma on kuitenkin se, että tarjolla voi olla jopa
liiankin paljon vaihtoehtoja toteuttaa asioita, joka osoittautuu erityisesti aloittelijoiden
kannalta hankalaksi omaksu. Lift-ohjelmistokehyksen ideologia soveltuu paremmin
kokeneille web-sovelluskehittäjille, jotka haluavat paljon vapautta ja joustavuutta toteuttaa ominaisuudet heidän mieltymysten mukaan. Grails-ohjelmistokehys on vastaavasti helpommin lähestyttävä ratkaisu aloitteleville kehittäjillä.
Imperatiivisen ja funktionaalisen ohjelmointilähestymistavan vaikutus on myös yksi
merkittävä tekijä web-sovelluksissa (Ghosh, et al., 2009). Lift-ohjelmistokehyksen
funktionaalinen ohjelmoinnin lähestymistapa tuottaa web-sovelluksia on tehokkaampi
ja vähemmän virhealtis imperatiiviseen Grails-ohjelmistokehykseen verrattuna. Syy
kahden ohjelmointilähestymistavan eroon on se, että imperatiivinen ohjelmointi pohjautuu tilamuutoksiin. Web-ympäristössä samasta ohjelmasta voi olla ajossa useampi eri
instanssi, jolloin tilamuunnoksien käsittely ja hallinta voi olla hyvin vaikea toteuttaa.
Funktionaalinen ohjelmointilähestymistapa pyrkii taas esittämään kaiken tiedon mahdollisimman muuttumattomana. Erityisesti säieohjelmoinnin ja AJAX sekä Cometominaisuuksien toteutuksessa Lift-ohjelmistokehyksen hyödyntämä toimijamalli palvelee kehittäjää hyvin yksinkertaisemmalla ja vähemmän virhealtiilla luonteella. Impera59
tiivisessa ohjelmoinnissa säietoteutuksilla on aina mahdollisuus joutua kuolonlukkoon
ja muihin vastaaviin virhetilanteisiin. Teoriassa tämä tarkoittaa sitä, että Liftohjelmistokehys soveltuu paremmin projekteihin ja web-sovelluksiin, jotka ovat laajoja
tai joissa on paljon säikeistettyä toiminnallisuutta, eikä virhetilanteita suoda kovinkaan
paljoa.
Tässä luvussa tuotettu Luma-projekti oli henkilökohtaisen mielipiteen mukaan paljon
tehokkaampi ja helpompi toteuttaa Grails-ohjelmistokehyksellä kuin Lift-ohjelmistokehyksellä, mutta tähän on selvät syyt: Luma web-sovellus on hyvin yksinkertainen
eikä se vaatinut liiketoimintalogiikka, joka olisi vaikuttanut moniin eri sivuston komponentteihin. Lift-projektissa tällainen logiikka olisi ollut helpompi toteuttaa, koska
Lift-projekti on ollut alusta lähtien matalamman tason toteutus kuin Grails. Matalamman tason toteutuksen lähestymistapa on hitaampi ja monimutkaisempi, mutta se rohkaisee kehittäjiä suunnittelemaan web-sovelluksen alusta saakka yhtenä kokonaisuutena, mutta joka toteutetaan yksittäisinä yhteenliitettävinä komponentteina. Odersky, et
al. (2008) mukaan Scala-ohjelmointikielen, johon Lift-ohjelmistokehys perustuu, onkin
tarkoitus palvella kehittäjiä monella aseteella. Scala-ohjelmointikeli ja täten myös Liftohjelmistokehys soveltuu skaalautuvasti moneen tarkoitukseen, mutta erityisen vahvoilla se on laajemmissa projekteissa tai projekteissa, joiden on tarkoitus kehittyä aseittain
pienestä kokonaisuudesta hyvinkin laajaksi kokonaisuudeksi. Luma-projektin kohdalla
Grails-ohjelmistokehyksellä toteutetun web-sovelluksen kehittämiseen meni noin puolet Lift-ohjelmistokehyksellä toteutetun web-sovelluksen kehitysajasta. Grails-projektin
lopputulos oli myös hiotumpi johtuen tehokkaammasta työajankäytöstä.
Molemmat ohjelmistokehykset soveltuvat hyvin evolutiiviseen protoiluun, jossa websovellus kehitetään asteittain hyvin yksinkertaisesta kokonaisuudesta monimutkaisemmaksi ja ominaisuusrikkaaksi kokonaisuudeksi. Grails-ohjelmistokehys tarjoaa kuitenkin näkyvää jälkeä Lift-ohjelmistokehystä nopeammin, mutta Lift-ohjelmistokehys
tarjoaa kehittäjillä paremmat työkalut toteutaa mitä monimutkaisimpia käyttäjävaatimuksia ja liiketoimintalogiikoita skaalautuvasti. Grails-ohjelmistokehys voidaan todeta
soveltuvan paremmin pieniin tai keskikokoisiin projekteihin, joissa web-sovelluksen
laajuus on rajattu hyvin tarkoin ja näkvvää jälkeä projektin etenemisestä halutaan saada
mahdollisimman nopeasti. Lift-ohjelmistokehys soveltuu taas paremmin minkä tahansa
kokoisiin skaalautuviin projekteihin joissa web-sovelluksen laajuus ei ole tarkoin määritetty tai joissa liiketoimintalogiikka on jotain hyvin yksilöllistä ja monimutkaista.
60
7 YHTEENVETO
Web-sovellukset ovat yleensä monimutkaisia toteuttaa, koska niiden ajoympäristönä on
kattava internet, jossa sovelluksella voi olla useampia samanaikaisia käyttäjiä. Myös
teknologitasolla web-sovelluksien toteutus vaatii paljon, mikäli sille halutaan staattisten
HTML-sivujen lisäksi jotain toiminnallisuutta. Maailma on täynnä erilaisia standardeja
ja speksejä web-teknologioista, joiden omaksuminen on hyvin työstä ja tämän lisäksi
liiketoiminnan alueella on hyvin usein tarve saada ostetut projektit nopeasti valmiiksi.
Myös muutoksen tarve on yksi yleinen tekijä liiketoiminnan alueella, joka estää tiukkojen vaatimusmäärittelyjen laatimisen projektin alkuvaiheessa. Yhtenä ratkaisuna on
hyödyntää evolutiivista protoilua projektin läpivetämiseen ja valmiiksi toteutettuja
web-sovellusohjelmistokehyksiä web-teknologian hyödyntämiseen.
Evolutiivinen protoilu edustaa nopean ohjelmistokehityksen inkrementaalista kehitysmallia, jonka mukaan kehitettävä tuote rakennetaan ja toimitetaan asteittain. Tällaista
lähestymistapaa voi hyödyntää esimerkiksi vaatimusten tarkentamiseen tai käyttöliittymän hienosäätämiseen. Ideana kuitenkin on, että sovellus toimitetaan asteittain toimivana kokonaisuutena joka kehittyy eteenpäin tai korjautuu seuraavissa toimituksissa.
Erityisesti web-sovellusprojekteissa evolutiivinen protoilu on hyödyllinen lähestymistapa, koska kehitettävän sovelluksen käyttöliittymä on yleensä hyvin kriittinen osa-alue,
jota on vaikea kuvata sellaisenaan vaatimusmäärittelyssä. Evolutiivisen protoilun lähestymistavassa asiakas pääsee kokeilemaan kehitettyä interaktiivista käyttöliittymää kunkin toimituksen jälkeen jolloin siitä saadaan konkreettista palautetta mihin suuntaan sitä
kannattaa kehittää ja mitä toiminnallisuuksia missäkin sivustolla tulisi olla, jotta se vastaisi asiakkaan tarpeita paremmin.
Grails ja Lift ovat web-sovellusohjelmistokehyksiä, jotka tarjoavat valmiit toteutukset
ja apuvälineet tuottaa laadukkaita web-sovelluksia korkeamman tason abstraktiotasolla.
Web-kehityksen tärkeimmät standardit ja teknologiat on upotettu web-sovellusohjelmistokehyksiin ja kehittäjät voivat hyödyntää näitä ilman tarvetta opetella kutakin teknologiaa ja standardia kattavasti. Molemmat ohjelmistokehykset on toteuttu ohjelmointikielillä, jotka ovat yhteensopivia Java-virtuaalikoneen kanssa. Grails-ohjelmistokehys
on toteutettu Groovy-ohjelmointikielellä ja Lift-ohjelmistokehys on toteutettu Scalaohjelmointikielellä. Molempien ohjelmointikielen on tarkoitus tarjota joustava vaihtoehto toteuttaa JVM-sovelluksia Java-ohjelmointikieltä tehokkaammin ja yksinkertai61
semmin. Koska molemmat ohjelmointikielet kääntyvät puhaaksi JVM-tavukoodiksi,
pystyvät ne hyödyntämään kaikkia Java-ohjelmointikielelle toteutettuja luokkia ja sovelluksia. Groovy-ohjelmointikieli on Java-ohjemointikielen tapaan imperatiivinen ja
dynaaminen olio-ohjelmointikieli kun taas Scala-ohjelmointikieli on funktionaalinen
olio-ohjelmointikieli. Grails-ohjelmistokehys tarjoaa kehittäjillä lähes kokonaisen kehitysympäristön toteuttaa web-sovelluksia projektin alkamisesta lopulliseen toimitukseen
saakka. Grails-ohjelmistokehys pyrkii myös tarjoamaan kehittäjillä mahdollisimman
paljon valmista, jotta kehittäjät voivat keskittyä projektissa olennaisiin tekijöihin, kuten
projektikohtaiseen liiketoimintamalliin. Vaikka Grails-ohjelmistokehyksen idea onkin
tarjoa mahdollisimman paljon valmista, ei se sisällytä kuin vain oleellisimmat toteutukset ohjelmistokehykseen itse, mutta se tarjoaa joustavan liitännäisjärjestelmän. Liitännäisjärjestlemän avulla kehittäjät voivat asentaa erilaisia laajennuksia projekteihin, jotka tarjoavat valmista toiminnallisuutta vain vähäisellä konfiguraatiolla. Lift-ohjelmistokehyksen lähestymistapa on vastakkainen: Se tarjoaa mahdollisimman paljon eri mahdollisuuksia toteuttaa erilaiset toiminnallisuudet, mutta vain vähän valmista. Liftohjelmistokehykseen on sisäänrakennettu vain muutama valmiiksi toteutettu toiminnallisuus, ja kehittäjiä rohkaistaan toteuttamaan kuhinkin projketiin paremmin soveltuvat
ratkaisut. Lift-ohjelmistokehyksen vahvuus on erityisesti sen skaalautuvuudessa ja
funktionaalisessa lähestymistavassa.
Kummatkin web-sovellusohjelmistokehykset ovat hyvin kypsiä toteutukseltaan ja niillä
on omat vahvuutensa ja heikkoutensa. Grails-ohjelmistokehys soveltuu erityisen hyvin
pieniin tai keskikokoisiin web-sovellusprojekteihin, joissa kriteereinä on nopea näkyvyys. Grails-ohjelmistokehys on myös hyvä lähtökohta projekteille, joissa kehityshenkilöstöllä ei ole paljoa kokemusta web-sovelluskehityksestä. Haittapuolena Grailsohjelmistokehyksessä on sen sitoutunut lähestymistapa keskitettyyn hallintamekanismiin, joka rajoittaa kehittäjien luovuutta toteuttaa erilaisia käsittelymekanismeja. Liftohjelmistokehys soveltuu vastaavasti kaikenkokoisiin projekteihinsen skaalautuvan
luonteen johdosta, mutta se vaatii kehittäjiltä enemmän kokemusta web-sovelluskehityksestä.
Lift-ohjelmistokehystä
käyttäen
kehittäjillä
on
Grails-
ohjelmistokehykseen verrattuna paljon enemmän mahdollisuuksia vaikuttaa siihen miten toiminnallisuus toteutetaan. Haittapuolena Lift-ohjelmistokehystä käyttäen kehittäjät joutuvat tekemään matalamman tason ratkaisuja jopa web-sovelluksen peruselementeille, joita Grails-ohjelmistokehysprojekteissa ei tarvitse välttämättä miettiä lainkaan.
Lift-ohjelmistokehys soveltuu täten paremmin kokeneille web-sovelluskehittäjille.
62
VIITTEET
Chen-Becker, D., Danciu, M., Weir, T. (2010) Exploring Lift: Documentation for the
Lift Web Framework. WWW-sivusto, http://exploring.liftweb.net/ (1.11.2010)
Chen-Becker, D., Danciu, M., Weir, T. (2009) The Definitive Guide to Lift: A ScalaBased Web Framework. Apress, New York.
Codehaus Foundation. (2008) Groovy: An agile dynamic language for the Java Platform. WWW-sivusto, http://groovy.codehaus.org/ (28.10.2010)
Fayad, M., Schmidt, D. (1997) Object-Oriented Application Frameworks. Communications of the ACM 40(10), 32–38.
Ghosh, D., Vinoski, S. (2009) Scala and Lift Functional Recipes for the Web. Internet
Computing, IEEE, Volume 13, 88-92.
Judd, C., Nusairat, J., Shingler, J. (2008) Beginning Groovy and Grails: From Novice
to Professional. Apress, New York.
Kaisler, S. (2005) Software Paradigms. John Wiley & Son,s, Inc. Hoboken.
Köning, D., Glover, A., King, P., Laforge, G., Skeet, J. (2007) Groovy in Action. Manning Publications Co., New York.
McConnel, S. (2002) Ohjelmistotuotannon hallinta. Edita Prima Oy, Helsinki.
Odersky, M., Spoon, L., Venners, B. (2008) Programming in Scala. Artima Press Inc.,
Mountain View.
Pollak, D. (2009) Beginning Scala. Apress, New York.
Rocher, G., Brown, J. (2009) The Definitive Guide to Grails, Second Edition. Apress,
New York.
Shan, T., Hua, W. (2006) Taxonomy of Java Web Application Frameworks. IEEE International Conference on e-Business Engineering, ICEBE ‘06, 378-385.
Sommerville, I. (2007) Software Engineering, Eighth Edition. Pearson Education Limited, Edinburgh Gate.
63
SpringSource (2009) Grails: The search is over. WWW-sivusto, http://www.grails.org/
(28.10.2010)
Stephens, M., Bates, P. (2002) Controlling prototyping and evolutionary development.
Rapid System Prototyping, 1993. Shortening the Path from Specification to Prototype.
Proceedings., Fourth International Workshop on., 164-185.
WorldWide Conferencing, LLC. (2010) Lift: webframework. WWW-sivusto,
http://liftweb.net/ (28.10.2010)
64
LIITE Y1: Java-toteutus esimerkkitehtävästä
import
import
import
import
java.util.ArrayList;
java.util.HashMap;
java.util.List;
java.util.Map;
public class intersectFrequence {
public static void main(String [] args) {
List<Integer> listA =
List<Integer> listB =
Map<Integer, Integer>
new HashMap<Integer,
new ArrayList<Integer>();
new ArrayList<Integer>();
result =
Integer>();
for(int i = 0; i < 10; i++) {
listA.add((int)(Math.random()*10));
listB.add((int)(Math.random()*10));
}
for(int i : listA) {
if(listB.contains(i)) {
if(result.get(i) == null) {
result.put(i, 0);
for(int k : listB)
if(k == i)
result.put(i, result.get(i) + 1);
}
else {
result.put(i, result.get(i) + 1);
}
}
}
for(int key : result.keySet()) {
System.out.println(key + " --> " +
result.get(key) + " times");
}
}
}
LIITE Y2: Groovy-toteutus esimerkkitehtävästä
class intersectFrequence {
static main(args){
def listA = []
def listB = []
def result = [:]
(0..9).each {
listA.add Math.round(Math.random()*10)
listB.add Math.round(Math.random()*10)
}
listA.each {
if(listB.contains(it)) {
if(!result.get(it)) {
result.put it, 0
listB.each { k ->
if(it == k) result.put it, result.get(it)+1
}
}
else {
result.put it, result.get(it)+1
}
}
}
result.each { key, value ->
println key + " --> " + value }
}
}
66
LIITE Y3: Scala-toteutus esimerkkitehtävästä
import scala.collection.mutable.HashMap
object intersectFrequence {
def main(args: Array[String]) {
val listA = random(10, List[Int]())
var listB = random(10, List[Int]())
val result = new HashMap[Int, Int]
for(i <- listA if listB contains i) {
if(result contains i)
result += i -> ((result get i).get + 1)
else {
result += i -> 1
for(k <- listB if k == i) {
result += i -> ((result get i).get + 1)
}
}
}
for((key, value) <- result) {
println(key + " --> " + value)
}
}
def random(max: Int, list: List[Int]): List[Int] = {
if(list.size < max)
return random(max, (Math.random *
10).asInstanceOf[Int] :: list)
return list
}
}
LIITE G1: Grails web-sovelluksen banneri-komponentin toteutus
<div>
<span id="logo" style="width:60px;">
<a href="http://www.uef.fi/uef">
<img src="${resource(
dir:'images',file:'itasuomen_yo_logo.png')}"
alt="IS-YO" border="0" />
</a>
</span>
<span id="logo" style="width:100px;">
<a href="http://localhost:8080/luma/">
<img src="${resource(
dir:'images',file:'itasuomen_yo_luma_logo.png')}"
alt="IS-Luma" border="0" />
</a>
</span>
</div>
67
LIITE G2: Grails web-sovelluksen pääsivun toteutus
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="enUS">
<head>
<title><g:layoutTitle default="Grails" /></title>
<link rel="stylesheet"
href="${resource(dir:'css',file:'main.css')}" />
<link rel="shortcut icon" href=
"${resource(dir:'images',file:'favicon.ico')}"
type="image/x-icon" />
<g:layoutHead />
<g:javascript library="application" />
<meta http-equiv="content-type" content="text/html;
charset=UTF-8"/>
<nav:resources override="true"/>
</head>
<body>
<div id="wrapper">
<div id="spinner" class="spinner" style="display:none;">
<img src="${resource(dir:'images',file:'spinner.gif')}"
alt="
${message(code:'spinner.alt',default:'Loading...')}" />
</div>
<div id="topbar">
<g:render template="/common/topbar"/>
</div>
<div id="banner">
<g:render template="/common/banner"/>
</div>
<div id="mainmenu">
<span id="left">
<nav:render group="lefttabs"/>
</span>
<span id="right">
<nav:render group="righttabs"/>
</span>
</div>
<div id="page">
<div id="sidebar">
<nav:renderSubItems group="lefttabs"/>
</div>
<div id="content">
<g:layoutBody />
</div>
</div>
<div id="footer">
<g:render template="/common/footer"/>
</div>
</div>
</body>
</html>
68
LIITE G3: Grails web-sovelluksen etusivukomponentin toteutus
<html>
<head>
<title><g:message code="eastern.finland.luma"/></title>
<meta name="layout" content="main"/>
</head>
<body>
<div>
<div id="content">
<g:render template="/common/content/welcome"/>
</div>
</div>
</body>
</html>
LIITE G4: Yhden hallintaluokan toteutus
Hallintaluokasta nähdään kuinka kahden linkityksen fronpage ja news sulkeumat on
muodostettu ja kuinka navigaatio-laajennus on konfiguroitu tälle hallintaluokalle.
package luma
class MainController {
def scaffold = News
static navigation = [
group:'lefttabs',
order:1,
title:'frontpage',
action:'index',
subItems: [
[group:'lefttabs',action:'news', order:1, title:'news']
]
]
def index = {
redirect(action: "frontpage")
}
def frontpage = {
}
def news = {
}
}
69
LIITE G5: Palautteen antamistoimintoa vastaava GSP-sivu
GSP-sivu esittää palautteenantamiskaavarakennetta, jossa on syöttökentät seuraavia
tietoja varten: nimi, sähköpostiosoite ja palaute. GSP-sivulla on myös lukuisia esimerkkejä miten <g:message/> tag-kenttää voidaan hyödyntää monikielisyystuen toteuttamiseen.
<html>
<head>
<title><g:message code="give.feedback"/></title>
<meta name="layout" content="main"/>
</head>
<body>
<div class="body">
<g:if test="${flash.message}">
<div class="message">
${flash.message}
</div>
</g:if>
<p>
<h2 style="padding-bottom:5px;"><g:message
code="feedback.about.website"/></h2>
</p>
<g:form action="proceedFeedback" method="post" >
<div>
<span>
<label for="name">
<g:message code="name"/>:
</label>
</span>
<br/>
<g:textField name="name" value="${params.name}"/>
<span id="notes">
(<g:message code="optional"/>)
</span>
</div>
<div>
<span>
<label for="email">
<g:message code="email-address"/>:
</label>
</span>
<br/>
<g:textField name="email" value="${params.email}"/>
<span id="notes">
(<g:message code="optional"/>)
</span>
</div>
<div>
<span>
<label for="content">
<g:message code="feedback"/>:
</label>
</span>
<br/>
<g:textArea name="content" rows="7" cols="30"
value="${params.content}"/>
</div>
<div class="buttons" style="width:75px;">
<span class="button" style="width:inherit;">
<g:actionSubmit value="${message(code:'send')}"
70
action="proceedFeedback" />
</span>
</div>
</g:form>
</div>
</body>
</html>
LIITE G6: ylävalikon GSP-sivutoteutus
GSP-sivu esittää web-sovelluksen ylävalikon toteutusta. Ylävalikon vasemmassa reunassa on kaksi linkkiä kielivalintaa varten ja oikeassa reunassa on sisäänkirjautumistoiminnallisuus.
<div id="labels">
<span id="label">
<g:link controller="language"
action="inFinnish">
<g:message code="in.finnish"/>
</g:link>
</span>
<span>
&nbsp;|&nbsp;
</span>
<span id="label">
<g:link controller="language"
action="inEnglish">
<g:message code="in.english"/>
</g:link>
</span>
<span id="label-right">
<nobr>
<g:isLoggedIn>
<b><g:loggedInUserInfo field="userRealName"/></b> |
<g:link controller="logout">
<g:message code="logout" />
</g:link>
</g:isLoggedIn>
<g:isNotLoggedIn>
<g:link controller="login">
<g:message code="login" />
</g:link>
</g:isNotLoggedIn>
</nobr>
</span>
</div>
71
LIITE G7: Kielivalikko hallintaluokan toteutus
Kielivalinnan hallintaluokka toteutuksessa on kaksi sulkeumaa vastaamaan linkkejä ja
kielien toimintoalueluokan dynaaminen rakennustelineistuminen. Sulkeumat ottavat
vastaan toiminta pyynnön, lisäävät URL-osoitteeseen lang-parametrin kielivalinnalla ja
uudelleenohjaavat käyttäjän web-sovelluksen etusivulle.
package luma
class LanguageController {
def scaffold = Languages
def inEnglish = {
redirect(controller:'main', action:'frontpage',
params:["lang":"en"])
}
def inFinnish = {
redirect(controller:'main', action:'frontpage',
params:["lang":"fi"])
}
}
72
LIITE G8: Palautteen toimintoalueluokka
Toimintoalueluokka sisältää kaikki palautteelle olennaiset tiedot, tarkastukset ja yleishyödylliset metodit.
package luma
import java.text.ParseException
import java.text.SimpleDateFormat
class Feedback {
String name
String email
String content
Date createdAt
Boolean checked
static constraints = {
name(blank:true)
email(blank:true)
content(maxSize:5000, blank:false, nullable:false)
createdAt(nullable:false)
checked(nullable:false)
}
String toString() {
SimpleDateFormat format =
new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
try {
value = format.format(createdAt) + "-" + name ?: "Anonymous"
log.debug(value)
return value
}
catch(pa) {
throw new ParseException("Cannot parse '" + dateString + "'")
}
}
}
73
LIITE G9: Palautteen hallintaluokka
Palautteen hallintaluokka sisältää lukuisia sulkeumia, mutta vain giveFeedback,
feedbackAccepted ja proceedFeedback sulkeumat ovat itse toteutettuja. Loput sulkeumista ovat Grails:in automaattisesti toteuttamia.
import luma.Feedback;
class FeedbackController {
def giveFeedback = {
}
def feedbackAccepted = {
}
def proceedFeedback = {
def feedbackInstance = new Feedback(name:params.name,
email:params.email,
content:params.content,
createdAt:new Date(),
read:false)
if (feedbackInstance.save(flush: true)) {
flash.message =
"${message(code: 'thank.you.for.the.feedback')}"
redirect(action: "feedbackAccepted")
}
else {
flash.message =
"${message(code: 'can.not.send.empty.feedback')}"
render(view: "giveFeedback")
}
}
static allowedMethods = [save: "POST", update: "POST",
delete: "POST"]
def index = {
redirect(action: "list", params: params)
}
def list = {
params.max = Math.min(params.max ? params.int('max'): 10, 100)
[feedbackInstanceList: Feedback.list(params),
feedbackInstanceTotal: Feedback.count()]
}
def create = {
def feedbackInstance = new Feedback()
feedbackInstance.properties = params
return [feedbackInstance: feedbackInstance]
}
def save = {
def feedbackInstance = new Feedback(params)
if (feedbackInstance.save(flush: true)) {
flash.message =
"${message(code: 'default.created.message',
74
args: [message(code: 'feedback.label',
default: 'Feedback'), feedbackInstance.id])}"
redirect(action: "show", id: feedbackInstance.id)
}
else {
render(view: "create",
model: [feedbackInstance: feedbackInstance])
}
}
def show = {
def feedbackInstance = Feedback.get(params.id)
if (!feedbackInstance) {
flash.message =
"${message(code: 'default.not.found.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "list")
}
else {
[feedbackInstance: feedbackInstance]
}
}
def edit = {
def feedbackInstance = Feedback.get(params.id)
if (!feedbackInstance) {
flash.message =
"${message(code: 'default.not.found.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "list")
}
else {
return [feedbackInstance: feedbackInstance]
}
}
def update = {
def feedbackInstance = Feedback.get(params.id)
if (feedbackInstance) {
if (params.version) {
def version = params.version.toLong()
if (feedbackInstance.version > version) {
feedbackInstance.errors.rejectValue("version",
"default.optimistic.locking.failure",
[message(code: 'feedback.label',
default: 'Feedback')] as Object[],
"Another user has updated this Feedback "+
"while you were editing")
render(view: "edit",
model: [feedbackInstance: feedbackInstance])
return
}
}
feedbackInstance.properties = params
if (!feedbackInstance.hasErrors() &&
feedbackInstance.save(flush: true)) {
flash.message =
"${message(code: 'default.updated.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), feedbackInstance.id])}"
redirect(action: "show", id: feedbackInstance.id)
75
}
else {
render(view: "edit",
model:[feedbackInstance:feedbackInstance])
}
}
else {
flash.message =
"${message(code: 'default.not.found.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "list")
}
}
def delete = {
def feedbackInstance = Feedback.get(params.id)
if (feedbackInstance) {
try {
feedbackInstance.delete(flush: true)
flash.message =
"${message(code: 'default.deleted.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "list")
}
catch (org.springframework.dao
.DataIntegrityViolationException e) {
flash.message =
"${message(code: 'default.not.deleted.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "show", id: params.id)
}
}
else {
flash.message =
"${message(code: 'default.not.found.message',
args: [message(code: 'feedback.label',
default: 'Feedback'), params.id])}"
redirect(action: "list")
}
}
}
76
LIITE G10: Type-toimintoalueluokan toteutus
package luma
import java.util.Date;
class Type {
String name
Date createdAt
static constraints = {
name(unique:true, blank:false, maxSize:100)
createdAt(nullable:false)
}
String toString() {
name
}
}
LIITE G11: Category-toimintoalueluokan toteutus
package luma
class Category {
String name
Date createdAt
static constraints = {
name(unique:true, blank:false, maxSize:100)
createdAt(nullable:false)
}
String toString() {
name
}
}
77
LIITE G12: Languages-toimintoalueluokan toteutus
package luma
import java.util.Date;
import java.util.Locale;
class Languages {
String title
Locale locale
Date createdAt
static constraints = {
locale(unique: true, nullable:false)
title(blank:false, maxSize:120)
createdAt(nullable:false)
}
String toString() {
title + " (" + locale.toString() + ")"
}
}
LIITE G13: Page-toimintoalueluokan toteutus
package luma
import java.util.Date;
import luma.Category
class Page {
String title
Type type
Category category
Date createdAt
Date modifiedAt
static belongsTo = [Type, Category]
static hasMany = [pagecontents: Pagecontent]
static constraints = {
title(blank:false, maxSize:120)
type(nullable:false)
createdAt(nullable:false)
modifiedAt(nullable:false)
}
String toString() {
title
}
}
78
LIITE G14: Pagecontent-toimintoalueluokan toteutus
package luma
import java.util.Date;
class Pagecontent {
Page page
Languages language
String title
String name
String content
Date createdAt
Date modifiedAt
Integer nodeId
static belongsTo = [Page, Languages]
static mapping = {
content(type: 'text')
}
static constraints = {
page(nullable:false)
language(nullable:false)
title(blank:false, maxSize:80)
name(blank:false, maxSize:30)
createdAt(nullable:false)
modifiedAt(nullable:false)
nodeId(min:0)
}
String toString() {
title
}
}
79
LIITE G15: Dynaamisen luonteen mahdollistava yleinen GSP-sivu
Tälle sivulle hahmotetaan kaikki Grails-projektin dynaamisten sivujen sisältö: Muokkaus- ja alisivujen lisäyspainikkeet sekä linkkilista sivun alisivuista, jotka generoidaan
ajonaikana. fyysinen HTML-tietosisältö esitetään ${content} kohdassa.
<html>
<head>
<title><g:message code="${title}"/></title>
<meta name="layout" content="main"/>
</head>
<body>
<g:if test="${fullpage}">
<div id="fullpage">
</g:if>
<g:else>
<div>
</g:else>
<g:isLoggedIn>
<div id="editcontent">
<g:link controller="pagecontent" action="editCurrent"
params="${control}">
<img width="12px" height="12px"
src="${resource(dir:'images/skin',
file:'database_edit.png')}"
alt="edit" border="0" />
</g:link>
<g:link action="addSubpage" params="${control}">
<img width="12px" height="12px"
src="${resource(dir:'images/skin',
file:'database_add.png')}"
alt="add" border="0" />
</g:link>
</div>
</g:isLoggedIn>
<div>
<g:if test="${subpages != '' }">
<div id="subpagelinks">
<ul>
<g:each in="${subpages}" var="subpage">
<li>
<g:if test="${subpage.thispage}">
<u>${subpage.name}</u>
</g:if>
<g:else>
<g:link action="${actionName}"
params="${control}"
id="${subpage.nodeId}">
${subpage.name}
</g:link>
</g:else>
</li>
</g:each>
</ul>
</div>
</g:if>
<div>
${content}
</div>
</div>
</div>
</body>
</html>
80
LIITE G16: Dynaamisen hahmottamisen mahdollistava hallintaluokka.
Tämä hallintaluokka on liitteen G4 päivitetty versio. Luokka hyödyntää uutta apuluokkaa PageContentHelper joka löytyy liitteestä G17.
package luma
import luma.PageContentHelper as PCH
class MainController {
def scaffold = News
static navigation = [
group:'lefttabs',
order:1,
title:'frontpage',
action:'index',
subItems: [
[group:'lefttabs',action:'news', order:1, title:'news']
]
]
def index = {
redirect(action: "frontpage")
}
def frontpage = {
render(view:'../common/_content',
model:PCH.getPageContent(params))
}
def news = {
render(view:'../common/_content',
model:PCH.getPageContent(params))
}
def subpage = {
render(view:'../common/_content',
model:PCH.getPageContent(params))
}
def addSubpage = {
redirect(controller:'pagecontent', action:'editCurrent',
params:PCH.addSubPageContent(params))
}
}
81
LIITE G17: Apuluokka dynaamisen hahmotuksen toteuttamiseen
package luma
import
import
import
import
java.util.Date;
java.util.HashMap;
org.springframework.context.i18n.LocaleContextHolder as LCH
luma.ContentUtility as CU
class PageContentHelper {
static HashMap getPageContent(params) {
// find unique entities
Type type = Type.findByName(params.controller)
Category category = Category.findByName(params.action)
if(!params.id) {
params.id = 0
}
else {
params.id = Integer.valueOf(params.id)
}
if(!type) {
println("Creating new type: " + params.controller);
type = new Type(name: params.controller,
createdAt: new Date()).save()
}
if(!category) {
println("Creating new category: " + params.action);
category = new Category(name: params.action,
createdAt: new Date()).save()
}
// find unique page
Page page = Page.findByTypeAndCategory(type, category)
// if page doens't exist, create it
if(!page) {
Date now = new Date()
String name = CU.createName(type.name, category.name)
println("Creating new page: $name");
page = new Page(title: name,
type: type,
category: category,
createdAt: now,
modifiedAt: now
).save()
}
// find unique language
Languages lang = Languages.findByLocale(LCH.getLocale())
if(!lang) {
return [content:"Bad Locale: " + LCH.getLocale().toString()]
}
// find unique page content
Pagecontent pageContent = Pagecontent.findWhere(["page": page,
"nodeId": params.id, "language": lang])
// if PageContent doesn't exist, create it
82
if(!pageContent && params.id == 0) {
println("Creating new page content: " + page.toString() +
" " + lang.toString())
Date now = new Date()
pageContent = new Pagecontent(
title: CU.createEmptyTitle(type.name, category.name,
lang.locale),
name: category.name,
content: CU.createEmptyPageContent(type.name,
category.name, lang.locale),
language: lang,
page: page,
createdAt: now,
modifiedAt: now,
nodeId:0
).save()
}
// collect all sub-page contents
def subpages = []
Pagecontent.findAllByPageAndLanguage(page, lang).each {
subpages.add([
name:it.name,
nodeId:it.nodeId,
thispage:(it.nodeId == params.id)
])
}
if(subpages.size() < 2)
subpages = ""
return [
title:pageContent.title,
content:pageContent.content,
control:[type:params.controller, category:params.action,
nodeId:params.id],
subpages:subpages,
nodeId:params.id
]
}
static HashMap addSubPageContent(params) {
// find unique entities
Type type = Type.findByName(params.type)
Category category = Category.findByName(params.category)
// find unique page
Page page = Page.findByTypeAndCategory(type, category)
// find unique language
Languages lang = Languages.findByLocale(LCH.getLocale())
if(!lang) {
return [content:"Bad Locale: " + LCH.getLocale().toString()]
}
Pagecontent newCreated = null
def pagecontents = Pagecontent.findAllByPageAndLanguage(page,
lang)
int pageNodeId = pagecontents.size
Date now = new Date()
println("Creating new sub-page(s) content: " + page.toString())
Languages.list().each {
Pagecontent newPageContent = new Pagecontent(
83
title: CU.createEmptySubTitle(type.name, category.name,
it.locale),
name:category.name,
content: CU.createEmptyPageContent(type.name, category.name,
it.locale),
language: it,
page: page,
createdAt: now,
modifiedAt: now,
nodeId:pageNodeId
).save()
if(lang.equals(it)) {
newCreated = newPageContent
}
}
return [createdInstanceId:newCreated.id,
type:params.type,
category:params.category,
nodeId:pageNodeId
]
}
}
84
LIITE G18: Pagecontent-instanssin muokkaussivu
Sivu on generoitu Grails komentorivi-rajapinnan kautta ja sen sisältö on muutettu hiukan piilottamalla tarpeettomien kenttien muokkausominaisuudet ja lisäämällä FCKeditori content-kentän muokkausalueeksi.
<%@ page import="luma.Pagecontent" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=UTF-8" />
<meta name="layout" content="main" />
<g:set var="entityName" value=
"${message(code: 'pagecontent.label',
default: 'Pagecontent')}" />
<title><g:message code="default.edit.label"
args="[entityName]" /></title>
</head>
<body>
<div class="hidden">
<span class="menuButton"><a class="home"
href="${createLink(uri: '/')}"><g:message
code="default.home.label"/></a></span>
<span class="menuButton"><g:link class="list"
action="list"><g:message code=
"default.list.label"
args="[entityName]" /></g:link></span>
<span class="menuButton"><g:link class="create"
action="create"><g:message code="default.new.label"
args="[entityName]" /></g:link></span>
</div>
<div class="body" style="width:100% !important;">
<h2>
<g:if test="${pagecontentInstance.nodeId != 0 }">
<g:message code="edit.subpagecontent" />
</g:if>
<g:else>
<g:message code="edit.pagecontent" />
</g:else>
</h2>
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
<g:hasErrors bean="${pagecontentInstance}">
<div class="errors">
<g:renderErrors bean="${pagecontentInstance}"
as="list" />
</div>
</g:hasErrors>
<g:form method="post" >
<g:hiddenField name="id"
value="${pagecontentInstance?.id}" />
<g:hiddenField name="version"
value="${pagecontentInstance?.version}" />
<div class="dialog">
<table>
<tbody>
<tr class="hidden">
<td valign="top" class="name">
<label for="page"><g:message
code="pagecontent.page.label"
85
default="Page" />
</label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'page', 'errors')}">
<g:select name="page.id"
from="${luma.Page.list()}"
optionKey="id"
value=
"${pagecontentInstance?.page?.id
}"/>
</td>
</tr>
<tr class="hidden">
<td valign="top" class="name">
<label for="language"><g:message
code="pagecontent.language.label"
default="Language" /></label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'language', 'errors')}">
<g:select name="language.id"
from="${luma.Languages.list()}"
optionKey="id"
value=
"${pagecontentIntance?.language?
.id}"/>
</td>
</tr>
<tr class="prop">
<td valign="top" class="name">
<label for="title"
style="line-height:22px;
"><g:message
code="pagecontent.title.label"
default="Title" /></label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'title', 'errors')}">
<g:textField style="width:400px;"
name="title" maxlength="80"
value=
"${pagecontentInstance?.title}" />
</td>
</tr>
<tr class="prop">
<td valign="top" class="name">
<label for="name"
style="line-height:22px;">
<g:message
code="pagecontent.name.label"
default="Name" /></label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'name', 'errors')}">
86
<g:textField style="width:200px;"
name="name" maxlength="30"
value=
"${pagecontentInstance?.name}"
/>
</td>
</tr>
<tr class="hidden">
<td valign="top" class="name">
<label for="createdAt"><g:message
code="pagecontent.createdAt.label"
default="Created At" /></label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'createdAt', 'errors')}">
<g:datePicker name=
"createdAt" precision="day"
value=
"${pagecontentInstance?
.createdAt}" />
</td>
</tr>
<tr class="hidden">
<td valign="top" class="name">
<label for="modifiedAt">
<g:message code=
"pagecontent.modifiedAt.label"
default="Modified At" />
</label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'modifiedAt', 'errors')}">
<g:datePicker name=
"modifiedAt" precision="day"
value=
"${pagecontentInstance?
.modifiedAt}" />
</td>
</tr>
<tr class="hidden">
<td valign="top" class="name">
<label for="nodeId">
<g:message code=
"pagecontent.nodeId.label"
default="Node Id" /></label>
</td>
<td valign="top" class=
"value ${hasErrors(
bean: pagecontentInstance,
field: 'nodeId', 'errors')}">
<g:textField name="nodeId"
value="${fieldValue(
bean: pagecontentInstance,
field: 'nodeId')}" />
</td>
</tr>
<tr class="prop">
87
<td colspan="2" valign="top"
class="value ${hasErrors(
bean: pagecontentInstance,
field: 'content', 'errors')}">
<fckeditor:editor
name="content"
width="670px"
height="400"
toolbar="Default"
fileBrowser="default">
${pagecontentInstance?.content}
</fckeditor:editor>
</td>
</tr>
</tbody>
</table>
</div>
<div class="buttons" style="width:686px;">
<span class="button"><g:actionSubmit class="save"
action="update" value=
"${message(code: 'default.button.save.label',
default: 'Update')}" /></span>
<span class="button"><g:actionSubmit
class="cancel" action="cancel"
value="${message(code:
'default.button.cancel.label',
default: 'Cancel')}" /></span>
<g:if test="${pagecontentInstance.nodeId != 0 }">
<span class="button" style="float:right;">
<g:actionSubmit
class="delete" action="delete"
value="${message(code:
'default.button.remove.label',
default:'Delete')}"
onclick="return confirm('${message(code:
'pagecontent.button.delete.confirm.message',
default: 'Are you sure?')}');" />
</span>
</g:if>
</div>
</g:form>
</div>
</body>
</html>
88
LIITE G19: Tietokantamääritys DataSource.groovy tiedostossa
dataSource {
pooled = true
driverClassName = "org.hsqldb.jdbcDriver"
username = "sa"
password = ""
}
hibernate {
cache.use_second_level_cache = true
cache.use_query_cache = true
cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
}
// environment specific settings
environments {
development {
dataSource {
dbCreate = "create-drop"
url = "jdbc:hsqldb:file:devDB;shutdown=true"
}
}
test {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:file:devDB;shutdown=true"
}
}
production {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:file:prodDb;shutdown=true"
pooled = true
driverClassName = "com.mysql.jdbc.Driver"
username = "root"
password = "admin"
dbCreate = "update"
url = "jdbc:mysql://localhost:3306/luma"
}
}
}
89
LIITE L1: Lift web-sovelluksen staattisen banneri-sivupohjan toteutus
<div>
<span id="logo" style="width:60px;">
<a href="http://www.uef.fi/uef">
<img src="/images/itasuomen_yo_logo.png"
alt="IS-YO"
border="0" />
</a>
</span>
<span id="logo" style="width:100px;">
<a href="http://localhost:8080/luma/main/frontpage">
<img src="/images/itasuomen_yo_logo.png"
alt="IS-Luma"
border="0" />
</a>
</span>
</div>
LIITE L2: Lift web-sovelluksen frontpage-sivupohjan toteutus
<lift:surround with="default" at="content">
<lift:embed what="luma/main/sidebar" />
<div id="content">
<h1>Frontpage></h1>
...
</div>
</lift:surround>
90
LIITE L3: Lift web-sovelluksen pääsivun toteutus
Pääsivu koostuu kolmesta staattisesti verkkosivu-komponentista ja dynaamisesta navigaatiovalikosta ja tietosisällöstä.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:lift="http://liftweb.net/">
<head>
<meta http-equiv="content-type"
content="text/html; charset=UTF-8" />
<link href="/css/main.css" type="text/css" rel="stylesheet"
media="screen, projection" />
<title>Itä-Suomen Luma</title>
</head>
<body>
<div id="wrapper">
<div id="topbar">
<lift:embed what="luma/common/topbar" />
</div>
<div id="banner">
<lift:embed what="luma/common/banner" />
</div>
<div id="mainmenu">
<span id="left">
<lift:Menu.group group="mainmenu-left"/>
</span>
<span id="right">
<lift:Menu.group group="mainmenu-right"/>
</span>
</div>
<div id="page">
<lift:bind name="content" />
</div>
<div id="footer">
<lift:embed what="luma/common/footer" />
</div>
</div>
</body>
</html>
LIITE L4: Main pääkategoriaan sisältyvien alakategorialinkkien hahmottaminen
<div id="sidebar">
<lift:Menu.group group="main"/>
</div>
91
LIITE L5: SiteMap-komponentin toteutus ensimmäisessä vaiheessa.
SiteMap-komponentti sisältää listan kaikista web-sovelluksen piiriin kuuluvista sivuista
ja määrittää niiden kutsumiseen tarvittavat uniikit tunnukset ja nimikkeet.
val loggedIn = If(() => User.loggedIn_?, () =>
RedirectResponse("/login"))
val entries = Menu(Loc("index",List("index"), S ? "index", Hidden)) ::
Menu(Loc("main", List("luma", "main", "frontpage"),
S ? "frontpage", LocGroup("mainmenu-left"))) ::
Menu(Loc("children", List("luma", "children", "general"),
S ? "children", LocGroup("mainmenu-left"))) ::
Menu(Loc("young", List("luma", "young", "general"),
S ? "young", LocGroup("mainmenu-left"))) ::
Menu(Loc("teachers", List("luma", "teachers", "general"),
S ? "teachers", LocGroup("mainmenu-left"))) ::
Menu(Loc("scifest", List("luma", "scifest", "general"),
S ? "scifest", LocGroup("mainmenu-right"))) ::
Menu(Loc("links", List("luma", "links", "general"),
S ? "links", LocGroup("mainmenu-right"))) ::
Menu(Loc("mainnews", List("luma", "main", "news"),
S ? "news", LocGroup("main"))) ::
Menu(Loc("childrenbiology", List("luma", "children", "biology"),
S ? "biology", LocGroup("children"))) ::
Menu(Loc("childrencomputerscience",
List("luma", "children", "computer-science"),
S ? "computerscience", LocGroup("children"))) ::
Menu(Loc("youngbiology", List("luma", "young", "biology"),
S ? "biology", LocGroup("young"))) ::
Menu(Loc("youngchemistry", List("luma", "young", "chemistry"),
S ? "chemistry", LocGroup("young"))) ::
Menu(Loc("youngmathematics", List("luma", "young", "mathematics"),
S ? "mathematics", LocGroup("young"))) ::
Menu(Loc("youngphysics", List("luma", "young", "physics"),
S ? "physics", LocGroup("young"))) ::
Menu(Loc("youngcomputerscience",
List("luma", "young", "computerscience"),
S ? "computer-science", LocGroup("young"))) ::
Menu(Loc("teachersbiology", List("luma", "teachers", "biology"),
S ? "biology", LocGroup("teachers"))) ::
Menu(Loc("teacherschemistry", List("luma", "teachers", "chemistry"),
S ? "chemistry", LocGroup("teachers"))) ::
Menu(Loc("teachersmathematics",
List("luma", "teachers", "mathematics"),
S ? "mathematics", LocGroup("teachers"))) ::
Menu(Loc("teachersphysics", List("luma", "teachers", "physics"),
S ? "physics", LocGroup("teachers"))) ::
Menu(Loc("teacherscomputerscience",
List("luma", "teachers", "computerscience"),
S ? "computer-science", LocGroup("teachers"))) ::
Menu(Loc("inFinnish", List("luma", "main", "frontpage?lang=fi"),
S ? "in.finnish", Hidden)) ::
Menu(Loc("inEnglish", List("luma", "main", "frontpage?lang=en"),
S ? "in.english", Hidden)) ::
Menu(Loc("giveFeedback", List("luma", "feedback", "feedback"),
S ? "feedback", Hidden)) ::
Menu(Loc("browseFeedback", List("luma", "feedback", "browse"),
S ? "browse.feedback", loggedIn)) ::
Menu(Loc("acceptFeedback", List("luma", "feedback", "accepted"),
S ? "thank.you", Hidden)) :: User.sitemap
LiftRules.setSiteMap(SiteMap(entries:_*))
92
LIITE L6: Lokalisoinnin määritysfunktio
Lokalisoinnin määritysfunktiossa tarkastetaan aluksi onko annetussa URL-osoitteessa
lang-nimistä parametriä
ja mikäli on, niin lokaali vaihdetaan sen määrittämään arvoon.
Jos kyseistä parametriä ei ole löydy URL-osoitteesta, funktio tarkastaa onko sellainen
tallennettu jo selaimen evästeisiin ja käyttää sen arvoa.
package bootstrap.liftweb
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
import _root_.net.liftweb.http._
import _root_.net.liftweb.http.provider._
import _root_.net.liftweb.sitemap._
import _root_.net.liftweb.sitemap.Loc._
import Helpers._
import _root_.net.liftweb.mapper.{DB, ConnectionManager, Schemifier,
DefaultConnectionIdentifier, StandardDBVendor}
import _root_.java.sql.{Connection, DriverManager}
import _root_.luma.liftluma.model._
import _root_.java.util.Locale;
import _root_.javax.servlet.http.HttpServletRequest;
object LocaleCalculator {
def localeCalculator(request : Box[HTTPRequest]): Locale =
request.flatMap(r => {
def localeCookie(in: String): HTTPCookie =
HTTPCookie("lang",Full(in),
Empty,Full("/"),Full(-1),Empty,Empty)
def localeFromString(in: String): Locale = {
val x = in.split("_").toList; new Locale(x.head,x.last)
}
def calcLocale: Box[Locale] =
S.findCookie("lang").map(
_.value.map(localeFromString)
).openOr(Full(LiftRules.defaultLocaleCalculator(request)))
S.param("lang") match {
case Full(selectedLocale) =>
S.addCookie(localeCookie(selectedLocale))
tryo(localeFromString(selectedLocale))
case _ => calcLocale
}
}).openOr(Locale.getDefault())
}
93
LIITE L7: Käytettävän lokaalin valintatoiminnallisuus
Kun käyttäjä klikkaa jompaakumpaa kielivalinnan linkkiä, niin hänet ohjataan etusivulle ja kielivalinta muutetaan halutuksi. Liitteessä L5 on esitetty käytetyt Menuilmentymät inFinnish ja inEnglish.
<div id="labels">
<span id="label">
<lift:Menu.item name="inFinnish"/>
</span>
<span>&nbsp;|&nbsp;</span>
<span id="label">
<lift:Menu.item name="inEnglish"/>
</span>
<span id="label-right">
<nobr>
lift:UserPanel.loginPanel/>
</nobr>
</span>
</div>
LIITE L8: Palautetta vastaava toimintoaluemalli
Tämä luokka kuvaa yhtä tietokantaan luotavaa taulua ja luokan tietokentät vastaavat
tietokantataulun sarakkeita.
package luma.liftluma.model
import _root_.net.liftweb.mapper._
import _root_.java.math.MathContext
class Feedback extends LongKeyedMapper[Feedback] with IdPK {
def getSingleton = Feedback
object createdAt extends MappedDateTime(this)
object name extends MappedString(this,100)
object email extends MappedString(this,50)
object content extends MappedString(this,5000)
object checked extends MappedBoolean(this)
}
object Feedback extends Feedback with LongKeyedMetaMapper[Feedback] {
override def fieldOrder = List(checked, createdAt, name, email)
}
94
LIITE L9: Tietokantayhteys olion toteutus
object DBVendor extends ConnectionManager {
Class.forName("com.mysql.jdbc.Driver")
def newConnection(name: ConnectionIdentifier) = {
try {
Full(DriverManager.getConnection(
"jdbc:mysql://localhost:3306/liftluma",
"root", "admin"))
} catch {
case e: Exception => e.printStackTrace; Empty
}
}
def releaseConnection(conn: Connection) { conn.close }
}
LIITE L10: Palautteenantolomake
Palautteen kaavarakenne sisältää e-etuliitteellä esitettyjä tag-kenttiä, joiden tietosisällöt
sidotaan GiveFeedback-nimisen Snippet-komponentin metodissa add. Palautelomaketta vastaava Snippet-komponentti on esitetty liitteessä L11
<lift:surround with="default" at="content">
<div class="column span-24">
<lift:GiveFeedback.add form="POST" multipart="true" >
<div class="column span-24">
<h3><lift:loc locid="give.feedback"/></h3>
<div id="entryform">
<table>
<tr>
<td><lift:loc locid="name"/></td>
<td><e:name /></td>
</tr>
<tr>
<td><lift:loc locid="email-address"/></td>
<td><e:email /></td>
</tr>
<tr>
<td><lift:loc locid="feedback"/></td>
<td id="fullfield"><e:content /></td>
</tr>
<tr>
<td><button><lift:loc locid="send"/></button></td>
</tr>
</table>
</div>
</div>
</lift:Feedback.add>
</div>
</lift:surround>
95
LIITE L11: Palautteenantolomakkeen käsitelevä Snippet-luokka
Snippet-komponentin add-metodi sitooja käsittelee palaute lomakkeen kaavarakenteiden tietosisällöt, jotka on esitetty e-etuliitteellä.
package luma.liftluma {
package snippet {
import
import
import
import
import
import
scala.xml.{ NodeSeq, Text }
java.util.Date
net.liftweb.common.{ Box, Empty, Full, Logger }
net.liftweb.http.{ S, SHtml, StatefulSnippet }
net.liftweb.util.Helpers._
luma.liftluma.model.{ Feedback }
class Feedback extends StatefulSnippet {
def dispatch: DispatchIt = {
case "add" => add _
}
var name = ""
var email = ""
var content = ""
def add(in: NodeSeq): NodeSeq = {
def submit(c: String) {
if (c.trim.length == 0)
error("Can't send empty feedback")
else {
val e = Feedback.create.createdAt(newDate)
.name(name)
.email(email)
.content(c)
.checked(false)
e.save
S.notice("Feedback was given!")
this.unregisterThisSnippet()
this.redirectTo("/luma/feedback/accepted")
}
}
bind("e", in,
"name" -> SHtml.text("", name = _),
"email" -> SHtml.text("", email = _),
"content" -> SHtml.text(content, submit))
}
}
}
}
96
LIITE L12: Käyttäjää mallintava toimintoalue luokkatoteutus
package luma.liftluma {
package model {
import _root_.net.liftweb.mapper._
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
object User extends User with MetaMegaProtoUser[User] {
override def dbTableName = "users" // define the DB table name
override def screenWrap = Full(<lift:surround with="default"
at="content"><lift:bind /></lift:surround>)
override def fieldOrder = List(id, firstName, lastName, email,
locale, timezone, password, textArea)
override def skipEmailValidation = true
}
class User extends MegaProtoUser[User] {
def getSingleton = User // what's the "meta" server
object textArea extends MappedTextarea(this, 2048) {
override def textareaRows = 10
override def textareaCols = 50
override def displayName = "Personal Essay"
}
}
}
}
97
LIITE L13: Sivupohja-apuluokan Snippet-komponentti toteutus
Snippet-komponentin metodit tarkastaa onko nykyinen käyttäjä sisäänkirjautunut käyttäjä ja palauttaa sitä vastaavan sivupohja-elementin.
package luma.liftluma {
package snippet {
import
import
import
import
import
import
import
_root_.scala.xml.{ NodeSeq, Text }
_root_.net.liftweb.util._
_root_.net.liftweb.common._
_root_.java.util.Date
luma.liftluma.lib._
Helpers._
_root_.luma.liftluma.model.User
class UserPanel {
def loginPanel(in: NodeSeq): NodeSeq =
{
if (User.loggedIn_?) {
<lift:Menu.item name="Logout">
<lift:loc locid="logout"/></lift:Menu.item>
} else {
<lift:Menu.item name="Login">
<lift:loc locid="login"/></lift:Menu.item>
}
}
def feedbackPanel(in: NodeSeq): NodeSeq =
{
if (User.loggedIn_?) {
<lift:Menu.item name="giveFeedback"/>
<lift:Menu.item name="browseFeedback"/>
} else {
<lift:Menu.item name="giveFeedback"/>
}
}
}
}
}
LIITE L14: Palautteiden selailuominaisuuden sivupohjatoteutus
<lift:surround with="default" at="content">
<table>
<tr>
<td><lift:loc locid="feedback.read"/></td>
<td><lift:loc locid="feedback.created.at"/></td>
<td><lift:loc locid="feedback.name"/></td>
<td><lift:loc locid="feedback.email"/></td>
</tr>
<lift:BrowseFeedback.list/>
</table>
</lift:surround>
98
LIITE L15: Palautteiden selailuominaisuuden Snippet-komponentti
Snippet-komponentti hakee tietokannasta kaikki palaute-ilmentymät, luo niistä HTMLtaulun rivejä ja palauttaa generoidun sivupohja-elementin.
package luma.liftluma {
package snippet {
import _root_.scala.xml.{ NodeSeq, Text }
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
import _root_.java.util.Date
import luma.liftluma.lib._
import Helpers._
import _root_.luma.liftluma.model.{ User, Feedback }
import net.liftweb.mapper.{OrderBy, Descending}
class BrowseFeedback {
val empty: NodeSeq = NodeSeq.Empty
def list: NodeSeq = {
(empty /: Feedback.findAll(OrderBy(Feedback.createdAt,
Descending)))((l, r) => l ++
<tr>
<td><a href={"/luma/feedback/show?id=" +
r.id}>{r.checked}</a></td>
<td><a href={"/luma/feedback/show?id=" +
r.id}>{r.createdAt}</a></td>
<td><a href={"/luma/feedback/show?id=" +
r.id}>{r.name}</a></td>
<td><a href={"/luma/feedback/show?id=" +
r.id}>{r.email}</a></td>
</tr>
)
}
}
}
}
99
LIITE L16: Yksittäisten palautteiden näkymä-sivupohja
Palautenäkymää vastaava Snippet-komponentti on esitetty liitteessä L17.
<lift:surround with="default" at="content">
<lift:ShowFeedback.show>
<h3><lift:loc locid="show.feedback" /></h3>
<table>
<b:id />
<tr>
<td><b:return /></td>
</tr>
<tr>
<td><lift:loc locid="name" /></td>
<td><b:name /></td>
</tr>
<tr>
<td><lift:loc locid="email-address" /></td>
<td><b:email /></td>
</tr>
<tr>
<td><lift:loc locid="feedback.created.at" /></td>
<td><b:createdAt /></td>
</tr>
<tr>
<td><lift:loc locid="feedback" /></td>
<td id="fullfield"><b:content /></td>
</tr>
<tr>
<td><b:actions /></td>
</tr>
</table>
</lift:ShowFeedback.show>
</lift:surround>
100
LIITE L17: Yksittäisten palautteiden näkymän Snippet-komponentti
Snippet-komponentin show-metodi sitoo b-etuliitteellä esitetyt kentät sivupohjasta ja
tarjoaa toimintoja, jotka luodana SHtml-komponentin avulla.
package luma.liftluma {
package snippet {
import
import
import
import
import
import
import
import
scala.xml.{ NodeSeq, Text }
java.util.Date
net.liftweb.common.{ Box, Empty, Full, Logger }
net.liftweb.http.{ S, SHtml, StatefulSnippet }
net.liftweb.util.Helpers._
net.liftweb.util.Helpers
_root_.luma.liftluma.model._
net.liftweb.mapper.By
class ShowFeedback extends StatefulSnippet {
def dispatch: DispatchIt = {
case "show" => show _
}
def show(in: NodeSeq): NodeSeq = {
S.param("id") match {
case Full(id) => {
Feedback.findById(id) match {
case feedback :: Nil => {
feedback.checked(true).save
bind("b", in,
"return" -> { SHtml.link("/luma/feedback/browse",
() => feedback, Text(S ? "back")) ++ Text(" ") },
"id" -> SHtml.hidden(() => id),
"name" -> feedback.name.asHtml,
"email" -> feedback.email.asHtml,
"createdAt" -> feedback.createdAt.asHtml,
"content" -> feedback.content.asHtml,
"actions" -> {
SHtml.link("/luma/feedback/browse", () =>
feedback.checked(false).save, Text(
S ? "feedback.unread")) ++ Text(" ") ++
SHtml.link("/luma/feedback/browse", () =>
feedback.delete_!, Text(S ? "feedback.delete"))
}
)
}
case _ => Text("Could not locate Feedback " + id)
}
}
case _ => Text("Feedback ID was not provided")
}
}
}
}
}
101
LIITE L18: Type-toimintoalueluokan toteutus
Luokka sisältää tietokantakenttien lisäksi kaksi apumetodia. Metodi nameOf hakee tietyllä tietokanta id-tunnuksella varustetun Type-ilmentymän nimen. Metodi findByName
hakee tietokannasta tietynnimisen Type-ilmentymän ja mikäli sellaista ei löydy, se luodaan tietokantaan.
package luma.liftluma.model
import _root_.net.liftweb.mapper._
import _root_.java.math.MathContext
import _root_.java.util.Date
class Type extends LongKeyedMapper[Type] with IdPK {
def getSingleton = Type
object name extends MappedString(this,100)
object createdAt extends MappedDateTime(this)
}
object Type extends Type with LongKeyedMetaMapper[Type] {
override def fieldOrder = List(name, createdAt)
import net.liftweb.util.Helpers.tryo
def nameOf(id:Long) : String =
Type.findAll(By(Type.id, id)) match {
case instance :: Nil => {
instance.name
}
case _ => {
"/"
}
}
def findByName (name : String) : Type =
Type.findAll(By(Type.name, name)) match {
case typeInstance :: Nil => {
typeInstance
}
case _ => {
println("Creating new Type with name " + name)
val newType = Type.create
.createdAt(new Date)
.name(name)
newType.save
return newType
}
}
}
102
LIITE L19: Category-toimintoalueluokan toteutus
Luokka sisältää tietokantakenttien lisäksi kaksi apumetodia. Metodi nameOf hakee tietyllä tietokanta id-tunnuksella varustetun Categiry-ilmentymän nimen. Metodi findByName
hakee tietokannasta tietynnimisen Category-ilmentymän ja mikäli sellaista ei
löydy, se luodaan tietokantaan.
package luma.liftluma.model
import _root_.net.liftweb.mapper._
import _root_.java.math.MathContext
import _root_.java.util.Date
class Category extends LongKeyedMapper[Category] with IdPK {
def getSingleton = Category
object name extends MappedString(this,100)
object createdAt extends MappedDateTime(this)
}
object Category extends Category with LongKeyedMetaMapper[Category] {
override def fieldOrder = List(name, createdAt)
def nameOf(id:Long) : String =
Category.findAll(By(Category.id, id)) match {
case instance :: Nil => {
instance.name
}
case _ => {
"/"
}
}
def findByName (name : String) : Category =
Category.findAll(By(Category.name, name)) match {
case categoryInstance :: Nil => {
categoryInstance
}
case _ => {
println("Creating new Category with name " + name)
val newCategory = Category.create
.createdAt(new Date)
.name(name)
newCategory.save
return newCategory
}
}
}
103
LIITE L20: Languages-toimintoalueluokan toteutus
Luokka sisältää tietokantakenttien lisäksi kaksi tietokantakyselymetodia ja apumetodin,
jolla voidaan määrittää käytössä oleva lokaali.
package luma.liftluma.model
import
import
import
import
_root_.net.liftweb.mapper._
_root_.java.math.MathContext
_root_.java.util.{Locale, Date}
net.liftweb.http.{ S }
class Languages extends LongKeyedMapper[Languages] with IdPK {
def getSingleton = Languages
object title extends MappedString(this,100)
object locale extends MappedLocale(this)
object createdAt extends MappedDateTime(this)
}
object Languages extends Languages with LongKeyedMetaMapper[Languages]
{
override def fieldOrder = List(title, createdAt)
import net.liftweb.util.Helpers.tryo
def findByCurrent () : List[Languages] =
Languages.findAll(By(Languages.locale, S.locale.toString))
def findByTitle (title : String) : List[Languages] =
Languages.findAll(By(Languages.title, title))
def findByLocale (lang : Locale) : Option[Languages] =
Languages.findAll(By(Languages.locale, lang.toString)) match {
case lang :: Nil => {
return Some(lang)
}
case _ => {
None
}
}
}
104
LIITE L21: Page-toimintoalueluokan toteutus
Luokka sisältää tietokantakenttien lisäksi neljä tietokantakyselymetodia ja apumetodin,
jolla voi luoda uusia Pagecontent-ilmentymiä.
package luma.liftluma.model
import
import
import
import
_root_.net.liftweb.mapper._
_root_.java.math.MathContext
_root_.java.util.Date
luma.liftluma.util._
class Page extends LongKeyedMapper[Page] with IdPK {
def getSingleton = Page
object title extends MappedString(this,100)
object typeRef extends MappedLongForeignKey(this, Type)
object categoryRef extends MappedLongForeignKey(this, Category)
object name extends MappedString(this,100)
object createdAt extends MappedDateTime(this)
object modifiedAt extends MappedDateTime(this)
}
object Page extends Page with LongKeyedMetaMapper[Page] {
override def fieldOrder = List(name, createdAt)
import net.liftweb.util.Helpers.tryo
def newPagecontent(id : String) : Option[Pagecontent] =
Page.findById(id) match {
case page :: Nil => {
Languages.findByCurrent match {
case lang :: Nil => {
Some(Pagecontent.create
.createdAt(new Date)
.nodeId(Util.nextNodeId(page,lang))
.pageRef(page.id)
.languageRef(lang.id)
)
}
case _ => None
}
}
case _ => None
}
def findByName (name : String) : List[Page] =
Page.findAll(By(Page.name, name))
def findById (id : String) : List[Page] =
findById(id.toLong)
def findById (id : Long) : List[Page] =
Page.findAll(By(Page.id, id.toLong))
def findByTypeAndCategory (typeId : Long, categoryId : Long) :
Page =
Page.findAll(By(Page.typeRef, typeId),
By(Page.categoryRef, categoryId)) match {
case page :: Nil => {
page
}
case _ => {
105
println("Creating new Page with name " + name)
val newPage = Page.create
.createdAt(new Date)
.modifiedAt(new Date)
.typeRef(typeId)
.categoryRef(categoryId)
.title("Generated")
.name(name)
newPage.save
return newPage
}
}
}
LIITE L22: Pagecontent-toimintoalueluokan toteutus
Luokka sisältää tietokantakenttien lisäksi kolme tietokantakyselymetodia.
package luma.liftluma.model
import _root_.net.liftweb.mapper._
import _root_.java.math.MathContext
import _root_.java.util.Date
class Pagecontent extends LongKeyedMapper[Pagecontent] with IdPK {
def getSingleton = Pagecontent
object pageRef extends MappedLongForeignKey(this, Page)
object languageRef extends MappedLongForeignKey(this, Languages)
object title extends MappedString(this, 100)
object name extends MappedString(this, 100)
object content extends MappedText(this)
object createdAt extends MappedDateTime(this)
object modifiedAt extends MappedDateTime(this)
object nodeId extends MappedLong(this)
}
object Pagecontent extends Pagecontent with LongKeyedMetaMapper[Pagecontent] {
override def fieldOrder = List(name, createdAt)
def findAllSimilar(instance: Pagecontent): List[Pagecontent] =
Pagecontent.findAll(
By(Pagecontent.pageRef, instance.pageRef),
By(Pagecontent.languageRef, instance.languageRef),
OrderBy(Pagecontent.nodeId, Ascending))
def findById(id: String): List[Pagecontent] =
Pagecontent.findAll(By(Pagecontent.id, id.toLong))
def find(page: Long, lang: Long, nodeId: Long):
Option[Pagecontent] =
Pagecontent.findAll(
By(Pagecontent.pageRef, page),
By(Pagecontent.languageRef, lang),
By(Pagecontent.nodeId, nodeId)) match {
case pagecontent :: Nil => {
return Some(pagecontent)
}
106
case _ => {
if (nodeId == 0) {
println("Creating new Pagecontent")
val newContent = Pagecontent.create
.createdAt(new Date)
.modifiedAt(new Date)
.pageRef(page)
.languageRef(lang)
.title("Generated")
.name("Generated")
.content("Generated Page")
.nodeId(0)
newContent.save
return Some(newContent)
} else {
None
}
}
}
}
LIITE L23: Dynaamisen sivuluonteen mahdollistava sivupohja
Sivupohja hahmottaa linkkilistan, tietosisällön ja toimintopainikkeet. Sivupohjaa vastaava Snippet-komponentin toteutus on esitetty liitteessä L25.
<div id="content">
<lift:Content.linklist/>
<lift:Content.display>
<c:content/>
<c:actions/>
</lift:Content.display>
</div>
107
LIITE L24: Pagecontent-instanssin muokkaussivupohja
Sivupohjaa vastaava Snippet-komponentin toteutus on esitetty liitteessä L25.
<lift:surround with="default" at="content">
<lift:Content.edit form="POST">
<h3><lift:loc locid="edit.content" /></h3>
<table>
<e:id />
<tr>
<td><lift:loc locid="title" /></td>
<td><e:title /></td>
</tr>
<tr>
<td><lift:loc locid="name" /></td>
<td><e:name /></td>
</tr>
<tr>
<td><lift:loc locid="content" /></td>
<td id="fullfield"><e:content /></td>
</tr>
<tr>
<td><e:update /></td>
<td><e:actions /></td>
</tr>
</table>
</lift:Content.edit>
</lift:surround>
108
LIITE L25: Dynaamisen sivupohjaluokan Snippet-komponentti
package luma.liftluma {
package snippet {
import
import
import
import
import
import
import
import
import
scala.xml.{ NodeSeq, Text }
java.util.Date
net.liftweb.common.{ Box, Empty, Full, Logger }
net.liftweb.http.{ RequestVar, S, SHtml, StatefulSnippet }
net.liftweb.util.Helpers._
net.liftweb.util.Helpers
_root_.luma.liftluma.model._
net.liftweb.mapper.By
luma.liftluma.util._
class Content {
def display(in: NodeSeq): NodeSeq = {
val content = Util.getContent()
if(content.nonEmpty) {
val actions = initializeActions(content.get)
bind("c", in,
"content" -> {
<title>{content.get.title}</title>
<div>{content.get.content}</div>},
"actions" -> actions
)
}
else
bind("c", in, "content" -> <h2>Page doesn't exist</h2>,
"actions" -> <br/>)
}
private def initializeActions(content : Pagecontent) : NodeSeq =
{
if(User.loggedIn_?)
{ SHtml.link("/luma/common/edit?edit=" + content.id, () =>
currentPagecontentVar(content), Text(S ? "edit")) ++
Text(" ") ++
SHtml.link("/luma/common/edit?add=" + content.pageRef, () =>
currentPagecontentVar(content), Text(S ? "add"))
}
else
{ <br/> }
}
def linklist(in: NodeSeq): NodeSeq = {
val content = Util.getContent()
if(content.nonEmpty) {
val pcs = Pagecontent.findAllSimilar(content.get)
if(pcs.length > 1) {
(NodeSeq.Empty /: pcs)((l, p) => l ++
<li>
<a href={Util.getUriToPagecontent(p)}>{p.name}</a></li>)
}
else {
<empty/>
}
}
else {
<empty/>
}
}
109
object currentPagecontentVar extends RequestVar[Pagecontent]({
Pagecontent.create
})
def currentPagecontent = currentPagecontentVar.is
def edit(in: NodeSeq): NodeSeq = {
def doUpdate () = {
currentPagecontent.modifiedAt(new Date)
currentPagecontent.save
S.redirectTo(Util.getUriToPagecontent(currentPagecontent))
}
S.param("edit") match {
case Full(id) => {
Pagecontent.findById(id) match {
case pagecontent :: Nil => {
val content = currentPagecontent
bind("e", in,
"id" -> SHtml.hidden(() =>
currentPagecontentVar(content)),
"title" -> SHtml.text(currentPagecontent.title.is,
currentPagecontent.title(_)),
"name" -> SHtml.text(currentPagecontent.name.is,
currentPagecontent.name(_)),
"content" ->
SHtml.textarea(currentPagecontent.content.is,
currentPagecontent.content(_),
"cols" -> "80", "rows" -> "10"),
"actions" -> editButtons(currentPagecontent),
"update" -> SHtml.submit(S ? "update", doUpdate)
)
}
case _ => Text("Could not locate Pagecontent with ID " +
id)
}
}
case _ => {
S.param("add") match {
case Full(id) => {
val content = Page.newPagecontent(id)
if(content.nonEmpty) {
bind("e", in,
"id" -> SHtml.hidden(() =>
currentPagecontentVar(content.get)),
"title" -> SHtml.text("",
currentPagecontent.title(_)),
"name" -> SHtml.text("",
currentPagecontent.name(_)),
"content" -> SHtml.textarea("",
currentPagecontent.content(_),
"cols" -> "80", "rows" -> "10"),
"actions" -> editButtons(currentPagecontent),
"update" -> SHtml.submit(S ? "update", doUpdate)
)
}
else {
Text("Important relations are missing!")
}
}
case _ => Text("Request missed important parameters!")
}
}
}
}
110
private def editButtons(instance: Pagecontent): NodeSeq = {
val actions = {
SHtml.link(Util.getUriToPagecontent(instance), () =>
currentPagecontent, Text(S ? "cancel"))
}
if (instance.nodeId != 0)
actions ++
SHtml.link(Util.getUriToBasePagecontent(instance),
() => instance.delete_!, Text(S ? "delete"))
else
actions
}
}
}
}
LIITE L26: Yleinen apuluokka sivujenkäsittelyyn
package luma.liftluma.util
import java.util.Date
import java.text.SimpleDateFormat
import net.liftweb.common.{Box,Empty,Full}
import net.liftweb.http.S
import luma.liftluma.model.{Type, Category, Languages, Page,
Pagecontent}
import _root_.net.liftweb.mapper._
object Util {
def nextNodeId(page:Page, lang:Languages) : Long = {
val lastPagecontent = Pagecontent.findAll(
By(Pagecontent.pageRef, page.id),
By(Pagecontent.languageRef, lang.id),
OrderBy(Pagecontent.nodeId, Descending)).head
return 1 + lastPagecontent.nodeId
}
def getContentIds : (String, String, Long) = {
val uri = S.uri.split("/")
(uri(1), uri(2), getLongParam("id", 0))
}
def getUriToPagecontent(pagecontent : Pagecontent) : String =
getUriToBasePagecontent(pagecontent) + "?id=" + pagecontent.nodeId
def getUriToBasePagecontent(pagecontent : Pagecontent) : String =
Page.findById(pagecontent.pageRef) match {
case page :: Nil => {
"/luma/" + Type.nameOf(page.typeRef) + "/" +
Category.nameOf(page.categoryRef)
}
case _ => "/"
}
def getContent() : Option[Pagecontent] = {
val uri = S.uri.split("/")
111
if(uri.length < 3) {
return None
}
val typeName = uri(2)
val categoryName = uri(3)
val nodeId = getLongParam("id", 0)
println(typeName + "/" + categoryName + "/" + nodeId)
val typeInstance = Type.findByName(typeName)
val categoryInstance = Category.findByName(categoryName)
val pageInstance = Page.findByTypeAndCategory(typeInstance.id,
categoryInstance.id)
val languageInstance = Languages.findByLocale(S.locale)
if(languageInstance.nonEmpty) {
Pagecontent.find(pageInstance.id, languageInstance.get.id,
nodeId)
}
else {
return None
}
}
def getLongParam(name : String, default : Long) : Long = {
try {
S.param(name).map(_.toLong) openOr default
}
catch {
case e => default
}
}
}
112