środa, 9 listopada 2011

Hibernate L1 cache a cachowanie zapytań.

Niby wszyscy wiedzą jak działa działa L1 cache w Hibernate. W ramach jednej sesji hibernata, jeśli wczytamy sobie jakiś obiekt za pomocą np. funkcji .get(), to każde kolejne wywołanie tej funkcji nie odpytuje już bezpośrednio bazy danych, tylko zwraca ten obiekt właśnie z sesji. Pytanie, czy jesteśmy świadomi wszystkich konsekwencji takiego podejścia?

Proponuje zagadkę.
W bazie jest 2 użytkowników z tego samego miasta: Andrzej i Wojtek. Metoda getAllUsersFromCity() tworzy odpowiedni obiekt Criteria i zwraca listę użytkowników z danego miasta. Przyjmijmy, że kod funkcji testCache() wykonywany jest w ramach jednej sesji hibernata:

public void testCache(){
    printUsers();
    //w tym momecie ktoś inny edytuje jednego z użytkowników,
    //w zupełnie innej sesji lub bezpośrednio na bazie,
    //zmienia nazwe z Andrzej na Bartek
    printUsers();
}

public void printUsers(){
    List<User> users = dao.getAllUsersFromCity("city");
    foreach(User user : users){
        System.out.println(user.getName());
    }
}

Co zostanie wyświetlone na ekranie?
a)AndrzejWojtekAndrzejWojtek
b)AndrzejWojtekBartekWojtek

środa, 31 sierpnia 2011

Velocity - "is not a valid reference".

Jeśli masz problem z odwołaniem się w Velocity do metody jakiegoś obiektu, np.
$someObject.getSomeField()
i dostajesz komunikat:
... is not a valid reference
To warto sprawdzić czy klasa obiektu someObject jest publiczna, w przeciwnym razie otrzymasz wspomniany wyżej komunikat.

wtorek, 28 czerwca 2011

Logika + intuicja = inteligencja ?

Rzadko mi się to zdarza, ale czasem trzeba usiąść i przewartościować to i owo w swoim życiu. Bez obaw, nie będzie to tekst o uczuciach, związkach i innych takich:) Będzie o sposobie myślenia, przyswajania wiedzy i to niekoniecznie w stricte programistycznym kontekście.

Czy zdarzyło Ci się kiedyś, że poświeciłeś 2/3 dni na jakiś problem, zadanie - bez większych rezultatów, a nagle (czasami nawet kilka dni później), nie wiadomo skąd - oświecenie - Twój mózg dostarcza rozwiązanie, praktycznie na talerzu. Ja miałem tak przy całkach, czasami potrafiłem zmarnować cały dzień na 1 całkę, żeby rano obudzić się i ją po prostu ładnie rozpisać. Jeśli chcesz wiedzieć skąd się to bierze, to koniecznie musisz przeczytać:


Tutył tcorhę nie ceikawy, ale jśeli ztsawnaaisz się delazcgo potarsfiz pzreyczatć ten tkest bez wikęzsyh porblmóew, to opdoeiwdź na m.in. to pyatine zanjzdisez w tej kąsicże.

Co do przewartościowania, to musiałem zmienić swój pogląd dotyczący wszelkich aktywności o naturze bardziej "artystycznej", które uważałem, do tej pory, za nieprzydatne i zbędne (przynajmniej dla mnie). Myślałem, że logika jest głównym silnikiem mojego mózgu. Co nie mija się mocno z prawdą, aczkolwiek każdy z nas ma drugi silnik - zupełnie nielogiczny i bardzo abstrakcyjny. Intuicja, instynkt (tak nazywany jest ten silnik) wydają się nie do końca potrzebne w przypadku np. programowania, ale tak de facto dopiero one pozwalają w pełni rozwinąć skrzydła (właściwie czymkolwiek byśmy się nie zajmowali). To właśnie silnik nr 2 pozwala obudzić się z rozwiązaną całką przed oczyma. Jego możliwości są praktycznie nieskończone jak i bardzo nieprzewidywalne. Ważne jest aby dbać i rozwijać oba silniki. Jak sugeruje autor książki, właśnie aktywności wywodzące się z szeroko rozumianej sztuki, rozwijają ten drugi silnik. Wątpię żebym nagle zaczął pisać wiersze, ale dostrzegam sens np. plastyki (i innych podobnych przedmiotów) w szkole, które (pomijając to, że były moim zdaniem po prostu źle prowadzone) mogą przydać się w ogólnym rozwoju.

