Agile Web Development with Rails - w druku

Opublikowane przez Jarosław Zabiełło Wed, 29 Nov 2006 03:45:00 GMT

Drugie wydanie kultowej książki na temat Railsów jest już w druku. Wersja papierowa powinna być gotowa na 15 grudnia. Pierwsze wydanie zostało uznane za najlepszą książką techniczną na świecie. Drugie jest jeszcze ciekawsze, bo opisuje wszystkie nowości jakie mają się pojawić w Railsach 1.2. Właściciele wersji PDF mogą kupić wersję papierową ze zniżką.

Posted in  | Tagi  | brak comments

Ruby on Rails 1.2 RC1

Opublikowane przez Jarosław Zabiełło Sat, 25 Nov 2006 13:52:00 GMT

Nadchodzi wielkimi krokami Ruby on Rails 1.2. Właśnie udostępniono wersję RC1. Wersja finalna powinna pojawić się w najbliższych tygodniach. Wprowadzono szereg istotnych usprawnień w działaniu Railsów powiększając tym samym jeszcze bardziej komfort pracy i dystans w stosunku do pozostałych frameworków.

Np. dodano dodatkowy parametr “format” do metody respond_to. Aby z tego korzystać, należy w routes.rb dodać regułę:

map.connect ':controller/:action/:id.:format'. 

Nie trzeba już więcej bawić się w analizę nagłówków HTTP.

class WeblogController < ActionController::Base
  def index
    @posts = Post.find :all
    respond_to do |format|
      format.html
      format.xml { render :xml => @posts.to_xml }
      format.rss { render :action => "feed.rxml" }
    end
  end
end

GET /weblog       # zwróci HTML
GET /weblog.xml   # zwróci XML
GET /weblog.rss   # zwróci RSS

Inną, istotną zmianą jest dodanie Multibyte. Nie jest to co prawda tak zaawansowane jak w Pythonie, ale do czasu pojawienie się Ruby 2 (który ma mieć pełne wsparcie dla Unicode), Multibyte poprawia trochę sytuację. programisty. Generalnie Ruby jakoś dawał sobie radę z UTF-8, ale w niektórych sytuacjach pojawiały się problemy. Np. metoda size() zwraca ilość bajtów a nie znaków. Multibyte wprowadza metodę proxy dla napisów – chars. Np.

Napisałeś <%= @post.body.chars.length %> znaków.

Jedną z bardziej rewolucyjnych zmian, jest dodanie obsługi REST. (która zakłada, że każdy zasób sieci powinien być jednoznacznie identyfikowany za pomocą samego adresu URL). W praktyce oznacza, to znacznie skrócenie kontrolerów w Railsach po przez wysłanie na ten sam adres różnych komend, tzn,. nie tylko GET i POST ale także PUT i DELETE. Mimo, że te polecenia są cześcią standardu protokołu HTTP, większość przeglądarek implementuje tylko GET I POST. W takim wypadku Rails emuluje działanie tych komend poprzez dodawanie do adresu znaku średnika i polecenia.

Plugin Simply RESTful już działa i można go używać. Np. wygeneruj sobie kod nowego, REST’owego scaffoldingu:

ruby script/generate scaffold_resource

Oczywiście, musisz mieć odpowiednią najnowszą wersję Railsów, którą najlepiej zainstalować za pomocą gemsów:

gem install rails --source http://gems.rubyonrails.org -y

Nie przejmuj się jak wyświetli ci się coś w stylu

Successfully installed rails-1.1.6.5618

To jest tymczasowe oznaczenie dla wersji RC1. Jak wyjdzie wersja finalna to będzie wyświetlać się numer 1.2.

Trwają jeszcze końcowe prace nad Active Resource, modułem obsługującym REST i nowy sposób mapowania.

I ostatnia wiadomość, nadchodzące w grudniu, drugie wydanie książki Agile Web Development in Rails powinno większość tych zmian uwzględniać, bo książka jest pisana z założeniem że używany jest Edge Rails (czyli najnowsza wersja z repozytorium SVN).

Posted in  | Tagi  | 2 comments

