čtvrtek 26. dubna 2012

Jak získat lokalizované texty z .properties souboru

Grails má dobře zmáklé message.properties soubory pro internacionalizaci aplikací. Díky tomu lze jednoduše udělat vícejazyčné aplikace. V dokumentaci je pěkně popsané, jak získat texty z těchto souborů z view nebo controlleru a taglib. Trochu však pozapomněli popsat, jak získat z doménových tříd a servisních tříd. Hodí se to, když chci v service třídě vyházet výjimku s vlastním textem.

Je potřeba deklarovat na začátku třídy field proměnnou (česky lokální?):
def messageSource
Grails do této proměnné provede automaticky DI. Pak lze pomocí této proměnné získat poměrně snadno texty z message.properties:
messageSource.getMessage("some.entity.label", null, "", LocaleContextHolder.getLocale())
getMessage() metoda má několik variant (přetěžovaných metod). Tu, kterou jsem použil, bere 4 parametry - první String je kód z message.properties souboru, druhý jsou možné argumenty v textu (žádné nemám), třetí je defaultní text a čtvrtý je Locale instance (což je zde ten, který má klient). Více zde.

neděle 22. dubna 2012

mysqldump v Grails

Dnes jsem poprvé vyzkoušel příkaz "mysqldump". Tento příkaz provede dump databáze v MySQL, což znamená, že se vytvoří skript pro kompletní (nebo i jiný) backup. Příkaz chci vyvolat v service třídě. Zatím jsem vykoumal toto:

public void createMySQLDump() {
        String username = grailsApplication.config.dataSource.username
        String password =  grailsApplication.config.dataSource.password
        "mysqldump --user=${username} --password=${password} --all-databases --result-file=mysql-dump-${new Date().format("ddMMyyyy")}.sql".execute()
    }
Tento dump vytvoří backup pro všechny dostupné databáze daného uživatele. Co jsem nepochopil, tak nefunguje mysqldump tímto způsobem:
"mysqldump --user= ${username} --password= ${password} --all-databases > C:\\mysql-dump-${new Date().time}.sql"
Jsem pak četl na jednom blogu, že znak ">" nelze použít, ale Grails to nijak neoznámí. Takže bacha.

Edit: Zjistil jsem, že dataSource objekt nefunguje spolehlivě pro zjištění uživ. jména a hesla do databáze, a je lepší použít grailsApplication objekt. Oba jsou pomocí DI automaticky vkládány do service tříd.

úterý 10. dubna 2012

MySQL - získání dalšího čísla při autoincrement

Potřeboval jsem získat ID další entity, která se má teprve uložit do databáze. V MySQL existuje na tohle příkaz:
SHOW TABLE STATUS WHERE Name='nazev_tabulky'
Vrátí to jeden řádek, kde sloupec "Auto_increment" nese v sobě hodnotu čísla, která bude přiřazena další entitě. V Groovy je celá metoda nějak takhle (pro tabulku/entitu "invoice"):

public long getNextInvoiceNumber() {
        def sql = new Sql(dataSource)
        def firstRow = sql.firstRow('SHOW TABLE STATUS WHERE Name=\'invoice\'')
        def nextInvoiceNumber = Long.parseLong(firstRow.Auto_increment.toString())
        return nextInvoiceNumber
}

Hosting v cloudu a problém s časy

Dnes jsem se poprvé setkal s problémem různých časových pásem. Používám cloud platformu od CloudFoundry, který má servery nevím kde (asi v cloudu). Moje aplikace (stále ve vývoji) je jednoduchý rezervační systém. Zákazník si může kliknout na datum a zarezervovat si určitý druh procedury.
Když už jsem si myslel, že aplikace je jakž takž hotová, při ručním testování jsem si všiml, že časy jsou vždy posunuté o 2 hodiny.Problém byl v tom, že server má nastavený čas Coordinated Universal Time, což je čas v Greenwich a.k.a. GMT. Já jako testovací zákazník klikám v ČR (GMT +2:00).
Prozatím jsem tento problém vyřešil následně:

1. Rezervace ukládám do databáze v čase GMT (což je čas Prahy mínus 2 hodiny kvůli letnímu času).

TimeZone serverTimeZone = TimeZone.getDefault()
long serverTimezoneOffset = serverTimeZone.getOffset(new Date().time)
reservation.visitTime.setTime(reservation.visitTime.getTime() - serverTimezoneOffset)   // GMT čas