Tak samo krytycznie nastawiony byłem do różnego rodzaju "niestandardowych" sposobów nauczania. Nie twierdze, że nagle wszystkie nabierają dla mnie sensu, ale cześć technik, ma swoje naukowe uzasadnienie i muszę przyznać, że chciałbym kilka z nich wypróbować. Mam nadzieję, że wystarczająco zaciekawiłem, aczkolwiek ciężko streścić książkę, która sama w sobie jest streszczeniem wielu prac i badań naukowych.

Dla tych bardziej leniwych, polecam wykład, który w dużej mierze pokrywa się z tą książką:
http://www.infoq.com/presentations/Developing-Expertise-Dave-Thomas

Wyjściowym źródłem informacji dla mnie był m.in. blog:
http://art-of-software.blogspot.com/2011/06/trawienie-confitury.html
http://art-of-software.blogspot.com/2010/01/wspinaczka-do-profesjonalizmu.html

wtorek, 21 czerwca 2011

Partial mock - stosować, czy nie?

Na samym początku chcę zaznaczyć, że prawdopodobnie ten post nie będzie udzielał jednoznacznej odpowiedzi, jego celem (przynajmniej na chwilę obecną) jest zebranie wszystkich za i przeciw stosowaniu częściowego mockowania.

Zakładam, że ewentualnemu czytelnikowi sama idea mockowania jest znana. Partial mock danego obiektu to taka hybryda prawdziwego obiektu i mocka, innymi słowy część metod danego obiektu może być zmockowana, a cześć działać normalnie. Więcej o tym:
Co to daje? Przede wszystkim ułatwia testowanie, czy raczej samo napisanie testu. Wyobraźmy sobie sytuację, w której dana klasa ma jedną publiczną metodę, która korzysta z 20 innych prywatnych, pozagnieżdżanych między sobą. Załóżmy, że te 20 metod tworzy spójną całość i wydzielanie ich do osobnych klas byłoby bezsensowne. Napisanie testu dla takiej publicznej metody jest zajęciem dość karkołomnym. Taki test, który np. sprawdzałby czy metoda 17 robi coś tam poprawnie, zawiera w sobie całą masę mocków i innych obiektów, które umożliwiają taki przepływ w kodzie klasy, aby dojść do tej metody 17 i ją przetestować. Zrozumienie takiego testu dla człowieka, który go nie pisał jest dramatem.

Czy nie lepiej napisać oddzielny test dla metody 17? A potem odpowiednio sprawdzić, czy metoda ta jest wywoływana w innej metodzie, właśnie za pomocą partial mockingu? Jak dla mnie takie podejście jest o wiele przejrzystsze.

Konserwatywni w dziedzinie testowania, na pewno już się gotują, że piszę o testowaniu metod prywatnych, ale powiedzmy, że zmieniam im dostęp na pakietowy:). Tak, czy inaczej na pewno nie pochwalą partial mocków, bo to podejście śmierdzi, jest "niekoszerne".

Ogólnie zgadzam się z nimi w jednej kwestii, na pewno konieczność zastosowanie partial mockingu do testów, jest bardzo wyraźnym sygnałem, że z naszym kodem jest coś nie tak i prawdopodobnie lepszym wyjściem będzie jego refaktoring.

Jednakże istnieją sytuacje (jak chociażby legacy code), gdzie partial mock jest nie dość, że wygodnym, to czytelnym rozwiązaniem.