Serwery kontynuacyjne - przyszłość webu

Opublikowane przez Jarosław Zabiełło Wed, 22 Nov 2006 01:26:00 GMT

Każdy, kto zajmował się kiedykolwiek programowaniem stron internetowych wie, że protokół HTTP jest bezstanowy. Innymi słowy, każde przeładowanie strony internetowej generuje kompletnie niezależne wywołanie serwera i nie ma on możliwości aby powiązać wywołanie z konkretnym klientem. Aby temu zaradzić, wprowadzono prostą sztuczkę. Wprowadzono tzw. mechanizm cookies (dosł. ciasteczek), malutkich plików które serwer może wysyłać do klienta (tam i z powrotem). Ten unikalny identyfikator (sesji) można też trzymać w adresie URL, ale ta metoda jest zdecydowanie odradzana z powodu łatwości nadużyć (łatwo wykraść taki numer i podszyć się pod autora) Od strony klienta jedynym wymaganiem jest aby nie blokował sobie w przeglądarce mechanizmu cookies.

Serwer znając identyfikator sesji, może związać z nim szereg danych, np. aktualną listę zamawianych produktów włożonych do koszyka w sklepie internetowym. Dane te zwykle trzyma się w bazie lub w pamięci operacyjnej. Oczywiście czas życia danych jest ustalany przez serwet i w normalnym wypadku są one cyklicznie kasowane. (Z tym “normalnym” wypadkiem to różnie bywa, bo wiele frameworków tego nie zapewnia i trzeba samemu o to zadbać)

Mechanizm sesji jest dosyć wydajny, bo zapytania wysyłane do serwera mogą być obsługiwane asynchronicznie, czyli bez żadnych blokad w oczekiwaniu na ukończenie obsługi zapytania. Przez lata “stanowość” protokołu HTTP zapewniało się właśnie za pomocą sesji.

Ten mechanizm ma jednak swoje wady. Największą jest złożoność jaką wprowadza po stronie skryptów na serwerze. (I to nie jest wcale taka trywialna sprawa jak to można było zaobserwować u developerów frameworka CherryPy którzy miesiącami nie potrafili napisać stabilnie działającego mechanizmu obsługi sesji.)

Kontynuacje

Kompletnie innym podejściem jest skorzystanie z tzw. mechanizmu kontynuacji. Mechanizm ten podobny jest do gracza, który w dowolnym momencie może zapisać i wczytać stan gry. Kontynuacje zapewniają taki mechanizm zapisu stanu programu aby w dowolnym momencie później go przywrócić. Niektórzy porównują to do maszyny czasu, bo można w dowolnym momencie przywrócić wcześniejszy stan aplikacji.

Zalet z takiego podejścia jest wiele. Weźmy np. sprawę obsługi “nieśmiertelnego” przycisku Back w przeglądarce. Obsługa tego przycisku sprawia spore problemy. Programiści najchętniej w ogóle by go zablokowali. W wypadku kontynuacji ten problem w ogóle nie istnieje. Kliknął ktoś Back aby cofnąć się do poprzedniej strony? Żaden problem. Serwer przywróci cały wcześniejszy stan aplikacji bez żadnego problemu. I to w sposób przezroczysty dla programisty! Programowanie stron webowych staje się znacznie prostsze, niczym tworzenie aplikacji desktopowej włącznie z debugowaniem krok po kroku. Wg różnych prognoz, tak będziemy programować serwisy internetowe za kilka lat!

Sama idea nie jest najwyraźniej jeszcze powszechnie znana, skoro w rozmowie z developerami irlandzkiego Google, zaskoczyłem ich informacją na ten temat. Kto wie, czy Google mocniej nie zainwestuje w Rubiego (aktualnie mocno korzysta z Pythona, który jest podstawą większości ich skryptów).

Smalltalk i Seaside

Smalltalk jest bardzo starym językiem bo jego początki sięgają lat 70-tych. Od zawsze był językiem w pełni obiektowym i dynamicznym. To m.in. Smalltalk pierwszy wymyślił wirtualną maszynę której to ideę później skopiowali twórcy Javy. W Smalltalku napisano najlepszy (w chwili obecnej) na świecie serwer kontynuacyjny – Seaside.

