Parser logów w Groovy

Podczas debugowania wadliwego działania redeliveringu w ActiveMQ ostateczną metodą okazało się ‘echo dupa’ ;) Jednakże przy próbie jednoczesnego obserwowania działania aplikacji przy 10 wiadomościach wpadających do kolejki okazało się, że metoda kartki i papieru zabiera trochę za dużo czasu. Zatem zaprzęgłem do pracy Grooviego i postanowiłem na szybko napisać mały parserek plików z logami.

Główne pomysły i kod podkradłem z tej strony. Jednakże ja do logów zapisywałem informację o tym, która to próba ponownego doręczenia się wykonuje, ilość łącznych, informację o wątku, który to wykonuje oraz informacji z samej wiadomości w kolejce JMS.  To oczywiście było dopisywane do wiersza ze wskazaniem informacji o wątku i daty. Całość wyglądała mniej więcej tak:

[#|2012-02-17T11:16:02.419+0100|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=80;_ThreadName=testowaKolejka-1;|CURRENT:4 MAX:4 id:identyfikator1-1-1-1|#]

I z tego narodził się poniższy kod. Być może komuś się przyda w przyszłości.

/* Mozna uzyc podejscia, ktore odczytuje plik linia po linii. Jednakze
 istnieje szansa, ze 1 wpis do loga zajmie wiecej niz domyslna 1 linie.
 Dlatego tez uzywamy podejscia, ktore wyciagnie plik do 1 lancucha tekstowego
 nastepnie zas kolejne przyporzadkowania do wzorca wyrazen regularnych.

 !!!UWAGA!!! - rozwiazanie moze byc malo wydajne przy duzych plikach

  */

def logLineStart = /^\[\#\|\d{4}-\d{2}-\d{2}/   // Wpis w logu o wyjatku
def log = new File('C:\\log.log').text
def informationEntries = []
def presentationData = [:]

def splitter = log =~ """(?xms)
    (    ${logLineStart}   .*?)
    (?=  ${logLineStart} | \\Z)
"""

splitter.each { matched, entry ->

    // Replace jest po to by otrzymac jeden lancuch tekstowy w przypadku dopasowan ze znakiem
    // nowej linii. Wowczas dane nie bylyby poprawne.
    if (entry =~ /CURRENT:/) {
         informationEntries.add( entry.replaceAll("[\r\n]","") )
    }

}

def redeliveryAttemptData = []

informationEntries.each {
    // Wpisy na liscie wygladaja mniej wiecej tak
    // [#|2012-02-17T15:49:50.525+0100|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=134;_ThreadName=testowaKolejka-1;|CURRENT:4 MAX:4 id:identyfikator1-1-1-1|#]

    // Zatem po prostu rozbijamy ten string i wyciagamy dane z listy
    redeliveryAttemptData = it.tokenize('|')

    String currentEntryData = redeliveryAttemptData.get(1)
    String currentEntryThread = redeliveryAttemptData.get(5)
    String currentEntryInfo = redeliveryAttemptData.get(6)

    // Wpierw potrzebna nam data i godzina z dokladnoscia do sekund
    def dataRegexp = /\d{2}:\d{2}:\d{2}\.\d{3}/
    def currentMinutesSec =  currentEntryData.find(dataRegexp)

    // Informacja o numerze watku, ktory procesowal wiadomosc
    def threadRegexp = /=jmsContainerEmailIn-\d{1,}/
    def currentThreadInfo =  currentEntryThread.find(threadRegexp).tokenize('-').get(1)

    // Logowane przeze mnie informacje o wiadomosci i metadane redeliveringu
    def listWithCurrentInfo = currentEntryInfo.tokenize(' ')
    def currentRound = listWithCurrentInfo.get(0).tokenize(':').get(1)
    def currentId = listWithCurrentInfo.get(2).tokenize(':').get(1)

    if( !presentationData[ currentId ] ) {
        presentationData[ currentId ] = [:]
    }

    presentationData[ currentId ][ 'id' ] = currentId
    presentationData[ currentId ][ currentRound ] = currentMinutesSec + '( ' + currentThreadInfo + ' )'

}

// Drukujemy na konsole w formie CSV. Mozna inaczej - stad
// zreszta zapisywanie danych do mapy
presentationData.each { key, value ->

    StringBuffer sb = new StringBuffer(64)

    sb.append( value.get('id')).append(',')

    for ( i in ['0','1','2','3','4'] ) {
        sb.append( value.get( i ) ).append(',')
    }

    println sb

}

Narzędzia JDK

W przypadku JDK najczęściej wiemy, że trzeba go zainstalować by móc coś zaprogramować. Często na samej instalacji nasza przygoda z JDK się kończy – resztę biorą na siebie IDE czy ścieżka systemowa do kompilatora i tyle. Tymczasem warto zapoznać się z JDK trochę bliżej – dostarcza on bowiem szeregu całkiem przyjemnych narzędzi. W swoim wywodzie pominę najbardziej oczywiste – java oraz javac. Chyba każdy wie, że używa kompilatora oraz uruchamia oddzielne instancje JVM ;)

Lista z całą pewnością nie jest kompletna, nie ma również służyć za tutorial dotyczący ich użycia. Ma tylko pokazać istnienie pewnych programików, które mogą nam się przydać. Wiekszość z niżej wymienionych posiada dodatkowe opcje i parametry, którymi możemy sterować uruchomienie oraz działanie programów. Całość testowałem na JDK od Sun/Oracle w wersjach 1.6 oraz 1.7

  • JPS  - jest to narzędzie pozwalające wylistować uruchomione (domyślnie) na lokalnym komputerze maszyny wirtualne. W charakterze parametru można podać dowolny adres komputera (zdalnego), oczywiście pod warunkiem, że mamy do niego dostęp. Również należy zwrócić uwagę na opis w dokumentacji:

    The jps command uses the java launcher to find the class name and arguments passed to the main method. If the target JVM is started with a custom launcher, the class name (or JAR file name) and the arguments to the main method will not be available. In this case, the jps command will output the string Unknown for the class name or JAR file name and for the arguments to the main method.

    The list of JVMs produced by the jps command may be limited by the permissions granted to the principal running the command. The command will only list the JVMs for which the principle has access rights as determined by operating system specific access control mechanisms.

    Dodatkowo dokumentacja mówi jasno, iż JPS nie jest już wspierane i może zniknąć w kolejnych wersjach Javy. U mnie w JDK 1.7.0 wciąż jest dostępne ;)

  • Appletviewer – zgodnie z nazwą służy do uruchamiania appletów bez konieczności osadzania ich na loklanej stronie WWW (czy też gdzieś indziej). Dla przykładu – mamy grę w Javie w formie apletu, ale nie chcemy uruchamiać całej otaczającej aplet strony (z masą reklam). Podajemy adres strony zawierającej element z apletem i do przodu. Niestety należy się również liczyć z tym, iż mogą wystąpić problemy z uruchomieniem – przekazywane parametry, sprawdzanie poprawności, cookie – ewentualnych przyczyn braku poprawnego uruchomienia jest wiele. Jednakże by zobaczyć choć w praktyce jak to wygląda wystarczy uruchomić z konsoli to narzędzie przekazując jako pierwszy argument adres: http://www.roseindia.net/tutorialfiles/java/applet-oline-example.html . Hello World!
  • JSTAT i JSTATD – to bardzo potężne narzędzia, które umożliwiają monitoring parametrów JVM. JStad uruchamia lokalny rejestr RMI, co umożliwia wystawienie informacji o JVM na zewnątrz (np. włączenie ładnego monitoringu w formie aplikacji webowej uruchomionej na innym serwerze). Na szybko zaś powiem więcej o Jstat, które to narzędzie pozwala na dość dokładne monitorowanie parametrów JVM. Głównie rzecz w zajętości pamięci oraz zdarzeniach Garbage Collectora. Jest to naprawdę bardzo istotne w przypadku wycieków pamięci, monitoringu zmian oraz badaniach efektywności. Zwracane wartości mogą być różnie formatowane i dobrze jest zapoznać się z nimi w dokumentacji .
  • Javap – narzędzie użyteczne dość w wąskim zakresie. Jego wywołanie z argumentem w formie klasy spowoduje zwrócenie danych o pakiecie oraz publicznych i chronionych polach/metodach. Oczywiście wynik programu możemy regulować za pomocą dużej ilości opcji. Przydatne kiedy debugujemy kod na zapomnianym serwerze z Javą 1.3 bez dostępu do API w innej postaci (man, internet, IDE) ;)
  • JDB – debugger dla Javy. Jeśli myśl o próbach zabawy z tym narzędziem bez IDE przyprawia Cię o drżenie rąk to wiedz, że nie jesteś jedynym.
  • JMap i JHat – narzędzia do tworzenia zrzutów sterty oraz do ich analizy. Parametrem do analizy przez program JHat może być efekt działania JMap. Program JHat po przeanalizowaniu pliku uruchamia serwer WWW i udostępnia dane na temat zrzutu.
  • JConsole i JVisualVM – ładne narzędzia graficzne do zarządzania i statystyki JVM (zarówno lokalnych jak i zdalnych). Są to nakładki wykorzystujace niektóre wspomniane powyżej narzędzia. Długo by pisać – zachęcam do zapoznania się z nimi.

