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> | </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> | </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
© Copyright 2024