Czy to spowoduje renesans na Smalltalka? Osobiście w to wątpię. Główną wadą Smalltalka jest jego monolityczność i hermentyczność. Uruchamiając środowisko Smalltalak uruchamia się cały, zamknięty świat Smalltalka. Np. nie ma tam dostępu do plików z zewnątrz tak, jak w innych językach. To spowodowało, że język ten, mimo pewnych sukcesów, nigdy nie zdobył sobie powszechnego uznania. I moim zdanie, nigdy już nie zdobędzie, bo w międzyczasie wyrosła mu młoda, obiecująca konkurencja.

Ruby

Ruby jest językiem relatywnie młodym. Tzn. został po raz pierwszy udostępniony publicznie w tym samym roku co Java – 1995. Ale przez większość lat był mało znany poza Japonią (skąd pochodzi jego twórca). Ruby jest także jednym z tych nielicznych języków, które (podobnie jak Smalltalk) posiadają wbudowaną obsługę kontynuacji. Istnieją co prawda różne próby implementacji tego mechanizmu w Javie1, ale tylko Ruby posiada ten mechanizm jako coś naturalnego i nie trzeba stosować żadnych specjalnych sztuczek aby go emulować. Z tego co pamiętam, Seaside pierwotnie był tworzony w Ruby, ale w tamtym okresie, kontynuacje w Ruby były jeszcze niedojrzałe, więc twórcy przerzucili się na Smalltalka. Ruby jednak szybko się rozwija, nabiera dojrzałości i moim zdaniem, ma bardzo duże szanse, aby wyprzedzić pozostałe języki przy tworzeniu serwisów internetowych nowej generacji, serwisów opartych na kontynuacjach.

Przykład kontynuacji.

Poniższy przykład2 pokazuje implementację generatora za pomocą kontynuacji.

class Generator

  def initialize
    do_generation
  end

  def next
    callcc do |here|
      @main_context = here;
       @generator_context.call
    end
  end

   private

   def do_generation
     callcc do |context|
       @generator_context = context;
       return
     end
     generating_loop
   end

   def generate(value)
     callcc do |context|
       @generator_context = context;
       @main_context.call(value)
     end
   end
end

A oto oparty na nim generator ciągu liczb Fibbonacciego:

class FibGenerator < Generator
  def generating_loop
    generate(1)
    a, b = 1, 1
    loop do
      generate(b)
      a, b = b, a+b
    end
  end
end

fib = FibGenerator.new
15.times { print "#{fib.next} " }
#wynik: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 

1 Np. Riff lub Jetty 6.

2 Przykład z książki “The Ruby Way”, 1ed.

Posted in , ,  | Tagi , , ,  | 23 comments

JavaScript idzie w dobrym kierunku

Opublikowane przez Jarosław Zabiełło Mon, 13 Nov 2006 16:56:00 GMT

Muszę przyznać, że od czasu grzebania w bibliotece Prototype spojrzałem w trochę świeży sposób na język JavaScript. Prototype jest przykładem tego, jak bardzo elegancki, dynamiczny i obiektowy kodu można pisać w tym języku. Pozwala też na pisanie bardzo eleganckiego kodu mocno przypominającego język Ruby

Np. poniższy kod wyświetla cztery razy okienko alertu. (Odpowiednikiem bloku kodu (ang. closure) w Ruby jest tu anonimowa funkcja).

(4).times(function() {
  alert("JavaScript is cool")
});

Co ciekawe, w JavaScript (podobnie jak Ruby) można przeciążać wszystkie obiekty. Np. poniżej do klasy Array dodano metodę inArray.

Array.prototype.inArray = function(needle) {
  for(var key in this) {
    if (this[key] === needle) {
      return true
    }
  }
  return false
}

JavaScript posiada też wbudowany operator do wyrażeń regularnych. Np. poniższy kod sprawdza poprawność pola w formularzu:

<script type="text/javascript">
//<![CDATA[
var MyForm = {
  validateEmail: function() {
    if ($F('email_id').match(/^.+?@([\w\d\.-]+?\.\w{2,5})\s*$/)) {
      $('email_error').innerHTML = '';
      return true;
    } else {    
      $('email_error').innerHTML = 'Wrong email format!';
      return false;
    }
  },
  validateForm: function() {
    return this.validateEmail() ? true: false;
  }
};
//]]>
</script>
<form action="" method="post" onsubmit="return MyForm.validateForm()">
  <table border="0" id="container">
    <tr>
      <th><label for="email_id">Email Adddress:</label></th>
      <td>
        <input type="text" id="email_id" onchange="MyForm.validateEmail()" />
        <span id="email_error"></span>
      </td>
    </tr>
    <tr>
      <td colspan="2"><input id="submit" type="submit" /></td>
    </tr>
  </table>
</form>

Jakby tego było mało, JavaScript w nadchodzącej wersji 1.7 przejmuje trochę wygodnej składni Pythona!

Szkoda, że taki popularny PHP wzoruje się na brzydkiej składni Perla, C++ i Javy. JavaScript wybrał sobie znacznie bardziej eleganckie wzorce.

Tagi ,  | 7 comments

Nginx - Apache killer

Opublikowane przez Jarosław Zabiełło Tue, 07 Nov 2006 23:47:00 GMT

W ostatnim artykule (Railsy: Lighttpd czy Apache 2.2.x?) porównywałem najbardziej popularne serwery HTTP dla Railsów. Zaintrygowany paroma wpisami w blogu postanowiłem przyjrzeć się dosyć mało znanemu serwerowi HTTP który zaczyna zdobywać coraz więcej uwagi na Zachodzie. Chodzi o ultraszybki serwer nginx napisany przez rosyjskiego programistę Igora Sysojewa.