Oczywiście nie jestem w dziedzinie testów aż takim ekspertem jakbym chciał, dlatego jeśli istnieją inne przesłanki za lub przeciw partial mockom, to proszę o komentarze.

Artykuły o partial mocking:
http://nathan.crause.name/entries/programming/partial-mocking-stubbing-and-why-it-s-not-evil
http://monkeyisland.pl/2009/01/13/subclass-and-override-vs-partial-mocking-vs-refactoring/
http://stackoverflow.com/questions/3806977/partial-mocking-as-code-smell

poniedziałek, 18 kwietnia 2011

Zagadka java Date()

Jaki będzie wynik poniższego kodu:

System.out.println(new Date().getTime() < new Date().getTime());

piątek, 8 kwietnia 2011

Equals, hashCode i Hibernate

Ok, to był dość długi wstęp (1, 2, 3), może czasami jak dla dzieci, ale przynajmniej wszystko jest w 1 miejscu, a składa się na meritum problemu, czyli jak powinno się implementować metody equals() i hashCode() w połączeniu z Hibernatem. Mówiąc jeszcze bardziej precyzyjnie, czy możną używać pola reprezentującego klucz główny tabeli w tych metodach.


Najpierw trochę literatury:
http://community.jboss.org/wiki/EqualsandHashCode

https://forum.hibernate.org/viewtopic.php?t=928172

http://onjava.com/pub/a/onjava/2006/09/13/dont-let-hibernate-steal-your-identity.html?page=2

Wnioski z tego, z czym się do tej pory zapoznałem, są takie, że Hibernate mówi: nie używajcie ID, ale jak już musicie, to przynajmniej upewnijcie się, że obiekty (które będą porównywane) są w tej samej sesji! Dlatego proponuję otwartą dyskusję na ten temat. Obecnie przyjąłem sobie następujący schemat pisania equals() i hashCode(). W equals() biorę pod uwagę tylko id jeśli oba obiekty mają ustawione id (w moim przypadku różne od 0). W przeciwnym wypadku porównuję odpowiednie pola. W hashCode() nie wykorzystuje id, ponieważ może dojść do sytuacji, w której 2 obiekty takie same, będą miały różne hashe.
    @Override
    public boolean equals(Object object) {
        
        if (this == object) return true;
        
        if ( !(object instanceof Entity) ) return false;
        
        Entity entity = (Entity) object;
        
        if (id != 0 && entity.getId()!=0){            
            return id == entity.getId();
        }
        return new EqualsBuilder().append(field, entity.getField())
            ...//i inne pola
            .isEquals();
   }

poniedziałek, 28 marca 2011

Testowanie equals i hashCode

Zgodnie z obecną modą na wszechobecne testy jednostkowe wypadałoby jakoś testować nasze implementacje equals() i hashCode(), żeby się przy tym zbytnio nie narobić proponuję następujące rozwiązanie: klasa abstrakcycja, która sprawdza podstawowe warunki konktraktu dla equals() i hashCode() na podstawie 3 dostarczonych przez klasę dziedziczącą obiektów, które powinnym być identyczne wg. equals (ale maksymalnie od siebie różne) oraz jednego, który jest różny od pozastałych wg. equals. Pełny kod źródłowy wraz z klasą abstrakcyjną można pobrać korzystając z tego linku. Zastosowanie najlepiej widać na przykładzie:

Klasa testowana:
public class Entity {
    
    private int field1;
    private int field2;
    private int notSignificantField;
    
    @Override
    public int hashCode() {
         ...
    }
    
    @Override  
    public boolean equals(Object object) {  
        ...
    }

    public Entity(int field1, int field2, int notSignificantField) {
        this.field1 = field1;
        this.field2 = field2;
        this.notSignificantField = notSignificantField;
    }
    ...
} 

Klasa testująca:
public class EntityTest extends AbstractEqualsTest<Entity> {

    @Override
    public Entity objectThatShouldBeEqualA() {
        return new Entity(1, 2, 33);
    }