2.  Klient při vytváření rezervace nepotřebuje posílat svůj časový, protože se čas posílá v milisekundách od r. 1970, který bere ohled na časové zóny (což jsem nikdy předtím nevěnoval pozornost). Server tento čas pak jen převede do GMT.

3. Při zobrazení existují rezervace klientovi server musí znát časový posun klienta, aby správně vygenerovalo odpověď. Btw. lze to udělat i na straně klienta, kdy server pošle čas v GMT.
/show?reservationId=" + event.id + "&timezoneOffset=" + new Date().getTimezoneOffset()
Zde je menší problém s jinými čas. formáty v Javascript (JS) a Javou. getTimezoneOffset() v JS (strana klienta) vrací posun v minutách a tento posun je odvozen jako klientovo čas + posun = GMT. V Javě (strana serveru) získáme čas. posun takto: TimeZone.getDefault().getOffset(new Date().time). Skoro stejné, jen Java vrací rozdíl v milisekundách a čas je brán jako server čas - posun = GMT.

4. Když klient chce rezervovat proceduru, musím zkontrolovat, kteří z pracovníků jsou v uvedený čas dostupní. Z toho důvodu chci získat čas, na který klikl klient, na serveru.

Calendar clientTimeCal = new Date(params.long("date")).toCalendar() // převod na původní čas u klienta
int clientTimezoneOffset = -params.int("timezoneOffset") // v minutách
TimeZone serverTimeZone = TimeZone.getDefault()
long serverTimezoneOffset = serverTimeZone.getOffset(new Date().time)
clientTimeCal.setTimeInMillis(clientTimeCal.timeInMillis - serverTimezoneOffset)    // GMT čas
clientTimeCal.add(Calendar.MINUTE, clientTimezoneOffset)  // čas klienta
Klient pošle spolu s časem v ms i jeho časový posun ("timezoneOffset"). Na serveru prvně vytvořím instanci Calendar pomocí času v ms. Tato nová instance je však v času serveru, proto ho potřebuju převést prvně na GMT čas. To provedu pomocí zjištěného časového posunu serveru. Pak stačí jen dosadit do vzorce server čas - posun serveru = GMT. Čas v GMT musím pak převést na čas klienta. Jen tohle je trochu jiný vzorec GMT - posun klienta = klientovo čas. Proto mínus znaménko u -params.int("timezoneOffset") .


pondělí 9. dubna 2012

SQL vyhledávání LIKE pro čísla

Pro svou aplikaci jsem potřeboval, aby fungovala filtrace podle ID entity. Mám v aplikaci faktury, které jsou rozlišené podle ID typu long (což je číslo). Když uživatel chce najít např. fakturu, která obsahuje "1" - tj. faktura č. 1, ale i č. 10 nebo 11, tak pomocí těch předdefinovaných Criteria v Hibernate to nejde. Teda ono jde, ale pomocí kritéria sqlRestriction. Tzn. uživatel si musí pomocí SQL nadefinovat kritérium sám :)

V případě, že mám číslo, ale chci ho vyhledat pomocí LIKE jako String, tak musím prvně změnit jeho typ. Vypadá to nějak takhle:
sqlRestriction "cast( id AS char ) like '%" + id.toString() + "%'"
kde id je ID typu long převedené na String.

Ještě jsem pak četl, že je potřeba dát pozor na SQL Injection, ale nezkoumal jsem to více, protože v mém případě se jedná o interní systém pro pár lidí. Každopádně tento problém by měl být odstraněn ve verzi 2.1 pomocí parametrů.

Zdroj:
http://grails.1312388.n4.nabble.com/LIKE-or-ILIKE-on-Integer-td2997745.html

středa 4. dubna 2012

Více souborů pro internalizaci v Grails

Grails je dobře připraven pro psaní aplikací s podporou více jazyků. V základu se jedná o properties soubory s názvem obsahující příponu s lokálem daného jazyka (cs, en atd.). V dokumentaci však chybí text o tom, jak přidávat další soubor pro ten samý lokál. Např. potřebuji pro přehlednost rozdělit lokalizaci češtiny do souboru  pro základní text a souboru pro texty v admin rozhraní.
Podle dokumentace stačí, aby se soubor obsahoval na začátku slovo "message" a na konci spojení "_kodJazyka". Tím jsem se řídil a přidal jsem soubor "messages_spring_security_cs_CZ.properties", který obsahoval texty pro Spring Security plugin. A ono nic.
Po chvíli googlení jsem našel, že název nesmí obsahovat "_" až na ten konec názvu s kódem jazyka. Tzn. předchozí soubor se musí jmenovat takhle: "messages-spring-security_cs_CZ.properties".

Hola.