Pozostałe narzędzia są w fazie eksperymentalnej (np. jrunscript) bądź też dość często używane, ale przez inne biblioteki i narzędzia (wsgen, javadoc, xjc, jar). Wielu z powyższych narzędzi możemy nigdy nie potrzebować. Warto jednak zajrzeć do folderu BIN, tak tylko z wrodzonej ciekawości ;)

NetworkManager w KUbuntu

Dzisiaj będzie trochę o czymś innym i do tego krótko. Mam laptopa z zainstalowanym KUbuntu. Laptop ma o tyle niefajnie, że raz podłączam go pod kabelek, czasem pod WiFi sąsiada, a czasem pod swoje własne ;) Do tego łączę się z niego poprzez VPNa do sieci firmowej, ale przy okazji na komputerze przez który idzie łącze ‘kabelkowe’ też czasem odpalam drugiego VPNa. Dlaczego o tym piszę? Gdyż gdzieś w tym galimatiasie linux po prostu staje dęba i nie wie czego konkretnie używać do połączenia z internetem.

Zazwyczaj w KDE mamy takie coś jak NetworkManager. Z tym tylko, że dość często po uruchomieniu systemu klikając na ikonkę w zasobniku dostaję piękne zdalnie ‘NetworkManager disabled’. Khem. Rozwiązanie jest dość banalne, ale trzeba wiedzieć co z czym i po kolei. Dokładny opis znaleźć można pod tym linkiem. Ja tylko o tym piszę bo może komuś się przydać i oszczędzić nerwów.