Na pierwszy bój poszedł prosty test wyświetlenia “Hello World!” Na używanym przeze mnie serwerze dedykowanym (Athlon 64 3000+, 1GB RAM, Linux Ubuntu 64bit) dla 100 tys. zapytań (musiałem użyć aż tyle, bo serwer jest za szybki na mniejszą liczbę zapytań) uzyskałem następujące wyniki (dla polecenia “ab -n 100000 -c 1 http://localhost”):

  • Apache 2.2.3 = 4439 req/s
  • Lighttpd 1.4.11 = 7150 req/s
  • Nginx 0.4.12 = 8700 req/s

Co prawda Nginx wykazuje miażdzącą przewagę wydajności nad Apachem ale, z racji tego, że używam Lighttpd, który co prawda odstaje od Nginxa ale nie aż tak, postanowiłem na razie zaczekać z ewentualną migracją.

Okazało się jednak, że będę musiał przeprowadzić taką migrację szybciej niż bym chciał. Coś złego zaczęło się dziać z Ligthttpd. Po paru godzinach pracy, przestawał odpowiadać na zapytania a nawet w ogóle proces znikał z pamięci. Ki diabeł? Trudno powiedzieć, nie mam czasu aby analizować dokładniej problem. Jedyne co pomagało to regularny restart Lighttpd. Trochę głupie rozwiązanie. Postanowiłem zatem zrobić wcześniejszą migrację do Nginxa. Wg statystyk Netcraftu z Nginxa korzysta już ponad 90 tys. domen. Wydaje się to wystarczającą ilością aby można było uznać ten serwer za stabilny.

Jednakże moja migracja ma pewną trudność. Używam bowiem równocześnie PHP, Django, Rails i Zope (ściślej: Plone). Czyli całkiem niezła mieszanka aplikacji. Z PHP i Railsami było najmniej problemów, bo przykłady konfiguracji są z grubsza podane w Wiki.

Z Plone było troszkę gorzej. Musiałem bowiem znaleźć odpowiednik mniej więcej takiego kodu w Apache:

RewriteRule "^/(.*)$" 
"http://88.198.15.160:6001/VirtualHostBase/http/apologetyka.com:80/app/VirtualHostRoot/$1"  [P,L]

To nie jest zwykły rewrite, to jest połączenie proxy z rewrite.

W Lighttpd (też się swego czasu namęczyłem aby to uzyskać) uzyskuje się to tak:

url.rewrite-once = (
  "^/(.*)$" => "/VirtualHostBase/http/apologetyka.com:80/plone/VirtualHostRoot/$1"
)
proxy.server = (
   "/VirtualHostBase/" => (
     ( "host" => "88.198.15.160" , "port" => 6001 ) )
)
Trochę prób i się udało. Nginx potrzebował takiego wpisu:
location / {
  rewrite ^/(.*)$ /VirtualHostBase/http/apologetyka.com:80/plone/VirtualHostRoot/$1;
}
location /VirtualHostBase/ {
  include /opt/nginx/conf/proxy.conf;
  proxy_pass http://88.198.15.160:6001;
}

Najtrudniej było z konfiguracją Django bo opisu do Django na Nginx nie ma ani w dokumentacji do Django, ani w dokumentacji do NGinxa. Zmęczony eksperymentowaniem napisałem na listę dyskusyjną Django i dostałem całkiem pożyteczną wskazówkę odnośnie strony http://www.python.rk.edu.pl/w/p/django-pod-serwerem-nginx/. Niestety miałem pecha, bo akurat trafiłem na zmianę wpisów w DNS i artykuł był niedostępny. Udało mi się na szczęście wyłuskać jego kopię z cache Googli. Autor międzyczasie podesłał mi też pliki z artykułami. Zauważyłem że napotkał pewne problemy ze zmuszeniem Django do wyświetlania statycznej treści. Trochę podłubałem w kodzie i problem rozwiązałem. :)

Zauważyłem że Django wyświetlał mi pliki statyczne w trybie debug. Wynikało to pewnie z tego, że w urls.py stosuję zawsze taki wpis:

...
if DEBUG:
  urlpatterns += patterns('',
    (r'^images/(?P<path>.*)$', 'django.views.static.serve', {'document_root': MEDIA_ROOT+'/images', 'show_indexes': True}),
    (r'^stylesheets/(?P<path>.*)$', 'django.views.static.serve', {'document_root': MEDIA_ROOT+'/stylesheets', 'show_indexes': True}),
    (r'^javascripts/(?P<path>.*)$', 'django.views.static.serve', {'document_root': MEDIA_ROOT+'/javascripts', 'show_indexes': True}),
    )

Dla trybu produkcyjnego (settings.DEBUG=False) trzeba zmusić serwer HTTP aby się zajmował plikami statycznymi. Django ma tylko przetwarzać Pythona. Mozna to zrobić np. tak:

...
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js|mov) {
  access_log   off; # po co mi logi obrazków :)
  expires      30d; 
}
location / {
  include /opt/nginx/conf/fastcgi.conf;
  fastcgi_pass 127.0.0.1:6002;
  fastcgi_pass_header Authorization;
  fastcgi_intercept_errors off;
}

Gdzie plik fastcgi.conf:

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx;

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

fastcgi_param  PATH_INFO          $fastcgi_script_name;

Plik startowy napisałem sobie już w Pythonie:

#!/usr/bin/env python

import os, sys, time
DEBUG = True
# All Django project are inside /home/app/django/

path = '/home/app/django'

projects = {
    'biblia' : {
        'project': 'searchers',
        'port':6002,
        'pidfile': '/var/run/django_searchers.pid',
        'children': 4,
        },
    'koran': {
        'project': 'django_project',
        'port':6003,
        'pidfile': '/var/run/django_koran.pid',
        'children': 4
        },
    }

def start(name):
    project = projects[name]['project']
    os.chdir('%s/%s/' % (path, project))
    appl = './manage.py runfcgi host=127.0.0.1'
    cmd = '%s port=%s minspare=1 maxspare=%s pidfile=%s --settings=%s.settings' \
          % (appl, projects[name]['port'], projects[name]['children'], projects[name]['pidfile'], project)
    if DEBUG: print cmd
    os.system(cmd)

def stop(name):
    pidfile = projects[name]['pidfile']
    if os.path.exists(pidfile):
        cmd = '/bin/kill -TERM %s' % open(pidfile).read()
        if DEBUG: print cmd
        os.system(cmd)
        os.unlink(pidfile)

