Arkkitehtuuri Termieditorin käyttö vaatii kirjautumisen. Peruskäyttäjälle myönnetään erikseen aineistokohtaisia luku- ja muokkausoikeuksia. Järjestelmän ylläpitäjä (admin) saa ylläpitää kaikkia aineistoja. Järjestelmän pääkäyttäjä (superuser) saa lisäksi ylläpitää käyttäjätietoja. Termieditoria hyödynnetään web-käyttöliittymän lisäksi sovellusrajapintojen kautta muiden järjestelmien tietovarastona. Ylätason sovellusarkkitehtuuri Editori jakautuu karkeasti käyttöliittymäkomponenttiin ja taustajärjestelmään. Taustajärjestelmä hyödyntää keskeisinä resursseina relaatiotietokantaa ja kokoteksti-indeksiä.
Ydin ja rajapinnat Termed-api sisältää Termieditorin keskeisen toimintalogiikan ja tarjoaa sovellusrajapinnat tietojen käsittelyyn. Termed API vastaa tietojen oikeellisuudesta, tallennuksesta, hauista, käyttöoikeuksista jne. Tietosisältö tallennetaan relaatiotietokantaan (esim. Postgresql). Hakujen nopeuttamiseksi tiedot ineksoidaan Lucene-indeksiin. Lucene säilyttää indeksin tiedot tiedostoissa. Taustajärjestelmällä ei itsessään ole graafista käyttöliittymää vaan se on käytettävissä ainoastaan HTTP/REST-rajapintojen kautta. Rajapintaan tunnistautumismenetelmänä on HTTP-Basic. APIa voi ajaa suoraan esimerkiksi Spring Bootin sulautetulla Tomcat-palvelimella. Seuraavassa kuvassa on esitelty API-toteutuksessa käytetyt keskeiset komponentit. Service-komponentit hoitavat merkittävimmän työn. Controller-oliot vastaavat HTTP-pyyntöjen ohjaamisesta palvelukerrokselle.
Sovelluksen kerrokset Seuraavassa on esitelty tyypillisiä sovelluksessa käytettyjä kerroksia ylhäältä alas (HTTP-pyynnöstä tietokantakyselyihin). Message converter (AbstractHttpMessageConverter<T>) Erilaisia pyyntöjen muuntajia kutsutaan automaattisesti (Spring-kehyksen toimesta) ennen kuin kutsut tulevat kontrolleri-kerroksen käsiteltäviksi. Muutajat saattavat esimerkiksi sarjallistaa JSON- tai XML-sanomat Javan domain-olioiksi. Controller (@RestController-annotaatiolla) Kontrolleri-kerroksessa käsitellään HTTP-pyynnöt ja ohjataan niiden tarvitsemat haku- ja muokkausoperaatiot palvelukerrokselle. Service (Service<K, V>) Palvelun käsite on keskeisin abstraktiotaso. Palveluiden on tarkoitus kutsua ainoastaan toisia palveluita ja kaikki muokkausoperaatiot tulisi tehdä palvelukerroksen kautta. Joissakin yhteyksissä tästä kokonaisuudesta käytetään myös komponentin nimeä. Palvelun alapuolella olevat abstraktiot on tarkoitus olla puhtaasti palvelun sisäiseen käyttöön. Palvelu voi itsessään koostua useista kerroksista (esim. validointi, autentikointi, autorisointi, lokitus jne.).
Repository (AbstractRepository<K, V>) Repositorion rajapinta on Termieditorissa sama kuin palvelun, mutta repositoriolla tässä tarkoitetaan palvelua, joka hoitaa nimenomaan olioiden tallentamisen hyödyntäen alempaa Dao-kerrosta. Jos tiedonhakuolion (dao) ajatellaan karkeasti vastaavan yhtä tietokantataulua, yhden repositorion voi ajatella vastaavan yhtä tallennettavaa kokonaisuutta, joka voi koostua useista tauluista. Dao (Dao<K, V>) Termieditorissa tällä tasolla valvotaan tietokantatauluun liittyvät käyttöoikeudet. Dao-kerroksessa voidaan toteuttaa myös esim. auditointia. Sallitut pyynnöt ohjataan alimmalle "system dao" -kerrokselle. System dao (SystemDao<K, V>) Järjestelmä-dao abstrahoi yhden tietokantataulun. Tällä tasolla voidaan toteuttaa myös välimuisteja ja lokitusta. Esimerkki Service-oliosta (NodeService) Verkon solmujen eli varsinaisen tietosisällön käsittelystä vastaa NodeService-palvelu. Palvelu koostuu useista luokista, joista tärkeimmät on kuvattu seuraavassa kaaviossa harmaalle pohjalle. Keskeinen käytetty suunnittelumalli on delegointi. Kukin kerros toteuttaa yhteisen palvelu-rajapinnan (Service<K, V>), mutta tekee vain yhden rajatun toiminnon ja ohjaa pyynnön eteenpäin.
Kooditason esimerkki Service-oliosta (UserService) Seuraavassa esimerkissä on konfiguroitu "user service" -palvelu. Kommenteilla on kuvattu kunkin kerroksen merkitys. package fi.thl.termed.service.user; import... @Configuration public class UserServiceConfiguration { // luodaan palvelu käyttäjätietojen CRUD-operaatiolle @Bean
public Service<String, User> userservice( DataSource datasource, PlatformTransactionManager transactionmanager) { // Geneerisen SystemDao<K, V> rajapinnan toteuttava userdao koostuu: // 1) sisemmästä varsinaiset tietokantakyselyt tekevästä JdbcUserDao:sta // 2) ulommasta geneerisestä SystemDao-välimuistista SystemDao<String, User> userdao = new CachedSystemDao<>( new JdbcUserDao(dataSource)); // Vastaavalla tavalla määritellään käyttäjään liittyvien roolien tallennus // tietokantaan. SystemDao<UserGraphRole, Empty> usergraphroledao = new CachedSystemDao<>( new JdbcUserGraphRoleDao(dataSource)); // Käyttöoikeuksia valvoo PermissionEvaluator. PermissionEvaluator kohdistuu objektin // avaimeen (Dao:n ensimmäinen geneerinen parametri), joka tässä on käyttäjän nimi. // Nimeä ei kuitenkaan seuraavassa oteta huomioon sillä ainoastaan pyynnön tekevän // käyttäjän rooli tarkastetaan - sen tulee olla SUPERUSER. PermissionEvaluator<String> userpermissionevaluator = (u, o, p) -> u.getapprole() == AppRole.SUPERUSER; // Myös käyttäjään liittyvät roolit autorisoidaan siten, että roolien lukuihin ja // muutoksiin vaaditaan SUPERUSER-tason käyttäjä. PermissionEvaluator<UserGraphRole> usergraphrolepermissionevaluator = (u, o, p) -> u.getapprole() == AppRole.SUPERUSER; // Seuraavassa koostetaan UserRepository edellä määritellyistä SystemDao- ja // PermissionEvaluator-toteutuksista. Service<String, User> service = // UserRepository koordinoi käyttäjäolion lukemisen, tallennuksen ja poiston // kutsumalla kahta Dao-toteutusta. User-olio tallennetaan siis kahteen tietokanta- // tauluun ja UserRepository abstrahoi tietokannan. new UserRepository( // SystemDao kääritään geneerisen AuthorizedDao-toteutuksen taakse. // AuthorizedDao tarkistaa käyttöoikudet annetun PermissionEvaluatorin avulla. // Viimeinen THROW-parametri kertoo, että käyttöoikeuksien puuttuessa heitetään // poikkeus. new AuthorizedDao<>(userDao, userpermissionevaluator, THROW), new AuthorizedDao<>(userGraphRoleDao, usergraphrolepermissionevaluator, THROW));
// lopuksi palvelu kääritään vielä transaktiot toteuttavaan geneeriseen // Service-toteutukseen. service = new TransactionalService<>(service, transactionmanager); // Valmiin em. paloista koostetun servicen toteutus kulkee kutsujan näkökulmasta // seuraavasti: // 1) avataan transaktio (TransactionalService) // 2) ohjataan kunkin olion ja "osaolion" CRUD-operaatio oikealle Dao:lle (UserRepository) // 3) autorisoidaan pyyntö (AuthorizedDao) // 4) tarkistetaan saadaankö välimuistista lukuoperaation vastaus tai tarvitseeko // välimuistia nollata muutosoperaation seurauksena (CachedSystemDao) // 5) jos ei saatu vastausta välim., suoritetaan tarvittava tietokantakysely (Jdbc*Dao), // palataan takaisin // 6) jos suoritettiin tietokantakysely, tallennetaan tuore vastaus välimuistiin // seuraavia kyselyitä varten (CachedSystemDao, vastinpari kohdalle 4) // 7) jos suoritettiin lukuoperaatio, tarkistetaan saako haetun tuloksen palauttaa // käyttäjälle (AuthorizedDao, vastinpari kohdalle 3) // 8) jos suoritettiin lukuoperaatio, koostetaan haetut osaoliot yhdeksi // (UserRepository, vastinpari kohdalle 2) // 8) suljetaan transaktio (TransactionalService, vastinpari kohdalle 1) return service;
} } Edellisessä esimerkissä varsinaista konkreettista toteutusta on luokissa: JdbcUserDao (tekee tietokantakyselyt users-tauluun) JdbcUserGraphRoleDao (tekee tietokantakyselyt user_graph_role-tauluun) PermissionEvaluator<UserGraphRole> (toteutus yhden rivin lambdana) PermissionEvaluator<String> (toteutus niin ikään yhden rivin lambdana) UserRepository (pilkkoo ja koostaa User-oliota käyttävät pyynnöt dao-kerrokselle) Loput luokat ovat geneerisiä ja datasta riippumattomia. Em. esimerkissä niillä siis hoidettiin transaktiot, käyttöoikeuksien tarkistukset sekä välimuistitukset. Toinen tyypillinen lähestymistapa on hoitaa nämä näkökulmat Javan annotaatioiden avulla. Sovelluskehys käsittelee annotaatiot (esim. @Transactional, @PreAuthorized ja @Cacheable). Motivaationa Termieditorissa käytetyssä lähestymistavassa on selkeys, ennakoitavuus ja testattavuus sekä pysytteleminen "perus" Java-koodissa. Ei tarvita monimutkaista reflektioon pohjautuvaa tyyppitarkistukset ohittavaa annotaatioiden prosessointia, suoritusjärjestys ei ole epäselvä ja kun kerroksia tarvitaan lisää (esim. lokitus, validoinnit), niitä on helppo lisätä mallin mukaisesti.