Z Hudsona na Jenkinsa i niepoprawna konfiguracja

Dzisiaj na szybko – jeśli przemigruje się z Hudsona na Jenkinsa to jest wielce prawdopodobne, że ten drugi nie będzie działał poprawnie. Najczęstszą przyczyną jest brak widoczności niektórych elementów konfiguracji. By mieć pewność, że wszystko będzie ok należy wejść w konfigurację projektu i po prostu bez zmian go zapisać. Powinno styknąć.

Klaster ActiveMQ i dlaczego kolejność XMLa ma znaczenie

Jednym z elementów specyfikacji korporacyjnej Javy są Message Driven Beans, które zasadniczo wrzuca się do wora z napisem JMS (Java Message System). Jak zawsze pominę dywagacje teoretyczne co jest czym. Kiedy wezmę się za zdawanie certyfikatu biznesowego Oracla to na pewno sporo na ten temat napiszę. Dzisiaj jednak coś prostego i na szybko – zrobimy klaster brokerów ActiveMQ.

ActiveMQ jest produktem ze stajni Apache, który dostarcza funkcjonalności JMS, ale poza kontenerem JEE. Ma to swoje plusy – do obsługi JMS można oddelegować oddzielną maszynę i nie przejmować się padami serwera. Trochę łatwiej skalować prostą aplikacyjkę niż kolejne serwery aplikacyjne. W przypadku ActiveMQ nie jest również problemem stworzenie klastra brokerów, co umożliwia ciągłość działania w przypadku gdyby jeden z brokerów odmówił posłuszeństwa.