    @Override
    public Entity objectThatShouldBeEqualB() {
        return new Entity(1, 2, 44);
    }

    @Override
    public Entity objectThatShouldBeEqualC() {
        return new Entity(1, 2, 55);
    }

    @Override
    public Entity objectThatShouldNotBeEqual() {
        return new Entity(1, 1, 33);
    }
}

czwartek, 3 marca 2011

Metoda hashCode w javie

Nudów ciąg dalszy, czyli kilka słów o metodzie hashCode(), która powinna być zawsze nadpisana, jeśli nadpisaliśmy metodą equals().

1. Kontrakt dla metody hashCode().

Metoda musi spełniać następujące warunki:
  • W czasie działania aplikacji metoda zawsze zwraca tą samą wartość int, dla tego samego obiektu.
  • Jeśli dwa obiekty równe wg. metody equals() to wartości zwracane przez hashCode() dla tych obiektów muszą być takie same.
  • Jeśli dwa obiekty nie są równe wg. metody equals() to wartości zwracane przez hashCode() dla tych obiektów nie muszą być takie same. Aczkolwiek zaleca się taką implementację hashCode(), żeby dla różnych obiektów zwracała różne wartości hash, co wpływa pozytywnie na szybkość operacji na hashowalnych kolekcjach.
Chyba największą trudnością przy implementacji hashCode jest wybór odpowiednich pól, na bazie których, będzie liczony hash. Jak ktoś lubi może stosować różne kombinację podczas obliczania hash'a (cała masa tego w necie), ale chyba lepiej i wygodniej skorzystać z gotowych rozwiązań (Commons, Guava):
@Override
    public int hashCode() {
        
        return new HashCodeBuilder().append(someField)
                                    .append(someField2)
                                    .toHashCode();
    }

środa, 2 marca 2011

Zagadka

Jak ten młody leszcz dałem się ostatnio nabrać na TreeSet w Javie. Oto zagadka:
chciałem sobie posortować liście na podstawie ich koloru, użyłem TreeSet'a, jaki będzie wynik funkcji shouldTestTreeSet(), jaka będzie kolejność liści?

class Leaf{
   
    int color;
    int size;
   
    public Leaf(int color, int size) {
        this.color = color;
        this.size = size;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (!(obj instanceof Leaf)) return false;
        Leaf leaf = (Leaf) obj;
        return new EqualsBuilder().append(this.color, leaf.color)
            .append(this.size, leaf.size).isEquals();
    }
   
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(color).append(size).toHashCode();
    }
   
    @Override
    public String toString() {

        return String.valueOf(color + " " +size);
    }
}

class LeafComparator implements Comparator<Leaf>{

    @Override
    public int compare(Leaf o1, Leaf o2) {
       
        return Ints.compare(o1.color, o2.color);
    }
}

public class TreeSetTest {

    public void shouldTestTreeSet() {

        Leaf leaf = new Leaf(1,1);
        Leaf leaf2 = new Leaf(1,2);
        Leaf leaf3 = new Leaf(1,3);

        Set<Leaf> tree = new TreeSet<Leaf>(new LeafComparator());
       
        tree.add(leaf3);
        tree.add(leaf);
        tree.add(leaf2);

        System.out.println(tree);
    }
}

czwartek, 24 lutego 2011

Metody equlas i hashCode w Javie.

Niestety, aby dojść do meritum problemu, muszę część rzeczy powielić po raz kolejny, żeby nie szukać tego po innych stronach. Dopiero kolejny post będzie dotyczył konkretnie problemu equals i hashCode w kontekście użycia z Hibernatem.

Czyli, na początek, standardowo - jak powinno się zaimplementować metodę equals i hashCode.

1. Sygnatura metody.

Niby wszyscy wiedzą, że metoda eguals powinna zawsze wyglądać tak:
public boolean equals(Object other)
ale warto wspomnieć dlaczego np. nie można zastosować (dla obiektu Entity) czegoś takiego:
public boolean equals(Entity entity)
problemy pojawiają się, np. przy użyciu kolekcji:
Entity entity = new Entity(1);
Entity entity2 = new Entity(2);
System.out.println(entity.equals(entity2)); //zwraca true

