Mapovanie s Orikou

1. Prehľad

Orika je Java Bean mapovací rámec, ktorý rekurzívne kopíruje údaje z jedného objektu do druhého. Môže to byť veľmi užitočné pri vývoji viacvrstvových aplikácií.

Pri presúvaní dátových objektov tam a späť medzi týmito vrstvami je bežné, že musíme prevádzať objekty z jednej inštancie do druhej, aby vyhovovali rôznym API.

Niektoré spôsoby, ako to dosiahnuť, sú: pevné kódovanie kopírovacej logiky alebo implementácia mapovačov fazule ako Dozer. Môže sa však použiť na zjednodušenie procesu mapovania medzi jednou vrstvou objektu a druhou.

Orika používa generovanie bajtového kódu na vytváranie rýchlych mapovačov s minimálnymi režijnými nákladmi, čo je oveľa rýchlejšie ako iné mapovače založené na odrazoch, ako je Dozer.

2. Jednoduchý príklad

Základným kameňom mapovacieho rámca je MapperFactory trieda. Toto je trieda, ktorú použijeme na konfiguráciu mapovaní a na získanie MapperFacade inštancia, ktorá vykonáva skutočné mapovacie práce.

Vytvárame a MapperFactory objekt takto:

MapperFactory mapperFactory = nový DefaultMapperFactory.Builder (). Build ();

Potom za predpokladu, že máme zdrojový údajový objekt, Zdroj.java, s dvoma poľami:

