úterý 10. dubna 2012

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.

sobota 31. března 2012

Bootstrap.groovy a velké množství počátečních dat

Dnes se mi podařilo vyřešit problém, který jsem ani nedoufal, že se dá vyřešit. Problém spočíval v tom, že v bootstrap souboru v Grailsu zkouším vytvářet velké množství testovacích dat - celý soubor má cca 1000 řádků. Jenže při spuštění Grails hlásil divnou chybu:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000000262b036, pid=4152, tid=8016
#
# JRE version: 6.0_29-b11
# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.4-b02 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# j  BootStrap$_closure1.doCall(Ljava/lang/Object;)Ljava/lang/Object;+87
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#

---------------  T H R E A D  ---------------

Current thread (0x000000000d985800):  JavaThread "pool-7-thread-1" [_thread_in_Java, id=8016, stack(0x000000000bcf0000,0x000000000bdf0000)]

siginfo: ExceptionCode=0xc0000005, reading address 0x0000000138c797cf

Registers:
RAX=0x000000000bdedd78, RBX=0x000000000bdedd78, RCX=0x00000000d9d1e488, RDX=0x00000000d9d0abff
RSP=0x000000000bdedd88, RBP=0x000000000bdeddb8, RSI=0x000000000d985800, RDI=0x000000000ee83ed0
R8 =0x0000000000000004, R9 =0x00000000d4b4e890, R10=0x000000006dfc4f80, R11=0x000000000ee83f38
R12=0x0000000000000000, R13=0x00000000d9d1d8ce, R14=0x000000000bdee5b0, R15=0x000000000d985800
RIP=0x000000000262b036, EFLAGS=0x0000000000010287

Když jsem ten dlouhej soubor okomentoval zhruba tak 3/4, tak vše jelo. Prvně jsem si myslel, že je to chyba v nedostatku paměti JVM. Zkoušel jsem tedy zvýšit paměť ze 128MB na 512 MB. Chyba však stále byla. Druhé, co mě pak napadlo, byla ne nejnovější verze JVM. Měl jsem 1.6-něco-29 a nejnovější bylo asi 1.6.-něco-31. Updatoval jsem, ale chyba tu furt byla. Už jsem to docela vzdal a smířil jsem se s tím, že prostě budu mít míň testovacích dat, stejně who cares.

Včera jsem však kouknul zpátky na StackOverflow, kam jsem položil otázku na tento problém. Nějakej týpek napsal, ať zkusím rozdělit soubor do více transakcí. Za chvíli jsem našel konstrukci, která to provádí:


Patient.withTransaction {

// vytváření a ukládání doménových tříd…

}


Najednou chyba už nebyla Veselý obličej Jen pak byl problém s tím, že se ukládaly jen 50 entit. Pak jsem zjistil, že se tedy musí dávat .save(flush:true) na konec té transakce, aby se uložila. Také je nutné, aby na sebe odkazující entity byly v jedné transakci. A to je celé Veselý obličej

čtvrtek 29. března 2012

Full Calendar a refresh celého kalendáře

Začal jsem využívat FullCalendar pro vytváření jednoho rezervačního systému. Musím pochválit tento kalendář, je super jednoduchý a nabízí hodně možností customizace. Co mi chvíli trvalo, než jsem našel odpověď, bylo jak ajaxově updatovat kalendář po vytvoření události.
Po chvíli googlení jsem našel nějakou otázku na StackOverflow.com (ale teď ji nemohu najít znova, takže nic). Každopádně bylo tam, jak updatovat kalendář:

$('#calendar').fullCalendar('refetchEvents');
$('#calendar').fullCalendar('rerenderEvents');

$('#calendar') odkazuje na div, kde byl již předtím vytvořen kalendář. Nikam se nemusí ukládat do proměnné odkaz na ten vytvořenej kalendář. Takže super :)

sobota 24. března 2012

Našeptávač pomocí jQuery a Grails

Jak udělat podobnou funkcionalitu našeptávače, jako má Google při vyhledávání, že vám zobrazí ty podobné výrazy? Zrovna včera jsem něco takového udělal.
Šlo o to, že při zadávání část rodného čísla server automaticky vyhledá ty RČ, která obsahují zadávaná čísla a vloží je za uživatele do textboxu. Pro zhotovení jsem použil jQuery UI Autocomplete: http://jqueryui.com/demos/autocomplete/ a serverová část je naprogramovaná v Grails 2.0.1.

Serverová část:
Tato část zahrnuje přijímání parametru z requestu, vyhledávání dle RČ a generování JSON.