Set<Entity> encje = new HashSet<Entity>();
encje.add(entity);
System.out.println(encje.contains(entity2)); //zwraca false
ostatnia linijka zwraca false, ponieważ źle nadpisaliśmy metodę equals() z klasy Object, która jako parametr przyjmuje Object. Dlatego nasza metoda to jedynie przeładowanie metody equals, a nie jej nadpisanie.

Reasumując, prawidłowa metoda equals ma sygnaturę:
@Override
public boolean equals(Object other)
i koniec kropka!!!!! Strażnikiem poprawności sygnatury jest adnotacja oczywiście @Override, jeśli kompilator nie warczy, że jest źle użyta, to z naszą sygnaturą jest wszystko OK (o ile nie dziedziczymy z jakiejś innej klasy...).


2. Kontrakt dla metody equals(). 

Metoda musi spełniać następujące warunki:
  • zwrotność, czyli x.equals(x) == true
  • symetryczność, czyli x.equals(y) == true, wtedy i tylko wtedy gdy y.equals(x) == true
  • przechodniość, czyli jeśli x.equals(y)==true i y.equals(z)==true, wtedy x.equals(z)==true
  • konsekwentność, czyli wielokrotne powtórzenie x.equals(y) zwraca zawsze tą wartość dla niezmienionych obiektów x i y.
  • dla wartości null (np. x.equals(null)) metoda zawsze powinna zwracać false
Największą trudność w implementacji sprawia punkt 3., dlatego warto się mu przyjrzeć dokładniej. Rozważmy następujące klasy:
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
    //...
}
public class ColorPoint extends Point {
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    } 
//łamie symetryczność
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    }
}  

Na pierwszy rzut oka wszystko jest ok, aczkolwiek złamany jest punkt drugi kontraktu, tj. symetryczność:
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

p.equals(cp); //true
cp.equals(p); //false
możemy oczywiście poprawić metodę tak aby symetryczność została zachowana:
    //łamie przechodniość 
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        // If o is a normal Point, do a color-blind comparison
        if (!(o instanceof ColorPoint))
            return o.equals(this);
        // o is a ColorPoint; do a full comparison
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    } 
niestety, w tym przypadku łamana jest przechodniość:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); 
Point p2 = new Point(1, 2); 
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1.equals(p2) //true
p2.equals(p3) //true
p1.equals(p3) //false 

Niestety powyższy problem nie ma jednego dobrego rozwiązania. Taka jest natura obiektowo zorientowanego programowania. Mało tego nawet niektóre obiekty języka Java mają ten problem, np equals() z klasy Timestamp (która jest podklasą Date) łamie symetryczność.

Można to obejść np. zamiast dziedziczenia Point dodać prywatne pole typu Point do klasy ColorPoint, wtedy:
class ColorPoint {
    private Point point;
    private Color color;

    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}
3. Przykład jak zaimplementować equals().
@Override
public boolean equals(Object object) {
    
    if (this == object) return true; //załatwia punkt 1. kontraktu
    
    if ( !(object instanceof Entity) ) return false; //dodatkowo załatwia punkt 5 kontraktu
    
    Entity entity = (Entity) object;

    //dla porównania konkretnych warto posłużyć się już gotowym builderem z biblioteki commons
    return new EqualsBuilder().append(field1, entity.getField1())
            .append(field2, entity.getField2())
            .isEquals();
}

ważnym jest aby dla buildera wybrać tylko te pola, mają znaczenie przy porównaniu!

4. Jeśli nadpisujesz equals(), to zawsze nadpisuj hashCode()

ale o tym w następnym poście.

czwartek, 17 lutego 2011

Internacjonalizcja w CakePHP