def restart(name):
    stop(name)
    time.sleep(1)
    start(name)

if __name__ == '__main__':
    try:
        action, project = sys.argv[1], sys.argv[2]
        if action in ['start','stop', 'restart'] and project in projects:
            if action == 'start':
                start(project)
            elif action == 'stop':
                stop(project)
            elif action == 'restart':
                restart(project)
            else:
                raise IndexError
        else:
            raise IndexError
    except IndexError:
        print "Usage: %s {start|stop|restart} {%s}" % (sys.argv[0], '|'.join(projects.keys()))

Migracja się udała. Plone, PHP, Django i Rails śmigają mi teraz na ultraszybkim (i zajmującym mało pamięci!) serwerze Nginx. Acha, zapomniałem dodać: Nginx to nie tylko duża wydajność i oszczędność pamięci. Nginx ma dużo modułów. Może nie tyle, co Apache, ale znacznie lepiej niż Lighttpd.

BTW, ciekawie wygląda także serwer Cherokee. Nginx działa tylko pod systemami POSIX (Unix, MacOS-X, Linux, FreeBSD). Cherokee natomiast posiada… binarną instalację pod Windows! Ale o tym może napiszę coś innym razem. :)

Posted in ,  | Tagi , , , ,  | 16 comments

Społeczność Rubiego rośnie w siłę

Opublikowane przez Jarosław Zabiełło Tue, 07 Nov 2006 23:34:00 GMT

Społeczność Rubiego jest może mniejsza niż Pythona, ale jest jakaś bardziej aktywna i zorganizowana (przynajmniej tak to widać w Polsce). Nie tylko trwają prace nad tłumaczeniem strony domowej Rubiego na język polski, ale także coraz częściej są organizowane spotkania miłośników tego języka.

Najbliższe spotkanie jest planowane na 15 listopada 2006 w Warszawie. Niedługo później, 18 listopada jest organizowane spotkanie w Krakowie.

Od niedawna też oficjalna strona języka Ruby http://ruby-lang.org zgłasza się po polsku (w wypadku przegłądarki ustawionej na jęz. polski). Inni też mogą wejść na stronę polską, która jest bezpośrednio dostępna spod adresu: http://www.ruby-lang.org/pl/.

Co ciekawe, zaczyna się pojawiać coraz więcej ofert pracy dla programistów Rubiego. Oczywiście siłą napędową języka jest wciąż Rails. :)

Posted in  | Tagi ,  | brak comments

RadRails i snippety TextMate

Opublikowane przez Jarosław Zabiełło Tue, 07 Nov 2006 23:13:00 GMT

RadRails, zintegrowany edytor dla Rubiego i Railsów zaczyna coraz bardziej niwelować wcześniejsze zachwyty nad komercyjnym edytorem TextMate. Nie dość, że jest darmowy i działa na Windows, MacOS-X i Linuksie (TextMate tylko na MacOS-X), to na dodatek uzyskał to, co do tej pory było główną atrakcją TextMate – makra przyśpieszające pisanie kodu, czyli tzw. snippety. RadRaila ma już przygotowanych 199 snippetów do Rubiego i 50 do RHTML!

Do tego wystarczy dorzucić kolorowanie kodu a’la TextMate i RadRails ma już wszystko (no może do pełni doskonałości brakuje mu uzupełniania kodu tak, jak to robi komercyjny edytor Komodo)

Posted in ,  | Tagi , , ,  | brak comments

Django Book

Opublikowane przez Jarosław Zabiełło Wed, 01 Nov 2006 09:07:00 GMT

Zgodnie z wcześniejszymi zapowiedziami, autorzy Django postanowili nie tylko napisać książkę na temat swego frameworka, ale także udostępnili za darmo książkę w wersji online . Nowo powstająca książka jest dostępna pod adresem http://www.djangobook.com. Każdy paragraf można komentować. Wersja papierowa ma być wydana w 2007 przez wydawnictwo Apress.

Posted in  | Tagi  | 4 comments