Samo ActiveMQ da się uruchomić z domyślnymi (na systemach *nixowych wystarczy nadać prawa do wykonywania plikowi activemq oraz zapisu do folderów z danymi i konfiguracją) ustawieniami i będzie to działało całkiem sprawnie. Każdy z brokerów posiada swoją nazwę, a także maszynę i port, na którym nasłuchuje. Domyślny plik konfiguracyjny wygląda nastepująco (wersja 5.5.1):

<!--
 Licensed to the Apache Software Foundation (ASF) under one or more
 contributor license agreements. See the NOTICE file distributed with
 this work for additional information regarding copyright ownership.
 The ASF licenses this file to You under the Apache License, Version 2.0
 (the "License"); you may not use this file except in compliance with
 the License. You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->
<!-- START SNIPPET: example -->
<beans
 xmlns="http://www.springframework.org/schema/beans"
 xmlns:amq="http://activemq.apache.org/schema/core"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
 http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">

 <!-- Allows us to use system properties as variables in this configuration file -->
 <bean>
 <property name="locations">
 <value>file:${activemq.base}/conf/credentials.properties</value>
 </property>
 </bean>

 <!--
 The <broker> element is used to configure the ActiveMQ broker.
 -->
 <broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.base}/data" destroyApplicationContextOnStop="true">

 <!--
 For better performances use VM cursor and small memory limit.
 For more information, see:

 http://activemq.apache.org/message-cursors.html

 Also, if your producer is "hanging", it's probably due to producer flow control.
 For more information, see:
 http://activemq.apache.org/producer-flow-control.html
 -->

 <destinationPolicy>
 <policyMap>
 <policyEntries>
 <policyEntry topic=">" producerFlowControl="true" memoryLimit="1mb">
 <pendingSubscriberPolicy>
 <vmCursor />
 </pendingSubscriberPolicy>
 </policyEntry>
 <policyEntry queue=">" producerFlowControl="true" memoryLimit="1mb">
 <!-- Use VM cursor for better latency
 For more information, see:

 http://activemq.apache.org/message-cursors.html

 <pendingQueuePolicy>
 <vmQueueCursor/>
 </pendingQueuePolicy>
 -->
 </policyEntry>
 </policyEntries>
 </policyMap>
 </destinationPolicy>

 <!--
 The managementContext is used to configure how ActiveMQ is exposed in
 JMX. By default, ActiveMQ uses the MBean server that is started by
 the JVM. For more information, see:

 http://activemq.apache.org/jmx.html
 -->
 <managementContext>
 <managementContext createConnector="false"/>
 </managementContext>

 <!--
 Configure message persistence for the broker. The default persistence
 mechanism is the KahaDB store (identified by the kahaDB tag).
 For more information, see:

 http://activemq.apache.org/persistence.html
 -->
 <persistenceAdapter>
 <kahaDB directory="${activemq.base}/data/kahadb"/>
 </persistenceAdapter>

 <!--
 The systemUsage controls the maximum amount of space the broker will
 use before slowing down producers. For more information, see:

 http://activemq.apache.org/producer-flow-control.html

 <systemUsage>
 <systemUsage>
 <memoryUsage>
 <memoryUsage limit="20 mb"/>
 </memoryUsage>
 <storeUsage>
 <storeUsage limit="1 gb"/>
 </storeUsage>
 <tempUsage>
 <tempUsage limit="100 mb"/>
 </tempUsage>
 </systemUsage>
 </systemUsage>
 -->

 <!--
 The transport connectors expose ActiveMQ over a given protocol to
 clients and other brokers. For more information, see:

 http://activemq.apache.org/configuring-transports.html
 -->
 <transportConnectors>
 <transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/>
 </transportConnectors>

 </broker>

 <!--
 Enable web consoles, REST and Ajax APIs and demos

 Take a look at ${ACTIVEMQ_HOME}/conf/jetty.xml for more details
 -->
 <import resource="jetty.xml"/>

</beans>
<!-- END SNIPPET: example -->