Niestety świat się nie kręci wokół javy i czasem trzeba wrócić do starego poczciwego php'a. Szukałem dość długo jak powinna wyglądać poprawna (w kontekście kodu jak i SEO) internacjonalizacja w CakePHP i generalnie w każdym tutorialu czegoś brakowało, coś nie do końca działało, dlatego postarałem się jakoś zebrać tą wiedzę i usystematyzować. Żeby zrozumieć co się dzieje w tutku, trzeba mieć podstawową wiedzę z Caka (wersja 1.2.9), oraz przeczytać rozdział o i18n i l10n z manuala.

1. Na początek radze zapoznać się ze sposobami internacjonalizacji aplikacji, które są dość dobrze opisane tu. Niestety nie stać mnie na różne domeny dla różnych języków (podejście (1)), dlatego wybrałem opcje (2), czyli zróżnicowanie linków pod kątem języków. Generalnie jako piaskownice i poligon doświadczalny postanowiłem zinternacjonalizować swoją bidną stronę domową. Plus będzie tego taki, że wszystkie przykłady będą miały pokrycie na działającym przykładzie.

2. Przyjmuję następującą strukturę linków:

http://www.ludwikowski.info/ - ładuje domyślną stronę w języku polskim

http://www.ludwikowski.info/pol - również ładuje domyślną stronę w języku polskim
http://www.ludwikowski.info/eng - ładuje domyślną stronę w języku angielskim

http://www.ludwikowski.info/pol/pages/display/projects - ładuje wybraną stronę w język polskim
http://www.ludwikowski.info/eng/pages/display/projects - ładuje wybraną stronę w języku angielskim

i teraz pytanie z dziedziny SEO, co z linkami typu:
http://www.ludwikowski.info/pages/display/projects - niby można to obsługiwać domyślnie po polsku, ale nie wiem czy jest sens, dlatego postanowiłem po pierwsze nie generować takich linków, po drugie ich nie obsługiwać. Aczkolwiek, jeśli ktoś ma na ten temat inne zdanie to zapraszam do dyskusji, generalnie zasady SEO są dla mnie czasami dość rozmyte.

3. Wszędzie tam gdzie treść ma być poddana internacjonalizacji używamy funkcji __().

4. Potrzebujemy plików .po, w których będą przechowywane przetłumaczone frazy. Możemy je stworzyć ręcznie, albo użyć jednego z poleceń cakekowych z poziomu konsoli, a konkretnie: cake i18n extract. Polecenie to generuje nam plik .pot będący szablonem na podstawie którego tworzymy już konkretne pliki .po. Czyli w moim przypadku, fragmenty plików .po wyglądają następująco.

app/locale/eng/LC_MESSAGES/default.po

#: \views\elements\rightMenu.ctp:5
msgid "Mój blog"
msgstr "My blog"

#: \views\elements\topMenu.ctp:3
msgid "O mnie"
msgstr "About me"

app/locale/pol/LC_MESSAGES/default.po
#: \views\elements\rightMenu.ctp:5
msgid "Mój blog"
msgstr "Mój blog"

#: \views\elements\topMenu.ctp:3
msgid "O mnie"
msgstr "O mnie"
jeśli zasada tworzenie plików .po nie jest nam znana odsyłam do literatury w necie.

5. Teraz trzeba dostosować aplikację, żeby w miarę wygodnie można było tworzyć linki oraz ich używać. Po pierwsze zasady routingu. W app/config/routes.php umieszczamy (koniecznie przed standardowymi definicjami routtingu!):
//mapowanie na stronę główną po angielsku
Router::connect('/eng', array('language' =>'eng', 'controller' => 'pages', 'action' => 'display', 'home'));
 
//mapowanie na stronę główną po polsku
Router::connect('/pol', array('language' =>'pol', 'controller' => 'pages', 'action' => 'display', 'home'));
 
//pobiera language z np. array('controller'=> 'contacts', 'action' => 'sendEmail', 'language' => 'pol') i umiesza go na początku url'a
Router::connect('/:language/:controller/:action/*', array(), array('language' => '[a-z]{3}'));
Jak dokładnie działa routowanie w cake'u odsyłam do manuala.