První je potřeba udělat vyhledávání pacienta (aplikace je pro zdravotnictví) podle RČ:

List patientList = Patient.createCriteria().list() {
ilike('RC', "%${params.RC}%")
}

Protože je RC uložena jako text, lze vyhledávat pomocí LIKE v SQL.

Tento výsledek pak připravíme pro generování JSON:

def resultList = [] // výsledný list
patientList.each {patient ->
def patientMap = [:] // add to map. jQuery autocomplete expects the JSON object to be with id/label/value.
patientMap.put("id", patient.id)
patientMap.put("label", patient.fullName) // to, co se zobrazí v našeptávači
patientMap.put("value", patient.RC) // to, co se vyplní do vyhledávaného pole
patientMap.put("rc", patient.RC)
patientMap.put("firstName", patient.firstName)
patientMap.put("lastName", patient.lastName)
patientMap.put("insuranceCompany.acronym", patient?.insuranceCompany?.acronym)
resultList.add(patientMap) // add to the arraylist
}
render patientList as JSON // generování JSON v Grails

Jak vidíme, lze zobrazit uživateli něco jiného, než co se vloží pak po vyhledání - zde uživateli zobrazím při zadání části RČ možného pacienta se stejným RČ, ale vložím zase RČ do pole pro RČ.

Klientská část:
Prvně je nutné získat RČ z textboxu - pomocí jQuery:

var rc = $('#patient\\.RC').val(); // za předpokladu, že id textboxu je "patient.RC"

Pak je potřeba vytvořit autocomplete plugin:

$("#patient\\.RC").autocomplete({
delay:500,
minLength:3,
source:contextPath + "/patient/autocompletePatient?rc=" + rc,
select:function (event, ui) { // event handler when user selects a company from the list.
$("#patient\\.id").val(ui.item.id);
$("#patient\\.RC").val(ui.item.rc);
$("#patient\\.firstName").val(ui.item.firstName);
$("#patient\\.lastName").val(ui.item.lastName);
$("#patient\\.insuranceCompany\\.acronym").val(ui.item["insuranceCompany.acronym"]);
}
});

Parametr delay je nastaven na 500 ms, díky tomu server nebude přetěžován dotazy - posílá se až za 500 ms od té doby, kdy klient přestal psát. Minimální délka omezuje vyhledávání až do té chvíle, kdy jsou zadány min. 3 znaky - nevrací se tolik výsledků. Source určuje, kde lze získat zdroj pro zobrazování našeptávač - zde je zdroj vzdálený. Select je event listener, který říká, co se má provést, pokud uživatel klikne na jeden z našeptávaných výrazů - chci, aby se nejen doplnil zbytek RČ, ale také jméno, příjmení a zkratku pojišťovny u daného pacienta.
Pak je nutné, aby se source aktualizoval pokaždé, kdy uživatel něco napíše - lze použít keyup event v jQuery. Celý kód vypadá takto:

$("#patient\\.RC").keyup(function () {
var rc = $('#patient\\.RC').val();
$("#patient\\.RC").autocomplete({
delay:500,
minLength:3,
source:contextPath + "/patient/autocompletePatient?rc=" + rc,
select:function (event, ui) { // event handler when user selects a company from the list.
$("#patient\\.id").val(ui.item.id);
$("#patient\\.RC").val(ui.item.rc);
$("#patient\\.firstName").val(ui.item.firstName);
$("#patient\\.lastName").val(ui.item.lastName); $("#patient\\.insuranceCompany\\.acronym").val(ui.item["insuranceCompany.acronym"]);
}
});
});

Zdroje inspirace:

neděle 18. března 2012

Některé constraint v Grails

Dnes jsem při definování doménové třídy pro novou aplikaci narazil na problém, kdy jsem nevěděl, jaký je rozdíl mezi constrainty size a range. Tušil jsem, že jeden nejspíš bude pro String a druhý pro čísla. Po chvíli prozkoumání zdrojáku v Grails jsem narazil na toto:

třída RangeConstraint metoda processValidate():
....
if (from instanceof Number && propertyValue instanceof Number) {
....

třída SizeConstraint metoda processValidate():
....
if (propertyValue.getClass().isArray()) {...
else if (propertyValue instanceof Collection) {...
else if (propertyValue instanceof String) {...
....
Tedy Size je pro String a Range pro čísla. Také jsem pak narazil na tuto část v dokumentaci: http://grails.org/doc/latest/guide/single.html#gormConstraints.