public class Zdroj {private String name; súkromný int vek; public Source (názov reťazca, vek int) {this.name = name; this.age = vek; } // štandardní zakladatelia a zakladatelia}

A podobný cieľový údajový objekt, Dest.java:

public class Dest {private String name; súkromný int vek; public Dest (názov reťazca, int vek) {this.name = meno; this.age = vek; } // štandardní zakladatelia a zakladatelia}

Toto je najzákladnejšie mapovanie fazule pomocou Oriky:

@Test public void givenSrcAndDest_whenMaps_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Zdroj src = nový zdroj („Baeldung“, 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Ako môžeme pozorovať, vytvorili sme a Dest objekt s identickými poľami ako Zdroj, jednoducho mapovaním. Štandardne je tiež možné obojsmerné alebo spätné mapovanie:

@Test public void givenSrcAndDest_whenMapsReverse_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nový Dest ("Baeldung", 10); Zdroj dest = mapper.map (src, Source.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

3. Nastavenie Maven

Aby sme mohli používať mapovač Orika v našich projektoch, musíme mať orika-jadro závislosť v pom.xml:

 ma.glasnost.orika orika-core 1.4.6 

Najnovšiu verziu nájdete vždy tu.

3. Práca s MapperFactory

Všeobecný model mapovania pomocou Oriky zahŕňa vytvorenie a MapperFactory objekt, nakonfigurujeme ho pre prípad, že musíme vyladiť predvolené správanie mapovania a získať a MapperFacade objekt z neho a nakoniec skutočné mapovanie.

Tento vzorec budeme sledovať vo všetkých našich príkladoch. Náš úplne prvý príklad ale ukázal predvolené správanie mapovača bez akýchkoľvek vylepšení z našej strany.

3.1. The BoundMapperFacade vs MapperFacade

Je potrebné si uvedomiť jednu vec, ktorú sme si mohli zvoliť BoundMapperFacade nad predvolené hodnoty MapperFacade čo je dosť pomalé. Toto sú prípady, keď máme na mapovanie konkrétny pár typov.

Náš počiatočný test by sa tak stal:

@Test public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Zdroj src = nový zdroj ("baeldung", 10); Dest dest = boundMapper.map (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Avšak pre BoundMapperFacade na mapovanie obojsmerne musíme explicitne zavolať znak mapReverse namiesto metódy mapy, ktorú sme skúmali pre prípad predvoleného nastavenia MapperFacade:

@Test public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Dest src = nový Dest ("baeldung", 10); Zdroj dest = boundMapper.mapReverse (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Inak test zlyhá.

3.2. Konfigurujte mapovania polí

Príklady, na ktoré sme sa doteraz pozreli, zahŕňajú zdrojové a cieľové triedy s rovnakými názvami polí. Táto podsekcia sa venuje prípadu, keď je medzi nimi rozdiel.

Zvážte zdrojový objekt, Osoba , s tromi poľami, a to názov, prezývka a Vek:

public class Osoba {private String name; súkromná prezývka reťazca; súkromný int vek; public Osoba (meno reťazca, prezývka reťazca, vek) {this.name = meno; this.nickname = nickname; this.age = vek; } // štandardní zakladatelia a zakladatelia}

Ďalšia vrstva aplikácie má potom podobný objekt, ale napísaný francúzskym programátorom. Povedzme, že sa to volá Personne, s poľami žiadne M, surnom a Vek, všetky zodpovedajú vyššie uvedeným trom:

public class Personne {private String nom; privátny reťazec surnom; súkromný int vek; public Personne (String nom, String surnom, int age) {this.nom = nom; this.surnom = surnom; this.age = vek; } // štandardní zakladatelia a zakladatelia}

Orika nemôže tieto rozdiely automaticky vyriešiť. Môžeme však použiť ClassMapBuilder API na registráciu týchto jedinečných mapovaní.

Používali sme ho už skôr, ale zatiaľ sme nečerpali žiadne z jeho výkonných funkcií. Prvý riadok každého z našich predchádzajúcich testov využívajúcich predvolené hodnoty MapperFacade používal ClassMapBuilder API na registráciu dvoch tried, ktoré sme chceli mapovať:

mapperFactory.classMap (Source.class, Dest.class);

Všetky polia by sme mohli namapovať aj pomocou predvolenej konfigurácie, aby bolo jasnejšie:

mapperFactory.classMap (Source.class, Dest.class) .byDefault ()

Pridaním predvolene() volanie metódy, už konfigurujeme správanie mapovača pomocou ClassMapBuilder API.

Teraz chceme mať možnosť mapovať Personne do Osoba, takže tiež konfigurujeme mapovania polí na mapovač pomocou ClassMapBuilder API:

@Test public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .field ("nom", "meno"). Pole ("surnom", "prezývka") .field ("vek", "" vek "). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = new Personne ("Claire", "cla", 25); Osoba englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (englishPerson.getName (), frenchPerson.getNom ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Nezabudnite zavolať Registrovať() Metóda API na registráciu konfigurácie s MapperFactory.

Aj keď sa líši iba jedno pole, ísť touto cestou znamená, že sa musíme výslovne zaregistrovať všetko mapovanie polí, vrátane Vek ktorý je v oboch objektoch rovnaký, inak sa neregistrované pole nezmapuje a test by zlyhal.

Toto bude čoskoro únavné, čo ak chceme zmapovať iba jedno pole z 20, musíme nakonfigurovať všetky ich priradenia?

Nie, nie, keď povieme mapovaču, aby použil predvolenú konfiguráciu mapovania v prípadoch, keď sme explicitne nedefinovali mapovanie:

mapperFactory.classMap (Personne.class, Person.class) .field ("nom", "meno"). pole ("surnom", "prezývka"). byDefault (). register ();

Tu sme nedefinovali mapovanie pre Vek pole, ale napriek tomu test prejde.

3.3. Vylúčte pole

Za predpokladu, že by sme chceli vylúčiť žiadne M oblasť Personne z mapovania - takže Osoba objekt prijíma nové hodnoty iba pre polia, ktoré nie sú vylúčené:

@Test public void givenSrcAndDest_whenCanExcludeField_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .exclude ("nom") .field ("surnom", "prezývka"). Pole ("vek", "vek"). Registrovať(); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = new Personne ("Claire", "cla", 25); Osoba englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (null, englishPerson.getName ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Všimnite si, ako to vylučujeme v konfigurácii súboru MapperFactory a potom si všimnite tiež prvé tvrdenie, kde očakávame hodnotu názov v Osoba objekt zostať nulový, v dôsledku jeho vylúčenia z mapovania.

4. Mapovanie zbierok

Cieľový objekt môže mať niekedy jedinečné atribúty, zatiaľ čo zdrojový objekt iba udržiava každú vlastnosť v kolekcii.

4.1. Zoznamy a polia

Zvážte zdrojový údajový objekt, ktorý má iba jedno pole, zoznam mien osôb:

verejná trieda PersonNameList {private List nameList; public PersonNameList (Zoznam nameList) {this.nameList = nameList; }}

Teraz zvážte náš cieľový údajový objekt, ktorý sa oddeľuje krstné meno a priezvisko do samostatných polí:

verejná trieda PersonNameParts {private String firstName; private String priezvisko; verejné PersonNameParts (reťazec krstné meno, reťazec priezvisko) {this.firstName = meno; this.lastName = priezvisko; }}

Predpokladajme, že sme si veľmi istí, že v indexe 0 vždy bude krstné meno osoby a pri indexe 1 budú vždy ich priezvisko.

Orika nám umožňuje používať zápis v zátvorke na prístup k členom zbierky:

@Test public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameList.class, PersonNameParts.class) .field ("nameList [0]", "firstName") .field ("nameList [1]", "lastName"). (); MapperFacade mapper = mapperFactory.getMapperFacade (); Zoznam nameList = Arrays.asList (nový reťazec [] {"Sylvester", "Stallone"}); PersonNameList src = nový PersonNameList (nameList); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Sylvester"); assertEquals (dest.getLastName (), "Stallone"); }

Aj keď namiesto PersonNameList, mali sme PersonNameArray, rovnaký test by prešiel aj pre rad mien.

4.2. Mapy

Za predpokladu, že náš zdrojový objekt má mapu hodnôt. Vieme, že na tejto mape je kľúč, najprv, ktorého hodnota predstavuje hodnotu osoby krstné meno v našom cieľovom objekte.

Rovnako vieme, že existuje ďalší kľúč, posledný, na tej istej mape, ktorej hodnota predstavuje hodnotu osoby priezvisko v cieľovom objekte.

verejná trieda PersonNameMap {private Map nameMap; verejná PersonNameMap (mapa nameMap) {this.nameMap = nameMap; }}

Podobne ako v prípade v predchádzajúcej časti, použijeme zátvorkovú notáciu, ale namiesto vloženia indexu odovzdáme kľúč, ktorého hodnotu chceme namapovať na dané cieľové pole.

Orika akceptuje dva spôsoby získania kľúča, oba sú uvedené v nasledujúcom teste:

@Test public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameMap.class, PersonNameParts.class) .field ("nameMap ['first']", "firstName") .field ("nameMap [\" last \ "posledný" "priezvisko") .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Názov mapy = nová HashMap (); nameMap.put ("prvý", "Leornado"); nameMap.put ("last", "DiCaprio"); PersonNameMap src = new PersonNameMap (nameMap); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Leornado"); assertEquals (dest.getLastName (), "DiCaprio"); }

Môžeme použiť jednoduché úvodzovky alebo dvojité úvodzovky, musíme však uniknúť.

5. Namapujte vnorené polia

V nadväznosti na predchádzajúce príklady kolekcií predpokladajme, že v našom zdrojovom dátovom objekte sa nachádza ďalší objekt Data Transfer Object (DTO), ktorý obsahuje hodnoty, ktoré chceme mapovať.

verejná trieda PersonContainer {súkromné ​​meno meno; public PersonContainer (meno mena) {this.name = meno; }}
verejná trieda Názov {private String firstName; private String priezvisko; verejné meno (reťazec meno, reťazec priezvisko) {this.firstName = meno; this.lastName = priezvisko; }}

Aby sme mohli získať prístup k vlastnostiam vnoreného DTO a mapovať ich na náš cieľový objekt, používame bodkovú notáciu, napríklad takto:

@Test public void givenSrcWithNestedFields_whenMaps_thenCorrect () {mapperFactory.classMap (PersonContainer.class, PersonNameParts.class) .field ("name.firstName", "firstName") .field ("name.lastName", "lastName"). Register () ; MapperFacade mapper = mapperFactory.getMapperFacade (); PersonContainer src = nový PersonContainer (nové meno („Nick“, „Canon“)); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Nick"); assertEquals (dest.getLastName (), "Canon"); }

6. Mapovanie nulových hodnôt

V niektorých prípadoch môžete chcieť skontrolovať, či sú hodnoty null mapované alebo ignorované, keď sa vyskytnú. V predvolenom nastavení bude Orika mapovať nulové hodnoty, keď sa vyskytnú:

@Test public void givenSrcWithNullField_whenMapsThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Zdroj src = nový zdroj (null, 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Toto správanie je možné prispôsobiť na rôznych úrovniach podľa toho, aké konkrétne by sme chceli byť.

6.1. Globálna konfigurácia

Náš mapovač môžeme nakonfigurovať tak, aby mapoval nuly alebo ich ignoroval na globálnej úrovni pred vytvorením globálu MapperFactory. Pamätáte si, ako sme tento objekt vytvorili v našom úplne prvom príklade? Tentokrát pridávame ďalšie volanie počas procesu zostavovania:

MapperFactory mapperFactory = nový DefaultMapperFactory.Builder () .mapNulls (false) .build ();

Môžeme spustiť test, aby sme potvrdili, že nuly sa skutočne nezmapujú:

@Test public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Zdroj src = nový zdroj (null, 10); Dest dest = nový Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Čo sa stane, je to, že predvolene sú mapované nuly. To znamená, že aj keď je hodnota poľa v zdrojovom objekte nulový a hodnota príslušného poľa v cieľovom objekte má zmysluplnú hodnotu, bude prepísaná.

V našom prípade sa cieľové pole neprepíše, ak jeho príslušné zdrojové pole má a nulový hodnotu.

6.2. Lokálna konfigurácia

Mapovanie nulový hodnoty je možné riadiť na a ClassMapBuilder pomocou mapNulls (true | false) alebo mapNullsInReverse (true | false) na riadenie mapovania nul v opačnom smere.

Nastavením tejto hodnoty na a ClassMapBuilder inštancie, všetky mapovania polí vytvorené na rovnakom ClassMapBuilder, po nastavení hodnoty nadobudne rovnakú hodnotu.

Poďme si to ilustrovať na príklade testu:

@Test public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .mapNulls (false) .field ("name", "name"). ).Registrovať(); MapperFacade mapper = mapperFactory.getMapperFacade (); Zdroj src = nový zdroj (null, 10); Dest dest = nový Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Všimnite si, ako voláme mapNulls tesne pred registráciou názov Toto spôsobí všetky polia nasledujúce za mapNulls keď majú volanie ignorované nulový hodnotu.

Obojsmerné mapovanie prijíma aj mapované nulové hodnoty:

@Test public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nový Dest (null, 10); Zdroj cieľ = nový zdroj ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Tomu tiež môžeme zabrániť volaním mapNullsInReverse a okolo nepravdivé:

@Test public void givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .mapNullsInReverse () ) .Registrovať(); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nový Dest (null, 10); Zdroj cieľ = nový zdroj ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Vin"); }

6.3. Konfigurácia na úrovni poľa

Môžeme to nakonfigurovať na úrovni poľa pomocou fieldMap, ako:

mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .fieldMap ("name", "name"). mapNulls (false) .add (). byDefault (). register ( );

V takom prípade konfigurácia ovplyvní iba server názov pole, ako sme ho nazvali na úrovni poľa:

@Test public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .fieldMap ("name", "name"). MapNulls ( ) .byDefault (). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Zdroj src = nový zdroj (null, 10); Dest dest = nový Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

7. Vlastné mapovanie Orika

Doteraz sme sa pozreli na jednoduché príklady vlastného mapovania pomocou ClassMapBuilder API. Stále budeme používať to isté API, ale prispôsobíme si naše mapovanie pomocou Oriky CustomMapper trieda.

Za predpokladu, že máme dva dátové objekty, z ktorých každý má určité volané pole dtob, predstavujúci dátum a čas narodenia osoby.

Jeden dátový objekt predstavuje túto hodnotu ako a reťazec datetime v nasledujúcom formáte ISO:

2007-06-26T21: 22: 39Z

a druhý predstavuje to isté ako a dlho zadajte nasledujúci formát časovej pečiatky unix:

1182882159000

Je zrejmé, že žiadne z úprav, ktoré sme doposiaľ pokryli, nestačí na prevod medzi týmito dvoma formátmi počas procesu mapovania, dokonca ani zabudovaný prevodník Orika túto úlohu nezvládne. Tu musíme napísať a CustomMapper vykonať požadovanú konverziu počas mapovania.

Vytvorme náš prvý údajový objekt:

public class Person3 {private String name; súkromný String dtob; public Person3 (String name, String dtob) {this.name = name; this.dtob = dtob; }}

potom náš druhý dátový objekt:

public class Personne3 {private String name; súkromný dlhý dtob; public Personne3 (názov reťazca, dlhý dtob) {this.name = meno; this.dtob = dtob; }}

Nebudeme teraz označovať, ktorý je zdroj a ktorý cieľ, ako CustomMapper nám umožňuje obstarávať obojsmerné mapovanie.

Tu je naša konkrétna implementácia CustomMapper abstraktná trieda:

trieda PersonCustomMapper rozširuje CustomMapper {@Override public void mapAtoB (Personne3 a, Person3 b, kontext MappingContext) {Dátum dátum = nový Dátum (a.getDtob ()); Formát DateFormat = nový SimpleDateFormat ("rrrr-MM-dd'T'HH: mm: ss'Z '" "); Reťazec isoDate = format.format (dátum); b.setDtob (isoDate); } @Override public void mapBtoA (Person3 b, Personne3 a, MappingContext context) {DateFormat format = new SimpleDateFormat ("yyyy-MM-dd'T'HH: mm: ss'Z '"); Dátum dátum = format.parse (b.getDtob ()); long timestamp = date.getTime (); a.setDtob (časová značka); }};

Všimnite si, že sme implementovali metódy mapAtoB a mapBtoA. Implementácia oboch robí našu mapovaciu funkciu obojsmernou.

Každá metóda odhalí dátové objekty, ktoré mapujeme, a postaráme sa o kopírovanie hodnôt polí z jednej do druhej.

Tam je miesto, kde napíšeme vlastný kód na manipuláciu so zdrojovými údajmi podľa našich požiadaviek pred ich zápisom do cieľového objektu.

Spustíme test a potvrdíme, že náš vlastný mapovač funguje:

@Test public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Reťazec dateTime = "2007-06-26T21: 22: 39Z"; long timestamp = new Long ("1182882159000"); Personne3 personne3 = nový Personne3 ("Leornardo", časová pečiatka); Person3 person3 = mapper.map (personne3, Person3.class); assertEquals (person3.getDtob (), dateTime); }

Všimnite si, že stále odovzdávame vlastný mapovač Oriinmu mapovačovi cez ClassMapBuilder API, rovnako ako všetky ostatné jednoduché prispôsobenia.

Môžeme tiež potvrdiť, že obojsmerné mapovanie funguje:

@Test public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Reťazec dateTime = "2007-06-26T21: 22: 39Z"; long timestamp = new Long ("1182882159000"); Person3 person3 = nová Person3 ("Leornardo", dateTime); Personne3 personne3 = mapper.map (person3, Personne3.class); assertEquals (person3.getDtob (), timestamp); }

8. Záver

V tomto článku máme preskúmali najdôležitejšie vlastnosti mapovacieho rámca Orika.

Určite existujú pokročilejšie funkcie, ktoré nám poskytujú oveľa väčšiu kontrolu, ale vo väčšine prípadov použitia bude tých, ktoré sú tu popísané, viac než dosť.

Celý kód projektu a všetky príklady nájdete v mojom projekte github. Nezabudnite si tiež prečítať náš návod o mapovacom rámci Dozer, pretože oba riešia viac-menej rovnaký problém.


$config[zx-auto] not found$config[zx-overlay] not found