By stworzyć klaster nie musimy się specjalnie wysilać. Wystarczy uruchomić 2 razy ActiveMQ. Można do tego użyć dokładnie tego samego kodu (lokalizacji na dysku), a użyć tylko innych plików startowych oraz konfiguracji (rzecz jasna każdy broker musi nasłuchiwać na oddzielnym porcie). U mnie rzecz była o tyle łatwiejsza, iż klaster miał być zbudowany z użyciem 2 oddzielnych maszyn. W związku z czym skopiowałem folder z całą aplikacją i konfiguracją na drugi serwer i jedynym co zmieniłem była nazwa brokera (parametr brokerName w elemencie broker).

Uruchomienie tym samym 2 oddzielnych węzłów nie było problemem – oba działały bez zastrzeżeń. Jednakże jak wspomniałem celem było stworzenie klastra brokerów. A tym samym obydwa węzły powinny widzieć siebie nawzajem.

Musimy wpierw odwiedzić też skrypty startujące AMQ i odnaleźć zmienną ACTIVEMQ_QUEUEMANAGERURL i nadać jej adres nie localhost, ale pełnej nazwy domeny – np. tcp://broker1.chlebik.pl:61616.

Potrzebna do klastra jest też zmiana elementu transportConnectors w konfiguracji brokera. U mnie wygląda on po zmianie tak:


<transportConnectors>
 <transportConnector name="openwire" uri="tcp://broker1.chlebik.pl:61616" updateClusterClients="true"
 rebalanceClusterClients="true" updateClusterClientsOnRemove="true" />
 </transportConnectors>

Uwaga! od razu ostrzegam, że przestawione domeny/porty są prezentacyjne i nie istnieją naprawdę. A jeśli już to na pewno nie są dostępne poprzez sieć ;)

Podobnie konfigurujemy (zmieniając dane) na drugim brokerze. Ich uruchomienie oddzielnie powinno się powieść bez problemu – po prostu dwie oddzielne instancje. By się zobaczyły wzajemnie musimy dodać do konfiguracji każdego z nich element o nazwie networkConnectors. W przypadku konfiguracji brokera słuchającego pod adresem tcp://broker1.chlebik.pl:61616 wyglądać będzie to tak:


<networkConnectors>
 <networkConnector uri="static:(tcp://broker2.chlebik.pl:61616)" conduitSubscriptions="false" />
</networkConnectors>

Oczywiście dla broker2 adres będzie odnosił się do broker1. I tutaj najważniejsza informacja – podana konfiguracja u mnie nie działała. Pomimo przeorania tutoriali w sieci oraz dokumentacji jakoś nikt nie wspomniał, iż plik konfiguracyjny ActiveMQ jest wrażliwy na kolejność elementów konfiguracyjnych!!! I by temat zadziałał element networkConnectors najbezpieczniej jest umieścić zaraz za elementem destinationPolicy. Dopiero wtedy przy odpaleniu drugiego brokera otrzymamy w logach coś na kształt:

2012-01-16 20:02:28,078 | INFO  | Establishing network connection from vm://broker1?async=false&network=true to tcp://broker2.chlebik.pl:61616 | org.apache.activemq.network.DiscoveryNetworkConnector | main

Lub coś podobnego do wyżej przedstawionego. Mi ten niuans zmarnował kilka godzin – mam nadzieję, że tym wpisem uchroniłem kogoś przed podobnym niebezpieczeństwem.

Co tam znów u konkurencji słychać?

Podobnie jak kawał czasu temu postanowiłem przejrzeć swoje RSSy by podzielić się z Wami ciekawymi materiałami w interesujących nas tematach. Co prawda materiały te obejmują 2 ostatnie lata (trochę zapuściłem bloga ostatnio, przepraszam), ale to na pewno nie odejmuje im wartości merytorycznej. W nowy rok dobrze jest wejść z solidną dawką wiedzy na rozruch.

Na początek linkowany już u mnie na blogu tutorial Darka Zonia wprowadzający w Spring Framework. Mój tutorial póki co stoi w miejscu zaś Darek bardzo szybko i sprawnie  stworzył kawałek niezgłego pisania.