6. Należy jakoś zautomatyzować tworzenie linków, żebyśmy nie musieli za każdym razem ręcznie odczytywać (czy to z sesji, czy to z cookie) jaki jest wybrany język i umieszczać go w linku. Dlatego tworzymy app/app_handler i nadpisujemy metodę tworzącą url'e:
class AppHelper extends Helper {
    
    function url($url = null, $full = false) {
        
        if(!isset($url['language'])){
            //sprawdzamy czy jest w coookie
            if (isset($_COOKIE['lang'])){                
                $language=$_COOKIE['lang'];
            }
            //sprawdzamy czy jest w sesji
            if (isset($_SESSION['Config']['language'])){                
                $language=$_SESSION['Config']['language'];        
            }else {
                //jeśli nie ma to domyślnie ustawiamy język polski
                $language='pol';
            }            
            //dodajemy do urla język
            $url['language'] = $language;
        }
        return parent::url($url, $full);
    }
}
Dzięki takiemu zabiegowi możemy tworzyć linki dokładnie tak samo jak to było do tej pory:
echo $html->link(__('Kontakt', true), array('controller'=>'pages', 'action'=>'display', 'contact'));
Język w postaci parametru 'language', będzie dodawany automatycznie.

7. Przechodzimy do dostosowania kontrolera, który będzie przełączał język (na podstawie url'a) jeśli różni się on od obecnie ustawionego. Tworzymy w tym celu odpowiedni komponent app/controllers/components/language.php:
class LanguageComponent extends Object {
    var $name = 'Language';    
    var $components = array('Cookie', 'Session');
    
    //pobiera język z sesji, lub z cookie, lub domyślny = 'pol' jeśli nie był do tej pory ustawiony język 
    function getLanguage(){    
        if ($this->Session->read('Config.language')){
            return $this->Session->read('Config.language');
        }else if ($this->Cookie->check('lang')){
            return $this->Cookie->read('lang');
        }else {
            return 'pol';
        }
    }
    
    // ustawia wybrany język, dodatkowo inicjalizuje parametr w sesji jeśli wygasła 
    function setLanguage($language  = 'pol'){        
        if ($this->Cookie->read('lang') && !$this->Session->check('Config.language')) {
            
            $this->Session->write('Config.language', $this->Cookie->read('lang'));
        }
        else if ($language !=  $this->Session->read('Config.language')) {
            $this->Session->write('Config.language', $language);
            $this->Cookie->write('lang', $language, false, '20 days');
        }
    }
    
    //zmienia $url np.  /eng/controller/action/... na /$lang/controller/action/... 
    function prepareUrl($url, $lang){
        return "/".$lang.substr($url, 4, strlen($url));
    }
}
w app/app_controller.php:
class AppController extends Controller {
    var $components = array('Session', 'Cookie', 'Visit', 'Language');

    function beforeFilter() {        
        $this->set('visitors', $this->Visit->visitCookieUpdate());
        $this->Language->setLanguage($this->_getLanguageFromParams());
    }
    
    function _getLanguageFromParams(){        
        if (isset($this->params['language'])){
            return $this->params['language'];
        }else{
            return 'pol';
        }
    }
}

8. Ok, ogólny kontroler załatwiony, przydałby się jeszcze jakiś, który będzie wymuszał zmianę języka i przenosił na aktualnie oglądaną stronę app/controllers/languages_controller.php:
class LanguagesController extends AppController{    
    var $uses = array();    
    var $components = array('Language');
    
    function change(){        
        $this->redirect($this->Language->prepareUrl($this->referer(), $this->Language->getLanguage()));
    }
}
kontroler jest tak prosty, ponieważ wszystko robi za nas app_controller, język wymuszamy poprzez odpowiedni link. Tak de facto do zadań tego kontrolera należy jedynie przekierowanie na tą samą stronę. Możliwe, że można to jakoś sprawniej załatwić, ale nic mi nie przyszło do głowy, jak ktoś ma pomysł to pisać.
echo $html->link('Polski', array('language' => 'pol', 'controller' => 'languages', 'action' => 'change');

9. Praktycznie to by było na tyle, aczkolwiek żeby mieć już pełny pogląd na sprawy internacjonalizacji, pozostaje ostatni aspekt. Funckja __(), powinna być jedynie zastosowana do krótkich wiadomości. W przypadku potrzeby przetłumaczenia np. całej zawartości paragrafu, należy użyć techniki podmiany widoków, która została opisana w oficjalnym manualu. Niestety pages_controller rządzi się trochę swoimi prawami i oddzielnie dla niego trzeba dorobić jedną rzecz. Ogólnie zastanawiam się, czy nie szybciej byłoby napisać swój własny pages_controller. Moje rozwiązania, które 'łata' pages_controller nie uważam, za zbyt finezyjne, dlatego jak ktoś ma inny pomysł jak to zrobić to z chęcią przeczytam propozycje.

Dorabiamy kolejny komponent pages_i18n:
class PagesI18nComponent extends Object {    
    var $name = 'PagesI18n';    
    var $components = array('Language');
    
    function viewName($viewName){        
        if (file_exists(VIEWS.'pages'.DS.$this->Language->getLanguage().DS.$viewName.'.ctp')){
            
            return $this->Language->getLanguage().DS.$viewName;
        }else{
            return $viewName;
        }
    }
}
w pages_controller wywołujemy podmianę nazwy widoku:
        if (isset($path[0])){            
            $path[0] = $this->PagesI18n->viewName($path[0]);
        }        
        
        $this->set(compact('page', 'subpage', 'title'));
        $this->render(join('/', $path));
a widoki umieszczamy odpowiednio: views/pages/pol/home.ctp, views/pages/eng/home.ctp.

środa, 26 stycznia 2011

Jak zamenić " na \" w java za pomocą String.replaceAll()

Z kategorii tak dziwne, że aż śmieszne. Otóż zamiana " w String'u w Javie na \" może przysporzyć pewnych trudności.

Używając oferowanej przez String'a funckji: replaceAll(), zapisujemy:

String s = "Adam Mickewicz \"Pan Tadeusz\" ";
s = s.replaceAll("\"", "\\\"");

efekt - żaden. Taka operacja nic z naszym String'iem nie zrobi. Należy pamiętać, że pierwszy argument funkcji replaceAll, to wyrażenie regularne. Dlatego znak '\', musi być dodatkowo wyeskejpowany dla regexp'a, czyli dopiero:

s = s.replaceAll("\\\"", "\\\\\"");

robi to o co nam chodziło. Zapis iście komiczny, ale co zrobisz...

piątek, 21 stycznia 2011

Zamiana wierszy na kolumny w tabeli - crosstab (postgresql).

Jakiś czas temu musiałem, zrobić dość nieprzyjemną operacje na tabeli, tzn. zamienić wiersze tak aby tworzyły one kolumny. Czyli coś takiego:
 x f01 a

 x f02 b

 x f03 c
 y f01 a
 y f02 b

 z f01 a

 z f02 b
 z f03 c

 z f04 d
 z f05 e

miało zamienić się w coś takiego:

 id f01 f02 f03 f04 f05
 x a b c
 y a b
 z a b c d e

Generalnie rzecz niby prosta, ale dość nienaturalna dla tabel w bazce . W realnym przykładzie miałem tak de facto 20 kolumn, co wiązało by się z wykonaniem 20 JOIN-ów, na tabelach, które posiadać będą olbrzymią ilość danych.

W postgresie na szczęście znalazłem funkcję: crosstab, która umożliwia szybkie (?) wykonanie zapytania krzyżowego (typu PIVOT) i pięknie realizuje to, o co mi chodzi.

Zastanawiam się tylko jak jest ona zaimplementowana, i jaka jest jej wydajność, czy poradzi sobie z zakładaną dość dużą ilością danych? Ktoś coś wie na ten temat?