Jak zawsze bajtów na dysku nie zasypuje Tomek Dziurko i przez ostatnie miesiące na blogu zaprezentował kilkanaście ciekawych wpisów dotyczących popularnych bibliotek i narzędzi. Przede wszystkim Tomek od zawsze popularyzuje użycie webframeworku Wicket i w związku z tym można u Niego znaleźć bardzo fajny tutorial na ten temat. Do tego ciekawym wpisem na pewno jest przedstawienie systemu kontroli wersji Mercurial – warto sprawdzić czy “konkurent” GITa może nam coś ciekawego zaoferować.

Nie jestem pewny, czy aby nie umieszczałem już linka do bardzo fajnego tutoriala o UMLu autorstwa Grześka Kukawskiego. Co ciekawe – materiał jest prezentowany w formie oddzielnych lekcji i do tego w formie krótkich filmo-prezentacji (pomysł z użyciem map myśli do prezentacji treści jest moim zdaniem bardzo dobry). Zachęcam do zapoznania się z tym kursem.

Często podczas nauki Javy brakowało mi ciekawych i przystępnych tutoriali, które opisywałyby warstwę middleware pisanego w Javie – tych wszystkich kolejek, webserwisów, wzorców integracyjnych i całej hałastry. Z wielką zatem ciekawością przeczytałem kilka wpisów na ten temat Łukasza Dywickiego.

Na koniec zostawiłem zwycięzcę mojego prywatnego konkursu o tytuł “Tytana Javowej Blogosfery” – Bartka “Koziołka” Kuczyńskiego. Na jego blogu można znaleźć tyle wartościowego materiału, że próba wyliczenia tych ciekawszych mocno rozdęłaby długość tego posta. Ja mogę polecić ze swej strony bardzo ciekawą serię o “Ekstremalnej obiektowości”.

To tyle na dziś i tak przy okazji – Szczęśliwego Nowego Roku.

Ćwiczenie w dowolnym języku programowania

Dzisiaj coś z zupełnie innej beczki. Pewnie nie raz rozpoczynaliście zabawę z nowym językiem programowania – niezależnie czy to był Groovy, Scala czy coś poza JVM - Ruby lub Pyton. Zawsze nadchodzi ten pierwszy raz, kiedy po przerobieniu tutoriala/książki/FAQ wypadałoby coś napisać, aby w praktyce przetestować sam język jak i swoją wiedzę.

W przypadku języków, które występują najczęściej w kontekście sieciowym tworzy się coś na kształt bloga, CMSa lub podobnych – prosty CRUD, konfiguracja logowania, zaciągnia zależności i tego wszystkiego. Tutaj niestety problem polega na tym, iż do pracy zaprzęgamy również dedykowany framework – Grailsy, Lifta, RoRa, Django czy co tam jeszcze nam do głowy przyjdzie. Nasz projekt działa, ale czy na pewno do końca rozumiemy język? W dzisiejszych rozwiązaniach otrzymujemy tak wiele rzeczy OOTB, że czasem nawet nie mamy okazji dowiedzieć się co działa pod spodem.

Jeśli takie podejście jest Ci znane – wówczas proponuję inną rzecz. Polecam ten link – jakiś czas temu bawiłem się akcjami (w sensie edukacyjnym – nigdy złotówki nie zainwestowałem) i próba zaprzęgnięcia komputera do dokonywania analiz danych wydawała mi się dość naturalna. Pod wspomnianym linkiem znajduje się algorytm prostego systemu na akcje – czy działa – trudno absolutnie powiedzieć przy dzisiejszej zmienności na GPW. Jednakże jako rzecz do zaimplementowania w nowym języku programowania nadaje się idealnie.

Mamy połączenie z zewnętrznymi zasobami (doczytajcie komentarze), parsowanie pliku tekstowego, rozbicie danych na struktury (listy/zbiory/mapy), pętle i wszystko co potrzebne. Podany system można rozbudowywać – zarówno o własne wariacje algorytmów, jak i o bardziej konkretne  funkcjonalności. Może dodać zapis do bazy danych? Może zapisywać dane inkrementacyjnie we własnym zakresie niż za każdym razem łączyć się po zewnętrzne dane. Dorobić interfejs webowy? Czemu nie. Wiem tylko jedno – da się poćwiczyć.

Jeśli znacie podobne fajne pomysły na ‘programistyczną piaskownicę’ to zachęcam do podzielenia się wiedzą w komentarzach.