Download as pdf or txt
Download as pdf or txt
You are on page 1of 489

Spis treści

SPIS TREŚCI...........................................................................................................................................................1

O AUTORACH....................................................................................................................................................... 9

WSTĘP................................................................................................................................................................... 10
SERVLET API 2.2................................................................................................................................................... 10
Servlet API 2.3................................................................................................................................................ 11
CZYTELNICY PIERWSZEGO WYDANIA............................................................................................................................ 11
CZYTELNICY........................................................................................................................................................... 12
Co należy wiedzieć..........................................................................................................................................12
PRZYKŁADY............................................................................................................................................................ 12
ORGANIZACJA..........................................................................................................................................................13
KONWENCJE WYKORZYSTYWANE W TEJ KSIĄŻCE........................................................................................................... 15
PROŚBA O KOMENTARZE............................................................................................................................................15
PODZIĘKOWANIA......................................................................................................................................................15
PODZIĘKOWANIA Z WYDANIA PIERWSZEGO....................................................................................................................16
ROZDZIAŁ 1. WPROWADZENIE.................................................................................................................... 18
HISTORIA APLIKACJI WWW..................................................................................................................................... 18
Common Gateway Interface........................................................................................................................... 18
Inne rozwiązania.............................................................................................................................................20
Serwlety Javy.................................................................................................................................................. 21
OBSŁUGA SERWLETÓW..............................................................................................................................................22
Samodzielne kontenery serwletów.................................................................................................................. 22
Dołączane kontenery serwletów..................................................................................................................... 23
Osadzane kontenery serwletów.......................................................................................................................24
Uwagi dodatkowe........................................................................................................................................... 24
POTĘGA SERWLETÓW................................................................................................................................................ 24
Przenośność.................................................................................................................................................... 24
Moc................................................................................................................................................................. 25
Wydajność i wytrzymałość.............................................................................................................................. 25
Bezpieczeństwo............................................................................................................................................... 25
Elegancja........................................................................................................................................................ 26
Integracja........................................................................................................................................................26
Rozszerzalność i elastyczność......................................................................................................................... 26
ROZDZIAŁ 2. APLETY HTTP —WPROWADZENIE................................................................................... 27
PODSTAWY HTTP.................................................................................................................................................. 27
Zlecenia, odpowiedzi, nagłówki......................................................................................................................27
Metody GET i POST....................................................................................................................................... 28
Pozostałe metody Http.................................................................................................................................... 29
INTERFEJS API (SERVLET API)................................................................................................................................ 29
TWORZENIE STRONY................................................................................................................................................. 30
Pisanie „Hello World”................................................................................................................................... 30
Uruchamianie „Hello World”........................................................................................................................ 31
Przetwarzanie danych formularzowych..........................................................................................................32
Obsługa zleceń POST..................................................................................................................................... 34
Obsługa zleceń HEAD.................................................................................................................................... 34
APLIKACJE WWW................................................................................................................................................. 35
Katalog WEB-INF...........................................................................................................................................36
Deskryptor wdrożenia.....................................................................................................................................37
PRZEJDŹMY DALEJ....................................................................................................................................................40
ROZDZIAŁ 3. CZAS ISTNIENIA (CYKL ŻYCIA) APLETU........................................................................ 41
ALTERNATYWA APLETU.............................................................................................................................................41
Pojedyncza maszyna wirtualna Javy.............................................................................................................. 41
Trwałość kopii.................................................................................................................................................42
Liczniki............................................................................................................................................................42
Liczniki zsynchronizowane..............................................................................................................................43
Liczniki całościowe......................................................................................................................................... 45
ODNAWIANIE (POWTÓRNE ŁADOWANIE) APLETU............................................................................................................ 46
METODY „INIT” I „DESTROY”...................................................................................................................................46
Licznik z metodą Init....................................................................................................................................... 47
Licznik z metodami Init i Destroy................................................................................................................... 49
Model jedno-wątkowy (Single-Thread Model)............................................................................................... 51
PRZETWARZANIE DRUGOPLANOWE...............................................................................................................................53
URUCHAMIANIE I ROZRUCH........................................................................................................................................54
BUFOROWANIE PODRĘCZNE PO STRONIE KLIENTA........................................................................................................... 55
BUFOROWANIE ZEWNĘTRZNE PO STRONIE SERWERA........................................................................................................57
ROZDZIAŁ 4. ODCZYTYWANIE INFORMACJI.......................................................................................... 65
APLET....................................................................................................................................................................66
Pobieranie parametru początkowego apletu.................................................................................................. 66
Pobieranie nazw parametrów początkowych apletów....................................................................................67
Pobieranie nazwy apletu.................................................................................................................................68
SERWER ................................................................................................................................................................ 68
Pobieranie informacji o serwerze...................................................................................................................68
Zapisywanie w pliku tymczasowym.................................................................................................................70
Ograniczanie działania apletu do serwera.....................................................................................................71
Pobieranie kontekstowego parametru początkowego.................................................................................... 72
Ustalanie wersji apletu................................................................................................................................... 73
KLIENT.................................................................................................................................................................. 75
Pobieranie informacji o komputerze klienta...................................................................................................75
Ograniczanie dostępu..................................................................................................................................... 76
Pobieranie informacji o użytkowniku............................................................................................................. 77
Personalizowane powitanie............................................................................................................................ 78
Zlecenie .......................................................................................................................................................... 79
Parametry zlecenia......................................................................................................................................... 79
Generowanie Klucza Licencji......................................................................................................................... 81
Informacje ścieżki........................................................................................................................................... 83
Pobieranie typów MIME.................................................................................................................................86
Podawanie plików...........................................................................................................................................87
Czytanie z zasobów oddzielonych................................................................................................................... 88
Podawanie zasobów........................................................................................................................................90
Podawanie zasobów do ściągnięcia............................................................................................................... 92
Określanie co było przedmiotem zlecenia...................................................................................................... 93
Sposób złożenia zlecenia.................................................................................................................................94
Nagłówki zleceniowe.......................................................................................................................................95
Strumień wyjściowy.........................................................................................................................................98
Ładowanie plików przy użyciu strumienia wejściowego................................................................................ 99
Dodatkowe atrybuty......................................................................................................................................106
ROZDZIAŁ 5. PRZESYŁANIE INFORMACJI HTTP..................................................................................108
STRUKTURA ODPOWIEDZI.........................................................................................................................................108
PRZESYŁANIE STANDARDOWEJ ODPOWIEDZI ............................................................................................................... 109
KORZYSTANIE ZE POŁĄCZEŃ STAŁYCH....................................................................................................................... 110
BUFOROWANIE ODPOWIEDZI..................................................................................................................................... 111
Regulowanie bufora odpowiedzi...................................................................................................................112
KODY STATUSU..................................................................................................................................................... 114
Ustanawianie kodu statusu........................................................................................................................... 114
Ulepszanie apletu „ViewFile” przy pomocy kodów statusu.........................................................................115
Nagłówki HTTP ........................................................................................................................................... 116
USTAWIANIE NAGŁÓWKÓW HTTP .......................................................................................................................... 117
Przekierowywanie zlecenia...........................................................................................................................118
Nadzorowanie łączników do innych stron.................................................................................................... 119
Klienckie ściąganie z serwera.......................................................................................................................120
ROZWIĄZYWANIE PROBLEMÓW................................................................................................................................. 121
Kody statusu..................................................................................................................................................122
Konfiguracja stron błędu.............................................................................................................................. 123
Rejestracja.................................................................................................................................................... 125
Raportowanie................................................................................................................................................126
Wyjątki.......................................................................................................................................................... 127
Wyjątki apletowe „ServletException”.......................................................................................................... 127
Wyjątki apletowe „UnavailableException”..................................................................................................128
Konfiguracja Stron Wyjątków.......................................................................................................................128
Jak rozpoznać, że nikt nie oczekuje na sygnały?.......................................................................................... 130
SZEŚĆ SPOSOBÓW WYCIĄGANIA KORZYŚCI Z APLETÓW ................................................................................................. 131
ROZDZIAŁ 6. PRZESYŁANIE TREŚCI MULTIMEDIALNEJ.................................................................. 134
WAP I WML..................................................................................................................................................... 134
WML..............................................................................................................................................................136
Symulatory urządzeń WAP............................................................................................................................137
Podawanie treści WAP................................................................................................................................. 137
Dynamiczna treść WAP.................................................................................................................................138
Poznajmy WAP..............................................................................................................................................142
OBRAZKI.............................................................................................................................................................. 142
Generowanie obrazków................................................................................................................................ 142
Obrazek „Hello World”................................................................................................................................143
Dynamicznie generowany schemat...............................................................................................................145
Składanie obrazu...........................................................................................................................................147
Rysowanie obrazków.....................................................................................................................................147
Obrazki — efekty...........................................................................................................................................152
TREŚĆ SKOMPRESOWANA.........................................................................................................................................155
SERWER CYKLICZNY............................................................................................................................................... 157
ROZDZIAŁ 7. ŚLEDZENIE SESJI ..................................................................................................................161
UWIERZYTELNIANIE UŻYTKOWNIKA........................................................................................................................... 162
UKRYTE POLA DANYCH FORMULARZA........................................................................................................................163
PRZEPISYWANIE URL-U......................................................................................................................................... 165
TRWAŁE COOKIES...................................................................................................................................................167
Praca z cookies............................................................................................................................................. 167
Robienie zakupów przy pomocy trwałych cookies........................................................................................169
API — ŚLEDZENIE SESJI.........................................................................................................................................171
Podstawy śledzenia sesji............................................................................................................................... 171
Wykorzystywanie śledzenia sesji — liczba wizyt...........................................................................................172
Czas istnienia (cykl życia) sesji.....................................................................................................................173
Ustawianie terminu ważności sesji............................................................................................................... 174
Metody czasu trwania................................................................................................................................... 176
Awaryjne zmiany trybu pracy — „nie-ciasteczkowe”.................................................................................. 178
Robienie zakupów przy użyciu śledzenia sesji.............................................................................................. 182
ROZDZIAŁ 8. BEZPIECZEŃSTWO............................................................................................................... 184
Uwierzytelnienie poprzez HTTP................................................................................................................... 185
KONFIGURACJA UWIERZYTELNIENIA HTTP................................................................................................................185
Uwierzytelnienie w oparciu o rolę................................................................................................................185
Ograniczanie dostępu do serwletu................................................................................................................186
WYSZUKIWANIE INFORMACJI UWIERZYTELNIENIA......................................................................................................... 188
Uwierzytelnienie w oparciu o formularz.......................................................................................................189
Uwierzytelnienie niestandardowe.................................................................................................................191
Certyfikaty cyfrowe....................................................................................................................................... 195
Protokół bezpiecznej transmisji danych (SSL)..............................................................................................197
ROZDZIAŁ 9. ŁĄCZNOŚĆ Z BAZĄ DANYCH.............................................................................................202
RELACYJNE BAZY DANYCH...................................................................................................................................... 203
JDBC API..........................................................................................................................................................205
Sterowniki JDBC...........................................................................................................................................205
Połączenie z bazą danych............................................................................................................................. 206
Otwieranie połączenia z serwletu................................................................................................................. 207
Wykonywanie zapytań SQL...........................................................................................................................208
Obsługa wyjątków SQL.................................................................................................................................209
Zestawy wyników w szczegółach...................................................................................................................210
Obsługa pól mających wartość null..............................................................................................................211
Aktualizacja baz danych............................................................................................................................... 212
Użycie gotowych zapytań..............................................................................................................................213
Ponowne użycie obiektów bazy danych........................................................................................................ 214
Ponowne użycie połączeń do baz danych..................................................................................................... 214
Ponowne użycie przygotowanych wyrażeń................................................................................................... 215
TRANSAKCJE......................................................................................................................................................... 215
Użycie transakcji w JDBC............................................................................................................................ 216
Optymalizacja transakcji.............................................................................................................................. 217
Pule połączeń................................................................................................................................................217
Serwlet księgi gości.......................................................................................................................................221
ZAAWANSOWANE TECHNIKI JDBC........................................................................................................................... 225
Przechowywane procedury........................................................................................................................... 225
Pliki binarne i księgi, czyli bardzo duże obiekty...........................................................................................226
CO DALEJ?........................................................................................................................................................... 227
ROZDZIAŁ 10. KOMUNIKACJA APLET-SERWLET................................................................................ 229
OPCJE KOMUNIKACJI...............................................................................................................................................229
Aplety godne i niegodne zaufania.................................................................................................................229
Połączenia przez HTTP i zwykłe porty......................................................................................................... 230
Serwlety i serializacja obiektu...................................................................................................................... 231
JDBC, RMI i CORBA....................................................................................................................................232
Podejście hybrydowe.................................................................................................................................... 233
SERWER GODZINY.................................................................................................................................................. 233
Aplet.............................................................................................................................................................. 233
Oparta na tekście komunikacja HTTP..........................................................................................................236
Oparta na obiektach komunikacja HTTP..................................................................................................... 240
Komunikacja przez port................................................................................................................................ 242
Komunikacja RMI......................................................................................................................................... 248
Wskazówka!!!!...............................................................................................................................................253
W którym miejscu uruchomić rejestr?.......................................................................................................... 253
SERWER POGAWĘDEK..............................................................................................................................................255
Projekt...........................................................................................................................................................256
Serwlet...........................................................................................................................................................257
Aplet HTTP................................................................................................................................................... 260
Aplet łączący się przez port...........................................................................................................................262
Aplet RMI...................................................................................................................................................... 263
Dyspozytor.................................................................................................................................................... 266
ROZDZIAŁ 11. WSPÓŁPRACA SERWLETÓW...........................................................................................268
DZIELENIE INFORMACJI............................................................................................................................................268
Współdzielenie przy pomocy ServletContext................................................................................................ 268
Współdzielenie z innym ServletContext........................................................................................................ 270
DZIELENIE KONTROLI..............................................................................................................................................270
Pobieranie dyspozytora żądań......................................................................................................................271
Dyspozycja przekazaniem............................................................................................................................. 271
Przekazanie czy przekierowanie................................................................................................................... 273
Dyspozycja dołączania................................................................................................................................. 274
ROZDZIAŁ 12. SERWLETY KORPORACYJNE I J2EE.............................................................................277
DYSTRYBUCJA ŁADUNKU.........................................................................................................................................277
Jak być dystrybuowalnym............................................................................................................................. 278
Wiele stylów dystrybucji................................................................................................................................279
INTEGRACJA Z J2EE.............................................................................................................................................. 279
Podział pracy w J2EE...................................................................................................................................280
Pozycje środowiskowe.................................................................................................................................. 280
Odwołania do elementów EJB......................................................................................................................282
Odwołania do zewnętrznych fabryk zasobów............................................................................................... 282
Dystrybucja serwletów w środowisku J2EE................................................................................................. 283
ROZDZIAŁ 13. INTERNACJONALIZACJA................................................................................................. 284
JĘZYKI ZACHODNIOEUROPEJSKIE................................................................................................................................284
Encje znakowe HTML................................................................................................................................... 285
Kody ucieczkowe Unicode............................................................................................................................ 286
HOŁDOWANIE LOKALNYM ZWYCZAJOM...................................................................................................................... 287
JĘZYKI SPOZA EUROPY ZACHODNIEJ......................................................................................................................... 288
Kodowanie.................................................................................................................................................... 288
Tworzenie wyników zakodowanych.............................................................................................................. 289
Odczyt i zapis wyników zakodowanych........................................................................................................ 290
WIĘKSZA ILOŚĆ JĘZYKÓW........................................................................................................................................291
UCS-2 i UTF-8..............................................................................................................................................291
Tworzenie UTF-8.......................................................................................................................................... 292
DYNAMICZNA NEGOCJACJA JĘZYKA........................................................................................................................... 293
Preferencje językowe.................................................................................................................................... 294
Preferencje kodowania................................................................................................................................. 294
Pakiety zasobów............................................................................................................................................294
Wyświetlanie odpowiednich informacji........................................................................................................ 295
Klasa LocaleNegotiator................................................................................................................................ 296
Lokalizacje dostarczane przez system...........................................................................................................299
FORMULARZE HTML............................................................................................................................................299
Ukryte kodowanie......................................................................................................................................... 300
ROZDZIAŁ 14. SZKIELET TEA......................................................................................................................304
JĘZYK TEA........................................................................................................................................................... 304
POCZĄTKI............................................................................................................................................................. 305
INFORMACJA O ŻĄDANIU..........................................................................................................................................306
Sięganie głębiej.............................................................................................................................................307
ADMINISTRACJA TEA..............................................................................................................................................308
Skompilowane szablony: szansa dla biznesu................................................................................................ 311
ZASTOSOWANIA TEA.............................................................................................................................................. 312
Przetwarzanie tekstu..................................................................................................................................... 312
Obsługa zawartości.......................................................................................................................................313
Obsługa żądań/odpowiedzi........................................................................................................................... 314
Tworzenie aplikacji Tea................................................................................................................................315
APLIKACJA „NARZĘDZIA”....................................................................................................................................... 317
OSTATNIE SŁOWO...................................................................................................................................................324
ROZDZIAŁ 15. WEBMACRO..........................................................................................................................325
SZKIELET WEBMACRO...........................................................................................................................................325
Powitanie przy pomocy WebMacro.............................................................................................................. 326
INSTALACJA WEBMACRO....................................................................................................................................... 328
Język szablonów WebMacro......................................................................................................................... 329
Narzędzia kontekstu WebMacro................................................................................................................... 330
INSTRUKCJE WEBMACRO....................................................................................................................................... 332
#if.................................................................................................................................................................. 332
#set................................................................................................................................................................ 332
#foreach........................................................................................................................................................ 332
#parse............................................................................................................................................................333
#include.........................................................................................................................................................333
#param.......................................................................................................................................................... 333
#use............................................................................................................................................................... 333
SZABLONY WEBMACRO......................................................................................................................................... 334
Gotowy do ponownego wykorzystania serwlet MacroPrzegl....................................................................... 335
Przetwarzanie szablonów..............................................................................................................................337
APLIKACJA „NARZĘDZIA”....................................................................................................................................... 337
FILTRY................................................................................................................................................................. 341
Filtry niestandardowe...................................................................................................................................341
ROZDZIAŁ 16. ELEMENT CONSTRUCTION SET.....................................................................................342
ELEMENTY STRONY JAKO OBIEKTY............................................................................................................................342
WYŚWIETLANIE ZBIORU WYNIKÓW............................................................................................................................ 343
Dostosowywanie wyświetlania......................................................................................................................345
ROZDZIAŁ 17. XMLC.......................................................................................................................................351
PROSTA KOMPILACJA XML.................................................................................................................................... 351
KLASA MANIPULACYJNA..........................................................................................................................................358
Modyfikacja listy...........................................................................................................................................359
APLIKACJA „NARZĘDZIA”....................................................................................................................................... 361
ROZDZIAŁ 18. JAVASERVER PAGES..........................................................................................................367
WYKORZYSTYWANIE JAVASERVER PAGES................................................................................................................. 368
ZASADY DZIAŁANIA................................................................................................................................................ 369
WYRAŻENIA I DEKLARACJE......................................................................................................................................371
INSTRUKCJE.......................................................................................................................................................... 371
Wykorzystanie instrukcji............................................................................................................................... 372
Unikanie kodu Javy na stronach JSP........................................................................................................... 374
JSP I JAVABEANS................................................................................................................................................. 375
Osadzanie komponentu Bean........................................................................................................................375
Kontrola parametrów komponentu...............................................................................................................377
Powitania przy pomocy komponentu............................................................................................................ 377
DOŁĄCZENIA I PRZEKAZANIA....................................................................................................................................379
APLIKACJA „NARZĘDZIA”....................................................................................................................................... 380
BIBLIOTEKI WŁASNYCH ZNACZNIKÓW.........................................................................................................................383
Stosowanie bibliotek własnych znaczników..................................................................................................383
Aplikacja „Narzędzia” wykorzystująca bibliotekę własnych znaczników....................................................385
ROZDZIAŁ 19. INFORMACJE DODATKOWE........................................................................................... 387
ANALIZA PARAMETRÓW...........................................................................................................................................387
Kod ParameterParser...................................................................................................................................388
WYSYŁANIE POCZTY ELEKTRONICZNEJ.......................................................................................................................391
Stosowanie klasy MailMessage.....................................................................................................................391
Wysyłanie pocztą danych formularza........................................................................................................... 392
STOSOWANIE WYRAŻEŃ REGULARNYCH......................................................................................................................392
Odnajdywanie łącz przy pomocy wyrażeń regularnych............................................................................... 393
URUCHAMIANIE PROGRAMÓW...................................................................................................................................396
Finger............................................................................................................................................................396
Uruchamianie polecenia finger.................................................................................................................... 396
Uruchamianie finger z argumentami............................................................................................................397
Uruchamianie finger z przekierowywanym wynikiem.................................................................................. 398
STOSOWANIE RDZENNYCH METOD............................................................................................................................. 398
WYSTĘPOWANIE JAKO KLIENT RMI..........................................................................................................................399
USUWANIE BŁĘDÓW............................................................................................................................................... 400
Sprawdzenie dzienników zdarzeń..................................................................................................................400
Wyświetlenie dodatkowych informacji..........................................................................................................401
Stosowanie standardowego programu uruchomieniowego..........................................................................401
Analiza żądania klienta.................................................................................................................................402
Utworzenie własnego żądania klienta.......................................................................................................... 403
Wykorzystanie niezależnego narzędzia......................................................................................................... 405
Ostatnie wskazówki.......................................................................................................................................405
POPRAWA WYDAJNOŚCI........................................................................................................................................... 406
Tworzyć, ale nie przesadzać......................................................................................................................... 406
Nie łączyć...................................................................................................................................................... 406
Ograniczać synchronizację...........................................................................................................................406
Buforować dane wprowadzane i wyświetlane.............................................................................................. 407
Spróbować wykorzystania OutputStream..................................................................................................... 407
Wykorzystać narzędzie profilujące............................................................................................................... 407
ROZDZIAŁ 20. ZMIANY W SERVLET API 2.3............................................................................................408
ZMIANY W SERVLET API 2.3................................................................................................................................. 408
Serwlety w J2SE i J2EE................................................................................................................................ 409
Filtry............................................................................................................................................................. 409
Zdarzenia okresu trwałości...........................................................................................................................411
Wybranie kodowania znaków....................................................................................................................... 413
Zależności plików JAR.................................................................................................................................. 413
Mechanizmy ładowania klas.........................................................................................................................414
Nowe atrybuty błędów.................................................................................................................................. 414
Nowe atrybuty bezpieczeństwa..................................................................................................................... 416
Niewielkie poprawki......................................................................................................................................417
Wyjaśnienia deskryptora DTD......................................................................................................................418
KONKLUZJA.......................................................................................................................................................... 418
DODATEK A. KRÓTKI OPIS SERVLET API...............................................................................................419
GenericServlet...............................................................................................................................................419
RequestDipatcher..........................................................................................................................................422
Servlet........................................................................................................................................................... 423
ServletConfig.................................................................................................................................................423
ServletContext............................................................................................................................................... 424
ServletException........................................................................................................................................... 428
ServletInputStream........................................................................................................................................429
ServletOutputStream..................................................................................................................................... 429
ServletRequest...............................................................................................................................................430
ServletResponse............................................................................................................................................ 434
SingleThreadModel.......................................................................................................................................436
UnavailableException ..................................................................................................................................437
DODATEK B. KRÓTKI OPIS HTTP SERVLET API................................................................................... 439
Cookie........................................................................................................................................................... 440
HttpServlet.................................................................................................................................................... 443
HttpServletRequest........................................................................................................................................445
HttpServletResponse..................................................................................................................................... 448
HttpSession................................................................................................................................................... 452
HttpSessionBindingEvent..............................................................................................................................454
HttpSessionBindingListener..........................................................................................................................455
HttpSessionContext....................................................................................................................................... 456
HttpUtils........................................................................................................................................................456
DODATEK C. KRÓTKI OPIS DESKRYPTORÓW DTD.............................................................................458
<auth-constraint>........................................................................................................................................ 462
<auth-method>.............................................................................................................................................463
<context-param>..........................................................................................................................................463
<description>............................................................................................................................................... 463
<display-name>............................................................................................................................................463
<distributable>.............................................................................................................................................463
<ejb-link>.....................................................................................................................................................464
<ejb-ref>...................................................................................................................................................... 464
<ejb-ref-name>............................................................................................................................................ 464
<ejb-ref-type>.............................................................................................................................................. 465
<env-entry>.................................................................................................................................................. 465
<env-entry-name>........................................................................................................................................ 465
<env-entry-type>.......................................................................................................................................... 465
<env-entry-value>........................................................................................................................................ 466
<error-code>................................................................................................................................................466
<error-page>................................................................................................................................................466
<exception-type>..........................................................................................................................................466
<extension>.................................................................................................................................................. 467
<form-error-page>.......................................................................................................................................467
<form-login-config>.....................................................................................................................................467
<form-login-page>.......................................................................................................................................467
<home>........................................................................................................................................................ 468
<http-method>..............................................................................................................................................468
<icon>.......................................................................................................................................................... 468
<init-param>................................................................................................................................................ 468
<jsp-file>...................................................................................................................................................... 469
<large-icon>................................................................................................................................................ 469
<load-on-startup>........................................................................................................................................469
<location>.................................................................................................................................................... 469
<login-config>............................................................................................................................................. 470
<mime-mapping>......................................................................................................................................... 470
<mime-type>................................................................................................................................................ 470
<param-name>.............................................................................................................................................470
<param-value>.............................................................................................................................................470
<realm-name>..............................................................................................................................................471
<remote>...................................................................................................................................................... 471
<res-auth>....................................................................................................................................................471
<res-ref-name>............................................................................................................................................ 471
<res-type>.................................................................................................................................................... 472
<resource-ref>............................................................................................................................................. 472
<role-link>................................................................................................................................................... 472
<role-name>.................................................................................................................................................472
<security-constraint>................................................................................................................................... 473
<security-role>.............................................................................................................................................473
<security-role-ref>....................................................................................................................................... 473
<servlet>.......................................................................................................................................................473
<servlet-class>............................................................................................................................................. 474
<servlet-mapping>....................................................................................................................................... 474
<servlet-name>............................................................................................................................................ 474
<session-config>.......................................................................................................................................... 474
<session-timeout>........................................................................................................................................ 475
<small-icon>................................................................................................................................................ 475
<taglib>........................................................................................................................................................475
<taglib-location>......................................................................................................................................... 475
<taglib-uri>..................................................................................................................................................476
<transport-guarantee>.................................................................................................................................476
<url-pattern>................................................................................................................................................476
<user-data-constraint>................................................................................................................................ 476
<web-app >.................................................................................................................................................. 477
<web-resource-collection>.......................................................................................................................... 477
<web-resource-name>................................................................................................................................. 477
<welcome-file>.............................................................................................................................................478
<welcome-file-list>.......................................................................................................................................478
DODATEK D. KODY STANU HTTP.............................................................................................................. 479

DODATEK E. ENCJE ZNAKOWE..................................................................................................................484

DODATEK F. KODOWANIA...........................................................................................................................488
O Autorach

Jason Hunter jest starszym technologiem w firmie CollabNet (http://collab.net), firmie dostarczającej narzędzia i
usługi dla współpracy Open Source. Oprócz bycia autorem książki „Java Servlet — programowanie” jest także
redaktorem witryny Servlets.com, twórcą biblioteki com.oreilly.servlet, współpracownikiem projektu Apache
Jakarta, który tworzy serwer Tomcat (od czasów, kiedy projekt był jeszcze wewnętrzną częścią firmy Sun),
członkiem grupy ekspertów odpowiedzialnej za tworzenie API Servlet/JSP i JAXP oraz jest członkiem Komitetu
Wykonawczego JCP nadzorującego platformę Javy, jako reprezentant Apache Software Foundation. Pisze
również artykuły dla JavaWorld oraz przemawia na wielu konferencjach programistycznych i Open Source. W
ostatnich czasach współtworzył bibliotekę Open Source JDOM (http://jdom.org), pozwalającą na optymalizację
integracji Javy i XML oraz przewodzi grupie ekspertów odpowiedzialnej za tworzenie JDOM.
Jason poprzednio pełnił funkcję głównego technologa w firmie K&A Software, specjalizującej się w treningach i
konsultacjach związanych z Javą i działał jako wynajęty ekspert dla wielu przedsiębiorstw włączając w to Sun
Microsystems. Jeszcze wcześniej pracował w Silicon Graphics, gdzie był odpowiedzialny za tworzenie (i
niszczenie) różnego rodzaju technologii WWW.
Jason ukończył z najwyższym wyróżnieniem kierunek nauki komputerowe w Willamette University (Salem,
Oregon) w 1995. Rozpoczął programowanie w Javie w lecie 1995, a z serwletami i innymi technologiami
programowania po stronie serwera jest związany od grudnia 1996. Jeżeli jakimś cudem nie pracuje,
przypuszczalnie można go znaleźć na górskiej wędrówce.
William „Will” Crawford związał się z tworzeniem stron WWW w 1995. Pracował przy programie
informatycznym szpitala Children's Hospital w Bostonie, gdzie pomagał przy tworzeniu pierwszego
elektronicznego systemu zapisów medycznych opartego na sieci WWW i był związany z jednymi z pierwszych
korporacyjnych zastosowań języka Java. Był konsultantem projektów sieci Intranet w między innymi Children's
Hospital w Massachusetts, General Hospital w Brigham, Women's Hospital, Boston Anesthesia Education
Foundation i Harvard Medical Center.
Will obecnie przewodzi zespołowi projektanckiemu w firmie Invantage, Inc. w Cambridge, Massachusetts, która
tworzy oparte na Javie narzędzia intranetowe dla przemysłu farmaceutycznego. W wolnym czasie jest zapalonym
amatorem fotografii, pisarzem i studentem ekonomii na Yale University.
Wstęp

Od czasu, kiedy napisane zostało pierwsze wydanie niniejszej książki, serwlety i platforma Javy działająca po
stronie serwera zyskała popularność, której nie można było spodziewać się w najśmielszych marzeniach.
Postępuje przyłączanie tych mechanizmów do istniejących. Producenci serwerów WWW oferują obecnie
obsługę serwletów jako standardową własność swojego oprogramowania. W specyfikacji Java 2, Enterprise
Edition (J2EE) serwlety istnieją jako podstawowy składnik, a niemożliwym jest obecnie znalezienie producenta
serwerów aplikacji, którego produkt nie zawierałby skalowalnej implementacji serwletów. Jest to jednak więcej
niż zjawisko napędzane przez producentów. Serwlety stały się podstawą dla JavaServer Pages (JSP) i innych
szkieletów tworzenia stron WWW, a technologia serwletów obsługuje aktualnie tak często odwiedzane witryny,
jak ESPN.com i AltaVista.com.
W związku z tym nie jest zaskakującym fakt, że krajobraz serwletów wygląda nieco inaczej niż w czasach
pierwszego wydania. Interfejs serwletów (Servlet API) został poddany dwóm przeglądom, a trzeci jest w trakcie
przygotowań. Znajome z początków istnienia serwletów firmy Live Software i New Atlanta, które niegdyś
zarabiały sprzedając mechanizmy serwletów (nazywane teraz kontenerami serwletów) Jrun i ServletExec, zostały
zauważone i wykupione przez większe firmy zorientowane na WWW, odpowiednio przez Allaire i Unify.
Oferują one teraz wiele własności wykraczających poza podstawową obsługę serwletów w celu odróżnienia się
od innych.
Co dziwne, oficjalne pakiety javax.servlet i javax.servlet.http były pierwszymi klasami Javy,
które zostały oficjalnie rozprowadzone jako Open Source. Zostały one przeniesione do projektu Apache
Software Foundation (ASF), i można je aktualnie odnaleźć pod adresem http://jakarta.apache.org. Pakiety te
dalej zgodne są ze specyfikacją Servlet API, jednak poprawa błędów i uaktualnianie specyfikacji znajduje się
teraz w rękach w zaufanych programistów Open Source — włączając autora, który miał niedawno okazję
poprawienia obsługi warunkowego żądania GET w HttpServlet. Dodatkowo, serwer, który jest traktowany
jako wzorcowa implementacja Servlet API, został również przeniesiony do ASF i udostępniony jako Open
Source pod nazwą Apache Tomcat. Od tego czasu Tomcat stał się jednym z najpopularniejszych kontenerów
serwletów. Większa ilość informacji na ten temat dostępna jest pod adresem http://opensource.org.
Świat serwletów zmienił się, a niniejsza książka zawiera uaktualnione informacje. Całą wiedzę potrzebną do
programowania serwletów Javy, od początku do końca. Pierwsze pięć rozdziałów opisuje podstawy — czym są
serwlety, jakie działania wykonują oraz w jaki sposób pracują. Następne 15 rozdziałów zawiera informacje
zaawansowane — opisuje działania podejmowane najczęściej przy pomocy serwletów oraz najpopularniejsze
narzędzia do tego służące. Można tam znaleźć wiele przykładów, kilka wskazówek i ostrzeżeń, a nawet opisy
kilku prawdziwych błędów, które umknęły uwagi korektorów technicznych.

Servlet API 2.2


Niniejsze wydanie książki opisuje wersję 2.2 Servlet API, która osiągnęła stan „wersji publicznej” w sierpniu
1999, a stan „wersji ostatecznej” w grudniu 1999. Wydanie pierwsze opisywało wersje 2.0. Zmiany pomiędzy
wersjami 2.0 i 2.2 są znaczne:

• Zostały wprowadzone zasady definiujące dystrybucje serwletów pomiędzy kilkoma serwerami


wspierającymi.
• Serwlety korzystają aktualnie z dołączanych aplikacji WWW, które mogą być konfigurowane i
wdrażane w sposób niezależny od serwera.
• Znacznie poprawione zostało bezpieczeństwo serwletów.
• Serwlety mogą teraz przekazywać obsługę żądań innym składnikom serwera.
• Serwlety mogą teraz dzielić się informacjami przy pomocy ich ServletContext
• Istnieje sposób przystosowania serwletów do obsługi dostępu rozproszonego.
• Serwlety posiadają teraz ściślejszą kontrolę nad zarządzaniem sesją.
• Dodane zostało buforowanie odpowiedzi.
• Rozszerzona została kontrola nad nagłówkami HTTP.
• Aktualnie może być zastosowana bardziej zaawansowana obsługa błędów.
• API został „wyczyszczony” w celu nadania większej spójności i przewidywalności nazwom metod.
• Servlet API jest teraz zdefiniowany poprzez formalny dokument specyfikacji, a przyszłe uaktualnienia
API są zarządzane przez formalny proces Java Specification Request (JSR).
• Serwlety są teraz zintegrowane z podstawową specyfikacją platformy Java 2, Enterpise Edition (J2EE).
Wszystkie te zmiany, oraz wiele innych drobnych usprawnień, są w pełni opisane w niniejszym nowym wydaniu.
Drugie wydanie zawiera również obszerny opis najciekawszego obszaru programowania serwletów — technik
tworzenia prawdziwych dynamicznych witryn opartych na serwletach. W niniejszym wydaniu znajdują się
samouczki pięciu najpopularniejszych technologii tworzenia zawartości opartej na serwletach, należących do
Open Source:

• JavaServer Pages (JSP), standard firmy Sun, tworzony i udostępniany w połączeniu z serwletami
• Tea, technologia utworzona przez Walt Disney Internet Group (dawniej GO.com), zastosowany w wielu
bardzo często odwiedzanych stronach, takich jak ESPN.com, NFL.com, Disney.com, DisneyLand.com,
GO.com i Movies.com
• WebMacro, utworzony przez Semiotek i wykorzystywany przez wyszukiwarkę AltaVista
• XMLC, utworzony przez Lutris Technologies w celu udostępnienia mocy technologii XML sieci
WWW, wykorzystywany przez innowacyjne witryny takie jak customatix.com
• Element Construcion Set (ECS), utworzony przez Apache w celu obsługi najbardziej wymagających
potrzeb programistycznych
Niniejsze drugie wydanie opisuje również WAP, Wireless Application Protocol (Protokół Aplikacji
Bezprzewodowych) oraz wyjaśnia, jak tworzyć oparte na serwletach aplikacje WWW dla urządzeń
bezprzewodowych.

Servlet API 2.3


W czasie pisania niniejszej książki, Servlet API 2.3 jest w trakcie tworzenia. Jednak nie został on jeszcze
ukończony. W związku z tym tekst niniejszego wydania zawiera w różnych miejscach krótkie uwagi na temat
zmian spodziewanych w z Servlet API 2.3. Dodatkowo, ostatni rozdział książki zawiera dokładniejszy opis
próbnej specyfikacji Servlet API 2.3, udostępnionej w październiku 2000, który pozwala na zapoznanie się z
najnowszymi własnościami Servlet API 2.3. Należy jednak zaznaczyć, że specyfikacje te ciągle podlegają
zmianom, a ostateczna wersja może się nieco różnić od materiału tu przedstawionego.

Czytelnicy pierwszego wydania


Czytelnicy książki „Java Servlet Programming, 1st ed.” zorientują się, że niniejsza książka została obszernie
uaktualniona do Servlet API 2.2 i, gdzie to tylko możliwe, Servlet 2.3. Każdy rozdział został znacząco
poprawiony w porównaniu z pierwszym wydaniem, a także dodano sześć nowych rozdziałów opisujących
techniki tworzenia zawartości opartej na serwletach, jak również nowy rozdział siódmy, „Serwlety korporacyjne i
J2EE”, który opisuje integrację serwletów w platformie J2EE.
Ze względu na znaczący wpływ modelu aplikacji WWW na wszystkie aspekty programowania serwletów, poleca
się czytelnikom pierwszego wydania przeczytanie każdego interesującego ich rozdziału oraz zwrócenie uwagi na
nowe mechanizmy, które pozwalają na wykonanie tradycyjnych zadań. Czytelnicy dysponujący ograniczonym
czasem powinni przejrzeć listę najbardziej znaczących zmian w podrozdziale „Organizacja”.

Czytelnicy
Dla kogo jest ta książka? Dla osób zainteresowanych tworzeniem aplikacji umieszczanych w sieci WWW.
Dokładniej rzecz biorąc, niniejszą książką powinni zainteresować się:

• Programiści J2EE — serwlety są integralną częścią standardu Java 2, Enterpise Edition. Programiści
tworzący aplikacje dla serwerów J2EE mogą nauczyć się jak najlepiej zintegrować serwlety z innymi
podobnymi technologiami.
• Programiści JSP — JavaServer Pages (JSP) tworzone są na podstawie serwletów. Wykorzystanie pełnej
mocy JSP wymaga zrozumienia serwletów, co też umożliwia niniejsza książka. Zawiera ona również
samouczek JSP oraz czterech podstawowych konkurencyjnych technologii.
• Programiści apletów Javy — porozumiewanie się apletów z serwerem zawsze sprawiało problemy.
Serwlety ułatwiają to zadanie poprzez dostarczenie apletom prostego w połączeniu agenta na serwerze.
• Programiści CGI — CGI jest popularną metodą rozszerzania funkcjonalności serwera WWW. Serwlety
są elegancką i wydajną alternatywą tej techniki.
• Programiści innych technik serwerów — istnieje wiele alternatyw dla CGI, między innymi FastCGI,
PHP, NSAPI, WAI, ISPAI, ASP, a teraz ASP+. Każda z nich posiada ograniczenia związane z
przenośnością, bezpieczeństwem, wydajnością i/lub integracją z innymi źródłami danych. Serwlety
przewyższają je w każdym z tych obszarów.

Co należy wiedzieć
Podczas rozpoczynania pracy z niniejszą książką, niespodzianką dla autorów okazało się, że jedną z
najtrudniejszych do określenia rzeczy jest docelowy czytelnik. Czy zna on Javę? Czy ma doświadczenie w
programowaniu CGI lub innych aplikacji WWW? Czy miał już kontakt z serwletami? Czy zna HTTP i HTML,
czy te skróty brzmią dla niego zupełnie niezrozumiale? Niezależnie od przyjmowanego poziomu doświadczenia,
zawsze okazywało się, że książka będzie zbyt uproszczona dla jednych użytkowników, a zbyt zaawansowana dla
drugich.
Ostatecznie zdecydowano się na zasadę, że niniejsza książka powinna zawierać w przeważającej części materiał
oryginalny — można pominąć obszerne opisy tematów i koncepcji dobrze opisanych w sieci lub innych
książkach. W tekście znaleźć można odwołania do tych zewnętrznych źródeł informacji.
Oczywiście zewnętrzne źródła informacji nie są wystarczające. Niniejsza książka zakłada, że czytelnicy dobrze
znają język Java oraz podstawowe techniki programowania obiektowego. Jeżeli nie spełnia się tych założeń,
polecane jest przygotowanie się poprzez przeczytanie ogólnej książki na temat programowania w Javie, takiej jak
„Learning Java” autorstwa Patricka Niemeyera i Jonathana Knudsena (O'Reilly). W książce tej można jedynie
krótko zapoznać się z rozdziałami na temat apletów i programowania Swing (graficznego), a skupić się na sieci i
programowaniu wielowątkowym. Aby zacząć od razu naukę serwletów i uczyć się Javy w trakcie, polecane jest
przeczytanie niniejszej książki równocześnie z „Java in a Nutshell” autorstwa Davida Flanagana (O'Reilly) lub
innym podręcznikiem.
Niniejsza książka nie wymaga od czytelników doświadczenia w programowaniu WWW, HTTP i HTML. Nie
zawiera jednak pełnego wprowadzenia lub wyczerpującego opisu tych technologii. Opisane zostaną podstawy
potrzebne do efektywnego programowania serwletów, a szczegóły (takie jak pełna lista znaczników HTML i
nagłówków HTTP 1.1) pozostawione zostaną innym źródłom.

Przykłady
W niniejszej książce znaleźć można ponad 100 przykładów serwletów. Ich kod jest całkowicie zawarty wewnątrz
tekstu, możliwe jest jednak także pobranie przykładów zamiast ręcznego ich wpisywania. Kod przykładów,
spakowany i gotowy do pobrania, można znaleźć pod adresem http://www.oreilly.com/catalog/jservlet2. Wiele z
tych serwletów można zobaczyć w działaniu pod adresem http://www.servlets.com.
Wszystkie przykłady zostały przetestowane przy pomocy serwera Apache Tomcat 3.2 działającego w trybie
samodzielnym, wirtualnej maszyny Javy (Java Virtual Machine — JVM) zawartej w Java Development KIT
1.1.8 i 1.2.2, zarówno pod Windows jak i Uniksem. Kilka zaawansowanych przykładów wymaga własności,
których nie obsługuje Tomcat w trybie samodzielnym. W tym przypadku przykłady były testowane na różnych
innych serwerach, jak opisano w tekście. Serwer Apache Tomcat jest oficjalną wzorcową implementacją Servlet
API, i jest dostępny w licencji Open Source pod adresem http://jakarta.apache.org.
Niniejsza książka zawiera również zbiór klas narzędziowych — wykorzystywane są one przez serwlety
przykładowe, mogą się także okazać przydatne przy tworzenie własnych. Klasy te zawarte są w pakiecie
com.oreilly.servlet. Między innymi są to klasy pomagające serwletom w analizie parametrów, obsłudze
wysyłania plików, generowaniu wieloczęściowych odpowiedzi (przepychanie serwera), negocjacji ustawień
lokalnych i internacjonalizacji, zwracaniu plików, zarządzaniu połączeniami i pracy jako serwer RMI. Pakiet te
zawiera również klasę wspomagającą komunikację apletów z serwletami. Od czasu pierwszego wydania dodane
zostały nowe klasy pomagające serwletom w wysyłaniu wiadomości poczty elektronicznej, przechowywaniu
odpowiedzi w pamięci podręcznej oraz automatycznym wykrywaniu obsługi Servlet API. Kod źródłowy
większości pakietu com.oreilly.servlet zawarty jest w tekście, a pełna, aktualna wersja jest dostępna w
formie elektronicznej (razem z dokumentacją javadoc) pod adresem http://www.servlets.com.1

Organizacja
Niniejsza książka składa się z 20 rozdziałów i 6 dodatków, są one następujące:

• Rozdział 1, „Wprowadzenie”. Wyjaśnia rolę i zalety serwletów Javy w tworzeniu aplikacji WWW. W
drugim wydaniu dodane zostały dodatkowe informacje na temat serwerów.
• Rozdział 2, „Podstawy serwletów HTTP”. Zawiera krótkie wprowadzenie do HTTP i funkcji, jakie
mogą pełnić serwlety HTTP. Przedstawia tworzenie prostej strony i wprowadza pojęcie dołączanej
aplikacji WWW. Drugie wydanie opisuje aplikacje WWW i ich deskryptory oparte na XML.
• Rozdział 3, „Cykl życia serwletów”. Wyjaśnia szczegółowe informacje na temat sposobu i czasu
ładowania serwletów, sposobu i czasu ich wykonywania, zarządzania wątkami oraz obsługi kwestii
synchronizacji w systemie wielowątkowym. Opisane są również stany trwałe. Drugie wydanie zawiera
nowe zasady kontekstowego przeładowywania i rejestracji serwletów, nowy podrozdział na temat
pamięci podręcznej po stronie serwera oraz uwagę na temat super.init(config).
• Rozdział 4, „Pobieranie informacji”. Wprowadza najpopularniejsze metody wykorzystywane przez
serwlety w celu pobrania informacji — na temat klienta, serwera, żądań klienta oraz samego siebie.
Przedstawia również działanie ogólnej klasy służącej do wysyłania plików. Drugie wydanie opisuje
ustawianie informacji w deskryptorze, pobieranie nazwy serwletu, dostęp do katalogów tymczasowych,
obsługę kontekstowych parametrów początkowych, określanie wersji Servlet API, przypisywanie
odwzorowania serwletów oraz dostęp do zasobów abstrakcyjnych. Przestawia również poprawiony,
bardziej elastyczny składnik służący do wysyłania plików.
• Rozdział 5, „Wysyłanie informacji HTML”. Opisuje sposoby tworzenia kodu HTML przez serwlet,
zwracania błędów, buforowania odpowiedzi, przekierowywania żądań, zapisywania danych w dzienniku
zdarzeń serwera oraz wysyłania dostosowanych nagłówków HTML. Drugie wydanie zawiera nowy opis
buforowania odpowiedzi, bardzo przydatny przykład przekierowywania oraz nowe podrozdziały na
temat konfiguracji stron zawierających błędy i obsługi błędów.
• Rozdział 6, „Wysyłanie zawartości multimedialnej”. Opisuje różne interesujące dane, które może
zwracać serwlet — zawartość WAP/WML dla urządzeń bezprzewodowych, dynamicznie tworzone
obrazki, zawartość skompresowana oraz odpowiedzi wieloczęściowe. W drugim wydaniu dodano opis
WAP/WML, listy plików powitalnych, dyskusję na temat PNG, usprawnioną pamięć podręczną
rysunków po stronie serwera oraz więcej szczegółów na temat tworzenia zawartości skompresowanej.
• Rozdział 7, „Śledzenie sesji”. Opisuje sposoby tworzenia śledzenia stanu w bezstanowym protokole
HTTP. Pierwsza część rozdziału opisuje tradycyjne techniki śledzenia sesji stosowane przez
programistów CGI. Druga część opisuje sposoby zastosowania wbudowanej w Servlet API obsługi

1
Niniejsza książka nie zawiera CD-ROM-u. Dołączenie CD-ROM-u podnosi koszty produkcji a w związku z tym cenę
książki. Założono, że każdy Czytelnik posiada dostęp do Internetu, a w związku z tym może oszczędzić pewną ilość
pieniędzy poprzez pobranie kodu przykładów przez sieć WWW. Nie uważa się również za sensowne dołączanie wersji
próbnych różnych serwerów WWW i aplikacji. Zważywszy na nieustanny szybki postęp na rynku serwletów, dołączone
serwery stałyby się przestarzałe jeszcze przed wydrukowaniem książki. Te same wersje próbne dostępne są w sieci i poleca
się pobranie ich własnoręcznie. Proszę pamiętać, że jeżeli zamierza się czytać niniejszą książkę offline, polecane jest
pobranie kodu przykładów i serwera WWW Apache Tomcata, kiesy tylko będzie to możliwe. Łącza do pobrań umieszczone
są pod adresem http://www.servlets.com.
śledzenia sesji. Drugie wydanie zawiera zasady tworzenia sesji aplikacji WWW, materiał na temat
nowych nazw metod sesji, dyskusję na temat zarządzania przekraczaniem czasu oraz śledzenie sesji
oparte na apletach.
• Rozdział 8, „Bezpieczeństwo”. Wyjaśnia kwestie bezpieczeństwa związane z programowanie
rozproszonym. Opisuje sposoby korzystania ze standardowych funkcji serwletów związanych z
zarządzaniem kontami użytkowników oraz sposoby tworzenia bardziej zaawansowanego systemu przy
pomocy dodatkowego uwierzytelniania i autoryzacji. Wyjaśni również rolę serwletów w bezpiecznej
komunikacji SSL. W drugim wydaniu całkowicie przeredagowany.
• Rozdział 9, „Łączność z bazami danych”. Opisuje sposoby wykorzystania serwletów w wysokowydajnej
łączności z bazami danych WWW. Zawiera samouczek JDBC. Drugie wydanie zawiera przykłady
konfiguracji połączeń z plikami właściwości, nowy przykład księgi gości oraz nowy podrozdział
opisujący JDBC 2.0.
• Rozdział 10, „Komunikacja aplet-serwlet”. Opisuje sposoby wykorzystania serwletów przez aplet, który
musi porozumieć się z serwerem. Uaktualniony w drugim wydaniu.
• Rozdział 11, „Współpraca serwletów”. Opisuje powody komunikacji serwletów i sposoby ich
współpracy przez dzielenie się informacjami lub wywoływanie sienie nawzajem. W drugim wydaniu
całkowicie przeredagowany.
• Rozdział 12, „Serwlety korporacyjne i J2EE”. Opisuje zaawansowane własności serwletów
wykorzystywane w witrynach korporacyjnych — dystrybucję ładunku i integrację składników J2EE.
Nowość w drugim wydaniu.
• Rozdział 13, „Internacjonalizacja”. Opisuje sposoby, dzięki którym serwlet może odczytywać i tworzyć
zawartość w różnych językach. Drugie wydanie opisuje zastosowanie javadoc w zarządzaniu
kodowaniem i sposoby wykorzystywania nowych metod API w zarządzaniu wersjami lokalnymi.
• Rozdział 14, „Szkielet Tea”. Przedstawia szkielet Tea, elegancki, ale zarazem potężny mechanizm
szablonów. Nowość w drugim wydaniu.
• Rozdział 15, „WebMacro”. Opisuje szkielet WebMacro, podobny do Tea lecz z kilkoma innymi
decyzjami projektanckimi. Nowość w drugim wydaniu.
• Rozdział 16, „Element Construction Set”. Zawiera krótki opis ECS, obiektowego podejścia do
tworzenia strony. Nowość w drugim wydaniu.
• Rozdział 17, „XMLC”. Przegląd XMLC, podejścia do tworzenia strony opartego na XML. Nowość w
drugim wydaniu.
• Rozdział 18, „JavaServer Pages”. Wyjaśnia JSP, standardową technologię firmy Sun, w której strony
WWW są automatycznie wkompilowane w serwer. Nowość w drugim wydaniu.
• Rozdział 19, „Informacje dodatkowe”. Przedstawia dodatkowe przykłady serwletów i podpowiedzi,
które nie zmieściły się w żadnym z poprzednich rozdziałów. Drugie wydanie zawiera analizator
parametrów zlokalizowanych, nową klasę poczty elektronicznej oraz uaktualniony podrozdział na temat
wyrażeń regularnych, nowy podrozdział na temat dodatkowych narzędzi oraz dodatkowe podpowiedzi
na temat wydajności.
• Rozdział 20, „Zmiany w Servlet API 2.3”. Opisuje zmiany w nadchodzącej wersji 2.3 Servlet API,
który ma zostać udostępniony w połowie 2001. Nowość w drugim wydaniu.
• Dodatek A, „Krótki opis Servlet API”. Zawiera pełny opis klas, metod i zmiennych w pakiecie
javax.servlet. W drugim wydaniu uaktualniony do Servlet API 2.2
• Dodatek B, „Krótki opis HTTP Servlet API”. Zawiera pełny opis klas, metod i zmiennych w pakiecie
javax.servlet.http. W drugim wydaniu uaktualniony do Servlet API 2.2
• Dodatek C, „Krótki opis deskryptorów DTD”. Przedstawia opis deskryptora Document Type Definition
(Definicja Typu Dokumentu) web.xml. Nowość w drugim wydaniu.
• Dodatek D, „Kody stanu HTTP”. Lista kodów stanu określonych przez HTTP, a także stałe
mnemoniczne wykorzystywane przez serwlety.
• Dodatek E, „Encje znakowe”. Lista encji znakowych zdefiniowanych w HTML, a także równoważne do
nich wartości kodów ucieczkowych Uniksa.
• Dodatek F, „Kodowania”. Lista sugerowanych kodowań wykorzystywanych przez serwlety w celu
tworzenia zawartości w różnych językach.
Proszę czuć się swobodnie i czytać rozdziały w niniejszej książce w dowolnej kolejności. Czytanie prosto od
początku do końca zapewnia uniknięcie wszelkich niespodzianek, jako że starano się unikać odwołań do
dalszych części książki. Przeskakiwanie jest jednak możliwe, zwłaszcza po rozdziale 5 — pozostała część
rozdziałów została zaprojektowana w celu oddzielonego istnienia. Jedna ostatnia sugestia — proszę przeczytać
podrozdział „Usuwanie błędów” w rozdziale 19, jeżeli kiedykolwiek napotka się fragment kodu pracujący
nieprawidłowo.

Konwencje wykorzystywane w tej książce


Kursywa wykorzystywana jest do:

• Ścieżek, nazw plików i programów


• Nowych terminów podczas ich definiowania
• Adresów internetowych, takich jak nazwy domen i URL-e
Czcionka pogrubiona wykorzystywana jest do:

• Konkretnych klawiszy na klawiaturze


• Nazw przycisków interfejsu użytkownika i menu
Czcionka o stałej szerokości wykorzystywana jest do:

• Wszystkich danych pojawiających się dokładanie w programie Javy, takich jak słowa kluczowe, typy
danych, stałe, nazwy metod, zmienne, nazwy klas oraz nazwy interfejsów
• Wszystkich wydruków kodu Javy
• Dokumentów HTML, znaczników i atrybutów
Czcionka o stałej szerokości z kursywą wykorzystywana jest do:

• Ogólnych obszarów zablokowanych wskazujących, że dany element jest zastępowany w programie


przez konkretną wartość.
Pogrubiona czcionka o stałej szerokości wykorzystywana jest do:

• Wpisów w wierszu poleceń

Prośba o komentarze
Prosimy o pomoc w poprawieniu następnych wydań poprzez zgłaszanie wszystkich błędów, nieścisłości,
niejasnych lub niewłaściwych wyrażeń oraz zwykłych literówek, które można odnaleźć w dowolnym miejscu
niniejszej książki. Proszę wysyłać komunikaty o błędach i komentarze pod adres bookquestions@oreilly.com.
(Przed wysłaniem komunikatu o błędzie prosimy sprawdzić erratę na stronie
http://www.oreilly.com/catalog/jservlet2 w celu sprawdzenia, czy dany błąd nie został już opisany.)
Prosimy również o opinie, co powinno znaleźć się w tej książce, aby stała się ona bardziej przydatna.
Wydawnictwo traktuje takie komentarze bardzo poważnie i próbuje dołączyć rozsądne sugestie do przyszłych
wydań książki.

Podziękowania
Kiedy pracowałem nad niniejszą książką, przyjaciel powiedział mi „Łatwiej musi być pisać drugie wydanie;
napisałeś już raz tę książkę”. Pomyślałem nad tym przez chwilę, roześmiałem się i odpowiedziałem, „To jest
łatwiejsze, ale ani trochę nie aż tak łatwe, jak się spodziewałem!”.
Patrząc wstecz, myślę że powód tego ma niewiele wspólnego z książkami, a bardziej z technologią. Pierwsze
wydanie opisywało Servlet API 2.0, specyfikację tworzoną przez około dwa lata. Niniejsze drugie wydanie
przedstawia Servlet API 2.2 i 2.3, co daje mniej więcej dwa dodatkowe lata pracy projektantów. Tak więc
jedynie z tej perspektywy można dostrzec, że jeżeli pierwsze wydanie zabrało mniej więcej rok aktywnego
pisania, to drugie powinno zabrać mniej więcej tyle samo czasu. I rzeczywiście tak było — około 9 miesięcy.
Wiele osób pomogło mi w tworzeniu tej książki. Jestem im głęboko wdzięczny. Po pierwsze są to redaktorzy
techniczni książki — James Duncan Davidson, przewodniczący specyfikacji Servlet API 2.1 i 2.2, oraz Danny
Coward, przewodniczący nadchodzącej wersji 2.3. Wszystko, co można o nich powiedzieć dobrego, to za mało.
Nie tylko dostarczyli mi nieocenionej pomocy i rad w trakcie pisania książki, lecz stworzyli wszystkim doskonałą
platformę do programowania dla WWW.
Dziękuję również wielu programistom, którzy swoim doświadczeniem wspomogli tworzenie rozdziałów na temat
tworzenia zawartości (i w wielu przypadkach tworzyli opisywaną technologię) — Reece Wilton i Brian O'Neill
dla Tea, Justin Wells dla WebMacro, Jon Stevens dla ECS, Mark Diekhnas i Christian Cryder dla XMLC oraz
Hans Bergsten i Craig McClanahan dla JSP.
Chciałbym również podziękować Bobowi Ecksteinowi, redaktorowi książki, którego ręczne notatki były zawsze
celne, choć czasami niemożliwe do odcyfrowania. Bob przejął obowiązki redaktorskie od Pauli Ferguson, po
tym, jak zajęła się ona zarządzaniem książkami O'Reilly na temat WWW i skryptów.
Dziękuję również Jimowi Grishamowi, który pomógł zlokalizować wszystkie rodzaje komputerów i przeglądarek
wykorzystywane przy testowaniu przykładów; Magnusowi Stenmanowi z firmy Orion, który wyjaśnił mi
implementację J2EE w serwerze Orion; Justynie Horwat, zwanej przez niektórych Boginią Biblioteki
Znaczników, za odpowiedzi na pytania dotyczący biblioteki znaczników JSP oraz Ethanowi Henry, który pomógł
sugestiami na temat poprawiania wydajności serwletów.
Nie mogę zapomnieć o Brett'cie McLaughlinie, autorze książki „Java and XML” (O'Reilly) i współtwórcy
JDOM. Jego współpraca ze mną na temat JDOM właściwie spowolniła pisanie tej książki, lecz prędkość, z jaką
on pisze inspiruje mnie, a ponieważ wspomniał mnie on w swojej książce, muszę napisać coś tutaj.
I ostatecznie dziękuję mojej dziewczynie, Kathlyn Bautista, która nie narzekała, kiedy pracowałem w niedziele,
lecz sprawiała, że wcale pracować nie chciałem.
Jason Hunter
Listopad 2000

Podziękowania z wydania pierwszego


Historia tej książki rozpoczęła się właściwie 20 marca 1997, w księgarni „Computer Literacy” w San Jose w
Kalifornii. Tam — po ciekawej rozmowie z Larrym Wallem i Randallem Schwartzem, w której Larry wyjaśniał,
jak automatyzuje swój dom przy pomocy Perla — spotkałem po raz pierwszy szacownego Tima O'Reilly.
Przedstawiłem się i bezczelnie powiedziałem, że pewnego dnia (w dalekiej przyszłości, myślałem), planuję
napisać książkę dla O'Reilly. Czułem się jakbym mówił Stevenowi Spielbergowi, że chcę zagrać główną rolę w
jego filmie. Ku mojemu kompletnemu zaskoczeniu, Tim odpowiedział, „Na jaki temat?”. Tak rozpoczęła się
szaleńcza jazda prowadząca do powstania tej książki.
Wystąpiło w tym czasie kilka jasnych punktów, które z dumą pamiętam — poznanie mojej redaktorki (świetnie,
też jest młoda!), podpisania oficjalnego kontraktu (czy wiecie, że cały papier firmowy O'Reilly jest ozdobiony
zwierzętami?), napisanie pierwszego zdania (znowu i znowu), drukowanie pierwszego rozdziału (i sprawienie,
żeby wyglądał on jak książka O'Reilly), po czym oglądanie rosnącej sterty wydruków, do momentu, kiedy nie
zostało już nic do napisania (oprócz podziękowań).
Było również kilka trudnych chwil. W pewnym momencie, kiedy książka była ukończona w połowie,
uświadomiłem sobie, że Servlet API zmieniał się szybciej, niż mogłem nadążyć. Wierzę w powiedzenie „Jeżeli
coś się nie udaje, poproś o pomoc”, tak więc po krótkich poszukiwaniach poprosiłem Williama Crawforda, który
pracował w tym czasie nad książką „Java Enterprise in a Nutshell”, czy pomógłby mi w przyśpieszeniu pracy nad
książką. Wspaniałomyślnie zgodził się on i pomógł w napisaniu dwóch rozdziałów, a także części dodatków.
Wielu innych ludzi pomogło mi w napisaniu niniejszej książki, zarówno bezpośrednio jak i pośrednio.
Chciałbym podziękować Pauli Ferguson, redaktorowi książki oraz Mike'owi Loukidesowi, redaktorowi serii
Java, za ich starania o zapewnienie (i poprawę) jakości tej książki. Oraz Timowi O'Reilly za danie mi szansy
spełnienia marzeń.
Dziękuję również moim menedżerom w firmie Silicon Graphics, Kathy Tansill i Waltowi Johnsonowi, za
dostarczenie większej pomocy i elastyczności niż miałem prawo się spodziewać.
Każde podziękowania są niewystarczające dla inżynierów firmy Sun, którzy odpowiadali na niezliczone pytania,
informowali mnie o zmianach w Servlet API i naprawiali niemal każdy błąd, jaki zgłosiłem — są to James
Duncan Davidson (Wyglądający niemal jak James Gosling), Jim Driscoll, Rob Clark i Dane Brownell.
Dziękuję również członkom listy dystrybucyjnej jserv-interest, których pytania i odpowiedzi ukształtowały
zawartość tej książki; Willowi Rameyowi, staremu przyjacielowi, który nie pozwolił, aby przyjaźń przesłoniła
jego krytyczne oko; Mike'owi Engberowi, człowiekowi, do którego zwróciłem się po ucieczce z eleganckich
miejsc pracy i byłem gotowy na zaakceptowanie jego szalonych pomysłów; Dave'owi Vandergriftowi, pierwszej
osobie, która przeczytała wiele rozdziałów; Billowi Dayowi, autorowi „Java Media Players”, który pomagał
poprzez przechodzenie przez proces tworzenia książki równolegle ze mną; Michaelowi O'Connellowi i Jill
Steinberg, redaktorom „JavaWorld”, dzięki którym napisałem mój pierwszy profesjonalny tekst; Dougowi
Youngowi, który dzielił się za mną technicznymi sztuczkami poznanymi przy pisaniu siedmiu własnych książek
technicznych oraz Shoji Kuwabara'rze, Mieko Aono, Song'owi Yung'owi, Matthew Kim'owi oraz Alexandrowi
Pashintsev'owi za ich pomoc w przetłumaczeniu skryptu „Witaj Świecie” w rozdziale 13.
Chciałbym gorąco podziękować recenzentom technicznym książki, których konstruktywny krytycyzm pomógł
znacznie w usprawnieniu pracy — są to Mike Slinn, Mike Hogarth, James Duncan Davison, Dan Protchett, Dave
McMurdie i Rob Clark. Ciągle jestem w szoku, po tym jak dowiedziałem się, że jednemu recenzentowi zabrało
trzy dni, aby przeczytać to, nad czego stworzeniem pracowaliśmy rok!
Ostatecznie, dziękuję Mamie i Tacie, za ich miłość i wsparcie i za czas, który poświęciliście dawno temu dna
nauczenie mnie podstaw pisania. Dziękuję też Kristi Taylor, która sprawiła, że ta niewielka część czasu, która nie
była wypełniona pracą, stała się przyjemnością.
Oraz Dziadkowi, chciałbym, żebyś mógł to zobaczyć.
Jason Hunter
Czerwiec 1998
Po pierwsze dziękuję Shelley Norton, dr Isaacowi Kohane, dr Jamesowi Facklerowi i dr Richardowi Kitzowi (a
także pozostałej części zespołu, której wkład pozostaje nieoceniony), których pomoc i wsparcie sprawiła, że
wszystko to stało się możliwe. A także Martinowi Streeterowi z firmy Invantage, Inc., za jego wsparcie w trakcie
trwania tego projektu.
Bez Roba Leitha, Rogera Stacey i Freda Sterbeigha, przypuszczalnie ciągle trwałbym w stronie biernej. Dale
Dogherty zaoferował mi pieniądze w zamian za słowa, wydarzenie, którego ciągle nie potrafię pojąć. Andy
Kwak, Joel Pomerantz i Matthew Proto, wspaniali ludzie, zechcieli przeczytać próbne wydruki i słuchać skarg o
godzinie pierwszej w nocy.
I, oczywiście Mamie i Tacie za ich lata wsparcia, oraz mojej siostrze Faith za (zazwyczaj) wybaczanie mi bycia
durniem.
William Crawford
Lipiec 1998
Rozdział 1. Wprowadzenie

Rozwój aplikacji Javy działających po stronie serwera — wszystko od działających samodzielnie serwletów do
pełnej platformy Java 2, Enterprise Edition (J2EE) — był jednym z najbardziej ekscytujących trendów w
programowaniu Javy. Język Java został utworzony pierwotnie w celu zastosowania w małych, osadzonych
urządzeniach. Był on opisywany jako język do tworzenia zawartości WWW po stronie klienta w formie apletów.
Jednak aż do kilku ostatnich lat potencjał Javy jako platformy do programowania po stronie serwera był niestety
pominięty. Aktualnie Java jest uważana za język idealnie nadający się do programowania po stronie serwera.
Szczególnie szybko rozpoznały potencjał Javy w serwerach firmy biznesowe — Java idealnie pasuje do dużych
aplikacji typu klient-serwer. Niezależna od platformy natura Javy jest niezwykle użyteczna dla organizacji
posiadających heterogeniczny zbiór serwerów pracujących pod różnymi odmianami systemów operacyjnych
UNIX i Windows (oraz coraz bardziej Mac OS X). Nowoczesny, obiektowy i chroniący pamięć projekt Javy
pozwala programistom na skrócenie cyklów programistycznych i zwiększenie niezawodności. Dodatkowo,
wbudowana w Javę obsługa sieci i interfejsów korporacyjnych dostarcza możliwości dostępu do starych danych,
ułatwiając przejście ze starszych systemów klient-serwer.
Serwlety Javy są kluczowym składnikiem programowania Javy po stronie serwera. Serwlet to małe, dołączane
rozszerzenie serwera, które rozszerza jego funkcjonalność. Serwlety pozwalają programistom na rozszerzanie i
dostosowywanie każdego serwera WWW lub aplikacji z obsługą Javy do wcześniej nieznanego poziomu
przenośności, elastyczności i łatwości. Jednak przed przejściem do szczegółów, należy spojrzeć na sprawę z
pewnej perspektywy.

Historia aplikacji WWW


Chociaż serwlety mogą być wykorzystywane do rozszerzenia funkcjonalności każdego serwera z obsługą Javy,
najczęściej używane są do rozszerzania serwerów WWW, stanowiąc potężny i wydajny zamiennik dla skryptów
CGI. Kiedy wykorzystuje się serwlet do utworzenia dynamicznej zawartości strony WWW lub podniesienia w
inny sposób funkcjonalności serwera WWW, w efekcie tworzy się aplikację WWW. Podczas, gdy strona WWW
wyświetla jedynie zawartość statyczną i pozwala użytkownikowi na nawigację poprzez tę zawartość, aplikacja
WWW dostarcza doświadczenia bardziej interaktywnego. Aplikacja WWW może być tak prosta jak
wyszukiwanie słowa kluczowego w archiwum dokumentów lub tak złożona, jak sklep elektroniczny. Aplikacje
WWW są umieszczane w Internecie oraz korporacyjnych sieciach intranet i extranet, w których posiadają one
potencjał do zwiększania produktywności i zmiany sposobu prowadzenia biznesu przez małe i duże firmy.
Aby zrozumieć potęgę serwletów, należy cofnąć się i spojrzeć na pewne inne podejścia do tworzenia aplikacji
WWW.

Common Gateway Interface


Common Gateway Interface (Wspólny Interfejs Bramek), w skrócie CGI, był jednym z pierwszych praktycznych
technik tworzenia zawartości dynamicznej. Przy pomocy CGI serwer WWW przekazuje konkretne żądania do
programu zewnętrznego. Wynik tego programu jest później przesyłany do klienta w miejscu statycznego pliku.
Powstanie CGI pozwoliło na implementację wielu nowych rodzajów funkcjonalności na stronach WWW, a CGI
szybko stał się de facto standardem, zaimplementowanym w ogromnej ilości serwerów WWW.
Interesujący jest fakt, że możliwość tworzenia przez programy CGI dynamicznych stron WWW jest ubocznym
efektem ich początkowego przeznaczenia — zdefiniowania standardowej metody porozumiewania się serwera
informacji z aplikacjami zewnętrznymi. To źródło wyjaśnia, dlaczego CGI posiada przypuszczalnie najgorszy do
wyobrażenia okres trwałości. Kiedy serwer otrzymuje żądanie, które uzyskuje dostęp do programu CGI, musi on
utworzyć nowy proces w celu uruchomienia programu CGI i potem przekazania mu, poprzez zmienne
środowiskowe i standardowe wpisy, każdego bitu informacji, który może być potrzebny do wygenerowania
odpowiedzi. Tworzenie procesu dla każdego takiego żądania wymaga czasu i znaczących zasobów serwera, co
ogranicza liczbę żądań, które serwer może obsługiwać równocześnie. Rysunek 1.1 przedstawia okres trwałości
CGI.

Rysunek 1.1. Okres trwałości CGI


Chociaż program CGI może być utworzony w prawie każdym języku, język programowania Perl stał się
podstawową opcją. Zaawansowane możliwości formatowania tekstu Perla stanowią ogromną pomoc w
zarządzaniu szczegółami interfejsu CGI. Tworzenie skryptu CGI w Perlu pozwala na uniezależnienie o
platformy, ale wymaga również uruchomienia osobnego interpretatora Perla dla każdego żądania, co zabiera
jeszcze więcej czasu i wymaga dodatkowych zasobów.
Innym często przeoczonym problemem z CGI jest niemożność interakcji programu CGI z serwerem WWW lub
skorzystania z możliwości serwera po rozpoczęciu działania tego programu, ponieważ działa on jako osobny
proces. Na przykład, skrypt CGI nie potrafi zapisywać informacji w dzienniku zdarzeń serwera. Większa ilość
informacji na temat programowania CGI jest dostępna w książce „CGI Programming on the World Wide Web”
autorstwa Shishira Gundavarama (O'Reilly).

FastCGI
Firma o nazwie OpenMarket stworzyła alternatywę dla standardu CGI o nazwie FastCGI. W większości
aspektów, FastCGI działa podobnie do CGI — ważną różnicą jest tworzenie przez FastCGI jednego trwałego
procesu dla każdego programu FastCGI, jak przedstawiono na rysunku 1.2. Eliminuje to konieczność tworzenia
nowego procesu dla każdego żądania.

Rysunek 1.2. Okres trwałości FastCGI


Chociaż FastCGI jest krokiem we właściwym kierunku, ciągle posiada on problem z mnożeniem się procesów —
istnieje co najmniej jeden proces dla każdego programu FastCGI. Jeżeli program FastCGI musi obsługiwać
żądania równoległe, potrzebuje puli procesów, jednego na każde żądanie. Pamiętając, że każdy proces może
wykonywać interpretator Perla, podejście na to nie jest skalowalne na tyle, na ile można się tego spodziewać.
(Chociaż trzeba przyznać, że FastCGI może rozkładać procesy pomiędzy wieloma serwerami.) Innym
problemem z FastCGI jest to, że nie pozwala on swoim programom na bliższą interakcję z serwerem. Poza tym,
programy FastCGI są przenośne jedynie tak, jak język, w którym zostały napisane. Większa ilość informacji na
temat FastCGI jest dostępna pod adresem http://www.fastcgi.com.
PerlEx
PerlEx, utworzony przez ActiveState, zwiększa wydajność skryptów CGI napisanych w Perlu pracujących na
serwerach WWW pod Windows NT (Internet Information Server Microsoftu, Website Professional O'Reilly oraz
FastTrack Server i Enterpise Server iPlanet). Posiada on zalety i wady podobne do FastCGI. Większa ilość
informacji na temat PerlEx dostępna jest pod adresem http://www.activestate.com/plex.

mod_perl
Przy korzystaniu z serwera WWW Apache, inną opcją zwiększenia wydajności CGI jest wykorzystanie
mod_perl. mod_perl jest modułem serwera Apache osadzającym kopię interpretatora Perla w pliku
wykonywalnym Apache'a, dostarczającym pełnej funkcjonalności Perla wewnątrz Apache'a. Jego efektem jest
prekompilowanie skryptów CGI przez serwer i wykonywanie ich bez rozdzielania, a związku z tym ich praca jest
dużo szybsza i wydajniejsza. Jego wadą jest możliwość wykorzystania jedynie w serwerze Apache. Większa
ilość informacji na temat mod_perl jest dostępna pod adresem http://perl.apache.org.

Inne rozwiązania
CGI/Perl posiada zaletę bycia mniej lub bardziej niezależnym od platformy sposobem na tworzenie dynamicznej
zawartości WWW. Inne dobrze znane technologie tworzenia aplikacji WWW takie jak ASP i JavaScript
działający po stronie serwera, są opatentowanymi rozwiązaniami pracującymi jedynie z określonymi serwerami
WWW.

Interfejsy rozszerzeń serwera


Kilka firm utworzyło własne interfejsy API rozszerzeń serwera dla swoich serwerów WWW. Na przykład,
iPlanet/Netscape dostarcza wewnętrzny API o nazwie WAI (dawniej NSAPI), a Microsoft dostarcza ISAPI. Przy
pomocy każdego z tych interfejsów, można utworzyć rozszerzenia serwera zwiększające lub zmieniające jego
podstawową funkcjonalność, pozwalającą mu na obsługę zadań wcześniej delegowanych do zewnętrznych
programów CGI. Jak można dostrzec na rysunku 1.3, rozszerzenia serwera występują wewnątrz głównego
procesu serwera WWW.

Rysunek 1.3. Okres trwałości rozszerzeń serwera


Ponieważ specyficzne dla serwera interfejsy wykorzystują połączony kod C lub C++, rozszerzenia serwera
działają niezwykle szybko i w pełni wykorzystują zasoby serwera. Nie są one jednak rozwiązaniem doskonałym.
Poza tym, że są one ciężkie w tworzeniu i utrzymaniu, stanowią poważne zagrożenia dla bezpieczeństwa i
niezawodności — załamanie rozszerzenia może prowadzić do załamania całego serwera, złośliwe rozszerzenie
może kraść hasła użytkowników i numery kart kredytowych. Oraz, oczywiście konkretne rozszerzenia są
nierozerwalnie związane z API serwera, dla którego zostały napisane, a także do konkretnego systemu
operacyjnego.

JavaScript działający po stronie serwera


iPlanet/Netscape posiada również technikę skryptów działających po stronie serwera, nazywaną server-side
JavaScript, w skrócie SSJS. Podobnie jak ASP, SSJS pozwala na osadzanie fragmentów kodu w stronach HTML
w celu utworzenia dynamicznej zawartości WWW. Różnica jest taka, że SSJS wykorzystuje JavaScript jako
język skryptowy. Z SSJS, strony są prekompilowane w celu poprawienia wydajności. Obsługa SSJS jest możliwa
jedynie w serwerach iPlanet/Netscape. Większa ilość informacji na temat programowania przy pomocy
JavaScript działającego po stronie serwera dostępna jest pod adresem
http://developer.netscape.com/tech/javascript/ssjs/ssjs.html.

Active Server Pages


Microsoft posiada technikę tworzenia dynamicznej zawartości WWW o nazwie Active Server Pages (Aktywne
Strony Serwera), w skrócie ASP. Przy pomocy ASP, strona HTML może zawierać fragmenty osadzonego kodu
(zazwyczaj VBScript lub Jscript — chociaż możliwe jest zastosowanie niemal każdego języka). Kod ten jest
odczytywany i wykonywany przez serwer WWW przed wysłaniem strony do klienta. ASP został
optymalizowany do tworzenia niewielkich porcji zawartości dynamicznej, większe pozostawiając składnikom
COM.
Obsługa ASP jest wbudowana w Internet Information Server Microsoftu w wersji 3.0 i wyższych, dostępnym
bezpłatnie pod adresem http://www.microsoft.com/iis. Obsługa innych serwerów WWW jest dostępna jako
komercyjny produkt firmy Chili!Soft pod adresem http://www.chilisoft.com. Proszę pamiętać, że strony ASP
działające na platformie nie będącej Windows mogą mieć problemy z powodu braku biblioteki Windows COM.
Większa ilość informacji na temat programowania ActiveServerPages jest dostępna pod adresem
http://www.microsoft.com/workshop/server/default.asp oraz http://www.activeserverpages.com/.

JavaServer Pages
JavaServer Pages, często nazywana po prostu JSP, jest opartą na Javie alternatywą dla ASP, utworzoną i
wystandaryzowaną przez firmę Sun. JSP wykorzystuje składnię podobną do ASP poza tym, że językiem
skryptowym w tym przypadku jest Java. Odwrotnie niż ASP, JSP jest otwartym standardem implementowanym
przez wielu producentów na wszystkich platformach. JSP jest ścisłe związana z serwletami, ponieważ strona JSP
jest przetwarzana do serwletu, co stanowi część jej wykonania. JSP jest bardziej szczegółowo opisana w
dalszych częściach niniejszej książki. Większa ilość informacji na temat JSP jest dostępna pod adresem
http://java.sun.com/products/jsp.

Serwlety Javy
W tym miejscu pojawiają się serwlety Javy. Jak wspomniano wcześniej, serwlet jest ogólnym rozszerzeniem
serwera — klasą Javy, która może być dynamicznie ładowana w celu rozszerzenia funkcjonalności serwera.
Serwlety są często używane w serwerach WWW, gdzie zajmują miejsce skryptów CGI. Serwlet jest podobny do
poprzednio omawianego rozszerzenia serwera, poza tym, że działa on wewnątrz wirtualnej maszyny Javy (Java
Virtual Machine — JVM) na serwerze (proszę spojrzeć na rysunek 1.4), tak więc są one bezpieczne i przenośne.
Serwlety działają wyłącznie w domenie serwera — inaczej niż aplety, nie wymagają one obsługi Javy przez
przeglądarkę WWW.

Rysunek 1.4. Okres trwałości serwletu


Inaczej niż CGI i FastCGI, które muszą wykorzystywać wiele procesów w celu obsługi oddzielnych programów
i/lub oddzielnych żądań, serwlety mogą być obsługiwane przez osobne wątki w tym samym procesie, z wieloma
procesami rozciągniętymi na klika serwerów wspierających. Oznacza to, że serwlety są również wydajne i
skalowalne. Ponieważ serwlety działają z komunikacją dwustronną do serwera WWW, mogą bardzo ściśle
współpracować z serwerem w celu wykonania działań niemożliwych dla skryptów CGI.
Inną zaletą serwletów jest ich przenośność — zarówno pomiędzy systemami operacyjnymi jak w przypadku
Javy, jak i pomiędzy serwerami WWW. Jak zostanie to opisane poniżej, większość głównych serwerów WWW
obsługuje serwlety. Uważa się, że serwlety stanowią najlepszą możliwą platformę dla tworzenia aplikacji WWW,
a więcej informacji na ten temat zostanie podane w dalszej części tego rozdziału.

Obsługa serwletów
Podobnie jak sama Java, serwlety zostały zaprojektowane w celu zapewnienia maksymalnej przenośności.
Serwlety obsługiwane są przez wszystkie platformy obsługujące również Javę, oraz pracują w większości
podstawowych serwerów WWW2. Serwlety Javy zdefiniowane przez dział Java Software firmy Sun
Microsystems (dawniej znany jako JavaSoft), stanowią Pakiet Opcjonalny (Optional Package) dla Javy (dawniej
znany jako Rozszerzenie Standardowe — Standard Extension). Oznacza to, że serwlety zostały oficjalnie
pobłogosławione przez Sun'a i stanowią część języka Java, lecz nie są częścią podstawowego API Javy. Zamiast
tego, są one znane jako część platformy J2EE.
W celu ułatwienia tworzenia serwletów, Sun i Apache udostępniły klasy API niezależnie od żadnego
mechanizmu WWW. Pakiety javax.servlet i javax.servlet.http składają się na Servlet API.
Najnowsza wersja tych klas jest dostępna do pobrania pod adresem
http://java.sun.com/products/servlet/download.html3. Wszystkie serwery WWW obsługujące serwlety muszą
wykorzystywać te klasy wewnętrznie (chociaż mogą stosować alternatywną implementację), tak więc generalnie
ten plik JAR może zostać znaleziony gdzieś wewnątrz dystrybucji serwera WWW obsługującego serwlety.
Nie jest ważne, skąd pobiera się klasy serwletów, należy posiadać je jednak w swoim systemie w celu
kompilowania serwletów. Dodatkowo konieczny jest program uruchamiający serwlety (technicznie nazywany
kontenerem serwletów, czasami mechanizmem serwletów), w celu przetestowania i udostępnienia serwletów.
Wybór kontenera serwletów zależy po części od działającego w danym systemie serwera(ów) WWW. Istnieją
trzy odmiany kontenerów serwletów — samodzielne, dołączane i osadzane.

Samodzielne kontenery serwletów


Samodzielny kontener serwletów to serwer zawierający wbudowaną obsługę serwletów. Taki kontener posiada tę
przewagę, że wszystko w nim działa niejako od razu. Jednak wadą jest konieczność oczekiwania na nową wersję
serwera WWW w celu uzyskania obsługi najnowszych serwletów. Inną wadą jest także fakt, że producenci
serwerów generalnie obsługują jedynie JVM dostarczoną przez samych siebie. Serwery WWW dostarczające
samodzielnej obsługi to między innymi:
• Tomcat Server Apache, oficjalna wzorcowa implementacja sposobu obsługi serwletów przez kontener.
Napisany całkowicie w Javie, dostępny bezpłatnie w licencji Open Source. Dostępny jest cały kod
źródłowy i każdy może pomóc w jego tworzeniu. Serwer ten może działać samodzielnie lub jako
dodatek dostarczający obsługi serwletów Apache'owi lub innym serwerom. Może być również
wykorzystywany jako kontener osadzony. Równolegle z Tomcatem, Apache tworzy standardową
implementację pakietów javax.servlet i javax.servlet.http. W trakcie pisania niniejszej
książki serwlety są jedynymi pakietami java.* lub javax.* utrzymywanymi jako Open Source4.
Proszę zobaczyć http://jakarta.apache.org.
• iPlanet Web Server Enterprise Edition Netscape'a (wersja 4.0 i późniejsze), przypuszczalnie
najpopularniejszy serwer WWW zawierający wbudowaną obsługę serwletów. Niektóre testy wykazują,
że posiada on najszybszą implementację serwletów. Proszę pamiętać, że chociaż wersje 3.51 i 3.6
zawierały wbudowaną obsługę serwletów, to jednak był to wczesny Servlet API 1.0 i zawierały one
dużą liczbę błędów tak poważnych, że obsługa serwletów była praktycznie bezużyteczna. W celu
wykorzystania serwletów z serwerami Netscape'a w wersji 3.x należy wykorzystać dołączany kontener.
Proszę zobaczyć http://www.iplanet.com.

2
Proszę zauważyć, że niektórzy producenci serwerów WWW posiadają swoje własne implementacje Javy działającej po
stronie serwera, niektóre z nich noszą również nazwę serwletów. Są one generalnie niekompatybilne z serwletami Javy
utworzonymi przez Sun'a. Większość z tych producentów konwertuje swoją obsługę Javy do standardowych serwletów lub
wprowadzają obsługę standardowych serwletów równolegle, w celu zapewnienia wstecznej kompatybilności.
3
W pewnym momencie planowano dołączenie tych klas do JDK 1.2. Później jednak zdecydowano na utrzymanie ich
niezależności od JDK w celu ułatwienia dokonywania poprawek do Servlet API.
4
Implementacja javax.servlet i javax.servlet.http w standardzie Open Source spowodowała naprawienie
wielu błędów (na przykład, autor miał okazję poprawić obsługę warunkowego GET w HttpServlet) i kwestii
niekompatybilności. Istnieje nadzieja, że przykład ten wspomoże w udostępnieniu większej ilości oficjalnych pakietów Javy
jako Open Source
• WebSite Professional O'Reilly, o podobnej funkcjonalności do Enterprise Server iPlanet, lecz za niższą
cenę. Proszę zobaczyć http://website.oreilly.com.
• Zeus Web Server, serwer WWW uważany przez niektórych za najszybszy z dostępnych. Jego lista
własności jest dość długa i zawiera obsługę serwletów. Proszę zobaczyć http://www.zeus.co.uk.
• Resin Caucho, kontener Open Source, uważany za bardzo wydajny. Może być uruchamiany w trybie
samodzielnym lub jako dodatek do wielu serwerów. Proszę zobaczyć http://www.caucho.com.
• LiteWebServer Gefion Software, niewielki (nieco ponad 100 KB) kontener serwletów utworzony dla
zastosowań, takich jak dołączanie do wersji demonstracyjnych, gdzie niewielki rozmiar ma znaczenie.
Proszę zobaczyć http://www.gefionsoftware.com/LiteWebServer.
• Jigsaw Server World Wide Web Consortium Open Source, napisany całkowicie w Javie. Proszę
zobaczyć http://www.w3.org/Jigsaw.
• Java Web Server firmy Sun, serwer, od którego wszystko się rozpoczęło. Serwer ten był pierwszym
serwerem implementującym serwlety oraz działał jako efektywna wzorcowa implementacja dla Servlet
API 2.0. Jest on napisany całkowicie w Javie (poza dwoma bibliotekami kodu macierzystego, które
powiększają funkcjonalność, lecz nie są konieczne). Sun nie kontynuuje już prac nad serwerem,
koncentrując się na produktach iPlanet/Netscape w ramach sojuszu Sun-Netscape. Proszę zobaczyć
http://java.sun.com/products.
Serwery aplikacji są rosnącym obszarem tworzenia. Serwer aplikacji oferuje obsługę po stronie serwera dla
tworzenia aplikacji korporacyjnych. Większość aplikacji opartych na Javie obsługuje serwlety i pozostałą część
specyfikacji Java 2, Enterprise Edition (J2EE).Serwery te to miedzy innymi:
• WebLogic Application Server BEA System, jeden z pierwszych i najsłynniejszych opartych na Javie
serwerów aplikacji. Proszę zobaczyć http://www.beasys.com/products/weblogic.
• Orion Application Server, wysoko wydajny serwer o stosunkowo niskiej cenie, napisany całkowicie w
Javie. Proszę zobaczyć http://www.orionserver.com.
• Enhydra Application Server, serwer Open Source firmy Lutris. Proszę zobaczyć
http://www.enhydra.org.
• Borland Application Server 4, serwer ze specjalnym naciskiem na technologię CORBA. Proszę
zobaczyć http://www.borland.com/appserver.
• WebSphere Application Server IBM, wysokowydajny serwer oparty w części na kodzie Apache'a.
Proszę zobaczyć http://www-4.ibm.com/software/webservers.
• Dynamo Application Server 3 ATG, kolejny wysokowydajny serwer napisany całkowicie w Javie.
Proszę zobaczyć http://www.atg.com.
• Application Server Oracle, serwer zaprojektowany do integracji z bazą danych Oracle. Proszę zobaczyć
http://www.oracle.com/appserver.
• iPlanet Application Server, zgodny z J2EE większy brat iPlanet Web Server Enterprise Edition. Proszę
zobaczyć http://www.iplanet.com/products/infrastructure/app_servers/nas.
• GemStone/J Application Server, serwer Javy stworzony przez firmę poprzednio znaną z serwera
Smalltalk. Proszę zobaczyć http://www.gemstone.com/products/j.
• Jrun Server Allaire (poprzednio Live Software), prosty kontener serwletów, który rozrósł się do
zaawansowanego kontenera dostarczającego wiele technologii J2EE włączając w to EJB, JTA i JMS.
Proszę zobaczyć http://www.allaire.com/products/jrun.
• Silverstream Application Server, w pełni zgodny z J2EE serwer, który rozpoczął również od skupienia
się na serwletach. Proszę zobaczyć http://www.silverstream.com.

Dołączane kontenery serwletów


Dołączany kontener serwletów działa jako moduł rozszerzający do istniejącego serwera — dodaje obsługę
serwletów do serwera, który w oryginale nie był do tego przeznaczony, lub do serwera ze słabą lub nieaktualną
implementacją serwletów. Dołączane kontenery serwletów zostały utworzone dla wielu serwerów, między innymi
Apache'a, FastTrack Server i Enterprise Server iPlanet, Internet Information Server i Personal Web Server
Microsoftu, Website O'Reilly, Go Webserver Lotus Domino, WebSTAR StarNine oraz AppleShare IP Apple.
Dołączane kontenery serwletów to między innymi:
• ServletExec New Atlanta — moduł rozszerzający zaprojektowany do obsługi serwletów we wszystkich
popularnych serwerach na wszystkich popularnych systemach operacyjnych. Zawiera bezpłatny program
uruchomieniowy. Proszę zobaczyć http://www.servletexec.com.
• Jrun Allaire (dawniej Live Software), dostępny jako moduł rozszerzający do obsługi serwletów we
wszystkich popularnych serwerach na wszystkich popularnych systemach operacyjnych. Proszę
zobaczyć http://www.allaire.com/products/jrun/.
• Moduł Jserv projektu Java-Apache, bezpłatny kontener serwletów Open Source, który dodaje obsługę
serwletów do niezwykle popularnego serwera Apache. Tworzenie Jserv zakończyło się, a Tomcat
Server (działający jako moduł rozszerzający) jest jego następcą. Proszę zobaczyć
http://java.apache.org/.
• Tomcat Server Apache, jak opisano poprzednio. Tomcat może być dołączony do innych serwerów
takich jak Apache, iPlanet/Netscape i IIS.

Osadzane kontenery serwletów


Osadzany kontener jest ogólnie niewielką platformą programistyczną, która może być osadzana w innych
aplikacjach. Aplikacja ta staje się prawdziwym serwerem. Osadzane kontenery serwletów to między innymi:
• Tomcat Server Apache, podczas gdy ogólnie używany samodzielnie lub dołączany, może być również
osadzany w innej aplikacji, kiedy jest to potrzebne. Ponieważ serwer ten to Open Source, tworzenie
większości innych osadzanych serwerów zatrzymano.
• Nexus Web Server autorstwa Andersa Kristensena, dostępny bezpłatnie program uruchamiający
serwlety, implementujący większą część Servlet API, który może być w łatwy sposób dołączany do
aplikacji Javy. Proszę zobaczyć http://www-uk.hpl.hp.com/people/ak/java/nexus/.

Uwagi dodatkowe
Przed przejściem do następnej części należy zapamiętać, że nie wszystkie kontenery serwletów są tworzone w
jednakowy sposób. Tak więc przed wybraniem kontenera serwletów (i prawdopodobnie serwera) przy pomocy
którego udostępniane będą serwlety, należy go wypróbować, prawie do granic możliwości. Sprawdzić listy
dystrybucyjne. Należy zawsze sprawdzać, czy serwlety zachowują się tak, jak we wzorcowej implementacji
Tomcata. Można również sprawdzić, jakie narzędzia programistyczne są dostarczane, które technologie J2EE są
wspierane i jak szybko można uzyskać pomoc u producenta. W przypadku serwletów nie trzeba się martwić o
zgodność z najsłabszą implementacją, tak więc należy pobrać kontener serwletów, który posiada wszystkie
pożądane właściwości.
Kompletna, aktualna lista dostępnych kontenerów serwletów razem z ich obecnymi cenami jest dostępna pod
adresem http://www.servlets.com.

Potęga serwletów
Jak dotychczas serwlety zostały opisane jako alternatywa dla innych technologii dynamicznej zawartości WWW,
lecz nie zostało tak naprawdę powiedziane, dlaczego powinny być one, zdaniem autorów, stosowane. Co
sprawia, że serwlety są jednym z najlepszych sposobów programowania WWW? Zdaniem autorów posiadają one
kilka zalet ponad innymi podejściami, włączając w to przenośność, moc, wydajność, wytrzymałość,
bezpieczeństwo, elegancję, integrację, rozszerzalność i elastyczność. Każda z tych własności zostanie kolejno
omówiona.

Przenośność
Ponieważ serwlety są pisane w Javie według dobrze zdefiniowanego i szeroko akceptowanego API, są one w
dużym stopniu przenośne pomiędzy systemami operacyjnymi i implementacjami serwerów. Można stworzyć
serwlet na komputerze pod Windows NT i z serwerem Tomcat, po czym bez problemu udostępnić go na wysoko
wydajnym serwerze Uniksowym z iPlanet/Netscape Application Server. Stosując serwlety można naprawdę
„napisać raz, udostępniać wszędzie”.
Przenośność serwletów nie jest tak ważną sprawą jak w przypadku apletów, z dwóch powodów. Po pierwsze,
przenośność serwletów nie jest obowiązkowa. Inaczej niż w przypadku apletów, które muszą zostać
przetestowane na wszystkich możliwych platformach klientów, serwlety muszą pracować jedynie na serwerach,
które są wykorzystywane do tworzenia i udostępniania. Dopóki nie sprzedaje się swoich serwletów, nie trzeba się
martwić o kompletną przenośność. Po drugie, serwlety unikają najbardziej pełnej błędów i niespójnie
zaimplementowanej części języka Java — Abstract Windowing Toolkit (AWT), który stanowi bazę graficznych
interfejsów Javy, takich jak Swing.

Moc
Serwlety mogą wykorzystywać pełną moc jądra API Javy — pracę w sieci i dostęp do URL-i, wielowątkowość,
kompresję danych, łączność z bazami danych (JDBC), serializację obiektów, zdalne wywoływanie metod (RMI)
oraz integrację ze starymi programami (CORBA). Serwlety mogą również korzystać z platformy J2EE, która
zawiera obsługę Enterprise JavaBeans (EJBs), transakcji rozproszonych (JTS), standaryzowanych wiadomości
(JMS), wyszukiwania katalogów (JNDI) oraz zaawansowanego dostępu do baz danych (JDBC 2.0). Lista
standardowych API dostępnych serwletom rośnie, czyniąc tworzenie aplikacji WWW szybszym, łatwiejszym i
bardziej niezawodnym.
Jako autor serwletów, można wykorzystać dowolną z mnóstwa niezależnych klas Javy i składników JavaBeans.
Serwlety mogą używać niezależnego kodu w celu obsługi zadań takich jak wyszukiwanie według wyrażeń
regularnych, tworzenie wykresów danych, dostosowany dostęp do baz danych, zaawansowana praca w sieci,
analiza składniowa XML oraz tłumaczenia XSLT.
Serwlety są również sprawne w umożliwianiu komunikacji klient-serwer. Posiadając oparty na Javie aplet i
oparty na Javie serwlet, można wykorzystać RMI i serializację obiektu w komunikacji klient-serwer, co oznacza,
że ten sam kod można wykonać zarówno na maszynie klienta, jak i na serwerze. Wykorzystywanie po stronie
serwera języków innych niż Java jest znacznie bardziej skomplikowane, jako że konieczne jest tworzenie swoich
własnych protokołów do obsługi komunikacji.

Wydajność i wytrzymałość
Wywoływanie serwletów charakteryzuje się bardzo wysoką wydajnością. Kiedy serwlet zostaje załadowany,
pozostaje w pamięci serwera jako pojedynczy egzemplarz obiektu. Następnie serwer wywołuje serwlet do
obsługi żądania przy pomocy prostego wywołania metody. Inaczej niż w przypadku CGI, nie trzeba wywoływać
procesu ani interpretatora, tak więc serwlet może rozpocząć obsługę żądania niemal natychmiast. Wielokrotne,
równoległe żądania są obsługiwane przez osobne wątki, tak więc serwlety są w wysokim stopniu skalowalne.
Serwlety są obiektami z natury trwałymi. Ponieważ serwlet zostaje w pamięci serwera jako pojedynczy
egzemplarz obiektu, automatycznie zachowuje swój stan i może utrzymywać kontakt z zasobami zewnętrznymi,
takimi jak połączenia z bazami danych. W innym przypadku przywrócenie połączenia mogłoby zabrać
kilkanaście sekund.

Bezpieczeństwo
Serwlety obsługują bezpieczne praktyki programowania na różnych poziomach. Ponieważ są one pisane w Javie,
dziedziczą po niej silne bezpieczeństwo typów. Podczas gdy większość wartości w programie CGI, włączając w
to element numeryczny taki, jak numer portu serwera, są traktowane jako łańcuchy, wartości w Servlet API są
manipulowane przy pomocy ich naturalnych typów, tak więc numer portu serwera jest reprezentowany jako
integer. Automatyczne zbieranie śmieci przez Javę i brak wskaźników oznaczają, że serwlety są generalnie
bezpieczne od problemów z zarządzaniem pamięcią, takich, jak uszkodzone wskaźniki, niewłaściwe odwołania
do wskaźników oraz uszczerbki pamięci.
Serwlety mogą bezpiecznie obsługiwać błędy, dzięki mechanizmowi obsługi wyjątków lub kontrolerowi dostępu
Javy. Jeżeli serwlet wykona dzielenie przez zero lub inne nieprawidłowe działanie, wyrzuca wyjątek, który może
być bezpiecznie wychwycony i obsłużony przez serwer, który zapisze błąd w dzienniku zdarzeń i przeprosi
użytkownika. Jeżeli podobny wyjątek napotkałoby rozszerzenie serwera oparte na C++, przypuszczalnie
nastąpiłoby załamanie serwera.
Serwer może chronić siebie w większym stopniu poprzez zastosowanie menedżera bezpieczeństwa lub kontrolera
dostępu Javy. Serwer może wykonywać swoje serwlety pod ochroną dokładnego kontrolera dostępu który, na
przykład wymusza politykę bezpieczeństwa zaprojektowaną do strzeżenia przed złośliwym lub źle
zaprojektowanym serwletem dążącym do zniszczenia systemu plików serwera.
Elegancja
Elegancja kodu serwletów jest uderzająca. Kod serwletów jest czysty, obiektowy, modularny i zadziwiająco
prosty. Jednym z powodów tej prostoty jest sam Servlet API, który zawiera metody i klasy obsługujące wiele
rutynowych elementów programowania serwletów. Nawet zaawansowane operacje, takie jak obsługa cookies i
śledzenie sesji, są rozkładane na odpowiednie klasy. Kilka bardziej zaawansowanych, lecz także popularnych
zadań zostało pozostawione poza API, i w tych przypadkach autorzy próbowali to naprawić i tak powstał zbiór
przydatnych klas w pakiecie com.oreilly.servlet.

Integracja
Serwlety są ściśle zintegrowane z serwerem. Ta integracja pozwala serwletowi na współpracę z serwerem w
sposób niedostępny dla programów CGI. Na przykład, serwlet może wykorzystywać serwer w celu
przetłumaczenia ścieżek plików, dokonania logowania, sprawdzenia uwierzytelnienia oraz wykonania
odwzorowania typu MIME. Właściwe dla konkretnego serwera rozszerzenia mogą wykonać większość tej pracy,
lecz proces ten jest zazwyczaj znacznie bardziej złożony i obfity w błędy.

Rozszerzalność i elastyczność
Servlet API jest zaprojektowany w celu zapewnienia łatwej rozszerzalności. W obecnym czasie, API zawiera
klasy z wyspecjalizowaną obsługą serwletów HTTP. Lecz w późniejszym okresie może być ona rozszerzona i
zoptymalizowana dla innego typu serwletów, czy to produkcji Suna, czy innej firmy. Jest również możliwe, że
jego obsługa serwletów HTTP może być dalej rozwijana.
Serwlety cechują się również elastycznością w tworzeniu zawartości. Mogą tworzyć prostą zawartość przy
pomocy wyrażeń out.println(), lub generować skomplikowany zbiór stron przy pomocy mechanizmu
szablonów. Mogą tworzyć stronę HTML przez traktowanie strony jako zestawu obiektów Javy, lub tworzyć
stronę HTML przez wykonanie transformacji XML do HTML. Serwlety mogą być nawet łączone w celu
utworzenia całkowicie nowych technologii takich jak JavaServer Pages. Nie wiadomo, do czego jeszcze zostaną
wykorzystane.
Rozdział 2. Aplety Http —

wprowadzenie

Ten rozdział to krótki samouczek pisania i uruchamiania prostych apletów HTTP.


Opisane tutaj zostanie jak wdrożyć aplet do standardowej aplikacji WWW i jak skonfigurować jego zachowanie
przy użyciu XML-owego deskryptora rozmieszczenia.
W przeciwieństwie do pierwszej edycji niniejszej książki, ten rozdział obecnej nie opisuje opartych na apletach
plików dołączanych serwera (SSI) lub wiązania łańcuchowego oraz filtrowania apletu. Mimo tego, iż techniki te
były bardzo przydatne oraz zostały umieszczone w serwerze WWW (Java Web Server), nie zostały zatwierdzone
w specyfikacji apletu, (która ukazała się po pierwszej edycji niniejszej pozycji). SSI zostały zastąpione przez
nowe techniki tworzenia plików dołączanych programu. Wiązanie łańcuchowe apletu zostało uznane za zbyt
nieczytelne dla oficjalnego zatwierdzenia, mimo tego jest wielce prawdopodobne, że sama jego idea zostanie
wykorzystana w Interfejsie API 2.3 (Servlet API 2.3) jako część oficjalnego mechanizmu ogólnego zastosowania
przed i po — filtrującego.

Zwróćmy uwagę, iż możliwe jest ściągnięcie kodów dla każdego z przykładów zamieszczonych w tym oraz
innych rozdziałach tej książki, zarówno w formie źródłowej jak i skompilowanej (jak zostało to opisane w
przedmowie). Jednakże, co się tyczy tego rozdziału, wydaje się, iż rzeczą najbardziej pomocną w nauce będzie
zapisanie przykładów ręcznie (za pomocą klawiatury). Lektura tego rozdziału może prowadzić do wniosku, iż
niektóre zagadnienia zostały potraktowane zbyt ogólnie. Aplety to narzędzia dające wiele możliwości i czasem
bywają skomplikowane, dlatego też rozdział ten ma na celu wprowadzenie w ogólne zasady ich działania i
zorientowania się w temacie. Czytelnik po lekturze niniejszej książki będzie w stanie samodzielnie tworzyć
najrozmaitsze aplety.

Podstawy HTTP
Zanim przejdziemy do omawiania prostych apletów HTTP, musimy sprawdzić znajomość podstaw działania
protokołu HTTP. Będąc doświadczonym w programowaniu w CGI (lub mając doświadczenie w tworzeniu stron
WWW na serwerach) można z powodzeniem pominąć czytanie tego podrozdziału. W takim przypadku
korzystnym wydaje się zatrzymanie się na istotnych zagadnieniach metod GET i POST. Jednakże będąc osobą
stawiającą pierwsze kroki w tworzeniu stron WWW na serwerach, należy przeczytać wspomniany materiał
uważnie, ponieważ zrozumienie dalszej części książki wymaga znajomości protokołu HTTP. Protokół HTTP
został szczegółowo omówiony w „Pocket Reference” Clintona Wong’a (wydawnictwo O’Reilly).

Zlecenia, odpowiedzi, nagłówki


HTTP jest prostym, międzynarodowym protokołem. Klient, np. przeglądarka WWW składa zlecenie, serwer
WWW odpowiada i dokonywana jest tzw. „obsługa zlecenia”. Kiedy klient składa zlecenie, pierwszą rzeczą
którą wykonuje, jest komenda HTTP zwana metodą, za pomocą której serwer orientuje się jaki rodzaj zlecenia
jest składany. Pierwszy wiersz zlecenia określa adres dokumentu (URL) oraz używaną wersję protokółu HTTP.
Oto przykład:
GET/intro.html Http/1.0

W tym zleceniu chodzi o uzyskanie dokumentu o nazwie intro.html za pomocą wersji 1.0 HTTP. Po przesłaniu
zlecenia klient może przesłać informacje nagłówkową w celu dostarczenia serwerowi dodatkowych informacji o
zleceniu, takich jak: jakie oprogramowanie jest używane przez klienta oraz jaka forma informacji potrzebna jest
klientowi do jej zrozumienia. Takie informacje nie odnoszą się bezpośrednio do tego, co było przedmiotem
zlecenia, jednakże mogą być wykorzystane przez serwer w tworzeniu odpowiedzi. Oto dwa przykłady
nagłówków zleceń:

User-Agent : Mozilla/4.0 (compatabile; MSIE 4.0; Windows 95)


Accept : image/gif, image/jpeg, text/*, */*

Nagłówek User-Agent dostarcza informacji o oprogramowaniu klienta, podczas gdy nagłówek Accept określa
rodzaj nośnika (MIME) najkorzystniejszy dla klienta (nagłówki zleceń zostaną omówione szerzej przy
omawianiu apletów, w rozdziale 4 „Odczytywanie informacji”). Celem zaznaczenia końca sekcji nagłówkowej,
po przesłaniu nagłówków, klient przesyła nie zapisany wiersz. Jeżeli wymaga tego używana metoda, klient może
również przesłać inne dodatkowe dane, tak jak w przypadku metody POST, która zostanie zaraz omówiona. Jeżeli
zlecenie nie zawiera żadnych danych, to kończy się nie zapisanym wierszem. Po przesłaniu przez klienta
zlecenia, serwer przetwarza je i przesyła odpowiedź. Pierwszy wiersz odpowiedzi zawiera wiersz statusu oraz
jego opis. Oto przykład:
HTTP/1.0 200 OK.
Powyższy wiersz statusu zawiera kod statusu 200, co oznacza że zlecenie zostało wykonane, stąd opis OK.
Kolejny często spotykany kod to 404 z opisem Not Found (nie znaleziono), jak łatwo się domyśleć opis ten
oznacza, że dokument nie został odnaleziony. W rozdziale 5 „Przesyłanie informacji HTML” zostały omówione
najczęściej spotykane kody statusu oraz w jaki sposób można je wykorzystać w apletach. Dodatek D „Kody
statusu HTTP” zawiera kompletną listę kodów statusu HTTP. Po przesłaniu wiersza statusu serwer przesyła
nagłówki odpowiedzi, które są informacją dla klienta taką jak np.: jakiego oprogramowania używa serwer oraz
nośnika informacji użytego do zapisania odpowiedzi serwera. Oto przykład:

Date: Saturday, 23-May-00 03:25:12 GMT


Server: Tomcat Web Server/3.2
MIME-version:1.0
Content-type: text/html
Content-length: 1029
Last-modified: Thursday, 7-May-00 12:15:35 GMT
Nagłówek Server dostarcza informacji o oprogramowaniu serwera, nagłówek Content-type określa rodzaj
rozszerzenia MIME odnośnie danych zawartych w odpowiedzi (nagłówki odpowiedzi zostaną omówione szerzej
w rozdziale 5). Po nagłówkach serwer przesyła „czysty wiersz” celem zakończenia sekcji nagłówkowej. Jeżeli
zlecenie zostało wykonane, żądane dane są następnie przesyłane jako część odpowiedzi. W przeciwnym
wypadku odpowiedź może zawierać informację tekstową dla osoby obsługującej przeglądarkę, która będzie
wyjaśniała dlaczego serwer nie mógł wykonać zlecenia.

Metody GET i POST


Klient łącząc się z serwerem może złożyć zlecenie w kilku różnych formach, zwanych metodami. Najczęściej
używane metody to GET i POST. Metoda GET służy do uzyskiwania informacji (dokumentów, wykresów,
informacji bez danych), podczas, gdy metoda POST została zaprojektowana z myślą o wysyłaniu informacji
(numerów kart kredytowych, danych statystycznych lub informacji baz danych). Wykorzystując analogię
elektronicznego biuletynu informacyjnego, GET służy do czytania a POST do zamieszczania w nim tekstu. GET to
metoda używana do wprowadzania URL-u bezpośrednio do przeglądarki lub podczas klikania na hiperlink;
jednakże zarówno metoda GET jak i POST mogą być używane do dostarczania formularzy HTML.
Mimo, iż metoda GET została zaprojektowana w celu odczytywania informacji, może zawierać jako część
zlecenia informacje własne, które dokładniej precyzują to zlecenie. Może to być np. układ współrzędnych x,y dla
wykresów. Takie informacje są przesyłane jako ciąg znaków dołączonych do URL-u w formie znanej ciągiem
zapytań. Ten sposób zamieszczania dodatkowych informacji w URL-u umożliwia przesłanie strony e-mailem
bądź utworzenie z niej zakładki. Ponieważ zlecenia GET nie są przeznaczone do przesyłania dużych partii
informacji, niektóre serwery ograniczają długość URL-ów i ciągów zapytań do około 240 znaków.
W metodzie POST używana jest odmienna technika przesyłania informacji do serwera, ponieważ niekiedy
metody tej używa się do przesyłania większych partii informacji. Zlecenie złożone za pomocą metody POST
przesyła bezpośrednio wszystkie informacje (nie ograniczone co do długości) w nim zawarte za pomocą
połączenia gniazdowego jako część swego zlecenia HTTP. Klient nie jest informowany o tej zamianie, URL nie
ulega w ogóle zmianie. W efekcie zlecenia POST nie mogą być ani zapisane jako zakładki, ani wysłane e-mailem,
ani też w niektórych przypadkach nie mogą być w ogóle powtórnie załadowane. Powód jest prosty — sytuacja
taka wynika z odpowiedniego zaprojektowania — informacja jak np. numer naszej karty kredytowej, powinna
być przesyłana do serwera tylko raz. Stosując metodę POST uzyskujemy dodatkowo pewien stopień
zabezpieczenia przy przesyłaniu poufnych informacji, ponieważ dziennik zdarzeń, który zapisuje wszystkie
zgłoszenia URL-ów, nie rejestruje danych metody POST.
W praktyce użycie metod GET i POST odbiega od celu, dla którego zostały zaprojektowane. Powszechną
praktyką przy składaniu długich parametryzowanych zleceń na informacje jest użycie POST zamiast GET w celu
uniknięcia problemów związanych z nadmiernie długimi URL-ami. Metoda GET jest również często
wykorzystywana do ładowania informacji przez proste formularze ponieważ, no cóż, po prostu da się to w ten
sposób zrobić. Powyższe problemy nie są jednak aż tak dramatyczne, wystarczy tylko pamiętać, iż zlecenia GET
(z powodu tego, iż mogą być w prosty sposób zamieniane na zakładki) mogą wywołać zmianę na serwerze, za
którą odpowiedzialny będzie klient. Chodzi o to, że zlecenia GET nie powinny być używane do składania zleceń,
uaktualniania baz danych oraz do innych działań umożliwiających identyfikację klienta (w przypadku
wystąpienia zmian na serwerze).

Pozostałe metody Http


Poza GET i POST istnieje jeszcze wiele, rzadziej używanych metod HTTP, takich jak na przykład metoda HEAD.
Metoda ta jest używana przez klienta tylko do uzyskiwania nagłówków odpowiedzi, w celu określenia rozmiaru
dokumentu, czasu modyfikacji lub ogólnej dostępności. Inne metody to PUT — do zamieszczania dokumentów
bezpośrednio na serwerze, DELETE — wykorzystywana do ich usuwania stamtąd. Dwie ostatnie metody nie
współpracują ze wszystkimi serwerami z powodu skomplikowanych procedur. Metoda TRACE jest powszechnie
używana do usuwania błędów — umożliwia przesłanie klientowi dokładnej treści jego zlecenia. Metoda
OPTIONS może być użyta do zapytania serwera, z którymi metodami współpracuje lub jak dotrzeć do
poszczególnych jego zasobów.

Interfejs API (Servlet API)


Po zapoznaniu się z podstawami HTTP, możemy przejść do omówienia interfejsów API, których z kolei używa
się do tworzenia apletów HTTP, lub innych rodzajów apletów odpowiednich dla tej materii. Aplety używają klas
i interfejsów z dwóch pakietów: javax.servlet i javax.servlet.http. Pakiet javax.servlet zawiera
klasy i współpracuje ze standardowymi protokołowo–niezależnymi apletami. Klasy te są rozszerzane przez klasy
w pakiecie java.servlet.http w celu dodania funkcjonalności specyficznej dla HTTP. Pakiet najwyższej
klasy nazywa się javax zamiast zwykłego java, aby zasygnalizować, iż Interfejs API jest pakietem
dodatkowym (uprzednio zwanym Standardowym Rozszerzeniem). Każdy aplet musi wdrożyć interfejs
javax.servlet. Większość apletów wdraża ten interfejs przez rozszerzenie jednej z dwóch specjalnych klas:
javax.servlet.GenericServlet lub javax.servlet.http.HttpServlet. Aplet niezależny
protokołowo powinien być podrzędny do GenericServlet, a aplet HTTP powinien być podrzędny w stosunku
do HTTPservlet, który sam jest podklasą GenericServlet z dodana funkcjonalnością HTTP.
W przeciwieństwie do zwykłego programu Java, i dokładnie tak jak zwykły aplet, aplet wykonywany na
serwerze nie zawiera metody main(). Zamiast tego pewne metody apletów wywoływane są przez serwer w
procesie obsługi zleceń. Za każdym razem, kiedy serwer wysyła zlecenie do apletu, wywołuje jego metodę
service().
Standardowy aplet w celu poprawnej obsługi zlecenia powinien zignorować swoją metodę service(). Metoda
service() akceptuje dwa parametry: obiekt zlecenia i obiekt odpowiedzi. Obiekt zlecenia informuje aplet o
zleceniu, a obiekt odpowiedzi używany jest do wysyłania odpowiedzi. Rysunek 2.1. ukazuje jak standardowy
aplet obsługujący zlecenie.
Rysunek 2.1. Standardowy aplet obsługujący zlecenie

Aplet HTTP zwykle nie ignoruje metody service(), tylko metodę doGet() (do obsługi zleceń GET) i metodę
doPost() (do obsługi zleceń POST). Aplet HTTP może zignorować jedną z powyższych metod lub obie, w
zależności od tego, jaki jest typ zlecenia, które ma obsłużyć. Metoda service() HttpServlet obsługuje
instalację oraz transfer do wszystkich metod doXXX(), dlatego właśnie zwykle nie powinna być ignorowana. Na
rysunku 2.2 został ukazany sposób, w jaki aplet HTTP obsługuje zlecenia metod GET i POST.

Rysunek 2.2. Aplet HTTP obsługujący zlecenia GET i POST


Aplet HTTP może zignorować odpowiednio metody doPut() i doDelete() celem obsłużenia zleceń PUT i
DELETE. Jednakże aplety HTTP generalnie nie modyfikują metod doTrace() czy doOptions(). Dla nich
prawie zawsze wystarczające są implementacje domyślne.
Pozostałe klasy w pakietach javax.servlet i javax.servlet.http to w większości klasy wspomagające.
Na przykład klasy ServletRequest i ServletResponse w javax.servlet umożliwiają dostęp do zleceń i
odpowiedzi standardowych serwerów, natomiast klasy HttpServletRequest i HttpServletResponse w
javax.servlet.http umożliwiają dostęp do zleceń i odpowiedzi HTTP. Pakiet javax.servlet.http
zawiera również klasę HttpSession, która oferuje wbudowaną funkcjonalność śledzenia sesji oraz klasę
Cookie, która pozwala na szybkie instalowanie i przetwarzanie cookies.

Tworzenie strony
Najbardziej podstawowy typ apletu HTTP tworzy pełną stronę HTML. Taki aplet ma zwykle dostęp do takich
samych informacji, co przesyłane do skryptu CGI oraz do pewnej partii innych. Aplet, który tworzy strony
HTML może zostać użyty do wykonywania wszystkich zadań, tych które obecnie są wykonywane przy pomocy
CGI, jak np. przetwarzanie formularzy HTML, tworzenie listy z bazy danych, przyjmowanie zamówień,
sprawdzanie zamówień, sprawdzanie identyfikacji, itd.

Pisanie „Hello World”


Przykład 2.1 ukazuje aplet HTTP tworzący kompletną stronę HTML. Dla uproszczenia sprawy aplet ten ,za
każdym połączeniem się z nim za pomocą przeglądarki WWW, wyświetla napis „Hello World”.*

*
Pierwszy przykład zarejestrowanego programu „Hello World” pojawił się w „A Tutorial Introduction to the Language B”,
napisanym przez Braiana Kernighana w 1973. Dla tych z czytelników, zbyt młodych, by pamiętać, język B był prekursorem
języka C. Więcej informacji o języku programowania B oraz link do tej książki można znaleźć na stronie: http://cm.bell-
labs.com/who/dmr/bintro.html.
Przykład 2.1. Aplet, który wyświetla „Hello World”
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HellWorld extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

res.setContentType ("text/html");
PrintWriter out = res.getWriter ( );

out.println("<HTML>");
out.println("<HEAD><TITLE>Hello World</TITLE></HEAD>");
out.println ("<BODY>") ;
out.println (<BIG>Hello World</BIG>");
out.println ("</BODY><HTML>");
}
}
Powyższy aplet rozszerza klasę HttpServlet oraz ignoruje odziedziczoną z niej metodę doGet(). Kiedy
serwer WWW otrzymuje zlecenie GET z tego apletu, każdorazowo wywołuje metodę doGet(), przekazując mu
obiekty HttpServletRequest oraz HttpServletResponse. Obiekt HttpServletRequest reprezentuje
zlecenie klienta. Obiekt ten daje apletowi dostęp do informacji o kliencie, parametrach zlecenia, nagłówkach
HTTP przekazywanych razem ze zleceniem oraz inne. W rozdziale 4. omówione zostały wszystkie możliwości
obiektu zlecenia. Dla tego przykładu możemy jednak je pominąć jako, ze niezależnie od typu zlecenia aplet ten
wyświetla „Hello World”.
Obiekt HttpServletResponse reprezentuje odpowiedź apletu. Aplet może wykorzystać ten obiekt do
dostarczenia danych klientowi. Mogą to być dane różnego typu, typ ten jednak powinien być określony jako
część odpowiedzi. Aplet może również użyć tego obiektu do ustalenia nagłówków odpowiedzi HTTP. W
rozdziałach 5. i 6 „Przesyłanie treści multimedialnej” zostało omówione wszystko co może zrobić aplet jako
część odpowiedzi.
Nasz aplet ustala najpierw za pomocą metody setContentType() typ zawartości swojej odpowiedzi do
text/html, standardowego typu zawartości MIME dla stron HTML. Następnie używa metody getWriter() w
celu odczytania PrintWriter, międzynarodowego odpowiednika PrintStream. PrintWriter przekształca
kod UNICODE Javy na kodowanie specyficzne lokalnie. Dla kodowania lokalnego — angielskiego zachowuje się
tak jak PrintStream. Aplet używa wreszcie PrintWriter do wysłania swojego HelloWorld HTML do
klienta.

Uruchamianie „Hello World”


Do tworzenia apletów potrzebne są dwie rzeczy: pliki klasy interfejsu API, które używane są w tłumaczeniu
programu źródłowego na język wynikowy oraz pojemnik apletu np. serwer WWW, który używany jest z kolei
przy uruchamianiu apletów. Wszystkie popularne pojemniki apletów oferują pliki klasy Interfejsu API, więc
można spełnić oba warunki (potrzebne do tworzenia apletów) za jednym ładowaniem.
Istnieją dziesiątki parametrów dostępnych pojemników apletów dla wdrażania apletów, kilkanaście z nich zostało
zamieszczonych w rozdziale 1 „Wprowadzenie”. Dokonując wyboru serwera należy pamiętać, że musi on
współpracować z wersją 2.2 Interfejsu API (Servlet API 2.2) lub późniejszą. Wersja 2.2 była pierwszą wersją
Interfejsu API, która zapewniała dostęp do aplikacji WWW, jak zostało to omówione w tym rozdziale. Aktualna
lista pojemników apletów oraz tego do jakiego poziomu API zapewniają one dostęp i jest dostępna pod adresem:
http://www.servlets.com.
Tak więc zapytajmy, co należy zrobić z naszym kodem, aby zadziałał w serwerze WWW? — to zależy od
rodzaju serwera. W przykładach zaprezentowanych w tej książce występuje serwer „Apache Tomcat 3.2”, serwer
implementacji odniesienia API, napisany całkowicie w Javie, dostępny pod adresem: http://jakarta.apache.org.
Serwer „Tomcat” zawiera mnóstwo dokumentacji, w której jest wyjaśnione jego zastosowanie. W niniejszej
książce dlatego właśnie omówione zostaną tylko ogólne zasady dotyczące pracy z serwerem. Poniższe
omówienia powinny być także zgodne z innymi serwerami (niż „Tomcat”) jednakże nie można tego
zagwarantować.
Jeżeli używamy serwera „Apache Tomcat”, to powinniśmy umieścić kod źródłowy dla apletu w katalogu
server_root/webapps/ROOT/WEB-INF/classes (gdzie server_root jest katalogiem , w którym zainstalowaliśmy
nasz serwer), jest to standardowa lokalizacja plików klasy apletu. Powód, dla którego aplety występują w tym
katalogu zostanie omówiony później w tym rozdziale.
Kiedy już mamy właściwie umiejscowiony kod źródłowy HelloWorld, musimy go skompilować. To zadanie
możemy wykonać przy pomocy standardowego kompilatora javac (lub naszego ulubionego środowiska
graficznego Javy). Należy się tylko upewnić, że mamy pakiety javax.servlet i javax.servlet.http w
naszej ścieżce klasy. Pracując z serwerem „Tomcat”, wystarczy tylko zamieścić server_root/lib/servlet.jar (lub
przyszły odpowiednik) gdzieś w naszej ścieżce klasy. Nazwa pliku oraz lokalizacja zależą od serwera, więc w
razie problemów trzeba zajrzeć do dokumentacji serwera, z którym mamy do czynienia. Jeżeli wyświetlony
zostanie komunikat informujący o błędzie taki jak np. Package javax.server not found in import,
oznacza to, że pakiety apletów nie są odnajdywane przez nasz kompilator, należy więc sprawdzić nasza ścieżkę
klasy i spróbować jeszcze raz.
Teraz, kiedy już skompilowaliśmy nasz pierwszy aplet, możemy uruchomić nasz serwer i wejść do apletu.
Uruchomienie serwera nie jest rzeczą trudną, należy uaktywnić skrypt startup.sh (lub plik startowy startup.bat w
Windows) znajdujący się w katalogu server_root/bin.
To powinno wystarczyć do uruchomienia naszego serwera, jeżeli pracujemy w Solairs lub w Windows. W
przypadku pracy na innych systemach operacyjnych może zdarzyć się sytuacja, że będziemy musieli dokonać
pewnych modyfikacji w skryptach startowych. W przypadku konfiguracji domyślnych, serwer oczekuje na porcie
8080.
Istnieje wiele sposobów dostępu do apletów. Dla przykładu możemy zrobić to przez wyraźne wprowadzenie do
URL-u nazwy klasy apletu. Możemy wprowadzić URL do swojej ulubionej
przeglądarki:http://server:8080/servlet/HelloWorld. Wyraz server zamieniamy na nazwę naszego komputera
— serwera lub na localhost — jeżeli serwer jest na naszym lokalnym komputerze. Powinna zostać wyświetlona
strona podobna do tej poniżej.

Rysunek 2.3. Aplet „HelloWorld”


Jeżeli aplet byłby częścią pakietu musiałby zostać umieszczony w server_root/webapps/ROOT/WEB-
INF/package/name.HelloWorld. Nie wszystkie serwery zezwalają automatycznie na dostęp do apletów przez
użycie rodzajowego przedrostka /servlet/. Funkcja ta może zostać wyłączona ze względów bezpieczeństwa,
aby zapewnić, że dostęp do apletów jest możliwy tylko przez określoną konfigurację URL-u, w czasie
administrowania serwerem. W celu zapoznania się ze szczegółami wyłączania i załączania przedrostka /
servlet/ należy zapoznać się z dokumentacją serwera, na którym pracujemy.

Przetwarzanie danych formularzowych


Aplet „HelloWorld” nie jest zbyt skomplikowany, przejdźmy więc do rzeczy bardziej zaawansowanych. Tym
razem utworzymy aplet, który będzie pozdrawiał użytkownika jego imieniem (i nazwiskiem). Nie jest to rzecz
specjalnie skomplikowana, potrzebny jest nam najpierw formularz HTML, który spyta użytkownika o jego imię i
nazwisko. Następująca strona powinna być w tym celu wystarczająca:

<HTML>
<HEAD>
<TITLE>Introductions</TITLE>
</HEAD>
<BODY>
<FORM METHOD=GET ACTION="/servlet/Hello">
Pozwól że spytam, jak się nazywasz?
<INPUT TYPE=TEXT NAME="imię i nazwisko"><P>
<INPUT TYPE=SUBMIT>
</FORM>
</BODY>
</HTML>
Na rysunku 2.4 został ukazany sposób, w jaki strona ta zostanie wyświetlona na stronie użytkownika.
Rysunek 2.4. Formularz HTML
Formularz ten powinien znaleźć się w pliku HTML, w katalogu serwera document_root. Jest to miejsce, w
którym serwer szuka plików statycznych. Dla serweru „Tomcat” katalog ten to server_root/webapps/ROOT.
Dzięki umieszczeniu pliku w tym katalogu, może być on dostępny bezpośrednio pod adresem:
http://server:8080/form.html.
Kiedy użytkownik przesyła ten formularz, jego imię (i nazwisko) jest przesyłane do apletu „Hello” ponieważ
uprzednio zainstalowaliśmy atrybut ACTION celem wskazania ich apletowi. Formularz używa metody GET, więc
jakiekolwiek dane są dodawane do URL-u zlecenia jako pasmo zapytań. Jeżeli np. użytkownik wprowadzi imię,
nazwisko „Inigo Montoya”, URL zlecenia będzie wyglądał następująco:
http://server:8080/servlet/Hello?name=Inigo+Montoya. Przerwa pomiędzy imieniem a nazwiskiem jest
wyjątkowo kodowana przez przeglądarkę jako znak „+”, ponieważ URL nie może zawierać spacji.
Obiekt apletu: HttpServletRequest umożliwia dostęp do danych formularzowych w paśmie zapytań. Na
przykładzie 2.2 została ukazana zmodyfikowana wersja naszego apletu „Hello”, która używa swojego obiektu
zlecenia do odczytywania parametru name.
Przykład 2.2. Aplet, który „wie” kogo pozdrawia

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Hello extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throwsServletException, IOException {

res.setContentType ("text/html");
PrintWriter out = res.getWriter ( );

String name = req.getParameter ("imię i nazwisko");


out.println("<HTML>");
out.println("<HEAD><TITLE>Hello, " + name + "</TITLE><HEAD>");
out.println("<BODY">);
out.println ("HELLO, " + name);
out.println (</BODY></HTML>");
}

public String getServletInfo() {


return "aplet, który wie jak nazywa się osoba, do której + "mówi hallo";
}
}
Powyższy aplet jest niemal identyczny z apletem „HelloWorld”. Najważniejsza różnica jest taka, że teraz
wywoływane jest reg.getParameter("name") w celu ustalenia imienia i nazwiska użytkownika oraz, fakt,
że następnie to imię i nazwisko jest wyświetlane ,zamiast nieprzyjemnie bezosobowego (nie mówiąc już, że
nadmiernie obszernego pojęciowo) „World”. Metoda getParameter() daje apletowi dostęp do parametrów w
jego paśmie zapytań. Wartość zdekodowana parametru jest następnie odsyłana, lub jeżeli parametr nie był
określony — null. Jeżeli parametr został przesłany lecz nie została podana jego wartość, tak jak w przypadku
pustego pola formularzowego, getParameter() odsyła pusty wiersz. Aplet ten dołącza również metodę
getServletInfo. Aplet może zignorować tę metodę w celu odesłania informacji opisowych o nim samym,
takich jak np. cel jego działania, jego autor, wersja i (lub) prawa autorskie.
Jest to podobna sytuacja do apletu getAppletInfo(). Metoda jest używana zasadniczo w celu umieszczania
wyjaśnień na administracyjnym programie serwisowym serwera WWW, dlatego nie będzie zamieszczana w
następnych przykładach jako że wprowadza tylko niepotrzebne zamieszanie w procesie uczenia.
Wygląd apletu jest mniej więcej taki, jak na rysunku 2.5.

Rysunek 2.5. Aplet „Hello” używający danych formularzowych

Obsługa zleceń POST


Po omówieniu dwóch apletów, które wdrażają metodę doGet()możemy przejść do przerobienia naszego apletu
„Hello” w taki sposób, aby również obsługiwał zlecenia POST. Naszym zadaniem jest doprowadzenie do tego,
aby aplet zachowywał się tak samo jak przy metodzie POST, jak to miało miejsce przy GET, możemy to uzyskać
wysyłając po prostu wszystkie zlecenia POST do metody doGet() za pomocą następującego kodu:
Public void do Post (HttpServletRequest reg, HttpServletResponse res)
throws ServletException, IOException {
doGet (reg, res);
}
Teraz już aplet „Hello” jest w stanie obsłużyć przedłożenia formularzy, które używają metody POST:
<FORM METHOD=POST ACTION="/servlet/Hello">
Ogólnie rzecz biorąc, najlepiej jest kiedy aplet wdraża albo metodę doGet() albo doPost().
Aplet wybierając jedną w wyżej wymienionych metod bierze pod uwagę jaki rodzaj zlecenia będzie musiał
obsłużyć (tak, jak to zostało omówione wcześniej). Kody wprowadzane, potrzebne do wdrożenia obu metod są
niemal takie same. Różnica polega na tym, że metoda doPost() ma dodatkowo zdolność przyjmowania dużych
ilości danych wejściowych.
Zastanowimy się co by się stało, gdybyśmy połączyli się z apletem „Hello” złożywszy zlecenie POST, przed
wdrożeniem doPost() — domyślne zachowanie apletu odziedziczone po HttpServlet dla obu metod doGet
() i doPost(), to zawiadomienie klienta, iż żądany URL nie współpracuje z tą metodą.

Obsługa zleceń HEAD


Odrobina pomysłowości sprawia, że obsługa zleceń HEAD (przesyłanych przez klienta, który chce oglądać tylko
nagłówki odpowiedzi) staje się dziecinnie prosta , zaraz wyjaśnimy na czym rzecz polega. W takiej sytuacji nie
piszemy przede wszystkim metody doHead() (jakkolwiek aplet, który jest podklasą dla HttpServlet oraz
wdraża metodę doGet() automatycznie współpracuje ze zleceniami HEAD).
A oto jak się to odbywa: metoda service() HttpServlet identyfikuje zlecenie HEAD i traktuje je
odmiennie. Konstruuje mianowicie zmodyfikowany obiekt HttpServletResponse i następnie przekazuje go
razem z niezmienionym zleceniem do metody doGet(). Ta ostatnia metoda postępuje w sposób standardowy, z
tą różnicą tylko, że nagłówki, które ustala przesyła klientowi. Obiekt specjalnej odpowiedzi skutecznie tłumi
wszystkie treści wychodzące.
Mimo, iż taka strategia jest wygodna, możliwe jest również usprawnienie procesu poprzez wykrywanie zleceń
HEAD w metodzie doGet(), dzięki temu możliwa jest wcześniejsza odpowiedź (bez niepotrzebnych cyklów
pisania danych wychodzących, których nikt nie będzie czytał). Na przykładzie 2.3 został ukazany sposób , w jaki
za pomocą metody getMethod() zlecenia, można wdrożyć wspomnianą strategię w naszym serwerze „Hello”.
Przykład 2.3. Aplet „Hello” zmodyfikowany w sposób umożliwiający szybkie odpowiadanie na zlecenia HEAD
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Hello extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

// Wstaw nagłówek Typ Zawartości


res.setContentType ("text/html");

// Odeślij bezzwłocznie jeżeli to jest HEAD


if. (req.getMethod ( ).equals ("HEAD")) return;

// W przeciwnym razie postępuj


PrintWriter out = res.getWriter ( );
String name = req.getParameter ("imię i nazwisko");
out.println("<HTML>");
out.println("<HEAD><TITLE>Hello, " + name + "</TITLE><HEAD>");
out.println("<BODY">);
out.println ("HELLO, " + name);
out.println (</BODY></HTML>");
}
}
Zwróćmy uwagę, iż nawet wtedy, kiedy obsługujemy zlecenia HEAD, wstawiamy nagłówek Content-Type.
Nagłówki takie są odsyłane do klienta. Niektóre wartości nagłówków, takie jak np. Content-Lenght mogą nie
być dostępne do czasu wygenerowania całej odpowiedzi. Skuteczność takiego skrótu może być ograniczona ,
należy więc być ostrożnym jeżeli chce się precyzyjne określać wartości nagłówków.
Jest rzeczą ważną ażeby zakończyć obsługę zlecenia instrukcją return. Nie należy wywoływać System.exit
(), ponieważ grozi to wyjściem z serwera.

Aplikacje WWW
Aplikacje WWW (czasem w skrócie określana web app) to zbiór apletów, stron systemów Javy (JSPs),
dokumentów HTML, obrazów, szablonów oraz innych zasobów sieci WWW, które zostały skonfigurowane w
taki sposób , aby mogły być łatwo zainstalowane na każdym serwerze WWW udostępniającym aplety. Dzięki
standaryzacji umiejscowienia plików w aplikacji WWW oraz standaryzacji formatu konfiguracji pliku, aplikacja
WWW może być przesyłana z jednego serwera na drugi bez potrzeby dodatkowego zarządzania serwerem.
Czasy, kiedy do zainstalowania komponentów sieciowych innej firmy potrzebne było mnóstwo stronic
zawierających szczegółowe instrukcje, odmienne dla każdego typu serwera należą już do przeszłości.
Wszystkie pliki w server_root/webapps/ROOT należą do jednej aplikacji WWW (aplikacji głównej). Pliki te,
celem uproszczenia instalacji, mogą zostać zebrane w jeden plik archiwalny i zainstalowane na innym serwerze
— poprzez samo umieszczenie tego pliku w określonym katalogu. Pliki te mają rozszerzenie .war (skrót od: web
application archive). Pliki WAR to właściwie pliki JAR (utworzone przy pomocy programu narzędziowego jar)
zapisane w zastępczym rozszerzeniu. Zastosowanie formatu JAR pozwala na przechowywanie plików WAR w
formie skompresowanej oraz opatrywanie ich zawartości podpisem elektronicznym. Zamiast rozszerzenia .war
wybrano rozszerzenie .jar, żeby zasygnalizować użytkownikom oraz narzędziom, których używają, iż pliki te
należy traktować inaczej.
Struktura organizacji plików w aplikacji WWW została ściśle określona. Na przykładzie 2.4 został ukazany
listing plików.
Przykład 2.4. Struktura organizacji plików w aplikacji WWW
index.html
feedback.jsp
images/banner.gif
images/jumping.gif
WEB-INF/web.xml
WEB-INF/lib/bhawk4j.jar

WEB-INF/classes/Myservlet.class
WEB-INF/classes/com/mycorp/frontend/CorpServlet.class
WEB-INF/classes/com/mycorp/frontend/SupportClass.class
Taka hierarchia może być utrzymana jako oddzielne pliki w katalogu jakiegoś serwera lub też pliki te mogą
zostać zapisane jako plik WAR. Przy Instalowaniu ta aplikacja WWW może zostać przekształcona z
jakimkolwiek przedrostkiem pliku URI na serwerze. Aplikacja WWW obsługuje wtedy wszystkie zlecenia
mające taki przedrostek. Jeżeli na przykład poprzednia struktura plików została utworzona pod przedrostkiem /
demo, serwer użyje tej aplikacji WWW w celu obsłużenia wszystkich zleceń zaczynających się od /demo.
Odpowiedzią dla zlecenia na demo/index/.html będzie plik index.html z aplikacji WWW. Zlecenie na /
demo/feedback.jsp lub /demo/images/banner.gif zostało by także obsłużone z aplikacji WWW.
Katalog WEB-INF
Katalog WEB-INF jest wyjątkowy — pliki nie są przesyłane bezpośrednio do klienta, zamiast tego zawierają one
klasy Java oraz informacje o konfiguracji dla aplikacji WWW. Katalog ten zachowuje się jak katalog META-INF
pliku JAR: zawiera meta-informacje o treściach archiwalnych.
Katalog WEB-INF/classes zawiera pliki klas dla apletów aplikacji WWW oraz klasy wspomagające. WEB-
INF/lib zawiera klasy przechowywane jako pliki JAR. Dla ułatwienia programy ładujace klasy serwera maja
automatyczny wgląd do WEB–INF/classes oraz do WEB–INF/lib podczas ładowania klas, wiec nie trzeba
wykonywać dodatkowych czynności przy instalowaniu tej aplikacji.
Aplety w tej aplikacji WWW mogą zostać wywołane przy użyciu URI , takich jak np.
/demo/servlet/Myservlet oraz /demo/ servlet/com.mycorp.frontend.CorpServlet. Zwróćmy uwagę , iż każde
zlecenie na tą aplikacje zaczyna sie od /demo nawet zlecenia na aplety. W przypadku serwera „Tomcat”
server_root/webapps/ROOT jest kontekstem domyślnym przekształconym na główną ścieżkę dostępu nazwy
pliku. Oznacza to, iż dostęp do apletów znajdujących się w server_root/webapps/ROOT/WEB-INF/classes
możliwy jest tak, jak pokazywaliśmy wcześniej, przy użyciu przedrostka /servlet/HelloWorld. W serwerze
„Tomcat” domyślne odwzorowanie kontekstowe może zostać zmienione, a nowe odwzorowania mogą zostać
dodane przy użyciu pliku konfiguracyjnego działającego na wszystkich serwerach o nazwie
serwer_root/conf/server.xml. Na innych serwerach konfiguracja odwzorowania przebiega w inny sposób
(szczegóły można znaleźć w dokumentacji serwera).
Plik web.xml, znajdujący się w katalogu WEB-INF zwany jest deskryptorem wdrożenia. Zawiera on informacje o
konfiguracji aplikacji WWW, w której się znajduje. Jest to plik XML ze standaryzowanym DTD. DTD zawiera
ponad 50 znaczników, pozwalając tym samym na pełną kontrolę nad aplikacją WWW. Plik deskryptora
wdrożenia kontroluje rejestrację apletu, odwzorowanie URL, przyjmuje pliki i typy MIME jak również funkcje
zaawansowane, takie jak regulacja poziomu zabezpieczeń strony oraz wpływa na zachowanie się apletu w
środowisku rozproszonym. Zawartość tego pliku będzie omówiona szerzej w następnych rozdziałach.
Szczegółowo opisane DTD można znaleźć w dodatku C „Opis deskryptora wdrożenia DTD”.
Struktura pliku web.xml nie jest rzeczą istotną na ten moment. Ważny jest fakt, iż plik deskryptora wdrożenia
pozwala na wyszczególnienie informacji konfiguracyjnych w sposób niezależny od serwera, upraszczając tym
samym znacznie proces wdrażania. Deskryptory wdrożenia nie tylko umożliwiają przenoszenie prostych apletów
lecz również pozwalają ma międzyserwerowy transfer podsekcji naszych stron.
Popyt na pliki WAR będzie prawdopodobnie rozwijał się z biegiem czasu. Staną się one prawdopodobnie
integralna częścią sieci. Prawdopodobnie będzie je można również ładować i instalować i będą od razu gotowe
do pracy — bez względu na to, jaki system operacyjny jest używany przez nas lub przez nasz serwer WWW.
!!!!!!!!!!początek ramki

XML i DTD
XML to angielski skrót (Extensible Markup Language)a , który można przetłumaczyć jako rozszerzalny język
oznaczania znaczników, oznaczający rozszerzenie HTML-a. Jest to uniwersalna składnia (syntaktyka) tworzenia
struktury danych, stworzona przez Konsorcjum „World Wide Web Consortium” (W3C), mająca swój początek w
1996 roku. Od czasu swojej standaryzacji w 1998 roku podbija sieć w zawrotnym tempie.
Cechą wspólna dla XML i HTML-u jest to ,że zarówno XML, jak i HTML określają treść i „mark it up”
używając znaczników zawartych w nawiasach ostrych jak np. <title> i </title>. Jednakże XML spełnia
nieco inną rolę niż HTML — załączniki w dokumentach XML nie określają sposobu wyświetlania tekstu, lecz
raczej wyjaśniają jego znaczenie. XML jest to „rozszerzalny” język, oznaczania znaczników, ponieważ nowe
znaczniki mogą być tworzone ze swoim własnym znaczeniem, właściwym dla tworzonego dokumentu. XML
działa najlepiej w formacie pliku jednorodnego, ponieważ jest to standardowa, dobrze opisana i niezależna od
platformy technika stworzona dla danych hierarchicznych, istnieje także wiele programów narzędziowych
wspomagających odczytywanie, tworzenie oraz operowanie plikami XML.
Zasady pisania w XML-u są bardziej restrykcyjne niż w przypadku HTML-u. Przede wszystkim znaczniki XML
reagują na wielkość liter i tak inna będzie reakcja na napis <servlet>, a inna na <SERVLET>. Drugą ważną
sprawą przy pisaniu w XML-u jest to, że rozpoczynając danym oznaczeniem musimy nim także zakończyć. Tak
np. jeżeli rozpoczniemy znacznikiem rozpoczynającym <servlet> to musimy zakończyć znacznikiem
końcowym </servlet>. Dla ułatwienia pusty znacznik składniowy <servlet> może zostać użyty jako
substytut oznaczenia jednoczesnego rozpoczęcia i zakończenia, pary znaczników <servlet></servlet>.
Trzecia zasada jest taka, że elementy zagnieżdżone nie mogą się na siebie nakładać. Tak więc właściwym będzie
układ <outside><inside>data</inside></outside>, układem niewłaściwym natomiast będzie
<outside><inside>data</outside></inside>. Po czwarte, wszystkie wartości atrybutów muszą być
„wzięte” w cudzysłów (pojedynczy lub podwójny). Tak więc właściwy będzie zapis <servlet id="O"/> a
niewłaściwy <servlet id=O/>. Dokumenty (zwane well-formed) spełniające powyższe zasady, będą
całkowicie przetwarzane przez automatyczne programy narzędziowe.
Poza tymi zasadami, istnieją także sposoby szczegółowego określania struktury znaczników w pliku XML.
Specyfikacja tego typu jest określana jako Dokumentacja typu dokumentu (DTD). DTD szczegółowo precyzuje
jakie znaczniki mogą znaleźć się w pliku zgodności XML, jaki rodzaj danych mają zawierać te znaczniki oraz to
gdzie w hierarchii mają (lub muszą) znajdować się te znaczniki. Każdy plik XML może być zadeklarowany do
pewnego DTD. Pliki które całkowicie dostosowują się do DTD, do których są zadeklarowane nazywamy valid.
XML jest używany w apletach jako format pamięci przy konfiguracji plików. XML może być również używany
przez aplety przy tworzeniu treści , jak to zostało opisane w rozdziale 17 „XMLC” Więcej informacji o XML-u
można znaleźć na stronie http://www.w3.org/XML/ oraz w książce Bretta McLaughlin’a „Java and XML”
(wydawnictwo O’Relly).
!!!!!!!!!!!!!!!!koniec ramki

Deskryptory wdrożenia z prostym sposobem obsługi wielu użytkowników na tym samym serwerze są również
oferowane przez firmy zajmujące się obsługują sieci WWW. Użytkownicy mogą uzyskać kontrole nad swoimi
domenami — mogą oni nadzorować rejestrację apletu, odwzorowanie URL-u, rodzaj MIME oraz poziom
zabezpieczenia strony — bez potrzeby ogólnego dostępu do serwera.

Deskryptor wdrożenia
Prosty deskryptor wdrożenia został pokazany na przykładzie 2. Żeby sprawić, by plik ten opisywał domyślną
aplikacją WWW serwera „Tomcat” musimy go umieścić w server_root/webapps/ROOT/WEB-INF/web.xml.
Przykład 2.5.
Prosty deskryptor wdrożenia
<?xml wersja="1.0" kodowannie="ISO-8859-1"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.// DTD Web Application 2.2 // EN"
"http: // java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
<servlet>
<servlet-name>
hi
</servlet-name>
<servlet-class>
HelloWorld
</servlet-class>
</servlet>
</web-app>
Pierwszy wiersz oznacza, iż jest to plik XML 1.0 zawierający znaki ze standardu ISO-8859-1 (Latin-1) charset.
Drugi wiersz określa DTD dla pliku, pozwalając tym samym programowi bibliotecznemu, czytającemu plik na
jego weryfikację (czy jest valid) oraz sprawdzenie czy odpowiada zasadom DTD. Wszystkie pliki deskryptora
wdrożenia zaczynają się opisanymi dwoma wierszami lub bardzo podobnymi do nich.
Pozostała część tekstu — pomiędzy <web-app> a </web-app> dostarcza serwerowi informacji o tej aplikacji
WWW. Ten prosty przykład rejestruje nasz aplet „Hello World” przed nazwą hi (otaczający biały obszar jest
wycinany). Zarejestrowana nazwa jest tu umieszczana pomiędzy znacznikami <servlet-class>. Znacznik
<servlet> „spina” znaczniki <servlet-name> oraz <servlet-class>. Składnia deskryptora wdrożenia
XML wydaje się być lepiej zoptymalizowana dla odczytu automatycznego, raczej niż dla bezpośredniego (przez
człowieka).
Dlatego też większość producentów oferuje środki graficzne pomocne w procesie tworzenia web.xml. Na rynku
dostępnych jest również wiele edytorów XML, które można wykorzystać przy tworzeniu XML-u.
!!!!!!!!!!!!!!!!!!!!początek ramki
Uwaga na kolejność znaczników
Nie wszyscy uświadamiają sobie fakt, iż znaczniki w web.xml są uwarunkowane co do kolejności. Dla przykładu,
żeby wszystko działało poprawnie znacznik <servlet-name> musi znajdować się przed znacznikiem
<servlet-class>. Taka jest ich kolejność, w której zostały zdeklarowane w DTD. Analizatory sprawdzające
poprawność składni wprowadzają taką kolejność oraz deklarują dokument jako nieprawidłowy (invalid) jeżeli
kolejność nie jest zachowana. Niektóre serwery, nawet te bez analizatorów poprawności składni, mogą być po
prostu przygotowane na przyjęcie takiej kolejności, mogą więc być zdezorientowane w przypadku otrzymania
innej. Dla pewności należy upewnić się, ze wszystkie znaczniki <web-app> zostały uporządkowane we
właściwej kolejności. Niektóre znaczniki mogą, lecz nie muszą być obecne, lecz te, które zostały przez nas
zapisane muszą być we właściwym porządku. Na szczęście istnieją narzędzia, które upraszczają to zadanie
(więcej informacji znajduje się w dodatku C — pod DTD).
!!!!!!!!!!koniec ramki

Po rejestracji, oraz po ponownym uruchomieniu serwera możemy wejść do apletu „Hello World” pod URL-em:
http://server:8080/servlet/hi. Może zastanawiać fakt dlaczego ktoś miałby zadawać sobie trud rejestracji apletu
pod specjalna nazwą. Odpowiedź jest taka, że procedura ta pozwala na „zapamiętanie” przez serwer szczegółów
związanych z apletem i traktować go w szczególny sposób. Jednym z przykładów takiego traktowania jest
możliwość ustalenia wzorców URL, które wywołują zarejestrowany aplet. Klient może postrzegać URL będący
przedmiotem zlecenia , tak jak każdy inny, jednakże serwer jest w stanie stwierdzić , że zlecenie jest zgodne z
odwzorowaniem modelu (wzoru) i dzięki temu może być obsłużone przez odpowiedni aplet. Możemy dla
przykładu sprawić, że http://server:8080/hello.html wywoła aplet „HelloWorld”. Użycie odwzorowań apletu w
ten sposób, pomaga ukryć , że strona używa apletów. Użycie to również pozwala apletowi na płynne zastąpienie
istniejącej pod jakimkolwiek URL-em adresem strony, tak więc wszystkie zakładki oraz łączniki (linki) do strony
nadal będą działać. Wzory URL konfigurowane są przy użyciu deskryptora wdrożenia, tak jak zostało to
zaprezentowane na przykładzie 2.6.

Przykład 2.6.
Dołączanie odwzorowania apletu
<?xml version="1.0" kodowanie="ISO-8859-1"?>

<!DOCTYPE web-app

PUBLIC "-//Sun Microsystems, Inc.// DTD Web Application 2.2 // EN"


"http: // java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
<servlet>
<servlet-name>
hi
</servlet-name>
<servlet-class>
HelloWorld
</servlet-class>
</servlet>
<servlet –mapping>
<servlet –name>
hi
<servlet –name>
<url-pattern>
/hello.html
</url-pattern>
</servlet-mapping>
</web-app>

Powyższy deskryptor wdrożenia dodaje zapis <servlet-mapping> informując tym samym serwer, że aplet hi
powinien obsłużyć wszystkie URL-e zgodne ze wzorem dostępu do ścieżki głównej /hello.html. Jeżeli aplikacja
WWW zostanie przekształcona do głównej ścieżki „/” pozwoli to apletowi „Hello World” obsłużyć zlecenie na
http://server:8080/hello.html. Jeżeli zamiast powyższego aplikacja WWW zostanie przekształcona do ścieżki
dostępu przedrostka /greeting: aplet „Hello” obsłuży zlecenia złożone na http://server:8080/greeting/hello.html.
Różne zasady odwzorowania URL-u mogą zostać wyszczególnione w deskryptorze wdrożenia. Istnieją cztery
rodzaje (style) odwzorowania, wyszukiwane w następującej kolejności:
• Odwzorowania jawne, takie jak /hello.html lub /images/chart.gif, które nie zawierają symboli
wieloznacznych. Ten styl odwzorowania jest pomocny w zastępowaniu istniejącej strony.
• Odwzorowania ścieżki dostępu przedrostka takie jak /lite/*, /dbfile/* lub /catalog/item/* Odwzorowania te
zaczynają się od a /, a kończą na a/* i obsługują wszystkie zlecenia rozpoczynające się takimi przedrostkami
(nie licząc kontekstu ścieżki dostępu). Ten styl odwzorowania pozwala apletowi na kształtowanie całej
hierarchii sieciowej. Dla przykładu, aplet obsługujący /dbfile/* może serwować pliki z bazy danych, podczas
gdy aplet obsługujący /lite/* może przesyłać pliki z systemu plików automatycznie skompresowanych.
• Odwzorowania rozszerzenia takie jak *.wm lub .jsp. Takie odwzorowania zaczynają się od a* i obsługują
wszystkie zlecenia kończące się tym przedrostkiem. To odwzorowanie pozwala apletowi na operowanie na
wszystkich plikach określonego rozszerzenia. Dla przykładu aplet może zostać przypisany do obsługi plików
kończących się na *.jsp celem obsługi stron JavaServer (jest to de facto odwzorowanie niejawne będące
domeną specyfikacji apletu).
• Odwzorowanie domyślne /. Odwzorowanie to określa serwer domyślny dla aplikacji WWW — jeżeli inne
nie będą pasowały. Jest ono identyczne z ograniczonym odwzorowaniem ścieżki dostępu przedrostka (/*),
wyjątek stanowi fakt iż odwzorowanie to jest brane pod uwagę po odwzorowaniach rozszerzenia.
Odwzorowanie to daje kontrolę nad tym, w jaki sposób są wysyłane są podstawowe pliki — znacząca
możliwość, która jednak powinna być wykorzystywana z rozwagą. W sytuacji kolizji odwzorowań dokładne
(ścisłe) dopasowania mają pierwszeństwo przed dopasowaniami przedrostków, a dopasowanie ścieżki
dostępu przedrostka przed dopasowaniami rozszerzenia. Odwzorowanie domyślne jest wywoływane tylko
wtedy, kiedy nie występują żadne inne dopasowania. Dopasowania o dłuższym ciągu znaków w grupie
traktowane są priorytetowo przed dopasowaniami o krótszym ciągu.
Deskryptor „snippet” zaprezentowany na przykładzie 2.7 prezentuje różne odwzorowania, które mogą być użyte
do wchodzenia do apletu „Hello World”.

Przykład 2.7. Sposoby powiedzenia „Hello”


<!--... -->
<servlet-mapping>
<servlet-name>
hi
</servlet-name>
<url-pattern>
/hello.html
</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>
hi
</servlet-name>
<url-pattern>
*.hello
</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>
hi
</servlet-name>
<url-pattern>
/hello/*
<url-pattern>
<servlet-mapping>
<!--... -->

Aplet „Hello World” może być wywołany poprzez użycie jednego z odwzorowań znajdujących sie na poniższej
liście:

/servlet/HelloWorld
/servlet/hi
/hello.html
/well.hello
/fancy/meeting/you/here.hello
/hello/to/you
Bardziej praktyczne odwzorowania zostaną zaprezentowane w dalszych rozdziałach niniejszej książki.
Przejdźmy dalej
Duża ilość informacji o apletach, aplikacjach WWW oraz plikach konfiguracyjnych XML zwartych w
niniejszym wprowadzeniu powinna być wystarczająca do uzyskania pojęcia jak tworzyć proste aplety, instalować
je na serwerach oraz informować serwery o ścieżkach, dla których chcemy wykonywać wspomniane aplety.
Możliwości apletów znacznie przekraczają wyświetlanie „Hello World” czy pozdrawianie użytkowników ich
imionami i nazwiskami — właśnie o tym będzie traktować dalsza część książki.
Rozdział 3. Czas istnienia (cykl

życia) apletu

Czas istnienia (cykl życia) apletu jest jednym z bardziej interesujących aspektów apletów. Czas istnienia jest
hybrydą czasów istnienia używanych w środkach programowania CGI oraz środkach programowania niskiego
poziomu WAI/NSAPI i ISAPI, tak jak zostało to omówione w rozdziale1 „Wprowadzenie”.

Alternatywa apletu
Czas istnienia (cykl życia) apletów pozwala ich pojemnikom na odniesienie się zarówno do wydajności, jak i do
problemów związanych z CGI oraz do problemów dotyczących bezpieczeństwa nisko-poziomowych środków
programowania serwerów API. Pojemniki apletów uruchamiają zwykle aplety wszystkie razem, w jednej
maszynie wirtualnej Javy (JVM). Dzięki umiejscowieniu wszystkich apletów w tej samej JVM mogą one
skutecznie wymieniać dane między sobą, jednak co się tyczy ich danych „prywatnych” — język Java nie daje
możliwości wglądu jednemu apletowi w dane znajdujące się na drugim. Aplety mogą istnieć w JVM-ie pomiędzy
zleceniami — jako kopie obiektów. Dzięki temu zajęte jest mniej pamięci niż w przypadku pełnej procedury, a
aplety są nadal w stanie utrzymać odniesienia do zewnętrznych zasobów. Cykl życia apletów nie jest wielkością
stałą. Jedyną rzeczą niezmienną i konieczną w tym cyklu jest to, iż pojemnik apletu musi przestrzegać
następnych zasad:

1. Stworzyć oraz uruchomić aplet


2. Obsłużyć wywołania usługi od klientów
3. Usunąć aplet a następnie go przywrócić

Jest rzeczą całkowicie naturalną w przypadku apletów, iż są one ładowane, tworzone konkretyzowane w swojej
własnej maszynie wirtualnej Javy — tylko po to aby być usuniętymi i odtworzonymi nie obsłużywszy żadnych
zleceń od klientów lub po obsłużeniu tylko jednego takiego zlecenia. Jednakże aplety zachowujące się w taki
sposób nie utrzymają się długo na rynku. W tym rozdziale omówimy najbardziej popularne oraz czułe realizacje
czasów istnienia apletów HTTP.

Pojedyncza maszyna wirtualna Javy


Większość pojemników apletowych wdraża wszystkie aplety do jednej JVM w celu maksymalizacji zdolności
apletów do wymiany informacji (wyjątkiem są tutaj pojemniki wyższej klasy, które realizują rozproszone
wywołanie apletu na wielu serwerach wewnętrznych, tak jak zostało to omówione w rozdziale 12 „Serwery
Przedsiębiorstw oraz J2EE”.
Wykonania wyżej wspomnianej pojedynczej maszyny wirtualnej Javy mogą by różne na różnych serwerach:
• Na serwerze napisanym w Javie, takim jak np. „Apache Tomcat”, sam serwer może wywoływać w JVM-ie
wraz ze swoimi apletami.
• Na pojedynczo przetwarzającym, wielo-wątkowym serwerze WWW, zapisanym w innym języku, wirtualna
maszyna Javy może zostać zawarta w procedurze serwera. JVM jako część procedury serwera zwiększa
wydajność ponieważ aplet staje się w pewnym sensie, kolejnym rozszerzeniem serwera API niskiego
poziomu. Serwer taki może wywołać aplet z nieskomplikowanym połączeniem kontekstu, może również
dostarczyć informacje o zleceniach poprzez wywołania metod bezpośrednich.

• Wieloprocedurowy serwer WWW (który uruchamia kilka procedur, aby obsłużyć zlecenia) właściwie nie
może zawrzeć JVM w swojej procedurze ponieważ takiej nie posiada. Ten typ serwerów zwykle uruchamia
zewnętrzny JVM, którego procedury może współdzielić. Taki sposób oznacza, iż każde wejście do apletu
wiązać się będzie ze skomplikowanym połączeniem kontekstu przypominającym FastCGI. Jednakże
wszystkie aplety będą nadal dzieliły tą samą zewnętrzną procedurę.
Na szczęście, z perspektywy apletów (a tym samym z naszej — jako ich twórców) wdrażanie serwerów nie ma
większego znaczenia ponieważ zachowują się one zawsze w te sam sposób.

Trwałość kopii
Tak jak to zostało opisane wcześniej, aplety istnieją pomiędzy zleceniami jako kopie obiektów. Inaczej mówiąc
w czasie ładowania kodu dla apletu, serwer tworzy pojedynczą kopię. Ta pojedyncza kopia obsługuje wszystkie
zlecenia, utworzone z apletu. Poprawia to wydajność w trzy następujące sposoby:

• zajmowana powierzchnia pamięci jest mała;


• pozwala to wyeliminować obciążenie tworzenia obiektu (w przeciwnym wypadku konieczne byłoby
utworzenie nowego obiektu apletu) aplet może być już ładowany w maszynie wirtualnej, kiedy zlecenie
dopiero wchodzi, pozwalając mu na rozpoczęcie wywoływania natychmiast;
• umożliwia trwanie — aplet może mieć wszystko, czego może potrzebować podczas obsługi zlecenia, już
załadowane np. połączenie z bazą danych może zostać ustanowione raz i używane wielokrotnie; z takiego
połączenia może korzystać wiele serwerów. Kolejnym przykładem może tutaj być aplet koszyka zakupów,
który ładuje do pamięci listę cen wraz z informacją o ostatnio połączonych klientach. Niektóre serwery w
sytuacji, kiedy otrzymują to samo zlecenie po raz drugi umieszczają całe strony w schowku, celem
zaoszczędzenia czasu.
Aplety nie tylko trwają pomiędzy zleceniami, lecz także wykonują wszystkie wątki stworzone przez siebie. Taka
sytuacja nie jest może zbyt korzystna w przypadku apletu „run-of-the-mill”, jednakże daje interesujące
możliwości. Rozważmy sytuację, w której podrzędny wątek przeprowadza pewne kalkulacje, podczas, gdy inne
wyświetlają ostatnie rezultaty. Podobnie jest w przypadku apletu animacyjnego, w którym jeden wątek zamienia
obraz, a inny nanosi kolory.

Liczniki
W celu przedstawienia cyklu życia (czasu istnienia apletu) posłużymy się prostym przykładem. Przykład 3.1
ukazuje serwer, który zlicza i wyświetla liczbę połączeń się z nim. Dla uproszczenia wynik przedstawiany jest
jako zwykły tekst (kod dla wszystkich przykładów dostępny jest w internecie — patrz Wstęp)

Przykład 3.1. Przykładowy prosty licznik

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SimpleCounter extends HttpServlet {

int count = 0;

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType(„text / zwykły”);
PrintWriter out = res.getWriter();
count++;
out.println ("Od załadowania z apletem łączono się" +
count + " razy.");
}
}
Kod jest prosty — po prostu wyświetla oraz zwiększa kopię zmiennej zwanej count, jednakże dobrze ukazuje
„potęgę” trwałości. Kiedy serwet ładuje ten aplet tworzy pojedynczą kopię celem obsłużenia wszystkich zleceń,
złożonych na ten aplet, dlatego właśnie kod bywa taki prosty. Takie same kopie zmiennych występują pomiędzy
wywołaniami, oraz w przypadku wszystkich wywołań.

Liczniki zsynchronizowane
Z punktu widzenia projektantów apletów każdy klient, to kolejny wątek, który wywołuje aplet poprzez metody
takie jak: service(), doGet (), doPost(), tak jak to pokazuje przykład 3.1*.

Rysunek 3.1. Wiele wątków — jedna kopia apletu

Jeżeli nasze aplety odczytują tylko zlecenia, piszą w odpowiedziach i zapisują informacje w lokalnych
zmiennych, (czyli w zmiennych określonych w metodzie) nie musimy obawiać się interakcji pomiędzy wątkami.
Jeżeli informacje zostają zapisane w zmiennych nielokalnych (czyli w zmiennych określonych w klasie, lecz
poza szczególną metodą) musimy być wtedy świadomi, iż każdy z wątków klienckich może operować tymi
zmiennymi apletu. Bez odpowiednich środków ostrożności sytuacja taka może spowodować zniszczenie danych
oraz sprzeczności. I tak np. jeżeli aplet SimpleCounter założy fałszywie, że przyrost na liczniku oraz
wyprowadzenie są przeprowadzanie niepodzielnie (bezpośrednio jeden po drugim, nieprzerwanie), to jeżeli dwa
zlecenia zostaną złożone do SimpleCounter prawie w tym samym czasie, możliwe jest wtedy, że każdy z nich
wskaże tą samą wartość dla count. Jak? Wyobraźmy sobie, że jeden wątek zwiększa wartość dla count i zaraz
po tym, zanim jeszcze pierwszy watek wypisze wynik count, drugi wątek również zwiększa wartość. W takim
przypadku, każdy z wątków wskaże tą samą wartość, po efektywnym zwiększeniu jej o 2‫أ‬.

Dyrektywa wykonania wygląda mniej więcej w ten sposób:

count++ // Wątek 1

count++ // Wątek 2

out.println // Wątek 3

out.println // Wątek 4

*
To, iż jedna kopia apletu może obsłużyć wiele zleceń w tym samym czasie może wydawać się dziwne, dzieje się tak
prawdopodobnie dlatego, że kiedy obrazujemy program uruchamiający zwykle obserwujemy jak kopie obiektów wykonują
zadanie wywołując nawzajem swoje metody. Mimo, że przedstawiony model działa w prostych przypadkach nie jest on
dokładnym przedstawieniem rzeczywistości. Prawdziwa sytuacja wygląda tak, że wszystkie zadania wykonują wątki. Kopie
obiektów nie są niczym więcej jak tylko strukturami danych, którymi operują wątki. Dlatego możliwa jest sytuacja, w której
dwa działające wątki używają w tym samym czasie tego samego obiektu.
‫أ‬
Ciekawostka: jeżeli wartość count byłaby zamiast 32-bitowego int, 64-bitowym long, teoretyczna możliwa byłaby
sytuacja, że przyrost będzie dokonany tylko w połowie, do czasu, gdy przerwie mu inny wątek. Dzieje się tak dlatego,
ponieważ w Jawie używana jest 32=bitowa kaskada
W tym przypadku ryzyko sprzeczności nie stanowi poważnego zagrożenia, jednakże wiele innych apletów
zagrożonych jest poważniejszymi błędami. W celu zapobieżenia temu typowi błędów oraz sprzecznościom, które
im towarzyszą, możemy dodać jeden lub więcej synchronicznych bloków do kodu. Jest gwarancja, że wszystko,
co znajduje się w bloku synchronicznym lub w metodzie synchronicznej nie będzie wywoływane przez inny
wątek. Zanim jakikolwiek z wątków rozpocznie wywoływanie kodu synchronicznego musi otrzymać monitor
(zamek) na określoną kopie obiektu. Jeżeli monitor ma już inny wątek np. z powodu tego, że wywołuje on ten
sam blok synchroniczny, lub inny tym samym monitorem, wtedy pierwszy wątek musi zaczekać. Działa to na
zasadzie łazienki na stacji benzynowej, zamykanej na klucz (zawieszany zwykle na dużej, drewnianej desce),
którym w naszym przypadku będzie monitor. Wszystko to dzieje się dzięki samemu językowi tak więc obsługa
jest łatwa. Synchronizacja jednakże powinna być używana tylko w ostateczności. W przypadku niektórych
platform sprzętowych otrzymanie monitora za każdym razem, kiedy wchodzimy do kodu synchronicznego
wymaga wiele wysiłku, a co ważniejsze w czasie, kiedy jeden wątek wywołuje kod synchroniczny, pozostałe
mogą być blokowane do zwolnienia monitora.
Dla SimpleCounter istnieją cztery sposoby rozwiązywania potencjalnych problemów. Po pierwsze możemy
dodać hasło zsynchronizowane z sygnaturą doGet():

public synchronized void doGet (HttpServletRequest req,


HttpServletResponse res)

Taka sytuacja gwarantuje zgodność synchronizacji całej metody, używa się w tym celu kopii apletu, jako
monitora. Nie jest to w rzeczywistości najlepsza metoda, ponieważ oznacza to, iż aplet może w tym samym
czasie obsłużyć tylko jedno zlecenie GET.
Drugim sposobem jest zsynchronizowanie tylko dwóch wierszy, które chcemy wywołać niepodzielnie.

PrintWriter out = res.getWriter();


synchronized(this) {
count++;
out.println ("Od załadowania z apletem łączono się" +
count + " razy.");
}

Powyższa technika działa lepiej, ponieważ ogranicza czas, który aplet spędza w swoim zsynchronizowanym
bloku, osiągając ten sam cel zgodności w wyniku liczenia. Prawdą jest, iż technika ta nie różni się specjalnie od
pierwszego sposobu.
Trzecim sposobem poradzenia sobie z potencjalnymi problemami jest utworzenie synchronicznego bloku, który
wykonywał będzie wszystko, co musi być wykonane szeregowo, a następnie wykorzystanie poza blokiem
synchronicznym. W przypadku naszego apletu, liczącego możemy zwiększyć wartość liczoną (count) w bloku
synchronicznym, zapisać zwiększoną wartość do lokalnej zmiennej (zmiennej określonej wewnątrz metody), a
następnie wyświetlić wartość lokalnej zmiennej poza blokiem synchronicznym:
PrintWriter out = res.getWriter();
int local_count;
synchronized(this) {
local_count= ++count;
}
out.println ("Od załadowania z apletem łączono się" +
localcount + " razy.");
Powyższa zmienna zawęża blok synchroniczny do najmniejszych, możliwych rozmiarów zachowując przy tym
zgodność liczenia.
Celem zastosowania czwartej, ostatniej z metod musimy zadecydować, czy chcemy ponieść konsekwencje
zignorowania wyników synchronizacji. Czasem bywa i tak, że konsekwencje te są całkiem znośne. Dla
przykładu, zignorowanie synchronizacji może oznaczać, że klienci otrzymają wynik trochę niedokładny. Trzeba
przyznać, iż to rzeczywiście nie jest wielki problem. Jeżeli jednak oczekiwano by od apletu liczb dokładnych,
wtedy sprawa wyglądałaby trochę gorzej.
Mimo, iż nie jest to opcja możliwa do zastosowania na omawianym przykładzie, to na innych apletach możliwa
jest zamiana kopii zmiennych na zmienne lokalne. Zmienne lokalne są niedostępne dla innych wątków i tym
samym nie muszą być dokładnie strzeżone przed zniszczeniem. Jednocześnie zmienne lokalne nie istnieją
pomiędzy zleceniami, tak więc nie możemy ich użyć do utrzymywania stałego stanu naszego licznika.
Liczniki całościowe
Model „jeden egzemplarz na jeden aplet” jest sprawą do omówienia ogólnego. Prawda jest taka, że każda
zarejestrowana nazwa (lecz nie każde URL-owe dopasowanie do wzorca) dla apletu jest związana z jedną kopią
apletu. Nazwa używana przy wchodzeniu do apletu określa, która kopia obsłuży zlecenie. Taka sytuacja wydaje
się być sensowna, ponieważ klient powinien kojarzyć odmienne nazywanie apletów z ich niezależnym
działaniem. Osobne kopie są ponadto wymogiem dla apletów zgodnych z parametrami inicjalizacji, tak jak to
zostało omówione dalej w tym rozdziale.
Nasz przykładowy SimpleCounter posługuje się kopią liczenia zmiennej przy zliczaniu liczby połączeń z nim
wykonanych. Jeżeli byłaby potrzeba liczenia wszystkich kopii (a tym samym wszystkich zarejestrowanych nazw)
możliwe jest użycie klasy zmiennej statycznej.
Zmienne takie są wspólne dla wszystkich kopii klasy. Przykład 3.2 ukazuje liczbę wejść na aplet, liczbę kopii
utworzonych przez serwer (na jedną nazwę) oraz całkowitą liczbę połączeń z tymi kopiami.

Przykład 3.2. Licznik całościowy

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HolisticCounter extends HttpServlet {

static int Classcount = 0; // dotyczy wszystkich kopii


int count = 0; // oddzielnie dla każdego apletu
static Hashtable instances = new Hashtable(); // również dotyczy
wszystkich kopii

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType ("text / zwykły");
PrintWriter out = res.getWriter();

count++;
out.println ("Od załadowania z apletem łączono się" +
count + " razy.");

// utrzymuj ścieżkę liczenia poprzez wstawienie odwołania do niej


// kopia w tablicy przemieszczania. Powtarzające się hasła są
// ignorowane

// Metoda size()odsyła liczbę kopii pojedynczych, umieszczonych w


pamięci
instances.put(this, this);
out.println ("Aktualnie jest" + instances.size() + "razy");

classCount++
out.println ("Licząc wszystkie kpoie, z apletem tym " + "łączono
"łączono się" + classCount + "razy")

}
}
Przedstawiony licznik całościowy — Holistic Counter, śledzi liczbę połączeń własnych przy pomocy
zmiennej kopii count, liczbę połączeń wspólnych za pomocą zmiennej klasy oraz liczbę kopii za pomocą tablicy
asocjacyjnej — instances (kolejny wspólny element, który musi być zmienną klasy). Widok przykładu ukazuje
rysunku 3.2.
Rysunek 3.2. Widok licznika całościowego

Odnawianie (powtórne ładowanie) apletu


Jeśli ktoś próbował używać umówionych liczników, we własnym zakresie być może zauważył, iż z każdą kolejną
rekompilacją liczenie zaczyna się automatycznie od 1. Wbrew pozorom to nie defekt, tylko właściwość.
Większość serwerów odnawia (powtórnie ładuje) aplety, po tym jak zmieniają się ich pliki klasy (pod
domyślnym katalogiem apletów WEB-INF/classes). Jest to procedura wykonywana na bieżąco, która znacznie
przyśpiesza cyklu testu rozbudowy oraz pozwala na przedłużenie czasu sprawnego działania serwera.
Odnawianie apletu może wydawać się proste, jednak wymaga dużego nakładu pracy. Obiekty ClassLoader
zaprojektowane są do jednokrotnego załadowania klasy.
Aby obejść to ograniczenie i wielokrotnie ładować aplety, serwery używają własnych programów ładujących,
które ładują aplety ze specjalnych katalogów, takich jak WEB-INF/classes.
Kiedy serwer wysyła zlecenie do apletu najpierw sprawdza, czy plik klasy apletu zmienił się na dysku. Jeżeli
okaże się, że tak wtedy serwer nie będzie już używał programu ładującego starej wersji pliku tylko utworzy nowa
kopię własnego programu ładującego klasy — celem załadowania nowej wersji. Niektóre serwery poprawiają
wydajność poprzez sprawdzanie znaczników modyfikacji czasu tylko co jakiś czas lub na wyraźne żądanie
administratora.
W wersjach Interfejsów API sprzed wersji 2.2, chwyt z programem ładującym klasy skutkował tym, że inne
aplety ładowane były przez odmienne programy ładujące — co skutkowało czasem zgłoszeniem
ClassCastException jako wyjątku, kiedy aplety wymieniały informacje (ponieważ klasa załadowana przez
jeden program ładujący nie jest tym samym, co klasa ładowana przez inny, nawet jeżeli dane dotyczące klasy są
identyczne).
Na początku Interfejsu API 2.2 jest gwarancja, że problemy z ClassCastException nie pojawi się dla apletów
w tym samym kontekście. Tak więc obecnie większość wdrożeń ładuje każdy kontekst aplikacji WWW w
jednym programie ładującym klasy, oraz używa nowego programu ładującego do załadowania całego kontekstu,
jeżeli jakikolwiek aplet w kontekście ulegnie zmianie.
Skoro więc wszystkim apletom oraz klasom wspomagającym w kontekście zawsze odpowiada ten sam program
ładujący, nie należy się więc obawiać żadnych nieoczekiwanych ClassCastException podczas uruchamiania.
Powtórne ładowanie całego kontekstu powoduje mały spadek wydajności, który jednakże występuje tylko
podczas tworzenia.
Powtórne ładowanie (odnawianie) klasy nie jest przeprowadzane tylko wtedy, kiedy zmianie ulega klasa
wspomagająca. Celem większej efektywności określenia, czy jest konieczne odnawianie kontekstu, serwery
sprawdzają tylko znaczniki czasu apletów klasy. Klasy wspomagające w WEB-INF/classes mogą być także
powtórnie załadowane, kiedy kontekst jest odnowiony, lecz jeżeli klasa wspomagająca jest jedyną klasą do
zmiany, serwer tego prawdopodobnie nie zauważy.
Odnowienie apletu nie jest także wykonywane dla wszystkich klas (apletu lub innych) znajdujących się w ścieżce
klasy serwerów. Klasy takie ładowane są przez rdzenny (pierwotny) program ładujący, a nie własny, konieczny
do powtórnego załadowania. Klasy te są również ładowane jednorazowo i przechowywane w pamięci nawet
wtedy, gdy ich pliki ulegają zmianie. Jeżeli chodzi o klasy globalne (takie jak klasy użyteczności
com.oreilly.servlet) to najlepiej jest umieścić je gdzieś na ścieżce klasy, gdzie unikną odnowienia.
Przyśpiesza to proces powtórnego ładowania oraz pozwala apletom w innych kontekstach wspólnie używać tych
obiektów bez ClassCastException.

Metody „Init” i „Destroy”


Tak jak zwykłe aplety, aplety wykonywane na serwerach mogą określać metody init() i destroy(). Serwer
wywołuje metodę init() po skonstruowaniu kopii apletu, jednak zanim jeszcze aplet obsłuży jakiekolwiek
zlecenie. Serwer wywołuje metodę destroy() po wyłączeniu apletu i zakończeniu wszystkich zleceń lub po
przekroczeniu ich limitu czasowego*.
W zależności od rodzaju serwera oraz konfiguracji aplikacji WWW, metoda init() może zostać wywołana w
poniższych momentach:

*
Specyfikacji projektów mającego ukazać się na rynku apletu API 2.3 (Servlet API 2.3), zakładają, że dodane zostaną
metody cyklu życia (czasu istnienia), które umożliwią apletom oczekiwanie na sygnały, kiedy kontekst lub sesja są tworzone
lub zakańczane oraz podczas wiązania i rozwiązywania atrybutu z kontekstem lub sesją.
• podczas uruchamiania apletu
• podczas pierwszego łączenia się z apletem, przed wywołaniem metody service()
• na żądania administratora serwera
W każdym przypadku metoda init() zostanie wywołana i zakończona ani zanim jeszcze aplet obsłuży swoje
pierwsze zlecenie.

Metoda init() jest zwykle wykorzystywana do inicjalizacji apletu — ładowania obiektów używanych przez
aplet w procesie obsługi zleceń. W czasie wykorzystywania metody init() aplet może „chcieć” odczytać swoje
parametry inicjalizacji (init). Parametry te są dostarczane samemu apletowi i nie są w jakikolwiek sposób
związane z jednym zleceniem. Mogą one określać takie wartości początkowe jak: odkąd licznik powinien zacząć
liczyć lub wartości domyślne takie jak np. szablon, który powinien zostać użyty w przypadku nie określenia tego
w zleceniu. * Specyfikacji projektów mającego ukazać się na rynku interfejsu API 2.3 (Servlet API 2.3),
zakładają, że dodane zostaną metody cyklu życia (czasu istnienia), które umożliwią apletom oczekiwanie na
sygnały, kiedy kontekst lub sesja są tworzone lub zakańczane oraz podczas wiązania i rozwiązywania atrybutu z
kontekstem lub sesją.
Parametry początkowe dla apletu można znaleźć w deskryptorze wdrożenia, niektóre serwery mają graficzne
interfejsy mogące zmodyfikować ten plik (patrz przykład 3.3).
Przykład 3.3. Ustalanie wartości parametrów w deskryptorze rozmieszczenia

<?xml version = "1.0" kodowanie = "ISO-8859-1"?>


<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.// DTD Web Application 2.2 // EN"
"http: // java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
<servlet>
<servlet-name>
counter
</servlet-name>
<servlet-class>
InitCounter
</servlet-class>
<init-param>
<param-name>
initial
</param-name>
<param-value>
1000
</param-value>
<description>
The initial value for the counter <!—optional 
</description>
</init-param>
</servlet>
</web-app>

Wielokrotne elementy <init-param> mogą zostać umieszczone w znaczniku <servlet>. Znacznik


<descriptor> jest opcjonalny, pierwotnie miał on być przeznaczony do graficznych programów
narzędziowych. Pełną definicję typu dokumentu dla pliku web.xml można znaleźć w dodatku F „Kodowania”.
Podczas stosowania metody destroy() aplet powinien zwolnić wszystkie zasoby, które wcześniej pozyskał i
które nie będą odzyskiwane. Metoda destroy() daje również apletowi możliwość wypisania jego nie
zapisanych informacji ze schowka lub innych trwałych informacji, które powinny zostać odczytane podczas
najbliższego wywołania metody init().

Licznik z metodą Init


Parametry początkowe mają wiele zastosowań. Zasadniczo jednak określają początkowe lub domyślne wartości
dla zmiennych apletu lub „mówią” apletowi jak w określony sposób dostosować jego zachowanie. Na
przykładzie 3.4 nasz SimpleCounter został rozszerzony celem odczytania parametru początkowego (zwanego
initial), który przechowuje wartość początkową dla naszego licznika. Poprzez ustawianie początkowego stanu
licznika na wysokie wartości możemy sprawić, że nasza strona będzie wydawała się bardziej popularna niż w
rzeczywistości.
Przykład 3.4. Licznik odczytujący parametry początkowe

import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class InitCounter extends HttpServlet {

int count;

public void init() throws ServletException {


String initial getInitParameter("początkowy");
try {
count = Integer.parseInt(initial);
}

catch (numberFormatException e) {
count = 0;
}
}

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType ("text/zwykły");
PrintWriter out = res.getWriter();
count++
out.println ("Od załadowania (oraz z możliwą inicjalizacją");
out.println ("parametr figurujący w ),z apletem tym łączono się");
out.println (count + "razy");
}
}

Metoda init() wykorzystuje metodę getinitParameter() w celu uzyskania wartości dla parametru
początkowego zwanego initial. Metoda ta przyjmuje tą nazwę jako String i oddaje wartość również jako
String. Nie ma możliwości uzyskania wartości jako innego typu. Dlatego aplet ten przekształca wartość
String na wartość int lub w razie problemów zamienia na wartość 0. Należy pamiętać, że jeżeli chcemy
wypróbować ten przykład może się okazać konieczne powtórne „zastartowanie” serwera celem wprowadzenia
zmian w web.xml, oraz odniesienie się do apletu, używając zarejestrowanej nazwy.

!!!!!!!!! początek ramki

Co się stało z super.init(config)?


W Interfejsie API 2.0, aplet wdrażając metodę init() musiał wdrożyć jej formularz, który przejmował parametr
ServletConfig, musiał on również wywołać super.init(config):
public void init(ServletConfig config) throws ServletException {
super.init(config);
//od inicjalizacji następuje
}
Parametr ServletConfig dostarczał informacje o konfiruracji do apletu, a wywołanie super.init(config)
przekazywało obiekt konfiguracyjny do nadklasy GenericServlet gdzie było zapisywane dla użytku apletu.
Specyficznie, klasa GenericServlet używała przekazany parametr config celem wdrożenia samego
interfejsu ServletConfig (przekazując wszystkie wywołania do delegowanej konfiguracji pozwalając tym
samym apletowi na wywołanie metody ServletConfig na siebie — dla wygody).
Powyższa operacja była bardzo zawiła w Interfejsie API 2.1, została jednak uproszczona do tego stopnia, że
obecnie wystarczy tylko jak aplet wdroży wersję bez-argumentową init(), a obsługa ServletConfig i
GenericServlet będzie zrealizowana na dalszym planie. Poza scenami, klasa GenericServlet współpracuje
z bez-argumentową metodą init, z kodem przypominającym poniższy:
public class GenericServlet implements Servlet, ServletConfig {

ServletConfig_config = null;

public void init(ServletConfig config) throws ServletException {


__config = config;
log("init zwany");
init();
}

public void init() throws ServletException {

public String getInitParameter(String name) {


return_config.getInitParameter(name);
}

// itd. ...
}
-—kontynuacja–
Zwróćmy uwagę, iż serwer wywołuje w czasie inicjalizacji metodę apletu init(ServletConfig config).
Zmiana w 2.1 dotyczyła tego, iż obecnie GenericServlet przekazuje to wywołanie do bez-argumentowego
init(), którą to metodą można zignorować nie martwiąc się o config.
Co się tyczy zgodności z poprzednimi wersjami należy nadal ignorować init(ServletConfig config) i
wywoływać super.init(config). W przeciwnym wypadku może być tak, iż nie będziemy mogli wywoływać
metody bez-argumentowej init().
Niektórzy z programistów uważają, iż dobrze jest wywołać najpierw super.destroy() podczas gdy wdrażamy
destroy() powoduje to, że GenericServlet wdraża destroy(), która to metoda „pisze” wiadomość do
rejestru zdarzeń, że aplet jest niszczony.
!!!!!!!!! koniec ramki

Licznik z metodami Init i Destroy


Do tego momentu przykłady liczników demonstrowały jak stan apletu utrzymuje się pomiędzy połączeniami. To
jednak rozwiązuje problem tylko częściowo. Za każdym razem, kiedy serwer jest wyłączany lub aplet
odnawiany, liczenie zaczyna się od nowa. Rzeczą naprawdę potrzebna jest trwanie licznika niezależnie od
ładowań, licznik który nie zaczyna ciągle od początku.
To zadanie mogą wykonać metody init() i destroy(). Przykład 3.5 poszerza jeszcze bardziej przykład
initCounter, dodając apletowi możliwość zachowania swojego stanu podczas destroy() oraz podczas
powtórnego ładowania stron w init(). Dla uproszczenia przyjmijmy, że aplet ten nie jest zarejestrowany i
dostępny tylko pod http://server:port/servlet/InitDestroyCountery. Gdyby ten aplet był zarejestrowany pod
różnymi nazwami, musiałby zachowywać oddzielny stan dla każdej z nazw.

Przykład 3.5. Licznik stale działający

import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class InitDestroyCounter extends HttpServlet {

int count;
public void init() throws ServletException {
//Spróbuj załadować liczenie początkowe z naszego zachowanego stałego stanu
FileReader fileReader = null;
BufferedReader bufferedReader = null;
try {
fileReader = new fileReader ("InitDestroyCounter.initial");
bufferedReader = new BufferedReader(fileReader);
String initial = bufferedReader.readLine();
count = Integer.parseInt(initial);
return;
}
catch (FileNotFoundException ignored) {} // nie ma stanu zapisanego
catch (IOException ignored) {} // problem podczas czytania
catch (NumberFormatException ignored) {} //zniekształć stan zapisany
finally {
// Nie zapomnij zamknąć plik
try {
if(bufferedReader ! = null) {
bufferedReader.close()
}
}
catch (IOException ignored) {}
}
// W razie braku powodzenia ze stanem zapisanym,
// sprawdź dla parametru init
String initial = getInitParameter("początkowy");
try {
count = Integer.parseInt(initial);
return;
}

catch (NumberFormatException ignored) {} //zero lub liczba nie całkowita

// Domyślne dla początkowego stanu licznika "0"


count = 0;
}

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

res.setContentType ("text/zwykły");

PrintWriter out = res.getWriter();


count++
out.println ("Od początku z apletem łączono się" +
count + " razy.");
}

publi void destroy() {


super.destroy(); //całkowicie opcjonalne
saveState();
}

publi void saveState() {


// Spróbuj zapisać wartość dodaną
FileWriter ( = null;
PrintWriter printWriter = null;
try {
fileWriter = new FileWriter("InitDestroyCounter.initial");
printWriter = new PrintWriter(fileWriter);
printWriter.println(count);
return;
}

catch (IOException e) { // problem podczas pisania


// Zarejestruj wyjątek. Patrz rozdział 5.
}

finally {
// Nie zapomnij zamknąć plik
if (printWriter ! = null) {
printWriter.close();
}
}
}
}
Za każdym razem, kiedy aplet jest usuwany, stan jego jest zachowywany w pliku o nazwie
InitDestroyCounter.initial. Jeżeli nie ma dostarczonej ścieżki dostępu, plik jest zapisywany w procedurze
serweru bieżącego katalogu, zwykle jest to katalog auto-startowy. Sposoby alternatywnej lokalizacji opisano w
rozdziale 4 „Odzyskiwanie informacji”. Plik ten również zawiera liczbę całkowitą, zapisana jako ciąg znaków,
reprezentujący ostatnie liczenie.
Za każdym ładowaniem serwera jest próba odczytu z pliku, zachowanego liczenia. Jeżeli z jakiegoś powodu
próba odczytu nie powiedzie się (jak ma to miejsce podczas pierwszego uruchomienia apletu — ponieważ plik
jeszcze wtedy nie istnieje), aplet sprawdza, czy parametr początkowy określa liczenie początkowe. Jeżeli i to nie
da efektu zaczyna od zera. Podczas stosowania metody init() zalecana jest najwyższa ostrożność.
Aplety mogą zachowywać swój stan na wiele różnych sposobów. Niektóre z nich mogą posłużyć się w tym celu
formatem użytkowym pliku, tak jak to zostało tutaj zrobione przez nas. Inne serwery zapisują swój stan jak
serializowane obiekty Java lub „umieszczają” go w bazie danych. Niektóre serwery nawet wykorzystują technikę
journaling, powszechnie stosowaną przy bazach danych oraz przy kopiach zapasowych taśm, gdzie pełny stan
apletu jest zapisywany rzadko, podczas gdy plik dziennika wprowadza do pamięci przyrostowe aktualizacje w
trakcie zmian. To, którą metodę użyje aplet zależy od sytuacji. Powinniśmy być zawsze świadomi tego, iż
zapisywany stan nie podlega żadnym zmianom na drugim planie.
Teraz może nasuwać się pytanie: co się stanie, jeżeli aplet ulegnie awarii? Odpowiedź brzmi: metoda destroy
() nie zostanie wywołana‫أ‬. Nie jest to jednakże problem dla metod destroy(), które muszą tylko zwolnić
zasoby; przeładowany serwer równie dobrze się do tego nadaje i (czasem nawet lepiej). Jednakże sytuacja taka
jest problemem dla apletu, który musi zapisywać swój stan w swojej metodzie destroy(). Ratunkiem dla tych
apletów jest częstsze zapisywanie swojego stanu. Aplet może „zdecydować się” na zapisanie swojego stanu po
obsłudze każdego ze zleceń tak jak powinien to zrobić aplet „chess server” (serwer szachowy), tak że nawet
kiedy serwer jest ponownie uruchamiany, gra może zostać wznowiona z ostatnią sytuacją na szachownicy. Inne
aplety mogą potrzebować zapisać strony tylko po zmianie jakiejś ważnej wartości — aplet „shopping cart” (lista
zakupów) musi zapisać swój stan tylko wtedy, gdy klient doda lub usunie pozycję z listy. I w końcu niektóre
aplety mogą tracić niektóre ze swoich ostatnich zmian stanu. Takie aplety mogą zapisywać stan po pewnej
określonej liczbie zleceń. Dla przykładu, w naszym przykładzie InitDestoyCounter, wystarczającym
powinno być zapisywanie stanu co dziesięć połączeń. Celem wdrożenia powyższego można dodać prosty wiersz
na końcu doGet():
if (count % 10 == 0) saveState();
Można zapytać czy jest to istotna zmiana. Wydaje się, że tak, biorąc pod uwagę zagadnienia związane z
synchronizacją. Stworzyliśmy możliwość utraty danych (jeśli saveState() zostanie uruchomionym przez dwa
wątki, w tym samym czasie) oraz ryzyko, że saveState() nie będzie w ogóle wywołane i jeżeli liczenie
zostanie zwiększone przez kilka wątków z rzędu przed sprawdzeniem. Załóżmy, że taka możliwość nie istniała
kiedy saveState() było wywoływane tylko z metody destroy() ponieważ metoda destroy() jest
wywoływana tylko raz na jedną kopię apletu. Jednakże teraz, kiedy saveState() jest wywoływana w metodzie
do Get() musimy ponownie się nad tym zastanowić. Jeżeli zdarzyłoby się kiedyś, że aplet ten byłby odwiedzany
tak często, iż byłoby więcej niż 10 niezależnie wywołujących wątków, jest prawdopodobne, że dwa aplety (10
osobnych zleceń) będą w saveState() w tym samym czasie. Może to spowodować zniszczenie pliku z danymi.
Może to również doprowadzić do jednoczesnego zwiększenia liczenia przez dwa watki, zanim któryś „zorientuje
się”, iż minął czas wywoływania saveState(). Rozwiązanie jest proste: przemieśćmy kontrolę liczenia do
bloku zsynchronizowanego, tam gdzie liczenie jest zwiększane:
int local_count ;
synchronizeed(this) {
local_count = ++count ;
if (count % 10 == 0) saveState();
}
out.println ("Od załadowania z apletem łączono się" +
count + " razy.");

Wniosek z powyższych rozważań jest jeden: bądźmy przezorni i chrońmy kod apletu od problemów związanym
z wielowątkowym dostępem.

Model jedno-wątkowy (Single-Thread Model)


Mimo, iż normalna sytuacja to jedna kopia apletu na jedną zarejestrowaną nazwę apletu, to możliwa jest również
pula kopii utworzonych dla każdej z nazw apletu, której każda kopia obsługuje zlecenia. Aplety sygnalizują taka
chęć poprzez wdrożenie interfejsu javax.servlet.SingleThreadModel. Jest to prosty interfejs „tag”, który
nie określa żadnych metod, ani zmiennych, służy tylko do oznaczenia apletu, jako „wyrażającego chęć” zmiany
stanu istnienia.
Serwer, który ładuje aplet SingleThreadModel (Model jedno-wątkowy) musi gwarantować, zgodnie z
dokumentacją InterfejsuAPI, że żadne dwa wątki nie będą wywoływały konkurencyjnie w metodzie apletu
„service”. W celu spełnienia powyższego warunku każdy wątek używa wolnej kopii apletu z puli, tak jak na
rycinie 3.3 dzięki temu każdy aplet wdrażający SingleThreadModel może zostać uznany jako bezpieczny co
do wątku oraz nie wymagający synchronizacji dostępu do jego zmiennych kopii. Niektóre serwery dopuszczają
konfigurację wielu kopii na pulę, inne nie. Niektóre z kolei serwery używają pul tylko z jedną kopią powodując
zachowanie identyczne z metodą zsynchronizowaną service().

‫أ‬
Jeżeli nie mamy pecha i nasz serwer nie będzie miał awarii podczas stosowania metody destroy(). W przeciwnym
wypadku możemy zostać z częściowo zapisanym plikiem stanu — pozostałościami napisanymi na górze naszego
poprzedniego stanu. Celem osiągnięcia całkowitego bezpieczeństwa aplet zapisuje swój stan w pliku roboczym, kopiując go
następnie na górze oficjalnego pliku stanu w jednej komendzie.
Rysunek 3.3. Model jedno-wątkowy
Czas istnienia SingleThreadModel (Modelu jedno-wątkowego) nie ma zastosowania dla liczników lub innych
aplikacji apletu, które wymagają obsługi centralnego stanu. Czas istnienia może mieć pewne zastosowanie,
jednak tylko w unikaniu synchronizacji, ciągle obsługując sprawnie zlecenie.
Dla przykładu aplety, które łączą się z bazami danych muszą czasem wykonać kilka poleceń bazy danych,
niepodzielnie jako część pojedynczej obsługi transakcji. Każda transakcja bazy danych wymaga wydzielonego
obiektu połączenia bazy danych, dlatego więc aplet musi jakoś zagwarantować, że żadne dwa wątki nie będą
próbowały „wchodzić” na to samo połączenie w tym samym czasie. Można tego dokonać poprzez użycie
synchronizacji, pozwalając apletowi na obsługę tylko jednego zlecenia w jednym momencie. Poprzez wdrożenie
SingleThreadModel oraz poprzez fakt, iż jest tylko jedno „połączenie” kopii zmiennej, aplet może w prosty
sposób obsługiwać konkurencyjne (jednoczesne) zlecenia ponieważ każda kopia będzie miała swoje połączenie.
Zarys kodu pokazano na przykładzie 3.6.

Przykład 3.6. Obsługa połączeń bazy danych przy użyciu Modelu jedno-wątkowego

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SingleThreadConnection extends HttpServlet


implements SingleThreadModel {

Connection con = null; // połączenie z bazą danych, jedno na jedną


kopię puli

public void init() throws ServletException {


// Ustanów połączenie dla tej kopii
try {
con = esablishConnection ();
con.AutoComit(false);
}
catch (SQLException e) {
throw new ServletException(e.getMessage());
}
}
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text/zwykły");
PrintWriter out = res.getWriter();

try {
// Użyj połączenia utworzonego specjalnie dla tej kopii

Statement stmt = con.createStatement();

// Aktualizuj bazę danych jakimikolwiek sposobami

// Zatwierdź obsługę żądania


com.commit();
}
catch (SQLException e ) {
try { con.rollback(); } catch (SQLException ignored) { }
}
}

public void destroy() {


if (con ! = null) {
try { con.close(); } catch (SQLException ignored) { }
}
}
private Connection establishConnection() throws SQLException {
// Nie wdwrożone. Patrz rozdział 9.
}
}
W rzeczywistości SingleThreadModel nie jest najlepszym rozwiązaniem dla takiej jak ta aplikacji.O wiele
lepszym rozwiązaniem dla apletu byłoby użycie wydzielonego obiektu ConnectionPool przechowywanego
jako kopia lub zmienna klasy, z którego mógłby on „zaewidencjonować” oraz „wyewidencjonować” połączenia.
Połączenie wyewidencjonowane może być przechowywane jako lokalna zmienna, zapewniająca wydzielony
dostęp. Zewnętrzna pula zapewnia apletowi więcej kontroli nad zarządzaniem połączeniami. Pula może również
zweryfikować poprawność każdego połączenia, może ona być także skonfigurowana w taki sposób, że będzie
zawsze tworzyła pewną minimalną liczbę połączeń, lecz nigdy większą niż określona liczba maksymalna.
Stosując metodę SingleThreadModel, mógłby utworzyć znacznie więcej kopii (a tym samym połączeń) niż
baza danych może obsłużyć.
Na ten moment należy więc unikać stosowania metody SingleThreadModel. Większość innych apletów
mogłaby być lepiej wdrażana przy użyciu synchronizacji oraz puli zasobów zewnętrznych. Prawdą jest, iż
interfejs daje pewien stopień kontroli programistom, nie zaznajomionym z programowaniem wielo-wątkowym;
jednakże podczas gdy SingleThreadModel czyni sam aplet bezpiecznym co do wątku, to interfejs nie czyni
tego z systemem. Interfejs nie zapobiega problemom związanym z synchronizacją, które wynikają z
jednoczesnego dostępu apletów do wspólnych zasobów takich jak np. zmienne statyczne czy obiekty poza
zasięgiem apletu. Problemy związane z wątkami będą się pojawiały zawsze kiedy pracujemy w systemie wielo-
wątkowym, z lub bez SingleThreadModel.

Przetwarzanie drugoplanowe
Aplety potrafią więcej niż tylko po prostu trwać pomiędzy wejściami na nie. Potrafią także wtedy wywoływać.
Każdy wątek uruchomiony przez aplet może kontynuować wywoływanie nawet po wysłaniu odpowiedzi.
Możliwość ta najlepiej sprawdza się przy dłuższych zadaniach, których wyniki przyrostowe są udostępniane
wielu klientom. Wątki drugoplanowe, uruchomione w init() wykonują pracę w sposób ciągły podczas gdy
wątki obsługujące zlecenia wyświetlają stan bieżący za pomocą doGet(). Jest to technika podobna to tej
używanej w apletach animacyjnych, gdzie jeden wątek dokonuje zmian na rysunku, a drugi nanosi kolory.
Przykład 3.7 ukazuje aplet wyszukujący liczb pierwszych, większych od kwadryliona. Tak wielka liczba
wybierana jest celowo, żeby uczynić liczenie powolnym tak, aby zademonstrować efekty buforujące (które będą
nam potrzebne przy omawianiu dalszej części materiału). Algorytm używany przez aplet jest bardzo prosty: aplet
selekcjonuje wszystkie liczby nieparzyste i następnie próbuje podzielić je przez liczby nieparzyste całkowite z
przedziału od 3 do ich pierwiastka kwadratowego. Jeżeli dana liczba, nie jest podzielna bez reszty przez żadną
tych liczb, wtedy jest ona uznawana za liczbę pierwszą.*

Przykład 3.7. W poszukiwaniu liczb pierwszych


import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class PrimeSearcher extends HttpServlet implements Runnable {

long lastprime = 0; // ostatnia znaleziona liczba pierwsza


Datelastprimeodified = new Date(); // kiedy została znaleziona
Thread searcher; // drugoplanowy wątek szukający

public void init() throws ServletException {


searcher = new Thread(this);

*
Można zapytać dlaczego sprawdzane są tylko czynniki mniejsze od pierwiastka kwadratowego. Odpowiedź jest prosta:
ponieważ jeżeli liczba miałaby dzielniki wśród liczb większych od tego pierwiastka, to musiałaby je także mieć wśród liczb
od niego mniejszych.
searcher.setPriority(Thread.MIN_PRIORITY); // bądź dobrym obywatelem
searcher.start();
}

public void run() {


// QTTTBBBMMMTTTOOO

long candidate = 1000000000000001L; // kwadrilion jeden

// Rozpocznij szukanie pętlowe liczb pierwszych


while (true) { // szukaj cały czas
if (isPrime(candidate)) {
lastprime = candidate; // nowa liczba pierwsza
lastprimeodified = new Date(); // nowy "czas" liczby pierwszej"

}
candidate += 2; // liczby parzyste nie są liczbami pierwszymi

// Pomiędzy potencjalnymi liczbami pierwszymi rób przerwę 0.2 sekundy


// Kolejny sposób aby być dobrym w zasobach systemu
try {
searcher.sleep(200);
}
catch (InterrruptedException ignored) {}
}
}
private static boolean isPrime(long candidate) {
// Spróbuj podzielić podzielić tą liczbę przez wszystkie liczby
// nieparzyste z przedziału od 3 do jej pierwiastka kwadratowego

long sqrt (long) Match.sqrt (candidate);


for (long i = 3; i <sqrt; i += 2) {
if (candidate % i == 0) return false; // znajdź czynnik
}

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType ("text/zwykły");
PrintWriter out = res.getWriter();
if (lastprime == 0) {
out.println ("Nadal szukam liczb pierwszych...");
}
else{
out.println("Ostatnia liczba pierwsza została znaleziona " + lastprime);
out.println("w" + lastprimeModified);
}
}
public void destroy()
searcher.stop();
}
}

Wątek wyszukujący rozpoczyna wyszukiwanie w metodzie init(). Ostatnia liczba przez niego znaleziona
zostaje zapisana w lastprime, a czas, w którym to się stało w lastprimeModified. Za każdym razem kiedy
klient łączy się z apletem, metoda doGet() informuje go jaka największa liczba pierwsza została znaleziona do
tej pory oraz o czasie, w którym to się stało. Wątek niezależnie przetwarza połączenia klientów; nawet kiedy
żaden klient nie jest połączony, wątek nadal kontynuuje poszukiwania liczb pierwszych. Jeżeli kilku klientów
połączy się w tym samym czasie z apletem, aktualny stan wyświetlony dla nich wszystkich będzie jednakowy.
Zwróćmy uwagę na fakt, iż metoda destroy() zatrzymuje wątek wyszukujący. Jest to bardzo istotne ponieważ
jeżeli aplet nie zatrzyma swoich drugoplanowych wątków, będą one działały tak długo jak długo będzie istniała
maszyna wirtualna. Nawet jeżeli aplet zostanie powtórnie załadowany (odnowiony) (jawnie bądź z powodu
zmiany pliku klasy) jego wątki nie przestaną działać. Zamiast tego jest prawdopodobne, że nowy aplet utworzy
kopie dodatkowe wątków drugoplanowych.

Uruchamianie i rozruch
Aby sprawić, że PrimeSearcher zacznie szukać liczb pierwszych tak szybko jak to możliwe, możemy
skonfigurować aplikację WWW apletu w taki sposób, że będzie ładowała aplet przy startowaniu serweru.
Dokonuje się tego poprzez dodanie znacznika <load-on-startup> do hasła <servlet>, deskryptora
wdrożenia, tak jak na przykładzie 3.8.

Przykład 3.8. Ładowanie apletu przy rozruchu


<?xml version = "1.0" kodowanie = "ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2 //EN"
"http: // java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
<servlet>
<servlet-name>
ps
</servlet-name>
<servlet-class>
PrimeSearcher
</servlet-class>
<load-on-startup/>
</servlet>
</web-app>

Powyższe komendy „mówią serwerowi”, żeby utworzył kopię PrimeSearcher pod rejestrowaną nazwą ps oraz
żeby zainicjował aplet podczas sekwencji uruchamiania serwera. Z apletem można połączyć się wtedy na URL-u
/servlet/ps. Zwróćmy uwagę, iż kopia apletu obsługująca URL /servlet/PrintWriter meSearcher nie jest ładowana
przy rozruchu.
Na przykładzie 3.8 znacznik <load-on-startup> jest pusty. Znacznik może również zawierać dodatnią liczbę
całkowitą, oznaczającą kolejność, w której aplet powinien być załadowany w odniesieniu do innych apletów
kontekstu. Aplety z niższymi liczbami ładowane są przed tymi z liczbami większymi. Aplety z wartościami
ujemnymi lub nie-całkowitymi mogą być ładowane w każdym momencie sekwencji uruchamiania, dokładna
kolejność zależna jest wtedy od serwera. Dla przykładu, web.xml ukazany na przykładzie 3.9 gwarantuje, że
first będzie załadowany przed second, podczas gdy anytime może zostać załadowany w każdym momencie
rozruchu serwera.
Przykład 3.9. Pokaż możliwości apletu

<web-app>
<servlet>
<servlet-name>

first
</servlet-name>
<servlet-class>
First
</servlet-class>

<load-on-startup >10</load-on-startup>
</servlet>
<servlet>
<servlet-name>
second
</servlet-name>
<servlet-class>
Second
</servlet-class>
<load-on-startup >20</load-on-startup>
</servlet>
<servlet>
<servlet –name>
anytime
</servlet-name>
<servlet-class>
Anytime
</servlet-class>
<load-on-startup/>
</servlet>
</web-app>

Buforowanie podręczne po stronie klienta


Do tej pory nauczyliśmy się, że aplety obsługują zlecenia GET przy pomocy metody doGet(). I jest to niemal
prawda. Cała prawda, jednakże jest taka, że nie do każdego zlecenia konieczne jest wywoływanie metody doGet
(). Dla przykładu, przeglądarka WWW, która stale łączy się z PrimeSearcher będzie musiała wywołać doGet
() tylko po tym jak wątek wyszukujący znajdzie nową liczbę pierwszą. Do tego czasu wszystkie wywołania
doGet() generują po prostu stronę, którą użytkownik już oglądał, stronę prawdopodobnie przechowywaną w
pamięci podręcznej przeglądarki. To co jest na ten moment najbardziej potrzebne, to sposób w jaki aplet mógłby
informować o zmianach w jego wydruku wyjściowym. I tutaj właśnie pomocne będzie omówienie metody
getlastModified().
Większość serwerów zawiera, jako część swojej odpowiedzi, nagłówek Last-Modified, wtedy kiedy odsyła
dokument. Przykład wartości nagłówka Last-Modified mógłby wyglądać w następujący sposób:
Tue, 06-May-98 15:41:02 GMT
Powyższy nagłówek informuje klienta o tym, kiedy ostatnio została zmieniona strona. Informacja sama w sobie
nie jest specjalnie wartościowa, jednak zyskuje na wartości w momencie kiedy przeglądarka powtórnie ładuje
stronę.
Większość przeglądarek WWW, podczas odnawiania strony, zawiera w swoich zleceniach nagłówek If-
Modified-Since, którego struktura jest identyczna z nagłówkiem Last-Modified:
Tue, 06-May-98 15:41:02 GMT
Nagłówek ten informuje serwer o czasie, w którym nagłówek Last-Modified strony, był po raz ostatni
ładowany przez przeglądarkę. Serwer może odczytać ten nagłówek oraz stwierdzić czy plik był zmieniany od
określonego czasu. Jeżeli okaże się, że plik został zmieniony, serwer musi przesłać nową treść. Jeżeli okażę się,
że plik nie uległ zmianie, wtedy serwer może wysłać przeglądarce krótką odpowiedź, informującą ją o tym oraz o
tym, że wystarczy powtórnie wyświetlić wersję dokumentu schowaną w schowku (ta odpowiedź to: kod stanu
304 Not Modified).
Technika ta działa najlepiej w przypadku stron statycznych: serwer może użyć systemu plików w celu
sprawdzenia kiedy określony plik został zmodyfikowany po raz ostatni. Jednakże dla treści tworzonej
dynamicznie, takiej jaka jest odsyłana przez aplety, serwer potrzebuje dodatkowej pomocy. Jednak wszystko co
może zrobić to odtwarzać je bezpiecznie zakładając jednocześnie, że zawartość ulega zmianie z każdym
połączeniem, eliminując w ten sposób konieczność użycia nagłówków Last-Modified oraz If-Modified-
Since.
Dodatkową pomocą aplet może służyć poprzez implementację (wdrożenie) metody getLastModified(). Aplet
wdroży tą metodę, aby przesłać dane dotyczące czasu, w którym po raz ostatni zmienił swój wydruk wyjściowy.
Serwery wywołują tą metodę dwa razy, pierwszy raz kiedy odsyłają odpowiedzi (w celu wstawienia nagłówka
odpowiedzi Last-Modified). Drugi raz, podczas obsługi zleceń GET, które zawierają nagłówek If-
Modified-Since, dzięki temu serwer może odpowiedzieć w sposób inteligentny. Jeżeli czas odesłania przez
getLastModified() jest taki sam, bądź wcześniejszy niż czas nadesłania nagłówka If-Modified-Since,
wtedy serwer przesyła kod stanu Not Modified. W przeciwnym wypadku serwer wywołuje metodę doGet()
oraz odsyła wydruk wyjściowy apletu*.
Niektórym apletom może sprawiać trudność ustalenie czasu ostatniej modyfikacji. Dlatego w takich sytuacjach
najlepszym wyjściem jest użycie zachowania domyślnego „play-it-safe”. Większość apletów jednakże doskonale
daje sobie z tym radę. Rozważmy przypadek apletu „bulletin board” („elektroniczny biuletyn informacyjny”),
aplet taki może zarejestrować i odesłać kiedy, po raz ostatni treść biuletynu została zmieniona. Nawet gdy ten
sam aplet obsługuje kilka biuletynów, nadal może przesyłać różne czasy modyfikacji, odpowiednie dla
parametrów podanych w zleceniu. Oto metoda get-Last-Modified dla naszego przykładu PrimeSearcher,
która przesyła czas znalezienia ostatniej liczby pierwszej:
public long getLastModified(HttpServletRequest req) {
return lastprimeModified.getTime() /1000 * 1000;
}
Zwróćmy uwagę, iż metoda ta przesyła wartość długą, która przedstawia czas jako liczbę milisekund, która
upłynęła od 1 stycznia 1970 roku GMT. Takiej samej reprezentacji używa
Java w celu przechowywania wartości czasowych. Dzięki temu aplet używa metody getTime() do wczytania
LastprimeModified() jako long.
Aplet zanim odeśle tą wartość czasową, zaokrągla ją do najbliższej sekundy, dzieląc ją przez tysiąc a następnie
mnożąc przez tysiąc. Wszystkie czasy odesłane przez getLastModified() powinny być zaokrąglone w ten
sposób. Powodem tego jest fakt, że nagłówki Last-Modified oraz If-Modified-Since są przypisane do
najbliższej sekundy. Jeżeli get-Last-Modified odeśle ten sam czas lecz z wyższą rozdzielczością, czas ten
może błędnie wydawać się parę milisekund późniejszy niż podany przez If-Modified-Since. Załóżmy dla
przykładu, że PrimeSearcher znalazł liczbę pierwszą dokładnie 869.127.442.359 milisekund od wyż. wsp.
daty. Fakt ten przekazywany jest przeglądarce lecz tylko do najbliższej sekundy:
Thu, 17 – Jul – 97 09:17:22 GMT
Teraz załóżmy znowu., że użytkownik powtórnie ładuje stronę i, że przeglądarka podaje serwerowi poprzez
nagłówek If-Modified-Since, czas który uważa za czas ostatniej modyfikacji:

*
Aplet może wstawić bezpośrednio swój nagłówek LastModified, w doGet(), przy użyciu technik omówionych w
rozdziale 5.(„Przesyłanie informacji HTML”). Jednakże do czasu kiedy nagłówek zostanie wstawiony w doGet() jest już
zbyt późno by zdecydować o tym czy wywoływać doGet() czy nie.
Thu, 17-Jul-97 09:17:22 GMT
Niektóre serwery przyjmują ten czas, przeliczają go na dokładnie 869 127 442 000 milisekundy, uznają iż jest on
359 milisekund wcześniejszy od odesłanego przez getLastModified(), a następnie fałszywie zakładają, że
treść apletu uległa zmianie. Dlatego właśnie, żeby zachować bezpieczeństwo („to play it safe”),
getLastModified() powinna zawsze zaokrąglać do najbliższego tysiąca milisekund. Obiekt
HttpServletRequest jest przekazywany do getLastModified() w razie jakby aplet potrzebował oprzeć
swoje rezultaty na informacjach specyficznych dla określonego zlecenia. Standardowy aplet elektronicznego
biuletynu informacyjnego może to wykorzystać np. do określenia który biuletyn jest przedmiotem zlecenia.

Buforowanie zewnętrzne po stronie serwera


Metoda getLastModified() może, przy odrobinie pomysłowości, być pomocna w zarządzaniu pamięcią
podręczną wydruku zewnętrznego apletu. Aplety stosujące taki chwyt mogą mieć swój wydruk zewnętrzny
przechwycony i umieszczony w pamięci podręcznej na stronie serwera, a następnie odesłany do klientów jak to
ma miejsce przy metodzie getLastModified(). Taka procedura może znacznie przyspieszyć tworzenie strony
apletu, szczególnie w przypadku apletów, którym zajmuje dużo czasu tworzenie wydruku wyjściowego,
zmieniającego się rzadko, takich jak np. aplety wyświetlające dane z bazy danych.
Celem wdrożenia buforowania zewnętrznego po stronie serwera, aplet musi:

• Rozszerzyć com.oreilly.servlet.CacheHttpServlet zamiast HttpServlet


• Wdrożyć metodę getLastModified (HttpServletRequest)
Przykład 3.10 ukazuje aplet korzystający z CacheHttpServlet. Jest to księga gości apletu, która wyświetla
przedłożone przez użytkowników komentarze. Aplet przechowuje komentarze użytkowników w pamięci jako
obiekty Vector of GuestbookEntry.W rozdziale 9 „Dołączalność bazy danych” poznamy wersję tego apletu
działającą poza bazą danych. W celu stymulacji czytania z wolnej bazy danych, pętla wyświetlacza ma pół-
sekundowe opóźnienie na hasło. Im dłuższa lista haseł tym wolniejsza wizualizacja strony. Jednakże z powodu
tego, że aplet rozszerza CacheHttpServlet, wizualizacja musi mieć miejsce tylko podczas pierwszego
zlecenia GET, po dodaniu nowego komentarza. Wszystkie późniejsze zlecenia GET wysyłają odpowiedź z
pamięci podręcznej. Przykładowy wydruk wyjściowy został pokazany na rycinie 3.4.

Przykład 3.10. Lista gości używająca apletu „CacheHttpServlet”

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.CacheHttpServlet;

public class Guestbok extends HttpServlet {

private Vector entries = new Vector(); // Lista haseł użytkownika


private long lastModified = 0; // Czas, w którym zostało
// dodane ostatnie hasło

// Wyświetl aktualne hasła, następnie poproś out.println nowe hasło


public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text / zwykły");
PrintWriter out = res.getWriter();

printHeader(out);
printForm(out);
printMassages(out);
printFooter(out);
}

// Dodaj nowe hasło, następnie odeślij z powrotem do doGet()


public void doPost (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
handleForm(req, res);
doGet(req, res);
}
private void printHeader(PrintWriter out) throws ServletException {
out.println ("<HTML><HEAD><TITLE><Guestbook</TITLE></HEAD");
out.println ("<BODY>");
}
private void printForm(printWriter out) throws ServletException {
out.println ("<FORM METHOD=POST>"); // księguje do siebie

out.println ("<B>Proszę prześlij swoje informacje kontrolne:<B><BR>");


out.println ("Twoje imię i nazwisko:<INPUT TYPE = TEXT NAME = name><BR>");
out.println ("Twój e-mail: :<INPUT TYPE = TEXT = e-mail><BR>");
out.println ("Komentarz: INPUT TYPE = TEXT SIZE = 50 NAME = comment ><BR>");
out.println ("<INPUT TYPE = SUBMIT VALUE=\"Prześlij informacje
kontrolne\"><BR>"); e-mail><BR>");

out.println ("</FORM>);
out.println ("<HR>");
}

private void printMessages(PrintWriter out) throws ServletException {

String name, email, comment;

Enumeration e = entries.elements();
while (e.hasMoreElements()) {
GuestbookEntry = (GuestbookEntry) e.nextElement();
name = entry.name;
if(name == null) name = "Nieznany użytkownik";
email = entry.email;
if(name == null) email = "Nieznany e-mail";
comment = entry.comment;
if (comment = null) comment = "Bez komentarza";
out.println ("<DL">);
out.println ("<DT><B>" + name + "</B>(" + e-mail +") jest następującej treści");
out.println ("<DD><PRE>" + comment + "</PRE>");
out.println ("</DL>");
// Wstrzymaj wykonywanie na pół sekundy w celu pobudzenia
// powolnego źródła danych
try {Thread.sleep(500);} catch (InterruptedExceptionignored) { }
}
}

private void printFooter(printWriter out) throws ServletException {


out.println ("</BODY>");
}

private void handleForm (HttpServletRequest req,


HttpServletResponse res) {

GuestbookEntry entry = new GuestbookEntry();

entry.name = req.getParameter("imię i nazwisko");


entry.email = req.getParameter("e-mail");
entry.coment = req.getParameter("komentarz");
entries.addElement(entry);

// Zwróć uwagę, ż mamy nowy czas ostatniej modyfikacji


lastModified = System.currentTimeMillis();
}

public long getLastModified (HttpServletRequest req) {


return LastModified;
}
}
class GuestbookEntry {
public String name;
public String email;
public String comment;
}
Źródło CacheHttpServlet zostało pokazane na przykładzie 3.11. Kod, w tym miejscu tej książki, może
wydawać się bardzo skomplikowany. Po przeczytaniu rozdziału 5, można spróbować odczytać kod dla tej klasy.
Przed obsługą zlecenia klasa CacheHttpServlet sprawdza wartość getLastModified(). Jeżeli pamięć
podręczna wydruku wyjściowego jest co najmniej tak aktualna jak czas ostatniej modyfikacji apletu, schowany w
pamięci podręcznej wydruk wyjściowy wysyłany jest bez wywoływania metody doGet() apletu.
Jeżeli klasa ta wykryje, że ciąg zapytań, informacje dodatkowej ścieżki dostępu lub ścieżki dostępu apletu
zostały zmienione, to w celu zachowania bezpieczeństwa pamięć podręczna jest unieważniana i tworzona od
nowa. Unieważniana nie jest jednakże pamięć oparta na różnych nagłówkach zleceń lub cookies; w przypadku
apletów, które różnicują swój wydruk wyjściowy, oparty na takich wartościach (tzn. serwerów śledzących sesję),
klasa ta raczej nie powinna być używana lub metoda getLastModified() powinna zająć się nagłówkami oraz
cookies. Buforowanie zewnętrzne nie jest stosowane przy zleceniach POST.

Rysunek 3.4. Wydruk wyjściowy księgi gości

CacheHttpServletResponse oraz CacheServletOutputStream są klasami pomocniczymi dla klasy i nie


powinny być używane bezpośrednio. Klasa została utworzona na podstawie Interfejsu API 2.2 (Servlet API 2.2),
dlatego używanie jej z poprzednimi wersjami InterfejsuAPI przebiega poprawnie; jednakże używanie jej z
przyszłymi wersjami prawdopodobnie nie będzie już przebiegało tak dobrze ponieważ interfejs
HttpServletResponse, który CacheHttpServletResponse musi wdrożyć, prawdopodobnie ulegnie
zmianie i w związku z tym niektóre metody interfejsowe pozostaną nie wdrożone. Jeżeli natknęlibyśmy się na
taki problem, to aktualna wersja tej klasy dostępna jest na stronie http://www.servlets.com.
Interesujący jest sposób w jaki CacheHttpServlet przechwytuje zlecenie w celu jego wczesnego
przetworzenia. Otóż wprowadza on metodę service (HttpServletRequest, HttpServerResponse),
którą serwer wywołuje w celu przekazania apletowi kontroli nad obsługą zleceń. Standardowa implementacja
HttpServlet tej metody przesyła zlecenie do doGet(), doPost() oraz innych metod zależnych od metody
HTTP zlecenia. CacheHttpServlet ignoruje taką implementację zyskując tym samym pierwszeństwo kontroli
nad obsługą zleceń. Kiedy klasa kończy przetwarzanie może wtedy przekazać z powrotem kontrolę do
implementacji egzekucyjnej za pomocą wywołania super.service().

Przykład 3.11. Klasa CacheHttpServlet

package com.oreilly.servlet;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public abstract class CacheHttpServlet extends HttpServlet {

CacheHttpServletResponse cacheResponse;
long cacheLastMod = -1;
String cacheQueryString = null;
String cachePathInfo = null;
String cacheServletPath = null;
Object lock = new Object();

protected void service (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Dla zleceń GET wykonuj tylko buforowanie podręczne
String method = req.getMethod();
if(!method.equals("GET")) {
super.service(req, res);
return ;
}
// Sprawdź czas ostatniej modyfikacji dla tego apletu
long servletLastMod = getLastModified(req);

// Ostatnio zmodyfikowany –1 oznacza, że nie powinniśmy w ogóle używać pamięci


logicznej
if(servletLastMod == -1) {
super.service(req, res);
return ;
}

// jeżeli klient przysłał nagłówek If-Modified-Since, w lub po


// czasie ostatniej modyfikacji apletu prześlij krótki kod statusu
// "Nie zmodyfikowany". Zaokrągl do najbliższej sekundy
// jako że nagłówki klienta są w sekundach
if((servletLastMod / 1000 * 1000) <=

req.getDateHeader("If-Modified-Since")) {
res.SetStatus(res.S.C._NOTMODIFIED);
return ;
}

// Wykorzystaj istniejącą pamięć podręczną jeżeli jest ona aktualna i poprawna


CacheHttpServletResponse localResponseCopy = null;
synchronized(lock) {
if (servletLastMod <= cacheLastMod &&
cacheResponse.isValid() &&
equal (cacheQueryString, req.getQueryString()) &&
equal (cachePathInfo, req.getPathInfo()) &&
equal (cacheServletPath, req.getServletPath()) {
localResponseCopy = cacheResponse;
}
}
if (localResponseCopy ! = null) {
localResponseCopy.writeTo(res);
return ;
}

// W przeciwnym razie utwórz nowy schowek, aby zachować odpowiedź


localResponseCopy = new CacheHttpServletResponse(res);
super.service(req, localResponseCopy);

synchronized (lock) {
cacheResponse = localResponseCopy;
cacheLastMod = servletLastMod;
cacheQueryString();
cachePathInfo = req.getPathInfo();
cacheServletPath = req.geServletPath();
}
}

private boolean equal(String s1, String s2) {


if (s1 == null && s2 == null) {
return true;
}
else if (s1 = null I I s2 == null) {
return false;
}
else {
return s1.equals(s2);
}
}
}

class CacheHttpServletResponse implements HttpServletResponse {


// Przechowuj kluczowe zmienne odpowiedzi w celu
// wstawienia ich później
private int status
private Hashtable headers;
private int contentLength;
private String contentType;
private Locale locale;
private Vector cookies;
private boolean didError;
private boolean didRedirect;
private boolean gotStream;
private boolean gotWriter;

private HttpServletResponse delegate;


private cacheServletOutputStream out;
private PrintWriter writer;
CacheHttpServletResponse (HttpServletResponse res) {
delegate = res;
try {
out = new cacheServletOutputStream(res.getOutputStream());

}
catch (IOExcepion e) {

System out.println (
" Otrzymałeś IOException tworząc odpowiedź z pamięci podręcznej:"
+ e.getMassage());
}
internalReset() {
}

private void internalReset() {


status = 200;
headers = new Hashtable;
contentLength = -1;
contentType = null;
locale = null;
cookies = new Vector();
didError = false;
didRedirect = false;
gotStream = false;
gotWriter = false;
out.grtBuffer().reset();
}

public boolean isValid() {

// Nie przechowujemy w pamięci podręcznej stron z błędami


// lub przekierowań
return didError != true && didRedirect !=true;
}

private void internalSetHeader(String name, Object value) {


Vector v = new Vector();
v.addElment(value);

headers.put(name, v);
}

private void internalAddHeader(String name, Object value) {


Vector v = (Vector) headers.get(name);
if (v == null ) {
v = new Vector();
}
v.addElement(value);
headers.put(name, v);
}

public void writeTo(HttpServletResponse res) {


// Pisz kod statusu
res.setStatus(status)
// Pisz nagłówki upraszczające
if (contentType != null)res.setContentType(contentType);
if (locale! = null) res.setLocale(locale);
// Pisz cookies
Enumeration enum = cookies.elements();
while (enum.hasMoreElements()) {
Cookie c =(Cookie) enum.nextElement();
res.addCookie(c);
}
// Pisz nagłówki standardowe
enum = header.keys();

while(enum.hasMoreElements()) {
String name = (String) enum.nextElement();
Vector values = (Vector) headers.get(name); // może mieć wielokrotne wartości
Enumeration enum2 = values.elements();
while (enum2.hasMoreElements()) {
Object value = enum2.nextElement();
if(value instanceof String ) {
res.setHeader(name, (String )value);

}
if(value instanceof Integer ) {
res.IntHeader(name, ((Integer )intvalue());
}

if((value instanceof Long ) {


res.setDateHeader(name, ((Long )value).longValue());
}
}
}
// Pisz długość treści
res.setContentLength(name, (out.getBuffer().size());
// Pisz treść
try {
out.getBuffer().writeTo(res.getOutputStream(();

}
catch (IOException e) {
System.out.println(
" Otrzymałeś IOException pisemną odpowiedź tekstową z pamięci
podręcznej:" + e.getMessage());
}

public ServletOutputStream() throws IOExcepion {


if (gotWriter) {
throw new IllegalStateException(
"Niemożliwość otrzymania strumienia wyjściowego po uzyskaniu wykonawcy
zapisu");
}
gotStream = true;
return out;
public PrintWriter getWriter() throws UnsupportedEncodingExcepion {

if (gotStream) {
throw new IllegalStateException(
" Niemożliwość otrzymania wykonawcy zapisu po uzyskaniu strumienia
wyjściowego ");
}
gotWriter = true;
if(writer == null) {

OutputStreamWriter w =
new OutputStreamWriter(out, getCharacterEncoding());
writer = new PrintWriter(w, true); // konieczne jest automatyczne
// opróżnienie pamięci podręcznej
}
return writer;
}
public void setContentLength(int len) {
delegate.setContentLength(len);
// Nie ma potrzeby zapisywania długości,
// możemy obliczyć to później
}

public void setContentType(String type) {


delegate.setContentType(type);

contentType = type;
}

public String getCharacterEncoding () {

return delegate.getCharacterEncoding ();


}

public void setBufferSize(int size) throws IllegalStateExcepion {


delegate.setBufferSize(size);
}

public int getBufferSize() {

return delegate.getBufferSize();
}

public void reset() throws IllegalStateExcepion {


delegate.reset();

internalReset();
}

public boolean isCommitted() {


return delegate.isCommitted();
}

public void flushBuffer() throws IOException {


delegate flushBuffer();
}

public void setLocale(Locale loc) {


delegate.setLocale(loc);
locale = loc;
}

public Locale getLocale(){


return delegate.getLocale();
}
public void addCookie(Cooki cookie) {
delegate.addCookie(cookie);
cookies.addElement(cookie);
}

public boolean containsHeader(String name) {


return delegate.containsHeader(name);
}

/**@deprecated*/
public void setStatus(int sc, String sm) {
delegate.setStatus(sc, sm);

status = sc;
}

public void setStatus(int sc) {


delegate.setStatus(sc);
status = sc;
}

public void setHeader(String name, String value) {


delegate.setHeader(name, value);

internalSetHeader(name, value);
}

public void setIntHeader(String name, int value) {


delegate.setIntHeader(name, value);
internalSetHeader(name, new Integer(value));
}

public void setDateHeader(String name, long date) {


delegate.setDateHeader(name, date);
internalSetHeader(name, new Long(date));
}

public void sendError(int sc, String msg) throws IOException {


delegate.sendError(sc, msg);
didError = true;
}

public void sendError (iny sc) throws IOExcepion {


delegate.sendError (sc);
didError = true;

public void sendRedirect(String location) throws IOExcepion {


delegate.sendRedirect(location);
didRedirect = true;
}

public String encodeURL(String url) {


return delegate.encodeURL(url);
}

public String encodeRedirectURL(String url) {


return delegate.encodeRedirectURL(url);
}

public void addHeader(String name, String value) {


internalAddHeader(name, value);
}
public void addIntHeader(String name, int value) {
internalAddHeader(name, newInteger(value));
}
public void addDateHeader(String name, long value) {
internalAddHeader(name, new Long(value));
}

/**@deprected*/
public String encodeUrl(String url) {
return this.encodeURL(url);
}

/**@deprected*/
public String encodeRedirectUrl(String url) {
return this.encodeRedirectURL(url);
}
}

class CacheServletOutputStream extends ServletOutputStream {

ServletOutputStream delegate;

ByteArrayOutputStream cache;

CacheServletOutputStream(ServletOutputStream out) {
delegate = out;
cache = new ByteArrayOutputStream(4096);
}

public ByteArrayOutputStream getBuffer() {


return cache;
)

public void write(int b) throws IOException {


delegate.write(b);
cache.write(b);
}

public void write(byte b[]) throws IOException {

delegate.write(b);
cache.write(b);
}

public void write(byte buf[], int offset, int len) throws IOException {
delegate.write(buf, offset, len);
cache.write(buf, offset, len);

}
}
Rozdział 4. Odczytywanie

informacji

Do utworzenia udanej aplikacji WWW niejednokrotnie potrzebna jest gruntowna znajomość środowiska, w
którym będzie ona uruchamiana. Dla przykładu konieczne może okazać się zaznajomienie z serwerem, który
wywołuje nasze aplety lub ze specyfikacjami klienta, który przesyła zlecenia. Bez względu na rodzaj środowiska,
w którym uruchomiana jest aplikacja, jest wielce prawdopodobne, że będziemy potrzebowali informacji o
zleceniach, które ona obsługuje.
Istnieje wiele metod, które umożliwiają apletom dostęp do takich informacji. Dla większości z nich każda
metoda odsyła jeden, określony wynik. W porównaniu ze sposobem, w jakim zmienne środowiskowe używane są
aby przekazać informacji programowi CGI, metoda apletowa ma wiele zalet, które sprawiają, że jest ona lepsza:

• Dokładniejsza kontrola zgodności typów. Aplety otrzymują więcej pomocy ze strony kompilatora w
wyłapywaniu błędów. Programy CGI używają jednej funkcji do odczytywania swoich zmiennych
środowiskowych. Wiele błędów może nie zostać wykrytych aż do czasu kiedy zaczną powodować problemy
wykonawcze. Teraz rzućmy okiem jak program CGI oraz aplet znajdują port, na którym działa ich serwer.
Skrypt CGI napisany w PERL-u wywołuje:

$port = $ENV{'SERVER_PORT'};
gdzie $port jest zmienną beztypową. Program CGI napisany w języku C wywołuje:

char *port = getenv ("SERVER_PORT");


gdzie jest zmienną wskaźnikową dla ciągu znaków. Ryzyko
port
przypadkowego błędu jest duże. Nazwa zmiennej może
zostać źle przeliterowana (zdarza się to dość często)
możliwe jest również, że typ danych nie będzie zgodny z
odsyłanym przez zmienną środowiskową.
Aplet, z kolei, wywołuje:
int port = req.getServerPort()

Pozwala to wyeliminować wiele przypadkowych błędów, ponieważ kompilator gwarantuje

prawidłowy zapis, ponadto wszystkie typy zwracane są pozbawione nieprawidłowości.

• Opóźnione obliczanie. W momencie uruchamiania programu CGI przez serwer wartość dla wszystkich
zmiennych środowiskowych oraz każdej z osobna musi być już uprzednio przeliczona i przekazana —
niezależnie od tego czy są one używane przez program CGI czy też nie są. Serwer uruchamiając aplet ma
możliwość poprawienia wydajności poprzez opóźnienie podobnych kalkulacji oraz wykonywanie ich na
żądanie wtedy kiedy jest to potrzebne.
• Więcej interakcji z serwerem. W momencie kiedy program CGI rozpoczyna uruchamianie nie jest już
zależny od swojego serwera. Jedyna ścieżka komunikacji, dla programu to jego standardowy port
wyjściowy. Jednakże co się tyczy apletu, to może on nadal współpracować z serwerem. Tak jak to zostało
omówione w poprzednim rozdziale, aplet działa albo (jeżeli to możliwe) w ramach serwera albo (jeżeli jest
to konieczne) jako przyłączona procedura, poza nim. Korzystając z takiej możliwości podłączenia aplet
może utworzyć zlecenia ad hoc dla informacji przeliczonych, których może dostarczyć tylko serwer. Dla
przykładu serwer może wykonać dla apletu translację dowolnej ścieżki uwzględniając swoje zamienniki oraz
ścieżki wirtualne.

Jeżeli przed apletami mieliśmy do czynienia z CGI, tabela 4.1 będzie pomocna w „przestawieniu się” na nowe

nazewnictwo. Na tej tabeli zestawione zostały wszystkie zmienne środowiskowe CGI z odpowiadającymi im

metodami apletów HTTP.

Tabela 4.1.
Zmienne środowiskowe CGI oraz odpowiadające im metody apletowe

Zmienna Środowiskowa CGI Metoda Apletu HTTP


SERVER_NAME req.getServerName()
SERVER_SOFTWARE getServletContext().getServerInfo()
SERVER_PROTOCOL req.getProtocol()
SERVER_PORT req.getServerPort()
REQUEST_METHOD req.getMethod()
PATH_INFO req.getPathInfo()
PATH_TRANSLATED req.getPathTranslated()
SCRIPT_NAME req.getServletPath()
DOCUMENT_ROOT getServletContext().getRealPath(„/”)
QUERY_STRING req.getQueryString()
REMOTE_HOST req.getRemoteHost()
REMOTE_ADDR req.getRemoteAddr()
AUTH_TYPE req.getAuthType()
REMOTE_USER req.getRemoteUser()
CONTENT_TYPE req.getContentType()
CONTENT_LENGTH req.getContentLength()
HTTP_ACCEPT req.getHeader(„Accept”)
HTTP_USER_AGENT req.getHeader(„User-Agent”)
HTTP_REFERER req.getHeader(„Referer”)

W pozostałej części tego rozdziału pokażemy kiedy i jak używa się tych metod oraz wielu
innych, które nie mają odpowiedników CGI. Dalej będziemy również używali metod w
konkretnych apletach.

Aplet
Z każdą zarejestrowaną nazwą apletu mogą być związane inne, specyficzne dla niej parametry początkowe. Aplet
może mieć w każdej chwili dostęp do tych parametrów: są one określone w deskryptorze wdrożenia web.xml
oraz stosowane w metodzie init() celem ustalenia wartości początkowych i domyślnych dla apletu lub
dostosowania jego zachowania w określony sposób. Parametry początkowe zostały szerzej opisane w rozdziale 3
„Czas istnienia (Cykl Życia) Apletu”.

Pobieranie parametru początkowego apletu


Aplet używa metody getInitParameter() celem dostępu do swoich parametrów początkowych
public String ServletConfig.getInitParameter(String name)

Metoda ta odsyła odsyła: wartość początkowego parametru nazwanego lub: null (jeżeli taki nie istnieje).
Odesłana wartość to zawsze pojedynczy String. Interpretacja tej wartości jest domeną apletu.
Klasa GenericServlet wdraża interfejs ServletConfig zapewniając tym samym dostęp do metody
getInitParameter(). Oznacza to, że metoda może zostać wywołana w następujący sposób:
public void init() thrcws ServletException {
String greeting = getInitParameter("pozdrowienie");
}
Aplet, któremu potrzebne jest połączenie z bazą danych może użyć swoich parametrów początkowych w celu określenia parametrów
połączenia. Celem odseparowania szczegółów JDBC możemy użyć metody dostosowanej establishConnection(), tak jak
zostało to pokazane na przykładzie 4.1.

Przykład 4.1.
Użycie parametrów początkowych w celu ustanowienia połączenia z bazą danych

java.sql.Connection con = null;

public void init() throws ServletException {


String host = getInitParameter("komputer główny");
int port = Integer.parseInt(getInitParameter( "port"));
String db = getInitParameter("db");
String user = getInitParameter("użytkownik");
String password = getInitParameter ("hasło") ;
String proxy = getInitParameter ("proxy") ;

con = establishConnection(host, port, db, user, password, proxy);


}
Istnieje również inny, bardziej zaawansowany, standardowy model oddzielania dla apletów
zaprojektowanych dla Java 2, Enterprise Edition (J2EE). Więcej na ten temat w rozdziale 12
„Aplety Przedsiębiorstw i J2EE”.

Pobieranie nazw parametrów początkowych apletów


Używając metody getInitParameterNames() aplet może sprawdzić wszystkie swoje parametry początkowe:

public Enumeration ServletConfig. getInitParameterNames()

Metoda ta odsyła nazwy wszystkich parametrów początkowych apletu jako Enumeration (wyliczenie)
wszystkich obiektów String lub jak puste Enumeration jeżeli nie istnieją żadne parametry. Znajduje to
zastosowanie przy usuwaniu błędów z programu (przy tzw. „debagowaniu”).

Klasa GenericServlet dodatkowo udostępnia bezpośrednio tą metodę apletom. Poniższy przykład 4.2 ukazuje
aplet, który drukuje nazwę oraz wartość dla wszystkich swoich parametrów początkowych.
Przykład 4.2.
Pobieranie nazw parametrów początkowych

import java.io.*;
import java.util.*;
import javax.servlet.*;

public class InitSnoop extends GenericServlet {

// Metoda init() nie jest potrzebna

public void service(ServletRequest req, ServletResponse res)


throws ServletException, IOException {
res. setContentType (" tekst/zwykły") ;
PrintWriter out = res.getWriter() ;
out.println("Parametry Początkowe:") ;
Enumeration enum = getInitParameterNames() ;
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement();
out. println( name + ": " + getInitParameter(name));
}
}
}

Zauważmy, że aplet ten jest bezpośrednio podrzędny do GenericServlet, dowodząc tym samym, że parametry
początkowe dostępne są także dla apletów, które nie są apletami HTTP. Standardowy aplet może zostać użyty w
aplikacji WWW serwera nawet w sytuacji kiedy nie realizuje zespołu funkcji. HTTP-specyficznych.

Pobieranie nazwy apletu


Interfejs ServletConfig również zawiera metodę, która odsyła zarejestrowaną nazwę apletu:

public String ServletConfig.getServletName()

Jeżeli aplet jest niezarejestrowany metoda odsyła nazwę klasy apletu. Metoda ta znajduje zastosowanie przy
pisaniu do dzienników zdarzeń wprowadzaniu informacji o stanie kopii obiektów apletów do wspólnych zasobów
takich jak baza danych czy SessionContext apletu, które wkrótce zostaną przez nas omówione.

Tak jak to zostało pokazane na przykładzie, poniższy kod prezentuje sposób w jaki stosuje się
nazwy apletu w celu odczytania wartości z kontekstu apletu, używając nazwy jako części
klucza wyszukiwania:
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, lOException {
String name = getServletName() ;
ServletContext context = getServletContext();
Object value = context. getAttribute (name + ".stan");
}

Przy użyciu nazwy apletu w kluczu każda kopia apletu może bez problemu utrzymywać oddzielną wartość
atrybutu we wspólnym kontekście.

Serwer
Aplet może uzyskać sporo informacji o serwerze, w którym wywołuje. Może, między innymi
poznać nazwę komputera, port na którym serwer oczekuje na sygnały, oprogramowanie,
którego używa serwer. Aplet może wyświetlić te informacje klientowi, użyć ich w celu
dostosowania swojego zachowania opartego na konkretnym zestawie serwera lub nawet użyć
ich w celu nałożenia wyraźnych ograniczeń na komputery, na których aplet będzie działał.

Pobieranie informacji o serwerze


Aplet większość swojego dostępu do informacji serwera uzyskuje poprzez obiekt ServletContext, w którym
wywołuje. Przed pojawieniem się wersji API 2.2 ServletContext
traktowany był głównie jako odwołanie do samego serwera. Od API 2.2 wiele rzeczy uległo
zmianie tak, że obecnie musi być odmienny obiekt ServletContext dla każdej aplikacji WWW
na serwerze. Obiekt ServletContext jest teraz odwołaniem do aplikacji WWW a nie
odwołaniem do serwera. Dla zapytań prostych serwerów nie sprawia to jednak większej
różnicy.
Istnieje pięć metod, które mogą zostać użyte przez aplet celem zdobycia informacji o swoim
serwerze: dwie, które są wywoływane przy użyciu obiektu ServletRequest przekazywanego do
apletu oraz trzy wywoływane z obiektu ServletContext, w którym aplet wywołuje.
Aplet może zdobyć nazwę serwera oraz numer portu dla poszczególnych zleceń za pomocą
metod getServerName() oraz getServerPort() odpowiednio:
public String ServletRequest.getServerName()
public int ServletRequest.getServerPort()

Metody te są atrybutami ServletRequest ponieważ jeżeli serwer ma więcej niż jedną nazwę, wartości mogą
ulegać zmianie dla różnych zleceń (technika virtual hosting). Odesłana nazwa może np. wyglądać w następujący
sposób:www.servlets.com, przykładem odesłanego portu może być 8080.
Metody getServerInfo() oraz getAtribut() ServletContext dostarczają informacji o
oprogramowaniu serwera i jego atrybutach:
public String ServletContext.getServerInfo()
public Object ServletContext.getAttribute(String name)
getServerInfo() odsyła przedzielone ukośnikiem: nazwę oraz wersję oprogramowania serwera. Odesłany
ciąg znaków mógłby wyglądać w następujący sposób: Tomcat Web Server/3.2. Niektóre serwery
zamieszczają na końcu dodatkowe informacje dotyczące środowiska operacyjnego serwera.
getAttribute() odsyła wartość atrybutu serwera nazwanego, jako Object lub jeżeli atrybut nie istnieje —
null. Serwery mogą umieścić atrybuty ustalone w kontekście, dla użytku apletów. O metodzie tej można by
powiedzieć, że jest furtką przez którą aplet może uzyskać dodatkowe informacje o swoim serwerze. Dla
przykładu serwer mógłby udostępnić statystyki dotyczące obciążenia, odwołanie do puli wspólnych zasobów lub
każdą inną potencjalnie użyteczną informację. Jedynym obowiązkowym atrybutem, który serwer musi udostępnić
jest atrybut zwany javax.servlet.context.tempdir, który zapewnia odwołanie java.io.File do
katalogu poufnego dla tego kontekstu.
Aplety mogą również dodawać do kontekstu swoje własne atrybuty używając do tego celu metody
setAttribute(), tak jak to zostało omówione w rozdziale 11 „Współpraca Serwerów”. Nazwy atrybutów
powinny być zapisywane zgodnie z tą samą konwencją co nazwy pakietów. Nazwa pakietów java.* oraz
javax* są zarezerwowane dla użytku Java Software division of Sun Microsystems, z kolei com.sun.*
zarezerwowana jest dla użytku Sun Microsystems. Lista atrybutów serwera zawarta jest w jego dokumentacji.
Wykaz wszystkich aktualnych atrybutów, przechowywanych w pamięci serwera oraz innych apletów dostępna
jest przy użyciu getAttributeNames():
public Enumeration ServletContext.getAttributeNames()

W związku z tym, że metody te są atrybutami ServletContext, w którym wywołuje aplet, musimy wywołać je
za pomocą poniższego obiektu:

String serverlnfo = getServletContext().getServerInfo() ;

Najprostszym zastosowaniem informacji o serwerze jest aplet About This Server, sytuacja taka została
zaprezentowana na przykładzie 4.3.

Przykład 4.3.
„Podsłuchiwanie” serwera

import java.io.* ;
import java.uti1.*;
import javax.servlet.* ;

public class ServerSnoop extends GenericServlet {

public void service(ServletRequest req, ServletResponse res)


throws ServletException, IOException
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

ServletContext context = getServletContext() ;


out.printn("req.getServerName(): " + req.getServerName()) ;
out.println("req.getServerPort(): " + req.getServerPort()) ;
out.println("context.getServerInfo(): "+context.getServerInfo());
out.println("getServerInfo() nazwa: " +
getServerInfoName(context.getServerInfo()));
out.println("getServerInfo() version: " +
getServerInfoVersion(context.getServerInfo())) ;
out.println("context.getAttributeNames():");
Enumeration enum = context. getAttributeNames () ;
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement();
out.printin(" context.getAttribute(\"" + name + "\"): " +
context.getAttribute(name)) ;
}
}

private String getServerInfoName(String serverlnfo) {


int slash = serverlnfo.indexOf('/');
if (slash == -1) return serverlnfo;
else return serverlnfo.substring(0, slash);
}

private String getServerInfoVersion(String serverlnfo) {

// Wersja info to wszystko to, co jest zawarte między ukośnikiem a


spacją
int slash = serverlnfo.indexOf('/');
if (slash == -1) return null;
int space = serverlnfo.indexOf(' ', slash);
if (space == -1) space = serverlnfo.length();
return server Info.substring(slash + 1, space);
}
}

Aplet ten jest bezpośrednio podrzędny (jest bezpośrednią podklasą) do GenericServlet,dowodząc tym
samym, że wszystkie informacje o serwerze dostępne są dla wszystkich typów apletów. Aplet wyświetla prosty,
nieprzetworzony tekst, w czasie połączenia z nim drukuje mniej więcej coś takiego:

req.getServerName(): localhost
req.getServerPort(): 8080
context.getServerInfo(): Tomcat Web Server/3.2 (JSP 1.1; Servlet 2.2; ...)
get Server Info() name: Tomcat Web Server
getServerInfo() version: 3.2
context .getAttributeNames () :
context. getAttribute (" javax. servlet. context. tempdir") : work/
localhost_8080

Zapisywanie w pliku tymczasowym


Atrybut javax.servlet.context.tempdir odwzorowuje do katalogu tymczasowego, w którym to katalogu
przechowywane mogą być „krótkotrwałe” pliki robocze. Każdemu kontekstowi przyznawany jest inny katalog
tymczasowy. Katalogiem dla poprzedniego przykładu jest server_root/work/localhost_8080. Na przykładzie 4.4
pokazano jak zapisywać w pliku tymczasowym, w katalogu tymczasowym.

Przykład 4.4.
Tworzenie pliku tymczasowego w katalogu tymczasowym

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Katalog podany jest jako obiekt pliku
File dir = (File) getServletContext()
.getAttribute("javax.servlet.context.tempdir") ;

// Utwórz plik tymczasowy w katalogu tymczasowym (metoda JDK 1.2)


File f = File.createTempFile("xxx", ".tmp", dir);

// Przygotuj się do zapisywania w pliku


FileOutputStream fout = new FileOutputStream(f) ;

// ...
}
Serwer ten najpierw lokalizuje swój katalog tymczasowy. Następnie używa metody createTempFile() celem
utworzenia w tym katalogu pliku tymczasowego o nazwie i rozszerzeniu odpowiednio xxx i .tmp i w końcu
konstruuje FileOutputStream, ażeby stworzyć możliwość zapisywania w pliku tymczasowym. Pliki, które
muszą pozostawać pomiędzy kolejnymi, ponownymi uruchomieniami serwera nie powinny być umieszczane w
katalogu tymczasowym.

Ograniczanie działania apletu do serwera


Istnieje wiele sposobów korzystnego wykorzystania informacji serwera. Załóżmy, że
napisaliśmy aplet i nie chcemy aby można go było uruchomić gdziekolwiek. Dla przykładu
chcemy go sprzedać i, w celu ograniczenia ryzyka nielegalnego skopiowania, ograniczyć jego
działanie do komputera naszego klienta, który kupił od nas aplet. Albo inaczej, napisaliśmy
licencyjny program działający jako aplet i chcemy się upewnić, że działa on tylko poza
naszym firewall’em. Można to wykonać względnie szybko ponieważ aplet ma stały dostęp do
informacji o swoim serwerze.

Przykład 4.5 ukazuje aplet, który ogranicza się (swoje działanie) do określonego
serwerowego adresu IP oraz numeru portu. Apletowi temu do usunięcia podobnego
ograniczenia i do obsługi zlecenia, potrzebny jest klucz parametru początkowego, właściwy
dla adresu IP oraz portu jego serwera. Jeżeli aplet nie otrzyma takiego klucza wstrzyma
dalsze działanie.Algorytm używany do odwzorowywania klucza do adresu IP oraz portu (i
vice versa) musi być bezpieczny.

Przykład 4.5.
Aplet ograniczony do serwera
import java. io. *;
import java.net. *;
import java.util.* ;
import javax.servlet.*;

public class KeyedServerLock.extends GenericServlet {

// Aplet ten nie ma klasy ani zmiennej egzemplarza


// związanej z blokowaniem (celem uproszczenia zagadnień
// związanych z synchronizacją)

public void service(ServletRequest req, ServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

// Kontrola antypiracka nie powinna być wykonywana w init


// ponieważ nazwa/port są częścią zlecenia.
String key = getInitParameter("key");
String host = req.getServerName() ;
int port = req.getServerPort() ;

// Sprawdź czy parametr początkowy „klucz” odblokowuje ten


serwer.

if (! keyFitsServer(key, host, port)) {


// Wyjaśnij, potęp/ zagróź/ itd.
out.println("kopia piracka!");
}
else (
// Dostarcz towar
out ,println( "kopia licencjonowana") ;
// itd...
}
}

// Metoda ta zawiera algorytm używany do dopasowania klucza z


// komputerem centralnym i portem. Niniejszy przykład implementacji
// jest dalece nie doskonały i nie powinien być używany przez
// strony komercyjne.
private boolean keyFitsServer(String key, String host, int port) {
if (key == null) return false;

long numericKey =0;


try {
numericKey = Long.parseLong(key);
}
catch (NumberFormatException e) {
return false;
}

// Klucz musi być liczbą 64-bitową równą logicznej negacji (~)


// 32-bitowego adresu IP połączonego z 32-bitowym numerem portu.

byte hostIP[] ;
try {
hostIP = InetAddress.getByName(host) . getAddress ();
}
catch (UnknownHostException e) {
return false;
}

// Pobierz 32-bitowy adres IP


long servercode = 0;
for (int i = 0; i < 4; i++) {
servercode «= 8;
servercode |= hostIP[i];
}

// Połącz 32-bitowy numer portu


servercode «= 32;
servercode |= port;

// logiczna negacja
long accesscode = -numericKey;

// Moment prawdy: Czy klucz pasuje?


return (servercode == accesscode) ;
}
}

Aplet, dopóki nie otrzyma właściwego klucza nie wykona żadnego polecenia. Jednakże celem zapewnienia
całkowitego bezpieczeństwa, prosta logiczna metoda keyFitsServer powinna być zastąpiona przez „mocny”
algorytm ponadto cały aplet powinien zostać uruchomiony poprzez zaciemniacz (ażeby uniknąć dekompilacji).
Przykład 4.13 (omówiony dalej w tym rozdziale) zawiera kod używany do generowania kluczy. Jeżeli będziemy
testowali ten aplet sami, lepiej jest połączyć się z serwerem zużywając jego rzeczywistej nazwy (niż localhost)
ponieważ aplet może dzięki temu określić prawdziwą nazwę serwera WWW oraz jego adres IP.

Pobieranie kontekstowego parametru początkowego


Parametry początkowe apletu, tak jak to zostało omówione wcześniej przekazywane są do
oddzielnych apletów. Jeżeli zdarzyłoby się tak, że aplety wielokrotne otrzymałyby te same
wartości parametrów początkowych, wartości te mogłyby zostać przypisane jako parametr
początkowy context. Klasa ServletContext ma dwie metody — getInitParameter() oraz
getInitParameterNames() — dla odczytywania szerokokontekstowych informacji inicjowania:
public String ServietContext.getInitParameter(String name)
public Enumeration ServietContext.getInitParameterNames()

Metody te są modelowane po ich odpowiednikach w ServletConfig. getInitParameter(String name)


odsyła wartość łańcucha określonego parametru. getInitParameterNames() odsyła Enumeration zawiera
nazwy wszystkich parametrów początkowych dostępnych dla aplikacji WWW lub puste Enumeration — w
przypadku gdy nie było żadnych nazw.
Parametry początkowe dla kontekstu zostały określone w deskryptorze rozmieszczenia web.xml, przy użyciu
znacznika <context-param> (tak jak zostało to pokazane na przykładzie 4.6).

Przykład 4.6.
Ustalanie kontekstowych parametrów początkowych w deskryptorze wdrożenia
<?xml version="1.0" kodowanie="ISO-8859-l"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://Java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app>
<context-param>
<param-name>
rmihost
< /param-name>
<param-value>
localhost
< /param-value>
</ context-param>
<context-param>
<param-name>
rmiport
</param-name>
<param-value>
1099
< /param-value>
< / context-param>
</web-app>

Wszystkie aplety w tej aplikacji WWW, aby zlokalizować wspólny rejestr RMI, mogą
odczytywać kontekstowe parametry początkowe, tak jak to zostało pokazane na przykładzie
4.7.

Przykład 4.7.
Znajdowanie rejestru przy użyciu kontekstowych parametrów początkowych

import java.io.*;
import java.rmi. registry.*;
import javax.servlet.*;
import javax.servlet.http.* ;

public class RmiDemo extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

try {
ServletContext context = getServletContext() ;
String rmihost = context.getInitParameter("komputer centralny
rmi");
int rmiport = Integer.parseInt(context.getInitParameter("port
rmi"));
Registry registry = LocateRegistry.getRegistry( rmihost,
rmiport) ;
// ...
}
catch (Exception e) {
// ...
}
}
}

Nie istnieje uniwersalny mechanizm umożliwiający tworzenie globalnych parametrów początkowych,


widocznych we wszystkich kontekstach.

Ustalanie wersji apletu


Aplet może również zapytać serwer jaka wersja Interfejsu API jest przez niego obsługiwana.
Poza tym, co jest przydatne podczas usuwania błędów z programu (debagowania), Aplet
podczas wykonywania zadania ma możliwość wybrać czy chce zastosować nowy sposób czy
też starszy, mniej wydajny. W Interfejsie API 2.1 zostały wprowadzone dwie metody
umożliwiające odsyłanie informacji dotyczących wersji:
public int ServletContext.getMajorVersion()
public int ServletContext.getMinorVersion()
Dla Interfejsu API 2.1 getMajorVersion() odsyła 2, a getMinorVersion() 1. Oczywiście metody te
działają tylko w przypadku apletów wywołujących w serwerach obsługujących wersję Interfejsu API 2.1 oraz
późniejsze. W celu określenia aktualnej wersji Interfejsu API na wszystkich serwerach można posłużyć się
com.oreilly.servlet.VersionDetector. Klasa ta nie „pyta” serwera o wersję; zamiast tego „przygląda”
się klasom i zmiennym dostępnym w czasie wykonywania, a opierając się na znajomości historii Interfejsu API
jest w stanie określić jego aktualną wersję z przedziału od 1.0 do 2.3. W związku z tym, że klasa ta nie wywołuje
ani getMajorVersion() ani też getMinorVersion() — działa tylko w wersjach API, kompiluje jednak we
wszystkich wersjach. Używając tej samej techniki klasa VersionDetector jest w stanie określić aktualną
wersję JDK począwszy od 1.0 do 1.3. Sposób ten, w przypadku wdrożeń producentów JVM, okazuje się być
bardziej wiarygodny niż pytanie System class. Na przykładzie 4.8 została zaprezentowana klasa
VersionDetector. Uaktualnienia do poniższej klasy umożliwiające obsługę wersji Interfejsów API oraz JDK,
będą zamieszczane pod adresem http://www.servlets.com.

Przykład 4.8.
Klasa VersionDetector

package corm.oreilly.servlet;

public class VersionDetector {

static String servletVersion;


static String javaVersion;

public static String getServletVersion() {


if (servletVersion != null) {
return servletVersion;
}

// javax.servlet.http.HttpSession wprowadzono w Interfejsie


API2.0
// javax.servlet.RequestDispatcher wprowadzono w Interfejsie API
2.1;
// javax. servlet.http.HttpServletResponse.SC__EXPECTATION_FAILED
// wprowadzono w Interfejsie API 2.2
// javax.servlet.Filter planuje się wprowadzić w Interfejsie API
2.3
String ver null;
try {
ver = "1.0";
Class.forName("javax.servlet.http.HttpSession");
ver = "2.0";
Class.forName("javax.servlet.RequestDispatcher");
ver = "2.1";
Class.forName("javax.servlet.http.HttpServletResponse")
.getDeclaredField("SC_EXPECTATION_FAILED") ;
ver = "2.2";
Class.forName("javax.servlet.Filter") ;
ver = "2.3";
}
catch (Throwable t) {
}
servletversion = ver;
return servletVersion;
}

public static String getJavaVersion() {


if (javaVersion != null) {
return javaVersion;
}

// java.lang.Void wprowadzono w JDK 1.1


// java.lang.ThreadLocal wprowadzono w JDK 1.2
// java.lang.StrictMath wprowadzono w JDK 1.3
String ver = null;
try {
ver = "1.0";
Class. forName (" java. lang. Void") ;
ver = "1.1"
Class.forName("java.lang.ThreadLocal") ;
ver = "1.2";
Class.forName("java.lang.StrictMath") ;
ver = "1.3";
}
catch (Throwable t) {
}
javaVersion = ver;
return javaVersion;
}
}

Klasa na zasadzie przeprowadzania prób ładowania klas oraz zmiennych dostępu — do momentu, w którym
NoClassDefFoundError lub NoSuchFieldException, zawiesi wyszukiwanie. Do tego czasu aktualna
wersja jest już znana. Przykład 4.9 prezentuje aplet, który „podsłuchuje” serwer oraz wersję Javy.

Przykład 4.9.
„Podsłuchiwanie” wersji

import java. io. *;


import javax.servlet.*;
import javax.servlet.http.*;

import com. oreilly. servlet. VersionDetector;

public class VersionSnoop extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse re


throws ServletException, IOException {
res.setContentType("tekst/zwykły");
PrintWriter out = res.getWriter();

out.println(" Wersja Interfejsu:"+VersionDetector.getServletVersion());


out.println(" Wersja Javy: "+VersionDetector.getJavaVersion());
}
}

Klient
Przy każdym zleceniu aplet ma możliwość uzyskania informacji o komputerze klienta a w przypadku stron
wymagających uwierzytelnienia — informacji o aktualnym użytkowniku. Informacja takie mogą zostać
wykorzystane przy rejestracji danych dostępu, kojarzeniu informacji z indywidualnymi użytkownikami lub przy
ograniczaniu do poszczególnych klientów.

Pobieranie informacji o komputerze klienta


Aplet może użyć getRemoteAddr() oraz getRemoteHost() w celu odczytania adresu IP oraz nazwy węzła
komputera klienta, odpowiednio:
public String ServletRequest.getRemoteAddr()
public String ServletRequest.getRemoteHost()

Obie wartości są odsyłane jako obiekty String. Informacje pochodzą z portu, który łączy serwer z klientem,
dlatego adres zdalny i nazwa węzła mogą być adresem zdalnym i nazwą węzła serwera pośredniczącego.
Przykładowym zdalnym adresem mógłby być 192.26.80.118, przykładowym zdalnym komputerem
dist.engr.sgi.com.
Adres IP oraz nazwa węzła odległego mogą zostać konwertowane na obiekt java.net.InetAddress przy
użyciu InetAddress.getByName():
InetAddressremoteInetAddress= InetAddress.getByName(req.getRemoteAddr());
Ograniczanie dostępu
Z powodu polityki rządu Stanów Zjednoczonych, ograniczającej export silnego szyfrowania,
niektóre strony WWW muszą być zachowywać środki ostrożności co do tego komu zezwalają
na pobieranie określonego oprogramowania. Aplety ze zdolnością do pobierania informacji o
komputerach klientów, dobrze nadają się do tego celu. Aplety te mogą sprawdzić komputer
klienta i udostępnić łącza do pobierania określonego oprogramowania, tylko wtedy jeżeli
klient będzie pochodził z kraju, który nie jest objęty ograniczeniem.

W pierwszej edycji niniejszej książki krajami, których nie obejmowało ograniczenia eksportu
były tylko Stany Zjednoczone i Kanada, a aplet ten mógł być ściągany tylko przez
użytkowników pochodzących z tych dwóch krajów. Od czasu tamtej edycji rząd Stanów
Zjednoczonych złagodził politykę dotyczącą eksportu silnego kodowania, i obecnie większość
oprogramowania związanego z kodowaniem może być sprowadzana przez użytkowników ze
wszystkich krajów, z wyjątkiem tych, którzy pochodzą z tzw. „Terrorystycznej siódemki”
(Kuba, Iran, Irak, Korea Północna, Libia Syria i Sudan). Na przykładzie 4.10 został pokazany
aplet, który pozwala na ładowanie wszystkim z poza owej siódemki.

Przykład 4.10.
Czy można im ufać?
import java.io. *;
import java.net.* ;
import java.util.* ;
import javax.servlet.*;
import javax.servlet.http.*;

public class ExportRestriction extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();

// ...Trochę wstępnego HTML-u...

// Pobierz nazwę węzła klienta


String remoteHost = req.getRemoteHost() ;

// Zobacz czy klient ma przyznany dostęp


if (! isHostAllowed(remoteHost)) {
out .println ("Dostęp <BLINK>nie przyznany</BLINK>") ;
}
else {
out.println("Dostęp przyznany") ;
// Wyświetl łącza pobierania...
}
}
// Nie przyznawaj dostępu komputerom głównym o zakończeniach .cu, .ir/ // .iq, .
kp, .ly, .sy, and .sd.

private boolean isHostAllowed(String host){


return (!host.endsWith(".cu")&&
!host.endsWith(".cu")&&
!host.endsWith(".ir")&&
!host.endsWith(".iq")&&
!host.endsWith(".kp")&&
!host.endsWith(".ly")&&
!host.endsWith(".sy")&&
!host.endsWith(".sd"));
}
}

Aplet uzyskuje nazwę węzła klienta za pomocą wywołania do req.getRemoteHost() i, w oparciu o


przyrostek, ustala czy klient nie pochodzi z któregoś z krajów objętych ograniczeniem. Ograniczenie to można
obejść przygotowując kod szyfrujący, działanie takie grozi jednak poważnymi skutkami prawnymi.
Pobieranie informacji o użytkowniku
Zastanówmy się co musimy zrobić jeżeli chcemy ograniczyć dostęp do niektórych z naszych stron WWW i
jednocześnie chcemy aby stopień kontroli nad podobnym ograniczeniem był większy niż w omówionej przed
chwilą metodzie „państwowej”. Załóżmy, że wydajemy czasopismo internetowe i chcemy ograniczyć dostęp do
artykułów użytkownikom, którzy nie uiszczą opłat. Tak więc, uwaga, nie musimy do tego celu używać apletów.

Niemal każdy serwer HTTP ma wbudowaną zdolność do ograniczania dostępu do pewnej liczby lub wszystkich
swoich stron dla określonych użytkowników. Jak ustawić ograniczenie dostępu opisano w rozdziale 8
„Bezpieczeństwo” teraz przedstawimy tylko ogólną zasadę działania. Kiedy przeglądarka „wchodzi” na jedną z
takich stron, serwer HTTP odpowiada, że potrzebne jest specjalne uwierzytelnienie użytkownika. W momencie,
w którym przeglądarka otrzymuje taką odpowiedź, otwiera okno prosząc użytkownika o podanie nazwy i hasła,
właściwych dla tej strony, tak jak to zostało pokazane na rysunku 4.1.

Kiedy użytkownik wprowadzi już żądane informacje, przeglądarka próbuje jeszcze raz wejść na tą stronę, tym
razem dołączając nazwę użytkownika oraz hasło razem ze zleceniem. Jeżeli serwer zaakceptuje nazwę i hasło —
rozpocznie obsługę zlecenia. Jeżeli jednak serwer nie zaakceptuje nazwy i hasła, przeglądarce ponownie
odmawiany jest dostęp, a użytkownik delikatnie mówiąc nie jest z tego zadowolony.

Jak do tego mają się aplety? Otóż jeżeli dostęp do apletu został ograniczony przez serwer, aplet może pobrać
nazwę użytkownika. która została zaakceptowana przez serwer, używając do tego metody getRemoteUser():

public String HttpServletRequest.getRemoteUser()

Rysunek 4.1.
Okno logowania dla strony z ograniczonym dostępem

Zauważmy, że informacje te są odczytywane z obiektu apletu HttpServletRequest, Http-


specyficznej podklasy ServletRequest. Metoda ta odsyła nazwę użytkownika ustawiając
zlecenia jako String lub null — w przypadku kiedy dostęp do apletu nie był ograniczony. Nie
istnieje metoda porównawcza do pobrania hasła zdalnego użytkownika (mimo, że można ją
ustalić ręcznie, tak jak to zostało zaprezentowane na przykładzie 8.2, w rozdziale 8).
Przykładem zdalnego użytkownika mógłby być jhunter.

Aplet, w celu ustalenia jaki rodzaj uwierzytelnienia został użyty, może użyć metody
getAuthType():

public String HttpServletRequest.getAuthType()


Metoda ta odsyła użyty typ uwierzytelnienia lub null — jeżeli dostęp do apletu nie był ograniczony. Typami
mogą być BASIC, DIGEST, FORM, czy CLIENT–CERT. Więcej informacji o typach uwierzytelnień można znaleźć
w rozdziale 8.

Do czasu kiedy aplet wywoła getRemoteUser(), serwer określi już, że użytkownik jest uprawniony do
wywołania apletu, jednakże nie oznacza to, że nazwa użytkownika zdalnego jest bezwartościowa. Aplet mógłby
przeprowadzić ponowną kontrolę uwierzytelnienia, bardziej restrykcyjną i bardziej dynamiczną od tej,
przeprowadzanej przez serwer. Dla przykładu bardziej poufne informacje mógłby odesłać tylko wtedy jeżeli
osoba złożyłaby na nie zlecenie, aplet mógłby również wprowadzić zasadę, że każdy użytkownik może wywołać
aplet tylko 10 razy dziennie.*

Wtedy, znowu, nazwa klienta może w prosty sposób „poinformować” aplet, kto próbuje go wywołać. W końcu
odległy komputer centralny nie jest niepowtarzalny dla każdego użytkownika. Serwery UNIX często obsługują
setki użytkowników, a serwery pośredniczące bramowe mogą działać w imieniu tysięcy. Miejmy jednak
świadomość, że za dostęp do nazwy klienta trzeba zapłacić pewną cenę — każdy użytkownik musi być
zarejestrowany przez nasz serwer i, przed wejściem na naszą stronę, musi podać jej nazwę oraz hasło. Ogólnie
rzecz biorąc uwierzytelnianie nie powinno być używane tylko po to, żeby aplet wiedział z kim ma do czynienia.
Rozdział 7 „Śledzenie Sesji” opisuje lepsze, prostsze w obsłudze techniki pobierania informacji o
użytkownikach. Jednakże w przypadku gdy aplet jest już chroniony i ma łatwo dostępną nazwę, może jej równie
dobrze użyć.

Za pomocą nazwy użytkownika zdalnego aplet może zapisać informacje o każdym kliencie. Po dłuższym okresie
aplet może zapamiętać preferencje każdego z użytkowników. Na krótszą metę może on zapamiętać serię stron
przeglądanych przez klienta i użyć jej w celu dodania odczytu stanu do bezstanowego protokołu HTTP. Efekty
związane ze śledzeniem sesji (opisane w rozdziale 7) mogą okazać się niepotrzebne w przypadku kiedy aplet zna
już nazwę użytkownika klienta.

Personalizowane powitanie
Prosty aplet, który używa getRemoteUser() może pozdrawiać swoich klientów imieniem oraz zapamiętywać
kiedy po raz ostatni każdy z nich był zalogowany, tak jak to pokazuje przykład 4.11.
Przykład 4.11.
Hej, Pamiętam Cię!

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class PersonalizedWelcome extends HttpServlet {

Hashtable accesses = new Hashtable();

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IQException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter() ;

// ...Trochę wstępnego HTML-u...

String remoteUser = req.getRemoteUser();


if (remoteUser == null) {

out.println("Witaj!");
}
else {
out .println ("Witaj, " + remoteUser + "!");
Date lastAccess = (Date) accesses.get(remoteUser);
if (lastAccess == null) {
out.println("To twoja pierwsza wizyta!");

*
W następnym rozdziale pokażemy jak „powiedzieć” „dostęp nie przyznany” po jedenastej próbie wywołania apletu.
}
else {
out.println("Twoja ostatnia wizyta miała miejsce
"+accesses.get(remoteUser));
}
if (remoteUser.equals("PROFESSOR FALKEN")) {
out.println("Zagramy?");
}

accesses.put(remoteUser, new Date()) ;


// ...Kontynuacja obsługi zlecenia...
}
}

Aplet ten używa Hashtable w celu zapisania czasu ostatniego wywołania dla każdego zdalnego użytkownika.
Pierwszą rzeczą, którą aplet wykonuje przy każdym zleceniu jest pozdrowienie osoby jej imieniem oraz
poinformowanie jej kiedy miała jej ostatnia wizyta. Następnie rejestruje czas wizyty (w celu przedstawienia go
następnym razem), poczym kontynuuje obsługę zlecenia.

Zlecenie
Po tym jak zapoznaliśmy się ze sposobem, w jaki aplet uzyskuje informacje o serwerze i o
kliencie, nadszedł czas na omówienia naprawdę ważnej sprawy: w jaki sposób aplet
dowiaduje się o tym czego chce klient.

Parametry zlecenia
Każdemu wywołaniu apletu może towarzyszyć dowolna liczba parametrów zlecenia. Parametrami są zwykle pary
nazwa/wartość, które dostarczają apletowi wszystkich dodatkowych informacji, które są mu potrzebne do obsługi
zlecenia. Nie należy mylić parametrów zlecenia z parametrami początkowymi apletu, które są związane z samym
apletem.
Aplet HTTP uzyskuje swoje parametry zlecenia jako część swojego ciągu zapytań (dla zleceń
GET) lub jako zakodowane dane POST (dla zleceń POST) lub, w niektórych przypadkach w
obu formach. Na szczęście każdy aplet odczytuje swoje parametry w ten sam sposób — przy
użyciu getParameter() i getParameterValues():
public String ServletRequest.getParameter(String name)
public String[] ServletRequest.getParameterValues(String name)

getParameter() odsyła wartość parametru nazwanego jako String lub null — jeżeli parametr nie był
określony.* Istnieje pewność, że wartość będzie w swojej zwykłej, zdekodowanej formie. Jeżeli byłoby
jakiekolwiek prawdopodobieństwo, że parametr będzie miał więcej niż jedną wartość powinniśmy użyć w
zamian metody getParameterValues(). Metoda ta odsyła wszystkie wartości parametrów nazwanych jako
układ obiektów String lub jako null — jeżeli parametr nie został określony. Wartość pojedyncza odsyłana jest
jako układ długości 1. Jeżeli wywołamy getParameter() na parametr z wielkościami wielokrotnymi, wartość
odesłana będzie taka sama jak pierwsza wartość odesłana przez getParameterValues().

Uwaga: jeżeli informacje parametrowe zostały wprowadzone jako zakodowane dane POST, wtedy nie będą one
już dostępne w przypadku gdy dane POST zostały już odczytane ręcznie — przy użyciu metod getReader()
lub getInputStream() (ponieważ dane POST mogą zostać odczytane tylko raz).

Liczba możliwych zastosowań dla parametrów zlecenia jest nieograniczona. Istnieją sposoby ogólnego
zastosowania poinformowania apletu co ma robić, jak to ma zrobić, lub jedno i drugie. Dla przykładu
przyjrzyjmy się jak aplet słownikowy mógłby zastosować metodę getParameter() w celu wyszukania słówka,
które ma za zadanie odnaleźć.

*
W Java Web Serwer 1.1 zrezygnowano z metody getParameter() na korzyść metody getParameterValues().
Jednakże na skutek protestów, „Sun”, w ostatecznej wersji Interfejsu API 2.0, usunął getParameter() z listy
wycofanych metod. Była to pierwsza metoda Javy, z której nie zrezygnowano.
Plik HTML mógłby zawierać poniższy formularz, w którym użytkownik jest proszony o podanie słówka do
wyszukania:
<FORM METHOD=GET ACTION="/aplet/Słownik">
Word to look up: <INPUT TYPE=TEXT NAME="słowo"><P>
< INPUT TYPE=SUBMIT><P>
</FORM>

Poniższy kod odczytuje parametr słowny:

String word = req. getParameter ("słowo");


String definition = getDefinition(word) ;
out. println (word + ": " + definition);

Ten kod obsługuje tylko wartość na parametr. Niektóre parametry maja wartości wielokrotne, tak jak w
przypadku używania <SELECT>:

<FORM METHOD=POST ACTION="/ aplet /Car0rder">


Please select the Honda S2000 features you would like:<BR>
<SELECT NAME="cechy" MULTIPLE>
OPTION VALUE="aero"> Aero Screen </OPTION>
OPTION VALUE="cd"> CD Changer </OPTION>
OPTION VALUE="spoiler"> Trunk Spoiler </OPTION>
</SELECT><BR>
<INPUT TYPE=SUBMIT VALUE="Dodaj do koszyka">
</FORM>

Aplet może skorzystać z metody getParameterValues() w celu obsłużenia tego formularza:

String [] words = req. getParameterValues ("cechy") ;


if (features != null) {
for (int i = 0; i < features.length; i++) {
cart.add(features[i]);
}
}

Poza możliwością uzyskania wartości parametrów, aplet może mieć dostęp do nazw parametrów. używając do
tego celu metody getParameterNames():

public Enumeration ServletRequest. getParameterNames ()

Metoda odsyła wszystkie nazwy parametrów jako Enumeration (wyliczenie) obiektu String lub puste
Enumeration — jeżeli aplet nie ma żadnych parametrów. Metoda ta jest najczęściej stosowana do
debagowania. Kolejność nazw nie koniecznie będzie zgodna z kolejnością w formularzu.

Wreszcie, aplet może odczytać nieprzetworzony ciąg zapytań za pomocą getQueryString:

public String ServletRequest.getQueryString()

Metoda ta odsyła nieprzetworzony ciąg zapytań (zakodowaną informację parametru GET) zlecenia lub null —
jeżeli nie było ciągu zapytań. Podobne nisko-poziomowe informacje są rzadko użyteczne dla obsługi danych
formularzowych. Najlepiej sprawdza się przy obsłudze pojedynczych, nienazwanych wartości, tak jak w
przypadku /servlet/Sqrt?576, gdzie odesłanym ciągiem zapytań jest 576.

Przykład 4.12 ukazuje zastosowanie tych metod z apletem, który drukuje nazwę i wartość dla wszystkich swoich
parametrów.

Przykład 4.12.
Podpatrywanie parametrów

import java.io. *;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http. * ;

public class ParameterSnoop extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

out.println("Ciąg Zapytań:") ;
out.println(req.getQueryString()) ;
out .printin ();

out.println("Parametry Zlecenia:");
Enumeration enum = req.getParameterNames ();
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement();
String values[] = req.getParameterValues (name)
if (values != null) {
for (int i = 0; i < values.length; i++) {
out.println(name + "("+ i +"):" + values[i]);
}
}
}
}
}

Wydruk wyjściowy apletu został pokazany na rysunku 4.2.

Rysunek 4.2. Podpatrzone parametry


Począwszy od Interfejsu API 2.2 możliwe jest utworzenie formularz POST za pomocą operacji URL, który
zawiera ciąg zapytań. Jeżeli wykonamy powyższą czynność informacje parametrów globalnych będą dostępne
poprzez metody getParameter() — w przypadku kolizji nazw wartości parametrów ciągu zapytań mają
pierwszeństwo przed wartościami POST. Dla przykładu jeżeli w zleceniu znajduje się ciąg zapytań a=hokey
oraz dane POST: a=pokey, metoda req.getParameterValues ("a") odeśle układ {"hokey", "pokey"}.

Generowanie Klucza Licencji


W związku z tym, że mamy już odpowiednią wiedzę jesteśmy gotowi do napisania apletu, który generuje klucz
licencji KeyedServerLock dla każdego danego komputera centralnego oraz numeru portu. Klucz z tego apletu
może zostać użyty do odblokowania (usunięcia ograniczenia) apletu KeyedServerLock. W tym momencie
może nasuwać się pytanie: „ W jaki sposób ten aplet rozpozna komputer centralny oraz numer portu apletu, który
ma zostać odblokowany? Odpowiedź jest prosta: oczywiście za pomocą parametrów zlecenia.
Na przykładzie 4.13 został zaprezentowany kod.
Przykład 4.13.
Odblokowywanie apletu „KeyedServerLock”

import java.io. *;
import java.net. *;
import java.util.* ;
import javax.servlet.*;
import javax.servlet.http.*;

public class KeyedServerUnlock extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
PrintWriter out = res.getWriter();

// Pobierz komputer centralny i port


String host = req.getParameter (" komputer centralny");
String port = req.getParameter (" port");

// Jeżeli nie ma komputera centralnego/ użyj bieżącego komputera


// centralnego
if (host == null) {
host = req.getServerName() ;
}

// Konwertuj port na liczbę rzeczywistą, jeżeli nie ma żadnego


//użyj portu bieżącego
int numericPort;
try {
numericPort = Integer.parseInt(port);
}
catch (NumberFormatException e) {
numericPort = req.getServerPort() ;
}

// Generuj i wydrukuj klucz


// Jakikolwiek KeyGenerationException jest wyłapywany i
wyświetlany
try {
long key = generateKey (host, numericPort) ;
out.println (host+":"+numericPort+" has the key"+key);
}
catch (KeyGenerationException e) {
out.println("Nie dało się wygenerować klucza:"+e.getMessage());
}
}

// Metoda ta zawiera algorytm używany do dopasowania klucza z


// komputerem centralnym serwera i portem. Niniejsza przykładowa
// implementacja jest dalece niedoskonała i nie powinna być
używana // przez strony komercyjne.
//
// Zgłasza wyjątek KeyGenerationException ponieważ wszystko
bardziej // sprecyzowane
// byłoby związane z wybranym algorytmem.

private long generateKey(String host,int port) throws


KeyGenerationException {

// Klucz musi być liczbą 64-bitową równą logicznej negacji not (~)
// 32-bitowego adresu IP połączonego przez 32-bitowy numer portu.

byte hostIP[];
try {
hostIP = InetAddress.getByName(host).getAddress () ;

}
catch (UnknownHostException e) {
throw new KeyGenerationException(e.getMessage());
}

// Pobierz 320bitowy adres IP


long servercode = 0;
for (int i = 0; i < 4; i++) {
servercode «= 8;
servercode |= hostIP[i];
}

// Połącz 32-bitowy numer portu


servercode «= 32;
servercode |= port;

// Klucz jest logicznym przeczeniem


return ~servercode;

}
}
class KeyGenerationException extends Exception {

public KeyGenerationException() {
super() ;
}

public KeyGenerationException(String msg) {


super (msg) ;
}
}
Możliwe jest użycie wydruku wyjściowego z tego apletu celem przypisania KeyedServerLock specjalnej
zmiennej egzemplarza „key”:

<servlet>
<servlet-name>
ksl ^
</servlet-name>
<servlet-class>
KeyedServerLock
</servlet-class>
<init-param>
<param-name>
key
</param-name>
<param-value>
-9151314447111823249
</param-value>
</init-param>
</servlet>
Pamiętajmy, aby zamienić logiczne generateKey() przed jakimkolwiek rzeczywistym
użytkowaniem.

Informacje ścieżki
Dodatkowo poza parametrami zlecenie HTTP może zawierać coś, co nazywa się informacją
dodatkowej ścieżki (extra path information) lub ścieżką wirtualną (virtual path). Zwykle taka
informacja dodatkowej ścieżki używana jest do wskazywania pliku na serwerze, który
powinien zostać użyty przez aplet w jakimś określonym celu. Taka informacja ścieżki jest
kodowana w URL-u zlecenia HTTP. Przykładowy URL wygląda w podobny sposób:
http://server:port/servlet/ViewFile/index.html

Wywołuje on aplet ViewFile, przekazując /index.html jako informacje dodatkowej ścieżki. Aplet może
mieć dostęp do tych informacji ścieżki, może również przesunąć łańcuch /index.html ścieżki rzeczywistej
pliku index.html. Czym zatem jest ścieżka rzeczywista /index.html? Jest to pełna ścieżka systemu plików do
pliku — to co serwer by odesłał jeżeli klient poprosiłby bezpośrednio o /index.html. Tym „czymś” okazałoby
się z pewnością document_root/index.html, lecz oczywiście serwer mógłby mieć specjalny aliasing zmieniający
to.
Poza tym, podobna informacja dodatkowej ścieżki będąc wyraźnie określoną w URL-u, może być kodowana w
parametrze ACTION, formularza HTML:

<FORM METHOD=GET ACTION="/servlet/Dictionary/dict/definitions.txt">


Word to look up: <INPUT TYPE=TEXT NAME="słowo"><P>
<INPUT TYPE=SUBMIT><P>
</FORM>

Formularz ten wywołuje aplet Dictionary w celu obsłużenia swoich przedłożeń oraz przekazuje mu informacje
dodatkowej ścieżki /dict/definitions.txt. Aplet Dictionary będzie wtedy wiedział, że ma sprawdzić
znaczenia słówek przy użyciu pliku definitions.txt, tego samego, który klient otrzymałby prosząc o /
dict/definitions.txt, na serwerze „Tomcat” byłby to prawdopodobnie:
server_root/webapps/ROOT/dict/definitions.txt.

Dlaczego Informacje Dodatkowej Ścieżki?

Dlaczego HTTP ma specjalne rozpoznawanie dla informacji dodatkowej ścieżki?


Czy nie jest wystarczy, że aplet ma przekazywany parametr ścieżki? Odpowiedź
brzmi: tak, aplety nie potrzebują specjalnego rozpoznawania, lecz programy CGI
tak.
Program CGI nie może współdziałać ze swoim serwerem w czasie uruchamiania, tak więc nie ma możliwości żeby otrzymał on
parametr ścieżki, poza poproszeniem serwera o odwzorowanie tego parametru do rzeczywistej lokalizacji systemu plików. Serwer
musi jakoś przetłumaczyć ścieżkę zanim wywoła program CGI. Dlatego właśnie musi istnieć rozpoznawanie dla specjalnej
„informacji dodatkowej ścieżki”.
Serwery tłumaczą wstępnie dodatkową ścieżkę a następnie przesyłają ją do programu CGI jako zmienną środowiskową. Jest to
całkiem elegancki sposób radzenia sobie z uchybieniami w CGI.
Oczywiście to, że aplety nie potrzebują specjalnej obsługi informacji dodatkowej ścieżki, nie oznacza, że nie powinny jej
wykorzystywać. Jest to prosty, wygodny sposób dołączania ścieżki do zlecenia.

Pobieranie informacji ścieżki


Aplet może użyć metodę getPathInfo() celem uzyskania informacji dodatkowej ścieżki:

public String HttpSertvletRequest.getPathInfo()

Metoda ta odsyła informacje dodatkowej ścieżki związane ze zleceniem (URL jest dekodowany jeżeli jest taka
potrzeba) lub null — jeżeli takich nie podano. Przykładowa ścieżka to /dict/definitions.txt. Informacja
ścieżki sama w sobie nie jest specjalnie użyteczna. Aplet zwykle musi jeszcze znać aktualną lokalizację w
systemie plików, pliku podanego w informacji ścieżki, tam gdzie zwykle ma miejsce getPathTranslated():

public String HttpServletRequest.getPathTranslated()

Metoda ta odsyła informacje dodatkowej ścieżki przetłumaczoną na rzeczywistą ścieżkę


systemu plików (URL jest dekodowany jeżeli jest taka potrzeba) lub null — w przypadku gdy
nie ma informacji dodatkowej ścieżki. Metoda ta, w przypadku kiedy ścieżka nie może być
przetłumaczona na sensowną ścieżkę pliku, odsyła null. Podobnie dzieje się kiedy aplikacja
WWW wywołuje z archiwum WAR, odległego systemu plików czy z bazy danych. Odesłana
ścieżka nie koniecznie wskazuje na istniejący plik lub katalog. Przykładem przetłumaczonej
ścieżki może być C:\tomcat\webapps\ROOT\dict\definitions.txt.
Przykład 4.14 prezentuje aplet, który używa dwóch metod w celu wydrukowania informacji
dodatkowej ścieżki, które otrzymał oraz następujące po tym tłumaczenie na ścieżkę
rzeczywistą.

Przykład 4.14.
Pokazywanie dokąd prowadzi ścieżka

import java.io. *;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class FileLocation extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

if (req.getPathInfo() != null) {

out.println("Plik \"" + req.getPathInfo() + "\"");


out.println("Znajduje się w\""+req.getPathTranslated()+"\"");
}
else {
out.println("Informacja ścieżki jest równa zero, plik nie
istnieje");
}
}
}

Przykładowy wydruk wyjściowy tego apletu mógłby wyglądać w następujący sposób:

The file "/index, html"


Is stored at "/usr/local/tomcat/webapps/ROOT/index.html"

Tłumaczenia ścieżki ad hoc


Czasami aplet musi przetłumaczyć ścieżkę, która nie była przekazana w informacji dodatkowej ścieżki. W celu
wykonania tego zadania możemy użyć metody getRealPath():

public String ServletContext.getRealPath(String path)

Metoda ta odsyła ścieżkę rzeczywistą jakiejkolwiek ścieżki wirtualnej (virtual path) lub null — jeżeli
tłumaczenie nie może zostać wykonane. Jeżeli daną ścieżką jest /, metoda odsyła źródło dokumentu (miejsce, w
którym są przechowywane dokumenty) dla serwera. Jeżeli daną ścieżką jest getPathInfo(), metoda odsyła tą
samą ścieżkę rzeczywistą, która byłaby odesłana przez getPathTranslated(). Metoda ta może być używana
przez aplety standardowe jak również aplety HTTP. Nie istnieje żaden odpowiednik CGI.

Pobieranie ścieżki kontekstu


Jak dowiedzieliśmy się z rozdziału 2 „Podstawy Apletów Http” aplikacje WWW są odwzorowywane do
przedrostków URI na serwerze. Aplet może określić przedrostek URI, w którym działa przy użyciu metody
getContextPath() w ServletRequest:

public String ServletRequest.getContextPath()


Metoda odsyła String odpowiadający przedrostkowi URI kontekstu obsługującego zlecenie. Wartość zaczyna
się na /, nie ma zakończenia /, i dla kontekstu domyślnego jest pusta. Dla zlecenia na /
catalog/books/servlet/BuyNow, dla przykładu, metoda getContextPath() odesłałaby /catalog/boks.

Możemy wykorzystać tą metodę do zapewnienia, że nasze aplety będą działały niezależnie od


tego do jakiego kontekstu zostały odwzorowane. I tak na przykład kiedy tworzymy łącze do
strony głównej dla kontekstu, powinniśmy zrezygnować z ustalania ścieżki kontekstu i zamiast
niej zastosować kod standardowy:
out.println("<a href=\""+req.getContextPath()+"/index.html\">Home</a>");

Pobieranie typów MIME


Kiedy już aplet ma ścieżkę pliku, cz estokroć musi jeszcze znać typ pliku. W tym celu można skorzystać z
metodt getMimeType():

public String ServletContexy.getMimeType(String File)

Metoda ta odsyła ty MIME danego pliku, na podstawie jego rozszerzenia lub null — jeżeli
jest ono nieznane. Najczęściej spotykane typy MIME to: text/html, text/plain, image/gif
oraz image/jpeg. Poniższy fragment kodu odnajduje typ MIME informacji dodatkowej ścieżki:
String type = getServletContext () .getMimeType(req.getPathTranslated())

Aplety generalnie są zaznajomione z rdzennym zestawem odwzorowań typu file-extention-to-mime. Mogą one
być ulepszane bądź ignorowane przez hasła w deskryptorze wdrożenia web.xml, dając każdemu kontekstowi
możliwe do skonfigurowania zachowanie, tak jak to zostało pokazane na przykładzie 4.15.

Przykład 4.15.
Każdy kocha Mime

<?xml version="1.0" kodowanie="ISO-8859-1"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems/ Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app>
<i-- .....-->
<mime-mapping>
<extension>
Java
</extension>
<mime-type>
text/plain
</mime-type>
</mime-mapping>
<mime-mapping>
<extension>
cpp
</extension>
<mime-type>
text/plain
</mime- type>
</mime-mapping>
</web-app>
Podawanie plików
Serwer „Tomcat” jako taki, używa apletów do obsługi każdego zlecenia. Poza tym
prezentowanie możliwości apletów powoduje, że serwer ma budowę modularną, co z kolei
umożliwia „hurtową” wymianę (zastąpienie) pewnych aspektów jego sposobu działania. Dla
przykładu wszystkie pliki są podawane przez aplet org.apache.tomcat.core.DefaultServlet,
na którym spoczywa obowiązek obsługi /path (tzn. jest on domyślnym programem
obsługującym zlecenie). Jednak nie jest tak, że nie można zastąpić DefaultServlet. Można
tego dokonać poprzez zmianę wzoru URL i użycie innego apletu. Nie jest również problemem
napisanie prostego zastępstwa dla DefaultServlet, używając do tego celu metod, które już
omówiliśmy.
Na przykładzie 4.16 został zaprezentowany aplet ViewFile, który wykorzystuje metody
getPathTranslated() oraz getMimeType(), ażeby odesłać plik podany w informacji
dodatkowej ścieżki.

Przykład 4.16.
Dynamiczne odsyłanie plików statycznych

import java.io. *;
import Java.util.* ;
import javax.servlet.* ;
import javax.servlet.http.* ;

import com.oreilly.servlet.ServletUtils;

public class ViewFile extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Użyj ServletOutputStream ponieważ możemy przekazać informację
// binarną
ServletOutputStream out = res.getOutputStream() ;

// Pobierz plik do przeglądania


String file = req.getPathTranslated();

// Plik nie istnieje, przeglądanie niemożliwe do realizacji


if (file == null) {
out.println("Nie można przeglądać pliku");
return;
}

// pobierz i ustal typ pliku


String contentType = getServletContext().getMimeType(file);
res.setContentType(contentType) ;

// Odeślij plik
try {
ServletUtils.returnFile(file, out);
}
catch (FileNotFoundException e) {
out.println("Plik nie odnaleziony") ;
}
catch (IOException e) (
out.println("Problem z przesłaniem pliku;"+e.getMessage());
}
}
}

Aplet najpierw korzysta z metody getPathTranslated() w celu pobrania nazwy pliku, który ma wyświetlić.
Następnie używa getMimeType(), ażeby ustalić typ treści tego pliku, ustala także odpowiedni typ treści
odpowiedzi. I w końcu odsyła plik, używając do tego metody returnFile() znajdującej się w klasie usługowej
com.oreilly.servlet.ServletUtils:
// Prześlij zawartość pliku do strumienia wyjściowego
public static void returnFile(String filename, OutputStream out)
throws FileNotFoundException, IOException {
// FileInputStream jest związany z bajtami
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
byte[] buf = new byte[4 * 1024]; //bufor 4K
int bytesRead;
while ((bytesRead = fis.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
}
finally {
if (fis != null) fis.close();
}
}

Obsługa błędu apletu jest na poziomie podstawowym — aplet odsyła stronę, która opisuje
błąd. Sytuacja taka jest do zaakceptowania w przypadku naszego prostego przykładu
(podobnie jak w przypadku wielu innych programów), jednakże poznamy w następnym
rozdziale lepszy sposób — wykorzystujący kody statusu. Aplet ten można wywołać
bezpośrednio za pomocą URL-u takiego jak poniższy:
http://server: port/servlet/ViewFile/index.html

Lub jeżeli przypiszemy ten aplet do obsługi domyślnego wzoru URL, tak jak poniżej:

<servlet>
<servlet-name>
vf
</servlet-name>
<servlet-class>
ViewFile
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>
vf
</servlet-name>
<url-pattern>
/
</url-pattem>
</servlet-mapping>

wtedy ViewFile jest wywoływane automatycznie, nawet dla URL-ów takich jak ten:

http://server:port/index.html

Aplet ten jest tylko przykładem, mającym na celu zademonstrowanie ogólnej zasady działania, w związku z tym,
nie ma pełnego zespołu funkcji DefaultServlet.

Czytanie z zasobów oddzielonych


Metoda getPathTranslated() ma niestety klika uciążliwych ograniczeń. Po pierwsze nie działa ona dla treści
podanej z plików WAR, ponieważ nie istnieje plik o dostępie bezpośrednim. Nie działa ona także w
zrównoważonym środowisku rozproszonym, gdzie mógłby istnieć plik o dostępie bezpośrednim — lecz nie na
serwerze aktualnie wywołującym aplet. Aby przezwyciężyć te ograniczenia w Interfejsie API 2.1 została
wprowadzona technika przeznaczona do oddzielania zasobów, która umożliwia serwerowi dostęp do zasobów
bez potrzeby dowiadywania się gdzie się one znajdują. Aplet uzyskuje dostęp do plików oddzielonych poprzez
zastosowanie metody getResource():

public URL ServletContext.getResource(String uripath)


Metoda ta odsyła URL, który może zostać użyty do badania określonego zasobu oraz do odczytania jego treści.
Sposób w jaki parametr ścieżki URI jest odwzorowywany do bieżącego zasobu (pliku, hasła WAR, hasła bazy
danych lub innego) określane jest przez serwer WWW. Kolejne dwa ograniczenia to: wymóg, że ścieżka musi
być absolutna (musi zaczynać się od ukośnika),druga sprawa to wymóg, iż URI nie powinien być zasobem
aktywnym, tak jak w przypadku innego apletu lub programu CGI. Metoda getResource() oryginalnie
obsługuje odczytywanie tylko treści statycznej (nie obsługuje czytania treści dynamicznej i pisanie treści).

Rzeczą o której należy pamiętać podczas korzystania z obiektu kontekstowego, jest nie zawieranie ścieżki
kontekstu w zleceniu. Nie należy tego robić ponieważ kontekst „zna” swoją własną ścieżkę, a dzięki nie —
określaniu jej w kodzie, sprawiamy, że aplikację będzie można przesunąć do innego przedrostka ścieżki unikając
rekompilacji. Poniższy kod pobiera oraz drukuje plik /includes/header.html dla bieżącego kontekstu:

URL url = getServletContext().getResource("/zawiera/nagłówek.html");


if (uri != null) {
ServletUtils.returnURL (uri, out) ;
}

Plik header.html może znajdować się w pliku archiwalnym na serwerze innym niż ten, który obsługuje aplet,
jednak nie ma to większego znaczenia. Kod wykorzystuje metodę złożoną z metod podstawowych klasy
com.oreilly.servlet.Servlet.Utils:

// Przesyła treści URL do OutputStream


public static void returnURL(URL url,OutputStream out)throws
IOException {
InputStream in = url. openStream () ;
byte[] buf = new byte[4 * 1024]; //bufor 4K
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
}

// Przesyła treści URL do PrintWriter


public static void returnURL(URL url,PrintWriter out)throws IO
Exception
// Określ kodowanie treści URL
URLConnection con = uri.openConnection() ;
con.connect();
String encoding = con.getContentEncoding() ;

// Konstruuj czytnik właściwy dla tego kodowania


BufferedReader in = null;
if (encoding == null) {
in = new BufferedReader(
new InputStreamReader(url.openStream())) ;
}
else {
in = new BufferedReader(
new InputStreamReader(url.openStream(), encoding));
}
char[] buf = new char[4 * 1024]; // bufor 4Kchar
int charsRead;
while ((charsRead = in.read(buf)) != -1) {
out.write(buf, 0, charsRead);
}
}

Tak jak to zostało pokazane w drugiej metodzie returnURL(), w poprzednim kodzie, możliwe jest stworzenie
obiektu URL do badania atrybutów zasobu oddzielonego. Nie wszystkie serwery oraz fazy wykonywania Javy
obsługują jednak taki sposób działania. Oto kod, który sprawdza stronę główną dla aktualnego kontekstu:
URL url = getServletContext() . getResource ("/index, html");
// kontekstowa strona główna
URLConnection con = url.openConnection() ;
con.connect() ;
int contentLength = con.getContentLength(); // rozpoznawanie nie // kompletne
String contentType = con.getContentType(); // rozpoznawanie nie // kompletne
long expiration = con.getExpiration(); // rozpoznawanie nie // kompletne
long lastModified = con.getLastModified(); // rozpoznawanie nie // kompletne
// itd...
Pamiętajmy, że treść podana dla ścieżki /URI jest całkowicie determinowana przez serwer. W
celu uzyskania dostępu do zasobów z innego kontekstu możemy użyć metody getContext():
public ServletContext ServletContext.getContext(String uripath)

A oto sposób w jaki można uzyskać odwołanie do strony głównej serwera:


getServletContext().getContext("/");

Pamiętajmy, że metoda getResource() nie koniecznie będzie zgodna z listą plików


akceptowanych, tak więc getResource"/" może nie odesłać nadającej się do użycia treści.

Poniższa metoda — getResourceAsStream() jest wygodna dla odczytywania zasobów jako strumień:

public InputStream ServletContext.getResourceAsStream(String uripath)

Zachowuje się ona zasadniczo tak samo jak get().openStream(). Celem utrzymania zgodności z
poprzednimi wersjami oraz aby złagodzić przejście na aplety (dla programistów CGI), Interfejs API będzie nadal
zawierał metody dostępu do plików, takie jak np. getPathTranslated(). Trzeba tylko pamiętać, że za
każdym razem kiedy uzyskujemy dostęp do zasobu używając obiektu File, „wiążemy się” z określonym
komputerem.

Podawanie zasobów
Używając zasobów oddzielonych możemy napisać poprawioną wersję apletu ViewFile, który będzie działał
nawet gdy treść będzie podana z pliku WAR oraz wtedy kiedy owa treść będzie znajdowała się na serwerze
innym niż ten, który wywołuje aplet. Przykład 4.17 prezentuje aplet UnsafeView Resource. Ma on etykietkę
„unsafe” (niebezpieczny, niepewny) ponieważ nie zapewnia on żadnej ochrony zasobów ponadto podaje zasoby
„w ciemno” na WEB-INF oraz źródło .jsp.

Przykład 4.17.
Podawanie zasobów (sposób niebezpieczny)

import java.io. *;
import java.net. *;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

public class UnsafeViewResource extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Zastosuj ServletOutputStream ponieważ możemy przekazać
informację binarną
ServletOutputStream out = res. getOutputStream ();
res. setContentType (" tekst/zwykły"); // sanity default

// Pobierz zasób do przeglądania


String file = req.getPathInfo();
if (file == null) {
out. println(" Informacja dodatkowej ścieżki wynosiła zero;
powinna być zasobem do przeglądania")
return;
}
// Konwertuj zasób na URL
// UWAGA:Przyznanie dostępu do plików pod źródłem WEB-INF oraz.jsp
URL url = getServletContext().getResource(file);
if (url == null) { // niektóre serwery odeślą null w przypadku
// nieznalezienia
out.println("Zasób" + file + " nie odnaleziony");
return;
}

// Połącz z zasobem
URLConnection con = null;
try {
con = url.openConnection();
con.connect();
}
catch (IOException e) {
out.println("Zasób"+file+" nie może zostać
odczytany:"+e.getMessage());
return;
}

// Pobierz i ustal typ zasobu


String contentType = con.getContentType();
res. setContentType (contentType) ;

// Odeślij zasób
// UWAGA: Odsyłanie plików źródłowych pod WEB-INF i . jsp
try {
ServletUtils. returnURL (url, out) ;
}
catch (IOException e) {
res. sendError (res. SC_INTERNAL_SERVER_ERROR,
"Problem przy przesyłaniu zasobu: " + e.getMessage());
}
}
}

Aplet ten przegląda pliki tylko w swoim własnym kontekście. Wszystkie pliki poza tym kontekstem nie są
dostępne z metody getServletContext().getResource(). Aplet ten nie zapewnia również żadnej ochrony
zasobów, więc pliki pod WEB-INF i pod źródłem .jsp mogą być podawane bezpośrednio. Przykład 4.18
prezentuje bezpieczniejszą wersję klasy, przy użyciu metody ServletUtils.getResource() z
com.oreilly.servlet.

Przykład 4.18.
Podawanie zasobu oddzielonego (sposób bezpieczny)

import java.io.*;
import Java.net.*;
import java.util.*;
import javax. servlet. * ;
import javax.servlet.http.* ;

import com.oreilly.servlet.ServletUtils ;

public class ViewResource extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Użyj ServletOutputStream ponieważ możemy przesłać informację
// binarną
ServletOutputStream out = res.getOutputStream{) ;
res.setContentType("tekst/zwykły"); // sanity default

// Pobierz zasób do przeglądania


URL url = null;
try {
url=ServletUtils.getResource(getServletContext(),req.getPathInfo());
}
catch (IOException e) {
res.sendError(
res.SC_NOT_FOUND,
"Informacje dodatkowej ścieżki muszą wskazywać na aktualny plik
do przeglądania: " +
e.getMessage());
return;
}

// Połącz z zasobem
URLConnection con = url.openConnection() ;
con.connect();

// Pobierz i ustal typ zasobu


String contentType = con.getContentType();
res. setContentType( contentType);

// Odeślij zasób
try {
ServletUtils. returnURL (uri, out);
}
catch (IOException e) {
res. sendError (res. SC_INTERNAL_SERVER_ERROR,
"Problem przy przesyłaniu zasobu: " + e.getMessage());
}
}
}

Metoda ServletUtils.getResource() zawija metodę context.getResource() oraz dodaje trzy


wygodne sprawdzania bezpieczeństwa: zasoby nie są podawane jeżeli zawierają podwójne punkty, kończą się
ukośnikiem lub kropką, kończą się na .jsp lub zaczynają WEB-INF lub META-INF. Kod wygląda w następujący
sposób:

public static URL getResource(ServletContext context. String resource)


throws IOException {
// Zwarcie jeżeli zasób wynosi zero
if (resource == null) {
throw new FileNotFoundException(
"Żądany zasób wynosił zero (przekazano zero)");
}

if (resource.endsWith("/") ||
resource.endsWith("\\")||
resource.endsWith(".")) {
throw new MalformedURLException(‘Ścieżka nie może się kończyć
ukośnikiem ani kropką”);
}

if (resource. indexOf ("...") != -1) {


throw new MalformedURLException("Ścieżka nie może zawierać
podwójnych kropek");
}

String upperResource = resource.toUpperCase() ;


if (upperResource.startsWith("/WEB-INF") ||
upperResource. startsWith (" /META-INF")) {
throw new MalformedURLException(
"Ścieżka nie może zaczynać się na /WEB-IMF lub /META-INF") ;
}

if (upperResource.endsWith(".JSP")) {
throw new MalformedURLException(
"Ścieżka nie może kończyć się na .jsp") ;
}

// Konwertuj zasób na URL


URL url = context.getResource(resource);
if (uri == null) {
throw new FileNotFoundException(
"Żądany zasób był równy zero (° + zasób + ")");
}

return url;
}

Podawanie zasobów do ściągnięcia


Aplet ViewResource ma nie tylko zastosowanie jako przykład podręcznikowy. Kiedy będzie ustalał Content-
Type (typ treści) odpowiedzi do application/octet-stream, będzie wtedy działał jako program ładujący
pliki standardowe. Większość przeglądarek w sytuacji kiedy otrzymują treść application/octet-stream,
oferuje użytkownikom okno wywoływane pytając gdzie zapisać treść. Wyjątek stanowi Microsoft Internet
Explorer, który rozpoznając typ treści empirycznie, ignoruje typ treści przypisany do serwera, i wyświetla treść
normalnie. Ten „atrybut” uniemożliwia ściąganie przez przeglądarkę Internet Explorer typów plików takich jak:
GIF, JPEG, HTML oraz wielu innych.

Określanie co było przedmiotem zlecenia


Aplet ma do dyspozycji wiele metod ażeby dowiedzieć się który dokładnie plik lub aplet był przedmiotem
zlecenia klienta. W końcu tylko bardzo pewni siebie twórcy apletów założyliby, że to zawsze ich aplet będzie
bezpośrednim przedmiotem zlecenia. Aplet może być niczym więcej jak tylko funkcją obsługi dla niektórych
treści.

Żadna metoda nie odsyła bezpośrednio oryginalnego Jednolitego Lokalizatora Zasobów (Uniform Resource
Locator — URL), użytego przez klienta w celu złożenia zlecenia. Klasa javax.servlet.httpHttpUtils,
oferuje jednakże metodę (getRequestURL()), która, właściwie to robi.*

public static StringBuffer HttpUtils.getRequestURL(HttpServletRequest req)

Metoda ta rekonstruuje URL opierając się na informacjach zawartych w obiekcie HttpServletRequest.


Odsyłany jest StringBuffer, który zawiera schemat (taki jak HTTP), nazwę serwera, port serwera oraz
informacje dodatkowej ścieżki. Zrekonstruowany URL powinien być nieomal identyczny jak ten użyty przez
klienta. Różnice pomiędzy oryginalnymi a zrekonstruowanymi URL-ami są nieznaczne (obszar zakodowany
przez klienta jako %20 serwer mógłby zakodować jako +). Jako że metoda ta odsyła StringBuffer, URL
zlecenie może być skutecznie modyfikowany (na przykład poprzez dodawanie parametrów zapytań). Metoda ta
jest często stosowana przy tworzeniu przekierowanych komunikatów i powiadamianiu o błędach.

Przez większość czasu jednakże apletowi tak naprawdę nie jest potrzebny URL zlecenia. Co jest mu potrzebne to
zleceniowe URI, które jest odsyłane przez metodę getRequestURI():
public String HttpServleCRequest.getReguestURI()
Metoda ta odsyła Uniwersalny Wskaźnik Zasobów (Universal ResourceIdentifier — URI) zlecenia — jeszcze
przed dekodowaniem URL-u. Normalne aplety HTTP mogą postrzegać URI zlecenia jako URL nie zawierający
schematu, komputera centralnego i ciągu zapytań lecz zawierający wszystkie informacji dodatkowej ścieżki.
Inaczej mówiąc byłaby to ścieżka kontekstu, plus ścieżka serwera, plus informacja ścieżki. *Na tabeli 4.2
zaprezentowano kilkanaście URI zleceniowych oraz ich odpowiedniki URL-owe.

Tabela 4.2.
URI oraz ich URL-owe odpowiedniki

URL zleceniowe Ich składniki URI


http://server:port/servlet/Classname /servlet/Classname
http://server:port/servlet/registeredName /servlet/registeredName
http://server:port/servlet/Classname?var=val /servlet/Classname
http://server:port/servlet/Classname/pathinfo /servlet/Classname/pathinfo
http://server:port/servlet/Classname/pathinfo?var=val /servlet/Classname/pathinfo
http://server:port/servlet/Classname/path % 20infoa /servlet/Classname/path%
20info
http://sefver:port/alias. html (alias to a servlet) /alias.html
http://server.port/context/path/servlet/Classname Context/path/servlet/Classname

a
%20 reprezentuje obszar zakodowany

*
Dlaczego nie istnieje metoda, która bezpośrednio odsyła oryginalny URL wyświetlany w przeglądarce? Ponieważ
przeglądarka nigdy nie przesyła pełnego URL-u. Dla przykładu numer portu jest wykorzystywany przez klienta w celu
realizacji połączenia HTTP lecz nie jest zawarty w zleceniu składanym serwerowi odpowiadającemu na tym porcie.
W niektórych przypadkach wystarczy że aplet zna nazwę apletową pod którą został wywołany. Informację tą
możemy odczytać przy pomocy metody getServletPath():

public String HttpServletRequest.getServletPath()

Metoda ta odsyła część URI, która odnosi się do wywoływanego apletu (URL jest w razie potrzeby dekodowany)
lub null — jeżeli URI nie wskazuje bezpośrednio na aplet. Ścieżka apletu nie zawiera informacji dodatkowej
ścieżki ani ścieżki kontekstu. Tablica 4.3 prezentuje nazwy apletów zleceniowych URL-ów.

Tabela 4.3.
URL-e i ich ścieżki apletów

URL zleceniowe Ich ścieżki apletów


http://server:port/servlet/Classname /servlet/Classname
http://server:port/servlet/registeredName servlet/registeredNam
e
http://server:port/servlet/Classname?var=val /servlet/Classname
http://server:port/servlet/Classname/pathinfo /servlet/Classname
http://server:port/servlet/Classname/pathinfo?var=val /servlet/Classname
http://server:port/servlet/Classname/path % 20info /servlet/Classname
http://sefver:port/alias.html (alias to a servlet) /alias.html
http://server.port/context/path/servlet/Classname /servlet/Classname
Poniżej przedstawiamy sposób pomocny w zapamiętywaniu informacji ścieżki:

decoded(getRequestURI) ==
decoded(getContextPath) + getServletPath + getPathInfo

Sposób złożenia zlecenia


Aplet poza tym, że ma możliwość dowiedzenia się co było przedmiotem zlecenia, zna również kilka sposobów
na zdobycie informacji związanych ze sposobem jego złożenia. Metoda getScheme() odsyła schemat, użyty do
składania tego zlecenia:

public String ServletRequest.getScheme()

Przykłady zawierają http, https oraz ftp jak również nowsze, Java-specyficzne schematy jdbc i rmi.
Bezpośredni odpowiednik CGI nie istnieje (chociaż niektóre wdrożenia CGI mają zmienną SERVER_URL, która
zawiera schemat). W przypadku apletów HTTP metoda ta informuje o tym czy zlecenie zostało złożone przez
połączenie bezpieczne, przy użyciu SSL — Warstwy Bezpiecznych Gniazdek (Secure Sockets Layer), tak jak w
przypadku schematu https czy też przez połączenie niebezpieczne, wskazane przez schemat http.

Metoda getProtocol() odsyła protocol (protokół) oraz numer wersji użytej do złożenia zlecenia:

public String ServletRequest.getProtocol()

Protokół i numer wersji oddzielone są ukośnikiem. Metoda odsyła null jeżeli nie można ustalić protokołu. Dla
apletów HTTP protokół to zwykle HTTP/1.0 lub HTTP/1.1. Aplety HTTP mogą wykorzystać wersję protokołu
w celu określenia czy mogą podczas współpracy z klientem nowych funkcji zawartych wersji 1.1 HTTP.
Aplet używa metody getMethod() ażeby dowiedzieć się jaka metoda została użyta dla zlecenia.
Metoda ta odsyła metodę HTTP wykorzystaną do złożenia zlecenia. Przykłady zawierają GET, POST, oraz
HEAD. Metoda service(), wdrożenia HttpServlet wykorzystuje tą metodę przy wysyłaniu zleceń.

Nagłówki zleceniowe
Zlecenia i odpowiedzi HTTP mogą mieć wiele nagłówków HTTP. Nagłówki te dostarczają pewną liczbę
dodatkowych informacji o zleceniu lub odpowiedzi. Wersja HTTP 1.0 protokołu określa dosłownie dziesiątki
możliwych nagłówków; wersja 1.1 zawiera ich nawet więcej. Opisu wszystkich nagłówków nie byliśmy w stanie
zamieścić w niniejszej książce; te które przedstawiamy to te, które są najczęściej wykorzystywane przez aplety.
Zainteresowanych pełną listą nagłówków HTTP oraz ich zastosowań odsyłamy do książki Clintona Wonga
„HTTP Pocket Reference”, pomocna może również być tutaj pozycja autorstwa Stephena Spainhour’a i Roberta
Ecksteina „Webmaster in a Nutshell”

Aplet rzadko musi odczytywać nagłówki HTTP, które towarzyszą zleceniu. Wiele nagłówków towarzyszących
zleceniu obsługiwanych jest przez sam serwer. Rozważmy dla przykładu sposób, w jaki serwer ogranicza dostęp
do swoich dokumentów. Serwer wykorzystuje nagłówki HTTP, a aplet nie musi znać szczegółów. Kiedy serwer
otrzymuje zlecenie na stronę, do której dostęp jest ograniczony, sprawdza czy zlecenie zawiera właściwy
nagłówek Authorization (Uwierzytelnianie), który powinien z kolei zawierać poprawną nazwę użytkownika
oraz hasło. W sytuacji kiedy powyższe dane nie są poprawne, sam serwer wysyła odpowiedź z nagłówkiem WWW-
Aythenticate powiadamiając w ten sposób przeglądarkę o tym, że dostęp do zasobu nie został jej przyznany.
Kiedy jednak klient prześle właściwy nagłówek Authorization, serwer przyznaje mu dostęp, a apletowi daje
wywołany dostęp do nazwy użytkownika poprzez wywołanie metody getRemoteUser().

Inne nagłówki są również używane przez aplety lecz nie bezpośrednio. Dobrym przykładem może tutaj być para
Last-Modified, If-LastModified (omówiona w rozdziale 3). Sam serwer widzi nagłówek If-Last-
Modified i wywołuje metodę apletu getLastModified() żeby dowiedzieć się jak postępować.

Istnieje kilka nagłówków HTTP, które aplet może „zechcieć” wywołać. Nagłówki te zostały zaprezentowane w
tabeli 4.4.

Tabela 4.4.
Lista przydatnych nagłówków zleceniowych HTTP
Nagłówek Zastosowanie
Accept Określa typ nośnika (MIME), który klient preferuje, oddzielony przecinkami. Niektóre starsze
typy przeglądarek przesyłają osobny nagłówek dla każdego typu nośnika. Każdy typ nośnika
podzielony jest na typ i podtyp podawane jako type/subtype. Symbol gwiazdka (*)
przyznany jest dla podtypu (type/*) lub zarówno dla typu jak i podtypu (*/*). Na przykład:
Accept: image/gif, image/jpeg, text/*, */*
Aplet może posłużyć się tym nagłówkiem jako pomocą w określeniu jaki typ treści odesłać.
Jeżeli nagłówek ten nie jest przekazywany jako część zlecenia możemy założyć, ze klient
akceptuje wszystkie typy nośników
Accept- Określa język lub języki, które preferuje klient, używając w tym celu skrótów języka
Language standardowego ISO-639 wraz z (wariantowym) kodem kraju ISO-3166. Na przykład:
Accept-Language: en, es, de, ja, zh-TW
Taki zapis oznacza, że klient (użytkownik) czyta posługuje się językiem angielskim,
hiszpańskim, niemieckim, japońskim oraz Tajwańskim dialektem języka chińskiego. Stosuje
się konwencję, według której języki są zapisywane w kolejności preferencji. W rozdziale 13
(„Internacjonalizacja”) można znaleźć więcej informacji na temat.
User-Agent Dostarcza informacji na temat oprogramowania klienta. Format odsyłanego strumienia jest
względnie dowolny, zwykle jednak zawiera nazwę oraz wersję przeglądarki oraz informacje o
komputerze na którym jest wykonywany. Netscape 4.7 na SGI Indy uruchamiający IRIX 6.2
relacjonuje:
User-Agent: Mozilla/4.7 [en] (Xll; U; IRIX 6.2 IP22)
Microsoft Internet Explorer 4.0 działający „pod” Windows’em 95 relacjonuje:
User-Agent: Mozilla/4.0 (conpatible; MSIE 4.0; Windows 95)
Aplet może wykorzystać ten nagłówek do prowadzenie statystyk lub do dostosowania swojej
odpowiedzi w oparciu o typ przeglądarki.
Referer Podaje URL dokumentu, który odnosi się do URL-u zlecenia (tj. dokumentu, który zawierał
łącznik, który z kolei został wykorzystany przez klienta w celu uzyskania dostępu do tego
dokumentu)*) Dla przykładu:
Referer: http://developer.java.sun.com/index.html
Aplet może posługiwać się tym nagłówkiem w celu prowadzenia statystyk lub w przypadku
istnienia jakiegoś błędu w zleceniu, utrzymywać ścieżkę dokumentu z błędami.
Authorizatio Zapewnia uprawnienie klienta do dostępu do żądanego URI, włącznie z nazwą użytkownika i
n hasłem kodowanymi w Base64. Aplety mogą stosować taką niestandardową autoryzację
(uprawnienie) w sposób omówiony w rozdziale 8.
*)
W słowniku słówko to zapisane byłoby jako Referrer. Jednakże fakt, że musimy stosować się do pisowni
zawartej w specyfikacji HTTP, determinuje taką a nie inną pisownię tego wyrazu.

Dostęp do wartości nagłówkowych


Dostęp do wartości nagłówkowych uzyskuje się wykorzystując obiektu HttpServletRequest. Wartość
nagłówkowa może zostać odczytana jako String, long (reprezentująca Date) lub int, przy użyciu metod
getHeader(), getDateHeader() i getIntHeader() odpowiednio:

public String HttpServletRequest.getHeader(String name)


public long HttpServletRequest.getDateHeader(String name)
public int HttpServletRequest.getIntHeader(String name)

getHeader() odsyła wartość nagłówka nazwanego, jako String (ciąg znaków) lub null (zero) — w wypadku
kiedy nagłówek nie był przesłany jako część zlecenia. W wypadku w nazwie nie są rozróżniane małe i wielkie
litery, przeciwnie do wszystkich tych metod. Przy pomocy tej metody można odczytać wszystkie typy
nagłówków.

getDateHeader() odsyła wartość nagłówka nazwanego, jako long (reprezentujący Date), określające ile
milisekund upłynęło od „epoki”) lub — 1 jeżeli nagłówek nie został przesłany jako część zlecenia. Metoda ta
zgłasza wyjątek IllegalArgumentException, kiedy wywoływany jest nagłówek, którego wartość nie może
zostać przekształcona na Date. Metoda ta jest użyteczna przy operowaniu nagłówkami takimi jak Last-
Modified czy If-Modified-Since.

GetIntHeader() odsyła wartość nagłówka nazwanego jako int, lub 1 — gdy nagłówek nie został przesłany
jako część zlecenia. Metoda zgłasza wyjątek NumberFormatException przy wywoływaniu nagłówka, którego
wartość nie może być przekształcona na int.

Aplet może również, używając metody getHeaderNames(), uzyskać nazwy wszystkich nagłówków:

public Enumeration HttpServletRequest.getHeaderNames()


Metoda ta odsyła nazwy wszystkich nagłówków jako Enumeration (wyliczenie) obiektów String. W
przypadku gdy nie było żadnych nagłówków metoda odsyła puste Enumeration. Interfejs API daje wdrożeniom
pojemników apletów prawo uniemożliwienia takiego sposobu dostępu do nagłówków, w przypadku którego
metoda ta odsyła null.

Niektóre nagłówki takie jak np. Accept czy Accept-Language, obsługują wartości wielokrotne. Zwykle
wartości te są przekazywane w jednym nagłówku, oddzielonym odstępami, jednak niektóre przeglądarki
preferują przesyłanie wartości wielokrotnych przez nagłówki wielokrotne:

Accept-Language: en
Accept-Language: fr
Accept-Language: ja

W celu odczytania nagłówka za pomocą wartości wielokrotnych, aplety mogą posłużyć się metodą getHeaders
():

public Enumeration HttpServletRequest.getHeaders(String name)

Omawiana metoda odsyła wszystkie wartości dla danego nagłówka jako Enumeration (wyliczenie)obiektów
String, lub puste Enumeration — jeżeli nagłówek nie został przesłany jako część zlecenia. W przypadku
gdy pojemnik apletu nie pozwala na dostęp do informacji nagłówka, wywołanie odsyła null. Nie istnieją
metody getDateHeaders() ani getIntHeaders().

Na przykładzie 4.19 zostało zaprezentowane zastosowanie powyższych metod w aplecie, który wyświetla
informacje o swoich nagłówkach zleceń HTTP.

Przykład 4.19.
„Podpatrywanie” nagłówków

import java.io. *;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http. *;
public class HeaderSnoop extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType(tekst/zwykły") ;
PrintWriter out = res.getWriter();

out.println("Nagłówki zleceniowe:") ;
out.println() ;
Enumeration names = req.getHeaderNames();
while (names. hasMoreElements ()) {
String name = (String) names. nextElement ();
Enumeration values=req.getHeaders(name);//obsługuj wartości
wielokrotne
if (values != null) {
while (values .hasMoreElements ()) {
String value = (String) values.nextElement();
out. println (name + ": " + value);

Przykładowy wydruk wyjściowy z tego apletu mógłby wyglądać w następujący sposób:

Request Headers;

Connection: Keep-Alive
If-Modified-Since: Thursday, 17-Feb-OO 23:23:58 GMT; length=297
User-Agent: Mozilla/4.7 [en] (WiriNT; U)
Host: localhost:8080
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Language: en
Accept-Language: es
Accept-Charset: iso-8859-1,*,utf-8
Cookie: JSESSIONID=ql886xlc31

Strumień wyjściowy
Z każdym zleceniem, obsługiwanym przez aplet związany jest strumień wejściowy. Tak jak w
przypadku PrintWriter czy OutpurStream związanych z związanych z obiektem odpowiedzi
apletu, do których może on pisać, w przypadku Reader czy InputStream związanych z
obiektem zlecenia apletu, może on z nich czytać. Dane odczytywane ze strumienia
wejściowego mogą mieć dowolny typ treści i dowolną długość. Strumień wejściowy ma dwa
zasadnicze cele:
• Przekazanie apletowi HTTP treści związanej ze zleceniem POST
• Przekazanie apletowi innemu niż HTTP dane nieprzetworzone, przesłane przez klienta
W celu odczytania danych znakowych ze strumienia wejściowego, powinniśmy użyć metody
getReader(), pobierając strumień wejściowy jako obiekt BufferReader:

public BufferedReader ServletRequest.getReader() throws IOException

Przewaga używania BufferReader dla odczytywania danych znakowych, to poprawne tłumaczenie zestawów
znaków. Metoda ta zgłasza wyjątek IllegalStateException w przypadku gdy metoda getInputStream()
została wywołana do tego samego zlecenia. Metoda ta zgłasza również wyjątek
UnsupportedEncodingException jeżeli kodowanie znaków sygnału wejściowego jest nieobsługiwane lub
nieznane.

W celu odczytania danych binarnych ze strumienia wejściowego, należy posłużyć się metodą getInputStream
(), pobierając strumień wejściowy jako obiekt ServletInputStream:

public ServletInputStream ServletRequest.getInputStream() throws IOException

Obiekt ServletInputStream jest bezpośrednią podklasą InputStream i może być traktowany jako normalny
InputStream, z dodaną zdolnością odczytywania sygnału wejściowego (wiersz na jeden raz) w tablicę bitów.
Metoda zgłasza wyjątek IllegalStateException jeżeli getReader() została wywołana wcześnie do tego
samego zlecenia. Kiedy już mamy ServletInputStream, możemy odczytać z stamtąd wiersz przy użyciu
readLine():

public int ServletInputStream.readLine(byte b[], int off, int len)


throws IOException

Metoda ta czyta bity ze strumienia wejściowego do tablicy bitów b, począwszy od pozycji w tablicy podanej
przez off. Przerywa zaś czytanie kiedy napotyka na \n lub kiedy przeczyta liczbę bitów len.W Znak końcowy
\n jest również czytany do bufora. Metoda ta odsyła sczytaną liczbę bajtów, lub 1 — jeżeli został osiągnięty
koniec strumienia.

W
drożenia readLine Interfejsu API 2.0 były obciążone błędem, przekazany parametr len był ignorowany, stwarzając
problemy włącznie z ArrayIndexOutOfBoundsException — jeżeli długość linii przekraczała objętość bufora.
Błąd ten został usunięty w Interfejsie 2.1 oraz późniejszych wersjach.
Aplet może dodatkowo sprawdzić typ zawartości oraz długość danych przesyłanych przez strumień wejściowy,
przy wykorzystaniu metod getContentType() oraz getContentLength() odpowiednio:

public String ServletRequest.getContentType()


public int ServletRequest.getContentLength()

getContentType() odsyła typ nośnika treści przesyłanej przez strumień wejściowy, lub null — jeżeli typ ten
nie jest znany (tak jak w przypadku kiedy nie ma żadnych danych).
GetContentLength() odsyła długość, w bitach, treści przesyłanej przez strumień wejściowy, lub -1 — jeżeli
długość ta nie jest znana.

Ładowanie plików przy użyciu strumienia wejściowego


Aplety mogą również otrzymywać załadowania plików przy użyciu swojego strumienia
wejściowego. Zanim dowiemy się jak to jest możliwe, musimy zaznaczyć, że ładowanie plików
jest na razie w fazie eksperymentowania i, w związku z tym nie jest obsługiwane przez
wszystkie przeglądarki. Co się tyczy Netscape’a to wprowadził on obsługę ładowania plików
w Netscape Navigator’ze 3; w przypadku Microsoftu był to Internet Explorer 4.

Pełna charakterystyka ładowani plików została zamieszczona w eksperymentalnym RFC


1867, dostępnym na stronie http:/www.ietf.org/rfc/rfc1867.txt, z uzupełnieniami w RFC 2388,
dostępnym na stronie http:/www.ietf.org/rfc/rfc/2388.txt. Podsumowując nasze rozważania
możemy powiedzieć, że każda liczba plików i parametrów może zostać przesłana jako dane
formularzowe, w pojedynczym zleceniu POST. Zlecenie POST jest formatowane inaczej niż
standardowe dane formularzowe application/x-www-form-urlencoded i informuje o tym
poprzez ustalenie ich typu treści do multipart/form-data.

Napisanie klientowi połowę ładowania pliku jest rzeczą całkiem prostą. HTML w przykładzie
4.20 generuje formularz, który prosi o podanie nazwy użytkownika oraz pliku do
załadowania. Warte odnotowania jest dodanie atrybutu ENCTYPE oraz zastosowanie typu FILE
sygnału wejściowego.

Przykład 4.20.
Formularz do wybierania ładowanego pliku

<FORM ACTION="/servlet/UploadTest" ENCTYPE="wieloczęściowe/dane formularzowe"


METHOD=POST>
What is your name? <INFOT TYPE=TEXT NAME=submitter> <BR>
Which file do you want to upload? <INPUT TYPE=FILE NAME=file> <BR>
<IMPUT TYPE=SUBMIT>
</FORM>

Użytkownik, który otrzymuje podobny formularz widzi stronę, która wygląda podobnie jak ta
przedstawiona na rysunku 4.3. Nazwa pliku może zostać umieszczona w obszarze tekstowym
lub wybrana poprzez przeglądanie. Wielokrotne pola danych <INPUT TYPE =FILE> mogą być
stosowane, jednakże obecnie stosowane przeglądarki obsługują ładowanie tylko jednego
pliku na pole. Po selekcji użytkownik przedkłada formularz standardowo.
Rysunek 4.3.
Wybieranie pliku do załadowania

Obowiązki serwera podczas ładowania plików są nieco bardziej skomplikowane. Z perspektywy apletu,
przedłożenie jest niczym innym jak tylko strumieniem danych nieprzetworzonych w jego strumieniu wejściowym
— strumienia danych formatowanych zgodnie z typem treści multipart/form-data podanego w RFC 1867.
Interfejs API nie oferuje żadnej metody wspomagającej parsowanie danych. W celu „uproszczenia spraw” Jason
napisał klasę użyteczności, która nie działa w naszym przypadku. Jej nazwa brzmi MultipartRequest i została
zaprezentowana na przykładzie 4.22, dalej w tym podrozdziale.

MultipartRequest zawija ServletRequest i przedstawia proste dla programistów apletów API. Klasa ta ma
dwóch konstruktorów:

public MultipartRequest(HttpServletRequest request, String saveDirectory,


int maxPostSize) throws IOException
public MultipartRequest(HttpServletRequest request,
String saveDirectory) throws IOException

Każda z tych metod tworzy nowy obiekt MultipartRequest aby obsłużyć określone zlecenie, zapisując
wszystkie załadowane pliki w saveDirectory. Obydwaj konstruktorzy właściwie parsują treść
multpart/form-data i zgłaszają wyjątek IOException w razie jakichkolwiek problemów (tak więc aplety
używając tej klasy nie mogą czytać strumienia wejściowego). Konstruktor, który przyjmuje parametr
MaxPostSize zgłasza również wyjątek IOException w wypadku gdy ładowana treść jest większa niż
maxPostSize. Drugi konstruktor przyjmuje domyślne MaxPostSize 1Mb.

Serwer, który otrzymuje ładowanie, którego długość treści jest zbyt wielka ma do wyboru dwa wyjścia: po
pierwsze, spróbować przesłać stronę błędu, poczekać aż klient się rozłączy i podczas „cichego” czekania
zniszczyć całą załadowaną treść. Postępowanie takie jest zgodne z opisem HTTP / 1.1 w RFC2616, sekcja 8.2.2,
która mówi, że klient powinien oczekiwać na „stan błędu” podczas ładowania (porównaj stronę
http:/www.ietf.org/rfc/rfc2616.txt) oraz zatrzymać ładowanie w momencie otrzymania informacji o błędzie.
Procedura taka gwarantuje, że każdy klient będzie widział właściwy komunikat błędu, jednakże dla wielu
przeglądarek, które nie oczekują na status błędu oznacza marnowanie przepustowości serwera ponieważ klient
będzie przeprowadzał pełne ładowanie. Z tego właśnie powodu wiele serwerów wdraża drugą opcję: próbują
przesłać stronę błędu i w razie potrzeby bezwzględnie rozłączyć. Sytuacja taka powoduje, że wielu klientów
pozostaje bez komunikatu błędu, lecz zapewnia to bezwzględne przerwanie ładowania.

Klasa MultipartRequest dysponuje siedmioma publicznymi metodami, które umożliwiają dostęp do


informacji o zleceniu. Jak się przekonamy większość z tych metod jest modelowanych po metodach
ServletRequest. Aby uzyskać nazwy wszystkich parametrów zlecenia należy skorzystać z metody
getParameterNames():

public Enumeration MultipartRequest.getParameterNames()

Metoda ta odsyła nazwy wszystkich parametrów jako Enumeration (wyliczenie) obiektów String, lub puste
Enumeration jeżeli nie ma żadnych parametrów.

W celu uzyskania nazwy parametru nazwanego należy zastosować metodę getParameter() lub metodę
getParameterValues():

public String MultipartRequest.getParameter(String name)

Metoda ta odsyła wartość parametru nazwanego jako String, lub null — jeżeli parametr nie został podany.
Jest pewność, że wartość będzie w swojej zdekodowanej formie. Jeżeli parametr ma wartości wielokrotne, tylko
ostatnia z nich jest odsyłana.

public String[] MultipartRequest.getParameterValues(String name)

Metoda ta odsyła wszystkie wartości parametrów nazwanych jako String, lub null — jeżeli parametr nie
został podany. Pojedyncza wartość jest odsyłana w tablicy o długości 1.

W celu pobrania listy wszystkich załadowanych plików wykorzystuje się metodę getFileNames():

public Enumeration MultipartRequest.getFileNames()

Powyższa metoda odsyła nazwy wszystkich ładowanych plików jako Enumeration obiektów String, lub puste
Enumeration — jeżeli nie ma żadnych załadowanych plików. Zwróćmy uwagę, iż wszystkie nazwy plików są
określane przez atrybut nazwy formularza HTML, a nie przez użytkownika. Kiedy mamy już nazwę pliku
możemy uzyskać jego nazwę systemu plików, wykorzystując w tym celu metodę getFilesystemName():

public String MultipartRequest.getFilesystemName(String name)

Metoda ta odsyła nazwę systemu plików określonego pliku, lub null — jeżeli plik nie został dołączony w
ładowaniu. Nazwa systemu plików jest określana przez użytkownika. Jest to jednocześnie nazwa pod którą plik
jest zapisywany. Używając metodę getContentType() możemy uzyskać typ treści pliku:
public String MultipartRequest.getContentType(String name)

Metoda ta odsyła typ treści danego pliku (jako taki, który jest obsługiwany przez przeglądarkę klienta), lub null
— jeżeli plik nie był zawarty w ładowaniu. Wreszcie możemy również uzyskać dla pliku obiekt java.io.File,
zasób pomocą metody grtFile():
public File MultipartRequest.getFile(String name)

Powyższa metoda odsyła obiekt File określonego pliku zapisanego w systemie plików serwera, lub null —
jeżeli plik nie był zawarty w ładowaniu.
Przykład 4.21 ukazuje sposób w jaki aplet stosuje MultipartRequest. Jedyną rzeczą, którą wykonuje aplet
jest statystyka tego co zostało załadowane. Zwróćmy uwagę, że aplet nie usuwa plików, które zapisuje.

Przykład 4.21.
Obsługiwanie ładowania plików

import java.io.*;
import java.util.*;
import javax.servlet. *;
import javax.servlet.http.*;

import com.oreilly.servlet.MultipartRequest ;

public class UploadTest extends HttpServlet {

public void doPost(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();

try {
// Weź to "na wiarę" iż jest to wieloczęściowe zlecenie danych
// formularzowych
// Skonstruuj MultipartRequest żeby usprawnić czytanie
//informacji.
// Przekaż zlecenie, katalog, w którym mają być zapisane pliki
//oraz
// maksymalny rozmiar POST, który powinniśmy spróbować obsłużyć.
// Tutaj niestety piszemy do bieżącego katalogu oraz nakładamy
// ograniczenie 5-cio megowe
MultipartRequest multi =
new MultipartRequest(req, ".", 5 * 1024 * 1024);

out.println("<HTML>") ;
out.println ("<HEAD><TITLE>UploadTest</TITLE></HEAD>") ;
out.println("<BODY>") ;
out.println("<Hl>UploadTest</Hl>") ;

// Wydrukuj parametry, które otrzymaliśmy


out.println("<H3>Params:</H3>") ;
out.println("<PRE>") ;
Enumeration params = multi. getParameterNames ();
while (params. hasMoreElements ()) {
String name = (String)params.nextElement();
String value = multi.getParameter (name);
out.println(name + " = " + value);
}
out.println("</PRE>") ;
// Pokaż pliki, które otrzymaliśmy
out.println("<H3>Files:</H3>") ;
out.println("<PRE>") ;
Enumeration files = multi. getFileNames ();
while (files.hasMoreElements()) {
String name = (String)files.nextElement();
String filename = multi.getFilesystemName (name) ;
String type = multi.getContentType (name);

File f = multi.getFile(name) ;
out.println("name: " + name);
out.println("filename: " + filename);
out.println("type: " + type);
if (f != null) {
out.println( "length: " + f.length());
}
out.println() ;
}
out.println("</PRE>") ;
}
catch (Exception e) {
out.println("<PRE>") ;
e.printStackTrace(out) ;
out.println("</PRE>") ;
}
out.println( "</BODY></HTML>") ;
}

}
Aplet przekazuje swój obiekt zlecenia do konstruktora MultipartRequest razem z katalogiem względnym do
katalogu macierzystego serwera, gdzie będą zapisane ładowane pliki (ponieważ obszerne pliki mogą nie zostać
umieszczone w pamięci) oraz z maksymalnym rozmiarem POST 5 MB. Aplet używa następnie
MultipartRequest do przechodzenia kolejno przez parametry, które zostały przesłane. Zwróćmy uwagę, iż
MultipartRequest API dla manipulowania parametrami zgadza się z ServletRequest. I wreszcie aplet
wykorzystuje swojego MultipartRequest do przechodzenia przez przesłane pliki. Dla każdego pliku
uzyskiwana jest nazwa pliku (zgodnie z formularzem), nazwa systemu plików (określona przez użytkownika)
oraz typ treści. Aplet uzyskuje również odwołanie File, którego używa w celu wyświetlenia zapisywanego
pliku. W razie jakichkolwiek problemów aplet powiadamia użytkownika o sytuacji wyjątkowej.

Na przykładzie 4.22 zaprezentowano kod dla MultipartRequest. Na przykładzie tym będziemy


mogli zaobserwować klasę MultipartRequest wykorzystującą
com.oreilly.servlet.multipart.MultipartParser „w tle”, w celu wykonania parsowania
zlecenia. Klasa MultipartParser zapewnia dostęp do poziomu podstawowego ładowania
poprzez „przechodzenie” przez zlecenie kawałek po kawałku. Pozwala to, na przykład na
bezpośrednie ładowanie plików do bazy danych lub na sprawdzenie czy plik przekazuje
określone kryteria przed zapisaniem. Kod oraz dokumentacja klasy MultipartParser można
znaleźć na stronie http://www.servlets.com (podziękowania od autorów dla Geoffa Souter'a
za wykonanie pracy niezbędnej do stworzenia parsera).

Należy mieć świadomość tego, iż wielu producentów serwerów nie testuje należycie swoich
wyrobów pod kątem ładowania plików, nie jest rzeczą niezwykłą w przypadku tej klasy, fakt iż
serwery bywają obarczone błędami. Jeżeli spotkamy się z problemami podczas korzystania tej
klasy, powinniśmy spróbować innego serwera (jak np. „Tomcat’a”) w celu ustalenia czy
rzeczywiście są one związane z serwerem — jeżeli okaże się, że to prawda należy
skontaktować się z producentem serwera.

Przykład 4.22.
Klasa MultipartRequest

package com.oreilly.servlet ;

import java.io.* ;
import java.uti1.*;
import javax.servlet.* ;
import javax.servlet.http. *;

import com.oreilly.servlet.multipart.MultipartParser ;
import com.oreilly.servlet.multipart.Part;
import com.oreilly.servlet.multipart.FilePart ;
import com.oreilly.servlet multipart.ParamPart ;

// Klasa użytkowa do obsługi<code>multipart/form-data</code> requests.


public class MultipartRequest {

private static final int DEFAULT_MAX_POST_SIZE=1024 * 1024; //1 Meg

private Hashtable parameters = new Hashtable(); // nazwa – Wektor –


// wartości
private Hashtable files = new Hashtable(); // nazwa –
// UploadedFile

public MultipartRequest (HttpServletRequest request,


String saveDirectory) throws IOException {
this(request, saveDirectory/ DEFAULT_MAX_POST_SIZE) ;
}

public MultipartRequest (HttpServletRequest request,


String saveDirectory,
int maxPostSize) throws IOException {
// Wartości kontroli poprawności
if (request == null)
throw new IllegalArgumentException ("zlecenie nie może mieć
wartości zero");
if (saveDirectory == null)
throw new IllegalArgumentException ("saveDirectory nie może mieć
wartości zero");
if (maxPostSize <= 0) {
throw new IllegalArgumentException ("maxPostSize musi być
dodatnie");

// Zapisz katalog
File dir = new File(saveDirectory) ;

// Sprawdź czy saveDirectory jest rzeczywiście katalogiem


if (!dir.isDirectory())
throw new IllegalArgumentException("Nie
katalog;"+saveDirectory);

// Sprawdź czy w saveDirectory można zapisywać


if (!dir.canWrite())
threw new IllegalArgunientException("Nie można zapisywać: " +
saveDirectory);

// Parsuj nadchodzące, wieloczęściowe, zapisywane pliki w podanym


// katalogu
// oraz ładuj do bazy meta obiekty, które opisują co znaleźliśmy
MultipartParser parser = new MultipartParser (request,
maxPostSize);
Part part;
while ((part = parser.readNextPart()) != null) {
String name = part. getName() ;
if (part.isParam()) {
// To jest parametr part, dodaj go do wektora wartości
ParamPart paramPart = (ParamPart) part;
String value = paramPart.getStringValue() ;
Vector existingValues = (Vector) parameters, get(name);
if (existingValues == null) {
existingValues = new Vector();
parameters. put (name, existingValues) ;
}
existingValues. addElement(value) ;
}
else if (part.isFile()) {
// To jest część pliku
FilePart filePart = (FilePart) part;
String fileName = filePart.getFileName();
if (fileName != null) {
// Część rzeczywiście zawierała plik
filePart.writeTo(dir) ;
files.put(name, new UploadedFile(
dir.toString(), fileName' filePart.getContentType())) ;
}
else {
// Pole danych nie zawierało pliku
files.put(name, new UploadedFile(null, null, null));
}
}
}
}

// Konstruktor ze starą sygnaturą, utrzymywany dla kompatybilności


// wstecznej.
public MultipartRequest(ServletRequest request,
String saveDirectory) throws IOException {
this ((HttpServletRequest) request, saveDirectory) ;
}

// Konstruktor ze starą sygnaturą, utrzymywany dla kompatybilności


// wstecznej.
public MultipartRequest (ServletRequest request,
String saveDirectory,
int maxPostSize) throws IOException {
this((HttpServletRequest)request, saveDirecfcory, maxPostSize)
}

public Enumeration getParameterNames() {


return parameters.keys() ;
}

public Enumeration getFileNames() {


return files.keys() ;
}

public String getParameter(String name) {


try {
Vector values = (Vector) parameters, get (name);
if (values == null || values.size() == 0) {
return null;
}
String value = (String)values.elementAt(values.size() - 1) ;
return value;
}

catch (Exception e) {
return null;
}
}

public String[] getParameterValues(String name) {


try {
Vector values = (Vector) parameters, get (name);
if (values == null || values.size() == 0) {
return null;
}
String[] valuesArray = new String [values, size ()];
values.copyInto(valuesArray) ;
return valuesArray;
}
catch (Exception e) {
return null;
}
}

public String getFilesystemName (String name) {


try {
UploadedFile file = (UploadedFile)files.get(name);
return file.getFilesystemName() ; // może być zero
}
catch (Exception e) {
return null;
}
}
public String getContentType(String name) {
try {
UploadedFile file = (UploadedFile)files.get(name);
return file. getContentType(); // może być zero
}
catch (Exception e) {
return null;
}
}

public File getFile(String name) {


try {
UploadedFile file = (UploadedFile)files.get(name);
return file.getFile(); // może być zero
}
catch (Exception e) {

return null;
}
}
}

// Klasa do przechowywania informacji o załadowanym pliku.


class UploadedFile {

private String dir;


private String filename;
private String type;

UploadedFile(String dir. String filename, String type) {


this.dir = dir;
this.filename = filename;
this.type = type;
}
public String getContentType() {
return type;
}

public String getFilesystemName () {


return filename;
}

public File getFile() {


if (dir == null || filename == null) {
return null;
)
else {
return new File(dir + File.separator + filename);
}
}
}

Klasa MultipartRequest jest produktem markowym, i w przeciwieństwie do wielu bibliotek ładowań plików,
obsługuje dowolnie duże ładowania. Zadajmy sobie pytanie dlaczego klasa ta nie wdraża interfejsu
HttpServletRequest? Odpowiedź brzmi: ponieważ ograniczałoby to jej kompatybilność z przyszłymi
wersjami. Jeżeli MultipartRequest wdrożyłaby HttpServletRequest i Interfejs API 2.3 dodałby tą
metodę do interfejsu, klasa nie wdrażałaby już całkowicie interfejsu powodując zakłócenia kompilacji apletów
używających tej klasy.

Dodatkowe atrybuty
Czasem aplety potrzebują informacji na temat zlecenia, dostępu do których nie można uzyskać poprzez metody
wymienione wcześniej. W takich przypadkach jest jeszcze jedna, ostatnia alternatywa — metoda getAttribute
(). Przypomnijmy sobie w jaki sposób ServletContext uzyskiwał metodę getAttribute, która odsyłała
specyficzne-serwerowo atrybuty związane z samym serwerem. ServletRequest również ma metodę
getAttribute():

public Object ServletRequest.getAttribute(String name)

Metoda ta odsyła wartość serwerowo-specyficznego atrybutu dla zlecenia, lub null — w przypadku gdy serwer
nie obsługuje nazwanego atrybutu zlecenia. Metoda ta pozwala również serwerowi na dostarczanie apletowi
niestandardowych informacji o zleceniu. Serwery mają wolną rękę w dostarczaniu jakichkolwiek atrybutów lub
mogą ich w ogóle nie dostarczać. prostu nie. Jedynym wymogiem jest to, że nazwy atrybutów powinny być w tej
samej konwencji co nazwy pakietów, z nazwami pakietów java.* oraz javax.* zarezerwowanymi dla użytku
„Java Software division of Sun Microsystems”, a com.sun.* zarezerwowane dla „Sun Microsystems”. Opis
atrybutów naszego serwera znajdziemy w jego dokumentacji, pamiętajmy, że używanie serwerowo-
specyficznych atrybutów ogranicza przenośność naszej aplikacji.

Aplety mogą również dodać do zlecenia swoje własne atrybuty wykorzystując metodę setAttribute() (tak
jak to zostało omówione w rozdziale 11). Wydruk wszystkich aktualnych atrybutów, ustalonych przez serwer lub
umieszczonych przez aplety, można otrzymać przy pomocy metody getAttributeNames():

public Enumeration ServletRequest.getAttributeNames()

Poniższy kod wyświetla wszystkie aktualne atrybuty:

Enumeration enum = req.getAttributeNames();


while (enum.hasMoreElements()) {

String name = (String) enum.nextElement() ;


out.println(" req.getAttributet(\"" + nazwa + "\"): " +
req.getAttribute(name));
}
Istnieje kilka standardowych atrybutów, które dotyczą zleceń (porównaj rozdział 11) oraz
cyfrowych certyfikatów po stronie serwera (porównaj rozdział 8).
Rozdział 5. Przesyłanie informacji

HTTP

W poprzednim rozdziale dowiedzieliśmy się, że aplet ma dostęp do różnego rodzaju informacji — informacji o
kliencie, o serwerze, o zleceniu i nawet o sobie samym. Czas więc abyśmy się zapoznali z tym, co aplet może
zrobić z tymi informacjami — dowiemy się jak są ustalane i przesyłane informacje.
Rozdział ten zaczyna się od ogólnej charakterystyki sposobu, w jaki aplet odsyła standardową odpowiedź HTTP.
W tym rozdziale omówimy również szczegółowo niektóre metody, które omówiliśmy tylko pobieżnie w
poprzednich przykładach. Dalej powiemy jak zmniejszyć obciążenie związane z utrzymywaniem połączenia z
klientem, dowiemy się również jak w tym celu wykorzystać buforowanie odpowiedzi. Następnie poznamy parę
dodatkowych, przydatnych zastosowań HTML-u i HTTP takich jak np. odsyłanie błędów, i innych kodów
statusu, przesyłanie niestandardowych informacji nagłówkowych, przekierowywanie zlecenia, wykorzystywanie
ściągania klienta, obsługa wyjątków apletu, ustalanie momentu rozłączenie klienta oraz wpisywanie danych do
dziennika zdarzeń serwera.
W przeciwieństwie do swojego odpowiednika z poprzedniego wydania niniejszej książki, rozdział ten nie opisuje
szczegółowo generowania treści HTML-owej. Jest on wprowadzeniem do następnych rozdziałów książki, w
których omówionych zostanie szereg oddzielnych osnów.

Struktura odpowiedzi
Aplety HTTP odsyłają trzy, różne rodzajowo kwestie: pojedynczy kod statusu, dowolną liczbę nagłówków
HTTP oraz treść odpowiedzi. Kod statusu jest liczbą całkowitą, która opisuje, jak się łatwo można domyśleć
status odpowiedzi. Kod statusu może informować o sukcesie bądź niepowodzeniu może również poinformować
oprogramowanie klienta, iż należy powziąć dodatkowe kroki w celu zakończenia zlecenia. Numerycznemu
kodowi statusu często towarzyszy reason phrase, które opisuje status językiem bardziej przystępnym dla ludzi.
Zwykle kod statusu działa „w tle” i jest interpretowany przez oprogramowanie przeglądarki. W niektórych
przypadkach, kiedy pojawiają się problemy, przeglądarka może pokazać kod statusu użytkownikowi. Najbardziej
chyba znanym kodem statusu jest kod 404NotFound, przesyłany przez serwer WWW, kiedy ten nie może
znaleźć żądanego URL-u.

W poprzednim rozdziale zapoznaliśmy się ze sposobem, w jaki klient wykorzystuje nagłówki HTTP w celu
przesłania dodatkowych informacji razem ze zleceniem. W tym rozdziale zobaczymy jak aplet może przesyłać te
nagłówki jako część swojej odpowiedzi,

Korpus odpowiedzi jest główną treścią odpowiedzi. Dla strony HTML, korpusem odpowiedzi jest sam HTML.
W przypadku grafiki, korpus odpowiedzi jest złożony z bitów, które składają się na obrazek. Korpus odpowiedzi
może mieć różny typ oraz różną długość; klient czytając oraz interpretując nagłówki HTTP zawarte w
odpowiedzi, wie czego może się spodziewać.
Standardowe aplety są znacznie mniej skomplikowane od apletów HTTP — odsyłają tylko korpus odpowiedzi
do swojego klienta. Co się jednak tyczy podklasy GenericServlet, to może ona przedstawiać API, które dzieli
pojedynczy korpus odpowiedzi na bardziej skomplikowaną strukturę, dając wrażenie odsyłania wielokrotnych
pozycji ewidencyjnych. W rzeczywistości jest to dokładnie to co robią aplety HTTP. Na najniższym poziomie,
serwer WWW przesyła całą odpowiedź do klienta jako strumień bitów. Wszystkie metody, które ustalają kody
statusu lub nagłówki są w stosunku do tego abstrakcjami.

Jest rzeczą ważną, aby zrozumieć, mimo iż programista apletów nie musi znać szczegółów protokołu HTTP, że
protokół ten jednak ma wpływ na kolejność z jaką aplet wywołuje swoje metody. Cechą charakterystyczną
protokołu HTTP jest to, że kod statusu oraz nagłówki muszą zostać przesłane przed korpusem odpowiedzi.
Dlatego właśnie aplet musi dopilnować, aby zawsze ustalić najpierw swój kod statusu oraz nagłówki zanim
jeszcze prześle klientowi jakikolwiek korpus odpowiedzi. Aplet może umieścić swój korpus odpowiedzi w
pamięci podręcznej, żeby uzyskać większą swobodę, jednak kiedy korpus odpowiedzi zostanie już wysłany,
odpowiedź uważana jest jako zatwierdzona, wtedy kod statusu oraz nagłówki nie mogą już zostać zmienione.

Przesyłanie standardowej odpowiedzi


Nasze rozważania na temat odpowiedzi apletów rozpoczniemy od powtórnego przyjrzenia się pierwszemu
apletowi, którego przedstawiliśmy w tej książce jako pierwszego, mowa oczywiście o aplecie HelloWorld
(aplet HelloWorld został pokazany w przykładzie 5.1). Mamy nadzieję, iż w tej chwili wydaje się on już
znacznie prostszy niż w rozdziale 2 „Podstawy Apletów HTTP”.

Przykład 5.1.
Jeszcze raz „Hello”

import java.io.* ;
import javax.servlet.* ;
import javax.servlet.http. *;

public class HelloWorld extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

res. setContentType (" tekst/html") ;


PrintWriter out = res.getWriter();

out.println("<HTML>") ;
out.println("<HEAD><TITLE>Hello World</TITLE></HEAD>") ;
out.println("<BODY>") ;
out.println("<BIG>Hello World</BIG>") ;
out.println("</BODY></HTML>") ;
}
}

Aplet ten używa dwóch metod oraz jednej klasy, które do tej pory zostały omówione tylko
skrótowo. Metoda setContentType() ServletResponse (odpowiedzi serwera) ustala typ
treści odpowiedzi, aby był on typem określonym.
public void ServletResponse.setContentType(String typ)

W aplecie HTTP metoda ta ustala nagłówek HTTP Content-Type.

Metoda getWriter() odsyła PrintWriter w przypadku pisania danych odpowiedzi opartych na znakach:

public PrintWriter ServletResponse.getWriter() throws IOException


Program wykonujący zapis koduje znaki zgodnie z zestawem znaków, podanym w typie treści. Jeżeli nie został
określony żaden zestaw znaków, tak jak to najczęściej ma miejsce, program wykonujący zapis używa kodowania
ISO-8859-1 (Latin-1) właściwego dla języków zachodnio-europejskich. Zestawy znaków zostały omówione
szczegółowo w rozdziale 13 „Internacjonalizacja”, na ten moment zapamiętajmy tylko, że dobrze jest zawsze
ustalić typ treści przed otrzymaniem PrintWriter. Metoda ta zgłasza wyjątek IllegalStateException —
w przypadku gdy metoda getOutputStream() została już wywołana do tej odpowiedzi; która to z kolei
metoda zgłasza wyjątek UnsupportedEncodingException jeżeli kodowanie strumienia wyjściowego jest nie
jest obsługiwane lub nieznane.

Poza możliwością wykorzystania PrintWriter w celu odesłania odpowiedzi, aplet może posłużyć się specjalną
podklasą java.io.OutoutStream, ażeby móc pisać dane binarne — mowa o ServletOutputStream, która
została zdefiniowana w javax.servlet. Klasę ServletOutputStream można uzyskać za pomocą metody
getOutputStream():

public ServletOutputStream ServletResponse.getOutputStream () throws IOException

Metoda ta odsyła ServletOutputStream dla pisania binarnych danych odpowiedzi. Nie jest w takim
przypadku wykonywane żadne kodowanie. Metoda ta zgłasza wyjątek IllegalStateeException jeżeli
getWriter() została już dla tej odpowiedzi wywołana.

Klasa ServletOputputStream przypomina standardową klasę Javy — PrintStream. W Interfejsie API 1.0,
klasa ta byłą używana dla całego strumienia wyjściowego apletu (zarówno tekstowego jak i binarnego). Jednakże
w wersji 2.0 Interfejsu API oraz późniejszych, została ograniczona tylko do obsługi danych binarnych. Będąc
bezpośrednią podklasą OutputStream, klasa ta zapewnia dostęp do metod klasy OutputStream takich jak:
write(), flush() oraz close().Poza powyższymi metodami klasa dodaje również swoje własne print() i
println() klasy ServletOutputStream, umożliwiające pisanie większości elementarnych Javowskich typów
danych (w celu uzyskania kompletnego zestawienia patrz Uzupełnienie A: „Interfejs API — charakterystyka
ogólna”). Jedyna różnica pomiędzy interfejsem ServletOutputStream a interfejsem klasy PrintStream, jest
taka, że metody print() oraz println() klasy servletOutputStream nie mogą (nie jest dokładnie jasne
dlaczego) drukować bezpośrednio parametrów typu Object czy char[].

Korzystanie ze połączeń stałych


Połączenia stałe (persistent connections) mogą zostać wykorzystane w celu optymalizacji sposobu, w jaki aplet
odsyła treść do klienta. Aby zrozumieć na czym polega taka optymalizacja musimy najpierw zapoznać się z
działaniem połączeń HTTP. Będziemy je poznawali możliwie najmniej szczegółowo, tak tylko aby poznać
ogólną zasadę działania. Problem jest omówiony dogłębnie w książce Clintona Wong’a „HTTP Pocket
Refference”.

Kiedy klient np. przeglądarka, chce złożyć do serwera zlecenie na określony dokument sieci WWW, rozpoczyna
poprzez ustanowienie połączenia do portu serwera. Właśnie poprzez to połączenie klient składa swoje zlecenie
oraz otrzymuje odpowiedź serwera. Klient daje znać, że jego zlecenie zostało zakończone przesyłając czystą
linię; serwer zaś, żeby poinformować, że odpowiedź została zakończona przerywa połączenie poprzez port.

Na powyższym etapie wszystko przebiega bez zakłóceń, lecz zastanówmy się jak wyglądałaby sytuacja, kiedy
wyszukana strona zawierałaby znaczniki <IMG> lub znaczniki <APPLET >, które obligują klienta do odczytania
większej ilości treści z serwera? W takim przypadku tworzone jest jeszcze jedno połączenie portowe. Jeżeli
strona zawiera 10 znaków graficznych wraz z apletem składającym się z 25 klas, daje to razem 36 połączeń
potrzebnych do przesłania strony. Wobec takich utrudnień nie dziwi zdenerwowanie internautów długimi
czasami oczekiwań w Internecie. Taką sytuacją można porównać do zamawiania pizzy poprzez składanie
zamówienia na każdą jej warstwę.
Znacznie lepszym rozwiązaniem jest wykorzystywanie jednego połączenia do pobrania więcej niż jednej strony
— metoda zwana trwałym połączeniem. Problem z trwałym połączeniem polega na tym, że klient i serwer muszą
jakoś uzgodnić gdzie zaczyna się odpowiedź serwera a gdzie zaczyna się następne zlecenie klienta.
Rozwiązaniem, które się nasuwa jest takie, że można by wykorzystać w tym celu jakiś element składniowy taki
jak np. czysta linia. Co by się jednak stało gdyby już sama odpowiedź zawierała czystą linię? Idea trwałego
połączenia polega na tym, że serwer informuje klienta o tym jakie rozmiary będzie miał korpus odpowiedzi,
ustalając nagłówek Content-Length jako część odpowiedzi. Klient wie dzięki temu, że jeżeli wszystko będzie
zgodne będzie miał znowu kontrolę nad połączeniem.

Większość serwerów wewnętrznie zarządza nagłówkiem Content-Length dla plików statycznych, które są
przez nie podawane; nagłówek Content-Length jest ustanawiany aby dostosować długość pliku. Aby określić
długość treści generowanego przez serwer wydruku wyjściowego, serwer korzysta z pomocy apletu. Aplet może
ustanowić odpowiedź Content-Length i zyskać w ten sposób korzyści płynące ze stosowania trwałego
połączenia dla treści dynamicznej poprzez użycie metody setContentLength():

public void ServletResponse.setContentLength(int len)

Metoda ta ustala długość (w bitach) treści odsyłanej przez serwer. W przypadku apletu HTTP, metoda ustanawia
nagłówek HTTP Content-Length. Zauważmy, że stosowanie tej metody jest opcjonalne. Jeżeli jednak
zdecydujemy się na jej zastosowanie, nasze aplety będą mogły czerpać korzyści płynącyce ze stosowania
trwałych połączeń, jeżeli takie się pojawią. Klient będzie również w stanie wyświetlać dokładne monitorowanie
stopnia bieżącego zaawansowania podczas ładowania.

Jeżeli wywołujemy metodę setContentLength(),musimy pamiętać o dwóch bardzo ważnych sprawach: aplet
musi wywołać tą metodę przed wysłaniem korpusu odpowiedzi oraz o tym, że podana długość musi być
dokładnie odwzorowana. W razie najmniejszej rozbieżności (nawet o jeden bit) musimy liczyć się z
potencjalnymi problemami.

Buforowanie odpowiedzi
Począwszy od wersji 2.2 Interfejsu API aplety maja kontrolę nad tym czy serwer buforuje swoją odpowiedź czy
nie oraz mogą mieć wpływ na wielkość bufora używanego przez serwer. W poprzednich wersjach API większość
serwerów wdrażała buforowanie odpowiedzi jako sposób na poprawienie wydajności; wielkość bufora była
determinowana przez serwer. Ogólnie rzecz biorąc serwery miały bufory wielkości około 8K.

Bufor pamięci pozwala apletowi na pisanie pewnej ilości wydruku wyjściowego z gwarancją, że odpowiedź nie
będzie od razu zatwierdzona. Jeżeli aplet wykryje błąd, kod statusu oraz nagłówki będą mogły być jeszcze
zmienione (do czasu opróżnienia bufora).

Buforowanie odpowiedzi jest również prostym sposobem uniknięcia stosunkowo trudnego szacowania długości
treści. Aplet może wykorzystać buforowanie aby automatycznie obliczyć długość treści, tak jak to zostało
zaprezentowane na przykładzie 5.2.

Przykład 5.2.
Aplet wykorzystujący buforowania do automatycznej obsługi trwałych połączeń

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class KeepAlive extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setBufferSize(8*1024); // 8K buffer

// Poproś o bufor 16k bitów; nie ustalaj długości treści


res.setBufferSize(16 * 1024);

PrintWriter out = res. getWriter () ;


out.println("<HTML>") ;
out.println("<HEAD><TITLE>Hello World</TITLEx/HEAD>) ;
out.println("<BODY>") ;
out.println("<BIG>Mniej niż 16 k korpusu odpowiedzi</BIG>") ;
out.println("</BODY></HTML>°) ;

Aplet wywołuje metodę setBufferSize() aby poprosić o minimum 16kB bufor odpowiedzi, a następnie
wysyła jak zwykle, swoją odpowiedź. Serwer wykorzystuje bufor wewnętrzny o pojemności co najmniej 16.384
bajtów aby przechować korpus odpowiedzi, po czym czeka z wysłaniem treści do klienta do momentu
wypełnienia bufora lub do czasu kiedy aplet poprosi o jego opróżnienie. Jeżeli cały korpus odpowiedzi mieści się
w buforze, serwer może (lecz nie musi) automatycznie ustawić nagłówek odpowiedzi Content-Length.

Trzeba wspomnieć, że buforowanie odpowiedzi związane jest z pewnymi kosztami.


Buforowanie całego wydruku wyjściowego i przesyłanie go w jednym wsadzie wymaga
dodatkowej pamięci i może opóźnić moment, w którym klient zacznie otrzymywać dane. Dla
apletów, wysyłających krótkie odpowiedzi trwałe połączenia są dobrym rozwiązaniem, lecz w
przypadku apletów z długimi odpowiedziami, niedogodności związane z dodatkową pamięcią
oraz opóźnienia w przesyłaniu danych mogą okazać się większe niż korzyści wynikające z
mniejszą liczbą połączeń.

Warto również zauważyć, iż nie wszystkie serwery i nie wszyscy klienci obsługują trwałe
połączenia. Oznacza to, iż dla apletu jest nadal ważne ustalanie swojej długości oraz
buforowanie swojego wydruku wyjściowego, tak żeby serwer mógł określić długość.
Informacja o długości treści będzie wykorzystywana przez te serwery i klientów, którzy
obsługują trwałe połączenia a ignorowana przez innych.

Regulowanie bufora odpowiedzi


Istnieje pięć metod w ServletResponse, które zapewniają kontrolę nad buforowaniem odpowiedzi. Aby
poinformować serwer o tym jaki minimalny rozmiar (podany w bajtach) bufora jest akceptowany przez nasz
aplet, możemy użyć metody setBufferSize():

Public void ServletResponse.setBufforSize(int size)


throws IllegalStateException

Serwer może zapewnić bufor większy niż żądany — może zdecydować, że bufory będą
utrzymywane w np. 8k blokach, w celu umożliwienia ponownego użytku. Większy bufor
pozwala na napisanie większej ilości treści zanim jeszcze cokolwiek zostanie wysłane, dając
tym samym apletowi więcej czasu na ustawienie kodów statusu oraz nagłówków. Mniejszy
bufor zmniejsza obciążenie pamięci serwera i pozwala na wcześniejsze rozpoczęcie
przesyłania danych do klienta. Metoda ta musi zostać wywołana zanim jeszcze zostanie
napisana jakakolwiek treść korpusu odpowiedzi; w przypadku gdy treść została już napisana
metoda ta zgłasza wyjątek IllegalStateException. W celu określenia rozmiaru bufora można
posłużyć się metodą getBufferSize():
public int ServletResponse.getBufferSize()

Metoda ta odsyła int informując o tym jak duży jest właściwie bieżący bufor, lub 0 — w (mało
prawdopodobnym) przypadku nie użycia żadnego buforowania.
Ażeby określić czy jakaś część zlecenia została już wysłana możemy wywołać metodę
isCommitted():

public boolean ServletResponse.isCommitted()

Jeżeli metoda ta odeśle true oznacza to, iż jest za późno aby zmienić kod statusu i nagłówki, a Content-
Length (długość treści) nie może zostać obliczona automatycznie.

Jeżeli musimy rozpocząć naszą odpowiedź od nowa, możemy wywołać metodę reset() Ażeby wyczyścić bufor
odpowiedzi, aktualnie przypisany kod statusu oraz nagłówki odpowiedzi:

public void ServletResponse.reset() throws IllegalStateException

Metoda ta musi zostać wywołana zanim jeszcze odpowiedź nie zostanie zatwierdzona, w przeciwnym wypadku
jest zgłaszany wyjątek IllegalStateException. Metody sendError() oraz sendRedirect() (które
zostaną omówione później) zachowują się podobnie i opróżniają bufor odpowiedzi, metody te jednak nie
modyfikują nagłówków odpowiedzi.

Możemy również wymusić zapisanie określonej treści do klienta w buforze, poprzez wywołanie metody
flushBuffer(), pozwalając tym samym na natychmiastowe rozpoczęcie przesyłania danych do klienta:

public void ServletResponse.flushBuffer() throws IOException

Wywołanie tej metody powoduje automatycznie zatwierdzenie odpowiedzi, oznacza to, iż kod
statusu oraz nagłówki zostaną napisane a reset() (restartowanie) nie będzie już możliwe.
Na przykładzie 5.3 pokazano aplet, który wykorzystuje metodę reset() aby napisać a następnie wyzerować
treść. Aplet drukuje domyślny rozmiar bufora do rejestru zdarzeń, zamiast do klienta, w ten sposób nie możliwe
jest wyzerowanie (za pomocą metody reset()).

Przykład 5.3.
Zarządzanie buforem odpowiedzi

import javax.servlet.* ;
import javax.servlet.http.*;
import java.io.*;

public class Buffering extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

res.setBufferSize(8 * 1024); // 8-kilowy bufor


res. setContentType (" tekst/html") ;
PrintWriter out = res.getWriter();

int size = res.getBufferSize(); // odsyła 8096 lub większy


// Zapisz rozmiar domyślny, w rejestrze zdarzeń
log("Domyślny rozmiar bufora to " + rozmiar);

out.println("Klient nie będzie tego widział");


res.reset();
out.println("Tego też nie będzie widział klient!");
res.reset() ;
out.println("A tego nie będzie można zobaczyć jeżeli zostanie
wywołana metoda sendError()");
if (req.getParameter("ważny_parametr") == null) {
res. sendError (res. SC_BAD_REQUEST, " ważny_parametr potrzebny") ;
}
}
}

Kody statusu
Do chwili obecnej nasze przykłady apletów nie ustalały kodów HTTP statusu odpowiedzi.
Korzystaliśmy z tego, że jeżeli aplet nie ustala kodu statusu, serwer wykonuje „krok do
przodu” i ustala swoją wartość na domyślny kod statusu 200 OK. Jest to duża dogodność
jeżeli odsyłamy standardowe, zwykle zakończone sukcesem odpowiedzi. Jednakże używając
kod statusu aplet może zrobić ze swoją odpowiedzią więcej. Dla przykładu może on
przekierować zlecenie lub zawiadomić o błędzie.

Najczęściej spotykane liczby kodu statusu są określane jako stałe skrótów mnemonicznych (pola danych public
final static int) w klasie HttpServletResponse. Parę z nich zostało wyszczególnionych w tabeli 5.1.
Kompletna listę można znaleźć w Dodatku D „Kody Statusu HTTP”.
Tabela 5.1.
Kody statusu HTTP
Stała skrótu Kod Komunikat Znaczenie
mnemonicznego domyślny
S.C._OK. 200 OK Zlecenie klienta zakończyło się sukcesem, odpowiedź serwera zawiera
żądane dane. Jest to domyślny kod statusu.
S.C._NO_CONTE 204 Nie ma treści Zlecenie zakończyło się sukcesem, lecz nie było żadnego korpusu
NT odpowiedzi do odesłania. Przeglądarki, które otrzymają ten kod powinny
zachować swój aktualny widok dokumentu. Kod ten jest bardzo przydatny
apletowi, kiedy przyjmuje dane z formularza, lecz chce aby widok
przeglądarki pozostał na formularzu, ponieważ unika komunikatu błędu
„Dokument nie zawiera żadnych danych”.
S.C._MOVED_PE 301 Stale Zamawiany zasób ma stale przenoszone miejsce lokalizacji. Przyszłe
RMANENTLY przenoszone odwołania powinny użyć w zleceniu nowy URL. Nowa lokalizacja podawana
jest przez nagłówek Location. Większość przeglądarek automatycznie
ustanawia połączenie z nową lokalizacją.
S.C._MOVED_TE 302 Czasowo Zamawiany zasób ma tymczasowo zmienioną lokalizację, jednak przyszłe
MPORARILY przeniesione odniesienia powinny nadal używać oryginalnego URL-u, aby ustanowić
połączenie z nową lokalizacją.
S.C._UNAUTHOR 401 Nieupoważni Zleceniu brak było właściwego upoważnienia. Używane razem z
IZED ono nagłówkami WWW-Authenticate i Authorization
S.C._NOT_FOUN 404 Nie znaleziono Zamówiony zasób nie został odnaleziony lub jest niedostępny
D
S.C._INTERNAL 500 Błąd Na serwerze zdarzył się nieoczeki wany błąd, który uniemożliwił mu
_SERVER_ERROR wewnętrzny wykonanie zlecenia.
serwera
S.C._NOT_IMPL 501 Nie Serwer nie obsługuje zestawu funkcji potrzebnych do zrealizowania zlecenia.
EMENTED wprowadzono
S.C._SERVICE_ 503 Obsługa Obsługa (serwer) jest tymczasowo niedostępna, lecz powinna być dostępna w
UNAVAILABLE niedostępna niedalekiej przyszłości. Jeżeli serwer wie kiedy wznowi obsługę, nagłówek
Retry-After może zostać użyty

Ustanawianie kodu statusu


Aby kod statusu odpowiedzi aplet może skorzystać z metody setStatus():

public void HttpServletResponse. setStatus (int sc)

Metoda ta ustala kod HTTP statusu dla danej wartości. Kod może zostać określony jako liczba, lub za pomocą
jednego z kodów S.C._XXX określonych w HttpServletResponse. Pamiętajmy, że metoda ta musi zostać
wywołana przed zatwierdzeniem odpowiedzi, w przeciwnym wypadku wywołanie będzie zignorowane.
Jeżeli aplet ustala kod statusu, który informuje o błędzie w czasie obsługi zlecenia, zamiast setStatus()może
on wywołać metodę sendError():

public void HttpServletResponse.sendError(int sc)


throws IQException, IllegalStateException
public void HttpServletResponse.sendError(intsc, String sm)
throws IOException, IllegalStateException

Metoda sendError() powoduje, że serwer generuje i przesyła właściwą, serwerowo-specyficzną stronę


opisującą błąd, pozwalając na to aby apletowa strona błędu miała podobny wygląd do innych stron błędu
serwera. Wywoływanie metody setStatus() do błędu obliguje serwer do wygenerowania strony błędu. Jeżeli
zastosowana zostanie dwu-argumentowa wersja tej metody, wtedy parametr komunikatu statusu może zostać
umieszczony bezpośrednio w korpusie odpowiedzi, w zależności od wdrożenia serwera. Metoda ta powinna
zostać wywołana zanim jeszcze odpowiedź zostanie zatwierdzona, w przeciwnym razie zgłoszony zostanie
wyjątek IllegalStateException. Metoda ta wykonuje niejawne restartowanie bufora odpowiedzi przed
generowaniem strony błędu. Nagłówki ustalone przed sendError(), pozostają nadal ustalone.

Ulepszanie apletu „ViewFile” przy pomocy kodów statusu


Jak dotąd nie zadawaliśmy sobie trudu wywołania jakiejkolwiek z wyżej opisanych metod w
celu ustalenia kodu statusu odpowiedzi. Korzystaliśmy po prostu z tego, że kod statusu
ustalany jest domyślnie jako S.C._OK. Niestety czasem zdarzają się wypadki kiedy aplet musi
odesłać odpowiedź, która nie ma kodu statusu S.C._OK. — sytuacje kiedy odpowiedź nie
zawiera zamówionych danych. Jako przykład przypomnijmy sobie jak aplet ViewFile, w
rozdziale 4 „Odczytywanie informacji” FileNotFoundException:
// Odeślij plik
try {
ServletUtils.returnFile(file, out);
}
catch (FileNotFoundException e) {

out.println("Plik nie odnaleziony");


}

Bez ustalenia kodu statusu, jedyną rzeczą, którą serwer może zrobić jest napisanie wyjaśnienia problemu,
przesyłając to wyjaśnienie jako, na ironię, część strony, która miała zawierać treści pliku. Jednakże stosując kody
statusu, aplet może wykonać dokładnie to co robi DefaultServlet: ustawić kod odpowiedzi na
S.C._NOT_FOUND, w celu zasygnalizowania, że plik będący przedmiotem zlecenia nie został odnaleziony i nie
może zostać odesłany. Oto ulepszona wersja apletu:

// Odeślij plik
try {
ServletUtils.returnFile(file, out);
}
catch (FileNotFoundException e) {
res.sendError(res.SC_NOT_FOUND) ;
}

Wygląd strony wygenerowanej przez wywołanie metody sendError() jest determinowany przez serwer, jednak
bardzo przypomina standardową stronę błędu serwera. Dla serwera „Apache/Tomcat” generowana jest jego
własna strona 404 NotFound, razem z jego stopką redakcyjną (tak jak to zostało pokazane na rysunku 5.1).
Zwróćmy uwagę, iż strona jest identyczna z każdą inną stroną 404NotFound Apache’a, co pozwala apletowi na
współdziałanie z serwerem.
Rysunek 5.1.
Strona „404NotFound” serwera „Apache/Tomcat”

Nagłówki HTTP
Aplety mogą w celu dostarczenia dodatkowych informacji o swojej odpowiedzi ustalić
nagłówki HTTP. Tak jak to wyjaśniliśmy w rozdziale 4, pełne omówienie wszystkich
możliwych nagłówków HTTP 1.0 i HTTP 1.1 wybiega poza zakres tej książki. Na tabeli 5.2
wyszczególniono nagłówki HTTP — ustawiane najczęściej przez aplety jako część
odpowiedzi.

Tabela 5.2.
Nagłówki HTTP odpowiedzi
Nagłówek Zastosowanie
Cache-Control Określa ewentualny specjalny sposób, w jaki buforowanie zewnętrzne
powinno traktować ten dokument. Najbardziej popularne wartości to no-cache (służąca do
poinformowania, że ten dokument nie powinien być buforowany), no-store (służąca do
poinformowania, że ten dokument nie powinien być -buforowany ani nawet przechowywany przez
serwer pośredniczący, zwykle z powodu niejawnego charakteru jego treści) oraz max-
age=seconds (służąca do określania ile czasu upłynąć, żeby dokument został uznany jako
„przestarzały”). Nagłówek ten został wprowadzony w HTTP 1.1.
Pragma Jest to odpowiednik HTTP 1.0 dla Cache-Control, z wartością no-cache jako jedyną możliwą
wartością. Szczególnie korzystne jest zastosowanie Pragma w połączenui z Cache-
Control, pozwala na obsługę przeglądarek starszych typów.
Connection Nagłówek ten jest używany do poinformowania o tym czy serwer wyraża chęć utrzymywania
otwartego (trwałego) połączenia z klientem. W przypadku gdy serwer wyraża taką chęć, wartość
nagłówka ustawia na jest na keep-alive.W przeciwnym wypadku wartością tą będzie close.
Większość serwerów WWW obsługuje ten nagłówek w imieniu swoich apletów automatycznie
ustalając wartość nagłówka na keep-alive — w przypadku gdy aplet ustawia swój nagłówek
Content_Length lub kiedy serwer jest w stanie automatycznie określić długość treści.
Retry-After Nagłówek ten okresla moment, w którym serwer będzie ponownie mógł
obsługiwać zlecenia, jest on używany z kodem statusu
S.C._SERVICE_UNAVAILABLE. Jego wartość to int, które reprezentuje
liczbę sekund lub ciąg danych reprezentujący czas rzeczywisty.
Expires Określa czas, w którym dokument może się zmienić lub kiedy jego
informacje staną się nieważne. Tym samym implikuje, że jest rzeczą
niemożliwą aby dokument się zmienił przed upływem tego czasu.
Location Określa nową lokalizację dokumentu, stosowany zwykle z kodami statusu S.C._CREATED,
S.C._MOVED_PERMANENTLY oraz S.C._MOVED_TEMPORARILY. Jego wartością musi być w
pełni kwalifikowany URL (włącznie z „http://”)
WWW- Określa schemat autoryzacji dziedzinę autoryzacji wymaganą przez klienta podczas wywoływania
Authenticate URL-u będącego przedmiotem zlecenia. Używany z kodem statusu S.C._UNAUTHORIZED.
Content- Określa schemat użyty do kodowania korpusu odpowiedzi. Przykładowe
Encoding wartości to gzip (lub x-gzip) compress (lub x-compress). Wartości
wielokrotne powinny być przedstawiane jako oddzielone przecinkami
listy, w kolejności w jakiej kodowania były stosowane do danych.
a
Programy Netscape Navigator oraz Microsoft Internet Explorer są obarczone błędami,
które sprawiają, że nagłówek ten nie zawsze jest wykonywany. Kiedy chcemy mieć
pewność, że wydruk wyjściowy nie będzie buforowany każde zlecenie kierowane do
apletu powinno wykorzystywać trochę inny URL, np. z dodatkiem pewnej ilości
informacji parametrów drugorzędnych

Ustawianie nagłówków HTTP


Klasa HttpServletResponse daje dostęp do wielu metod, pomagających apletom w ustalaniu nagłówków
HTTP odpowiedzi. Aby ustalić wartość nagłówka używamy metody setHeader():

public void HttpServletResponse.setHeader(String name, String value)

Metoda ta ustala wartość nagłówka nazwanego, jako String. Nazwa nie uwzględnia wielkości liter, tak jak ma
to miejsce w przypadku tego typu metod. Kiedy nagłówek został już uprzednio ustalony, nowa wartość
zapisywana jest w miejsce starej. Przy pomocy tej metody mogą być ustalane nagłówki wszystkich typów.
Jeżeli musimy określić znacznik czasu dla nagłówka, możemy wykorzystać w tym celu metodę setDateHeader
():

public void HttpServletResponse. setDateHeader (String name, long date)

Metoda ta ustala wartość nagłówka nazwanego na określoną datę i czas. Metoda akceptuje wartość daty jako
long, reprezentujące liczbę milisekund, jaka upłynęła od „epoki” (od północy 1 stycznia, 1970 roku, GMT).
Jeżeli nagłówek został już wcześniej ustalony, nowa wartość jest zapisywana w miejsce poprzedniej.

I wreszcie możemy użyć metody setIntHeader() celu określenia wartości całkowitej dla nagłówka:

public void HttpServletResponse.setIntHeader(String name, int value)

Metoda ta ustala wartość nagłówka nazwanego na int. Jeżeli nagłówek został już uprzednio ustalony stara
wartość jest zastępowana przez nową.

Metoda containsHeader() demonstruje sposób, w jaki można sprawdzić czy nagłówek już istnieje:

public boolean HttpServletResponse.containsHeader(String name)

Metoda ta odsyła true w przypadku gdy nagłówek nazwany został już ustalony, w przeciwnym razie metoda ta
odsyła false.

Dla względnie małej liczby przypadków kiedy to aplet musi odesłać wartości wielokrotne dla tego samego
nagłówka, stosuje się dodatkowe metody.

public void HttpServletResponse.addHeader(String name, String value)


public void HttpServletResponse.addDateHeader (String name, long value)
public void HttpServletResponse.addIntHeader(String name, int value)
Metody te ustalają nagłówki na daną wartość, lecz podczas gdy tradycyjna metoda setHeader() zamieniłaby
jakąkolwiek istniejącą wartość lub wartości, metoda addHeader() pozostawia dotychczasowe ustawienia bez
zmian, ustalając tylko wartość dodatkową.

I w końcu, w specyfikacji HTML 3.2 został przedstawiony alternatywny sposób ustalania wartości nagłówków,
zakładający użycie znacznika <META HTTP_EQUIV> wewnątrz samej strony HTML:

<META HTTP-EQUIV="nazwa" CONTENT=" wartość ">

Znacznik ten musi zostać przesłany jako część sekcji <HEAD> strony HTML. Powyższa technika nie przynosi
jakichś specjalnych korzyści dla apletów; została ona stworzona z myślą o dokumentach statycznych, które nie
mają dostępu do swoich nagłówków.

Przekierowywanie zlecenia
Jednym z pożytecznych zastosowań, do których aplet może wykorzystać kody statusu i nagłówki jest
przekierowywanie zleceń. Jest to realizowane poprzez przesłanie do klienta instrukcji, zalecających użycie
innego URL-u w odpowiedzi. Przekierowanie generalnie stosuje się kiedy dokument jest przemieszczany
(wysyłanie klienta do nowej lokalizacji), dla wyrównywania obciążenia (tak że jeden URL może rozprowadzać
obciążenia do kilku różnych komputerów) lub przy prostej randomizacji (przy losowym wybieraniu miejsca
przeznaczenia).

Na przykładzie 5.4 został pokazany aplet, który wykonuje proste losowe przekierowanie,
wysyłając klienta do strony wybranej losowo z jego listy stron. Bazując na liście stron, aplet
podobny do poniższego mógłby mieć wiele zastosowań. W stanie obecnym jest to punkt
wyjściowy do selekcji ciekawych stron apletów. W przypadku listy stron zawierającej obrazki
reklamowe, aplet ten może zostać użyty do przechodzenia do następnego paska reklamowego.

Przykład 5.4.
Losowa zwrotnica

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SiteSelector extends HttpServlet {

Vector sites = new Vector();


Random random=new Random();

public void init() throws ServletException {


sites.addElement("http://www.oreilly.com/catalog/jservlet2");
sites.addElement("http://www.servlets.com");
sites.addElement("http://Java.sun.com/products/servlet");
sites.addElement("http://www.newlnstance.com");
}
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("tekst/html");
PrintWriter out = res.getWriter() ;
int sitelndex = Math. abs( random, nextInt ()) % sites. size();
String site = (String)sites.elementAt(sitelndex);

res.setStatus(res.SC_MOVED_TEMPORARILY) ;
res.setHeader("Lokalizacja",site);
}
}
Rzeczywiste przekierowanie jest realizowane w dwóch wierszach:

res.setStatus(res.SC_MOVED_TEMPORARILY) ;
res.setHeader("Lokalizacja", site);

Pierwszy wiersz ustala kod statusu, aby poinformować, że ma zostać wykonane przekierowanie, w drugim zaś
wierszu podana jest nowa lokalizacja. Aby mieć pewność, że metody te będą działać musimy wywołać je zanim
jeszcze zostanie zatwierdzona odpowiedź. Pamiętajmy, że protokół HTTP przesyła kody statusu oraz nagłówki
przed bryłą treści. Również protokół HTTP określający nową stronę musi zostać podany jako URL bezwzględny
(np. http://server:port/path/file.html). Jakikolwiek brak w podobnym URL-u może spowodować dezorientację
klienta.

Wyżej wspomniane dwa wiersze mogą zostać uproszczone do jednego przy użyciu metody złożonej z metod
podstawowych: sendRedirect():

public void HttpServletResponse.sendRedirect(String location)


throws IOException, IllegalStateException

Dla potrzeb naszego przykładu powyższe da wiersze upraszczane są do:

res.sendRedirect(site);

Metoda ta przekierowuje odpowiedź do określonej lokalizacji, automatycznie ustalając kod statusu oraz
nagłówek Location. Poza obsługą klientów nie wyposażonych w zdolność przekierowania czy tych nie
rozpoznających kodu statusu S.C._MOVED_TEMPORARILY, metoda ta pisze krótki korpus odpowiedzi
zawierający hiperłącze do nowej lokalizacji. Tak więc kiedy używamy tej metody nie piszemy własnego korpusu
odpowiedzi. Metoda sendRedirect() może, począwszy od Interfejsu API 2.2 akceptować URL względny.
Specyfikacja HTTP „mówi”, że wszystkie przekierowywane URL-e muszą być bezwzględne; metoda ta jednak
przekształca URL-e względne na formę bezwzględną (automatycznie protokół bieżący, serwer oraz port — lecz
nie ścieżkę kontekstu, w razie konieczności należy to zrobić we własnym zakresie) zanim prześle go do klienta.
Prawda, że to proste? Pamiętajmy, że metodę tą należy wywołać przed zatwierdzeniem odpowiedzi, w
przeciwnym wypadku zostanie zgłoszony wyjątek IllegalStateException. Metoda ta wykonuje niejawne
zerowanie buforu odpowiedzi przed generowaniem przekierowywanej strony. Nagłówki ustalone przed
sendRedirect() nadal pozostają ustalone.

Nadzorowanie łączników do innych stron


Przekierowywanie może być również wykorzystane do zdobycia informacji na temat tego z jaką lokalizacją łączą
się klienci po opuszczeniu naszej strony. Załóżmy, że mamy kilka stron zawierających listy łączników do innych
stron. zamiast łączenia bezpośrednio z zewnętrznymi stronami, możemy połączyć się z apletem
przekierowywującym, który może zarejestrować każdorazowy wybór zewnętrznego łącznika. HTML wygląda
wtedy w następujący sposób:

<a href="/goto/http://www.servlets.com">Servlets.com</a>

Aplet może być zarejestrowany ażeby obsługiwać przedrostek ścieżki /goto/*, gdzie uzyska wybrany URL jako
informację dodatkowej ścieżki, przekierowując następnie klienta do lokalizacji po uprzednim zapisaniu adnotacji
w rejestrze zdarzeń serwera. Kod apletu GoTo został zaprezentowany na przykładzie 5.5.

Przykład 5.5
Jak myślisz dokąd się udajesz?”
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class GoTo extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
// Określ stronę do której chcą się udać
String site = req.getPathInfo();
String query = req.getQueryString();

// Obsłuż błędne zlecenie


if (site == null) {

res.sendError(res.SC_BAD_REQUEST, "Wymagane informacje


dodatkowej ścieżki");

// Odetnij interlinię "/" oraz dodaj ciąg zapytań


// Zakładamy, że ścieżka informacji URL jest zawsze bezwzględna
String url=site.substring(1)+(query==null ?"" : "?" +
query");

// Zaloguj URl będący przedmiotem zlecenia i przekieruj


log (url); //lub pisz do pliku specjalnego
res.sendRedirect(url);
}
}
Aplet podobny do omawianego powyżej jest w stanie obsługiwać aliasing, tak że /got/servlets automatycznie
przekierowywałoby do http://www.servlets.com. Informacje dotyczące sprzedaży i dystrybucji podobnych
zamienników można znaleźć na stronie http://go.to oraz na http://i.am.

Klienckie ściąganie z serwera


Klienckie ściąganie z serwra przypomina przekierowywanie, istnieje jednak, jedna istotna różnica: przeglądarka
wyświetla w danym momencie treść z pierwszej strony, a następnie czeka pewien czas na pobranie i wyświetlenie
treści następnej strony. Nazwa brzmi klienckie ściąganie z serwera ponieważ to klient jest odpowiedzialny za
ściąganie treści następnej strony.

Ktoś mógłby zapytać: „czy jest to metoda skuteczna?” Odpowiedź brzmi: „tak”. Po pierwsze, treść pierwszej
strony może wyjaśnić klientowi, że strona będąca przedmiotem zlecenia została przeniesiona przed
automatycznym załadowaniem strony następnej. Po drugie, strona może być pobierana w sekwencji,
umożliwiając tym samym powolną animacje strony.

Informacje klienckiego ściąganie z serwera są przesyłane do klienta przy użyciu nagłówka HTTP Refresh.
Wartość tego nagłówka określa liczbę sekund do wyświetlenia strony, przed ściągnięciem następnej, nagłówek
ten zawiera dodatkowo ciąg URL, określający z którego URL-u ma zostać przeprowadzone ściąganie. W
przypadku gdy żaden URL nie jest podany używany jest ten sam. Poniżej przedstawiamy wywołanie do metody
setHeader(), które informuje klienta, o tym, że ma on powtórnie załadować ten sam aplet, po uprzednim
pokazaniu przez 3 sekundy swojej treści:

setHeader("Odśwież", "3");

A oto wywołanie, które „mówi” klientowi, że ma wyświetlić stronę główną Netscape’a po trzech sekundach:

setHeader("Odśwież", "3; URL=http://home.netscape.com");

Przykład 5.6 ukazuje aplet, który wykorzystuje klienckie ściąganie z serwera w celu wyświetlenia czasu
rzeczywistego, uaktualnianego co 10 sekund.
Przykład 5.6.
Czas rzeczywisty uaktualniany

import java.io.*;
import java.util.*;
import javax.servlet.* ;
import javax.servlet.http.*;

public class ClientPull extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res. setContentType (" tekst/zwykły") ;
PrintWriter out = res.getWriter() ;

res.setHeader("Odśwież", "10");
out.println(new Date().toString()) ;
}
}
Jest to przykład animacji opartej na tekście — animacje graficzne omówimy w następnym rozdziale. Zwróćmy
uwagę, iż nagłówek Refresh nie powtarza się. Nie ma konieczności aby ładować dokument w sposób
powtarzający się. Nagłówek Refresh jest jednakże określany przy każdym pobraniu, tworząc wyświetlanie
ciągłe.
Wykorzystanie klienckiego ściągania z serwera w celu pobrania drugiego dokmentu zostało zaprezentowane na
przykładzie 5.7. Aplet ten przekierowuje zlecenia kierowane do jednego komputera centralnego, na inny
komputer centralny, podając klientowi wyjaśnienie przed wykonaniem wspomnianego przekierowania.

Przykład 5.7.
„Wyjaśniona” zamiana komputera centralnego

import java.io. *;
import java.util.*;
import javax.servlet.* ;
import javax.servlet.http.* ;

public class ClientPullMove extends HttpServlet {

static final String NEW_HOST = "http://www.oreilly.com";


public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();
String newLocation = NEW_HOST + req.getRequestURI();

res.setHeader("Odśwież", "10; URL=" + newLocation) ;

out.println("URL będący przedmiotem zleceni został przekazany do


innego komputera centralnego.<BR>");
out.println("Jego nowa lokalizacja to " + newLocation + "<BR>");
out.println(" Twoja przeglądarka połączy Cię z tym miejscem za 10
sekund.");
}
}
Aplet ten generuje nową lokalizację z URI będącego przedmiotem zlecenia, które pozwala na przekierowywanie
wszystkich zleceń złożonych na stary serwer, na tą samą ścieżkę na nowym serwerze. Przy użyciu deskryptora
wdrożenia web.xml, aplet ten mógłby być skonfigurowany do obsługi każdego zlecenia, stopniowo przekazując
klientów do nowej lokalizacji.

Rozwiązywanie problemów
Nadszedł czas abyśmy zmierzyli się ze sprawami najtrudniejszymi — błędami, problemami, awariami. Jest wiele
przyczyn występowania problemów: nieprawidłowe parametry, brakujące zasoby i bieżące błędy. To co jest na
ten moment istotne, to to, że aplet musi być przygotowany na problemy i to zarówno na przewidywalne jak i na
te, których przewidzieć nie sposób. Dwie najważniejsze sprawy podczas występowania problemów to:
• Ograniczenie uszkodzeń serwera
• Właściwe poinformowanie klienta
Ponieważ aplety są pisane w Javie, potencjalne uszkodzenia serwera są minimalne. Serwer
może bezpiecznie osadzić aplety (nawet w swojej formalnej procedurze postępowania), tak
jak to może równie bezpiecznie zrobić przeglądarka WWW z załadowanymi apletami.
Bezpieczeństwo to oparte jest na właściwościach bezpieczeństwa Javy, takich jak np.
zastosowanie zabezpieczonej pamięci, obsług wyjątków czy programy zarządzanie
bezpieczeństwem. Javowska ochrona pamięci gwarantuje, że aplety nie mogą przypadkowo
(lub celowo) uzyskać dostępu do organizacji wewnętrznej serwera. Javowska obsługa wyjątku
pozwala serwerowi na wychwycenie każdego wyjątku zgłoszonego przez aplet. Nawet kiedy
aplet przypadkowo podzieli przez zero lub wywoła metodę na obiekt null, serwer może dalej
sprawnie działać. Mechanizm zarządzania bezpieczeństwem Javy umożliwia serwerom
umieszczania niewiarygodnych apletów w „piaskownicy”, ograniczając ich możliwości i nie
dopuszczając aby powodowały problemy.

Należy pamiętać, że wiarygodne aplety wywołujące poza „piaskownicą” mechanizmu zarządzania


bezpieczeństwem mają możliwości, które mogą potencjalnie wywołać szkody na serwerze. Dla przykładu aplet
może zamazać obszar pliku serwera lub nawet wywołać metodę System.exit(). Jest również prawdą, że aplet
nie powinien nigdy spowodować szkód (chyba, że przypadkowo, co jest raczej trudne w przypadku
wywoływania metody System.exit()). Jednak, jeżeli jest taka potrzeba, nawet aplety wiarygodne mogą być (i
często są) uruchamiane wewnątrz całkiem „łagodnego” lecz sprawdzającego poprawność programu zarządzania
bezpieczeństwem.
Właściwe opisanie problemu klientowi, nie może być wyłącznie pozostawione technologii języka Java. Istnieje
wiele spraw, które trzeba wsiąść pod uwagę, takich jak np.:
Jak wiele powiedzieć klientowi?
Czy aplet powinien przesłać standardowy kod statusu strony błędu, proste wyjaśnienie błędu czy też (w
przypadku zgłoszonego wyjątku) szczegółowy ślad kaskady? A co kiedy aplet ma odesłać nie-tekstową treść,
taką jak np. obrazek?
Jak zarejestrować problem?
Czy powinien on zostać zapisany w pliku? czy do rejestru zdarzeń serwera, ma być przesłany do klienta czy też
zignorowany?
Jak pozbyć się problemu?
Czy ta sama kopia apletu może obsługiwać następujące po sobie zlecenia? Lub czy serwer jest uszkodzony i
trzeba go powtórnie załadować?
Odpowiedzi na powyższe pytania zależą od apletu oraz zastosowania dla którego został on
zaprojektowany, powinny one być kierowane do każdego apletu na zasadzie „przypadek po
przypadku”. Sposób w jaki poradzimy sobie z błędami zależy od nas samych i powinien być
oparty na poziomie niezawodności i odporności na błędy, wymaganymi dla naszego apletu.
Kolejną kwestią, której poświęcimy naszą uwagę jest ogólne omówienie mechanizmów
obsługi błędu apletu, których używamy w celu wdrożenia jakiejkolwiek ze strategii.

Kody statusu
Najprostszym (i prawdopodobnie najlepszym) sposobem, w który aplet może powiadomić o błędzie jest
wykorzystanie metody sendError(), aby ustawić poprawnie 400 lub 500 szeregów kodu statusu. Dla
przykładu, w przypadku gdy do apletu zostanie złożone zlecenie na plik, który nie istnieje, może on odesłać
S.C._NOT_FOUND. Kiedy od apletu wymaga się czegoś, co przekracza jego możliwości odsyła on
S.C._NOT_IMPLEMENTED. W razie wystąpienia okoliczności całkowicie nieprzewidywalnych, aplet może
odesłać S.C._INTERNAL_SERVER_ERROR.
Poprzez zastosowanie metody sendError() w celu ustalenia kodu statusu, serwer może zamienić korpus
odpowiedzi apletu, serwerowo-specyficzną stroną wyjaśniającą błąd. Jeżeli błąd ma taką naturę, iż aplet
powinien wystosować swoje własne wyjaśnienie do klienta w korpusie odpowiedzi, może on ustalić kod statusu
za pomocą metody setStatus() i następnie wysłać właściwy korpus odpowiedzi — który może być zarówno
tekstowym, wygenerowanym obrazkiem jak i też czymkolwiek właściwym.
Aplet musi uważać aby wyłapać i poradzić sobie z wszystkimi ewentualnymi błędami zanim jeszcze odpowiedź
zostanie zatwierdzona. Jak pamiętamy (wspominaliśmy o tym kilka razy), HTTP określa, że kod statusu oraz
nagłówki HTTP muszą zostać wysłane przed korpusem odpowiedzi. W momencie kiedy jakiekolwiek dane
zostały już przesłane do klienta, jest już za późno aby zmienić nasz kod statusu czy nagłówki HTTP. Najlepszym
sposobem uniknięcia takiej sytuacji jest jak najwcześniejsze sprawdzenie poprawności oraz zastosowanie
buforowania w celu opóźnieni przesłania danych do klienta.

Konfiguracja stron błędu


Może się zdarzyć, ze zamiast używania standardowych stron błędu serwera, będziemy chcieli utworzyć zestaw
standardowych stron błędu dla aplikacji WWW, stron które będą mogły być wykorzystywane bez względu na to
gdzie aplikacja zostanie zainstalowana. Dla przykładu aplikacja WWW może zostać skonfigurowana za pomocą
strony błędu 404, zawierającej pole wpisywania wyszukiwarki, w przypadku gdy użytkownik zechciałby
skorzystać z pomocy w lokalizacji zasobu NotFound. Możemy tego dokonać poprzez bezpośrednie użycie
metody setStatus() i spowodowanie, że każdy aplet będzie generował identyczną stronę błędu, jednakże
lepszym podejściem jest zainstalowanie reguły <error-page> w deskryptorze wdrożenia web.xml. Na
przykładzie 5.8 została zaprezentowana omawiana sytuacja.

Przykład 5.8.
Konfiguracja stron błędu 400 i 404

<?xml version="1.0" kodowanie="ISO-8859-l"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//Aplikacja WWW DTD 2.2//EM"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app>
<!--.....-->
<error-page>
<error-code>
400
</error-code>
<location>
/400.html
</location>
</error-page>
<error-page>
<error-code>
404
</error-code>
<location>
/404.html
</location>
</error-page>
</web-app>

Powyższe dwa pola wpisywania <error-page> „mówią” serwerowi, że jakiekolwiek wywołanie do metody
sendError() z kodem statusu 400 powinno wyświetlić treści z zasobu /400.html oraz, że jakiekolwiek
wywołanie do tej metody z kodem 404 powinno wyświetlić zasób /404.html. Obsługa adaptacji stron błędu
jest praktyką powszechnie stosowana przez serwery WWW, jednakże pola wpisywania w pliku web.xml
zastępują domyślną konfiguracji serwera i udostępniają mechanizm, umożliwiający aplikacjom WWW
dopuszczanie standardowych stron błędu do użytku we wszystkich wdrożeniach serwerowych. Serwery są
zobligowane to tego by przestrzegać zasad <error-page> dla wszystkich treści podawanych z aplikacji WWW,
nawet dla plików statycznych.

Należy pamiętać, iż wartość <location>, która musi zaczynać się od ukośnika, jest traktowana jako
umiejscowiona w kontekstowym katalogu macierzystym i musi odwoływać się do zasobu w kontekście. Aby
odnieść się do zasobu poza bieżącym kontekstem, możemy wskazać na plik HTML wewnątrz kontekstu, który
zawiera bezpośrednie przekierowanie poza kontekstem:

<META HTTP-EQUIV="Odśwież" TREŚĆ"=0;

URL=http://www.errors.com/404.html">
Przeznaczenie <location> może być zasobem dynamicznym, takim jak aplet czy JSP. Dla zasobów
dynamicznych serwer udostępnia dwa specjalne atrybuty zlecenia, dostarczające informacji o błędzie:

javax.servlet.error.status_code
Integer (liczba całkowita) podająca kod statusu błędu. Typ był początkowo nie określony, dlatego
niektóre wczesne wdrożenia serwerowe mogą odsyłać kod jako String.

javax.servlet.error.message
String (strumień) podający komunikat statusu, na ogół przekazywany jako drugi
argument do metody sendError().

Powyższe atrybuty pozwalają nam na napisanie apletu wyświetlania strony błędu, ogólnego zastosowania,
zaprezentowane na przykładzie 5.9. W celu użycia tego apletu, należy przypisać go ścieżkę dla znacznika
<location>.

Przykład 5.9.
Dynamiczne tworzenie kodu statusu strony błędu

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ErrorDisplay extends HttpServlet{


public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("tekst /html") ;
PrintWriter out = res.getWriter();

Object codedbj =
req.getAttribute("javax.servlet.error.status_code");
Object messageObj = req. getAttribute
("javax.servlet.error.message");

// Kod i komunikat nigdy nie powinny być równe zero w przypadku


// apletów zgodnych API 2.2
String code = (codeObj != null ?
codeObj.toString() : "Brakujący kod statusu");
String message = (messageObj != null ?
messageObj . toString () : "Komunikat Brakującego Błędu") ;

out.println("<HTML>");
out.println("<HEAD><TITLE>" + kod + ": " + komunikat +
"</TITLE><HEAD>");
out.println("<BODY>");
out.println("<H1>" + kod + "</H1>");
out.println("<H2>" + komunikat + "</H2>");
out.println("<HR>");
out.println("<I>dostęo do błędu " + req.getRequestURI() + "</I>");
out.println (" < /BODY></HTML>") ;
}
}

Ścieżka do tego apletu może zostać skonfigurowana jako przeznaczenie dl wszystkich kodów statusu, które
potrzebują prostego tłumaczenia strony błędu. Bardziej zaawansowana wersja tego apletu byłaby w stanie śledzić
kody błędów generowane w celu wykrycia dziwnych tendencji, takich jak na przykład ilość błędów 404 dla tego
samego URI, wskazując z dużym praedopodobieństwem, nowo przesłany błędny łącznik gdzieś, gdzie powinien
być poprawiony.
Podsumowując jest to fałszywa strona dla przesyłania kodów statusu błędu. Metoda setStatus() pozwala na
ustalenie kodu statusu błędu przy zatrzymaniu pełnej kontroli nad odpowiedzią. Metoda sendError() pozwala
z kolei na ustalenie kodu statusu błędu, przy przekazaniu kontroli nad tworzeniem strony do serwera. Serwer
przesyła domyślnie swoją standardową stronę błędu dla tego kodu. Za pomocą pozycji <error-page> możemy
polecić serwerowi aby przesłał specjalną stronę błędu.

Rejestracja
Aplety mają zdolność zapisywania, czynności które wykonują oraz błędów, które popełniają, do pliku rejestru
zdarzeń. Używają w tym celu metody log():

public void GenericServlet. log (String msg)


public void GenericServlet.log(String msg, Throwable t)

Metoda jedno-argumentowa zapisuje dany komunikat w dzienniku zdarzeń apletu, którym jest
zwykle plik rejestru zdarzeń. Dwu-argumentowa wersja metody zapisuje dany komunikat oraz
ślad kaskady Throwable w dzienniku zdarzeń apletu. Dokładny format wydruku wyjściowego
oraz lokalizacja dziennika zdarzeń, są serwerowo-specyficzne, jednak na ogół zawierają
znacznik czasu oraz zarejestrowaną nazwę apletu.

Metoda log() wspomaga usuwanie błędów (debagowanie) poprzez zapewnianie sposobu śledzenia operacji
apletu. Zapewnia ona również możliwość zapisywania kompletnego opisu wszystkich błędów, z którymi miał do
czynienia aplet. Opis ten może być taki sam, jak ten przesłany do klienta lub bardziej wyczerpujący i
szczegółowy.

Teraz już możemy wrócić do dalszego ulepszania apletu ViewFile, tak że będzie używał metodę log() do
rejestrowania na serwerze w sytuacji kiedy plik będący przedmiotem zlecenia nie będzie istniał, odsyłając do
klienta prostą stronę 404NotFound:

// Odeślij plik
try {
ServletUtils.returnFile(file, out) ;
}
catch (FileNotFoundException e) {
log("Nie można było odnaleźć pliku : " + e.getMessage());
res.sendError(res.SC_NOT_FOUND) ;
}

W przypadku bardziej skomplikowanych błędów aplet może zarejestrować kompletny ślad kaskady, tak jak to
zostało przedstawione poniżej:

// Odeślij plik
try {
ServletUtils.returnFile(file, out) ;
)
catch (FileNotFoundException e) {
log("Pliku nie można było odnaleźć: " + e.getMessage());
res.sendError(res.SC_NOT_FOUND) ;
}
catch (IOException e) {
log("Problem z przesyłaniem pliku", e);
res.sendError(res.SC_INTERNAL_SERVER_ERROR) ;
}
Raportowanie
Poza rejestrowaniem błędów i wyjątków dla administratora serwera, podczas rozbudowy jest rzeczą korzystną
aby drukować pełny opis problemu wraz z śladem kaskady. Niestety wyjątek nie może odesłać swojego śladu
kaskady jako String (strumień) — może on tylko drukować swój ślad kaskady do PrintStream lub do
PrintWriter. W celu odczytania śladu kaskady jako String, musimy pokonać pewne trudności. Musimy
pozwolić Exception (wyjątkowi) drukować do specjalnego PrintWriter utworzonego wokół
ByteArrayOutputStream. ByteArrayOutputStream może wychwycić strumień wyjściowy i przekształcić
go na String. Klasa com.oreilly.servlet.ServletUtils ma metodę getStackTraceAsString(),
która wykonuje mniej więcej coś takiego:

public static String getStackTraceAsString(Throwable t) {


ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(bytes, true);
t.printStackTrace(writer) ;
return bytes.toString() ;
}

Oto sposób w jaki aplet ViewFile jest w stanie dostarczyć informacji, zawierających ślad kaskady
IOException:

// Odeślij plik
try {
ServletUtils.returnFile(file, out);
}
catch (FileNotFoundException e) {
log("Pliku nie można było odnaleźć: " + e.getMessage());
res. sendError (res. SC_NOT_FOUND) ;
}
catch (IQException e) {
log("Problem przy przesyłaniu pliku", e) ;
res.sendError(res.SC_INIEKNAL_SERVER_EEROR,
ServletUtils.getStackTraceAsString(e)) ;
}

Wydruk wyjściowy dla przykładowego wyjątku został przedstawiony na rysunku 5.2.

Rysunek 5.2.
Rzetelne informowanie klienta „na bieżąco”
Wyjątki
Tak jak powiedzieliśmy wcześniej, każdy wyjątek, który jest zgłaszany lecz nie wyłapywany przez aplet, jest
wyłapywany przez swój serwer. Sposób w jaki serwer obsługuje wyjątek jest zależny tylko od niego samego:
może on przekazać komunikat klientowi lub nie. Może on nawet wywołać metodę destroy() na aplet. Lecz
równie dobrze może tego nie robić.
Aplety zaprojektowane i skonstruowane do tego aby działać na określonym serwerze mogą optymalizować w tym
celu zachowanie serwera. Aplet zaprojektowany do współdziałania z wieloma serwerami nie może „oczekiwać”
ze strony serwera żadnej szczególnej obsługi błędu, musi on wyłapać swoje własne wyjątki i je obsłużyć
odpowiednio.

Istnieje pewna ilość typów wyjątków, które aplet musi wyłapać we własnym zakresie. Aplet może propagować
do swojego serwera tylko te wyjątki, które są podklasami IOException, ServletException lub
RuntimeException. Powodem są sygnatury metod. Metoda service() Servlet deklaruje swoją klauzulę
throws, którą zgłasza wyjątki IOException oraz ServletException. Dla metody tej (lub metod doGet() i
doPost() które wywołuje) zgłoszenie wyjątku i nie wyłapanie niczego więcej powoduje błąd związany z
czasem kompilacji. Wyjątek RuntimeException jest wyjątkiem specjalnym, który nigdy nie musi być
deklarowany w klauzuli throws. Popularnym przykładem może tutaj być NullPointerException.

Metody init() i destroy() również mają swoje własne sygnatury. Metoda init() deklaruje zgłoszenie
wyjątków tylko ServletException, metoda destroy(), nie deklaruje z kolei zgłaszania żadnych wyjątków.

Wyjątki apletowe „ServletException”


ServletException jest podklasą klasy java.lang.Exception, która jest określona dla apletów — klasa ta
jest zdefiniowana w zestawie javax.servlet. Aplet zgłasza ten wyjątek, aby zasygnalizować swój ogólny
problem. Ma on takich samych konstruktorów co java.lang.Exception: jednego, który nie przyjmuje
żadnych argumentów i jednego, który przyjmuje pojedynczy ciąg komunikatu:

javax.servlet.ServletException()
javax.servlet.ServletException(String msg)

Klasa ServletException może również obsługiwać „główną przyczynę” obiektu Throwable. Pozwala to
działać ServletException jako opakowanie wszystkich typów wyjątków i błędów, umożliwiając serwerowi
„dowiedzenie się” jaki powód „główny” spowodował zgłoszenie wyjątku ServletException. W celu obsługi
powyższego klas ServletException ma dwóch dodatkowych konstruktorów:

javax.servlet.ServletException(Throwable rootCause)
javax.servlet.ServletException(String msg, Throwable rootCause)

Używając zdolności powodu głównego, możemy przekazać jakikolwiek stosowany wyjątek lub
błąd:
try {
thread.sleep(60000) ;
}
catch (InterruptedException e) {
throw new ServletException (e); // zawiera pełny stosowany wyjątek
}

Serwer może pobrać i rozpatrzyć stosowany wyjątek, ślad kaskady i inne, poprzez wywołanie metody
getRootCause():

public Throwable ServletException.getRootCause()


W razie gdy nie istnieje żaden wyjątek zagnieżdżony, wywołanie odsyła null.

Wyjątki apletowe „UnavailableException”


Zestaw javax.servlet określa jedną podklasę klasy ServletException — UnavailableException,
możemy oczywiście dodać swoją własną. Wyjątek ten informuje, że aplet jest niedostępny — tymczasowo lub
trwale.

Trwała niedostępność oznacza, iż kopia apletu zgłaszająca wyjątek UnavailableException nie może usunąć
błędu. Aplet mógł zostać źle skonfigurowany lub jego stan nie jest poprawny. Aplet, który zgłasza trwały wyjątek
UnavailableException podczas obsługi zlecenia zostaje wyłączony z pracy, a na jego miejsce zostaje
utworzona nowa kopia kontynuująca obsługę zleceń. Jeżeli nie zostanie utworzony żadna „dostępna” kopia
apletu, klient otrzyma błąd. Aplet który zgłasza trwały wyjątek UnavailableException (lub regularnie
wyjątek ServletException) podczas stosowania swojej metody int() nigdy nie będzie obsługiwał zleceń.;
zamiast tego serwer będzie próbował inicjalizować nową kopię do obsługi przyszłych zleceń.

Tymczasowa niedostępność oznacza, że aplet nie może obsługiwać zleceń przez pewien okres czasu, z powodu
ogólnosystemowych problemów. Dla przykładu, mogłaby zdarzyć się sytuacja, że trójpoziomowy serwer nie
byłby dostępny lub też nie byłoby wystarczająco dużo pamięci czy miejsca na dysku żeby obsługiwać zlecenia.
Problem może rozwiązać się sam, tak jak w przypadku nadmiernego obciążenia, może również być i tak, że
administrator będzie zmuszony się nim zająć.

Podczas okresu niedostępności zlecenia apletu obsługuje serwer — poprzez odsyłanie kodu statusu
S.C._SERVICE_UNAVAILABLE (503) z nagłówkiem Retry-After, informując o przybliżonym czasie
niedostępności apletu. Jeżeli aplet zgłosi tymczasowy wyjątek UnavailableException podczas stosowania
swojej metody int(), nie będzie on już nigdy zajmował się obsługą zleceń, a serwer będzie próbował
inicjalizować nową kopię po okresie niedostępności. Dla wygody, serwery mogą traktować czasową
niedostępność jak niedostępność trwałą.

Wyjątek UnavailableException ma dwóch konstruktorów:

javax.servlet.UnavaliableException(string msg)
javax.servlet.UnavaliableException(String msg, int seconds)

Konstruktor jedno-argumentowy tworzy nowy wyjątek, informujący o tym, że aplet jest trwale niedostępny, z
wyjaśnieniem podanym przez msg. Natomiast wersja dwu-argumentowa tworzy nowy wyjątek informujący o
tym, że aplet jest niedostępny tymczasowo, z wyjaśnieniem podanym również przez msg. Poprawnie napisany
aplet powinien zamieścić w swoim wyjaśnieniu powód powstania problemu oraz działania, które powinien
podjąć administrator systemu aby przywrócić dostępność apletu. Wspomniany czas trwania niedostępności apletu
podawany jest w sekundach, jest on jednak tylko szacunkowy. Jeżeli nie można wykonać oszacowania czasu,
można użyć wartości nie-dodatniej. Wyjątek UnavailableException zapewnia dostęp do metod:
isPermanent() i getUnavailableSeconds() w celu pobrania informacji na temat wyjątku.

Konfiguracja Stron Wyjątków


Generalnie, zachowanie serwera podczas wyłapywania przez niego wyjątków apletu zależy od samego serwera,
jednakże aplikacja WWW może wskazać poprzez swój deskryptor wdrożenia, zestaw stron błędu dla obsługi
określonych rodzajów wyjątków. Strony błędu określane są przy użyciu znacznika <error-page> (tak jak w
przypadku kodów statusu), z wyjątkiem sytuacji kiedy zastępujemy znacznik <error-code> znacznikiem
<exception-type> (porównaj przykład 5.10).
Przykład 5.10.
Konfiguracja stron błędu wyjątków

<?xml versions"1.0"kodowanie="ISO-8859-l"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//Aplikacja WWW DTD 2.2//EN"
"http://Java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app>
<!-- ..... -->
<error-page>
<exception-type>
javax.servlet.ServletException
</exception-type>
<location>
/servlet/ErrorDisplay
</location>
</error-page>
</web-app>

Pozycja wskazuje, że jakikolwiek ServletException zgłoszony do serwera, powinien zostać wyświetlony


przy użyciu apletu ErrorDisplay. Zwróćmy uwagę, iż znacznik <exception-type> musi być w pełni zgodny
z nazwą zestawu; sam ServletException nie będzie działał. Zasada ta stosuje się również do wszystkich
podklas klasy servletException, takich jak UnavailableException (jeżeli nie ma w deskryptorze
wdrożenia bardziej uściślającej zasady, którą stosuje się najpierw.

Dla przeznaczeń <location> (lokalizacji) dynamicznych, aplet udostępnia dwa atrybuty zleceniowe, służące do
opisania zgłoszonego wyjątku:

javax.servlet.error.exception_type
Kopia java.lang.Class podająca typ wyjątku. Typ atrybutu był początkowo nieokreślony, dlatego
niektóre wczesne wdrożenia serwera mogą odsyłać formularz String nazwy klasy.
javax.server.error.message
String (strumień) podający komunikat wyjątku, przekazywany do konstruktora wyjątku. Nie ma żadnej
możliwości uzyskania wyjątku lub jego śladu kaskady.

Wykorzystując powyższe atrybuty jesteśmy w stanie ulepszyć ErrorDisplay z przykładu 5.9, tak że będzie
działał jako zasób monitora ogólnego błędu, obsługujący wyświetlanie zarówno kodów statusu błędów i
wyjątków, tak jak to zostało ukazane na przykładzie 5.11.

Przykład 5.11.
Dynamiczne tworzenie strony błędu ogólnego zastosowania

import java.io.*;
import javax.servlet.* ;
import javax.servlet.http.*;

public class ErrorDisplay extends HttpServlet {

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();

String code = null, message = null, type = null;


Object codeObj, messageObj, typeObj;

// Pobierz trzy możliwe atrybuty błędu, niektóre z nich mogą być


równe // zero
codeObj = req.getAttribute("j avax.servlet.error.status_code") ;
messageObj = req.getAttribute("javax.servlet.error.message") ;

typeObj = req.getAttribute("javax.servlet.error.exception_type") ;

// Zamień atrybuty na wartości ciągu


if (codeObj != null) code = codeObj.toString();
if (messageObj != null) message = messageObj.toString();
if (typeObj != null) type = typeObj.toString();

// Przyczyną błędu jest albo kod statusu albo typ błędu


String reason = (code != null ? code : type);

out .println ("<HTML>") ;


out.println("<HEAD><TITLE>" + przyczyna + ": " + komunikat +
"</TITLE></HEAD>") ;
out.println("<BODY>");
out.println("<H1>" + przyczyna + "</H1>");
out.println("<H2>" + komunikat + "</H2>");

out.println("<HR>") ;
out.println("<I>Łaczenie z błędem " + req.getRequestURI() +
"</I>");
out.println( "</BODY></HTML>") ;
}
}

Niestety, tylko typ wyjątku i komunikat — a nie ślad kaskady wyjątku — są dostępne dla apletu ErrorDisplay,
ograniczając użyteczność strony błędu wyjątku. Aby zagwarantować wyświetlanie lub rejestrację śladu kaskady
wyjątku, musimy wychwycić wyjątek w oryginalnym apletcie i obsłużyć go przed jego propagacją do serwera.
Będziemy zmuszeni napisać dodatkowy, niewielkich rozmiarów kod, jednak dzięki osobistej obsłudze wyjątków
mamy pewność, że obsługa błędu będzie spójna i właściwa. Oczekuje się, że w Interfejsie API 2.3 zostanie
dodany nowy atrybut zlecenia, zawierający sam wyjątek, ma to zostać wykonane przy użyciu nazwy
javax,servlet.error.exception.

Jak rozpoznać, że nikt nie oczekuje na sygnały?


Czasem bywa i tak, że klienci przerywają połączenie z serwerami. Tak, to prawda to jest nie
fair, lecz niestety zdarza się. Raz spowodowane jest to tym, że klient połączył się ze złą stroną,
innym znów razem powodem jest zbyt długi czas generowania odpowiedzi apletu.
Pamiętajmy, że przez cały czas kiedy aplet przygotowuje swoją odpowiedź, użytkownik może
przerwać połączenie — klikając po prostu przycisk „stop”. Zastanówmy się: co się dzieje
kiedy ów przycisk zostaje uruchomiony?

Niestety aplet nie jest bezpośrednio informowany, że użytkownik kliknął przycisk „stop”, aplet nie wie o tym, że
powinien przerwać generowanie odpowiedzi. Dowiaduje się on o tym dopiero w momencie kiedy próbuje wysłać
wydruk wyjściowy do nieistniejącego klienta, kiedy to pojawiają się błędy.

Aplet, który wysyła informacje przy użyciu ServletOutputStream „widzi” zgłaszany wyjątek IOException,
w momencie kiedy próbuje napisać wydruk wyjściowy. W przypadku apletów, które buforują swój wydruk
wyjściowy, wyjątek IOException jest zgłaszany w momencie wypełnienia bufora i usuwania jego zawartości.

Ponieważ wyjątek IOException może zostać zgłoszony zawsze kiedy aplet próbuje wysłać swój wydruk
wyjściowy, poprawnie napisane aplety uwalniają swoje zasoby w bloku finally. (Blok finally jest
dodatkową częścią konstrukcji try/catch/finally. Występuje on po zerze lub większej ilości bloków,
ponadto jego kod jest wywoływany tylko raz — bez względu na to jak wywołuje kod w bloku try). Poniżej
przedstawiamy wersję metody returnFile() z apletu ViewFile, gdzie do zagwarantowania zamknięcia jej
FileInputStream, wykorzystywany jest blok finally:

void returnFile( String filename, OutputStream out)


throws FileNotFoundException, IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
byte[] buf = new byte[4 * 1024]; // bufor 4-kilowy
int bytesRead;
while ((bytesRead = fis.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
}
finally {
if (fis != null) fis.close();
}
}
Dodanie bloku finally nie zmienia faktu, że metoda ta propaguje wszystkie wyjątki do programu żądającego,
jednak gwarantuje to, że przed propagacją, metoda otrzyma szansę zamknięcia otwartego FileInputStream.

Aplet przesyłający dane znakowe, wykorzystujący w tym celu PrintWriter, nie otrzymuje wyjątku
IOException w czasie kiedy próbuje napisać wydruk wyjściowy, ponieważ metody PrintWriter’a nigdy nie
zgłaszają wyjątków. Zamiast tego aplet przesyłający dane znakowe musi wywołać metodę PrintWriter’a
checkError(). Metoda ta opróżnia wydruk wyjściowy — zatwierdzając odpowiedź, a następnie odsyła
boolean, informując czy pojawił się problem przy pisaniu do stosowanego OutputStream. Jeżeli klient
zatrzyma zlecenie, odsyłane jest true.
Długo-wywołujacy aplet, który nie „ma nic przeciwko” wczesnemu zatwierdzaniu odpowiedzi, powinien
regularnie wywoływać metodę checkError() aby określić czy przetwarzanie może zostać zatrzymane przed
zakończeniem. Jeżeli żaden wydruk wyjściowy nie był wysyłany od ostatniego sprawdzenia, aplet może przesłać
treść wypełniającą. Dla przykładu:

out.println("<H2>Here's the solution for your differential equation:</H2>");


if (out. checkError ()) return;

preliminaryCalculation() ;
out.print(" "); // treść wypełniająca, dodatkowe światło na stronie
//jest ignorowane w HTML-u
if (out.checkError()) return ;

additionalCalculation() ;

Należy tutaj zaznaczyć, że serwer nie jest zobligowany do zgłaszania wyjątku IOException lub do ustawienia
znacznika błędu PrintWriter’a, po rozłączeniu się klienta. Serwer może zdecydować, że pozwoli odpowiedzi
na zakończenie, przy zignorowaniu jej wydruku wyjściowego. Ogólnie rzecz biorąc sytuacja taka nie stwarza
problemów, jednak oznacza to, iż aplet wywołując w takim serwerze powinien zawsze mieć ustawiony punkt
końcowy i nie powinien być pisany do ciągłego wykonywania pętli, aż do naciśnięcia „stopu” przez użytkownika.

Sześć sposobów wyciągania korzyści z apletów


Od czasu pierwszej edycji niniejszej książki aplety stały się, de facto, standardem dla Javowskiego
konstruowania oprogramowania WWW po stronie serwera. „Ewolucyjna walka” rozgrywana pomiędzy apletami
wykonywanymi na serwerach, zwykłymi apletami strony serwera oraz innymi, podłączalnymi, opartymi na Javie
strukturami, ostatecznie się zakończyła, a aplety są jej zwycięstwami, obsługiwane dzisiaj przez każdy serwer
WWW oraz każdy serwer aplikacji. Nowa dziedzina intensywnych innowacji usytuowana jest już poza apletami,
na prezentacjach (obrazowaniach) oraz na poziomach osnów, w obszarze na którym osoby fizyczne oraz
przedsiębiorstwa szukają sposobu zbudowania, na bazie apletów, tworzenia efektownych stron WWW.

Rozwiązania takie są bardzo potrzebne ponieważ proste podejście do generowania treści znaczników, zmusza
programistów apletów do pisania wywołania out.println() dla każdego wiersza treści, okazało się być
dużym problemem w świecie rzeczywistym. Przy podejściu out.println(), treść znaczników musi być
tworzona wewnątrz kodu, a to staje się zadaniem wymagającym wiele wysiłku i czasu w przypadku długich stron.
Poza tym twórcy stron muszą prosić programistów o dokonanie wszystkich niezbędnych zmian strony.
Celem wspólnym dla niemal wszystkich utworzeń treści jest „oddzielenie treści od prezentacji (obrazowania)”.
Termin treść oznacza tutaj nieprzetworzone dane strony i manipulowanie danymi. Być może lepszym
określeniem byłoby przetwarzanie danych, jednakże „oddzielanie przetwarzania danych od prezentacji” nie
byłoby najtrafniejszym określeniem. Termin prezentacja oznacza reprezentację danych, podanych ostatniemu
użytkownikowi (często są to dane HTML, jednakże przy obecnym wzroście urządzeń podłączanych do sieci
WWW, coraz częściej zdarza się, że są to dane WML — języka znaczników w telefonii bezprzewodowej).
Oddzielenie treści od prezentacji daje wiele korzyści, przede wszystkim prostsze tworzenie stron oraz jej
obsługa, jako że treść strony (determinowana przez programistę) może być konstruowana i zmieniana niezależnie
od jej prezentacji (determinowanej przez projektanta lub producenta). Czasem określa się takie oddzielenia jako
strukturę Model-Widok-Kontroler (Model-View-Controler) — MVC. Model jest odpowiednikiem treści, widok
— prezentacji, kontroler zaś ma wiele odniesień, w zależności od technologii.

W późniejszych rozdziałach niniejszej książki omówimy bliżej kilka z popularnych alternatyw dla apletowego
tworzenia treści. Dla każdego przedstawionej alternatywy zapewnimy pomoc w postaci odpowiednich narzędzi,
zademonstrujemy jak używa się owych narzędzi, i wypróbujemy gdzie najlepiej działa. Z powodu ograniczeń
objętościowych oraz czasowych jak również i faktu, że każdy omawiany temat jest „szybko umykającym celem”,
nie jesteśmy w stanie dostarczyć pełnego omówienia dla każdej alternatywy. Jednego możemy być pewni: jest to
coś więcej niż tylko pięć implementacji tej samej idei. Każda alternatywa traktuje o problemie tworzenia treści z
innej perspektywy, w związku z tym techniki rozwiązywania problemów różnią się znacznie. Narzędzie, które
sprawdza się w jednej sytuacji może w ogóle nie działać w innej. Tak więc nie czytajmy przykładów w
poniższych rozdziałach, z myślą o poznaniu najlepszej technologii — starajmy się w zamian rozglądać za
najlepszą technologią dla naszych projektów.

Dotąd unikaliśmy dyskusji na temat swoistych, komercyjnych rozwiązań, z powodu związanego z tym ryzyka
ograniczania się do rozwiązań jednego producenta, nacechowanych motywami finansowymi.*Alternatywy, które
zostaną omówione w dalszej części książki są następujące:

JavaServer Pages
JavaServer Pages (JSP) to technologia stworzona przez „Sun Microsystems”, ściśle
związana z apletami. Tak jak w przypadku apletów, „Sun” wydaje specyfikację JSP, a
sprzedawcy nie związani z tą firmą starają się wprowadzić swoje wdrożenia jako
standardowe. Fakt, iż JSP zostało wydane przez „Sun” stawia tą technologię w
uprzywilejowanej pozycji i gdyby JSP było rozwiązało wystarczającą liczbę problemów
użytkowników, prawdopodobnie zdobyłoby rynek jeszcze przed pojawieniem się na nim
innych pozycji. Ponieważ zaskakująco wielu użytkowników jest niezadowolonych z JSP,
alternatywy dla tej technologii zyskują popularność. Więcej na ten temat w rozdziale 18
„JavaServer Pages” oraz na stronie http://java.sun.com/products/jsp.

Tea
Tea jest nowym autorskim produktem firmy „Walt Disney Internet Group” (wcześniej
znanej jako GO.com), rozwijanym przez lata na potrzeby wewnętrzne, do zaspokojenia
ich ogromnego zapotrzebowania na produkcję sieciową, dla stron takich jak
ESPN.com. Jest to technologia przypominająca JSP, jednak pozbawiona wielu jego
błędów, ma już ponadto znaczną obsługę narzędziową. Więcej na ten temat w rozdziale
14 „Osnowa technologii tea” oraz na stronie http://opensource.go.com.

*
Przekonaliśmy się o tym „na własnej skórze” — w pierwszej edycji tej książki omówiliśmy komercyjny zestaw htmlKona
wyprodukowany przez „WebLogic” ponieważ nie było w tym czasie żadnej dostępnej alternatywy, ponadto zostaliśmy
zapewnieni przez „WebLogic”, że cena zestawu pozostanie na rozsądnym poziomie. Po przejęciu „WebLogic” przez „BEA
Systems” , zapewnienie nie było już aktualne, i w rzeczywistości htmlKona stał się mało znaczącym szczegółem w ofercie
cenowej „BEA Systems”. Szczęśliwie htmlKona został ponownie wdrożony jako otwarte źródło projektu „Element
Construction Set” (ECS) przez „Apache Jakarta Project” i teraz będzie dostępny tak długo jak długo będzie cieszył się
zainteresowaniem.
WebMarco
WebMarco jest mechanizmem wzorcowym stworzonym przez „Semiotek, Inc.” jako część projektu
„Shimari”, który w przyszłości pewnie połączy się z Projektem „Apache Jakarta Project”.Istnieje wiele
mechanizmów wzorcowych wartych omówienia, jednak WebMarco jest najbardziej popularny, jest
używany na wyjątkowo często uczęszczanych stronach, takich jak AltaVista.com., został wbudowany w
osnowach o otwartym dostępie do kodu źródłowego takich jak „Turbine” czy „Melati”, ponadto jest
używany w przodujących projektach o otwartym dostępie do kodu źródłowego takich jak „JetSpeed”.
Więcej na ten temat można znaleźć w rozdziale 15 „WebMarco” oraz na stronach: http://webmarco.org
i http://jakarta.apache.org/velocity.

Element Construction Set


Zestaw Element Construction Set (ECS), będący częścią „ApacheJakartaProject,” jest zestawem klas
modelowanym po htmlKona, produkcie „WebLogic” (obecnie „BEA Systems”). ECS ma wiele
ograniczeń, jednak rozwiązuje pewną grupę problemów, ponadto zapoznanie się z podejściem ECS jest
dobrym punktem wyjściowym dla omawiania bardziej elastycznego XMLC (porównaj rozdział 17
„XMLC” oraz stronę http://jkarta.apache.org/ecs)

XMLC
XMLC wykorzystuje XML, uzyskując prawie wszystkie możliwości ECS, unikając wielu jego
ograniczeń. XMLC został wyprodukowany przez „Lutris” jako część ich projektu otwartego dostępu do
kodu źródłowego: „Enhydra Application Server” i może być używany jako oddzielny komponent.
Więcej informacji na ten temat w rozdziale 17 „XMLC” oraz na stronie http://xmlc.enhydra.org.

Cocoon
Cocoon to kolejna, oparta na XML-u alternatywa stworzona przez „XML Apache Project”. Cocoon
wykorzystuje XML oraz XSLT do obsługa treści tworzenia czegoś, co określa się jako osnowa
publikacji WWW. Cocoon jest sterowaną serwerowo osnową przez co XML, tworzony statycznie lub
dynamicznie, uruchamiany jest przez filtr XSLY, jeszcze przed prezentacją klientowi. Filtr ten może
sformatować treść jako każdy typ treści włącznie z HTML, XML, WML czy PDF — a osnowa może
wybrać, w zależności od klienta, różne filtry. Nie będziemy tutaj omawiali szerzej Cocoon’a ponieważ
ma on więcej wspólnego z programowaniem XSLT niż z programowaniem Javy. ponadto (przynajmniej
w chwili obecnej) na ogół nie jest to często wybierane narzędzie dla pisania interaktywnych aktywacji
WWW. Cocoon bardziej nadaje się do obszernych, najczęściej statycznych, stron, które dopuszczają
występowanie w swojej treści wielokrotnych obrazów. Więcej na temat Cocoon można znaleźć na
stronie http://xml.apa.cheorg oraz w książce Brett’a McLaughlin’a „Java and XML”. Rozdział tej
książki traktujący o Cocoon’ie dostępny jest na stronie
http://www.oreily.com/catalog/javaxml/chapter/ch09.html.

Jeżeli nasze ulubione lub popularne narzędzie nie zostało tutaj omówione, nie jest to niczym
nadzwyczajnym. Przyjrzyjmy się dacie produkcji tego narzędzia: jest prawdopodobnie
późniejsza od terminu oddania tej książki do druku. Tempo wprowadzania w tym obszarze
innowacji jest ogromne, z XML-em dostarczającym nam nowych możliwości oraz
dołączanych do Internetu urządzeń wymagających nowych właściwości, dziedzina ta to
eldorado dla nowych rozwiązań. Miejmy również świadomość tego, że ulepszenia tych
narzędzi będą również wprowadzane bardzo szybko. Każdy rozdział omawiający jakiś
problem jest skazany na co najmniej częściowe przedawnienie w momencie, kiedy czytelnik
będzie go czytał. Aby być na bieżąco zorientowanym w temacie należy zaglądać na stronę
http://www.servlets.com.
Rozdział 6. Przesyłanie treści

multimedialnej

Do tej pory wszystkie aplety, które pisaliśmy odsyłały standardową stronę HTML. Sieć WWW
nie składa się jednak tylko z HTML-u, dlatego właśnie w niniejszym rozdziale zajmiemy się
bardziej ciekawymi aspektami odsyłania apletowego. Rozpoczniemy od omówienia protokołu
WAP oraz języka znaczników WML używanego w telefonii komórkowej oraz urządzeniach
przenośnych i poznamy jak aplety mogą wykorzystywać tą technologię. Następnie pokażemy
jak tworzyć dynamicznie, obrazy z apletów aby wygenerować wykresy i manipulować
zdjęciami. Pod koniec rozdziału dowiemy się kiedy i jak przesyła się skompresowane
odpowiedzi, ponadto zbadamy użycie odpowiedzi wieloczęściowych w celu wdrażania
serwera cyklicznego.

WAP i WML
Protokół Aplikacji Bezprzewodowej (WAP) jest de facto standarem dla dostarczania łączności internetowej do
telefonów komórkowych, pejdżerów oraz do Osobistych Asystentów Cyfrowych (PDA) w sieciach komunikacji
bezprzewodowej na całym świecie. Protokół ten został stworzony przez Ericssona, Nokię, Motorolę oraz
Phone.com (wcześniej „Unwired Planet”), które to firmy rozsądnie wyraziły chęć stworzenia protokołu
standardowego, a nie własnych, konkurujących między sobą protokołów. Owe cztery firmy stworzyły razem
forum WAP (http://www.wapforum.org), demokratycznie zarządzaną organizację, która dzisiaj liczy już 400
członków.
Protokół WAP składa się z szeregu specyfikacji, opisujących projektowanie aplikacji przeznaczonych do
uruchamiania w sieciach komunikacji bezprzewodowej. Protokół WAP obejmuje zarówno poziom aplikacji
(język znaczników WML oraz język skryptów WMLScript, znane pod wspólną nazwą Środowisko Aplikacji
WWW lub WAE) jak i stosowanej sieci transportu poziomów (protokołów: WDP, WTLS, WTP oraz WSP).
Kaskada WAP jest odpowiednikiem kaskady protokołu WWW, tak jak to zostało zaprezentowane na rysunku
6.1.
Rysunek 6.1.
Kaskady: WWW i WAP

Podstawowa różnica pomiędzy WAP-em a WWW polega na tym, że WAP jest zoptymalizowany do nisko-
przepustowej komunikacji bezprzewodowej. Dla przykładu WAP używa formatu binarnego dla struktur
odpowiedzi i zlecenia, zamiast formatu tekstowego przez WWW.

Brama WAP odgrywa rolę pośrednika pomiędzy siecią WAP a siecią WWW. Zadaniem bramy jest
konwertowanie zleceń WAP na zlecenia WWW oraz odpowiadające im odpowiedzi WWW na odpowiedzi
WAP. O Bramie WAP można powiedzieć, iż jest ona „konwertorem kaskady protokołu”* Bramy WAP
udostępniają urządzeniom WAP dostęp do standardowych serwerów WWW. Zostało to zaprezentowane na
rysunku 6.2.

Rysunek 6.2.
Rola Bramy WAP

*
Bramy WAP są zwyczajowo dostarczane przez właściciela sieci bezprzewodowej. Czytelników zainteresowanych
utworzeniem swojej własnej bramy lub tym jak działają bramy odsyłamy do Bramy WAP zwanej „Kannel” dostępnej na
stronie http://www.wapgateway.org.
Prostą sprawą jest konstruktor bezprzewodowy — możemy zignorować protokół warstwy transportowej. Brama
WAP „dba” o to, aby zlecenie urządzenia wyglądało jak zlecenie HTTP. Warstwą na której musimy się
skoncentrować jest warstwa aplikacji, w której zamiast generowania HTML-u, generujemy WML.

WML
Urządzenia przenośne są znacznie bardziej ograniczone niż komputery osobiste. Mają one zwykle wolne
procesory, małe pojemności pamięci, małe wyświetlacze oraz niezwykle ograniczoną przepustowość. Z powodu
podobnych ograniczeń urządzenia WAP nie współdziałają ze zwykłymi treściami HTML i treściami
obrazkowymi. Zamiast tego używają one języka znaczników telefonii komórkowej (WML) — dla treści
tekstowej, języka WMLScript dla wykonywania skryptów oraz Bezprzewodowej Mapy Bitowej (Wireless
Bitmap) WBMP — monochromatycznego formatu obrazu dla grafiki.

WML jest aplikacją XML, podobną do HTML lecz z mniejszą ilością znaczników. Można tutaj zastosować
porównanie do talii kart, gdzie każda karta odpowiada ekranowi strony, a każda strona zbiorowi kart, które mogą
zostać natychmiast przesłane. Przesyłanie zawartości talii za jednym razem, zamiast jednej karty, minimalizuje
opóźnienie (okres potrzebny do obejrzenia kolejnej karty) podczas „łączenia się” z treścią.
Przykład 6.2 ukazuje statyczny dokument WML, który spełnia rolę „minibarmana”. Wyświetla listę drinków do
wyboru, a następnie składniki wybranego drinka.* Zwróćmy uwagę, iż jest to dokument XML z dobrze znanym
DTD.

Przykład 6.1.
„Minibarman WML”, drinks.xml

<?xml versions"1.0"?>
<!DOCTYPE wml PUBLIC

"-//WAPFORUM//DTD WML 1.1//EN"


"http: / /www. wapforum.org/DTD/wml_l.1. xml" >
<wml>
<card id="Barman" title="Wybierz Drinka">
<p>
Select a Drink:

<anchor>
Kamikaze <go href="#Kamikaze" />
</anchor><br/>
<anchor>
Margarita <go href="#Margarita" />
</anchor><br/>
<anchor>
Boilermaker <go href="#Boilermaker" />
</anchor><br/>
</P>
</card>

<card id="Kamikadze" title="Kamikadze">


<p>
To make a Kamikaze:<br/>
1 part Vodka<br/>
1 part Triple Sec<br/>
1 part Lime Juice<br/>
</P>
</card>

<card id="Margarita" title="Margarita">


<p>
To make a Margarita:<br/>
1 1/2 oz Tequila<br/>

1/2 oz Triple Sec<br/>


1 oz Lime Juice<br/>
Salt<br/>

*
Pełna implementacja tej idei dostępna jest na stronie http://www.wap2bar.com/index.wml
</p>
</card>

<card id="Boilermaker" title="Boilermaker">


<p>
To make a Boilermaker :<br/>
2 oz Whiskey<br/>
10 oz Beer<br/>
</p>
</card>
</wml>

Dokument zawiera cztery karty. Pierwsza karta, wyświetlana domyślnie, ukazuje krótką listę drinków. Każda
nazwa drinka to hiperłącze do następnej karty dokumentu, z nazwą karty podaną przy użyciu składni
#Cardname. Każda późniejsza karta wyświetla składniki dla drinka. Cały dokument może być przesłany do
urządzenia podczas jednej operacji, mimo iż w danym momencie będą dostępne tylko jego części.

Symulatory urządzeń WAP


Aby umożliwić wywoływanie aplikacji WWW, bez odwoływania się do fal powietrznych, różne
firmy tworzą symulatory telefoniczne obsługujące WAP, oprogramowanie do uruchamiania
na komputerach osobistych, które działają jak telefon (i nawet tak wyglądają). Na rysunku 6.3
zaprezentowano jak wygląda dokument na symulatorze Phone.com UP.Simulator (dostępnym
na http://phone.com). Inne symulatory oraz zestawy projektowania WAP można znaleźć na
następujących stronach: http://www.nokia.com, http://www.ericsson.com i,
http:/www.motorola.com oraz na wielu innych. Niektóre standardowe przeglądarki WWW,
włącznie z przeglądarką „Opera” również obsługują WML.

Rycina 6.3. „Drinkowe telefonowanie”

Podawanie treści WAP


Aby treść WAP została podana poprawnie, serwer musi przesłać plik WML z jawnym typem treści
text/vnd.wap.wml. Niektóre serwery ustalają ten typ treści automatycznie na pliki .wml. Inne muszą być
informowane o nowym typie. Serwer musi być powiadomiony o typie treści odnoszącym się do WAP, z
kolejnymi elementami <mime-mapping> w deskryptorze wdrożenia web.xml serwera. Bit informacji web.xml z
przykładu 6.2 dodaje właściwy typ WML-u, WMLScript oraz pliki bezprzewodowej Mapy Bitowej.
Przykład 6.2.
Dodawanie typów nośników WAP do web.xml

<!- ... ->


<mime-mapping>
<extension>
wml
</extension>
<mime-type>
text /vnd. wap. wml
</mime-type>
</mime-mapping>
<mime-mapping>
<extension>
wmls
</extension>
<mime-type>
text /vnd. wap. wmlscript
</mime-type>
</mime-mapping>
<mime-mapping>
<extension>
wbmp
</extension>
<mime-type>
image /vnd. wap. wbmp
</mime-type>
< /mime-mapping>
<!-- ... -->

Dodatkowo niektórzy programiści aplikacji WWW uważają, że dobrze jest dodać index.wml do domyślnej listy
plików wyszukiwania w katalogu, znanej jako welcome file list. Blok deskryptora wdrożenia w przykładzie 6.3
ustawia welcome file list do index.html, index.htm oraz do index.wml, w takiej właśnie kolejności. Taka kolej
rzeczy daje znać serwerowi o zleceniu na http://localhost/wap/index.wml jeżeli index.html oraz index.htm nie
istnieją.

Przykład 6.3.
Dodawanie plików WAP Welcome Files do web.xml

<!-- ... -->


<welcome-file-list>
<welcome-file>
index.html
</welcome-file>
<welcome-file>
index.htm
</welcome-file>
<welcome-file>
index.wml
</welcome-file>
</welcome-file-list>
<!-- ... -->

Dynamiczna treść WAP


Z technicznego punktu widzenia nie ma prawie żadnej różnicy pomiędzy tym jak aplet podaje dynamiczną treść
WAP a sposobem w jaki podawana jest dynamiczna treść WWW. Aby podać treść WAP aplet musi po prostu
zmienić typ treści odpowiedzi na text.vnd.wap.wml a treść odpowiedz z HTML-u na WML. Dla apletu,
zlecenie jest zwykłym HTTP; zewnętrzna Brama WAP obsługuje WML oraz konwersję kaskady protokołu
WWW. Podczas gdy różnica techniczna jest niewielka, przypadków w których lepiej jest posłużyć się apletem
dla dynamicznego tworzenia treści może być niewiele.
W sieci WWW, nie ma praktycznie żadnych sankcji za kontaktowanie się z serwerem tylko po to aby wykonać
mało znaczące zadania czy uaktualnienia strony. Przy zastosowaniu WAP-u, sankcje te mogą zostać
ustanowione. W wyniku tego proste zadania takie jak: nawigacja kartowa i kontrola poprawności danych
formularzowych, które w sieci WWW mogłyby angażować aplet, są najlepiej przeprowadzane w WAP przy
użyciu metafory kartowej WML oraz zdolności wykonywania skryptów po stronie klienta WMLScript. Dla
przykładu poprzedni aplet „barman” używa zestawu kart w celu podawania drinków bez kontaktowania się z
serwerem.

Aplety nie pozostają oczywiście bez użyteczne. Aplet (lub inny skrypt serwera) jest potrzebny do generowania
talii kart zawierających informacje dynamiczne pobrane z bazy danych. Aplety są również potrzebne do obsługi
zleceń na dane zbyt duże aby zmieścić się na zestawie kart. Urządzenia są często ograniczone do przechowania
tylko 1400 bitów skompilowanych danych strony.

Na przykładach 6.4 i 6.5 zaprezentowano, formularz WML oraz aplet generujący WML, które razem dostarczają
aplikację wyszukania kodu kierunkowego. Klient może wprowadzić do formularz telefoniczny kod kierunkowy,
przedłożyć go do apletu oraz dowiedzieć się który stan lub region odpowiada temu kodowi. Osoby posiadające
telefony obsługujące WAP mogą wykorzystać tą aplikację do fizycznej lokalizacji jakiegokolwiek numeru
identyfikacyjnego rozmówcy.

Przykład 6.4.
Wykorzystanie WML-u w celu uzyskania o kodu kierunkowego

<?xml version="1.0"?>

<!DOCTYPE wml PUBLIC


"-//WAPFORUM//DTD WML 1.1//EN"
"http: //www. wapforum. org/DTD/wml_l.1. xml">

<wml>
<card id="AreaCode" title="Wprowadź Kod Kierunkowy">
<do type="akceptuj" label="Wprowadź">
<go href="aplet /AreaCode?code=$(code)"/>
</do>
<p>
Enter an Area Code: <input type="tekst" nazwa="kod"/>
</p>
</card>
</wml>

Ten dokument WML przechowuje prosty formularz z obszarem wejściowym tekstu. Niezależnie od tego jaki kod
kierunkowy zostanie wprowadzony, zostaje przesłany do apletu AreaCode jako parametr code. Aby utworzyć
ręcznie ciąg zapytań używamy zmiennej zastąpienia $(code).

Przykład 6.5.
Wykorzystywanie WAP do zlokalizowania kodu kierunkowego

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class AreaCode extends HttpServlet {

Properties lookup = new Properties() ;

public void init() {


// Przenieś poniższe dane nieprzetworzone do a Listy właściwości
// szybkiego odszukania
for (int i = 0; i < data.length; i++) {
Object[] record = data[i];
String state = (String) record[0] ;
int[] codes = (int[]) record[1] ;
for (int j = 0; j < codes.length; j++) {
lookup.put(String.valueOf(codes[j]),state);
}
}
}

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/vnd.wap.wml") ;
PrintWriter out = res.getWriter();

String msg = null;

String code = req.getParameter("kod") ;


String region = null;
if (code != null) {
region = lookup.getProperty(code) ;
}

out.println("<?xml version=\"1.0\"?>") ;
out.println ("<!DOCTYPE wml PUBLIC " +
"\"-//WAPFORUM//DTD WML 1.1//EN\" " +
" \" http: / /www.wapforum. org/DTD/wml_l.1.xml \" >") ;

out.println("<wml>") ;
out.println("<card id=\"Code\" title=\"Code\">") ;
out.println(" <p>") ;
out.println (" Numer kierunkowy '" + code + '"<br/>");
if (region != null) {
out.println(" jest " + region + ",<br/>");
}
else {
out.println(" jest nie ważny.<br/>");
}
out.println(" </p>") ;
out.println("</card>") ;
out.println("</wml>") ;
}

// Nieprzetworzony numer kierunkowy dla każdego regionu


private Object[][] data = new Object[][] {
{ "Toll Free", new int[] { 800, 855, 866, 877, 888 } },
{ "Alabama", new int[] { 205, 256, 334 } },
{ "Alaska", new int[] { 907 } },
{ "Alberta", new int[] { 403, 780 }},
{ "Arizona", new int[] {480, 520, 602, 623 } },
{ "Arkansas", new int[] { 501, 870 } },
{ "British Columbia", new int[] { 250, 604 } },
{ "California", new int[] { 209, 213, 310, 323, 369, 408, 415, 424,
510,530, 559, 562, 619, 626, 627, 650, 661, 707, 714,
760,805,818,831,858, 909, 916, 925, 949 } },
{ "Colorado", new int[] { 303, 719, 720, 970 }},
{ "Connecticut", new int[] { 203, 475, 860, 959 }},
{ "Deleware", new int[] { 302 } },
{ "District of Columbia", new int[] {202 } },
{ "Florida", new int[] { 305, 321, 352, 407, 561, 727, 786, 813,
850, 863,904, 941, 954 } },
{ "Georgia", new int[] { 229, 404, 478, 678, 706, 770, 912 }},
{ "Hawaii", new int[] { 808 } },
{ "Idaho", new int[] { 208 } },
{ "Illinois", new int[] { 217, 224, 309, 312, 618, 630, 708,
773,815,847}},
{ "Indiana", new int[] { 219, 317, 765, 812 }},
{ "Iowa", new int[] { 319, 515, 712 } },
{ "Kansas", new int[] { 316, 785, 913 } },
{ "Kentucky", new int[] { 270, 502, 606, 859 } },
{ "Louisiana", new int[] { 225, 318, 337, 504 } },
{ "Maine", new int[] { 207 } },
{ "Manitoba", new int[] { 204 } },
{ "Maryland", new int[] { 240, 301, 410, 443 } },
{ "Massachusetts", new int[] { 413, 508, 617, 781, 978 } },
{ "Michigan", new int[] { 231, 248, 313, 517, 586, 616, 734,
810,906,} },
{ "Minnesota", newint[] { 218, 320, 507, 612, 651, 763, 952 } },
{ "Mississippi", new int[] { 228, 601, 662 } },
{ "Missouri", new int[] { 314, 417, 573, 636, 660, 816 } },
{ "Montana", new int[] { 406 } },
{ "Nebraska", new int[] { 308, 402 } },
{ "Nevada", new int[] { 702, 775 }},
{ "New Brunswick", new int[] { 506 } },

{"New Hanpshire", new int[] { 603 } },


{"New Jersey", new int[] { 201, 609, 732, 856, 908, 973 } }

{"New Mexico", new int[] { 505 } },


{"New York", new int[] { 212, 315, 347, 516, 518, 607, 631,
646,716,718, 845, 914, 917 }},
{"Newfoundland", new int[] { 709 } },
{"North Carolina", new int[] { 252, 336, 704, 828, 910, 919,918 }},
{"North Dakota", new int[] { 701 } },
{"Northwest Territories", new int[] { 867 } },
{"Nova Scotia", new int[] { 902 } },
{"Ohio",new int[] { 216, 234, 330, 419, 440, 513, 614, 740,937 } },
{"Oklahoma", new int[] { 405, 580, 918 ) },
{"Ontario", new int[] { 416, 519, 613, 647, 705, 807, 905 } !,
{"Oregon", new int[] { 503, 541, 971 }},
{"Pennsylvania", newint[] { 215, 267, 412, 484, 570, 610,717,
724,814,878,902 } }
{"Puerto Rico", new int[] { 787 } },
{"Quebec", new int[] { 418, 450, 514, 819 } },
{"Rhode Island", new int[] { 401 } },
{"Saskatchewan", new int[] { 306 } },
{"South Carolina", new int[] { 803, 843, 864 } },
{"South Dakota", new int[] { 605 } },
{"Tennessee", newint[] { 423, 615, 865, 901, 931 } },
{"Texas", new int[] { 210, 214, 254, 281, 361, 409, 469,
512,682,713 806, 817, 830, 832, 903, 915, 940, 956, 972 } },
{"US Virgin Islands", new int[] { 340 } },

{ "Utah", new int[] { 435,.801 } },


{"Vermont", new int[] { 802 } },
{"Virginia", new int[] { 540, 571, 703, 757, 804 } },
{"Washington", new int[] ( 206, 253, 360, 425, 509, 564 }},
{"West Virginia", new int[] ( 304 } },
{"Wyoming", new int[] ( 307 } },
{"Yukon Territory", new int[] { 867 } ),
};
}

Aplet ten otrzymuje zlecenie WAP jako zwykłe zlecenie HTTP GET. W swojej metodzie doGet() aplet ustawia
typ treści na text/vnd.wap.wml, pobiera parametr code, lokalizuje odpowiadający kod poprzez tabelę
powiązaną, i wreszcie generuje dokument WML zawierający region. Informacja numeru kierunkowego pochodzi
z ręcznie wprowadzanej tablicy, która jest konwertowana na tabelę Properties po inicjalizacji apletu. „Zrzut”
ekranu wydruku wyjściowego, pokazano na rysunku 6.4.
Rysunek 6.4.
Wykorzystanie WAP-u do lokalizacji rozmówców

Poznajmy WAP
Jeżeli chcielibyśmy nauczyć się czegoś więcej na temat tego jak się tworzy strony obsługujące urządzenia WAP,
sięgnijmy po książkę Martina Frosta pt. „Learning WML & WMLScript”. Pomocne mogą również być dla nas
strony źródłowe takie jak: http://www.wap-resources.net, http://AnywhereYouGo.com oraz
http://www.wirelessdevnet.com. Istniej również administrowana przez „Sun” lista adresowa archiwizująca na
http://archives.java.sun.com/archives/web-wireless.html.

Obrazki
Ludzie to istoty postrzegające świat głównie poprzez obraz — chcą oglądać, nie tylko czytać informacje. W
konsekwencji jest prawie niemożliwym znalezienie strony WWW, nie wykorzystującej w jakiś sposób obrazków,
niestety najczęściej są to obrazki wykonane nieprofesjonalnie. Podsumowując zacytujmy wyświechtany banał
„Obraz jest wart tysiące słów”.
Szczęśliwie aplet może względnie łatwo przesłać obrazek jako odpowiedź. Właściwie już poznaliśmy aplet,
który to robi: aplet ViewResource, opisany w rozdziale 4 „Odczytywanie informacji”. Jak na pewno pamiętamy
aplet ten może odesłać wszystkie pliki pod dokumentem katalogu macierzystego serwera. Jeżeli zdarzy się tak, że
plik będzie plikiem obrazu, aplet wykrywa ten fakt za pomocą metody getMimeType() a następnie ustawia
swoją treść odpowiedzi za pomocą metody setContentType(), jeszcze przed przesłaniem nieprzetworzonych
bitów do klienta.
Wymaganiem potrzebnym do zastosowania powyższej techniki potrzeba, aby żądane przez nas pliki były już
zapisane na dysku, co nie zawsze jest naszym udziałem. Często aplet musi wygenerować lub przesunąć plik
przed przesłaniem go klientowi. Rozpatrzmy przykład strony, która zawiera obrazek zegara analogowego,
wyświetlającego aktualny czas. Oczywiście można by zapisać 720 obrazków (60 minut pomnożone przez 12
godzin) na dysku, a następnie wykorzystać aplet do wysłania jednego — właściwego. Lecz z pewnością nie
powinniśmy tego robić. Zamiast tego powinniśmy napisać aplet, który będzie generował dynamicznie obrazek
cyferblatu oraz wskazówek zegara lub wariantowo — aplet, który ładuje obrazek cyferblatu dodając tylko
wskazówki. Będąc oszczędnymi programistami mamy również do dyspozycji buforowanie obrazka przez aplet
(przez około minutę) ażeby zapisać cykle serwera.

Można sobie wyobrazić wiele powodów, dla których moglibyśmy chcieć, aby aplet odesłał obrazek. Dzięki
generowaniu obrazków, aplet może wyświetlić rzeczy takie jak: stan magazynu z dokładnością do minuty,
aktualny rezultat dla meczu bejzbolowego (opatrzonego w ikonki reprezentujące biegaczy na bazach) czy
graficzną reprezentację stanu butelek Coca-Coli w automacie na bilon. Poprzez manipulowanie wcześniej-
istniejącymi obrazkami aplet może dokonać nawet więcej. Może zmieniać ich podstawowe parametry takie jak:
kolor, rozmiar lub wygląd; może również kombinować kilka obrazków w jeden.

Generowanie obrazków
Załóżmy, że mamy nieprzetworzone dane pikselowe, które chcemy komuś
przesłać. Ktoś zapyta: ”W jaki sposób można tego dokonać?”.
Już odpowiadamy. Załóżmy więc znowu, iż jest to 24-bitowy
obrazek prawdziwych kolorów (3 bity na jeden piksel), oraz że
ma on wymiary 100x100 pikseli. Moglibyśmy zrobić rzecz, która
nasuwa się od razu — przesłać jeden piksel na jeden raz, w
strumieniu 30.000 bitów. Jednak czy to wystarczy? Skąd
odbiorca będzie wiedział co z nimi zrobić? Odpowiedź brzmi:
nie będzie wiedział. Naszym obowiązkiem jest również
poinformowanie, o tym, że przesyłamy wartości pikselowe
prawdziwych kolorów, o tym że zaczynamy od lewego, górnego
rogu, o tym że będziemy przesyłali wiersz po wierszu oraz o
tym, iż każdy wiersz ma długość 100 pikseli. A co, jeżeli
zdecydujemy się na przesłanie mniejszej liczby bitów stosując
kompresję? W takim przypadku musimy poinformować jaki rodzaj
kompresji stosujemy, aby odbiorca mógł zdekompresować obrazek.
I nagle sprawa staje się bardziej skomplikowana.

Na szczęście jest to problem, który został rozwiązany i to rozwiązany na wiele różnych sposobów. Każdy format
obrazu (GIF, JPEG, PNG, TIFF, itd.) odpowiada jednemu rozwiązaniu. Każdy format obrazu określa jeden
standardowy sposób kodowania obrazków, tak że może on później zostać zdekodowany dla oglądania lub
manipulowania. Każda z technik kodowania ma swoje zalety i ograniczenia. Dla przykładu kompresja używana
dla kodowania GIF lepiej obsługuje obrazki generowane komputerowo, jednak forma GIF ograniczony jest do
256 kolorów. Kompresja używana dla kodowania JPEG, sprawdza się znowuż lepiej przy obrazkach foto-
realistycznych, które zawierają miliony kolorów, jednak to lepsze działanie „okupione” jest zastosowaniem
kompresji „stratnej”, która może rozmyć szczegóły zdjęcia. Kodowanie PIN (wymawiane jako „ping”) jest
relatywnie nowym kodowaniem, którego celem jest zastąpienie GIF, jako że jest ono mniejsze, obsługuje miliony
kolorów, wykorzystuje „bezstratną” kompresją, ponadto ma kanał alfa dla efektów przezroczystości — jest
również wolna od problemów związanych z patentami, które były zmorą GIF.*

Zrozumienia na czym polega kodowanie obrazków, pomoże nam zrozumieć sposób w jaki aplety obsługują
obrazki. Aplety takie jak ViewResource są w stanie odesłać wcześniej istniejące obrazki poprzez przesłanie
swojej zakodowanej reprezentacji — niezmienionej do klienta, przeglądarka dekoduje wtedy obrazek do
oglądania. Jednak aplet, który generuje lub modyfikuje obrazek musi skonstruować przedstawienie wewnętrzne
obrazka, rozmieścić go, a następnie, przed przesłaniem go do klienta, zakodować go.

Obrazek „Hello World”


Przykład 6.6 ukazuje prosty przykład apletu, który generuje oraz odsyła obrazek GIF. Grafika ukazuje napis
„Hello World!”, tak jak na rysunku 6.5.

Przykład 6.6.
Grafiki „Hello World”

import java.io.*;
import java.awt.* ;
import javax.servlet.* ;
import javax.servlet.http.* ;

import Acme.JPM.Encoders.GifEncoder;

public class HelloWorldGraphics extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

ServletOutputStream out = res.getOutputStream (); // binarny


wydruk wyjściowy!

Frame frame = null;


Graphics g = null;

try {
// Utwórz niepokazywaną ramkę
frame = new Frame () ;
frame. addNotify ();

// Pobierz obszar grafiki, przy użyciu ramki


Image image = frame.createlmage(400, 60);
g = image.getGraphics() ;

*
Więcej informacji na temat PNG można znaleźć na stronie: http://graphicswiz.com/png.
// Rysuj "Hello World!" do poza-ekranowego kontekstu grafiki

g. setFont (new Font (" Szeryfowy", Font. ITALIC ,48));


g.drawString("Hello World!", 10, 50);

// Zakoduj poza-ekranowy obrazek w GIF i prześlij go klientowi


res.setContentType("obrazek/gif");
GifEncoder encoder = new GifEncoder(image, out);
encoder.encode();
}
finally {
// Wyczyść zasoby
if (g != null) g. dispose();
if (frame i= null) frame.removeNotify() ;
}
}
}

Mimo iż aplet ten wykorzystuje zestaw java.awt, właściwie nigdy nie wyświetla okna na wyświetlaczu serwera.
Nie wyświetla on również okna na wyświetlaczu klienta. Wykonuje on natomiast wszystkie operacje w poza-
ekranowym kontekście grafiki, pozwalając przeglądarce wyświetlać obrazek. Strategia jest następująca:
stworzenie poza-ekranowego obrazka, grafik do, umieszczenie jego grafik w kontekście, rysowanie do kontekstu
grafiki, a następnie zakodowanie wynikłego obrazka dla transmisji do klienta.

Rysunek 6.5.
Grafiki „Hello World”

Otrzymywanie poza-ekranowego obrazka wiąże się z pokonaniem kilku trudności. W Javie, obrazek jest
reprezentowany przez klasę java.awt.Image. Niestety w JDK 1.1, obiekt Image nie może zostać
konkretyzowany bezpośrednio przez konstruktora. Musi on być uzyskany bezpośrednio przez fabryczną metodę
taką jak np. createImage() Component lub metodę getImage() Toolkit. Jako że tworzymy nowy obrazek
używamy do tego celu metody createImage(). Zwróćmy uwagę, iż zanim komponent butworzyć obrazek, jego
własny peer musi już wtedy istnieć. W wyniku tego zanim utworzymy Image musimy utworzyć Frame, utworzyć
peer ramki z wywołaniem do addNotify(), i wreszcie użyć ramki do stworzenia naszego Image.*
W JDK 1.2 cały proces został uproszczony, tak że Image może zostać utworzony bezpośrednio przez
skonstruowanie nowego java.awt.image.BufferedImage. Nasze przykłady w tym rozdziale używają
techniki frame.createImage() dla maksymalnej przenośności we wszystkich wersjach JDK.
Kiedy mamy już obrazek rysujemy na nim używając kontekstu grafiki, którą można pobrać przy użyciu
wywołania do getGraphics(), metody Image. W tym przykładzie rysujemy tylko prosty strumień.

*
Dla serwerów WWW działających na systemach Unix, własny peer ramki musi zostać utworzony w serwerze X. Następnie,
w celu optymalnej ustawienia wydajności, należy upewnić się, że zmienna środowiskowa DISPLAY (która określa, który
serwer X będzie użyty) nie jest ustawiona lub ustawiona na lokalny serwer X, który może wymagać zastosowania komputera
centralnego x lub uwierzytelniania x.
Serwery „Headless”, mogą bez działającego serwera X, mogą użyć Xvfb (wirtualnego bufora ramki X) obsłużyć prace
pomocnicze grafiki; należy tylko pamiętać, żeby wskazać DISPLAY na serwerze Xvfb.
Po rysowaniu do kontekstu grafiki, wywołujemy setContentType() aby ustawić typ nośnika na image/gif
ponieważ mamy zamiar użyć kodowania GIF. Dla przykładów w tym rozdziale użyliśmy kodera GIF napisanego
przez Jefa Poskanzera. Jest to koder napisany solidnie i dostępny na stronie http://www.acme.com.
Zwróćmy uwagę, iż algorytm kompresji LZW użyty dla kodowania GIF jest chroniony patentami „Unisys’u”
oraz IBM-a, które w zgodzie z fundacją wspierającą bezpłatne oprogramowanie, umożliwiają dostęp do
bezpłatnego oprogramowania generującego format GIF. Koder Acme GIF wykorzystuje kompresję LZ, pozwoli
to być może uniknązć patentu. Więcej informacji można znaleźć na stronach:
http://www.fsf.org/philozofy/gif.html i http://www.burnallgifs.org. Oczywiście aplet może zakodować Image na
każdy format obrazu. Dla treści WWW, JPEG i PNG są najbardziej odpowiednimi alternatywami dla GIF.
Istnieje wiele dostępnych koderów JPEG i PNG. Dla użytkowników JDK wersji 1.2 i późniejszych, dostępny jest
koder wbudowany w zestawie com.sun.image.codec.jpeg; kodery dla PNG i innych formatów dostępne są
(dla JDK 1.2) w oficjalnym „Java Advanced Imagining API” na stronie http://java.sun.com/products/java-
media/jai. Dla użytkowników, którzy potrzebują również obsługi JDK 1.1, istnieje program narzędziowy Java
Image Management Interface (JIMI), wcześniej komercyjny produkt „Activated Intelligence”, obecnie „Sun
library” dostępny bezpłatnie na stronie http://java.sun.com/products/jimi.

Aby zakodować obrazek tworzymy obiekt GifEncoder, przekazując mu obiekt Image i


ServletOutputStream dla apletu. Kiedy wywołamy na obiekt GifEncoder metodę encoder(), obrazek
zostanie zakodowany i przesłany do klienta.

Po przesłaniu obrazka, aplet wykonuje to, co wszystkie poprawnie zachowujące się aplety powinny zrobić:
zwalnia swoje wszystkie zasoby graficzne. Będą one automatycznie odzyskane podczas odśmiecania, jednak
zwolnienie ich pomaga systemom przy zasobach ograniczonych. Kod służący do zwolnienia zasobów,
umieszczony został w bloku finally w celu zagwarantowania jego wywołania, nawet w przypadku gdy aplet
zgłasza wyjątek.

Dynamicznie generowany schemat


Teraz przyjrzyjmy się apletowi, który generuje bardziej interesujący obrazek. Przykład tworzy wykres słupkowy,
który porównuje jabłka do pomarańczy, w odniesieniu do ich rocznej konsumpcji.

Przykład 6.7.
Wykres porównujący jabłka do pomarańczy

import java.awt.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

import Acme.JPM.Encoders.GifEncoder;

import javachart.chart.*; // z Techniki Wizualnej

public class SimpleChart extends HttpServlet (

static final int WIDTH = 450;


static final int HEIGHT = 320;

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException ,IOException {
ServletOutputStrearo out = res.getOutputStream();

Frame frame = null;


Graphics g = null;

try {
// Utwórz prosty wykres
BarChart chart = new BarChart ("Jabłka i Pomarańcze");

// Nadaj mu tytuł
chart.getBackground().setTitleFont(new Font("Szeryfowy",
Font.PLAIN, 24));
chart.getBackground().setTitleString("Porównywanie Jabłek i
Pomarańczy");

// Pokaż miejsce i dostosuj jego objaśnienia


chart.setLegendVisible(true) ;
chart.getLegend().setL1X(0.4); // normalizowane z niższego lewego
chart.getLegend(),setLlY(0.75); // normalizowane z niższego lewego
chart.getLegend().setIconHeight(0.04);
chart.getLegend().setIconWidth(0.04);
chart.getLegend().setIconGap(0.02);
chart.getLegend().setVerticalLayout(false) ;

// Nadaj mu jego dane i oznaczenia


double[] appleData = {950, 1005, 1210, 1165, 1255};

chart.addDataSet("Jabłka", appleData) ;

double[] orangeData = {1435, 1650, 1555, 1440, 1595};


chart.addDataSet("Pomarańcze", orangeData) ;

String[] labels = {"1993", "1994", "1995", "1996", "1997"};


chart.getXAxis().addLabels(labels) ;

// Pokoloruj jabłka na czerwono, a pomarańcze na pomarańczowo


chart.getDatasets()[0].getGc().setFillColor(Color.red) ;
chart.getDatasets()[1],getGc().setFillColor(Color.orange);

// Nazwij osie
chart.getXAxis().setTitleString("Rok") ;

chart.getYAxis().setTitleString("Tony Skonsumowane") ;
// Nadaj odpowiedni rozmiar
chart.resize(WIDTH, HEIGHT);

// Utwórz niepokazywaną ramkę


frame = new Frame () ;
frame.addNotify() ;

// Pobierz obszar grafiki o odpowiednim rozmiarze, używając Ramki


Image image = frame.createlmage(WIDTH, HEIGHT);
g = image.getGraphics();

// Poproś wykres aby wrysował się w poza-ekranowy kontekst grafiki


chart.drawGraph(g) ;

// Zakoduj i odeślij to, co zostało namalowane


res. setContentType ("obraz/gif") ;
GifEncoder encoder = new GifEncoder(image, out);
encoder.encode() ;
}
finally (
// Wyczyść zasoby
if (g != null) g.dispose();
if (frame != null) frame.removeNotify();
}
}
}

Na rysunku 6.6 zostały zaprezentowane rezultaty. Właściwie nie ma potrzeby, aby wykres ten był generowany
dynamicznie, mimo to pozwala osiągnąć wiele, bez zbędnego kodowania.
Rysunek 6.6.
Wykres porównujący jabłka do obrazków

Zasady są takie same: utworzyć poza-ekranowy obrazek oraz pobrać jego kontekst grafiki, rysować do tego
kontekstu, a następnie zakodować rysunek do transmisji dla klienta. Różnica polega na tym, że aplet konstruuje
obiekt BarChart do rysowania. W Javie dostępnych jest kilkanaście zestawów tworzących wykresy. Klasa
BarChart, naszego przykładu pochodzi z zestawu „Visual Engineering’s KavaChart” (wcześniej: JavaChart),
który dostępny jest na stronie http://www.ve.com/kavachart. Jest to produkt komercyjny jednakże dla
czytelników niniejszej książki został przyznany bezpłatny dostęp do „porcji” API zaprezentowanej w naszym
przykładzie. Zestaw „KavaChart” zawiera również bezpłatny pakiet apletów generujących wykresy, ze strony
http://www.ve.com/kavachart/servlets.html. Kolejnym dobry zestawem tworzącym wykresy jest zestaw „Sitraki”,
dostępny na stronie http://www.sitraka.com (uprzednio KL Group).

Składanie obrazu
Jak dotąd rysowaliśmy nasze grafiki na puste obrazki. W tym rozdziale omówimy w jaki sposób przyjmować
wcześniej istniejące obrazki i rysować na ich szczycie lub zespalać je w obrazki łączone. Przeanalizujemy
również obsługę błędu w apletach odsyłających obrazki.

Rysowanie obrazków
Czasami aplet może czerpać korzyści z rysowania na górze istniejącego obrazka. Dobrym przykładem może tutaj
być aplet -lokator składowy, który wie gdzie znajduje się każdy pracownik. Kiedy zapytamy gdzie znajduje się
określony pracownik, wyświetli dużą, czerwoną kropkę nad jego biurem.
Jedną ze zwodniczo prostych technik dla rysowania wcześniej istniejących obrazków jest uzyskiwanie obrazków
za pomocą Yoolkit.getDefaultToolkit().getImage(imagename), pobranie jego kontekstu grafiki przy
pomocy wywołania do metody Image – getGraphics(), a następnie wykorzystanie odesłanego kontekstu
grafiki do rysowania na górze obrazka. Niestety nie jest to takie proste jak się może wydawać. Powodem jest to,
że nie można użyć metody getGraphics() jeżeli obrazek nie został utworzony przy pomocy metody
Component createImage(). W przypadku pracy z narzędziami wzorowanymi na Windows AWT, konieczny
jest zawsze własny peer, generujący w tle grafikę.

Istnieje jednak rzecz, którą można wykonać zamiast powyższego: pobranie wcześniej istniejącego obrazka
poprzez metodę Toolkit.getDefaultToolkit().getImage(imagename), a następnie „powiedzenie” mu,
aby narysował się na inny kontekst grafiki utworzony za pomocą metody createImage() Component, tak jak
to zostało w poprzednich dwóch przykładach. Teraz możemy już wykorzystać omawiany kontekst grafiki do
rysowania na górze oryginalnego rysunku.

Przykład 6.8 wyjaśnia na czym polega ta technika na konkretnym przykładzie. Prezentuje on aplet, który pisze
„POUFNE” na każdym rysunku, który odsyła. Obrazek jest przekazywany do apletu jako informacja dodatkowej
ścieżki; aplet załaduje obrazek z odpowiedniej lokalizacji pod serwerowym katalogiem źródłowym dokumentu,
wykorzystując metodę getResource() w celu obsługi rozproszonego wywołania.

Przykład 6.8.
Rysowanie obrazka do oznaczenia go jako poufny

import java.awt.*;
import java.io.*;
import java.net.* ;
import javax.servlet.* ;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

import Acme.JPM.Encoders.GifEncoder;
public class Confidentializer extends HttpServlet {
Frame frame = null;

public void init() throws ServletException {


// Skonstruuj niepokazywaną ramkę wielokrotnego użytku
frame = new Frame();

frame.addNotify() ;
}

public void doGet (HttpServletRequest req, HttpServietResponse res)


throws ServletException, IOException {
ServletOutputStream out = res.getOutputStream();
Graphics g = null;

try {
// Pobierz lokalizację obrazka z informacji ścieżki
// Dla bezpieczeństwa użyj ServletUtils (Rozdział 4)
URL source =
ServletUtils.getResource(getServletContext(), req.getPathInfo()) ;
}

// Ładuj obrazek (z bajtów do obiektu obrazka)


MediaTracker mt = new MediaTracker(frame); // ramka działa jako
ImageObserver
Image image = Toolkit.getDefaultToolkit().getlmage(source);

mt.addlmage(image, 0) ;
try {
mt .waitForAll();
}
catch (InterruptedException e) {
res.sendError(res.SC_INTERNAL_SERVER_ERROR,
"Przerwanie podczas ładowania obrazka: " +
ServletUtils.getStackTraceAsString(e)) ;
return;
}

// Pobierz szerokość oraz długość


int w = image.getWidth(frame) ;
int h = image.getHeight(frame);

// Upewnij się, że odczytujemy poprawne dane obrazu


if (w <= 0 || h <= 0) {
res.sendError(res.SC_NOT_FOUND,
"Informacje dodatkowej ścieżki muszą wskazywać na poprawny
obrazek"),
return;
}

// Skonstruuj odpowiedni rozmiar poza-ekranowego kontekstu grafiki


Image offscreen = frame.createlmage (w, h);
g = offscreen.getGraphics();

// Rysuj obrazek do poza-ekranowego kontekstu grafiki


g.drawlmage( image, 0, 0, frame);

// Napisz POUFNE na jego górze


g.setFont(new Font("Monospaced", Font.BOLD | Fond.ITALIC, 30));
g.drawString("POUFNE", 10, 30);

// Zakoduj poza-ekranowe grafiki w GIF i prześlij je do klienta


res.setContentType("obraz/gif") ;
GifEncoder encoder = new GifEncoder(off screen, out);
encoder.encode()

}
finally {
// Wyczyść zasoby
if (g != null) g.dispose();
}
}

public void destroy() (


// Wyczyść zasoby
if (frame != null) frame.removeNotifyt() ;
}
}
Przykładowy wydruk wyjściowy można zobaczyć na rysunku 6.7.

Rysunek 6.7.
Rysowanie obrazka, w celu oznaczenia go jako poufny

Jak widać aplet ten przeprowadza każdy krok, dokładnie tak jak opisaliśmy to wcześniej, dodając trochę
tematyki gospodarstwa domowego. Aplet tworzy swoją niepokazywaną ramkę w metodzie init().Tworzenie
Frame, a następnie powtórne wykorzystywanie to optymalizacja, nie przeprowadzona uprzednio z powodów
estetyczno-porządkowych. Dla każdego zlecenia aplet zaczyna od odczytania nazwy wcześniej istniejącego
obrazka z informacji dodatkowej ścieżki, a następnie konwertuje ścieżkę do zasobu używając metody
getResource(). Następnie pobiera odwołanie do obrazka za pomocą metody Toolkit — getImage() i
fizycznie ładuje go do pamięci korzystając z pomocy MediaTracker.Zwykle ładowanie asynchroniczne nie jest
problemem dla obrazka, ponieważ jego częściowe rezultaty malowane są wraz z postępem ładowania, jednak w
tym przypadku obrazek jest malowany tylko raz i musimy mieć pewność, że zostanie on z góry załadowany w
całości. W następnej kolejności aplet pobiera szerokość i długość ładowanego obrazka i tworzy poza-ekranowy
obrazek do dopasowania. I wreszcie — decydująca chwila: ładowany obrazek jest rysowany na górze nowo-
utworzonego, pustego. Potem wszystko dzieje się szybko. Aplet pisze swoje wielkie „POUFNE” i koduje
obrazek do transmisji. Zauważmy w jaki sposób aplet radzi sobie z warunkami występowania błędu, wywołując
metodę sendError(). Podczas odsyłania obrazków trudno o coś bardziej wyszukanego. Takie ujęcie sprawy
pozwala apletowi na wykonanie tego, co uważa za słuszne.

Zestawianie obrazków
Aplet mają również możliwość zestawiania obrazków w jeden — obrazek kombinowany. Wykorzystując tą
możliwość aplet-lokator składowy mógłby wyświetlić uśmiechnięta twarz pracownika — zamiast czerwonej
kropki nad jego biurem. Technika używana przy zestawianiu obrazków, przypomina używaną do rysowania na
górze obrazka: odpowiednie obrazki są ładowane, rysowane na utworzonym w odpowiedni sposób obiekcie
Image, i w końcu obrazek ten jest kodowany do transmisji.

Przykład 6.9 pokazuje jak wykonać coś podobnego dla apletu, który wyświetla liczbę wywołań jako sekwencję
osobnych liczbowych obrazków kombinowanych w jeden duży. Obrazki liczbowe, używane przez wspomniany
aplet, dostępne są na http://www.geocities.com/SiliconValley/6742, razem z kilkoma innymi stylami.

Przykład 6.9.
Zestawianie obrazków do jednego — kombinowanego

import java.awt.*;
import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.* ;

import com.oreilly.servlet.ServletUtils;

import Acme.JPM.Encoders.GifEncoder;

public class GraphicalCounter extends HttpServlet {

public static final String DIR = "/images/odometer";


public static final String COUNT = "314159";

public void doGet(HttpServletRequest reg, HttpServletResponse res)


throws ServletException, IQException {

ServletOutputStream out = res.getOutputStream ();

Frame frame = null;


Graphics g = null;

try {
// Pobierz liczenia do wyśeietlenia, musi być wartość wyłączna w ciągu
// Lub użyj domyślnej
String count = (String)req.getQueryString();
if (count == null) count = COUNT;
int countlen = count.length() ;
Image images[] = new Image[countlen];

for (int i = 0; i < countlen; i++) {


URL imageSrc =
getServletContext().getResource(DIR + "/" + count. charAt(i) +
"GIF")
if (imageSrc == null) {
imageSrc = new URL (" plik:"); // znak-wypełniacz, błędy obsłuż póżniej
}
images[i] = Toolkit.getDefaultToolkit().getImage(imageSrc) ;
}

// Utwórz niepokazywaną ramkę


frame == new Frame () ;
frame.addNotify () ;

// Ładuj rysunki
MediaTracker mt = new MediaTracker(frame) ;
for (int i = 0; i < countlen; i++) {
mt.addImage(images[i], i) ;
}

try {
mt .waitForAll() ;
}
catch (InterruptedException e) {
res. sendError (res. SC_INTERNAL_SERVER_ERROR,
"Przerwanie podczas ładowania obrazka: " +
ServletUtils.getStackTraceAsString(e)) ;
return;
}

// Sprawdź czy nie pojawiają się problemy podczas ładowania obrazków


if (mt.isErrorAny()) {
//Mieliśmy problem ustalić który obrazek(i)
StringBuffer problemChars = new StringBuffer() ;
for (int i = 0; i < countlen; i++) {
if (mt.isErrorID(i)) {
problemChars.append(count.charAt(i)) ;
}
}
res.sendError(res.SC_INTERNAL_SERVER_ERROR,
"Nie można było załadować obrazka dla tych znaków: " +
probleinChars. toString ()) ;
return;
}

// Pobierz zbiorowy rozmiar obrazków


int width = 0;
int height = 0;
for (int i = 0; i < countlen; i++) {
width += images[i].getWidth(frame) ;
height = Math.max(height, images[i].getHeight(frame));
}

// Pobierz obszar grafiki do dopasowania, używając Ramki


Image image = frame.createImage(width, height);

g = image.getGraphics() ;

// Rysuj obrazki
int xindex = 0;
for (int i = 0; i < countlen; i++) {
g. drawlmage(images[i], xindex, 0, frame) ;
xindex += images [i].getWidth (frame);
}

// Zakoduj i odeślij kompozyt


res.setContentType("obrazek/gif") ;
GifEncoder encoder = new GifEncoder(image, out)
encoder.encode() ;
}
finally {
// Wyczyść zasoby
if (g != null) g.dispose();
if (frame != null) frame.removeNotify();

Strumień wyjściowy został pokazany na rysunku 6.8.

Rysunek 6.8.
Kombinowanie obrazków do graficznego licznika
Aplet ten pobiera liczbę do wyświetlenia poprzez odczytanie nieprzetworzonego ciągu
zapytań. Dla każdej liczby w liczeniu, aplet wyszukuje i ładuje odpowiadający jej obrazek
liczbowy z katalogu podanego przez DIR. (DIR znajduje się zawsze pod serwerowym
katalogiem macierzystym dokumentu. Jest on podany jako ścieżka wirtualna i konwertowany
na abstrakcyjną ścieżkę zasobu.) Następnie oblicza kombinowaną szerokość i maksymalną
wysokość wszystkich tych obrazków i konstruuje poza-ekranowy rysunek do dopasowania.
Aplet rysuje każdy obrazek liczbowy, na przemian, od lewa do prawa. W końcu koduje
obrazek do transmisji.

Aby na coś się przydać aplet ten musi zostać wywołany przez inny aplet, który zna liczbę
wywołań, która ma być wyświetlona i umieszcza tą liczbę w ciągu zapytań. Dla przykładu
aplet ten może zostać wywołany przez stronę JSP lub inną, dynamicznie utworzoną stronę
przy użyciu składni jak poniżej:
<IMG SRC="/aplet/Licznik Graficzny?121672">
Aplet obsługuje warunki powstania błędu w ten sam sposób, w jaki robi to aplet poprzedni,
poprzez wywołanie metody sendError() i pozostawienie jej serwerowi aby zachowywał się w
poprawny sposób.

Obrazki — efekty
Dowiedzieliśmy się już w jaki sposób aplety mogą tworzyć i kombinować obrazki. W tym
podrozdziale dowiemy się jak apety mogą wykonywać specjalne efekty związane z obrazkami.
Aplet może na przykład ograniczyć czas transmisji obrazka, poprzez zmianę jego rozmiaru
przed transmisją. Może on również dodać specjalne cieniowanie do obrazka, aby
przypominał on przycisk, który można nacisnąć. Dla przykładu przyjrzyjmy się w jaki sposób
aplet może konwertować obrazek kolorowy na czarno biały.

Konwertowanie obrazka na „czerń i biel”


Na przykładzie 6.10 został ukazany aplet, który przed odesłaniem obrazka, konwertuje go na
skalę czarno-białą. Aplet wykonuje ten efekt właściwie bez tworzenia poza-ekranowego
kontekstu grafiki. Zamiast tego tworzy obrazek używając specjalnego ImageFilter (filtru
obrazu). (Celowo nie zamieszczamy tutaj obrazków, ponieważ nie wyglądały by one najlepiej
w czarno białej książce).

Przykład 6.10.
Efekt obrazkowy — konwertowanie obrazka na czarno białą skalę

import java.awt.* ;
import java.awt. image.* ;
import java.io.* ;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils ;

import Acme.JPM.Encoders.* ;

public class Decolorize extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("obraz/gif") ;
ServletOutputStream out = res.getOutputStream();

// Pobierz lokalizację obrazka z informacji ścieżki


URL source = ServletUtils.getResource(getServletContext(),
req.getPathInfo()) ;

if (source == null) {
res.sendError(res.SC_NOT_FOUND,
"Informacja dodatkowej ścieżki musi wskazywać na dodatkowy
obrazek");
return;
}

// Skonstruuj niepokazywaną ramkę


// Nie będzie żadnej addNotify()ponieważ jej peer nie jest
potrzebny
Frame frame = new Frame () ;

// Ładuj obrazek
Image image = Toolkit.getDefaultToolkit().getImage(source);
MediaTracker mt = new MediaTracker(frame);
mt. addImage (image, 0);
try {
mt .waitForAll<) ;
}
catch (InterruptedException e) {

res. sendError (res. SC_INTERNAL_SERVER_ERROR,


"Przerwanie podczas ładowania pliku: " +
ServletUtils.getStackTraceAsString(e)) ;
return;
}

// Pobierz rozmiar obrazka


int width = image. getWidth( frame) ;
int height = image.getHeight(frame);

// Upewnij się, że odczytujemy poprawne dane


if (width <= 0 || height <= 0) {
res.sendError(res.SC_NOT_FOUND,
"Informacje dodatkowej ścieżki muszą wskazywać na poprawny
obrazek") ;
return;
}

// Utwórz obrazek do dopasowania, zastosuj filtr


Image filtered = frame.createImage(
new FilteredImageSource(image.getSource(),
new GrayscaleImageFilter()));

//Zakoduj i odeślij przefiltrowany obrazek


GifEncoder encoder = new GifEncoder(filtered, out);
encoder.encode() ;
}
}

Kod dla tego apletu pokrywa się w większości z kodem przykładu Confidentialer. Główna różnica została
zaprezentowana poniżej:

// Utwórz obrazek do dopasowania, Zastosuj filtr


Image filtered = frame.createImage(
new FilteredImageSource(image.getSource(),
new GrayscaleImageFilter())) ;

Aplet nie używa metody Component — createImage(int, int), którą wykorzystywaliśmy dotychczas.
Zamiast tego czerpie korzyści z innej metody Component — createImage(ImageProducer). Aplet tworzy
„image producer” za pomocą FilteredImageSource, które następnie przekazuje obrazek poprzez
GrayscaleImageFilter. Filtr konwertuje każdy kolorowy piksel na jego czarno biały odpowiednik. Dzięki
temu obrazek jest konwertowany na skalę czarno białą w czasie kiedy jest tworzony. Kod dla
GrayscaleImageFilter został pokazany na przykładzie 6.11.

Przykład 6.11.
Klasa „GrayscaleImageFilter”

import java.awt.* ;
import java.awt.image.*;

public class GrayscaleImageFilter extends RGBImageFilter {

public GrayscaleImageFilter() {
canFilterIndexColorModel = true;
}

// Konwertuj kolorowe piksele na skalę czarno białą


// Algorytm jest zgodny ze specyfikacją NTSC
public int filterRGB(int x, int y, int pixel) {

// Pobierz średnie natężenie RGB


int red = (pixel & OxOOffOOOO) » 16;
int green = (pixel & OxOOOOffOO) » 8;
int blue = pixel & OxOOOOOOff;

int luma = (int) (0.299 * red + 0.587 * green + 0.114 * blue);

// Odeślij wartość luma dla każdego komponentu RGB


// Uwaga: (Przezroczystość) Alfa jest zawsze ustalona na wartość
maksymalną (nie przeżroczysta)
return (Oxff « 24) | (luma « 16) | (luma « 8) | luma;
}
}

Dla każdej wartości w mapie kolorów, filtr ten otrzymuje wartość pikselową i odsyła nową przefiltrowaną
wartość pikselową. Dzięki ustawieniu zmiennej canFilterIndexColorModel na true, zaznaczamy, że filtr
ten działa na mapie kolorów a nie na poszczególnych wartościach pikselowych. Wartość pikselowa podana jest
jako 32-bitowe int, gdzie pierwszy bajt reprezentuje wartość (przezroczystości) alfa, drugi bajt natężenie
czerwieni, trzeci oktet natężenie zieleni, a czwarty bajt natężenie koloru niebieskiego. Aby konwertować wartość
pikselową na skalę czarno białą, natężenia czerwieni, zieleni i koloru niebieskiego muszą zostać ustalone na takie
same wartości. Można również obliczyć wartość średnią dla wartości czerwieni, zieleni oraz koloru niebieskiego,
a następnie zastosować średnią wartość natężenia każdego koloru. Spowodowałoby to konwersję obrazka na
skalę czarno białą. Jeżeli jednak weźmiemy pod uwagę sposób, w jaki ludzie postrzegają kolory (i inne
czynniki), okaże się jednak, że potrzebna jest średnia ważona. Ustawianie według priorytetów 0,299 0,587 0,114,
które zostało tutaj zastosowane odpowiada temu, które jest stosowane przez Komitet Państwowego Systemu
Telewizyjnego USA (National Television Systems Committee) dla telewizji czarno białej. Więcej na ten temat
można znaleźć w książce Charlesa A. Poyntona pt. „A Technical Introduction to Digital Video” oraz na stronie
http://www.color.org.

Buforowanie przekonwertowanego obrazka


Proces tworzenia i kodowani obrazka może być rzeczą kosztowną, biorąc pod uwagę zarówno czas jak i cykle
CPU serwera. Buforowanie zakodowanych obrazków zawsze znacznie podnosi wydajność. Zamiast
wykonywania wszystkich czynności dla każdego zlecenia, rezultaty mogą zostać zapisane i przesłane ponownie
dla późniejszych zleceń. Idea tarczy zegara, o której wspomnieliśmy wcześniej może tutaj być idealnym
przykładem. Obrazek zegara musi być tworzony co najwyżej raz na minutę. Wszystkim innym zleceniom
złożonym w tej minucie może zostać przesłany ten sam obrazek. Wykres dla tabulacji głosowania stanowi
następny przykład. Jest on tworzony raz, a następnie zmieniany wtedy kiedy przychodzą nowe głosy.

Nadklasa com.oreilly.servlet.CacheHttpServlet, z rozdziału 3 „Czas istnienia (cykl życia) apletu”


dostarcza prosty mechanizm buforujący dla obrazków oraz dla tekstu. Aplet generujący obrazek zegara mógłby
rozszerzyć CacheHttpServlet oraz wdrożyć metodę getLastModified(), odsyłającą czas, w którym został
oddany ostatni głos.

Nasz przykład Decolorize mógłby rozszerzyć CacheHttpServlet za pomocą metody getLastModified


(), która odsyła czas ostatniej zmiany zasobu obrazka. Niestety CacheHttpServlet buforuje tylko ostatnią
odpowiedź, dając niewiele korzyści, w (prawdopodobnej) sytuacji, w której aplet DeColorize będzie wywołany
na więcej niż jeden obrazek. DeColorize powinien prawdopodobnie używać bardziej wyszukanego algorytmu
buforującego do obsługi obrazków wielokrotnych. Cykl życia apletu sprawia, że jest to całkiem proste. Nasz
aplet DeColorize może zapisać każdy przekonwertowany obrazek tablicę bitów,
przechowywaną w Hashtable nastrojonego przez nazwę obrazka. Najpierw aplet musi
utworzyć zmienną kopii Hashtable. Musi ona zostać zdeklarowana poza metodą doGet():

Hashtable gifs = new Hashtable() ;

W celu wypełnienia tej tablicy-przewodnika musimy utrwalić zakodowane grafiki. Tak więc zamiast podawania
GifEncoder ServletOutputStream, podajemy mu ByteArrayOutputStream. Następnie kodujemy
obrazek za pomocą metody encode(), zakodowany obrazek jest przechowywany w
ByteArrayOutputStream. Potem składujemy utrwalone bity w tablicy-przewodniku, by w końcu napisać je do
ServletOutputStream i odesłać obrazek do klienta. Oto nowy kod do kodowania, składowania oraz odsyłania
przefiltrowanego obrazka:

// zakoduj, składuj i odeślij przefiltrowany obrazek


ByteArrayOutputStream baos = new ByteArrayOutputStream(1024;; / 1K initial
GifEncoder encoder = new GifEncoder(filtered, baos);
encoder.encode () ;
gifs.put(source, baos) ;
baos.writeTo(out) ;

Taki kod wypełnia tablicę-przewodnika zakodowanymi obrazkami nastrojonymi przez nazwę


obrazka. Teraz, tak jak wcześniej w aplecie, możemy przejść bezpośrednio do pamięci
podręcznej, kiedy zostałaby poproszona o odesłanie wcześniej zakodowanego obrazka. Kod
ten powinien znajdować się bezpośrednio po kodzie wykonanym jeżeli source==null (źródło
=zero).
// Zwarcie jeżeli to zostało wykonane wcześniej
if (gifs.containsKey(source)) {

ByteArrayOutputStream baos = (ByteArrayOutputStream) gifs.get(source);


baos.writeTo(out);
return;
}

W trakcie podobnych modyfikacji każdy obrazek, który znajdzie się w pamięci podręcznej jest szybko odsyłany,
bezpośrednio z pamięci.

Oczywiście buforowanie obrazków wielokrotnych pociąga za sobą zużywanie ogromnych ilości pamięci.
Buforowanie pojedynczego obrazka nie jest problemem, jednakże aplet taki jak ten powinien użyć w tym celu
jakiejś metody. Dla przykładu mógłby on buforować jedynie 100 ostatnich obrazków, na które składano zlecenia.
Bardziej rozbudowana wersja tego apletu, mogłaby również sprawdzać znacznik czasu pliku, aby upewnić się, że
oryginalny obrazek nie zmienił się od czasu buforowania czarno białej wersji.

Treść skompresowana
Zestaw java.util.zip został wprowadzony w JDK 1.1. Zestaw ten zawiera klasy, które
obsługują odczytywanie i pisanie formatów kompresji ZIP i GZIP. Mimo iż zestaw ten został
dodany w celu obsługi Plików Archiwalnych Javy (Java Archive Files) — JAR, zapewnia on
również apletowi wygodny, standardowy sposób przesyłania treści skompresowanej.

Końcowy użytkownik nie widzi żadnej różnicy pomiędzy treścią skompresowaną, a treścią nie skompresowaną,
ponieważ pierwsza jest dekompresowana przez przeglądarkę przed wyświetleniem. Jednak mimo, iż prezentuje
się tak samo, może przysłużyć się użytkownikowi poprzez ograniczanie czasu ładowania treści z serwera. Dla
bardzo „kompresowalnych” treści, takich jak HTML, kompresja może zredukować czas transmisji poprzez
dyrektywę rozmiaru. Uświadomijmy sobie, że dynamiczne kompresowanie treści zmusza serwer do
wykonywania dodatkowej pracy, tak więc ewentualne przyspieszenie transmisji musi wiązać z obniżeniem
wydajności serwera.
Jak pamiętamy, aplet może przesyłać nagłówek Content-Type jako część swojej odpowiedzi, ażeby
poinformować klienta o tym jaki typ ma przesyłana informacja. Aby przesłać treść skompresowaną, aplet musi
również przesłać nagłówek Content-Encoding, aby poinformować klienta o schemacie, zgodnie z którym
treść została zakodowana. Możliwe schematy kodowania specyfikacji HTTP 1.1 to: gzip (lub x-gzip),
compress (lub x-compress) oraz deflate.
Nie wszyscy klienci rozumieją wszystkie kodowania. Aby poinformować aplet o tym, który schemat kodowania
jest rozumiany przez klienta, klient może przesłać nagłówek Accept-Encoding, który określa akceptowane
przez klienta schematy kodowania jako oddzielaną przecinkami listę. Nie wszystkie przeglądarki (wyłączając,
kilka które obsługują kodowanie skompresowane) dostarczają ten nagłówek. Na ten moment aplet musi
zdecydować, że bez tego nagłówka nie prześle treści skompresowanej lub że musi zbadać nagłówek User-
Agent, aby stwierdzić czy przeglądarka obsługuje kompresję. Większość spośród obecnych, popularnych
przeglądarek (jednak nie wszystkie) obsługuje kodowanie gzip, żadna nie obsługuje jednakże kodowania
kompresji, a tylko MIcrosoft Internet Explorer 4 lub 5 (w Windowsie) obsługuje kodowanie typu deflate. W
prostym określeniu zdolności przeglądarki, w tym tego czy obsługuje GZIP, można posłużyć się
„BrowserHawk4J” — produktem „Cyscape”. Ten komercyjny produkt wykrywa obsługę GZIP, jak również
wiele innych właściwości przeglądarki, takich jak: typ przeglądarki, wersję przeglądarki, system operacyjny
klienta, zainstalowane moduły dodatkowe, szerokość i wysokość przeglądarki, szerokość i wysokość monitora
klienta, wyłączenie cookies, zablokowany JavaScript, a nawet prędkość połączenia. Więcej na:
http://www.cysape.com/products/bh4j.5
Mimo iż uzgadnianianie, który format kompresji ma zostać użyty może wiązać się ze sporą ilością logiki,
właściwie rzecz biorąc trudno sobie wyobrazić łatwiejszy sposób przesyłania treści skompresowanej. Aplet po
prostu zawija swój servletOutputStream przy pomocy GZIPOutputStream lub ZIPOutputStream.
Pamiętajmy aby wywołać out.close() kiedy nasz aplet pisze wydruk wyjściowy, tak żeby właściwy dla
formatu kompresji znak kończący został napisany.
Przykład 6.12 ukazuje aplet ViewResource z rozdziału 4, powtórnie napisany, tak żeby mógł przesyłać
skompresowaną treść kiedy jest to możliwe. Nie zamieszczamy tym razem zrzutu ekranu, ponieważ nie ma na
nim nic ciekawego do oglądania. Tak jak powiedzieliśmy wcześniej, użytkownik końcowy nie jest w stanie
rozpoznać czy serwer przesłał treść skompresowaną — może z wyjątkiem ograniczonych czasów ładowania.
Przykład 6.12.
Przesyłanie treści skompresowanej

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.zip.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.ServletUtils ;

public class ViewResourceCompress extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException (
OutputStream out = null;

// Wybierz właściwe kodowanie treści oparte na


// nagłówku klienta Accept-Encoding. Wybierz GZIP jeżeli nagłówek
// zawiera "gzip". Wybierz ZIP jeżeli nagłówek zawiera "compress".
// W przeciwnym wypadku nie wybieraj żadnej kompresji. Upewnij się,
że // Content-Encoding wykorzystuje przedrostek "x-" wtedy i
tylko wtedy // jeżeli robi to Accept-Encoding.
String encodings = req.getHeader("Accept-Encoding");
if (encodings != null && encodings.indexOf("gzip") != -1) {
//Startuj z GZIP
if (encodings.index0f("x-compress") != -1) {
res.setHeader("Content-Encoding", "x-compress") ;
}
else {
res.setHeader("Content-Encoding ", "compress") ;
}
out = new ZipOutputStream(res.getOutputStream()) ;

5
((ZipOutputStream)out),putNextEntry(new ZipEntry("nazwa pusta"));
}
else {
// Nie kompresuj
out = res.getOutputStreamO ;
}
res.setHeader("Vary", "Accept-Encoding") ;

// Pobierz zasób do przeglądania


URL url = null;
try {
url=ServletUtils.getResource(getServletContext(), req.getPathInfo());
}
catch (IOException e) (
res.sendError(
res. SC_NOT_FOUMD,
"Informacje dodatkowej ścieżki muszą wskazywać na poprawny zasób do przeglądania:
" +

e.getMessaget));
}

// Połącz z zasobem
URLConnection con = url.openConnection();
con.connect() ;

// Pobierz i ustal typ zasobu


String contentType = con.getContentType();
res-setContentTypetcontentType);

// Odeślij zasób
try {
ServletUtils.retumURL(url, out);
}

catch (IOException e)
res.sendError(res.Sc_INTERNAL_SERVER_ERROR,
"Problem przy przesyłaniu zasobu: " + e.getMessage());
}

// Pisz znak kończący i zamknij strumień wyjściowy


out.closet);
}
}

Aplet rozpoczyna od zdeklarowania null OutputStream, a następnie ustala to OutputStream na


GZIPOutputStream lub ZipOutputStream lub na ServletOutputStream, w zależności od otrzymanego
nagłówka Accept-Encoding.
Jako że aplet wybiera wydruk wyjściowy, który ma zostać przez niego użyty, ustala odpowiedni nagłówek
Content-Encoding. Podczas przesyłania treści skompresowanej, nagłówek ten musi być ustawiony dla klienta
w sposób umożliwiający działanie odpowiedniego algorytmu dekompresji. Aplet ustala również nagłówek Vary
na wartość Accept-Encoding, aby „być grzecznym” i poinformować klienta, że aplet różnicuje swój wydruk
wyjściowy w zależności od nagłówka Accept-Encoding. Większość klientów ignoruje ten nagłówek.
Po tej wczesnej logice, aplet może traktować wydruk wyjściowy jako kolejny OutputStream. Aplet mógłby
zawinąć strumień za pomocą PrintStream lub PrintWriter, mógłby również Przekazać go do GifEncoder.
Jednak bez względu na to co zrobi, musi wywołać metodę out.close(), kiedy już zakończy przesyłanie treści.
Wywołanie to pisze właściwy znak kończący do skompresowanego strumienia.
Jest jednak pewien rodzaj treści, która nie powinna być kompresowana. Dla przykładu obrazki GIF i JPEG, są
już kompresowane jako część ich kodowania, tak więc powtórne ich kompresowanie jest bezcelowe (a niektóre
przeglądarki mają problemy przy wyświetlaniu obrazków przesłanych w skompresowanej formie). Ulepszona
wersja apletu FileViewCompressed byłaby w stanie wykryć kiedy odsyła obrazek, i zaniechałaby dalszej
kompresji

Serwer cykliczny
Do tej pory każda strona, odesłana przez aplet zawierała tylko jeden typ treści. Jednak tak wcale nie musi być.
Dlaczego aplet miałby nie odsyłać kilku stron, każda zawierająca inny typ treści, wszystkie znajdujące się w
jednej odpowiedzi? To może wydawać się nie do zrealizowania, jednak przy użyciu techniki zwanej serwerem
cyklicznym (server push) jest to całkiem proste.
W przypadku stosowania tej techniki serwer umieszcza na stosie lub przesyła sekwencję stron odpowiedzi do
klienta. Porównajmy tą technikę do techniki z poprzedniego rozdziału — ściągania klienckiego, w przypadku
której to techniki klient musi otrzymać lub ściągnąć każdą stronę z serwera. Mimo iż rezultaty zastosowania obu
technik są podobne dla użytkownika — pojawienie się sekwencji stron, szczegóły wdrażania oraz ich poprawne
zastosowania są odmienne dla każdej z nich.
Przy stosowaniu serwera cyklicznego, połączenie gniazdowe pomiędzy serwerem a klientem pozostaje otwarte
do czasu przesłania ostatniej ze stron. Takie rozwiązanie daje serwerowi możliwość szybkiego przesyłania
uaktualnień strony oraz kontrolę nad tym, w jakim czasie wspomniane uaktualnienia będą przesyłane. Z takimi
możliwościami technika „serwer cykliczny” nadaje się idealnie do stron, które wymagają częstych uaktualnień
(takich jak animacje wykonywane na poziomie podstawowym) lub dla stron, które wymagają kontrolowanych
przez serwer, jednak nie częstych uaktualnień (takich jak uaktualnienia aktywnego statusu). Zwróćmy jednak
uwagę, iż metoda serwer cykliczny nie jest jeszcze obsługiwana przez Microsoft Internet Explorera i powinno się
unikać jej szerokiego stosowania jako, iż okazało się to szkodliwe dla liczenia dostępnego portu serwera.
Przy stosowaniu klienckiego ściągania. połączenie gniazdowe jest przerywane po każdej stronie, tak więc
odpowiedzialność za uaktualnianie stron spada na klienta. Klient wykorzystuje wartość nagłówkową Refresh,
przesłaną przez serwer, aby określić kiedy przeprowadzić swoje uaktualnienie, tak więc technika klienckie
ściąganie jest najlepszym rozwiązaniem dla stron, które nie wymagają częstych uaktualnień lub, które mają
uaktualnienia w znanych przerwach.
Serwer cykliczny może również okazać się pomocny dla animacji o ograniczonej długości oraz dla uaktualnień
statusu, wykonywanych w czasie rzeczywistym. Weźmy dla przykładu aplet, który jest w stanie przesłać do
serwera cztery najświeższe pogodowe mapy satelitarne, tworząc animację poziomu podstawowego. Spróbujmy
sobie przypomnieć aplet PrimeSearcher z rozdziału 3, zastanówmy się w jaki sposób moglibyśmy zastosować
serwer cykliczny, aby zaznaczyć ograniczoną liczbę klientów, bezpośrednio po tym jak serwer znajdzie każdą
nową liczbę pierwszą.
Na przykładzie 6.13 został zaprezentowany aplet, który wykorzystuje serwer cykliczny do wyświetlania
odliczania czasu pozostałego do startu rakiety. Rozpoczyna się to wysłaniem serii stron odliczających od 10 do
1. Każda strona zastępuje swoją poprzedniczkę. Kiedy odliczanie dotrze do 0, aplet przesyła zdjęcie startu
rakiety. Aplet używa klasy użyteczności com.oreilly.servlet.MultipartResponse (pokazanej na
przykładzie 6.14), aby rozwiązać problemy związane ze szczegółami serwera cyklicznego.
Przykład 6.13.
Odliczanie do startu rakiety

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.MultipartResponse;
import com.oreilly.servlet.ServletUtils;

public class Countdown extends HttpServlet {

static final String LAUNCH = "/images/launch.gif";

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
ServletOutputStream out = res. getOutputStream (); / / trochę
// binarnego wydruku wyjściowego
// Przygotuj odpowiedź wieloczęściową
MultipartResponse multi = new MultipartResponse(res);

// Najpierw prześlij odliczanie


for (int i = 10; i > 0; i--) {
multi.startResponse("tekst/zwykły") ;
out.println(i + "...");
multi.endResponse() ;
try ( Thread.sleep(lOOO); } catch (InterruptedException e) { }
}

// Następnie prześlij obrazek startu


multi.startResponse("obraz/gif") ;
try {
ServletUtils.returnFile(req.getRealPath (LAUNCH), out) ;
}
catch (FileNotFoundException e) {
throw new ServletException ("Nie można było odnaleźć pliku:
" + e.getMessage());
}
// Pamiętaj aby zakończyć odpowiedź wieloczęściową
multi.finish() ;
}
}

Klasa MultipartResponse ukrywa większość paskudnych, uciążliwych szczegółów związanych ze


stosowaniem techniki serwer cykliczny. Możemy swobodnie stosować tą klasę w naszych własnych apletach. Jak
widzieliśmy na poprzednim przykładzie jej stosowanie jest całkiem proste.

Najpierw tworzy się obiekt MultipartResponse, przekazując mu obiekt odpowiedzi apletu. Klasa
MultipartResponse wykorzystuje obiekt odpowiedzi aby załadować wydruk wyjściowy apletu oraz aby
ustawić typ treści odpowiedzi. Następnie, dla każdej strony treści, zaczynamy od wywołania metody
startResponse() i przekazania jej typu treści dla tej strony. Treść dla tej strony przesyłamy, jak zwykle do
strumienia wyjściowego. Wywołanie do endResponse() kończy stronę i wysyła treść z bufora, tak że klient
może ją zobaczyć. Na tym etapie możemy dodać wywołanie do metody sleep(), lub jakiś inny rodzaj
opóźnienia, stosowanego do czasu w którym następna strona będzie gotowa do wysłania. Wywołanie do metody
endResponse() nie jest konieczne, ponieważ metoda startResponse() rozpoznaje czy poprzednia
odpowiedź została zakończona i zakańcza ją w razie potrzeby. W przypadku gdy spodziewamy się opóźnienia
pomiędzy czasem zakończenia jednej odpowiedzi a początkiem następnej, powinniśmy jednak wywołać metodę
endResponse(). Pozwala to klientowi na wyświetlanie ostatniej odpowiedzi, podczas oczekiwania na następną.
Ostatecznie, po przesłaniu wszystkich stron, wywołanie do metody finish() kończy wieloczęściową
odpowiedź i przesyła kod informujący klienta, że nie żadne odpowiedzi nie będą już przesyłane.

Przykład 6.14 zawiera kod dla klasy MultipartResponse.

Przykład 6.14.
Klasa „MultipartResponse”

package com.oreilly.servlet;

import java.io.* ;
import javax.servlet.* ;
import jvax.servlet.http.* ;

public class MultipartResponse {


HttpServletResponse res ;
ServletOutputStream out;
boolean endedLastResponse = true;

public MultipartResponse(HttpServletResponse response) throws


IOException {
// Zapisz obiekt odpowiedzi i strumień wyjściowy
res = response;
out = res.getOutputStream() ;

// Ustaw wszystko
res. setContentType ("mulltipart/x-mixed-replace; boundary=End") ;
out.println() ;
out.println (" --End") ;
}

public void startResponse(String contentType) throws IOException {


// W razie konieczności zakończ ostatnią odpowiedź
if (!endedLastResponse) {
endResponse() ;
}
// Rozpocznij następną
out.println("Content-Type"+ contentType) ;
out.println() ;
endedLastResponse = false;
}

public void endResponse() throws IOException (


// Zakończ ostatnią odpowiedź i opróżnij bufor, tak żeby klient
mógł // zobaczyć treść
out.println() ;
out.println("--End") ;
out.flush() ;
endedLastResponse = true;
}

public void finish() throws IOException {


out.println("--End-") ;
out.flush();
}
}
Rozdział 7. Śledzenie sesji

HTTP jest protokołem bezstanowym: nie ma wbudowanej funkcji, która umożliwiałaby


serwerowi rozpoznawanie, że cała sekwencja pochodzi w całości od tego samego
użytkownika. Zwolennicy poszanowania prywatności mogą postrzegać taką sytuację
pozytywnie, jednakże większość programistów WWW uważa to za wielki kłopot, jako że
aplikacje WWW nie są bezstanowe. Rozbudowane aplikacje WWW muszą współdziałać z
klientem zarówno wstecznie jak i z wyprzedzeniem, zapamiętując informacje o nim pomiędzy
zleceniami. Klasycznym przykładem może tutaj być aplikacja koszyka zakupów — klient musi
mieć możliwość wkładania artykułów do swojego wirtualnego koszyka, a serwer musi
zapamiętać te artykuły, aż do czasu kiedy klient wyloguje się kilka stron zleceniowych później
(często trwa to nawet kilka dni!).
Problem stanu HTTP można najlepiej zrozumieć wyobrażając sobie szerokie forum dyskusji,
na którym jesteśmy gościem honorowym. Następnie wyobraźmy sobie, na tym forum,
dziesiątki internautów rozmawiających z nami w tym samym czasie. Wszyscy zadają nam
pytania, odpowiadając na nasze, powodując że klepiemy w klawiaturę jak opętali usiłując
odpowiedzieć wszystkim. Teraz wyobraźmy sobie, z kolei, że kiedy ktoś do nas pisze forum
dyskusyjne nie podaje jego tożsamości, jedyne co widzimy to mieszanina pytań i odpowiedzi.
W forum tego typu najlepsze co możemy zrobić to prowadzenie prostych konwersacji, np.
poprzez odpowiadanie na bezpośrednie pytania. Jeżeli będziemy próbowali zrobić coś więcej,
np. zadać swoje pytanie, może okazać się, że nie będziemy wiedzieli w którym momencie
nadejdzie odpowiedź. Na tym właśnie polega problem stanu HTTP. Serwer HTTP „widzi”
tylko serię zleceń, potrzebuje więc dodatkowej pomocy aby dowiedzieć się kto właściwie
składa zlecenie.*
Rozwiązaniem, jak już może się zdążyliśmy domyśleć, jest przedstawienie się klienta w
momencie składania każdego zlecenia. Każdy klient musi dostarczyć niepowtarzalny
identyfikator, który umożliwi serwerowi jego identyfikację, mogą to być również informacje,
które pozwolą serwerowi poprawnie obsłużyć zlecenie. Wykorzystując przykład forum
dyskusji, każdy uczestnik musi rozpocząć swoje każdą wypowiedź od czegoś na wzór: „Cześć,
Mam na imię Jason i...” lub „Cześć, właśnie zapytałem o twój wiek i...”. Jak się przekonamy
podczas lektury tego rozdziału, istnieje wiele sposobów umożliwiających klientom „HTTP-
owym” na przesłanie wstępnych informacji wraz z każdym zleceniem.
Pierwsza połowa tego rozdziału poświęcona będzie tradycyjnym technikom śledzenia sesji,
wykorzystywanych przez programistów CGI: uwierzytelnianie użytkownika, ukryte pola
danych formularza, przepisywanie URL-u oraz trwałe cookies. Natomiast druga połowa
rozdziału omawia wbudowaną obsługę dla śledzenia sesji w Interfejsie API. Obsługa ta jest
zbudowana na szczycie tradycyjnych technik i znacznie upraszcza zadanie śledzenia sesji w

*
Jeżeli ktoś zastanawia się nad tym, dlaczego serwer HTTP nie może zidentyfikować klienta poprzez adres IP komputera
łączącego, odpowiedź brzmi: zgłoszonym adresem IP może być adres serwera pośredniczącego lub serwera, który obsługuje
wielokrotnych użytkowników.
naszych apletach. Wszystkie omówienia zawarte w tym rozdziale opierają się na założeniu, że
używany jest serwer pojedynczy. Rozdział 12 „Aplety Przedsiębiorstw i J2EE” wyjaśnia jak
jest realizowana obsługa stanu wspólnej sesji w serwerach wielo-wejściowych.

Uwierzytelnianie użytkownika
Jednym ze sposobów przeprowadzenia śledzenia sesji jest wykorzystanie informacji
pochodzącej z uwierzytelniania użytkownika. Uwierzytelnianie użytkownika zostało
omówione w rozdziale 4 „Odczytywanie Informacji”, jednak w celu przypomnienia,
uwierzytelnianie użytkownika stosowane jest kiedy serwer WWW ogranicza dostęp do
niektórych ze swoich zasobów tylko do tych klientów, którzy logują się przy użyciu
określonego hasła i nazwy użytkownika. Po zalogowaniu się klienta, nazwa użytkownika jest
dostępna dla apletu poprzez metodę getRemoteUser().
Możemy wykorzystać nazwę użytkownika do śledzenia sesji klienta. Kiedy użytkownik już się
zaloguje, przeglądarka pamięta jego nazwę i odsyła ją wraz z hasłem kiedy użytkownik
przegląda kolejne strony witryny WWW. Aplet może zidentyfikować użytkownika
wykorzystując jego nazwę, tym samym może również wyśledzić jego sesję. Dla przykładu
kiedy użytkownik doda coś do swojego wirtualnego koszyka na zakupy, fakt ten może zostać
zapamiętany (we wspólnej klasie lub np. w zewnętrznej bazie danych) i wykorzystany w
przyszłości przez inny aplet — kiedy użytkownik wejdzie na stronę końcową.
Aplet, który wykorzystuje uwierzytelnianie użytkownika, mógłby dla przykładu dodać artykuł
do koszyka użytkownika, przy pomocy następującego kodu:
String name = req.getRemoteUser();
if (name == null) {
// Wyjaśnij, że administrator serwera powinien chronić tą stronę
}
else (
String[] items = req.getParameterValues("artykuł");
if (items != null) {
for (int i = 0; i < items.length; i++) (
addItemToCart(name, items[i]) ;
}
}
}

Inny aplet, może w przyszłości odczytać artykuły z koszyka zakupów za pomocą następującego kodu:

String name = req.getRemoteUser();


if (name == null) {
// Wyjaśnij, że administrator serwera powinien chronić tą stronę
}
else (
String[] items = getItemsFromCart(name);
}

Największą zaletą wykorzystywania uwierzytelniania użytkownika w celu przeprowadzania


śledzenia sesji jest prosta implementacja. Po prostu mówimy serwerowi, żeby chronił
określony zestaw stron (zgodnie z instrukcjami z rozdziału 8 „Bezpieczeństwo”), a następnie
stosujemy metodę getRemoteUser() w celu identyfikacji każdego klienta. Kolejna zaleta jest
taka, iż technika ta działa nawet kiedy użytkownik wchodzi na naszą stronę z różnych
komputerów. Technika ta działa również kiedy użytkownik nie łączy się z naszą witryną lub
kiedy zamyka przeglądarkę przed powrotem na naszą stronę.

Największym mankamentem uwierzytelniania użytkownika jest fakt, iż każdy użytkownik musi zarejestrować się
na konto, a następnie logować się za każdym razem kiedy odwiedza naszą witrynę. Większość użytkowników
toleruje rejestrowanie i logowanie się jako zło konieczne kiedy uzyskują dostęp do informacji poufnych,
jednakże w przypadku zwykłego śledzenia sesji jest to prawdziwa uciążliwość. Kolejna uciążliwość to fakt, że
podstawowe uwierzytelnianie HTTP nie zapewnia żadnego mechanizmu wylogującego; użytkownik musi
zamknąć swoją przeglądarkę żeby się wylogować. Problemy, w przypadku uwierzytelniania użytkownika stwarza
niemożność utrzymania przez użytkownika więcej niż jednej sesji na tej samej stronie (witrynie) WWW.
Wniosek z powyższych rozważań nasuwa się sam: koniecznie potrzebujemy innych rozwiązań dla obsługi
śledzenia anonimowego konta i dla obsługi uwierzytelnionego śledzenia sesji z wylogowaniem.

Ukryte pola danych formularza


Jednym ze sposobów obsługi śledzenia anonimowego konta jest zastosowanie ukrytych pól danych formularza.
Jak sama nazwa wskazuje, są to pola dodane do formularza HTML, które nie są wyświetlane w przeglądarce
klienta. Pola te są odsyłane do serwera, kiedy formularz zawierający je, zostaje przedłożony. Ukryte pola danych
formularza dołączamy przy pomocy HTML-u podobnego do poniższego:

<FORM ACTION="/ servlet /MovieFinder" METHOD="POST">


...
<INPUT TYPE=hidden NAME="zip" VALUE=" 94040 ">
<INPUT TYPE=hidden MAME="poziom" VALUE="expert">
...
</FORM>

W pewnym sensie ukryte pola danych formularza definiują zmienne stałe dla formularza. Dla
apletu otrzymującego przedłożony formularz nie ma żadnej różnicy pomiędzy ukrytym polem
danych a polem, które jest widoczne.

Przy zastosowaniu ukrytych pól danych formularzowych, możemy przepisać nasze aplety
koszyka zakupów, tak że użytkownicy będą mogli dokonywać zakupów anonimowo, do czasu
zakończenia. Na przykładzie 7.1 została zaprezentowana technika wykorzystująca aplet, który
wyświetla zawartość koszyka zakupów użytkownika i pozwala użytkownikowi na dodanie
większej liczby artykułów lub na zakończenie zakupów. Przykładowy widok ekranu został
przedstawiony na rysunku 7.1.

Przykład 7.1.
Śledzenie sesji przy użyciu ukrytych pól danych formularza

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ShoppingCartViewerHidden extends HttpServlet {

public void doGet(HttpServletReguest req, HttpServletResponse res)


throws ServletException, IOException {
res. setContentType (" tekst/html") ;
PrintWriter out = res.getWriter();

out.println("<HEAD><TITLE>Current Shopping Cart


Items</TITLE></HEAD>");
out.println("<BODY>") ;

// Artykuły z koszyka przekazywane są jako parametr artykułu.


String [ ] items = req.getParameterValues ("artykuł") ;

// Drukuj bieżące artykuły z koszyka.


out.println("Aktualnie w swoim koszyku masz następujące artykuły
cart:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else (
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]);
}
out.println"</UL>") ;
}

// Spytaj czy użytkownik chce jeszcze dodać jakieś artykuły, czy


też // chce zakończyć.
// Dołącz bieżące artykuły jako ukryte pola danych, tak żeby mogły
być // przekazane.
out.println("<FORM ACTION=\"/servlet/ShoppingCart\" METHOD=POST>");
if (items != null) {
for (int i = 0; i < items.length; i++) {
out.println("<INPUT TYPE=HIDDEN NAME="artykuł" VALUE=\"" +
items[i] + "\">");
}
}
out.println("Chciałbyś<BR>");
out.println("<INPOT TYPE=SUBMIT VALUE=\" Dodaj Więcej Artykułów \">");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończ \">");
out.println("</FORM>")

out.println("</BODY></HTML>");

}
}

Rysunek 7.1.
Zawartość koszyka na zakupy

Aplet zaczyna od czytania artykułów, które już znajdują się w koszyku, używając getParameterValues
("item"). Przypuszczalnie wartości parametrów artykułów zostały przesłane do tego apletu przy wykorzystaniu
ukrytych pól danych. Następnie aplet wyświetla użytkownikowi bieżące artykuły i pyta go czy chce jeszcze
jakieś dodać, czy też chce już zakończyć zakupy. Pytania zadawane są za pomocą formularza zawierającego
ukryte pola danych, tak więc adresat formularza (aplet ShoppingCart) otrzymuje aktualne artykuły jako część
przedłożenia.

W sytuacji, w której sesji klienta towarzyszy coraz więcej informacji, przekazywanie ich za pomocą ukrytych pól
danych formularza może okazać się uciążliwe. W takich sytuacjach, można przesłać tylko jedną, określoną
identyfikację (ID) sesji, która umożliwi zidentyfikowanie określonej (niepowtarzalnej) sesji klienta. ID sesji
może być częścią kompletnych informacji o sesji, które są przechowywane na serwerze.

Zwróćmy uwagę, iż identyfikacje sesji muszą być przechowywane jako informacje tajne serwera, ponieważ klient
znający ID sesji innego klienta mógłby, używając sfałszowanego ukrytego pola danych formularza, przyjąć drugą
tożsamość. W konsekwencji identyfikacje sesji powinny być generowane w taki sposób, żeby nie dało się ich
łatwo odgadnąć lub sfałszować, a bieżące ID sesji powinny być chronione, tak więc dla przykładu, nie
upubliczniajmy loginu dostępu serwera, ponieważ zarejestrowane URL-e mogą zawierać identyfikacje sesji dla
formularzy, przedłożonych za pomocą zleceń GET.
Ukryte pola danych formularza mogą zostać wykorzystane do wdrożenia uwierzytelniania bez wylogowania się.
Prezentujemy po prostu formularz HTML jako ekran logujący, kiedy użytkownik został już raz uwierzytelniony
przez serwer, jego tożsamość może zostać powiązana z jego określonym ID sesji. Przy wylogowywaniu się ID
sesji może zostać usunięte (poprzez nie przesyłanie do klienta późniejszych formularzy) lub po prostu nie
zapamiętane. Rozwiązanie to zostało szerzej omówione w rozdziale 8.

Zaletą ukrytych pól danych formularza jest ich powszechność oraz obsługa „anonimowości”. Ukryte pola danych
są obsługiwane we wszystkich popularnych przeglądarkach, ponadto mogą być wykorzystywane przez klientów,
którzy się nie zarejestrowali lub nie zalogowali. Główną, z kolei, wadą tej techniki jest fakt, że sesja utrzymuje
się tylko poprzez sekwencję dynamicznie generowanych formularzy. Sesja nie może być utrzymana za pomocą
dokumentów statycznych, dokumentów przesłanych jako e-mail, dokumentów z których utworzono zakładki czy
za pomocą zamknięć przeglądarki.

Przepisywanie URL-u
Przepisywanie URL-u jest kolejnym sposobem na obsługę śledzenia anonimowego konta. W
przypadku stosowania przepisywania URL-u, każdy lokalny URL, na który kliknie użytkownik,
jest dynamicznie modyfikowany lub przepisywany — w celu dołączenia dodatkowych
informacji. Te dodatkowe informacje mogą być w formie informacji dodatkowej ścieżki, w
formie parametrów dodanych lub w formie jakiejś własnej, serwerowo — specyficznej zmiany
URL-u. Z powodu ograniczonej przestrzeni dostępnej dla przepisywania URL, dodatkowe
informacje są zwykle ograniczane do ID określonej sesji. Dla przykładu, poniższe URL-e
zostały przepisane aby przekazać identyfikację sesji 123:*
http://server:port/servlet/Rewritten original
http://server:port/servlet/Rewritten/123 extra path infonnation
http://server:port/servlet/Rewritten?sessionid=123 added parameter
http://server:port/servlet/Rewritten;jsessionid=123 custom change

Każda technika przepisywania ma swoje złe i dobre strony. Informacja dodatkowej ścieżki
działa poprawnie na wszystkich serwerach, z wyjątkiem sytuacji kiedy serwer musi użyć tą
informację jako informację prawdziwej ścieżki. Zastosowanie parametru dodanego również
sprawdza się na wszystkich serwerach, jednak może ono powodować konflikt nazw
parametrów. Standardowa, serwerowo-specyficzna zmiana działa we wszystkich warunkach
dla apletów, które obsługują tą zmianę.

Przykład 7.2 prezentuje poprawioną wersję naszego „shopping cart viewer”, który
wykorzystuje przepisywanie URL-u (w formie informacji dodatkowej ścieżki) w celu
anonimowego śledzenia koszyka zakupów.

Przykład 7.2.
Śledzenie sesji za pomocą przepisywania URL-u

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ShoppingCartViewerRewrite extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IQException {
res.setContentType (" tekst/html") ;
PrintWriter out = res.getWriter();

*
Pamiętajmy, że wartości ID sesji powinny być trudne do odgadnięcia i sfałszowania. Identyfikacja 123 nie jest dobrym
przykładem ID sesji, nadaje się tylko jako przykład książkowy.
out.println("<HEAD><TITLE>Current Shopping Cart
Items</TITLE></HEAD>");
out.println("<BODY>");

// Pobierz bieżące ID sesji lub, w razie konieczności, wygeneruj


String sessionid = req.getPathInfo();
if (sessionid == null) {
sessionid = generateSessionId();
}

// Artykuły z koszyka związane są z ID sesji


String[] items = getItemsFromCart(sessionid);

// Drukuj bieżące artykuły z koszyka.


out.println("Aktualnie masz w swoim koszyku nastepujące
artykuły:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else {
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]) ;
}
out.println("</UL>") ;
}

// Spytaj użytkownika czy chce jeszcze dodać jakieś artykuły, czy też //
zakończyć.
// Dołącz ID sesji do URL-u operacyjnego.
out.println("<FORM ACTION=\"/servlet/ShoppingCart/" + sessionid+
"\" METHOD=POST>") ;
out.println("Czy chcesz<BR>");
out.println("<INPUT TYPE=SUHMIT VALUE=\" Dodać Więcej Artykułów \">"),
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończyć \">");
out.println("</FORM>") ;

// zaproponuj stronę pomocy. Dołącz ID sesji do URL-u.


out. println ("W celu uzyskania pomocy kliknij <A
HREF=\"/servlet/Help/" + sessionid+
"?topic=ShoppingCartViewerRewrite\">here</A>");
out.println("</BODY></HTML>") ;
}

private static String generateSessionId() {


String uid = new java.rmi.server. UID().toStringO; // gwarantowana
niepowtarzalność
return Java.net.URLEncoder.encode(uid); // zakoduj wszystkie
specjalne // znaki
}

private static String[] getItemsFromCart(String sessionid) {


// Nie wdrożone
}
}

Pierwszą rzeczą wykonywaną przez ten aplet jest pobranie aktualnej identyfikacji sesji przy użyciu metody
IDgetPathInfo(). Jeżeli ID sesji nie jest określone, aplet wywołuje generateSessionId(), aby
wygenerować nową, niepowtarzalną identyfikację sesji, wykorzystując w tym celu specjalnie do tego celu
zaprojektowaną klasę RMI. ID sesji jest również wykorzystywane do załadowania i wyświetlenia aktualnych
artykułów z koszyka. Identyfikacja jest później dodawana do atrybutu formularza ACTION, tak że może on zostać
odczytany przez aplet ShoppingCart. Identyfikacja sesji jest również dodawana do URL-u pomocy, który
wywołuje aplet Help. Coś takiego nie było możliwe przy ukrytych polach danych formularza, ponieważ aplet
Help nie jest adresatem przedłożenia formularza.
Złe i dobre strony przepisywania URL-u ściśle pokrywają się z dobrymi i złymi stronami ukrytych pól danych
formularza. Obie techniki są stosowane we wszystkich typach przeglądarek, pozwalają na dostęp anonimowy
obie również mogą zostać użyte do wdrożenia uwierzytelniania z wylogowaniem się. Główna różnica między
tymi technikami polega na tym, że przepisywanie URL-u działa dla wszystkich dynamicznie utworzonych
dokumentów, takich jak aplet Help, a nie tylko, jak ma to miejsce w przypadku ukrytych pól danych formularza
— dla formularzy. Dodatkowo, przy poprawnej obsłudze serwera, własne przepisywanie URL-u może działać,
nawet z dokumentami statycznymi. Niestety często przepisywanie URL-u bywa męczące.
Trwałe cookies
Czwarta z kolei technika umożliwiająca śledzenie sesji wiąże się z trwałymi cookies. Cookie
(ciasteczko) to bit informacji przesyłany do przeglądarki przez serwer WWW, który następnie
może zostać odczytany z tej przeglądarki. Kiedy przeglądarka otrzymuje cookie, zapisuje go, i
następnie odsyła go z powrotem do serwera, za każdym razem kiedy wchodzi na jakąś jego
stronę, zgodnie z pewnymi zasadami. Ponieważ wartość cookie może jednoznacznie
zidentyfikować klienta, cookies są często używane do śledzenia sesji.

Cookies zostały jako pierwsze wprowadzone w Netscape Navigator’ze. Mimo iż nie były one oficjalną częścią
specyfikacji HTTP, szybko stały się, de facto, standardem we wszystkich popularnych przeglądarkach, włącznie
z Netscape 0.94 Beta (i późniejszych) oraz Microsoft Internet Explorer’ze 2 (i późniejszych). Obecnie grupa
robocza Internet Engineering Task Force (IETF) — HTTP Working Group, pracuje nad przekształceniem
cookies w oficjalny, napisany w RFC 2109, standard. Więcej informacji o cookies można znaleźć w specyfikacji
Netscape'a — Netscape's cookie specification, na stronie http://home.netscape.com/newsref/sts/cookie_spec.html
i w RFC 2109, na stronie http://www.ietf.org/rfc/rfc2109.txt. Inną, godną polecenia stroną poświęconą cookies,
jest http://www.cookiecentral.com.

Praca z cookies
Interfejs API ofreuje klasę javax.servlet.http.Cookie, dla pracy z cookies. Szczegóły nagłówka HTTP
dla cookies, są obsługiwane przez Interfejs API. „Ciasteczko” (cookie) tworzymy przy pomocy konstruktora
Cookie():

public Cookie(String name, String value)

Konstruktor ten tworzy nowe cookie z początkową wartością i nazwą. Zasady dla
poprawnych nazw i wartości podane są w specyfikacji „Netscape’s cookie specyfication” i w
RFC 2109.

Aplet może przesłać „ciasteczko” do klienta przekazując obiekt Cookie do metody


HttpServletResponse — addCookie():

public void HttpServletResponse.addCookie(Cookie cookie)

metoda ta dodaje określone cookie do odpowiedzi. Dodatkowe „ciasteczka” są dodawane poprzez kolejne
wywołania do metody addCookie(). Ponieważ cookie są wysyłane przy użyciu nagłówków HTTP, powinny
zostać dodane do odpowiedzi zanim jeszcze zostanie ona zatwierdzona. Od przeglądarek wymaga się, żeby
przyjmowały 20 cookies na witrynę WWW, 300 (wszystkich) na użytkownika, mogą one ponadto ograniczyć
rozmiar każdego „ciasteczka” do 4096 bajtów.

Kod potrzebny do ustawienia cookie prezentuje się w poniższy sposób:

Cookie cookie = new Cookie("ID", "123");


res.addCookie(cookie) ;

Aplet pobiera cookies poprzez wywołanie metody HttpServletRequest — getCookies():

public Cookie[] HttpServletRequest.getCookies()


Metoda ta odsyła tablicę obiektów Cookie, zawierających wszystkie cookies przesłane przez przeglądarkę jako
część zlecenia lub pustą tablicę — jeżeli żadne cookies nie zostały przesłane. Wersje Interfejsu API, z przed 2.1
powodowały, że metoda getCookies() odsyłała null, jeżeli żadne cookies nie zostały przesłane. Dla
maksymalnej kompatybilności najlepiej jest przyjąć null, albo jakąkolwiek pustą tablicę.
Kod, potrzebny do ściągania cookies wygląda w następujący sposób:

Cookie[] cookies = req.getCookies();


if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
String name = cookies[i].getName() ;
String value = cookies[i].getValue();
}
}

Niestety, w standardowym Interfejsie API nie ma żadnej metody umożliwiającej ściągnięcie wartości cookie
poprzez nazwę. Poza jego wartością i nazwą, możemy ustawić wiele atrybutów dla cookie. Poniższe metody
używane są do ustawiania tych atrybutów. Jak można się przekonać przeglądając uzupełnienie B „HTTP Interfejs
API — Krótkie Omówienie” dla każdej metody „set” istnieje odpowiadająca jej metoda „get”. Metody „get” są
rzadko używane, ponieważ kiedy cookie jest przesyłane do serwera, zawieraja tylko swoje imię, wartość i wersję.
Jeżeli ustawimy atrybut na cookie, otrzymane od klienta, musimy dodać go do odpowiedzi, żeby zmiana doszła
do skutku, powinniśmy również dopilnować, aby wszystkie atrybuty z wyjątkiem nazwy, wartości i wersji zostały
wyzerowane, podobnie jak cookie.

public void Cookie.setversion(int v)

ustala wersję „ciasteczka”. Aplety mogą przesyłać i otrzymywać cookies sformatowane, tak
aby pasować zarówno do „Netscape persistent cookies” (Wersja 0) lub nowszego, jednak
trochę eksperymentalnego RFC 2109 cookies (Wersja 1). Nowo zaprojektowane cookies
domyślne dla wersji 0, utworzone dla zmaksymalizowania interoperacyjności.
public void Cookie.setDomain(String pattern)

określa wzór ograniczenia domeny. Wzór domeny, z kolei, określa które serwery powinny
zobaczyć cookie. Domyślnie „ciasteczka” są odsyłane tylko do komputera centralnego który
je zapisał. Określanie wzoru nazwy domeny przesłania to. Wzór musi rozpoczynać się kropką
i zawierać co najmniej dwie kropki. Wzór pokrywa się tylko z jedną pozycją, poza początkową
kropką. Dla przykładu .foo.com jest poprawne i pokrywa się z www.foo.com i z
upload.foo.com, lecz nie z www.upload.foo.com.* Więcej o wzorach domen można znaleźć w
specyfikacji „Netscape’s cookie specyfication i z RFC 2109.
public void Cookie.setMaxAge(int expiry)

określa maksymalny wiek cookie w sekundach, po którym traci ono ważność. Wartość
negatywna wskazuje domyślnie, że „ciasteczko” powinno się „przeterminować” kiedy
przeglądarka „wychodzi”. Wartość zerowa informuje przeglądarkę, że ma natychmiast
wyzerować cookie.
public void Cookie.setPath(String uri)

*
Technicznie rzecz ujmując, zgodnie ze specyfikacją „Netscape cookie specyfication”, dla domen najwyższej klasy
wymagane są dwie kropki, domenami tymi mogą być .com, .edu, .net, .org, .gov, .mil, i .int, natomiast dla pozostałych domen
wymagane są trzy kropki. Reguła nie pozwala serwerowi na przypadkowe lub szkodliwe ustawianie cookie dla domen .co.uk
i przekazywanie ich do wszystkich przedsiębiorstw w Wielkiej Brytanii, dla przykładu. Niestety prawie wszystkie
przeglądarki ignorują metodę trzech kropek. Więcej na ten temat na stronie
http://homepages.paradise.net.nz~glineham/cookiemonster.html.
— określa ścieżkę dla cookie, które jest podzbiorem URI, do których cookie powinno być wysłane. Domyślnie
„ciasteczka” są wysyłane do strony, która ustawia cookie oraz do wszystkich stron w tym katalogu lub pod tym
katalogiem. Dla przykładu, jeżeli /servlet/CookieMonster ustawi cookie, wartością domyślną ścieżki będzie /
servlet. Ścieżka ta informuje o tym, że cookie powinno być przesłane do /servlet/Elmo oraz do /
servlet/subdir/BigBird, jednak nie do zamiennika apletu /Oscar.html lub do jakichkolwiek programów pod /cgi-
bin. Ścieżka ustalona na / powoduje iż cookie jest przesyłane do wszystkich stron na serwerze. Ścieżka cookie
musi zawierać aplet, który ustawia cookie.

public void Cookie.setSecure(boolean flag)

informuje czy cookie powinno zostać przesłane wyłącznie przez bezpieczny kanał, taki jak SSL. Wartością
domyślną jest false.

public void Cookie.setComment(String comment)

ustawia pole komentarza „ciasteczka”. Komentarz opisuje cel, dla którego cookie zostało stworzone.
Przeglądarki WWW mogą zdecydować się na wyświetlenie tego tekstu użytkownikowi. Komentarze nie są
obsługiwane przez wersję „0” cookies.

public void Cookie.setValue(String newValue)

— przypisuje nową wartość do cookie. W przypadku wersji „0” cookies, wartości nie powinny zawierać: spacji,
nawiasów kwadratowych, nawiasów zwykłych, znaków równości, przecinków, cudzysłowów, ukośników,
znaków zapytania, znaków @, dwukropków oraz średników. Puste wartości nie mogą zachowywać się w ten sam
sposób na wszystkich przeglądarkach.

Robienie zakupów przy pomocy trwałych cookies


Przykład 7.3 ukazuje wersję naszego „shopping cart viewer”, zmodyfikowanego aby
obsługiwać koszyk zakupów przy użyciu trwałych cookies.

Przykład 7.3.
Śledzenie sesji wykorzystujące trwałe cookies

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ShoppingCartViewerCookie extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException (
res.setContentType (" tekst/html") ;
PrintWriter out = res.getWriter() ;
// Pobierz aktualne ID sesji poprzez przeszukanie otrzymanych
cookies.
String sessionid = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("sessionid")) {
sessionid = cookies[i].getValue() ;
break;
}
}
}

// Jeżeli ID sesji nie zostało przesłane, wygeneruj je.


// Następnie prześlij go do klienta razem z odpowiedzią.
if (sessionid == null) {
sessionid = generateSessionId();
Cookie c = new Cookie("sessionid", sessionid);
res.addCookie(c);
}

out.println("<HEAD><TITLE>Current Shopping Cart


Items</TITLE></HEAD>");
out.println("<BODY>") ;

// Artykuły z koszyka są związane z ID sesji


String[] items = getItemsFronCart(sessionid);

// Drukuj aktualne artykuły z koszyka.


out.println("aktualnie masz w koszyku następujące artykuły
cart;<BR>");
if (items == null) {
out. println (" <B>None< /B>") ;
)
else {
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]) ;
)
out.println("</UL>") ;
}

// Spytaj czy chcą dodać jeszcze jakieś artykuły czy też zakończyć // zakupy.
out.println. ("<FORM ACTION=\"/servlet/ShoppingCart\" METHOD=POST>°);
out.println("Czy chcesz <BR>");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Dodać Więcej Artykułów \">");
out.println ("<INPUT TYPE=SUBMIT VALUE=\" Check Out \">");
out.println("</FORM>") ;

// Zaproponuj stronę pomocy.


out.println("Dla pomocy, kliknij <A HKEF=\"/servlet/Help" +
"?topic=ShoppingCartViewerCookie\">here</A>") ;

out.println( "</BODY></HTML>") ;
}

private static String generateSessionId() {


String uid = new java.rmi.server.UID().toString(); // gwarantowana
// niepowtarzalniość
return java.net.URLEncoder.encode(uid); // zakoduj wszystkie
specjalne znaki
}
private static String[] getItemsFromCart (String sessionid) {
// Nie wdrożono
}
}
Aplet ten próbuje najpierw ściągnąć identyfikację klienta, przechodząc kolejno przez cookies,
które otrzymał jako część swojego zlecenia. Jeżeli żadne „ciasteczko” nie zawiera ID sesji,
aplet generuje nowe identyfikację, wykorzystując metodę generateSessionId() i dodając
cookie zawierające nowe ID sesji do zlecenia. Pozostała część apletu pokrywa się z wersją —
przepisywanie URL-u, z wyjątkiem wersji, która nie przeprowadza przepisywania.
Trwałe cookie oferują elegancki, prosty i wydajny sposób wdrażania śledzenia sesji.
„Ciasteczka” zapewniają najprostsze z możliwych wprowadzeń dla każdego zlecenia. Dla
każdego również zlecenia, cookie może automatycznie dostarczyć identyfikację klienta lub
nawet listę preferencji klienta. Dodatkowo, zdolność „Ciasteczek” do dostosowywania, daje
im dodatkową przewagę i urozmaicenie.
Największym mankamentem cookies jest fakt, iż przeglądarki nie zawsze je akceptują. Czasem
jest to spowodowane tym, że po prostu nie obsługują „Ciasteczek”. Częściej jednak dzieje się
tak z powodu tego, że użytkownik ma skonfigurowaną przeglądarkę aby odrzucała cookies (w
grę może wchodzić, np. chęć zachowania prywatności). Urządzenia WAP, z powodu
ograniczonej objętości pamięci, również obecnie nie obsługują „Ciasteczek”, a bramy WAP
dopiero niedawno zaczęły radzić sobie z nimi, w imieniu swoich urządzeń. Jeżeli któryś z
naszych klientów nie zaakceptuje cookies, powinniśmy wtedy odwołać się do rozwiązań
omówionych wcześniej w tym rozdziale.
API — śledzenie sesji
Szczęśliwie dla nas, projektantów apletów, aplety nie muszą wykorzystywać do obsługi
swoich własnych sesji, technik które omówiliśmy wcześniej. Interfejs API dostarcza wielu
metod oraz klas zaprojektowanych specjalnie w celu obsługi krótko terminowego śledzenia
sesji, w imieniu apletów. Inaczej mówiąc aplety mają wbudowane krótko terminowe śledzenie
sesji.*
Śledzenie Sesji API, jako że powołujemy się na fragment Interfejsu API, poświęcony śledzeniu
sesji, powinno być obsługiwane przez wszystkie serwery WWW obsługujące aplety. Jednakże
stopień obsługi zależny jest od serwera. Większość serwerów obsługuje śledzenie sesji
poprzez stosowanie trwałych cookies, zachowując zdolność powrotu do przepisywania URL-
u, w sytuacji gdy „Ciasteczko” zawiedzie. Niektóre serwery pozwalają na to, aby obiekty sesji
były zapisywane na dysku serwera lub, w razie wypełnienia pamięci lub wyłączenia serwera,
w bazie danych. (Artykuły które umieszczamy w sesji, aby opcja ta przynosiła korzyści,
wymagają implementacji interfejsu Serializable). Szczegóły związane z naszym serwerem
można znaleźć w jego dokumentacji. Pozostała część tego podrozdziału opisuje funkcję —
najniższy wspólny mianownik, wymagany przez Interfejs API 2.2 dla nie rozproszonych
serwerów. Rozproszone śledzenie sesji zostało omówione w rozdziale 12.

Podstawy śledzenia sesji


Śledzenie sesji jest zadziwiająco „eleganckie”. Każdy użytkownik jest związany z obiektem
javax.servlet,http.HttpSession, który może zostać wykorzystany przez aplety do
przechowywania i pobierania informacji dotyczących użytkownika. W obiekcie sesji możemy
zapisać dowolny zestaw obiektów Javy. Dla przykładu obiekt sesji użytkownika zapewnia
wygodną lokalizację do przechowywania (przez aplet) zawartości koszyka na zakupy
użytkownika lub, jak się przekonamy czytając rozdział 9 Podłączalność do bazy danych”
połączenie z bazą danych użytkownika.
W celu zapewnienia niezależności działania aplikacji WWW, sesje są „kalibrowane” na
poziomie aplikacji WWW. Oznacza to, iż każdy ServletContext utrzymuje swoją własną pulę
kopii HttpSession oraz że aplet działający wewnątrz jednego kontekstu nie ma dostępu do
informacji sesji zapisanej przez aplet w innym kontekście.
Aplety wykorzystują metodę getSession() swojego obiektu zlecenia, w celu pobrania
bieżącego obiektu HttpSession:
public HttpSession HttpServletRequest.getSession(boolean create)
Metoda ta odsyła bieżącą sesję, związaną z użytkownikiem składającym zlecenie. W
przypadku gdy użytkownik nie ma żadnej aktualnej sesji, metoda ta tworzy ją, jeżeli create
jest prawdziwe (true) lub odsyła null (zero) — gdy create jest fałszywe (false).
Standardowym zastosowaniem tej metody jest getSession (true), tak więc w celu
uproszczenia, w Interfejsie API 2.1 nie wprowadzono żadnej bezargumentowej wersji, która
przyjmowałaby znacznik create lub true.
public HttpSession HttpServletRequest.getSession()
Aby mieć pewność, że sesja jest właściwie utrzymywana, metoda getSession() musi zostać
wywołana przynajmniej raz przed zatwierdzeniem odpowiedzi.
Za pomocą metody setAttribute() możemy dodać dane do obiektu HttpSession:

*
Takie, a nie inny układ materiału niniejszej książki może przypominać sytuację nauczyciela trzeciej klasy szkoły
podstawowej, który tłumaczy swoim uczniom rachunkowe zasady dzielenia, tylko po to, aby powiedzieć im później, że
dzielenie najlepiej wykonuje się za pomocą kalkulatora. Wierzymy jednak, iż poznanie tradycyjnych metod pozwala na
lepsze zrozumienie problemu.
public void HttpSession.setAttribute(String name. Object value)
Metoda wiąże określoną wartość obiektu z określoną nazwą. Wszystkie wcześniejsze
powiązania z tą nazwą są usuwane. Aby pobrać obiekt z sesji, wykorzystujemy metodę
getAttribute():

public Object HttpSession.getAttribute(String name)


Metoda ta odsyła związany z określoną nazwą lub null — jeżeli nie ma żadnego powiązania.
Możemy również uzyskać nazwy wszystkich obiektów związanych z sesją, za pomocą metody
getAttributeNames():

public Enumeration HttpSession.getAttributeNames()


odsyła Enumeration (wyliczenie) zawierające nazwy wszystkich obiektów związanych z tą
sesją jako obiekty String lub puste Enumeration — jeżeli nie było żadnych powiązań. Możemy
też wreszcie usunąć obiekt z sesji przy pomocy metody removeAttribute():
public void. HttpSession.removeAttribute(String name)
Metoda ta usuwa obiekt związany z określoną nazwą lub, jeżeli nie ma wiązania, nie
wykonuje żadnych operacji. Każda z tych metod może zgłosić wyjątek
java.lang.IllegalStateException, w przypadku gdy sesja będąca przedmiotem połączenia
jest nieprawidłowa (nieprawidłowe sesje zostaną omówione w kolejnym podrozdziale).
Warto zwrócić uwagę, iż w Interfejsie 2.2 zostały zmienione nazwy tych metod z: setValue(), getValue(),
getValueNames() oraz removeValue(), na bardziej standardowe setAttribute(), getAttribute(),
getAttributeNames() oraz removeAttribute(). Metody „value” działają nadal, jednak nie są zalecane.

Wykorzystywanie śledzenia sesji — liczba wizyt


Przykład 7.4 prezentuje prosty aplet, wykorzystujący śledzenie sesji do zliczania ile razy
klient go wywoływał, tak jak to zostało pokazane na rysunku 7.2. Aplet wyświetla również
wszystkie powiązania z bieżącą sesją.

Przykład 7.4.
Sesja śledząca liczbę wizyt

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionTracker extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException (
res. setContentType (" tekst /html") ;
PrintWriter out = res.getWriter();

// pobierz bieżący obiekt sesji, w razie konieczności utwórz


HttpSession session = req.getSession();

// powiększ o jednostkę liczbę wizyt dla tej strony. Wartość


//jest zapisywana w sesji tego klienta pod nazwą "
tracker.count".
Integer count = (Integer)session.getAttrifcute("tracker.count");
if (count == null)
count = new Integer(1) ;
else
count = new Integer(count.intValue() + 1);
session.setAttribute("tracker.count", count) ;

out. println("<HTML><HEAD><TITLE>SessionTracker< /TITLE></HEAD>") ;


out.println("<BODY><Hl>Session Tracking Demo</Hl>");

// Wyświetl liczbę wizyt dla tej strony


out.println("Odwiedziłeś tą stronę " + count +
((count. intValue () ==1) ? " raz." : " razy."));
out.println("<P>") ;

out.println("<H2>Oto twoje dane sesji:</H2>");


Enumeration enum = session.getAttributeNames();
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement() ;
out.println(name + ": " + session.getAttribute(name) + "<BR>");
}
out.println("</BODY></HTML>") ;
}
}

Rysunek 7.2.
Liczenie liczby wizyt
Aplet pobiera najpierw obiekt HttpSession, związany z aktualnym klientem. Poprzez wykorzystanie
bezargumentowej wersji metody getSession(), aplet prosi aby w razie konieczności sesja została utworzona.
Aplet pobiera następnie obiekt Integer, związany z nazwą tracker.count. Jeżeli nie m żadnego obiektu,
rozpoczyna liczenie od nowa. W przeciwnym wypadku, zastępuje obiekt Integer nowym obiektem Integer,
którego wartość została zwiększona o jeden. W końcu wyświetla aktualną liczbę wizyt oraz wszystkie aktualne
pary nazwa-wartość, w sesji.

Czas istnienia (cykl życia) sesji


Sesje nie trwają nieskończenie. Czas istnienia sesji kończy się automatycznie, albo po jakimś,
określonym czasie nie wykorzystywania, (dla serwera „Tomcat” domyślny czas nie
wykorzystywania wynosi 30 minut) lub jest to wykonywane ręcznie — w wypadku gdy sesja
jest unieważniona przez aplet w sposób wyraźny. Wyłączenie serwera może, ale nie musi
oznaczać unieważnienia sesji, jest to zależne od możliwości serwera. Kiedy czas istnienia
kończy się (lub kiedy sesja jest unieważniana) obiekt HttpSession oraz wartości danych, które
zawiera, są usuwane z systemu.
Pamiętajmy o tym, że wszystkie informacje zapisane w obiekcie sesji użytkownika zostają
utracone w momencie, w którym sesja jest unieważniana. Jeżeli takie informacje potrzebne
będą nam jeszcze w późniejszym terminie, powinniśmy je przechowywać w lokalizacji
zewnętrznej, (takiej jak baza danych) i zaopatrzyć nasz własny mechanizm wiążący
użytkownika, w pozycje bazy danych użytkownika. Powszechnymi rozwiązaniami tego
problemu są: jakaś forma logowania się, ustawianie długo terminowego cookie, które może
być odczytywane przez lata, i (lub) dostarczenie klientowi przepisanego do zakładki URL-u.
Przykładem wziętym z codziennego życia jest „My Yahoo!” gdzie użytkownicy logują się przy
użyciu formularza HTML, ich sesja logująca jest śledzona przy użyciu „Ciasteczka”, którego
czas istnienia kończy się wraz z zamknięciem przeglądarki, i gdzie (jeżeli zaznaczą pole
„Remember My ID & Password box — Zapamiętaj moją identyfikację i hasło) ich tożsamość
jest rejestrowana przez ustawione w tym celu, trwałe cookie, aż czasu kiedy ostatecznie się
wylogują. Jest to szczególnie interesujące, że nawet gdy „Yahoo” zna tożsamość
użytkownika, dzięki trwałemu cookie, nadal jednak wymaga od użytkownika wprowadzenia
swojego hasła — przed uzyskaniem dostępu do poczty elektronicznej oraz innych poufnych
informacji.
Wbudowana zdolność śledzenia sesji może być nadal używana w połączeniu z
długookresowymi sesjami, w celu obsłużenia sesji logowania, przechowania dojścia do
danych zewnętrznych i (lub) utrzymywania przechowywanej w pamięci podręcznej bazy
danych, informacji, w celu szybkiego z nich skorzystania w przyszłości.

Ustawianie terminu ważności sesji


Idealną sytuacją byłoby unieważnienie sesji w momencie kiedy użytkownik zamyka swoją
przeglądarkę, łączy się z inną stroną (witryną), lub odchodzi od swojego komputera. Niestety
serwer nie ma żadnej możliwości wykrycia podobnych zdarzeń. W konsekwencji sesje „żyją”
przez cały okres bezaktywności, po upływie którego serwer zakłada, że użytkownik nie będzie
już dalej aktywny i że w związku z tym nie ma sensu utrzymywania dalej dla niego stanu sesji.
Domyślny termin ważności (limit czasu) sesji może zostać określony przy pomocy deskryptora
wdrożenia web.xml; odnosi się to do wszystkich sesji, utworzonych w danej aplikacji WWW.
Zobaczmy jak prezentuje się sytuacja na przykładzie 7.5.
Przykład 7.5.
Ustawianie terminu ważności na jedną godzinę

<?xml version="1.0" kodowanie="ISO-8859-l"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.// Aplikacja WWW DTD 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app>
<!-- ..... ->
<session-config>
<session-tiineout>
60 <!-- minutes -->
</session-timeout>
</session-config>
</web-app>

Znacznik <session-timeout> utrzymuje wartość limitu czasu (terminu ważności) podaną w minutach. Dla
przykładu, ustaliliśmy, że jeżeli użytkownik nie złoży żadnego zlecenia do aplikacji WWW, wtedy serwer może
unieważnić sesję użytkownika i „rozwiązać” obiekty w niej przechowywane. Specyfikacja apletu wymaga, aby
wartość terminu ważności wyrażona była liczbą całkowitą, co wyklucza wartości ujemne, jednak niektóre
serwery używają takich wartości, aby zasygnalizować, że sesja nigdy nie ulegnie przeterminowaniu.

Wartości limitu czasu mogą być również dla sesji konfigurowane indywidualnie. Obiekt HttpSession
dysponuje metodą setMaxInactivateInterval() do takiego precyzyjnego sterowania:

public void HttpSession.setMaxInactiveInterval(int secs)


Metoda ta określa wartość terminu ważności dla sesji, podanego w sekundach. Wartość
ujemna przedziału oznacza iż sesja nie ulegnie nigdy przeterminowaniu. Tak, jednostki nie
pokrywają się z tymi ze znacznika <session-timeout>, nie jest to niczym uwarunkowane, jest
to po prostu przypadek. Aby łatwiej zapamiętać, które jednostki należy zastosować, stosujmy
następujący schemat myślenia: „Dla precyzyjnej kontroli używamy precyzyjnych jednostek”.
Aktualna (bieżąca) wartość limitu czasu może być uzyskana przy wykorzystaniu metody
getMaxInactiveInterval():

public int HttpSession.getMaxInactiveInterval()


Metoda ta odsyła wartość terminu ważności dla każdej sesji, w sekundach. Jeżeli nie określimy znacznika
<session-timeout> możemy wywołać tą metodę na nową sesję, aby określić domyślny limit czasu naszego
serwera.
Na przykładzie 7.6 zaprezentowaliśmy omawiane metody, w aplecie, który wyświetla bieżącą wartość limitu
czasu, a następnie ustawia nową wartość terminu ważności na dwie godziny. Podczas pierwszego wywołania,
bieżąca wartość limitu czasu wyświetla ogólno-aplikacyjne ustawienia. Podczas drugiego wywołania aktualny
limit czasu wyświetla dwie godziny — ponieważ jest to limit czasu ustawiony podczas pierwszego wywołania.

Przykład 7.6.
Ustawianie terminu ważności

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionTimer extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IQException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter();

// Pobierz aktualny obiekt sesji, w razie konieczności utwórz go


HttpSession session = req.getSession();

out.println("<HTML><HEAD><TITLE>SessionTimer</TITLE></HEAD>") ;
out.println("<BODY><Hl>Session Timer</Hl>") ;

// Wyświetl poprzedni termin ważności


out.println("Poprzedni termin ważności to " +
session. getMaxInactiveInterval ()) ;
out.println("<BR>") ;

// Ustaw nowy termin ważności


session.setMaxInactiveInterval(2*60*60); // dwie godziny

// Wyświetl nowy termin ważności


out.println("Nowo ustalony termin ważności to " +
session.getMaxInactiveInterval()) ;

out.println("</BODY></HTML>") ;
}
}

Wybieranie właściwego terminu ważności


Tak więc znamy, na ten moment, kilka sposobów na kontrolowanie terminu ważności sesji, lecz jaka wartość
limitu czasu jest najlepsza? Odpowiedź brzmi: to (oczywiście) zależy.
Pierwszą sprawą, którą trzeba zapamiętać, jest fakt iż wartość limitu czasu sesji nie
determinuje jak długo sesja będzie trwała. Limit ten determinuje tylko jak długo będzie czekał
z unieważnieniem sesji pomiędzy zleceniami. Sesje z półgodzinnym limitem czasu mogłaby
trwać godzinami.
Określenie prawidłowego okresu bez-aktywności musi być kompromisem pomiędzy wygodą użytkownika, jego
bezpieczeństwem a jego skalownością. Dłuższe terminy ważności dają użytkownikowi większą wygodę,
ponieważ może on robić dłuższe przerwy pomiędzy zleceniami, uzyskując czas aby np. wykonać telefon lub aby
sprawdzić pocztę elektroniczną — bez utraty stanu. Krótsze limity czasu zwiększają bezpieczeństwo
użytkownika, ponieważ ograniczają czas wrażliwości (jeżeli użytkownik np. zapomniał się wylogować)
jednocześnie zwiększają skalowalność serwera, jako że serwer może wtedy uwolnić obiekty w sesji dużo
wcześniej.
Na wstępie zadajmy sobie pytanie jaki jest maksymalny czas oczekiwania naszego użytkownika pomiędzy
zleceniami? Zwykle odpowiedź brzmi: pół godziny.
Rozważmy sobie tą odpowiedź i poznajmy parę niezmiennych zasad:
• Bezpieczne aplikacje WWW, takie jak bankowość „on line”, powinny mieć krótsze niż zwykłe limity czasu
aby umożliwić serwerowi odzyskanie lub „wypuszczenie” artykułów tak szybko jak to możliwe.
• Sesje nie przechowujące „kosztownych” artykułów, mogą mieć dłuższe niż zwykłe terminy ważności.
• Sesje przechowujące zawartości koszyków zakupów (kosztowne lub nie) powinny mieć dłuższe limity czasu,
ponieważ może się zdarzyć, że użytkownicy nie będą pamiętali zawartości swych koszyków zakupów, co, w
przypadku wczesnego unieważnienia, oznaczałoby dla nas koszty finansowe!
• Sesje przechowujące w pamięci podręcznej informacje bazy danych powinny mieć, w przypadku gdy pamięć
podręczna ma dużą objętość, krótsze terminy ważności, jednak terminy te powinny być dłuższe dla tych sesji
jeżeli połączenie z bazą danych jest wyjątkowo wolne.
• Jeżeli wymagamy od naszych użytkowników, aby wylogowywali się kiedy kończą pracę, domyślny limit
czasu może być ustawiony na dłuższy.
Pamiętajmy również, iż termin ważności nie musi być taki sam dla każdego użytkownika. W celu ustawienia
własnego limitu czasu, opartego na preferencjach użytkownika lub nawet w celu jego zmiany w czasie trwania
sesji — np. aby uczynić limit czasu krótszym po przechowywaniu „kosztownego” artykułu, możemy posłużyć się
metodą setMaxInactiveInterval().

Metody czasu trwania


Istnieje wiele dodatkowych metod związanych z obsługą czasu trwania sesji:
public boolean HttpSession.isNew()
Metoda ta odsyła informację odnośnie tego czy sesja jest nowa czy nie. Sesja jest uznawana za nową,
jeżeli została utworzona przez serwer, lecz klient nie potwierdził jeszcze połączenia z nią.
public void HttpSession.invalidate()
Metoda ta powoduje, iż sesja jest natychmiast unieważniana. Wszystkie obiekty przechowywane w sesji
zostają „rozwiązane”. Metodę tą wywołuje się w celu wdrożenia wylogowania.
public long HttpSession.getCreationTime()
Metoda ta odsyła czas, w którym sesja została utworzona, jako wartość long reprezentującą liczbę
milisekund, która upłynęła od północy, 1 stycznia, 1970, czasu GMT.
public long HttpSession.getLastAccessedTime()
Metoda ta odsyła czas, w którym klient przesłał ostatnie zlecenie związane z sesją, jako wartość long
reprezentującą liczbę milisekund, która upłynęła od północy, 1 stycznia, 1970 roku. Bieżące zlecenie nie
jest uznawane jako ostatnie.
Każda z metod może zgłosić wyjątek java.lang.IllegalStateException w przypadku gdy sesja, do której
uzyskiwany jest dostęp jest nieważna.

„Ręczne” unieważnianie starej sesji


Aby zademonstrować działanie omawianych metod, na przykładzie 7.7 został zaprezentowany aplet „ręcznie”
unieważniający sesję — jeżeli istnieje ona dłużej niż jeden dzień lub jeżeli nie była aktywna przez okres dłuższy
niż jedną godzinę.

Przykład 7.7.
Unieważnianie starej sesji

import java. io. *;


import java.uti1.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ManualInvalidate extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/html") ;
PrintWriter out = res.getWriter() ;

// Pobierz bieżący obiekt sesji, w razie konieczności utwórz go


HttpSession session = reg.getSession() ;

Unieważnij sesję, jeżeli istnieje dłużej niż jeden dzień lub nie
była // aktywna dłużej niż jedną godzinę.
if (! session. isNew()) { // pomiń nowe sesje
Date dayAgo = new Date(System.currentTimeMillis() - 24*60*60*1000);
Date hourAgo = new Date (System.currentTiineMillis () -
60*60*1000);
Date created = new Date (session.getCreationTime ());
Date accessed = new Date (session.getLastAccessedTtme ());

if (created.before(dayAgo) || accessed.before(hourAgo)) {
session.invalidate() ;
session = reg.getSession() ; // pobierz nową sesję
}
}

// Kontynuuj przetwarzanie
}
}

Zasada działania sesji


Tak więc zastanówmy się jak serwer WWW wdraża śledzenie sesji? Kiedy użytkownik po raz pierwszy
uruchamia aplikację WWW, jest mu przypisywany nowy obiekt sesji HttpSession oraz niepowtarzalna
identyfikacja sesji (ID sesji). ID sesji identyfikuje użytkownika i jest wykorzystywana do dopasowania
użytkownika z obiektem HttpSession w kolejnych zleceniach. Niektóre serwery wykorzystują pojedynczą
identyfikację sesji dla całego serwera, z każdą aplikacją WWW odwzorowującą to ID do innej kopii
HttpSession. Z kolei inne serwery przypisują jedną identyfikację sesji do jednej aplikacji WWW, jako
dodatkowe zabezpieczenie przed złośliwymi aplikacjami WWW, pomagającymi intruzowi w
„zdepersonalizowaniu” nas.
W tle, ID sesji jest zwykle zapisywane po stronie klienta w cookie zwanym JSESSIONID. Dla klientów, którzy
nie obsługują „Ciasteczek”, identyfikacja sesji może zostać przesłana jako część przepisanego URL-u,
zakodowanego przy użyciu parametru ścieżki jsessionid, np.
http://www.servlets.com/catalog/servlet/ItemDisplay;jsession=123?item=156592391X. Inne implementacje,
takie jak wykorzystanie SSL — protokołu bezpiecznej transmisji danych sesji, są również możliwe.
Aplet może poznać ID sesji za pomocą metody getId():

public String HttpSession.getId()

Metoda ta odsyła niepowtarzalny identyfikator String, przypisany do sesji. Dla przykładu, ID serwera
„Tomcat” mogłoby wyglądać w następujący sposób awj4gyhsn2. Metoda zgłasza wyjątek
IllegalStateException w przypadku gdy sesja nie jest ważna.

Pamiętajmy, iż identyfikacja sesji powinna być traktowana jako tajna informacja serwera. Zwracajmy uwagę na
to, co robimy z tą wartością.

Wycofanie Obiektu HttpSessionContext


W Interfejsie API 2.0 istniał obiekt HttpSessionContext, który był wykorzystywany do
„tropienia” aktualnych sesji (oraz odpowiadającym im ID sesji), wykonywanych przez serwer.
Prawie zawsze klasa była wykorzystywana do „debagowania” i sprawdzania jakie sesje jeszcze
istnieją. Klasa ciągle jeszcze istnieje — dla zachowania kompatybilności binarnej, jednak
począwszy od wersji 2.1 Interfejsu API, jest wycofana i określana jako pusta. Powodem jest fakt,
iż identyfikacje sesji muszą być pilnie strzeżone, więc nie powinny być przechowywane w łatwo
dostępnej, pojedynczej lokalizacji, zwłaszcza jeżeli istnienie takiej lokalizacji nie daje żadnych
znaczących korzyści, poza „debagowaniem”.
Apletowe śledzenie sesji
Niemal każdy serwer obsługujący aplety wdraża śledzenie sesji oparte na „Ciasteczkach”,
gdzie identyfikacja sesji jest zapisywana po stronie klienta w trwałym cookie. Serwer
odczytuje ID sesji z cookie JSESSIONID, a następnie determinuje który obiekt sesji uczynić
dostępnym podczas każdego zlecenia.
Dla klientów apletu taka sytuacja może przedstawiać problem. Większość środowisk apletowych wdraża
HttpURLConnection w taki sposób, że kiedy aplet tworzy połączenie HTTP, środowisko automatycznie dodaje
zawierające cookies przeglądarki, do zlecenia. Pozwala to apletowi na uczestniczenie w tej samej, co inne
zlecenia przeglądarki, sesji. Problemem jest jednak, iż inne środowiska apletowe, takie jak starsze wersje
środowiska „Java Plug-In enviroment” nie są zintegrowane z przeglądarką i dlatego zlecenia apletów jawią się
jako oddzielone od normalnej sesji przeglądarki. Rozwiązanie dla sytuacji, w której aplety muszą działać w
podobnych środowiskach jest przesyłane identyfikacji sesji do apletu oraz pozwalanie apletowi na przekazanie
tego ID z powrotem do serwera, jako sztucznie utworzone cookie JSESSIONID. W rozdziale 10 „Komunikacja
aplet zwykły — aplet tworzony na serwerze” została zamieszczona klasa HttpMessage — aby pomagać w tego
typu sytuacjach. Aplet może otrzymać identyfikację sesji jako zwykły parametr apletu (dodany dynamicznie do
strony HTML zawierającej aplet).

Awaryjne zmiany trybu pracy — „nie-ciasteczkowe”


Specyfikacja apletu mówi, iż serwery WWW muszą obsługiwać śledzenie sesji również dla przeglądarek, które
nie akceptują cookies, których tak wiele wykorzystuje przepisywanie URL-u jako awaryjną zmianę trybu pracy.
Wymaga to dodatkowej pomocy ze strony apletów, które generują strony zawierające URL-e. W celu obsłużenia
śledzenia sesji poprzez przepisywanie URL-u, aplet musi przepisać każdy lokalny URL, zanim odeśle go
klientowi. Interfejs API zawiera dwie metody, aby wykonać to zadanie: encode() oraz encodeRedirectURL
():

public String HttpServlrtresponse.encodeURL(String url)


Metoda ta koduje (przepisuje) określony URL aby dołączyć ID sesji, a następnie odsyła nowy URL lub, jeżeli kodowanie nie jest potrzebne
albo nie obsługiwane, pozostawia URL niezmienionym. Zasady decydujące o tym kiedy i jak ma być zakodowany URL są domeną serwera.
Wszystkie URL-e wychodzące z serwera, powinny być uruchamiane poprzez tą właśnie metodę. Zwróćmy uwagę, iż metoda encodeURL
() mogłaby być bardziej precyzyjnie nazwana rewriteURL() — aby nie myliła się z procesem kodowania URL-u, który koduje
specjalne znaki w strumieniu URL-u.

public String HttpServlrtresponse.encodeRedirectURL(String url)


Metoda ta koduje (przepisuje) określony URL aby dołączyć identyfikację sesji, a następnie odsyła nowy URL lub, jeżeli kodowanie nie jest
potrzebne albo nie obsługiwane — pozostawia URL niezmienionym. Zasady decydujące o tym kiedy i jak ma być zakodowany URL są
domeną serwera. Metoda ta może używać odmiennych zasad niż metoda encodeURL(). Wszystkie URL-e przekazane do metody
HttpServletResponse — sendDirect(), powinny być uruchamiane poprzez tą metodę.

Poniższy fragment kodu ukazuje aplet piszący łącznik do samego siebie, który jest kodowany tak, aby zawierał bieżącą identyfikację sesji:

out.println("Click <A HREF=\"" +


res.encodeURL(req.getRequestURI()) + "\">here</A>");
out.println("aby powtórnie załadować tą stronę.");

Na serwerach, które nie obsługują przepisywania URL-u lub mają wyłączoną tą funkcję,
końcowy URL pozostaje bez zmian. Poniżej przedstawiamy fragment kodu, na którym jest
zaprezentowany aplet przekierowujący użytkownika do URL-u zakodowanego tak, aby
zawierał ID sesji:
res.sendRedirect(res.encodeRedirectURL("/servlet/NewServlet")) ;

Aplet jest w stanie wykryć czy identyfikacja sesji, wykorzystywana do zidentyfikowania aktualnego obiektu
HttpSession, pochodzi od cookie czy od zakodowanego URL-u wykorzystującego metody
isRequestedSessionIdFromCookie() i isRequestedSessionIdFromURL():
public boolean HttpServletRequest.isRequestedSessionIdFromCookie()
public boolean HttpServletRequest.isRequestedSessionIdFromURL()

Określenie czy identyfikacja sesji pochodzi z innego źródła, takiego jak sesja SSL, nie jest aktualnie możliwe.

ID sesji będące przedmiotem zlecenia może nie pokrywać się z identyfikacją sesji, odesłaną przez metodę
getSession(), tak jak ma to miejsce kiedy ID sesji jest nieważne. Aplet może jednak ustalić czy identyfikacja
sesji będąca przedmiotem zlecenia jest ważna, przy pomocy metody isRequestedSessionIdValid():

public boolean HttpServletRequest.isRequestedSessionIdValid()

Miejmy również świadomość, iż kiedy używamy śledzenia sesji opartego na przepisywaniu URL-u, wielokrotne
okna przeglądarki mogą należeć do różnych sesji lub do tej samej sesji, w zależności od tego w jaki sposób okna
te zostały utworzone oraz czy tworzący je łącznik miał zastosowane przepisywanie URL-u.

Aplet „SessionSnoop”
Zaprezentowany na przykładzie 7.8 aplet SessionSnoop wykorzystuje większość opisanych w rozdziale metod,
aby „podsłuchać” informację o aktualnej sesji. Rysunek 7.3 prezentuje przykładowy wydruk wyjściowy apletu.

Przykład 7.8.
„Podsłuchiwanie” informacji sesji

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionSnoop extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("tekst/html");
PrintWriter out = res.getWriter() ;

// Pobierz aktualny obiekt sesji, w razie konieczności utwórz go


HttpSession session = req.getSession();

// Zwiększ o jeden liczbę wejść na tą stronę. Wartość jest zapisana


// w sesji klienta pod nazwą "snoop.count".
Integer count = (Integer)session.getAttribute("snoop.count");
if (count == null)
count = new Integer(1);
else
count = new Integer(count.intValue() + 1) ;
session.setAttribute("snoop.count", count) ;

out.println("<HTML><HEAD><TITLE>SessionSnoop</TITLE></HEAD>");
out.println("<BODY><Hl>Session Snoop</Hl>");

// Wyświetl ilość wejść na tą stronę


out.println("Odwiedziłeś już tą " + count +
((count.intValue() == 1) ? " raz." : " razy."));

out.println("<P>") ;

out.println("<H3>Oto twoje zachowane dane sesji:</H3>");


Enumeration enum = session.getAttributeNames();
while (enum.hasMoreElements()) {
name = (String) enum.nextElement();
intln (name + ": " + session.getAttribute (name) + "<;BR>");
}

out.println("<H3>Oto parę ważniejszych stanów twojej sesji:</H3>");


out.println("ID sesji: " + session.getid() +
" <I>(keep it secret )</I><BR>");
out.println("Now sesja: " + session.isNew() + "<BR>");
out.println("Termin ważności: " +
session.getMaxInactiveInterval());
out.println("<I>(" + session.getMaxInactiveInterval() / 60 +
" minut) < / Ix><BR>") ;
out.println("Czas utworzenia: " + session. getCreationTime() );
out. println ("<I>(" + new Date (session.getCreationTime ()) +
")</I>><BR>") ;
out.println("Ostatni czas wejścia: " +
session.getLastAccessedTime());
out.println("<I>(" + new Date(session.getLastAccessedTime()) +
")</I><BR>") ;

out.println("ID sesji będące przedmiotem zlecenia z URL: " +


req. isRequestedSessionIdFromURL () + " <BR>") ;
out.println("ID sesji, będące przedmiotem zlecenia jest ważne: " +
req.isRequestedSessionIdValid() + "<BR>");

out.println("<H3>Test URL Rewriting</H3>");


out.println("Kliknij <A HKEF=\"" +
res.encodeURL (req.getRequestURI()) + "\">here</A>") ;
out.println("Aby sprawdzić czy śledzenie sesji działa poprzez
przepisywanie");
out.println("URL-u, nawet wtedy gdy cookies nie są obsługiwane.");

out.println("</BODY></HTML>") ;
}
}

Aplet rozpoczyna tym samym kodem, który został zamieszczony w przykładzie 7.4, w aplecie
SessionTracker. Następnie przechodzi do dalszego wyświetlania bieżącego ID sesji, tego czy
jest to sesja nowa, wartości limitu czasu, czasu utworzenia sesji oraz czas ostatniego
połączenia z sesją. Później aplet wyświetla czy identyfikacja sesji (jeżeli jest taka), będąca
przedmiotem zlecenia, pochodzi od cookie czy od URL-u oraz czy to ID jest ważne. W końcu
aplet drukuje zakodowany URL, który może zostać wykorzystany do powtórnego załadowania
tej strony, w celu sprawdzenia czy przepisywanie URL-u działa nawet wtedy kiedy cookies nie
są obsługiwane.

Zdarzenia wiążące sesję


Niektóre obiekty mogą „chcieć” przeprowadzić operację zarówno wtedy, kiedy są związane z
sesją, jak i wtedy kiedy nie są z nią związane. Dla przykładu, połączenie z bazą danych może
rozpoczynać transakcję (obsługę zlecenia) w stanie związania z sesją, a kończyć kiedy nie są z
nią związane. Wszystkie obiekty, które wdrażają interfejs
javax.servlet.http.HttpSessionBindingListner, są powiadamiane o związaniu z sesją oraz
o tym, że nie są z nią związane. Interfejs deklaruje dwie metody: valueBound() oraz
valueUnbound(), które muszą zostać wdrożone:

public void HttpSessionBindingListener.valueBound(


HttpSessionBindingEvent event)
public void HttpSessionBindingListener.valueUnbound (
HttpSessionBindingEvent event)

Metoda valueBound() jest wywoływana, kiedy odbiornik jest związany z sesją, a metoda valueUnbound()
kiedy odbiornik nie jest z nią związany — poprzez usunięcie, zastąpienie albo poprzez unieważnienie sesji.

Argument javax.servlet.http.HttpSessionBindingEvent zapewnia dostęp do nazwy pod którą obiekt


jest wiązany (lub odwiązywany od niej) z metodą getName():

public String HttpSessionBindingEvent.getName()


Rysunek 7.3.
Przykładowy wydruk wyjściowy apletu „SessionSnoop”

Obiekt HttpSessionBindingEvent zapewnia również dostęp do obiektu, do którego jest wiązany (lub od
którego jest „odwiązywany”) odbiornik, wykorzystując w tym celu metodę getSession():

public HttpSession HttpSessionBindingEvent.getSession()

Na przykładzie 7.9 zaprezentowano użycie HttpSessionBindingListner oraz


HttpSessionBindingEvent, z odbiornikiem rejestrującym w czasie związania jak i w czasie rozwiązania z
sesją.

Przykład 7.9.
Zdarzenia wiążące śledzenie sesji

import java.io.*;
import java.util.*;
import javax.servlet.* ;
import javax.servlet.http.*;

public class SessionBindings extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IQException {
res.setContentType("tekst/zwykły") ;
PrintWriter out = res.getWriter();

// Pobierz aktualny obiekt sesji. w razie potrzeby utwórz go


HttpSession session = reg.getSession();

// Dodaj CustomBindingListener
session.setAttribute("bindings.listener",
new CustomBindingListener(getServletContext()));

out.println("Ta strona jest celowo pozostawiona pustą");


)
}

class CustomBindingListener inplements HttpSessionBindingListener {

// Zapisz ServletContext dla użytku jego metody log ()


ServletContext context;

public CustomBindingListener(ServletContext context) {


this.context = context;
}

public void valueBound(HttpSessionBindingEvent event) {


context.log("[" + new Dated + "] BOUND as " + event.getName() +
" to " + event. getSession () . getid ()) ;
}
public void valueUnbound(HttpSessionBindingEvent event) {
context.log("[" + new Date() + "] UNBOUND as " + event.getName () +
" from " + event. getSession().getId() );
}
}

Za każdym razem kiedy obiekt CustomBindingListner jest wiązany z sesją, jego metoda valueBound(),
jest wywoływana i zdarzenie jest rejestrowane. Za każdym razem kiedy obiekt ten jest „odwiązywany” od sesji,
wywoływana jest jego metoda valueUnbound(), tak że zdarzenie jest również rejestrowane. Możemy śledzić
sekwencję zdarzeń poprzez obserwowanie dziennika zdarzeń serwera.

Załóżmy, iż aplet ten jest wywołany jeden raz, powtórnie ładowany 30 sekund później i następnie nie
wywoływany co najmniej przez następne pół godziny. Dziennik zdarzeń mógłby wyglądać w następujący sposób:

[Tue Sep 27 22:46:48 PST 2000]


BOUND as bindings.listener to awj4qyhsn2
[Tue Sep 27 22:47:18 PST 2000]
UNBOUND as bindings.listener from awj4qyhsn2
[Tue Sep 27 22:47:18 PST 2000]
BOUND as bindings.listener to awj4qyhsn2
[Tue Sep 27 23:17:18 PST 2000]
UNBOUND as bindings.listener from awj4qyhsn2

Pierwsza pozycja występuje podczas zlecenia pierwszej strony, kiedy odbiornik jest związany z nową sesją.
Pozycje: druga oraz trzecia występują podczas ponownego ładowania, ponieważ odbiornik jest „odwiązywany” i
powtórnie wiązany z sesją, w czasie tego samego wywołania setAttribute(). Czwarta pozycja ma miejsce
pół godziny później, kiedy limit czasu sesji kończy się, a ona sama jest unieważniana.

Robienie zakupów przy użyciu śledzenia sesji


Zakończmy ten rozdział spojrzeniem na to, jak zadziwiająco prostym może stać się nasz aplet „shopping cart
viewer, przy zastosowaniu śledzenia sesji. Na przykładzie 7.10 zaprezentowano aplet zapisujący każdy artykuł z
koszyka, w sesji użytkownika, pod nazwą cart.items. Zwróćmy uwagę, iż URL-e, znajdujące się na stronie,
zostały przepisane tak, aby obsługiwać klientów z zablokowanymi „Ciasteczkami”.

Przykład 7.10.
Wykorzystanie Śledzenia Sesji API

import java.io.*;
import javax.servlet.* ;
import javax.servlet.http.*;
public class ShoppingCartViewerSession extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res. setContentType ("tekst/html") ;
PrintWriter out = res.getWriter () ;

// Pobierz obiekt bieżącej sesji.


HttpSession session = req.getSession();

// Artykuły z koszyka są utrzymywane w obiekcie sesji.


String [] items = (String []) session. getAttribute ("cart.
items") ;

out.println("<HTML><HEAD><TITLE>SessionTracker</TITLE></HEAD>");
out.println("<BODY><Hl>Session Tracking Demo</Hl>");

// Drukuj aktualne artykuły z koszyka.


out.println("Aktualnie masz w swoim koszyku następuje
artykuły:<BR>");
if (items == null) {
out.println("<B>None</B>") ;
}
else (
out.println("<UL>") ;
for (int i = 0; i < items.length; i++) {
out.println("<LI>" + items[i]);
}
out.println("</UL>") ;
}

// Spytaj czy chcą dodać jeszcze do koszyka jakieś artykuły, czy też // chcą
zakończyć.
out.println("<FORM ACTIQN=\"" +
res.encodeURLC/servlet/ShoppingCart") + "\" METHOD=POST>") ;
out.println("Czy chcesz<BR>");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Dodać Więcej Artykułów \">");
out.println("<INPUT TYPE=SUBMIT VALUE=\" Zakończyć \">");
out.println("</FORM>") ;

// Zaproponuj stronę pomocy. Zakoduj w razie potrzeby.


out.println("Dla pomocy, kliknij <A HREF=\"" +
res.encodeURL("/servlet/Help?topic=ShoppingCartViewer") +
"\">here</A>") ;
out.println("</BODY></HTML>") ;
}
}
Rozdział 8. Bezpieczeństwo

Bezpieczeństwo danych jest jednym z najważniejszych zagadnień programowania sieciowego, ponieważ coraz
więcej firm decyduje się na obsługę klientów za pośrednictwem sieci i ich Intranety zapełniane są tajnymi
danymi. Bezpieczeństwo danych zajmuje się ochroną tajnych informacji przed niepowołanymi użytkownikami.
W przypadku aplikacji sieciowych bezpieczeństwo rozpatrujemy jako:
• Uwierzytelnienie — możliwość weryfikacji tożsamości biorących udział w wymianie informacji.

• Autoryzacja — udostępnienie zasobów systemu wybranej grupie


użytkowników lub programów.
• Poufność — pewność, że przesyłane dane mogą odczytać tylko
przez nas określeni użytkownicy.
• Integralność — możliwość sprawdzenia, czy informacja nie
została zmieniona podczas transmisji.
Wymienione powyżej aspekty bezpieczeństwa można zilustrować prostym przykładem: klient chce być pewien,
że jest połączony z właściwym serwerem (uwierzytelnienie) i żadna informacja (np. numer karty kredytowej) nie
zostanie przechwycona (poufność). Firma, sprzedając usługę lub przesyłając tajne informacje poprzez sieć do
pracowników, chce zabezpieczyć dane przed nieuprawnionymi użytkownikami. Ponadto obie strony (klient firmy
i jej pracownik) chcą mieć pewność, że otrzymali niezmienioną informację.
Wiarygodność, autoryzacja, poufność i integralność są zapewniane przy użyciu technologii certyfikatów
cyfrowych. Certyfikaty cyfrowe pozwalają serwerom i klientom na używanie zaawansowanych technik
kryptograficznych do zapewnienia identyfikacji i kodowania protekcyjnego. Java zawiera wbudowany zestaw
narzędzi wspomagających użycie certyfikatów cyfrowych, co sprawia, że serwlety są wspaniałą platformą do
obsługi bezpiecznych aplikacji sieciowych używających technologii certyfikatów cyfrowych.
Bezpieczeństwo powinno również zapewnić ochronę danych przechowywanych na serwerze przed krakerami.
Java od podstaw została zaprojektowana jako bezpieczny, sieciowo-zorientowany język, w którym istnieje
możliwość wykorzystania komponentów gwarantujących bezpieczeństwo własnych i obcych danych na serwerze.
Niniejszy rozdział przedstawia podstawy zabezpieczania danych w sieci i użycia technik certyfikatów cyfrowych
w serwletach. Opisano tu również sposoby zabezpieczania serwera podczas uruchamiania serwletów
pochodzących z niepewnych źródeł. W stosunku do poprzednich rozdziałów przedstawiono tu mniej przykładów,
gdyż podejmuje on problem wyższego poziomu, a wiele poruszonych tu tematów wymaga implementacji obsługi
serwera. Serwlety potraktowano tu jako dodatki.
Autorzy niniejszej książki nie biorą odpowiedzialności za problemy związane z bezpieczeństwem danych, które
mogą pojawić się po zastosowaniu rad tu zawartych. Szerszy opis dotyczący problematyki bezpieczeństwa w
sieci można znaleźć w książce Web Security & Commerce napisanej przez Simsona Garfinkela i Gene Spafford
(O'Reilly). Oczywiście, oni także nie wezmą na siebie odpowiedzialności.
Uwierzytelnienie poprzez HTTP
W rozdziale 4, „Odczytywanie informacji” wspomniano, że protokół HTTP posiada wbudowaną obsługę
uwierzytelnienia — zwaną uwierzytelnieniem podstawowym, która jest oparta o prosty model wezwanie-
odpowiedź, nazwa_użytkownika-hasło. Korzystając z tej techniki serwer zarządza bazą danych zawierającą
nazwy użytkowników i hasła dostępu oraz identyfikuje określone zasoby (pliki, katalogi, serwlety, itp.) jako
chronione. Gdy użytkownik zażąda dostępu do zastrzeżonych zasobów, serwer odpowie żądaniem podania
nazwy użytkownika i hasła. W tym momencie przeglądarka zwykle wyświetla okno dialogowe zawierające pola,
w które użytkownik wpisuje swoją nazwę i hasło, po czym dane te przesyłane są do serwera. Jeśli przesłane
serwerowi informacje istnieją w jego bazie danych, dostęp do zasobów chronionych jest przyznawany. Cały
opisany tu proces uwierzytelnienia odbywa się na serwerze.
Uwierzytelnienie podstawowe nie zapewnia poufności i integralności, co sprawia, że jest to słaby system
zabezpieczenia dostępu do danych. Problem polega na tym, że przesyłane poprzez sieć hasła są kodowane za
pomocą powszechnie znanej metody Base64. Każdy monitorując strumień danych TCP/IP ma pełny i
natychmiastowy dostęp do przesyłanej informacji łącznie z nazwą użytkownika i hasłem chyba, że zostanie
wykorzystana dodatkowa metoda kryptografii SSL (omówiona w dalszej części rozdziału). Ponadto hasła są
przechowywane na serwerze w postaci czystego tekstu czyniąc je łatwym łupem dla każdego, kto włamie się do
systemu plików serwera. Strony zabezpieczone opisaną tu metodą nie mogą zostać uznane za bezpieczne.
Uwierzytelnienie szyfrowane (typu digest) bazuje na schemacie omówionej metody, ale poprzez sieć przesyłany
jest łańcuch tworzony za pomocą algorytmu szyfrowania MD5 z nazwy użytkownika, hasła, URI, metody
żądania HTTP i losowo generowanej przez serwer wartości jednokrotnego użycia (nounce). Obie strony
transakcji (klient i serwer) znają hasło i na jego podstawie generują szyfr. Dostęp do danych jest dozwolony, jeśli
łańcuchy klienta i serwera są zgodne. Transakcje zabezpieczone w ten sposób są bezpieczniejsze, gdyż łańcuch
jest ważny tylko dla jednego żądania i jednej wartości jednokrotnego użycia (nonce value). Niestety, tak jak w
poprzedniej metodzie, serwer nadal zawiera bazę danych z oryginalnymi (nie zaszyfrowanymi) hasłami. Poza
tym niewiele przeglądarek obsługuje tą technikę.
Podsumowując można stwierdzić, że uwierzytelnianie za pomocą protokołu HTTP jest użyteczne w
środowiskach wymagających niskiego poziomu bezpieczeństwa danych. Dobrym przykładem takiego środowiska
jest płatna witryna gazety internetowej, gdzie twórcom bardziej zależy na dostępności serwisu niż na ścisłym
bezpieczeństwie danych. W tym przypadku uwierzytelnianie za pomocą HTTP jest wystarczającą metodą.

Konfiguracja uwierzytelnienia HTTP


W starszych interfejsach Servlet API (wersje wcześniejsze niż 2.2) sposób konfiguracji uwierzytelnienia różnił
się w zależności od typu serwera. Począwszy od wersji 2.2 sposób uwierzytelniania został znormalizowany. W
pliku opisu rozmieszczenia* web.xml określa się metodę zabezpieczenia danych. Za pomocą tego pliku można
stosować wybraną metodę zabezpieczenia w różnych serwerach.
Instalacja mechanizmu zabezpieczeń dla interfejsu Servlet API 2.2 na serwerze nie jest konieczna, aby zapewnić
kompatybilność z tym interfejsem. Zalecana jest implementacja pełnego systemu bezpieczeństwa, ale kontener
serwletu może użyć tylko część mechanizmu bezpieczeństwa lub nie użyć go wcale. Implementacja pełnego
mechanizmu bezpieczeństwa na serwerze jest wymagana tylko wtedy, gdy chcemy, aby serwer był kompatybilny
z bardziej zaawansowaną platformą Java 2 (Enterprise Edition — J2EE),w której Servlet API 2.2 stanowi tylko
część systemu. Poza tym, bezpieczeństwo jest jednym z najnowszych i nie do końca poznanych aspektów
interfejsu Servlet API 2.2, co sprawia, że serwery sieciowe mogą różnić się pod względem implementacji
mechanizmów bezpieczeństwa. Aby mieć pewność, że witryna pozostanie bezpieczna, należy ją co najmniej raz
przetestować podczas przenoszenia danych pomiędzy serwerami.

Uwierzytelnienie w oparciu o rolę


Za pomocą znaczników (umieszczonych w pliku rozmieszczenia aplikacji sieciowej) można określić strony, do
których dostęp mają tylko użytkownicy posiadający specjalne uprawnienia. W ten sposób zapewniamy dostęp
wszystkim użytkownikom do witryny, ale niektóre strony zawarte w witrynie możemy udostępniać przez nas
wybranym użytkownikom. Jednym ze sposobów zapewnienia ograniczonego dostępu jest skorzystanie z metody
uwierzytelnienia opartego o rolę. W tej metodzie, zezwolenia dostępu są przyznawane abstrakcyjnemu
podmiotowi security role, a dostęp do danych mają tylko użytkownicy (lub grupy użytkowników) należący do
określonej roli. Można, na przykład, utworzyć witrynę w ten sposób, aby strony zawierające informacje o
zamiennie.
wynagrodzeniach były dostępne tylko użytkownikom w roli manager. W pliku opisu rozmieszczenia określany
jest tylko sposób dostępu do danych w zależności od roli użytkownika, a konkretne przypisanie ról
użytkownikom (lub grupom użytkowników) ma miejsce podczas wdrażania aplikacji, przy pomocy narzędzi
serwerowych. To przypisanie można realizować w oparciu o informacje zawarte w plikach tekstowych, tabelach
baz danych, systemie operacyjnym, itd.

Ograniczanie dostępu do serwletu


Załóżmy, że chcemy zapewnić ograniczony dostęp do serwletu, co pokazano w przykładzie 8.1. (Za pomocą
omawianej metody można zabezpieczać nie tylko serwlety, ale również pliki i inne aplikacje).
Przykład 8.1.
Czy jesteś pewien, że masz pozwolenie na czytanie tego przykładu?
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SalaryServer extends HttpServlet {


public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text/plain");
PrintWriter out = res.getWriter();
out.println("Informacja ściśle tajna:"); out.println("Każdy zarabia więcej od
Ciebie!");
}
}

Załóżmy, że posiadamy bazę danych użytkowników naszego serwera, zawierającą listę identyfikatorów, haseł i
ról. Nie wszystkie serwery obsługują taką bazę danych, a te, które to robią, implementują ją w dowolny sposób.
W przypadku serwera Tomcat 3.2 dane użytkowników są umieszczane w pliku conf/tomcat-users.xml,
pokazanym w przykładzie 8.2. Kolejne wersje Tomcata pozwolą na bezpieczniejszy zapis informacji (nasza
wersja umieszcza dane w postaci niezaszyfrowanej).
Przykład 8.2.
Plik conf/tomcat-users.xml
<tomcat-users>
<user name="Dilbert" password="dnrc" roles="engineer" />
<user name="Willy" password="iluvalice" roles="engineer,slacker" />
<user name="MrPointyHair" password="MrPointyHair" roles="manager,slacker" />
</tomcat-users>
Należy zauważyć, że tylko MrPointyHair jest w roli manager. Zakładając, że potrafi podłączyć swój
komputer do sieci, powinien być jedyną osobą mającą dostęp do naszego tajnego serwletu. Określimy to w pliku
web.xml, pokazanym w przykładzie 8.3.
Zwróćmy uwagę na to, że istotne znaczenie ma kolejność wystąpienia znaczników w pliku web.xml. Należy
zawsze stosować znaczniki w następującym porządku: <security-constraint>, <login-config>, a
następnie <security-role>. Nie bez znaczenia jest również kolejność występowania elementów wewnątrz
tych znaczników. Tworząc pliki web.xml najłatwiej skorzystać z pomocy narzędzi graficznych, które
automatycznie „zadbają” o odpowiednią strukturę pliku.
Przykład 8.3.
Ograniczenie dostępu za pomocą uwierzytelniania podstawowego.
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web_app_2_2.dtd">
<web-app>
<servlet>
<servlet_name>
secret
</servlet-name>
<servlet-class>
SalaryServer
</servlet-class>
</servlet>

<security-constraint>
<web-resource-collection>
<web-resource-name>
SecretProtection
</web-resource-name>
<url-pattern>
/servlet/SalaryServer
</url-pattern>
<url-pattern>
/servlet/secret
</url-pattern>
<http-method>
GET
</http-method>
<http-method>
POST
</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>
manager
</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>
BASIC <!-- BASIC, DIGEST, FORM, CLIENT-CERT -->
</auth-method>
<realm-name>
Default <!-- Opcjonalne, użyteczne tylko dla BASIC -->
</realm-name>
</login-config>
<security-role>
<role-name>
manager
</role-name>
</security-role>
</web-app>
Za pomocą tego pliku opisu rozmieszczenia zabezpieczono wszystkie metody GET i POST, które udostępniają /
servlet/secret i /servlet/SalaryServer użytkownikom w roli manager, zalogowanym za pomocą uwierzytelnienia
podstawowego. Pozostała część witryny jest dostępna bez ograniczeń.
Znacznik <security-constraint> chroni dostęp do danych, których ścieżki dostępu znajdują się
wewnątrz znacznika <web-resource-collection> w ten sposób, że dostęp jest zapewniony tylko
użytkownikom, których role umieszczono wewnątrz znacznika <auth-constraint>. Każdy znacznik
<web-resource-collection> zawiera nazwę, chronione adresy URL oraz kilka metod HTTP o
ograniczonym dostępie. Dla wzorców adresów URL można użyć identycznych symboli wieloznacznych, jakich
używa się do mapowania serwletu — co opisano w rozdziale 2 „Podstawy serwletu HTTP”. Powinno się
wyznaczyć metody chronione, przynajmniej GET i POST. Jeśli nie wpiszemy znacznika <http-method>, to
wszystkie metody zostaną uznane za chronione. Znacznik <auth-constraint> zawiera nazwy ról
użytkowników, którym udostępnia się zbiór zasobów.
Znacznik <login-config> określa sposób logowania się, którego używa aplikacja. W tym przypadku
określamy podstawowe uwierzytelnienie za pomocą dyrektywy Default. W znaczniku <auth-method>
umieszcza się wartości BASIC, DIGEST, FORM i CLIENT-CERT, które odpowiadają typom uwierzytelnienia:
podstawowemu, szyfrowanemu, w oparciu o formularz i przy użyciu certyfikatów klienckich. O
uwierzytelnieniach w oparciu o formularz i przy użyciu certyfikatów klienckich będzie mowa w dalszej części
rozdziału. Znacznik <realm-name> określa zakres użycia loginu (tu zawarta jest informacja określająca
uprawnienia poszczególnych ról). Ten znacznik jest używany tylko w uwierzytelnieniu podstawowym i
szyfrowanym.
Znacznik <security-role> zawiera listę ról, która może być użyta przez aplikację.
W omówionej metodzie nie ma możliwości udostępniania zasobów wszystkim z pominięciem użytkowników z
„czarnej listy”. Najprostszym wyjściem z tej sytuacji może być utworzenie grupy użytkowników nie należących
do „czarnej listy”.
Teraz, gdy plik web.xml jest już gotowy, wszystkie żądania dostępu do chronionych danych zostaną
przechwycone przez serwer i dane uwierzytelniające użytkownika zostaną sprawdzone. Dostęp jest przydzielany,
jeśli uprawnienia są ważne, a serwer przypisuje użytkownikowi rolę manager. W przeciwnym razie dostęp jest
zabroniony, a okno dialogowe przeglądarki poprosi użytkownika o kolejną próbę zalogowania się.
Wyszukiwanie informacji uwierzytelnienia
Serwlet może odczytać informację na temat uwierzytelnienia dzięki dwóm metodom przedstawionym w rozdziale
4: getRemoteUser() i getAuthType(). Serwlet API 2.2 zawiera dodatkową metodę
getUserPrincipal(), która zwraca obiekt implementujący interfejs java.security.Principal
public java.security.Principal HttpServletRequest.getUserPrincipal()
Principal to termin techniczny określający uwierzytelniany podmiot. Może nim być użytkownik, grupa,
korporacja lub po prostu identyfikator. Interfejs Principal zawiera metodę getName() zwracającą nazwę
podmiotu. Metoda getUserPrincipal() służy do określenia uwierzytelnionej tożsamości użytkownika,
podczas gdy getRemoteUser()do zapewnienia kompatybilności ze skryptami CGI. Metoda
IsUserInRole() została także wprowadzona w Servlet API 2.2. Ta metoda zwraca wartość true, gdy
użytkownik należy do określonej roli:
public boolean HttpServletRequest.isUserInRole(String role)
Ta metoda pozwala na wykonanie pewnych decyzji wewnątrz serwletu. Załóżmy, że deskryptor rozmieszczenia
pozwala uzyskać dostęp wielu różnym rolom. Wywołanie tej metody pozwala serwletowi na zróżnicowanie
dostępu do danych zależności od uwierzytelnionej roli użytkownika.
W deskryptorze rozmieszczeń (pliku opisu rozmieszczenia) można utworzyć pseudonimy, (można sprawić, by
zasięg roli mgr będzie taki sam, jak zasięg roli manager). Ma to zastosowanie podczas integracji serwletów
pochodzących z innych aplikacji sieciowych, które używają innych nazw ról niż nasz serwlet. Pseudonimy są
konfigurowane osobno dla każdego serwletu, za pomocą znacznika <security-role-ref> wewnątrz
znacznika <servlet>, jak pokazano w poniższym fragmencie web.xml:
<servlet>
<servlet-name>
secret
</servlet-name>
<servlet-class>
SalaryViewer
</servlet-class>
<security-role-ref>
<role-name>
mgr <!-- Nazwa używana przez serwlet -->
</role-name>
<role-link>
manager <!-- Nazwa używana przez deskryptor rozmieszczenia -->
</role-link>
</security-role-ref>
</servlet>

Może istnieć dowolna ilość znaczników <security-role-ref>. Należy pamiętać, że dane pseudonimy są
ważne tylko podczas dostępu do serwletu poprzez jego zarejestrowaną nazwę.
W przykładzie 8.4 serwlet wyświetla klientowi jego nazwę, podmiot, rodzaj uwierzytelnienia (BASIC, DIGEST,
FORM, CLIENT-CERT) oraz przynależność do roli manager. Aby uruchomić ten serwlet należy zainstalować
go na serwerze sieciowym i zabezpieczyć korzystając ze schematu objaśnionego w poprzednim podpunkcie
(należy przy tym upewnić się, że dostęp do nowego <url-pattern> jest ograniczony).
Przykład 8.4.
Szpiegowanie informacji uwierzytelniania
import java.io.*;
import java.security.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class AuthenticationSnoop extends HttpServlet {


public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println("<HTML><HEAD><TITLE>AuthenticationSnoop</TITLE></HEAD><BODY>");
out.println("<PRE>");
out.println("Nazwa Uzytkownika: "+ req.getRemoteUser());
String name = (req.getUserPrincipal() == null) ?
null : req.getUserPrincipal().getName();
out.println("Nazwa Podmiotu: " + name);
out.println("Typ uwierzytelnienia: " + req.getAuthType());
out.println("Przynależność do roli Manager: " + req.isUserInRole("manager"));
out.println("</PRE>");
out.println("</BODY></HTML>");
. }
}
Po wykonaniu programu powinniśmy zobaczyć na ekranie:
This is a password protected resource
Nazwa Użytkownika: jhunter
Nazwa Podmiotu: jhunter
Typ Uwierzytelnienia: BASIC
Przynależność do roli Manager: false

Uwierzytelnienie w oparciu o formularz


Zamiast uwierzytelnienia poprzez HTTP, serwlety mogą używać formularzy HTML. Użycie tej techniki pozwala
użytkownikom wejść na chronioną witrynę poprzez odpowiednio zaprojektowaną, przyjazną użytkownikowi
stronę logującą. Wyobraźmy sobie, że prowadzimy bank internetowy. Lepiej utworzyć niestandardowy formularz
logujący (rys. 8.1), który poprosi o odpowiednie dane, niż korzystać z obskurnego okna przeglądarki.
Wiele banków i innych serwisów online wybrało uwierzytelnienie oparte o formularz. Implementacja takiego
systemu jest stosunkowo prostym zadaniem, ponieważ takie uwierzytelnienie jest wbudowane w Servlet API 2.2.
Aby przekształcić uwierzytelnienie z podstawowego na oparte o formularz należy zamienić fragment pliku
web.xml <login-config> z przykładu 8.3 na fragment z przykładu 8.5.

Rysunek 8.1. Strona logująca do banku online

Przykład 8.5.
Konfiguracja uwierzytelnienia opartego o formularz
<login-config>
<auth-method>
FORM <!-- BASIC, DIGEST, FORM, CLIENT-CERT 
</auth-method>
<form-login-config> <!-- Przydatne tylko dla FORM -->
<form-login-page>
/loginpage.html
</form-login-page>
<form-error-page>
/errorpage.html
</form-error-page>
</form-login-config>
</login-config>
Należy zauważyć, że wewnątrz znacznika <auth-method> zmieniono tryb BASIC na FORM. To oznacza, że
w aplikacji sieciowej należy użyć uwierzytelnienia za pomocą formularza HTML. Znacznik <realm-name>
zamieniono na <form-login-config>. Ten znacznik określa stronę logującą i stronę błędnego logowania
używane w procesie uwierzytelnienia. Obie strony powinny być dobrze zaprojektowane i opisowe, przy czym
strona logująca powinna prosić użytkownika o podanie danych, a strona błędnego logowania informować serwer
o błędnych danych. Obie ścieżki adresów URL muszą być umieszczone w context root.
Za każdym razem, gdy serwer otrzymuje żądanie do chronionych danych, sprawdza czy użytkownik nie jest już
zalogowany. Serwer może na przykład przeszukiwać obiekt HttpSession w celu znalezienia obiektu
Principal. Jeśli serwer odnajdzie ten obiekt, porównuje zawarte w nim role do tych, które mają dostęp do
danych. Dostęp przyznawany jest tylko temu użytkownikowi, którego rola jest umieszczona w obiekcie
Principal. Jeśli serwer nie zlokalizuje obiektu Principal albo ten obiekt nie zawiera uprawnionych ról,
klient jest przekierowywany na stronę logującą (ale najpierw serwer zapisuje żądany adres URL w obiekcie
użytkownika HttpSession).
Strona logująca zawiera formularz, gdzie użytkownik wprowadza nazwę i hasło, które są przesyłane do serwera.
Dostęp do zasobów jest możliwy tylko wtedy, gdy nazwa użytkownika i hasło są ważne i należą do
uprawnionych ról obiektu Principal. Wówczas serwer przekierowuje użytkownika do chronionych zasobów,
a w przeciwnym przypadku do strony błędnego logowania.
Strona logująca musi zawierać formularz ze specjalnymi wartościami, aby zapewnić dostarczenie właściwych
danych do serwera. Formularz musi być zadeklarowany jako metoda POST dla URL j_security_check
(nie należy poprzedzać go ukośnikiem, chociaż niektóre serwery mogą uznać to za błąd) z nazwą użytkownika
przesłaną w zmiennej j_username i hasłem w j_password, na przykład:
<FORM METHOD=POST ACTION="j_security_check">
Username: <INPUT TYPE=TEXT NAME="j_username"><br>
Password: <INPUT TYPE=PASSWORD NAME="j_password"><br>
<INPUT TYPE=SUBMIT>
</FORM>
Przykład 8.6 przedstawia plik loginpage.html, który generuje formularz pokazany na rysunku 8.2.

Przykład 8.6.
Plik loginpage.html
<HTML>
<TITLE>Login</TITLE>
<BODY>
<FORM METHOD=POST ACTION=j_security_check>
<CENTER>
<TABLE BORDER=0>
<TR><TD COLSPAN=2>
<P ALIGN=center>
Witam! Proszę o podanie nazwy użytkownika <br>
i hasła aby się zalogować.
</TD></TR>

<TR><TD>
<P ALIGN=right><B>Nazwa:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="j_username" VALUE="" SIZE=15>
</TD></TR>

<TR><TD>
<P ALIGN=RIGHT><B>Hasło:</B>
</TD>
<TD>
<P><INPUT TYPE=PASSWORD NAME="j_password" VALUE="" SIZE=15>
</TD></TR>

<TR><TD>
<CENTER>
<INPUT TYPE=submit VALUE=" OK. ">
</CENTER>
</TD></TR>
</TABLE>
</FORM>
</BODY></HTML>
Rysunek 8.2 przedstawia wygenerowany formularz.
Rysunek 8.2. Formularz logujący przyjazny dla użytkownika
Strona błędnego logowania określona w sekcji <login-config> pliku web.xml może być jakimkolwiek
plikiem HTML. Nie przewidziano specjalnych znaczników, by ten plik dołączyć. Niestety, nie ma też dostępu do
żadnych specjalnych informacji mówiących o tym, dlaczego dostęp do danych jest zabroniony i wskazujących
użytkownikowi stronę, za pomocą której powinien spróbować zalogować się ponownie. Przykład 8.7 ilustruje
prostą stronę błędnego logowania.

Przykład 8.7.
Plik errorpage.html
<HTML>
<TITLE> Login Denied <TITLE>
<BODY>
Niestety, operacja logowania nie powiodła się.
Proszę wrócić do poprzedniej strony i spróbować jeszcze raz.
</BODY></HTML>

W porównaniu z uwierzytelnieniem podstawowym, logowanie przez formularz jest lepsze, ponieważ użytkownik
może wejść do witryny poprzez przyjazną i przejrzystą stronę logującą. W obu uwierzytelnieniach hasło jest
przesyłane jako tekst jawny chyba, że kanał komunikacyjny zostanie zabezpieczony innymi metodami.
Oba omówione uwierzytelnienia nie zapewniają mechanizmu wylogowania. W przypadku uwierzytelnienia
opartego o formularz można wywołać metodę session.Invalidate(), ale nie gwarantuje to zamierzonego
efektu. W obu sposobach serwer sprawdza ważność użytkowników nawet wówczas, gdy kontrola nie powinna
być dokonywana przez serwer (na przykład: niektóre banki wymagają podania numeru konta, hasła i PIN-u, by
umożliwić dostęp). Aby rozwiązać ten problem, należy skorzystać z uwierzytelnienia niestandardowego.

Uwierzytelnienie niestandardowe
Zwykle uwierzytelnienie klienta odbywa się w serwerze sieciowym. Deskryptor rozmieszczenia przekazuje
serwerowi informację o zastrzeżonych zasobach i sposobie przydzielania dostępu.
Często takie podejście jest wystarczające, ale czasem pożądany system ochrony nie może być zaimplementowany
na serwerze. Może być to spowodowane tym, że lista użytkowników jest zapisana w formacie nieznanym
serwerowi albo udostępnianie zasobów użytkownikom odbywa się za pomocą wspólnego hasła. Aby poradzić
sobie w takich sytuacjach, można skorzystać z serwletów. Serwlety mogą pobierać informacje o użytkownikach
ze specjalnie sformatowanego pliku lub relacyjnej bazy danych. Dodatkowo można zastosować określony system
ochrony (standardowy lub niestandardowy). Za pomocą tych serwletów można dodawać, usuwać lub zmieniać
dane użytkowników.
Serwlety używają kodu stanu i nagłówków HTTP do zarządzania własnym systemem ochrony. Serwlet otrzymuje
zakodowane dane użytkownika w nagłówku Authorization. Odmawia dostępu danemu użytkownikowi
poprzez przesłanie statusu SC_UNAUTHORIZED i nagłówka WWW-Authenticate, opisującego określone
dane użytkownika. Serwer zazwyczaj obsługuje wyżej opisane zdarzenia bez udziału serwletu, ale nie ogranicza
dostępu do danych, gdy autoryzacja jest przeprowadzona w serwlecie.
Nagłówek Authorization przesłany przez klienta zawiera jego nazwę i hasło. W podstawowym schemacie
autoryzacji, nagłówek Authorization zawiera łańcuch tekstowy username:password, zakodowany za
pomocą kodera Base64. Na przykład, nazwa webmaster i hasło try2gueSS są przesyłane w nagłówku
Authorization o wartości:
Authorization: BASIC d2VibWFzdGVyOnRyeTJndWVTUw
Serwlet może przesłać nagłówek WWW-Authenticate, aby poinformować klienta o sposobie autoryzacji i o
obszarze weryfikacji użytkowników. Obszar stanowi zbiór kont użytkowników i chronionych zasobów. Na
przykład, do przesłania klientowi informacji, aby użył uwierzytelnienia podstawowego w dziedzinie Admin,
nagłówek WWW-Authenticate wygląda następująco:
WWW-Authenticate: BASIC realm="Admin"
W przykładzie 8.8 pokazano, jak serwlet wykonuje niestandardową autoryzację, otrzymując nagłówek
Authorization i wysyłając status SC_UNAUTHORIZED oraz nagłówek WWW-Authenticate. Tak
skonfigurowany serwlet ogranicza dostęp do „ściśle tajnego zbioru” tylko do grona użytkowników, których
rozpozna na liście. W tym przykładzie, lista jest przechowywana w prostej tablicy mieszającej, a jej
zawartość jest zakodowana. Listę użytkowników można także dołączyć poprzez zewnętrzną relacyjną bazę
danych.
Aby odczytać nazwę i hasło zakodowane za pomocą Base64, należy skorzystać z dekodera Base64. Na szczęście
jest on dostępny w darmowej wersji. W naszym serwlecie użyjemy własnej klasy
com.oreilly.servlet.Base64Decoder dostępnej na stronie http://www.servlets.com. Szczegóły
dotyczące kodowania Base64 można znaleźć na stronie http://www.ietf.org/rfc/rfc1521.txt*.
Przykład 8.8.
Bezpieczeństwo w serwlecie.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.Base64Decoder;
public classs CustomAuth extends HttpServlet {
Hashtable users = new Hashtable();
public void init (ServletConfig config) throws ServletException {
super.init(config);
// Nazwy i hasła są tajne!
users.put("Wallace:cheese", "allowed");
users.put("Gromit:sheepnapper", "allowed");
users.put("Penguin:evil", "allowed");
}
public void doGet (httpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/plain");
PrintWriter out = res.getWriter();
// pobierz nagłówek autoryzacji
String auth = req.GetHeader("Authorization");
//Czy zezwalamy na dostęp temu użytkownikowi?
if(!allowUser(auth)) {
//nie zezwalamy, nie jest autoryzowany
res.setHeader("WWW-Autheticate", "BASIC realm=\"users\"");
res.sendError(res.S.C._UNAUTHORIZED);
//można zaoferować mu wpisanie na listę zatwierdzonych użytkowników
}
else{
//dopuszczony, wiec pokażmy mu tajne rzeczy
out.println("Ściśle tajne rzeczy");
}
}

//ta metoda sprawdza informacje użytkownika przesłana w nagłówku Authorization


//z baza danych użytkowników zawarta w tablicy mieszającej users
protected boolean allowUser (String auth) throws IOException {
if (auth==null) return false; //brak auth
if(!auth.toUpperCase().startsWith("BASIC"))
return false;//my tylko przeprowadzamy tryb BASIC
//popierz zakodowaną nazwę użytkownika i hasło po "BASIC"
String userpassEncoded = auth.substring(6);
//zdekoduj, używając jakikolwiek dekoder Base64 (my używamy com.oreilly.servlet)
String userpassDecoded = Base64Decoder.decode(userpassEncoded);
//sprawdź nasza listę użytkowników czy ten jest "allowed"
if("allowed".equals(users.get(userPassDecoded)))
return true;

*
Można także użyć klasy sun.misc.BASE64Decoder, która towarzyszy JDK.
else
return false;
}

Dostęp do serwletu mają wszyscy użytkownicy. W serwlecie dokonuje się proces uwierzytelnienia: dostęp do
chronionych zasobów jest przydzielany użytkownikom których dane uwierzytelniające znajdują się na liście
uprawnionych. Po dokonaniu kilku modyfikacji za pomocą tego serwletu można udostępnić dane użytkownikom
posiadającym hasło grupowe albo, podobnie do anonimowego FTP, można udostępnić zasoby użytkownikowi o
nazwie anonymous i haśle, którym jest adres email.
Autoryzację niestandardową można użyć nie tylko do ograniczenia dostępu do pojedynczego serwletu.
Gdybyśmy taką logikę dodali do naszego serwletu ViewResource, moglibyśmy zaimplementować
niestandardowy sposób dostępu do pełnego zestawu plików. Reguła mapowania za pomocą przedrostków URL
mogłaby być utworzona w ten sposób, aby ochrona serwletu ViewResource udostępniała całą strukturę
katalogów chronionych plików. Gdybyśmy utworzyli specjalną podklasę z HttpServlet i dodali do niej tą
logikę, moglibyśmy w łatwy sposób ograniczyć dostęp do każdego serwletu pochodnego tej podklasy. W
metodzie niestandardowej autoryzacji sposób ograniczenia zabezpieczeń serwera nie wpływa na sposób
zabezpieczenia jego serwletów.

Autoryzacja niestandardowa w oparciu o formularz


Serwlety mają możliwość wykonania autoryzacji niestandardowej w oparciu o formularz. Poprzez logowanie
niestandardowe oparte o formularz, aplikacja sieciowa może użyć strony logującej HTML. Na przykład, aby
zalogować się do niektórych aplikacji bankowych, należy oprócz nazwy i hasła podać także numer konta i PIN.
Takiej możliwości nie zapewnia użycie FORM <auth-method>, lecz autoryzacja niestandardowa w oparciu o
formularz. Jest to bardziej skomplikowane zadanie, bo szczegóły procesu logowania muszą być obsługiwane
ręcznie.
Najpierw trzeba stworzyć stronę logującą, którą można napisać tak jak inne formularze HTML. Przykład 8.9
przedstawia plik login.html, który generuje formularz pokazany na rysunku 8.3.

Przykład 8.9
Plik login.html
<HTML>
<TITLE Login </TITLE>
<BODY>
<FORM ACTION=/servlet/LoginHandler METHOD=POST>
<CENTER>
<TABLE BORDER=0>
<TR><TD COLSPAN=2>
<P ALIGN=CENTER>
Witamy!<br>
Proszę wprowadzić numer rachunku, <br>
hasło i PIN, aby się zalogować
</TD></TR>
<TR><TD>
<P ALIGN=RIGHT><B> Rachunek:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="account" VALUE="" SIZE=15>
</TD></TR>

<TR><TD>
<P ALIGN=RIGHT><B>Hasło: </B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="password" VALUE="" SIZE=15>
</TR></TD>

<TR><TD>
<P ALIGN=RIGHT><B>PIN:</B>
</TD>
<TD>
<P><INPUT TYPE=TEXT NAME="PIN" VALUE="" SIZE=15>
</TR></TD>

<TR><TD COLSPAN=2>
<CENTER>
<INPUT TYPE=SUBMIT VALUE=" OK. ">
</CENTER>
</TD></TR>
</TABLE>
</FORM>
</BODY></HTML>

Rysunek 8.3 przedstawia formularz.

Rysunek 8.3. Przyjazny formularz logowania się do banku


Ten formularz prosi klienta o podanie numeru konta, hasła i kodu PIN, po czym przesyła informację do serwletu
LoginHandler zatwierdzającego logowanie. Klient może dostać się do tej strony bezpośrednio lub poprzez
link ze strony głównej witryny. Gdyby użytkownik chciał dostać się bezpośrednio do zasobów (bez logowania
się) powinien być automatycznie skierowany na stronę logującą, a po pomyślnym zalogowaniu przekierowany z
powrotem. Proces powinien przebiegać niezauważalnie. Użytkownik odnosi wrażenie, że to jego przeglądarka
otworzyła okno.
Przykład 8.10 przedstawia serwlet implementujący przekierowania. Serwlet ten wysyła tajne informacje tylko
wtedy, gdy obiekt sesji klienta wskazuje zalogowanie. Jeśli tak nie jest, serwlet zapamiętuje żądany URL w sesji
do przyszłego użytku i przekierowuje klienta na stronę logującą.
Przykład 8.10.
Chronione Źródło
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class protectResource extends HttpServlet {


public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/plain";
PrintWriter out = res.getWriter();

//pobierz sesję
HttpSession session = req.getSession();
//czy sesja rozpozna użytkownika jako już zalogowanego?
Object done=sesion.getAttribute("logon.isDone");//zaznacz obiekt
if (done == null) {
//zaprzeczenie logon.isDone znaczy, że użytkownik się nie zalogował
//zapisz zadany URL jako prawdziwy cel i prześlij do login page
session.setAttribute("login.target",
HttpUtils.getRequestURL(req).toString());
res.sendRedirect(„/login.html”);
return;
}
//użytkownik zalogował się i może zobaczyć towary
out.println("Nieopublikowane książki wydawnictwa Helion czekają na ciebie!");
}
}
Ten serwlet sprawdza, czy klient jest już zalogowany szukając obiektu logon.isDone. Jeśli taki obiekt
istnieje, serwlet ma pewność, że klient już się zalogował, w związku z czym pozwala mu przeglądać tajne
informacje. Brak tego obiektu oznacza, że klient nie jest zalogowany, wobec czego serwlet zapisuje żądany adres
URL w zmiennej login.target i przekierowuje klienta na stronę logującą. W niestandardowej autoryzacji
opartej o formularz należy zapewnić takie zachowanie. Użycie podklas lub klas użytkowych może uprościć to
zadanie.
W naszym przykładowym serwlecie sprawdzana jest ważność numeru konta, hasła i kodu PIN. Jeśli
wprowadzone przez klienta dane są nieprawidłowe, dostęp do informacji jest zabroniony. W przeciwnym
wypadku, fakt ten jest zapisywany w obiekcie sesji i klient jest przekierowywany na żądaną stronę.
Przykład 8.11.
Obsługa logowania.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
//pobierz numer rachunku, hasło i PIN użytkownika
String account = req.getparametr("account");
String password = req.getParametr("password");
String pin = req.getParametr("pin");

//sprawdzenie ważności hasła i nazwy


if (!allowUser(account, password, pin)) {
out.println("<HTML><HEAD><TITLE>Odmowa dostępu</TITLE></HEAD>");
out.println("<BODY>Hasło i login są nieważne. <BR>");
out.println("Możesz <A HREF=\"/login.html\"> sprobować ponownie</A>");
out.println("</BODY></HTML>");
}
else
{
//login ważny, zanotować to w obiekcie sesji.
HttpSession session = req.getSession();
session.setAttribute ("logon.isDone", account); //zaznaczenie obiektu

spróbuj przesłać klienta do strony, na która chciał wejść


try{
String target = (String) session.getAttribute("login.target");
if (target != null {
res.sendRedirect(target);
return;
}
}
catch (Exception ignored){}
//nie było możliwości przesłania do target. Przekierowanie do strony startowej.
res.sendRedirect ("/");
}
}
protected boolean allowUser (String account, String password, String pin) {
return true;//zaufaj każdemu
}
}
Właściwy sposób kontroli danych w serwlecie jest prosty: serwlet zakłada, że wszystkie dane użytkownika są
ważne. Jeśli logowanie się powiedzie, serwlet zachowuje numer rachunku użytkownika w sesji klienta pod nazwą
logon.isDone. Dla chronionych zasobów oznacza to, że klient ma do nich prawo dostępu. Klient zostaje
przekierowany do strony, której adres znajduje się w zmiennej login.target. Jeśli z jakiegokolwiek powodu
ta operacja się nie powiedzie, serwlet przekieruje użytkownika na stronę główną.

Certyfikaty cyfrowe
Prawdziwe aplikacje wymagają zabezpieczeń wyższego poziomu niż opisane do tej pory. Wymagają również
zagwarantowania poufności i integralności informacji oraz bardziej niezawodnego systemu uwierzytelniania. To
właśnie oferuje technologia certyfikatów cyfrowych.
Główną koncepcją jest system kryptografii oparty o klucze publiczne. W tym systemie każdy uczestnik wymiany
informacji posiada unikatową parę kluczy używanych do kodowania i dekodowania informacji. Jednym z nich
jest ogólnie dostępny klucz publiczny, a drugim — tajny klucz prywatny. Aby zademonstrować zasadę działania
systemu, załóżmy, że Jaś chce przesłać Krzysiowi tajną informację. Jaś znajduje w swoich zasobach klucz
publiczny Krzysia i za jego pomocą koduje informację. Gdy Krzyś otrzyma wiadomość, użyje swojego klucza
prywatnego do jej zdekodowania. Informacja staje się bezwartościowa dla każdego, kto ją przechwyci i nie
posiada klucza prywatnego Krzysia.
Metody kodowania wykorzystujące klucze cyfrowe istnieją już od kilku lat i są dość dobrze rozwinięte.
Większość z nich działa w oparciu o algorytm RSA stworzony przez Rona Rivesta, Adiego Shamira i Leonarda
Adelmana.* RSA używa dużej ilości liczb pierwszych do generowania unikatowej pary kluczy asymetrycznych
(wiadomość zakodowaną przez jeden klucz można zdekodować używając drugiego). Generowane klucze mogą
różnić się długością, zwykle wyrażaną w ilości bitów składających się na klucz. Klucze 1024- lub 2048-bitowe są
wystarczające do zapewnienia bezpiecznej komunikacji RSA.
Ponieważ klucze składają się z tak wielu znaków, niepraktycznym podejściem byłoby zmuszanie użytkownika do
wpisywania klucza za każdym razem. Zamiast tego, klucze publiczne są przechowywane na dysku w formie
certyfikatów cyfrowych, a klucze prywatne w zakodowanej formie. Certyfikaty cyfrowe mogą być generowane za
pomocą specjalnych programów (takich jak np. pakiet PGP) lub są wydawane przez specjalistyczną firmę. Pliki z
certyfikatami mogą być ładowane przez najbardziej chronione aplikacje (takie jak serwery, przeglądarki i
programy obsługujące pocztę elektroniczną).
Kryptografia kluczy publicznych rozwiązuje problem poufności, ponieważ cały proces komunikacji jest
zakodowany. Metoda ta zapewnia również integralność: w naszym przykładzie Krzyś wie, że wiadomość, którą
otrzymał nie została zmodyfikowana (gdyby tak było, nie byłby w stanie jej zdekodować). Nie rozwiązaliśmy
jeszcze problemu wierzytelności: Krzyś nie ma pewności, że wiadomość, którą odebrał została wysłana przez
Jasia. Ten problem rozwiązuje podpis elektroniczny. Ponieważ klucze prywatne i publiczne są asymetryczne, Jaś
może najpierw użyć własnego klucza prywatnego do zakodowania wiadomości, a następnie zaszyfrować
informację jeszcze raz, za pomocą publicznego klucza Krzysia. Gdy Krzyś odbierze wiadomość, zdekoduje ją
najpierw własnym kluczem prywatnym, a następnie kluczem publicznym Jasia. Teraz Krzyś wie, że jedynym
nadawcą otrzymanej wiadomości mógł być tylko Jaś, bo wiadomość zakodowana jego kluczem prywatnym może
być zdekodowana jedynie za pomocą jego klucza publicznego.
Prostszym systemem jest użycie kluczy symetrycznych, czyli jednego klucza kodującego i dekodującego. Klucze
asymetryczne mają dużą zaletę: przy przesyłaniu informacji nie trzeba zapewniać bezpieczeństwa kanału
komunikacyjnego. Mają też ogromną wadę: kodowanie i dekodowanie informacji za pomocą tych kluczy
wymaga sporej mocy obliczeniowej. Jako kompromis, wiele systemów kryptograficznych korzysta z
asymetrycznych kluczy prywatnych i publicznych do wzajemnej identyfikacji i poufnego przekazania
oddzielnego klucza symetrycznego do kodowania właściwej informacji. Klucz symetryczny jest zwykle tworzony
w oparciu o standard DES.
Rząd Stanów Zjednoczonych ograniczył rozmiar eksportowanych kluczy symetrycznych do długości 56 bitów
(co daje około 72*1015 możliwych kombinacji kluczy). Wiadomości zakodowane 56-bitowym kluczem są trudne
do zdekodowania, ale nie ma rzeczy niemożliwych — wyspecjalizowane maszyny potrafią złamać kod w ciągu
kilku godzin. Wewnątrz USA wiele systemów używa 128-bitowych kluczy DES (około 3.40282*1038
możliwości). Ponieważ nie znaleziono jeszcze sposobu na złamanie kodu informacji zakodowanej kluczem DES
w sposób brutalny, przekazywanie wiadomości z wykorzystaniem kluczy o dużych rozmiarach jest bardzo
bezpieczne.
Pozostaje jeszcze jeden problem: w jaki sposób jeden użytkownik może upewnić się, że ten drugi jest faktycznie
tym, za kogo się podaje? Jaś i Krzyś znają się, więc Krzyś wierzy, że klucz publiczny dostarczony mu osobiście
przez Jasia jest prawdziwy**. Z drugiej strony, jeśli np. Małgosia, nie znając wcześniej Jasia, chciałaby mu
udostępnić swój klucz publiczny, Jaś może podejrzewać, że Małgosia jest w rzeczywistości Markiem. Jeśli
założymy, że Krzyś zna Małgosię, możemy poprosić Krzysia, by za pomocą swojego klucza prywatnego
podpisał klucz publiczny Małgosi. Wówczas, Jaś dostanie klucz Małgosi potwierdzony przez Krzysia, do
którego ma zaufanie.
W rzeczywistości, ta „trzecia strona” potwierdzająca autentyczność nazywana jest Centrum Certyfikacji. Takim
centrum jest np. korporacja VeriSign. Ponieważ VeriSign jest ogólnie znaną organizacją (z powszechnie znanym
kluczem publicznym) klucze zweryfikowane przez to centrum można uznać za pewne. VeriSign oferuje kilka
klas identyfikatorów (wraz ze wzrostem numeru klasy rośnie jego poziom zaufania i cena). Certyfikaty pierwszej
klasy można otrzymać po wypełnieniu odpowiedniego formularza zamieszczonego w witrynie VeriSign.
Certyfikaty wyższych klas są weryfikowane indywidualnie przez pracowników tego centrum certyfikacji, przy
użyciu specjalnych metod weryfikacyjnych.
Wybierając centrum autoryzacji należy zwrócić uwagę na jego prestiż. Certyfikaty VeriSign są dołączone do
przeglądarek MS IE i Netscape, więc każdy użytkownik Internetu będzie je akceptował. Poniżej wypisano
najpopularniejsze centra certyfikacji:
• VeriSign (http://www.verisign.com)

*
Patent USA nr 4.405.829. Utracił ważność 20 września 2000 r.
**
Szczerze mówiąc, ludzie nie spotykają się w ciemnej alejce po to, aby wymienić swoje klucze publiczne. Zamiast tego wymieniają klucze
w cyfrowy sposób (np. za pomocą email) i rozpoznają klucz po kilku pierwszych znakach.
• Thawte Consulting (http://www.thawte.com)
• Entrust Technologies (http://www.entrust.com)
Więcej informacji na temat certyfikatów cyfrowych znajduje się w książce Gail'a L.Granta (McGraw-Hill)
Understanding Digital Signatures, która dostarcza wprowadzenie odpowiednie dla programistów i zwykłych
użytkowników. O powiązaniu kryptografii z Javą traktuje książka Jonathana Knudsena Java Cryptography
(O'Reilly).

Protokół bezpiecznej transmisji danych (SSL)


Protokół SSL zajmuje miejsce pomiędzy protokołem poziomu aplikacji (w tym przypadku HTTP) a protokołem
transportowym niskiego poziomu (w przypadku Internetu — prawie wyłącznie TCP/IP). Protokół ten
wykorzystuje klucze publiczne do wymiany kluczy symetrycznych, które służą do zakodowania komunikacji
pomiędzy klientem i serwerem. Po raz pierwszy wykorzystano ten protokół w przeglądarce Netscape Navigator
1. Od tamtej pory SSL jest standardem bezpieczeństwa komunikacji online i jest podstawą nowego protokołu
TLS (który jest tworzony przez IETF). Więcej informacji na temat TLS można uzyskać na stronie
http://www.ietf,org/rfc/rfc2246.txt.
SSL Version 2.0 jest pierwszą wersją, która zyskała szeroką akceptację. Zawiera ona wsparcie tylko dla
certyfikatów serwerowych, zapewnia uwierzytelnienie serwera, poufność i integralność. Zasady działania
protokołu są następujące:
1. Użytkownik łączy się z zabezpieczoną witryną przy użyciu protokołu HTTPS (protokół HTTP i
SSL).Adresy URL witryn, które używają protokołu HTTPS, zaczynają się od https: zamiast http:.
2. Serwer podpisuje kluczem prywatnym swój klucz publiczny i przesyła go przeglądarce.
3. Przeglądarka używa klucza publicznego serwera, by sprawdzić, czy osoba podpisująca klucz jest w
jego posiadaniu.
4. Przeglądarka upewnia się, czy zaufane centrum certyfikacji podpisało klucz. Jeśli nie, przeglądarka
pyta użytkownika, czy klucz można uznać za wiarygodny i postępuje zgodnie z jego zaleceniami.
5. Klient generuje dla tej sesji klucz symetryczny DES, koduje publicznym kluczem serwera i odsyła
serwerowi. Powstały w ten sposób nowy klucz jest użyty do zakodowania transakcji. Klucz
symetryczny jest używany dlatego, że systemy oparte o klucze asymetryczne bardzo obciążają
system.
Wszystkie opisane tu procedury są niewidoczne dla twórców serwerów i serwletów. Należy po prostu otrzymać
odpowiedni certyfikat serwerowy, zainstalować go i odpowiednio skonfigurować serwer. Informacje przesyłane
pomiędzy serwletami i klientami będą od tej pory zakodowane.

Uwierzytelnianie klienta za pomocą SSL


Nasza skrzynka z narzędziami zabezpieczającymi zawiera już solidne systemy kodowania i uwierzytelnienia
serwera, ale słaby system uwierzytelniania klienta. Oczywiście użycie SSL 2.0 stawia nas w dużo lepszej
sytuacji. Serwery wyposażone w SSL mogą jednocześnie używać podstawowych metod uwierzytelniania
omówionych na początku niniejszego rozdziału, ponieważ nie ma możliwości przechwycenia informacji. Nadal
jednak nie mamy dowodu na wiarygodność klienta, bo każdy mógłby zgadnąć lub zawładnąć hasłem i nazwą
użytkownika.
Powyższy problem rozwiązano w nowszej wersji SSL 3.0 poprzez zapewnienie obsługi certyfikatów klienckich.
Te certyfikaty są zarejestrowane jako klienckie, ale niczym nie różnią się od serwerowych. SSL 3.0 z
uwierzytelnieniem klienckim pracuje podobnie do SSL 2.0, ale po uwierzytelnieniu serwera przez klienta, serwer
żąda od niego certyfikatu. Klient przesyła podpisany certyfikat i serwer przeprowadza taką samą procedurę
uwierzytelniającą, jaka odbyła się po stronie klienta (serwer porównuje certyfikat klienta z biblioteką istniejących
certyfikatów lub przechowuje certyfikat, aby zweryfikować użytkownika przy kolejnej wizycie). Wiele
przeglądarek zanim wyśle certyfikat, jako zabezpieczenie, wymaga podania hasła przez użytkownika.
Klientowi raz uwierzytelnionemu serwer udostępni chronione zasoby (serwlety lub pliki) tak, jak za pomocą
uwierzytelnienia HTTP. Cały proces przebiega w sposób niewidoczny dla użytkownika. Ponadto ten sposób
dostarcza dodatkowy poziom uwierzytelnienia, bo serwer wie, że klient posługujący się certyfikatem Jasia
Kowalskiego jest w istocie Jasiem Kowalskim (w dodatku może rozpoznać konkretnego Jasia Kowalskiego
poprzez jego unikatowy certyfikat). Wadami certyfikatów klienckich jest konieczność ich otrzymania i instalacji
przez użytkowników. Ponadto serwery muszą zawierać bazę danych z kluczami publicznymi, a przede wszystkim
obsługiwać SSL 3.0.

Konfigurowanie ochrony za pomocą SSL


Aplikacja sieciowa, która wymaga ochrony za pomocą SSL (HTTPS) może o tym powiadomić serwer przy
użyciu deskryptora rozmieszczenia. Wymaganie SSL modyfikuje plik web.xml w ten sposób, że znacznik
<security-constraint> zawiera znacznik <user-data-constraint> wskazujący wymagania
dotyczące systemu zabezpieczeń. Przykład 8.12 przedstawia ten plik.
Przykład 8.12.
Ta kolekcja wymaga bezpiecznego połączenia
<!-- ...itp... -->
<security-constraint>
<web-resource-collection>
<web-resource-name>
SecretProtection
</web-resource-name>
<url-pattern>
/servlet/SalaryServer
</url-pattern>
<url-pattern>
/servlet/secret
</url-pattern>
<url-method>
GET
</url-method>
<url-method>
POST
</url-method>
</web-resource-collection>
<auth-constraint>
<role-name>
manager
</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>
CONFIDENTIAL
</transport-guarantee>
</user-data-constraint>
</security-constraint>
<!-- ...itp... -->
Dodany znacznik <user-data-constraint> informuje o tym, że dostęp do danych jest zapewniany
użytkownikom w roli manager oraz, że użytkownicy muszą łączyć się używając trybu CONFIDENTIAL.
Znacznik <transport-guaratee> może zawierać jedną z dwóch wartości: INTEGRAL albo
CONFIDENTIAL. INTEGRAL wymaga niezmienności danych podczas transmisji. Z kolei CONFIDENTIAL
wymaga poufności danych. Zastosowanie CONFIDENTIAL pociąga za sobą jednoczesne użycie INTEGRAL.
Serwer decyduje, które algorytmy kryptograficzne można zakwalifikować jako INTEGRAL, a które jako
CONFIDENTIAL. Większość powszechnie znanych algorytmów SSL zalicza się do obu grup. Jednakże serwer
może zaliczyć algorytmy wykorzystujące kodowanie 56-bitowe DES do INTEGRAL (informacja nie może być
naruszona, aby ją zdekodować), a nie do CONFIDENTIAL (bo łatwo złamać taki kod)*. W praktyce pomiędzy
tymi trybami jest niewielka różnica, a ogólnie gwarantowanym standardem jest CONFIDENTIAL.
Znaczniki <auth-constraint> i <user-data-constraint> mogą występować razem lub osobno. Na
przykład, strona obsługująca kartę kredytową wymaga komunikacji typu CONFIDENTIAL, ale może pozostać
widoczna dla wszystkich użytkowników.

Konfiguracja uwierzytelnienia SSL


SSL 3.0 zyskuje popularność, bo coraz więcej witryn uwierzytelnia użytkowników za pomocą certyfikatów
klienckich. To zapewnia automatyczną i bezpieczną metodę identyfikacji użytkownika klienta. W zależności od
poziomu zaufania powiązanego z certyfikatem klienckim, można uważać mechanizm uwierzytelnienia za
dostatecznie bezpieczny, by wspomagać podpisywanie prawnie wiążących kontraktów lub nawet głosowanie
online.

*
Połączenie klient-serwer poprzez bezpieczną sieć VPN może spotkac się z ograniczeniem CONFIDENTIAL nawet bez użycia SSL.
Można zgadnąć, że uwierzytelnienie oparte o certyfikaty klienckie może być wskazane wewnątrz znacznika
<login-config>:
<login-config>
<auth-method>
CLIENT_CERT <!—Klient musi zostać zidentyfikowany za pomocą certyfikatu X.509 --!>
</auth-method>
</login-config>
Tak określony znacznik jest informacją dla serwera, że wszystkie procesy uwierzytelnienia dla danej aplikacji
będą wykonywane przy użyciu certyfikatów klienckich zamiast tradycyjnych metod: podstawowej lub w oparciu
o formularz. Klient nigdy nie ujrzy strony logującej, mimo że przeglądarka poprosi o hasło do odblokowania
certyfikatu przed przesłaniem do serwera. Jeśli przeglądarka nie otrzyma certyfikatu klienckiego, dostęp do
danych będzie zabroniony.

Wyszukiwanie informacji uwierzytelnienia SSL


Podczas uwierzytelnienia podstawowego i szyfrowanego wszystkie szczegóły dotyczące SSL obsługiwane są
przez serwer w sposób niewidoczny dla serwletów. Innymi słowy, serwlet nie musi spełniać jakichkolwiek
kryteriów, by mieć dostęp do zabezpieczonego serwera! Serwlet może czasami wyszukiwać użytecznych
informacji dotyczących uwierzytelnienia SSL. Na przykład, serwlet może sprawdzić, czy połączenie z klientem
jest bezpieczne używając metody isSecure():
public boolean ServletRequest.isSecure()
Metoda zwraca true, jeśli serwer uzna połączenie za bezpieczne. Poziom kodowania zależy od implementacji
serwera. Potrzebne detale można znaleźć w dokumentacji serwera. Niestety, serwlet nie może w standardowy
sposób zażądać właściwego algorytmu kodowania użytego do połączenia, a nawet długości symetrycznego
klucza (40, 56, 128). Taką opcje można ustawić w serwerze jako atrybut o nieokreślonej do tej pory nazwie.
Taka cecha jest oczekiwana w Servlet API 2.3, który używa atrybutów żądania:
javax.servlet.request.cipher_suite i javax.servlet.request.key_size.
Gdy klient zostanie uwierzytelniony za pomocą CLIENT-CERT metoda getUserPrincipal() zwróci
nazwę jego podmiotu. Nazwa ta jest wzięta z pola certyfikatu Distinguished Name.
Za każdym razem, gdy certyfikat kliencki jest wysyłany do serwera, serwlet może otrzymać go jako atrybut
żądania. Może to pojawić się podczas zwykłego procesu przesłania z potwierdzeniem SSL 3.0, nawet gdy
CLIENT-CERT nie jest określony.
java.security.cert.X509Certificate cert =
(java.security.cert.X509Certificate)
req.getAttribute(„javax.servlet.request.X509Certificate”);
Dla każdego serwera pracującego na J2SE 1.2 (JDK 1.2) lub obsługującego J2EE 1.2 atrybut żądania
javax.servlet.request.X509Certyficate zwróci obiekt
java.security.cert.X509Certyficate reprezentujący certyfikat X.509v3 (instrukcje konfigurujące
można znaleźć w dokumentacji serwera). Serwery pracujące na JDK 1.1, które są niekompatybilne z J2EE 1.2
nie wspierają tego atrybutu, ponieważ JDK 1.1 nie zawiera pakietu java.security.cert. Serwlet z
przykładu 8.13 drukuje łańcuch certyfikatu klienta.
Przykład 8.13
Sprawdzanie certyfikatów klienckich

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class X509Snoop extends HttpServlet {
public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType ("text/plain");
PrintWriter out = res.getWriter();
X509Certificate[] certs = (X509Certificate[])
req.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null) {
for (int i=0; i<certs.length; i++) {
out.println("certyfikat klienta [" + i + "] = " + certs[i].tostoring());
}
}
else {
if („https”.equals(req.getScheme())){
out.println(„To było HTTPS, ale brak certyfikatu,”+”...”);
}
else {
out.println(„to nie był HTTPS,”+
„wiec żaden certyfikat nie jest możliwy”);
}
}
}
}

Zwrócony certyfikat X509Certyficate może być użyty do sprawdzenia ważności, wydawcy, numeru
seryjnego, podpisu i tak dalej. Wydruk certyfikatu jako String pokazano w przykładzie 8.14. Pierwszy
certyfikat jest kluczem publicznym użytkownika. Drugi z nich to podpis VeriSign potwierdzający autentyczność
pierwszego podpisu.
Przykład 8.14.
Przykładowy certyfikat X509 dla Ramesha Mandavy.
Client Certificate [0] =[
[
Version: V3
Subject: EmailAddress=rmandava@talentportal.com, CN=Ramesh babu mandava,
OU=Digital
ID Class 1 –
Netscape, OU=Persona Not Validated, OU="www.verisign.com/repository/RPA Incorp. By
Ref., LIAB.LTD©98", OU=VeriSign Trust Network, O="VeriSign, INC."
Signature Algorithm: MD5withRSA, OID = 1.2.840.113549.1.1.4

Key: com.sun.net.ssl.internal.ssl.JSA_RSAPublicKey@5b5870e3
Validity: [From: Tue Oct 10 17:00:00 PDT 2000,
To: sun Dec 10 15:59:59 PST 2000]
Issuer: CN=VeriSign Class 1 CA Individual subscriber-Persona not Validated,
OU="www.verisign.com/repository/RPA incorp. By Ref.,LIAB.LTD(c)98", OU=VeriSign
Trust Network, O="VeriSign, Inc."
SerialNumber: [ 1ef11638 5ab8aaa1 bfa2b11b3 c0fb9cd9]
Certificate Extensions: 4
[1]: ObjectId: 2.16.840.1.113730.1.1 Criticality=false
NetscapecertType [
SSL client
]
[2]: ObjectId: 2.5.29.32 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 3D 30 3B 30 39 06 0B 60 86 48 01 86 F8 45 01 .=0;09..'.H...E.
0010: 07 01 08 30 2A 30 28 06 08 2B 06 01 05 05 07 02 ...0*0(..+......
0020: 01 16 1C 68 74 74 70 73 3A 2F 2F 77 77 77 2E 76 ...https:/www.v
0030: 65 72 69 73 69 67 6E 2E 63 6F 6D 2F 72 70 61 erisign.com/rpa

[3]: ObjectId: 2.5.29.31 Criticality=false


Extension unknown: DER encoded OCTET string =
0000: 04 2C 30 2A 30 28 A0 26 A0 24 86 22 68 74 74 70 .,0*0(.&.$."http
0010: 3A 2F 2F 63 72 6C 2E 76 65 72 69 73 69 67 6E 2E ://crl.verisign.
0020: 63 6F 6D 2F 63 6C 61 73 73 31 2E 63 72 6C com/class1.crl

[4]: ObjectId: 2.5.29.19 Criticality=false


BasiConstraints:[
CA:false
PathLen: undefined
]
]
Algorithm: [MD5withRSA]
Signature:
0000: 5E EC 5C F9 96 D5 3F F6 19 8B 66 0A 46 DE 02 FC ^.\...?...f.F...
0010: 52 4E 32 70 5F DA 8B 92 43 F4 19 51 C3 A3 36 7D RN2P^...C...Q..6
0020: 02 4A 5B 35 B6 76 05 F8 FE C0 4F D7 9C B1 5B BA .J[5.v....O...[.
0030: EE 38 A7 98 C5 57 A7 6B 86 B9 B2 A1 4F 25 5F FF .8...W.k....0%_.
0040: OB 19 54 86 D7 14 7A F7 97 A1 E8 E7 D3 89 75 B0 ..T...z.......u.
0050: 72 4F 4B 77 E4 56 5D B2 40 D2 7E 69 26 77 DD F1 rOkw.V].@..i&w..
0060: E6 31 3D F2 EF 5A 11 22 78 23 47 C2 D6 ED DD 14 .1=..Z."x#G.....
0070: 2F E9 2E 46 73 D9 20 72 BF 9B 6C 04 12 0D 68 C7 /..Fs. r..l...h.
]
Client Certificate [1] = [
[
Version: V3
Subject: CN=VeriSign Class 1 CA Individual Subsciber-Persona Not Validated,
OU="www.verisign,com/repository/RPA Incorp. By Ref., LIAB.LTD(c)98", OU=VeriSign
Trust Network, O="VeriSign, Inc."
Signature Algorithm: MD2withRSA, OID = 1.2.840.113549.1.1.2
Key: com.sun.net.ssl.internal.ssl.JSA_RSAPublicKey@9ae870e3
Validity: [From: Mon May 11 17:00:00 PDT 1998,
To: Sun May 12 16:59:59 PST 2008]
Issuer: OU=Class 1 Public Primary Certification Authority, O="VeriSign, Inc.",
C=US
SerialNumber: [ d2762e8d 140c3d7d b2a8255d afee0d75 ]
Certificate Extensions: 4
[1]: ObjectId: 2.16.840.1.113730.1.1 Criticality=false
NetscapeCerttype [
SSL CA
S/MIME CA
]
[2]: ObjesctID: 2.5.29.32 Criticality=false
Extension unknown: DER encoded OCTET string =
0000: 04 40 30 3E 30 3C 06 0B 60 86 48 01 86 E8 45 01 .@0>0<..'.H...E.
0010: 07 01 01 30 2D 30 2B 06 08 2B 06 01 05 05 07 02 ...0-0+..+......
0020: 01 16 1F 77 77 77 2E 76 65 72 69 73 69 67 6E 2E ...www.verisign.
0030: 63 6F 6D 2F 72 65 70 6F 73 69 74 6F 72 79 2F 52 com/repository/R
0040: 50 41 PA

[3]: ObjectId: 2.5.29.15 Criticality=false


KeyUsage [
Key_CertSign
Crl_Sign
]
[4]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
CA:true
PathLen:0
]
]
Algorithm: [MD2withRSA]
Signature:
0000: 88 B8 37 3B DD DA 94 37 00 AD AA 9F E1 81 01 71 ..7;...7.......q
0010: 1E 92 6A 6D 2F F6 F1 9D D3 CA 64 38 DC 1B 98 0C ..jm/.....d8....
0020: 07 86 5B 85 15 6A 0F B9 49 85 A4 95 F1 17 7D 67 ..[..j..I......g
0030: B4 7F 2D 2C DD 9A 42 9E C3 3E B4 8E AA E5 0B 06 ..-,..B..>......
0040: DE F2 56 2A FA 33 C7 BE 19 D7 53 4C C3 BD C8 E3 ..V*.3....SL....
0050: 17 B5 A4 49 42 63 EC C2 A6 17 0F 5D 58 1A 49 3C ...Ibc.....]X.I<
0060: 90 5C 55 A3 65 20 00 FD 18 20 E5 5F 82 A6 B1 A8 .\U.e... ._....
0070: 92 C5 58 6A C1 8D 03 3C EB C3 CD 05 A2 90 AE 6E ..Xj...<.......n

]
Rozdział 9. Łączność z bazą

danych

W dzisiejszych czasach trudno znaleźć profesjonalną witrynę, która nie ma powiązania z bazą danych.
Administratorzy WWW korzystają z baz danych w wyszukiwarkach internetowych, aplikacjach sklepów
internetowych i programów wysyłających wiadomości online. Współpraca aplikacji sieciowych z bazami danych
ma swoją cenę: zorientowane bazodanowo witryny internetowe mogą być trudne do tworzenia i często powodują
ograniczenie wydajności. Korzystanie z baz danych ma jednak więcej zalet niż wad co sprawia, że bazy danych
coraz bardziej „sterują” siecią.
W niniejszym rozdziale przedstawiono relacyjne bazy danych, język zapytań SQL i interfejs JDBC. Serwlety,
dzięki długiemu czasowi istnienia, oraz JDBC, dobrze zdefiniowany interfejs łączności z bazami danych, są
skutecznym rozwiązaniem dla administratorów WWW pragnących wykorzystać bazy danych w swych witrynach.
Wprawdzie w książce założono, że czytelnik zna Javę, ale ten rozdział zaczyna się krótkim kursem
wprowadzającym interfejs programistyczny JDBC.
Największą zaletą serwletów jest ich czas istnienia (szeroko opisany w rozdziale 3,”Cykl Życia Serwletu”), który
pozwala na zarządzanie pulą otwartych połączeń. Takie otwarte połączenie pozwala na szybszą komunikację
aplikacji z bazą danych. Korzystając ze skryptów CGI musimy pamiętać, że przy każdym wywołaniu trzeba
ponownie otwierać połączenie (co może zająć sporo czasu).
Przewagą serwletów nad skryptami CGI jest niezależność interfejsu JDBC od typów baz danych. Serwlet
obsługujący np. bazę danych Sybase może korzystać z bazy Oracle po niewielkiej zmianie w pliku właściwości
lub modyfikacji kilku linii (zakładając, że dostawca nie określił wywołania konkretnego typu bazy danych).
Przykłady w tym rozdziale są napisane w ten sposób, by przedstawić sposób zapewnienia dostępu do
różnorodnych baz danych, łącznie z bazami danych ODBC, takimi jak MS Access, Oracle, czy Sybase.
Serwlety w warstwie pośredniej
Warstwa pośrednia służy do połączenia aplikacji klienckich z aplikacjami serwerowymi (np. aplety
z bazami danych). Gromadzi głównie serwlety, które wykorzystują bazy danych.
Powinniśmy umieścić warstwę pośrednią pomiędzy aplikacją klienta a naszym krańcowym źródłem
danych, ponieważ oprogramowanie warstwy pośredniej (nazywane middleware) zawiera logikę
biznesową. Logika biznesowa oddziela skomplikowane zadania niskiego poziomu (takie jak
aktualizacja tabel baz danych) od zadań wysokiego poziomu (np. składanie zamówienia), co
sprawia, że operacja wykonania zamówienia jest łatwiejsza i bezpieczniejsza.
Aplikacja klienta, składająca zamówienia bez użycia middleware, musi łączyć się bezpośrednio z
serwerem bazy danych, który gromadzi zapisy rozkazów i adekwatnie do nich zmienia pola bazy.
Jakakolwiek zmiana w serwerze (przeniesienie do innej maszyny, zmiana struktury tabel bazy
danych, itp.) może spowodować przerwanie połączenia z aplikacją klienta. Jeśli dokonamy zmiany
w kliencie (zamierzonej lub przypadkowej), baza danych może błędnie zapisać informacje o
zamówieniach złożonych za pośrednictwem aplikacji klienta.
Do Middleware przesyłane są informacje o zamówieniu (np. nazwisko, adres, towar, ilość i numer
karty kredytowej) i za pomocą tych informacji dokonywana jest weryfikacja użytkownika
składającego zamówienie (np. sprawdzana jest ważność karty kredytowej). Jeśli informacje są
prawdziwe — zostaną wprowadzone do bazy danych. Jeśli baza danych ulegnie zmianie,
middleware aktualizuje się bez konieczności modyfikacji aplikacji klienta. Nawet, gdy zamówienia
bazy danych są tymczasowo zapisane w postaci pliku jednorodnego, middleware może przekazać
klientowi informacje odczytane z takiego pliku.
Korzystając z middleware można podnieść wydajność systemu poprzez rozproszenie procesu
zapisywania informacji w bazie danych na kilka wewnętrznych serwerów. Middleware potrafi
wykorzystać przepustowość sieci: zamiast powolnego połączenia klient-serwer, klient może
przekazać wszystkie informacje middleware, który użyje szybkiego połączenia sieciowego z puli
połączeń i skomunikuje się z bazą danych.
Warstwy pośrednie w sieci są często tworzone przy użyciu serwletów. Serwlety dostarczają
odpowiednie sposoby łączenia klientów z wewnętrznym serwerem za pomocą apletów lub
formularzy HTML. Klient przesyła żądania do serwletu za pomocą HTTP, a logika biznesowa w
serwlecie przekazuje żądanie do wewnętrznego serwera (więcej informacji na temat komunikacji
aplet-serwlet znajduje się w rozdziale 10, „Komunikacja aplet-serwlet”).
Serwlety często używają dodatkowej warstwy pośredniej poza serwerem sieciowym (takiej jak
Enterprise Java Beans) do łączenia się z bazą danych. Jeśli przeglądarka prześle formularz HTML z
zamówieniem do serwletu, to może on przekształcić tą informację i wykonać wywołanie do EJB
innej maszyny odpowiedzialnej za obsługę wszystkich zamówień — pochodzących zarówno z
serwletów, jak i niezależnych programów. W omówionych przypadkach mamy do czynienia ze
strukturą czterowarstwową.

Relacyjne bazy danych


We wcześniejszych przykładach omówiono serwlety, które przechowywały dane w postaci pliku umieszczonego
na lokalnym dysku. Użycie jednorodnego pliku jest świetnym rozwiązaniem dla małych ilości informacji, ale
można szybko stracić nad nim kontrolę. W miarę wzrostu ilości danych umieszczonych w pliku dostęp do nich
staje się coraz wolniejszy. Wyszukanie odpowiednich informacji może stać się nie lada wyzwaniem: wyobraźmy
sobie wyszukiwanie w pliku tekstowym nazwisk, miast i adresów e-mail wszystkich naszych klientów. Takie
podejście jest dobre dla nowo otwartej firmy, ale nie sprawdza się w przypadku korporacji obsługującej setki
tysięcy klientów. Wyszukanie na przykład listy klientów z Bostonu, których adresy e-mail są zakończone
aol.com z dużego pliku tekstowego stanowiłoby ogromny problem.
Jednym z najlepszych rozwiązań tego problemu jest skorzystanie z systemu zarządzania relacyjnymi bazami
danych (RDBMS). W najprostszej postaci RDBMS umieszcza dane w tabelach. Tabele te podzielone są na
wiersze i kolumny podobnie do tabel arkusza kalkulacyjnego. Pomiędzy poszczególnymi wierszami i kolumnami
jednej tabeli może zachodzić określona relacja (czyli powiązanie) — stąd termin relacyjne.
Jedna tabela relacyjnej bazy danych może zawierać informacje o klientach, druga o ich zamówieniach, a trzecia o
przedmiotach zamówienia. Poprzez włączenie unikatowych identyfikatorów (powiedzmy, takich jak numery
klientów i zamówień), te trzy tabele mogą być wzajemnie powiązane. Rysunek 9.1 pokazuje, w jaki sposób
wygląda ta relacja.
Dane w tabeli mogą być odczytywane, aktualizowane, dopisywane i kasowane za pomocą poleceń języka SQL.
JDBC API języka Java wprowadzony w JDK 1.1 używa pewnej odmiany SQL znanej jako ANSI SQL-2 Entry
Level. W przeciwieństwie do większości języków programowania SQL jest językiem deklaracyjnym: interpreter
SQL wykonuje polecenia wpisywane przez użytkownika. Inne języki programowania takie jak C/C++, Java
wymagają użycia procedur, czyli określenia kolejnych kroków wykonania pewnego zadania. SQL nie jest
szczególnie złożony, ale jest zbyt szerokim tematem do opisu w ramach tej książki. Więcej informacji na temat
relacyjnych baz danych i języka SQL znajduje się w książkach: SQL dla Opornych Allena Tayllora i SQL in a
Nutshell Kevina i Daniela Kline.

Rysunek 9.1. Powiązane tabele


Najprostszym i najbardziej pospolitym wyrażeniem SQL jest SELECT, które przeszukuje bazę danych i zwraca
zestaw wierszy pasujących do kryterium. Na przykład, poniższe wyrażenie zaznacza wszystkie wiersze tabeli
KLIENCI:
SELECT * FROM KLIENCI
Słowa kluczowe SQL takie jak SELECT i FROM oraz obiekty takie jak KLIENCI nie są wrażliwe na wielkość
liter. Uruchomienie interpretera SQL SQL*PLUS dla Oracle powinno wygenerować na wyjściu:
KLIENT_ID NAZWISKO TELEFON
---------------------------------------------------
1 Bob Copier 617-555-1212
2 Janet Stapler 617 555-1213
3 Joel Laptop 507 555-7171
4 Larry Coffee 212 555-6225
Bardziej zaawansowane wyrażenia mogłyby ograniczać szukanie do wybranych kolumn albo według określonych
kryteriów, np.:
SELECT ZAMÓWIENIE_ID, KLIENT_ID, SUMA FROM ZAMÓWIENIA
WHERE ZAMÓWIENIE_ID = 4
Za pomocą tego wyrażenia zaznaczono kolumny ZAMÓWIENIE_ID, KLIENT_ID i SUMA ze wszystkich
rekordów, gdzie w polu ZAMÓWIENIE_ID jest wartość 4. Oto przykładowy rezultat:
ZAMÓWIENIE_ID KLIENT_ID SUMA
------------------------------------
4 1 72.19
Instrukcja SELECT może również łączyć kilka tabel na podstawie zawartości poszczególnych pól. Może to być
związek jeden-do-jednego albo częściej używany jeden-do-wielu, np. jeden klient do kilku zamówień:
SELECT KLIENCI.NAZWISKO, ZAMÓWIENIA.SUMA FROM KLIENCI, ZAMÓWIENIA
WHERE ZAMÓWIENIA.KLIENT_ID = KLIENCI.KLIENT_ID AND ZAMÓWIENIA.ZAMÓWIENIE_ID = 4
Powyższa instrukcja spaja tabelę KLIENCI z tabelą ZAMÓWIENIA za pomocą pola KLIENT_ID. Należy
zauważyć, że obie tabele posiadają to pole. Zapytanie zwraca informacje z dwóch tabel: nazwę użytkownika,
który wykonał zamówienie nr 4 i całościowy koszt zamówienia. Oto przykładowy wynik tej operacji:
NAZWISKO SUMA
-------------------------------------
Bob Copier 72,19
Język SQL jest również używany do aktualizacji baz danych. Na przykład:
INSERT INTO KLIENCI (KLIENT_ID, NAZWISKO, TELEFON)
VALUES(5, "Bob Smith", "555 123-3456")
UPDATE KLIENCI SET NAZWISKO = "Robert Copier" WHERE KLIENT_ID = 1
DELETE FROM KLIENCI WHERE KLIENT_ID = 2
Pierwsza instrukcja tworzy nowy rekord w tabeli KLIENCI, zapełniając pola KLIENT_ID i TELEFON
pewnymi wartościami. Druga aktualizuje istniejący rekord, zmieniając pole NAZWISKO określonego
użytkownika. Ostatnia kasuje wszystkie rekordy, gdzie KLIENT_ID = 2. Należy ostrożnie posługiwać się tymi
instrukcjami, a w szczególności instrukcją DELETE. Użycie tej instrukcji bez warunku WHERE usunie wszystkie
rekordy z tabeli!

JDBC API
Wcześniej zakładaliśmy, że czytelnik posiada ogólną wiedze dotyczącą Java API. Ponieważ nawet wprawieni
programiści Javy mogą mieć małe doświadczenie z bazami danych, ten podpunkt wprowadza JDBC na
podstawie najpopularniejszej wersji JDBC 1.2. Pod koniec rozdziału dodano fragment na temat JDBC 2.0.
Jeśli czytelnik po raz pierwszy ma do czynienia z bazami danych, powinien przeczytać książki o ogólnych
założeniach baz danych i JDBC, takie jak: Database Programming with JDBC and Java George'a Reese'a i
JDBC Database Access with Java Grahama Hamiltona, Ricka Cattela, Maydene Fisher. Krótki przegląd znajduje
się w Java Enterprise in a Nutshel Davida Flangana. Oficjalna specyfikacja JDBC znajduje się na stronie
http://java.sun.com/products/jdbc
JDBC jest interfejsem programowym poziomu SQL, który pozwala wykonywać instrukcje SQL i odczytywać
otrzymane wyniki. API jest zbiorem interfejsów i klas zaprojektowanym tak, aby możliwa była współpraca z
jakąkolwiek bazą danych. Rysunek 9.2 pokazuje schemat struktury JDBC.

Rysunek 9.2. Java i baza danych

Sterowniki JDBC
JDBC API, znajdujący się w pakiecie java.sql zawiera zestaw klas służących do implementacji określonych
interfejsów. Większa część API jest rozprowadzana jako klasy interfejsów neutralnych dla baz danych, a
współpracę z konkretnymi bazami danych zapewniają interfejsy dostarczone przez producentów baz danych.
Indywidualny system baz danych jest dostępny poprzez sterownik JDBC, który implementuje interfejs
java.sql.Driver. Sterowniki istnieją prawie dla wszystkich popularnych systemów RDBMS, ale nie
wszystkie są dostępne za darmo. Firma Sun dodaje darmowy sterownik wykorzystujący technikę pomostową
JDBC-ODBC w JDK, aby umożliwić dostęp do danych z baz standardu ODBC, takich jak baza danych
Microsoft Access. Jest to bardzo prosty sterownik, którego można używać w bardzo prostych aplikacjach.
Twórcy serwletów powinni w szczególności zwrócić uwagę na to ostrzeżenie, ponieważ jakikolwiek problem w
treści kodu własnego sterownika JDBC-ODBC może zniszczyć cały serwer.
Sterowniki JDBC są dostępne dla większości platform baz danych tworzonych przez wielu producentów. Istnieją
cztery kategorie sterownika:

• Typ 1: JDBC-ODBC Bridge Driver — sterowniki typu 1 używają technologii pomostowej do łączenia
klienta Javy i serwisu bazy danych ODBC.JDBC-ODBC dostarczony z JDK jest najpopularniejszym
sterownikiem tego typu. Te sterowniki są implementowane przy użyciu kodu własnego i wymagają instalacji
dodatkowego oprogramowania po stronie klienta.
• Typ 2: Native-API Partly Java Driver — sterowniki typu 2 są napisane w Javie, ale korzystają z bibliotek
napisanych w innych językach. Dla Oracle, biblioteki kodu własnego mogłyby bazować na bibliotekach
OCI, które były pierwotnie zaprojektowane dla programistów C/C++. Ponieważ sterowniki typu 2 są
zaimplementowane za pomocą kodu własnego, zazwyczaj zapewniają lepszą wydajność niż ich wszystkie
odpowiedniki w całości napisane w Javie. Użycie tych sterowników może być jednak ryzykowne: błąd w
sekcji kodu własnego sterownika może zniszczyć cały serwer. Sterowniki tego typu wymagają instalacji
dodatkowego oprogramowania po stronie klienta.

• Typ 3: Net – Protocoll All-Java Driver — te sterowniki komunikują się z


bazami danych za pośrednictwem komponentów pośrednich (ang.
middleware) poprzez protokół sieciowy. Komponenty pośrednie
zapewniają dostęp do różnych baz danych. Sterowniki tego
typu są w całości napisane w Javie i nie wymagają instalacji
dodatkowego oprogramowania po stronie klienta, dzięki czemu
są wygodnym i bezpiecznym narzędziem zapewniającym apletom i
serwletom dostęp do baz danych.
• Typ 4: Native — Protocoll All Java Driver — sterowniki tego typu są
napisane w całości w Javie i komunikują się bezpośrednio z
maszyną bazodanową (ang. database engine) za pomocą
protokołów sieciowych. Te sterowniki mają bezpośredni dostęp
do baz danych bez dodatkowego oprogramowania.
Wykaz obecnie dostępnych sterowników JDBC znajduje się na
stronie http://industry.java.sun.com/products/jdbc/drivers.

Połączenie z bazą danych


Pierwszym krokiem, jaki trzeba wykonać, aby użyć sterownika JDBC do połączenia z bazą danych jest
załadowanie określonej klasy sterownika do aplikacji JVM (Java Virtual Machine). Dzięki temu sterownik
będzie dostępny przy otwieraniu połączenia z bazą danych. Prostym sposobem załadowania klasy sterownika jest
użycie metody Class.forName():
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Załadowany sterownik sam rejestruje się wewnątrz klasy java.sql.DriverManager, jako dostępny
sterownik bazy danych.
Następnym krokiem jest użycie klasy DriverManager do otwarcia połączenia z bazą danych, gdzie za
pomocą URL określa się żądaną bazę danych. Metodą używaną do otwierania połączenia jest
DriverManager.getConnection, która zwraca klasę implementującą interfejs
java.sql.Connection:
Connection con =
DriverManager.getConnecton("jdbc:odbc:somedb","user","passwd");
Podany adres URL identyfikuje bazę danych w sposób specyficzny dla określonego sterownika. Różne
sterowniki mogą potrzebować różnych informacji zawartych w URL do określenia hosta bazy danych. Adresy
URL zazwyczaj zaczynają się od jdbc:subprotokół:subnazwa. Subprotokół określa nazwę
wykorzystywanego sterownika, a subnazwa identyfikuje bazę danych. Na przykład, sterownik Oracle JDBC-
Thin wymaga URL w formie jdbc:oracle:thin:@dbhost:port:sid, a JDBC-ODBC Bridge używa
jdbc:odbc:datasourcename:odbcoptions.
Podczas wywołania metody getConnection(), obiekt DriverManager rozpoznaje odpowiedni sterownik
za pomocą URL przeszukując bazę dostępnych sterowników. Jeśli wymagany sterownik istnieje w bazie,
zostanie użyty do stworzenia obiektu Connection. Poniżej znajduje się fragment kodu, jakiego można użyć w
serwlecie do ładowania sterownika bazy danych JDBC-ODBC Bridge i realizacji połączenia:
Connection con = null;
try{
// ładujemy (i w ten sposób rejestrujemy) sterownik
//możliwość wystąpienia wyjątku ClassNotFoundException
Class.forName(„sun.jdbc.odbc.JdbcOdbcDriver”);
//otwieramy połączenie z bazą danych
// możliwość wystąpienia wyjątku SQLException
con=DriverManager.getConnection("jdbc:odbc:somedb", "user", "passwd");
// reszta kodu źródłowego ma tu swoje miejsce
}
catch(ClassNotFoundException e){
// obsługa wyjątku ClassNotFoundException
}
catch(SQLException e){
// obsługa wyjątku SQLException
}
finally {
// zamknięcie połączenia z bazą danych
try{
if (con!=null) con.close();
}
catch (SQLException ignored) {}
}

Właściwie dostępne są trzy formy metody getConnection(). Najprostszą z nich jest ta, która pobiera tylko
adres URL: getConnection (String url). Ta metoda ma zastosowanie w przypadku, gdy nie
przewidziano konieczności zalogowania się (podania nazwy użytkownika i hasła) lub umieszczenia informacji
logujących w adresie URL. Istnieje jeszcze jedna forma, która pobiera adres URL i obiekt Properties:
getConnection(String url, Properties props). Ta metoda zapewnia największą elastyczność.
Obiekt Properties (tablica mieszająca zawierająca klucze i wartości typu String) zawiera
standardowo nazwę użytkownika i hasło, a także dodatkowe informacje przekazywane odpowiedniemu
sterownikowi bazy danych. Na przykład, niektóre sterowniki respektują właściwość cacherows, która określa
jak dużo wierszy ma trafić do pamięci cache w jednostce czasu. Używanie tej metody ułatwia otwarcie
połączenia z bazą danych w oparciu o plik z rozszerzeniem .properties.
Sterownik, adres URL i potrzebne do zalogowania się dane mogą być określone w następującym pliku
sql.properties. Format nazwa=wartość jest formatem standardowym w plikach właściwości Javy:
connection.driver=sun.jdbc.odbc.JdbcOdbcDriver
connection.url=jdbc:odbc:somedb
user=user
password=passwd
Za pomocą kodu przedstawionego w przykładzie 9.1 otwiera się połączenie z bazą danych używając wartości
zgromadzonych wewnątrz pliku sql.properties. Należy zauważyć, że nazwa użytkownika i hasło to informacja
standardowo wymagana do zalogowania się, natomiast właściwości connection.driver i
connection.url są specjalnymi nazwami użytymi w poniższym kodzie do rozpoznania sterownika i
odpowiadającego mu URL, a wszelkie dodatkowe własności będą przekazane do wybranego sterownika.
Przykład 9.1.
Użycie odpowiedniego pliku do otwarcia połączenia bazodanowego.
// pobierz właściwości połączenia bazodanowego
Properties props = new Properties();
InputStream in = new FileInputStream("sql.properties");
props.load(in);
in.close(); ta instrukcja powinna znaleźć się w bloku finally
// załadowanie sterownika
Class.forName(props.getProperty("connection.driver");
//otwarcie połączenia
con = DriverManager.getConnection("connection.url"),props);
Utworzony obiekt Properties jest wypełniany wartościami odczytanymi z pliku sql.properties, a następnie
zawarte w tym pliku właściwości są użyte do otwarcia połączenia z bazą danych. Wykorzystanie pliku
właściwości pozwala na zmianę wszystkich informacji o połączeniach z bazą danych bez zbędnego
przekompilowania kodu Javy.

Otwieranie połączenia z serwletu


Serwlet może użyć przedstawionego powyżej sposobu ładowania informacji o połączeniu z bazą danych z pliku
właściwości znajdującego się w katalogu WEB-INF. Metoda getResourceAsStream() pobiera zawartość
tego pliku:
Properties props = new Properties();
InputStream in = getServletContext().getResourceAsStream(
"/WEB-INF/sql.properities");
props.load(in);
in.close();
Ponieważ serwlety są najczęściej wdrażane przy użyciu graficznych narzędzi, możemy użyć tych samych
narzędzi do skonfigurowania bazy danych. Jednym ze sposobów osiągnięcia tego celu jest umieszczenie
połączeń z bazą danych wewnątrz serwera JNDI, gdzie połączenia są rozpoznawane przez serwlety za pomocą
określonych nazw. Ten sposób jest wykorzystywany przez serwlety pracujące wewnątrz serwerów J2EE.
Rozdział 12 omawia szczegółowo to zagadnienie. Innym zadaniem, które nie wymaga serwera JNDI, a
wykorzystuje graficzne narzędzia rozmieszczenia, jest użycie parametrów inicjacji kontekstu do zachowania
informacji o konfiguracji. Te parametry inicjujące mogą zostać skonfigurowane za pomocą graficznych narzędzi
rozmieszczeń, a potem można je zapisać w pliku web.xml. (Więcej informacji na ten temat jest w rozdziale 4).
Klasa ContextProperties przedstawiona w przykładzie 9.2 udostępnia parametry inicjujące kontekst jako
obiekt Properties. To pozwala na przekazanie wszystkich par nazwa-wartość metodzie
DriverManager.getConnection().
Przykład 9.2.
Klasa ContextProperties
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ContextProperties extends Properties {


public ContextProperties(ServletContext context) {
Enumeration props = context.getInitParameterNames();
while (props.hasMoreElements()) {
String name = (String) props.nextElement();
String value = (String) context.getInitParameter(name);
put (name,value);
}
}
}
Serwlet otrzymuje informacje o połączeniu za pośrednictwem tej klasy (nie pobiera danych z sql.properties), co
przedstawiono poniżej:
// pobierz kontekst init params jako obiekt properties
ContextProperties props = new ContextProperties(getServletContext());
//załaduj sterownik
Class.forName(props.getProperty("connection.driver"));
// itd.
Wystarczy przypisać parametry inicjujące kontekst konkretnym wartościom bazy danych w sposób ręczny w
pliku web.xml lub używając narzędzia rozmieszczenia.

Wykonywanie zapytań SQL


Aby naprawdę używać bazy danych, musimy poznać sposób wykonywania zapytań. Najprostszym sposobem jest
użycie klasy java.sql.Statement. Tego typu obiekty nigdy nie są tworzone bezpośrednio, lecz za pomocą
metody createStatement() obiektu Connection, która dostarcza referencje do utworzonego w danej
chwili obiektu typu Statement:
Statement stmt = con.createstatement();
Zapytanie zwracające dane może być wykonane przy użyciu metody executeQuery() obiektu Statement.
Po wykonaniu tej metody dane zwracane są w java.sql.ResultSet w postaci hermetycznej:
ResultSet rs = stmt.executeQuery("SELECT*FROM KLIENCI");
ResultSet jest reprezentacją danych otrzymanych w wyniku wykonania zapytania. Obiekt ten zwraca za
każdym razem jeden wiersz. Użycie metody next() obiektu ResultSet pozwala na przemieszczanie się
pomiędzy wierszami danych reprezentowanych przez ResultSet. Istnieje wiele metod pozwalających na
odczytywanie danych z danego wiersza obiektu ResultSet. Do najczęściej używanych należą metody
getString() i getObject(), które służą do odczytywania wartości kolumn:
while(rs.next()) {
String event = rs.getstring("event");
Object count = (Integer) rs.getObject("count");
}
Należy wiedzieć, że obiekt ResultSet jest powiązany ze swym nadrzędnym (rodzicielskim) obiektem
Statement. Jeśli obiekt Statement zostanie zamknięty lub wykorzystany do obsługi innego zapytania,
wszystkie odpowiadające mu obiekty ResultSet zostaną również automatycznie zamknięte.
Przykład 9.3 pokazuje bardzo prosty serwlet, w którym użyto sterownika Oracle JDBC by wykonać proste
zapytanie, zwracające nazwiska i numery telefonu wszystkich pracowników zawartych w tabeli bazy danych.
Zakładamy, że baza danych zawiera tabelę PRACOWNICY z co najmniej dwoma polami, a także tabele
NAZWISKO i TELEFON.

Przykład 9.3.
Serwlet JDBC-enabled
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class DBPhoneLookup extends HttpServlet {


public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
Connection con = null;
Statement stmt = null;
ResultSet rs = null;

res.setContentType("text/html");
Printwriter out = res.getWriter();

try{
//ładujemy sterownik oracle
Class.forName("oracle.jdbc.driver.OracleDriver");

// nawiązujemy połączenie z bazą danych


// udostępnioną na serwerze dbhost
// na porcie 1528
con = DriverManager.getConnection (
"jdbc:oracle:thin:@dbhost:1528:ORCL","user","passwd");
//tworzymy obiekt typu Statement
stmt = con.createStatement();
// wykonujemy zapytanie SQL, otrzymujemy wynik w ResultSet
rs=stmt.executeQuery("SELECT NAZWISKO, TELEFON FROM PRACOWNICY");
// przedstawiamy otrzymane dane w postaci listy
out.println("<HTML><HEAD><TITLE>Książka telefoniczna</TITLE></HEAD>");
out.println("<BODY");
out.println("<UL.");
while (rs.next()) {
out.println("<LI> + rs.getString("name") + ""
+ rs.getString("phone")");
}
out.println("</UL.>");
out.println("</BODY></HTML>");
}
catch (ClassNotfoundException e ) {
out.println("Nie można zaladowac sterownika bazy danych: "+e.getMessage());
}
catch (SQLException e) {
out.println("SQLException :" + e.getMessage());
}
finally {
// zawsze zamykamy połączenie z bazą danych
try {
if (con!=null) con.close();
}
catch (SQLException ignored){}
}
}
}
Dla każdego obiektu DBPhoneLookup nawiązywane jest połączenie z bazą danych, następnie wykonuje się
zapytanie w celu przeszukania nazwisk i numerów telefonów wszystkich znajdujących się w tabeli pracowników
i wynikowa lista jest dostarczana użytkownikowi.

Obsługa wyjątków SQL


DBPhoneLookup obejmuje większość kodu w bloku obsługi wyjątku try-catch. W tym blok wyłapuje dwa
wyjątki: ClassNotFoundException i SQLException. Pierwszy z nich jest zgłaszany za pomocą metody
Class.forName(), gdy nie można załadować klasy sterownika JDBC. Drugi jest zgłaszany przez
którąkolwiek z metod JDBC, gdy wystąpi problem w trakcie jej wykonania. Obiekty klasy SQLException
obok standardowych właściwości klas wyjątków mają dodatkową cechę: mogą tworzyć łańcuchy wyjątków.
Klasa SQLException definiuje dodatkową metodę getNextException() pozwalającą na
„hermetyzację” dodatkowych obiektów Exception. Nie przedstawiono tego w poprzednim przykładzie, ale
użycie tej metody może wyglądać w następujący sposób:
catch (SQL Exception e){
out.println(e.getMessage());
while ((e=e.getNextException())!=null){
out.println(e.getMessage());
}
}
Powyższy kod wyświetla informację z pierwszego wyjątku, a następnie, za pomocą pętli, przechodzi poprzez
kolejne wyjątki, wyświetlając informacje o konflikcie związanym z każdym z nich. W praktyce, pierwszy
wyjątek będzie zawierał najbardziej znaczącą informację.

Zestawy wyników w szczegółach


Przyjrzyjmy się obiektowi ResultSet i obiektowi pokrewnemu ResultSetMetaData. W przykładzie 9.1
wiedzieliśmy w jaki sposób wykonać nasze zapytanie, jakiego wyniku należało się spodziewać, więc
sformatowaliśmy wyjście w odpowiedni sposób. Jeśli chcielibyśmy wyświetlić wyniki zapytania w tabeli HTML,
dobrze byłoby mieć taki program Javy, który tworzy tę tabelę automatycznie korzystając z obiektu ResultSet
zamiast pisać kilkakrotnie tą samą część kodu. Dodatkowym atutem takiego programu byłaby możliwość zmiany
zawartości tabeli wraz ze zmianą konstrukcji zapytania.
Obiekt ResultSetMetaData przekazuje programowi informacje na temat danego obiektu ResultSet.
Możemy wykorzystać tę cechę do budowy obiektu, który będzie generował dynamicznie tabelę HTML z
wykorzystaniem ResultSet, tak jak to pokazano w Przykładzie 9.4. Wiele narzędzi Javy wspomagających
HTML ma podobne możliwości, co omówiono w rozdziałach 14 – 18.
Przykład 9.4.
Klasa generująca tabelę HTML z ResultSet przy użyciu ResultSetMetaData
import java.sql.*;

public class HtmlResultSet {

private ResultSet rs;


public HtmlResultSet(ResultSet rs) {
this.rs = rs;
}

public String toString() { // może być wywołany tylko raz


StringBuffer out = new StringBuffer();
// twórzymy tabelę aby wyświetlić zestaw wyników
out.append("<TABLE>\n");

try {
ResultSetMetaData rsmd = rs.getMetaData();

int numcols = rsmd.getColumnCount();

// Tytuł tabeli z etykietami kolumn zestawów wyników


out.append("<TR>");
for (int i = 1; i <= numcols; i++) {
out.append("<TH>" + rsmd.getColumnLabel(i));
}
out.append("</TR>\n");

while (rs.next()) {
out.append("<TR>"); // tworzymy nowy wiersz
for (int i = 1; i <= numcols; i++) {
out.append("<TD>"); // tworzymy nowy element danych
Object obj = rs.getObject(i);
if (obj != null)
out.append(obj.toString());
else
out.append("&nbsp;");
}
out.append("</TR>\n");
}

// Koniec tabeli
out.append("</TABLE>\n");
}
catch (SQLException e) {
out.append("</TABLE><H1>ERROR:</H1> " + e.getMessage() + "\n");
}

return out.toString();
}
}
Ten przykład pokazuje w jaki sposób użyć dwóch podstawowych metod pochodzących od obiektu
ResultSetMetaData: getColumnCount() i getColumnLabel(). Pierwsza z nich zwraca ilość
kolumn obiektu ResultSet, a druga dostarcza nazwę określonej kolumny w oparciu o jej indeks.
Indeksowanie w obiektach ResultSet jest oparte na standardzie RDMBS zamiast C++/Java, co oznacza, że
obiekty numerowane są od 1 do n, a nie od 0 do n-1.
W przykładzie użyto metody getObject() obiektu ResultSet, by odczytać zawartość każdej kolumny.
Wszystkie metody getXXX() działają zarówno w oparciu o indeksy, jaki i nazwy kolumn. Dostęp do danych w
ten sposób jest bardziej wydajny i przenośny, jeśli właściwie napiszemy wyrażenie SQL. Używamy tu
getObject().toString() zamiast getString(), aby uprościć obsługę pól mających wartość null, co
omówiono w następnym podrozdziale.
Tabela 9.1 pokazuje metody Javy, które można użyć do wyszukania najbardziej powszechnych typów danych
SQL. Metoda getObject() obiektu ResultSet zwróci obiekt Javy którego typ zależy od typu obiektu
SQL, co pokazano w tabeli. Można również użyć innej metody getXXX(). Metody te pokazano w trzeciej
kolumnie, wraz ze zwracanymi typami danych. Należy pamiętać, że dostępne typy danych SQL są różne w
zależności od bazy danych.
Tabela 9.1.
Metody otrzymywania danych z obiektu ResultSet
Typ danych SQL Typ Java zwracany przez Zalecana alternatywa do
getObject() getObject()
BIGINT Long long getlong()
BINARY byte[ ] byte[ ] getBytes()
BIT Boolean Boolean getBoolean()
CHAR String String getString()
DATE java.sql.Date java.sql.Date getDate()
DECIMAL java.math.BigDecimal java.math.BigDecimal
getBigDecimal()
DOUBLE Double double getDouble()
FLOAT Double double getDouble()
INTEGER Integer int getInt()
LONGVARBINARY byte[ ] InputStream
getBinaryStream()
LONGVARCHAR String InputStream
getAsciiStream ()
InputStream
getUnicodeStream
NUMERIC java.math.BigDecimal java.math.BigDecimal
getBigDecimal()
REAL Float float getFloat()
SMALLINT Integer short getShort()
TIME Java.sql.Time java.sql.Time getTime()
TIMESTAMP Java.sql.Timestamp java.sql.Timestamp
getTimestamp()
TINYINT Integer byte getByte()
VARBINARY byte[ ] byte[ ] getBytes()
VARCHAR String String getString()

Obsługa pól mających wartość null


Pole w bazie danych, podobnie do obiektu w Javie może mieć wartość null, co oznacza, że to pole jest
niewypełnione. Obsługa takich pól bazy danych może stanowić pewien problem, bowiem nie jesteśmy w stanie
stwierdzić, czy uzyskana wartość (0 lub -1 zwrócona np. przez metodę getInt()) jest faktyczną wartością
wpisaną w to pole, czy też jest to informacja, że w dane pole nic nie wpisano. Z tego powodu JDBC zawiera
metodę wasNull() w obiekcie ResultSet zwracającą true lub false w zależności od tego, czy ostatnia
przeczytana kolumna w bazie danych miała wartość typu null czy nie. To oznacza, że należy wczytać dane z
obiektu ResultSet do konkretnej zmiennej, wywołać metodę wasNull() i w zależności od wyniku
postępować dalej. Oto przykład zastosowania tej metody:
int age = rs.getInt( "wiek");
if (!rs.wasNull())
out.println("Wiek: " + wiek);

Innym sposobem pozwalającym na odnajdywanie wartości typu null jest użycie metody getObject(). Jeśli
kolumna ma wartość null, getObject() zawsze zwróci wartość null. Użycie metody getObject()
eliminuje konieczność wywołania metody wasNull(), ale czasem lepiej skorzystać z pierwszej metody.

Aktualizacja baz danych


Większość aplikacji bazodanowych, oprócz obsługi zapytań, umożliwia modyfikację rekordów w tabelach bazy
danych. Kiedy klient przesyła zamówienie lub dostarcza jakąś informację, dane muszą być wprowadzone do
bazy danych. Gdy obsługiwana jest instrukcja SQL UPDATE, INSERT lub DELETE wiadomo, że nie wystąpi
obiekt ResultSet i można użyć metodę executeUpdate() obiektu Statement. Zwraca ona liczbę
oznaczającą ilość zmodyfikowanych wierszy w wyniku wywołania konkretnego wyrażenia. Tej metody używa się
w następujący sposób:
int count =
stmt.executeUpdate("DELETE FROM KLIENCI WHERE KLIENT_ID = 5);
Jeśli wyrażenie SQL może zwrócić obiekt ResultSet lub tylko zmodyfikować odpowiednie pole (czyli
wykonać instrukcję typu DELETE, INSERT lub UPDATE) należy skorzystać z metody execute()obiektu
Statement. Ta metoda zwraca wartość true, gdy wykonanie wyrażenia SQL utworzyło co najmniej jeden
obiekt ResultSet lub false, gdy w wyniku wykonania zapytania zaktualizowano bazę danych.
boolean b = stmt.execute(sql);
Metody getResultSet() i getUpdateCount()zapewniają dostęp do wyników wywołania metody
execute(). Przykład 9.5 demonstruje użycie tych metod w nowej wersji HtmlResultSet, nazwaną
HtmlSQLResult, która tworzy tabelę HTML w wyniku wykonania zapytania SQL.
Przykład 9.5.
Klasa generująca tabelę HTML z ResultSet przy użyciu ResultSetMetaData
import java.sql.*;

public class HtmlSQLResult {


private String sql;
private Connection con;

public HtmlSQLResult(String sql, Connection con) {


this.sql = sql;
this.con = con;
}

public String toString() { // może być wywołany tylko raz


StringBuffer out = new StringBuffer();

// Poniższa linia służy do wyświetlenia komendy SQL na początku tabeli


// out.append("Wyniki zapytania SQL: " + sql + "<P>\n");

try {
Statement stmt = con.createStatement();
if (stmt.execute(sql)) {
//?????
ResultSet rs = stmt.getResultSet();
out.append("<TABLE>\n");

ResultSetMetaData rsmd = rs.getMetaData();

int numcols = rsmd.getColumnCount();

// Tytuł tabeli z etykietami kolumn zestawów wyników


out.append("<TR>");
for (int i = 1; i <= numcols; i++)
out.append("<TH>" + rsmd.getColumnLabel(i));
out.append("</TR>\n");

while (rs.next()) {
out.append("<TR>"); // rozpocznij nowy wiersz
for (int i = 1; i <= numcols; i++) {
out.append("<TD>"); // rozpocznij nowy element danych
Object obj = rs.getObject(i);
if (obj != null)
out.append(obj.toString());
else
out.append("&nbsp;");
}
out.append("</TR>\n");
}

// Koniec tabeli
out.append("</TABLE>\n");
}
else {
//Tu powinna wystąpić liczba
out.append("<B>Records Affected:</B> " + stmt.getUpdateCount
());
}
}
catch (SQLException e) {
out.append("</TABLE><H1>ERROR:</H1> " + e.getMessage());
}

return out.toString();
}
}

Powyższy przykład wykorzystuje metodę execute(), aby wykonać wyrażenie SQL przesłane do konstruktora
HtmlSQLResult. Potem, w zależności od zwróconej wartości, wywołuje getResultSet() lub
getUpdateCount(). Należy zauważyć, że ani getResultSet(), ani getUpdateCount() nie powinny
być wykonane więcej niż raz w trakcie jednego zapytania.

Użycie gotowych zapytań*


Obiekt PreparedStatement ma podobne właściwości do obiektu Statement. Ważna różnicą jest fakt, że
wyrażenia SQL w obiekcie PreparedStatement są wcześniej zinterpretowane przez bazę danych, co
przyspiesza ich wykonanie. Raz skompilowany obiekt PreparedStatement można dostosowywać w
zależności od potrzeb poprzez korygowanie wcześniej zdefiniowanych parametrów. Preinterpretowane
wyrażenia są użyteczne w aplikacjach, w których wymagane jest wielokrotnie wykonanie tych samych wyrażeń
SQL.

Metoda preparedStatement(String) obiektu Connection służy do tworzenia obiektów


PreparedStatement. W miejsce znacznika '?' zostaną umieszczone określone wartości, na przykład:
PreparedStatement pstmt = con.prepareStatement(
"INSERT INTO ZAMÓWIENIA (ZAMÓWIENIE_ID, KLIENT_ID, TOTAL) VALUES (?,?,?)");

// Inny kod

pstmt.clearParameters(); // czyści wszystkie poprzednie wartości parametru


pstmt.setInt(1, 2); // ustawia ZAMÓWIENIE_ID
pstmt.setInt(2, 4); // ustawia KLIENT_ID
pstmt.setDouble(3, 53.43); // ustawia TOTAL
pstmt.executeUpdate(); // wykonuje tak skonstruowane wyrażenie SQL
Metoda clearParametres() usuwa wszystkie wcześniej zdefiniowane wartości parametrów, a metody
setXXX() służą do przypisania właściwych wartości do '?'. Po przypisaniu wartości parametrom należy
wywołać metodę executeUpdate(), aby utworzyć obiekt PreparedStatement.
Podczas wpisywania tekstu przesłanego przez użytkownika przy użyciu obiektu Statement i dynamicznego
SQL-a, należy uważać, aby przypadkowo nie wprowadzić jakiegoś znaku kontrolnego SQL (takiego jak ' lub ")
w odniesieniu do bazy danych. Baza danych Oracle umieszcza łańcuchy tekstowe pomiędzy apostrofami, tak
więc próba wstawienia łańcucha John d'Artagan w bazę danych spowoduje wystąpienie błędu SQL:
INSERT INTO MUSKETEERS (NAME) VALUES ('John d'Artagan')
Łańcuch w tym przykładzie „kończy się” w dwóch miejscach. Jednym z rozwiązań jest ręczna zamiana
pojedynczego apostrofu (') na dwa (' '), czyli wykonanie sekwencji unikowej dla Oracle. Takie podejście
wymaga unikania znaków specjalnych w sposób ręczny, co kłóci się z zasadą przenośności pisanego kodu. Dużo

*
„Prepared statements” to w dosłownym tłumaczeniu „gotowe zapytania”, ale ponieważ te zapytania są interpretowane przed
użyciem, lepszym ich określeniem jest „preinterpretowane zapytania”
lepszym rozwiązaniem jest użycie obiektu PreparedStatement i wprowadzanie łańcucha za pomocą
metody setString(), co pokazano poniżej. Metoda ta unika automatycznie znaków specjalnych, gdy
zaistnieje taka potrzeba:
PreparedStatement pstmt = con.prepareStatement(
" INSERT INTO MUSKETEERS (NAME) VALUES (?)");
pstmt.setString (1, "John d'Artagan");
pstmt.executeUpdate();

Ponowne użycie obiektów bazy danych


We wprowadzeniu wspomnieliśmy o tym, że czas życia serwletu pozwala na bardzo szybki dostęp do bazy
danych. Przy otwieraniu połączenia do bazy danych pojawia się problem wydajności związany z efektem
„wąskiego gardła”. Problem ten nie ma wielkiego znaczenia dla aplikacji i apletów, gdyż kilka sekund
opóźnienia, potrzebnego na utworzenie obiektu Connection, nie wpływa na funkcjonowanie programu. W
przypadku serwletów ten problem ma znaczenie, ponieważ nowy obiekt Connection jest tworzony i usuwany
na każde żądanie strony. Na szczęście czas życia serwletu pozwala na ponowne użycie tego samego połączenia
dla wielu żądań, nawet konkurujących między sobą.

Ponowne użycie połączeń do baz danych


Serwlet może utworzyć wiele obiektów Connection za pomocą metody init() i ponownie ich użyć za
pomocą metod service(), doGet() i doPost(). Przykład 9.6 przedstawia zmodyfikowany serwlet
wyszukujący numery telefonów, w którym najpierw tworzony jest obiekt Connection, a później w trakcie
programu otwierane jest połączenie z bazą danych. Używa także sterownika Sybase JDBC oraz klasy
HtmlSQLResult z przykładu 9.5 do wyświetlania wyników.

Przykład 9.6.
Ulepszony serwlet z numerami telefonów.
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class DBPhoneLookup extends HttpServlet {


private Connection con = null;
public void init() throws ServletException {
try {
//załaduj i zarejestruj sterownik Sybase
Class.forName("com.sybase.jdbc.SybDriver");
con = DriverManager.getConnection (
"jdbc:sybase:Tds:dbhost:7678","user","passwd");
}

catch (ClassNotfoundException e ) {
throw new UnavailableException ("Nie można zaladowac sterownika bazy danych");
}
catch (SQLException e) {
throw new UnavailableException("Nie można połączyć się z bazą danych");
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
Printwriter out = res.getWriter();

out.println("<HTML><HEAD><TITLE>Książka telefoniczna</TITLE></HEAD>");
out.println("<BODY");

HtmlSQLResult result =
new HtmlSQLResult("SELECT NAME, PHONE FROM EMPLOYEES", con);

// Wyświetl wyniki przeszukiwania


out.println("<H2>Pracownicy:</H2>");
out.println(result);
out.println("</BODY></HTML>");
}
public void destroy() {
// Wyczyść
try {
if (con!=null) con.close();
}
catch (SQLException ignored){}
}
}
}

Ponowne użycie przygotowanych wyrażeń.


Można w łatwy sposób przyspieszyć wydajność serwletu przez utworzenie z wyprzedzeniem innych obiektów
współpracujących z bazami danych. Obiekt PreparedStatement idealnie nadaje się do wykonania tego
zadania, bo za jego pomocą można wcześniej skompilować wyrażenie SQL. Oszczędza to zaledwie kilka
milisekund, ale taka oszczędność ma znaczenie w witrynach odwiedzanych przez kilkaset tysięcy użytkowników
dziennie.
Współdzielenie obiektów może powodować wystąpienie błędów. Dostęp do obiektu PreparedStatement
może wymagać wywołania 3 lub 4 metod. Jeśli jeden wątek wywołuje metodę clearParameters()na
chwile przed tym, jak kolejny wątek wywoła execute(), wyniki tej drugiej operacji będą błędne. Istnieje
także ograniczenie obsługi tylko jednego zapytania w danym czasie przez obiekt Statement. Rozwiązaniem
tego problemu jest synchronizacja tego fragmentu kodu, który używa współdzielonych obiektów, co omówiono
w rozdziale 3 i pokazano poniżej:
synchronized (pstmt) {
pstmt.clearParameters();
pstmt.setInt(1, 2);
pstmt.setInt(2, 4);
pstmt.setDouble(3, 53.43);
pstmt.executeUpdate();
}

Niestety, takie rozwiązanie ma również wady. Wprowadzenie bloku synchronizacji na pewnych platformach
zajmuje dodatkowy czas, a zsynchronizowane obiekty mogą być użyte tylko przez jeden wątek w danej chwili.
Mimo to niektóre serwlety wymagają użycia bloku synchronizacji. Dobrym wyjściem z tej sytuacji jest
wcześniejsze utworzenie połączeń z obiektami (takimi jak PreparedStatement), które mogą być szybko
użyte wewnątrz bloków synchronizacji.
Powyższe metody nie mają zastosowania w przypadku serwletów używających interfejsu
SigleThreadModel, a ponadto ograniczają wydajność poprzez ładowanie naraz wielu kopii serwletu.

Transakcje
Transakcje są ważnym zagadnieniem związanym ze współczesnymi systemami relacyjnych baz danych. W wielu
witrynach internetowych zachodzi potrzeba posługiwania się rozbudowanymi wyrażeniami i strukturami danych.
Spójrzmy na aplikację banku online. Aby wykonać przelew 50000$ pomiędzy dwoma kontami, program musi
wykonać operację składającą się z dwóch oddzielnych, ale powiązanych ze sobą akcji: skredytować jedno konto i
obciążyć drugie. Wyobraźmy sobie, że z pewnych powodów powiodła się tylko operacja uznania pierwszego
konta, a do obciążenia drugiego konta nie doszło. W wyniku takiego działania na jednym koncie jest o 50000$
więcej, a na drugim koncie nic się nie zmieniło.
Błędne wykonanie wyrażenia SQL w tym przykładzie powoduje, że w baza danych zawiera błędne informacje
dotyczące ilości środków dostępnych na koncie. Prawdopodobieństwo takiego zdarzenia jest niewielkie, ale jego
wystąpienie nie jest wykluczone. Ten rodzaj problemu jest zbliżony do zagadnienia synchronizacji omówionego
w rozdziale 3. Tym razem, zamiast zajmować się ważnością danych zgromadzonych w serwlecie, omówimy
ważność danych zapisywanych w bazie. Prosta synchronizacja nie jest dostatecznym rozwiązaniem omawianego
problemu, ponieważ z zasobów jednej bazy danych może korzystać wiele serwletów. W aplikacjach bankowych
prawdopodobne jest korzystanie z zasobów bazy danych przez wiele aplikacji nie napisanych w Javie. Na
szczęście tym problemem zajęto się na długo przed zaistnieniem Javy. Większość głównych systemów RDMBS
wspiera koncepcję transakcji. Technologia transakcji pozwala na grupowanie wielu wyrażeń SQL: transakcja to
zbiór operacji wykonywanych przez serwer bazodanowy na bazie. Za pomocą systemu RDMBS, obsługującego
tę technologię, można rozpocząć transakcję (czyli wykonać określoną liczbę operacji) i przesłać wyniki do bazy
danych. Jeśli wykonanie wszystkich operacji wchodzących w skład transakcji jest niemożliwe, cała transakcja
jest anulowana i żadne zmiany nie zachodzą w bazie danych. Gdybyśmy naszą aplikację bankową zbudowali w
oparciu o transakcje, pobranie pieniędzy zostałoby anulowane, gdyby obciążenie konta nie powiodło się.
Transakcja podczas realizacji zadania jest odizolowana od bazy danych. Transakcje są niepodzielne (operacje
wchodzące w skład transakcji są traktowane jako logiczna całość). To oznacza, że użytkownicy korzystający z
bazy zawsze mają dostęp do aktualnych, choć nie widzą procesu aktualizacji. Jeśli użytkownik zażąda raportu na
temat sprzedanych produktów w trakcie wykonywania transakcji sprzedaży, raport nie uwzględni wyników tej
transakcji.

Użycie transakcji w JDBC


Obsługa transakcji w JDBC jest realizowana za pomocą obiektu Connection. Nowe połączenia domyślnie są
otwierane w trybie autocommit (automatyczne zatwierdzenie), co oznacza, że każde wyrażenie SQL jest
traktowane jako pojedyncza transakcja, a wyniki wykonania wyrażenia są natychmiast zapisywane w bazie
danych. Aby mieć możliwość grupowania wyrażeń SQL w transakcje i samodzielnej kontroli wykonywania
transakcji, należy wywołać metodę setAutoCommit() (wywoływanej na rzecz obiektu Connection) z
parametrem false. Za pomocą metody getAutoCommit()możemy sprawdzić, w jakim trybie realizowane
jest połączenie z bazą danych. Funkcja zwróci wartość true, gdy ustawiony jest tryb autocommit. Po
zakończeniu wykonywania wszystkich wyrażeń SQL należy wywołać metodę commit(), aby zatwierdzić
transakcję i zapisać wszystkie dokonane zmiany w bazie danych. Jeśli wystąpi błąd, wywołana jest metoda
rollback(), odwołująca transakcję(powodująca cofnięcie wszystkich zmian).
W przykładzie 9.7 pokazano serwlet używający transakcji do przeprowadzenia podstawowej formy zamówienia.
Dla uproszczenia zakładamy istnienie dwóch tabel w bazie danych ODBC: MAGAZYN (tabela zawiera
identyfikator produktu i jego ilość w magazynie) oraz WYSYŁKA (w tej tabeli wpisano identyfikator produktu,
numer zamówienia oraz ilość wysłanych produktów). Serwlet korzysta z metody chargeCard(), która
obsługuje wystawianie rachunków i zgłasza wyjątek, jeśli karta kredytowa klienta jest nieważna.
Przykład 9.7.
Zarządzanie zamówieniem za pomocą transakcji
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class OrderHandler extends HttpServlet {

public void doPost(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();

Connection con = null;


try{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
con = DriverManager.getConnection("jdbc:odbc:ordersdb","user","passwd");

// włącz tryb transakcji


con.setAutoCommit(false);

Statement stmt = con.createStatement();


stmt.executeUpdate(
"UPDATE MAGAZYN SET ILOŚĆ = (ILOŚĆ - 10) WHERE PRODUCTID = 7");
stmt.executeUpdate(
"UPDATE WYSYŁKA SET WYSŁANE = (WYSŁANE + 10) WHERE PRODUCTID = 7");

chargeCard(); // metoda faktycznie nie istnieje ...

con.commit();
out.println("Operacja zamówienia zakończona sukcesem! Dziękujemy za współpracę");
}
catch(Exception e) {
// Jakikolwiek błąd jest podstawą do cofnięcia transakcji

try {
con.rollback();
}

catch (SQLException ignored){}


out.println("Operacja zamówienie nie powiodła się. Proszę skontaktować
się z obsługą techniczną.");
}
finally {
// wyczyść
try {
if (con!=null) con.close();
}
catch (SQLException ignored){}
}
}
}

Uwagi do powyższego przykładu: po pierwsze, logika transakcji zamówienia znajduje się w doPost(), dopóki
klient nie będzie mógł powtórzyć akcji. Po drugie, celem przykładu jest przedstawienie logiki transakcji, więc
dla uproszczenia założono, że użytkownik kupuje 10 sztuk towaru nr 7 i nie wyświetla formularza zawierającego
informacje o karcie kredytowej i zamówieniu. Ponadto, zgłoszenie jakiegokolwiek wyjątku podczas inicjacji
sterownika, łączenia z bazą danych, wykonywania SQL lub obsługi karty kredytowej spowoduje wykonanie
skoku do bloku catch(), gdzie wywoływana jest metoda rollback() anulująca całą transakcję.

Optymalizacja transakcji
W poprzednim przykładzie umieściliśmy obiekt Connection wewnątrz metody doPost(). Zrezygnowaliśmy
więc ze zwiększonej wydajności aplikacji, jaką daje umieszczenie tego obiektu wewnątrz metody init().
Transakcje są ściśle związane z połączeniami, więc połączenia używające transakcji nie mogą być
współdzielone. Gdyby połączenia mogły być współdzielone, w serwlecie z poprzedniego przykładu jednocześnie
mogłyby być wykonane dwa polecenia: pobranie dziesięciu sztuk towaru i zatwierdzenie transakcji (wywołanie
metody commit()). W tabeli MAGAZYN brakowałoby wówczas 10 elementów.
Istnieje kilka możliwości użycia transakcji bez konieczności łączenia się z bazą danych na każde żądanie strony:

• Synchronizacja metody doPost(). Oznacza to, że każdy egzemplarz serwletu zajmuje


się tylko jednym żądaniem. Jest to dobry sposób dla rzadko odwiedzanych witryn, ale
spowalnia działanie po stronie użytkowników, bo każda transakcja musi się zakończyć,
zanim następna się rozpocznie. Jeśli wymagane jest częste odświeżanie bazy danych i
wprowadzanie nowych danych, opóźnienie działania aplikacji może być niedopuszczalne.
• Tworzenie nowego obiektu Connection dla każdej transakcji. Jeśli wymaga się
uaktualnienia danych tylko raz na kilka tysięcy żądań, to rozwiązanie jest najprostsze.
• Utworzenie zbioru obiektów Connection wewnątrz metody init() i korzystanie z
nich w zależności od potrzeb, co pokazano na rysunku 9.3. Jest to najefektywniejsza
metoda rozwiązania tego problemu, ale bez wsparcia obcych klas ten sposób może stać się
bardzo skomplikowany.
• Implementacja śledzenia sesji w serwlecie i użycie obiektu HttpSesion do
przechowywania obiektów Connection dla każdego użytkownika. Jest to najlepsze
rozwiązanie, bo pozwala rozszerzyć transakcję na wiele żądań i wiele serwletów.

Rysunek 9.3. Serwlety korzystające z puli połączeń bazy danych

Pule połączeń
Za pomocą puli połączeń możemy podwajać tylko te zasoby, których aktualnie potrzebujemy (w tym wypadku
obiekty Connection). Pula połączeń może w sposób inteligentny zarządzać własnym rozmiarem i upewniać
się, że każde połączenie jest ważne. Obecnie jest wiele dostępnych pakietów tworzących pule połączeń. Niektóre
z nich, np. DbConnectionBroker są dostępne za darmo na stronie http://javaexchange.com. Pakiety te
tworzą obiekty, których zadaniem jest wydawanie połączeń na żądanie. Są dostępne również sterowniki puli
połączeń (pool drivers), które implementują nowy sterownik JDBC obsługujący połączenia innego (wcześniej
załadowanego) sterownika. Użycie sterownika jest najprostszym sposobem na utworzenie puli połączeń w
serwletach. Sterownik obsługujący pulę połączeń jest bardziej przeciążony niż sterownik obsługujący jedno
połączenie.
Przykład 9.8 przedstawia prosty system puli połączeń. Najpierw tworzone są połączenia, które będą otwierane w
miarę potrzeb. Gdy wszystkie połączenia zostaną otwarte i nie będzie już wolnych połączeń, serwlet utworzy
nowe połączenia.
Nasz klasa ConnectionPool jest w pełni funkcjonalna, ale można stworzyć klasy korzystające z pakietów
komercyjnych służących do tworzenia puli połączeń.

Przykład 9.8.
Klasa ConnectionPool
import java.sql.*;
import java.util.*;

public class ConnectionPool {


private Hashtable connections = new Hashtable();
private Properties props;

public ConnectionPool(Properties props, int initialConnections)


throws SQLException, ClassNotFoundException {
this.props = props;
initializePool(props, initialConnections);
}

public ConnectionPool(String driverClassName, String dbURL,


String user, String password,
int initialConnections)
throws SQLException, ClassNotFoundException {
props = new Properties();
props.put("connection.driver", driverClassName);
props.put("connection.url", dbURL);
props.put("user", user);
props.put("password", password);
initializePool(props, initialConnections);
}

public Connection getConnection() throws SQLException {


Connection con = null;

Enumeration con = connections.keys();

synchronized (connections) {
while (cons.hasMoreElements()) {
con = (Connection)cons.nextElement();

Boolean b = (Boolean)connections.get(con);
if (b == Boolean.FALSE) {
// Znaleźliśmy nieużywane połączenie.
// Należy sprawdzić jego integralność szybkim
// wywołaniem setAutoCommit(true).
// Do użytku komercyjnego powinno się wykonać
//więcej testów, takich jak wykonanie prostego zapytania
try {
con.setAutoCommit(true);
}
catch(SQLException e) {
// Jeśli jest problem z połączeniem należy je zamienić na nowe
connections.remove(con);
con = getNewConnection();
}
//Zaktualizuj tablicę mieszającą
//Zwróć połączenie
return con;
}
}

// Jeśli się tu dostaliśmy, to oznacza, że nie ma wolnych połączeń. Należy


utworzyć nowe połączenie.
//
//
con = getNewConnection();
connections.put(con, Boolean.FALSE);
return con;
}
}
public void returnConnection(Connection returned) {
if (connections.containsKey(returned)) {
connections.put(returned, Boolean.FALSE);
}
}

private void initializePool (Properties props, int initialConnections)


throws SQLException, ClasNotFoundException {
//Ładujemy sterownik
Class.forName(props.getProperty("connection.driver"));

// Umieszcamy nasz zbiór połączeń w tablicy mieszającej


// Wartość FALSE wskazuje na nieużywane połączenie
for (int i = 0; i < initialConnections; i++) {
Connection con = getNewConnection();
connections.put(con, Boolean.FALSE);
}
}

private Connection getNewConnection() throws SQLException {


return DriverManager.getConnection(
props.getProperty("connection.url"),props);
}
}

Klasa ConnectionPool korzysta z tablicy mieszającej (Hashtable) przy użyciu obiektów Connection
jako kluczy i obiektów Boolean jako wartości. Wartości Boolean wskazują, czy połączenie jest używane
(true), czy wolne (false). Program wywołuje metodę getConnection() obiektu ConnectionPool,
aby wykorzystać obiekt Connection. Połączenie jest zwalniane za pomocą wywołania metody
returnConnection().Przedstawiony tu model puli połączeń jest bardzo prosty, ale w praktyce często
wymaga się bardziej rozbudowanego modelu puli połączeń, w którym jest możliwość sprawdzenia spójności
połączenia.
Przykład 9.9 przedstawia ulepszoną wersję serwletu zamówieniowego, który korzysta z puli połączeń.
Przykład 9.9.
Serwlet transakcji zbioru połączeń
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class OrderHandlerPool extends HttpServlet {


private ConnectionPool pool;

public void init () throws ServletException {


try {
pool = new ConnectionPool("oracle.jdbc.driver.Oracledriver",
"jdbc:oracle:oci7:orders", "user", "passwd", 5);
}
catch(Exception e) {
throw new UnavailableException("Nie można utworzyć zbioru połączeń");
}
}

public void doPost(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

Connection con = null;

res.setContentType("text/html");
PrintWriter out = res.getWriter();

try {
con = pool.getConnection();

// Rozpocznij transakcję
con.setAutoCommit(false);
Statement stmt = con.createStatement();
stmt.executeUpdate(
"UPDATE MAGAZYN SET ILOSC = (ILOSC - 10) WHERE PRODUCTID = 7");
stmt.executeUpdate(
"UPDATE WYSYLKA SET WYSLANE = (WYSLANE + 10) WHERE PRODUCTID = 7");

changeCard(); // metoda faktycznie nie istnieje ...

con.commit();
out.println("Zamówienie powiodło się! Dziękujemy za współpracę!");
}
catch(Exception e) {
// Każdy błąd jest podstawą do cofnięcia transakcji

try {
con.rollback();
}

catch (SQLException ignored){}


out.println("Zamówienie nie powiodło się. Proszę skontaktować się z
obsługą techniczną.");
}
finally {
if (con != null ) pool.returnConnection(con);
}
}
}

Połączenia jako część sesji


Śledzenie sesji, opisane dokładnie w rozdziale 7, pozwala na wykonywanie transakcji w inny sposób. Używając
sesji, możemy stworzyć lub przydzielić określone połączenie z bazą danych indywidualnym użytkownikom
witryny internetowej lub aplikacji intranetowej. W przykładzie 9.10 serwlet ConnectionPerClient
przypisuje obiekt Connection każdemu klientowi HttpSession. To powoduje umieszczenie obiektu
Connection wewnątrz obiektu ConnectionHolder, który jest odpowiedzialny za zapewnienie cyklu życia
połączenia.
Przykład 9.10.
Skojarzenie Connection z Session.
/* Właściwy Serwlet*/
public class ConnectionPerClient extends HttpServlet {
public void init() throws ServletException {
try {
Class.forName("oracle.jdbc.driver.OracleDriver”);
}

catch (ClassNotfoundException e ) {
throw new UnavailableException ("Nie można zaladowac sterownika bazy Oracle");
}
}

public void doGet (HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();

HttpSession session = req.getSession(true);


Connection con;

// zsynchronizuj: Bez tego dwa uchwyty mogą być utworzone dla jednego
klienta.
synchronized (session) {
// spróbuj otrzymać dla tego klienta uchwyt połączenia
ConnectionHolder holder =
(ConnectionHolder) session.getAttribute("servletapp.connection");

// stwórz (i przechowuj) nowe połączenie i uchwyt


if ( holder = null) {
try {
holder = new ConnectionHolder(DriverManager.getConnection(
"jdbc:oracle:oci7:ordersdb", "user", "passwd"));
session.setAttribute("servletapp.connection", holder);
}
catch (SQLException e) {
log(" Nie można otrzymać połączenia db", e);
}
}
// Pobierz właściwe połączenie od uchwytu
con = holder.getConnection();
}

// użyj teraz połączenia


try {
Statement stmt = con.createStatement();
stmt.executeUpdate(
"UPDATE MAGAZYN SET ILOSC = (ILOSC - 10) WHERE PRODUCTID = 7");
stmt.executeUpdate(
"UPDATE WYSYLKA SET WYSLANE = (WYSLANE + 10) WHERE PRODUCTID = 7");

// Obsłuż kartę kredytową i potwierdź transakcję w kolejnym serwlecie


res.sendRedirect(res.encodeRedirectURL(
req.getContextPath() + "/servlet/CreditCardHandler"));
}
catch(Exception e) {
// Jakikolwiek błąd powoduje cofnięcie transakcji

try {
con.rollback();
session.removeAttribute("servletapp.connection");
}

catch (Exception ignored){}


out.println("Zamówienie sie nie powiodło. Skontaktuj się z serwisem
technicznym.");
}
}
}
Zamiast bezpośrednio związania połączenia z sesją, stworzono prostą klasę przechowującą połączenia, która
implementuje interfejs HttpSessionBindingListener. Takie działanie jest konieczne, gdyż połączenia z
bazą danych są jednym z najbardziej ograniczonych zasobów w aplikacji JDBC i trzeba się upewnić, że każde
zakończone połączenie zostanie od razu zwolnione. Klasa ta ponadto pozwala na cofnięcie wszystkich nie
zatwierdzonych zmian. Jeśli użytkownik naszego hipotetycznego sklepu internetowego opuści system przed
weryfikacją danych, jego transakcja zostanie automatycznie anulowana po zakończeniu sesji.
Przechowywanie połączeń w sesjach wymaga dokładnej analizy potrzeb aplikacji. Większość prostych serwerów
bazodanowych ma możliwość obsługi maksymalnie ok. 100 połączeń; biurkowe bazy danych takie jak Microsoft
Access mogą obsłużyć nawet mniej żądań.

Serwlet księgi gości


Aby zrozumieć koncepcję bazy danych, spójrzmy na prawdziwy serwlet. Przykład 9.11 przedstawia kod
źródłowy typowego serwletu księgi gości współpracującego z bazą danych. Obsługuje on stronę www, której
goście mają możliwość wpisywania komentarzy i czytania wpisanych wiadomości przez innych użytkowników
odwiedzających stronę. Na rysunku 9.4 przedstawiono prezentację tej strony. Wszystkie komentarze gości są
wprowadzane do bazy danych. Aby mieć dostęp do informacji zapisanych w bazie, serwlet korzysta z kilku
przedstawionych w tym rozdziale technik (m.in. z puli połączeń, preinterpretowanych wyrażeń oraz z obiektu
ContextProperties, służącego do odczytania informacji o konfiguracji bazy danych z parametrów
kontekstu inicjującego). Serwlet jest rozszerzony o CacheHttpServlet z rozdziału 4, który optymalizuje
wydajność wyjścia.
Rysunek 9.4.
Księga gości

Przykład 9.11.
Proszę, Wpisz Się

import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.CacheHttpServlet;

public class Guestbook extends CacheHttpServlet {


static final String SELECT_ALL =
"SELECT name, email, cmt, id FROM guestlist ORDER BY id DESC";
static final String INSERT =
"INSERT INTO guestlist (id, name, email, cmt) " +
"VALUES (?,?,?,?)";
private long lastModified = 0; //
private ConnectionPool pool;

// pobierz wskaźnik do puli połączeń


public void init() throws ServletException {
try {
ServletContext context = getServletContext();
synchronized (context) {
// Pula może już istnieje, jako atrybut kontekstu
pool = (ConnectionPool) context.getAttribute("pool");
if (pool = null) {
// Skonstruuj pulę połączeń używając parametrów kontekstu inicjującego
// connection.driver, connection.url, user, password, itd.
pool= new ConnectionPool( new ContextProperties(context), 3);
context.setAttribute("pool", pool);
}
}
}

catch(Exception e) {
throw new UnavailableException("
Nie można pobrać puli połączeń z kontekstu: " + e.getMessage());
}
}

// Wyświetl istniejące w bazie danych komentarze, poproś o wpisanie nowego


public void doGet (HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();

printHeader(out);
printForm(out);
printMessages(out);
printFooter(out);
}

// Dodaj nowy komentarz i wyślij go do doGet


public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {

handleForm (req, res);


doGet(req, res);
}

private void printHeader (PrintWriter out) {


out.println("<HTML><HEAD><TITLE>Księga gości</TITLE></HEAD>");
out.println("<BODY>");
}

private void printForm (PrintWriter out) {


out.println("<FORM METHOD=POST>"); // wysyła do siebie
out.println("<B>Prześlij swój komentarz:</B><BR>");
out.println("Twoje nazwisko: <INPUT TYPE=TEXT NAME=name><BR>");
out.println("Twój adres email: <INPUT TYPE=TEXT NAME=email><BR>");
out.println("Komentarz: <INPUT TYPE=TEXT SIZE=50 NAME=comment><BR>");
out.println("<INPUT TYPE=SUBMIT VALUE=\"Wyslij\"><BR>");
out.println("</FORM>");
out.println("<HR>");
}
// przeczytaj i wyświetl wiadomości z bazy danych
private void printMessages (PrintWriter out) throws ServletException {
String name, email, comment;

Connection con= null;


Statement stmt = null;
ResultSet rs = null;

try {
con = pool.get.Connection();
stmt = con.createStatement();
rs = stmt.executeQuery (SELECT_ALL);

while (rs.next()) {
name = rs.getString(1);
if (rs.wasNull()||name.length()==0) name = "Nieznany użytkownik";
email = rs.getString(2);
.if (rs.wasNull()||email.length()==0) name = "Nieznany użytkownik";
.comment = rs.getString(3);
if (rs.wasNull()||comment.length()==0) name = "Bez komentarzy";
out.println("<DL>");
out.println("<DT><B>" + name + "</B> (" + email + ") mówi ");
out.println("<DD><PRE>" + comment + "</PRE>");
out.println("</DL>");
}
}
catch (SQLException e) {
throw new ServletException(e);
}
finally {
try {
.if (stmt != null) stmt.close();
}
catch (SQLException ignored) {}
pool.returnConnection(con);
}
}
private void printFooter (PrintWriter out) {
out.println("</BODY>");
}
// zachowaj nowy komentarz w bazie danych
private HandleForm (HttpServletRequest req,
HttpServletResponse res) throws ServletException {
String name = req.getParameter("name");
String email = req.getParameter("email");
String comment = req.getParameter("comment");

Connection con = null;


PreparedStatement pstmt = null;
try {
con = pool.getConnection();
//Użyj przygotowanego wyrażenia aby automatycznie unikać znaków specjalnych
pstmt = con.prepareStatement(INSERT);
long time = System.currentTimeMillis();
pstmt.setString(1, Long.toString(time));
pstmt.setString(2, name);
pstmt.setString(3, email);
pstmt.setString(4, comment);
pstmt.executeUpdate();
}
catch (SQLException e) {
throw new ServletException(e);
}
finally {
try {
if (pstmt != null) pstmt.close();
}
catch (SQLException ignored) {}
pool.returnConnection(con);
}
//zauważ,że posiadamy nowo zmodyfikowany czas
lastModified = System.currentTimeMillis();
}

public long getLastModified(HttpServletRequest req) {


return lastModified; // wspiera CacheHttpServlet
}
}
Wyrażenia SQL, umożliwiające dostęp do bazy danych znajdują się w górnej części klasy Guestbook w
zmiennych typu static final. Wydzielenie tych wyrażeń pozwala na łatwe dokonywanie zmian aplikacji w
przyszłości.
Metoda init() pobiera pulę połączeń z obiektu ServletContext lub tworzy ją, gdy w obiekcie
ServletContext nie ma zapisanej puli połączeń. Istniejąca pula połączeń jest zapisana jako atrybut
ServletContext pod nazwą pool. Jeśli dany atrybut nie istnieje, metoda init() tworzy nową pulę
połączeń za pomocą klasy ContextProperties i zapisuje pulę. Pula połączeń nie jest wprawdzie potrzebna
temu serwletowi, bo nie obsługuje on transakcji, więc przepustowość przez pojedynczy obiekt Connection
jest wystarczająca. Użycie puli połączeń pozwala tu na zarządzanie obiektu Connection, który jest używany
jednocześnie przez wiele serwletów.
Metoda doGet() powoduje wyświetlenie nagłówka, formularza HTML z miejscem na wpisanie komentarza i
wcześniejszych komentarzy odczytanych z bazy danych. Techniki wykonania bardziej eleganckiej strony HTML
opisane są w dalszych rozdziałach, począwszy od rozdziału 14.
Metoda printMessages() używa obiektu Connection pobranego z puli połączeń, wykonuje zapytanie
SELECT_ALL za pomocą obiektu Statement. Na początku każdego wiersza obiektu ResultSet powyższa
metoda powoduje „dopisanie” znacznika <DL> w formularzu HTML. W końcowym bloku obiekt Connection
jest zwalniany i powraca do puli połączeń.
Metoda doPost() jest wywoływana, gdy użytkownik przesyła komentarze za pomocą formularza
generowanego za pomocą metody doGet(). Ta metoda wywołuje funkcję handleForm(), która gromadzi
komentarze wewnątrz bazy danych, a następnie przesyła komentarze metodzie doGet(), która powoduje
wyświetlenie ich na stronie. Metoda handleForm() odczytuje parametry nazwy, adresu email i komentarza
oraz umieszcza je w bazie danych za pomocą instrukcji INSERT obiektu PreparedStatement. Używając
obiektów PreparedStatement do zachowywania łańcuchów tekstowych automatycznie unikamy znaków
specjalnych (zabronionych). Oprócz komentarza, w bazie danych znajduje się informacja o dacie wpisanych
komentarzy.
W dolnej części metody handleForm() ustawiany jest parametr lastModified. Pozwala to metodzie
getLastModified() zwrócić czas ostatniej aktualizacji bazy danych. Ponieważ ten serwlet jest rozszerzony
o CacheHttpServlet, ostatnia zmodyfikowana funkcja będzie wykorzystana przez superklasę do
zarządzania pamięcią wyjścia, która będzie zmieniana tylko po aktualizacji bazy danych.*

Zaawansowane techniki JDBC


Teraz, gdy poznaliśmy podstawy, możemy pomówić o kilku zaawansowanych technikach używających
serwletów i JDBC. Najpierw sprawdzimy, w jaki sposób serwlety używają procedur bazy danych. Potem
dowiemy się, w jaki sposób serwlety pobierają z bazy danych złożone typy danych, takie jak dane binarne
(obrazy, aplikacje, itp.), duże ilości tekstu, a nawet wykonywalny kod manipulujący bazą danych.

Przechowywane procedury
Większość baz danych posiada pewien rodzaj wewnętrznego języka programowania służącego do tworzenia i
przechowywania procedur działających wewnątrz bazy danych. Utworzone procedury mogą być wywoływane
przez zewnętrzne aplikacje. Jednym z przykładów jest język PL/SQL dla Oracle. Bazodanowe języki
programowania są często lepiej przystosowane do wykonania pewnych akcji w bazie danych. Wiele już
istniejących baz danych posiada wiele zgromadzonych procedur gotowych do użycia, więc dobrze byłoby poznać
pewne techniki wykorzystania tych procedur we własnych aplikacjach.
Poniżej przedstawiono kod procedury napisanej w Oracle PL/SQL:

CREATE OR REPLACE PROCEDURE sp_interest


(id IN INTEGER
BAL IN OUT FLOAT) IS
BEGIN
SELECT balance
INTO bal
FROM accounts
WHERE account_id = id;

bal := bal + bal * 0.03;


UPDATE accounts
SET balance = BAL
WHERE account_id = id;

END;

Ta procedura, oprócz instrukcji SQL, wykonuje tylko pewne obliczenie. Ten program jest łatwy do napisania w
SQL (transakcja z wcześniejszego przykładu działa w podobny sposób) i stanowi tylko prosty przykład
procedury przechowywanej. Istnieje kilka powodów, dla których warto używać procedur przechowywanych:

• Przechowywane procedury są wcześniej skompilowane w RDBMS*, więc działają szybciej niż


dynamicznie używane wyrażenia SQL.
• Przechowywane procedury są w całości wykonywane w RBDMS, więc zapytania i proces
uaktualniania bazy danych mogą odbyć się bez korzystania z sieci.
• Raz napisaną procedurę można użyć w wielu różnych aplikacjach i językach programowania.
• Zmiany w strukturze tabel, na których operują procedury, wymagają modyfikacji kodu tych
procedur. Kod aplikacji korzystającej z danej procedury pozostaje niezmieniony.

*
Należy pamiętać, że nie ma możliwości buforowania dla żądań POST. Strona jest odświeżana po każdym przesłanym komentarzu.
Gdybyśmy w miejsce komentarza nie wpisali nic, to aplikacja spowoduje ponowne załadowanie całej strony, bo żądanie POST nie zostało
zapamiętane.
*
RDBMS — maszyna bazodanowa
• Wiele starszych baz danych posiada dużą ilość przechowywanych procedur, więc dobrze byłoby to
wykorzystać.
Procedura Oracle PL/SQL w tym przykładzie pobiera początkowe wartości, np. identyfikator konta i zwraca
zaktualizowane saldo. Ponieważ każda baza danych ma swoją własną składnię dostępu do przechowywanych
procedur, w JDBC utworzono interfejs pozwalający na korzystanie z przechowywanych procedur niezależnie od
rodzaju bazy danych. Ten interfejs to java.sql.CallableStatement. Składnia wywołania procedury nie
zwracającej wyniku to np. {call procedure_name(?,?)}, a zwracającej wynik to np.{?=call
procedure_name(?,?)}. Parametry wewnątrz nawiasów są opcjonalne.
Sposób użycia klasy CallableStatement jest podobny do sposobu, w jaki używa się klasy
PreparedStatement.

CalableStatement cstmt = con.prepareCall("{call sp_interest(?,?)}");


cstmt.registerOutParameter(2, java.sql.Types.FLOAT);
cstmt.setInt(1, accountID);
cstmt.execute();
out.println("Zaktualizowane saldo: " + cstmt.getFloat(2));
Ten program tworzy najpierw obiekt CallableStatement poprzez wywołanie metody prepareCall()
obiektu Connection. Ponieważ nasza procedura przechowywana ma parametry wyjściowe, używa metody
registerOutParameter obiektu CallableStatement, by zarejestrować wyjściowe parametry jako
wartości typu FLOAT. Program wykonuje procedurę i używa metody getFloat() obiektu
CallableStatement aby wyświetlić zaktualizowane saldo. Metody getXXX() interfejsu
CallableStatement mają podobne właściwości do metod getXXX()interfejsu ResultSet.

Pliki binarne i księgi, czyli bardzo duże obiekty


Większość baz danych wspomaga obsługę dużych obiektów takich jak łańcuchy tekstowe o dużych rozmiarach
(do kilku GB) czy plików binarnych, np. plików z obrazkami (*.gif). W zależności od typu bazy obsługa tych
danych odbywa się w różny sposób, ale metody zapewniane przez JDBC do pobierania dużych obiektów są
standardowe i mogą być stosowane dla różnych typów baz danych. Metoda getAsciiStream() obiektu
ResultSet obsługuje duże łańcuchy tekstowe, a getBinaryStream() pracuje na dużych obiektach
binarnych. Są to metody operujące na strumieniach, więc każda z tych metod zwraca obiekt InputStream.
Obsługa dużych obiektów stanowi główne źródło problemów pojawiających się podczas kożystania z JDBC. W
trakcie projektowania aplikacji należy przetestować sterowniki przy użyciu największych obiektów dla niej
przewidzianych. Sterownik JDBC dla Oracle ma szczególną skłonność do błędnej pracy podczas używania
dużych obiektów.
Oto fragment kodu serwletu, w którym jest pobierany długi łańcuch ASCII. Zakładamy, że reszta serwletu jest
już stworzona:
try {
ResultSet rs = stmt.executeQuery(
"SELECT TITLE, SENDER, MESSAGE FROM MESSAGES WHERE MESSAGE_ID = 9");
if (rs.next()) {
out.println("<H1>" + rs.getString("title") + "</H1>");
out.println("<B>From:</B> ") + rs.getString("sender") + "<BR>");
BufferedReader msgText = new BufferedReader(rs.getAsciiStream("message")));
while (msgText.ready()) {
out.println(msgText.readLine());
}
}
}
catch (SQLException e) {
// zdaj relację
}

Podczas odczytywania z InputStream, serwlet nie otrzymuje wartości z żadnej innej kolumny zbioru
wyników. To bardzo ważne, bo wywołanie każdej innej metody getXXX() obiektu ResultSet zamyka
poprzedni strumień InputStream.
Dane binarne mogą być pobrane w ten sam sposób używając ResultSet.getBinaryStream(). W tym
wypadku należy ustalić właściwy typ danych i przesłać na wyjście w formie strumienia bajtowego. Przykład 9.12
pokazuje serwlet zwracający plik GIF pobrany z bazy danych.
Przykład 9.12.
Wczytywanie binarnego obrazka GIF z bazy danych
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class DBGifReader extends HttpServlet {


Connection con;

public void init() throws ServletException {


try {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver”);
con = DriverManager.getConnection("jdbc:odbc:imagedb","user","passwd");
}

catch (ClassNotfoundException e ) {
throw new UnavailableException ("Nie można zaladowac sterownika JdbcOdbc");
}
catch (SQLException e) {
throw new UnavailableException("Nie można połączyć się z db");
}
}

public void doGet(HttpServletRequest req, HttpServletResponse res)


throws ServletException, IOException {

try{
res.setContentType("image/gif");
ServletOutputStream out = res.getOutputStream()

Statement stmt =con.createStatement();


ResultSet rs = stmt.executeQuery(
"SELECT IMAGE FROM PICTURES WHERE PID = " + req.getParameter("PID"));

if (rs.next()) {
BufferedInputStream gifData =
new BufferedInputStream (rs.getBinaryStream("image"));
byte[] buf = new byte[4 * 1024]; //bufor 4K
int len;
while(( len=gifdata.read(buf,0,buf.length))!=-1){
out.write(buf,0,len);
}
}
else {
res.sendError(res.SC_NOT_FOUND);
}
}
catch (SQLException e) {
// zdaj relację
}

Co dalej?
Poza metodami zawartymi standardowo w JDBC i technikami przedstawionymi w tym rozdziale istnieją
dodatkowe interfejsy wspomagające pisanie skomplikowanych aplikacji umożliwiających dostęp do baz danych.
Jednym z nich jest interfejs JDBC 2.0, następca omówionego w tym rozdziale JDBC 1.2.
JDBC 2.0 jest szeroko stosowanym narzędziem. Implementacja JDBC 2.0 wymaga bazy danych, która może
obsłużyć zaawansowane metody zawarte w interfejsie oraz sterownika, który zapewnia dostęp do tych metod. Na
szczęście, wsparcie JDBC 2.0 jest gwarantowane przez wszystkie serwery J2EE, bo każdy serwer zgodny z J2EE
wymaga zapewnienia dostępu do sterownika i bazy danych współpracującej z JDBC2.0. Instrukcja implementacji
firmy Sun zawiera dodatkowo ewaluacyjną wersję bazy danych Cloudscape.
JDBC 2.0 składa się z dwóch części. Jedna z nich znajduje się w pakiecie Javy 2 java.sql i wymaga bazy
danych i sterownika zgodnych z JDBC 2.0. Druga część jest umieszczona w pakiecie javax.sql, który
zawiera kilka dodatkowych metod (użycie tych metod nie jest wymagane).
Do udoskonaleń pakietu java.sql należy możliwość modyfikacji i przeglądania zbioru wyników. W obiekcie
ResultSet kursor można przesuwać do przodu, do tyłu lub można ustawić go na dowolnym rekordzie (w
interfejsie JDBC 1.2 można poruszać się tylko o jeden element do przodu). Dodano możliwość obsługi typów
danych SQL3 (BLOB — dla dużych obiektów binarnych, CLOB dla dużych obiektów tekstowych i ARRAY dla
tablic).
Opcjonalny pakiet javax.sql zawiera wiele metod szczególnie pomocnych w aplikacjach komercyjnych.
Jedną z nich jest wsparcie wyszukiwania JNDI, które pozwala otrzymywać połączenie z bazą danych na
podstawie nazwy z serwera JNDI. Kolejną cechą jest wbudowany mechanizm puli połączeń (to bardzo różni
interfejs JDBC 2.0 od JDBC 1.2, ale nie wpływa na działanie aplikacji napisanych pod starszy interfejs). Bardzo
ważną opcjonalną cechą w JDBC 2.0 jest wspomaganie transakcji rozproszonych. Pozwala to na rozmieszczenie
transakcji w różnych oddzielnych bazach danych i aktualizowanie obu baz jedną niepodzielną operacją. W
dodatku, JDBC 2.0 wspiera obiekty RowSet, które „opakowują”wiersze danych ResultSet. Ułatwia to
zapamiętywanie danych, zapewnia prosty transfer danych przez sieć oraz standaryzowany dostęp do źródeł
danych tabelarycznych (np. do arkusza kalkulacyjnego lub pliku jednorodnego).
Więcej informacji o cechach JDBC 2.0 można znaleźć w książce George'a Reese'a Database Programming with
JDBC and Java (O'Reilly) i JDBC API Tutorial and Reference autorstwa Seth White oraz na stronie internetowej
http://java.sun.com/products/jdbc.
Dla tych, którzy oczekują czegoś więcej niż oferuje JDBC, firma ClearInk sponsoruje otwartą bibliotekę z
kodami źródłowymi interfejsu Village, który jest lepszy od JDBC 1.2 i łatwiej współpracuje z bazami danych.
Bibliotekę stworzono w oparciu o produkt komercyjny dbKona firmy BEA/WebLogic. Na podstawie Village
stworzono bardziej rozbudowaną wersję o nazwie Town. Oba produkty Village i Town są dostępne na stronie
http://www.working-dogs.com (jak na razie nie ma żadnych informacji o pakiecie City).
Rozdział 10. Komunikacja aplet-

serwlet

Niniejszy rozdział przedstawia kilka technik, przy pomocy których aplety mogą komunikować się z serwletami.
Zaprezentowane zostanie nieco inne podejście, niż to, jakiego można by się spodziewać. Zamiast przyjmować, że
istnieje aplet i serwlet, które muszą się ze sobą porozumieć, przyjęto, że istnieje aplet, który musi porozumieć się
z pewnym elementem serwera, i opisane zostanie, dlaczego czasami elementem tym powinien być serwlet.
Aby rozpocząć, proszę pomyśleć o apletach, które muszą porozumieć się z serwerem. Istnieje kilka dobrych
przykładów. Proszę spojrzeć na aplet administracyjny, który działa na serwerze Java Web Server. Proszę
pomyśleć, jak on działa — jest wykonywany na kliencie, ale konfiguruje serwer. Aby to uczynić, aplet i serwer
muszą być w niemal stałym kontakcie. Inny przykład, proszę spojrzeć na jeden z popularnych apletów
pogawędek. Jeden klient coś mówi, a cała reszta to widzi. Jak to działa? Na pewno aplety nie komunikują się ze
sobą. Zamiast tego, każdy aplet wysyła swoje wiadomości do centralnego serwera, a serwer zajmuje się
uaktualnieniem informacji innych klientów. W końcu, proszę sobie wyobrazić aplet, który śledzi cenę zbioru
akcji i dokonuje ciągłego uaktualniania. Jak aplet poznaje aktualne ceny i, co ważniejsze, skąd wie, kiedy się one
zmieniają? Odpowiedzią jest, że komunikuje się on z serwerem.

Opcje komunikacji
Zainteresowanie handlem akcjami wzrasta razem z indeksami giełdowymi, więc kontynuowany będzie
hipotetyczny aplet, odpowiadający za akcje. Należy ostrzec, że aplet ten pozostanie hipotetyczny. Zostanie on
wykorzystany jedynie jako punkt odniesienia w dyskusji nad kwestiami należącymi do komunikacji aplet-serwlet.
Ale proszę się nie martwić, w dalszej części tego rozdziału występuje duża ilość kodu przedstawiająca techniki tu
opisane, jedynie w nieco prostszych przykładach.
Ten aplet akcji musi pobierać ceny z jakiegoś serwera. Przyjmując, że jest to zwykły, niegodny zaufania aplet,
jest tyko jedno wyjście — z komputera, z którego został pobrany. Każda próba połączenia się z innym
komputerem daje wynik w SecurityException, tak więc należy przyjąć, że aplet pobiera ceny z serwera, z
którego został pobrany6. Pytanie pozostaje — jak aplet i serwer mogą się ze sobą porozumiewać?

Aplety godne i niegodne zaufania

Kiedy aplet Javy zostaje osadzony w stronie WWW, przeglądarka może pobrać go i wykonać automatycznie.
Jeżeli się nad tym zastanowić, jest to bardzo niebezpieczna sprawa. Tak więc, aby chronić klienta, JDK 1.0
przyjmował, że wszystkie aplety są niegodne zaufania i uruchamiał je pod kontrolą SecurityManager, który
w poważny sposób ograniczał ich funkcjonalność. Na przykład, menedżer bezpieczeństwa zapewniał
niemożność apletów do zapisywania informacji w systemie plików użytkownika, odczytywania pewnych
właściwości systemu, przyjmowania przychodzących połączeń przez porty, czy też uruchamiania połączeń
wychodzących z każdym komputerem, który nie był serwerem macierzystym. Własności te zabezpieczały
klienta, ale ograniczały użyteczność apletów.

6
Można zastanawiać się, skąd sam serwer uzyskał informacje na temat cen. Dla celów tego przykładu należy przyjąć, że była
to magia
Konsekwentnie, JDK 1.1 wprowadził koncepcję apletów godnych zaufania — apletów, które mogły działać,
jak zwykłe aplikacje z pełnym dostępem do komputera klienta. Aby aplet został uznany za godny zaufania,
musi zostać elektronicznie podpisany przez osobę z firmy, której ufa klient (zaznaczonej w przeglądarce
użytkownika). Podpis uwierzytelnia pochodzenie apletu i gwarantuje spójność podczas transferu tak, aby
klient wiedział, że kod apletu nie został złośliwie zmieniony. Pozwoliło to na tworzenie bardziej użytecznych
apletów, ale było podejściem typu wszystko albo nic.

Aby dać klientowi więcej kontroli, JDK 1.2 wprowadził złożony system kontroli dostępu. W tym nowym
systemie, podpisany elektronicznie aplet może być częściowo uznany za godny zaufania i może on otrzymać
pewne możliwości bez uzyskania pełnej kontroli nad systemem. Pozwala to na przyznawanie apletom
nieznanego pochodzenia niewielkich przywilejów (takich jak zapis w jednym, konkretnym katalogu), bez
dostarczania im możliwości przeglądania twardego dysku klienta. Producenci przeglądarek powoli
wprowadzają obsługę nowych wersji JDK, ale na szczęście możliwe jest uaktualnienie JVM przeglądarki przy
pomocy modułu rozszerzającego Java Plug-in, bezpłatnego produktu dostępnego pod adresem
http://java.sun.com/products/plugin.

Połączenia przez HTTP i zwykłe porty


Przed stworzeniem JDK 1.1 i serwletów istniały dwie możliwości komunikacji aplet-serwer:

• Połączenie HTTP apletu z programem CGI na serwerze. Aplet działa jak przeglądarka i żąda strony,
analizując odpowiedź na własny użytek. Aplet może dostarczać informacji przy pomocy łańcucha
zapytania lub danych POST oraz otrzymywać informacje ze zwracanej strony.
• Połączenie przez zwykły port apletu z serwerem nie-HTTP. Serwer nie-HTTP może słuchać na danym
porcie i porozumiewać się z apletem przy pomocy dowolnego protokołu, który zostanie wynegocjowany
Każde z tych podejść posiada wady i zalety. Połączenie HTTP apletu z programem CGI działa dobrze z
następujących powodów:

• Jest ono łatwe do napisania. Aplet może korzystać z klas java.net.URL i


java.net.URLConnection w celu zarządzania kanałem komunikacji, a program CGI może zostać
napisany tak, jak każdy inny.
• Działa nawet w przypadku apletów pracujących za firewallem. Większość firewalli pozwala na
połączenia HTTP, a nie pozwala na połączenia przez zwykłe porty.
• Pozwala apletowi Javy na komunikację z programem napisanym w dowolnym języku. Program CGI nie
musi być napisany w Javie. Może być to Perl, C, C++, lub każdy inny język.
• Współpracuje z apletami utworzonymi przy pomocy JDK 1.0, tak więc pracuje z wszystkimi
przeglądarkami posiadającymi obsługę Javy.
• Pozwala na bezpieczną komunikację. Aplet może porozumiewać się z bezpiecznym serwerem przy
pomocy zaszyfrowanego protokołu HTTPS (HTTP + SSL).
• Program CGI może być wykorzystywany przez palety, a także przez przeglądarki. W przypadku
przykładu akcji, program CGI może wykonywać podwójne zadanie, działając również jako wsparcie dla
serwisu opartego na formularzach HTML. Zwłaszcza wygodne jest wykorzystanie przez aplet
istniejącego programu CGI.
Jednak połączenie HTTP z programem CGI posiada również pewne wady:

• Jest wolne. Z powodu paradygmatu HTTP żądanie-odpowiedź, aplet i program CGI nie mogą
porozumiewać się interaktywnie. Dla każdego żądania i odpowiedzi muszą one tworzyć nowy kanał
komunikacyjny. Dodatkowo istnieje pewne standardowe opóźnienie spowodowane uruchamianiem i
inicjalizacją programu CGI w celu obsługi żądania.
• Zazwyczaj wymaga, aby żądania były utworzone w formie okropnej tablicy par nazwa-wartość. Na
przykład, kiedy przykładowy aplet akcji pyta o najwyższy dzienny kurs akcji Sun Microsystems, musi
zadać pytanie przy pomocy okropnego łańcucha takiego jak akcja=sun&zapytanie=maxdzien.
• Wymaga, aby wszystkie odpowiedzi były sformatowane według arbitralnego, wcześniej uzgodnionego
standardu. Na przykład, kiedy przykładowy aplet akcji otrzymuje odpowiedź zawierającą najwyższy
dzienny kurs akcji, musi dokładnie znać sposób analizy danych. Czy zwrócona cena rozpoczyna się
znakiem dolara? Czy odpowiedź zawiera również czas wystąpienia ceny maksymalnej? A jeżeli tak,
gdzie i w jakim formacie został on podany?
• Jedynie aplet może rozpoczynać komunikację. Program CGI musi biernie czekać na żądanie od apletu,
aby mógł zacząć odpowiadać. Jeżeli kurs akcji zmienia się, aplet może się o tym dowiedzieć jedynie
wtedy, gdy zada właściwe pytanie.
Aplet i serwlet mogą się również porozumiewać poprzez połączenie zwykłym portem apletu z procesem serwera
nie-HTTP. Podejście do posiada następujące zalety w stosunku do podejścia opartego na HTTP:

• Pozwala na dwukierunkową stałą komunikację. Aplet i serwlet mogą wykorzystywać ten sam port (a
nawet kilka portów) w celu interaktywnego porozumiewania się, wysyłania wiadomości w jedną i drugą
stronę. Z powodów bezpieczeństwa, aplet musi zawsze rozpoczynać połączenie poprzez połączenie się z
portem serwera na komputerze-serwerze, lecz po ustanowieniu połączenia przez port, każda strona
może zapisywać do portu w dowolnym czasie. Pozwala to apletowi akcji na otrzymywanie uaktualnień
cen akcji, kiedy tylko są one dostępne.
• Pozwala na wykonywanie po stronie serwera bardziej wydajnego programu. Serwer nie-HTTP może
być utworzony w celu obsługi żądania bezpośrednio bez uruchamiania zewnętrznego programu CGI w
celu wykonania pracy.
Lecz połączenie przez zwykły port posiada również wady w stosunku do podejścia opartego na HTTP:

• Nie działa w przypadku apletów działających za firewallami. Większość firewalli nie pozwala na
połączenia przez zwykły port, tak więc nie pozwala na ten typ komunikacji aplet-serwlet. Tak więc
mechanizm ten powinien zostać wykorzystywany jedynie wtedy, gdy pewne jest, że aplet nie będzie
uruchomiony po drugiej stronie firewalla, na przykład w wypadku aplikacji Intranetu.
• Tworzenie kodu działającego na serwerze może być stosunkowo skomplikowane. Zawsze musi istnieć
pewien proces (taki, jak serwer kursów akcji) słuchający na dobrze znanym porcie serwera. Tworzenie
takiej aplikacji w Javie jest łatwiejsze niż w C++, ale wciąż nie jest proste.
• Może wymagać utworzenia nowego protokołu. Aplet i serwer muszą zdefiniować protokół, który
wykorzystają w celu komunikacji. Chociaż protokół ten może być prostszy i bardziej wydajny niż
HTTP, często musi zostać specjalnie napisany.
• Serwer nie-HTTP nie może zostać bezpośrednio podłączony do przeglądarki WWW. Przeglądarki
posługują się HTTP; nie mogą komunikować się z serwerem nie-HTTP.
Standardowe historyczne podejście wymagało od apletów porozumiewania się przy pomocy HTTP z programami
CGI na serwerze. Jest to łatwe oraz działa we wszystkich typach przeglądarek, nawet tych pracujących za
firewallami. Wykorzystanie połączenia przez zwykły port zostało generalnie zarezerwowane dla sytuacji, w
której jest to absolutnie konieczne, takich jak ta, w której aplet i serwer wymagają komunikacji dwukierunkowej.
A także, nawet w tym wypadku, często możliwe jest wykorzystanie połączeń HTTP w celu symulacji
komunikacji dwukierunkowej, co ma służyć przejściu przez firewalle, jak przedstawione to będzie w
późniejszym przykładzie.

Serwlety i serializacja obiektu


Niedawne wprowadzenie serwletów Javy i serializacji obiektów tchnęło nowe życie w te tradycyjne techniki
komunikacji aplet-serwer. Serwlety zastępują wolno uruchamiające się programy CGI, poprawiając wydajność
komunikacji aplet-serwer opartej na HTTP i czyniąc częste połączenia aplet-serwer możliwymi. Chociaż ogólnie
rzecz biorąc prawdą jest, że aplet i serwlet ciągle wymagają czasu do ponownego otworzenia połączenia dla
każdego żądania i odpowiedzi, aplet nie musi już dłużej czekać, aż serwer uruchomi program CGI w celu obsługi
każdego z powtarzających się żądań.
Serializacja obiektów Javy uprościła kwestie związane z formatowaniem odpowiedzi. W związku z tym, że
zarówno aplety, jak i serwlety napisane są w Javie, naturalnym jest, że powinny się one komunikować poprzez
wymianę obiektów Javy. Na przykład, kiedy hipotetyczny aplet akcji pyta odpowiedni serwlet o najwyższy
dzienny kurs akcji Suna, może otrzymać CenaAkcji w formie zserializowanego obiektu. Z niego można
pobrać najwyższą wartość jako float oraz czas tej wartości jako Czas. Jest to spójne i dostarcza prostego
zabezpieczenia typów. Proszę jednak pamiętać, że serializacja obiektów działa jedynie z apletami pracującymi
wewnątrz przeglądarek obsługujących JDK 1.1 i późniejszych.
JDBC, RMI i CORBA
JDK 1.1 zawiera dwie dodatkowe własności wywierające wpływ na komunikację aplet-serwlet — JDBC i RMI.
API JDBC (Java Database Connectivity — Łączność z Bazami Danych Javy), omówiony w rozdziale 9,
„Łączność z bazami danych”, pozwala na połączenie się z relacyjną bazą danych na tym samym lub innym
komputerze. Aplety Javy napisane od wersji JDK 1.1 mogą wykorzystywać JDBC do komunikowania się z bazą
danych na serwerze. Ta przeznaczona do specjalnych celów komunikacja generalnie nie wymaga komunikacji
aplet-serwlet. Jednak często okazuje się pomocne, aby aplet (szczególnie napisany dla JDK 1.0) nie łączył się
bezpośrednio z bazą danych (lub z proxy ba serwerze WWW) a zamiast tego łączył się z serwletem
obsługującym łączność z bazą danych zamiast apletu, jak opisano w ramce „Serwlety pośredniczące” w rozdziale
9, „Łączność z bazami danych”. Na przykład, aplet który chce wyszukać adres danej osoby może połączyć się z
serwletem przy pomocy HTTP, przekazać nazwisko tej osoby wykorzystując parametry HTTP, po czym
otrzymać adres jako specjalnie sformatowany łańcuch lub obiekt zserializowany. To zastosowanie komunikacji
aplet-serwlet opiera się w znacznym stopniu na istniejących protokołach takich jak HTTP, tak więc nie zostanie
tu szerzej opisane.
API RMI (Remote Method Invocation — Zdalne Wywoływanie Metod) pozwala apletowi na wywołanie metod
obiektu Javy uruchomionego na serwerze oraz, w pewnych wypadkach, pozwala również obiektowi na serwerze
na wywoływanie metod apletu. Zalety wykorzystania RMI w komunikacji aplet-serwer są bardzo znaczące:

• Pozwala on apletom i obiektom serwera na porozumiewanie się przy pomocy eleganckiego,


obiektowego paradygmatu wysokiego poziomu. Żądania mogą zostać wykonywane jako wywołania
metod, z przekazaniem zserializowanych parametrów obiektu, kiedy jest to konieczne. Odpowiedzi
mogą być otrzymywanie jako obiekty zserializowane lub nawet jako odwołania do innych zdalnych
obiektów. Lecz nawet używanie słów żądanie i odpowiedź pokazuje na zbytnie przywiązanie do HTTP!
Przy korzystaniu z RMI nie ma żadnych żądań i odpowiedzi, jedynie wywołania metod. Patrząc na
przykład apletu akcji, aplet może pobrać maksymalny dzienny kurs akcji Suna poprzez wywołanie
sun.pobierzMaxDzien(), gdzie sun jest obiektem Javy istniejącym na serwerze.
• Pozwala on obiektom serwera na wykonywanie odwołań wstecznych do metod apletu. Na przykład,
patrząc na aplet akcji, serwer może powiadomić zainteresowane aplety, że cena akcji zmieniła się
poprzez wywołanie applet.update(akcja).
• Możliwe jest przedostanie się przez firewalle (chociaż nie jest to zbyt dobry pomysł, ich obsługa przez
obecne przeglądarki jest niedokładna). Warstwa transportująca RMI normalnie opiera się na
bezpośrednich połączeniach przez port w celu wykonania swojej pracy. Kiedy jednak aplet
wykonywane jest za firewallem, komunikacja przez zwykły port zawodzi. W tym przypadku warstwa
transportująca RMI może zacząć działać wyłącznie wewnątrz protokołu HTTP7.
Nie odbywa się to jednak bez pewnych kosztów. Wykorzystanie HTTP ma wpływ na wydajność, a paradygmat
żądanie-odpowiedź HTTP nie obsługuje odwołań wstecznych. Wady RMI są równie zajmujące:

• Jest on skomplikowany. Komunikacja RMI wykorzystuje specjalne klasy szkieletowe dla każdego
zdalnego obiektu, a także wymaga rejestru nazw, w którym klienty mogą otrzymać odwołania do tych
zdalnych obiektów.
• Jest on obsługiwany przez niewielką ilość przeglądarek. Z wszystkich popularnych przeglądarek
dostępnych w trakcie tworzenia niniejszej książki, jedynie Netscape Navigator 4 (i powyżej) zawierał
obsługę RMI. Ani poprzednie wersje przeglądarki Netscape'a, ani żadne wersje Internet Explorera
Microsoftu nie obsługują RMI bez instalacji odpowiedniego modułu rozszerzającego. (Chociaż
ponieważ klasy RMI to czysta Java, udało się dodać RMI do Internet Explorera jako dodatkową
bibliotekę.)
• Może być on wykorzystywany jedynie przez klienty Javy. Obiekt serwera nie może być współdzielony
przez przeglądarkę a nawet klienta C++.
Większa ilość informacji na temat programowania RMI dostępna jest w książkach „Java Network Programming”
autorstwa Elliotte Rusty Harold (O'Reilly) i „Java Distributed Computing” autorstwa Jima Farleya (O'Reilly).

7
Dokładny opis właściwości systemu potrzebnych aplikacji-klientowi RMI do przejścia przez firewalle znajduje się w 42
podpowiedzi Javy Johna D. Mitchela w JavaWorld pod adresem http://www.javaworld.com/javaworld/javatips/jw-
javatip42.html (niewspomniane w artykule, a bardzo ważne są również własności socksProxySet, socksProxyHost i
socksProxyPort konieczne dla proxy opartych na SOCKS). Wszystkie te właściwości systemu powinny być
automatycznie ustawiane przez przeglądarkę WWW, lecz niestety obecnie niewiele przeglądarek to robi, pozostawiając
swoje aplety bez sposobu określenia właściwych ustawień i bez możliwości wykorzystanie RMI przez firewall.
CORBA (Common Object Request Broker Architecture — Wspólna Architektura Żądań Obiektów) to
technologia podobna do RMI, która pozwala na komunikację pomiędzy obiektami rozproszonymi napisanymi w
różnych językach. Przy pomocy CORBA'y i jej protokołu komunikacyjnego IIOP (Internet Inter-ORB Protocol),
klient C++ może porozumiewać się z serwletem Javy. Przedstawienie tej techniki przekracza zakres niniejszej
książki. Większa ilość informacji dostępna jest pod adresami http://www.omg.org i
http://java.sun.com/products/jdk/idl.

Podejście hybrydowe
Teraz, kiedy przeanalizowane zostały już wszystkie opcje, pytanie pozostaje: jak przykładowy aplet akcji
powinien porozumiewać się ze swoim serwerem cen akcji? Odpowiedź brzmi: to zależy.
Jeżeli można zagwarantować, że wszyscy potencjalni klienci posiadają jego obsługę, elegancja i moc RMI czynią
go idealnym wyborem. Brzmi to jednak tak, jak przyjmowanie, że wszyscy znajomi uwielbiają dowcipy na temat
Star Treka. Może to być prawda przy dokładnym wybieraniu przyjaciół (lub klientów), lecz generalnie nie zdarza
się to w prawdziwym świecie.
Kiedy RMI nie jest dostępny, dwukierunkowe możliwości połączenia przez port nie-HTTP sprawiają, że
wygląda ono atrakcyjnie. Niestety, ta dwukierunkowa komunikacja staje się nieistniejącą komunikacją, kiedy
aplet kończy się na firewallu.
Zawsze jest stary sposób, komunikacja HTTP. Jest ono proste w implementacji i pracuje na każdym kliencie z
obsługą Javy. A jeżeli można zagwarantować, że klient obsługuje JDK 1.1 (jest to łatwiejsze do
zagwarantowania, niż obsługa RMI przez klientów), można wykorzystać serializację obiektów.
Przypuszczalnie najlepszym rozwiązaniem jest wykorzystanie wszystkich rozwiązań. Java sprawia, że możliwe
jest połączenie technik komunikacji aplet-serwlet HTTP, nie-HTTP i RMI, umieszczając obsługę ich wszystkich
w jednej aplikacji. Dlaczego ktoś miałby chcieć to zrobić? Dlatego, że jest to poręczna technika, kiedy aplet chce
porozumieć się przy pomocy RMI lub protokołu nie-HTTP, lecz musi przełączyć się na HTTP, kiedy okaże się
to potrzebne (w przypadkach takich jak znalezienie się przed firewallem). Poprzez zastosowanie tego samego
serwera do obsługi wielu klientów, podstawowa logika serwera i stan serwera mogą być zebrane w jednym
miejscu. Kiedy środowisko jest pod kontrolą można usunąć jeden lub więcej z tych protokołów. Lecz czy nie jest
miło wiedzieć, że nie jest to konieczne?
Dla skomplikowanych aplikacji pracujących na serwerze aplikacji standardowym projektem jest udostępnienie
zdalnego obiektu RMI klientom RMI, urządzenia słuchającego na portach klientom portów a serwletu klientom
HTTP. Obiekty te wykorzystują wspólnie zbiór logicznych klas biznesowych w celu obsługi żądań klienta
(podobnie jak w restauracji z wynosem można zamawiać poprzez telefon, faks lub pocztę elektroniczną). Jednak
w pozostałej części niniejszego rozdziału zostanie to nieco uproszczone, i przedstawiona zostanie komunikacja
RMI, przez porty i HTTP obsługiwana przez pojedynczy serwlet. Jeden serwlet, wiele protokołów dostępu.

Serwer godziny
W celu prostego przedstawienia każdej z technik komunikacji, napisany zostanie aplet proszący serwer o
aktualną datę i godzinę. Aplet na początku wykorzystuje połączenie HTTP, później połączenie przez port nie-
HTTP, a w końcu połączenie RMI. Oczywiście, aplet może normalnie pobierać obecny czas z systemu, na
którym pracuje. Aby nadać temu przykładowi cień praktyczności, można przyjąć, że aplet potrzebuje dokładnego
znacznika czasu dla jakiegoś zdarzenia i nie może polegać na tym, że komputer klienta posiada prawidłowo
ustawiony zegar.

Aplet
W całym niniejszym podrozdziale wykorzystane będzie ten sam aplet. Szkielet kodu tego apletu,
ApletGodziny jest przedstawiony na przykładzie 10.1. W tym momencie aplet ten po prostu tworzy interfejs
użytkownika, na którym wyświetlane będą pobrane przez niego czasy, jak przedstawiono na rysunku 10-1. W
dalszej części tego rozdziału zaimplementowane zostaną metody pobierzDataHttpTekst(),
pobierzDataHttpObiekt(), pobierzDataPortTekst(), pobierzDataPortObiekt() i
pobierzDataRMIObiekt().
Rysunek 10.1.
Interfejs użytkownika apletu ApletGodziny
Proszę zauważyć, że przykłady w niniejszym rozdziale korzystają z kilku metod JDK 1.0, które zostały
zarzucone w JDK 1.1. Zostało to uczynione w celu poprawienie przenośności. Podczas kompilowania
przykładów w nowych JDK wyświetlone zostaną ostrzeżenia na temat zarzucenia, mogą one jednak zostać
bezpiecznie zignorowane.
Przykład 10.1.
ApletGodziny, bez ulepszeń
import java.applet.*;
import java.awt.*;
import java.io.*;
import java.util.*;

public class ApletGodziny extends Applet {

TextField httpTekst, httpObiekt, portTekst, portObiekt, RMIObiekt;


Button odswiez;

public void init() {


// Konstruowanie interfejsu użytkownika

setLayout(new BorderLayout());

// Po lewej stronie dodanie etykiet dla różnych mechanizmów komunikacji


Panel zachod = new Panel();
zachod.setLayout(new GridLayout(5, 1));
zachod.add(new Label("Tekst HTTP: ", Label.RIGHT));
zachod.add(new Label("Obiekt HTTP: ", Label.RIGHT));
zachod.add(new Label("Tekst portu: ", Label.RIGHT));
zachod.add(new Label("Obiekt portu: ", Label.RIGHT));
zachod.add(new Label("Obiekt RMI: ", Label.RIGHT));
add("Zachod", zachod);

// Po prawej utworzenie pól tekstowych wyświetlających otrzymane wartości


czasu
Panel centrum = new Panel();
centrum.setLayout(new GridLayout(5, 1));

httpTekst = new TextField();


httpTekst.setEditable(false);
centrum.add(httpTekst);

httpObiekt = new TextField();


httpObiekt.setEditable(false);
centrum.add(httpObiekt);

portTekst = new TextField();


portTekst.setEditable(false);
centrum.add(portTekst);

portObiekt = new TextField();


portObiekt.setEditable(false);
centrum.add(portObiekt);

RMIObiekt = new TextField();


RMIObiekt.setEditable(false);
centrum.add(RMIObiekt);

add("Centrum", centrum);

// Na dole utworzenie przycisku uaktualniającego czas


Panel poludnie = new Panel();
odswiez = new Button("Odswież");
poludnie.add(odswiez);
add("Poludnie", poludnie);
}

public void start() {


odswiez();
}

private void odswiez() {


// pobranie i wyświetlenie wartości czasu
httpTekst.setText(pobierzDataHttpTekst());
httpObiekt.setText(pobierzDataHttpObiekt());
portTekst.setText(pobierzDataPortTekst());
portObiekt.setText(pobierzDataPortObiekt());
RMIObiekt.setText(pobierzDataRMIObiekt());
}

private String pobierzDataHttpTekst() {


// Pobranie obecnego czasu przy pomocy opartego na tekście połączenia HTTP
return "niedostępny";
}

private String pobierzDataHttpObiekt() {


// Pobranie obecnego czasu przy pomocy opartego na obiektach połączenia HTTP
return "niedostępny";
}

private String pobierzDataPortTekst() {


// Pobranie obecnego czasu przy pomocy opartego na tekście połączenia z portem
//nie-HTTP
return "niedostępny";
}

private String pobierzDataPortObiekt() {


// Pobranie obecnego czasu przy pomocy opartego na obiektach połączenia z
portem
//nie-HTTP
return "niedostępny";
}

private String pobierzDataRMIObiekt() {


// Pobranie obecnego czasu przy pomocy komunikacji RMI
return "niedostępny";
}

public boolean obslugaWyjatek(Event zdarzenie) {


// Po przyciśnięciu przycisku "Odśwież" odświeżenie ekranu
// Wykorzystanie zdarzeń JDK 1.0 w celu zapewnienia maksymalnej przenośności
switch (zdarzenie.id) {
case Event.ACTION_EVENT:
if (zdarzenie.target == odswiez) {
odswiez();
return true;
}
}
return false;
}
}
Aby aplet ten był dostępny do pobrania przez przeglądarkę klienta, musi być umieszczony w macierzystym
katalogu dokumentów serwera, razem z plikiem HTML odwołującym się do niego. Kod HTML może wyglądać
następująco:
<HTML>
<HEAD><TITLE>Aplet godziny</TITLE></HEAD>
<BODY>
<CENTER><H1>Aplet godziny</H1></CENTER>
<CENTER><APPLET CODE=ApletGodziny CODEBASE=/ WIDTH=300 HEIGHT=180>
</APPLET></CENTER>
</BODY></HTML>
Parametr CODEBASE wskazuje na katalog (z perspektywy klienta), gdzie umieszczony jest plik klasy apletu.
Wartość parametru CODEBASE / oznacza, że pliki klasy zostaną pobrane z katalogu macierzystego dokumentów
dla domyślnej aplikacji WWW. Jeżeli parametr CODEBASE nie zostanie określony, jego domyślną wartością jest
katalog, w którym znajduje się plik HTML. Przyjmując, że plik HTML nosi nazwę godziny.html, aplet ten jest
dostępny pod adresem http://serwer:port/godziny.html, a klasa apletu zostanie pobrana z URL-a
http://serwer:port/ApletGodziny.class.

Oparta na tekście komunikacja HTTP


Na początku omówiona zostanie implementacja podejścia najpopularniejszego — oparta na tekście komunikacja
HTTP.

Serwlet
Aby aplet ApletGodziny mógł pobierać aktualny czas z serwera, musi on porozumiewać się z serwletem,
który zwraca aktualny czas. Przykład 10.2 przedstawia taki serwlet. Odpowiada on na wszystkie żądania POST i
GET tekstową reprezentacją aktualnego czasu.
Przykład 10.2.
Serwlet SerwletGodziny obsługujący prosty dostęp HTTP
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SerwletGodziny extends HttpServlet{

public Data getDate() {


return new Data();
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter out = odp.getWriter();
wyj.println(getDate().toString());
}
}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
doGet(zad, odp);
}
}
Powyższa klasa serwletu powinna zostać umieszczona w standardowym dla serwerów miejscu,
katalog_kontekstowy/WEB-INF/classes. Zakładając, że są one umieszczone w domyślnym kontekście, są
dostępne dla każdej przeglądarki WWW przy pomocy URL-a http://serwer:port/servlet/SerwletGodziny.

Powrót do apletu
W tym momencie, aby ApletGodziny mógł porozumiewać się z serwerem, musi zachowywać się tak, jak
przeglądarka i ustanowić połączenie HTTP z URL-em serwletu, jak to przedstawia implementacja
pobierzDataHttpTekst() w przykładzie 10.3.
Przykład 10.3.
ApletGodziny pobierający czas przy pomocy HTTP
import java.net.URL; // nowy dodatek
import com.oreilly.servlet.HttpMessage; // klasa wspierająca, przedstawiona dalej

private String pobierzDataHttpTekst() {


try {
// Tworzenie URL-a odnoszącego się do serwletu
URL url = new URL(getCodeBase(), "/servlet/SerwletGodziny");
// Tworzenie com.oreilly.servlet.HttpMessage w celu porozumienia się z tym
URL-em
HttpMessage wiad = new HttpMessage(url);

// Wysłanie wiadomości GET serwletowi, bez łańcucha zapytania


// Pobranie odpowiedzi jako łańcucha InputStream
InputStream in = wiad.sendGetMessage();

// Dołączenie InputStream do DataInputStream


DataInputStream wynik =
new DataInputStream(new BufferedInputStream(in));

// Odczytanie pierwszej linii odpowiedzi, która powinna być łańcuchową


// reprezentacją aktualnego czasu
String data = wynik.readLine();

// Zamknięcie InputStream
in.close();

// Zwrócenie odczytanego czasu


return data;
}
catch (Exception e) {
// Jeżeli nastąpił problem, wynik zapisany w System.out
// (zazwyczaj konsoli Javy) i zwrócenie null
e.printStackTrace();
return null;
}
}
Powyższa metoda odczytuje aktualny czas na serwerze przy pomocy opartego na tekście połączenia HTTP. Po
pierwsze, tworzy obiekt URL, który odwołuje się do serwletu SerwletGodziny pracującego na serwerze.
Komputer macierzysty serwera i port dla tego URL-a pochodzi z własnej metody apletu getCodeBase().
Gwarantuje to zgodność komputera i portu z danymi o miejscu, z którego został pobrany aplet. Następnie metoda
tworzy obiekt HttpMessage w celu porozumienia się z tym URL-em. Obiekt ten wykonuje całą ciężką pracę,
w tym ustanawianie połączenia. Aplet prosi go o wykonanie żądania GET do SerwletGodziny, po czym
odczytuje odpowiedź ze zwróconego InputStream.
Kod HttpMessage jest przedstawiony na przykładzie 10.4. Jest on dość swobodnie wymodelowany na
podstawie klasy ServletMessage autorstwa Roda McChesneya.
Przykład 10.4.
Klasa wspierająca HttpMessage
package com.oreilly.servlet;

import java.io.*;
import java.net.*;
import java.util.*;

public class HttpMessage {

URL servlet = null;


Hashtable headers = null;

public HttpMessage(URL servlet) {


this.servlet = servlet;
}

public InputStream sendGetMessage() throws IOException {


return sendGetMessage(null);
}

public InputStream sendGetMessage(Properties args) throws IOException {


String argString = ""; // domyślny

if (args != null) {
argString = "?" + toEncodedString(args);
}
URL url = new URL(servlet.toExternalForm() + argString);

// Wyłączenie buforowania
URLConnection con = url.openConnection();
con.setUseCaches(false);

// Wysłanie nagłówków
sendHeaders(con);
return con.getInputStream();
}

public InputStream sendPostMessage() throws IOException {


return sendPostMessage(null);
}

public InputStream sendPostMessage(Properties args) throws IOException {


String argString = ""; // default
if (args != null) {
argString = toEncodedString(args); // notice no "?"
}

URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania


con.setDoInput(true);
con.setDoOutput(true);

// Wyłączenie buforowania
con.setUseCaches(false);

// Ominięcie błędu w Netscape'ie


con.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");

// Wysłanie nagłówków
sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych


DataOutputStream out = new DataOutputStream(con.getOutputStream());
out.writeBytes(argString);
out.flush();
out.close();

return con.getInputStream();
}

public InputStream sendPostMessage(Serializable obj) throws IOException {


URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania


con.setDoInput(true);
con.setDoOutput(true);

// Wyłączenie buforowania
con.setUseCaches(false);

// Ustawienie typu zawartości jako application/x-java-serialized-object


con.setRequestProperty("Content-Type",
"application/x-java-serialized-object");

// Wysłanie nagłówków
sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych


ObjectOutputStream out = new ObjectOutputStream(con.getOutputStream());
out.writeObject(obj);
out.flush();
out.close();

return con.getInputStream();
}

public void setHeader(String name, String value) {


if (headers == null) {
headers = new Hashtable();
}
headers.put(name, value);
}

// Wysłanie zawartości tablicy asocjacyjnej nagłówków do serwera


private void sendHeaders(URLConnection con) {
if (headers != null) {
Enumeration enum = headers.keys();
while (enum.hasMoreElements()) {
String name = (String) enum.nextElement();
String value = (String) headers.get(name);
con.setRequestProperty(name, value);
}
}
}
public void setCookie(String name, String value) {
if (headers == null) {
headers = new Hashtable();
}
String existingCookies = (String) headers.get("Cookie");
if (existingCookies == null) {
setHeader("Cookie", name + "=" + value);
}
else {
setHeader("Cookie", existingCookies + "; " + name + "=" + value);
}
}

public void setAuthorization(String name, String password) {


String authorization = Base64Encoder.encode(name + ":" + password);
setHeader("Authorization", "Basic " + authorization);
}

private String toEncodedString(Properties args) {


StringBuffer buf = new StringBuffer();
Enumeration names = args.propertyNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String value = args.getProperty(name);
buf.append(URLEncoder.encode(name) + "=" + URLEncoder.encode(value));
if (names.hasMoreElements()) buf.append("&");
}
return buf.toString();
}
}
Można by się spodziewać, że klasa HttpMessage ustanawia połączenie przez zwykły port do serwera, po
czym przechodzi na HTTP. To podejście na pewno działałoby, nie jest jednak konieczne. Klasy wyższego
poziomu java.net.URL i java.net.URLConnection dostarczają już tej funkcjonalności we właściwy
sposób.
Poniżej przedstawiony jest krótki opis działania HttpMessage. Klasa ta jest zaprojektowana do komunikacji z
jednym tylko URL-em, podanym w konstruktorze. Może ona wysyłać do niego wielokrotne żądania GET i/lub
POST, ale zawsze łączy się tylko z jednym URL-em.
Kod wykorzystywany przez HttpMessage do wysyłania wiadomości GET jest stosunkowo prosty. Po
pierwsze, sendGetMessage() tworzy zakodowany w URL-u łańcuch zapytania z przekazanej listy
java.util.Properties. Dołącza ten łańcuch zapytania do zapisanego URL-a, tworząc nowy obiekt URL. W tym
momencie można by się zdecydować na wykorzystanie tego nowego obiektu (pod nazwą url) w celu
porozumienia się z serwerem. Wywołanie url.openStream() zwróciłoby łańcuch InputStream
zawierający odpowiedź. Jednak, niestety dla celów tego przykładu, domyślnie wszystkie połączenia wykonane
przy pomocy obiektu URL są buforowane. Nie jest to pożądane — zwrócony ma być czas aktualny, a nie czas
ostatniego żądania. Tak więc HttpMessage musi wyłączyć buforowanie8.
Klasa URL nie obsługuje bezpośrednio tej kontroli niskiego poziomu, tak więc HttpMessage pobiera
URLConnection obiektu URL i instruuje go, aby nie wykorzystywał buforowania. Ostatecznie,
HttpMessage zwraca InputStream obiektu URLConnection, który zawiera odpowiedź serwletu.
Kod wykorzystywany przez HttpMessage do wysłania żądania POST (sendPostMessage()) jest
podobny. Podstawową różnicą między nimi jest fakt, że ten drugi zapisuje przechowywaną w URL-u informacje
bezpośrednio w kodzie żądania. Jest to zgodne z protokołem wysłania informacji przez żądania POST. Inna
różnica to fakt, że HttpMessage obowiązkowo ustawia typ zawartości żądania na application/x-www-
form-urlencoded. Informuje to serwer, że zawartość POST zawiera informacje o parametrach.
Należy wspomnieć, że HttpMessage to klasa ogólnego przeznaczenia, służąca do komunikacji HTTP. Nie
musi ona być wykorzystywana przez aplety, ani też łączyć się z serwletami. Może z niej skorzystać każdy klient
Javy, który musi połączyć się z dowolnymi zasobami HTTP. Jest ona jednak dołączona do pakietu
com.oreilly.servlet, ponieważ często okazuje się przydatna w komunikacji aplet-serwlet.
Aby klasa HttpMessage mogła być wykorzystywana przez aplety, musi być udostępniona przez pobranie jej
równolegle do klas apletów. Oznacza to, że musi być ona umieszczona w odpowiednim miejscu w katalogu
macierzystym dokumentów serwera. Dla serwera Tomcat, pozycja ta to
katalog_macierzysty/webapps/ROOT/com/oreilly/servlet. Poleca się skopiowanie klasy z

8
Tak właściwie można pozostawić wyłączenie buforowania serwletowi, przez ustawienie nagłówka serwletu Pragma na
no-cache, Nie zaszkodzi jednak wyłączenie jej również w aplecie.
miejsca, w którym pakiet com.oreilly.servlet został oryginalnie zainstalowany (zazwyczaj
katalog_macierzysty/classes/com/oreilly/servlet).
Proszę zauważyć, że HttpMessage posiada kilka swoich metod, które pozwalają na wysyłanie nagłówków
żądań, cookies i podstawowych informacji uwierzytelniających do serwera jako części żądania. Metoda
setHeader(String nazwa, String wartość) dodaje do żądania nagłówek o odpowiedniej nazwie i
wartości. Jeżeli nagłówek został ustawiony wcześniej, nowa wartość zastępuje starą. Metoda setCookie
(String nazwa, String wartość) dodaje do żądania cookie o odpowiedniej nazwie i wartości.
Metoda ta jest szczególnie przydatna przy omijaniu ograniczeń śledzenia sesji dla apletów działających wewnątrz
Java Plug-In, jak omówiono w rozdziale 7, „Śledzenie sesji”. Ostatecznie, metoda setAuthorization
(String nazwa, String hasło) dodaje dane użytkownika do żądania, pozwalając apletowi na dostęp
do stron chronionych hasłem. Wykorzystuje klasę com.oreilly.servlet.Base64Encoder, która nie
jest przedstawiona w niniejszej książce, lecz jest dostępna pod adresem http://servlets.com. Dla wszystkich
powyższych metod, ustawienia pozostają niezmienione pomiędzy żądaniami, a wywołujący jest odpowiedzialny
za to, aby w nazwie i wartości nie występowały żadne niedozwolone znaki.
Aby nawiązać bezpieczne połączenie z apletu, należy przekazać HttpMessage URL rozpoczynający HTTPS.
Jest to tak łatwe! (Proszę tylko nie zapomnieć o tym, że serwer musi mieć włączoną obsługę HTTPS.) Własność
ta działa jednak jedynie w przypadku apletów, ponieważ przeglądarka, w której uruchomiono aplet może pomóc
w wynegocjowaniu połączenia HTTPS. W przypadku klienta nie będącego apletem konstruktor URL zwróci błąd
nieznanego protokołu HTTPS. Można to ominąć wykorzystując klasę
com.oreilly.servlet.HttpsMessage dostępną pod adresem http://www.servlets.com. Zawiera ona
samodzielną obsługę HTTPS autorstwa Matta Towersa. Artykuł JavaWorld opisujący jej działanie znajduje się
pod adresem http://www.javaworld.com/javaworlsd/javatips/jw-javatip96.html.
W tym momencie istnieje już działający aplet odczytujący aktualny czas z serwera przy pomocy opartej na
tekście komunikacji HTTP aplet-serwer. Po wypróbowaniu jego działania można zobaczyć, że data „Tekst
HTTP” jest wypełniona, podczas gdy inne są oznaczone jako „niedostępny”.

Oparta na obiektach komunikacja HTTP


Po kilku modyfikacjach, można dostosować ApletGodziny tak, aby pobierał aktualny czas jako
zserializowany obiekt Data.

Serwlet
W celu zachowania wstecznej kompatybilności, można zmienić SerwletGodziny tak, aby zwracał
zserializowany obiekt jedynie, gdy wykonane zostanie odpowiednie żądanie przez przekazanie parametru
format o wartości obiekt. Odpowiedni kod przedstawiony jest w przykładzie 10.5.
Przykład 10.5.
SerwletGodziny wykorzystujący HTTP w celu pracy na obiekcie
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SerwletGodziny extends HttpServlet{

public Data getDate() {


return new Data();
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
// Jeżeli klient przekaże "format=obiekt"
// Wysłanie Data jako obiektu zserializowanego
if ("obiekt".equals(zad.getParameter("format"))) {
ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());
wyj.writeObject(getDate());
}
// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha
else {
PrintWriter wyj = odp.getWriter();
wyj.println(getDate().toString());
}
}
public void doPost(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
doGet(zad, odp);
}
}
Jak widać na powyższym przykładzie, wysyłanie zserializowanego obiektu Javy jest stosunkowo proste.
Technika ta może być wykorzystana w celu wysłania wszystkich prymitywnych typów i/lub obiektów Javy, które
posiadają implementację interfejsu Serializable, włączając w to obiekty Vector, zawierające obiekty
Serializable. W jednym łańcuchu ObjectOutputStream może zostać zapisana duża ilość obiektów,
jeżeli klasa odbierająca obiekty odczytuje je w tej samej kolejności i nadaje im te same typy.
Można zauważyć, że serwlet nie ustawił typu zawartości odpowiedzi w celu wskazania, że jest ona
zserializowanym obiektem Javy. Powodem tego jest aktualny brak standardowych typów MIME
reprezentujących obiekty zserializowane. Jednak nie ma to znaczenia. Typ zawartości jest jedynie wskaźnikiem
dla klienta, jak ma on obsłużyć lub wyświetlić odpowiedź. Jeżeli aplet już przewiduje odbieranie konkretnego
zserializowanego obiektu Javy, wszystko działa bez zakłóceń. Jednak czasami warto jest zastosować własne typy
MIME (specyficzne dla danej aplikacji), tak aby serwlet mógł wskazać apletowi zawartość jego odpowiedzi.

Aplet
Kod apletu odczytującego zserializowany obiekt Data jest bardzo podobny do kodu odczytującego zwykły
tekst. Metoda pobierzDataHttpObiekt() jest przedstawiona na przykładzie 10.6.
Przykład 10.6.
ApletGodziny wykorzystujący HTTP do odczytania obiektu
private String pobierzDataHttpObiekt() {
try {
// Tworzenie URL-a odnoszącego się do serwletu
URL url = new URL(getCodeBase(), "/servlet/SerwletGodziny");

// Tworzenie com.oreilly.servlet.HttpMessage w celu porozumienia się z tym


URL-em
HttpMessage wiad = new HttpMessage(url);

// Tworzenie listy właściwości Properties w celu podania format=obiekt


Properties wlasc = new Properties();
wlasc.put("format", "obiekt");

// Wysłanie wiadomości GET serwletowi, przekazanie „wlasc” jako łańcucha


zapytania
// Pobranie odpowiedzi jako łańcucha ObjectInputStream
InputStream in = wiad.sendGetMessage(wlasc);
ObjectInputStream wynik = new ObjectInputStream(in);

// Odczytanie obiektu Data z potoku


Object obi = wynik.readObject();
Date data = (Date)obi;
in.close();

// Zwrócenie łańcuchowej reprezentacji Data


return data.toString();
}
catch (Exception e) {
// Jeżeli nastąpił problem, wynik zapisany w System.out
// (zazwyczaj konsoli Javy) i zwrócenie null
e.printStackTrace();
return null;
}
}
Występują dwie różnice pomiędzy tą konkretną metodą i metodą pobierzDataHttpTekst(). Po pierwsze,
metoda ta tworzy listę właściwości Properties w celu nadania parametrowi formatu wartości obiektu.
Działanie to ma na celu nakazanie serwletowi SerwletGodziny zwrócenia obiektu zserializowanego. Po
drugie, nowa metoda odczytuje zwróconą zawartość jako Obiekt, przy pomocy klasy ObjectInputStream
i jej metody readObject().
Jeżeli serializowana klasa nie jest częścią Core API Javy (czyli nie jest od razu dostępna apletowi), musi ona
również zostać udostępniona we właściwym miejscu w katalogu macierzystym dokumentów serwera. Aplet może
zawsze otrzymywać zserializowane zawartości obiektu, musi jednak pobrać plik jego klasy w celu pełnej jego
rekonstrukcji.
W tym momencie aplet może odczytywać aktualny czas przy pomocy zarówno opartej na tekście, jak i na
obiektach, komunikacji HTTP. Po wypróbowaniu przykładu (przy pomocy przeglądarki WWW lub przeglądarki
apletów obsługującej JDK 1.1) można dostrzec, że pola „Tekst HTTP” i „Obiekt HTTP” zostały wypełnione.

Przesyłanie zserializowanego obiektu lub pliku


Przed przejściem do następnych metod, należy opisać jeszcze jedną (dotychczas nieomówioną) metodę z klasy
HttpMessage — sendPostMessage(Serializable). Metoda ta pomaga apletowi wysłać
zserializowany obiekt do serwletu metodą POST. Taki transfer obiektu nie jest szczególnie użyteczny w
opisywanym tu przykładzie (i raczej do niego nie pasuje), lecz zostanie wspomniany, ponieważ może okazać się
użyteczny, gdyby aplet musiał wysłać skomplikowane struktury danych na serwer. Na przykład, aplet może
wysłać obiekt Date, obiekt Person lub tablicę HighScore zawierającą obiekty Date i Person. Aplet
może nawet wysłać dokument XML, jako dokument JDOM (http://jdom.org) lub jako zwykły plik. Proszę
jedynie pamiętać, że przy wysyłaniu pliku powinno wysłać się zawartość jako obiekt byte[], nie File,
ponieważ obiekt File przechowuje jedynie nazwę pliku, a nie jego zawartość. Przykład 10.7 zawiera kod
metody sendPostMessage(Serializable).
Przykład 10.7
Wysyłanie obiektu zserializowanego
// Wysyła zserializowany obiekt przy pomocy żądania POST.
// Ustawia typ zawartości na application/x-java-serialized-object

public InputStream sendPostMessage(Serializable obj) throws IOException {


URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania


con.setDoInput(true);
con.setDoOutput(true);

// Wyłączenie buforowania
con.setUseCaches(false);

// Ustawienie typu zawartości jako application/x-java-serialized-object


con.setRequestProperty("Content-Type",
"application/x-java-serialized-object");

// Wysłanie nagłówków
sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych


ObjectOutputStream wyj = new ObjectOutputStream(con.getOutputStream());
wyj.writeObject(obj);
wyj.flush();
out.close();

return con.getInputStream();
}
Aplet wykorzystuje sendPostMessage(Serializable) tak, jak wykorzystuje sendPostMessage
(Properties). Poniżej przedstawiony jest kod apletu wysyłający wszystkie napotkane wyjątki do serwletu9:
catch (Exception w) {
URL url = new URL(getCodeBase(), "/servlet/DziennikWyjatkow");
HttpMessage wiad = new HttpMessage(URL);
InputStream = wiad.sendPostMessage(e);
}
Tymczasem serwlet otrzymuje wyjątek Exception przez swoją metodę doPost(), w następujący sposób:
ObjectInputStream obiin = new ObjectInputStream(zad.getInputStream());
Object obi = obiin.readObject();
Exception w = (Exception) obi;
Serwlet może otrzymywać typ wysłanego obiektu jako podtyp (druga część typu zawartości). Proszę zauważyć,
że powyższa metoda sendPostMessage(Serializable) pobiera tylko jeden obiekt w danym czasie
oraz, że pobiera jedynie obiekty Serializable (to znaczy, żadnych prymitywnych typów).

Komunikacja przez port


Teraz opisane zostaną sposoby komunikacji apletu i serwletu przy pomocy połączeń portów nie-HTTP.

9
Zazwyczaj kiedy aplet wywoła nieobsługiwany wyjątek, przeglądarka zapisuje ścieżkę do wyjątku w konsoli Javy. Ten kod
zawiera aplet naprawdę wyrzucający wyjątek na serwer, gdzie może być przechowywany w dzienniku zdarzeń. W dzienniku
tym zapisywane są ścieżki, mogące być później przeglądane przez twórcę apletu
Serwlet
Rola serwletu w tej technice komunikacji ogranicza się do pasywnego słuchania. Z powodów bezpieczeństwa,
jedynie aplet może nawiązać połączenie przez zwykły port. Generalnie, serwlet musi być skonfigurowany do
rozpoczęcia nasłuchu począwszy od metody init(), a skończywszy na metodzie destroy(). Pomiędzy tymi
dwoma metodami, powinien utworzyć wątek obsługujący dla każdego otrzymanego połączenia z klientem.
W przypadku połączenia HTTP, te trudne w obsłudze szczegóły są zarządzane przez serwer WWW. Serwer ten
prowadzi nasłuch przychodzących żądań HTTP i odpowiednio je rozsyła, wywołując odpowiednio metody
serwletu service(), doGet() lub doPost(). Jednak kiedy serwlet nie wykorzystuje komunikacji HTTP,
serwer WWW nie może dostarczyć żadnej pomocy. Właściwie to serwlet działa jako swój własny serwer i w
związku z tym musi zarządzać połączeniami przez port samodzielnie.
Może to brzmieć nieco zastraszająco. Prawda jest taka, że można stworzyć superklasę serwletu, która przejmie
na siebie szczegóły związane z zarządzaniem połączeń przez port. Klasa ta, nazwana DaemonHttpServlet,
może być wykorzystana przez każdy serwlet, który ma zostać udostępniony poprzez połączenie przez port nie-
HTTP10.
DaemonHttpServlet rozpoczyna nasłuch żądań klienta w swojej metodzie init(), a kończy na metodzie
destroy(). W międzyczasie, dla każdego odebranego połączenia wywołuje abstrakcyjną metodę
handleClient(Socket). Metoda ta powinna zostać zaimplementowana przez każdy serwlet, który jest
podklasą DaemonHttpServlet.
Przykład 10.8 przedstawia sposób, w jaki SerwletGodziny wykorzystuje klasę DaemonHttpServlet i
implementuje handleClient() w celu stania się dostępnym przez połączenie przez port nie-HTTP.
Przykład 10.8.
Serwer HTTP
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.DaemonHttpServlet;

public class SerwletGodziny extends DaemonHttpServlet {

public Date getDate() {


return new Date();
}

public void init() throws ServletException {


// Jak poprzednio, nie ma potrzeby wywoływania super.init()
// z bezargumentowej metody init, trzeba jednak wywołać super.init(config) ze
// starszej metody init(ServletConfig). Szczegóły można znaleźć w rozdziale 3.
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
// Jeżeli klient przesłał "format=obiekt",
// to wysłanie daty jako obiektu zserializowanego
if ("obiekt".equals(zad.getParameter("format"))) {
ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());
wyj.writeObject(getDate());
}
// W przeciwnym wypadku wysłanie daty jako zwykłego łańcucha ASCII
else {
PrintWriter wyj = odp.getWriter();
wyj.println(getDate().toString());
}
}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
doGet(zad, odp);
}

10
Nazwa daemon (demon) został wprowadzona w odniesieniu do demonów Uniksa, programów działających w tle i
obsługujących konkretne zdarzenia. W jaki sposób otrzymały one nazwę demonów? Według słownika „New Hacker's
Dictionary”, początkowo pochodziło „ze znaczenia mitologicznego, później zracjonalizowanego do akronimu 'Disk And
Execution Monitor' (Monitor Dysku i Wykonania)”.
public void destroy() {
// W tym miejscu, inaczej niż poprzednio, po przejęciu destroy() należy
wywołać
// super.destroy()
super.destroy();
}

// Obsługa połączenia klienta przez port poprzez dodanie wątku PolaczenieGodziny


public void handleClient(Socket klient) {
new PolaczenieGodziny(this, klient).start();
}
}

class PolaczenieGodziny extends Thread {

SerwletGodziny serwlet;
Socket klient;

PolaczenieGodziny(SerwletGodziny serwlet, Socket klient) {


this.serwlet = serwlet;
this.klient = klient;
setPriority(NORM_PRIORITY - 1);
}

public void run() {


try {
// Odczytanie pierwszej linii wysłanej przez klienta, jako tekstu Latin-1
BufferedReader in = new BufferedReader(
new InputStreamReader(
klient.getInputStream(), "ISO-8859-1"));
String line = in.readLine();

// Jeżeli brzmiała ona „Obiekt”, zwrócenie Data jako obiektu


zserializowanego
if ("obiekt".equals(line)) {
ObjectOutputStream wyj =
new ObjectOutputStream(klient.getOutputStream());
wyj.writeObject(serwlet.getDate());
wyj.close();
}
// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha
else {
// Owinięcie PrintStream dookoła OutputStream portu Socket
PrintStream wyj = new PrintStream(klient.getOutputStream());
wyj.println(serwlet.getDate().toString());
wyj.close();
}

// Należy zapewnić zamknięcie połączenia


klient.close();
}
catch (IOException w) {
serwlet.log("Wyjątek IOException podczas obsługi żądania klienta", w);
}
catch (Exception w) {
serwlet.log("Wyjątek Exception podczas obsługi żądania klienta", w);
}
}
}
Klasa SerwletGodziny pozostała w większej części niezmieniona w porównaniu z poprzednią formą.
Główną różnicą polega na tym, że jest ona teraz rozszerzeniem klasy DaemonHttpServlet i wykorzystuje
metodę handleClient(Socket), która tworzy nowy wątek PolaczenieGodziny. Ten egzemplarz
PolaczenieGodziny ponosi odpowiedzialność za obsługę konkretnego połączenia przez port.
PolaczenieGodziny działa w sposób opisany poniżej. Kiedy zostaje utworzone, zapamiętuje odwołanie do
SerwletGodziny, tak więc może wywołać metodę serwletu getDate() i odwołanie do portu Socket, w
celu zapewnienia komunikacji z klientem. PolaczenieGodziny ustawia swój priorytet o jeden poziom niżej
od normalnego, aby wskazać, że połączenie to może zaczekać, kiedy inne wątki wykonują ważniejszą pracę.
Bezpośrednio po utworzeniu wątku PolaczenieGodziny, SerwletGodziny rozpoczyna wątek,
powodując wywołanie swojej metody run(). W tej metodzie, PolaczenieGodziny porozumiewa się z
klientem przy pomocy nienazwanego (ale na pewno nie będącego HTTP) protokołu. Rozpoczyna od odczytania
pierwszej linii wysłanej przez klienta. Jeżeli wartość tej linii wynosi obiekt, zwraca on aktualny czas jako
zserializowany obiekt Data. Jeżeli linia ta zawiera dowolną inną wartość, zwraca aktualny czas w postaci
zwykłego łańcucha. Kiedy kończy swoją działalność, zamyka połączenie.
Superklasa
Niskiego poziomu zarządzanie portem jest wykonywane przez klasę DaemonHttpServlet. Ogólnie rzecz
biorąc, klasa ta może być zastosowana bez modyfikacji, warto jest jednak zrozumieć jej wnętrze. Jaj kod
przedstawiony w przykładzie 10.9. Proszę zauważyć, że również ona jest napisana według Servlet API 2.0 w celu
zapewnienia wstecznej kompatybilności.
Przykład 10.9.
Superklasa DaemonHttpServlet
package com.oreilly.servlet;

import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public abstract class DaemonHttpServlet extends HttpServlet {

protected int DEFAULT_PORT = 1313; // nie określony ostatecznie


private Thread daemonThread;

public void init(ServletConfig config) throws ServletException {


super.init(config);

// rozpoczęcie wątku demona


try {
daemonThread = new Daemon(this);
daemonThread.start();
}
catch (Exception e) {
log("Problem starting socket server daemon thread" +
e.getClass().getName() + ": " + e.getMessage());
}
}

// Zwraca port połączenia, na którym ten serwlet będzie nasłuchiwał


// Serwlet może określić port na trzy sposoby — przy pomocy parametru init
//socketPort, przez określenie zmiennej DEFAULT_PORT przed wywołaniem super.init
(),
//lub przez przejecie implementacji tej metody
protected int getSocketPort() {
try { return Integer.parseInt(getInitParameter("socketPort")); }
catch (NumberFormatException e) { return DEFAULT_PORT; }
}

abstract public void handleClient(Socket client);

public void destroy() {


try {
daemonThread.stop();
daemonThread = null;
}
catch (Exception e) {
log("Problem stopping server socket daemon thread: " +
e.getClass().getName() + ": " + e.getMessage());
}
}
}

// Praca ta wykonywana jest przez klasę pomocniczą tak, aby podklasy


DaemonHttpServlet
// może bez problemów zdefiniować swoją własną metodę run()

class Daemon extends Thread {

private ServerSocket serverSocket;


private DaemonHttpServlet servlet;

public Daemon(DaemonHttpServlet servlet) {


this.servlet = servlet;
}

public void run() {


try {
// Stworzenie portu serwera akceptującego połączenia
serverSocket = new ServerSocket(servlet.getSocketPort());
}
catch (Exception e) {
servlet.log("Problem establishing server socket: " +
e.getClass().getName() + ": " + e.getMessage());
return;
}

try {
while (true) {
// Po otrzymaniu połączenia, wywołanie metody handleClient() serwera.
// Proszę zauważyć, że metoda ta jest blokująca. Na serwlet spada
// odpowiedzialność utworzenia wątku obsługującego długo trwające
połączenia.
try {
servlet.handleClient(serverSocket.accept());
}
catch (IOException ioe) {
servlet.log("Problem accepting client's socket connection: " +
ioe.getClass().getName() + ": " + ioe.getMessage());
}
}
}
catch (ThreadDeath e) {
// Kiedy wątek zostaje zabity, zamknięcie portu serwera
try {
serverSocket.close();
}
catch (IOException ioe) {
servlet.log("Problem closing server socket: " +
ioe.getClass().getName() + ": " + ioe.getMessage());
}
}
}
}
Metoda init() DaemonHttpServlet tworzy i rozpoczyna nowy wątek Daemon, który odpowiada za
nasłuchiwanie połączeń przychodzących. Metoda destroy() zatrzymuje wątek. Sprawia to że imperatywem
staje się, żeby każdy serwlet będący podklasą DaemonHttpServlet wywoływał super.init() i
super.destroy(), jeżeli serwlet ten wykorzystuje własne metody init() i destroy(). (Serwlet
napisany według Servlet API 2.1 i wykorzystujący bez argumentów init() nie musi wywoływać
super.init(), lecz musi wywołać super.destroy().)
Wątek Daemon rozpoczyna działalność utworzeniem ServerSocket w celu nasłuchiwania na konkretnym
porcie połączenia. Numer portu jest określany przez wywołanie metody serwletu getSocketPort(). Wartość
przez nią zwracana to wartość parametru inicjacji SocketPort, lub, w wypadku nieistnienia tego parametru,
obecna wartość zmiennej DEFAULT_PORT. Serwlet może zdecydować się na przejęcie implementacji
getSocketPort, jeżeli jest to konieczne.
Po utworzeniu ServerSocket, wątek Daemon czeka na nadchodzące żądania przy pomocy wywołania
serverSocket.accept(). Metoda jest blokująca — zatrzymuje wykonywanie tego wątku do czasu
połączenia się przez klienta. Kiedy się to dzieje, metoda accept() zwraca obiekt Socket, który wątek
Daemon natychmiast przekazuje do metody serwletu handleClient(). Ta metoda zazwyczaj tworzy wątek
obsługujący i natychmiast powraca, powodując przejście wątku Daemon w stan gotowości do przyjęcia
następnego połączenia.
Oczyszczenie portu jest tak samo ważne, jak jego konfiguracja. Należy upewnić się, że port połączenia działa tak
długo jak serwlet, ale nie dłużej. W tym momencie metoda destroy() DaemonHttpServlet wywołuje
metodę stop() wątku Daemon. Wywołanie to nie zatrzymuje jednak natychmiast wątku Daemon. Powoduje
jedynie wystąpienie wyjątku ThreadDeath wątku Daemon w jego aktualnym miejscu wykonania. Wątek
Daemon przejmuje ten wyjątek i zamyka port połączenia.
Przy stworzeniu serwletu działającego jako serwer nie-HTTP mogą wystąpić dwa problemy. Po pierwsze,
jedynie jeden serwlet w jednym czasie może nasłuchiwać na konkretnym porcie. Sprawia to, że absolutnie
konieczne staje się, aby każdy serwlet-demon wybierał swój własny port połączenia — poprzez ustawienie
swojego parametru inicjacji socketPort, ustawiając zmienną DEFAULT_PORT przed wywołaniem
super.init(config) lub bezpośrednim przejęciem getSocketPort(). Po drugie, serwlet-demon musi
być załadowany na serwer i wywołana musi być jego metoda init(), zanim będzie on mógł przyjmować
przychodzące połączenia nie-HTTP. W związku z tym należy nakazać serwerowi ładowanie go podczas startu,
lub upewnić się, że jest on zawsze dostępny przez HTTP, zanim można będzie uzyskać do niego dostęp
bezpośredni.
Aplet
Kod apletu łączącego się z serwletem przy pomocy komunikacji nie-HTTP, przede wszystkim metody
pobierzDataPortTekst() i pobierzDataPortObiekt(), jest przedstawiony w przykładzie 10.10.
Przykład 10.10.
ApletGodziny pobierający czas przy pomocy połączenia przez zwykły port.
import java.net.socket; // Nowy dodatek

static final int DEFAULT_PORT=1313; // Nowy dodatek


private int getSocketPort() {
try { return Integer.parseInt(getParameter("socketPort")); }
catch (NumberFormatException w) { return DEFAULT_PORT; }
}

private String pobierzDataPortTekst() {


InputStream in = null;
try {
// Ustanowienie połączenia przez port z serwletem
Socket port = new Socket(getCodeBase().getHost(), getSocketPort());

// wyświetlenie pustej linii, oznaczającej chęć pobrania czasu jako zwykłego


tekstu
PrintStream wyj = new PrintStream(port.getOutputStream());
wyj.println();
wyj.flush();

// Odczytanie pierwszej linii odpowiedzi


// Powinna ona zawierać aktualny czas
in = port.getInputStream();
DataInputStream wynik =
new DataInputStream(new BufferedInputStream(in));
String data = wynik.readLine();

// Zwrócenie otrzymanego łańcucha


return data;
}
catch (Exception w) {
// Jeżeli wystąpił problem, wyświetlenie w System.out
// (zazwyczaj konsoli Javy) i zwrócenie null
w.printStackTrace();
return null;
}
finally {
// Zawsze zamknięcie połączenia
// Poniższy kod jest wykonywany niezależnie od wykonania poprzednich działań
if (in != null) {
try { in.close(); }
catch (IOException ignored) { }
}
}
}

private String pobierzDataPortObiekt() {


InputStream in = null;
try {
// Ustanowienie połączenia przez port z serwletem
Socket port = new Socket(getCodeBase().getHost(), getSocketPort());

// Wyświetlenie linii "obiekt", wskazującej chęć pobrania Data jako obiektu


// zserializowanego
PrintStream wyj = new PrintStream(port.getOutputStream());
wyj.println("obiekt");
wyj.flush();

// Stworzenie ObjectInputStream odczytującego odpowiedź


in = port.getInputStream();
ObjectInputStream wynik =
new ObjectInputStream(new BufferedInputStream(in));

// Odczytanie obiektu i zapamiętania go jako Data


Object obi = wynik.readObject();
Date data = (Date)obi;

// Zwrócenie łańcuchowej reprezentacji otrzymanej Data


return data.toString();
}
catch (Exception w) {
// Jeżeli wystąpił problem, wyświetlenie w System.out
// (zazwyczaj konsoli Javy) i zwrócenie null
w.printStackTrace();
return null;
}
finally {
// Zawsze zamknięcie połączenia
// Poniższy kod jest wykonywany niezależnie od wykonania poprzednich działań
if (in != null) {
try { in.close(); }
catch (IOException ignored) { }
}
}
}
W obu powyższych metodach, aplet rozpoczyna działanie przez utworzenie obiektu Socket wykorzystywanego
do komunikacji z serwerem. Aby go utworzyć, musi znać nazwę komputera i numer portu, na którym nasłuchuje
serwlet. Określenie komputera jest proste — musi to być ten sam komputer, z którego został on pobrany,
dostępny przy pomocy wywołania getCodeBase().getHost(). Określenie portu jest trudniejsze, jako że
zależy wyłącznie od serwletu, z którym łączy się aplet. Aplet ten wykorzystuje metodę getSocketPort() w
celu określenia tego. Przedstawiona powyżej implementacja getSocketPort() zwraca wartość parametru
apletu socketPort lub (jeżeli parametr ten nie jest podany) wartość zmiennej DEFAULT_PORT.
Po ustanowieniu połączenia przez port, aplet wykorzystuje nienazwany protokół w celu porozumienia się z
serwerem. Protokół ten wymaga, aby aplet wysłał jedną linię w celu wskazania, czy aktualny czas wysyłany w
odpowiedzi miał formę tekstu, czy obiektu. Jeżeli linia ta zawiera słowo obiekt, aplet otrzymuje obiekt. Jeżeli
zawiera cokolwiek innego, otrzymuje zwykły tekst. Po wysłaniu tej linii, aplet może odczytać odpowiedź we
właściwy sposób.
Aplet i serwlet mogą kontynuować porozumiewanie się przy pomocy tego portu. Jest to jedna z głównych zalet
niestosowania komunikacji HTTP. Jednak w tym przypadku, aplet otrzymał pożądane informacje i może po
prostu zamknąć połączenie. Zamknięcie to jest wykonywane w bloku finally. Umieszczenie zamknięcia w
tym miejscu zapewnia koniec połączenia niezależnie od tego, czy try spowoduje dowolny wyjątek, czy nie.
Po dodaniu dwóch powyższych metod aplet jest niemal kompletny. Po uruchomieniu go w tym momencie,
wynikiem będzie wyświetlenie dat we wszystkich polach poza „Obiekt RMI”.

Komunikacja RMI
We wcześniejszej części tego rozdziału powiedziano, że jednym z powodów nie wykorzystywania komunikacji
RMI jest jej skomplikowanie. Chociaż jest to prawda, jest również prawdą, że przy pomocy innej superklasy
serwletu, kod wymagany, aby serwlet był dostępny przez komunikację RMI, może być aż śmiesznie prosty. Po
pierwsze, dokładnie opisany zostanie proces nadawania serwletowi właściwości obiektu zdalnego. Następnie, po
udowodnieniu prostoty tego działania wyjaśniona zostanie cała praca mająca miejsce w tle.

Serwlet
Wszystkie obiekty zdalne RMI muszą wykorzystywać specyficzny interfejs, Interfejs ten wykonuje dwa działania
— deklaruje, które metody obiektu zdalnego mają zostać udostępnione zdalnym klientom, oraz rozszerza
interfejs Remote w celu wskazania, że jest to interfejs obiektu zdalnego. W przypadku przykładowego serwletu
SerwletGodziny, można stworzyć interfejs SerwerGodziny przedstawiony w przykładzie 10.11.
Przykład 10.11. Interfejs SerwerGodziny
import java.util.Date;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface SerwerGodziny extends Remote {


public Date getDate() throws RemoteException;
}
Powyższy interfejs deklaruje udostępnianie przez SerwletGodziny klientom zdalnym metody getDate().
Proszę zauważyć, że podpis getDate() został nieco zmieniony — wywołuje ona teraz RemoteException. Dla
każdej metody udostępnionej przez RMI musi być zadeklarowane wywołanie wyjątku. Chociaż sama metoda nie
musi go wywoływać, może on być wywołany przez system w celu wskazania błędu w usługach sieciowych.
Kod SerwletGodziny pozostaje w przeważającej części niezmieniony w porównaniu ze swoją wersją
oryginalną. Właściwie jedyne zmiany to aktualna implementacja SerwerGodziny i rozszerzanie
com.oreilly.servlet.RemoteHttpServlet, superklasy pozwalającej temu serwletowi na pozostanie
niezmienionym. Serwlet wykorzystuje również metodę destroy(), która wywołuje super.destroy().
Prawdą jest, że metoda ta jest właściwie bezużyteczna w niniejszym przykładzie, ale przypomina o tym, że każda
metoda destroy() zaimplementowana w zdalnym serwlecie musi wywoływać super.destroy() w celu
dostarczenia metodzie destroy() obiektu RemoteHttpServlet możliwości zakończenia komunikacji
RMI. Przykład 10.12 przedstawia nowy kod SerwletGodziny.
Przykład 10.12.
SerwletGodziny obsługuje teraz dostęp RMI
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet; // Nowy dodatek

public class SerwletGodziny extends RemoteDaemonHttpServlet // Nowy dodatek


implements SerwerGodziny { // Nowy dodatek

// Pojedyncza metoda z SerwerGodziny


// Uwaga: klauzula na temat wywoływania nie jest tu potrzebna
public Data getDate() {
return new Data();
}

public void init(ServletConfig konfig) throws ServletException {


super.init(konfig);
// Miejsce na dodatkowy kod
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
// Jeżeli klient prześle "format=obiekt", to wysłanie Data jako obiektu
// zserializowanego
if ("obiect".equals(zad.getParameter("format"))) {
ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());
wyj.writeObject(getDate());
}
// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha ASCII
else {
PrintWriter wyj = odp.getWriter();
wyj.println(getDate().toString());
}
}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
doGet(zad, odp);
}

public void destroy() {


// Po przejęciu destroy() należy wywołać super.destroy()
super.destroy();
}

W taki właśnie sposób tworzy się zdalny obiekt serwletu. Kompilacja zdalnego obiektu serwletu jest taka sama,
jak dla każdego innego serwletu, z jednym dodatkowym krokiem. Po skompilowaniu kodu źródłowego serwletu
należy skompilować klasę serwletu przy pomocy kompilatora RMI rmic. Kompilator RMI pobiera plik klasy
obiektu zdalnego i tworzy szkieletową i końcową wersję klasy. Klasy te pracują w tle w celu umożliwienia
komunikacji RMI. Nie należy przejmować się szczegółami, trzeba jednak wiedzieć, że końcówka pomaga
klientowi w wywoływaniu metod na zdalnym obiekcie, a szkielet pomaga serwerowi w obsłudze tych wywołań.
Wykorzystywanie rmic jest podobne do stosowania javac. W przypadku tego przykładu można skompilować
SerwletGodziny przy pomocy następującego polecenia:
% rmic SerwletGodziny
Proszę zauważyć, że rmic należy dostarczyć nazwę klasy Javy — nie pliku — do skompilowania. W związku z
tym, jeżeli kompilowany serwlet jest częścią pakietu powinien zostać podany rmic jako
nazwa.pakietu.NazwaSerwletu. Program rmic może pobierać ścieżkę klasy do poszukiwania przy
pomocy parametru –classpath, a także katalog docelowy dla plików szkieletu i końcówki przy pomocy
parametru –d.
Po wykonaniu powyższego polecenia rmic, powinny ukazać się dwa nowe pliki klas —
SerwletGodziny_Stub.class (końcówka) i SerwletGodziny_skel.class (szkielet). Poniżej
przedstawione zostanie ich zastosowanie. Po pierwsze, należy pamiętać, że nie jest konieczne ponowne
uruchamianie kompilatora RMI za każdym razem, kiedy modyfikowany jest kod zdalnego serwletu. Jest tak,
ponieważ klasy szkieletu i końcówki są wbudowane w interfejs serwletu, a nie w implementację tego interfejsu.
W związku z tym, należy zregenerować go jedynie po modyfikacji interfejsu SerwerGodziny (lub interfejsu
do niego równoważnego).
Teraz następuje ostatni krok w tworzeniu zdalnego serwletu — kopiowanie kilku plików klas do katalogu
macierzystego dokumentów serwera, z którego to miejsca mogą być one pobrane przez aplet. Pobrane muszą
zostać dwa pliki klas — końcówka SerwletGodziny_Stub.class i zdalny interfejs klasy SerwerGodziny.class.
Klient (w tym przypadku aplet) potrzebuje końcówki w celu wykonania swojej części komunikacji RMI, a sama
końcówka wykorzystuje interfejs zdalnej klasy. Proszę pamiętać, że serwlet również wykorzystuje te klasy, więc
należy skopiować je do katalogu macierzystego dokumentów serwera oraz pozostawić je w ścieżce klas
serwera11. Rysunek 10.2 przedstawia pozycje wszystkich plików serwera.

Rysunek 10.2.
To jest koniec! Jeżeli postępuje się według powyższych instrukcji możliwe jest utworzenie w krótkim czasie
działającego zdalnego serwletu. Poniżej przedstawiona jest klasa RemoteHttpServlet i opisane działania
zachodzące w tle.

Superklasa
Obiekt zdalny musi wykonać dwa działania, aby przygotować się do komunikacji RMI — musi się
wyeksportować i zarejestrować. Kiedy obiekt zdalny eksportuje siebie, zaczyna nasłuchiwanie na porcie,
oczekując na przychodzące wywołania metod. Kiedy obiekt zdalny rejestruje się, podaje serwerowi
rejestrującemu swoją nazwę i numer portu tak, aby klient mógł go zlokalizować (a zwłaszcza poznać numer jego
portu) i porozumieć się z nim. Te dwa zadania są obsługiwane przez klasę RemoteHttpServlet,
przedstawioną w przykładzie 10.13.

11
Zarządzanie wieloma plikami klas może stać się poważnym utrudnieniem przy tworzeniu oprogramowania. W systemie
Uniksowym można wykorzystać łącza symboliczne w celu ułatwienia tego zadania. W każdym systemie, można zastosować
bardziej ogólne rozwiązanie — zmienić ścieżkę klas serwera tak, aby zawierała ona
katalog_macierzysty/webapps/ROOT/classes. (Proszę zauważyć, że nie jest to WEB_INF/classes). W tym miejscu należy
pozostawić klasy końcówki i szkieletu. Następnie serwer może je odnaleźć w nowej ścieżce klas, a baza kodu apletu może
zostać ustawiona na /classes, aby aplet również mógł je odnaleźć.
Przykład 10.13.
Superklasa RemoteHttpServlet
package com.oreilly.servlet;

import java.io.*;
import java.net.*;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public abstract class RemoteHttpServlet extends HttpServlet


implements Remote {
protected Registry registry;

public void init(ServletConfig config) throws ServletException {


super.init(config);
try {
// Eksportowanie siebie
UnicastRemoteObject.exportObject(this);
// Rejestrowanie siebie
bind();
}
catch (RemoteException e) {
log("Problem binding to RMI registry: " + e.getMessage());
}
}

public void destroy() {


// Wyrejestrowanie siebie
unbind();
}

// Zwraca nazwę pod którą obiekt zostanie zarejestrowany


protected String getRegistryName() {
// Pierwszą wybieraną nazwą jest parametr inicjacji "registryName"
String name = getInitParameter("registryName");
if (name != null) return name;

// Drugim wyborem jest nazwa tej klasy


return this.getClass().getName();
}

// Zwraca port na którym nasłuchuje (lub powinien nasłuchiwać) serwer


rejestrujący
protected int getRegistryPort() {
// Pierwszym wybieranym portem jest parametr inicjacji "registryPort"
try { return Integer.parseInt(getInitParameter("registryPort")); }

// Drugim wyborem jest domyślny port rejestracji (1099)


catch (NumberFormatException e) { return Registry.REGISTRY_PORT; }
}

protected void bind() {


// Próba znalezienia właściwego działającego już rejestru
try {
registry = LocateRegistry.getRegistry(getRegistryPort());
registry.list(); // Verify it's alive and well
}
catch (Exception e) {
// Couldn't get a valid registry
registry = null;
}

// Jeżeli go nie ma, należy go utworzyć


// (Ekwiwalent uruchomienia "rmiregistry")
if (registry == null) {
try {
registry = LocateRegistry.createRegistry(getRegistryPort());
}
catch (Exception e) {
log("Could not get or create RMI registry on port " +
getRegistryPort() + ": " + e.getMessage());
return;
}
}
// Jeżeli program dotarł do tego miejsca, to istnieje prawidłowy rejestr
// Teraz należy zarejestrować ten egzemplarz serwletu w tym rejestrze
// „Rebind” podmienia inne obiekty wykorzystujące tą nazwę
try {
registry.rebind(getRegistryName(), this);
}
catch (Exception e) {
log("Could not bind to RMI registry: " + e.getMessage());
return;
}
}

protected void unbind() {


try {
if (registry != null) registry.unbind(getRegistryName());
}
catch (Exception e) {
log("Problem unbinding from RMI registry: " + e.getMessage());
}
}
}
Osoby, które wcześniej wykorzystywały lub czytały o RMI prawdopodobnie napotkały zdalne obiekty
rozszerzające klasę java.rmi.server.UnicastRemoteObject. Jest to standardowy — i właściwie
polecany — sposób tworzenia zdalnego obiektu. Jednak klasa RemoteHttpServlet nie jest rozszerzeniem
UnicastRemoteObject — rozszerza ona HttpServlet. Jak wiadomo, Java nie obsługuje
wielodziedziczenia. Oznacza to, że RemoteHttpServlet musi wybrać, czy jest rozszerzeniem
UnicastRemoteObject, czy HttpServlet — nawet, jeżeli potrzebuje funkcjonalności obu klas. Jest to
trudny wybór. Niezależnie od tego, której klasy nie rozszerza, musi po prostu reimplementować ją samodzielnie.
Ostatecznie rozszerzono HttpServlet, ponieważ łatwiej jest napisać ponownie funkcjonalność
UnicastRemoteObject niż HttpServlet.
To ponowne napisanie wymaga, aby klasa RemoteHttpServlet wykonywała dwa działania, których nie
musiałaby wykonywać, jeżeli byłaby rozszerzeniem UnicastRemoteObject. Musi ona zadeklarować, że
rozszerza interfejs Remote. Wszystkie obiekty zdalne muszą wykorzystywać ten interfejs, ale zazwyczaj
poprzez rozszerzanie UnicastRemoteObject, klasa otrzymuje to za darmo. Wysiłek związany z
samodzielnym wykonaniem tego działania nie jest duży, jako że interfejs Remote właściwie nie definiuje
żadnych metod. Obiekt deklaruje, że implementuje interfejs Remote, aby być traktowany jako obiekt zdalny.
Drugim działaniem, jakie musi wykonać RemoteHttpServlet jest ręczne wyeksportowanie siebie.
Zazwyczaj jest to wykonywane automatycznie przez konstruktora UnicastRemoteObject(). Wykonanie
tego bez konstruktora nie jest jednak problemem. Klasa UnicastRemoteObject posiada statyczną metodę
exportObject(Remote), którą każdy obiekt Remote może wykorzystać w celu wyeksportowania siebie.
RemoteHttpServlet wykorzystuje tę metodę i eksportuje siebie przy pomocy poniższej pojedynczej linii:
UnicastRemoteObject.exportObject(this);
Powyższe dwa kroki, implementacja Remote i eksportowanie siebie są wykonywane przez
RemoteHttpServlet, ponieważ nie jest ona rozszerzeniem UnicastRemoteObject12.
Pozostała część kodu RemoteHttpServlet wykonuje rejestrowanie i wyrejestrowanie siebie w rejestrze
RMI. Jak powiedziano wcześniej, serwer rejestrujący RMI działa jako miejsce, w którym klienty mogą
zlokalizować obiekty serwera. Zdalny obiekt (obiekt serwera) rejestruje siebie w rejestrze pod konkretną nazwą.
Klienty mogą więc wyszukać ten obiekt według nazwy w rejestrze. W takim razie, aby być dostępny dla
klientów, serwlet musi odnaleźć (lub stworzyć) serwer rejestrujący i zarejestrować się w tym serwerze pod
konkretną nazwą. W języku specjalistycznym nosi to nazwę dowiązania do rejestru. RemoteHttpServlet
wykonuje to dowiązanie przy pomocy swojej metody bind(), wywoływanej wewnątrz metody init().
Metoda bind() wykorzystuje dwie metody wspierające, getRegistryPort() i getRegistryName()
w celu określenia portu, na którym powinien działać serwlet i nazwy, pod którą powinien on zostać
zarejestrowany. W przypadku przykładowej implementacji, port jest określany przy pomocy parametru inicjacji
registryPort, lub posiada domyślną wartość 1099. Nazwa jest pobierana z parametru inicjacji
registryName lub posiada domyślną nazwę klasy serwletu — w tym przypadku SerwletGodziny.
12
Aby być całkowicie poprawnym, należy wykonać jeszcze dodatkowe działania. Według dokumentacji
java.rmi.remoteUnicastRemoteObject, „Jeżeli UnicastRemoteObject nie jest rozszerzana, klasa
implementująca musi przejąć odpowiedzialność za poprawną semantykę metod hashCode, equals i toString
dziedziczonych z klasy Object tak, aby zachowywały się one poprawnie w przypadku obiektów zdalnych”. Według
dokumentacji java.rmi.remoteRef, „Metody te powinny zagwarantować, że końcówki obiektów zdalnych będą
posiadać ten sam kod asocjacyjny (w celu obsługi obiektów zdalnych jako kluczy w tablicach asocjacyjnych).”
Implementacja mechanizmów obsługujących tę gwarancję jest stosunkowo trudna i, według autorów, nie zawsze konieczna
do komunikacji aplet-serwlet; w związku z tym podjęto ryzyko wykorzystania RemoteHttpServlet
Poniżej opisana jest dokładniej metoda bind(). Rozpoczyna ona działanie przez wykorzystanie następującego
kodu, próbując odnaleźć właściwy działający już rejestr.
registry = LocateRegistry.getRegistry(getRegistryPort());
registry.list();
Pierwsza linia próbuje odnaleźć rejestr pracujący na danym porcie. Druga prosi rejestr o listę jego obecnie
zarejestrowanych obiektów. Jeżeli oba wywołania zakończą się sukcesem, wynikiem będzie prawidłowy rejestr.
Jeżeli dowolne wywołanie spowoduje wyjątek Exception, metoda bind() dowiaduje się, że nie istnieje
prawidłowy rejestr i samodzielnie go tworzy. Wykonuje to działanie przy pomocy następującej linii kodu:
registry = LocateRegistry.createRegistry(getRegistryPort());
Po wykonaniu tych działań metoda bind() powinna znaleźć lub utworzyć serwer rejestrujący. Jeżeli pobranie
rejestru nie powiodło się, a nie powiodło się też jego tworzenie, powraca ona i serwlet pozostaje
niezarejestrowany. Następnie RemoteHttpServlet dowiązuje się do rejestru przy pomocy poniższej linii
kodu:
registry.rebind(getRegistryName(), this);
Wykorzystuje ona metodę Registry.rebind() zamiast metody Registry.bind() aby wskazać, że to
dowiązanie powinno zastąpić wszystkie poprzednie dowiązania wykorzystujące tę nazwę. Dowiązanie to trwa
dopóki serwlet nie zostaje zniszczony, w którym to miejscu metoda destroy() RemoteHttpServlet
wywołuje jego metodę unbind(). Kod wykorzystywany przez unbind() w celu zniszczenia dowiązania do
rejestru jest niezwykle prosty:
if (registry != null) registry.unbind(getRegistryName());
Po prostu prosi ona rejestr o zniszczenie dowiązania do swojej nazwy.

Wskazówka!!!!

W którym miejscu uruchomić rejestr?


Szeroko akceptowanym sposobem uruchamiania serwera rejestrującego RMI jest samodzielny program Javy
rmiregistry. Polecane jest jednak opuszczenie rmiregistry i pozwolenie RemoteHttpServlet na
samodzielne utworzenie rejestru. Jest to łatwiejsze i bardziej wydajne. Pierwszy serwlet korzystający z
rejestru może go utworzyć. Z powodu uruchamiania rejestru wewnątrz serwletu, rejestr pracuje przy pomocy
tej samej JVM, co serwlet. Pozwala to na wykorzystanie tylko jednej wirtualnej maszyny Javy dla serwera,
wszystkich jego serwletów (obiektów zdalnych) i rejestru. Niektóre serwery aplikacji na starcie uruchamiają
rejestr wewnątrz własnej JVM.

Możliwe jest również całkowite ominięcie uruchomienia rejestru, przy pomocy serwletów. Klient może
połączyć się ze specjalnym serwletem przy pomocy HTTP i zażądać danego obiektu zdalnego (przy pomocy
parametru), a serwlet może zwrócić mu dane odpowiedzi jako zserializowaną końcówkę obiektu zdalnego —
podobnie jak zwraca te dane rejestr. Zastosowanie serwletu zamiast rmiregistry pozwala serwletowi na
zaimplementowanie polityki bezpieczeństwa ograniczającej prawa przeglądania obiektów, pozwala
serwletowi na nadanie każdemu klientowi odwołania do różnych obiektów zdalnych oraz (ponieważ serwlet
może tworzyć zdalny obiekt w ramach obsługi żądań) powoduje, że obiekt zdalny jest dostępny nawet przed
jego utworzeniem.

Proszę zauważyć, że zdalny serwlet musi zostać załadowany na swój serwer, a jego metoda init() musi zostać
wywołana, zanim będzie on gotowy do komunikacji RMI. Podobnie jak w przypadku serwletu-demona, należy
nakazać serwerowi ładowanie go przy uruchamianiu lub upewnić się, że jest on zawsze dostępny przez HTTP
zanim będzie dostępny bezpośrednio.

Aplet
Teraz należy odwrócić uwagę od serwera i skupić się na kliencie. Kod wykorzystywany przez ApletGodziny
do wywołania metody GetDate() nowego SerwletGodziny() jest przedstawiony w przykładzie 10.14.
Przykład 10.14.
ApletGodziny pobierający czas przy pomocy RMI
import java.rmi.*; // Nowy dodatek
import java.rmi.registry.*; // Nowy dodatek

private String getRegistryHost() {


return getCodeBase().getHost();
}

private int getRegistryPort() {


try { return Integer.parseInt(getParameter("registryPort")); }
catch (NumberFormatException w) { return Registry.REGISTRY_PORT; }
}

private String getRegistryName() {


String nazwa = getParameter("registryName");
if (nazwa == null) {
nazwa = "SerwletGodziny"; // domyślnie
}
return nazwa;
}

private String pobierzDataRMIObiekt() {


try {
Registry rejestr =
LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort());
SerwerGodziny godziny =
(SerwerGodziny)rejestr.lookup(getRegistryName());
return godziny.getDate().toString();
}
catch (ClassCastException w) {
System.out.println("Otrzymany obiekt to nie SerwerGodziny: " +
w.getMessage());
}
catch (NotBoundException w) {
System.out.println(getRegistryName() + " nie dowiązany: " + w.getMessage());
}
catch (RemoteException w) {
System.out.println("Wyjątek zdalny: " + w.getMessage());
}
catch (Exception w) {
System.out.println("Problem z pobraniem odwołania do SerwerGOdziny: " +
w.getClass().getName() + ": " + w.getMessage());
}
return null;
}
Pierwsze trzy metody to metody wspomagające. getRegistryHost() zwraca komputer, na którym powinien
pracować serwer rejestrujący. Musi to zawsze być komputer, z którego pobrany został aplet.
getRegistryPort() zwraca port, na którym powinien nasłuchiwać serwer rejestrujący. Zazwyczaj jest to
domyślny port rejestru 1099, chociaż może on zostać przejęty przy pomocy parametru registryPort.
getRegistryName() zwraca nazwę, pod którą serwlet powinien zostać zarejestrowany. Jej domyślna
wartość to SerwletGodziny, lecz może ona zostać przejęta przy pomocy parametru registryName.
Właściwe poszukiwanie zdalnego obiektu serwletu i wywołanie jego metody getDate() następuje w
poniższych trzech liniach metody pobierzDataRMIObiekt():
Registry rejestr =
LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort());
SerwerGodziny godziny =
(SerwerGodziny)rejestr.lookup(getRegistryName());
return godziny.getDate().toString();
Pierwsza linia odnajduje rejestr dla danego komputera i danego portu. Druga linia wykorzystuje ten rejestr do
wyszukania zdalnego obiektu zarejestrowanego pod daną nazwą, wysyłając ten obiekt do obiektu
SerwerGodziny. Trzecia linia wywołuje metodę getDate() tego obiektu i otrzymuje w odpowiedzi
zserializowany obiekt Data. Następnie, w tej samej linii, zwraca reprezentację łańcuchową (String) tego
obiektu Data.
Pozostała część metody pobierzDataRMIObiekt() obsługuje mogące wystąpić wyjątki. Przejmuje ona
wyjątek ClassCastException, jeżeli wywoływany obiekt to nie SerwerGodziny,
NotBoundException jeżeli rejestr nie posiada obiektu zarejestrowanego pod tą nazwą, a
RemoteException w przypadku, gdy występuje błąd w usługach sieciowych. Przejmuje także ogólny wyjątek
Exception, w przypadku wystąpienia innych problemów.
Można się zastanawiać, dlaczego ApletGodziny wykorzystuje Registry.lookup(String) zamiast
java.rmi.Naming.lookup(String) do pobrania swojego odwołania do zdalnego serwletu. Nie ma ku
temu żadnego powodu — to tylko sprawa osobistego gustu. Aplet ten pracowałby podobnie po wymianie dwóch
pierwszych linii pobierzDataRMIObiekt() na następujący kod:
SerwerGodziny godziny =
(SerwerGodziny)Naming.lookup("rmi:// + getRegistryHost() +
":" + getRegistryPort() +
"/" + getRegistryName());
To jest koniec piątej i ostatniej metody ApletGodziny. Proszę uruchomić teraz aplet. Czy wszystkie pola są
wypełnione? Nie powinny być. Pola odpowiadające komunikacji przez port powinny być puste. Proszę
przypomnieć sobie, że podczas zmiany SerwletGodziny na obiekt zdalny, usunięta została obsługa
komunikacji przez port. Zostanie ona ponownie umieszczona poniżej.

W pełni funkcjonalny serwlet


Potrzebny jest pojedynczy serwlet dostępny przez komunikację HTTP, nie-HTTP i RMI. Serwlet tego typu może
być rozszerzeniem nowej superklasy, com.oreilly.servlet.RemoteDaemonHttpServlet,
wykorzystującym możliwości opisane dotychczas dla RemoteHttpServlet i DaemonHttpServlet.
Poniżej przedstawiony jest kod deklarujący ten w pełni funkcjonalny serwlet:
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet;

public class SerwletGodziny extends RemoteDaemonHttpServlet


implements SerwerGodziny {

public Data getDate() {


return new Data();
}

// Pozostała część bez zmian


Powyższy kod jest niemal identyczny jak przykład 10.8. Jest to po prostu tamten przykład napisany ponownie,
aby zadeklarować, że rozszerza on RemoteDaemonHttpServlet i implementuje SerwerGodziny.
Kod superklasy RemoteDaemonHttpServlet również jest niemal identyczny jak RemoteHttpServlet.
Występują jedynie dwie zmiany — rozszerza on DaemonHttpServlet zamiast HttpServlet, a jego
metoda destroy() jako pierwsza wywołuje super.destroy():
package com.oreilly.servlet;

import java.io.*;
import java.net.*;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public abstract class RemoteDaemonHttpServlet extends DaemonHttpServlet


implements Remote {
public void destroy() {
super.destroy();
unbind();
}

// Pozostała część bez zmian


W tym momencie ApletGodziny może połączyć się z poprawionym zdalnym serwerem-demonem i utworzyć
pełny wynik przedstawiony wcześniej na rysunku 10.1.

Serwer pogawędek
Przykład serwera godziny z poprzedniego podrozdziału przedstawiał wady i zalety stosowania wszystkich trzech
technik komunikacji aplet-serwer. Nie korzystał jednak z zalet utrzymywania połączenia w przypadku połączenia
przez port. Nie przedstawiał również prostoty komunikacji RMI i elegancji wywołań wstecznych RMI (w których
serwlet może wywoływać metody apletu). Nie przedstawiał poważnego powodu, dla którego jeden serwlet
powinien obsługiwać wszystkie techniki komunikacji — nie istniał powód dla którego należało utrzymywać
skomplikowany kod w jednym miejscu. Tak więc przed zakończeniem dyskusji na temat komunikacji aplet-
serwlet przedstawiony zostanie przykład bardziej skomplikowany — serwer pogawędek (chat),
zaimplementowany jako serwlet, który obsługuje klientów łączących się przez HTTP, porty nie-HTTP i RMI.
Ten serwer pogawędek zostanie utworzony przy pomocy wszystkich trzech technik komunikacji, aby mógł
korzystać z zalet najlepszego, najbardziej wydajnego rozwiązania dla każdego klienta. Na przykład, kiedy klient
obsługuje RMI, serwlet może być traktowany jako obiekt zdalny, a (gdzie to możliwe) może traktować aplet
również jako zdalny obiekt. Kiedy klient nie obsługuje HTTP, ale obsługuje bezpośrednie połączenia przez port,
serwer pogawędek może skorzystać z trwałości portów i łączyć się z klientem przy pomocy protokołu portu nie
będącego HTTP. I, oczywiście, jeżeli inne techniki zawiodą, serwer pogawędek wykorzystać HTTP. Nie jest to
polecane, ponieważ HTTP jako protokół bezstanowy wymaga od klienta aktywności w celu dokonania
uaktualnień. Lecz dla wielu klientów HTTP jest jedynym możliwym rozwiązaniem.
Serwer pogawędek jest implementowany jako pojedyncza klasa z pojedynczym egzemplarzowaniem, ponieważ
posiada on dużą ilość związanych ze sobą danych i sporą ilość kodu, który musiałby być powtarzany. Podzielenie
go na trzy klasy, jedną dla każdego protokołu, wymagałoby nadmiernej komunikacji między nimi i trzykrotnego
powtarzania podstawowego kodu serwera pogawędek. Implementacja serwera pogawędek jako serwletu
dostarcza prostej metody udostępnienia jednego obiektu przy pomocy wszystkich trzech technik komunikacji.
Jako że jest on serwletem HTTP, posiada wbudowaną obsługę HTTP. A dzięki temu, że jest rozszerzeniem klasy
RemoteDaemonHttpServlet, może również łatwo uzyskać możliwość obsługi portów nie-HTTP i
komunikacji RMI.
Proszę zauważyć, że chociaż kod zostanie przedstawiony w całości, nie zostanie wyjaśniona każda pojedyncza
linia. Spowodowałoby to powiększenie niniejszego rozdziału poza rozsądny rozmiar, jeżeli się to już nie stało.
Tak więc zostaną wyjaśnione kwestie ściśle związane z komunikacją aplet-serwlet, a analiza kodu i zrozumienie
wszystkich szczegółów będą pozostawione Czytelnikowi.

Projekt
Rysunek 10.3 przedstawia aplet pogawędek w działaniu. Proszę zauważyć, że wykorzystuje on duży element
TextArea w celu wyświetlenia toczącej się konwersacji. Mały element TextArea poniżej służy
użytkownikowi do wpisywania i wysyłania jednolinijkowych wiadomości. Kiedy dowolny użytkownik wpisze
wiadomość zostaje ona wysłana do serwera i rozprowadzona na różne sposoby do innych klientów.

Rysunek 10.3.
Aplet pogawędek w działaniu
Klienci HTTP wysyłają swoje wiadomości na serwer przy pomoc metody HTTP POST. Aplet pobiera nową
wiadomość z elementu TextInput, kiedy użytkownik naciska Enter, koduje wiadomość w URL-u i wysyła ją
do serwletu jako parametr wiadomości. Jest to działanie bardzo proste. Nieco bardziej skomplikowany jest
sposób zarządzania pobieraniem wiadomości od innych użytkowników przez klienta pogawędek HTTP.
Wykorzystuje on metodę HTTP GET do odebrania każdej wiadomości, występuje jednak pewien problem: nie
wie on, kiedy pojawia się nowa wiadomość do odebrania. Jest to problem związany z paradygmatem
jednokierunkowej komunikacji żądanie/odpowiedź. Klient musi albo od czasu do czasu wysłać jakąś wiadomość
w celu uaktualnienia, lub symulować komunikację dwukierunkową przy pomocy serii blokujących żądań GET.
Oznacza to, że klient pogawędek inicjuje żądanie GET, które działa blokująco, dopóki serwer nie zdecyduje, że
nadszedł czas na zwrócenie jakiejś informacji. W niniejszym przykładzie zaimplementowana zostanie ta
symulowana komunikacja dwukierunkowa.
Klienci uzyskujący dostęp przez port, w celu zachowania spójności, wysyłają swoje wiadomości w identyczny
sposób jak klienci HTTP, przy pomocy metody HTTP POST. Mogą oni wysyłać swoje wiadomości przy pomocy
połączeń przez zwykły port, lecz daje to jedynie minimalny zysk w wydajności, który, przynajmniej w tym
przypadku, nie przeważa zwiększonej złożoności. Klienci korzystający ze zwykłych portów wykorzystują je
jednak do odbierania wiadomości od innych użytkowników. W tym przypadku symulowana łączność
dwukierunkowa zostaje zastąpiona przez komunikację prawdziwą. Kiedy dowolna nowa wiadomość jest
odbierana przez serwlet, jest wysyłana bezpośrednio z serwletu do klientów korzystających z portów przy
pomocy opartych na zwykłym tekście połączeń przez port.
Klienci pogawędek RMI wykonują swoje żądania POST i GET przy pomocy wywołań metod. Aby wysłać nową
wiadomość, aplet po prostu wywołuje metodę zdalnego serwletu nadajWiadomosc(String). Aby odebrać
nowe wiadomości, może zastosować jedną z dwóch opcji. Może wywołać blokującą serwlet metodę
pobierzNastepnaWiadomosc() lub, przy pomocy wywołań wstecznych, może poprosić serwer o
wywołanie swojej własnej metody ustawNastepnaWiadomosc(String) za każdym razem, kiedy nadana
zostaje nowa wiadomość. W niniejszym przykładzie zastosowana zostanie technika wywołań wstecznych.
Pierwszym z wszystkich powyższych apletów jest aplet-dyspozytor. Pozwala on użytkownikowi na wybór
techniki komunikacji aplet-serwlet (HTTP, port lub RMI), któryą chce on wykorzystać i, w oparciu o jego
wybór, generuje stronę zawierającą właściwy aplet. Prawdą jest, że pojedynczy aplet mógłby obsługiwać
wszystkie te trzy techniki i automatycznie wybierać między nimi w oparciu o środowisko uruchomieniowe, lecz
działanie takie w tym miejscu jedynie niepotrzebnie skomplikowałoby przykład. Serwlet-dyspozytor podaje
również apletowi nazwę jego użytkownika, zostanie to jednak opisane później.

Serwlet
Pełne wydruki interfejsu SerwerPogaw i klasy SerwletPogaw, która go wykorzystuje są przedstawione w
przykładach 10.15 i 10.16.
Przykład 10.15.
Interfejs SerwerPogaw, wykorzystywany przez SerwletPogaw
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface SerwerPogaw extends Remote {


public String pobierzNastepnaWiadomosc() throws RemoteException;
public void nadajWiadomosc(String message) throws RemoteException;

public void dodajKlient(KlientPogaw klient) throws RemoteException;


public void usunKlient(KlientPogaw klient) throws RemoteException;
}
Przykład 10.16.
W pełni funkcjonalny serwer-serwlet pogawędek
import java.io.*;
import java.net.*;
import java.rmi.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet;

public class SerwletPogaw extends RemoteDaemonHttpServlet


implements SerwerPogaw {

// zrodlo działa jako dystrybutor nowych wiadomości


ZrodloWiadomosci zrodlo = new ZrodloWiadomosci();

// portKlienci przechowuje odwołania do wszystkich klientów łączących się przez


// porty
Vector portKlienci = new Vector();

// rmiKlienci przechowuje odwołania do wszystkich klientów łączących się przez


RMI
Vector rmiKlienci = new Vector();

// doGet() zwraca następną wiadomość. Blokuje dopóki nie ona nie wystąpi.
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
// Zwrócenie następnej wiadomości (blokowanie)
wyj.println(pobierzNastepnaWiadomosc());
}

// doPost() przyjmuje nową wiadomość i wysyła ją do wszystkich


// aktualnie nasłuchujących klientów łączących się przez HTTP i porty.
public void doPost(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
// Przyjęcie wiadomości jako parametru „wiadomosc”
String wiadomosc = zad.getParameter("wiadomosc");

// Wysłanie jej do wszystkich nasłuchujących klientów


if (wiadomosc != null) nadajWiadomosc(wiadomosc);

// Ustawienie kodu stanu aby zaznaczyć, że nie będzie odpowiedzi


odp.setStatus(odp.SC_NO_CONTENT);
}

// pobierzNastepnaWiadomosc() zwraca następną nową wiadomość.


// Blokuje dopóki ona nie wystąpi.
public String pobierzNastepnaWiadomosc() {
// Stworzenie pojemnika na wiadomości czekającego na nową wiadomość ze źródła
// wiadomości
return new PojemnikWiadomosc().pobierzNastepnaWiadomosc(zrodlo);
}

// nadajWiadomosc() informuje wszystkich aktualnie nasłuchujących klientów że


jest
// nowa wiadomość. Powoduje odblokowanie wszystkich wywołań do
// pobierzNowaWiadomosc().
public void nadajWiadomosc(String wiadomosc) {
// Wysłanie wiadomości do wszystkich klientów HTTP poprzez przekazanie
wiadomości do
// źródła wiadomości
zrodlo.wyslijWiadomosc(wiadomosc);

// Bezpośrednie wysłanie wiadomości do wszystkich klientów połączonych przez


port
Enumeration enum = portKlienci.elements();
while (enum.hasMoreElements()) {
Socket klient = null;
try {
klient = (Socket)enum.nextElement();
PrintStream wyj = new PrintStream(klient.getOutputStream());
wyj.println(wiadomosc);
}
catch (IOException w) {
// Problem z klientem, zamknięcie i oddalenie go
try {
if (klient != null) klient.close();
}
catch (IOException ignored) { }
portKlienci.removeElement(klient);
}
}

// Bezpośrednie wysłanie wiadomości do wszystkich klientów RMI


enum = rmiKlienci.elements();
while (enum.hasMoreElements()) {
KlientPogaw klientPogaw = null;
try {
klientPogaw = (KlientPogaw)enum.nextElement();
klientPogaw.ustawNastepnaWiadomosc(wiadomosc);
}
catch (RemoteException w) {
// Problem z komunikacją z klientem, zamknięcie go
usunKlient(klientPogaw);
}
}
}

protected int getSocketPort() {


// Nasłuchiwanie na porcie 2428 (proszę spojrzeć na telefon, dlaczego)
return 2428;
}

public void obslugaKlient(Socket klient) {


// Nowy klient łączący się przez port. Dodanie go do listy.
portKlienci.addElement(klient);
}
public void dodajKlient(ChatClient klient) {
// Nowy klient RMI. Dodanie go do listy.
rmiKlienci.addElement(klient);
}

public void usunKlient(ChatClient klient) {


// Usunięcie wybranego klienta z listy.
rmiKlienci.removeElement(klient);
}
}

// ZrodloWiadomosc działa jako źródło nowych wiadomości.


// Klienci zainteresowani przyjmowaniem nowych wiadomości mogą obserwować ten
obiekt
class ZrodloWiadomosc extends Observable {
public void wyslijWiadomosc(String wiadomosc) {
setChanged();
notifyObservers(wiadomosc);
}
}

// PojemnikWiadomosc działa jako odbierający nowe wiadomości.


// Nasłuchuje źródła.
class PojemnikWiadomosci implements Observer {

String wiadomosc = null; // ustawienie według uaktual() I odczytanie według


// pobierzNastepnaWiadomosc()

// Wywoływany przez źródło wiadomości kiedy odbiera nową wiadomość


synchronized public void uaktual(Observable o, Object arg) {
// odebranie nowej wiadomości
wiadomosc = (String)arg;

// Obudzenie czekającego wątku


notify();
}

// Pobranie następnej wiadomości wysłanej ze źródła wiadomości


synchronized public String pobierzNastepnaWiadomosc(ZrodloWiadomosci zrodlo) {
// Powiedz źródłu, że ma powiadamiać o nowych wiadomościach
zrodlo.addObserver(this);

// Czekanie aż metod uaktual() odbierze wiadomość


while (wiadomosc == null) {
try { wait(); } catch (Exception ignored) { }
}

// Powiedz źródłu żeby przestało informować o nowych wiadomościach


zrodlo.deleteObserver(this);

// Zwrócenie otrzymanej wiadomości


// ale wcześniej wyczyszczenie zmiennej egzemplarza wiadomości
// aby uaktual() i pobierzNastepnaWiadomosc mogły zostać wywołane ponownie.
String wiadomoscKopia = wiadomosc;
wiadomosc = null;
return wiadomoscKopia;
}
}
Metody pobierzNastepnaWiadomosc() i nadajWiadomosc(String wiadomosc) są najbardziej
interesującymi częściami SerwletPogaw. Metoda pobierzNastepnaWiadomosc zwraca następną
wiadomość, kiedy ona nadejdzie, blokując port dopóki jej nie ma. Aby umożliwić to blokowanie, wykorzystuje
ona klasy ZrodloWiadomosc i PojemnikWiadomosc. Bez wchodzenia w zbytnie szczegóły tych dwóch
klas, można powiedzieć, że serwlet konstruuje nowy PojemnikWiadomosc i prosi pojemnik o pobranie
następnej wiadomości ze źródła. Aby to wykonać, pojemnik rejestruje się jako obserwator źródła i wywołuje
wait() w celu blokowania. Kiedy źródło otrzymuje nową wiadomość, pojemnik (będący obserwatorem) jest
powiadamiany o zmianie przy pomocy wywołania jego metody uaktual(). Metoda PojemnikWiadomosc
uaktual() zapamiętuje ostatnią wiadomość ze źródła w swojej zmiennej wiadomości, po czym wywołuje
notify(). Powoduje to odblokowanie metody pobierzNastepnaWiadomosc() i zwrócenie
wiadomości.
Metoda nadajWiadomosc() powiadamia wszystkich klientów, kiedy występuje nowa wiadomość. Klientów
HTTP poprzez wysłanie wiadomości do ZrodloWiadomosci, innych klientów bezpośrednio poprzez przeglądanie
list klientów. Dla każdego klienta połączonego przez port, wyświetla wiadomość do portu klienta. Dla każdego
klienta RMI, wywołuje metodę ustawNastepnaWiadomosc(String). Jest to omawiane wcześniej
wywołanie wsteczne. Jeżeli, w dowolnym momencie, występuje problem z klientem RMI lub łączącym się przez
port, usuwa tego klienta z listy.
Dwie listy, portKlienci i rmiKlienci, zyskują elementy, kiedy klienci nadają wiadomość serwletowi.
Kiedy klient łączy się przez port, wywoływana jest metoda serwletu obslugaKlient(Socket), a nowy
klient jest dodawany do Vector portKlienci. Klienci RMI muszą dodać się do listy sami poprzez
wywołanie metody serwletu dodajKlient(klientPogaw).
Metody doGet() i doPost() SerwletPogaw są właściwie małymi obwódkami metod
pobierzNastepnaWiadomosc() i nadajWiadomosc(). Obwódka doGet() jest tak cienka, że niemal
przezroczysta — doGet() wysyła jako odpowiedź jakikolwiek łańcuch String, który zostaje zwrócony przez
pobierzNastepnaWiadomosc(). Obwódka doPost() jest nieco bardziej widoczna, Pobiera ona
wysłaną wiadomość z parametru formularza danych POST wiadomosc, nadaje wiadomość poprzez
przekazanie jej metodzie nadajWiadomosc() oraz ustawia kod stanu odpowiedzi na SC_NO_CONTENT, aby
wskazać, że w odpowiedź nie posiada żadnej zawartości. Właściwie wykonywanie żądania GET jest
równoznaczne z wywołaniem pobierzNastepnaWiadomosc(), a wykonywanie żądania POST jest
równoznaczne z wywołaniem nadajWiadomosc(). Jedyny problem jest taki, że w projekcie HTTP istnieje
mały odstęp czasu pomiędzy otrzymaniem wiadomości przez klienta i zażądaniem przez niego nowej, co może
spowodować ominięcie pewnych wiadomości w sytuacji dużego obciążenia. Lepszy projekt (pozostawiony
Czytelnikowi) wykorzystywałby obiekt HttpSession klienta w celu utworzenia kolejki oczekujących
wiadomości.
Można zauważyć, że port połączenia, na którym nasłuchuje SerwletPogaw to 2428. Przejmowanie metody
getSocketPort() w sposób taki, w jaki czyni to SerwletPogaw to prosty sposób ustawienia portu
połączenia bez korzystania z parametru inicjacji.

Aplet HTTP
Kod pierwszego apletu, apletu pogawędek HTTP, jest przedstawiony w przykładzie 10.17.
Przykład 10.17.
Klient pogawędek wykorzystujący komunikację HTTP
import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;

import com.oreilly.servlet.HttpMessage;

public class ApletPogawHTTP extends Applet implements Runnable {

TextArea tekst;
Label etykieta;
TextField wpis;
Thread watek;
String uzyt;

public void init() {


// Sprawdzenie, czy aplet został pobrany bezpośrednio z systemu plików.
// Jeżeli tak, wyjaśnienie użytkownikowi, że musi on być pobrany z serwera w
celu
// komunikacji z serwletami tego serwera
URL kodbazy = getCodeBase();
if (!"http".equals(kodbazy.getProtocol())) {
System.out.println();
System.out.println("*** Ups! ***");
System.out.println("Ten aplet musi być pobrany z serwera WWW.");
System.out.println("Proszę spróbować ponownie, tym razem ładując plik
HTML");
System.out.println("zawierający ten serwlet jako");
System.out.println("\"http://serwer:port/plik.html\".");
System.out.println();
System.exit(1); // Działa jedynie z przeglądarką apletów
// Przeglądarki wyświetlają błąd i kontynuują
}

// Pobranie nazwy tego użytkownika z parametru apletu ustawionego przez


serwlet
// Można o to po prostu spytać użytkownika, ale jest to przedstawienie formy
// komunikacji serwlet->aplet.
uzyt = getParameter("user");
if (uzyt == null) uzyt = "anonymous";

// konfiguracja interfejsu użytkownika...


// Na górze duże pole TextArea przedstawiające całą rozmowę.
// Poniżej opisane TextField przyjmujące wpisy tego użytkownika.
tekst = new TextArea();
tekst.setEditable(false);
etykieta = new Label("Powiedz coś: ");
wpis = new TextField();
wpis.setEditable(true);

setLayout(new BorderLayout());
Panel panel = new Panel();
panel.setLayout(new BorderLayout());

add("Centrum", tekst);
add("Dol", panel);

panel.add("Lewo", etykieta);
panel.add("Centrum", wpis);
}

public void start() {


watek = new Thread(this);
watek.start();
}

String pobierzNastepnaWiadomosc() {
String nastepnaWiadomosc = null;
while (nastepnaWiadomosc == null) {
try {
URL url = new URL(getCodeBase(), "/servlet/SerwletPogaw");
HttpMessage wiad = new HttpMessage(url);
InputStream in = wiad.sendGetMessage();
DataInputStream dane = new DataInputStream(
new BufferedInputStream(in));
nastepnaWiadomosc = dane.readLine();
}
catch (SocketException w) {
// Niemożliwe połączenie z komputerem macierzystym, zgłoszenie i
oczekiwanie
// przed ponowną próbą
System.out.println("Połączenie z komputerem niemożliwe: " + w.getMessage
());
try { Thread.sleep(5000); } catch (InterruptedException ignored) { }
}
catch (FileNotFoundException w) {
// Serwlet nie istnieje, zgłoszenie i oczekiwanie
System.out.println("Zasoby nie odnalezione: " + w.getMessage());
try { Thread.sleep(5000); } catch (InterruptedException ignored) { }
}
catch (Exception w) {
// Inny problem, zgłoszenie i oczekiwanie
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
try { Thread.sleep(1000); } catch (InterruptedException ignored) { }
}
}
return nastepnaWiadomosc + "\n";
}

public void run() {


while (true) {
tekst.appendText(pobierzNastepnaWiadomosc());
}
}

public void stop() {


watek.stop();
watek = null;
}

void nadajWiadomosc(String wiadomosc) {


wiadomosc = uzyt + ": " + wiadomosc; // Na początku dodanie nazwy użytkownika
try {
URL url = new URL(getCodeBase(), "/servlet/SerwletPogaw");
HttpMessage wiad = new HttpMessage(url);
Properties wlasc = new Properties();
wlasc.put("wiadomosc", wiadomosc);
wiad.sendPostMessage(wlasc);
}
catch (SocketException w) {
// Niemożliwe połączenie z komputerem macierzystym, zgłoszenie i
zaprzestanie
// nadawania
System.out.println("Niemożliwe połączenie z komputerem: " + w.getMessage());
}
catch (FileNotFoundException w) {
// Serwlet nie istnieje, zgłoszenie i zaprzestanie nadawania
System.out.println("Zasoby nie odnalezione: " + w.getMessage());
}
catch (Exception w) {
// inny problem, zgłoszenie i zaprzestanie nadawania
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
}
}

public boolean obslugaWyjatek(Event wyjatek) {


switch (wyjatek.id) {
case Event.ACTION_EVENT:
if (wyjatek.target == wpis) {
nadajWiadomosc(wpis.getText());
wpis.setText("");
return true;
}
}
return false;
}
}
Powyższy aplet posiada dwie takie same metody robocze jak SerwletPogaw —
pobierzNastepnaWiadomosc() i nadajWiadomosc(). Jego metoda
pobierzNastepnaWiadomosc() pobiera następną wiadomość od serwletu. Jest ona wywoływana w celu
uaktualnienia pola TextArea. Działa wykorzystując HttpMessage do wykonania żądania GET dla serwletu,
po czym interpretuje pierwszą linie odpowiedzi jako następną nową wiadomość. Metoda apletu
nadajWiadomosc() wysyła wiadomość do serwletu w celu udostępnienia jej pozostałym klientom. Metoda ta
jest wywoływana w metodzie apletu obslugaWyjatek() za każdym razem, kiedy użytkownik naciśnie Enter
w elemencie TextInput. Działa podobnie do pobierzNastepnaWiadomosc(). Wykorzystuje
HttpMessage do wykonania żądania POST, przekazując tekst z TextInput jako parametr wiadomosc i nie
zajmuje się odczytaniem odpowiedzi.

Aplet łączący się przez port


Jedyną różnicą pomiędzy łączącym się przez port ApletPogawPort i opartym na HTTP ApletPogawHTTP
jest przeprojektowana metoda pobierzNastepnaWiadomosc(). Metoda ta przedstawiona jest w
przykładzie 10.18.
Przykład 10.18.
Klient pogawędek wykorzystujący połączenie przez zwykły port
static final int PORT = 2428;
DataInputStream potokSerwer;

String pobierzNastepnaWiadomosc() {
String nastepnaWiadomosc = null;
while (nastepnaWiadomosc == null) {
try {
// Połączenie z serwerem jeżeli nie nastąpiło ono wcześniej
if (potokSerwer == null) {
Socket s = new Socket(getCodeBase().getHost(), PORT);
potokSerwer = new DataInputStream(
new BufferedInputStream(
s.getInputStream()));
}

// Odczytanie linii
nastepnaWiadomosc = potokSerwer.readLine();
}
catch (SocketException w) {
// Połączenie z komputerem niemożliwe, zgłoszenie i odczekanie przed kolejną
próbą
System.out.println("Połączenie z komputerem niemożliwe: " + w.getMessage());
potokSerwer = null;
try { Thread.sleep(5000); } catch (InterruptedException ignored) { }
}
catch (Exception w) {
// Inny problem, zgłoszenie i odczekanie przed kolejną próbą
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
try { Thread.sleep(1000); } catch (InterruptedException ignored) { }
}
}
return nastepnaWiadomosc + "\n";
}
Powyższa metoda odczytuje nadane wiadomości z portu, który podłączony jest do serwletu pogawędek.
Wykorzystuje ona prosty protokół portu — cała zawartość to zwykły tekst, jedna wiadomość na linię. Po
pierwszym wywołaniu tej metody ustanawia ona połączenie z portem po czym wykorzystuje je do pobrania
DataInputStream, przy pomocy którego może odczytywać z portu jedną linię za każdym razem. Przy
każdym ponownym wywołaniu ponownie wykorzystuje ona ten sam potok i po prostu zwraca następną
przeczytaną linię. Jeżeli kiedykolwiek wystąpi wyjątek SocketException, ponownie ustanawia ona
połączenie.

Aplet RMI
Kod interfejsu KlientPogaw jest przedstawiony w przykładzie 10.19. Aplet pogawędek oparty na RMI, który
wykorzystuje ten interfejs jest przedstawiony w przykładzie 10.20.
Przykład 10.19.
Interfejs KlientPogaw, wykorzystywany przez ApletPogawRMI
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface KlientPogaw extends Remote {


public void pobierzNastepnaWiadomosc(String message) throws RemoteException;
}
Przykład 10.20.
Klient pogawędek wykorzystujący komunikację RMI
import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.util.*;

public class ApletPogawRMI extends Applet implements KlientPogaw {

TextArea tekst;
Label etykieta;
TextField wpis;
Thread watek;
String uzyt;

SerwerPogaw serwerPogaw;

private int pobierzRejestrPort () {


try { return Integer.parseInt(getParameter("port")); }
catch (NumberFormatException ignored) { return Registry.REGISTRY_PORT; }
}

private String pobierzRejestrNazwa() {


String nazwa = getParameter("nazwa");
return (nazwa == null ? "SerwletPogaw" : nazwa);
}

// Zwraca odwołanie do zdalnej pogawędki serwer/serwlet


// Próbuje wyjść, jeżeli wystąpi problem
private SerwerPogaw pobierzSerwerPogaw() {
try {
Registry rejestr =
LocateRegistry.getRegistry(getCodeBase().getHost(), pobierzRejestrPort());
Object obi = rejestr.lookup(pobierzRejestrNazwa());
return (SerwerPogaw)obi;
}
catch (java.rmi.UnknownHostException w) {
// Nieznany komputer rejestru, próba wyjścia
System.out.println("Komputer nieznany, URL: " + w.getMessage());
System.exit(1);
}
catch (NotBoundException w) {
// Odnalezienie obiektu niemożliwe, próba wyjścia
System.out.println("Nazwa niedowiązana: " + w.getMessage());
System.exit(1);
}
catch (ClassCastException w) {
// Obiekt to nie SerwerPogaw, próba wyjścia
System.out.println(pobierzRejestrNazwa() + " to nie SerwerPogaw:" +
w.getMessage());
System.exit(1);
}
catch (RemoteException w) {
// Ogólny problem RMI, próba wyjścia
System.out.println("Wyjątek zdalny: " + w.getMessage());
System.exit(1);
}
catch (Exception w) {
// Inny problem, próba wyjścia
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
System.exit(1);
}
return null; // zwraca null, jeżeli exit() nie powiedzie się
}

// Dodanie siebie jako klienta serwera pogawędek


// Proszę zauważyć, że rejestr RMI nie jest potrzebny
private void rejestrujSerwerPogaw(SerwerPogaw serwer) {
try {
UnicastRemoteObject.exportObject(this);
serwer.dodajKlient(this);
}
catch (RemoteException w) {
// Ogólny problem RMI, próba wyjścia
System.out.println("Wyjątek zdalny: " + w.getMessage());
System.exit(1);
}
catch (Exception w) {
// Inny problem, próba wyjścia
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
System.exit(1);
}
}

public void init() {


// Sprawdzenie, czy aplet został pobrany bezpośrednio z systemu plików.
// Jeżeli tak, wyjaśnienie użytkownikowi, że musi on być pobrany z serwera w
celu
// komunikacji z serwletami tego serwera
URL kodbazy = getCodeBase();
if (!"http".equals(kodbazy.getProtocol())) {
System.out.println();
System.out.println("*** Ups! ***");
System.out.println("Ten aplet musi być pobrany z serwera WWW.");
System.out.println("Proszę spróbować ponownie, tym razem ładując plik
HTML");
System.out.println("zawierający ten serwlet jako");
System.out.println("\"http://serwer:port/plik.html\".");
System.out.println();
System.exit(1); // Działa jedynie z przeglądarką apletów
// Przeglądarki wyświetlają błąd i kontynuują
}

// Pobranie zdalnego serwera pogawędek


serwerPogaw = pobierzSerwerPogaw();

// Zarejestrowanie siebie jako jednego z klientów


rejestrujSerwerPogaw(serwerPogaw);

// Pobranie nazwy tego użytkownika z parametru apletu ustawionego przez


serwlet
// Można o to po prostu spytać użytkownika, ale jest to przedstawienie formy
// komunikacji serwlet->aplet.
uzyt = getParameter("user");
if (uzyt == null) uzyt = "anonymous";

// konfiguracja interfejsu użytkownika...


// Na górze duże pole TextArea przedstawiające całą rozmowę.
// Poniżej opisane TextField przyjmujące wpisy tego użytkownika.
tekst = new TextArea();
tekst.setEditable(false);
etykieta = new Label("Powiedz coś: ");
wpis = new TextField();
wpis.setEditable(true);
setLayout(new BorderLayout());
Panel panel = new Panel();
panel.setLayout(new BorderLayout());

add("Centrum", tekst);
add("Dol", panel);

panel.add("Lewo", etykieta);
panel.add("Centrum", wpis);
}

String pobierzNastepnaWiadomosc() {
String nastepnaWiadomosc = null;
while (nastepnaWiadomosc == null) {
try {
nastepnaWiadomosc = serwerPogaw.pobierzNastepnaWiadomosc();
}
catch (RemoteException w) {
// Wyjątek zdalny, zgłoszenie i odczekanie przed następną próbą
System.out.println("wyjątek zdalny:" + w.getMessage());
try { Thread.sleep(1000); } catch (InterruptedException ignored) { }
}
}
return nastepnaWiadomosc + "\n";
}

public void ustawNastepnaWiadomosc(String wiadomosc) {


tekst.appendText(wiadomosc + "\n");
}

void nadajWiadomosc(String wiadomosc) {


wiadomosc = uzyt + ": " + wiadomosc; // Dodanie na początku nazwy klienta
try {
serwerPogaw.nadajWiadomosc(wiadomosc);
}
catch (RemoteException w) {
// Wyjątek zdalny zgłoszenie i zaprzestanie nadawania
System.out.println("Wyjatek zdalny: " + w.getMessage());
}
catch (Exception w) {
// Inny wyjątek, zgłoszenie i zaprzestanie nadawania
System.out.println("Ogólny wyjątek: " +
w.getClass().getName() + ": " + w.getMessage());
}
}

public boolean obslugaWyjatek(Event wyjatek) {


switch (wyjatek.id) {
case Event.ACTION_EVENT:
if (wyjatek.target == wpis) {
nadajWiadomosc(wpis.getText());
wpis.setText("");
return true;
}
}
return false;
}
}
Implementacje pobierzNastepnaWiadomosc() i nadajWiadomosc() są najprostsze z możliwych.
Muszą one jedynie wywołać metody zdalnego serwletu o tych samych nazwach. Lecz ich prostota ma swoją cenę
— bardziej skomplikowany kod konfiguracji. Konkretnie, metoda init() musi teraz wywołać stosunkowo
długą (ale zrozumiałą w tym momencie) pobierzSerwerPogaw() w celu pobrania odwołania do zdalnego
serwera pogawędek.
Po dokładniejszym przyjrzeniu się apletowi ApletPogawRMI można dostrzec, że właściwie nie wykorzystuje
on swojej metody pobierzNastepnaWiadomosc(). Zamiast tego, prosi serwlet o wywołanie metody
pobierzNastepnaWiadomosc() za każdym razem, kiedy nadana zostaje nowa wiadomość.
ApletPogawRMI wykonuje to żądanie w swojej metodzie init(), kiedy wywołuje
rejestrujSerwerPogaw(SerwerPogaw). Metoda ta eksportuje aplet jako zdalny obiekt, po czym
wywołuje metodę serwletu addClient() przekazując jej odwołanie do siebie. Następnie metoda serwletu
nadajWiadomosc() wysyła wywołanie zwrotne do apletu za każdym razem, kiedy pojawia się nowa
wiadomość.
Podczas stosowania wywołań zwrotnych na własną rękę, należy pamiętać o opisanych wcześniej podstawach.
Należy uruchomić kompilator RMI rmic na zdalnym aplecie w celu utworzenia jego klas końcówki i szkieletu.
Należy również upewnić się, że serwer posiada pliki ApletPogawRMI.class i KlientPogaw.class
gdzieś w swojej ścieżce klas.
Należy uważać także na fakt, ze współczesne przeglądarki są o wiele bardziej restrykcyjne w kwestii pozwalania
na wywołania zwrotne niż przeglądarki wykorzystywane w przeszłości i często właściwość ta jest w nich
wyłączona. W takim wypadku można zobaczyć jedynie zakodowane komunikaty o błędach, a czasami po prostu
pusty ekran. Wywołania zwrotne mogą zostać włączone poprzez dołączenie do apletu cyfrowego podpisu
(działanie to różni się w zależności od przeglądarki) i nadanie podpisanym apletom dodatkowych uprawnień lub
przez uzyskanie dostępu do apletu przy pomocy HTTPS z zaufanego serwera. Generalnie, może to być uważane
za kolejną wadę RMI i kolejny powód, by wykorzystywać HTTP do komunikacji aplet-serwer.

Dyspozytor
Ostatni przykład kodu w niniejszym rozdziale, serwlet DyspozytorPogaw przedstawiony jest w przykładzie
10.21. Serwlet ten posiada dwa obowiązki. Po pierwsze, kiedyn jest wywoływany bez żadnych parametrów
żądania, wyświetla on przyjazną stronę powitalną, zapytującą użytkownika o wersje apletu, której chciałby on
użyć, jak przedstawiono na rysunku 10.4. Po drugie, kiedy wywoływany jest z parametrem żądania, wyświetla
stronę zawierającą właściwy aplet, jak przedstawiono wcześniej na rysunku 10.3. Proszę pamiętać, że URL
wykorzystywany do uzyskania dostępu do serwletu-dyspozytora powinien zawierać prawdziwą nazwę serwera,
nie localhost, w celu uniknięcia problemów bezpieczeństwa RMI.
Rysunek 10.4.
Strona powitalna dyspozytora pogawędek
Przykład 10.21.
Powitalny serwlet-dyspozytor
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class DyspozytorPogaw extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws IOException, ServletException {
odp.setContentType("text/html");

if (!zad.getParameterNames().hasMoreElements()) {
// Nie było parametrów żądania. Wyświetlenie strony powitalnej.
wyswietlStronaPowitalna(zad, odp);
}
else {
// Wystapił przynajmniej jeden parametr żądania.
// Wyświetl stronę apletu.
wyswietlApletStrona(zad, odp);
}
}

// Strona powitalna wita użytkownika i wyświetla formularz, w którym użytkownik


może
// wybrać metodę komunikacji aplet-serwlet
private void wyswietlStronaPowitalna(HttpServletRequest zad,
HttpServletResponse odp)
throws IOException {
PrintWriter wyj = odp.getWriter();
String ja = zad.getServletPath();

wyj.println("<HTML>");
wyj.println("<HEAD><TITLE>");
wyj.println("Witamy w Absurdalnie Prostej Pogawędce");
wyj.println("</TITLE></HEAD>");
wyj.println();
wyj.println("<BODY>");
wyj.println("<H1> Witamy w Absurdalnie Prostej Pogawędce </H1>");
wyj.println();
wyj.println("Proszę wybrać formę komunikacji:");
wyj.println("<UL>");
wyj.println(" <LI><A HREF=\"" + ja + "?metoda=http\">http</A>");
wyj.println(" <LI><A HREF=\"" + ja + "?metoda=port\">port</A>");
wyj.println(" <LI><A HREF=\"" + ja + "?metoda=rmi\">rmi</A>");
wyj.println("</UL>");
wyj.println("</BODY></HTML>");
}
// Strona apletu wyświetla aplet pogawędek.
private void wyswietlApletStrona(HttpServletRequest zad,
HttpServletResponse odp)
throws IOException {
PrintWriter wyj = odp.getWriter();

wyj.println("<HTML>");
wyj.println("<HEAD><TITLE>Absurdalnie Prosta Pogawędka</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H1> Absurdalnie Prosta Pogawędka </H1>");

String metoda = zad.getParameter("metoda");


String uzyt = zad.getRemoteUser();
String aplet = null;

if ("http".equals(metoda)) {
aplet = "ApletPogawHTTP";
}
else if ("port".equals(metoda)) {
aplet = "ApletPogawPort";
}
else if ("rmi".equals(metoda)) {
aplet = "ApletPogawRMI";
}
else {
// Brak podanej metody lub metoda nieprawidłowa.
// Wyjaśnienie użytkownikowi oczekiwań.
wyj.println("Przepraszamy, ten serwlet potrzebuje parametru <TT>metoda</TT>
" +
"o jednej wartości jednej z poniższych: " +
"http, port, rmi");
return;
}

// Wyświetlenie kodu HTML wyświetlającego aplet.


// Wybranie kodu apletu oparte na parametrze metoda.
// Dostarczenie parametru użytkownik, jeżeli użytkownik jest znany.
wyj.println("<APPLET CODE=" + aplet + " CODEBASE=/ " +
"WIDTH=500 HEIGHT=170>");
if (uzyt != null)
wyj.println("<PARAM NAME=uzyt VALUE=\"" + uzyt + "\">");
wyj.println("</APPLET>");

wyj.println("</BODY></HTML>");
}
}
W powyższym kodzie nie występuje nic zaskakującego. Właściwie kod ten powinien wyglądać odświeżająco
prosto po przykładzie SerwletPogaw. Przykład ten przedstawia jednak ostatnią formę komunikacji aplet-
serwlet — generowane przez serwlet parametry apletu. Przy pomocy tej techniki serwlet generuje stronę
zawierającą aplet i przekazuje apletowi informacje poprzez manipulację znacznikami apletu <PARAM>. Każda
informacja, jaką serwlet chce przesłać nowemu apletowi, może być wysłana w ten właśnie sposób. W
powyższym przykładzie, serwlet wysyła nazwę zwróconą przez odp.getRemoteUser(). W innym
przykładzie serwlet mógłby przekazać apletowi jego typ przeglądarki poprzez wysłanie mu łańcucha zwróconego
przez req.getHeader("User-Agent"). Lub, aby okazać się bardziej pomocnym, serwlet mógłby
wykorzystać bazę danych w celu określenia możliwości przeglądarki i przekazać apletowi dokładne informacje.
Mógłby nawet przekazać apletowi wiadomość z pytaniem, czy przeglądarka obsługuje komunikację RMI.
Rozdział 11. Współpraca

serwletów

Serwlety pracujące wspólnie na jednym serwerze mogą porozumiewać się ze sobą na kilka sposobów. Istnieją
dwa główne typy współpracy serwletów:
• Dzielenie informacji — dwa lub więcej serwletów dzieli pewien zbiór zasobów. Na przykład, zbiór
serwletów zarządzających sklepem online może współdzielić zbiór inwentarza sklepu lub połączenie z
baza danych. Specjalnym przypadkiem dzielenia informacji jest śledzenie sesji (proszę zobaczyć
rozdział 7, „Śledzenie sesji”)
• Dzielenie kontroli — dwa lub więcej serwletów dzieli kontrolę żądania. Na przykład, jeden serwlet
może otrzymywać żądanie, ale pozwalać innemu serwletowi na przejęcie części lub całej
odpowiedzialności za jego obsługę
W przeszłości (przed pojawieniem się Servlet API 2.1) wymieniona zostałaby inna technika współpracy —
manipulacja bezpośrednia. W tej technice serwlet może otrzymać odwołanie do innego poprzez metodę
getServlet() i wywoływać jego metody. Ta metoda współpracy nie jest już obsługiwana; metoda
getServlet() została wyłączona i zdefiniowana tak, by zwracała null w przypadku Servlet API 2.1 i
późniejszych. Powód — serwlet może zostać zniszczony przez serwer WWW w dowolnym momencie, tak więc
nic poza serwerem nie powinno posiadać bezpośredniego odwołania do serwletu. Wszystkie działania, które
mogły być wykonywane przy pomocy getServlet() mogą być wykonane w sposób lepszy i bezpieczniejszy
przez alternatywy opisane w niniejszym rozdziale.

Dzielenie informacji
Serwlety często współpracują poprzez dzielenie pewnych informacji. Informacje te mogą być informacjami o
stanie, współdzielonymi zasobami, źródłem zasobów, lub czymkolwiek innym. W Servlet API 2.0 i
wcześniejszych nie istniały wbudowane mechanizmy, przy pomocy których serwlety mogły dzielić informacje, a
w pierwszym wydaniu niniejszej książki (napisanej według Servlet API 2.0) musiały zostać przedstawione pewne
kreatywne sposoby pracy, między innymi umieszczanie informacji na liście właściwości System! Na szczęście
sposoby te nie są już potrzebne, ponieważ począwszy od Servlet API 2.1 rozszerzona została klasa
ServletContext, która działa na zasadzie składnicy współdzielonych informacji.

Współdzielenie przy pomocy ServletContext


Serwlet odczytuje ServletContext swojej aplikacji WWW przy pomocy wywołania
getServletContext(). Serwlet może wykorzystywać kontekst tak, jakby był on zmienną typu
Hashtable lub Map, przy pomocy następujących metod:
public void ServletContext.setAttribute(String nazwa, Object o)
public Object ServletContext.getAttribute(String nazwa)
public Enumeration ServletContext.getAttributeNames()
public void ServletContext.removeAttribute(String nazwa)
Metoda setAttribute() dowiązuje obiekt pod podaną nazwą. Każde istniejące dowiązanie o takiej samej
nazwie zostaje usunięte. Nazwy atrybutów powinny stosować tę samą konwencję, co nazwy pakietów, w celu
uniknięcia nadpisywania siebie. Nazwy pakietów java.*, javax.* i com.sun.* są zarezerwowane.
Metoda getAttribute() odczytuje obiekt dowiązany pod podaną nazwą lub zwraca null, jeżeli pakiet taki
nie istnieje. Wywołanie może również odczytywać specyficzne dla danego serwera, zakodowane atrybuty (na
przykład javax.servlet.context.tempdir), jak opisano w rozdziale 4, „Pobieranie informacji”.
Metoda getAttributeNames() zwraca zmienną typu Enumeration, która zawiera nazwy wszystkich
dowiązanych atrybutów lub pustą Enumeration, jeżeli nie istnieją żadne dowiązania.
Metoda removeAttribute() usuwa obiekt dowiązany pod daną nazwą lub nie wykonuje żadnego działania,
jeżeli atrybut nie istnieje. Dobrym pomysłem jest usuwanie niepotrzebnych już atrybutów w celu zredukowania
wykorzystania pamięci.

Zastosowanie kontekstu do sprzedawania pizzy


Jako zabawny przykład, proszę wyobrazić sobie zbiór serwletów sprzedających pizzę i dzielących ofertę dnia.
Serwlet administracyjny mógłby ustawiać ofertę dnia w sposób przedstawiony w przykładzie 11.1.
Przykład 11.1.
Pizzeria Italia
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class UstawiaOferta extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

ServletContext kontekst = getServletContext();


kontekst.setAttribute("com.pizzeria.oferta.pizza", "Cappricciosa");
kontekst.setAttribute("com.pizzeria.oferta.dzien", new Date());

wyj.println("Pizza — oferta dnia została ustawiona.");


}
}
Następnie każdy inny serwlet na serwerze może uzyskać dostęp do oferty i wyświetlić ją przy pomocy kodu
przedstawionego w przykładzie 11.2.
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class pobierzOferta extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

ServletContext kontekst = getServletContext();


String pizza = (String)
context.getAttribute("com.pizzeria.oferta.pizza");
Date dzien = (Date)
context.getAttribute("com.pizzeria.oferta.dzien");

DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM);
String dzisiaj = df.format(dzien);

wyj.println("Oferta dnia(" + dzisiaj + ") to pizza: " + pizza);


}
}
Współdzielenie z innym ServletContext
Wykorzystanie ServletContext do współdzielenia informacji daje dodatkowy efekt w postaci posiadania
przez każdą aplikację WWW swojego własnego składu informacji. Nie istnieje ryzyko przypadkowej kolizji
nazw lub nawet kolizji nazw z tej samej aplikacji uruchomionej dwukrotnie na serwerze. Jednak czasami
informacja musi być dzielona pomiędzy dwoma kontekstami WWW. W tej sytuacji można postąpić na dwa
sposoby. Po pierwsze, wykorzystać zewnętrzny skład informacji taki, jak plik bądź bazę danych, lub, po drugie,
wykorzystać specjalne punkty zaczepienia w celu bezpośredniego dostępu do innego kontekstu. Kwestia ta
zostanie opisana później.
Serwlet może otrzymać uchwyt do innego kontekstu na tym samym serwerze przy pomocy punktu zaczepienia
getContext() w swoim własnym kontekście:
public ServletContext ServletContext.getContext(String sciezkauri)
Metoda zwraca ServletContext zawierający podaną ścieżkę URI. Podana ścieżka musi być absolutna
(rozpoczynająca się od /) i jest interpretowana według katalogu macierzystego dokumentów serwera. Metoda ta
pozwala serwletowi na uzyskanie dostępu do kontekstu poza jego własnym. W środowisku wrażliwym na
bezpieczeństwo lub rozpowszechnianym (proszę zobaczyć rozdział 12, „Serwlety korporacyjne i J2EE”),
kontener serwletów może zwrócić null dla wszystkich ścieżek.
Aby to zademonstrować, proszę wyobrazić sobie, że serwlet poza dostępem do pizzerii musi uzyskać dostęp do
oferty dnia. Proszę także przyjąć, że kontekst pizzerii znajduje się w katalogu /pizzeria. W końcu proszę założyć,
że kontrola bezpieczeństwa serwera (nie dopuszczająca do komunikacji międzykontekstowej) została wyłączona.
Pod tymi warunkami następujący kod pozwala dowolnemu serwletowi na serwerze na odczytanie ofert dnia z
aplikacji pizzerii:
ServletContext mojKontekst = getServletContext();
ServletContext innyKontekst = mojKontekst.getContext("/pizzeria/indeks.httml");
String pizza = innyKontekst.getAttribute("com.pizzeria.oferta.pizza");
Date dzien = (Date)innyKontekst.getAttribute("com.pizzeria.oferta.dzien");
Aktualnie jedyną metodą pobierania odwołania do innego kontekstu jest wyszukiwanie ścieżki, tak więc
powyższy przykład nie będzie działał, jeżeli pizzeria przeniesie się do innego URL-a. W przyszłości może
pojawić się sposób na pobieranie odwołania według nazwy.

Kwestie mechanizmu ładowania klas


Jeżeli obiekt umieszczony w kontekście nie jest obiektem standardowym, dzielenie obiektu pomiędzy
kontekstami staje się o wiele trudniejsze z powodu kwestii związanych z mechanizmem ładowania klas. Proszę
pamiętać, że każda aplikacja WWW posiada swoje klasy w WEB-INF, ładowanym przez inny mechanizm
ładowania klas. Proszę również pamiętać, że mechanizm ładowania klas jednej aplikacji nie umie zlokalizować
klas umieszczonych w innej aplikacji. Nieszczęśliwy wynik — obiekt, którego plik .class istnieje jedynie
wewnątrz WEB-INF/classess lub WEB-INF/lib nie może być w łatwy sposób wykorzystany przez żadną inną
aplikację. Każda próba wywołania obiektu według jego właściciela daje wynik w postaci błędu
NoClassDefFoundError.
Rozwiązanie w postaci skopiowania pliku .class do obu aplikacji również nie działa. Klasy ładowane przez różne
aplikacje nie są tą samą klasą, nawet jeżeli ich definicje są identyczne. Próba wykonania tego daje w efekcie
jedynie zamianę błędu NoClassDefFoundError na wyjątek ClassCastException.
Istnieją trzy możliwe sposoby obejścia tego problemu. Po pierwsze, skopiowanie pliku .class współdzielonego
obiektu do standardowej ścieżki klas serwera (w przypadku serwera Tomcat jest to
katalog_macierzysty_serwera/classes). Pozwala to na odnalezienie klasy przez podstawowy mechanizm
ładowania klas współdzielony przez wszystkie aplikacje. Po drugie, uniknięcie wywoływania zwracanego
obiektu i wywoływanie jego metod przy pomocy refleksji (techniki, w której klasa Javy może przeglądać i
manipulować sobą w trakcie uruchomienia). Nie jest to tak eleganckie, ale pomaga, jeżeli nie posiada się pełnej
kontroli nad serwerem. Po trzecie, wywołanie zwracanego obiektu do interfejsu, który deklaruje pożądane
metody i umieszczenie interfejsu w standardowej ścieżce klas serwera. Każda klasa oprócz interfejsu może
pozostać w pojedynczych aplikacjach WWW. Wymaga to przeprogramowania klasy dzielonego obiektu, ale
pomaga w przypadkach, w których implementacja musi pozostać w swoich aplikacjach WWW.

Dzielenie kontroli
W celu bardziej dynamicznej współpracy, serwlety mogą dzielić kontrole żądania. Po pierwsze, serwlet może
przekazać całe żądanie wykonując pewne wstępne przetwarzanie, po czym przekazać żądanie innemu
elementowi. Po drugie, serwlet może dołączyć do swojej odpowiedzi pewną zawartość generowaną przez inny
składnik, właściwie tworząc programowe dołączenie po stronie serwera. Patrząc pojęciowo, jeżeli myśli się o
stronie wynikowej takiej, jak ekran, przekazanie daje innemu serwletowi pełną kontrole nad ekranem, podczas
gdy dołączanie wyświetla jedynie część zawartości w pewnym miejscu ekranu.
Ta możliwość delegowania daje serwletom większą elastyczność i pozwala na lepsze rozdzielanie. Przy pomocy
delegowania, serwlet może skonstruować swoją odpowiedź jako zbiór zawartości tworzonych przez różne
składniki serwera WWW. Funkcjonalność ta jest szczególnie ważna dla JavaServer Pages, w której to technice
często zdarza się, że jeden serwlet przetwarza żądanie, po czym przekazuje je stronie JSP w celu dokończenia
zachowania spójności (proszę zobaczyć rozdział 18, JavaServer Pages).

Pobieranie dyspozytora żądań


W celu zapewnienia obsługi delegowania, Servlet API 2.1 wprowadził interfejs
javax.servlet.RequestDispatcher. Serwlet pobiera egzemplarz RequestDispatcher wykonując
metodę getRequestDispatcher() na pożądanym obiekcie. Metoda ta zwraca RequestDispatcher,
który działa jako dyspozytor dla elementu (serwletu, JSP, pliku statycznego itp.) odnalezionego w podanej
ścieżce URI:
public RequestDispatcher ServletRequest.getRequestDispatcher(String sciezka)
Przekazywana ścieżka może być ścieżką względną, chociaż nie powinna sięgać poza aktualny kontekst serwletu.
Można wykorzystać opisaną w poprzednim podrozdziale metodę getContext() w celu sięgnięcia poza
aktualny kontekst. Nie istnieje sposób rozciągnięcia dyspozycji na kontekst znajdujący się na innym serwerze.
Jeżeli ścieżka rozpoczyna się od /, to jest interpretowana jako ścieżka względna wobec katalogu macierzystego
aktualnego kontekstu, jeżeli ścieżka zawiera łańcuch zapytanie, parametry dodawane są na początku zbioru
parametrów odbierającego elementu. Metoda zwraca null, jeżeli kontener serwletów nie jest w stanie zwrócić
RequestDispatcher, niezależnie od powodu.
Co ciekawe, istnieje metoda o tej samej nazwie w klasie ServletContext:
public RequestDispatcher ServletContext.getRequestDispatcher(String sciezka)
Różnica pomiędzy tymi metodami polega na tym, że wersja znajdująca się w ServletContext
(wprowadzona w API 2.1) przyjmuje jedynie URL-e absolutne (rozpoczynające się ukośnikiem), a wersja w
ServletRequest (wprowadzona w API 2.2) akceptuje URL-e zarówno absolutne, jak i względne. W związku
z tym nie ma powodu, aby stosować metodę znajdującą się w ServletContext. Istnieje ona jedynie z
powodów historycznych i może być uważana za wyłączoną, choć decyzja o jej oficjalnym wyłączeniu jeszcze nie
nastąpiła.
Możliwe jest również pobranie RequestDispatcher dla zasobu określonego przy pomocy nazwy, a nie
ścieżki, przy pomocy metody getNamedDispatch(), znajdującej się w ServletContext:
public RequestDispatcher ServletContext.getNamedDispatcher(String nazwa)
Pozwala to na zarządzanie zasobami, które niekoniecznie są dostępne publicznie. Serwletom (a także stronom
JSP) można nadawać nazwy poprzez deskryptor aplikacji WWW, jak opisano w rozdziale 3, „Cykl życia
serwletów”. Metoda ta zwraca null, jeżeli kontekst nie może zwrócić dyspozytora, niezależnie od powodu.
RequestDispatcher posiada dwie metody, forward() i include(). Metoda forward() przekazuje całe
żądanie innemu elementowi. Metoda include() dodaje wynik pracy innego elementu do odpowiedzi
wywołującego serwletu, pozostawia jednak kontrolę w jego rękach.

Dyspozycja przekazaniem
Metoda forward() przekazuje żądanie od serwletu do innego zasobu na serwerze. Metoda pozwala jednemu
serwletowi na wykonanie wstępnego przetwarzania żądania, a innemu elementowi na wygenerowanie
odpowiedzi. Inaczej niż sendRedirect(), forward() działa jedynie wewnątrz serwera, a klient nie jest w
stanie rozpoznać, że nastąpiło przekazanie. Informacja może zostać przekazana delegatowi przy pomocy
dołączonego łańcucha zapytania lub przy pomocy atrybutów żądania ustawionych przy pomocy metody
setAttribute(). Przykład 11.3 przedstawia serwlet wykonujący poszukiwanie, po czym przekierowujący
jego wyniki innej stronie, która je wyświetla.
Przykład 11.3.
Wewnętrzny mechanizm wyszukiwarki
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class LogikaSzukaj extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
// Typ zawartości ani urządzenie wyświetlające nie jest określone

// Pobranie łańcucha do poszukiwań


String szukaj = zad.getParameter("szukaj");

// Utworzenie URL-i zawierających atrybut żądania


String[] wyniki = pobierzWyniki(szukaj);

// Określenie wyników jako atrybutu żądania


zad.setAttribute("wyniki", wyniki);

// Przekazanie do strony wyświetlającej


String wyswietl = "/servlet/wyswietlSzukaj";
RequestDispatcher dyspozytor = zad.getRequestDispatcher(wyswietl);
dyspozytor.forward(zad, odp);
}

// W prawdziwym zastosowaniu metoda ta wywoływałaby właściwą logikę wyszukiwarki


// i zwracałaby większą ilość informacji na temat każdego wyniku niż zwykły URL
String[]pobierzWyniki(String szukaj) {
return new String[] { "http://www.abc.com",
"http://www.xyz.com" };
}
}
Zadaniem tego serwletu jest bycie mózgiem wyszukiwarki online. Wykonuje on poszukiwania tekstu podanego w
parametrze szukaj, po czym przechowuje wynikowe URL-e w atrybucie żądania wyniki. Następnie serwlet
przekazuje żądanie do elementu wyświetlającego. Powyżej został on sztywno zakodowany jako /
servlet/wyswietlSzukaj, ale ścieżka może być utworzona tak, aby mogła podlegać zmianom zależnie od
preferencji użytkownika co do języka, kolorów witryny, tego, czy jest on użytkownikiem początkującym, czy
zaawansowanym, itp.
Zasady, według których działać musi serwlet przekazujący, są stosunkowo rygorystyczne:
• Może on ustawiać nagłówki i kod stanu, ale nie może wysyłać klientowi właściwej odpowiedzi (jest o
zadanie drugiego elementu). Konsekwentnie, metoda forward() musi zostać wywołana przed
zatwierdzeniem odpowiedzi.
• Jeżeli odpowiedź została już zatwierdzona, wywołanie forward() powoduje wystąpienie wyjątku
IllegalStateException.
• Jeżeli odpowiedź nie została zatwierdzona, lecz w buforze odpowiedzi istnieje zawartość, bufor zostaje
automatycznie wyczyszczony — jest to część przekazywania.
• Dodatkowo, nie jest możliwe wysyłanie nowych obiektów żądania i odpowiedzi. Metoda forward()
musi zostać wywołana z tymi samymi obiektami żądania i odpowiedzi, które zostały przekazane
metodzie usługowej serwletu wywołującego, a forward() musi zostać wywołana wewnątrz tego
samego wątku obsługującego.
Element odbierający może zostać napisany tak, jak każdy inny element, jak przedstawia to przykład 11.4.
Przykład 11.4.
Fronton wyszukiwarki
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WyswietlSzukaj extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

// Pobranie wyników przeszukiwania z atrybutu żądania


String[] wyniki = (String[]) zad.getAttribute("wyniki");

if (wyniki == null) {
wyj.println("Brak wyników.");
wyj.println("Czy przypadkiem serwlet nie został wywołany bezpośrednio?");
}
else {
wyj.println("Wyniki:");
for (int i = 0; i < wyniki.length; i++) {
wyj.println(wyniki[i]);
}
}

wyj.println();
wyj.println("URI żądania: " + zad.getRequestURI());
wyj.println("Ścieżka kontekstu: " + zad.getContextPath());
wyj.println("Ścieżka serwletu: " + zad.getServletPath());
wyj.println("Informacje o ścieżce: " + zad.getPathInfo());
wyj.println("Łańcuch zapytania: " + zad.getQueryString());
}
}
Kiedy serwlet ten zostanie wypróbowany, można zauważyć, że uaktualniona została informacja o ścieżce. Nie
jest ona taka, jak oryginalna informacja o żądaniu; zamiast tego pasuje do ścieżki wykorzystywanej do dotarcia
do dyspozytora:
URI żądania: /servlet/WyswietlSzukaj
Ścieżka kontekstu:
Ścieżka serwletu:/ servlet/WyswietlSzukaj
Informacje o ścieżce: null
Łańcuch zapytania: null
Powód tego uaktualnienia jest taki, że jeżeli wykonane zostaje przekazanie serwletowi, serwlet ten powinien
otrzymać tę samą informację o ścieżce, jaką otrzymałby, gdyby został wywołany bezpośrednio; od tej pory
posiada on pełną kontrolę nad żądaniem. Jest to również powód, dla którego ważne jest, żeby bufor odpowiedzi
został wyczyszczony przed jego wywołaniem i żaby odpowiedź nie została zatwierdzona.

Dyspozycja według nazwy


Jednym z problemów występujących przy dyspozycji według ścieżki jest fakt, że jeżeli element docelowy jest
udostępniony elementom serwera pod pewną ścieżką, jest również pod tą samą ścieżką dostępny klientom. Dla
bezpieczeństwa można rozważyć uczynienie komponentu niedostępnym publicznie (na przykład przez
wyłączenie logiki wywołania /servlet) i dyspozycję żądań według nazwy zamiast według ścieżki. Umożliwia to
metoda getNamedDispatcher(). Logika dyspozycji w przykładzie 11-3 może zostać zmieniona na
następującą:
// Przekazanie do strony wyświetlającej
String wyswietl = "wyswietlSzukaj";
RequestDispatcher dyspozytor = zad.getNamedDispatcher(wyswietl);
dyspozytor.forward(zad, odp);
Przy korzystaniu z getNamedDispatcher() należy pamiętać o fakcie, że ponieważ nie jest wykorzystana
ścieżka URI, nie mogą zostać dołączone żadne łańcuchy zapytania i nie występuje uaktualnianie ścieżki.

Przekazanie czy przekierowanie


Czy lepiej jest zastosować forward(), czy sendRedirect()? Obie metody posiadają swoje zastosowania.
forward() działa najlepiej, kiedy jeden z elementów musi wykonać pewną logikę biznesową, po czym
podzielić się wynikami z innym elementem, a sendRedirect() działa najlepiej wtedy, gdy klient powinien
zostać przekierowany z jednej strony na inną. Kuszące jest zastosowanie forward() zamiast
sendRedirect() w przypadku prostych przekierowań, ponieważ forward() pracuje wewnątrz serwera i
działa szybciej od sendRedirect(), który wymaga bardzo okrężnej komunikacji z klientem. Niestety,
powoduje to problemy ze względną obsługą URL-i, jak przedstawiono w przykładzie 11.5, serwlecie, który
przekazuje żądanie do strony głównej witryny.
Przykład 11.5.
Bezpośrednia podróż do strony głównej
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class PrzekazStronaGlowna extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
RequestDispatcher dyspozytor = zad.getRequestDispatcher("/indeks.html");
dyspozytor.forward(zad, odp);
}
}
Jeżeli wykorzysta się serwer Tomcat w celu uzyskania dostępu do powyższego serwletu pod URL-em
http://localhost:8080/servlet/PrzekazStronaGlowna, można zauważyć, że wszystkie obrazki na stronie są
uszkodzone. Dzieje się tak dlatego, że strona ta zawiera znaczniki <IMG> z względnymi ścieżkami do plików
obrazków. Podczas, gdy sendRedirect() powiadomiłaby klienta o tym, że źródłem pliku jest katalog
macierzysty dokumentów serwera, forward() nie przekazuje tej wiadomości. W związku z tym nie działają
żadne względne URL-e. Metoda sendRedirect() ułatwia również dyspozycję do zasobów w innych
kontekstach, ponieważ nie jest w tym przypadku konieczne wyszukiwanie getContext(). Ogólnie poleca się
wykorzystywane sendRedirect(), gdzie to tylko możliwe, a forward() tylko wtedy, gdy jest to
konieczne.

Dyspozycja dołączania
Metoda include() RequestDispatcher dołącza zawartość zasobu do aktualnej odpowiedzi. Pozwala na
coś, co mogłoby być nazwane programistycznym dołączaniem po stronie serwera. Różni się to od forward(),
ponieważ wywołujący serwlet utrzymuje kontrolę nad odpowiedzią i może dołączyć zawartość zarówno przed,
jak i po zawartości dołączonej. Przykład 11.16 przedstawia serwlet, który dołącza egzemplarz z katalogu do
aktualnej odpowiedzi.
Przykład 11.6.
Dołączanie egzemplarza z katalogu
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Ksiazki extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

wyj.println("<HTML><HEAD><TITLE>Witamy</TITLE></HEAD>");
wyj.println("<BODY>");

// Wyświetlenie egzemplarza z katalogu


wyj.println("Proszę nacieszyć się tym pięknem:");

RequestDispatcher dyspozytor =
zad.getRequestDispatcher("/servlet/Egzemplarz?egzemplarz=0596000405");
dyspozytor.include(zad, odp);

wyj.println("A ponieważ lubimy Cię, te książki są o 20% tańsze!");

wyj.println("</BODY></HTML>");
}
}
Podobnie jak w przypadku forward(), informacje mogą być przesyłane do wywołanego zasobu przy pomocy
dołączonego łańcucha zapytania lub przy pomocy atrybutów żądania ustawionych za pomocą metody
setAttribute(). Wykorzystanie atrybutów zamiast parametrów umożliwia przekazywanie obiektów zamiast
prostych łańcuchów, jak przedstawiono w przykładzie 11.7.
Przykład 11.7.
Dołączanie kilku egzemplarzy z katalogu
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Ksiazki extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

wyj.println("<HTML><HEAD><TITLE>Witamy</TITLE></HEAD>");
wyj.println("<BODY>");

// Wyświetlenie egzemplarza z katalogu


RequestDispatcher dyspozytor =
zad.getRequestDispatcher("/servlet/Egzemplarz");
wyj.println("Proszę nacieszyć się tym pięknem:");
zad.setAttribute("egzemplarz", Ksiazka.pobierzKsiazka("0596000405"));
dyspozytor.include(zad, odp);

// Usunięcie atrybutu „egzemplarz” po jego wykorzystaniu


zad.removeAttribute("egzemplarz");

wyj.println("A może ta:");


zad.setAttribute("egzemplarz", Ksiazka.pobierzKsiazka("0395282659"));
dyspozytor.include(zad, odp);

wyj.println("A ponieważ lubimy Cię, te książki są o 20% tańsze!");

wyj.println("</BODY></HTML>");
}
}

// prosta klasa Ksiazka


class Ksiazka {
String isbn;
String tytul;
String autor;

private static Ksiazka JSERWLET =


new Ksiazka("0596000405", "Java Serwlet — programowanie", "Hunter");

private static Ksiazka HOBBIT =


new Ksiazka("0395282659", "Hobbit", "Tolkien");

// Symulacja wyszukiwania w bazie danych


public static Ksiazka pobierzKsiazka(String isbn) {
if (JSERWLET.pobierzISBN().equals(isbn)) {
return JSERWLET;
}
else if (HOBBIT.pobierzISBN().equals(isbn)) {
return HOBBIT;
}
else {
return null;
}
}

private Ksiazka(String isbn, String tytul, String autor) {


this.isbn = isbn;
this.tytul = tytul;
this.autor = autor;
}

public String pobierzISBN() {


return isbn;
}

public String pobierzTytul() {


return tytul;
}

public String pobierzAutor() {


return autor;
}
}
W powyższym przykładzie zamiast wysyłać jedynie ISBN, serwlet przekazuje kompletny obiekt Ksiazka. W
prawdziwym zastosowaniu, informacja o książce pochodziłaby z wyszukiwania ISBN w bazie danych. Powyżej
para książek została po prostu sztywno zakodowana.
Serwlet Egzemplarz może otrzymywać atrybut żądania egzemplarz poprzez wywołanie
zad.getAttribute("egzemplarz"), jak przedstawiono w przykładzie 11.8.
Przykład 11.8.
Wyświetlanie egzemplarza z katalogu.
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Egzemplarz extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {

// Typ zawartości nie jest ustawiany


PrintWriter wyj = odp.getWriter();

Ksiazka ksiazka = (Ksiazka) zad.getAttribute("egzemplarz");

wyj.println("<BR>");
if (ksiazka != null) {
wyj.println("<I>" + ksiazka.pobierzTytul() + "</I>");
wyj.println(" autorstwa " + ksiazka.pobierzAutor());
}
else {
wyj.println("<I>Nie znaleziono książki</I>");
}
wyj.println("<BR>");
}
}
Zawartość dołączonego serwletu może zostać umieszczona w dowolnym miejscu strony. W związku z tym nie
posiada ona możliwości zmiany kodu stanu ani nagłówków HTTP wysłanych w odpowiedzi. Każda próba
dokonania zmiany jest ignorowana.
Inaczej niż w przypadku forward(), elementy ścieżki i parametry żądania pozostają niezmienione w
porównaniu z elementami serwletu wywołującego. Jeżeli dołączony składnik wymaga dostępu do elementów
swojej własnej ścieżki, może on je odczytać przy pomocy następujących preasygnowanych atrybutów żądania:
javax.servlet.include.request_uri
javax.servlet.include.context_path
javax.servlet.include.servlet_path
javax.servlet.include.path_info
javax.servlet.include.query_string
Parametry żądania i odpowiedzi muszą być tymi samymi obiektami, które zostały przekazane metodzie usług
serwletu wywołującego, a include() musi być wywołana wewnątrz tego samego wątku obsługującego.
Dołączane zasoby muszą wykorzystywać mechanizm wyjścia, pasujący do mechanizmu wywołującego. Jeżeli
mechanizm wywołujący wykorzystuje PrintWriter, dołączane zasoby muszą również wykorzystywać
PrintWriter. Jeżeli mechanizm wykorzystuje OutputStream, dołączane zasoby również muszą
wykorzystywać OutputStream. Jeżeli mechanizmy nie pasują do siebie, dołączony serwlet powoduje wyjątek
IllegalStateException. Jeżeli jest to możliwe, należy się upewnić, co do stosowania PrintWriter
dla każdego wyświetlanego tekstu, a nie stanie się to problemem.
Rozdział 12. Serwlety

korporacyjne i J2EE

Niniejszy rozdział opisuje serwlety korporacyjne. Termin korporacyjny (enterprise) jest w obecnych czasach
mocno związany z Javą, co jednak oznacza? Według starszego wydania słownika „American Heritage
Dictionary” słowo enterprise posiada trzy znaczenia:
1. Podejmowanie działań, zwłaszcza o dużej skali lub ryzyku
2. Biznes
3. Gotowość na ryzyko, inicjatywa
Jest to zaskakująco bliska definicja tego, co próbują powiedzieć ludzie mówiący o korporacyjnej Javie i
korporacyjnych serwletach. Można połączyć tradycyjne definicje w celu utworzenia definicji nowoczesnej:
1. Gotowość do obsługi działań biznesowych o dużej skali
Innymi słowy, serwlety korporacyjne to serwlety zaprojektowane do obsługi zorientowanych biznesowo witryn
WWW o dużej skali — witryny o wysokim obciążeniu i niezawodności wiążą się z dodatkowymi wymaganiami
na temat skalowalności, rozkładu ładunku, obsługi błędów i integracji z innymi technologiami Java 2, Enterprise
Edition (J2EE).
Odkąd serwlety zaczęły być coraz bardziej popularne i profesjonalne, a także kontenery serwletów stały się
bardziej niezawodne i wszechstronne, coraz większa liczba witryn biznesowych tworzona jest przy pomocy
serwletów. Tworzenie serwletów dla takich witryn różni się od tworzenia ich dla witryn tradycyjnych, i w tym
rozdziale omówione zostaną specjalne wymagania i możliwości serwletów korporacyjnych.

Dystrybucja ładunku
W przypadku witryn o wysokiej wydajności i/lub niezawodności, często korzystna jest dystrybucja zawartości i
przetwarzania witryny pomiędzy większą ilością serwerów wspierających. Dystrybucja ta pozwala kilku
serwerom na współdzielenie ładunku, co zwiększa ilość równoczesnych żądań, które mogą zostać obsłużone,
oraz pozwala na przetrwanie i działanie witryny nawet wtedy, gdy zawiedzie jeden konkretny składnik.
Dystrybucja nie jest polecana dla każdej witryny. Tworzenie i utrzymanie witryny dystrybuowanej może być w
znaczący sposób trudniejsze niż wykonanie tych samych czynności w przypadku witryny samodzielne, oraz
bardziej kosztowne w kwestii sprzętu dzielącego ładunek i/lub wymagań programów. Dystrybucja nie przynosi
również znaczącej poprawy wydajności, jeżeli serwer nie znajduje się pod maksymalnym obciążeniem. Po
wystąpieniu problemu z wydajnością, często najłatwiej jest „rzucić sprzęt na problem” poprzez instalację
pojedynczego wysokowydajnego komputera, a nie podzielić ładunek pomiędzy dwoma komputerami o
niewystarczającej wydajności.
Istnieje jednak wiele witryn, które wymagają skalowalności poza osiągami dowolnego pojedynczego komputera i
poziomu niezawodności niemożliwego do uzyskania na pojedynczym komputerze. W witrynach tych należy
zastosować dystrybucję.
Jak być dystrybuowalnym
Wymagania programistyczne dla serwletu dystrybuowalnego są dużo bardziej restrykcyjne niż wymagania
odnoszące się do innych serwletów. Serwlet dystrybuowalny musi zostać napisany według konkretnych zasad
tak, aby różne egzemplarze serwletu mogły być wykonywane na wielu serwerach wspierających. Każdy
programista, który przyjmuje, że istnieje tylko jedna kopia serwletu, jeden kontekst serwletu, jedna wirtualna
maszyna Javy i jeden system plików, może sprawić sobie poważne problemy.
Aby zrozumieć, jak serwlety mogą być dystrybuowane, proszę spojrzeć na technologię Enterprise JavaBeans
(EJB), model elementów po stronie serwera stworzony w celu implementacji dystrybuowanych obiektów
biznesowych, który jest sercem J2EE. EJB jest zaprojektowany od początku jako zbiór obiektów
dystrybuowalnych. ELB implementuje logikę biznesową i pozwala kontenerowi (zwykle serwerowi), na którym
pracuje, na uruchamianie usług zarządzających takich, jak transakcje, trwałość, współbieżność i bezpieczeństwo.
EJB może być dystrybuowany pomiędzy większą ilością komputerów wspierających i może być przenoszony
pomiędzy komputerami przy pomocy kontenera. Aby wykorzystywać ten model dystrybucji, EJB musi spełniać
ścisły zestaw zasad opartych na specyfikacji, mówiący, co może on robić, a czego nie13.
Serwlety nie posiadają takiego opartego na specyfikacji zestawu zasad. Wynika to z ich pochodzenia jako
frontowych elementów po stronie serwera, wykorzystywanych do komunikacji z klientem i wywoływania
dystrybuowanych EJB, podczas, gdy same serwlety nie są przeznaczone do dystrybucji. Jednak w przypadku
witryn o dużym obciążeniu lub witryn, które wymagają wysokiej niezawodności, również serwlety muszą być
dystrybuowane. Przypuszcza się, że przyszłe wersje Servlet API będą zawierać ściślejszą definicję implementacji
dystrybuowalnych kontenerów serwletów.
Poniżej podano utworzone przez autorów niniejszej książki skrótowe zasady tworzenia serwletów, które mają
być umieszczane w środowisku dystrybuowalnym:
• Należy pamiętać, że różne egzemplarze serwletu mogą pracować w różnych JVM i/lub
komputerach. W związku z tym zmienne egzemplarza i zmienne statyczne nie powinny być
wykorzystywane do przechowywania stanu. Każdy stan powinien być przechowywany w obiekcie
zewnętrznym, takim jak baza danych lub EJB (do poszukiwań można zastosować nazwę serwletu).
• Należy pamiętać, że w każdym JVM i/lub komputerze mogą istnieć różne kopie
ServletContext. W związku z tym kontekst nie powinien być wykorzystywany do
przechowywania stanu aplikacji. Każdy stan powinien być przechowywany w obiekcie
zewnętrznym, takim jak baza danych lub EJB (do poszukiwań można zastosować nazwę serwletu).
• Należy pamiętać, że dowolny obiekt umieszczony w HttpSession powinien mieć możliwość
przeniesienia do (lub uzyskania dostępu z) innego komputera. Na przykład, obiekt może
wykorzystywać java.io.Serializable. Proszę pamiętać, że ponieważ sesje mogą poruszać
się, zdarzenie usunięcia dowiązania sesji może wystąpić w innym komputerze niż zdarzenie jej
dowiązania!
• Należy pamiętać, że pliki nie muszą występować na wszystkich komputerach wspierających. W
związku z tym powinno się unikać wykorzystywania pakietu java.io w celu uzyskania dostępu
do pliku i zamiast tego wykorzystać mechanizm getServletContext().getResource()
— lub upewnić się, że wszystkie wykorzystywane pliki zostały umieszczone na wszystkich
komputerach wspierających.
• Należy pamiętać, że synchronizacja nie jest globalna i działa jedynie w przypadku lokalnej JVM.
Aplikacja WWW, której elementy spełniają powyższe zasady może zostać oznaczona jako dystrybuowalna, a to
oznaczenie pozwala serwerowi na uruchomienie jej na kilku serwerach wspierających. Znak dystrybuowalności
jest umieszczany w deskryptorze web.xml jako pusty znacznik <distribuable/> umieszczony pomiędzy
opisem aplikacji i jej parametrami kontekstu:
<web-app>
<description>
Wszystkie serwlety i strony JSP są gotowe do dystrybucji
</description>

<distribuable/>

<context-param>
<!--...-->
</context-param>

13
Większa ilość informacji na temat Enterprise JavaBeans dostępna jest pod adresem http://java.sun.com/products/ejb i w
książce „Enterprise JavaBeans” autorstwa Richarda Monson-Haefela (O'Reilly).
</web-app>
Aplikacje z definicji nie są dystrybuowalne w celu zapewnienia niedoświadczonym programistom serwletów
możliwości tworzenia ich bez potrzeby martwienia się o dodatkowe zasady związane z dystrybucją. Zaznaczenie
aplikacji jako dystrybuowalnej niekoniecznie oznacza, że aplikacja zostanie podzielona pomiędzy kilkoma
różnymi komputerami. Wskazuje to jedynie, że aplikacja ma możliwość bycia dystrybuowaną. Należy myśleć o
tym jako o wystawionym przez programistę świadectwie aplikacji.
Serwery nie wymuszają większości podanych powyżej zasad dystrybucji aplikacji. Na przykład, serwlet może
wykorzystywać zmienne egzemplarza i statyczne, a także przechowywać obiekty w swoim ServletContext
oraz uzyskiwać bezpośredni dostęp do plików przy pomocy pakietu java.io. Od programisty zależy
zabezpieczenie tych własności przed nadużyciem. Jedynym działaniem, jakie może podjąć serwer jest wywołanie
wyjątku IllegalArgumentException, jeżeli obiekt dowiązany do HttpSession nie jest implementacją
java.io.Serializable (a nawet to działanie jest opcjonalne, ponieważ, jak zostanie to opisano później,
serwer kompatybilny z J2EE musi pozwalać na przechowywanie w sesji dodatkowych typów obiektów).

Wiele stylów dystrybucji


Dystrybucja serwletów (często nazywana klastrowaniem) jest opcjonalną własnością kontenera serwletów, a
kontenery serwletów obsługujące klastrowanie mogą wykonywać je na kilka różnych sposobów. Istnieją cztery
standardowe architektury, wymienione poniżej, od najprostszej do najbardziej zaawansowanej.
1. Brak klastrowania. Wszystkie serwlety wykonywane są wewnątrz jednej wirtualnej maszyny Javy, a
znacznik <distributable/> jest właściwie ignorowany. Projekt jest prosty, jednak działa
poprawnie w przypadku standardowych witryn. W ten sposób działa samodzielny serwer Tomcat.
2. Obsługa klastrowania, brak sesji wędrujących i unikania błędów. Serwlety w aplikacji WWW
zaznaczone jako <distributable/> mogą być wykonywane na kilku komputerach. Żądania
niezwiązane z sesjami są dystrybuowane losowo (z zachowaniem równomiernego obciążenia). Żądania
sesji są „lepkie” i przywiązane do konkretnego serwera wspierającego, na którym rozpoczęły działanie.
Dane sesji nie przemieszczają się pomiędzy komputerami, co posiada zaletę taką, że sesje mogą
przechowywać nie przenoszone dane (nie-Serializable) oraz wadę taką, że sesje nie mogą być
przenoszone do niewykorzystywanych serwerów, a serwer może załamać się z powodu uszkodzonej
sesji. Architektura ta wykorzystywana jest przez Apache/JServ i Apache/Tomcat. Sesje są przywiązane
do konkretnego komputera poprzez mechanizm, w którym łącznik mod_jserv/mod_jk będący
częścią Apache'a wykorzystuje część identyfikatora sesji w celu wskazania, który wspierający JServ lub
Tomcat jest właścicielem sesji. Wykorzystanych może być również kilka egzemplarzy Apache'a,
obsługujących sprzęt lub oprogramowanie rozkładające obciążenie.
3. Obsługa klastrowania i sesji wędrujących, brak unikania błędów. Architektura ta pracuje podobnie jak
poprzednia, poza tym, że sesje mogą przenosić się z jednego serwera do drugiego w celu lepszego
rozłożenia obciążenia. Aby uniknąć kwestii współbieżności, każda wędrówka sesji posiada gwarancję
wystąpienia pomiędzy żądaniami klienta. Specyfikacja serwletów mówi, że „wewnątrz aplikacji
oznaczonej jako dystrybuowalna wszystkie żądanie będące częścią sesji mogą być obsługiwane jedynie
przez pojedynczą maszynę wirtualną w jednym czasie”. Wszystkie obiekty umieszczone w sesji, które
mają być przenoszone muszą wykorzystywać java.io.Serializable lub mieć możliwość
przenoszenia w pewien inny sposób.
4. Obsługa klastrowania, sesji wędrujących i unikania błędów. Serwer wykorzystujący tę architekturę
posiada dodatkową możliwość powielania zawartości sesji tak, że załamanie pojedynczego elementu
niekoniecznie niszczy sesje klienta. Wyzwaniem tej architektury jest koordynacja wydajnego przepływu
informacji. Architekturę tę wykorzystuje większość wysokowydajnych serwerów.

Szczegóły implementacji klastrowania różnią się między serwerami i są miejscem rywalizacji poszczególnych
producentów serwerów. Proszę spojrzeć do dokumentacji własnego serwera w celu uzyskania szczegółów na
temat obsługiwanego poziomu klastrowania. Inną przydatną własnością jest trwałość sesji, czyli zapisywanie w
tle informacji sesji na dysk lub do bazy danych, co pozwala informacjom na przetrwanie załamań i ponownych
uruchomień serwera.

Integracja z J2EE
W poprzednich częściach książki serwlety były wykorzystywane jako samodzielna technologia utworzona na
standardowej podstawie Javy. Serwlety posiadają także inne życie, w którym działają jako integralna część
czegoś, co nazywane jest Java 2, Enterprise Edition, w skrócie J2EE14. J2EE 1.2 zbiera razem kilka interfejsów
pracujących po stronie serwera, w tym Servlet API 2.2, JSP 1.1, EJB, JavaMail, Java Messaging Service (JMS),
Java Transactions (JTA), CORBA, JDBC, Java API for XML Parsing (JAXP) i Java Naming and Directory
Interface (JNDI). J2EE integruje te elementy w coś więcej niż prostą sumę części poprzez zdefiniowanie sposobu
współpracy tych technologii, ich wykorzystywania siebie nawzajem oraz dostarczania zaświadczeń, że konkretne
serwery aplikacji są zgodne z J2EE, co oznacza, że obsługują wszystkie potrzebne usługi, a także mechanizmy
ich integracji.

Podział pracy w J2EE


J2EE rozbija tworzenie aplikacji korporacyjnych na sześć różnych ról. Oczywiście pojedyncza osoba może
pełnić więcej niż jedną rolę, albo też kilka osób może pracować wspólnie nad zadaną rolą.
• Dostawca produktów J2EE — producent systemu operacyjnego, systemu baz danych, serwera aplikacji
i/lub serwera WWW. Dostawca produktów dostarcza implementacji interfejsów J2EE i narzędzi do
tworzenia i zarządzania aplikacją.
• Dostawca elementów aplikacji — autor serwletów aplikacji, EJB i innego kodu, a także ogólnej
zawartości takiej jak HTML. (Innymi słowy, Czytelnik tej książki.)
• Monter aplikacji — pobiera elementy aplikacji i (przy pomocy narzędzi dostarczonych przez dostawcę
produktów) nadaje im formę odpowiednią do wdrożenia. Częścią tej funkcji jest utworzenie opisu
zewnętrznych zależności aplikacji, które mogą zmieniać się w zależności od wdrożenia, takich jak baza
danych bądź informacje na temat logowania.
• Wdrożeniowiec — pobiera wyniki pracy montera i (przy pomocy narzędzi dostarczonych przez
dostawcę produktów) instaluje, konfiguruje i uruchamia aplikację. Zadanie konfiguracji wymaga
dostosowania produktu do zewnętrznych zależności wymienionych przez montera.
• Administrator systemu — konfiguruje i administruje infrastrukturę sieci, utrzymując pracę aplikacji.
• Dostawca narzędzi — Tworzy narzędzia wspierające wdrażanie J2EE, poza tymi, które są dostarczone
przez dostawcę produktów.
Podział pracy pomiędzy dostawcą elementów, monterem i wdrożeniowcem ma wpływ na zachowanie
programisty serwletów w roli dostawcy zawartości. Zwłaszcza należy zaprojektować kod tak, aby uczynić
zewnętrzne zależności jasnymi dla montera, a poza tym powinno się wykorzystać mechanizmy pozwalające
wdrożeniowcowi na dostosowanie się do tych zależności bez konieczności modyfikacji plików otrzymanych od
montera. Oznacza to, że żaden wdrożeniowiec nie edytuje pliku web.xml! Dlaczego nie? Ponieważ aplikacje
J2EE są wmontowane w pliki archiwa Enterprise Archive (.ear), w których plik web.xml aplikacji WWW jest
jedyną częścią, której nie można edytować.
Brzmi to o wiele trudniej, niż tak naprawdę wygląda. J2EE dostarcza standardowego mechanizmu w celu
osiągnięcia tego wyłączenia przy pomocy JNDI i kilku specjalnych znaczników w deskryptorze web.xml. JNDI
to mechanizm wyszukiwania obiektów, sposób na związanie ich z konkretnymi ścieżkami i późniejszego
odnalezienia przy pomocy danych ścieżek. Można myśleć o tym mechanizmie jak o rejestrze RMI, poza tym, że
jest on bardziej ogólny poprzez obsługę dostępu do wielu usług, włączając w to LDAP i NIS (a nawet, właściwie,
rejestr RMI!). Monter deklaruje zewnętrzne zależności w web.xml przy pomocy specjalnych znaczników,
wdrożeniowiec dostosowuje aplikacje do tych zależności przy pomocy właściwych serwerowi narzędzi, a
podczas uruchomienia kod Javy wykorzystuje interfejs JNDI w celu uzyskania dostępu do zasobów
zewnętrznych — umieszczonych tam przez serwer zgodny z J2EE. Wszystkie cele zostają wypełnione — kod
Javy pozostaje przenośny pomiędzy serwerami zgodnymi z J2EE, a wdrożeniowiec może dostosować się do
zależności zewnętrznych bez konieczności modyfikowania plików otrzymanych od montera. W tym miejscu
pozostawiono nawet wystarczającą elastyczność, która pozwala producentom serwerów na rywalizację w
implementacji standardu.

Pozycje środowiskowe
Parametry inicjacji kontekstu są użyteczne dla serwletów, ale w modelu J2EE istnieje z nimi problem— każda
zmiana wartości parametru wymaga modyfikacji pliku web.xml. Zamiast wartości parametrów, które mogą
zmienić się w trakcie wdrażania lepiej jest zastosować pozycje środowiskowe, wskazywane przez znaczniki
<env-entry>. Znacznik <env-entry> może zawierać znaczniki <description> (opis), <env-

14
Większość ludzi wymawia J2EE jako J-2-E-E, ale specjaliści z firmy Sun mówią po prostu „jah-too-ee”
entry-name> (nazwa), <env-entry-value> (wartość) i <env-entry-type> (typ). Poniższy <env-
entry> określa, czy aplikacja powinna umożliwiać wysyłanie kodu PIN przy pomocy poczty elektronicznej:
<env-entry>
<description>Wysyłanie kodu PIN pocztą</description>
<env-entry-name>pocztaPIN</env-entry-name>
<env-entry-value>false</env-entry-value>
<env-entry-type>java.lang.Boolean</env-entry-type> <!--PNK-->
</env-entry>
Znacznik <description> wyjaśnia wdrożeniowcowi cel tej pozycji. Jest on opcjonalny, lecz warto go
umieszczać. <env-entry-name> jest wykorzystywany przez kod Javy jako część wyszukiwania JNDI.
<env-entry-value> definiuje domyślną wartość przekazywaną wdrożeniowcowi. On także jest opcjonalny,
lecz wart umieszczenia. <env-entry-type> definiuje domyślną pełną nazwę klasy (PNK) pozycji. Typ może
być jednym z następujących — String, Byte, Short, Integer, Long, Boolean, Double lub Float
(wszystkie z ich pełną kwalifikacją java.lang). Typ pomaga wdrożeniowcowi w zorientowaniu się, czego
spodziewa się serwer. Powyższe znaczniki mogą wydawać się znajome osobom, którym nieobcy jest deskryptor
EJB, posiadają one identyczne nazwy i semantykę.
Kod Javy może odczytać wartości <env-entry> przy pomocy JNDI:
Context poczKontekst = new InitialContext();
Boolean pocztaPIN = (Boolean) poczKontekst.lookup("java:comp/env/pocztaPIN");
Wszystkie pozycje umieszczane są przez serwer w kontekście java:comp/env. Osoby nie znające JNDI
mogą o nim myśleć jako o bazie URL-a lub katalogu w systemie plików. Kontekst java:comp/env ma
własności tylko do odczytu i jest unikatowy dla aplikacji WWW, tak więc jeżeli dwie różne aplikacje WWW
zdefiniują taką samą pozycję środowiskową, pozycje te nie kolidują. Skrót nazwy kontekstu oznacza component
environment (środowisko elementów).
Przykład 12.1 przedstawia serwlet wyświetlający wszystkie jego pozycje środowiskowe, wykorzystując interfejs
JNDI do przeglądania kontekstu java:comp/env.
Przykład 12.1.
Przeglądanie kontekstu java:comp/env
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import javax.naming.*;

public class PrzeglPozSrod extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

try {
Context poczKontekst = new InitialContext();
NamingEnumeration enum = poczKontekst.listBindings("java:comp/env");

// Wykorzystywane są metody JDK 1.2; można to zrobić, ponieważ J2EE wymaga


JDK 1.2
while (enum.hasMore()) {
Binding wiazanie = (Binding) enum.next();
wyj.println("Nazwa: " + wiazanie.getName());
wyj.println("Typ: " + wiazanie.getClassName());
wyj.println("Wartość: " + wiazanie.getObject());
wyj.println();
}
}
catch (NamingException w) {
w.printStackTrace(wyj);
}
}
}
Przyjmując poprzednią pozycję web.xml, serwlet wygenerowałby:
Nazwa: pocztaPIN
Typ: java.lang.Boolean
Wartość: false
Proszę pamiętać, że serwer, który nie obsługuje J2EE niekoniecznie obsługuje powyższe znaczniki, ani żadne
inne opisywane w tym podrozdziale.
Odwołania do elementów EJB
Jeśli obiekt w pozycji środowiskowej to element EJB, należy wykorzystać specjalny znacznik <ejb-ref>.
Daje on serwletom możliwość obsługi EJB przy pomocy abstrakcyjnej nazwy. Wdrożeniowiec zapewnia
dostępność odpowiedniego elementu w trakcie uruchamiania w oparciu o ograniczenia podane w znaczniku
<ejb-ref>. Znacznik ten może zawierać znaczniki <description>, <ejb-ref-name>, <ejb-ref-
type>, <home>, <remote> i <ejb-link>. Poniżej przedstawiony jest typowy <ejb-ref>:
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>
Powyższe znaczniki posiadają również swoje odpowiedniki w EJB, i właściwie przykład ten jest zapożyczony z
książki „Enterprise JavaBeans” autorstwa Richarda Monson-Haefela (O'Reilly). <ejb-ref-name> nadaje
nazwę do poszukiwań JNDI. Poleca się (chociaż nie jest to konieczne) umieszczenie nazwy w podkontekście
ejb/, co sprawia, że pełna ścieżka do elementu to java:comp/env/ejb/KabinaGlowny. <ejb-ref-
type> musi posiadać wartość Entity lub Session (są to typy elementów EJB15). Natomiast element
<home> określa pełną nazwę klasy interfejsu właściwego EJB, podczas gdy element <remote> określa PNK
interfejsu zdalnego EJB.
Serwlet może odczytać odwołanie do elementu Kabina przy pomocy następującego kodu:
InitialContext poczKontekst = new InitialContext();
Object odw = poczKontekst.lookup("java:comp/env/ejb/KabinaPocz");
KabinaPocz pocz = (KabinaPocz) PortableRemoteObject.narrow(odw, KabinaPocz.class);
Jeżeli monter tworzący plik web.xml chce umieścić w odwołaniu EJB konkretny element EJB, informacja ta
może zostać przekazana wdrożeniowcowi przy pomocy opcjonalnego elementu <ejb-link>. Element <ejb-
link> powinien odwoływać się do <ejb-name> komponentu EJB zarejestrowanego w deskryptorze EJB w
tej samej aplikacji J2EE. Wdrożeniowiec posiada możliwość skorzystania z sugestii bądź opuszczenia jej.
Poniżej przedstawiona jest uaktualniona pozycja web.xml:
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
<ejb-link>KabinaElement</ejb-link>
</ejb-ref>

Odwołania do zewnętrznych fabryk zasobów


Ostatecznie, w przypadkach, w których pozycja środowiskowa to fabryka zasobów, wykorzystuje się znacznik
<resource-ref>. Fabryka to obiekt, który na żądanie tworzy inne obiekty. Fabryka zasobów tworzy obiekty
zasobów, takie jak połączenia z bazami danych lub kolejki wiadomości.
Znacznik <resource-ref> może zawierać znaczniki <description>, <res-ref-name>, <res-
type> i <res-auth>. Poniżej przedstawiony jest typowy znacznik <resource-ref>:
<resource-ref>
<description>Podstawowa baza danych</description>
<res-ref-name>jdbc/podstawBD</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>CONTAINER</res-auth>
</resource-ref>
W tym przypadku <description> również służy jak wsparcie dla wdrożeniowca i jest opcjonalny, ale
polecany. <res-ref-name> zawiera nazwę do poszukiwań JNDI. Poleca się, lecz nie jest to wymagane, aby
fabryki zasobów były umieszczane w podkontekście, który określa typ zasobów:
• jdbc/ dla fabryki JDBC javax.sql.DataSource
• jms/ dla javax.jms.QueueConnectionFactory lub
javax.jms.TopicConnectionFactory JMS
• mail/ dla fabryki JavaMail javax.mail.Session

15
Specyfikacja Servlet API 2.2 głosi, że „element ejb-ref-type zawiera przewidywany typ klasy Javy dla EJB
stanowiącego cel odwołania”. Jest to potwierdzony błąd. Właściwy cel jest taki, jak wymieniony powyżej.
• url/ dla fabryki java.net.URL
Element <res-type> określa PNK fabryki zasobów (a nie utworzonego zasobu). Typy fabryk na powyższej
liście to typy standardowe. Serwer posiada możliwość obsługi dodatkowych typów; nie mogą zostać zastosowane
fabryki użytkownika. Specyfikacja nadchodzącej wersji 1.3 J2EE zawiera mechanizm „łącznika” służący do
rozszerzania tego modelu o fabryki zdefiniowane przez użytkownika.
<res-auth> służy do przekazania serwerowi informacji, kto jest odpowiedzialny za uwierzytelnianie.
Znacznik ten może posiadać dwie wartości — CONTAINER lub SERVLET. Jeżeli jego wartość wynosi
CONTAINER, wtedy kontener serwletów (serwer J2EE) obsługuje uwierzytelnianie przed dowiązaniem fabryki
do JDNI, przy pomocy danych dostarczonych przez wdrożeniowca. Jeżeli wynosi SERVLET, uwierzytelnienie
musi zostać przeprowadzone programowo przez serwlet. Poniższy kod przedstawia ten mechanizm:
InitialContext poczKontekst = new InitialContext();
DataSource zrodlo = (DataSource) poczKontekst.lookup
("java:comp/env/jdbc/podstawBD");

// Jeżeli "CONTAINER"
Connection pol1 = zrodlo.getConnection();

// Jeżeli "SERVLET"
Connection pol2 = zrodlo.getConnection("user", "password");
Powyższe znaczniki również posiadają swoje odpowiedniki w deskryptorze EJB. Jedyną różnicą jest fakt, że w
EJB dwie możliwe wartości <res-auth> to Container i Application (proszę zauważyć niewyjaśnioną
różnicę w wielkości liter).

Dystrybucja serwletów w środowisku J2EE


Ostatnia różnica pomiędzy serwletami w środowisku samodzielnym i serwletami w środowisku J2EE związana
jest z niewielką zmianą w zasadach dystrybucji sesji. Podczas gdy standardowy serwer WWW musi obsługiwać
w sesji jedynie obiekty java.io.Serializable aplikacji dystrybuowalnej, serwer zgodny z J2EE, który
obsługuje dystrybuowalny kontener serwletów musi również obsługiwać kilka dodatkowych typów obiektów:
• każdy javax.ejb.EJBObject
• każdy javax.ejb.EJBHome
• każdy javax.transaction.UserTransaction
• javax.naming.Context dla java:comp/env
Wszystkie powyższe interfejsy to interfejsy, które nie wykorzystują Serializable. W celu przeniesienia tych
obiektów kontener może wykorzystać swój własny mechanizm, który może (ale nie musi) być oparty na
serializacji. Dodatkowe typy klas mogą być obsługiwane w zależności od serwera, ale powyższe to jedyne typy,
których obsługa jest gwarantowana.
Rozdział 13. Internacjonalizacja

Pomimo swojej nazwy, sieć WWW musi przebyć jeszcze długą drogę, zanim będzie mogła być uważana za
całkowicie światową. Oczywiście, kable doprowadzają zawartość sieci do prawie wszystkich krajów świata.
Jednak, aby zawartość sieci była naprawdę międzynarodowa, musi ona być możliwa do odczytania przez każdą
osobę ją odbierającą — własność rzadko dziś spotykana, gdy większość stron WWW posiada jedynie wersję
angielską.
Sytuacja zaczyna się jednak zmieniać. Wiele największych witryn WWW posiada obszary zaprojektowane dla
języków innych niż angielski. Na przykład, strona główna firmy Netscape jest dostępna w języku angielskim pod
adresem http://home.netscape.com/index.html, natomiast w wersji francuskojęzycznej pod adresem
http://home.netscape.com/fr/index.html, a dla osób znających inne języki pod wieloma innymi URL-ami.
Serwery WWW mogą również obsługiwać rozwiązanie pojedyncze, w którym pojedynczy URL może zostać
wykorzystany do przeglądania tej samej zawartości w różnych językach, których wybór zależny jest od
preferencji klienta. To, który język zostanie wyświetlony jest uzależnione od konfiguracji przeglądarki16. Chociaż
technika ta daje wrażenie, że następuje dynamiczne tłumaczenie, tak naprawdę serwer dysponuje po prostu
kilkoma specjalnie nazwanymi wersjami statycznego dokumentu.
Podczas gdy te techniki działają dobrze w przypadku statycznych dokumentów, nie rozwiązują one jednak
problemu internacjonalizacji i lokalizacji zawartości dynamicznej. Jest to temat niniejszego rozdziału. Opisane
zostaną tu sposoby wykorzystania możliwości internacjonalizacji dodanych w JDK1.1. przez serwlety w celu
prawdziwego rozszerzenia sieci WWW na cały świat.
Po pierwsze należy omówić terminologię. Internacjonalizacja (Internationalization — słowo często miłosiernie
skracane do I18N, ponieważ rozpoczyna się od I, kończy na N, a pomiędzy nimi jest 18 liter) jest zadaniem
uczynienia programu na tyle elastycznym, aby można go było uruchomić w każdym miejscu. Lokalizacja
(Localization, często skracana do L10N) to proces ustawiania programu tak, aby działał w konkretnym miejscu.
Większa część tego rozdziału opisuje internacjonalizację serwletów. Lokalizacja zostanie opisana jedynie na
przykładach dat, godzin, liczb i innych obiektów, które domyślnie obsługuje Java.

Języki zachodnioeuropejskie
Na początku zostanie opisany sposób wyświetlania przez serwlet strony utworzonej w języku
zachodnioeuropejskim, takim jak angielski, hiszpański, niemiecki, francuski, włoski, holenderski, norweski,
fiński lub szwedzki. W przykładzie wyświetlony zostanie napis „Witaj świecie!” po hiszpańsku, jak
przedstawiono to na rysunku 13.1.

16
Wiele starszych przeglądarek nie obsługuje jednak dostosowywania języków. Na przykład, własność ta została
wprowadzona po raz pierwszy w przeglądarkach Netscape Navigator 4 i Microsoft Internet Explorer 4.
Rysunek 13.1.
En Español — ¡Hola
Mundo!

Proszę zauważyć zastosowanie specjalnych znaków ñ i ¡. Znaki takie jak te, chociaż praktycznie nieobecne w
języku angielskim, są przypisane do języków zachodnioeuropejskich. Serwlety posiadają dwa sposoby
generowania takich znaków — poprzez encje znakowe HTML i kody ucieczkowe Unicode.

Encje znakowe HTML


HTML 2.0 wprowadził możliwość wyświetlania specyficznych sekwencji znaków na stronie HTML jako znaku
pojedynczego. Sekwencje te, nazywane encjami znakowymi, rozpoczynają się znakiem &, a kończą średnikiem
(;). Encje znakowe mogą mieć nazwy, jak i wartości numeryczne. Na przykład, nazwana encja &ntilde;
przedstawia ñ, a &iexcl; przedstawia ¡. Kompletna lista znaków specjalnych i ich nazw jest podana w dodatku
A, „Encje znakowe”. Przykład 13.1 przedstawia serwlet wykorzystujący nazwane encje w celu wyświetlenia
„Witaj świecie” po hiszpańsku.
Przykład 13.1.
Powitanie dla posługujących się językiem hiszpańskim, przy pomocy nazwanych encji znakowych
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajHiszpania extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "es");

wyj.println("<HTML><HEAD><TITLE>En Espa&ntilde;ol</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H3>En Espa&ntilde;ol:</H3>");
wyj.println("&iexcl;Hola Mundo!");
wyj.println("</BODY<>/HTML>");
}
}
Można zauważyć, że oprócz wykorzystywania encji znakowych powyższy serwlet nadaje swojemu nagłówkowi
Content-Language wartość es. Nagłówek Content-Language jest wykorzystywany do określania
języka następujących po nim encji. W tym przypadku serwlet wykorzystuje nagłówek do wskazania klientowi, że
strona jest napisana w języku hiszpańskim (Español). Większość klientów ignoruje tę informację, lecz do
dobrego tonu należy jej wysyłanie. Języki są zawsze przedstawiane przy pomocy dwuliterowych skrótów
zapisanych małymi literami. Kompletna lista jest dostępna w standardzie ISO-639 pod adresem
http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt.
Encje znakowe mogą być również określane przy pomocy liczb. Na przykład, &#241; przedstawia ñ, a &#161;
przedstawia ¡. Liczba odnosi się do dziesiętnej wartości znaku w standardzie ISO-8859-1 (Latin-1), który
zostanie opisany w dalszej części niniejszego rozdziału. Kompletna lista wartości numerycznych encji
znakowych również znajduje się w dodatku E. Przykład 13-2 przedstawia serwlet WitajHiszpania
zmieniony tak, aby wykorzystywał encje numeryczne.
Przykład 13.2.
Powitanie dla posługujących się językiem hiszpańskim, przy pomocy numerycznych encji znakowych
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajHiszpania extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "es");

wyj.println("<HTML><HEAD><TITLE>En Espa&#241;ol</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H3>En Espa&#241;ol:</H3>");
wyj.println("&#161;Hola Mundo!");
wyj.println("</BODY<>/HTML>");
}
}
Niestety, z wykorzystaniem encji znakowych wiąże się jeden poważny problem — działają one jedynie ze
stronami HTML. Jeżeli wynikiem serwletu nie jest kod HTML, strona będzie wyglądać podobnie do
przedstawionej na rysunku 13.2. Aby obsługiwać wyniki nie będące HTML, należy wykorzystać kody
ucieczkowe Unicode.
Rysunek 13.2.
Nie do końca hiszpański

Kody ucieczkowe Unicode


W Javie znaki, łańcuchy i identyfikatory są złożone wewnętrznie z 16-bitowych (2-bajtowych) znaków Unicode
2.0. Unicode został wprowadzony przez Unicode Consortium, które opisuje standard w następujący sposób
(proszę zobaczyć http://unicode.org):

Światowy standard znaków Unicode to system kodowania znaków


zaprojektowany w celu obsługi wymiany, przetwarzania i
wyświetlania tekstów pisanych w różnych językach
współczesnych. Dodatkowo, obsługuje on klasyczne i
historyczne teksty w wielu językach pisanych.
W swojej aktualnej wersji (2.0) standard Unicode zawiera 38,885 osobno oznaczonych znaków
odziedziczonych ze wspieranych skryptów. Znaki te umożliwiają obsługę głównych języków pisanych w
Amerykach, Europie, Bliskim Wschodzie, Afryce, Indiach, Azji i rejonie Pacyfiku.
Większa ilość informacji na temat Unicode jest dostępna pod adresem http://www.unicode.org, a także w książce
„The Unicode Standard, Version 2.0” (Addison-Wesley). Proszę zauważyć, że pomimo wprowadzenia Unicode
3.0, Java wciąż obsługuje wersję 2.0.
Wykorzystanie przez Javę Unicode jest bardzo ważne dla niniejszego rozdziału, ponieważ oznacza ono, że
serwlet może wewnętrznie przedstawiać właściwie każdy znak w powszechnie stosowanym języku pisanym. 16-
bitowe znaki Unicode są przedstawiane w 7-bitowym kodzie źródłowym US-ASCII przy pomocy kodów
ucieczkowych Unicode o postaci \uxxxx, gdzie xxxx oznacza sekwencję czterech cyfr w formacie
szesnastkowym. Kompilator Javy traktuje każda sekwencję ucieczkową jako pojedynczy znak.
Nieprzypadkowo i dla zachowania wygody pierwsze 256 znaków Unicode (\u0000 do \u00ff) są równe z
256 znakami ISO 8859-1 (Latin-1). Tak więc znak ñ może zostać opisany jako \u00f1, a znak ¡ jako \u00a1.
Kompletna lista sekwencji ucieczkowych Unicode dla znaków ISO-8859-1 jest również zawarta w dodatku.
Przykład 13.2 przedstawia WitajHiszpania przepisany przy pomocy kodów ucieczkowych Unicode.
Przykład 13.2.
Powitanie dla posługujących się językiem hiszpańskim, przy pomocy kodów ucieczkowych Unicode
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajHiszpania extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "es");

wyj.println("En Espa\u00f1ol:");
wyj.println("\u00a1Hola Mundo!");
}
}
Wynik powyższego serwletu wyświetla się prawidłowo, kiedy wykorzystywany jako część strony HTML lub
kiedy wykorzystywany jest do wyświetlania zwykłego tekstu.

Hołdowanie lokalnym zwyczajom


Teraz wiadomo już jak wykorzystywać encje znakowe HTML lub kody ucieczkowe Unicode do wyświetlania
znaków w językach zachodnioeuropejskich. Pozostaje pytanie, co powiedzieć przy pomocy tych języków?
Generalnie, problem tłumaczenia najlepiej pozostawić odpowiedniemu zespołowi odpowiedzialnemu za
lokalizację, Jednak w niektórych przypadkach Java dostarcza pewnej pomocy.
Na przykład, jeżeli oprócz powiedzenia „Witaj świecie” przykładowy serwlet powinien wyświetlać aktualną datę
w formacie naturalnie rozumianym przez odbiorcę. To, co mogłoby być skomplikowanym problemem z
formatowaniem jest tak naprawdę stosunkowo proste, ponieważ JDK 1.1 posiada wbudowaną obsługę lokalizacji
dynamicznych obiektów takich jak daty i godziny.
Sztuczka polega na zastosowaniu egzemplarza java.text.DateFormat odpowiedniego dla docelowego
odbiorcy. Obiekt DateFormat może przekonwertować Date do odpowiednio zlokalizowanego obiektu
String. Na przykład, znacznik czasu utworzony w języku angielskim jako „February 16, 1998 12:36:18 PM
PST” zostanie zapisany po hiszpańsku jako „16 de febrero de 1998 12:36:18 GMT-8:00”.
Obiekt DateFormat jest tworzony przy pomocy metody fabryki, która przyjmuje style formatowania (krótki,
średni, długi, pełny) oraz obiektu java.util.Locale, który identyfikuje docelowego odbiorcę (amerykański
angielski, podstawowy chiński itp.). Najpopularniejszy konstruktor Locale pobiera dwa parametry —
dwuliterowy skrót języka zapisany małymi literami (jak opisano wcześniej) oraz dwuliterowy kod kraju zapisany
dużymi literami zdefiniowany przez ISO-3166 (dostępny pod adresem http://www.chemie.fu-
berlin.de/diverse/doc/ISO_3166.html). Pusty łańcuch kodu kraju wskazuje na kraj domyślny dla danego języka.
Przykład 13.4 przedstawia serwlet WitajHiszpania wykorzystujący obiekt DateFormat w celu
wyświetlenia aktualnego czasu w formacie naturalnie rozumianym przez odbiorców posługujących się językiem
hiszpańskim.
Przykład 13.4.
Powitanie dla posługujących się językiem hiszpańskim, łącznie ze zlokalizowaną datą
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajHiszpania extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "es");

Locale lokal = new Locale("es", "");


DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
fmt.setTimeZone(TimeZone.getDefault());
wyj.println("En Espa\u00f1ol:");
wyj.println("\u00a1Hola Mundo!");
wyj.println(fmt.format(new Date()));
}
}
Powyższy serwlet na początku tworzy obiekt Locale przedstawiający ogólne środowisko jako hiszpańskie.
Następnie wykorzystuje ten Locale do utworzenia egzemplarza DateFormat, który formatuje daty w języku
hiszpańskim. Następnie ustawia on strefę czasową na strefę domyślną (strefę czasową serwera). Dzieje się tak,
ponieważ obiekt DateFormat formatuje swoje czasy w sposób taki, aby pasowały one do strefy czasowej, w
której znajduje się domyślny klient, w tym przypadku Hiszpanii. Ponieważ serwlet ten nie może być pewnym,
czy podejmuje właściwe decyzje, zamienia on wartość domyślną i ustawia strefę czasową tak, aby pasowała do
serwera. Oczywiście lepsze byłoby ustawienie strefy czasowej tak, aby pasowała ona do miejsca, w którym
znajduje się klient, ale nie jest to aktualnie możliwe bez dodatkowych informacji dostarczanych przez
użytkownika. Ostatecznie, po wypowiedzeniu swojego „Witaj świecie”, serwlet wyświetla prawidłowo
sformatowana datę i godzinę. Wynik działania powyższego skryptu jest przedstawiony na rysunku 13.3.
Rysunek 13.3.
Hola Mundo con Tiempo

Powyższy przykład stanowi jedynie bardzo krótki podgląd możliwości dynamicznego formatowania zawartości
Javy. Osoby zainteresowane bardziej skomplikowanym formatowaniem powinny obejrzeć kilka przydatnych klas
znajdujących się w pakiecie java.text. Szczególnie należy zwracać uwagę na te będące rozszerzeniem
java.text.Format.

Języki spoza Europy Zachodniej


Teraz omówione zostanie tworzenie przez serwlet strony w języku spoza Europy Zachodniej, takim jak rosyjski,
czeski, litewski czy japoński. Aby pojąć sposoby pracy związane z tymi językami, należy najpierw poznać pewne
mechanizmy zachodzące poza ekranem w poprzednich przykładach.

Kodowanie
Na początku sytuacja zostanie omówiona z punktu widzenia przeglądarki. Proszę wyobrazić sobie spełnianie
pracy przeglądarki. Wykonuje się żądanie HTTP dla pewnego URL-a i otrzymuje odpowiedź. Odpowiedź ta w
najprostszym znaczeniu nie jest niczym więcej niż długą sekwencją bajtów. Ale w jaki sposób można się
dowiedzieć, jak wyświetlić tę odpowiedź?
Popularnym i właściwie domyślnym sposobem jest przyjęcie, że każdy bajt reprezentuje jeden z 256 możliwych
znaków, a następnie przyjęcie, że znak, który bajt reprezentuje może zostać określony przez wyszukanie wartości
bajtu w pewnej tabeli. Domyślna tabela jest zdefiniowana przez standard ISO-8859-1, noszący także nazwę
Latin-1. Zawiera on odwzorowanie typu bajt-do-znaku dla najpopularniejszych znaków stosowanych w językach
zachodnioeuropejskich. Tak więc domyślnie przeglądarka może otrzymywać sekwencję bajtów i konwertować ją
do sekwencji znaków zachodnioeuropejskich.
Co natomiast należy uczynić, jeżeli pragnie się otrzymać tekst, który nie jest napisany w języku
zachodnioeuropejskim? Należy pobrać długą sekwencję bajtów odpowiedzi i zinterpretować ją w inny sposób,
przy pomocy innego odwzorowania sekwencja-bajtów-do-znaku. Mówiąc językiem technicznym, należy
zastosować inne kodowanie17. Istnieje nieskończona ilość potencjalnych kodowań. Na szczęście, najczęściej
stosuje się jedynie kilkadziesiąt z nich.

17
Kodowanie (charset — odwzorowanie sekwencja-bajtów-do-znaku) to nie to samo co zestaw znaków (character set). Pełne
wyjaśnienie można znaleźć w dokumencie RFC 2278 pod adresem http://www.ietf.org/rfc/rfc2278.txt.
Niektóre kodowania wykorzystują znaki jednobajtowe w sposób podobny do ISO-8859-1, chociaż z innym
odwzorowaniem bajt-do-znaku. Na przykład, ISO-8859-5 definiuje odwzorowanie bajt-do-znaku dla znaków
cyrylicy (alfabetu rosyjskiego), a ISO-8859-8 definiuje odwzorowanie dla alfabetu hebrajskiego18.
Inne kodowania wykorzystują znaki wielobajtowe, w których jeden znak może być przedstawiany przez więcej
niż jeden bajt. Najczęściej zdarza się to w przypadku języków zawierających tysiące znaków, takich jak chiński,
japoński czy koreański — często nazywanych po prostu CJK. Kodowania wykorzystywane do wyświetlania tych
języków to między innymi Big5 (chiński), Shift_JIS (japoński) i EUC-KR (koreański). Tabela zawierająca języki
i odpowiadające im kodowania znajduje się w dodatku F, „Kodowania”.
Oznacza to, że jeżeli znane jest kodowanie, w którym zapisana została odpowiedź, można określić sposób
interpretacji otrzymanych bajtów. Pozostaje jedno pytanie — jak określić kodowanie? Można wykonać to na
dwa sposoby. Po pierwsze, można zażądać ujawnienia kodowania przez użytkownika. W przeglądarce Netscape
Navigator 4 można to wykonać poprzez opcję menu View → Encoding, w Netscape Navigator 6 poprzez View
→ Character Coding. W przeglądarce Microsoft Internet Explorer 4 poprzez Widok → Czcionki, a Microsoft
Internet Explorer 5 poprzez Widok → Kodowanie. Podejście to często wymaga, aby użytkownik wypróbował
kilku kodowań, zanim obraz zacznie wyglądać sensownie. Drugą możliwością jest określenie kodowania przez
serwer (lub serwlet) w nagłówku Content-Type. Na przykład, następująca wartość Content-Type:
text/html; charset=ISO-8859-5
wskazuje, że wykorzystywane kodowanie to ISO-8859-5. Niestety, niektóre starsze przeglądarki mogą błędnie
zinterpretować dodanie kodowania do nagłówka Content-Type.

Tworzenie wyników zakodowanych


Kiedy znana jest już zasada działania kodowań z perspektywy przeglądarki, można wrócić do perspektywy
serwletu. Rola serwletu w tym działaniu jest następująca:
1. Wybranie kodowania i ustawienie go dla serwletu.
2. Wybranie PrintWriter dla tego kodowania.
3. Wyświetlenie znaków, które mogą zostać wyświetlone przy pomocy tego kodowania.
Przykład 13.5 przedstawia serwlet wyświetlający „Witaj świecie” oraz aktualną datę i godzinę w języku
rosyjskim.
Przykład 13.5.
Powitanie dla użytkowników rosyjskich
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajRosja extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain; charset=ISO-8859-5");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "ru");

Locale lokal = new Locale("ru", "");


DateFormat pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("Po rosyjsku (cyrylica):");
wyj.print("\u0417\u0434\u0440\u0430\u0432\u0441\u0442"); // Witaj świecie
wyj.println("\u0432\u0443\u0439, \u041c\u0438\u0440");
}
}
Rysunek 13.4 przedstawia wynik uruchomienia przykładu 13-5.

18
Warto zapamiętać, że dla prawie wszystkich kodowań wartości bajtów pomiędzy dziesiętnym 0 i 127 przedstawiają
standardowe znaki US-ASCII, co pozwala na dodanie angielskiego tekstu do strony utworzonej w prawie dowolnym języku.
Rysunek 13.4.
Royjskie powitanie

Powyższy serwlet rozpoczyna działanie od ustawienia typu zawartości na text/plain oraz kodowania na
ISO-8859-5. Następnie wywołuje odp.getWriter() tak, jak zazwyczaj — poza tym, że w tym przypadku
wynik tej metody otrzymuje specjalny PrintWriter. Ten PrintWriter koduje cały wynik działania
serwletu przy pomocy kodowania ISO-8859-5., ponieważ to kodowanie zostało wymienione w nagłówku
Content-Type. Tak więc druga linia jest równoznaczna poniższej:
PrintWriter wyj = new PrintWriter(
new OutputStreamWriter(odp.getOutputStream(), "ISO-8859-5"), true);
Należy również zapamiętać, że wywołanie odp.GetWriter() może spowodować wyjątek
UnsupportingEncodingException, jeżeli kodowanie nie zostanie rozpoznane przez Javę19, lub
IllegalStateException, jeżeli getOutputStream() został wywołany wcześniej w tym samym
żądaniu.
Następnie serwlet tworzy obiekt Locale przy pomocy języka ru w celu przedstawienia ogólnego rosyjskiego
środowiska, po czym tworzy pasujący do niego DateFormat. Na końcu wyświetla po rosyjsku wyrażenie
równoznaczne z „Witaj świecie”, przy pomocy kodów ucieczkowych Unicode, po czym wyświetla aktualną datę
i godzinę.
Aby powyższy serwlet mógł działać, ścieżka klas serwera musi zawierać klasy sun.io.CharToByte lub ich
ekwiwalent. Poza tym, aby znaki rosyjskie (lub innego języka) były wyświetlane poprawnie w przeglądarce, musi
ona obsługiwać dane kodowanie i mieć dostęp do potrzebnych czcionek w celu wyświetlenia kodowania.
Większa ilość informacji na temat możliwości internacjonalizacji przeglądarki Netscape Navigator jest dostępna
pod adresem http://home.netscape.com/eng/intl/index.html. Większa ilość informacji na temat możliwości
internacjonalizacji przeglądarki Microsoft Internet Explorer jest dostępna pod adresem
http://www.microsoft.com/ie/intlhome.htm.

Odczyt i zapis wyników zakodowanych


Ręczne wprowadzanie setek lub tysięcy kodów ucieczkowych Unicode w plikach źródłowych Javy może być
czynnością bardzo powolną. Łatwiejszym sposobem jest utworzenie serwletu przy pomocy
zinternacjonalizowanego edytora i zapisanie pliku przy pomocy odpowiedniego kodowania. Jeżeli kodowanie to
jest rozpoznawane przez Javę, źródło może być skompilowane przy pomocy niemal każdego nowoczesnego
kompilatora Javy. Na przykład, przy pomocy kompilatora javac dołączonego do JDK, plik źródłowy serwletu
zakodowany przy pomocy ISO-8859-5, powinien zostać skompilowany w następujący sposób:
javac –encoding ISO-8859-5 WitajRosja.java
Plik źródłowy WitajRosja.java powinien wyglądać niemal identycznie jak ten przedstawiony w przykładzie 13.5,
z jedyną różnicą taką, że kody ucieczkowe Unicode mogą zostać zastąpione przez oryginalne znaki rosyjskie.
Jeżeli plik ten zostanie otwarty przy pomocy rosyjskiego edytora tekstu, pomiędzy cudzysłowami w
wyj.println() można będzie zobaczyć znaki rosyjskie, W dowolnym innym edytorze znaki te — i zależnie
od kodowania nawet cały plik — wyglądałby jak zbiór śmieci. Co interesujące, zawartość pliku .class jest
identyczna niezależnie od tego, czy kompilowano go przy pomocy kodów ucieczkowych Unicode, czy
zakodowanych plików źródłowych.
Inną opcją, przydatną, jeżeli programista nie zna języka, w którym utworzona została strona wynikowa, jest
utworzenie serwletu przy pomocy standardowego kodu ASCII, ale odczytanie zlokalizowanego tekstu z
zakodowanego pliku. Na przykład, jeżeli rosyjski tekst „Witaj świecie” został zapisany przez osobę z zespołu
lokalizującego w pliku o nazwie WitajSwiecie.ISO-8859-5, przy pomocy kodowania ISO-8859-5, wtedy serwlet
może odczytać ten plik i wysłać zawartość do przeglądarki przy pomocy kodowania ISO-8859-5, jak
przedstawiono w przykładzie 13.6.

19
W niektórych wczesnych wersjach Javy może ona w niektórych sytuacjach błędnie wywoływać wyjątek
IllegalArgumentException, jeżeli kodowanie nie zostanie rozpoznane.
Przykład 13.6.
Wysyłanie zlokalizowanego wyniku odczytanego z pliku
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WitajRosjaCzytanie extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain; charset= ISO-8859-5");
PrintWriter wyj = odp.getWriter();
odp.setHeader("Content-Language", "ru");

Locale lokal = new Locale("ru", "");


DateFormat pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("Po rosyjsku:");

try {
FileInputStream fis =
new FileInputStream(zad.getRealPath("/WitajSwiecie.ISO-8859-5"));
InputStreamReader isr = new InputStreamReader(fis, "ISO-8859-5");
BufferedReader czytanie = new BufferedReader(isr);
String linia = null;
while ((linia = czytanie.readLine()) != null) {
wyj.println(linia);
}
}
catch (FileNotFoundException w) {
// Brak powitania
}

wyj.println(pelny.format(new Date()));
}
}
Powyższy serwlet jest właściwie konwerterem kodowań znaków. Odczytuje on tekst WitajSwiecie.ISO-8859-5
zakodowany w standardzie ISO-8859-5, i wewnętrznie konwertuje go do Unicode. Następnie wyświetla ten sam
tekst poprzez konwersję go z Unicode do ISO-8859-5.

Większa ilość języków


Teraz należy podnieść nieco poprzeczkę i spróbować czegoś, co stało się możliwe dopiero niedawno. Napisany
zostanie serwlet zawierający kilka języków na tej samej stronie. Właściwie serwlet taki został już utworzony.
Poprzedni przykład, WitajRosja, zawierał zarówno tekst angielski/polski, jak i rosyjski. Można jednak
zauważyć, że jest to przypadek specjalny. Dodanie do strony tekstu angielskiego jest prawie zawsze możliwe, z
powodu wygodnego faktu, że prawie wszystkie kodowania zawierają 128 znaków US-ASCII. W bardziej
ogólnym przypadku, kiedy tekst na stronie składa się z różnych języków, a żadne z wymienionych poprzednio
kodowań nie zawiera wszystkich potrzebnych znaków, konieczna jest inna technika.

UCS-2 i UTF-8
Najlepszym sposobem tworzenia strony zawierającej większa ilość języków jest wyświetlenie klientom 16-
bitowych znaków Unicode. Istnieją dwa popularne sposoby wykonania tego działania — UCS-2 i UTF-8. UCS-2
(Universal Character Set, 2-byte form — Uniwersalny Zestaw Znaków, forma dwubajtowa) wysyła znaki
Unicode w czymś, co może być nazwane ich naturalnym formatem, 2 bajty na znak. Wszystkie znaki, włączając
w to znaki US-ASCII, wymagają dwóch bajtów. UTF-8 (UCS Transformation Format, 8-bit form — Format
Transformacji UTF, forma 8-bitowa) to kodowanie o zmiennej długości. Przy pomocy UTF-8, znak Unicode jest
zmieniany w swoją 1-, 2- lub 3-bajtową reprezentacją. Generalnie, UTF-8 jest bardziej wydajne niż UCS-2,
ponieważ może zakodować znak z zestawu US-ASCII przy pomocy zaledwie jednego bajtu. Z tego powodu
zastosowanie w sieci WWW UTF-8 mocno przewyższa UCS-2. Większa ilość informacji na temat UTF-8 jest
dostępna w dokumencie RFC 2279 pod adresem http://www.ietf.org/rfc/rfc2279.txt.
Przed przejściem dalej należy zapamiętać, że obsługa UTF-8 nie jest jeszcze gwarantowana. Netscape po raz
pierwszy wprowadził obsługę kodowania UTF-8 w przeglądarce Netscape Navigator 4, a Microsoft w Internet
Explorer 4.
Tworzenie UTF-8
Przykład 13.7 przedstawia serwlet wykorzystujący UTF-8 do wyświetlenia „Witaj świecie!” i aktualnego czasu
(w lokalnej strefie czasowej) po angielsku, hiszpańsku, niemiecku, czesku i rosyjsku.
Przykład 13.7.
Serwletowa wersja kamienia z Rosetty
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

public class WitajRosetta extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
Locale lokal;
DateFormat pelny;

try {
odp.setContentType("text/plain; charset=UTF-8");
PrintWriter wyj = odp.getWriter();

lokal = new Locale("en", "US");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("American English:");
wyj.println("Hello World!");
wyj.println(pelny.format(new Date()));
wyj.println();

lokal = new Locale("es", "");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("En Espa\u00f1ol:");
wyj.println("\u00a1Hola Mundo!");
wyj.println(pelny.format(new Date()));
wyj.println();

lokal = new Locale("cs", "");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("\u010ceski:");
wyj.println("Halo svete!");
wyj.println(pelny.format(new Date()));
wyj.println();

lokal = new Locale("de", "");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("Deutsch:");
wyj.println("Hallo Welt!");
wyj.println(pelny.format(new Date()));
wyj.println();

lokal = new Locale("fr", "");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("Fran\u00e7ais:");
wyj.println("Salut le monde!");
wyj.println(pelny.format(new Date()));
wyj.println();

lokal = new Locale("ru", "");


pelny = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG,
lokal);
wyj.println("Po rosyjsku (cyrylica):");
wyj.print("\u0417\u0434\u0440\u0430\u0432\u0441\u0442");
wyj.println("\u0432\u0443\u0439, \u041c\u0438\u0440");
wyj.println(pelny.format(new Date()));
wyj.println();
}
catch (Exception w) {
log(ServletUtils.getStackTraceAsString(w));
}
}
}
Rysunek 13.5 przedstawia stronę wyświetlaną przez powyższy serwlet.
Rysunek 13.5.
Prawdziwe witaj świecie

Aby powyższy serwlet działał tak, jak opisano, serwer musi obsługiwać JDK w wersji 1.1.6 lub późniejszej.
Poprzednie wersje Javy wyświetlają wyjątek UnsupportedEncodingException podczas próby pobrania
PrintWriter, a strona pozostaje pusta. Problemem jest brakujący alias kodowania. Java posiadała obsługę
kodowania UTF-8 od wprowadzenia JDK 1.1. Niestety JDK wykorzystywał nazwę kodowania UTF8, podczas
gdy przeglądarki spodziewały się nazwy UTF-8. Tak więc kto miał rację? Nie było to jasne do początków 1998,
kiedy IANA (Internet Assigned Numbers Authority — Komisja Przyznawania Numerów Internetowych)
zadeklarowała, że preferowaną nazwą będzie UTF-8. (Proszę zobaczyć http://www.isi.edu/in-
notes/iana/assignments/character-sets). Niewiele później JDK 1.1.6 dodał UTF-8 jako alternatywny alias dla
kodowania UTF8. W celu zachowania maksymalnej przenośności pomiędzy wersjami Javy można wykorzystać
bezpośrednio nazwę UTF8 w następującym kodzie:
odp.setContentType("text/plain; charset=UTF-8");
PrintWriter wyj = new PrintWriter(
new OutputStreamWriter(odp.getOutputStream(), "UTF8"), true);
Również klient musi posiadać obsługę kodowania UTF-8 oraz mieć dostęp do wszystkich potrzebnych czcionek.
W przeciwnym wypadku część wyświetlanych danych może być niepoprawna.

Dynamiczna negocjacja języka


Teraz poprzeczka zostanie podniesiona jeszcze wyżej (przypuszczalnie do maksymalnej wysokości) poprzez
serwlet, który zmienia swoją zawartość tak, aby pasowała ona do preferencji językowych klienta. Pozwala to na
wykorzystanie jednego URL-a przez użytkowników mówiących różnymi językami.
Preferencje językowe
Istnieją dwie metody, przy pomocy których serwlet może poznać preferencje językowe klienta. Po pierwsze,
przeglądarka może wysłać tę informacje jako część swojego żądania. Nowsze przeglądarki, począwszy od
Netscape Navigator 4 i Microsoft Internet Explorer 4, pozwalają użytkownikom na określenie ich preferowanych
języków. W przeglądarce Netscape Navigator 4 i 6 jest to wykonywane w menu Edit → Preferences →
Navigator → Languages. W Microsoft Internet Explorer 4 jest to wykonywane w menu Widok → Opcje
internetowe → Ogólne → Języki; Internet Explorer 5 przesuwa tę opcje z menu Widok do Narzędzia.
Przeglądarka wysyła preferencje językowe użytkownika przy pomocy nagłówka HTP Accept-Language.
Wartość tego nagłówka określa język lub języki, które preferuje otrzymywać klient. Proszę zauważyć, że
specyfikacja HTTP pozwala na zignorowanie tych preferencji. Wartość nagłówka Accept-Language
wygląda podobnie do następującej linii kodu:
en, fr, de, pl, cs, zh-TW
Oznacza to, że klient zna język angielski, francuski, niemiecki, polski, czeski i chiński w wersji tajwańskiej.
Konwencja głosi, że języki wymieniane są w kolejności preferencji. Każdy język może również zawierać
wartość q, która wskazuje, na skali od 0.0 do 1.0, siłę tej preferencji. Domyślna wartość q wynosi 1.0
(maksymalna preferencja). Wartość nagłówka Accept-Language zawierająca wartości q wygląda
następująco:
en, fr;q=0.8, de;q=0.7, pl;q=0.3, cs;q=0.2, zh-TW;q=0.1
Powyższa wartość nagłówka znaczy mniej więcej to samo, co ta w poprzednim przykładzie.
Drugą metodą odczytywania przez serwlet preferencji językowych klienta jest zapytanie. Na przykład, serwlet
może wygenerować formularz zapytujący klienta, jaki język preferuje. Następnie może zapamiętać i wykorzystać
odpowiedź, na przykład stosując techniki śledzenia sesji omówione w rozdziale 7, „Śledzenie sesji”.

Preferencje kodowania
Oprócz nagłówka HTTP Accept-Language, przeglądarka może wysyłać nagłówek Accept-Charset,
który określa, jakie kodowania rozpoznaje. Wartość nagłówka Accept-Charset może wyglądać następująco:
iso-8859-1, utf-8
Powyższa wartość wskazuje, że przeglądarka zna kodowania ISO-8859-1 i UTF-8. Jeżeli Accept-Charset
nie jest ustawiony, lub jego wartość zawiera gwiazdkę (*), można przyjąć, że klient przyjmuje wszystkie
kodowania. Proszę zauważyć, że aktualna przydatność tego nagłówka jest ograniczona — wysyłają go tylko
niektóre przeglądarki, a i one zazwyczaj wysyłają gwiazdkę.

Pakiety zasobów
Wykorzystując Accept-Language (i w niektórych przypadkach Accept-Charset), serwlet może określić
język, w którym mówi do konkretnego klienta. Ale jak serwlet może efektywnie zarządzać kilkoma
zlokalizowanymi wersjami strony? Można w tym celu wykorzystać wbudowaną w Javę obsługę pakietów
zasobów.
Pakiet zasobów przechowuje zbiór zlokalizowanych zasobów odpowiednich dla danej lokalizacji. Na przykład,
pakiet zasobów dla lokalizacji francuskiej może zawierać francuskie tłumaczenie wszystkich zdań wyświetlanych
przez serwlet. Następnie, kiedy serwlet określi, że preferowanym językiem klienta jest francuski, może
załadować ten pakiet zasobów i wykorzystać zapamiętane zdania. Wszystkie pakiety zasobów to rozszerzenia
java.util.ResourceBundle. Serwlet może załadować pakiet zasobów przy pomocy statycznej metody
ResourceBundle.getBundle():
public static final
ResourceBundle ResourceBundle.getBundle(String nazwaPakietu, Locale lokal)
Serwlet może pobierać zdania z pakietu zasobów przy pomocy metody ResourceBundle getString():
Public final String ResourceBundle.getString(String klucz)
Pakiet zasobów może zostać utworzony na klika sposobów. W przypadku serwletów najbardziej przydatną
techniką jest umieszczenie specjalnego pliku właściwości, który zawiera przetłumaczone zdania, w ścieżce klas
serwera. Plik powinien zostać nazwany w specjalny sposób, według wzoru nazwapakietu_jezyk.properties lub
nazwapakietu_język_kraj.properties. Na przykład, można wykorzystać Wiadomosci_fr.properties dla pakietu
francuskiego lub Wiadomosci_zh_TW.properties dla pakietu chińsko-tajwańskiego. Plik powinien zawierać znaki
US-ASCII w następującej formie:
nazwa1=wartosc1
nazwa1=wartosc1
...
Każda linia może również zawierać puste miejsca i kody ucieczkowe Unicode. Informacje zawarte w tym pliku
mogą zostać automatycznie pobrane przy pomocy metody getBundle().

Wyświetlanie odpowiednich informacji


Przykład 13.8 przedstawia wykorzystanie nagłówków Accept-Language i Accept-Charset oraz
pakietów zasobów w serwlecie, który wyświetla „Witaj świecie” każdemu klientowi w języku przez niego
preferowanym. Poniżej umieszczono przykładowy plik pakietu zasobów dla języka angielskiego, który należy
nazwać WitajBabel_en.properties i umieścić w miejscu przeszukiwanym przez mechanizm ładowania klas (takim
jak WEB-INF/classes):
powitanie=Hello world
Poniżej przedstawiono pakiet zasobów dla języka rosyjskiego, przechowywany w WitajBabel_ru.properties:
powitanie=\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439,
\u041c\u0438\u0440
Poniższy serwlet WitajBabel wykorzystuje klasę com.oreilly.servlet.LocaleNegotiator, która
zawiera logikę czarnej skrzynki określającą, jakie wartości Locale, ResourceBundle i kodowanie zostaną
użyte. Jej kod przedstawiony zostanie w następnym podrozdziale.
Przykład 13.8.
Serwletowa wersja wieży Babel
import java.io.*;
import java.util.*;
import java.text.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.LocaleNegotiator;
import com.oreilly.servlet.ServletUtils;

public class WitajBabel extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
try {
String nazwaPakietu = "WitajBabel";
String akceptJezyk = zad.getHeader("Accept-Language");
String akcepKod = zad.getHeader("Accept-Charset");

LocaleNegotiator negocjator =
new LocaleNegotiator(nazwaPakietu, akceptJezyk, akceptKod);

Locale lokal = negocjator.getLocale();


String kodowanie = negocjator.getCharset();
ResourceBundle pakiet = negocjator.getBundle(); // może być równy null

odp.setContentType("text/plain; charset=" + kodowanie);


odp.setHeader("Content-Language", lokal.getLanguage());
odp.setHeader("Vary", "Accept-Language");

PrintWriter wyj = odp.getWriter();

DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.LONG,


DateFormat.LONG,
lokal);
if (pakiet != null) {
wyj.println("Po " + lokal.getDisplayLanguage() + ":");
wyj.println(pakiet.getString("powitanie"));
wyj.println(fmt.format(new Date()));
}
else {
wyj.println("Nie znaleziono pakietu.");
}
}
catch (Exception w) {
log(ServletUtils.getStackTraceAsString(w));
}
}
}
Powyższy serwlet rozpoczyna działanie od ustawienia nazwy pakietu, który chce wykorzystać, po czym pobiera
swoje nagłówki Accept-Language i Accept-Charset. Tworzy LocaleNegotiator, przekazuje mu
informacje i szybko pyta go, jakie Locale, ResourceBundle i kodowanie powinien wykorzystać. Proszę
zauważyć, że serwlet może porzucić zwrócone kodowanie na rzecz kodowania UTF-8. Proszę także pamiętać, że
UTF-8 nie jest obsługiwane w tak szerokim zakresie, jak kodowania zwracane przez LocaleNegotiator.
Następnie serwlet ustawia swoje nagłówki — jego nagłówek Content-Type określa kodowanie, Content-
Language określa lokalny język, a nagłówek Vary wskazuje klientowi (jeżeli by się tym przejmował), że
serwlet ten może wyświetlać różną zawartość w zależności od nagłówka klienta Accept-Language.
Kiedy nagłówki zostaną ustawione, serwlet wyświetla informacje. Po pierwsze pobiera PrintWriter w celu
dopasowania kodowań. Następnie wyświetla — w domyślnym języku, zazwyczaj po angielsku — w jakim języku
zostanie wyświetlone powitanie. Następnie pobiera i wyświetla odpowiednie powitanie z pakietu zasobów. Na
końcu wyświetla datę i godzinę odpowiednią dla lokalizacji klienta. Jeżeli wartość pakietu zasobów wynosi
null, jak zdarza się to, kiedy nie ma pakietu zasobów pasującego do preferencji klienta, serwlet po prostu
informuje, że pakiet zasobów nie mógł zostać odnaleziony.

Klasa LocaleNegotiator
Kod klasy LocaleNegotiator jest przedstawiony w przykładzie 13.9. Jej klasa wspomagająca,
LocaleToCharsetMap, jest przedstawiona w przykładzie 13.10. Osoby, których nie interesuje negocjacja
lokalizacji mogą opuścić ten podrozdział.
LocaleNegotiator działa poprzez skanowanie preferencji językowych klienta w poszukiwaniu dowolnego
języka, dla którego występuje odpowiadający mu pakiet zasobów. Kiedy go znajduje, wykorzystuje
LocaleToCharsetMap w celu określenia kodowania. Jeżeli wystąpi dowolny problem, próbuje powrócić do
amerykańskiego angielskiego. Logika ignoruje preferencje kodowania klienta.
Najbardziej skomplikowanym aspektem kodu LocaleNegotiator jest konieczność obsługi nieszczęśliwego
zachowania ResourceBundle.getBundle(). Metoda getBundle() próbuje działać inteligentnie.
Jeżeli nie może ona odnaleźć pakietu zasobów, który dokładnie pasuje do określonej lokalizacji, próbuje
odnaleźć taki, który jest podobny. Dla celów tego przykładu problemem jest fakt, że getBundle() uważa za
podobny pakiet zasobów dla domyślnej lokalizacji. Tak więc podczas przeglądania języków klienta ciężko jest
określić, kiedy występuje dokładne dopasowanie pakietu zasobów, a kiedy nie. Sposobem na to jest, po pierwsze
pobranie awaryjnego pakietu zasobów, po czym wykorzystanie odwołania do niego w celu określenia, czy
znaleziono dokładne dopasowanie. Logika ta zawarta jest w metodzie getBundleNoFallback().
Przykład 13.9.
Klasa LocaleNegotiator
package com.oreilly.servlet;

import java.io.*;
import java.util.*;

import com.oreilly.servlet.LocaleToCharsetMap;

public class LocaleNegotiator {

private ResourceBundle chosenBundle;


private Locale chosenLocale;
private String chosenCharset;

public LocaleNegotiator(String bundleName,


String languages,
String charsets) {

// Określenie wartości domyślnych:


// Język angielski, kodowanie ISO-8859-1 (Latin-1), pakiet angielski
Locale defaultLocale = new Locale("en", "US");
String defaultCharset = "ISO-8859-1";
ResourceBundle defaultBundle = null;
try {
defaultBundle = ResourceBundle.getBundle(bundleName, defaultLocale);
}
catch (MissingResourceException e) {
// Nie odnaleziono domyślnego pakietu. Działanie bez zabezpieczenia.
}

// Jeżeli klient nie określi przyjmowanych języków, można zatrzymać domyślne


if (languages == null) {
chosenLocale = defaultLocale;
chosenCharset = defaultCharset;
chosenBundle = defaultBundle;
return; // szybkie wyjście
}
//Wykorzystanie znaczników do oddzielenia akceptowanych języków
StringTokenizer tokenizer = new StringTokenizer(languages, ",");

while (tokenizer.hasMoreTokens()) {
// Pobranie następnego przyjmowanego języka
// (Język może wyglądać tak: "en; wartoscq=0.91")
String lang = tokenizer.nextToken();

// Pobranie lokalizacji dla danego języka


Locale loc = getLocaleForLanguage(lang);

// Pobranie pakietu dla tej lokalizacji. Nie należy pozwolić na dopasowanie


innych
// języków!
ResourceBundle bundle = getBundleNoFallback(bundleName, loc);

// Zwrócony pakiet wynosi null, jeżeli nie znaleziono doapsowania. W tym


przypadku
// nie można wykorzystać tego języka, ponieważ serwlet go nie zna.
if (bundle == null) continue; // przejście do następnego języka

// Odnalezienie kodowania, które można wykorzystać do wyświetlenia języka


tej
// lokalizacji
String charset = getCharsetForLocale(loc, charsets);

// Zwrócone kodowanie wynosi null, jeżeli nie znaleziono doapsowania. W tym


// przypadku nie można wykorzystać tego języka, ponieważ serwlet nie może go
// zakodować
if (charset == null) continue; // on to the next language

// Jeżeli w tym miejscu, to nie ma problemów z tym językiem.


chosenLocale = loc;
chosenBundle = bundle;
chosenCharset = charset;
return; // koniec
}

// Brak dopasowania, zostaje domyślny


chosenLocale = defaultLocale;
chosenCharset = defaultCharset;
chosenBundle = defaultBundle;
}

public ResourceBundle getBundle() {


return chosenBundle;
}

public Locale getLocale() {


return chosenLocale;
}

public String getCharset() {


return chosenCharset;
}

private Locale getLocaleForLanguage(String lang) {


Locale loc;
int semi, dash;

// Odcięcie wszystkich wartości q, które mogą następować po średniku


if ((semi = lang.indexOf(';')) != -1) {
lang = lang.substring(0, semi);
}

// Obcięcie wolnych miejsc


lang = lang.trim();

// Utworzenie Locale z języka. Myślnik może oddzielać język od kraju


if ((dash = lang.indexOf('-')) == -1) {
loc = new Locale(lang, ""); // Brak myślnika, brak kraju
}
else {
loc = new Locale(lang.substring(0, dash), lang.substring(dash+1));
}

return loc;
}

private ResourceBundle getBundleNoFallback(String bundleName, Locale loc) {


// Po pierwsze pobranie pakietu awaryjnego — pakietu, który zostanie pobrany,
jeżeli
// getBundle() nie może odnaleźć bezpośredniego doapsowania. Pakiet ten będzie
// porównywany z pakietami dostarczanymi przez następne wywołania getBundle()
w celu
// wykrycia, czy getBundle() znalazła bezpośrednie dopasowanie.
ResourceBundle fallback = null;
try {
fallback =
ResourceBundle.getBundle(bundleName, new Locale("bogus", ""));
}
catch (MissingResourceException e) {
// Nie odnaleziono pakietu awaryjnego
}

try {
// Pobranie pakietu dla określonej lokalizacji
ResourceBundle bundle = ResourceBundle.getBundle(bundleName, loc);

// Czy pakiet różni się od pakietu awaryjnego?


if (bundle != fallback) {
// Prawdziwe dopasowanie!
return bundle;
}
// Tak więc pakiet jest taki sam, jak pakiet awaryjny.
// To ciągle może być dopasowanie, ale tylko wtedy, gdy lokalny język pasuje
do
// języka domyślnej lokalizacji.
else if (bundle == fallback &&
loc.getLanguage().equals(Locale.getDefault().getLanguage())) {
// Inny sposób na dopasowanie
return bundle;
}
else {
// Brak dopasowania, kontynuacja poszukiwań
}
}
catch (MissingResourceException e) {
// Brak pakietu dla tej lokalizacji
}

return null; // Brak dopasowania


}

protected String getCharsetForLocale(Locale loc, String charsets) {


// Uwaga — ta metoda ignoruje kodowania określone przez klienta
return LocaleToCharsetMap.getCharset(loc);
}
}
Przykład 13.10.
Klasa LocaleToCharsetMap
package com.oreilly.servlet;

import java.util.*;

public class LocaleToCharsetMap {

private static Hashtable map;

static {
map = new Hashtable();

map.put("ar", "ISO-8859-6");
map.put("be", "ISO-8859-5");
map.put("bg", "ISO-8859-5");
map.put("ca", "ISO-8859-1");
map.put("cs", "ISO-8859-2");
map.put("da", "ISO-8859-1");
map.put("de", "ISO-8859-1");
map.put("el", "ISO-8859-7");
map.put("en", "ISO-8859-1");
map.put("es", "ISO-8859-1");
map.put("et", "ISO-8859-1");
map.put("fi", "ISO-8859-1");
map.put("fr", "ISO-8859-1");
map.put("hr", "ISO-8859-2");
map.put("hu", "ISO-8859-2");
map.put("is", "ISO-8859-1");
map.put("it", "ISO-8859-1");
map.put("iw", "ISO-8859-8");
map.put("ja", "Shift_JIS");
map.put("ko", "EUC-KR"); // Wymaga JDK 1.1.6
map.put("lt", "ISO-8859-2");
map.put("lv", "ISO-8859-2");
map.put("mk", "ISO-8859-5");
map.put("nl", "ISO-8859-1");
map.put("no", "ISO-8859-1");
map.put("pl", "ISO-8859-2");
map.put("pt", "ISO-8859-1");
map.put("ro", "ISO-8859-2");
map.put("ru", "ISO-8859-5");
map.put("sh", "ISO-8859-5");
map.put("sk", "ISO-8859-2");
map.put("sl", "ISO-8859-2");
map.put("sq", "ISO-8859-2");
map.put("sr", "ISO-8859-5");
map.put("sv", "ISO-8859-1");
map.put("tr", "ISO-8859-9");
map.put("uk", "ISO-8859-5");
map.put("zh", "GB2312");
map.put("zh_TW", "Big5");

public static String getCharset(Locale loc) {


String charset;

// Próba dopasowania pełnej nazwy (może zawierać kraj)


charset = (String) map.get(loc.toString());
if (charset != null) return charset;

// Jeżeli pełna nazwa nie pasuje, to może to być tylko język


charset = (String) map.get(loc.getLanguage());
return charset; // może wynosić null
}
}

Lokalizacje dostarczane przez system


Począwszy od Servlet API 2.2 serwlet może pobierać i wykorzystywać preferowane lokalizacje klienta przy
pomocy kilku prostych metod. ServletRequest posiada nową metodę getLocale(), która zwraca obiekt
Locale wskazujący na najbardziej preferowaną przez klienta lokalizację. W przypadku serwletów HTTP
preferencja oparta jest na nagłówku Accept-Language. Istnieje również metoda getLocales(), która
zwraca Enumeration obiektów Locale wskazując wszystkie akceptowane przez klienta lokalizacje,
rozpoczynając od najbardziej preferowanej. Metodom tym towarzyszy dołączona do ServletResponse
metoda setLocale(Locale loc), która pozwala serwerowi na określenie lokalizacji odpowiedzi. Metoda
ta automatycznie ustawia nagłówek Content-Language oraz wartość kodowania Content-Type. Metoda
setLocale() powinna zostać wywołana zaraz po setContentType(), a przed getWriter()
(ponieważ modyfikuje typ zawartości i wpływa na tworzenie PrintWriter). Na przykład:
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException {
odp.setContentType("text/html");
Locale lokal = zad.getLocale();
odp.setLocale(lokal);
PrintWriter out = odp.getWriter();

// Wyświetlenie wyników w oparciu o lokal.getLanguage()


}
Metody te pozwalają na odwoływanie się do kodu, ale nie dostarczają pomocy w określaniu, czy dana Locale
jest obsługiwana przez aplikację WWW. W tym celu konieczna jest dodatkowa logika taka, jak
LocaleNegotiator.

Formularze HTML
Zarządzanie formularzami HTML wymaga pewnej ilości dodatkowej pracy i kilku sztuczek, kiedy obsługuje się
zawartość zlokalizowaną. Aby zrozumieć problem, proszę wyobrazić sobie następującą sytuację. Formularz
HTML jest wysyłany jako część strony rosyjskiej. Prosi on użytkownika o jego nazwisko, które on wprowadza
jako łańcuch rosyjskich znaków. Jak to nazwisko jest wysyłane do serwletu? I, co ważniejsze, jak serwlet może
je odczytać?
Odpowiedź na pierwsze pytanie jest taka, że wszystkie dane formularzy HTML są wysyłane jako sekwencja
bajtów. Bajty te to zakodowana reprezentacja oryginalnych znaków. W przypadku języków
zachodnioeuropejskich, kodowanie jest domyślne, ISO-8859-1, z jednym bajtem na znak. W przypadku innych
języków mogą występować inne kodowania. Przeglądarki kodują dane formularzy przy pomocy tych samych
kodowań, które zostały wykorzystane w przypadku strony zawierającej formularz. Tak więc jeżeli wspomniana
wcześniej rosyjska strona została zakodowana przy pomocy ISO-8859-5, wysyłane dane formularza również
zostaną zakodowane przy pomocy ISO-8859-5. Proszę zauważyć, że jeżeli strona nie określa kodowania, i
użytkownik musi samodzielnie wybrać kodowanie ISO-8859-5 w celu przeglądania, duża część przeglądarek
wyśle dane przy pomocy ISO-8859-120. Generalnie, zakodowany łańcuch bajtów zawiera dużą ilość specjalnych
bajtów, które muszą zostać zakodowane w URL-u. Jeżeli przyjmie się, że rosyjski formularz wysyła nazwisko
użytkownika przy pomocy żądania GET, odpowiedni URL może wyglądać następująco:
http://serwer:port/servlet/ObslugaNazwisk?nazwisko=%8CK%8C%B4%90%B3%8E%9F
Odpowiedź na drugie pytanie, czyli jak serwlet odczytuje zakodowane informacje, jest nieco bardziej
skomplikowana. Serwlet posiada dwie opcje wyboru. Po pierwsze, serwlet może pozostawić dane formularza w
ich surowej formie zakodowania, traktując je jak sekwencję bajtów — z każdym bajtem obrzydliwie
zakodowanym jako znak w łańcuchu parametrów. Taktyka ta jest użyteczna jedynie wtedy, gdy serwlet nie musi
manipulować danymi i może być pewny, że dane te zostaną wyświetlone jedynie temu samemu użytkownikowi
wykorzystującemu to samo kodowanie. Drugą metodą jest konwersja przez serwlet danych formularza z ich
początkowego formatu do przyjaznego Javie łańcucha Unicode. Pozwala to serwletowi na swobodną manipulację
tekstem i wyświetlanie go przy pomocy innych kodowań. Jednak z metodą tą związany jest pewien problem.
Aktualnie przeglądarki nie dostarczają żadnych informacji wskazujących na kodowanie wykorzystane w danych
formularza. W przyszłości własność ta może zostać dodana (można na przykład wykorzystać w tym celu
nagłówek Content-Type w żądaniu POST), ale aktualnie to serwlet jest odpowiedzialny za śledzenie tej
informacji.

Ukryte kodowanie
Szeroko akceptowaną techniką śledzenia kodowania wysłanych danych formularza jest wykorzystanie ukrytego
pola formularza zawierającego kodowanie21. Jego wartość powinna zostać ustawiona na kodowanie strony, która
go zawiera. Następnie każdy serwlet odbierający formularz mógłby odczytać wartość pola kodowania i wiedzieć,
jak zdekodować wysłane dane formularza.
Przykład 13.11 przedstawia tą technikę w przypadku generatora formularza, który ustawia kodowanie tak, aby
pasowało ono do kodowania strony. Poniżej znajduje się angielski pakiet zasobów, który może zostać dołączony
do serwletu, przechowany jako FormKod_en.properties:
tytul=FormKod
naglowek=<h1>Charset Form</h1>
tekst=Enter text:
Poniżej znajduje się pakiet rosyjski, przechowywany jako FormKod_ru.properties:
tytul=FormKod
naglowek=<h1>\u0424\u043e\u0440\u043c\u0443\u043b\u044f\u0440</h1>
tekst=\u041d\u0430\u043f\u0438\u0448\u0438 \u0442\u0435\u043a\u0441\u0442
Przykład 13.11.
Zapisywanie kodowania w ukrytym polu formularza
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.LocaleNegotiator;
import com.oreilly.servlet.ServletUtils;

public class FormKod extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
try {
String nazwaPakiet = "FormKod";
String akceptJezyk = zad.getHeader("Accept-Language");
String akceptKod = zad.getHeader("Accept-Charset");

LocaleNegotiator negocjator =

20
Większa ilość informacji na temat internacjonalizacji HTML i formularzy HTML jest dostępna w dokumencie RFC 2070
pod adresem http://www.ietf.org.rfc/rfc2070.txt.
21
Ukryte pola formularzy zostały wprowadzone w rozdziale 7, w którym były wykorzystywane do śledzenia sesji.
new LocaleNegotiator(nazwaPakiet, akceptJezyk, akceptKod);

Locale lokal = negocjator.getLocale();


String kodowanie = negocjator.getCharset();
ResourceBundle pakiet = negocjator.getBundle(); // może wynosić null

odp.setContentType("text/html; charset=" + kodowanie);


odp.setHeader("Content-Language", lokal.getLanguage());
odp.setHeader("Vary", "Accept-Language");

PrintWriter wyj = odp.getWriter();

if (pakiet != null) {
wyj.println("<HTML><HEAD><TITLE>");
wyj.println(pakiet.getString("tytul"));
wyj.println("</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println(pakiet.getString("naglowek"));
wyj.println("<FORM ACTION=/servlet/AkcjaKod METHOD=GET>");
wyj.println("<INPUT TYPE=HIDDEN NAME=kodowanie VALUE=" + kodowanie + ">");
wyj.println(pakiet.getString("tekst"));
wyj.println("<FONT FACE='Czar'>");
wyj.println("<INPUT TYPE=TEXT NAME=tekst>");
wyj.println("</FORM>");
wyj.println("</BODY></HTML>");
}
else {
wyj.println("Nie odnaleziono pakietu.");
}
}
catch (Exception w) {
log(ServletUtils.getStackTraceAsString(w));
}
}
}
Wynik uruchomienia wersji rosyjskiej jest przedstawiony na rysunku 13.6.

Rysunek 13.6.
Formularz rosyjski, z tekstem wpisanym przez użytkownika
Serwlet odpowiedzialny za obsługę wysłanego formularza jest przedstawiony w przykładzie 13-12. Serwlet ten
odczytuje wysłany tekst i dokonuje jego konwersji na Unicode, po czym wyświetla znaki przy pomocy
kodowania UTF-8. Dodatkowo, wyświetla on również otrzymany łańcuch jak łańcuch kodów ucieczkowych
Unicode, pokazując, że należałoby dane wpisać w pliku źródłowym Javy lub pakiecie zasobów, aby otrzymać
identyczny wynik. Pozwala to serwletowi na działanie jako oparty na WWW tłumacz kodowania początkowego
na łańcuch Unicode.
Przykład 13.12.
Otrzymywanie kodowania w ukrytym polu formularza
import java.io.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AkcjaKod extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
try {
odp.setContentType("text/plain; charset=UTF-8");
PrintWriter wyj = odp.getWriter();

String kodowanie = zad.getParameter("kodowanie");

// Pobranie parametru tekstu


String tekst = zad.getParameter("tekst");

// Teraz konwersja go z tablicy bajtów do tablicy znaków.


// Wykonanie tego przy pomocy kodowania wysłanego w ukrytym polu.
// Traktowanie oryginalnych wartości jako surowych 8-bitowych bajtów
wewnątrz
//łańcucha
BufferedReader reader = new BufferedReader(
new InputStreamReader(new StringBufferInputStream(tekst), kodowanie));
tekst = reader.readLine();

wyj.println("Otrzymane kodowanie: " + kodowanie);


wyj.println("Otrzymany tekst: " + tekst);
wyj.println("Otrzymany tekst (Unicode): " + doLancuchUcieczkiUnicode
(tekst));
}
catch (Exception w) {
w.printStackTrace();
}
}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
doGet(zad, odp);
}

private static String doLancuchUcieczkiUnicode(String lan) {


// utworzony według kodu w java.util.Properties.save()
StringBuffer buf = new StringBuffer();
int dlug = lan.length();
char zn;
for (int i = 0; i < dlug; i++) {
zn = lan.charAt(i);
switch (zn) {
case '\\': buf.append("\\\\"); break;
case '\t': buf.append("\\t"); break;
case '\n': buf.append("\\n"); break;
case '\r': buf.append("\\r"); break;

default:
if (zn >= ' ' && zn <= 127) {
buf.append(zn);
}
else {
buf.append('\\');
buf.append('u');
buf.append(doHeks((zn >> 12) & 0xF));
buf.append(doHeks((zn >> 8) & 0xF));
buf.append(doHeks((zn >> 4) & 0xF));
buf.append(doHeks((zn >> 0) & 0xF));
}
}
}
return buf.toString();
}

private static char doHeks(int polbajtu) {


return hexCyfra[(polbajtu & 0xF)];
}

private static char[] hexCyfra = {


'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
};
}
Przykładowy wynik przedstawiony jest na rysunku 13.7.
Rysunek 13.7.
Obsługa rosyjskiego formularza
Najbardziej interesującą częścią powyższego serwletu jest fragment, który otrzymuje i konwertuje wysłany tekst:
tekst = new String(tekst.getBytes("ISO-8859-1"), kodowanie);
Wywołanie tekst.getBytes("ISO-8859-1") dokonuje konwersji tekstu do jego surowego formatu byte.
Chociaż wartość parametru jest zwracana jako String, nie jest to prawdziwy łańcuch. Każdy znak w String
naprawdę przechowuje jeden bajt zakodowanego tekstu, wymagając tej specjalnej konwersji. Otaczający
konstruktor String tworzy następnie String z surowych bajtów przy pomocy kodowania określonego w polu
kodowania. Nie wygląda to zbyt ładnie, ale działa. Bardziej eleganckim rozwiązaniem jest klasa
com.oreilly.servlet.ParameterParser opisana w rozdziale 19, „Informacje dodatkowe”.
Rozdział 14. Szkielet Tea

W tym momencie można przejść do omawiania opartych na serwletach szkieletów służących do tworzenia
zawartości stron WWW. Technika JSP zostanie omówiona później, ponieważ jest on najbardziej skomplikowaną
alternatywą tworzenia zawartości. Jako pierwsza omówiona zostanie Tea. TeaServlet (znana powszechnie jako
Tea) to produkt Walt Disney Internet Group (WDIG) — dawniej GO.com — rozwijany przez wiele lat wewnątrz
firmy w celu wspomożenia tworzenia witryn o wysokim obciążeniu takich jak ESPN.com, NFL.com, Disney.com,
DisneyLand.com, Movies.com, ABC.com i GO.com. Niedawno produkt ten został udostępniony jako Open
Source w nadziei, że inni uznają Tea za narzędzie przydatne i pomogą w jego rozwoju. Strategia ta posiada sens
— dzielenie się narzędziami w nadziei, że inni pomogą je wyostrzyć. Tu omówiona zostanie TeaServlet 1.1.0,
dostępna pod adresem http://opensource.go.com. Wymaga ona Servlet API 2.1 i JDK 1.2 lub późniejszych.
Licencja TeaServlet jest oparta na licencji Apache'a, jednej z najmniej restrykcyjnych i przez to atrakcyjnych dla
programistów, ponieważ według niej można wykorzystywać Tea do tworzenia nowych produktów i witryn bez
udostępniania tych produktów jako Open Source. Ten model licencji wykorzystywany jest przez wszystkie
projekty Apache, włączając w to serwery Apache i Tomcat.
Tea została zaprojektowana dla zadań prowadzonych przez małe zespoły programistów i producentów
technicznych. Role programisty to tworzenie „aplikacji” napisanych w Javie i zainstalowanych w TeaServlet.
Producent tworzy i utrzymuje ostateczny wygląd dynamicznych stron WWW poprzez tworzenie szablonów Tea,
które wywołują funkcje dostarczone przez aplikacje programisty. Na przykład, w witrynie ESPN, jeden
programista tworzy aplikację służącą do zarządzania statystykami zespołów, inny (pracujący niezależnie) tworzy
aplikację służącą do zarządzania statystykami zawodników, a producent techniczny umieszcza dane w sieci przy
pomocy szablonów Tea. Właściwie kilku producentów pracujących niezależnie może wykorzystywać te same
dane wspierające w celu utworzenie stron ukierunkowanych na różnych użytkowników — działanie, które firma
WDIG wykonała w swoich witrynach włączając w to Disney.com, Movies.com, ESPN.com i GO.com. Szablony
tworzone są w języku Tea w celu wymuszenia całkowitego oddzielenia zawartości od prezentacji.

Język Tea
Tea to język programowania zaprojektowany do formatowania tekstu. Posiada on silnie zaznaczone typy, musi
być kompilowany i został zaprojektowany do pracy w środowisku opartym na Javie. Środowiskiem tym jest
TeaServlet, narzędzie wykorzystujące Tea do tworzenia stron WWW i umożliwiające dostęp do mechanizmów
serwletów, z których wywoływane są pliki szablonów Tea22.
Głównym celem języka Tea jest wymuszenie oddzielenia zawartości i prezentacji, bez poświęcania
podstawowych konstruktów programistycznych. Do czasu pisania tej książki, Tea została utworzona na
podstawie czterech ograniczeń:
• Dane i typy danych nie mogą być tworzone bezpośrednio. Są one odczytywane.
• Odczytane dane nie mogą zostać bezpośrednio modyfikowane w żaden sposób.
• Szablon nie może w bezpośredni sposób uszkadzać swojego środowiska.
• Dostarczana jest jedynie minimalna ilość konstruktów programistycznych.

22
Chociaż język Tea został zaprogramowany do formatowania tekstu, właściwie możliwa jest kontrola szablonu Tea nad
klasą aplikacji tworzącą rysunek i łatwo jest stworzyć wspólnie dynamiczny obrazek. Inaczej niż pozostałe narzędzia, które
zostaną omówione, szablony Tea mogą wyświetlać również dane binarne, tak samo jako znakowe.
Ograniczenia te dodatkowo chronią system przed uszkodzonymi bądź błędnymi szablonami, co jest własnością
niezwykle pożądaną w przypadku witryn stale przyjmujących dużą ilość szablonów, jak zdarza się to w
popularnych witrynach informacyjnych. Projekt ten jest wygodny również dla firm hostingowych, które muszą
dawać klientom możliwość tworzenia stron dynamicznych, ale nie chcą, aby żaden konkretny błąd klienta mógł
wpłynąć negatywnie na pozostałą część serwera.
Ale czy kolejny język, zwłaszcza język, który potrafi o wiele mniej niż Java jest tak naprawdę potrzebny? Tea
Template Language Manual (Podręcznik Języka Szablonów Tea) odpowiada na to pytanie:
Tea jest wynikiem wielu lat doświadczenia z innymi mechanizmami tworzenia stron WWW. Większość
opartych WWW aplikacji rozpoczyna się od znaczników HTML osadzonych w kodzie, niezależnie od
tego, czy jest to C, Perl, czy Java. Podejście to jest odpowiednie dla małych lub początkujących
projektów, ponieważ nie wymaga dużej ilości programowania.
Ponieważ zmiany w formatowaniu strony mogą zdarzać się często, a twórcy nie chcą ich dokonywać,
nieodwołalnie przesuwają się oni w stronę wykorzystywania pewnego rodzaju szablonowego
mechanizmu wymiany elementów. Każdy element jest po prostu obszarem zablokowanym dla łańcucha,
który zawiera dane utworzone przez aplikację. Te systemy szablonów dalej ewoluują w stronę obsługi
specjalnych konstrukcji służących do formatowania tabeli, formularzy i prostej logiki warunkowej.
Podczas wprowadzania konstrukcji programistycznych do szablonu, podstawową trudnością jest
stworzenie czegoś, co będzie posiadać wystarczającą moc, a zarazem będzie proste i bezpieczne. Jeżeli
jest to zbyt potężne, wtedy kompletne aplikacje mogą być tworzone przez szablony. Jeżeli jest zbyt
słabe, wtedy w aplikacji musi być zawarte formatowanie HTML. Jeżeli nie jest proste lub bezpieczne,
wtedy programiści aplikacji musza sami stworzyć szablony.
Inaczej niż w przypadku istniejących języków osadzanych w czymś podobnym do ASP lub JSP, Tea jest
językiem zaprojektowanym specjalnie do tego, aby spełniać wymagania systemu szablonów. Jest on
bezpieczny, prosty, wydajny i potężny.
Każdy egzemplarz Tea jest zintegrowany ze specjalnym serwletem. Serwlet ten daje szablonom Tea
kontrolę nad tworzeniem strony, zachowując silne powiązania z aplikacją wspierającą utworzoną przez
programistę Javy. Podczas gdy serwlet ten dostarcza funkcjonalności podobnej do JSP, Tea wymusza
poprawne rozdzielenie model-widok ze względu na międzynarodowe ograniczenia języka. Chociaż jest
to także sugerowany model separacji w JSP, nie może on być w tamtym przypadku wymuszony.
Dodatkowo szablony Tea nie obsługują własności programów, które mogą zostać wykorzystane
nieodpowiedzialnie. Modyfikacje nie muszą przechodzić przez ścisłą korektę i fazę testów, która jest
wymagana w przypadku JSP.
Każda osoba pracująca nad projektem powinna mieć dostęp do narzędzi pozwalających na pracę z
maksymalną wydajnością, a Tea wykonuje swoją część pracy poprzez umożliwienie programiście
wykonywania dokładnie tego, co jest mu potrzebne, tak łatwo jak to tylko możliwe, i żadnej dodatkowej
pracy ponad to. Nawet w przypadku projektów tworzonych przez początkujących programistów
wykorzystanie Tea posiada swoje zalety. Narzędzie wspomaga nabywanie dobrych praktyk
programistycznych i ułatwia utrzymanie aplikacji.
Czytelnicy niniejszej książki powinni również pamiętać, że Tea nie jest językiem zaprojektowanym dla
programistów, ale dla producentów technicznych. Jest językiem prostszym niż Java, bezpieczniejszym, ale tak
samo wydajnym.

Początki
Aby zapoznać się z Tea, można przyjrzeć się kilku samodzielnym szablonom. Szablony te nie wykorzystują
żadnych wspierających „aplikacyjnych” klas Javy, i w związku z tym nie mają wielkich możliwości działania.
Poniżej przedstawiono pierwszy z szablonów:
<% template ProstaStrona() %>
To jest prosta strona, która nie robi praktycznie nic.
Powyższy szablon po prostu wyświetla „To jest prosta strona, która nie robi praktycznie nic.” każdemu, kto
uzyska do niej dostęp. Szablony złożone są z obszarów kodu i tekstu. Obszary kodu są ograniczane znakami <% i
%> (nie są wymagane żadne inne znaki ograniczające). Tekst poza obszarem kodu jest wyświetlany przez
szablon bez żadnego formatowania, właśnie dlatego powyższy szablon po prostu wyświetla proste zdanie.
Aby uruchomić szablon, należy po pierwsze zapamiętać go w pliku o nazwie ProstaStrona.tea. Podobnie jak w
klasach Javy, nazwa pliku musi być taka sama jak nazwa szablonu zadeklarowana przez konstruktora (łącznie z
wielkością liter), a plik musi posiadać rozszerzenie .tea. Miejsce, w którym szablon powinien zostać zapisany,
jest konfigurowalne. Poleca się katalog będący podkatalogiem WEB-INF, taki jak WEB-INF/szablony.
Następnym etapem przed uruchomieniem aplikacji jest instalacja samej Tea. Należy pobrać dystrybucję dostępną
pod adresem http://opensource.go.com i wypełnić instrukcje instalacji w niej zawarte — umieścić plik
TeaServlet.jar w ścieżce klas serwera WWW. Następnie należy dokonać edycji deskryptora web.xml aplikacji
WWW w celu zarejestrowania TeaServlet pod nazwą tea z parametrem inicjacji określającym pozycję
informacji o konfiguracji. Należy również skonfigurować zasadę odwzorowania przedrostków tak, że tea/*
wywołuje serwlet tea. Przykład dodatku do web.xml jest przedstawiony w przykładzie 14.1.
Przykład 14.1.
Instalacja TeaServlet
<!-- ... -->
<servlet>
<servlet-name>
tea
</servlet-name>
<servlet-class>
com.go.teaservlet.TeaServlet
</servlet-class>
<init-param>
<param-name>
properties.file
</param-name>
<param-value>
<!—Należy dokonać edycji tak, aby wskazywało na bezwzględną ścieżkę do
-->
<!—pliku właściwości-->
/tomcat/webapps/teatime/WEB-INF/TeaServlet.properties
</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>
tea
</servlet-name>
<url-pattern>
/tea/*
</url-pattern>
</servlet-mapping>
<!-- ... -->
Jedyną informacją, którą należy dostosować, jest ścieżka do pliku TeaServlet.properties. Powinna być to ścieżka
bezwzględna. Plik TeaServlet.properties definiuje zachowanie TeaServlet. Może być on stosunkowo długi,
dlatego, że plik web.xml wskazuje na zewnętrzny plik zamiast bezpośrednio zawierać informacje konfiguracyjne.
Pełne omówienie pliku właściwości nastąpi później, a teraz wykorzystany będzie najprostszy z możliwych plik
TeaServlet.properties:
# TeaServlet.properties
#
# Określenie miejsca szablonów Tea
template.path = /tomcat/webapps/herbatka/WEB-INF/szablony
Proszę upewnić się co do umieszczenia pliku właściwości w miejscu określonym w pliku web.xml. Ostatnim
krokiem jest ponowne uruchomienie serwera tak, aby mógł odczytać nowy plik web.xml (w przypadku niektórych
serwerów nie jest to konieczne).
Kiedy TeaServlet zostanie właściwie zainstalowany, dostęp do ProstaStrona można uzyskać w miejscu takim jak
http://localhost:8080/herbatka/tea/ProstaStrona. Część ścieżki /herbatka określa aplikację WWW, do której
uzyskiwany jest dostęp. Następna część /tea jest tak sama, jak odwzorowanie przedrostków podane w web.xml
herbatka, co powoduje uruchomienie TeaServlet w celu obsłużenia żądania. Szablony mogą również zostać
umieszczone w podkatalogach pod template.path, w którym to przypadku wywoływane są przy pomocy
ścieżki zawierającej podkatalogi takie jak /herbatka/tea/podkat/ProstaStrona.

Informacja o żądaniu
Tworzenie statycznej zawartości wiąże się z dużą ilością pracy, tak więc teraz utworzone zostanie coś bardziej
dynamicznego. Konstruktor szablonu może przyjmować dowolną ilość parametrów, których wartości są
automatycznie przekazywane z parametrów żądania (poprzez łańcuch zapytania i dane POST). Parametry te
mogą być typu String, Integer, Float, Double i każdego innej podklasy java.lang.Number.
Konwersja do typów numerycznych następuje automatycznie. Jeżeli nie zostanie przekazany żaden parametr o
danej nazwie, lub konwersja typu nie powiedzie się, szablon otrzymuje wartość parametru null. Szablon
przedstawiony w przykładzie 14-2 wykorzystuje swój konstruktor do przyjęcia parametru o nazwie nazwisko.
<% template ProstaStrona(String nazwisko) %>
To jest prosta strona, która nie robi praktycznie nic
poza wyświetleniem nazwiska: <% nazwisko ?>
Jeżeli uzyska się dostęp do szablonu jako herbatka/tea/ProstaStrona?imię=Janek, na stronie wyświetlone
zostanie „Janek”. Jeżeli dostęp do szablonu nastąpi bez parametru nazwisko, na stronie wyświetlone zostanie
„null”. Przykład 14.2 przedstawia szablon sprawdzający ten parametr.
Przykład 14.2.
Szablon sprawdzający nazwiska
<% template ProstaStrona(String nazwisko) %>
To jest prosta strona, która nie robi praktycznie nic
poza wyświetleniem nazwiska: <% nazwisko ?>
<P>

<%
if (nazwisko == null) {
"Nie podano nazwiska"
}
if (name == "Sędzia") {
"Proszę wstać"
}
%>
Powyższy nowy szablon sprawdza , czy nazwisko wynosi null i, jeżeli tak, wyświetla użytkownikowi
ostrzeżenie. Szablon sprawdza również, czy podane nazwisko brzmi Sędzia i , jeżeli tak, traktuje użytkownika
w specjalny sposób. Ważne jest, by zauważyć dwa elementy porównania Sędzia — po pierwsze wartości
łańcuchów w Tea mogą zostać sprawdzone pod kątem równości przy pomocy operatora == (jak to jest w
przypadku wszystkich obiektów). Oznacza to, że nie jest konieczne wyjaśnianie producentowi technicznemu celu
metody .equals(). Po drugie, łańcuch null może być porównany z innym łańcuchem bez ryzyka wystąpienia
wyjątku NullPointerException, co mogłoby się zdarzyć w przypadku .equals().

Sięganie głębiej
Inne informacje o żądaniu są dostępne przy pomocy TeaServlet. Na przykład, szablon Tea może uzyskać
bezpośredni dostęp do nagłówków i cookies żądania, a także do parametrów. Przykład 14.3 przedstawia szablon
Tea przeglądający informacje o żądaniu. W praktycznych przypadkach, rzadkością jest konieczność uzyskania
przez szablon Tea bezpośredniego dostępu do tych informacji, zamiast tego można wykorzystać aplikację, która
może dostarczyć przetworzoną wersję informacji.
Przykład 14.3.
Szablon przeglądający informacje o żądaniu
<% template Przegl() %>

<HTML><HEAD><TITLE>Przeglądanie</TITLE></HEAD><BODY>

<% zadanie = getRequest() // Typ obiektu jest narzucony %>

<H1>Różne informacje</H1>
QueryString: <% zadanie.queryString %> <BR>
RemoteUser: <% zadanie.remoteUser %> <BR>

<H1>Informacje o parametrach</H1>
<%
foreach (paramName in zadanie.parameters.names) {
paramName ": " zadanie.parameters[paramName] "<BR>"
}
%>

<H1>Informacja o atrybutach żądania</H1>


<%
foreach (attribName in zadanie.attributes.names) {
attribName ': ' zadanie.attributes[attribName] '<BR>'
}
%>

<H1>Header Informacja o nagłówkach</H1>


<%
foreach (headerName in zadanie.headers.names) {
headerName ': ' zadanie.headers[headerName] '<BR>'
}
%>

<h1>Informacja o cookies</h1>
<%
foreach (cookie in request.cookies.all) {
cookie.name ': ' cookie.value '<BR>'
}
%>
Wartość cookie identyfikatora sesji wynosi<% zadanie.cookies["JSESSIONID"].value %
>.

<% // Rzeczy niemożliwe do wykonania w Tea... %>


<%/*
"<H1>Odczyt z sesji użytkownika</H1>"
zadanie.session["moze"] = "popracuje pozniej"

"<H1>Ustawienie atrybutu</H1>"
zadanie.attributes["nie"] = "będzie pracował"

"<H1>Konfiguracja cookie</H1>"
// Manipulacje odpowiedzią poprzez funkcje aplikacji
*/%>

</BODY></HTML>
Powyższy przykład może powiedzieć dużo o Tea. Po pierwsze, zmienne lokalne (takie jak zadanie) nie muszą
być deklarowane według typu obiektu. Posiadają one silnie zaznaczony typ, ale przydzielanie typu jest ukryte. W
języku istnieje słowo kluczowe isa pozwalające na obsługę kwestii związanych z tworzeniem zmiennych, jeżeli
jest to konieczne.
Wykorzystane zostało wyrażenie foreach, które obsługuje często występujące zadanie wykonywania pętli nad
zestawem obiektów. Jest to elastyczna konstrukcja, która może działać zarówno na tablicach, jak i na każdych
innych obiektach będących rozszerzeniem java.util.Collection. Wyrażenie foreach obsługuje
również działanie według zakresu i wstecz. Na przykład:
foreach (licznik in 1 .. 10 reverse) { licznik '<BR>' }
wyświetla odliczanie począwszy od 10. Wyrażenie foreach obsługuje w Tea wszystkie zadania związane z
pętlami, nie występują wyrażenia for i while. (W związku z tym w Tea nie występują żadne pętle
nieskończone.)
Wyrażenia w Tea nie muszą kończyć się średnikiem, jak to jest w przypadku Javy, ani też nie muszą występować
w osobnych liniach. Zamiast tego, kompilator Tea wykorzystuje skonstruowaną uważnie gramatykę Tea w celu
obliczenia końca wyrażeń! Średniki są obsługiwane w celu umożliwienia rzadkich okoliczności, w których
średnik jest konieczny do uniknięcia dwuznaczności wyrażeń i jako ułatwienie dla programistów Javy, którzy
instynktownie dodają średniki.
Zazwyczaj nie ma potrzeby wykorzystania w Tea operatora łączenia łańcuchów. Obecność łańcuchów w
„podwójnych cudzysłowach” i 'pojedynczych cudzysłowach' wewnątrz kodu Tea dostarcza wystarczających
wskazówek, w którym miejscu powinno wystąpić łączenie. W celu uzyskania bardziej zaawansowanego łączenia
należy wykorzystać znak &. Znak + nie jest wykorzystywany, ponieważ jego nadmierne stosowanie powoduje
niejasności.
Komentarze w Tea tworzone są według standardów Javy, w których /* i */ ograniczają komentarze o długości
większej niż jedna linia, a // rozpoczyna komentarz jednoliniowy. Komentarze te nie są wysyłane do klienta,
jeżeli pamięta się o umieszczeniu ich wewnątrz bloku kodu Tea.
W powyższym przykładzie wykorzystano długi komentarz w celu zademonstrowania działań, które nie mogą
zostać wykonane przez Tea — odczyt z sesji użytkownika, ustawianie atrybutów żądania, konfiguracja cookie i
właściwie wysyłanie czegokolwiek w żądaniu. Zdolność do uzyskania dostępu do sesji może zostać dodana w
późniejszym czasie jako część procesu projektowania Open Source tworzącego Tea. Nie jest ona obecnie
obsługiwana tylko dlatego, że WDIG wewnętrznie stosuje inny mechanizm śledzenia sesji. Inne własności
celowo nie są obsługiwane i muszą zostać wykonane przez kod Javy w aplikacji wspierającej, z powodów
założeń projektowych, które głoszą, że „otrzymane dane nie mogą być modyfikowane w żaden sposób”.

Administracja Tea
Szkielet TeaServlet wyświetla stronę WWW (napisaną oczywiście w Tea) służącą jako centrum administracyjne,
przy pomocy której administrator witryny może zarządzać wszystkimi szablonami Tea i wspierającymi klasami
aplikacji. Ekran administratora umożliwia przeglądanie aktualnie załadowanych szablonów i uruchomionych
aplikacji, a także wykorzystanie funkcji udostępnionych przez te aplikacje w stylu Javadoc. Umożliwia również
dostęp do dziennika zdarzeń dla szablonów, pozwalając administratorowi przeglądanie wszystkich dzienników
lub ich wybranych części, w oparciu o szablon i poziom dziennika (debug, info, warn lub error). Ekran
administracyjny dostarcza również mechanizmu służącego do kontrolowanego przeładowywania szablonów,
które zostanie szczegółowo omówione w dalszej części. Rysunek 14.1 przedstawia przykładową stronę
wyświetlającą szablony.
Rysunek 14.1.
Administrowanie
załadowanymi
obecnie
szablonami

W celu uruchomienia aplikacji administracyjnej TeaServlet należy wcześniej skonfigurować plik


TeaServlet.properties. Przykładowa konfiguracja przedstawiona jest w przykładzie 14.4.
Przykład 14.4. Początkowy plik TeaServlet.properties
# TeaServlet.properties

# Określenie lokalizacji szablonów Tea (plików .tea)


template.path = /tomcat/webapps/herbatka/WEB-INF/szablony

# Określenie opcjonalnej ścieżki do miejsca, W którym system będzie zapisywał


# skompilowane szablony jako pliki .class
template.classes = /tomcat/webapps/herbatka/WEB-INF/klasySzablony

# Określenie opcjonalnego domyślnego szablonu, który zostanie załadowany, jeżeli


nie
# poda się innego
template.default = Indeks

# Określenie wspierających aplikacji, które zostaną załadowane do systemu


TeaServlet
applications {
"System" {
# SystemApplication dostarcza wsparcia administracyjnego dla TeaServlet
class = com.go.teaservlet.AdminApplication
init {
# klucz bezpieczeństwa dla strony administracyjnej
admin.key = admin
admin.value = true
}
}
"Inne" {
class = WiecejNaTematAplikacjiPozniej
}

# Dla przykładu 14-8


"NarzedziaAp" {
class = NarzedziaAp
init {
narzedziaPlik = /tomcat/webapps/herbatka/WEB-INF/narzedzia.xml
}
}
}

# Określenie, które wiadomości aplikacji są umieszczane w dzienniku zdarzeń


log.debug = true
log.info = true
log.warn = true
log.error = true
Plik TeaServlet.properties konfiguruje system TeaServlet podobnie jak plik web.xml konfiguruje aplikację
WWW. Plik ten podaje TeaServlet lokalizację szablonów oraz gdzie powinny zostać umieszczone skompilowane
klasy szablonów tak, aby mogły być przechowywane podczas ponownych uruchomień serwera. Zawiera także
informacje, które aplikacje powinny być załadowane dla wsparcia szablonów oraz który poziom wiadomości
dziennika zdarzeń powinien zostać domyślnie zastosowany. Każda aplikacja musi zostać zarejestrowana w
TeaServlet.properties przy pomocy określonego pliku klasy oraz opcjonalnie określonymi parami parametrów
nazwa-wartość.
Struktura TeaServlet.properties jest podobna do tradycyjnego pliku java.util.Properties, z kilkoma
poważnymi ulepszeniami — zachowano kolejność elementów, cudzysłowy (pojedyncze lub podwójne) mogą
zostać wykorzystane do zdefiniowania kluczy, w których osadzone są spacje, a własności mogą zostać
zagnieżdżone przy pomocy zakręconych nawiasów. Przykład 14.4 mógłby zostać napisany jako zwykły plik
właściwości Javy, jak przedstawiono poniżej (chociaż po odczytaniu go przez java.util.Properties
stracony zostałby porządek elementów). Plik ten mógłby zostać napisany przy pomocy XML, chociaż dla
prostych zastosowań takich jak to XML byłby niepotrzebnym utrudnieniem.
template.path = /tomcat/webapps/herbatka/WEB-INF/szablony
template.classes = /tomcat/webapps/herbatka/WEB-INF/klasySzablony
applications.System.class = com.go.teaservlet.AdminApplication
applications.System.admin.key = admin
applications.System.admin.value = true
applications.Inne.class = WiecejNaTematAplikacjiPozniej
log.debug = true
log.info = true
log.warn = true
log.error = true
Po dokonaniu edycji pliku TeaServlet.properties i ponownym uruchomieniu serwera, system jest gotowy do
działań administracyjnych. Domyślnym położeniem aplikacji administracyjnej (zakładając, że została ona
umieszczona w kontekście /herbatka) jest
http://localhost:8080/herbatka/tea/system/teaservlet/Admin?admin=true. Wyrażenie ?admin=true działa jako
prymitywne zabezpieczenie. Nazwa i wartość muszą zgadzać się z wartościami admin.key/admin.value z
pliku TeaServlet.properties. Tak naprawdę nic by się nie stało, jeżeli człowiek z zewnątrz uzyskałby dostęp do
stron administracyjnych, ale pozostawienie frontowych drzwi otwartych nie jest mądre, tak więc istnieje
prymitywny zamek. Przypuszczalnie społeczność Open Source szybko poprawi mechanizm bezpieczeństwa.
Najczęstszym zastosowaniem dla aplikacji administracyjnej jest ponowne ładowanie plików szablonów.
Szablony nie są przeładowywane podczas otwierania, jak strony JSP. Zmiany w szablonach nie są widoczne,
dopóki nie zostanie naciśnięty guzik „Reload Changes” („Przeładuj zmiany”) na ekranie administratora
Templates (Szablony). Kompilowanie szablonów tylko na żądanie jest bardziej wydajne niż sprawdzanie
znacznika czasu podczas żądania oraz pozwala także na wystąpienie wyraźniejszego kroku „opublikowanie”
aktywnej witrynie. Aplikacja administracyjna nie przyjmuje zmian, jeżeli szablonu nie uda się skompilować,
pozostawiając poprzednią wersję, tak więc klienci absolutnie nie mogą zobaczyć błędu kompilacji. Wszystkie
błędy występujące podczas kompilacji zostają wyświetlone przez aplikację administracyjną, jak przedstawiono
na rysunku 14.2.
Rysunek 14.2.
Obszerne i precyzyjne
wiadomości o błędach
kompilacji

Proszę zauważyć, że lokalizacja błędów i ich opis jest są absolutnie precyzyjne, TeaServlet może to uczynić,
ponieważ szablony Tea są kompilowane bezpośrednio do kodu bajtowego Javy! Nie jest tworzony żaden
pośredni plik .java, a w związku z tym nie jest konieczne zastosowanie żadnego kompilatora Javy.
Kompilowanie szablonów bezpośrednio do kodu bajtowego jest wykonywane przy pomocy technik JIT i
HotSpot w celu osiągnięcia maksymalnej wydajności, ale inaczej niż w przypadku stron JSP, kompilator pracuje
bezpośrednio na źródle utworzonym przez człowieka i w związku z tym potrafi lepiej zdiagnozować błędy bez
pośredniczącego pliku rozmywającego bezpośredniość. Poza tym, Tea nie wymaga dołączenia kompilatora Javy
do serwera WWW, który jest potrzebny w przypadku JSP. Wymaganie to sprawia problemy związane z
licencjonowaniem. Tea może również prekompilować szablony, umożliwiając dystrybucję szablonów bez ich
źródeł.

Skompilowane szablony: szansa dla biznesu

Uwaga!!!!

Możliwość dystrybucji prekompilowanych szablonów Tea tworzy nowy, interesujący model biznesowy.
Producent tworzący aplikację WWW przy pomocy Tea może sprzedawać aplikację WWW bez udostępniania
oryginalnego kodu źródłowego szablonów. Potrzebna jest jedynie biblioteka Tea, która posiada możliwość
przenoszenia pomiędzy serwerami i może być dołączana do aplikacji. Możliwość udostępniania produktu bez
kodów źródłowych może spowodować, że niektórzy producenci utworzą nowe aplikacje WWW na sprzedaż.

Proszę zauważyć, że JSP również obsługuje prekompilowanie stron. Niestety, kod bajtowy tworzony przez
JSP jest często uzależniony od klas związanych z danym kontenerem, a tworzona klasa musi być zgodna z
zależną od kontenera konwencją nadawania nazw (proszę zobaczyć przykład 18-2 z rozdziału 18,
„JavaServer Pages”). Własność ta powoduje przywiązanie prekompilowanych stron JSP do konkretnego
kontenera.

Tea elegancko obsługuje również błędy czasu wykonania. Wyjątki, które wystąpią wewnątrz Tea zawierają
ścieżki, w których znajdują się numery linii szablonów Tea, na przykład:
2001/10/23 19:29:12.105 GMT> java.lang.NullPointerException:
at com.go.teaservlet.template.ProstaStrona.execute(ProstaStrona.tea:5)
at java.lang.reflect.Method.invoke(Native Method)
at co.go.tea.runtime.TemplateLoader$TemplateImpl.execute(TemplateLoader.java,
Compiled Code)
at com.go.teaservlet.TeaServlet.processTemplate(TeaServlet.java, Compiled
Code)
at com.go.teaservlet.TeaServlet.doGet(TeaServlet.java:238)
<itd.>
Wiadomości takie jak powyższa mogą być przeglądane przy pomocy aplikacji administracyjnej TeaServlet.
Kiedy występują, klient domyślnie otrzymuje stronę błędu o kodzie stanu 500, celowo bez dodatkowych
informacji w celu zachowania bezpieczeństwa.
Tea posiada również własność „strażnik wyjątków”. Własność ta pozwala Tea na kontynuowanie przetwarzania
strony podczas zapisywania błędu w dzienniku. Tea tworzy wszystkie części strony, które może omijając
niewielką część, na którą wpływ ma wyjątek. Dla wielu witryn strona, której brakuje niewielkiej części jest
lepsza niż błąd serwera zwracany użytkownikowi.

Zastosowania Tea
Jak dotychczas omawiane były samotne szablony Tea, które są bardzo słabe — celowo słabe. Ale szablony Tea
są jak frontowi żołnierze piechoty — kiedy samotni, słabi, ale kiedy w zespole z zaawansowanym wsparciem
lotniczym, niezwykle potężni. Poniżej opisano sposoby uczynienia szablonów Tea potężniejszymi.
Szablony mogą uzyskać dostęp do każdej funkcji z dowolnej „aplikacji” zainstalowanej w TeaServlet poprzez
plik TeaServlet.properties. (Każda aplikacja WWW może posiadać inny plik TeaServlet.properties i w związku z
tym inny zestaw zainstalowanych aplikacji.) Szablony wywołują funkcję poprzez wymienienie w bloku kodu
nazwy funkcji, po której następuje para nawiasów. Funkcje mogą przyjmować parametry i zwracać wartości:
<% lista = pobierzRekordy() %>
<% usunRekord(8086) %>
Ekran administracyjny TeaServlet wyświetla listę wszystkich dostępnych funkcji z wszystkich zainstalowanych
aplikacji. Jeżeli dwie aplikacje deklarują tę samą funkcję, szablon może poprzedzić wywołanie funkcji nazwą
aplikacji w celu uniknięcia niejasności:
<% lista = ApListaRekordów.pobierzRekordy() %>
Istnieje zbiór klas narzędziowych dostępnych dla wszystkich szablonów utworzonych w celu wspomożenia w
podstawowych zadaniach przetwarzania tekstu. Poniższe podrozdziały wymieniają niektóre z najczęściej
wywoływanych funkcji narzędziowych.

Przetwarzanie tekstu
Poniżej przedstawiono niektóre popularne funkcje przetwarzające tekst.
Date currentDate()
Zwraca aktualną datę.
void dateFormat (String format)
Zmienia format wszystkich dat wyświetlanych w późniejszym czasie przez ten szablon.
void numberFormat (String format)

Zmienia format wszystkich liczb wyświetlanych w późniejszym


czasie przez ten szablon (wyłączając liczby zawarte
dosłownie w łańcuchach).
void nullFormat (String format)

Przydziela łańcuch, który zostanie wykorzystany zamiast null


podczas wyświetlania zmiennej o wartości null.
void setLocale(String jezyk, String kraj)

Zmienia lokalizację dla wszystkich dat, liczb itp.


wyświetlanych w późniejszym czasie przez ten szablon.
boolean startsWith(String lan, String przedrostek)

Zwraca true, jeżeli łańcuch rozpoczyna się od określonego


przedrostka.
boolean endsWith(String lan, String przyrostek)

Zwraca true, jeżeli łańcuch kończy się określonym


przyrostkiem.
String subString(String lan, int poczIndeks, int konIndeks)

Zwraca część łańcucha od indeksu początkowego do końcowego


(wyłączając indeksy).
String trim (String lan)
Zwraca łańcuch bez początkowych i końcowych pustych miejsc
int[] find(String lan, String szuka)
int[] find(String lan, String szuka, int odIndeks)

Zwraca indeksy, w których łańcuch zawiera poszukiwany String,


z opcjonalnym indeksem początkowym.
String replace(String zrodlo, String wzor, String zamiana)
String replace(String zrodlo, String wzor, String zamiana int odIndeks)
String replace(String zrodlo, String wzor, String zamiana int odIndeks int
doIndeks)

Zamienia wszystkie dane dokładnie pasujące do wzoru na podany


łańcuch zamiany, z opcjonalnym indeksem początkowym i
końcowym.

Obsługa zawartości
Poniżej wymieniono popularne metody obsługujące zawartość.
void InsertFile (string nazwapliku)

Wstawia zawartość danego pliku do wyświetlanej strony. Nie


wstawia nic, jeżeli plik nie może zostać odczytany.
Nazwa pliku może być względna lub bezwzględna. Jeżeli
bezwzględna, ścieżka jest odnogą katalogu macierzystego
szablonów (zazwyczaj katalogu macierzystego dokumentów
serwera WWW).
void InsertURL (string url)

Wstawia zawartość danego URL-a do wyświetlanej strony. Nie


wstawia nic, jeżeli URL nie może zostać odczytany. URL
może być względny lub bezwzględny.
boolean fileExists(String nazwapliku)

Zwraca true, jeżeli plik o podanej nazwie istnieje.


boolean URLExists(String url)
Zwraca true, jeżeli podany URL istnieje.

Obsługa żądań/odpowiedzi
Dodatkowo przedstawiono kilka często wykorzystywanych metod żądań/odpowiedzi.
com.go.teaservlet.HttpContext.Request getRequest()
com.go.teaservlet.HttpContext.Request getRequest(String kodowanie)

Zwraca obiekt zawierający informacje o żądaniu, z opcjonalnym


kodowaniem pozwalającym na automatyczną konwersję
wartości parametrów.
void setContentType(String typZawartosci)

Ustawia typ zawartości odpowiedzi.


void setHeader(String nazwa, String wartosc)

Ustawia nagłówek odpowiedzi.


void setStatus(int ks)
Ustawia kod stanu odpowiedzi.
void sendRedirect(String url)
Przekierowuje żądanie do danego URL-a.
void sendError(int ks)
void sendError(int ks, String wiadomosc)

Wysłanie strony z błędem o danym kodzie stanu z opcjonalną


wiadomością o błędzie.
Szablon w przykładzie 14.5 demonstruje sposób wywoływania klas narzędziowych przez szablon w celu
wyświetlenia danych procentowych dla klikania na reklamy danego dnia. Data i procenty formatowane są w stylu
francuskim.
Przykład 14.5.
Francuskie klikanie
<%template klik() %>

<HTML<HEAD><TITLE>Testowanie funkcji wbudowanych w Tea</TITLE></HEAD><BODY>

<%
// Ustawienie lokalizacji. Nie zmienia się ona dla wszystkich danych wyświetlanych
// później przez szablon
set locale ("fr", "CA") // Quebec

// Określenie formatowania dat I liczb


dateFormat("d MMMM yyyy")
numberFormat("#0,00%")

// Pewne fałszywe dane


klik- 485.0
dostep = 7673.0
%>

<H2><% currentDate %></H2>


Le pourcentage: <% klik/dostep %>

</BODY></HTML>
Szablon wyświetla dane podobne do poniższych, z datą i liczbami właściwymi dla kogoś, kto mieszka w
Quebecu:
23 octobre 2001
Le pourcentage: 6,32%

Tworzenie aplikacji Tea


Teraz przedstawiony zostanie sposób tworzenia własnej aplikacji Tea. Jest to łatwe — należy po prostu napisać
klasę aplikacji i klasę kontekstu. Wszystkie klasy aplikacji są rozszerzeniem interfejsu
com.go.teaservlet.Application. Podobnie jak serwlet, interfejs Application posiada metody
init() i destroy() obsługujące kwestie związane z okresem trwałości. Zamiast metody service()
Application posiada parę metod createContext() i getContextType(). Metoda
createContext() zwraca obiekt kontekstu, którego metody zostają udostępnione szablonom jako funkcje.
Metoda getContextType() zwraca java.lang.Class obiektu zwracanego przez createContext()
w celu ułatwienia sprawdzania typu.
Poniższe dwa przykłady przedstawiają prostą aplikację próbującą na różne sposoby określić nazwę użytkownika.
Klasa NazwaAp to klasa aplikacji (przykład 14.6); klasa NazwaKontekst to obiekt kontekstu tworzony przez
NazwaAp i udostępniany szablonom (przykład 14.7).
Przykład 14.6.
Klasa NazwaAp
import com.go.teaservlet.*;
import javax.servlet.*;

public class NazwaAp implements Application {

// Zarówno init(ApplicationConfig) jak i destroy() muszą być zaimplementowane


ponieważ
// zostały zadeklarowane w interfejsie Application interface. Mogą pozostać
puste.
public void init(ApplicationConfig konfig) throws ServletException {
}

// Tworzenie kontekstu dostarcza funkcji dostępnych z szablonów.


public Object createContext(ApplicationRequest zadanie,
ApplicationResponse odpowiedz) {
// Często przekazuje się żądanie i odpowiedź, nawet jeżeli nie są
wykorzystywane,
// ponieważ mogą być wykorzystane później
return new NameContext(zadanie, odpowiedz);
}

// Ta metoda musi zostać zaimplementowana, by zwracać klasę obiektu zwracanego


przez
// createContext()
public Class getContextType() {
return NameContext.class;
}

public void destroy() {


}
}
TeaServlet wywołuje metodę init() jeden raz, kiedy aplikacja zostaje załadowana po raz pierwszy, w celu
dania jej możliwości przeprowadzenia inicjacji. Wywołuje jeden raz destroy() podczas kończenia pracy w
celu dania aplikacji możliwości wyczyszczenia. Metoda init() przyjmuje parametr ApplicationConfig,
który jest podinterfejsem ServletConfig z trzema dodatkowymi metodami — getLog(), getName() i
getProperties() — służącymi do odczytywania dziennika zdarzeń, nazwy i właściwości inicjacji.
TeaServlet wywołuje metodę createContext() przed obsługą żądań i udostępnia wszystkie metody
zwracanego obiektu kontekstu jako funkcje żądanego szablonu Tea. Jeżeli każdy użytkownik musi posiadać swój
własny kontekst, może to być w łatwy sposób osiągnięte przy pomocy standardowego śledzenia sesji w metodzie
createContext(). Metoda CreateContext() przyjmuje jako parametry ApplicationRequest i
ApplicationResponse. Są to podinterfejsy HttpServletRequest i HttpServletResponse z
dodatkiem kilku metod Tea.
Przykład 14.7.
Klasa NazwaKontekst
import com.go.teaservlet.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class NazwaKontekst {

ApplicationRequest zadanie;
ApplicationResponse odpowiedz;
String nazwa;

public NazwaKontekst(ApplicationRequest zadanie,


ApplicationResponse odpowiedz) {
this.zadanie = zadanie;
this.odpowiedz = odpowiedz;
}

public String pobierzNazwa() {


// Jeżeli nazwa wcześniej określona, zwrócenie jej
if (nazwa != null) {
return nazwa;
}

// Próba określenia nazwy użytkownika


nazwa = zadanie.getRemoteUser();

// Jeżeli nazwa logowania nie jest dostępna, próba odczytania parametru


if (nazwa == null) {
nazwa = zadanie.getParameter("nazwa");
}

// Jeżeli nazwa nie jest dostępna jako parametr, próba sesji


if (nazwa == null) {
nazwa = (String) zadanie.getSession().getAttribute("nazwa");
}

// Jeżeli nazwy nie ma w sesji, próba cookie


if (nazwa == null) {
Cookie[] cookies = zadanie.getCookies();
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].pobierzNazwa().equals("nazwa")) {
nazwa = cookies[i].getValue();
}
}
}

// Jeżeli nazwy nie ma też w sesji, poddanie się


return nazwa;
}
}
Metoda pobierzNazwa() próbuje określić nazwę użytkownika-klienta poprzez przeszukanie danych
logowania, listy parametrów, danych sesji i cookies żądania. Zwraca null, jeżeli odnalezienie informacji o
nazwie nie jest możliwe. Szablony uzyskują dostęp do nazwy przez wywołanie funkcji pobierzNazwa().
Proszę zauważyć, że kontekst określa nazwę klienta w metodzie pobierzNazwa(), a nie w konstruktorze. Jest
to sztuczka mająca na celu poprawienie wydajności. Jeżeli logika znajdowałaby się w konstruktorze, byłaby
wykonywana podczas obsługi każdego żądania, niezależnie od tego, czy szablon wywoływałby
pobierzNazwa(), czy nie.
Uczynienie aplikacji NazwaAp dostępną wymaga następującego krótkiego dodatku do pliku
TeaServlet.properties:
"NazwaAp" {
class = NazwaAp
}
Można sprawdzić, czy aplikacja została prawidłowo załadowana przez przejrzenie łącza Applications (Aplikacje)
aplikacji administracyjnej TeaServlet23. Po załadowaniu tej aplikacji każdy szablon może wyświetlić nazwę
klienta przy pomocy funkcji pobierzNazwa():
<% template nazwy() %>

<%
setContentType("text/plain")
nullFormat("Nieznane")
%>

23
Jeżeli klasy aplikacji nie zostały odnalezione nawet jeżeli wygląda na to, że powinny były, powodem może być kwestia
związana z mechanizmem ładowania klas. Problemy mogą wystąpić, jeżeli TeaServlet.jar i klasy aplikacji są ładowane przez
inne mechanizmy. Należy albo umieścić obie w ścieżce klas systemu, gdzie zostaną odnalezione przez początkowy
mechanizm, albo umieścić obie w katalogu WEB-INF, gdzie zostaną odnalezione przez mechanizm ładowania klas aplikacji
WWW. (Proszę umieścić TeaServlet.jar w katalogu WEB-INF/lib, a klasy aplikacji w WEB-INF/classes.) Proszę także
pamiętać, że z tych samych powodów klasy wspierające nie są ładowane ponownie nawet po wybraniu Reload All (Przeładuj
wszystko) ze strony administracyjnej TeaServlet.
Nazwa użytkownika to <%pobierzNazwa() %>

Aplikacja „Narzędzia”
Określanie nazwy użytkownika nie jest zbyt realistycznym zastosowaniem, tak więc opisane teraz zostanie coś
bliższego rzeczywistości. Utworzona zostanie aplikacja, która wyświetla listę różnych dostępnych narzędzi do
tworzenia zawartości (aplikacja mniej więcej taka, jakiej potrzebuje Servlets.com). Informacje o narzędziach
będą pochodzić z pliku XML, chociaż równie dobrze mogłyby pochodzić z bazy danych. Przykład 14.8
przedstawia klasę NarzedziaAp.
Przykład 14.8.
Centrum aplikacji „Narzędzia”
import com.go.teaservlet.*;
import com.go.trove.log.*;
import java.sql.Timestamp;
import java.util.*;
import javax.servlet.*;

public class NarzedziaAp implements Application {

private Log dziennik;


private Narzedzie[] narzedzia;

public void init(ApplicationConfig konfig) throws ServletException {


// Dziennik zdarzeń dla wydarzeń specyficznych dla tej aplikacji
dziennik = konfig.getLog();

// Pobranie danych o narzędziach w metodzie init() dla zachowania prostoty


String narzedziaPlik = konfig.getInitParameter("narzedziaPlik");
if (narzedziaPlik == null) {
throw new ServletException(
"Plik danych o narzędziach musi zostać określony jako parametr inicjacji
narzedziaPlik");
}
dziennik.debug("Pobieranie narzędzi z " + narzedziaPlik);
try {
narzedzia = Narzedzie.pobierzNarzedzia(narzedziaPlik);
if (nazredzia.length == 0) {
dziennik.warn("Nie znaleziono narzędzi w " + narzeziaPlik);
}
}
catch (Exception w) {
dziennik.error(w);
throw new ServletException(e);
}
}

public Object createContext(ApplicationRequest zadanie,


ApplicationResponse odpowiedz) {
return new NarzedziaKontekst(zadanie, odpowiedz, this);
}

public Class getContextType() {


return NarzedziaKontekst.class;
}

public void destroy() {


}

public Narzedzie[] pobierzNarzedzia() {


// Zazwyczaj „aplikacja” utrzymywałaby lub miałaby dostęp do wcześniej
utworzonej
// bazy danych. Tutaj dla zachowania prostoty zastosowano plik XML
return narzedzia;
}

public Narzedzie[] pobierzNarzedzie(String stan) {


// Zwracane tylko narzędzi o określonym stanie
// (wysłane, żyje, odrzucone, lub martwe)
List lista = new LinkedList();
for (int i = 0; i < narzedzia.length; i++) {
if (narzedzia[i].pobierzZnacznikStanu().equalsIgnoreCase(stan)) {
lista.add(narzedzia[i]);
}
}
return (Narzedzie[]) lista.toArray(new Narzedzie[0]);
}
}
Metoda init() NarzedziaAp wykorzystuje przekazany jej ApplicationConfig w celu odczytania i
zapisania odwołania do dziennika zdarzeń, następnie wykorzystuje konfig w celu pobrania parametru inicjacji
narzedziaPlik. Wartość tego parametru musi zostać określona w pliku TeaServlet.properties:
"NarzedziaAp" {
class = NarzedziaAp
init {
narzedziaPlik = /tomcat/webapps/herbatka/WEB-INF/narzedzia.xml
}
}
Metoda init() wywołuje następnie pobierzNarzedzia() w celu pobrania informacji o narzędziach z
pliku XML do tablicy narzędzi. Plik XML powinien wyglądać podobnie do przykładu 14.9.
<?xml version="1.0"?>

<narzedzia>
<narzedzie id="1">
<nazwa>JavaServer Pages</nazwa>
<domURL>http://java.sun.com/products/jsp</domURL>
<komentarz>
JavaServer Pages (JSP) to technologia utworzona przez Sun Microsystems, blisko
związana z serwletami. Tak jak w przypadku serwletów, Sun udostępnia specyfikację
JSP i inni producenci rywalizują swoimi implementacjami standardu. To, że jest
dziełem firmy Sun, stawia JSP w bardzo uprzywilejowanej pozycji i jeżeli JSP
rozwiązywałaby wystarczającą ilość problemów użytkowników, przypuszczalnie
zagarnęłaby rynek jeszcze przed pojawieniem się alternatyw. Jednak zadziwiająco
duża liczba użytkowników jest rozczarowana JSP i jej alternatywy zyskują
popularność.
</komentarz>
<znacznikStanu>ŻYJE</znacznikStanu>
<czasUtworzenia>1998-03-17 00:00:00.000</czasUtworzenia>
<czasModyfikacji>1999-12-16 00:00:00.000</czasModyfikacji>
</narzedzie>
<narzedzie id="2">
<nazwa>Tea</nazwa>
<domURL>http://opensource.go.com</domURL>
<komentarz>
Tea to nowy produkt Open Source Walt Disney Internet Group, tworzony wewnętrznie
przez lata w celu rozwiązania problemów z tworzeniem witryn takich jak ESPN.com.
Tea jest podobna do JSP, ale pozbawiona jej wielu wad, i już posiada ogromną ilość
narzędzi wspierających.
</komentarz>
<znacznikStanu>ŻYJE</znacznikStanu>
<czasUtworzenia>2000-07-12 00:00:00.000</czasUtworzenia>
<czasModyfikacji>2000-07-12 00:00:00.000</czasModyfikacji>
</narzedzie>
<narzedzie id="3">
<nazwa>WebMacro</nazwa>
<domURL>http://jakarta.apache.org</domURL>
<komentarz>
WebMacro to mechanizm szablonów utworzony przez firmę Semiotek jako część projektu
Shimari, włączony teraz do Apache Jakarta Project. WebMacro było wykorzystywane w
komercyjnych witrynach takich jak AltaVista.com, zintegrowane w szkieletach Open
Source takich jak Turbine i Melati oraz wykorzystywane w znanych projektach Open
Source takich jak JetSpeed.
</komentarz>
<znacznikStanu>ŻYJE</znacznikStanu>
<czasUtworzenia>1998-11-19 00:00:00.000</czasUtworzenia>
<czasModyfikacji>2000-08-31 00:00:00.000</czasModyfikacji>
</narzedzie>
<narzedzie id="4">
<nazwa>Element Construction Set</nazwa>
<domURL>http://java.apache.org/ecs</domURL>
<komentarz>
Pakiet Element Construction Set (ECS) pochodzący z Java Apache Project to zbiór
klas utworzonych na podstawie htmlKona, produktu WebLogic (teraz BEA Systems). ECS
posiada wiele ograniczeń, ale rozwiązuje konkretny zestaw problemów.
</komentarz>
<znacznikStanu>ŻYJE</znacznikStanu>
<czasUtworzenia>1999-03-31 00:00:00.000</czasUtworzenia>
<czasModyfikacji>2000-06-16 00:00:00.000</czasModyfikacji>
</narzedzie>
<narzedzie id="5">
<nazwa>XMLC</nazwa>
<domURL>http://xmlc.enhydra.org</domURL>
<komentarz>
XMLC wykorzystuje XML w celu osiągnięcia niemal tej samej mocy, co ECS, bez wielu
jego ograniczeń. Technika ta została utworzona przez Lutris jako część ich serwera
Open Source — Enhydra Application Server, może być stosowana jako osobny element.
</komentarz>
<znacznikStanu>ŻYJE</znacznikStanu>
<czasUtworzenia>1998-10-11 00:00:00.000</czasUtworzenia>
<czasModyfikacji>2000-03-09 00:00:00.000</czasModyfikacji>
</narzedzie>
<!—itd. -->
</narzedzia>
Aplikacja odczytuje powyższy plik przy pomocy interfejsu JDOM, interfejsu Open Source służącego do odczytu,
zapisu i manipulacji XML przy pomocy Javy. Jeżeli zamiast tego wyniki powinny być odczytywane z bazy
danych, można to uczynić po prostu zamieniając SAXBuilder JDOM (który tworzy Document z pliku lub
potoku przy pomocy analizatora SAX) na ResultSetBuilder JDOM (który tworzy Document z
java.sql.ResultSet, klasy przekazanej JDOM).24 W trakcie pisania niniejszej książki, JDOM ciągle był w fazie
tworzenia. Niniejszy przykład wykorzystuje JDOM Beta 5. W celu pracy z ostateczną wersją JDOM mogą się
okazać konieczne drobne zmiany.
Metoda createContext() konstruuje nawy egzemplarz NarzedziaKontekst. Kod
NarzedziaKontekst jest przedstawiony w przykładzie 14.10.
Przykład 14.10.
Kontekst zawierający wszystkie dostępne funkcje
import com.go.teaservlet.*;

public class NarzedziaKontekst {

ApplicationRequest zadanie;
ApplicationResponse odpowiedz;
NarzedziaAp ap;

public NarzedziaKontekst(ApplicationRequest zadanie,


ApplicationResponse odpowiedz,
NarzedziaAp ap) {
this.zadanie = zadanie;
this.zadanie = zadanie;
this.ap = ap;
}

public Narzedzie[] pobierzNarzedzia() {


return ap.pobierzNarzedzia();
}

public Narzedzie[] pobierzNarzedzia(String stan) {


return ap.pobierzNarzedzia(stan);
}
}
Metody publiczne w kontekście są funkcjami udostępnianymi szablonom Tea. W tym miejscu metody odwołują
się z powrotem do aplikacji, w można powiedzieć dobrym stylu, ponieważ konteksty powinny być tak małe, jak
to jest tylko możliwe; stan powinien być przechowywany w aplikacji. Klasa Narzedzie sama działa jako
magazyn informacji Narzedzie. Jej kod przedstawiony jest w przykładzie 14.11.
Przykład 14.11.
Klasa pobierająca i przechowująca dane Narzedzie
import java.io.*;
import java.sql.*;
import java.util.*;
import org.jdom.*;
import org.jdom.input.*;

public class Narzedzie {


// Dane na temat tego rekordu narzedzie
public int id;
public String nazwa;
public String domURL;
public String komentarz;
public String znacznikStanu;
public Timestamp czasUtworzenia;
public Timestamp czasModyfikacji;

// Tea może uzyskać dostęp jedynie do podstawowych własności, potrzebne są więc


// specjalne metody dostępu
public int pobierzId() { return id; }
public String pobierzNazwa() { return nazwa; }
public String pobierzDomURL() { return domURL; }

24
Jason Hunter jest jednym z twórców JDOM, razem z Brettem McLaughlinem. Większa ilość informacji na temat JDOM
dostępna jest pod adresem http://jdom.org.
public String pobierzKomentarz() { return komentarz; }
public String pobierzZnacznikStanu() { return znacznikStanu; }
public Timestamp pobierzCzasUtworzenia() { return czasUtworzenia; }
public Timestamp pobierzCzasModyfikacji() { return czasModyfikacji; }

public int pobierzCzasUtworzeniaDni() {


return (int) ((System.currentTimeMillis() - czasUtworzenia.getTime()) /
(24 * 60 * 60 * 1000)); // milisekundy w dniu
}

public int pobierzCzasModyfikacjiDni() {


return (int) ((System.currentTimeMillis() - czasModyfikacji.getTime()) /
(24 * 60 * 60 * 1000)); // milisekundy w dniu
}

// Idealnie użyto by metod takich jak te, ale Tea pozwala jedynie na dostęp
// do własności obiektu. Nie będą one widoczne.
public boolean czyNowy(int dni) {
return pobierzCzasUtworzeniaDni() < dni;
}

public boolean czyUaktualniony(int dni) {


return pobierzCzasModyfikacjiDni () < dni;
}

public static Narzedzie[] pobierzNarzedzia(String plikNarzedzia) throws


Exception {
// Odczytanie danych narzędzi z pliku XML zawierającego elementy <narzedzie>
// Wykorzystanie interfejsu JDOM by uprościć to działanie (http://jdom.org)
List obiektyNarzedzia = new LinkedList();

SAXBuilder budowa = new SAXBuilder();


Document dokument = budowa.build(new File(plikNarzedzia));
Element root = dokument.getRootElement();
List elementyNarzedzia = root.getChildren("narzedzie");
Iterator i = elementyNarzedzia.iterator();
while (i.hasNext()) {
Element narzedzie = (Element) i.next();
Narzedzie n = new Narzedzie();
n.id = narzedzie.getAttribute("id").getIntValue();
n.nazwa = narzedzie.getChild("nazwa").getTextTrim();
n.domURL = narzedzie.getChild("domURL").getTextTrim();
n.komentarz = narzedzie.getChild("komentarz").getTextTrim();
n.znacznikStanu = narzedzie.getChild("znacznikStanu").getTextTrim();
n.czasUtworzenia = Timestamp.valueOf(
narzedzie.getChild("czasUtworzenia").getTextTrim());
t.czasModyfikacji = Timestamp.valueOf(
narzedzie.getChild("czasModyfikacji").getTextTrim());
obiektyNarzedzia.add(n);
}

return (Narzedzie[]) obiektNarzedzia.toArray(new Narzedzie[0]);


}
}
Proszę zauważyć, że chociaż klasa powyższa posiada publiczne zmienne przechowujące jej stan, to nie są one
widoczne przez szablony Tea. Szablony Tea mają dostęp jedynie do funkcji zadeklarowanych w zainstalowanych
kontekstach i do podstawowych własności obiektów zwracanych przez te funkcje. Dlatego klasa Narzedzie
posiada różne metody dostępu. Narzedzie posiada również dwie metody ułatwiające, które zwracają ilość dni
od utworzenia i modyfikacji rekordu. Można by pomyśleć o wykorzystaniu metody isNewWithin(int
dni) lub isUpdateWithin(int dni). Jednak szablon nie posiada dostępu do tych metod, ponieważ nie
należą one do podstawowych własności obiektu.
Przykład 14.12 przedstawia prosty fronton szablonu dla tej aplikacji. Nosi on nazwę widoknarz1 i przyjmuje
opcjonalny parametr stan, przy pomocy którego można ograniczyć wyświetlane narzędzia do posiadających
określony stan. Przykładowa strona wyświetlana przez ten szablon jest przedstawiona na rysunku 14.3.
Przykład 14.12.
Prosty widok narzędzi
<% template widoknarz1(String stan) %>

<%
if (stan == null) {
narzedzia = pobierzNarzedzia("zyje");
}
else {
narzedzia = pobierzNarzedzia(stan)
}
%>

<% foreach (narzedzie in narzedzia) { %>

<HR SIZE=2 ALIGN=LEFT>

<H3>
<% narzedzie.nazwa %>
<%
if (narzedzie.czasUtworzeniaDni < 45) {
'<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>'
}
else if (narzedzie.czasModyfkacjiDni < 45) {
'<FONT COLOR=#FF0000><B> (Uaktualnienie!) </B></FONT>'
}
%>
</H3>
<A HREF="<% narzedzie.domURL %>"><% narzedzie.domURL %></A><BR>

<% tool.komentarz %>

<% } %>

Rysunek 14.3.
Lista narzędzi bez dodatków
Jak można dostrzec, na powyższym rysunku nie ma żadnych ozdobników (nagłówka, stopki, paska bocznego)
koniecznych na profesjonalnej stronie. Dekoracje te można dodać przez wywołanie innych szablonów, jak
przedstawiono w poprawionym szablonie widoknarz2 w przykładzie 14.13.
Przykład 14.13
Bardziej złożony widok narzędzi
<% template widoknarz2(String stan) %>

<%
tytul = "Lista narzędzi"
tytul2 = "Lista narzędzi służących do tworzenia zawartości"
opis = "Bez narzędzi, ludzie nie są niczym więcej niż zwierzętami. I to " &
"dość słabymi. Poniżej przedstawiono listę opartych na serwletach " &
"narzędzi do tworzenia zawartości, które można wykorzystać w celu " &
"wzmocnienia się."
%>
<% call naglowek(tytul, tytul2, opis) %>

<%
if (stan == null) {
narzedzia = pobierzNarzedzia("zyje");
}
else {
narzedzia = pobierzNarzedzia(stan)
}
%>

<% foreach (narzedzie in narzedzia) { %>

<HR SIZE=2 ALIGN=LEFT>

<H3>
<% narzedzie.nazwa %>
<%
if (narzedzie.czasUtworzeniaDni < 45) {
'<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>'
}
else if (narzedzie.czasModyfkacjiDni < 45) {
'<FONT COLOR=#FF0000><B> (Uaktualnienie!) </B></FONT>'
}
%>
</H3>
<A HREF="<% narzedzie.domURL %>"><% narzedzie.domURL %></A><BR>

<% tool.komentarz %>

<% } %>

<% call stopka() %>


Powyższy nowy szablon definiuje zmienne dla tytułów i opisu strony. Następnie wywołuje szablon nagłówka
przekazując mu te wartości. Szablon nagłówka tworzy zawartość nagłówka i paska bocznego, po czym
umożliwia widoknarz2 dodanie właściwej zawartości. Na końcu strony szablon stopki dodaje stopkę strony.
Przykładowe szablony nagłówka i stopki przedstawione są w przykładach 14.14 i 14.15. Strona wyświetlana
przez poprawiony szablon jest przedstawiona na rysunku 14.4.
Przykład 14.14.
Plik nagłówka
<% template naglowek(String tytul, String tytul2, String opis) %>

<HTML><HEAD><TITLE><% tytul %></TITLE></HEAD>

<BODY BGCOLOR="#FFFFFF" BACKGROUND="/obrazki/tlo.gif"


LINK="#003333" ALINK="#669999" VLINK="#333333">

<IMG SRC="/obrazki/banner.gif" WIDTH=600 HEIGHT=87 BORDER=0><BR>

<TABLE>
<TR>
<TD WIDTH=125 VALIGN=TOP>
<BR><BR><BR>
<FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#FF0000">
<A HREF="/indeks.html">Strona główna</A><BR>
<A HREF="/hosting.html">Hosting</A><BR>
<A HREF="/mechanizmy.html">Mechanizmy</A><BR>
</FONT>
</TD>

<TD WIDTH=475>

<TABLE CELLPADDING=5><TR><TD WIDTH=600 BGCOLOR="#006699" VALIGN=TOP>


<B><FONT FACE="Arial,Helvetica" SIZE="+2">
<% tytul %>
</FONT></B>
</TD></TR></TABLE>

<B><FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#003366">


<% tytul2 %>
</FONT></B><P>

<P>

<FONT FACE="Arial,Helvetica">
<% opis %>
Przykład 14.15.
Plik stopki
<% template stopka() %>

</FONT>

</TD>
</TR>

<TR>
<TD></TD>
<TD WIDTH=475 ALIGN=CENTER COLSPAN=3>
<HR>
<FONT FACE="Arial,Helvetica">
<A HREF="/indeks.html">Strona główna</A>&nbsp;&nbsp;
<A HREF="/hosting.html">Hosting</A>&nbsp;&nbsp;
<A HREF="/mechanizmy.html">mechanizmy</A>&nbsp;&nbsp;<P>
</FONT>

<TABLE WIDTH=100%>
<TR>
<TD WIDTH=260 ALIGN=LEFT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
<A HREF="/wlasnosc.html">Wlasność</A> &copy; 2000 Jason Hunter<BR>
Wszystkie prawa zastrzeżone.</TD>
<TD WIDTH=5></FONT></TD>
<TD WIDTH=230 ALIGN=RIGHT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
Kontakt: <A HREF="mailto:webmaster@servlets.com">webmaster@servlets.com</A>
</FONT></TD>
</TR>
</TABLE>

</TD>
</TR>
</TABLE>

</BODY>
</HTML>

Rysunek 14.4.
Lista narzędzi z otaczającymi dodatkami
Według powyższego projektu zmiany w nagłówku, paku bocznym i stopce są ograniczone do pojedynczych
plików. Możliwe jest również wykorzystanie pojedynczego pliku do stworzenia wyglądu całej strony, wstawiając
właściwą zawartość przy pomocy polecenia <% ... %>. Podejście to dostarcza większych możliwości i
pozwala na oddzielenie właściwej zwartości od formatowania, chociaż trzeba się do tego podejścia
przyzwyczaić. Proszę przeczytać dokumentację TeaServlet w celu znalezienia dodatkowych informacji.

Ostatnie słowo
Jedną z najbardziej przydatnych własności Tea trudno opisać w książce — jest to graficzne środowisko
programistyczne o nazwie Kettle (imbryk), przedstawione na rysunku 14.5. Oprócz tradycyjnych własności IDE,
Kettle dostarcza dokonywanej w czasie rzeczywistym oceny procesu tworzenia — podkreślając na bieżąco
wszystkie błędy składni na zielono, błędy semantyczne na czerwono, a błędy kompilacji na niebiesko. Podobna
funkcjonalność w narzędziach wspierających inne mechanizmy tworzenia zawartości byłaby wspaniała, ale
praktycznie niemożliwa do osiągnięcia. Kettle oferuje również przywoływaną listę właściwości, która pozwala
twórcy szablonu na natychmiastowe sprawdzenie, jakie własności są dostępne dla danego obiektu. Kettle jest
bezpłatne, ale działa jedynie pod Windows i nie jest to Open Source. Dodatkowo istnieją plany utworzenia
reimplementacji Open Source opartej na Java Swing.

Rysunek 14.5.
Herbatę najlepiej robi się w imbryku
Podczas pracy z Tea i TeaServlet można dostrzec brak obsługi specjalnych rodzajów własności, gdyż ich twórcy
(WDIG) ich nie potrzebowali. Ponieważ od niedawna jest to Open Source, inni użytkownicy będą dodawać
potrzebne im własności i własne pomysły, a produkt będzie rósł. Chociaż język Tea dojrzał, to TeaServlet jest
ciągle narzędziem nowym, posiadającym duży potencjał.
Rozdział 15. WebMacro

Szkielet programistyczny WebMacro, dostępny pod adresem http://www.webmacro.org, pochodzi z tej samej
rodziny, co Tea, ale charakteryzuje się podejściem opartym w większym stopniu na serwletach. WebMacro
zostało utworzone przez Justina Wellsa z firmy Semiotek jako część kontraktu na stworzenie witryny firmy.
Następnie udostępnione zostało jako Open Source w celu poprawienia go i podniesienia pozycji firmy. Obecnie
istnieją plany włączenia WebMacro do Apache Jakarta Project dostępnym pod adresem
http://jakarta.apache.org w celu uzyskania szerszego wsparcia programistycznego. Aktualnie w projekcie
Jakarta tworzony jest klon WebMacro o nazwie Velocity.
WebMacro często określa się jako mechanizm szablonów, co jest ogólną nazwą systemów wykonujących zadania
zamiany tekstu wewnątrz szablonów stron WWW. Dostępne są także inne mechanizmy szablonów Open Source,
najbardziej znany z nich to FreeMarker (http://freemarker.sourceforge.net). Jednak w niniejszej książce opisane
zostanie WebMacro, ponieważ posiada ono największą popularność, jest wykorzystywane w dużych
komercyjnych witrynach takich jak AltaVista, zostało zintegrowane z dużymi szkieletami Open Source takimi jak
Turbine (http://java.apache.org/turbine) i Melati (http://www.melati.org) oraz zostało wykorzystane jako
podstawa znanych projektów Open Source takich jak JetSpeed (http://java.apache.org/jetspeed). Podobnie jak
Tea, WebMacro jest rozprowadzane według licencji typu Apache, tak więc może zostać bez problemu
wykorzystane w połączeniu z aplikacjami komercyjnymi.
Niniejszy rozdział opisuje WebMacro 0.94, wersję próbną z października 2000. Na pewno dużo szczegółów
zmieni się. Jeżeli przykładowy kod tu umieszczony nie będzie działał z ostateczną wersją, należy przeczytać
dokumentacje dołączoną do WebMacro aby określić, co się zmieniło. WebMacro wymaga Servlet API w wersji
2.0 lub późniejszej i JDK 1.1.7 lub późniejszego.

Szkielet WebMacro
Szkielet WebMacro działa w następujący sposób: serwlet odbiera żądanie od klienta, serwlet wykonuje logikę
konieczną do obsługi żądania (taką jak wykorzystanie klas wspierających lub elementów takich, jak EJB), po
czym tworzy obiekt kontekstowy wypełniony „obiektami odpowiedzi” zawierającymi wyniki logiki, które
powinny zostać wyświetlone klientowi. Następnie serwlet wybiera plik szablonu (zazwyczaj w oparciu o wyniki
logiki) po czym przepycha obiekty odpowiedzi przez plik szablonu tworząc zawartość wyświetlaną klientowi.
Programista Javy tworzy serwlet i logikę w czystej Javie, a inżynier szablonów tworzy plik szablonu
wyświetlający dane. Szablon składa się z zawartości HTML i XML wypełnionej prostą logiką do podstawiania i
niewielkimi możliwościami skryptowymi. Składnia jest zgodna z edytorami HTML i XML, omija nawiasy
kątowe, aby zapewnić, że analizatory HTML i XML nie będą popełniać omyłek. Programista przekazuje
inżynierowi szablonów listę zastosowanych zmiennych, a także listę właściwości i metod tych zmiennych.
Model ten posiada pewne podobieństwa z modelem TeaServlet, z tą różnicą, że WebMacro domyślnie
wykorzystuje model przepychania w większym stopniu oparty na serwletach, nie obsługuje zagnieżdżania
szablonów i wykorzystuje bardziej skomplikowaną składnię skryptową. Zdaniem autorów, WebMacro posiada
przewagę nad Tea przy tworzeniu wysoko funkcjonalnych stron WWW, które posiadają pewien punkt
oznaczający zakończenie pracy nad nimi, Tea natomiast przoduje w tworzeniu stron, w których stale dodawana
jest nowa zawartość szablonów, konieczne jest zagnieżdżanie szablonów i wymagane jest mniejsze
zaangażowanie programistyczne.
Powitanie przy pomocy WebMacro
Przykład 15.1 przedstawia prosty serwlet wykorzystujący system szablonów WebMacro. Przykład 15.2
przedstawia plik szablonu witaj.wm. Wspólnie wyświetlają one aktualna datę i godzinę W następnym
podrozdziale opisane zostaną miejsca, w których powinny one zostać umieszczone.
Przykład 15.1.
Prosty serwlet prowadzący szablon Witaj
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.webmacro.*;
import org.webmacro.servlet.*;

public class WMWitaj extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
FastWriter wyj = new FastWriter(odp.getOutputStream(),
odp.getCharacterEncoding());

try {
WebMacro wm = new WM(); // opcjonalnie WM
("/ścieżka/do/pliku/konfiguracyjnego")
Context k = wm.getWebContext(zad, odp);
k.put("data", new Date());
Template scab = wm.getTemplate("witaj.wm");
szab.write(wyj, k);
wyj.flush();
}
catch (WebMacroException w) {
throw new ServletException(w);
}
}
}
Przykład 15.2.
Prosty szablon Witaj
## witaj.wm

#set $Response.ContentType = "text/html"

<HTML><HEAD><TITLE>Test WebMacro</TITLE></HEAD><BODY>

Witaj! <P>

Obecny czas to $data. <BR>


(Dla Was, kujony to $date.Time milisekund.) <P>

</BODY></HTML>
Na początku opisany zostanie serwlet. Jest to normalny serwlet, będący rozszerzeniem HttpServlet i
wykorzystujący metodę doGet(). Na pierwszy rzut oka można jednak zauważyć, że jego logika jest inna.
Pobiera on FastWriter zamiast normalnego PrintWriter. Pozwala to na specjalną optymalizację
pozwalającą na uniknięcie kosztu konwersji Unicode dla zawartości statycznej. Działa on jak zwykłe urządzenie
zapisujące z jednym ulepszeniem takim, że WebMacro może przepchnąć wcześniej zakodowane bajty i wywołać
setAsciiHack(true) w celu przyśpieszenia wyświetlania danych Latin-1 lub US-ASCII. Należy jedynie
pamiętać, by (przynajmniej obecnie) wywołać metodę flush() urządzenia zapisującego w celu wysłania
zbuforowanej zawartości urządzenia do klienta.
Kod wewnątrz bloku try tworzy nowy obiekt WebMacro, który działa jako jego podstawowy zaczep do
systemu WebMacro. WebMacro to interfejs, tak więc serwlet właściwie konstruuje egzemplarz konkretnej klasy
WM, która implementuje interfejs WebMacro. Następnie serwlet wywołuje wm.getWebContext(zad,
odp) w celu odczytania kontekstu WebContext, w którym umieszczone zostaną obiekty odpowiedzi, które
zostaną przekazane szablonowi. Kontekst działa podobnie jak tablica asocjacyjna Hashtable. Posiada on
metodę put(Object, Object), którą serwlet wykorzystuje do umieszczenia obiektu Date w kontekście,
pod nazwą data. Obiekt ten zostanie później udostępniony szablonowi działającemu w obrębie tego kontekstu.
Użytkownicy zaawansowani mogą zainteresować się faktem, że WebContext jest rozszerzeniem Context, a
serwlet mógłby wywoływać kontekst nieświadomy sieci WWW — Context przy pomocy getContext().
Własność ta jest przydatna w niektórych sytuacjach, takich jak zastosowania offline lub niezwiązane z
serwletami.
W celu pobrania szablonu, serwlet wywołuje wm.getTemplate("witaj.wm"). Metoda ta pobiera nazwę
szablonu do odczytania, łącznie z rozszerzeniem pliku. Szablon może posiadać dowolne rozszerzenie, ale
większość użytkowników wykorzystuje standardową konwencję .wm. Posiadając szablon, serwlet może wywołać
jego metodę write() i przekazać jako jej parametry FastWriter do zapisu i WebContext wypełniony
obiektami odpowiedzi. Wyświetlane dane powinny również zostać ujęte jako łańcuch, przy pomocy metody
Template String evaluate(Context kontekst).
Jeżeli dowolna metoda spowoduje wyjątek WebMacroException, serwlet został przygotowany na wywołanie
go wewnątrz ServletException. Prawie każda metoda WebMacro może wywołać
WebMacroException lub pewne jego podklasy. Konstruktor WM() może wywołać InitException jeżeli,
na przykład, pożądany plik konfiguracyjny nie może zostać odnaleziony. Metoda getTemplate() może
wywołać NotFoundException, jeżeli niemożliwe jest odnalezienie szablonu. Natomiast metoda write()
może wywołać ContextException, jeżeli pożądane dane szablonu nie znajdowały się w kontekście, a także
IOException, jeżeli wystąpił problem z zapisem do klienta. Wszystkie zdefiniowane przez WebMacro
wyjątki są rozszerzeniami WebMacroException, który nie jest wyjątkiem czasu wykonania, i często stosuje
się obsługę tego ogólnego wyjątku na końcu metody doGet().
Stosując inne podejście, można utworzyć serwlet obsługujący WebMacro będący rozszerzeniem superklasy
org.webmacro.servlet.WMServlet, jak przedstawiono w przykładzie 15.3.
Przykład 15.3.
Inne podejście do tworzenia serwletu WebMacro
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.webmacro.*;
import org.webmacro.servlet.*;

public class WMSerwletWitaj extends WMServlet {

public Template handle(WebContext kontekst) throws HandlerException {


try {
kontekst.put("data", new Date());
return getTemplate("witaj.wm");
}
catch (NotFoundException w) {
throw new HandlerException(w.getMessage());
}
}
}
W powyższej metodzie, serwlet jest rozszerzeniem WMServlet i implementuje pojedynczą metodę handle
(). Superklasa tworzy automatycznie obiekt WebMacro i przekazuje mu jako parametr WebContext. Servlet
musi wykonać jedynie swoją logikę, wypełnić WebContext odpowiednimi obiektami odpowiedzi oraz zwrócić
szablon wykorzystywany do utworzenia strony (lub null, jeżeli serwlet wewnętrznie obsługuje tworzenie
strony). Metody start() i stop() również mogą zostać wykorzystane przez serwlet, zamiast zwykłych
metod init() i destroy(). Jeżeli potrzebne będą obiekty HttpServletRequest i
HttpServletResponse, mogą one zostać odczytane z WebContext przy pomocy metod getRequest
() i getResponse().
Wykorzystanie superklasy WMServlet może okazać się wygodniejsze niż samodzielna obsługa obiektów
WebMacro. Jego ceną jest utrata mocy. Konieczne jest wykorzystanie samodzielnego podejścia do umieszczania
na stronie dwóch niezależnych szablonów, przepuszczenia wyświetlanych danych przez filtr taki jak
GZIPOutputStream, wykonania tworzenia offline, rozróżnienia między żądaniami GET i POST oraz
określenia, czy serwlet musi być rozszerzeniem innej niestandardowej superklasy.
Teraz opisany zostanie szablon. Wygląda on jak zwykła strona HTML poza niewielkimi fragmentami. W
głównej części strony można dostrzec obiekt Date, dodany wcześniej do kontekstu pod nazwą data, dołączony
do strony przy pomocy składni $data. W następnej linii można zauważyć, że właściwość time obiektu Date
(która przechowuje czas w formie licznika milisekund) została dołączona przy pomocy składni $data.Time.
Jest to prosta składnia podstawienia wykorzystywana przez WebMacro — $nazwazmiennej wyświetla wartość
zmiennej (po konwersji do String, jeżeli jest to konieczne), a $nazwazmiennej.Wlasciwosc wyświetla
wartość właściwości. Podwłaściwości właściwości są również dostępne przy pomocy składni
$nazwazmiennej.Wlasciwosc.Podwlasciwosc, a tak naprawdę można uzyskać dostęp do większej
ilości danych niż podstawowe właściwości, poprzez zaawansowane działanie refleksji WebMacro, co zostanie
przedstawione w dalszej części. W tym momencie należy jedynie pamiętać, że podczas odczytywania
podstawowej właściwości jej nazwa musi zaczynać się od wielkiej litery.
Przyglądając się szablonowi dokładniej, można dostrzec, że na początku znajduje się komentarz zawierający
nazwę pliku. Komentarze w WebMacro rozpoczynają się od ## i mają długość jednej linii. Zaraz pod
komentarzem znajduje się polecenie #set, które w WebMacro nazywane jest instrukcją. Nadaje ona
właściwości ContentType obiektu Response wartość "text/html", co jest równoznaczne z wywołaniem
response.setContentType("text/html"). Zmienna $Response reprezentuje odpowiedź serwletu i
podobnie jak $Request jest dostępna we wszystkich obiektach WebContext. WebMacro posiada kilka
instrukcji służących do manipulacji zmiennymi, dołączania plików i tworzenia pętli. Zostaną one opisane
szczegółowo w dalszej części niniejszego rozdziału.

Instalacja WebMacro
W celu uruchomienia serwletu WebMacro i szablonu konieczne jest wykonanie niewielkiej pracy związanej z
instalacją. Po pierwsze należy pobrać dystrybucję z witryny http://www.webmacro.org i rozpakować ją.
Następnie należy umieścić webmacro.jar i collections.jar w ścieżce klas serwera25. Następnie należy odnaleźć
plik WebMacro.properties znajdujący się wewnątrz dystrybucji i skopiować go do katalogu znajdującego się w
ścieżce klas serwera. Proszę zauważyć, że ponieważ klasy w webmacro.jar wykorzystują swój własny
mechanizm ładowania klas w celu odnalezienia pliku, plik ten musi znajdować się w ścieżce klas mechanizmu
ładującego webmacro.jar. W celu zapewnienia tej własności, należy umieścić oba te pliki w systemowej ścieżce
klas, gdzie mogą zostać odnalezione przez podstawowy mechanizm ładowania klas, lub umieścić oba w katalogu
WEB-INF, gdzie zostaną odnalezione przez mechanizm aplikacji WWW. (webmacro.jar należy umieścić w
katalogu WEB-INF/lib, a WebMacro.properties w katalogu WEB-INF/classes.)
Domyślny plik WebMacro.properties zawarty w dystrybucji zawiera długą listę opcji konfiguracyjnych, których
pierwsza część jest przedstawiona w przykładzie 15.4. Większość aspektów tej klasy jest dobrze
udokumentowana w samym pliku. Wszystko, co należy teraz skonfigurować, to parametr TemplatePath,
określający katalog lub katalogi, w których przechowywane są szablony.
Przykład 15.4.
Standardowy plik WebMacro.properties
# UWAGA DLA UŻYTKOWNIKÓW NT
#
# Proszę pamiętać, że znak \ to znak ucieczkowy w plikach właściwości Javy
# Należy je zdublować (\\) lub wykorzystać w pliku znak Uniksowy (/)
# Oba powinny działać. Również podczas określania TemplatePath, proszę pamiętać
# o stosowaniu znaku oddzielającego NT (;), a nie Uniksa (:)

###########################################################
#
# KONFIGURACJA PODSTAWOWA:
#

# Należy co najmniej określić TemplatePath! Jest to lista katalogów, która będzie


# przeglądana w poszukiwaniu szablonów, jeżeli podana zostanie ścieżka względna
# Jest to lista oddzielana przez : (UNIX) lub ; (NT).

TemplatePath = /tomcat/webapps/webmacro/WEB-INF/szablony;/local/webmacro/szablony

# WebMacro kompiluje i przechowuje szablony w celu osiągnięcia maksymalnej


wydajności.
# Podczas programowania poleca się wyłączenie tego przez ustawienie wartości na 0
# tak, że zmiany w szablonach natychmiastowo odbijają się w witrynie.
# W systemach produkcyjnych jest to ilość milisekund wolnego czasu, przez który
szablon
# szablon umieszczony w pamięci podręcznej będzie przechowywany np. 600000 to 10
minut.

TemplateExpireTime = 0

25
Archiwum collections.jar zawiera klasy Java Collections (wprowadzone w JDK 1.2) utworzone do stosowania z
wcześniejszym JDK 1.1. Ten plik JAR nie jest wymagany, jeżeli dysponuje się kompilacją WebMacro przeznaczoną
konkretnie dla JDK 1.2; jednak w trakcie tworzenie niniejszej książki domyślna kompilacja webmacro.jar była
przystosowana dla JDK 1.1.7 i w związku z tym ten plik jest wymagany nawet podczas korzystania z JDK 1.2 lub
późniejszego.
# TemplateExpireTime == 600000

# LogLevel może wynosić: ALL, DEBUG, EXCEPTION, ERROR, WARNING, INFO, or NONE
# w porządku od największej do najmniejszej ilości wyświetlanych informacji.

LogLevel = EXCEPTION

# LogTraceExceptions powoduje umieszczanie stosów wyjątków w pliku dziennika,


# co powoduje zwiększenie długości wyjątków i ich wskazywanie na konkretną linię
lub
# metodę, która powoduje błąd.

LogTraceExceptions = TRUE

# Usunięcie komentarza powoduje zapisywanie w pliku dziennika, nie w stderr.


# Jeżeli standardowy błąd już zapisuje w przydatnym dzienniku, nie trzeba tego
robić,
# ale duża część mechanizmów serwletów po prostu połyka standardowy błąd.

# LogFile = /usr/local/webmacro/wm.log

# Ustawienie szablonu (związanego z TemplateDirectory) wykorzystywanego do błędów.


# Można dokonać edycji aby dostosować sposób wyświetlania niepowodzeń skryptu

ErrorTemplate = blad.wm
Zaawansowany użytkownik może utworzyć wiele wersji powyższego pliku i przełączać się pomiędzy nimi w celu
dokonania szybkiej i kompletnej zmiany wyglądu witryny (zmiana TemplatePath), lub po prostu by
przesunąć się z trybu programowania do produkcji (zmiana właściwości takich jak TemplateExpireTime).
Ta zamiana może zostać wykonana również na poziomie serwlet-do-serwletu ponieważ konstruktor WM()
pobiera opcjonalny argument typu String, który określa położenie pliku konfiguracyjnego, który powinien
zostać odczytany. Jeżeli serwlet potrzebuje bezpośredniego dostępu do właściwości w tym pliku, są one dostępne
przy pomocy metody getConfig(String klucz) obecną zarówno w interfejsie WebMacro i klasie
WMServlet. Pozwala to plikowi konfiguracyjnemu na przechowywanie ekwiwalentów parametrów inicjacji
kontekstu, poza parametrami przywiązanymi do konkretnej konfiguracji — na przykład wskazującymi na testową
bazę danych w trakcie programowania, a na produkcyjną bazę danych po udostępnieniu.
Kiedy wszystkie pliki zostaną właściwie zainstalowane, wywoływanie serwletów obsługujących WebMacro jest
wykonywane tak, jak wywoływanie wszystkich innych serwletów. Należy wywołać serwlet jako
http://localhost:8080/servlet/WMWitaj lub, w przypadku serwletów umieszczonych w kontekście /webmacro, jak
przedstawiono w pliku WebMacro.properties, URL wynosi http://localhost:8080/webmacro/servlet/WMWitaj.
Proszę spojrzeć na rysunek 15.1.
Rysunek
15.1.
Test
WebMacr
o

Język szablonów WebMacro


Język szablonów WebMacro przypomina składnię Perla i preprocesora C, która jest znajoma większości
użytkowników. Zmienne rozpoczynają się znakiem dolara ($), a kończą pustym miejscem lub różnymi znakami
interpunkcyjnymi takim jak <, / i ". Pozwala to na tworzenie łatwych do odczytania podstawień, na przykład:
$Request.ContextPath/servlet/WMWitaj
<A HREF="$url">$url</A>
Kiedy jest to potrzebne, można wykorzystać nawiasy w celu wyraźnego oddzielenia nazwy zmiennej i
wyjaśnienia analizy. Nawiasy są usuwane i niewyświetlane. Na przykład:
#set $przedrostek = "wyb"
#set $przyrostek = "aczanie"
Błądzenie to rzecz ludzka, $(przedrostek)$przyrostek — boska.
Mechanizm WebMacro nie wykorzystuje klasy java.beans.Introspector w celu uzyskania dostępu do
właściwości obiektu; zamiast tego wykonuje bardziej wyczerpujące przeszukiwanie refleksyjne. Kiedy napisze
się $Request.ContextPath, WebMacro po pierwsze odnajduje obiekt określany jako $Request, po
czym próbuje odnaleźć właściwość przy pomocy następującego wzoru poszukiwań:
request.ContextPath
request.getContextPath()
request.get("ContextPath");
Jeżeli utworzy się dłuższe podstawienie, takie jak $Request.Header.Accept, WebMacro dodaje
następujący wzór poszukiwań na końcu listy:
Request.getHeader("Accept")
Przydzielenia zmiennych wykorzystują podobny wzór poszukiwań.
Przy pomocy WebMacro możliwe jest bezpośrednie wywoływanie metod na wszystkich obiektach w zakresie
(czego nie można było w jasny sposób wykonać w Tea). Na przykład, poniższe podstawienie odnajduje listę
produktów w zapasie, wyszukuje produkt zawias i zwraca jego numer części:
$Zapas.Produkty.znajdzProdukt("zawias").NumerCzesci
Własność ta może okazać się przydatna, kiedy metody dostępu nie są zgodne z wzorem nazywania, którego
spodziewa się WebMacro. Z drugiej strony, bezpośrednie wywoływanie metod oznacza, że w szablonie
WebMacro nie może istnieć całkowite zagnieżdżanie.

Narzędzia kontekstu WebMacro


Jedną z potężnych cech WebMacro jest możliwość udostępnienia przez WebContext niewielkiej liczby
zmiennych nazywanych narzędziami kontekstu, wszystkim szablonom, a narzędzia te są zawsze dołączane bez
konieczności umieszczenia ich w kontekście przez wywołujący serwlet. Lista narzędzi, które powinny zostać
dołączone jest określana w pliku WebMacro.properties jako WebContextTools. Domyślnie WebMacro dostarcza
następujących narzędzi:
$Request
Odwołanie do HttpServletRequest serwletu. Może zostać wykorzystane do uzyskania dostępu do
nagłówka Accept żądania przy pomocy $Request.Header.Accept lub równoważnie
$Request.getHeader("Accept").
$Response
Odwołanie do HttpServletResponse serwletu. Może zostać wykorzystane do ustawienia Content-
Type odpowiedzi przy pomocy #set $Response.ContentType = "text/html".
$Session
Odwołanie do HttpSession użytkownika. Może zostać wykorzystane do odczytania wartości sesji przy
pomocy $Session.Attribute.Count lub równoważnie $Session.getAttribute
("Count").
$Form
Narzędzie służące do uzyskania dostępu do danych formularza żądania. Może zostać wykorzystane do
pobrania parametru numerczesci przy pomocy $Form.numerczesci. Skrót dla
$Request.getParameter("numerczesci") lub $Request.Parameter.numerczesci.
$FormList
Narzędzie służące do uzyskania dostępu do danych formularza żądania, kiedy dane formularza posiadają
więcej niż jedną wartość. Stosowane w połączeniu z instrukcją #foreach.
$Cookie
Narzędzie służące do odczytywania i ustawiania cookies. Może zostać wykorzystane do ustawienia cookie
przy pomocy $Cookie.set("nazwa", "wartosc") lub #set $Cookie.nazwa =
"wartosc". Cookies są odczytywane przy pomocy $Cookie.get("nazwa").Wartosc lub
$Cookie.nazwa.Wartosc.
$CGI
Narżedzie służące do uzyskiwania dostępu do informacji żądania przy pomocy nazwa w stylu CGI. Może
zostać wykorzystane przez inżynierów szablonów znających CGI do uzyskania dostępu do zmiennych
takich, jak katalog macierzysty dokumentów — $CGI.DOCUMENT_ROOT.
Dodatkowe narzędzia kontekstu mogą zostać udostępnione poprzez utworzenie klasy wykorzystującej
org.webmacro.ContextTool i dodanie klasy do listy WebContextTools. Można napisać lub pobrać
narzędzia odpowiedzialne za działania matematyczne, internacjonalizację, dostęp do bazy danych lub nawet
tworzenie obiektów HTML przy pomocy ECS opisanego w następnym rozdziale. W wielu aspektach narzędzia
kontekstu mają takie znaczenie dla WebMacro, jak biblioteki znaczników dla JSP.
Przykład 15.5 przedstawia proste narzędzie kontekstu wykonujące arytmetykę na liczbach typu integer. To
narzędzie o nazwie MatTool często okazuje się przydatne, ponieważ język szablonów WebMacro został tak
uproszczony, że nie zawiera nawet podstawowych działań arytmetycznych26.
Przykład 15.5.
Narzędzie kontekstu służące do wykonywania arytmetyki na liczbach integer (skomplikowany kalkulator)
import org.webmacro.*;

public class MatTool implements ContextTool {


/**
* Dla każdego żądania utworzony zostanie nowy obiekt-narzędzie poprzez
wywołanie tej
* metody. ContextTool jest właściwie fabryką wykorzystywaną do tworzenia
obiektów
* wykorzystywanych w szablonach. Niektóre obiekty mogą po prostu zwracać
siebie
* przez tę metodę; inne mogą tworzyć nowe egzemplarze obiektów w celu
utrzymania
* stanu jeden-na żądanie.
*/
public Object init(Context k) {
return this;
}

public static int dodaj(int x, int y) {


return x + y;
}

public static int odejmij(int x, int y) {


return x - y;
}

public static int pomnoz(int x, int y) {


return x * y;
}

public static int podziel(int x, int y) {


return x / y;
}

public static int modul(int x, int y) {


return x % y;
}

public static boolean mniejszeNiz(int x, int y) {


return (x < y);
}

public static boolean wiekszeNiz(int x, int y) {


return (x > y);
}
}
Kiedy narzędzie to zostanie dodane do listy WebContextTools (proszę nie zapomnieć o tym kroku),
wszystkie szablony mogą wykonywać podstawowe działania matematyczne. Na przykład, wyświetlenie roku z
obiektu Date wymaga dodania 1900 do właściwości Year:
Obecny rok to $Mat.dodaj($data.Year, 1900).

26
Jednak z powodu dużych nacisków operacje arytmetyczne są dodawane i mogą być dostępne w momencie wydania książki
Instrukcje WebMacro
Instrukcje to wyrażenia WebMacro wykonujące pewne operacje, warunkowo dołączające tekst lub powtarzające
blok wewnątrz szablonu. Lista dostępnych instrukcji jest przechowywana w pliku WebMacro.properties jako
właściwość Directives. Wszystkie instrukcje rozpoczynają się znakiem #. Niektóre instrukcje działają na
bloku, i bloki takie mogą zostać zaznaczone przy pomocy zakręconych nawiasów, { i }, lub słów kluczowych
#begin i #end. Słowa kluczowe nie są tak proste w napisaniu jak nawiasy lecz powodują mniejszą ilość
problemów z dołączonym JavaScriptem.
Istnieje siedem popularnych instrukcji. Podobnie jak w przypadku narzędzi kontekstu, można utworzyć swoje
własne instrukcje dodawane do domyślnych, chociaż nie jest to tak popularne. Poniżej przedstawiona jest
domyślna siódemka.

#if
#if (warunek) { ... } #else { ... }
Instrukcja #if, samodzielna lub zastosowana w połączeniu z instrukcją #else, może zostać wykorzystana do
warunkowego dołączania tekstu. Tekst jest dołączany, jeżeli warunek jest prawdziwy, a nie dołączany, jeżeli
fałszywy. Warunek jest uważany za prawdziwy, jeżeli posiada niezerową wartość inną niż boolowskie false.
Warunek #if może wykorzystywać znajome operatory boolowskie &&, || i !, a także nawiasy w celu
określenia porządku. Obiekty mogą być porównywane jako część warunku przy pomocy operatorów == i !=,
które właściwie wywołują metodę equals(),wykonujacą porównanie. Nie istnieje operator dla porównań
większe-niż i mniejsze-niż, ponieważ obiekty nie są zobowiązane do bycia porównywalnym w ten sposób.
MatTool zawiera także porównania do użytku z liczbami typu integer.
#if ($Klient.winienPieniadze() && $Klient.Nazwa != "Jason") {
Zapłać koniecznie!
}
#else {
Witamy!
}

#set
#set $wlasciwosc = wartosc
Instrukcja #set przypisuje wartość danej zmiennej lub właściwości zmiennej. Właściwość musi zostać
pomyślnie odnaleziona przez logikę refleksyjną WebMacro, oraz musi posiadać własność publicznego
ustawiania w pewien sposób. Jeżeli takiej zmiennej nie ma w zakresie, tworzona jest nowa zmienna i przypisana
jej zostaje podana wartość. Zmiennym w ukryty sposób przypisywany jest typ String lub Integer. Typ
String to typ domyślny, chyba że wartość może zostać zanalizowana jako Integer i nie jest otoczona
podwójnymi nawiasami, Dla ułatwienia, wartości Integer mogą zostać przekonwertowane do wartości
String, kiedy jest to potrzebne. Ustawiane mogą być również tablice, kiedy jest to potrzebne. Należy użyć
lewego ukośnika w celu wyłączenia znaku dolara:
#set $liczba1 = 4
#set $liczba2 = 7
#set $cena = "90"
#set $faktura = "Jesteś winny \$$cena"
#set $cytat = "$liczba1 mil i $liczba2 lat temu..."
#set $wszystko = [ $liczba1, $liczba2, $cytat ] ## składnia tablicy

#foreach
#foreach $rzecz in $lista { ... }
Instrukcja #foreach iteruje według listy, dołączając swój blok kodu raz dla każdego elementu na liście. Przy
każdej pętli zmienna $rzecz pobiera wartość następnego egzemplarza a $lista. Lista może być tablicą
zadeklarowaną wewnątrz WebMacro lub obiektem Javy, który spełnia jeden z poniższych warunków
(przeszukiwane przez refleksję w tej kolejności):
1. Obiekt sam w sobie jest tablicą.
2. Obiekt sam w sobie to Iterator.
3. Obiekt sam w sobie to Enumeration.
4. Obiekt posiada metodę Iterator iterator().
5. Obiekt posiada metodę Enumeration elements().
Poniższy kod wyświetla preferowane przez klienta lokalizację (jeżeli takie istnieją):
#set $tablica = $Request.Locales
<UL>
#foreach $element in #tablica {
<LI>$element
}
</UL>

#parse
#parse plik
Instrukcja #parse dołącza zawartość docelowego pliku w miejscu instrukcji tak, jak by był on częścią
obecnego szablonu. Dostarcza to łatwego mechanizmu dołączania popularnego kodu szablonów do różnych
plików szablonów. Plik może zostać określony przy pomocy ścieżki bezpośredniej lub pośredniej umieszczonej
wewnątrz TemplatePath:
#set $tytul = "Tytuł strony"
#parse "naglowek.wm" ##zmienna $tytul jest widoczna w naglowek.wm

#include
#include url
Instrukcja #include dołącza zawartość docelowego URL-a w miejscu instrukcji, ale nie próbuje analizować
jego zawartości. Dostarcza to łatwego mechanizmu dołączania JavaScriptu i innych rzeczy, których składnia
może wchodzić w konflikt z WebMacro, Jeżeli URL nie posiada protokołu, przyjmuje się file::
#include "http://webmacro.org/CREDITS ## Dołączenie surowego tekstu

#param
#param $nazwa = wartosc
Instrukcja #param określa wartości wewnątrz szablonu, które mogą być przeglądane przez serwlet wywołujący
szablon. Dostarczają one autorowi szablonu metody przekazywania informacji wspierającemu programiście Javy.
Informacje te mogą zostać wykorzystane do określenia typu informacji, który ma zostać umieszczony w
kontekście:
#param $autor = "Maria Kowalska"
#param $wymagane = [ "uzyt", "dokument", "sesja" ]
Serwlet może pobrać wartość przez wywołanie Object getParam(String) na obiekcie Template.
Metoda ta zwraca String lub Integer, jeżeli istnieje jedna wartość i Object[] zawierający obiekty
String i/lub Integer, jeżeli wartość jest listą:
String autor = (String) szab.getParam("autor");
Object[] wymagane = (Object[]) szab.getParam("wymagane");

#use
#use 'analizator'
#begin
...
#end
Ostatnią z instrukcji jest #use, która pozwala blokowi tekstu szablonu (oznaczonemu przez znaczniki #begin i
#end) na bycie zanalizowanym przez alternatywny analizator. Własność ta pozwala na rozszerzenie lub nawet
całkowitą wymianę składni WebMacro. Generalnie nie jest ona jednak stosowana do tak dużych zadań. Jej dwa
najbardziej popularne zastosowania to dosłowne dołączenie bloku tekstu (przy pomocy analizatora text) lub
usunięcia bloku tekstu (przy pomocy analizatora null):
#use 'text'
#begin
Cały tekst tu umieszczony jest dołączany bez zmian
## Komentarze również ponieważ format komentarza to kwestia analizatora
#end
#use 'null'
#begin
Tekst w tym miejscu można uznać za „wykomentowany”
#end
Plik WebMacro.properties zawiera listę dostępnych domyślnych analizatorów we właściwości Parsers27.

27
Dołączany mechanizm analizatora może zostać zastąpiony przez docelowe instrukcje. Na przykład #use 'text' może
zostać zastąpiony przez #text, a #use 'null' przez #comment.
Szablony WebMacro
Aby zademonstrować narzędzia kontekstu i instrukcje dostępne szablonom WebMacro, przykład 15.6
przedstawia prosty szablon wyświetlający informacje z żądania.
Przykład 15.6.
Przeglądanie żądania przy pomocy WebMacro
## przegl.wm

#set $Response.ContentType = "text/html"

<HTML><HEAD><TITLE>Przegladanie!</TITLE></HEAD>
<BODY>

## Szablon przeglądający pozwalający na przyzwyczajenie się do WebMacro

<H1>Rozne informacje</H1>
##Lancuch zapytania: $Request.QueryString<BR>
##Uzytkownik zdalny: $Request.RemoteUser<BR>

## WebMacro jeszcze nie obsługuje właściwości isXXX() trzeba użyć wywołania metody
## Nie posiada też aktualnie elseif (jest dodawane)

##if ($Request.isRequestedSessionIdFromCookie()) {
## Sesja dzieki cookies!
##}
##else {
##if ($Request.isRequestedSessionIdFromURL()) {
## Sesja dzieki przepisywaniu URL!
## }
## #else {
## Brak sesji, wspolczucia.
## }
##}

<H1>Informacja o parametrach</H1>
#foreach $paramName in $Request.ParameterNames {
$paramName: $Request.getParameter($paramName) <BR>
}

<H1>Informacja o naglowku</H1>
#foreach $headerName in $Request.HeaderNames {
$headerName: $Request.getHeader($headerName) <BR>
}

<H1>Informacja o cookie</H1>
#foreach $cookie in $Request.Cookies {
$cookie.Name: $cookie.Value <BR>
}
</BODY></HTML>
Rysunek 15.2 przedstawia przykładową stronę wyświetlaną przez powyższy przykład.
Rysunek 15.2.
Przeglądanie z WebMacro

Powyższy szablon demonstruje instrukcję #set ustawiającą typ zawartości odpowiedzi. Wykorzystuje
instrukcje #if i #else w celu określenia, czy klient utworzył sesję przy pomocy cookie, czy przepisywania
URL-a, czy też nie jest częścią sesji. Wykorzystuje także instrukcję #foreach do wykonania pętli na
wartościach parametrów, nagłówka i cookies obecnych w żądaniu.
Warto zauważyć pewne sztuczki — po pierwsze, podstawowa metoda dostępu o nazwie według wzoru
isWlasciwosc() nie jest automatycznie odnajdywana przez WebMacro i musi zostać osobno wywołana
poprzez wywoływanie metod. Po drugie, obecnie nie istnieje instrukcja #elseif, tak więc konstrukcja
if/elseif/else musi być wykonywana poprzez zagnieżdżanie.

Gotowy do ponownego wykorzystania serwlet MacroPrzegl


Szablon z przykładu 15.6 może być uważany za „samodzielny”, ponieważ nie wykorzystuje on żadnych
zmiennych dostarczanych przez serwlet. Samodzielne szablony nie mogą być wywoływane bezpośrednio i
wymagają serwletu, który je wywoła, ale ponieważ szablon nie ma żadnych specjalnych wymagań, serwlet ten
może być ogólnym i możliwym do ponownego wykorzystania serwletem MacroPrzegl, jak przedstawiono w
przykładzie 15.7.
Przykład 15.7.
Ogólnie możliwy do ponownego wykorzystania serwlet WebMacro
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.webmacro.*;
import org.webmacro.servlet.*;
import org.webmacro.engine.*;
import org.webmacro.broker.*;

// Rozszerzanie com.oreilly.servlet.CacheHttpServlet może poprawić czas odpowiedzi

public class MacroPrzegl extends HttpServlet {


WebMacro wm; // główny zaczep WebMacro

public void init() throws ServletException {


try {
wm = new WM();
}
catch (InitException w) {
throw new ServletException(w);
}
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
FastWriter wyj = new FastWriter(odp.getOutputStream(),
odp.getCharacterEncoding());

// Nazwa szablonu jest pobierana jako dodatkowy element ścieżki


// /servlet/MacroPrzegl/szab.wm
// Lub jako ścieżka serwletu poprzez zasadę *.wm
// /szab.wm
String szablon = zad.getPathInfo();
if (szablon == null) {
szablon = zad.getServletPath();
szablon = szablon.substring(1); // odcięcie początkowego "/"
}

// Jeżeli ciągle brak szablonu, problem


if (szablon == null) {
throw new ServletException(
"Nie określono żadnego szablonu jako dodatkowej ścieżki lub serwletu");
}

try {
Template szab = wm.getTemplate(szablon);
WebContext kontekst = wm.getWebContext(zad, odp);
szab.write(wyj, kontekst);
}
catch (WebMacroException w) {
throw new ServletException(w);
}
finally {
wyj.flush();
}
}

public void destroy() {


super.destroy();
if (wm != null) wm.destroy();
}
}
Powyższy serwlet wywołuje dowolny szablon, którego nazwa zostanie przekazana jako dodatkowa informacja o
ścieżce, lub jeżeli jej nie ma, po prostu szablon, na który wskazuje ścieżka serwletu (poprzez dopasowanie
rozszerzeń plików). Ułatwieniem może okazać się zarejestrowanie powyższego serwletu tak, aby obsługiwał
wszystkie żądania *.wm przy pomocy fragmentu pliku web.xml przedstawionego w przykładzie 15.8. Pozwala to
plikom szablonów WebMacro na bycie wywoływanym bezpośrednio przy pomocy URL-i takich jak /
przegl.wm lub webmacro/przegl.wm, podczas gdy w rzeczywistości serwlet MacroPrzegl wykonuje
całą czarną robotę.
Przykład 15.8.
Rejestrowanie MacroPrzegl w celu obsługi *.wm
<servlet>
<servlet-name>
mp
</servlet-name>
<servlet-class>
MacroPrzegl
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>
mp
</servlet-name>
<url-pattern>
*.wm
</url-pattern>
</servlet-mapping>
Przydatnym może kazać się rozszerzenie serwletu MacroPrzegl tak, aby wykonywał pewną standardową
logikę na każdym żądaniu i przypuszczalnie utworzył zbiór popularnych obiektów dostępnych w kontekście
wszystkim szablonów systemowym. Serwlet podobny do powyższego, połączony z omówionymi wcześniej
narzędziami kontekstu, pozwala szablonom WebMacro na działanie z modelem przepychania podobnym do Tea
(i to właściwie jest sposób, w jaki AltaVista wykorzystuje WebMacro).

Przetwarzanie szablonów
WebMacro wykonuje w ukryciu dużą ilość analizy szablonów, o którą programiści i projektanci nie muszą się
martwić, ale warto poznać mechanizmy tych działań w celu maksymalnego ich wykorzystania. WebMacro
analizuje szablony podczas ich pierwszego wykorzystania, a następnie umieszcza reprezentację odpowiedniego
szablonu w pamięci, w celu ich szybkiego wykonywania. WebMacro automatycznie przeładowuje i ponownie
analizuje zawartość szablonu po zmianie pliku szablonu, ale dla zachowania wydajności sprawdza znaczniki
czasu jedynie co określony czas, wymieniony jako TemplateExpireTime w WebMacro.properties.
Domyślnie czas przechowywania wynosi 0 milisekund tak, aby znaczniki czasy były sprawdzane na każde
żądanie. Jest to ułatwienie dla programowania, ale należy się upewnić, że dla wykorzystania produkcyjnego
znacznik czasu zostanie zwiększony. Dla użytkowników zaawansowanych obiekty szablonów posiadają metodę
parse(), która wymusza pobranie i analizę. Metoda ta może zostać wykorzystana do analizy wszystkich
szablonów podczas uruchomienia, lub wymuszenia wcześniejszej analizy, jeżeli szablon został zmieniony.
Kiedy w szablonach występują błędy, mogą wydarzyć się różne rzeczy. Jeżeli zmienna, do której następuje
odwołanie, lub właściwość posiada wartość null lub nie istnieje, WebMacro traktuje tę niepomyślną zamianę
jako błąd niekrytyczny. WebMacro generuje stronę w najlepszy możliwy sposób, ale zapisuje wiadomość WARN
w dzienniku, a na wygenerowanej stronie umieszcza komentarz HTML/XML w miejscu błędu (podejście to
posiada pewne wady, jeżeli generowana strona nie jest typu HTML ani XML):
<!-- warning: attempt to write out undefined variable Request.ContentType:
java.lang.NullPointerException -->.
Jeżeli szablon zawiera błąd składni, WebMacro traktuje go jako błąd krytyczny i zapisuje wiadomość ERROR w
dzienniku zdarzeń, a na wygenerowanej stronie wyświetla opis błędu28. W przypadku niektórych typów błędów,
takich jak nieodnalezienie szablonu przez serwlet, WebMacro wyświetla domyślny szablon błędu,
konfigurowany przy pomocy właściwości ErrorTemplate w WebMacro.properties.
Oczywiście większość błędów w witrynie WebMacro powinna zostać usunięta na etapie kodu serwletu —
niewłaściwe parametry, uszkodzone bazy danych, brakujące pliki i wszystkie inne błędy, które powinny zostać
poprawione przed przekazaniem kontroli szablonowi. WebMacro posiada duże możliwości w tym względzie.
Serwlet może wykorzystać zbiór standardowych szablonów WebMacro do wyświetlenia informacji o tych
błędach, wybierając, do którego szablonu przekazać kontrolę i co zawrzeć w kontekście, zależnie od błędu. Na
przykład, jeżeli wystąpił błąd bazy danych , kontrola powinna zostać przekazana do pliku sqlException.wm
razem ze stosem ścieżek wyjątków do wyświetlenia.

Aplikacja „Narzędzia”
Aby dokonać podsumowania opisu WebMacro, opisany zostanie sposób przetworzenia aplikacji „Narzędzia” z
poprzedniego rozdziału przy pomocy WebMacro zamiast Tea. Jest to najprostsze do zrozumienia, jeżeli na
początku spojrzy się na szablon, przedstawiony w przykładzie 15.9.
Przykład 15.9.
Szablon WebMacro aplikacji „Narzędzia”
## widoknarz.wm

#set $Response.ContentType = "text/html"

#set tytul = "Lista narzędzi"


#set tytul2 = "Lista narzędzi służących do tworzenia zawartości"
#set opis = Bez narzędzi, ludzie nie są niczym więcej niż zwierzętami. I to \
dość słabymi. Poniżej przedstawiono listę opartych na serwletach \
narzędzi do tworzenia zawartości, które można wykorzystać w celu \
wzmocnienia się."

#parse "naglowek.wm"

28
W WebMacro 0.94 (według tej wersji sprawdzany był kod w niniejszym rozdziale) występuje błąd, w którym błędy składni
szablonów mogą spowodować wyświetlenie przez WebMacro pustej strony zamiast strony zawierającej opis błędu Bez
wątpienia błąd ten zostanie naprawiony w następnych wersjach.
## Zdefiniowanie wartości, które powinny zostać odczytane przez serwlet
#param $domyslnyStan = "ZYJE"

#foreach $narzedzie in $narzedzia {

<HR SIZE=2 ALIGN=LEFT>

<H3>
$narzedzia.Nazwa

#if ($narzedzie.czyNowy(45)) {
<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>
}
#else {
#if ($narzedzie.czyUaktualniony(45)) {
<FONT COLOR=#ff0000><B> (Uaktualnienie!) </B></FONT>
}
}
</H3>
<A HREF="$narzedzie.DomURL">$narzedzie.DomURL</A><BR>

$narzedzie.Komentarz

#parse "stopka.wm"
Początek powyższego szablonu przypisuje wartości tytułom i opisom stron (przy pomocy znaków \ jako znaków
kontynuacji opisu). Zmienne te są wykorzystywane wewnątrz zanalizowanego szablonu naglowek.wm w celu
utworzenia struktury strony otaczającej podstawową zawartość. Zmienne są również widoczne wewnątrz
szablonu stopka.wm przeanalizowanego na końcu szablonu, chociaż stopka nie wykorzystuje ich. Szablony
nagłówka i stopki są przedstawione odpowiednio w przykładach 15.10 i 15.11.
Szablon widoknarz.wm wykorzystuje instrukcję #param w celu zdefiniowania ZYJE jako stałej wartości
parametru szablonu domyslnyStan, pozwalając serwletowi wywołującemu ten szablon na poznanie
domyślnego stanu narzędzi, które powinna wyświetlać niniejsza strona.
Następnie szablon wykorzystuje instrukcję #foreach w celu dokonania działań na całej liście narzedzie
przekazanej przez serwlet. Zmienna narzędzi może być tablicą, Iterator, Enumeration lub obiektem takim
jak Vector() lub List() posiadającym metodę iterator() lub elements() — nie jest to ważne dla
szablonu. Dla każdego egzemplarza narzędzi, szablon wyświetla jego nazwę, informację, czy jest nowy lub
uaktualniony, jego URL i ostatecznie komentarz na temat narzędzia. Szablon wywołuje metody czyNowy
(int) i czyUaktualniony(int) w celu określenia, czy zawartość powinna zostać uznana za nową lub
uaktualnioną, Szablon powinien mieć dostęp do $narzedzie.CzasUtworzeniaDni i
$narzedzie.CzasModyfikacjiDni, jednak wymagałoby to obsługi narzędzia kontekstu MatTool z
przykładu 15.5 w celu wykonania porównania mniejsze-niż.
Przykład 15.10.
Plik naglowek.wm
## naglowek.wm

<HTML><HEAD><TITLE>$tytul</TITLE></HEAD>

<BODY BGCOLOR="#FFFFFF" BACKGROUND="/obrazki/tlo.gif"


LINK="#003333" ALINK="#669999" VLINK="#333333">

<IMG SRC="/obrazki/banner.gif" WIDTH=600 HEIGHT=87 BORDER=0><BR>

<TABLE>
<TR>
<TD WIDTH=125 VALIGN=TOP>
<BR><BR><BR>
<FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#FF0000">
<A HREF="/indeks.html">Strona główna</A><BR>
<A HREF="/hosting.html">Hosting</A><BR>
<A HREF="/mechanizmy.html">Mechanizmy</A><BR>
</FONT>
</TD>

<TD WIDTH=475>

<TABLE CELLPADDING=5><TR><TD WIDTH=600 BGCOLOR="#006699" VALIGN=TOP>


<B><FONT FACE="Arial,Helvetica" SIZE="+2">
<% tytul %>
</FONT></B>
</TD></TR></TABLE>
<B><FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#003366">
<% tytul2 %>
</FONT></B><P>

<P>

<FONT FACE="Arial,Helvetica">

$opis
Przykład 15.11.
Plik stopka.wm
## stopka.wm

</FONT>

</TD>
</TR>

<TR>
<TD></TD>
<TD WIDTH=475 ALIGN=CENTER COLSPAN=3>
<HR>
<FONT FACE="Arial,Helvetica">
<A HREF="/indeks.html">Strona główna</A>&nbsp;&nbsp;
<A HREF="/hosting.html">Hosting</A>&nbsp;&nbsp;
<A HREF="/mechanizmy.html">mechanizmy</A>&nbsp;&nbsp;<P>
</FONT>

<TABLE WIDTH=100%>
<TR>
<TD WIDTH=260 ALIGN=LEFT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
<A HREF="/wlasnosc.html">Własność</A> &copy; 2000 Jason Hunter<BR>
Wszystkie prawa zastrzeżone.</TD>
<TD WIDTH=5></FONT></TD>
<TD WIDTH=230 ALIGN=RIGHT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
Kontakt: <A HREF="mailto:webmaster@servlets.com">webmaster@servlets.com</A>
</FONT></TD>
</TR>
</TABLE>

</TD>
</TR>
</TABLE>

</BODY>
</HTML>
Kod klasy Narzedzie pozostaje taki sam jak w przypadku aplikacji Tea, chociaż przy stosowaniu WebMacro
nie są konieczne podstawowe metody dostępu, a funkcje czyNowy(int) i czyUaktualniony(int) są
widoczne w szablonie. Kod klasy SerwletNarz jest przedstawiony w przykładzie 15.12.
Przykład 15.12.
Serwlet prowadzący aplikację „Narzędzia”
import org.webmacro.*;
import org.webmacro.servlet.*;
import org.webmacro.util.*;
import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;

public class SerwletNarz extends WMServlet {

private Log dziennik;


private Narzedzie[] narzedzia;

public void start() throws ServletException {


// Załadowanie danych narzędzi do init w celu zachowania prostoty
String plikNarz = getInitParameter("plikNarz"); // z web.xml
if (plikNarz == null) {
throw new ServletException(
"Plik danych narzędzi musi być określony w parametrze inicjacji
plikNarz");
}
dziennik = new Log(getServletName(), "Dziennik błędów przykładu Narzędzia");
dziennik.debug("Ładowanie narzędzi z " + plikNarz);
try {
narzedzia = Narzedzie.ladujNarzedzia(plikNarz);
if (narzedzia.length == 0) {
dziennik.warning("Nie znaleziono żadnych narzędzi w " + plikNarz);
}
else {
dziennik.info(narzedzia.length + " narzędzi znalezionych w " + plikNarz);
}
}
catch (Exception w) {
dziennik.error(w);
throw new ServletException(w);
}
}

// Tworzenie kontekstu dostarcza funkcji dostępnych z szablonów.


public Template handle(WebContext kontekst) throws HandlerException {
// Często przekazuje się żądanie odpowiedź i aplikację, nawet jeżeli nie są
// wykorzystane wszystkie obiekty, ponieważ mogą zostać wykorzystane później
try {
Template widok = getTemplate("widoknarz.wm");
String stan = kontekst.getRequest().getParameter("stan");
if (stan == null) {
stan = (String)widok.getParam("domyslnyStan");
}

if (stan == null) {
kontekst.put("narzedzia", pobierzNarzedzia());
}
else {
kontekst.put("narzedzia", pobierzNarzedzia(stan));
}
return view;
}
catch (WebMacroException w) {
dziennik.exception(w);
throw new HandlerException(w.getMessage());
}
catch (IOException w) {
dziennik.exception(w);
throw new HandlerException(w.getMessage());
}
}

public Narzedzie[] pobierzNarzedzia() {


return narzedzia;
}

public Narzedzie[] pobierzNarzedzie(String stan) {


List lista = new LinkedList();
for (int i = 0; i < narzedzia.length; i++) {
if (narzedzia[i].getStateFlag().equalsIgnoreCase(state)) {
lista.add(tools[i]);
}
}
return (Narzedzie[]) lista.toArray(new Narzedzie[0]);
}
}
Serwlet jest rozszerzeniem WMServlet i w związku z tym definiuje metodę start() zamiast init() oraz
metodę handle() zamiast doGet() i doPost(). Metoda start() odczytuje parametr inicjacji
plikNarz i ładuje dane narzędzi z pliku przy pomocy Narzedzie.ladujNarzedzia(plikNarz).
Metoda ta tworzy również egzemplarz Log i wykorzystuje go do zapisywania raportów na temat pracy, ostrzeżeń
i komunikatów o błędach.
Metoda handle() ładuje szablon widoknarz.wm, po czym odczytuje parametr żądania stan w celu określenia
stanu narzędzi, które powinny być wyświetlane. Jeżeli taki parametr nie istnieje, serwlet wykorzystuje domyślną
wartość określoną wewnątrz samego szablonu. Następnie serwlet umieszcza w WebContext listę odpowiednich
narzędzi jako zmienną o nazwie narzedzie i zwraca szablon. Każdy wywołany wyjątek jest zapisywany w
dzienniku zdarzeń i przekazywany jako HandlerException.
Metody pobierzNarzedzia() umieszczone na końcu serwletu są jedynie metodami wspierającymi. Nie są
one dostępne szablonowi, chociaż mogłyby zostać uwidocznione poprzez zawarcie w kontekście obiektu
lookup zawierającego te metody. Następnie szablon mógłby wykonywać własną obsługę parametru stan:
#set $stan = $Form.stan;
#if (!stan) {
#set $narzedzia = $lookup.pobierzNarzedzia("zyje")
}
#else {
#set $narzedzia = $lookup.pobierzNarzedzia($stan)
}
Wybór pomiędzy podejściem opartym na przeciąganiu i modelem przepychania przedstawionym powyżej jest w
większość rzeczą gustu. WebMacro popiera raczej model przepychania, a Tea — przeciągania; jednak oba
narzędzia obsługują oba modele.
Można wykorzystać klasę com.oreilly.servlet.CacheHttpServlet, opisaną w rozdziale 3, „Okres
trwałości serwletu” w celu zapisania wyświetlanych danych w pamięci podręcznej i poprawienia wydajności
szablonów. Technika ta jest najlepsza w przypadku szablonów, które wyświetlają dane wymagające dłuższego
czasu przygotowania, a zmieniają się stosunkowo rzadko, na przykład szablony odczytujące wyniki z bazy
danych. Aby skorzystać z pamięci podręcznej należy utworzyć swój serwlet jako rozszerzenie
CacheHttpServlet (wiąże się to z niemożnością skorzystania z WMServlet i koniecznością samodzielnej
obsługi szablonów) oraz wykorzystać metodę getLastModified(). Jeżeli dane pochodzą z bazy danych,
metoda getLastModified() może zwrócić czas ostatniego uaktualnienia bazy (poprzez znajdującą się w
kontekście zmienną serwletu wykonującego uaktualnienie) lub, jeżeli czas uaktualnienia nie może zostać
określony, metoda może po prostu zwrócić aktualny czas minus pewną wartość, tak więc uaktualnienia mogą być
wykonywane w regularnych odstępach czasu. Jeżeli dane pochodzą z pliku, metoda może zwrócić ostatni czas
modyfikacji pliku (przypuszczalnie sprawdzając ją jedynie co pewien czas w celu poprawienia wydajności).

Filtry
W trakcie pisania niniejszego tekstu, proces tworzenia WebMacro dopiero od niedługiego czasu był
udostępniony większej ilości osób i został przeniesiony ze stosunkowo restrykcyjnej licencji GPL do mniej
ograniczającej licencji w stylu Apache'a. Wynikiem udostępnienia narzędzia większej ilości programistów jest
aktualna ich praca nad wieloma interesującymi i przydatnymi rozszerzeniami.
Najbardziej istotne rozszerzenie zawiera dołączany mechanizm filtrów. Jego ideą jest wyświetlanie zmiennej
poprzez filtr kontrolujący sposób jej wyświetlania. Standardowe filtry wykonują następujące działania:
• Kody ucieczkowe HTML — filtrowanie wprowadzonego przez użytkownika pola komentarza w celu
usunięcia specjalnych znaków, które mogłyby przypadkowo lub rozmyślnie sprawić kłopoty.
• Lokalizacja — filtrowanie daty poprzez pewną zlokalizowaną logikę wyświetlania.
• Przepisywanie URL-i — filtrowanie URL-i w celu dodania śledzącej sesji wartości jsessionid.
• Wyświetlanie wartości null — filtrowanie wyświetlania zmiennych o wartości null w celu
wyświetlenia komunikatu o błędzie lub ukrytego zignorowania zmiennej.

Filtry niestandardowe
Filtry niestandardowe wykonują konkretne działania. Na przykład, zmienna klient może zostać przefiltrowana
przez szablon klienta zaprojektowany w celu wyświetlania zmiennej i jej właściwości w oparciu o zewnętrzny
plik szablonu. Szablon klienta może nawet przefiltrować adres klienta poprzez zewnętrzny szablon adresu.
Pozwala to na ponowne stosowanie i łatwe dostosowywanie logiki wyświetlania. Filtry mogą być także łączone w
łańcuchy. Pozwoliłoby to na przykład na zarówno lokalizację jak i wyłączenie zmiennej, określoną wcześniej
wartością, jeżeli zmienna posiadałaby wartość null.
Możliwe zastosowania filtrów znajdują się ciągle jedynie w wyobraźni ludzi. Interesujące może okazać się
śledzenie, które zastosowania filtrów WebMacro staną się popularne.
Rozdział 16. Element

Construction Set

Pakiet Element Construction Set (ECS) prezentuje całkowicie inne podejście do tworzenia zawartości niż JSP,
Tea i WebMacro. ECS odchodzi daleko od tekstu HTML i traktuje HTML jako jedynie kolejny zestaw obiektów
Javy. Strona WWW w ECS jest zarządzana jak obiekt, który może zawierać inne obiekty HTML (takie jak listy i
tabele), które mogą zawierać jeszcze więcej obiektów HTML (takich jak elementy listy i komórki tabeli). Ten
model „obiektowego tworzenia HTML” okazuje się być bardzo potężny, ale również skoncentrowany na
programiście.
Stephan Nagy i Jon Stevens stworzyli ECS i udostępnili go jako Open Source jako część Java Apache Project,
oczywiście w licencji Apache. Biblioteka został utworzona według produktu htmlKona firmy WebLogic,
produktu, który stracił wsparcie po wykupieniu WebLogic przez BEA Systems. W niniejszym rozdziale
opisywany jest ECS w wersji 1.3.3, dostępny pod adresem http://jakarta.apche.org/ecs.

Elementy strony jako obiekty


ECS zawiera klasy dla wszystkich konstrukcji HTML 4.0. Przykład 16.1 przedstawia sposób tworzenia prostej
strony HTML przy pomocy ECS.
Przykład 16.1.
Strona jako zbiór obiektów
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class ECSWitaj extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {

odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

Document dok = new Document();


dok.appendTitle("Test ECS");
dok.appendBody(new Big("Witaj!"))
.appendBody(new P())
.appendBody("Aktualny czas to " + new Date());
dok.output(wyj);
}
}
Proszę zauważyć, że wszystkie znaczniki HTML zostały zastąpione obiektami. Powyższy serwlet tworzy nowy
obiekt Document przedstawiający stronę WWW, która zostanie zwrócona. Następnie dodaje do strony tytuł
„Test ECS” i dodaje do jej głównej części duży napis „Witaj!”, przerwę akapitową oraz wyświetla aktualny czas.
Na końcu wyświetla stronę w jej PrintWriter. W ten sposób działa obiektowe generowanie HTML —
pobranie obiektu Document, dodanie do niego obiektów składowych i wysłanie go do klienta.
Aby uruchomić powyższy serwlet należy zainstalować ECS przez umieszczenie pliku JAR ECS w ścieżce klas
serwera (lub w katalogu WEB-INF/lib aplikacji WWW). Dla ECS 1.3.3 plik JAR nosi nazwę ecs-1.3.3.jar.
Konieczne może się okazać ponowne uruchomienie serwera w celu odnalezienia nowego pliku JAR. Po
zainstalowaniu ECS można wywołać serwlet w zwykły sposób, a wygeneruje on wynik podobny do poniższego:
<html><head><title>Test ECS</title></head><body><big>Witaj!</big><p>
Aktualny czas to FRI OCT 25 23:17:37 GMT 2001</body></html>
Domyślnie wszystkie dane wyświetlane przez ECS pojawiają się w jednej linii bez wcięć i powrotów karetki.
Przyśpiesza to transfer podczas komunikacji z przeglądarką klienta. Aby otrzymać zawartość łatwiejszą do
odczytania należy dokonać edycji pliku ecs.properties, który jest dołączony do dystrybucji oraz zmienić wartość
pretty_print na true. Następuje teraz trudniejsza część: należy upewnić się, że plik ecs.properties
zostanie odnaleziony w ścieżce serwera przed plikiem JAR ECS, tak aby wyedytowany plik powodował
pominięcie pliku ecs.properties zawartego w pliku JAR. Plik ecs.properties jest poszukiwany jako
org/apache/ecs/ecs.properties, tak więc plik nie może zostać umieszczony bezpośrednio w ścieżce klas, ale w
podkatalogu org/apache/ecs katalogu wewnątrz ścieżki klas (na przykład WEB-
INF/classes/org/apache/ecs/ecs.properties).
Serwlet importuje dwa pakiety ECS — org.apache.ecs zawierający podstawowe klasy ECS i
org.apache.ecs.html zawierający klasy specyficzne dla HTML. (Istnieją inne pakiety odpowiedzialne za
XML, WML i RTF.)29 Pakiet org.apache.ecs.html zawiera prawie sto klas reprezentujących wszystkie
elementy HTML 4.0. Większość klas HTML nosi nazwy odpowiadające nazwom znaczników HTML — Big,
Small, P, Table, TR, TD, TH, H1, H2, H3, Frame, A, Head, Body i tak dalej. Każda klasa HTML posiada
metody służące do konfiguracji elementu. Na przykład klasa TD posiada metodę setBackground(String
url), która ustawia tło tej komórki tabeli. Klasa Body również posiada podobną metodę służącą do ustawiania
tła dla całej strony. Żaden inny element ECS nie posiada metody setBackground(), ponieważ żaden inny
element nie posiada możliwości ustawienia swojego tła, co pozwala ECS na upewnienie się, że programowo
utworzone elementy zawsze zawierają prawidłowo utworzony HTML.
Aby dodać elementy głównej części, serwlet wykorzystuje łączenie metod w łańcuchy, w których kilka metod
jest wywoływanych na tym samym obiekcie. W ECS można dotrzeć wiele takich konstrukcji. Na przykład, w
celu utworzenia tabeli:
Table tab = new Table()
.setCellPadding(0)
.setCellSpacing(0);
Puste miejsca nie mają znaczenia. Powyższy kod jest równy następującemu:
Table tab = new Table().setCellPadding(0).setCellSpacing(0);
Powyższy łańcuch jest możliwy do utworzenia, ponieważ każda metoda set i append zwraca odwołanie do
obiektu, na którym została wywołana — odwołanie to jest wykorzystywane do wywołania następnej metody.
Sztuczka ta często okazuje się przydatna przy korzystaniu z ECS.

Wyświetlanie zbioru wyników


Wykorzystanie ECS do pełnego tworzenia strony wypadło z łask po ulepszeniu opartych na serwletach
technologii szablonów. Po prostu dynamiczne tworzenie czegoś, co w większości jest statyczną zawartością
strony zajmuje zbyt wiele czasu. Jednak ECS ciągle posiada swoje miejsce. ECS sprawdza się w przypadku tych
części strony, które są wyjątkowo dynamiczne, w których do określenia zawartości do utworzenia konieczna jest
pełna moc Javy. Jak powiedział Jon Stevens, jeden z jego twórców, „należy wykorzystywać ECS wszędzie tam,
gdzie w innym przypadku wystąpiłoby wyj.println().”
Na przykład, proszę wyobrazić sobie aplikację WWW pozwalającą klientom na wykonywanie ad hoc zapytań w
bazie danych. Na przykład mógł zostać zaimplementowany system śledzenia błędów i potrzebny jest serwlet
dający zaawansowanym użytkownikom możliwość wykonywania swoich własnych zapytań w bazie danych (przy
pomocy połączenia z uprawnieniami tylko-do-odczytu). ECS sprawdza się w tworzeniu strony wyświetlającej
wyniki z baz danych, programowo tworząc tabelę dostosowaną do danych. Przykład 16.2 przedstawia prosty
element przeglądający ResultSet. Przypomina on klasę HtmlSQLWynik przedstawioną w rozdziale 9,
„Łączność z bazą danych”, która wykorzystywała wyj.println(). Zastosowanie ECS zamiast
wyj.println() pozwala na uproszczenie i większe możliwości dostosowania kodu.

29
Osoby zainteresowane programowym tworzeniem XML przy pomocy Javy powinny się raczej skupić na wykorzystaniu
JDOM (http://jdom.org), ponieważ JDOM jest lepiej zintegrowany z technologiami XML.
Przykład 16.1.
Ulepszona tabela ZbiorWynik
import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class ProstaTabelaResultSet extends Table {

public ProstaTabelaResultSet(ResultSet rs) throws SQLException {


setBorder(1);

ResultSetMetaData rsmd = rs.getMetaData();


int colCount = rsmd.getColumnCount();

TR rzad = new TR();


for (int i = 1; i <= colCount; i++) {
addElement(new TH().addElement(rsmd.getColumnName(i)));
}
addElement(rzad);

while (rs.next()) {
rzad = new TR();
for (int i = 1; i <= colCount; i++) {
addElement(new TD().addElement(rs.getString(i)));
}
addElement(rzad);
}
}
}
Powyższa kasa jest rozszerzeniem org.apache.ecs.html.Table, w związku z tym reprezentuje ona
element HTML <TABLE>. Wykonuje ona całą swoją pracę w konstruktorze, odczytując ResultSet i jego
ResultSetMetaData w celu wygenerowania prostej tablicy wyników. Konstruktor po pierwsze wywołuje
setBorder(1) w celu ustawienia atrybutu obramowania tabeli. Następnie tworzy wiersz (TR) wypełniony
elementami nagłówka tabeli (TH), z których każdy wyświetla nazwę kolumny odczytaną z metadanych.
Ostatecznie konstruktor wykonuje pętlę nad zbiorem wyników i tworzy wiersz dla każdego wpisu, wypełniając
wiersz elementami danych tabeli (TD) zawierającymi prostą reprezentację String zbioru wyników.
Klasa ProstaTabelaResultSet może zostać bezpośrednio wykorzystana przez serwlet przy pomocy
fragmentu kodu przedstawionego w przykładzie 16.3.
Przykład 16.3.
Wykorzystanie ProstaTabelaResultSet
Statement wyraz = lacz.createStatement();
boolean gotResultSet = wyraz.execute(sql); // SQL od użytkownika

if (!gotResultSet) {
wyj.println(wyraz.getUpdateCount() + "wierszy uaktualnionych.");
}
else {
wyj.println(new ProstaTabelaResultSet(wyraz.getResultSet()));
}
Powyższy kod tworzy tabelę podobną do przedstawionej na rysunku 16.1.
Rysunek
16.1.
Surowy
widok
wyników

Powyższa klasa może zostać wykorzystana w połączeniu z technologiami szablonów. Nie istnieje powód, dla
którego szablon musiałby tworzyć całą zawartość HTML samodzielnie; jeżeli pewna część strony potrzebuje
mocy ECS szablon może dołączyć dane wyświetlane przez element ECS do strony w odpowiednim miejscu.
Co się stanie, jeżeli tekst zwracany przez ResultSet będzie zawierał znaki, które HTML traktuje jako
specjalne, jak <, > i &? Domyślnie są one dołączane bezpośrednio i potencjalnie mogą zniszczyć strukturę
HTML. Aby rozwiązać ten problem, ECS zawiera klasę org.apache.ecs.filter.CharacterFilter,
która dokonuje konwersji znaków specjalnych HTML na odpowiadające im encje znakowe. Każdy element
używa tego filtru jako filtru domyślnego, ale z drugiej strony domyślnie całe filtrowanie jest wyłączone w celu
przyśpieszenia działania. Filtrowanie można włączyć dla danego elementu poprzez wywołanie
setFilterState(true) lub dla całego systemu poprzez edycję ecs.properties i ustawienie wartości
filter_state i filter_attribute_state na true. W takim przypadku znaki specjalne w całym
wyświetlanym tekście zostaną automatycznie przekonwertowane na encje znakowe.

Dostosowywanie wyświetlania
ECS wykorzystuje obiektową naturę Javy w celu stworzenia modelu wyświetlania danych o wysokim stopniu
dostosowalności. Przy pomocy niewielkiej ilości kodu Javy możliwe jest utworzenie na klasie
ProstaTabelaResultSet nie uproszczonej w takim stopniu tabeli TabelaResultSet. Ta nowa klasa
tabeli będzie przyjmować tablicę obiektów TabelaDostosuj w celu kontrolowania zawartości dodanej do
tablicy. Przykład 16.4 przedstawia interfejs TabelaDostosuj.
Przykład 16.4.
Klasa TabelaDostosuj
import java.sql.*;
import org.apache.ecs.*;
import org.apache.ecs.html.*;

public interface TabelaDostosuj {


public boolean przyjmij(int kolumnaTyp, String kolumnaTypNazwa,
String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException;

public Element wyswietl(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException;
}
Dla mechanizmów dostosowujących, implementujących powyższy interfejs metoda accept() powinna
zwracać true, jeżeli mechanizm posiada cel w obsłudze aktualnej komórki tabeli, a false, jeżeli nie. Metoda
przyjmij() posiada dostęp do typu kolumny, nazwy typu kolumny, nazwy kolumny, obiektu ResultSet i
indeksu kolumny w celu wspomożenia jej decyzji. Metoda wyswietl() zostaje wywołana, jeżeli przyjmij
() zwróci true. Tworzy ona i zwraca Element zawierający dane komórki tabeli, które mają zostać dodane do
tabeli. Wszystkie elementy HTML wykorzystują interfejs Element, tak więc poprzez zwracanie Element
metoda wyswietl() ma możliwość zwracania dowolnego typu elementu.
Możliwe jest utworzenie mechanizmów dostosowujących, służących do wyświetlania wartości null, dat i liczb,
jak przedstawiono w przykładach 16.5, 16.6 i 16.7. Tworzenie obiektów elementów HTML zamiast łańcuchów
pozwala na uproszczenie i łatwiejsze rozszerzanie kodu. Możliwe jest także programowe ograniczenie rodzaju
wyświetlanych elementów.
Przykład 16.5.
Mechanizm dostosowujący wartości null
import java.sql.*;
import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class NullDostosuj implements TabelaDostosuj {


public boolean przyjmij(int kolumnaTyp, String kolumnaTypNazwa,
String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
rs.getObject(indeks);
return rs.wasNull();
}

public Element wyswietl(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
// Wyświetlenie „niedostepne” dla wpisów null
return new StringElement("niedostepne");
}
}
Przykład 16.6.
Mechanizm dostosowujący wartości dat
import java.sql.*;
import java.text.*;
import java.util.*;
import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class DataDostosuj implements TabelaDostosuj {

DateFormat fmt;

public DataDostosuj(Locale lok) {


fmt = DateFormat.getDateTimeInstance(
DateFormat.SHORT, DateFormat.SHORT, lok);
}

public boolean przyjmij(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
return (kolumnaTyp == Types.DATE || kolumnaTyp == Types.TIMESTAMP);
}

public Element wyswietl(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
// Wyświetlenie skróconej daty i godziny przy pomocy podanej lokalizacji
return new StringElement(fmt.format(rs.getDate(indeks)));
}
}
Przykład 16.7.
Mechanizm dostosowujący wartości liczbowe
import java.sql.*;
import java.text.*;
import java.util.*;
import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class LiczbaDostosuj implements TabelaDostosuj {

NumberFormat fmt;

public LiczbaDostosuj(Locale lok) {


fmt = NumberFormat.getNumberInstance(lok);
}

public boolean przyjmij(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
return (kolumnaTyp == Types.TINYINT ||
kolumnaTyp == Types.SMALLINT ||
kolumnaTyp == Types.INTEGER ||
kolumnaTyp == Types.BIGINT ||
kolumnaTyp == Types.REAL ||
kolumnaTyp == Types.FLOAT ||
kolumnaTyp == Types.DOUBLE);
}
public Element wyswietl(int kolumnaTyp, String kolumnaTypNazwa,
String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
// Wyświetlenie liczby przy pomocy podanej lokalizacji
if (kolumnaTyp == Types.TINYINT ||
kolumnaTyp == Types.SMALLINT ||
kolumnaTyp == Types.INTEGER ||
kolumnaTyp == Types.BIGINT) {
return new StringElement(fmt.format(rs.getLong(indeks)));
}
else {
return new StringElement(fmt.format(rs.getDouble(indeks)));
}
}
}
Możliwe jest również utworzenie bardziej zaawansowanego mechanizmu dostosowującego, zmieniającego
wszystkie identyfikatory błędów w tabeli na hiperłącza do serwletu „widok błędów”. Następnie niezależnie od
zapytania przekazanego przez użytkownika, identyfikatory błędów będą tworzone jako hiperłącza. Przykład 16.8
przedstawia ten mechanizm.
Przykład 16.8.
Mechanizm dostosowujący identyfikatory błędów
import java.sql.*;
import java.text.*;
import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class BladIdDostosuj implements TabelaDostosuj {

String bladWidokSerwlet;

public BladIdDostosuj(String bladWidokSerwlet) {


this. bladWidokSerwlet = bladWidokSerwlet;
}

public boolean przyjmij(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
return ((kolumnaTyp == Types.CHAR ||
kolumnaTyp == Types.VARCHAR ||
kolumnaTyp == Types.LONGVARCHAR) &&
"idbledu".equalsIgnoreCase(kolumnaNazwa));
}

public Element wyswietl(int kolumnaTyp, String kolumnaTypNazwa,


String kolumnaNazwa, ResultSet rs, int indeks)
throws SQLException {
// Utworzenie łącza do serwletu wyświetlającego dany błąd
String idbledu = rs.getString(indeks);
return new A(bladWidokSerwlet + "?idbledu=" + idbledu, idbledu);
}
}
Klasa TabelaResultSet przyjmuje w swoim konstruktorze tablicę elementów TabelaDostosuj. Dla
każdej komórki mechanizmy dostosowujące umieszczone w tablicy mają możliwość kontroli tworzenia każdej
komórki tabeli. Mechanizmy będą wywoływane w porządku, w jakim umieszczone są w tablicy, a zwycięża
pierwszy mechanizm przyjmujący komórkę tabeli. Kod klasy TabelaResultSet jest przedstawiony w
przykładzie 16.9.
Przykład 16.9.
Dostosowywana tabela ResultSet
import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.ecs.*;
import org.apache.ecs.html.*;

public class TabelaResultSet extends Table {

public TabelaResultSet (ResultSet rs) throws SQLException {


this(rs, null);
}
public TabelaResultSet(ResultSet rs, TabelaDostosuj[] dostosuj)
throws SQLException {
setBorder(1);

if (dostosuj == null) {
dostosuj = new TabelaDostosuj[0];
}

ResultSetMetaData rsmd = rs.getMetaData();


int iloscKol = rsmd.getColumnCount();

TR rzad = new TR();


for (int i = 1; i <= iloscKol; i++) {
addElement(new TH().addElement(rsmd.getColumnName(i)));
}
addElement(rzad);

while (rs.next()) {
rzad = new TR();
for (int i = 1; i <= iloscKol; i++) {
TD td = new TD();
int kolumnaTyp = rsmd.getColumnType(i);
String kolumnaTypNazwa = rsmd.getColumnTypeName(i);
String kolumnaNazwa = rsmd.getColumnName(i);

// Danie każdemu mechanizmowi szansy kontrolowania sposobu wyświetlania


boolean dostosowany = false;
for (int c = 0; c < dostosuj.length; c++) {
TabelaDostosuj dost = dostosuj[c];
if (dost.przyjmij(kolumnaTyp, kolumnaTypNazwa,
kolumnaNazwa, rs, i)) {
td.addElement(dost.wyswietl(kolumnaTyp, kolumnaTypNazwa,
kolumnaNazwa, rs, i));
dostosowany = true;
break;
}
}

// Jeżeli brak właściwych mechanizmów, wyświetlenie wartości jako String


if (!dostosowany) {
td.addElement(rs.getString(i));
}

addElement(td);
}
addElement(rzad);
}
}
}
Zewnętrzna pętla while dokonuje iteracji na wierszach tabeli, podczas gdy zewnętrzna pętla for dokonuje
iteracji na kolumnach tabeli, a wewnętrzna pętla for zarządza logiką nowego mechanizmu dostosowującego.
Pierwszy mechanizm, który „przyjmie” komórkę obsługuje sposób jej wyświetlania. Jeżeli komórki nie przyjmie
żaden mechanizm, tabela wyświetla prostą wartość String. Serwlet wywołujący TabelaResultSet jest
przedstawiony w przykładzie 16.10.
Przykład 16.10.
Serwlet pracujący z TabelaResultSet
import java.io.*;
import java.sql.*;
import java.text.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.apache.ecs.*;
import org.apache.ecs.html.*;

import com.oreilly.servlet.*;

public class SerwletResultSet extends HttpServlet {

public void doPost(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {

odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();
String url = zad.getParameter("url");
String driver = zad.getParameter("driver");
String sql = zad.getParameter("sql");

// Szybkie sprawdzenie, czy url/driver/sql istnieje


ParameterParser analiza = new ParameterParser(zad);
String[] wymagany = { "url", "driver", "sql" };
String[] brakujacy = analiza.getMissingParameters(wymagany);
if (brakujacy != null && brakujacy.length > 0) {
odp.sendError(odp.SC_BAD_REQUEST,
"Podane muszą być łańcuchyURL, Driver i SQL ");
return;
}

String param1 = zad.getParameter("param1");


String param2 = zad.getParameter("param2");
String param3 = zad.getParameter("param3");
String param4 = zad.getParameter("param4");
String param5 = zad.getParameter("param5");
String param6 = zad.getParameter("param6");
String wart1 = zad.getParameter("wart1");
String wart2 = zad.getParameter("wart2");
String wart3 = zad.getParameter("wart3");
String wart4 = zad.getParameter("wart4");
String wart5 = zad.getParameter("wart5");
String wart6 = zad.getParameter("wart6");

Properties wlasc = new Properties();


if (param1 != null && wart1 != null) { wlasc.put(param1, wart1); }
if (param2 != null && wart2 != null) { wlasc.put(param2, wart2); }
if (param3 != null && wart3 != null) { wlasc.put(param3, wart3); }
if (param4 != null && wart4 != null) { wlasc.put(param4, wart4); }
if (param5 != null && wart5 != null) { wlasc.put(param5, wart5); }
if (param6 != null && wart6 != null) { wlasc.put(param6, wart6); }

Connection lacz = null;

try {
Class.forName(driver);
lacz = DriverManager.getConnection(url, wlasc);

Statement wyraz = lacz.createStatement();


boolean gotResultSet = wyraz.execute(sql);

if (!gotResultSet) {
wyj.println(wyraz.getUpdateCount() + " rzędów uaktualniono.");
}
else {
TabelaDostosuj[] dostosuj = {
new NullDostosuj(),
new DataDostosuj(zad.getLocale()),
new BladIdDostosuj(zad.getContextPath() + "/servlet/BladWidok"),
new LIczbaDostosuj(zad.getLocale()),
};
wyj.println(new ResultSetTable(wyraz.getResultSet(), dostosuj));
}
}
catch (Exception w) {
throw new ServletException(w);
}
}
}
Rysunek 16.2 przedstawia przykładowy wynik uruchomienia powyższego kodu. Proszę zauważyć, że teraz
identyfikator błędu jest hiperłączem, data jest ładnie sformatowana, a opis null wyświetla „niedostepne”.
Rysunek
16.2.
Dostosowany
widok zbioru
wyników
ResultSet
(data
europejska)

Przykład 16.11 przedstawia prosty fronton HTML dla serwletu.


Przykład 16.11.
Prosty fronton HTML dla SerwletResultSet
<HTML><HEAD><TITLE>Zapytanie SQL</TITLE></HEAD><BODY>

<P>
Niniejsza aplikacja wykonuje zapytanie SQL lub uaktualnia dowolną bazę danych
znajdującą się w sieci.
</P>

<FORM METHOD="POST" ACTION="servlet/SerlwetResultSet">


<TABLE WIDTH="75%" BORDER="1">
<TR>
<TD WIDTH="35%">URL bazy danych:</TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="url" SIZE="60"></TD>
</TR>
<TR>
<TD WIDTH="35%">Sterownik bazy danych:</TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="driver" SIZE="60"></TD>
</TR>
<TR>
<TD COLSPAN="2"><P ALIGN=CENTER> -- Właściwości bazy danych -- </p></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="wlasc1" VALUE="Nazwa
użytkownika"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart1"></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="wlassc2" VALUE="password"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart2"></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="walasc3" VALUE="cacherows"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart3"></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="wlasc4"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart4"></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="wlasc5"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart5"></TD>
</TR>
<TR>
<TD WIDTH="35%"><INPUT TYPE=TEXT NAME="wlasc6"></TD>
<TD WIDTH="65%"><INPUT TYPE=TEXT NAME="wart6"></TD>
</TR>
</TABLE>
<P>
<TEXTAREA NAME="sql" COLS="80" ROWS="5"></TEXTAREA>
<BR>
<INPUT TYPE=SUBMIT VALUE="Wyślij SQL">
</P>
</FORM>
Powyższa demonstracja ECS jedynie dotknęła możliwości ECS. Przy dostępności całej mocy Javy można
tworzyć do własnej dyspozycji strony lub części stron, które zostały w idealny sposób dostosowane do danych
oraz, przy pomocy obiektowej natury ECS można ułatwić sobie pracę przy pomocy dziedziczenia, poliformizmu
i silnego sprawdzania typów.
Rozdział 17. XMLC

XMLC wykorzystuje technologię XML w celu zmiany plików HTML w zasoby Javy. Został on utworzony przez
firmę Lutris jako część serwera Open Source Enhydra Application Server i może zostać pobrany niezależnie, w
celu zastosowania go w połączeniu z dowolnym innym kontenerem serwletów30. W niniejszym rozdziale
omówiony zostanie XMLC 1.2b1 dostępny pod adresem http://xmlc.enhydra.org. XMLC wymaga JDK1.1 lub
późniejszego i dowolnej wersji Servlet API. Jest ona udostępniania w licencji Enhydra Public License (EPL),
licencji Open Source podobnej do Mozilla Public License (MPL). Mówiąc krótko, według tej licencji zmiany w
istniejącym kodzie powinny być przesłane z powrotem do producenta, ale pozwala ona na prywatne, a nawet
komercyjne rozszerzenia.
XMLC oznacza XML Compiler (Kompilator XML). Narzędzie to pobiera standardowy dokument HTML lub
XML i „kompiluje” go do klasy Javy. Klasa ta zawiera instrukcje Javy potrzebne do utworzenia reprezentacji
dokumentu XML DOM (Document Object Model — Model Obiektu Dokumentu) w formie drzewa w pamięci.
Programista może manipulować drzewem znajdującym się w pamięci w celu dodania zawartości dynamicznej, a
po przetworzeniu może wyświetlić zmodyfikowane drzewo jako HTML, XHTML lub dowolny inny XML.
W tradycyjnym podejściu, projektant strony tworzy jedną lub więcej „makiet” tworzonej strony WWW. Makiety
to czyste pliki HTML. Są one przeglądane przez zainteresowane osoby, odbywa się nad nimi dyskusja, po czym,
jeżeli to konieczne, zostają one poprawione. Projektant nie musi dodawać do pliku żadnych instrukcji XMLC,
jedynie zgodne z HTML 4.0 atrybuty ID do obszarów strony, które mają zostać zastąpione zawartością
dynamiczną.
Narzędzie XMLC kompiluje offline makietę strony do klasy Javy zawierającej reprezentację dokumentu w
formie drzewa DOM. Programista Javy wykorzystuje publiczny interfejs tej klasy w celu utworzenia klasy
manipulacyjnej, która tworzy egzemplarz drzewa, odnajduje części dokumentu, które powinny zostać zmienione
i odpowiednio jej modyfikuje. Części te są odnajdywane przy pomocy metod dostępu dodanych do drzewa przez
XMLC dla każdego atrybutu ID w dokumencie. W ten sposób pliki HTML stają się właściwie zasobami dla
Javy.
Jeżeli na stronie powinna znaleźć się lista elementów, makieta może zawierać pięć lub dziesięć z nich, aby
wyglądać realistycznie. Programista Javy może usunąć wszystkie elementy oprócz pierwszego, a następnie
skonstruować właściwą listę poprzez powtarzane klonowanie pierwszego elementu — w ten sposób
dostosowując się do stylu makiety, ale wykorzystując dane tworzone dynamicznie. Możliwe jest nawet nakazanie
kompilatorowi XMLC automatycznego usunięcia wszystkich elementów oprócz pierwszego, co poprawia
wydajność.
XMLC osiąga wysoki poziom oddzielenia zawartości i prezentacji. Plik HTML to czysty HTML, pliki Javy to
czysta Java, a pliki łączą się jedynie poprzez uzgodnione znaczniki ID. Kiedy utworzone zostają nowe makiety,
mogą one zostać wykorzystane bezpośrednio bez konieczności wstecznego dopasowania, a jeżeli ważnego
znacznika nie ma w nowej makiecie, wywołany zostaje błąd kompilacji. Dzieje się tak, kiedy klasa
manipulacyjna próbuje wywołać metodę odczytującą nieistniejący ID.

Prosta kompilacja XML


Aby nauczyć się stosowania XML należy rozpocząć od prostej aplikacji podstawiającej. Przykład 17.1
przedstawia prostą makietę strony WWW witaj.html, która wita użytkownika po imieniu i podaje informację, ile

30
Enhydra Lutris to naukowa nazwa morskiej wydry, co jest odpowiednim identyfikatorem dla firmy z siedzibą w Santa
Cruz. W pobliżu tego miasta znajduje się zatoka Monterey, w której żyje mnóstwo wydr.
wiadomości czeka na niego. Tekst, który zostanie podmieniony jest otoczony zgodnymi z HTML 4.0
znacznikami <SPAN>.
Przykład 17.1.
Proste podstawienie XMLC
<!-- witaj.html -->
<HTML>
<HEAD><TITLE>Witaj</TITLE></HEAD>
<BODY>

<H2><SPAN ID="Powitanie">Witaj Agnieszka</SPAN></H2>


Masz <SPAN ID="Wiadomosci">103</SPAN> nowe wiadomości.

</BODY>
</HTML>
W celu uruchomienia XMLC z powyższym plikiem, po pierwsze należy zainstalować XMLC. Należy pobrać plik
z wiryny http://xmlc.enhydra.org, rozpakować archiwum i postępować według instrukcji w pliku README
mówiących o uruchomieniu xmlc-config. Potem można już uruchomić kompilator xmlc:
xmlc –class Witaj –keep witaj.html
Powyższa linia kodu nakazuje XMLC skompilowanie pliku witaj.html do klasy o nazwie Witaj, zatrzymując
wygenerowany plik Witaj.java do sprawdzenia. Ponieważ standardowe pliki HTML nie muszą posiadać
właściwej formy XML, XMLC wykorzystuje analizator składniowy Tidy do obsługi konwersji z HTML do
DOM. Proszę jednak pamiętać, że Tidy może popełniać błędy podczas obsługi plików HTML zbytnio
oddalonych od prawidłowego HTML, a kiedy to nastąpi, podczas kompilacji XMLC wyświetlona zostanie długa
lista błędów. Aby wyeliminować ten problem, można wykorzystać narzędzia służące do projektowania HTML
(takie jak Dreamweaver) w celu utworzenia HTML nadającego się do przetwarzania XMLC. Większa ilość
informacji na temat Tidy jest dostępna pod adresem www.w3.org/People/Raggett/tidy, a na temat portu Javy o
nazwie Jtidy pod adresem http://www3.sympatico.ca/ac.quick/jtidy.html.
XMLC obsługuje wiele opcji kompilacyjnych. Najpopularniejsze z nich zostały wymienione poniżej31. Składnia
linii poleceń wygląda następująco:
xmlc [opcje] plikdok
-class Nazwaklasy
Określa pełną nazwę generowanej klasy. Na przykład Witaj lub com.servlets.xml.Witaj.
-keep
Nakazuje zapamiętanie utworzonego źródła Javy. Przydatne podczas rozpoczynania nauki XMLC.
-methods
Wyświetla podpis każdej tworzonej metody dostępu. Również przydatne przy rozpoczynaniu nauki.
-verbose
Wyświetla dodatkowe informacje na temat procesu kompilacji
-urlmapping staryURL nowyURL

Określa zasadę podmiany URL-i. Wszystkie egzemplarze staryURL


zostaną zastąpione przez nowyURL. Pozwala na zmiany w
URL-u pomiędzy makietą i produktem. Opcja może zostać
podana wielokrotnie.
-urlregexpmapping wyrreg podst
Określa zaawansowaną zasadę podmiany URL-i przy pomocy wyrażeń regularnych. Opcja może zostać
podana wielokrotnie.
-urlsetting id nowyURL

31
XMLC jest dostępny również jako zadanie Ant, co może zainteresować osoby wykorzystujące doskonałe narzędzie
kompilacji ANT. Proszę zobaczyć http://jakarta.apache.org.
Określa URL, który powinien zostać zmodyfikowany w DOM według
jego znacznika ID. Url o identyfikatorze id zostanie
zmieniony na nowyURL.
-Java prog.

Określa kompilator Javy, który powinien zostać wykorzystany.


-d katalog

Określa docelowy katalog. Przekazywany bezpośrednio


kompilatorowi Javy podczas fazy kompilacji.
-delete-class nazwaklasy

Usuwa wszystkie elementy dokumentu posiadające atrybut CLASS


nazwaklasy. Przydatne także przy usuwaniu danych
makiety, jak zostanie to przedstawione w dalszej
części. Opcja może zostać podana wielokrotnie.
-version

Wyświetla wersję XMLC.


Po uruchomieniu XMLC ze znacznikiem – keep można przejrzeć wygenerowane źródło. Źródło Witaj.java jest
przedstawione w przykładzie 17.2.
Przykład 17.2.
Generowane automatycznie źródło Witaj.java (komentarze przetłumaczone)
/*
*******************************************
* KOD WYGENEROWANY PRZEZ XMLC NIE EDYTOWAĆ*
*******************************************
*/
import org.w3c.dom.*;
import org.enhydra.xml.xmlc.XMLCError;
import org.enhydra.xml.xmlc.XMLCUtil;
import org.enhydra.xml.xmlc.dom.XMLCDomFactory;

/**
* klasa dokument XMLC utworzona z pliku
* witaj.html
*/
public class Witaj extends org.enhydra.xml.xmlc.html.HTMLObjectImpl implements
org.enhydra.xml.xmlc.XMLObject, org.enhydra.xml.xmlc.html.HTMLObject {
private int $elementId_Powitanie = 8;

private int $elementId_Wiadomosci = 13;

private org.enhydra.xml.lazydom.html.LazyHTMLElement $element_Powitanie;

private org.enhydra.xml.lazydom.html.LazyHTMLElement $element_Wiadomosci;

/**
* Pole wykorzystywane do stwierdzenia, że to jest klasa stworzona przez XMLC
* w łańcuchu dziedziczenia. Zawiera odwołanie do obiektu klasy.
*/
public static final Class XMLC_GENERATED_CLASS = Witaj.class;

/**
* Pole zawierające zależną od CLASSPATH nazwę pliku źródłowego, z którego
* ta klasa została wygenerowana.
*/
public static final String XMLC_SOURCE_FILE = "/witaj.html";

/**
* fabryka XMLC DOM skojarzona z tą klasą.
*/
private static final org.enhydra.xml.xmlc.dom.XMLCDomFactory fDOMFactory =
org.enhydra.xml.xmlc.dom.XMLCDomFactoryCache.getFactory
(org.enhydra.xml.xmlc.dom.lazydom.LazyHTMLDomFactory.class);;

/**
* Opcje wykorzystywane do preformatowania dokumentu po kompilacji
*/
private static final org.enhydra.xml.io.OutputOptions fPreFormatOutputOptions;

/**
* Szablon wykorzystywany przez wszystkie egzemplarze.
*/
private static final org.enhydra.xml.lazydom.TemplateDOM fTemplateDocument;

/**
* dokument Lazy DOM
*/
private org.enhydra.xml.lazydom.LazyDocument lazyDocument;

/*
* Inicjator klas.
*/
static {
org.enhydra.xml.lazydom.html.LazyHTMLDocument doc =
(org.enhydra.xml.lazydom.html.LazyHTMLDocument)fDOMFactory.createDocument(null,
"HTML", null);

buildTemplateSubDocument(doc, doc);

fTemplateDocument = new org.enhydra.xml.lazydom.TemplateDOM(doc);

fPreFormatOutputOptions = new org.enhydra.xml.io.OutputOptions();

fPreFormatOutputOptions.setFormat
(org.enhydra.xml.io.OutputOptions.FORMAT_AUTO);

fPreFormatOutputOptions.setEncoding("ISO-8859-1");

fPreFormatOutputOptions.setPrettyPrinting(false);

fPreFormatOutputOptions.setIndentSize(4);

fPreFormatOutputOptions.setPreserveSpace(true);

fPreFormatOutputOptions.setOmitXMLHeader(false);

fPreFormatOutputOptions.setOmitDocType(false);

fPreFormatOutputOptions.setOmitEncoding(false);

fPreFormatOutputOptions.setDropHtmlSpanIds(true);

fPreFormatOutputOptions.setOmitAttributeCharEntityRefs(true);

fPreFormatOutputOptions.setPublicId(null);

fPreFormatOutputOptions.setSystemId(null);

fPreFormatOutputOptions.setMIMEType(null);

fPreFormatOutputOptions.markReadOnly();

/**
* Domyślny konstruktor.
*/
public Witaj() {
buildDocument();

/**
* Konstruktor z opcją tworzenia DOM.
*/
public Witaj(boolean buildDOM) {
if (buildDOM) {

buildDocument();

}
}

/**
* Kopiowanie konstruktora.
*/
public Witaj(Witaj src) {
setDocument((Document)src.getDocument().cloneNode(true), src.getMIMEType
(), src.getEncoding());

syncAccessMethods();

/**
* Stworzenie dokumentu jako DOM i inicjacja pól metod dostępu.
*/
public void buildDocument() {
lazyDocument = (org.enhydra.xml.lazydom.html.LazyHTMLDocument)
((org.enhydra.xml.xmlc.dom.lazydom.LazyDomFactory)fDOMFactory).createDocument
(fTemplateDocument);

lazyDocument.setPreFormatOutputOptions(fPreFormatOutputOptions);

setDocument(lazyDocument, "text/html", "ISO-8859-1");

/**
* Utworzenie poddrzewa dokumentu.
*/
private static void buildTemplateSubDocument
(org.enhydra.xml.lazydom.LazyDocument document,
org.w3c.dom.Node parentNode) {
Node $node0, $node1, $node2, $node3, $node4, $node5;

Element $elem0, $elem1, $elem2, $elem3, $elem4;

Attr $attr0, $attr1, $attr2, $attr3, $attr4;

Element $docElement = document.getDocumentElement();

$node1 = document.createTemplateComment(" witaj.html ", 1);

parentNode.insertBefore($node1, $docElement);

$elem1 = document.getDocumentElement();

((org.enhydra.xml.lazydom.LazyElement)$elem1).makeTemplateNode(2);

((org.enhydra.xml.lazydom.LazyElement)$elem1).setPreFormattedText
("<HTML>");

$elem2 = document.createTemplateElement("HEAD", 3, "<HEAD>");

$elem1.appendChild($elem2);

$elem3 = document.createTemplateElement("TITLE", 4, "<TITLE>");

$elem2.appendChild($elem3);

$node4 = document.createTemplateTextNode("Witaj", 5, "Witaj");

$elem3.appendChild($node4);

$elem2 = document.createTemplateElement("BODY", 6, "<BODY>");

$elem1.appendChild($elem2);
$elem3 = document.createTemplateElement("H2", 7, "<H2>");

$elem2.appendChild($elem3);

$elem4 = document.createTemplateElement("SPAN", 8, "<SPAN>");

$elem3.appendChild($elem4);

$attr4 = document.createTemplateAttribute("id", 9);

$elem4.setAttributeNode($attr4);

$node5 = document.createTemplateTextNode("Powitanie", 10, "Powitanie");

$attr4.appendChild($node5);

$node5 = document.createTemplateTextNode("Witaj Agnieszka", 11, "Witaj


Agnieszka");

$elem4.appendChild($node5);

$node3 = document.createTemplateTextNode("Masz ", 12, "Masz ");

$elem2.appendChild($node3);

$elem3 = document.createTemplateElement("SPAN", 13, "<SPAN>");

$elem2.appendChild($elem3);

$attr3 = document.createTemplateAttribute("id", 14);

$elem3.setAttributeNode($attr3);

$node4 = document.createTemplateTextNode("Wiadomosci", 15, "Wiadomosci");

$attr3.appendChild($node4);

$node4 = document.createTemplateTextNode("103", 16, "103");

$elem3.appendChild($node4);

$node3 = document.createTemplateTextNode(" nowe wiadomo\u0153ci.", 17, "


nowe wiadomo&#339;ci.");

$elem2.appendChild($node3);

/**
* Kolonowanie dokumentu.
*/
public Node cloneNode(boolean deep) {
cloneDeepCheck(deep);

return new Witaj(this);

/**
* Pobranie fabryki XMLC DOM skojarzonej z klasą.
*/
protected final org.enhydra.xml.xmlc.dom.XMLCDomFactory getDomFactory() {
return fDOMFactory;

}
/**
* Pobranie dokumentu o identyfikatorze <CODE>Powitanie</CODE>.
* proszę zobaczyć org.w3c.dom.html.HTMLElement
*/
public org.w3c.dom.html.HTMLElement getElementPowitanie() {
if (($element_Powitanie == null) && ($elementId_Powitanie >= 0)) {

$element_Powitanie = (org.enhydra.xml.lazydom.html.LazyHTMLElement)
lazyDocument.getNodeById($elementId_Powitanie);

return $element_Powitanie;

/**
* Pobranie dokumentu o identyfikatorze <CODE>Wiadomosci</CODE>.
* proszę zobaczyć org.w3c.dom.html.HTMLElement
*/
public org.w3c.dom.html.HTMLElement getElementWiadomosci() {
if (($element_Wiadomosci == null) && ($elementId_Wiadomosci >= 0)) {

$element_Wiadomosci = (org.enhydra.xml.lazydom.html.LazyHTMLElement)
lazyDocument.getNodeById($elementId_Wiadomosci);

return $element_Wiadomosci;

/**
* Pobranie wartości potomka tekstowego elementu <CODE>Powitanie</CODE>.
* Proszę zobaczyć org.w3c.dom.Text
*/
public void setTextPowitanie(String text) {
if (($element_Powitanie == null) && ($elementId_Powitanie >= 0)) {

$element_Powitanie = (org.enhydra.xml.lazydom.html.LazyHTMLElement)
lazyDocument.getNodeById($elementId_Powitanie);

doSetText($element_Powitanie, text);

/**
* Pobranie wartości potomka tekstowego elementu <CODE>Wiadomosci</CODE>.
* Proszę zobaczyć org.w3c.dom.Text
*/
public void setTextWiadomosci(String text) {
if (($element_Wiadomosci == null) && ($elementId_Wiadomosci >= 0)) {

$element_Wiadomosci = (org.enhydra.xml.lazydom.html.LazyHTMLElement)
lazyDocument.getNodeById($elementId_Wiadomosci);

doSetText($element_Wiadomosci, text);

/**
* Rekursja funkcji ustawiającej pola funkcji dostępu DOM.
* Pola brakujących identyfikatorów ustawione na null.
*/
protected void syncWithDocument(Node node) {
if (node instanceof Element) {

String id = ((Element)node).getAttribute("id");

if (id.length() == 0) {

} else if (id.equals("Powitanie")) {

$elementId_Powitanie = 8;

$element_Powitanie =
(org.enhydra.xml.lazydom.html.LazyHTMLElement)node;
} else if (id.equals("Wiadomosci")) {

$elementId_Wiadomosci = 13;

$element_Wiadomosci =
(org.enhydra.xml.lazydom.html.LazyHTMLElement)node;

Node child = node.getFirstChild();

while (child != null) {

syncWithDocument(child);

child = child.getNextSibling();

}
Powyższy przykład zawiera dużą ilość kodu, nie jest jednak skomplikowany. Klasa nosi nazwę Witaj, jak
podano w opcji –class. Jest ona rozszerzeniem org.ehydra.xml.xmlc.html.HTMLObjectImpl,
superklasy XMLC dla wszystkich obiektów HTML i implementuje org.w3c.dom.html.HTMLObject,
standardowy interfejs DOM przedstawiający dokument HTML. Konstruktor Witaj wywołuje metodę
buildDocument() wypełniającą drzewo DOM zawartością z pliku HTML. DOM jest po prostu zbiorem
interfejsów i do wyboru jest wiele jego implementacji. W metodzie createDocument() można dostrzec, że
niniejsza klasa wykorzystuje domyślną implementację XMLC, oznaczoną przez DefaultHTMLDomFactory,
którą w wersji 1.2b1 jest Apache Xerces (proszę zobaczyć http://xml.apache.org).
Po uruchomieniu xmlc z opcją –methods można ujrzeć podsumowania wygenerowanych metod dostępu w
źródle:
% xmlc –class Witaj –keep –methods witaj.html
public org.w3c.dom.html.HTMLElement getElementPowitanie();
public void setTextPowitanie(String text);
public org.w3c.dom.html.HTMLElement getElementWiadomosci();
public void setTextWiadomosci (String text);
Metody get zwracają obiekt DOM HTMLElement reprezentujący element HTML oznaczony atrybutem ID.
Metody set ustawiają zawartość tekstową tych elementów. Proszę zauważyć, że wartość atrybutu ID jest
zawarta w nazwach metod. Dla wartości, które nie mogą zostać przekonwertowane na prawidłowe identyfikatory
Javy nie zostaną wygenerowane metody dostępu. Tak więc należy być ostrożnym i stosować jedynie litery, cyfry
i kreski dolne. Proszę również pamiętać, że wartości identyfikatorów nie powinny powtarzać się na jednej
stronie.

Klasa manipulacyjna
Po skompilowaniu pliku HTML do jego opartej na Javie reprezenatcji XML, następnym krokiem jest utworzenie
tak zwanej klasy manipulacyjnej, Klasa ta tworzy egzemplarz dokumentu, modyfikuje jego zawartość i wyświetla
go we właściwym miejscu. Przykład 17-3 przedstawia pojedynczą klasę manipulacyjną dla strony Witaj.
Proszę zauważyć, że jest to samodzielny program. Klasa manipulacyjna powinna być serwletem, lub klasą
wywoływaną przez serwlet, ale nie jest to absolutnie konieczne. XMLC nie jest zależny od Servlet API.
Przykład 17.3
Klasa manipulacyjna dla strony Witaj
import java.io.*;
import org.w3c.dom.*;
import org.w3c.dom.html.*;

import org.enhydra.xml.io.DOMFormatter;

public class WitajManipulacja {

public static void main(String[] args) {


// Pewna pseudo-dynamiczna zawartość
String nazwaUzyt = "Jan Kowalski";
int iloscWiadomosci = 43;
// Stworzenie drzewa DOM
Witaj witaj = new Witaj();

// Ustawienie tytułu przy pomocy standardowej metody DOM


witaj.setTitle("Witaj XMLC!");

// Ustawienie wartości dla „powitanie”


witaj.setTextPowitanie("Witaj, " + nazwaUzyt);

// Ustawienie wartości dla „wiadomosci”


witaj.setTextWiadomosci("" + iloscWiadomosci);

try {
DOMFormatter formatuj = new DOMFormatter(); // może być dostosowana
formatuj.write(witaj, System.out);
}
catch (IOException w) {
w.printStackTrace();
}
}
}
Powyższa klasa na początku ustawia pseudodynamiczne wartości, które zostaną dodane do strony — nazwę
użytkownika i ilość wiadomości. Następnie tworzy egzemplarz klasy Witaj, która przechowuje zawartość
dokumentu HTML. Jeżeli egzemplarz witaj zostałby w tym momencie wyświetlony, ukazałaby się dokładna
reprezentacja oryginalnego pliku HTML. Klasa manipulacyjna następnie ustawia tytuł strony, wartość powitania
i wartość ilości wiadomości. Powitanie i ilość wiadomości są ustawiane przy pomocy metod dostępu dodanych
przez XMLC. Tytuł ustawiany jest przy pomocy standardowej metody DOM. Istnieje wiele ciekawych
standardowych metod dostępu dla klas HTML DOM, jak na przykład getApplets(), getImages(),
getLinks(), getForms(), getAnchors() i tak dalej. Większa ilość informacji na temat możliwości klas
DOM jest zawarta w dokumentacji dołączonej do XMLC. Na końcu klasa wykorzystuje
org.enhydra.xml.io.DOMFormatter w celu wyświetlenia egzemplarza Hello w System.out.
DOMFormatter może próbować zastosowania w swoim konstruktorze klasy OutputOption w celu
sprawdzenia, jak wyświetlany jest XML, ale w przypadku standardowych przeglądarek WWW domyślne
zachowanie działa prawidłowo i tworzy:
<HTML><HEAD><TITLE>Witaj XMLC!</TITLE></HEAD><BODY><H2><SPAN id='Powitanie'>Witaj,
Jan Kowalski</SPAN></H2>Masz <SPAN id='Wiadomosci'>43</SPAN> nowe
wiadomości.</BODY><!-- witaj.html --></HTML>
Proszę zauważyć, że wartości z makiety zostały zastąpione nowymi i usunięta została niepotrzebna wolna
przestrzeń w celu poprawy wydajności. Można także dostrzec, że komentarz został umieszczony na końcu pliku.
Jest to nieszkodliwy błąd Xerces.
W celu samodzielnego uruchomienia klasy manipulacyjnej należy upewnić się, że plik xmlc.jar zawierający klasy
XMLC może zostać odnaleziony w ścieżce klas (lub ścieżce klas serwera, jeżeli XMLC jest wywoływany przez
serwlet). Proszę pamiętać, że xmlc.jar zawiera kilka pakietów wspierających, które mogą powodować konflikty,
jeżeli zainstalowane zostały także inne wersje tych pakietów. W przypadku XMLC 1.2b1 należy uważać na
zewnętrzne kolizje z Apache Xerces 1.0.2, SAX 1.0, DOM Level 2, GNU RegExp 1.0.8 i Jtidy 26jul1999.
Wynikiem kolizji pakietów mogą być dziwne komunikaty o błędach. Na przykład, podczas uruchomienia xmlc
kompilator może zgłosić, że klasa utworzona przez XMLC jest abstrakcyjna i w związku z tym nie może zostać
wykonany jej egzemplarz, ponieważ klasa nie definiuje konkretnej metody z konkretnej klasy. Prawdziwym
problemem jest niedopasowanie dwóch wersji DOM. Rozwiązaniem jest upewnienie się że najnowsza wersja
występuje wcześniej w ścieżce klas. Rozwiązanie to działa, dopóki wersje pakietów są wstecznie kompatybilne.

Modyfikacja listy
Teraz zostanie opisana nieco bardziej zaawansowana operacja — modyfikacja listy. Po pierwsze utworzona
zostanie strona prototypowa zawierająca makietę listy, jak przedstawiono w przykładzie 17.4.
Przykład 17.4.
Makieta listy języków
<!-- przegl.html -->
<HTML><HEAD><TITLE>Przeglądanie XMLC </TITLE></HEAD>

<H1>Lokalizacje klienta</H1>

<UL>
<LI ID="lokal">en
<LI CLASS="makieta">es
<LI CLASS="makieta">jp
</UL>
</BODY></HTML>
W rzeczywistości powyższa lista byłaby częścią dużo większej strony, ale dla XMLC nie jest to w ogóle
interesujące. Strona zostaje skompilowana przy pomocy kompilatora XMLC, z opcją –delete-class
makieta w celu automatycznego usunięcia w fazie kompilacji elementów oznaczonych jako makieta:
% xmlc –class Przegl –keep –delete-class makieta –methods przegl.html
public org.w3c.dom.html.HTMLLIElement getElementLokal();
Proszę zauważyć, że do dokumentu została dodana tylko jedna metoda dostępu — getElementLokal(),
która zwraca element <ID> oznaczony jako lokal w obiekcie HTMLLIElement. Klasa manipulacyjna
dynamicznie zmieniająca powyższy dokument jest przedstawiona w przykładzie 17.5.
Przykład 17.5.
Klasa manipulacyjna dla listy języków
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.w3c.dom.*;
import org.w3c.dom.html.*;

import org.enhydra.xml.io.DOMFormatter;

public class PrzeglManipulacja extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

// Pobranie do wyświetlenia pewnych danych dynamicznych


Enumeration lokale = zad.getLocales();

// Utworzenie drzewa DOM


Przegl przegl = new Przegl();

// Pobranie pierwszego, „prototypowego” elementu listy


// Reszta została usunięta podczas kompilacji xmlc
HTMLLIElement element = przegl.getElementLokal();

// Pobranie przodka prototypu w celu umożliwienia zarządzania potomkami


Node przodek = element.getParentNode();

// Pętla nad lokalizacjami i dodanie węzła dla każdej


while (lokale.hasMoreElements()) {
Locale lok = (Locale)lokale.nextElement();
HTMLLIElement nowyElement = (HTMLLIElement) element.cloneNode(true);
Text tekst = przegl.createTextNode(lok.toString());
nowyElement.replaceChild(tekst, newItem.getLastChild());
przodek.insertBefore(nowyElement, null);
}

// Usunięcie elementu prototypowego


przodek.removeChild(element);

// Wyświetlenie dokumentu
DOMFormatter formatuj = new DOMFormatter(); // może być poprawiony
formatuj.write(przegl, out);
}
}
Interesującą częścią powyższego serwletu jest pętla while. Dla każdej lokalizacji serwlet klonuje element
prototypowy, zmienia tekst przechowywany przez klon i wstawia klon na końcu listy. Po pętli while serwlet
usuwa element prototypowy, pozostawiając listę pełną klonów zawierającą prawdziwe dane. Wynik
przedstawiono na rysunku 17.132.

32
Proszę pamiętać o interakcjach w ścieżce klas podczas uruchamiania powyższego przykładu. Wykorzystywane są biblioteki
potrzebne ZMLC, JDOM i przypuszczalnie samemu serwerowi. Usatysfakcjonowanie wymagań biblioteki wersji XML dla
wszystkich trzech składników bez konfliktów może wymagać sporych umiejętności w czarnej magii i nie zawsze jest
możliwe.
Rysunek 17.1. Skróty lokalizacji — angielski (USA), wietnamski i tajski
W powyższym przykładzie można dostrzec, że jedną z trudności XMLC jest obsługa nieintuicyjnego modelu
obiektów DOM. Ze stosowaniem DOM związane są pewne sztuczki. Na przykład, można dostrzec, że zawartość
String może zostać utworzona jedynie przy pomocy metody fabryki createTextNode() wywoływanej na
dokumencie, do którego ma zostać dodany tekst, a kiedy zawartość jest już gotowa, model dodawania tekstu do
dokumentu wymaga dodania go jako węzła potomnego swojego elementu i usunięcia istniejącego potomka.
Rozważa się możliwość dołączenia dużo łatwiejszego w obsłudze modelu obiektów JDOM do przyszłych wersji
XMLC. Poza tym, pakiety narzędziowe dla XMLC przejmują DOM i odchodzą od niektórych z bardziej
nieprzyjemnych jego wymagań.

Aplikacja „Narzędzia”
W celu zakończenia dyskusji nad XMLC opisany zostanie sposób tworzenia przy pomocy XMLC aplikacji
„Narzędzia” wykorzystanej wcześniej do zademonstrowania Tea i WebMacro. Na początku opisany zostanie
szablon HTML przedstawiający wspólny projekt graficzny witryny, ale nie posiadający żadnej zawartości
odwołującej się do listy narzędzi. Jest on przedstawiony w przykładzie 17.6.
Przykład 17.6
Plik szablonu aplikacji „Narzędzia”
<!-- szablon.html -->
<HTML><HEAD><TITLE>Przykładowy Tytuł</TITLE></HEAD>

<BODY BGCOLOR="#FFFFFF" BACKGROUND="/obrazki/tlo.gif"


LINK="#003333" ALINK="#669999" VLINK="#333333">

<IMG SRC="/obrazki/banner.gif" WIDTH=600 HEIGHT=87 BORDER=0><BR>

<TABLE>
<TR>
<TD WIDTH=125 VALIGN=TOP>
<BR><BR><BR>
<FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#FF0000">
<A HREF="/indeks.html">Strona główna</A><BR>
<A HREF="/hosting.html">Hosting</A><BR>
<A HREF="/mechanizmy.html">Mechanizmy</A><BR>
</FONT>
</TD>

<TD WIDTH=475>

<TABLE CELLPADDING=5><TR><TD WIDTH=600 BGCOLOR="#006699" VALIGN=TOP>


<B><FONT FACE="Arial,Helvetica" SIZE="+2">
<SPAN ID="tytul">Przykładowy tytuł</SPAN>
</FONT></B>
</TD></TR></TABLE>

<B><FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#003366">


<SPAN ID="tytul2">Przykładowy drugi tytuł</SPAN>
</FONT></B><P>

<P>

<FONT FACE="Arial,Helvetica">
<SPAN ID="opis">Przykładowy opis</SPAN>

<DIV id="zawartosc">Tu właściwa zawartość</DIV>

</FONT>

</TD>
</TR>

<TR>
<TD></TD>
<TD WIDTH=475 ALIGN=CENTER COLSPAN=3>
<HR>
<FONT FACE="Arial,Helvetica">
<A HREF="/indeks.html">Strona Główna</A>&nbsp;&nbsp;
<A HREF="/hosting.html">Hosting</A>&nbsp;&nbsp;
<A HREF="/mechanizmy.html">Mechanizmy</A>&nbsp;&nbsp;<P>
</FONT>

<TABLE WIDTH=100%>
<TR>
<TD WIDTH=260 ALIGN=LEFT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
<A HREF="/wlasnosc.html">Własność</A> &copy; 2000 Jason Hunter<BR>
Wszystkie prawa zastrzeżone.</TD>
<TD WIDTH=5></FONT></TD>
<TD WIDTH=230 ALIGN=RIGHT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
Kontakt: <A HREF="mailto:webmaster@servlets.com">webmaster@servlets.com</a>
</FONT></TD>
</TR>
</TABLE>

</TD>
</TR>
</TABLE>

</BODY>
</HTML>
Proszę zauważyć, że tytuł strony to obszar zablokowany oraz że występują trzy elementy <SPAN> o nazwach
tytul, tytul2 i opis zawierające makietowy tekst, który zostanie wymieniony. Występuje także element
<DIV> o nazwie zawartosc, oznaczający miejsce, w którym powinna zostać wyświetlona właściwa zawartość
strony. Plik HTML, który zostanie wykorzystany do wygenerowania zawartości aplikacji „Narzędzia” jest
przedstawiony w przykładzie 17.7.
Przykład 17.7.
Plik zawartości aplikacji „Narzędzia”
<!-- widoknarz.html -->
<HTML><HEAD><TITLE>Lista narzędzi</TITLE></HEAD>

<BODY BGCOLOR="#FFFFFF" BACKGROUND="/obrazki/tlo.gif"


LINK="#003333" ALINK="#669999" VLINK="#333333">

<IMG SRC="/obrazki/banner.gif" WIDTH=600 HEIGHT=87 BORDER=0><BR>

<TABLE>
<TR>
<TD WIDTH=125 VALIGN=TOP>
<BR><BR><BR>
<FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#FF0000">
<A HREF="/indeks.html">Strona główna</A><BR>
<A HREF="/hosting.html">Hosting</A><BR>
<A HREF="/mechanizmy.html">Mechanizmy</A><BR>
</FONT>
</TD>

<TD WIDTH=475>

<TABLE CELLPADDING=5><TR><TD WIDTH=600 BGCOLOR="#006699" VALIGN=TOP>


<B><FONT FACE="Arial,Helvetica" SIZE="+2">
<SPAN ID="tytul">Lista narzędzi</SPAN>
</FONT></B>
</TD></TR></TABLE>

<B><FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#003366">


<SPAN ID="tytul2">Lista narzędzi do tworzenia zawartości</SPAN>
</FONT></B><P>
<P>

<FONT FACE="Arial,Helvetica">

<SPAN ID="opis">
Bez narzędzi, ludzie nie są niczym więcej niż zwierzętami. I to dość słabymi.
Poniżej
przedstawiono listę opartych na serwletach narzędzi do tworzenia zawartości,
które
można wykorzystać w celu wzmocnienia się."
</SPAN>

<DIV ID="rekord">
<HR SIZE=2 ALIGN=LEFT>

<FONT FACE="Arial,Helvetica">
<H3>
<SPAN ID="nazwaNarz">Jakaś nazwa narzędzia</SPAN>
<FONT COLOR=#FF0000><B> <SPAN ID="stanNarz">(Nowość!)</SPAN> </B></FONT>
</H3>
<A ID="laczeNarz" HREF="http://narzedzia.com">http://narzedzia.com</A><BR>

<SPAN ID="komentarzNarz">
Tutaj komentarz na temat narzędzia.
</SPAN>
</FONT>
</DIV>

<DIV>
<HR SIZE=2 ALIGN=LEFT>

<FONT FACE="Arial,Helvetica">
<H3>
Inna nazwa narzędzia
<FONT COLOR=#FF0000><B> (Uaktualnienie!) </B></FONT>
</H3>
<A HREF="http://narzedzia.com">http://narzedzia.com</A><BR>

Tu komentarz na temat tego narzędzia.

</FONT>
</DIV>

</FONT>

</TD>
</TR>

<TR>
<TD></TD>
<TD WIDTH=475 ALIGN=CENTER COLSPAN=3>
<HR>
<FONT FACE="Arial,Helvetica">
<A HREF="/indeks.html">Strona Główna</A>&nbsp;&nbsp;
<A HREF="/hosting.html">Hosting</A>&nbsp;&nbsp;
<A HREF="/mechanizmy.html">Mechanizmy</A>&nbsp;&nbsp;<P>
</FONT>

<TABLE WIDTH=100%>
<TR>
<TD WIDTH=260 ALIGN=LEFT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
<A HREF="/wlasnosc.html">Własność</A> &copy; 2000 Jason Hunter<BR>
Wszystkie prawa zastrzeżone.</TD>
<TD WIDTH=5></FONT></TD>
<TD WIDTH=230 ALIGN=RIGHT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
Kontakt: <A HREF="mailto:webmaster@servlets.com">webmaster@servlets.com</a>
</FONT></TD>
</TR>
</TABLE>

</TD>
</TR>
</TABLE>

</BODY>
</HTML>
Powyższy plik wygląda bardzo podobnie do szablonu, ale wartości obszarów zablokowanych zostały
wymienione na realistyczne i wypełniono obszar zawartości pewnymi prototypowymi rekordami. Oto sposób
wykorzystania tego pliku: programowo pobrane zostaną jego kluczowe elementy (tytuły, opis i prototypowy
rekord), po czym zostaną one skopiowane i umieszczone w pliku szablonu w celu utworzenia ostatecznej wersji
strony.
Po co wykorzystywać dwa pliki? Czy nie jest możliwe po prostu bezpośrednie zmodyfikowanie pliku
widoknarz.html? Jest to możliwe, ale szablon wykorzystywany jest po to, by w przypadku przyszłego
uaktualnienia nagłówka, paska bocznego lub stopki konieczne było uaktualnienie jedynie pliku szablon.html i
zmiana ta została uwidoczniona na wszystkich stronach. Innymi słowy, szablon narzuca ogólny wygląd strony.
Plik widoknarz.html jest wykorzystywany jedynie w kluczowych częściach.
Pliki HTML są poddawane kompilacji XMLC przy pomocy poniższych poleceń. Mogą wystąpić ostrzeżenia,
ponieważ HTML w nich zawarty nie jest prawidłowym XML:
% xmlc –class Szablon –keep –methods szablon.html
% xmlc –class WidokNarz –keep –methods widoknarz.html
Następnie można utworzyć serwlet który działa jako klasa manipulacyjna. Jest on przedstawiony w przykładzie
17.8.
Przykład 17.8.
Klasa manipulacyjna aplikacji „Narzędzia”
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import org.w3c.dom.*;
import org.w3c.dom.html.*;

import org.enhydra.xml.io.DOMFormatter;

public class WidokNarzSerwlet extends HttpServlet {

private Narzedzie[] narzedzia;

public void init() throws ServletException {


// Załadowanie danych narzędzi w init w celu uproszczenia
String plikNarz = getInitParameter("plikNarz"); // z web.xml
if (plikNarz == null) {
throw new ServletException(
"Plik danych narzędzi musi być określony jako parametr inicjacji plikNarz
");
}
log("Ładowanie narzędzi z " + plikNarz);
try {
narzedzia = Narzedzie.ladujNarzedzia(plikNarz);
if (narzedzia.length == 0) {
log("Nie odnaleziono narzędzi w " + plikNarz);
}
else {
log(narzedzia.length + " narzędzi znaleziono w " + plikNarz);
}
}
catch (Exception w) {
throw new ServletException(w);
}
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

// Utworzenie drzewa DOM dla pełnego dokumentu


Szablon szablon = new Szablon();

// Utworzenie drzewa DOM przechowującego wewnętrzną zawartość


WidokNarz widoknarz = new widokNarz();

// Pobranie prototypowego rekordu narzędzia


HTMLDivElement rekord = widoknarz.getElementRekord();

// Pobranie odwołania do punktu wstawiania dla listy narzędzi


HTMLDivElement punktWstaw = szablon.getElementZawartosc();
Node przodekWstaw = punktWstaw.getParentNode();

// Ustawienie tytułów i opisu


// Usunięcie danych z pliku widoknarz.html
String tytul = ((Text)widoknarz.getElementTytul().getFirstChild()).getData();
String ttul2 = ((Text)widoknarz.getElementTytul2().getFirstChild()).getData();
String opis = ((Text)widoknarz.getElementOpis().getFirstChild()).getData();
szablon.setTitle(tytul); // tytuł strony
szablon.setTextTytul(tytul); // element oznaczony „tytul”
template.setTextTytul2(tytul2); // element oznaczony „tytul2”
template.setTextOpis(opis); // element oznaczony „opis”

// Pętla nad narzędziami, dodanie nowego rekordu dla każdego


for (int i = 0; i < narzedzia.length; i++) {
Narzedzie narzedzie = narzedzia[i];

widoknarz.setTextNazwaNarz(narzedzie.nazwa);
widoknarz.setTextToolKomentarz(narzedzie.komentarz);

if (narzedzie.czyNowy(45)) {
widoknarz.setTextToolStan(" (Nowość!) ");
}
else if (narzedzie.czyUaktualniony(45)) {
widoknarz.setTextToolStan(" (Uaktualnienie!) ");
}
else {
widoknarz.setTextToolStan("");
}

HTMLAnchorElement lacze = toolview.getElementLaczeNarz();


lacze.setHref(narzedzie.domURL);
Text laczeTekst = widoknarz.createTextNode(narzedzie.domURL);
lacze.replaceChild(laczeTekst, lacze.getLastChild());

// importNode() to DOM Level 2


przodekWstaw.insertBefore(szablon.importNode(rekord, true), null);
}

// Usunięcie obszaru zablokowanego


przodekWstaw.removeChild(punktWstaw);

// Wyświetlenie dokumentu
DOMFormatter formatuj = new DOMFormatter(); // może być poprawiony
formatuj.write(szablon, out);
}
}
Metoda init() serwletu pobiera dane narzędzi z pliku określonego w parametrze inicjacji plikNarz, przy
pomocy klasy Narzedzie z rozdziału 14, „Szkielet Tea”. Ciekawe działanie następuje w metodzie doGet().
Tworzone są egzemplarze dokumentów Szablon i WidokNarz. Następnie odnajdywany jest prototypowy
rekord narzędzia w WidokNarz oraz punkt wstawiania dla rekordów narzędzie w Szablon. Następnie
odczytywane są tytuły i opis z dokumentu WidokNarz, po czym wartości te zostają skopiowane do Szablon.
W pętli for dokonywana jest obsługa zadania tworzenia listy rekordów narzędzi. Dla każdego narzędzia do
prototypowego rekordu przypisane zostają odpowiednie wartości — na początku nazwa i komentarz, po czym
łącze. Po zmodyfikowaniu rekordu zostaje on dodany do szablonu w punkcie wstawiania. W DOM węzły
przypisane są do dokumentu, który je utworzył, tak więc zastosowana zostaje metoda importNode() w celu
udostępnienia rekordu przesuwanego z dokumentu WidokNarz do Szablon. Metoda importNode() wykonuje
głębokie kopiowanie (ponieważ przekazano jej jako drugi argument true), tak więc każda iteracja pętli for dodaje
inna kopię zawartości rekordu. Po zakończeniu pętli for punkt wstawiania zostaje usunięty z szablonu i dokument
jest wyświetlany klientowi.
Podczas uruchamiania powyższego serwletu należy pamiętać, że metoda importNode() jest nowością w
DOM Level 2, tak więc, aby mógł on zostać uruchomiony, serwer musi posiadać ścieżkę klas zawierającą DOM
Level 2 przed wszystkimi klasami DOM Level 1. Archiwum xmlc.jar zawiera DOM Level 2, tak więc można
pomyśleć, że nie ma żadnego problemu, ale niektóre serwery (włączając w to Tomcat 3.2) posiadają w swojej
domyślnej ścieżce implementację DOM Level 1 pomagającą w odczytywaniu plików web.xml. Aby powyższy
serwlet mógł pracować na takich serwerach konieczna jest edycja ścieżki klas serwera w celu upewnienia się, że
plik XMLC xmlc.jar znajduje się przed własnymi bibliotekami XML serwera. (Tomcat 3.2 automatycznie ładuje
pliki JAR w porządku alfabetycznym, tak więc konieczna może okazać się zmiana nazw plików JAR.)
Jednym z problemów związanych z XMLC jest przedstawiony przez powyższy przykład fakt, że warunkowo
dołączane bloki tekstu, których zawartość może się zmieniać, podobnie jak uwagi (Nowość! i Uaktualnienie!)
mogą sprawiać trudności przy dołączaniu do strony, ponieważ szablon może zadeklarować tylko jeden możliwy
blok tekstu do dołączenia. W powyższym przykładzie oczywiste jest, że kod Javy wie więcej niż powinien na
temat tworzenia uwag.
Miłą własnością XMLC jest automatyczne umieszczanie kodów ucieczkowych przed znakami specjalnymi takimi
jak <, > i & podczas wyświetlania ich do klienta, ponieważ wtedy narzędzie dokonujące wyświetlenia może bez
problemu rozpoznać, co jest strukturą, a co zawartością. Ta wiedza o strukturze dokumentu umożliwia również
mechanizmowi formatującemu na wykonywanie zaawansowanych zadań takich jak modyfikacja wszystkich łączy
w celu zakodowania identyfikatorów sesji, chociaż ta konkretna własność nie została jeszcze wprowadzona.
Strona wygenerowana przez powyższy serwlet wygląda identycznie jak strona utworzona przez przykłady Tea,
albo WebMacro.
Rozdział 18. JavaServer Pages

JavaServer Pages, znana powszechnie jako JSP, to technologia utworzona przez Sun Microsystems, blisko
związana z serwletami. JSP była jedną z pierwszych prób utworzenia ogólno dostępnego systemu tworzenia
zawartości opartego na serwletach. Osoby od dawna zajmujące się serwletami mogą pamiętać, że JSP została
przedstawiona po raz pierwszy na wiosnę 1998 — na tyle wcześnie, by pierwsze wydanie niniejszej książki
mogło zawierać krótki samouczek testowej wersji 0.91 JSP. Oczywiście JSP zmieniła się bardzo od tego czasu.
W tym rozdziale opisana zostanie JSP 1.1, opierająca się na Servlet API 2.2.
Podobnie jak w przypadku serwletów, Sun udostępnia specyfikację JSP (utworzoną przez grupę ekspertów
składającą się z niezależnych producentów i osób), po czym inni producenci konkurują w swoich
implementacjach tego standardu. Różnica pomiędzy JSP i innymi technologiami opartymi bezpośrednio na
serwletach polega na tym, że JSP jest specyfikacją, nie produktem i wymaga wsparcia ze strony serwera.
Większość producentów kontenerów serwletów zapewnia tę obsługę, między innymi Tomcat, w którym
mechanizm JSP nosi nazwę Jasper. JSP jest podstawowym składnikiem Java 2, Enterprise Edition (J2EE).
Jednym z głoszonych celów JSP (cytat ze specyfikacji) jest „umożliwienie oddzielenia zawartości dynamicznej i
statycznej”. Innym celem jest „umożliwienie tworzenia stron WWW, które zawierają elementy dynamiczne w
łatwy sposób, ale z maksymalną mocą i elastycznością”. JSP spełnia dobrze oba te zadania. Jednak, zgodnie z
kwestią „maksymalnej mocy” przy tworzeniu JSP, autor strony JSP zawsze posiada całkowitą kontrolę nad
systemem, i w związku z tym można powiedzieć, że JSP umożliwia oddzielenie zawartości od grafiki, ale nie
wymusza jej ani nie narzuca, jak to się dzieje w przypadku jej alternatyw. Jest to zjawisko podobne do tego, że
C++ umożliwia projektowanie obiektowe, ale nie promuje tego doskonałego sposobu tak mocno, jak to się dzieje
w przypadku Javy.
JSP jest również technologią bardzo elastyczną, i istnieje wiele sposobów jej wykorzystania. Jednym z nich jest
„skrótowy” sposób tworzenia serwletów — programista serwletów, dobrze znający Javę, może umieścić kod
Javy bezpośrednio na stronie JSP, zamiast pisać kompletny serwlet — co daje ułatwienia takie jak
wyeliminowanie wywołań wyj.println(), umożliwienie bezpośredniego wskazywania na pliki JSP i
wykorzystanie własności autokompilacji JSP, Jednak poprzez tworzenie strony JSP zamiast serwletu programista
traci pełny kontakt z kodem oraz możliwość kontroli prawdziwego środowiska uruchomieniowego (np.
rozszerzanie com.oreilly.servlet.CacheHttpServlet lub tworzenie wyniku binarnego (obrazka)).
Z tego powodu najlepiej jest pozostawić prawdziwe kodowanie zwykłym serwletom, a strony JSP wykorzystać
przede wszystkim do tworzenia logiki prezentacyjnej.
Nawet podczas zastosowania stron JSP jedynie do logiki prezentacyjnej, można je wykorzystać na wiele
sposobów. Jednym z nich jest użycie komponentów JavaBeans osadzonych na stronie. Innym tworzenie
własnych znaczników wyglądających jak HTML, ale będących tak naprawdę punktami zaczepienia do
wspomagającego kodu Javy. Jeszcze innym jest wysyłanie wszystkich żądań do serwletu, który wykonuje logikę
biznesową, po czym przesyła żądanie do JSP tworzącej stronę przy pomocy mechanizmu
RequestDispatcher. Technika ta jest często nazywana architekturą Model 2, która to nazwa pochodzi od
specyfikacji JSP 0.92. Istnieją również inne mechanizmy noszące dziwne nazwy takie jak Model 1½, Model 2½,
czy Model 2+1. Niemożliwe jest wskazanie najlepszej techniki stosowania JSP.

W niniejszym rozdziale opisane zostaną różne sposoby wykorzystania JSP, począwszy od „skrótu do serwletu” a
skończywszy na własnych znacznikach. Opis będzie krótki, ale powinien dostarczyć podstaw do porównania JSP
z jej alternatywami. Nie zostaną opisane wszystkie opcje architektury, a zamiast tego nacisk zostanie położony na
szczegóły techniczne. Opcje architektury i większa ilość informacji na temat JavaServer Pages jest dostępna na
stronie głównej JSP pod adresem http://java.sun.com/products/jsp („ściąga” składni jest dostępna pod adresem
http://java.sun.com/products/jsp/syntax.pdf) oraz w książce „JavaServer Pages” autorstwa Hansa Bergstena
(O'Reilly).

Wykorzystywanie JavaServer Pages


Najbardziej podstawową funkcją JSP jest umożliwienie bezpośredniego umieszczenia kodu serwletu w
statycznym pliku HTML33. Każdy blok kodu serwletu (nazywany skryptletem) jest ograniczany otwierającym
znacznikiem <% i zamykającym %>. Wykorzystywanie skryptletu ułatwia kilka predefiniowanych zmiennych.
Sześć najpopularniejszych to:
HttpServletRequest request
Żądanie serwletu.
HttpServletResponse response
Odpowiedź serwletu.
javax.servlet.jsp.JspWriter out

Urządzenie wyświetlające, stosowane podobnie jak PrintWriter,


posiadające jednak inną charakterystykę buforowania.
HttpSession session

Sesja użytkownika.
ServletContext application

Aplikacja WWW.
javax.servlet.jsp.PageContext pageContext

Obiekt wykorzystywany przede wszystkim do rozdzielania


implementacji serwera, ale często wykorzystywany
bezpośrednio do współdzielenia zmiennych pomiędzy
stronami JSP i wspomagającymi elementami i znacznikami.
Można dostrzec, że klasy JSP umieszczone są w pakiecie javax.servlet.jsp.
Przykład 18.1 przedstawia prostą stronę JSP wyświetlającą spersonalizowane „Witaj” przy pomocy
predefiniowanych zmiennych request i out. Jeżeli posiada się serwer obsługujący JavaServer Pages i pragnie
się przetestować tę stronę, należy umieścić plik w podkatalogu katalogu macierzystego dokumentów serwera i
zapamiętać ją z rozszerzeniem .jsp. Zakładając, że strona została zapamiętana jako witaj1.jsp, dostęp do niej
można uzyskać pod URL-em http://serwer:port/witaj1.jsp lub, jeżeli plik zostanie umieszczony w ścieżce
kontekstowej jsp, URL będzie wynosił http://serwer:port/jsp/witaj1.jsp.
Przykład 18.1.
Powitanie przy pomocy JSP
<HTML>
<HEAD><TITLE>Witaj</TITLE></HEAD>
<BODY>
<H1>
<%
if (request.getParameter("nazwa") == null) {
out.println("Witaj świecie");
}
else {
out.println("Witaj, " + request.getParameter("nazwa"));
}
%>
</H1>

33
Przed rozpoczęciem opisu warto zauważyć, że umieszczanie kodu Javy na stronie JSP jest uważane za działanie w złym
stylu. Uważane za lepsze zaawansowane zastosowania JSP zostaną opisane w dalszej części rozdziału.
</BODY></HTML>
Przykładowy wynik uruchomienia powyższego programu przedstawiony jest na rysunku 18.1.
Rysunek 18.1.
Powitanie przy pomocy
JavaServer Pages

Zasady działania
Jak działa JSP? Poza obszarem widoczności serwer automatycznie tworzy, kompiluje, ładuje i uruchamia
specjalny serwlet tworzący zawartość strony, jak przedstawiono na rysunku 18.2. Można myśleć o tym
specjalnym serwlecie jak o serwlecie roboczym, działającym w tle. Statyczne części strony HTML są tworzone
przez serwlet roboczy przy pomocy ekwiwalentów wywołań wyj.println(), podczas gdy części dynamiczne
są dołączane bezpośrednio. Na przykład, serwlet przedstawiony w przykładzie 18.2 mógłby być serwletem
roboczym dla witaj1.jsp działającym na serwerze Tomcat34.

Rysunek 18.2.
Generowanie stron JavaServer Pages

Przykład 18.2.
Generowany automatycznie serwlet roboczy dla witaj1.jsp
import javax.servlet.*;

34
Osoby zainteresowane zobaczeniem prawdziwego kodu źródłowego serwletu dla strony JSP, w większości przypadków
mogą odnaleźć go w tymczasowym katalogu określonym w atrybucie kontekstu javax.servlet.context.tempdir
(proszę zobaczyć rozdział 4, „Pobieranie informacji”. Kiedy odnajdzie się prawdziwe źródło serwletu można zobaczyć, że
jest ono o wiele bardziej skomplikowane niż to przedstawione w tym miejscu.
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;
import java.beans.*;
import java.io.*;
import java.util.*;
import org.apache.jasper.runtime.*;
import org.apache.jasper.*;

public class _0002fwitaj_00031_0002ejspwitaj1_jsp_0 extends HttpJspBase {

static {
}
public _0002fwitaj_00031_0002ejspwitaj1_jsp_0( ) {
}

private static boolean _jspx_inited = false;

public final void _jspx_init() throws JasperException {


}

public void _jspService(HttpServletRequest request, HttpServletResponse


response)
throws IOException, ServletException {

JspFactory _jspxFactory = null;


PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
String _value = null;
try {

if (_jspx_inited == false) {
_jspx_init();
_jspx_inited = true;
}
_jspxFactory = JspFactory.getDefaultFactory();
response.setContentType("text/html;charset=ISO-8859-1");
pageContext = _jspxFactory.getPageContext(this, request, response,
"", true, 8192, true);

application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();

// HTML // begin [file="C:\\ witaj1.jsp";from=(0,0);to=(4,0)]


out.write
("<HTML>\r\n<HEAD><TITLE>Witaj</TITLE></HEAD>\r\n<BODY>\r\n<H1>\r\n");
// end
// begin [file="C:\\ witaj1.jsp";from=(4,2);to=(11,0)]

if (request.getParameter("nazwa") == null) {
out.println("Witaj œwiecie");
}
else {
out.println("Witaj, " + request.getParameter("nazwa"));
}
// end
// HTML // begin [file="C:\\ witaj1.jsp";from=(11,2);to=(14,0)]
out.write("\r\n</H1>\r\n</BODY></HTML>\r\n");
// end

} catch (Exception ex) {


if (out != null && out.getBufferSize() != 0)
out.clearBuffer();
if (pageContext != null) pageContext.handlePageException(ex);
} finally {
if (out != null) out.flush();
if (_jspxFactory != null) _jspxFactory.releasePageContext
(pageContext);
}
}
}
Kiedy wyświetla się stronę JSP po raz pierwszy można zauważyć, że jej wywołanie zabiera pewną ilość czasu.
Jest to czas potrzebny serwerowi na stworzenie i skompilowanie serwletu roboczego, co zostało nazwane karą
pierwszej osoby. Następne żądania powinny następować ze zwykłą szybkością, ponieważ serwlet może ponownie
wykorzystać serwlet w pamięci. Jedynym wyjątkiem jest zmiana pliku .jsp, co serwer rozpoznaje podczas
wykonywania następnego żądania tej strony, po czym rekompiluje serwlet działający w tle. Jeżeli kiedykolwiek
wystąpi błąd kompilacji, można spodziewać się, że serwer zakomunikuje go w pewien sposób, zazwyczaj
poprzez stronę zwracaną klientowi i/lub dziennik zdarzeń serwera.

Wyrażenia i deklaracje
Oprócz skryptletów, JavaServer Pages pozwala na umieszczanie kodu na stronie przy pomocy wyrażeń i
deklaracji. Wyrażenie JSP rozpoczyna się znacznikiem <%=, a kończy %>. Każde wyrażenie Javy pomiędzy tymi
dwoma znacznikami jest oceniane, jego wynik konwertowany do łańcucha String, a tekst dołączany
bezpośrednio do strony. Technika ta eliminuje nieporządek wywołania wyj.println(). Na przykład, <%=
buu => dołącza wartość zmiennej buu.
Deklaracja rozpoczyna się od <%!, a kończy na %>. Pomiędzy tymi znacznikami można umieścić dowolny kod
serwletu, który powinien pozostać poza metodą usługową serwletu. Zadeklarować można zmienne statyczne lub
egzemplarzowe, albo też zdefiniować nowe metody. Przykład 18-3 przedstawia stronę JSP, która wykorzystuje
deklarację do zdefiniowania metody pobierzNazwa() oraz wyrażenie ją wyświetlające. Komentarz na
początku pliku pokazuje, że komentarze JSP oznaczane są znacznikami <%-- i --%>.
Przykład 18.3.
Powitanie przy pomocy deklaracji JSP
<%-- witaj2.jsp --%>
<HTML>
<HEAD><TITLE>Witaj</TITLE></HEAD>
<BODY>
<H1>
Witaj, <%= pobierzNazwa(request) %>
</H1>
</BODY>
</HTML>

<%!
private static final String DOMYSLNA_NAZWA = "świecie";

private String pobierzNazwa(HttpServletRequest zad) {


String nazwa = zad.getParameter("nazwa");
if (nazwa == null)
return DOMYSLNA_NAZWA;
else
return nazwa;
}
%>
Powyższy kod JSP zachowuje się identycznie jak witaj1.jsp.
Specjalne metody jspInit() i jspDestroy() mogą zostać zaimplementowane wewnątrz deklaracji. Są
one wywoływane przez metody init() i destroy() serwletu działającego w tle i dają stronie JSP
możliwość zadeklarowania kodu, który powinien zostać wykonany podczas inicjacji i niszczenia. Strona JSP nie
może omijać standardowych metod init() i destroy(), ponieważ metody te są ostatecznie deklarowane
przez serwlet roboczy.

Instrukcje
Instrukcja JSP pozwala stronie JSP na kontrolowanie pewnych aspektów jej serwletu roboczego. Instrukcje mogą
zostać wykorzystane do nakazania serwletowi roboczemu ustawienia jego typu zawartości, importowania
pakietu, kontroli buforowania wyświetlania i zarządzania sesją, rozszerzania różnych superklas oraz specjalnej
obsługi błędów. Instrukcja może nawet określać wykorzystanie języka skryptowego innego niż Java.
Składnia instrukcji musi zawierać jej nazwę oraz parę nazwa-wartość atrybutu, a całość musi być zwarta
pomiędzy znacznikami <%@ i %>. Cudzysłowy zamykające wartość atrybutu są obowiązkowe:
<%@ nazwaInstrukcji nazwaAtrybutu="wartoscAtrybutu" %>
Instrukcja page pozwala JSP na kontrolowanie generowanego serwletu poprzez ustawienie specjalnych
atrybutów wymienionych poniżej:
contentType
Określa typ zawartości generowanej strony. Na przykład:
<%@ page contentType="text/plain" %>

Domyślnym typem zawartości jest text/plain; charset=8859_1.


import
Określa listę klas i pakietów, które tworzony serwlet powinien importować. Większa ilość klas może zostać
określona w formie listy rozdzielonej przecinkami. Na przykład:
<%@ page import="java.io.*,java.util.Hashtable" %>
Domyślna, ukryta i zawsze dostępna lista klas to
java.lang.*,javax.servlet.*,javax.servlet.http.*,javax.servlet.jsp.*.
buffer
Określa minimalną wymaganą wielkość bufora odpowiedzi w kilobajtach, podobnie do metody serwletu
setBufferSize(). Wartość powinna zostać zapisana w formie ##kb. Specjalna wartość none
wskazuje, że zawartość powinna zostać przekazana bezpośrednio do odpowiedniego PrintWriter w
ServletResponse (który może, ale nie musi przekazać zawartość klientowi). Na przykład:
<%@ page buffer="12kb" %>
Domyślna wartość wynosi 8kb.
autoFlush
Określa, czy bufor powinien być opróżniany, kiedy jest pełny, czy zamiast tego powinien zostać wywołany
wyjątek IOException. Wartość true wskazuje na opróżnianie, false na wyjątek. Na przykład:
<%@ page autoFlush="true" %>
Wartość domyślna wynosi true.
session
Wskazuje, że strona pragnie posiadać dostęp do sesji użytkownika. Wartość true umieszcza w zakresie
zmienną session i może ustawić cookie klienta zarządzające sesją. Na przykład:
<%@ page session="false" %>
Wartość domyślna wynosi true.
errorPage
Określa stronę, którą należy wyświetlić, kiedy na stronie wystąpi Throwable, które nie zostanie
przechwycone przed dotarciem do serwera. Okazuje się to przydatne, ponieważ nie jest trudno wykonać
operacje try i catch na blokach podczas tworzenia stron JSP. Na przykład:
<%@ page errorPage="blad.jsp" %>
Domyślne zachowanie zależy od implementacji. Ścieżka jest względna wobec kontekstu, tak więc nie trzeba
się przejmować dodawaniem właściwej ścieżki kontekstu. Celem może być JSP, ale nie jest to konieczne.
Jeżeli cel jest serwletem, może on pobrać Throwable jako atrybut kontekstu
javax.servlet.jsp.jspException.
isErrorPage
Wskazuje, że strona została zaprojektowana jako cel errorPage. Jeżeli wartość wynosi true, strona
może uzyskać dostęp do ukrytej zmiennej o nazwie exception w celu odczytania Throwable.
language
Określa język skryptowy wykorzystany w częściach strony zawierających kod. Zastosowany język musi na
tyle dobrze współpracować z Javą, aby uwidocznić potrzebne obiekty Javy środowisku skryptu. Na
przykład:
<%@ page language="javascript" %>
Domyślna wartość wynosi java, jest to jedyny język rekomendowany przez specyfikację.

Wykorzystanie instrukcji
Przykład 18.4 przedstawia stronę JSP noszącą nazwę bladRob.jsp, która wykorzystuje kilka instrukcji. Po
pierwsze ustawia atrybut session dyrektywy page na false, ponieważ strona nie wykorzystuje obiektu sesji
i w związku z tym nie istnieje potrzeba, by serwer tworzył sesję. Następnie ustawia atrybut errorPage na /
bladBierz.jsp, tak więc jeżeli na stronie wydarzy się niewyłapany wyjątek, strona bladBierz.jsp będzie
obsługiwać wyświetlanie komunikatu o błędzie. Główna część strony jest prosta. Wyzwala ona wyjątek w celu
wywołania zachowania errorPage. (Powód wykorzystania sprawdzenia if zostanie opisany później.)
Przykład 18.4.
Zły chłopiec
<%-- bladRob.jsp --%>

<%@ page session="false" %> <%-- Wyłączenie niepotrzebnych cookies --%


>
<%@ page errorPage="/bladBierz.jsp" %> <%-- Ogólna strona obsługująca błędy --%>

<%-- A teraz wywołanie błędu --%>


<%
if (System.currentTimeMillis() > 0) {
throw new Exception("uups");
}
%>
Strona bladBierz.jsp jest przedstawiona w przykładzie 18.5. Ustawia ona instrukcję strony isErrorPage na
true w celu wskazania, że ta strona obsługuje błędy, oraz wykorzystuje atrybut import w celu zaimportowania
klas com.oreilly.servlet. JSP wykorzystuje skryptlet do ustawienia kodu stanu odpowiedzi na 500, po
czym tworzy zawartość strony przy pomocy nazwy klasy wyjątku, jego ścieżkę oraz wiadomość sugerującą
kontakt z administratorem WWW w celu zgłoszenia problemu.
Przykład 18.5.
A oto i problem...
<%-- bladBierz.jsp --%>

<%@ page isErrorPage="true" %>


<%@ page import="com.oreilly.servlet.*" %>

<% response.setStatus(500); %>

<HTML>
<HEAD><TITLE>Błąd: <%= exception.getClass().getName() %></TITLE></HEAD>
<BODY>

<H1>
<%= exception.getClass().getName() %>
</H1>

Wystąpił błąd podczas tworzenia oryginalnego WWW

<PRE>
<%= ServletUtils.getStackTraceAsString(exception) %>
</PRE>

<% String name = request.getServerName(); %>


Proszę skontaktować się z <A HREF="mailto:webmaster@<%= name %>">webmaster@<%=
name %></A> w celu zgłoszenia błędu.

</BODY>
</HTML>
Podczas każdego żądania bladRob.jsp wywoływany jest wyjątek, kontrola jest przekazywana do bladBierz.jsp,
który wyświetla elegancką stronę informującą o błędzie. Proszę zobaczyć rysunek 18.3.
Rysunek 18.3.
Własna strona błędów

Tak więc dlaczego w bladRob.jsp występuje wywołanie System.currentTime Millis()? Bierze się to z
faktu, że strony JSP muszą przechowywać wszystkie puste miejsca z tekstu dokumentu. Pozwala to na
wykorzystanie JSP do tworzenia nie tylko HTML, ale także XML, w którym puste miejsca mogą być niezwykle
ważne. Niestety zachowanie wszystkich pustych miejsc oznacza, że prosty skryptlet:
<% throw new Exception("uups"); %>
musi wygenerować kod w celu wyświetlenia nowej linii po wywołaniu błędu — kod, który nigdy nie zostanie
osiągnięty! Kompilacja tego skryptu na Tomcat-cie, generuje następujący komunikat o błędzie.
org.apache.jasper.JasperException: unable to compile class for
JSPC: \engines\jakarta-tomcat\work\localhost_8080%2Fjsp\
-0001fbladRob_0002ejspbladRob-jsp-3.java:75:
Statement not reached.
out.write("\r\n");
^
Poprzez dodanie sprawdzenia, czy System.currentTimeMillis() jest większe niż zero, kompilator
przyjmuje, że istnieje możliwość, że wyjątek nie zostanie wywołany i w związku z tym nie wystąpi błąd
kompilacji. Podobne problemy mogą pojawiać się w przypadku wyrażeń return i throw wewnątrz
skryptletów.
Tak naprawdę istnieje wiele sztuczek powiązanych z wykorzystaniem skryptletów w JSP. Jeżeli przypadkowo
napisze się skryptlet zamiast wyrażenia (poprzez pominięcie znaku równości), zadeklaruje statyczną zmienną
wewnątrz skryptletu (gdzie zmienne te nie są dozwolone), zapomni przecinka (nie są konieczne w wyrażeniach,
ale konieczne w skryptletach) lub napisze się cokolwiek nie będącego idealnym kodem Javy, przypuszczalnie
otrzyma się dziwny komunikat o błędzie, ponieważ kompilator działa na wygenerowanym kodzie Javy, nie na
pliku JSP. Aby zademonstrować problem, proszę wyobrazić sobie, że w pliku bladBierz.jsp <%= nazwa %>
zostało zastąpione przez <% nazwa %>. Tomcat wygeneruje wtedy następujący błąd:
org.apache.jasper.JasperException: unable to compile class for
JSPC: \engines\jakarta-tomcat\work\localhost_8080%2Fjsp\
-0001fbladRob_0002ejspbladRob-jsp-3.java:91:
Class name not found.
name
^
Usunięcie błędu podobnego do przedstawionego powyżej często wymaga od programisty przyjrzenia się
generowanemu kodowi w celu rekonstrukcji powodu błędu.

Unikanie kodu Javy na stronach JSP


Skryptlet, wyrażenia i deklaracje umożliwiają umieszczanie kodu Javy na stronie JSP. Są to narzędzia o dużej
mocy, jednak ich wykorzystywanie nie jest polecane. Nawet w rękach doświadczonego programisty
umieszczanie kodu na stronie jest programowaniem „nieświeżym”. Tworzy się kod Javy, ale nie bezpośrednio.
W przypadku wystąpienia błędu ciężko jest go zdiagnozować z powodu dodatkowej niejasności.
Sytuacja staje się jeszcze gorsza, kiedy mechanizm ten wykorzystuje nowicjusz. Osoby nie będące programistami
muszą nauczyć się Javy chociaż w pewnym stopniu, a Java nie jest zaprojektowana jako język skryptowy, nie jest
też przeznaczona dla takich osób. Język taki jak JavaScipt — prostszy mniej ścisły i lepiej znany przez
projektantów WWW — mógłby być lepszym rozwiązaniem, ale obsługa JavaScriptu nie jest czynnością
standardową i w związku z tym jej implementacja nie jest popularna.
Ostatnią kwestią jest projekt. Jeżeli projektanci i programiści pracują nad tym samym plikiem, tworzy się wąskie
gardło i dodatkowa możliwość wystąpienia błędu. Zawartość i grafika powinny być od siebie oddzielone. Aby to
umożliwić i usunąć kod Javy ze strony, Java pozwala na zastosowanie JavaBeans i bibliotek własnych
znaczników. Poniżej opisane zostaną JavaBeans.

JSP i JavaBeans
Jedną z najbardziej interesujących i potężnych metod wykorzystania JavaServer Pages jest współpraca z
komponentami JavaBeans. JavaBeans to klasy Javy zaprojektowane do wielokrotnego wykorzystania, a nazwy
ich metod i zmiennych tworzone są według pewnej konwencji, która daje im dodatkowe możliwości. Mogą one
być osadzane bezpośrednio na stronie Javy przy pomocy znacznika <jsp:useBean>, czyli działania
nazywanego przez JSP akcją. Komponent JavaBean może wykonywać dobrze zdefiniowane zadanie (zapytanie
bazy danych, łączenie się z serwerem poczty, utrzymywanie informacji na temat klienta itp.), którego wynik jest
dostępny stronie JSP poprzez proste metody dostępu. Większa ilość informacji na temat JavaBeans znajduje się
pod adresem http://java.sun.com/bean i w książce „Developing JavaBeans” autorstwa Roberta Englandera
(O'Reilly).
Różnica pomiędzy komponentem JavaBeans osadzonym na stronie JSP i każdą inną dodatkową klasą
wykorzystywaną przez wygenerowany serwlet jest taka, że serwer WWW może traktować JavaBeans osadzone
na stronie w specjalny sposób. Na przykład, serwer może automatycznie skonfigurować komponent (zwłaszcza
zmienne egzemplarza) przy pomocy wartości parametrów w żądaniu klienta. Innymi słowy, jeżeli żądanie
zawiera parametr nazwa, a serwer odkryje poprzez introspekcję (technikę, w której metody i zmienne klasy Javy
mogą zostać określone programowo w czasie uruchomienia), że komponent posiada parametr nazwa i metodę
ustawNazwe(String nazwa), serwer może automatycznie wywołać pobierzNazwe() z wartością
parametru nazwa. Nie jest konieczne wykorzystanie metody getParameter().
Serwer może także automatycznie zarządzać zakresem komponentu. Komponent może zostać przypisany do
konkretnej strony (gdzie jest wykorzystany raz i zniszczony), konkretnego żądania (podobne do zakresu strony
poza tym, że komponent może przetrwać wewnętrzne wywołania dołączenia i przekazania), sesji klienta (w
którym komponent jest automatycznie udostępniany, kiedy tan sam klient połączy się ponownie) lub aplikacji (w
którym to zakresie ten sam komponent jest udostępniany całej aplikacji WWW).

Osadzanie komponentu Bean


Komponenty są osadzane na stronie JSP przy pomocy akcji <jsp:useBean>. Posiada ona następującą
składnię (zależną od wielkości liter, cudzysłowy obowiązkowe):
<jsp:useBean> id="nazwa" scope="page|request|session|application"
class="nazwaKlasy" type "nazwaTypu">
</jsp:useBean>
Ustawione mogą być następujące atrybuty akcji <jsp:useBean>:
id

Określa nazwę komponentu. Jest to klucz pod którym komponent


jest przechowywany, jeżeli jego zakres rozciąga się
poza stronę. Jeżeli egzemplarz komponentu zapamiętany
pod podaną nazwą już istnieje w zakresie strony,
egzemplarz ten zostaje wykorzystany na stronie. W
pozostałych przypadkach tworzony jest nowy komponent.
Na przykład:
id="preferencjeUzyt"
scope
Określa zakres widoczności komponentu. Wartość musi wynosi page, request, session lub
application. Jeżeli page, zmienna jest tworzona jako zmienna egzemplarza. Jeżeli request,
zmienna jest przechowywana jako atrybut żądania; jeżeli session, komponent przechowywany jest w
sesji użytkownika, a jeżeli application, w kontekście serwletu. Na przykład:
scope="session"
Domyślną wartością jest page.
class

Określa nazwę klasy komponentu. Jest ona wykorzystywana przy


pierwszym tworzeniu komponentu. Nazwa klasy musi być
podana w pełnej formie. Na przykład:
class="com.firma.sledzenie.PreferencjeUzytImpl"
Atrybut class nie jest potrzebny, jeżeli komponent istnieje już w aktualnym zakresie, ale ciągle musi
wystąpić atrybut type pozwalający na stworzeni obiektu odpowiedniego typu. Jeżeli wystąpi problem ze
stworzeniem danej klasy, strona JSP zgłasza wyjątek InstatiationException.
type

Określa typ komponentu, który powinien zostać pobrany z


systemu, wykorzystywany do utworzenia obiektu po
pobraniu z żądania, sesji lub kontekstu. Wartość typu
powinna zostać podana w pełnej formie superklasy lub
interfejsu dla klasy aktualnej. Na przykład:
type="com.firma.sledzenie.PreferencjeUzyt"
Jeżeli typ nie zostanie określony, domyślna wartość jest równa atrybutowi class. Jeżeli typ nie pasuje do
prawdziwego typu obiektu, strona JSP zgłasza wyjątek ClassCastException.
beanName
Atrybut class może zostać zastąpiony atrybutem beanName. Różnica między nimi jest taka, że
beanName wykorzystuje Beans.instatiate() do utworzenia egzemplarza komponentu, który
poszukuje zserializowanej wersji komponentu (pliku .ser) przed utworzeniem egzemplarza od zera.
Pozwala to na wykorzystanie komponentów prekonfigurowanych. Proszę przejrzeć dokumentację Javadoc
dla Beans.instatiate() w celu uzyskania większej ilości informacji. Na przykład:
BeanName="com.firma.sledzenie.PreferencjeUzytImpl"

Główna część elementu <jsp:useBean> — wszystkie informacje


pomiędzy znacznikiem otwierającym <jsp:useBean> a
zamykającym </jsp:useBean> — jest interpretowane po
utworzeniu komponentu. Jeżeli komponent nie zostanie
utworzony (ponieważ w danym zakresie odnaleziono
istniejący egzemplarz), wtedy część ta jest ignorowana.
Na przykład:
<jsp:useBean id="preferencje" class=com.firma.sledzenie.PreferencjeUzytImpl">
Jeżeli ten napis jest widoczny, to utworzono nowy komponent!
<jsp:useBean>
Jeżeli nie jest konieczne umieszczenie głównej części, może zostać wykorzystany skrót XML dla pustego
znacznika />:
<jsp:useBean id="preferencje" class=com.firma.sledzenie.PreferencjeUzytImpl" />
Kontrola parametrów komponentu
Akcja <jsp:setProperty> umożliwia parametrom żądania automatyczne (poprzez introspekcję) ustawianie
właściwości komponentów osadzonych na stronie. Daje to komponentom automatyczny dostęp do parametrów
żądania bez konieczności wywoływania getParameter(). Własność ta może zostać wykorzystana na kilka
sposobów. Po pierwsze:
<jsp:setProperty name="nazwaKomponentu" property="*" />
Powyższa linia określa, że dowolny parametr żądania o tej samej nazwie i typie, co własność komponentu,
powinien zostać wykorzystany do ustawienia tej własności komponentu. Na przykład, jeżeli element posiada
metodę ustawWielkosc(int wielkosc) oraz żądanie posiada parametr wielkosc o wartości 12, serwer
automatycznie wywoła komponent.ustawWielkosc(12) na początku obsługi żądania. Jeżeli wartość
parametru nie może zostać przekonwertowana na właściwy typ, parametr jest ignorowany. (Proszę zauważyć, że
pusta wartość parametru łańcuchowego jest traktowana tak, jakby parametr nie istniał, i pusta wartość łańcucha
nie zostanie przypisana własności String).
Proszę zauważyć, że akcja wykorzystuje skrót XML dla pustego znacznika. Jest to bardzo ważne. Specyfikacja
JSP wymaga od wszystkich akcji JSP by posiadały one właściwą formę XML, nawet kiedy są umieszczone w
plikach HTML. Poniżej przedstawiono drugi sposób wykorzystania <jsp:setProperty>:
<jsp:setProperty name="nazwaKomponentu" property="nazwaWlasnosci" />
Powyższa linia nakazuje ustawienie danej własności, jeżeli istnieje parametr żądania o tej samej nazwie i typie.
Na przykład, aby ustawić własność wielkosc, a nie żadną inną, należy wykorzystać następującą akcję:
<jsp:setProperty name="preferencje" property="wielkosc" />
Oto trzeci sposób wykorzystania <jsp:setProperty>:
<jsp:setProperty name="nazwaKomponentu" property="nazwaWlasnosci"
param="nazwaParam" />
Powyższa linia nakazuje ustawienie danej własności, jeżeli występuje parametr żądania o danej nazwie i tym
samym typie. Pozwala to parametrowi o pewnej nazwie na ustawianie własności o nazwie innej. Na przykład,
aby ustawić własność wielkosc przy pomocy parametru wielkoscCzcionki:
<jsp:setProperty name="preferencje" property="wielkosc" param="wielkoscCzcionki" /
>
Poniżej przedstawiono ostatni sposób wykorzystania:
<jsp:setProperty name="nazwaKomponentu" property="nazwaWlasnosci" value="stala" />
Powyższa linia kodu nakazuje nadanie danej własności danej wartości, która zostanie przekonwertowana do
właściwego typu, jeżeli okaże się to konieczne. Na przykład, aby ustawić domyślną wielkość na 18 z
pominięciem parametru:
<jsp:setProperty name="preferencje" property="wielkosc" value="18" />
<jsp:setProperty name="preferencje" property="wielkosc" param="wielkoscCzcionki" /
>
Zaawansowani użytkownicy powinni wiedzieć, że atrybut value nie musi być stałą; może on zostać określony
poprzez wyrażenie, przy pomocy mechanizmu nazywanego wyrażeniem atrybutu czasu uruchomienia. Na
przykład, aby upewnić się, że wielkość nigdy nie spadnie poniżej 6:
<jsp:setProperty name="preferencje" property="wielkosc" param="wielkoscCzcionki"/>
<jsp:setProperty name="preferencje" property="wielkosc"
value="<%= Mat.maks(6, preferencje.pobierzWielkosc()) %>" />
Natomiast akcja <jsp:getProperty> dostarcza mechanizmu służącego do odczytywania wartości własności
bez konieczności wykorzystywania kodu Javy na stronie. Jego zastosowanie jest podobne do
<jsp:setProperty>:
<jsp:getProperty name="nazwaKomponentu" property="nazwaWlasnosci" />
Powyższa linia nakazuje dołączenie w jej miejscu wartości danej własności do danego komponentu. Jest ona
dłuższa niż wyrażenie, ale pozwala na uniknięcie umieszczania kodu Javy na stronie. Wybór pomiędzy
<jsp:getProperty> a wyrażeniem jest kwestią osobistego gustu. Proszę zauważyć, że
<jsp:getProperty>, bezpośrednio osadza wartość własności, tak więc jeżeli zawiera ona znaki traktowane
przez HTML jako specjalne, może to powodować problemy. Projekt Apache Struts pozwala na rozwiązanie tego
problemu przy pomocy własnego znacznika <property>, co zostanie opisane w dalszej części tego rozdziału.

Powitania przy pomocy komponentu


Przykład 18.6 przedstawia zastosowanie komponentu JavaBeans na stronie JSP. Strona ta wyświetla „Witaj”
przy pomocy WitajKomp.
Przykład 18.6.
Powitanie przy pomocy JavaBean
<%-- witaj3.jsp --%>

<%@ page import="WitajKomp" %>

<jsp:useBean id="witaj" class="WitajKomp">


<jsp:setProperty name="witaj" property="*" />
</jsp:useBean>

<HTML>
<HEAD><TITLE>Witaj</TITLE></HEAD>
<BODY>
<H1>
Witaj, <jsp:getProperty name="witaj" property="nazwa" />
</H1>
</BODY>
</HTML>
Jak można dostrzec, wykorzystanie komponentu JavaBeans na stronie JavaServer Pages może zredukować ilość
kodu umieszczoną na stronie. Klasa WitajKomp zawiera logikę określającą nazwę użytkownika, a strona JSP
działa jedynie jako szablon.
Kod WitajKomp jest przedstawiony w przykładzie 18.7. Plik jego klasy powinien zostać umieszczony w
standardowym katalogu klas wspierających (WEB-INF/classes).
Przykład 18.7.
Klasa WitajKomp
public class WitajKomp {
private String nazwa = "świecie";

public void ustawNazwe(String nazwa) {


this.nazwa = nazwa;
}

public String pobierzNazwe() {


return nazwa;
}
}
Jest to chyba najprostszy możliwy komponent. Posiada pojedynczą własność nazwa ustawianą przy pomocy
ustawNazwe(), a odczytywaną przy pomocy pobierzNazwe(). Domyślną wartością nazwa jest
świecie, ale kiedy nadchodzi żądanie zawierające parametr nazwa, własność jest automatycznie ustawiana
przez serwer przy pomocy wywołania ustawNazwe(). W celu przetestowana mechanizmu, proszę obejrzeć
stronę http://serwer:port/witaj3.jsp. Powinien pojawić się wynik podobny do przedstawionego na rysunku 18.4.
Rysunek 18.4.
Powitanie przy pomocy
JavaServer Pages we
współpracy z
komponentem JavaBeans

Istnieje jedna kwestia, na którą należy zwrócić uwagę: na niektórych serwerach (włączając w to Tomcat 3.2),
jeżeli istnieje komponent o zakresie session lub application, a implementacja klasy komponentu zostanie
zmieniona, w późniejszych żądaniach może pojawić się ClassCastException. Wyjątek ten pojawia się,
ponieważ kod generowanego serwletu musi wywołać egzemplarz komponentu, który pobierany jest z sesji lub
aplikacji, a typ starego komponentu przechowywany w sesji lub aplikacji nie pasuje do spodziewanego typu
nowego komponentu. Najprostszym rozwiązaniem jest ponowne uruchomienie serwera.

Dołączenia i przekazania
Poprzez połączenie instrukcji i akcji, JSP umożliwia obsługę dołączeń i przekazań. Istnieją dwa typy dołączeń i
jedno przekazań. Pierwszym dołączeniem jest instrukcja include:
<%@ include file="SciezkaDoPliku" %>
Powyższe dołączenie następuje w czasie tłumaczenia, co oznacza, że następuje podczas tworzenia serwletu
działającego w tle. Cała zawartość zewnętrznego pliku jest dołączana tak, jak gdyby była bezpośrednio
umieszczona na stronie JSP. Programiści C mogą dostrzec jej bliskie podobieństwo do #include. Ścieżka do
pliku jest zakotwiczona w katalogu macierzystym kontekstu (tak więc /indeks.jsp odnosi się do index.jsp
aktualnej aplikacji WWW), a ścieżka nie może sięgać poza katalog macierzysty kontekstu (proszę nie próbować
../../../innyKontekst). Zawartość wewnątrz pliku jest zazwyczaj fragmentem strony, nie całą stroną, tak więc
można pomyśleć o zastosowaniu rozszerzenia pliku .inc lub .jin w celu wskazania tego faktu. Poniższa strona
JSP wykorzystuje kilka instrukcji dołączenia w celu utworzenia strony ze zbioru elementów (elementy nie są
pokazane):
<%@ include file="/naglowek.html" %>
<%@ include file="obliczNazwe.inc" %>
Witaj, <%= nazwa %>
<%@ include file="/stopka.html" %>
Skąd pochodzi zmienna nazwa? Jest ona tworzona przez plik obliczNazwe.inc. Ponieważ zawartość
dołączanego pliku jest dołączana bezpośrednio, nie występuje oddzielenie zakresu zmiennej.
Drugim typem dołączenia jest akcja <jsp:include>:
<jsp:include page="sciezkaDoZasobuDynamicznego" flush="true" />
Powyższe dołączenie następuje w czasie żądania. Nie jest to surowe dołączenie takie, jak w instrukcji include;
zamiast tego serwer uruchamia określony zasób dynamiczny i dołącza jego wynik do zawartości wysyłanej do
klienta. Niewidocznie akcja <jsp:include> generuje kod, który wywołuje metodę include()
RequestDispatcher, tak więc do <jsp:include> stosują się te same zasady wykonania, co do include()
— dołączana strona musi być dynamiczna, nie może ustawiać kodu stanu, nie może ustawiać żadnych
nagłówków, a ścieżka strony jest zakotwiczona w katalogu macierzystym kontekstu35. Wymagany jest atrybut
flush, który w JSP 1.1 musi być ustawiony na true. Wskazuje to na konieczność opróżnienia bufora
odpowiedzi przed dołączeniem. Zważywszy na możliwości Servlet API 2.2, wartość false nie może być
obsługiwana; spodziewane jest, że JSP 1.2 utworzone na podstawie Servlet API 2.3 będzie zezwalać na wartość
false.
Jako przykład, poniższa akcja <jsp:include> dołącza zawartość generowaną przez wywołanie strony
powitanie.jsp. Strona powitanie.jsp może wyglądać podobnie do witaj1.jsp poza tym, że pożądane może być
usunięcie otaczającego kodu HTML sprawiającego, że witaj1.jsp jest kompletną stroną HTML.
<jsp:include page="/powitanie.jsp" />

35
Istnieje jedna drobna różnica pomiędzy metodą include() i akcją <jsp:include> — metoda include() wymaga,
żeby ścieżka do strony rozpoczynała się od /. Akcja <jsp:include> dodatkowo zezwala na względne ścieżki takie, jak
../indeks.jsp, tak długo, jak ścieżka nie wychodzi poza aktualny kontekst.
Akcja <jsp:include> może opcjonalnie zawierać w swojej głównej części dowolną ilość znaczników
<jsp:param>. Znaczniki te dodają parametry do dołączanego żądania. Na przykład, poniższa akcja
przekazuje powitaniu domyślną nazwę:
<jsp:include page="/powitanie.jsp">
<jsp:param name="domyslnaNazwa" value="Nowy użytkownik" />
</jsp:include>
JSP może także przekazywać żądanie przy pomocy akcji <jsp:forward>:
<jsp:forward page="sciezkaDoZasobuDynamicznego" />
Przekazanie powoduje przejęcie kontroli obsługi żądania przez konkretny zasób. Tak jak w przypadku akcji
<jsp:include> jest ono wykonywane w czasie żądania i jest oparte na metodzie forward()
RequestDispatcher. Stosowane są zasady wykorzystania forward() — w czasie przekazania cały
zbuforowany wynik zostaje wyczyszczony, a jeżeli stało się to wcześniej, system zgłasza wyjątek. Poniższa akcja
przekazuje żądanie specjalnej stronie, jeżeli użytkownik nie jest zalogowany:
<% if (session.getAttribute("uzyt") == null { %>
<jsp:forward page="/logowanie.jsp" />
<% } %>
Akcja <jsp:forward> również przyjmuje znaczniki <jsp:param>.

Aplikacja „Narzędzia”
W tym momencie, znając już zasady dołączeń i osadzania JavaBeans na stronach JSP, można wykorzystać te
własności do stworzenia aplikacji wyświetlającej narzędzia. Na stronie JSP zostanie osadzony komponent dający
stronie dostęp do informacji na temat narzędzi i pozwalający na traktowanie strony jako przeglądarki tych
danych. Przykład 18.8 przedstawia stronę JSP o nazwie widoknarz.jsp.
Przykład 18.8.
Aplikacja przeglądająca narzędzia przy pomocy JSP
<%-- widoknarz.jsp --%>

<%
String tytul = "Lista narzędzi";
String tytul2 = "Lista narzędzi do tworzenia zawartości";
String opis = " Bez narzędzi, ludzie nie są niczym więcej niż zwierzętami. " +
I to dość słabymi. Poniżej przedstawiono listę opartych na " +
serwletach narzędzi do tworzenia zawartości, które można " +
wykorzystać w celu wzmocnienia się.";

%>

<%@ include file="/naglowek.jsp" %>

<%@ page session="false" %>


<%@ page errorPage="/bladBierz.jsp" %>

<jsp:useBean id="narzkomp" class="NarzKomp" scope="application">


<jsp:setProperty name="narzkomp" property="plikNarz"
value='<%= application.getInitParameter("plikNarz") %>' />
</jsp:useBean>

<%
Narzedzie[] narzedzia = plikNarz.pobierzNarz(request.getParameter("stan"));

for (int i = 0; i < narzedzia.length; i++) {


Narzedzie narzedzie = narzedzia[i];
%>
<HR SIZE=2 ALIGN=LEFT>

<H3>
<%= narzedzie.nazwa %>

<% if (narzedzie.czyNowy(45)) { %>


<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>
<% } else if (narzedzie.czyUaktualniony(45)) { %>
<FONT COLOR=#FF0000><B> (Uaktualniony!) </B></FONT>
<% } %>

</H3>
<A HREF="<%= narzedzie.domURL %>"><%= narzedzie.domURL %></A><BR>

<%= narzedzie.komentarz %>

<% } %>
<%@ include file="/stopka.jsp" %>
Na początku wewnątrz skryptletu, jako łańcuchy String, definiowane są tytuły i opis strony. Następnie
wykorzystana jest instrukcja include w celu dołączenia standardowej zawartości nagłówka z pliku naglowek.jsp
oraz standardowej zawartości stopki (na końcu pliku) z pliku stopka.jsp. Instrukcja include umieszcza
zawartość plików nagłówka i stopki do pliku podczas fazy tłumaczenia, tak więc zmienne tytul, tytul2 i
opis są widoczne wewnątrz dołączanych plików, i plik naglowek.jsp wykorzystuje te zmienne. Pliki
naglowek.jsp i stopka.jsp są przedstawione w przykładach 18.9 i 18.10.
Przykład 18.9.
Plik naglowek.jsp
<%-- naglowek.jsp --%>
<%-- Zależy od obecności zmiennych tytul, tytul2 i opis --%>

<HTML><HEAD><TITLE><%= tytul %></TITLE></HEAD>

<BODY BGCOLOR="#FFFFFF" BACKGROUND="/obrazki/tlo.gif"


LINK="#003333" ALINK="#669999" VLINK="#333333">

<IMG SRC="/obrazki/banner.gif" WIDTH=600 HEIGHT=87 BORDER=0><BR>

<TABLE>
<TR>
<TD WIDTH=125 ROWS=10 VALIGN=TOP>
<BR><BR><BR>
<FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#FF0000">
<A HREF="/indeks.html">Strona główna</A><BR>
<A HREF="/hosting.html">Hosting</A><BR>
<A HREF="/mechanizmy.html">Mechanizmy</A><BR>
</FONT>
</TD>

<TD WIDTH=475>

<TABLE CELLPADDING=5><TR><TD WIDTH=600 BGCOLOR="#006699" VALIGN=TOP>


<B><FONT FACE="Arial,Helvetica" SIZE="+2">
<%= tytul %>
</FONT></B>
</TD></TR></TABLE>

<B><FONT FACE="Arial,Helvetica" SIZE="+1" COLOR="#003366">


<%= tytul2 %>
</FONT></B><P>

<P>

<FONT FACE="Arial,Helvetica">

<%= opis %>


Przykład 18.10.
Plik stopka.jsp
<%-- stopka.jsp --%>
<%-- Nie zależy od obecności żadnych zmiennych --%>

</FONT>

</TD>
</TR>

<TR>
<TD></TD>
<TD WIDTH=475 ALIGN=CENTER COLSPAN=3>
<HR>
<FONT FACE="Arial,Helvetica">
<A HREF="/indeks.html">Strona główna</A>&nbsp;&nbsp;
<A HREF="/hosting.html">Hosting</A>&nbsp;&nbsp;
<A HREF="/mechanizmy.html">Mechanizmy</A>&nbsp;&nbsp;<P>
</FONT>

<TABLE WIDTH=100%>
<TR>
<TD WIDTH=260 ALIGN=LEFT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
<A HREF="/wlasnosc.html">Własność</A> &copy; 2000 Jason Hunter<BR>
Wszystkie prawa zastrzeżone.</TD>
<TD WIDTH=5></FONT></TD>
<TD WIDTH=230 ALIGN=RIGHT VALIGN=TOP>
<FONT FACE="Arial,Helvetica">
Kontakt: <A HREF="mailto:webmaster@servlets.com">webmaster@servlets.com</a>
</FONT></TD>
</TR>
</TABLE>

</TD>
</TR>
</TABLE>

</BODY>
</HTML>
Następnie na stronie widoknarz.jsp wykorzystana zostaje akcja <jsp:useBean> w celu osadzenia
komponentu JavaBean o nazwie narzkomp, będącego egzemplarzem klasy NarzKomp. Komponent posiada
zakres application, tak więc pojedynczy egzemplarz danych zostanie automatycznie zachowany w
kontekście i udostępniony wszystkim stronom.
Wewnątrz głównej części znacznika <jsp:useBean> umieszczona została akcja <jsp:setProperty>,
która ustawia własność komponentu plikNarz na wartość parametru inicjacji kontekstu plikNarz,
wykorzystując wyrażenie atrybutu czasu uruchomienia w celu odczytania wartości. Informuje to komponent,
którego pliku powinien użyć do pobrania informacji na temat narzędzi. Idealnym rozwiązaniem byłoby, jeżeli
komponent mógłby uzyskać bezpośredni dostęp do zmiennej aplikacji w celu odczytania tych wartości, ale
komponenty mają dostęp jedynie do informacji udostępnionych przez stronę JSP. Eleganckim rozwiązaniem
byłaby możliwość akceptowania przez komponent nazwy pliku jako argumentu jego konstruktora, ale
egzemplarze komponentów są tworzone przy pomocy domyślnego, nie posiadającego argumentów konstruktora,
tak więc to również nie jest możliwe. Metoda zastosowana powyżej to dołączenie akcji <jsp:setProperty>
do głównej części znacznika <jsp:UseBean> tak więc podczas pierwszego konstruowania komponentu
bezpośrednio otrzymuje on nazwę pliku, z którego powinien pobierać informacje.
Należy zauważyć dwie sprawy dotyczące wyrażenia atrybutu czasu uruchomienia. Po pierwsze, konieczne było
wykorzystanie pojedynczych nawiasów dookoła wartości, ponieważ podwójne cudzysłowy zostały zastosowane
wewnątrz wyrażenia. Pozwala to na uniknięcie błędów analizatora JSP. Możliwe jest również wyłączenie
podwójnych cudzysłowów z wyrażenia przy pomocy lewych ukośników. Po drugie, jeżeli parametr inicjacji nie
istnieje i getInitParameter() zwraca null, wywołany zostaje wyjątek JspException, ponieważ
wartość null nie jest dozwolona w wyrażeniu atrybutu czasu uruchomienia36.
Po znaczniku <jsp:useBean> skryptlet wykorzystany zostaje w celu pobrania tablicy narzędzi z komponentu,
co wykonywane jest poprzez wywołanie metody pobierzNarzedzia() z wartością parametru stan, na
wypadek gdyby użytkownik chciał przeglądać jedynie narzędzia o określonym stanie. Kuszące może być
pragnienie napisania akcji <jsp:setProperty>, która umożliwiłaby komponentowi bezpośredni dostęp do
wartości parametru stan ale, ponieważ komponent posiada zakres application, nie jest to możliwe —
wykonanie tego spowodowałoby problemy z współbieżnością, gdyby klika żądań próbowało ustawić tę samą
własność pojedynczego współdzielonego egzemplarza komponentu.
Po odczytaniu tablicy narzędzi, wykorzystany zostaje kolejny fragment kodu skryptletu w celu rozpoczęcia pętli
for dokonującej iteracji na tablicy. Wewnątrz pętli for umieszczona zostaje logika wyświetlająca każde osobne
narzędzie. Wyrażenia zostają wykorzystane do wyświetlenia prostych wartości takich, jak nazwa narzędzia, URL
i komentarz. Skryptlety służą do wyświetlania uwag na temat stanu — Nowość lub Uaktualnienie.37 Kod klasy
NarzKomp przedstawiony jest w przykładzie 18.11. Kod klasy Narzedzie jest identyczny jak ten
przedstawiony w rozdziale 14, „Szkielet Tea”.
Przykład 18.11.
Klasa wspierająca NarzKomp

36
Z technicznego punktu widzenia, specyfikacja JSP nie określa sposobu postępowania z zerowymi wartościami wyrażeń
atrybutów czasu uruchomienia. W związku z tym modelem staje się wzorcowa implementacja Tomcata, a Tomcat 3.2 zgłasza
wyjątek.
37
Z uwagi na zachowujące puste miejsca zasady JSP należy być ostrożnym podczas tworzenia wyrażeń if/else wewnątrz
skryptletów. Poniższy kod(narzedzie.czyNowy(45))
<% if nie działałby: { %>
<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>
<% } %>
<% else if (narzedzie.czyUaktualniony(45)) { %>
<FONT COLOR=#FF0000><B> (Uaktualniony!) </B></FONT>
<% } %>

W przypadku powyższego kodu, serwlet działający w tle próbowałby wstawić nową linię pomiędzy klauzulami if i else,
powodując paskudny błąd kompilacji — 'else' without 'if'.
import java.util.*;

public class NarzKomp {

private Narzedzie[] narzedzia;


private String stan;

public NarzKomp() { }

public void ustawPlikNarz(String plikNarz) throws Exception {


// Nie ma sposobu dostępu do kontekstu aplikacji bezpośrednio z komponentu
narzedzia = Narzedzie.ladujNarz(plikNarz);
}

public Narzedzie[] pobierNarz(String stan) throws Exception {


if (narzedzia == null) {
throw new IllegalStateException(
"Zawsze należy ustawić własność plikNarz NarzKomp");
}

if (stan == null) {
return narzedzia;
}
else {
// Zwrócenie jedynie narzędzi posiadających dany „stan”
List lista = new LinkedList();
for (int i = 0; i < narzedzia.length; i++) {
if (narzedzia[i].getStateFlag().equalsIgnoreCase(stan)) {
lista.add(narzedzia[i]);
}
}
return (Narzedzie[]) lista.toArray(new Narzedzie[0]);
}
}
}
Metoda ustawPlikNarz() jest automatycznie wywoływana przez akcję <jsp:setProperty> zaraz po
skonstruowaniu elementu. Pobiera ona informacje na temat narzędzi z określonego pliku.
Metoda pobierzNarz() jest wywoływana przez stronę JSP bezpośrednio wewnątrz skryptletu. Metoda
pobiera jako parametr stan narzędzi, według którego będą one sprawdzane. Jeżeli stan posiada wartość null,
zwracana jest pełna lista. Jeżeli zmienna narzędzi posiada wartość null oznacza to, że ustawPlikNarz()
nie zakończyła się sukcesem, a komponent zgłasza wyjątek IllegalStateException, który ostrzega
użytkownika przed problemem.

Biblioteki własnych znaczników


Na koniec opisany zostanie najbardziej interesujący aspekt JavaServer Pages — obsługa bibliotek własnych
znaczników. Własne znaczniki (znane także jako własne akcje) pozwalają stronie JSP na zawieranie znaczników
XML które, chociaż wyglądają jak HTML, tak naprawdę pozwalają na wykonywanie konkretnego kodu Javy,
kiedy wystąpi znacznik. Możliwe zastosowania bibliotek własnych znaczników są naprawdę interesujące. Na
przykład, firma Live Software (twórca Jrun) wykorzystała własne znaczniki JSP do zaimplementowania
całkowicie przenośnej wersji znaczników Cold Fusion Allaire o nazwie CF_Anywhere. (Krótko później Live
Software została wykupiona przez Allaire.)
Szczegółowe zasady tworzenia biblioteki własnych znaczników są dość skomplikowane i wykraczają poza zakres
niniejszego rozdziału. Na szczęście tworzonych i już przydatnych jest kilka bibliotek znaczników Open Source.
Dwie najbardziej znane to Apache Taglibs i Apache Struts, obie pochodzące z projektu Apache Jakarta
znajdującego się pod adresem http://jakarta.apache.org. Apache Taglibs zawiera biblioteki znaczników o
ogólnym przeznaczeniu; Apache Struts zawiera bibliotekę znaczników przeznaczoną do obsługi architektury w
stylu Model 2, ale duża ilość znaczników jest ogólnie przydatna. Lista bibliotek znaczników JSP jest dostępna
pod adresem http://jsptags.com. Podejmowane są również próby stworzenia standardowej biblioteki znaczników
występujące jako formalny proces żądania specyfikacji Javy o kodzie JSR-052. Większa ilość informacji na
temat JSR-052 jest dostępna pod adresem
http://java.sun.com/aboutJava/communityprocess/jsr/jsr_052_jsptaglib.html.

Stosowanie bibliotek własnych znaczników


Własne akcje umożliwiają osadzanie logiki aplikacji na stronach JSP przy pomocy znaczników podobnych do
HTML. Przy pomocy odpowiedniego zestawu znaczników logika, która wcześniej musiała być utworzona przy
pomocy skryptletów może być napisana przy pomocy znaczników. Na przykład, projekt Apache Struts posiada
znacznik <iterate>, który może zastąpić pętlę for. Znacznik <iterate> powtarza główną część
znacznika raz dla każdego elementu ze zbioru (List, Set, Vector, Map, tablica itp.). Podczas każdej iteracji
umieszcza element, który może zostać wykorzystany w głównej części, w zakresie page38. Struts posiada
również znacznik <property>, który działa jak <jsp:getProperty> z dodatkowym usprawnieniem
takim, że filtruje zawartość wyświetlaną przez HTML poprzez konwersję wszystkich znaków specjalnych HTML
na odpowiadające im encje znakowe. Na przykład < staje się %lt;.
Poprzez wspólne wykorzystanie <iterate> i <property> można utworzyć poniższy fragment strony
wyświetlający wszystkie cookies wysłane w żądaniu — z właściwie przefiltrowanymi wszystkimi znakami
specjalnymi:
<%@ taglib uri="/WEB-INF/struts.tld" prefix="struts" %>

<struts:iterate id="cookie" collection="<%= request.getCookies() %>">


<struts:property name="cookie" property="name" /> =
<struts:property name="cookie" property="value" /> <BR>
</struts:iterate>
Aby zainstalować i korzystać z biblioteki takiej jak Struts należy wykonać kilka czynności:
1. Pobrać i rozpakować dystrybucje biblioteki znaczników. Struts, na przykład, jest dostępny pod adresem
http://jakarta.apache.org.
2. Umieścić klasy biblioteki znaczników Javy tak, aby mogły zostać odnalezione przez serwer. Na
przykład umieścić plik struts.jar w katalogu WEB-INF/lib lub umieścić skompilowane klasy w WEB-
INF/classes.
3. Umieścić plik Tag Library Descriptor (Deskryptor Biblioteki Znaczników — TLD) wewnątrz katalogu
WEB-INF. Zazwyczaj umieszcza się go w WEB-INF/tlds lub bezpośrednio WEB-INF, dokładne
umiejscowienie nie jest ważne. Na przykład proszę umieścić struts.tld w WEB-INF. Plik TLD to plik
danych XML dostarczający serwerowi informacji na temat każdego znacznika w bibliotece — jego
nazwy, klasy, sposoby wykorzystania jego atrybutów i części głównej itd. Posiadanie plików TLD ma
dodatkową zaletę — na podstawie TLD można utworzyć stronę dokumentującą bibliotekę znaczników
(na przykład przy pomocy arkusza stylów XSL).
4. Dodać pozycję <taglib> do pliku web.xml. Następnie wewnątrz niej umieścić pozycję <taglib-
uri> podającą nazwę do poszukiwań biblioteki znaczników, a także <taglib-location>
podającą umiejscowienie pliku TLD. Krok ten nie jest konieczny, jeżeli pozycja <taglib-uri> jest
równoważna pozycji <taglib-location>. Na przykład:
<taglib> <!—jeżeli URI pasuje do umiejscowienia to jest opcjonalne -->
<taglib-uri>/WEB-INF/struts.tld</tablib-uri>
<taglib-location>/WEB-INF/struts.tld</taglib-location>
</taglib>

Pozycja <taglib-uri> musi być ścieżką URI, tak więc jej prawidłowe wartości to struts,

http://jakarta.apache.org/struts, i /WEB-INF/struts.ltd. Ścieżka URI tradycyjnie

odnosi się do prawdziwego umiejscowienia zasobów, ale w powyższym przykładzie została zastosowana

jedynie jako unikatowy identyfikator biblioteki znaczników.

5. Na każdej stronie JSP wykorzystującej bibliotekę własnych znaczników należy dodać poniższą
instrukcję taglib. Nakazuje ona serwerowi pobranie biblioteki znaczników o podanym „URI
poszukiwań” i umieszczenie tych znaczników pod przedrostkiem nazwy XML struts:
<%@ taglib uri="/WEB-INF/struts.tld" prefix="struts" %>
6. Należy wykorzystać znaczniki na stronie JSP, upewniając się, że przedrostek nazw XML jest taki, jak
zadeklarowany w instrukcji taglib:
<struts:property name="nazwakomponentu" property="nazwawlasciwosci" />
Kreator biblioteki znaczników może połączyć TLD z plikiem JAR biblioteki znaczników. W tym
przypadku, zamiast wskazywać na .tld należy wskazać bezpośrednio na .jar.

38
Znacznik <iterate> wymaga JDK 1.2 lub późniejszego. W JDK 1.1 występuje znacznik <enumerate>.
Aplikacja „Narzędzia” wykorzystująca bibliotekę własnych znaczników
Wykorzystanie znaczników Struts <iterate> i <property> może uprościć stronę widoknarz.jsp. Przy
okazji zademonstrowana zostanie oparta na serwletach architektura Model 2, w której serwlet otrzymuje żądanie,
dodaje atrybuty do obiektu żądania i przekazuje żądanie do JSP, która działa jak szablon. Przykład 18.12
przedstawia serwlet kontrolujący.
Przykład 18.12.
Serwlet kontrolujący Model 2
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SerwletNarz extends HttpServlet {

Narzedzie[] narzedzia = null;

public void init() throws ServletException {


// Pobranie danych narzędzi w init w celu zachowania prostoty
String plikNarz =
getServletContext().getInitParameter("plikNarz"); // z web.xml
if (plikNarz == null) {
throw new ServletException("Plik danych narzędzi musi być określony jako " +
"parametr inicjacji kontekstu plikNarz");
}
log("Pobieranie narzędzi z " + plikNarz);
try {
narzedzia = Narzedzie.ladujNarz(plikNarz);
if (narzedzia.length == 0) {
log("Nie odnaleziono narzędzi w " + plikNarz);
}
else {
log(narzedzia.length + " narzędzi znaleziono w " + plikNarz);
}
}
catch (Exception w) {
throw new ServletException(w);
}
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
Narzedzie[] narzedzia = null;

// Umieszczenie właściwego atrybutu „narzedzia” w kontekście


String stan = zad.getParameter("stan");
if (stan == null) {
zad.setAttribute("narzedzia", pobierzNarz());
}
else {
zad.setAttribute("narzedzia", pobierzNarz(stan));
}

// Przesłanie żądania do JSP w celu dalszego przetworzenia


RequestDispatcher disp = zad.getRequestDispatcher("/widoknarz-znacznik.jsp");
disp.forward(zad, odp);
}

public Narzedzie[] pobierzNarz() {


return narzedzia;
}

public Narzedzie[] pobierzNarz(String stan) {


List lista = new LinkedList();
for (int i = 0; i < narzedzia.length; i++) {
if (narzedzia[i].getStateFlag().equalsIgnoreCase(stan)) {
lista.add(narzedzia[i]);
}
}
return (Narzedzia[]) lista.toArray(new Narzedzia[0]);
}
}
Serwlet zachowuje się podobnie do NarzedziaAp w Tea i SerwletNarz w WebMacro. Pobiera informacje
o narzędziach w swojej metodzie init(), a dla każdego żądania dokłada do obiektu żądania odpowiedni
podzbiór narzędzi. Serwlet przekazuje każde żądanie plikowi JSP, który właściwie tworzy stronę. Posiada on
dostęp do narzędzi w każdym obiekcie żądania przy pomocy znacznika <jsp:useBean>. Przykład 18.13
przedstawia plik JSP noszący nazwę widoknarz-znacznik.jsp.
Przykład 18.13.
Aplikacja „Narzędzia” przystosowana do bibliotek znaczników
<%-- widoknarz-znacznik.jsp --%>

<%@ taglib uri="/WEB-INF/struts.tld" prefix="struts" %>

<%
String tytul = "Lista narzędzi";
String tytul2 = "Lista narzędzi do tworzenia zawartości";
String opis = " Bez narzędzi, ludzie nie są niczym więcej niż zwierzętami. " +
I to dość słabymi. Poniżej przedstawiono listę opartych na " +
serwletach narzędzi do tworzenia zawartości, które można " +
wykorzystać w celu wzmocnienia się.";
%>

<%@ include file="/naglowek.jsp" %>

<%@ page session="false" %>


<%@ page errorPage="/bladBierz.jsp" %>

<%-- Ustawienie tablicy narzędzi jako atrybutów żądania--%>


<jsp:useBean id="narzedzia" class="Narzedzie[]" scope="request"/>

<struts:iterate id="narzedzie" collection="<%= narzedzia %>">


<HR SIZE=2 ALIGN=LEFT>

<H3>
<%-- Automatycznie wartości opuszczane przez HTML --%>
<struts:property name="narzedzie" property="nazwa" />

<% if (((Narzedzie)narzedzie).czyNowy(45)) { %>


<FONT COLOR=#FF0000><B> (Nowość!) </B></FONT>
<% } else if (((Narzedzie)narzedzie).czyUaktualniony(45)) { %>
<FONT COLOR=#FF0000><B> (Uaktualnienie!) </B></FONT>
<% } %>

</H3>
<A HREF="<struts:property name="narzedzie" property="domURL"/>">
<struts:property name="narzedzie" property="domURL"/></A><BR>

<%-- Przyjęcie, że w komentarzu ma nie występować HTML --%>


<struts:property name="narzedzie" property="komentarz" />

</struts:iterate>

<%@ include file="/stopka.jsp" %>


Powyższa strona po pierwsze wykorzystuje instrukcję taglib do pobrania biblioteki własnych znaczników
Struts. Nie wolno zapomnieć o tym kroku! Jeżeli krok ten zostanie pominięty, nie będzie wyświetlony
bezpośredni komunikat o błędzie, ale strona nie będzie działać. Powodem tego jest fakt, że bez instrukcji
taglib znaczniki <struts:iterate> i <struts:property> serwer traktuje jak wszystkie inne
znaczniki HTML i w związku z tym jako zawartość do wyświetlenia, a nie logikę strony do wykonania!
Główna część strony wykorzystuje znacznik <struts:iterate> w celu wykonania pętli nad tablicą narzędzi
oraz znacznik <struts:property> w celu wyświetlenia nazwy, URL-a i komentarza dla każdego narzędzia.
Jeżeli komentarze mogą zawierać kod HTML, właściwym znacznikiem byłby <jsp:getProperty>
przeciwdziałający przefiltrowaniu zawartości HTML.
Wykorzystanie popularnych znaczników Apache Struts usuwa dużą ilość kodu ze strony HTML, pozostaje
jednak kilka skryptletów. Usunięcie wszystkich skryptletów wiązałoby się z koniecznością utworzenia własnego
znacznika wykonującego porównanie znaczników czasu. Niestety, z powodu skomplikowania procesu tworzenia
własnych znaczników, samouczek nie został umieszczony w tej książce. Osoby zainteresowane tworzeniem
własnych znaczników powinny przejrzeć książkę „JavaServer Pages” autorstwa Hansa Bergstena (O'Reilly).
Rozdział 19. Informacje

dodatkowe

W każdym domu znajduje się szuflada na szpargały — szuflada załadowana do pełna rzeczami, które nie do
końca pasują do żadnej zorganizowanej szuflady, ale nie mogą też zostać wyrzucone, ponieważ kiedy są
potrzebne, to są naprawdę potrzebne. Niniejszy rozdział pełni funkcję takiej szuflady. Zawiera spory zestaw
przydatnych przykładów serwletów i porad, które nie pasują do żadnego innego miejsca. Zawarto tu serwlety,
które analizują parametry, wysyłają pocztę elektroniczną, uruchamiają programy, wykorzystują mechanizmy
wyrażeń regularnych i rdzenne metody oraz działają jako klienty RMI. Rozdział ten zawiera również
demonstrację technik usuwania błędów, a także pewne sugestie dotyczące poprawy wydajności serwletów.

Analiza parametrów
Osoby, które próbowały tworzyć własne serwlety podczas lektury niniejszej książki przypuszczalnie zauważyły,
że pobieranie i analiza parametrów żądania może być dość nieprzyjemnym zajęciem, zwłaszcza jeżeli parametry
te muszą zostać przekonwertowane do formatu innego niż String. Na przykład, konieczne jest odczytanie
parametru count i zmiany jego wartości na format int. Poza tym, obsługa warunków błędów powinna być
wykonywana przez wywołanie obslugaBezCount(), jeżeli count nie jest podany, a obslugaZlyCount
(), jeżeli count nie może zostać przekształcony na liczbę całkowitą. Wykonanie tego działania przy pomocy
standardowego Servlet API wymaga następującego kodu:
int count;

String param = zad.getParameter("count");


if (param=null || param.length() == 0) {
obslugaBezCount();
}
else {
try {
count = Integer.parseInt(param);
}
catch (NumberFormatException w) {
obslugaZlyCount();
}
}
Czy to wygląda jak porządny kod? Nie jest zbyt piękne, prawda? Lepszym rozwiązaniem jest przekazanie
odpowiedzialności za pobieranie i analizę parametrów klasie narzędziowej. Klasa
com.oreilly.servlet.ParameterParser jest właśnie taką klasą. Przy pomocy ParameterParser
możliwe jest przepisanie powyższego kodu w bardziej elegancki sposób:
int count;

ParameterParser analiza = new ParameterParser(zad);


try {
count = analiza.getIntParameter("count");
}
catch (NumberFormatException w) {
obslugaZlyCount();
}
catch (ParameterNotFoundException w) {
obslugaBezCount();
}
Analizująca parametry metoda getIntParameter() zwraca wartość określonego parametru jako int.
Zgłasza wyjątek NumberFormatException, jeżeli parametr nie może zostać przekonwertowany na int
oraz ParameterNotFoundException, jeżeli parametr nie jest częścią żądania. Zgłasza również
ParameterNotFoundException, jeżeli wartość parametru to pusty łańcuch. Często dzieje się tak w
przypadku wysyłanych formularzy, gdy nie wpisano żadnych wiadomości do pól tekstowych, co we wszystkich
przypadkach powinno być traktowane w ten sam sposób, co brakujący parametr.
Jeżeli wystarczy, aby w przypadku problemów z parametrem serwlet wykorzystywał domyślną wartość, co jest
częstą praktyką, kod może zostać uproszczony w jeszcze większym stopniu:
ParameterParser analiza = new ParameterParser(zad);
int count = analiza.getIntParameter("count", 0);
Powyższa druga wersja getIntParameter() pobiera domyślną wartość 0, która jest zwracana zamiast
zgłoszenia błędu.
Istnieje również możliwość sprawdzenia, czy w żądaniu brakuje jakichkolwiek parametrów:
ParameterParser analiza = new ParameterParser(zad);
String[] wymagane = { "nazwa1", "nazwa2", "konto" };
String[] brakujace = analiza.getMissingParameters(wymagane);
Powyższa metoda zwraca null, jeżeli nie brakuje żadnego parametru.
ParameterParser obsługuje także internacjonalizację poprzez metodę setCharacterEncoding().
Określa ona kodowanie, które powinno zostać wykorzystane podczas interpretacji wartości parametrów. Wartość
może pochodzić z cookie użytkownika, ukrytego pola formularza lub sesji użytkownika:
ParameterParser analiza = new ParameterParser(zad);
analiza.setCharacterEncoding("Shift_JIS");
String wartoscJaponia = analiza.getStringParameter("nazwaLatin");
Wewnętrznie ParameterParser wykorzystuje sztuczkę getBytes() przedstawioną w rozdziale 13,
„Internacjonalizacja” do obsługi konwersji. Nazwy parametrów muszą być ciągle podane w kodowaniu Latin1,
ponieważ mechanizm poszukiwania wykorzystuje jeszcze nie zinternalizowane metody Servlet API
getParameter() i getParameterValues().

Kod ParameterParser
Klasa ParameterParser zawiera ponad tuzin metod, które zwracają parametry żądania — dwie dla każdego
rdzennego typu Javy. Posiada również dwie metody getStringParameter() w przypadku, gdyby
konieczne było pobranie parametru w jego surowym formacie String. Kod ParameterParser jest
przedstawiony w przykładzie 19.1. Wyjątek ParameterNotFoundExceptoion znajduje się w przykładzie
19.2.
Przykład 19.1.
Klasa ParameterParser
package com.oreilly.servlet;

import java.io.*;
import java.util.*;
import javax.servlet.*;

public class ParameterParser {

private ServletRequest req;


private String encoding;

public ParameterParser(ServletRequest req) {


this.req = req;
}

public void setCharacterEncoding(String encoding)


throws UnsupportedEncodingException {
// Sprawdzenie prawidłowości kodowania
new String("".getBytes("8859_1"), encoding);
// Jeżeli tutaj, to prawidłowe, więc jego ustawienie
this.encoding = encoding;
}

public String getStringParameter(String name)


throws ParameterNotFoundException {
String[] values = req.getParameterValues(name);
if (values == null) {
throw new ParameterNotFoundException(name + " not found");
}
else if (values[0].length() == 0) {
throw new ParameterNotFoundException(name + " was empty");
}
else {
if (encoding == null) {
return values[0];
}
else {
try {
return new String(values[0].getBytes("8859_1"), encoding);
}
catch (UnsupportedEncodingException e) {
return values[0]; // should never happen
}
}
}
}

public String getStringParameter(String name, String def) {


try { return getStringParameter(name); }
catch (Exception e) { return def; }
}

public boolean getBooleanParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
String value = getStringParameter(name).toLowerCase();
if ((value.equalsIgnoreCase("true")) ||
(value.equalsIgnoreCase("on")) ||
(value.equalsIgnoreCase("yes"))) {
return true;
}
else if ((value.equalsIgnoreCase("false")) ||
(value.equalsIgnoreCase("off")) ||
(value.equalsIgnoreCase("no"))) {
return false;
}
else {
throw new NumberFormatException("Parameter " + name + " value " + value +
" is not a boolean");
}
}

public boolean getBooleanParameter(String name, boolean def) {


try { return getBooleanParameter(name); }
catch (Exception e) { return def; }
}

public byte getByteParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return Byte.parseByte(getStringParameter(name));
}

public byte getByteParameter(String name, byte def) {


try { return getByteParameter(name); }
catch (Exception e) { return def; }
}

public char getCharParameter(String name)


throws ParameterNotFoundException {
String param = getStringParameter(name);
if (param.length() == 0)
throw new ParameterNotFoundException(name + " is empty string");
else
return (param.charAt(0));
}

public char getCharParameter(String name, char def) {


try { return getCharParameter(name); }
catch (Exception e) { return def; }
}

public double getDoubleParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return new Double(getStringParameter(name)).doubleValue();
}

public double getDoubleParameter(String name, double def) {


try { return getDoubleParameter(name); }
catch (Exception e) { return def; }
}

public float getFloatParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return new Float(getStringParameter(name)).floatValue();
}

public float getFloatParameter(String name, float def) {


try { return getFloatParameter(name); }
catch (Exception e) { return def; }
}

public int getIntParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return Integer.parseInt(getStringParameter(name));
}

public int getIntParameter(String name, int def) {


try { return getIntParameter(name); }
catch (Exception e) { return def; }
}

public long getLongParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return Long.parseLong(getStringParameter(name));
}

public long getLongParameter(String name, long def) {


try { return getLongParameter(name); }
catch (Exception e) { return def; }
}

public short getShortParameter(String name)


throws ParameterNotFoundException, NumberFormatException {
return Short.parseShort(getStringParameter(name));
}

public short getShortParameter(String name, short def) {


try { return getShortParameter(name); }
catch (Exception e) { return def; }
}

public String[] getMissingParameters(String[] required) {


Vector missing = new Vector();
for (int i = 0; i < required.length; i++) {
String val = getStringParameter(required[i], null);
if (val == null) {
missing.addElement(required[i]);
}
}
if (missing.size() == 0) {
return null;
}
else {
String[] ret = new String[missing.size()];
missing.copyInto(ret);
return ret;
}
}
}
Przykład 19.2.
Klasa ParameterNotFoundException
package com.oreilly.servlet;

public class ParameterNotFoundException extends Exception {

public ParameterNotFoundException() {
super();
}

public ParameterNotFoundException(String s) {
super(s);
}
}
Wysyłanie poczty elektronicznej
Czasami wysłanie przez serwlet wiadomości jest konieczne, a czasami jest to po prostu ułatwienie. Na przykład,
proszę sobie wyobrazić serwlet otrzymujący dane z formularza użytkownika, który umieszcza tam swoje
komentarze. Serwlet ten może chcieć przesłać dane formularza na listę dystrybucyjną zainteresowanych stron.
Albo proszę sobie wyobrazić serwlet, który napotyka niespodziewany problem i może wysłać administratorowi
pocztą stronę prosząc o pomoc.
Serwlet może wykorzystać do wysłania poczty elektronicznej jedną z czterech metod:
• Może sam zarządzać szczegółami — nawiązać połączenie przez zwykły port z serwerem poczty i
wysyłając wiadomość poprzez protokół pocztowy niskiego poziomu, zazwyczaj tak zwany Simple Mail
Transfer Protocol (Prosty Protokół Transferu Poczty — SMTP).
• Może uruchomić z wiersza poleceń zewnętrzny program pocztowy, jeżeli system serwera posiada taki
program.
• Może wykorzystać interfejs JavaMail, zaprojektowany do pomocy w skomplikowanej obsłudze,
tworzeniu i przetwarzaniu poczty (proszę zobaczyć http://java.sun.com/products/javamail).
• Może wykorzystać jedną z wielu dostępnych bezpłatnie klas pocztowych, które zmieniają szczegóły
wysyłania poczty na proste i łatwe do wywołania metody.
W przypadku nieskomplikowanego wysyłania poczty poleca się ostatni sposób ze względu na jego prostotę. Dla
bardziej skomplikowanych zastosowań rekomendowany jest JavaMail — zwłaszcza w przypadku serwletów
działających pod kontrolą serwera J2EE, w którym na pewno dostępne są dwa pliki JAR wymagane przez
JavaMail.

Stosowanie klasy MailMessage


Dla celów tego przykładu zademonstrowany zostanie serwlet wykorzystujący klasę
com.oreilly.servlet.MailMessage, która nie zostanie tu przedstawiona, ale jest dostępna pod
adresem http://www.servlets.com. Została ona utworzona na podstawie klasy sun.net.smtp.SmtpClient
zawartej w JDK firmy Sun, ale unika politycznego problemu wykorzystywania „niewspieranej i będącej celem
zmian” klasy sun.*. Posiada ona również kilka przyjemnych usprawnień funkcjonalności. Jej stosowanie jest
proste:
1. Wywołanie MailMessage wiad = new MailMessage(). Opcjonalnie przekazanie
konstruktorowi nazwy komputera, który należy wykorzystać jako serwer poczty i który zastępuje
domyślny localhost. Większość komputerów pracujących pod Uniksem może działać jako serwer
poczty SMTP.
2. Wywołanie wiad.from(adresOd), określające adres nadawcy. Adres nie musi być prawidłowy.
3. Wywołanie wiad.to(adresDo), określające adres odbiorcy. Metoda ta może zostać wywołana
kilka razy, jeżeli istnieją dodatkowi odbiorcy. Występują również metody cc() i bcc().
4. Wywołanie wiad.setSubject(temat) w celu określenia tematu. Technicznie nie jest to
wymagane, ale umieszczenie tematu zawsze jest dobrym pomysłem.
5. Wywołanie wiad.setHeader(nazwa, wartosc) jeżeli powinny zostać dołączone dodatkowe
nagłówki. Nagłówki To: (Do), CC: (DW) i Subject: (Temat) nie muszą być ustawiane, ponieważ są
one automatycznie określane przez metody to(), cc() i setSubject(). (Metoda bcc()
oczywiście nie wysyła żadnych nagłówków.) Metodę setHeader() można wykorzystać do
nadpisania wartości domyślnych. Nazwy i wartości nagłówków powinny być podporządkowane
zasadom podanym w dokumencie RFC 822, dostępnym pod adresem http://www.ietf.org/rfc/rfc0822.txt.
6. Wywołanie PrintStream wiad = wiad.getPrintStream() w celu pobrania potoku
wynikowego wiadomości.
7. Wysłanie zawartości wiadomości pocztowej do PrintStream.
8. Wywołanie wiad.sendAndClose() w celu wysłania wiadomości i zamknięcia połączenia z
serwerem.
Wysyłanie pocztą danych formularza
Przykład 19.3 przedstawia serwlet wysyłający dane, które otrzymuje, pocztą na listę dystrybucyjną. Proszę
zauważyć bardzo szerokie wykorzystanie klasy ParametrParser.
Przykład 19.3.
Wysyłanie wiadomości z serwletu.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.MailMessage;
import com.oreilly.servlet.ParameterParser;
import com.oreilly.servlet.ServletUtils;

public class SerwletPoczta extends HttpServlet {

static final String FROM = "SerwletPoczta";


static final String TO = "komentarze@wazna-firma.com";

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

ParameterParser analiza = new ParameterParser(zadanie);


String from = analiza.getStringParameter("from", FROM);
String to = analiza.getStringParameter("to", TO);

try {
MailMessage wiad = new MailMessage(); // przyjęcie, że localhost
wiad.from(from);
wiad.to(to);
wiad.setSubject("Komentarz klienta");

PrintStream glowny = wiad.getPrintStream();

Enumeration enum = zad.getParameterNames();


while (enum.hasMoreElements()) {
String name = (String)enum.nextElement();
if (nazwa.equals("to") || nazwa.equals("from")) continue; //Opuszczenie
góra/dół
String wartosc = analizator.getStringParameter(nazwa, null);
glowny.println(nazwa + " = " + wartosc);
}

glowny glowny.tytul2();
glowny.println("---");
body.println("Wysłany przez " + HttpUtils.getRequestURL(zad));

wiad.sendAndClose();

wyj.println("Dziękujemy za komentarz...");
}
catch (IOException w) {
wyj.println("Wystąpił problem z obsługą komentarza...");
log("Wystąpił problem z wysyłaniem wiadomości", e);
}
}
}
Serwlet po pierwsze określa adresy „Od” i „Do” wiadomości. Domyślne wartości są ustawione w zmiennych
FROM i TO, chociaż wysyłany formularz może zawierać (najprawdopodobniej ukryte) pola określające
alternatywy adresów Od i Do. Serwlet następnie rozpoczyna tworzenie wiadomości pocztowej SMTP. Łączy się
z lokalnym komputerem i adresuje wiadomość. Następnie ustawia temat i wstawia dane formularza do
zawartości, ignorując zmienne FROM i TO. Na końcu wysyła wiadomość i dziękuje użytkownikowi za komentarz.
Jeżeli wystąpi problem, informuje o tym użytkownika i zapisuje wyjątek w dzienniku zdarzeń.
Klasa MailMessage aktualnie nie obsługuje załączników, ale ich obsługa może zostać w prosty sposób
dodana. W przypadku zastosowań bardziej zaawansowanych, dobrą alternatywą jest JavaMail.

Stosowanie wyrażeń regularnych


Ten podrozdział przeznaczony jest szczególnie dla osób znających oparte na CGI skrypty Perla i
przyzwyczajonych do możliwości wyrażeń regularnych w Perlu. W tym rozdziale zostaną przedstawione zasady
korzystania z wyrażeń regularnych w Javie. Osobom nie znającym tematu należy się wyjaśnienie, że wyrażenia
regularne są mechanizmem umożliwiającym bardzo zaawansowaną manipulację łańcuchami przy minimalnej
ilości kodu. Cała moc wyrażeń regularnych jest doskonale przedstawiona w książce „Mastering Reular
Expressions” autorstwa Jefreya E. F. Friedla (O'Reilly).
Pomimo dużej ilości klas i możliwości dodawanych przez firmę Sun do JDK przez wiele lat, ciągle brakuje w
nim wyrażeń regularnych. Nie należy się tym jednak martwić. Podobnie jak w przypadku większości funkcji
Javy, jeżeli nie dostarcza ich Sun, można je uzyskać za rozsądną cenę od innego niezależnego producenta, a
jeżeli jest to coś tak przydatnego, jak wyrażenia regularne, istnieje duża szansa, że dostępna jest odpowiednia
biblioteka Open Source. Tak jest również w tym przypadku — istnieje mechanizm wyrażeń regularnych Open
Source dostępny jako część Apache Jakarta Project, stworzony początkowo przez Jonathana Locke'a i dostępny
w licencji Apache. Licencja Apache jest dużo bardziej liberalna niż Ogólna Licencja Publiczna GNU (GPL),
ponieważ pozwala programistom na wykorzystanie mechanizmu wyrażeń regularnych przy tworzeniu nowych
produktów bez konieczności udostępniania tych produktów jako Open Source.

Odnajdywanie łącz przy pomocy wyrażeń regularnych


W celu zademonstrowania zastosowań wyrażeń regularnych, wykorzystany zostanie mechanizm wyrażeń
regularnych Apache w celu utworzenia serwletu, który pobiera i wyświetla listę wszystkich łącz <A HREF>
HTML odnalezionych na stronie WWW. Jego kod jest przedstawiony w przykładzie 19.4.
Przykład 19.4.
Wyszukiwanie wszystkich łącz
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.*;

import org.apache.regexp.*;

public class Lacza extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

// URL do przetworzenia pobierany jest jako dodatkowa część ścieżki


// np. http://localhost:8080/servlet/Lacza/http://www.servlets.com/
String url = zad.getPathInfo();
if (url == null || url.length() == 0) {
odp.sendError(odp.SC_BAD_REQUEST,
"Proszę przekazać URL do odczytania jako dodatkową część
ścieżki ");
return;
}
url = url.substring(1); // odcięcie wiodącego '/'

String strona = null;


try {
// Żądanie strony
HttpMessage wiad = new HttpMessage(new URL(url));
BufferedReader in =
new BufferedReader(new InputStreamReader(wiad.sendGetMessage()));

// Przekształcenie całej odpowiedzi do String


StringBuffer buf = new StringBuffer(10240);
char[] znaki = new char[10240];
int znakiOdczyt = 0;
while ((znakiOdczyt = in.read(znaki, 0, znaki.length)) != -1) {
buf.append(znaki, 0, znakiOdczyt);
}
strona = buf.toString();
}
catch (IOException w) {
odp.sendError(odp.SC_NOT_FOUND),
"Niemożliwe odczytanie " + url + ":<BR>" +
ServletUtils.getStackTraceAsString(w));
return;
}
wyj.println("<HTML><HEAD><TITLE>Pobieranie łączy</TITLE>");

try {
// Konieczne określenie <BASE> żeby względne łącza działały właściwie
// Jeżeli strona już go posiada, można go użyć
RE wr = new RE("<base[^>]*>", RE.MATCH_CASEINDEPENDENT);
boolean maBase = wr.match(strona);

if (maBase) {
// Wykorzystanie istniejącego <BASE>
wyj.println(wr.getParen(0));
}
else {
// Obliczenie BASE z URL-a, wykorzystanie wszystkiego do ostatniego '/'
wr = new RE("http://.*/", RE.MATCH_CASEINDEPENDENT);
boolean odczytBase = wr.match(url);
if (odczytBase) {
// Sukces, wyświetlenie odczytanego BASE
wyj.println("<BASE HREF=\"" + wr.getParen(0) + "\">");
}
else {
// Brak wiodącego ukośnika, dodanie go
wyj.println("<BASE HREF=\"" + url + "/" + "\">");
}
}

wyj.println("</HEAD><BODY>");

wyj.println("Łącza na <A HREF=\"" + url + "\">" + url + "</A>" +


" są następujące: <BR>");
wyj.println("<UL>");

String szukaj = "<a\\s+[^<]*</a\\s*>";


wr = new RE(szukaj, RE.MATCH_CASEINDEPENDENT);

int indeks = 0;
while (wr.match(strona, indeks)) {
String pasuje = wr.getParen(0);
indeks = wr.getParenEnd(0);
wyj.println("<LI>" + pasuje + "<BR>");
}

wyj.println("</UL>");
wyj.println("</BODY></HTML>");
}
catch (RESyntaxException w) {
// Nie powinien wystąpić, bo łańcuchy poszukiwań są sztywne
w.printStackTrace(wyj);
}
}
}
Przykładowy wynik uruchomienia powyższego przykładu jest przedstawiony na rysunku 19.1.
Rysunek 19.1.
Przeglądanie przy minimalnej szybkości transferu
Teraz powyższy kod zostanie opisany dokładniej. Po pierwsze, serwlet określa URL, którego łącza powinny
zostać pobrane poprzez odszukanie dodatkowych informacji ścieżki. Oznacza to, że powyższy serwlet powinien
zostać wywoływany podobnie do http://localhost:8080/servlet/Lacza/http://www.servlets.com. Następnie serwlet
odczytuje zawartość tego URL-a przy pomocy klasy HttpMessage i przechowuje stronę jako String. Dla
bardzo dużych stron podejście to nie jest wydajne, ale może służyć jako dobry przykład.
Następnym krokiem jest upewnienie się, że wyświetlana strona posiada właściwy znacznik <BASE> tak, aby
łącza względne na stronie były właściwie interpretowane przez przeglądarkę. Jeżeli na pobieranej stronie istnieje
już znacznik <BASE>, to można go wykorzystać, tak więc następuje jego poszukiwanie przy pomocy wyrażenia
regularnego <base[^>]*>. Łańcuch ten zostaje przekazany do konstruktora org.apache.regexp.RE
razem ze znacznikiem wskazującym na niezależność od wielkości liter. Ta składnia wyrażenia regularnego jest
standardowa i identyczna jak w Perlu. Nakazuje ona wyszukanie tekstu <base>, po którym następuje dowolna
ilość znaków nie będących >, po których następuje tekst >. Jeżeli właściwy tekst zostanie odnaleziony, jest on
pobierany przy pomocy wr.getParen(0). Metoda ta pobiera najbardziej zewnętrzne łańcuch (pasujące
łańcuchy mogą być zagnieżdżane przy pomocy nawiasów).
Jeżeli na stronie nie występuje znacznik <BASE>, należy go skonstruować. <BASE> powinien zawierać cały
źródłowy URL razem z ostatnim ukośnikiem39. Informacja ta może zostać pobrana przy pomocy prostego
wyrażenia regularnego http://.*/. Nakazuje ono dopasowanie do tekstu http://, po którym następuje
dowolna ilość znaków kończąca się /. Wzór .* odczytuje maksymalną możliwą ilość znaków, która nie koliduje
z innymi warunkami wyrażenia regularnego (terminologia wyrażeń regularnych nazywa to działanie
zachłannym), tak więc wyrażenie to zwraca wszystko łącznie z wiodącym ukośnikiem. Jeżeli ukośnik taki nie
występuje, zostaje on po prostu dodany.
http://www.jdom.org/news/. Aby się zabezpieczyć, należy uwidocznić każdy wiodący ukośnik w UTL-u przekazanym
serwletowi.
Na końcu ze strony pobrane zostają znaczniki <A HREF>, przy pomocy stosunkowo skomplikowanego
wyrażenia regularnego <a\s+[^<]*</a\s*>. (Można dostrzec, że w kodzie znaki \ zostały wyłączone przy
pomocy dodatkowego znaku \.) Nakazuje to poszukiwanie tekstu <a, po którym następuje dowolna liczba
znaków, które nie są <, następnie tekst </a, dalej dowolna ilość pustego miejsca, potem >. Jeżeli złoży się te
informacje, okaże się, że następuje pobranie znaczników <A HREF> od początkowego <A do końcowego
</A>, niezależnie od wielkości liter i ilości pustego miejsca, zabezpieczone przed omyłkowym dopasowaniem
znaczników takich jak <APPLET>. Po odnalezieniu każdego pasującego tekstu jest on wyświetlany na liście, a
poszukiwanie jest kontynuowane przy pomocy indeksu zapisującego następny punkt początkowy.
Większa ilość informacji na temat działań możliwych do wykonania przy pomocy wyrażeń regularnych jest
dostępna w dokumentacji dołączonej do biblioteki.

Uruchamianie programów
Czasami serwlet musi wykonać program zewnętrzny. Jest to generalnie działanie ważne w sytuacjach, w których
zewnętrzny program oferuje funkcjonalność niedostępną przy pomocy samej Javy. Na przykład, serwlet może
wywołać zewnętrzny program w celu manipulowania obrazkiem lub sprawdzenia stanu serwera. Uruchamianie
programów zewnętrznych utrudnia przenośność i zachowanie bezpieczeństwa. Jest to działanie, które powinno
być podejmowane tylko wtedy, gdy jest absolutnie konieczne i jedynie przez serwlety, pracujące pod kontrolą
stosunkowo liberalnego mechanizmu bezpieczeństwa — konkretnie mechanizmu zezwalającego serwletom na
wywoływanie metody exec() java.lang.Runtime.

Finger
Program finger zapytuje (przypuszczalnie zdalny) komputer o listę aktualnie zalogowanych użytkowników. Jest
on dostępny w praktycznie wszystkich systemach Uniksowych i niektórych komputerach z Windows NT
posiadających zdolność obsługi sieci. Program finger działa przez połączenie z demonem finger (zazwyczaj
noszącym nazwę fingerd), który nasłuchuje na porcie 79. finger wykonuje swoje żądanie do fingerd przy pomocy
własnego protokołu „finger”, a fingerd odpowiada właściwą informacją. Większość systemów Uniksowych
posiada fingerd, ale duża część skoncentrowanych na bezpieczeństwie administratorów wyłącza go w celu
ograniczenia ilości informacji, które mogłyby zostać wykorzystane podczas prób włamania. Ciągle ciężko jest
odnaleźć fingerd w systemach Windows. Uruchomiony bez żadnych argumentów, finger zgłasza wszystkich
użytkowników na lokalnym komputerze. Na przykład:
% finger
Login Name TTY Idle When Office
jkowalski Jan Kowalski q0 3:13 Thu 12:13
anowak Anna Nowak q1 Thu 12:18
Uruchomiony z nazwą użytkownika jako argumentem, finger zwraca informacje na temat tego użytkownika:
% finger jkowalski
Login name: jkowalski In real life: Jan Kowalski
Directory: /usr/people/jkowalski Shell: /bin/tcsh
On since Jan 1 12:13:28 on ttyq0 from :0.0
3 hours 13 minutes Idle Time
On since Jan 1 12:13:28 on ttyq2 from :0.0
Uruchomiony z nazwą komputera jako argumentem, finger zwraca informacje o wszystkich użytkownikach
określonego komputera. Zdalny komputer musi posiadać działający fingerd:
% finger @fikcja
Login Name TTY Idle When Office
ppawlak Piotr Pawlak q0 17d Mon 10:45
I, oczywiście, jeżeli uruchomiony z nazwą użytkownika i nazwą komputera, finger zwraca informacje na temat
określonego użytkownika na określonym komputerze:
% finger ppawlak@fikcja
[fikcja.calkowita.com]
Login name: ppawlak In real life: Piotr Pawlak
Directory: /usr/people/ppawlak Shell: /bin/tcsh
On since Dec 15 10:45:22 on ttyq0 from :0.0
17 days Idle Time

Uruchamianie polecenia finger


Zakładając, że serwlet pragnie dostępu do informacji odczytanych przez finger, posiada on dwie możliwości —
może ustanowić połączenie przez port z fingerd i wykonać żądanie informacji tak, jak dowolny inny klient
finger, lub może uruchomić z wiersza poleceń program finger, który wykonuje połączenie na własną rękę i
odczytać informacje zwracane przez finger. Tu zostanie przedstawiona druga technika.
Przykład 19.5 przedstawia sposób wykonania przez serwlet polecenia finger w celu sprawdzenia, którzy
użytkownicy są zalogowani na lokalnym komputerze. Odczytuje on wynik polecenia i wyświetla go w
standardowym urządzeniu.
Przykład 19.5.
Wykonywanie polecenia finger przy pomocy serwletu
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

public class Finger extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

String polecenie = "finger";

Runtime czas = Runtime.getRuntime();


Process proces = null;
try {
proces = czas.exec(polecenie);
DataInputStream in = new DataInputStream(proces.getInputStream());

// Odczytanie i wyświetlenie wyników


String linia = null;
while ((linia = in.readLine()) != null) {
wyj.println(linia);
}
}
catch (Exception w) {
wyj.println("Problem z finger: " +
ServletUtils.getStackTraceAsString(w));
}
}
}
Serwlet wykorzystuje polecenie exec() tak, jak każda inna klasa Javy. Uruchamia polecenie finger, po czym
odczytuje i wyświetla wynik. Jeżeli wystąpi problem, serwlet wyłapuje wyjątek i wyświetla użytkownikowi
ścieżkę stosu. Powyższy serwlet zakłada, że polecenie finger znajduje się w domyślnej ścieżce programów. Jeżeli
nie jest to prawda, należy zmienić łańcuch polecenia tak, by określał on ścieżkę do finger.
Należy wskazać, że chociaż Java wykonuje rdzenny kod podczas uruchamiania programu finger, sama nie naraża
się na ryzyko normalnie związane z wykonywaniem rdzennego kodu. Dzieje się tak, ponieważ program finger
jest wykonywany jako oddzielny proces. Może on załamać się lub zostać zabity bez wpływu na serwer, na
którym działa serwlet.

Uruchamianie finger z argumentami


Zakładając, że polecenie finger powinno zostać wywołane z argumentem, jego wywołanie powinno wyglądać
nieco inaczej. Metoda exec() pobiera pojedynczy łańcuch określający polecenie, lub tablicę łańcuchów,
określającą polecenie i argumenty, które należy mu przekazać. Kod uruchamiający finger jkowalski
przedstawiony jest w przykładzie 19.6.
Przykład 19.6.
Dodawanie parametru do uruchamianego polecenia
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

public class Finger extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();
String[] polecenie = { "finger", "jkowalski" }; // jedyna zmiana

Runtime czas = Runtime.getRuntime();


Process proces = null;
try {
proces = czas.exec(polecenie);
DataInputStream in = new DataInputStream(proces.getInputStream());

// Odczytanie i wyświetlenie wyników


String linia = null;
while ((linia = in.readLine()) != null) {
wyj.println(linia);
}
}
catch (Exception w) {
wyj.println("Problem z finger: " +
ServletUtils.getStackTraceAsString(w));
}
}
}
Zmienna polecenie jest teraz tablicą łańcuchów {"finger", "jkowalski"}. Polecenie nie działałoby
jako pojedynczy łańcuch "finger jkowalski".

Uruchamianie finger z przekierowywanym wynikiem


Ostatnim przykładem będzie opis przekierowywanego wyniku polecenia finger. Wynik może zostać
przekierowany do pliku w celu późniejszego wykorzystania przy pomocy wywołania finger jkowalski >
/tmp/jkowalski. Można również przekierować wynik do programu grep w celu usunięcia odwołań do
pewnego użytkownika, na przykład finger|grep –v jkowalski.
Zadanie to jest trudniejsze, niż może się wydawać. Jeżeli zmienna polecenia jest ustawiona jako łańcuch
finger|grep –v jkowalski, Java traktuje ten łańcuch jako nazwę pojedynczego programu — którego
najprawdopodobniej nie odnajdzie. Jeżeli zmienna polecenia jest ustawiona jako tablica łańcuchów
{"finger", "|", "grep", "–v", "jkowalski"}, Java wykonuje polecenie finger i przekazuje
mu następne cztery łańcuchy jako parametry, co bez wątpienia zmyli program finger.
Rozwiązanie tego problemu wymaga zrozumienia, że przekazanie jest funkcją powłoki. Powłoka to program, w
którym zazwyczaj wpisuje się polecenia. W Uniksie najpopularniejszymi powłokami są csh, tsh, bash i sh. W
Windows 95/98 powłoka to zazwyczaj command.com. W Windows NT i Windows 2000, powłoką jest
command.com lub cmd.exe.
Zamiast bezpośrednio uruchamiać finger, uruchomiona zostaje powłoka i przekazuje się jej łańcuch polecenia,
które powinno zostać wykonane. Taki łańcuch może zawierać polecenie finger i dowolny rodzaj przekierowania.
Powłoka może zanalizować polecenie, prawidłowo rozpoznać i wykonać przekierowanie. Dokładne polecenie
musi uruchomić powłokę, tak więc program zależy od powłoki, czyli od systemu operacyjnego. Technika ta w
związku z tym ogranicza niezależność serwletów od platformy. W systemie Uniksowym poniższa zmienna
polecenia nakazuje csh wykonanie polecenia finger|grep –v jkowalski:
String[] polecenie = { "/bin/csh", "-c", "finger | grep –v jkowalski" };
Program, który wykonuje Java to /bin/csh. csh zostają przekazane dwa argumenty: -c, które nakazuje powłoce
wykonanie następnego parametru, oraz finger|grep –v jkowalski, który jest parametrem
wykonywanym przez powłokę.
W systemie Windows zmienna polecenia powinna wyglądać następująco:
String[] polecenie = { "command.com", "/c", "finger | grep –v jkowalski" };
Argument /c command.com działa w ten sam sposób, co –c csh. Przyrostek .com jest konieczny. Użytkownicy
Windows NT powinni zapamiętać, że wykorzystanie cmd.exe może sprawiać problemy, ponieważ przekierowuje
ono wynik do nowego okna zamiast do konsoli Javy, która je uruchomiła. Właściwie nawet uruchomienie
serwera z powłoki cmd.exe może spowodować błąd w poleceniu command.com.

Stosowanie rdzennych metod


Pomimo nacisków firmy Sun na stosowanie czystego kodu Javy, rdzenny kod wciąż posiada swoje zastosowania.
Rdzenny kod jest konieczny przy wykonywaniu zadań, których Java (i zewnętrzne problemy przez nią
uruchamiane) nie może wykonać — są to blokowanie plików, uzyskiwanie dostępu do identyfikatorów
użytkowników i współdzielonej pamięci, wysyłanie faksów i tak dalej. Rdzenny kod jest przydatny również
podczas uzyskiwania dostępu do starych danych poprzez bramki nie przystosowane do Javy. Także w sytuacjach,
w których ważny jest każdy element wydajności, biblioteki rdzennego kodu mogą spowodować poważne
przyśpieszenie serwletu.
Jednak rdzenny kod nie powinien być wykorzystywany poza sytuacjami, w których jest absolutnie konieczny,
ponieważ jeśli rdzenny kod uruchomiony przez serwlet spowoduje błąd, cały serwer może się załamać!
Zabezpieczenia Javy nie mogą chronić serwera przed załamaniami rdzennego kodu. Z tego powodu nie jest
rozsądnym wykorzystywanie rdzennego mostu JDBC-ODBC przez serwlet, ponieważ wiele sterowników ODBC
może mieć problemy z dostępem wielowątkowym. Rdzenny kod ogranicza również niezależność serwletu od
platformy. Chociaż nie jest to ważne w przypadku własnych serwletów przywiązanych do konkretnego serwera,
należy o tym pamiętać.
Sposób uzyskiwania przez serwlet dostępu do rdzennych metod zależy od serwera WWW i JVM, na których jest
uruchomiony. Można podjąć ryzyko i powiedzieć ogólnie, że można z dużym prawdopodobieństwem
spodziewać się, że serwer WWW i wirtualna maszyna Javy obsługują standardowy Java Native Interface
(Rdzenny Interfejs Javy — JNI). Wykorzystywanie JNI jest stosunkowo skomplikowane i nawet podstawowe
wprowadzenie do niego wykracza poza zakres tego rozdziału.
Podczas stosowania JNI z serwletami należy pamiętać o następujących sprawach:
• Jedynie najbardziej liberalne mechanizmy bezpieczeństwa serwerów pozwalają serwletem na
wykonywanie rdzennego kodu.
• W JDK 1.1.x występuje błąd, który nie pozwala na wykorzystywanie rdzennego kodu przez klasę
pobraną przy pomocy własnego mechanizmu ładowania klas (takiego jak mechanizm pobierający
serwlety z domyślnego katalogu serwletów). Serwlety wykorzystujące rdzenny kod muszą w związku z
tym być umieszczone w ścieżce klas serwera (takim jak katalog_macierzysty/classes).
• Katalog, w którym umieszczona jest współdzielona biblioteka (lub biblioteki dołączane dynamicznie,
czyli DLL) zawierająca rdzenny kod zależy od serwera WWW i JVM. Niektóre serwery posiadają
konkretne miejsca przeznaczone dla bibliotek współdzielonych. Jeżeli serwer nie określa konkretnego
katalogu bibliotek współdzielonych, proszę spróbować umieścić bibliotekę w miejscu zależnym od
JVM, takim jak katalog_JDK/bin lub katalog_JDK/lib (gdzie katalog_JDK jest katalogiem
macierzystym instalacji JDK), lub miejscu zależnym od systemu, takim jak katalog_windows/system32
lub /usr/lib.

Występowanie jako klient RMI


W rozdziale 10, „Komunikacja aplet-serwlet” opisany został sposób, w jaki serwlet występować może jako
serwer RMI. Tutaj role zostaną odwrócone i przedstawiony zostanie serwlet działający jako klient RMI. Poprzez
podjęcie się roli klienta RMI, serwlet może wykorzystać usługi innych serwerów w celu wypełnienia swojego
zadania, skoordynować swoje wysiłki z innymi serwerami lub serwletami na tych serwerach i/lub działać jako
proxy dla apletów nie mogących samodzielnie komunikować się z serwerami RMI.
Przykład 19.7 przedstawia SerwletKlientGodziny, serwlet, który pobiera aktualną godzinę dnia z serwera
RMI SerwletGodziny przedstawionego w rozdziale 10.
Przykład 19.7.
Serwlet jako klient RMI
import java.io.*;
import java.rmi.*;
import java.rmi.registry.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SerwletKlientGodziny extends HttpServlet {

SerwerGodziny godzina;

// Zwraca odwołanie do SerwerGodziny, lub null jeżeli wystąpi problem.


protected SerwerGodziny pobierzSerwerGodziny() {
// Jeżeli wykorzysta się funkcję dynamicznego ładowania kodu RMI.
// trzeba ustawić mechanizm bezpieczeństwa taki jak RMISecurityManager
// if (System.getSecurityManager() == null) {
// System.setSecurityManager(new RMISecurityManager());
// }

try {
Registry rejestr =
LocateRegistry.getRegistry(pobierzRejestrKomp(), pobierzRejestrPort());
return (SerwerGodziny)registry.lookup(pobierzRejestrNazwa());
}
catch (Exception w) {
getServletContext().log(w, "Problem z pobraniem odwołania do
SerwerGodziny");
return null;
}
}

private String pobierzRejestrNazwa() {


String nazwa = getInitParameter("registryName");
return (nazwa == null ? "SerwletGodziny" : nazwa);
}

private String pobierzRejestrKomp() {


// Zwrócenie nazwy komputera podanej przez "registryHost"
// lub w przeciwnym wypadku null nakazujący wybranie localhost
return getInitParameter("registryHost");
}

private int pobierzRejestrPort() {


try { return Integer.parseInt(getInitParameter("registryPort")); }
catch (NumberFormatException w) { return Registry.REGISTRY_PORT; }
}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/plain");
PrintWriter wyj = odp.getWriter();

// pobranie obiektu godzina jeżeli nie zostało to zrobione wcześniej


if (godzina == null) {
godzina = pobierzSerwerGodziny();
if (godzina == null) {
// Pobranie niemożliwe, więc zgłoszenie niedostępności.
throw new UnavailableException(this, "Zlokalizowanie godzina niemożliwe");
}
}

// Pobranie i wyświetlenie aktualnego czasu na (zazwyczaj zdalnym) komputerze


// godzina
wyj.println(godzina.getDate().toString());
}
}
Powyższy serwlet powinien wydawać się podobny do apletu opisanego w rozdziale 10. Zarówno serwlety jak i
aplety muszą wykonać te same podstawowe działania w celu uzyskania dostępu do serwera RMI. Oba lokalizują
rejestr przy pomocy nazwy komputera i numeru portu, następnie wykorzystują ten rejestr do odszukania
odwołania do obiektu zdalnego. Jedyna możliwa różnica jest taka, że serwlet, jeżeli korzysta z funkcji
dynamicznego ładowania kodu RMI w celu automatycznego pobrania klasy-końcówki z innego komputera, musi
najpierw upewnić się, że działa pod strażą mechanizmu bezpieczeństwa w celu uchronienia siebie od
potencjalnie niebezpiecznej pobranej zdalnie klasy-końcówki. Aplet zawsze działa pod kontrolą mechanizmu
bezpieczeństwa apletów, tak więc krok ten nie jest konieczny. Serwlet może jednak działać bez domyślnego
mechanizmu bezpieczeństwa, tak więc kiedy działa jako klient RMI, należy mu go przypisać.

Usuwanie błędów
Faza testowania/usuwania błędów może być jednym z najtrudniejszych aspektów programowania serwletów.
Serwlety mogą wykorzystywać w dużym stopiniu interakcję klient/serwer, co ułatwia powstawanie błędów, ale
utrudnia ich reprodukcję. Ciężkie może również okazać się wyśledzenie nieoczywistego błędu, ponieważ
serwlety nie współpracują zbyt dobrze ze standardowymi programami uruchomieniowymi, ponieważ działają na
mocno wielowątkowych i ogólnie złożonych serwerach WWW. Poniżej umieszczono kilka rad i sztuczek
pomocnych przy usuwaniu błędów.

Sprawdzenie dzienników zdarzeń


Kiedy podejrzewa się istnienie błędu, należy po pierwsze sprawdzić dzienniki zdarzeń. Większość serwerów
tworzy dziennik błędów, w którym można odnaleźć listę wszystkich błędów zaobserwowanych przez serwer oraz
dziennik zdarzeń, który zawiera listę interesujących zdarzeń serwletów. Dziennik zdarzeń może również
przechowywać wiadomości zapisane przez serwlety przy pomocy metody log(), ale nie zawsze jest to
możliwe.
Proszę zauważyć, że wiele serwerów buforuje wyświetlane informacje w dziennikach w celu poprawienia
wydajności. Podczas odkrywania problemu można spróbować zatrzymania buforowania (zazwyczaj przez
zmniejszenie wielkości buforu serwera do zera bajtów), co pozwala na dostrzeżenie problemu w momencie jego
wystąpienia. Należy pamiętać o przywróceniu później rozsądnej wielkości bufora.

Wyświetlenie dodatkowych informacji


Jeżeli w dzienniku zdarzeń serwera nie można odnaleźć w dziennikach serwera, można spróbować zapisania w
dzienniku dodatkowych informacji przy pomocy metody log(). Jak przedstawiono w przykładach w innych
miejscach niniejszej książki, zazwyczaj w dziennikach zapisywane są ścieżki stosów i inne sytuacje błędów.
Podczas usuwania błędów można dodać kilka tymczasowych poleceń log() działających jako prosty program
uruchomieniowy w celu poznania ogólnego sposobu wykonywania kodu i wartości zmiennych serwletu. Czasami
dobrze jest umieścić polecenia log() w serwlecie otoczone klauzulami if tak, aby uruchamiały się jedynie
wtedy, gdy konkretny parametr inicjacji uruchamiania błędów posiada wartość true.
Pobieranie dodatkowych informacji z dzienników serwera może czasami okazać się nieporęczne. W celu
ułatwienia odnajdywania tymczasowych informacji błędów można nakazać serwletowi wyświetlanie klientowi
informacji o błędach (poprzez PrintWriter) lub do konsoli serwera (poprzez System.out). Nie wszystkie
konsole serwerów są połączone z System.out serwletu; niektóre przekierowują wyświetlane informacje do
pliku.

Stosowanie standardowego programu uruchomieniowego


Możliwe jest zastosowanie standardowego programu uruchomieniowego w celu wyśledzenia problemów
serwletów, chociaż nie musi to być intuicyjnie oczywiste. Nie można wykonać tego działania bezpośrednio na
serwlecie, ponieważ serwlety nie są samodzielnymi programami. Serwlety są rozszerzeniami serwera i jako takie
muszą działać wewnątrz serwera.
Na szczęście Tomcat jest serwerem napisanym wyłącznie w Javie i w związku z tym jest idealny do usuwania
błędów z serwletów. Jedyną sztuczką, o której należy pamiętać jest konieczność uruchomienia Tomcata
wewnątrz programu uruchomieniowego. Dokładne procedury mogą różnić się w zależności od wersji Tomcata
(lub innego opartego na Javie serwera WWW), idea jednak pozostaje ta sama:
1. Ustawienie ścieżki klas programu uruchomieniowego tak, aby mogła odnaleźć klasy i pliki JAR
konieczne do uruchomienia Tomcata. Można sprawdzić informacje wyświetlane podczas startu oraz
skrypty startowe serwera (tomcat.sh i tomcat.bat), aby uzyskać pomoc w określeniu ścieżki klas.
2. Ustawienie ścieżki klas programu uruchomieniowego tak, aby mógł on również odnaleźć serwlety i
klasy wspierające, co oznacza zazwyczaj katalog WEB-INF/classes i pliki w katalogu WEB-INF/lib.
Zazwyczaj te katalogi i pliki JAR nie powinny znajdować się w ścieżce klas, ponieważ uniemożliwia to
przeładowywanie serwletów. To dołączenie jednak jest przydatne przy usuwaniu błędów. Umożliwia
ono programowi uruchomieniowemu ustawienie punktów kontrolnych w serwlecie zanim mechanizm
ładujący serwlety odpowiedzialny za pobieranie klas z WEB-INF załaduje serwlet.
3. Po właściwym ustawieniu ścieżki klas należy rozpocząć usuwanie błędów poprzez uruchomienie klasy
serwera zawierającej podstawową metodę main(). W przypadku Tomcata 3.2 klasa ta to
org.apache.tomcat.startup.Tomcat. Inne serwery oparte na Javie i przyszłe wersje
Tomcata mogą wykorzystywać inne klasy. Informacje na temat nazwy podstawowej klasy powinny
znajdować się w skrypcie startowym.
4. Tomcat może nakazywać ustawienie konkretnych właściwości systemu lub zmiennych środowiskowych.
Na przykład, Tomcat 3.2 poszukuje własności systemu tomcat.home lub zmiennej środowiskowej
TOMCAT_HOME w celu określenia swojego podstawowego katalogu. Proszę ustawić je, jeżeli okaże się
to konieczne.
5. Ustawienie punktów kontrolnych w dowolnym serwlecie, w którym powinny zostać usunięte błędy, a
następnie wykorzystanie przeglądarki WWW w celu wykonania żądania do HttpServer o dany
serwlet (http://localhost:8080/servlet/SerwletDoSprawdzenia). Wykonywanie powinno być przerywane
w punktach kontrolnych.
Duża część graficznych programów uruchomieniowych ukrywa te szczegóły i umożliwia zintegrowane
sprawdzanie serwletów wykorzystując wbudowany serwer do uruchamiania serwletów. Często dobrze jest jednak
znać ręczną procedurę, ponieważ serwery dołączane do graficznych programów uruchomieniowych są zazwyczaj
nieco przestarzałe.
Niektóre programy dołączane do kontenerów serwletów (które normalnie działają jedynie w połączeniu z
serwerem WWW nie utworzonym w Javie) posiadają samodzielne wersje utworzone w czystej Javie
zaprojektowane specjalnie do wykorzystania podczas usuwania błędów, jak opisano powyżej. Zaletą
wykorzystania tych serwerów, kiedy są one dostępne, jest możliwość przesuwania dowolnych własnych plików
konfiguracyjnych z serwera zewnętrznego do środowiska testowego i z powrotem.

Analiza żądania klienta


Czasami kiedy serwlet nie zachowuje się w spodziewany sposób, warto jest przyjrzeć się surowemu żądaniu
HTTP, na które serwlet ten odpowiada. Osoby znające strukturę HTTP mogą odczytać żądanie i zobaczyć
dokładnie, w którym miejscu serwlet mógł się pomylić40. Jednym ze sposobów dojrzenia surowego żądania jest
zamiana procesu serwera WWW na własną aplikację, która wyświetla wszystkie otrzymane informacje. Przykład
19.8 przedstawia taki serwer.
Przykład 19.8.
Przechwytywanie żądania klienta
import java.io.*;
import java.net.*;
import java.util.*;

public class PatrzPort {

private static void printUsage() {


System.out.println("zastosowanie: java PatrzPort port");
}

public static void main(String[] arg) {


if (arg.length < 1) {
printUsage();
return;
}

// Pierwszym argumentem jest port na którym należy nasłuchiwać


int port;
try {
port = Integer.parseInt(arg[0]);
}
catch (NumberFormatException w) {
printUsage();
return;
}

try {
// Ustanowienie portu serwera przyjmującego połączenia klienta
// Każde połączenie przekazywane do wątku obsługującego
ServerSocket ss = new ServerSocket(port);
while (true) {
Socket zadanie = ss.accept();
new WatekObslugi(zadanie).start();
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}

class WatekObslugi extends Thread {

Socket s;

public WatekObslugi(Socket s) {
this.s = s;
}

public void run() {


try {
// Wyświetlenie każdego bajtu wychodzącego z portu
InputStream in = s.getInputStream();
byte[] bajty = new byte[1];
while ((in.read(bajty)) != -1) {

40
Oczywiście jeżeli sprawdzenia dokonuje osoba nie znająca struktury HTTP, to ona może się pomylić, W tym przypadku
warto jest przeczytać wprowadzenie do HTTP w rozdziale 2, „Podstawy serwletów HTTP” oraz książkę „HTTP Pocket
Reference” autorstwa Clintona Wonga (O'Reilly).
System.out.print((char)bajty[0]);
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
Nasłuchiwanie na porcie 8080 można rozpocząć przy pomocy następującego polecenia powłoki:
java PatrzPort 8080
Proszę zauważyć, że na jednym porcie nie mogą nasłuchiwać równocześnie dwie aplikacje, tak więc należy
upewnić się, że na wybranym porcie nie nasłuchuje żaden inny serwer. Po uruchomieniu serwera można do niego
przekierować żądania HTTP tak, jakby był on zwykłym serwerem WWW. Na przykład, można wykorzystać
przeglądarkę WWW do obejrzenia strony http://localhost:8080. Kiedy PatrzPort otrzymuje żądanie HTTP
przeglądarki, wysyła żądanie do standardowego urządzenia wyświetlającego, aby można je było obejrzeć,
Przeglądarka może być zajęta czekaniem na odpowiedź, która nigdy nie nadejdzie, Można to zakończyć przez
naciśnięcie przycisku Stop.
Poniżej przedstawiono przykładowy wynik uruchomienia PatrzPort, który pokazuje szczegóły żądania GET
na http://localhost:8080:
GET / HTTP 1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.7 [pl] (X11; U; IRIX 6.2 IP22)
Pragma: no-cache
Host: localhost:8080
Accept: image.gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: JSESSIONIN=To1010mC10934500694587412At

Utworzenie własnego żądania klienta


Oprócz przechwytywania i przeglądania żądań HTTP klienta, przydatne może okazać się utworzenie własnego
żądania HTTP. Można to wykonać poprzez połączenie się z portem serwera, na którym serwer nasłuchuje, a
następnie wprowadzenie właściwie uformowanego żądania HTTP. W celu ustanowienia połączenia można
wykorzystać program telnet, dostępny we wszystkich komputerach Uniksowych i większości komputerów
Windows posiadających funkcje sieciowe. Program telnet przyjmuje jako argumenty nazwę komputera i numer
portu, z którym powinien się połączyć. Po połączeniu można wykonać żądanie, które będzie wyglądać podobnie
do przedstawionego w poprzednim podrozdziale. Na szczęście własne żądanie może być o wiele prostsze —
należy określić jedynie pierwszą linię, określającą cel żądania oraz ostatnią, która musi być pustą linią
wskazującą na koniec żądania. Na przykład:
% telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost
Escape character is '^]'.
GET /servlet/PrzeglParametr?nazwa=wartosc HTTP/1.0

http/1.1 200 OK.


Server: Tomcat Web Server/3,2
Content-Type: text/plain
Connection: close
Date: Sun, 25 Jun 2000 20:29:06 GMT

Wuery String:
Nazwa=wartosc

Request Parameters
Nazwa (0): wartosc
Connection closed by foreign host.
Jak często się to zdarza, Windows zachowuje się nieco inaczej, niż jest to pożądane. Domyślny program
telnet.exe Windows 95/98/NT nadaje złą formę wielu odpowiedziom serwerów WWW, ponieważ nie rozumie,
że w sieci WWW znak końca linii powinien być traktowany jako koniec linii i powrót karetki. Zamiast telnet.exe,
programiści Windows mogą wykorzystać zachowujący się bardziej odpowiednio program Javy przedstawiony w
przykładzie 19.9.
Przykład 19.9.
Inny sposób łączenia się z serwerem WWW
import java.io.*;
import java.net.*;
import java.util.*;

public class KlientHttp {

private static void printUsage() {


System.out.println("usage: java KlientHttp komputer port");
}

public static void main(String[] arg) {


if (arg.length < 2) {
printUsage();
return;
}

// Komputer to pierwszy parametr, port to drugi


String komp = arg[0];
int port;
try {
port = Integer.parseInt(arg[1]);
}
catch (NumberFormatException w) {
printUsage();
return;
}

try {
// Otwarcie portu do serwera
Socket s = new Socket(host, port);

// Rozpoczęcie wątku wysyłającego wpisy z klawiatury do serwera


new ZarzadWpisKlaw(System.in, s).start();

// Teraz wyświetlenie wszystkich informacji otrzymanych z serwera


BufferedReader in =
new BufferedReader(new InputStreamReader(s.getInputStream()));
String linia;
while ((linia = in.readLine()) != null) {
System.out.println(linia);
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}

class ZarzadWpisKlaw extends Thread {

InputStream in;
Socket s;

public ZarzadWpisKlaw(InputStream in, Socket s) {


this.in = in;
this.s = s;
setPriority(MIN_PRIORITY); // Odczyty z portu powinny posiadać wyższy
priorytet
// Niestety nie można użyć select() !
setDaemon(true); // Pozwolenie na śmierć aplikacji nawet jeżeli ten wątek
pracuje
}

public void run() {


try {
BufferedReader klaw = new BufferedReader(new InputStreamReader(in));
PrintWriter serwer = new PrintWriter(s.getOutputStream());

String linia;
System.out.println("Połączono… Proszę wprowadzić żądanie HTTP");
System.out.println("------------------------------------------");
while ((linia = klaw.readLine()) != null) {
server.print(linia);
server.print("\r\n"); // linia HTTP kończy się \r\n
server.flush();
}
}
catch (Exception w) {
w.printStackTrace();
}
}
}
Powyższy program KlientHttp działa podobnie do telnet:
% java KlientHttp localhost 8080
Połączono… Proszę wprowadzić żądanie HTTP
------------------------------------------
GET /index.html HTTP/1.0

HTTP /1.0 200 OK.


Content-Type: text/html
Content-Length: 2582
Last-Modified: Fri, 15 Sep 2000 22:20:15 GMT
Servlet-Engine: Tomcat Web Server/3.2 (JSP 1.1; Servlet 2.2; Java 1.2.2;
Windows NT 4.0 x86; java.vendor=Sun Microsystems Inc.)

<!doctype html public "-//w3c//dtd html 4.0 transitional//en">


<html>
<head>
<meta http-equiv="Content-Type" content="text/html"; charset=iso-8859-1">
<title>Tomcat v3.2</title>
</head>
...

Wykorzystanie niezależnego narzędzia


Niezależne narzędzia dają nowe możliwości i łatwość wykorzystania zadaniu usuwania błędów. IBM
AlphaWorks produkuje program o nazwie Distributed Application Tester (DAT), który przegląda żądania HTTP
i HTTPS, umożliwiając przeglądanie i zapisywanie obu stron ruchu klient-serwer. DAT zawiera możliwość
wykonywania testów funkcjonalności wydajności aplikacji WWW poprzez automatyczne generowanie żądań i
przeglądanie odpowiedzi. Program jest utworzony całkowicie w Javie, ale jest udostępniany z programem
instalacyjnym działającym jedynie pod Windows. Jego jedyną licencją jest bezpłatny 90-dniowy czas testowania,
ponieważ oprogramowanie to występuje w wersji „alpha”, a co dziwne w wersji tej jest od stycznia 199. DAT
jest dostępny pod adresem http://www.alphaworks.ibm.com.
Firma Allaire, twórca popularnej „wtyczki” serwletów Jrun (po wykupieniu Live Software), posiada mało znane
narzędzie służące do usuwania błędów z serwletów o nazwie ServletDebugger. Narzędzie to jest zaprojektowane
programowego wspomagania testów i usuwania błędów z serwletów. ServletDebugger nie wymaga
wykorzystania serwera WWW lub przeglądarki do wykonania żądania. Zamiast tego, wykorzystuje się zbiór klas
do stworzenia niewielkiej klasy-końcówki, która przygotowuje i wykonuje żądanie serwletu. Końcówka określa
wszystko — parametry inicjacji serwletu, nagłówki HTTP żądania oraz parametry żądania. ServletDebugger jest
stosunkowo prosty i dobrze przystosowany do zautomatyzowanego testowania. Jego największą wadą jest
konieczność wykonania sporej ilości pracy w celu właściwego przygotowania realistycznego żądania.
ServletDebugger można odnaleźć w cenniku Allaire pod adresem http://www.allaire.com41.

Ostatnie wskazówki
Jeżeli wszystkie powyższe porady nie pomogły w odnalezieniu i usunięciu błędu, proszę obejrzeć poniższe
ostateczne wskazówki dotyczące suwania błędów z serwletów:
• Proszę wykorzystać System.getProperty("java.class.path") przy pomocy serwletu w
celu uzyskania pomocy w rozwiązywaniu problemów związanych ze ścieżką klas. Ponieważ serwlety są
często uruchamiane na serwerach WWW z osadzonymi wirtualnymi maszynami Javy, trudne może być
dokładne określenie ścieżki klas poszukiwanej przez JVM. Określić to może właściwość
java.class.path.
• Proszę pamiętać, że klasy odnalezione w bezpośredniej ścieżce klas serwera
(katalog_macierzysty/classes) przypuszczalnie nie są przeładowywane podobnie jak, w większości
serwerów niezwiązanych z serwletami, klasy wspierające w katalogu klas aplikacji WWW (WEB-
INF/classes). Zazwyczaj przeładowywane są jedynie klasy serwletów w katalogu klas aplikacji WWW.

41
Nie byłoby zaskakujące, jeżeli w najbliższym czasie firma Allaire zrzuciłaby obsługę ServletDebugger. Jeżeli się to zdarzy,
a nawet jeżeli się nie wydarzy, można poszukać wersji Open Source.
• Należy zażądać od przeglądarki pokazania surowej zawartości wyświetlanej strony. Może to pomóc w
zidentyfikowaniu problemów z formatowaniem. Zazwyczaj jest to opcja w menu Widok.
• Należy upewnić się, że przeglądarka nie przechowuje w pamięci podręcznej wyników poprzedniego
żądania poprzez wymuszenie pełnego przeładowania strony. W przeglądarce Netscape Navigator,
należy zastosować Shift-Reload; W Internet Explorer należy zastosować Shift-Refresh.
• Jeżeli pomija się wersję init(), która pobiera ServletConfig należy upewnić się, że pomijająca
metoda od razu wywołuje super.init(config).

Poprawa wydajności
Serwlety poprawiające wydajność wymagają nieco innego podejścia niż aplikacje lub aplety Javy wykonujące to
samo działanie. Powodem tego jest fakt, że JVM uruchamiająca serwlety zazwyczaj obsługuje kilkadziesiąt,
jeżeli nie kilkaset wątków, z których każdy uruchamia serwlet. Te współistniejące serwlety muszą dzielić się
zasobami JVM w sposób inny, niż zwykłe aplikacje. Tradycyjne sztuczki poprawiające wydajność oczywiście
działają, ale posiadają mniejszy wpływ, kiedy zostają wykorzystane w systemie wielowątkowym. Poniżej
przedstawiono niektóre sztuczki najczęściej wykorzystywane przez programistów serwletów.

Tworzyć, ale nie przesadzać


Należy unikać niepotrzebnego tworzenia obiektów. To zawsze była dobra rada — tworzenie niepotrzebnych
obiektów marnuje pamięć i dużą ilość czasu. W przypadku serwletów to jest rada jeszcze lepsza. Tradycyjnie
wiele JVM wykorzystywało globalną stertę obiektów, która musi zostać przypisana do każdej nowej alokacji
pamięci. Kiedy serwlet tworzy nowy obiekt lub alokuje dodatkową pamięć, działania tego nie może wykonać
żaden inny serwlet.

Nie łączyć
Należy unikać łączenia kilku łańcuchów. Zamiast StringBuffer poleca się zastosowanie metody append
(). To również zawsze była dobra rada, ale w przypadku serwletów szczególnie pociągające jest napisanie kodu
przygotowującego łańcuch do późniejszego wyświetlania w następujący sposób:
String wyswietl;
wyswietl += "<TITLE>";
wyswietl += "Witaj, " + uzyt;
wyswietl += "</TITLE>";
Chociaż powyższy kod wygląda miło i sprawnie, w trakcie uruchomienia działa, jak gdyby wyglądał mnie więcej
tak jak poniżej, z nowymi StringBuffer i String tworzonymi w każdej linii:
String wyswietl;
wyswietl = new StringBuffer().append("<TITLE>").toString();;
wyswietl = new StringBuffer(wyswietl).append("Witaj, ").toString();
wyswietl = new StringBuffer(wyswietl).append(uzyt).toString();
wyswietl = new StringBuffer(wyswietl).append("</TITLE>").toString();
Kiedy wydajność jest ważną kwestią, należy przepisać oryginalny kod tak, aby wyglądał jak poniższy, tak aby
utworzone zostały tylko pojedyncze StringBuffer i String:
StringBuffer buf = new StrngBuffer();
buf.append("<TITLE>");
buf.append("Witaj, "").append(uzyt);
buf.append("</TITLE>");
wyswietl = buf.toString();
Proszę zauważyć, że jeszcze bardziej wydajne jest zastosowanie tablicy bajtów.

Ograniczać synchronizację
Należy synchronizować bloki, kiedy jest to konieczne, ale nic poza tym. Każdy zsynchronizowany blok w
serwlecie wydłuża czas odpowiedzi serwletu. Ponieważ ten sam egzemplarz serwletu może obsługiwać wiele
współbieżnych żądań, musi, oczywiście, zająć się ochroną swojej klasy i zmiennych egzemplarza przy pomocy
zsynchronizowanych bloków. Jednak przez cały czas jeden wątek żądania znajduje się w zsynchronizowanym
bloku i żaden inny wątek nie może zostać do bloku wprowadzony. W związku z tym najlepiej jest uczynić te
bloki jak najmniejszymi.
Należy także przyjrzeć się najgorszemu z możliwych wyników sporu między wątkami. Jeżeli najgorszy
przypadek jest znośny (jak w przykładzie licznika w rozdziale 3, „Okres trwałości serwletów”), można rozważyc
całkowite usuniecie bloków synchronizacji. Można również rozważyć zastosowanie interfejsu znaczników
SingleThreadModel, w którym serwer zarządza pulą egzemplarzy serwletów, aby zagwarantować, że każdy
egzemplarz jest wykorzystywany przez co najwyżej jeden wątek w jednym czasie. Serwlety będące
implementacją SingleThreadModel nie muszą synchronizować dostępu do swoich zmiennych egzemplarza.
W końcu należy również pamiętać, że java.util.Vector i java.util.Hashtable są zawsze
wewnętrznie zsynchronizowane, podczas gdy równoważne im java.util.ArrayList i
java.util.HashMap, wprowadzone w JDK 1.2, nie są zsynchronizowane, jeżeli nie nastąpi odpowiednie
żądanie. Tak wiec jeżeli dany Vector lub Hashtable nie potrzebuje synchronizacji, można go zastąpić
ArrayList lub HashMap.

Buforować dane wprowadzane i wyświetlane


Należy buforować dane wprowadzane i wyświetlane, wszystkie pliki magazynowe, wszystkie potoki pobrane z
bazy danych itd. To prawie zawsze podnosi wydajność, ale poprawa ta może być szczególnie widoczna w
przypadku serwletów. Jej powód jest taki, że odczyt i zapis jednego elementu za jednym razem może spowolnić
cały serwer w związku z koniecznością dokonywania częstych przełączeń kontekstu. Na szczęście ogólnie
buforowanie podczas zapisu do PrintWriter lub ServletOutputStream lub podczas odczytywania z
BufferedReader lub ServletInputStream nie jest konieczne. Większość implementacji serwerów
sama buforuje te potoki.

Spróbować wykorzystania OutputStream


W przypadku stron WWW wykorzystujących kodowanie znaków Latin-1, technicznie możliwe jest
wykorzystanie PrintWriter lub ServletOutputStream. Rekomendowanym podejściem jest
wykorzystanie PrintWriter, ponieważ obsługuje on internacjonalizację, ale na niektórych serwerach
wykorzystanie ServletOutputStream powoduje zauważalny wzrost wydajności, a
ServletOutputStream posiada także ułatwiające pracę metody print() i println() będące
dziedzictwem Servlet API 1.0, w którym nie występowała opcja PrintWriter. Proszę jednak uważać. W
przypadku wielu serwerów zależność jest odwrotna i to PrintWriter powoduje wyższą wydajność. Osoby,
które nie są pewne swojej platformy programistycznej i nie przeprowadzały porównywalnych prób czasowych
powinny trzymać się PrintWriter.

Wykorzystać narzędzie profilujące

Dostępnych jest kilka narzędzi profilujących Javy które mogą wspomóc w odnalezieniu wąskich gardeł w kodzie.
Większość problemów z wydajnością w Javie działającej po stronie serwera jest powodowana nie przez język
czy JVM, ale kilka wąskich gardeł. Sztuką jest odnalezienie tych wąskich gardeł. Narzędzia analizujące pracują
w tle, obserwując obsługę żądań przez serwer i zwracając dokładne podsumowanie spędzonego czasu, a także
opis alokacji pamięci. Dwa popularne narzędzia to OptimizeIT! Firmy Intuitive Systems
(http://www.optimizeit.com) i Jprobe formy Sitraka, dawniej KL Group (http://sitraka.com/jprobe). Duża ilość
JVM może przyjmować również znaczniki z linii poleceń (-prof w JDK 1.1 i Xrunhproof() w JDK 1.2).
Aby uruchomić obciążony serwer, można wykorzystać narzędzie takie jak Apache JMeter
(http://java.apache.org).
Rozdział 20. Zmiany w Servlet

API 2.3

Krótko przed przekazaniem niniejszej książki do druku firma Sun Microsystems opublikowała Proposed Final
Draft (Proponowaną Ostateczną Próbę) specyfikacji Servlet API 2.342. Nie jest to ostateczna wersja specyfikacji;
Proposed Final Draft to krok do formalnej Final Release (Wersji Ostatecznej), tak więc szczegóły techniczne
ciągle mogą ulec zmianie. Jednak zmiany te nie powinny być znaczące — tak naprawdę producenci serwerów już
rozpoczęli implementację nowych funkcji. W niniejszym rozdziale opisane zostaną w skrócie wszystkie zmiany,
jakie zaszły pomiędzy API 2.2 i 2.3. Wyjaśnione także zostaną powody zmian i przedstawione zostaną sposoby
tworzenia serwletów przy pomocy nowych funkcji43.

Zmiany w Servlet API 2.3


Servlet API 2.3 właściwie pozostawia nietknięte jądro serwletów, co oznacza, że serwlety osiągnęły wysoki
poziom dojrzałości. Większość działań związana była z dodaniem nowych funkcji poza jądrem. Zmiany te to
między innymi:
• Serwlety wymagają teraz JDK 1.2 lub późniejszego.
• Utworzony został (nareszcie) mechanizm filtrów.
• Dodane zostały nowe zdarzenia okresu trwałości aplikacji.
• Dodana została nowa obsługa internacjonalizacji.
• Sformalizowana została technika wyrażania zależności pomiędzy plikami JAR.
• Wyjaśnione zostały zasady ładowania klas.
• Dodane zostały nowe atrybuty błędów i bezpieczeństwa.
• Opuszczona została klasa HttpUtils.
• Dodane zostały różne nowe pomocne metody.
• Rozszerzone i wyjaśnione zostało kilka zachowań DTD.
Wykonane zostały także inne wyjaśnienia, ale skupiają się one głównie na producentach serwerów, nie ogólnie
na programistach serwletów (poza faktem, że programiści ujrzą poprawioną przenośność), tak więc te szczegóły
zostaną tu ominięte.

42
Chociaż specyfikacja ta została opublikowana przez firmę Sun, Servlet API 2.3. został tak naprawdę utworzony przez
wiele osób i przedsiębiorstw działających jako grupa ekspertów JSR-053, zgodnie z procesem Java Community Process
(JCP) 2.0. Ta grupa ekspertów działała pod przewodnictwem Danny'ego Cowarda z Sun Microsystems.
43
Niniejszy materiał pojawił się po raz pierwszy w artykule „Servlet 2.3: New Features Exposed” autorstwa Jasona Huntera,
opublikowanym przez JavaWorld (http://www.javaworld.com), własność ITworld.com, Inc., styczeń 2001. Przedruk za
pozwoleniem. (Proszę zobaczyć także http://www.javaworld.com/j2-01-2001/jw-0126-servletapi.html).
Przed rozpoczęciem przyglądania się zmianom należy zaznaczyć, że wersja 2.3 została udostępniona jedynie
jako specyfikacja próbna. Większość opisanych tu funkcji nie będzie działać na wszystkich serwerach. Aby
przetestować te funkcje, poleca się pobranie oficjalnego wzorcowego serwera, Apache Tomcat 4.0. Jest to Open
Source i może być pobrany za darmo. Tomcat 4.0 jest aktualnie dostępny w wersji beta; obsługa Servlet API 2.3
staje się coraz lepsza, ale ciągle jest niekompletna. Proszę przeczytać plik NEW_SPECS.txt dołączony do
Tomcata 4.0 w celu poznania jego poziomu wsparcia dla wszystkich funkcji nowej specyfikacji.

Serwlety w J2SE i J2EE


Jedną z pierwszych rzeczy, jakie zazwyczaj zauważa się na temat Servlet API 2.3 jest fakt, że serwlety zależą
teraz od platformy Java 2, Standard Edition (znanej także jako J2SE 1.2 lub JDK 1.2). Ta mała, ale ważna
zmiana oznacza, że w serwletach można teraz wykorzystywać funkcje J2SE 1.2 z gwarancją, że serwlety te będą
działać na wszystkich kontenerach. Poprzednio funkcje J2SE mogły być wykorzystywane, ale ich obsługa przez
serwery nie była obowiązkowa.
Servlet API2.3 ma stać się częścią platformy Java 2, Enterprise Edition 1.3 (J2EE 1.3). Poprzednia wersja,
Servlet API 2.2 była częścią J2EE 1.2. Jedyną zauważalną różnicą jest fakt dodania kilku stosunkowo
obskurnych znaczników deskryptora związanych z J2EE w deskryptorze web.xml — <resource-env-ref>
obsługującego „obiekty administrowane”, takie jak te wymagane przez Java Messaging System (JMS), <res-
ref-sharing-scope> pozwalający na współdzielony lub wyłączny dostęp do odwołania do zasobów oraz
<run-as> określający identyfikator bezpieczeństwa dla wywołującego EJB. Większość autorów serwletów nie
powinna przejmować się tymi znacznikami J2EE; pełny ich opis dostępny jest w specyfikacji J2EE 1.3.

Filtry
Najbardziej znaczącą częścią API 2.3 jest dodanie filtrów. Filtry są obiektami potrafiącymi zmieniać kształt
żądania lub modyfikować odpowiedź. Proszę zauważyć, że nie są to serwlety; tak naprawdę nie tworzą one
odpowiedzi. Przetwarzają one wstępnie żądanie zanim dotrze ono do serwletu, oraz przetwarzają odpowiedź
opuszczającą serwlet. W skrócie, filtry są dojrzałą wersją starego pomysłu „łańcuchów serwletów”. Filtr potrafi:
• Przechwycić wywołanie serwletu przed jego wywołaniem.
• Sprawdzić żądanie przed wywołaniem serwletu.
• Zmodyfikować nagłówki i dane żądania poprzez dostarczenie własnej wersji obiektu żądania, który
zostaje dołączony do prawdziwego żądania.
• Zmodyfikować nagłówki i dane odpowiedzi poprzez dostarczenie własnej wersji obiektu odpowiedzi,
który zostaje dołączony do prawdziwej odpowiedzi.
• Przechwycić wywołanie serwletu po jego wywołaniu.
Filtr może być skonfigurowany tak, by działał jako serwlet lub grupa serwletów; ten serwlet lub grupa może
zostać przefiltrowana przez dowolną ilość filtrów. Niektóre praktyczne idee filtrów to między innymi filtry
uwierzytelniania, filtry logowania i nadzoru, filtry konwersji obrazków, filtry kompresji danych, filtry
kodowania, filtry dzielenia na elementy, filtry potrafiące wywołać zdarzenia dostępu do zasobów, filtry XSLT
przetwarzające zawartość XML lub filtry łańcuchowe typu MIME (podobne do łańcuchów serwletów).
Filtr jest implementacją javax.servlet.filter i definiuje trzy metody tej klasy:
void setFilterConfig(FilterConfig konfig)
Ta metoda ustawia obiekt konfiguracyjny filtra.
FilterConfig getFilterConfig()
Ta metoda zwraca obiekt konfiguracyjny filtra.
void doFilter(ServletRequest zad, ServletResponse odp, FilterChain lancuch)
Ta metoda wykonuje właściwą pracę filtra.
Serwer wywołuje jeden raz setFilterConfig() w celu przygotowania filtru do działania, następnie
wywołuje doFilter() dowolną ilość razy dla różnych obiektów. Interfejs FilterConfig posiada metody
pobierające nazwę filtra, jego parametry inicjacji oraz aktywny kontekst filtra. Serwer może również przekazać
setFilterConfig() wartość null aby wskazać, że filtr zostaje wyłączony.
UWAGA!!!!

Przewiduje się, że w ostatecznej wersji 2.3 metoda getFilterConfig() zostanie usunięta, a metoda
setFilterConfig(FilterConfig konfig)zostanie zastąpiona przez init(FilterConfig) i
destroy().

Każdy filtr pobiera aktualne żądanie i odpowiedź w swojej metodzie doFilter(), a także FilterChain
zawierający filtry, które muszą stale być przetwarzane. W metodzie doFilter() filtr może wykonać dowolne
działanie żądania i odpowiedzi. (Na przykład może zbierać dane przez wywoływanie ich metod, dołączać
informacje nadające im nowe zachowanie). Filtr następnie wywołuje lancuch.doFilter() w celu
przekazania kontroli następnemu filtrowi. Kiedy wywołanie to wraca, filtr może, na końcu swojej własnej
metody doFilter(), wykonać dodatkowe działania na odpowiedzi; na przykład może zapisać informacje na
temat odpowiedzi w dzienniku. Jeżeli filtr chce całkowicie zatrzymać przetwarzanie żądania i zyskać pełną
kontrolę nad odpowiedzią, może specjalnie nie wywoływać następnego filtra.
Filtr może zmieniać obiekty żądania i odpowiedzi w celu dostarczenia własnego zachowania, zmiany konkretnej
metody wywołują implementację wpływającą na późniejsze działania obsługujące żądania. Serwlet API 2.3
dostarcza nowych klas HttpServletRequestWrapper i HttpServletResponseWrapper w tym
pomagających. Posiadają one domyślną implementację wszystkich metod żądań i odpowiedzi oraz domyślnie
przekazują wywołania do oryginalnych żądań i odpowiedzi. Poniżej przedstawiony jest kod prostego filtra
logowania, który zapamiętuje czas trwania wszystkich żądań:
import javax.servlet.*;
import javax.servlet.http.*;

public class FiltrDziennik implements Filter {

FilterConfig konfig;

public void setFilterConfig(FilterConfig konfig) {


this.konfig = konfig;
}

public FilterConfig getFilterConfig() {


return konfig;
}

public void doFilter(ServletRequest zad,


ServletResponse odp,
FilterChain lancuch) {
ServletContext kontekst = getFilterConfig().getServletContext();
long przed = System.currentTimeMillis();
lancuch.doFilter(zad, odp); // nie jest potrzebny żaden parametr łańcucha
long po = System.currentTimeMillis();
kontekst.log("Żądanie" + zad.getRequestURI() + ": " +
(przed-po));
}
}
Kiedy serwer wywołuje setFilterConfig(), filtr zapamiętuje w swojej zmiennej konfig odwołanie do
konfiguracji filtra, które później zostanie wykorzystane w metodzie doFilter() w celu pobrania
ServletContext. Logika doFilter() jest prosta — obliczenie czasu obsługi żądania i zapamiętanie czasu
po zakończeniu przetwarzania. Aby wykorzystać powyższy filtr, trzeba wcześniej zadeklarować go w
deskryptorze DTD przy pomocy znacznika <filter>, jak przedstawiono poniżej:
<filter>
<filter-name>
dziennik
</filter-name>
<filter-class>
FiltrDziennik
</filter-class>
</filter>
Powyższy fragment kodu przekazuje serwerowi informację, że filtr o nazwie dziennik jest zaimplementowany
w klasie FiltrDziennik. Można przypisać zarejestrowany filtr do konkretnych wzorów URL-i lub nazw
serwletów przy pomocy znacznika <filter-mapping>:
<filter-mapping>
<filter-name>log</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Powyższy fragment konfiguruje filtr tak, aby był stosowany do wszystkich żądań serwera (statycznych lub
dynamicznych), co jest działaniem odpowiednim dla filtra zapisującego informacje w dzienniku. Jeżeli nastąpi
połączenie, dziennik mógłby wyglądać następująco:
Żądanie /indeks.jsp: 10

Zdarzenia okresu trwałości


Drugą bardzo poważną zmianą w Servlet API 2.3 jest dodanie zdarzeń okresu trwałości aplikacji, które
umożliwiają powiadomienie obiektów „nasłuchujących", kiedy inicjowane bądź niszczone są konteksty
serwletów i sesje, a także kiedy dodawane lub usuwane z kontekstu lub sesji są atrybuty.
Okres trwałości serwletu jest podobny do zdarzeń Swing. Każdy obiekt nasłuchujący zainteresowany obserwacją
okresu trwałości ServletContext może zaimplementować interfejs ServletContextListener.
Interfejs ten posiada dwie metody:
void contextInitialized(ServletContextEvent z)

Wywoływana z aplikacji WWW, kiedy jest ona po raz pierwszy


gotowa o przetwarzania żądań (tzn. podczas uruchomienia
serwera lub dodania albo przeładowania kontekstu).
Żądania nie są obsługiwane, dopóki metoda nie zwróci
swoich wartości.
void contextDestroyed(ServletContextEvent z)

Wywoływana z aplikacji WWW, która powinna zostać zakończona


(tzn. podczas wyłączania serwera lub usuwania albo
przeładowania kontekstu). Obsługa żądań zostaje
zatrzymana przed wywołaniem tej metody.
Klasa ServletContextEvent przekazywana obu metodom zawiera jedynie metodę
pobierzZrodloKontekst(), która zwraca kontekst, który jest inicjowany lub niszczony.
Obiekt nasłuchujący zainteresowany obserwacją okresem trwałości atrybutu ServletContext może
zaimplementować interfejs ServletContextAttributesListener, który posiada trzy metody:
void attributeAdded(ServletContextAttributeEvent z)
Wywoływana, kiedy atrybut zostaje dodany do kontekstu serwletu.
void attributeRemoved(ServletContextAttributeEvent z)

Wywoływana, kiedy atrybut zostaje usunięty z kontekstu


serwletu.
void attributeReplaced(ServletContextAttributeEvent z)

Wywoływana, kiedy atrybut w kontekście serwletu zostaje


wymieniony na inny.
Klasa ServletContextAttributeEvent jest rozszerzeniem ServletContextEvent i dodaje metody
getName() i getValue(), tak więc obiekt nasłuchujący może uzyskać informacje na temat zmieniających
się atrybutów. Jest to przydatna własność, ponieważ aplikacje WWW które muszą zsynchronizować stan
aplikacji (atrybuty kontekstu) z czymś w rodzaju bazy danych mogą to teraz uczynić w jednym miejscu.
Model obiektu nasłuchującego sesji jest podobny do modelu obiekt nasłuchującego okresu trwałości. W modelu
sesji występuje interfejs HttpSessionListener posiadający dwie metody:
void SessionCreated(HttpSessionEvent z)
Wywoływana podczas tworzenia sesji.
void SessionDestroyed(HttpSessionEvent z)

Wywoływana podczas zniszczenia (unieważnienia) sesji.


Powyższe metody przyjmują egzemplarz HttpSessionEvent z metodą dostępu getSession() w celu
zwrócenia sesji, która jest tworzona bądź niszczona. Metody te mogą być zastosowane podczas implementacji
interfejsu administratora, który śledzi wszystkich aktywnych użytkowników w aplikacji WWW.
Model sesji posiada również interfejs HttpSessionAttributesListener posiadający trzy metody.
Metody te informują obiekt nasłuchujący, kiedy zmieniają się atrybuty i mogłyby być zastosowane, na przykład
przez aplikację synchronizującą dane profilu przechowywane w sesji do bazy danych:
void attributeAdded(HttpSessionBindingEvent z)
Wywoływana, kiedy atrybut zostaje dodany do sesji.
void attributeRemoved(HttpSessionBindingEvent z)

Wywoływana, kiedy atrybut zostaje usunięty z sesji.


void attributeReplaced(HttpSessionBindingEvent z)

Wywoływana, kiedy atrybut w sesji zostaje wymieniony na inny.


Jak można się było spodziewać, klasa HttpSessionBindingEvent jest rozszerzeniem
HttpSessionEvent i dodaje metody getName() i getValue(). Jedyna dość niezwykła własność jest
taka, że klasa zdarzeń nosi nazwę HttpSessionBindingEvent, a nie
HttpSessionAttributeEvent. Dzieje się tak z powodu tradycji — API posiadał już klasę
HttpSessionBindingEvent, tak więc została ona wykorzystana ponownie. Ten nieco mylący aspekt API
może zostać poprawiony w ostatecznej wersji.
Możliwym praktycznym zastosowaniem dla zdarzeń okresu trwałości jest współdzielone połączenie z bazą
danych zarządzane przez obiekt nasłuchujący kontekstu. Obiekt ten deklarowany jest w pliku web.xml, w
następujący sposób:
<listener>
<listener-class>
com.firma.MojZarzadcaPolaczen
</listener-class>
</listener>
Serwer tworzy egzemplarz klasy nasłuchującej służącej do otrzymywania zdarzeń i wykorzystuje introspekcje w
celu określenia, jaki interfejs (lub interfejsy) nasłuchu powinien zostać wykorzystany przez klasę. Należy
pamiętać, że ponieważ mechanizm nasłuchujący jest konfigurowany w deskryptorze, można dodawać nowe
mechanizmy bez konieczności zmiany kodu. Sam obiekt nasłuchujący mógłby wyglądać następująco:
import javax.servlet.*;
import javax.servlet.http.*;

public class MojZarzadcaPolaczen implements ServletContextListener {

public void contextInitialized(ServletContextEvent z) {


Connection pol = // utworzenie połączenia
z.getServletContext().setAttribute("pol", pol);
}

public void contextDestroyed(ServletContextEvent z) {


Connection pol =
(Connection) z.getServletContext().getAttribute("pol");
try { pol.close(); }
catch (SQLException ignored) { } // zamknięcie połączenia
}
}
Powyższy obiekt nasłuchujący zapewnia dostępność połączenia z bazą danych w każdym nowym kontekście
serwletu oraz zamknięcie wszystkich połączeń po zamknięciu kontekstu.
Interfejs HttpSessionActivationListener, kolejny nowy interfejs nasłuchu w API 2.3 jest
zaprojektowany do obsługi sesji wędrujących z jednego serwera do drugiego. Obiekt nasłuchujący będący
implementacją HttpSessionActivationListener jest powiadamiany o gotowości każdej sesji do
wędrówki oraz kiedy sesja gotowa jest do aktywacji na drugim komputerze. Metody te dają aplikacji szansę
przesuwania danych niemożliwych do zserializowania pomiędzy wirtualnymi maszynami Javy, lub doklejania i
odklejania obiektów zserializowanych pomiędzy pewnego rodzaju modelem obiektów przed lub po migracji.
Interfejs ten posiada dwie metody:
void sessionWillPassivate(HttpSessionEvent z)

Sesja ma zamiar dokonać wędrówki. Sesja jest już niedostępna


kiedy następuje to wywołanie.
void sessionDidActivate(HttpSessionEvent z)

Sesja została aktywowana. Sesja nie jest jeszcze w użytku,


kiedy następuje to wywołanie.
Taki obiekt nasłuchujący jest rejestrowany tak, jak inne. Jednak różnica pomiędzy nimi jest taka, że wywołania
migracji i aktywacji najprawdopodobniej będą występować na dwóch różnych serwerach!

Wybranie kodowania znaków


Servlet API 2.3 dostarcza tak bardzo potrzebnej obsługi formularzy wysyłanych w różnych językach. Nowa
metoda, zadanie.setCharacterEncoding(String kodowanie) pozwala na poinformowanie
serwera o kodowaniu znaków żądania. Kodowanie znaków (albo po prostu kodowanie) to sposób odwzorowania
bajtów na znaki. Serwer może wykorzystać określone kodowanie, aby prawidłowo zanalizować parametry i dane
POST.
Domyślnie serwer analizuje parametry przy pomocy popularnego kodowania Latin-1 (ISO 8859-1) Niestety jest
ono odpowiednie tylko dla języków zachodnioeuropejskich. Kiedy przeglądarka wykorzystuje inne kodowanie,
powinna wysyłać informacje o kodowaniu w nagłówku żądania Content-Type, ale prawie żadna przeglądarka
nie stosuje się do tej zasady. Metoda setCharacterEncoding() pozwala serwletowi na poinformowanie
serwera, które kodowanie jest wykorzystywane (zazwyczaj jest to kodowanie strony, która zawiera formularz);
serwer zajmuje się resztą. Na przykład, serwlet pobierający japońskie parametry z formularza zakodowanego
przy pomocy Shift_JIS powinien odczytywać parametry w następujący sposób:
// ustawienie kodowania na Shift_JISD
zad.setCharacterEncoding("Shift_JIS");

// Odczytanie parametru przy pomocy tego kodowania


String nazwa = zad.getParameter("nazwa");
Proszę pamiętać o ustawieniu kodowania przed wywołaniem getParameter() lub getReader().
Wywołanie setCharacterEncoding() może zgłosić wyjątek
java.io.UnsupportedEncodingException, jeżeli dane kodowanie nie jest obsługiwane.
Funkcjonalność ta jest również dostępna użytkownikom API 2.2 i wcześniejszych, jako część klasy
com.oreilly.servlet.ParameterParser.

Zależności plików JAR


Plik WAR (plik Web Application Archive — Archiwum Aplikacji WWW, dodane w Servlet API 2.2) często
wymaga istnienia i prawidłowego działania na serwerze innych bibliotek JAR. Na przykład, aplikacja WWW
wykorzystująca klasę ParameterParser wymaga w ścieżce klas serwera pliku cos.jar. Aplikacja WWW
wykorzystująca WebMacro wymaga webmacro.jar.
Przed powstaniem API 2.3, zależności te musiały być udokumentowane (ale czy ktoś tak naprawdę czyta
dokumentację!), lub każda aplikacja WWW musiała zawierać wszystkie wymagane pliki JAR w swoim własnym
katalogu WEB-INF/lib. Servlet API 2.3 pozwala na zadeklarowanie zależności JAR w WAR przy pomocy
pozycji META-INF/MANIFEST.MF archiwum WAR. Jest to standardowy sposób deklarowania zależności
plików JAR, ale od API 2.3 pliki WAR muszą oficjalnie obsługiwać ten mechanizm. Jeżeli zależność nie może
zostać wypełniona, serwer może delikatnie odrzucić aplikację WWW w trakcie tworzenia zamiast wywoływać
obskurne komunikaty o błędach w trakcie uruchomienia. Mechanizm ten pozwala na wysoki stopień ziarnistości.
Na przykład można wyrazić zależność w konkretnej wersji opcjonalnego pakietu, a serwer musi ją odnaleźć przy
pomocy algorytmu wyszukiwania.
Mechanizmy ładowania klas
Nastąpiła tu mała zmiana, posiadająca jednak ogromne znaczenie — w API 2.3, kontener serwletów (czyli
serwer) musi blokować możliwość dostrzeżenia klas implementacji serwera przez klasy aplikacji WWW. Innymi
słowy, mechanizmy ładowania klas powinny być od siebie oddzielone.
Nie brzmi to jak wielka zmiana, ale eliminuje możliwość kolizji pomiędzy klasami aplikacji WWW i klasami
serwera. Stały się one poważnym problemem z powodu konfliktów analizatora XML. Każdy serwer potrzebuje
analizatora XML w celu zinterpretowania plików web.xml, a obecnie wiele aplikacji WWW wykorzystuje
analizator XML do obsługi odczytu, manipulacji i zapisu danych XML. Jeżeli analizatory obsługiwały różne
wersje DOM lub SAX, powodowało to nienaprawialny konflikt. Oddzielenie zakresów klas elegancko ten
problem rozwiązuje.

Nowe atrybuty błędów


Poprzednia wersja interfejsu, Servlet API 2.2 wprowadziła kilka atrybutów żądania, które mogą być
wykorzystywane przez serwlety i strony JSP pełniące funkcję celu zasady <error-page>. Wcześniej w
niniejszej książce opisano, że zasady <error-page> pozwalają na skonfigurowanie aplikacji WWW tak, aby
konkretne kody stanu lub typy wyjątków powodowały wyświetlenie konkretnych stron:
<web-app>
<!-- ..... -->
<error-page>
<error-code>
404
</error-code>
<location>
/404.html
</location>
</error-page>
<error-page>
<exception-type>
javax.servlet.ServletException
</exception-type>
<location>
/servlet/WyswietlBlad
</location>
</error-page>
<!-- ..... -->
</web-app>
Serwlet umieszczony w <location> zasady <error-page> mógł otrzymywać następujące trzy atrybuty:
javax.servlet.error.status_code

Liczba Integer przekazująca kod stanu błędu, jeżeli taki


istnieje.
javax.servlet.error.exception_type

Egzemplarz Class wskazujący na typ wyjątku, który spowodował


błąd, jeżeli taki istnieje.
javax.servlet.error.message

Łańcuch String przekazujący wiadomość o błędzie, przekazywany


do konstruktora wyjątku.
Przy pomocy powyższych atrybutów serwlet mógł wygenerować stronę błędu dostosowaną do błędu, jak
przedstawiono poniżej:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WyswietlBlad extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();
String kod = null, wiadomosc = null, typ = null, uri = null;
Object kodObi, wiadomoscObi, typObi;

// Odczytanie trzech możliwych atrybutów błędów. Niektóre mogą mieć wartość


null
kodObi = zad.getAttribute("javax.servlet.error.status_code");
wiadomoscObi = req.getAttribute("javax.servlet.error.message");
typObi = req.getAttribute("javax.servlet.error.exception_type");

// Konwersja atrybutów na wartości łańcuchowe


// Dlatego, że niektóre stare typy serwerów zwracają typy String
// a nowe typy Integer, String i Class

if (kodObi != null) kod = kodObi.toString();


if (wiadomoscObi != null) wiadomosc = wiadomoscObi.toString();
if (typObi != null) typ = typObi.toString();

// Powód błędu to kod stanu lub typ wyjątku


String powod = (kod != null ? kod : typ);

wyj.println("<HTML>");
wyj.println("<HEAD><TITLE>" + powod + ": " + wiadomosc +
"</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H1>" + powod + "</H1>");
wyj.println("<H2>" + wiadomosc + "</H2>");
wyj.println("<PRE>");
}
wyj.println("</PRE>");
wyj.println("<HR>");
wyj.println("<I>Błąd przy dostępie do " + zad.getRequestURI() + "</I>");
wyj.println("</BODY></HTML>");
}
}
Ale co by się stało, gdyby strona błędu mogła zawierać ścieżkę stosu wyjątku lub URI serwletu, który naprawdę
spowodował problem, (ponieważ nie zawsze jest to początkowo zażądany URI)? W API 2.2 nie było to możliwe.
W API 2.3 informacje te są dostępne poprzez dwa nowe atrybuty:
javax.servlet.error.exception

Obiekt Throwable, zawierający właściwy zgłoszony wyjątek.


javax.servlet.error.request_uri

Łańcuch String przekazujący URI zasobu sprawiającego problem.


Atrybuty te pozwalają na dołączenie do strony błędu ścieżki stosu
wyjątku oraz URI zasobu sprawiającego problem. Serwlet
przedstawiony poniżej został ponownie napisany tak, aby
wykorzystywał nowe atrybuty. Proszę zauważyć, że elegancko
przerywa działanie, jeżeli one nie istnieją. Dzieje się tak w
celu zachowania wstecznej kompatybilności.
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class WyswietlBlad extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)


throws ServletException, IOException {
odp.setContentType("text/html");
PrintWriter wyj = odp.getWriter();

String kod = null, wiadomosc = null, typ = null, uri = null;


Object kodObi, wiadomoscObi, typObi;
Throwable throwable;

// Odczytanie trzech możliwych atrybutów błędów. Niektóre mogą mieć wartość


null
kodObi = zad.getAttribute("javax.servlet.error.status_code");
wiadomoscObi = req.getAttribute("javax.servlet.error.message");
typObi = req.getAttribute("javax.servlet.error.exception_type");
throwable = (Throwable)
zad.getAttribute("javax.servlet.error.exception");
uri = (String)
zad.getAttribute("javax.servlet.error.request_uri");

if (uri == null) {
uri = zad.getRequestURI(); // gdyby nie podano URI
}

// Konwersja atrybutów na wartości łańcuchowe


if (kodObi != null) kod = kodObi.toString();
if (wiadomoscObi != null) wiadomosc = wiadomoscObi.toString();
if (typObi != null) typ = typObi.toString();

// Powód błędu to kod stanu lub typ wyjątku


String powod = (kod != null ? kod : typ);

wyj.println("<HTML>");
wyj.println("<HEAD><TITLE>" + powod + ": " + wiadomosc +
"</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H1>" + powod + "</H1>");
wyj.println("<H2>" + wiadomosc + "</H2>");
wyj.println("<PRE>");
if (throwable != null) {
throwable.printStackTrace(wyj);
}
wyj.println("</PRE>");
wyj.println("<HR>");
wyj.println("<I>Błąd przy dostępie do " + uri + "</I>");
wyj.println("</BODY></HTML>");
}
}

Nowe atrybuty bezpieczeństwa


Servlet API 2.3 dodaje również dwa nowe atrybuty żądania, które mogą pomóc serwletowi w podejmowaniu
dobrze umotywowanych decyzji dotyczących obsługi bezpiecznych połączeń HTTPS. Do żądań wykonanych
przy pomocy HTTPS serwer dołączy następujące nowe atrybuty żądania:
javax.servlet.request.cipher_suite

Łańcuch String reprezentujący typ szyfrowania stosowany przez


HTTPS, jeżeli taki występuje.
javax.servlet.request.key_size

Liczba Integer reprezentująca wielkość algorytmu w bitach,


jeżeli taka występuje.
Serwlet może wykorzystać powyższe atrybuty do programowej decyzji, czy połączenie jest na tyle bezpieczne, że
można z niego skorzystać. Aplikacja może odrzucić połączenia o niewielkiej liczbie bitów, lub algorytmy
niegodne zaufania. Na przykład, serwlet mógłby wykorzystać poniższą metodę w celu upewnienia się, że jego
połączenie wykorzystuje klucz przynajmniej 128-bitowy:
public boolean czyPonad128(HttpServletRequest zad) {
Integer wielkosc = (Integer) zad.getAttribute("javax.servlet.request.key_size");

if (wielkosc == null || wielkosc.intValue() < 128) {


return false;
}
else {
return true;
}
}

UWAGA!!!!

Nazwy atrybutów w wersji próbnej wykorzystują myślniki zamiast kresek dolnych. Jednak zostaną one
zmienione w wersji ostatecznej, jak przedstawiono powyżej, w celu uzyskania większej spójności z nazwami
istniejących atrybutów.
Niewielkie poprawki
W Servlet API 2.3 znajdzie się także pewna ilość drobnych zmian. Po pierwsze, metoda getAuthType(),
która zwraca typu uwierzytelnienia wykorzystywanego do identyfikacji klienta została zdefiniowana tak, aby
zwracać jedną z czterech nowych stałych typu String klasy HttpServletRequest — BASIC_AUTH,
DIGEST_AUTH, CLIENT_AUTH i FORM_AUTH. Umożliwia to wykorzystanie uproszczonego kodu:
if (zad.getAuthType() == zad.BASIC_AUTH) {
//obsługa uwierzytelniania podstawowego
}
Oczywiście cztery stałe posiadają ciągle tradycyjne wartości String, tak więc poniższy kod z API 2.2 również
działa, ale nie jest ani tak szybki, ani elegancki. Proszę zwrócić uwagę na odwrócone sprawdzenia equals() w
celu uniknięcia wyjątku NullPointerException, jeżeli getAuthType() zwróci null:
if ("BASIC".equals(zad.getAuthType())) {
//obsługa uwierzytelniania podstawowego
}
Inną zmianą w API 2.3 jest opuszczenie HttpUtils. Klasa HttpUtils była zawsze uważana za zbiór
różnych statycznych metod — wywołań, które mogły być czasami użyteczne, ale równie dobrze mogły się
znaleźć w innych miejscach. Klasa ta zawierała metody służące do zrekonstruowania oryginalnego URL-a z
obiektu żądania i do przeformatowania danych parametrów do tablicy asocjacyjnej. API 2.3 przesuwa tę
funkcjonalność do obiektu żądania, co jest miejscem bardziej odpowiednim i opuszcza HttpUtils. Nowe
metody obiektu żądania wyglądają następująco:
StringBuffer zad.getRequestURL()
Zwraca StringBuffer zawierający URL oryginalnego żądania, odtworzony na podstawie informacji
żądania.
java.util.Map zad.getParameterMap()
Zwraca niemodyfikowalne odwzorowanie Map parametrów żądania. Nazwy parametrów pełnią funkcję
kluczy, a wartości parametrów funkcję wartości odwzorowania. Nie zapadła decyzja co do obsługi
parametrów o wielu wartościach; najprawdopodobniej wszystkie wartości zostaną zwrócone jako String
[]. Metody te wykorzystują nową metodę setCharacterEncoding() do obsługi konwersji znaków.
API 2.3 dodaje również dwie nowe metody do ServletContext, które pozwalają na odczytanie nazwy
kontekstu i wyświetlenia wszystkich przechowywanych przez niego zasobów:
String kontekst.getServletContextName()
Zwraca nazwę kontekstu zadeklarowaną w pliku web.xml.
java.util.Set kontekst.getResourcePaths()
Zwraca ścieżki do wszystkich zasobów dostępnych w kontekście, jako niemodyfikowalny zbiór obiektów
String. Każdy String posiada wiodący ukośnik (/) i powinien być uważany za względny do katalogu
macierzystego kontekstu.
Pojawiła się także nowa metoda obiektu odpowiedzi zwiększająca kontrolę programisty nad buforem
odpowiedzi. API 2.2 wprowadził metodę odp.reset() pozwalającą na usunięcie odpowiedzi i wyczyszczenie
jej głównej części, nagłówków i kodu stanu. API 2.3 dodaje odp.resetBuffer(), który czyści jedynie
główną część odpowiedzi:
void resetBuffer()
Czyści bufor odpowiedzi, ale pozostawia nagłówki i kod stanu. Jeżeli odpowiedź został już wysłana,
zgłasza wyjątek IllegalStateException.
I wreszcie, po długich naradach grupy ekspertów, Servlet API 2.3 wyjaśnia raz na zawsze, co dokładnie dzieje
się podczas wywołania odp.sendRedirect("indeks.html") w przypadku serwletu nie działającego w
kontekście macierzystym serwera. Niejasność wynikała z tego, że Servlet API 2.2 wymaga, aby niekompletna
ścieżka taka jak "/indeks.html" została przetłumaczona przez kontener serwletów na ścieżkę kompletną,
nie określa jednak zasad obsługi ścieżek kontekstów. Jeżeli serwlet wykonujący wywołanie znajduje się w
kontekście o ścieżce "/sciezkakontekstu", czy URI przekierowania powinien być określony względem
katalogu macierzystego kontenera (http://serwer:port/indeks.html), czy katalogu macierzystego
kontekstu (http://serwer:port/sciezkakontekstu/indeks.html)? W celu zapewnienia
maksymalnej przenośności zdefiniowanie zachowania jest konieczne. Po długiej debacie eksperci wybrali
tłumaczenie ścieżek względem katalogu macierzystego serwera. Osoby pragnące tłumaczenia względem
kontekstu powinny dodać na początku URI wynik wywołania getContextPath().
Wyjaśnienia deskryptora DTD
Servlet API 2.3 rozwiewa kilka niejasności związanych z zachowaniem deskryptora web.xml. Obowiązkowe
stało się przycięcie wartości tekstowych w pliku web.xml przed jego zastosowaniem. (W standardowym nie
sprawdzanym XML wszystkie miejsce są ogólnie rzecz biorąc zachowywane.) Zasada ta zapewnia, że się
poniższe pozycje będą traktowane identycznie:
<servlet-name>witaj</servlet-name>
i
<servlet-name>
witaj
</servlet-name>
Servlet API 2.3 zezwala również na wykorzystanie zasady <auth-constraint>, tak więc specjalna wartość
„*” może zostać wykorzystana jako wieloznacznik <role-name> zezwalający na wszystkie role. Pozwala to
na utworzenie zasady podobnej do poniższej, która pozwala na wejście wszystkim użytkownikom, jeżeli zostali
oni poprawnie rozpoznani jako posiadający dowolną rolę w aplikacji WWW:
<auth-constraint>
<role-name>*</role-name> <!—dopuść wszystkie rozpoznane role -->
</auth-constraint>
Ostatnią zmianą jest dopuszczenie nazwy roli zadeklarowanej przez zasadę <security-role> jako
parametru metody isUserInRole(). Na przykład, proszę spojrzeć na następujący fragment pozycji web.xml:
<servlet>
<servlet-name>
sekret
</servlet-name>
<servlet-class>
PrzegladPensji
</servlet-class>
<security-role-ref>
<role-name>
men <!-- nazwa wykorzystywana przez serwlet -->
</role-name>
<role-link>
menedzer <!-- nazwa wykorzystywana w deskryptorze-->
</role-link>
</security-role-ref>
</servlet>

<!-- ... -->

<security-role>
<role-name>
menedzer
</role-name>
</security-role>
Przy pomocy powyższego kodu możliwe jest wywołanie zarówno isUserInRole("men") jak i
isUserInRole("menedzer"); oba wywołania spowodują to samo zachowanie. Najprościej rzecz ujmując,
security-role-ref tworzy alias, chociaż nie jest to konieczne. Jest to zachowanie, którego można się
intuicyjnie spodziewać, ale specyfikacja API 2.2 mogła być interpretowana jako narzucająca wykorzystanie
jedynie tych ról, które zostały zdefiniowane w zasadzie aliasów <security-role-ref>. (Osoby, które nie
zrozumiały powyższego wywodu, nie powinny się tym przejmować; należy po prostu pamiętać, że teraz funkcje
powinny działać tak, jak się tego po nich spodziewa.)

Konkluzja
Servlet API 2.3 zawiera ekscytujący nowy mechanizm filtrujący, rozwinięty model okresu trwałości oraz nową
funkcjonalność wspierającą internacjonalizację, obsługę błędów, połączenia bezpieczne i role użytkowników.
Również dokument specyfikacji został zacieśniony w celu usunięcia niejasności, które mogłyby przeszkadzać w
tworzeniu programów niezależnych od platformy.
Dodatek A. Krótki opis Servlet API

Pakiet javax.servlet jest jądrem Servlet API. Zawiera on podstawowy interfejs Servlet, który musi być
implementowany w tej czy innej formie przez wszystkie serwlety oraz abstrakcyjną klasę GenericServlet
służącą do tworzenia prostych serwletów. Zawiera on również klasy służące do komunikowania się z serwerem i
klientem komputera (ServletRequest i ServletResponse) oraz komunikowania z klientem
(ServletInputStream i ServletOutputStream). Hierarchia klas pakietu javax.servlet jest
przedstawiona na rysunku A.1. Serwlety powinny graniczyć z klasami tego pakietu w sytuacjach, w których
nieznany jest leżący poniżej protokół.

Rysunek A.1.
Pakiet javax.servlet

GenericServlet

Zestawienie
Nazwa Klasy javax.servlet.GenericServlet
Superklasa java.lang.Object
Bezpośrednie podklasy javax.servlet.http.HttpServet
Implementowane interfejsy javax.servlet.Servlet
javax.servlet.ServletConfig
java.io.Serializable
Dostępność Servlet API 1.0 i późniejsze

Opis
GenericServlet dostarcza podstawowej implementacji interfejsu Servlet dla serwletów niezależnych od
protokołu. Jako ułatwienie implementuje ona również interfejs ServletConfig. Większość programistów
serwletów tworzy swoje klasy jako podklasy tej klasy lub HttpServlet, zamiast bezpośrednio
implementować interfejs Servlet.
GenericServlet zawiera podstawowe wersje metod init() i destroy(), które wykonują podstawowe
zadania tworzenia i czyszczenia, takie jak zarządzanie obiektem serwera ServletConfig. Serwlet omija
jedną z tych metod powinien wywoływać wersje tych metod pochodzące z superklasy. GenericServlet
zawiera również metody log() umożliwiające dostęp do funkcji zapisujących dane w dzienniku pochodzących
z ServletContext.
Metoda service() jest deklarowana abstrakcyjnie musi zostać ominięta. Dobrze napisane serwlety omijają
również getServletInfo().

Podsumowanie klasy
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable {
// Konstruktory
public GenericServlet()

// Metody egzemplarzy
public void destroy();
public String getInitParameter(String name);
public Enumeration getInitParameterNames();
public ServletConfig getServletConfig();
public ServletContext getServletContext();
public String getServletInfo();
public String getServletName(); // Nowość w
2.2
public void init() throws ServletException; // Nowość w
2.1
public void init(ServletConfig konfig) throws ServletException;
public void log(String wiad);
public void log(String wiad, Throwable t); // Nowość w
2.1
public abstract void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException;
}

Konstruktory
GenericServlet()
public GenericServlet()
Domyślny konstruktor GenericServlet() nie wykonuje żadnej pracy. Wszystkie zadania związane z
inicjalizacją serwletu powinny być wykonywane w init(), a nie w konstruktorze.

Metody egzemplarzy
destroy()
public void destroy()
Wywoływane przez kontener serwletów, aby wskazać serwletowi, że został on wyłączony. Metoda ta jest
wywoływana jedynie po wyłączeniu wszystkich wątków wewnątrz metody usługowej serwletu lub po określonym
czasie. Po wywołaniu tej metody przez kontener serwletów, nie będzie wywoływał on już metody usługowej
serwletu. Domyślna implementacja zapisuje zniszczenie serwletu w dzienniku przy pomocy metody log().
Serwlet może ominąć tę metodę w celu zachowania jej stanu, uwolnienia jej zasobów (połączenia z bazami
danych, wątki, uchwyty plików itd.) itp.
getInitParameter()
public String getInitParameter(String nazwa)
Zwraca wartość danego parametru inicjacji serwletu lub null, jeżeli nie odnajdzie pasujących parametrów.
Pochodzi z interfejsu ServletConfig.

getInitParameterNames()
public Enumeration getInitParameterNames()
Zwraca nazwy wszystkich parametrów inicjacji serwletu jako Enumeration obiektów String lub pustą
Enumeration, jeżeli nie występują żadne parametry. Pochodzi z interfejsu ServletConfig.

getServletConfig()
public ServletConfig getServletConfig()
Zwraca obiekt ServletConfig serwletu. W praktyce, metoda ta jest rzadko wywoływana przez
GenericServlet ponieważ wszystkie metody ServletConfig są wewnętrznie duplikowane.

getServletContext()
public ServletContext getServletContext ()
Zwraca obiekt ServletContext serwletu. Pochodzi z interfejsu ServletConfig.

getServletInfo()
public String getServletInfo()
Zwraca zdefiniowany przez programistę łańcuch String opisujący serwlet. Serwlet powinien omijać tę metodę i
dostarczać własnego łańcucha identyfikacyjnego (na przykład „Serwlet Wiadomości Janka v.1.21”), ale nie jest
to wymagane.

getServletName()
public String getServletName()
Zwraca nawę aktualnego egzemplarza serwletu. Nazwa może być dostarczona prze administrację serwera lub
przypisana w deskryptorze aplikacji WWW, lub, w przypadku niezarejestrowanego (i w związku z tym
nienazwanego) egzemplarza serwletu, będzie równa nazwie klasy serwletu. Pochodzi z interfejsu
ServletConfig. Metoda ta została wprowadzona w Servlet API 2.2.

init()
public void init() throws ServletException
public void init(ServletConfig konfig) throws ServletException
Wywoływana przez kontener serwletów po pierwszym pobraniu serwletu i przed wywołaniem jego metody
service(). Serwlet może omijać tę metodę w celu przeprowadzenia jednorazowej konfiguracji, stworzenia
zasobów i tak dalej. Serwlety utworzone według Servlet API 2.1 lub późniejszego mogą implementować wersję
bez argumentów. Serwlety, które muszą być kompatybilne wstecz z Servlet API 2.0 powinny implementować
wersję pobierającą parametr ServletConfig. Nie należy implementować obu wersji. Domyślna
implementacja init() zapisuje w dzienniku inicjalizację serwletu oraz przechowuje obiekt ServletConfig
w celu wykorzystania go przez metody interfejsu ServletConfig. Serwlet implementujący wersję
pobierającą ServletConfig musi wywołać super.init(konfig) przed wykonaniem własnego kodu
inicjacji. Serwlety korzystające z nowej bezargumentowej wersji nie muszą się tym przejmować.

log()
public void log(String wiad)
public void log(String wiad, Throwable t)
Zapisuje daną wiadomość w dzienniku serwletu po nazwie serwletu wywołującego. Miejsce zapisu jest zależne
od serwera, zazwyczaj jest to dziennik zdarzeń.

service()
public abstract void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException;
Wywoływana w celu obsłużenia pojedynczego żądania klienta. Serwlet otrzymuje informacje żądania poprzez
obiekt ServletRequest i odsyła dane poprzez obiekt ServletResponse. Jest to jedyna metoda, która
musi zostać ominięta podczas rozszerzania GenericServlet.
RequestDipatcher

Zestawienie
Nazwa interfejsu javax.servlet.RequestDispatcher
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 2.1 i późniejsze

Opis
Interfejs obiektu, który może wewnętrznie rozsyłać żądania do dowolnych zasobów (takich jak serwlety, pliki
HTML lub pliki JSP) na serwerze, Kontener serwletów tworzy obiekt RequestDispatcher, który następnie
wykorzystywany jest jako obwódka wokół zasobu serwera umieszczonego w konkretnej ścieżce lub
posiadającego konkretną nazwę. Interfejs ten został zaprojektowany w celu wykorzystania z serwletami i plikami
JSP, ale kontener serwletów może utworzyć obiekty RequestDispatcher służące jako obwódki dowolnego
typu zasobu. Rozsyłanie może zostać wykorzystane do przekazania żądania do zasobu lub do dołączenia
zawartości tego zasobu do aktualnej odpowiedzi. Klasa ta została wprowadzona w Servlet API 2.1.

Deklaracja interfejsu
public interface RequestDispatcher {
//Metody
public abstract void forward(ServletRequest zad, ServletResponse odp)
throws ServletException, java.io.IOException // Nowość w
2.1.
public abstract void include(ServletRequest zad, ServletResponse odp)
throws ServletException, java.io.IOException // Nowość w
2.1.
}

Metody
forward()
public abstract void forward(ServletRequest zad, ServletResponse odp)
throws ServletException, java.io.IOException
Przekazuje żądanie z serwletu do innego zasobu na serwerze. Metoda ta pozwala jednemu serwletowi na
wykonanie wstępnego przetwarzania żądania, po czym innemu na wygenerowanie odpowiedzi. W przypadku
RequestDispatcher otrzymanego przez getRequestDispatcher(), obiekt ServletRequest
posiada elementy ego ścieżki i parametry dołączone w celu dopasowania ścieżki zasobu docelowego. Metoda ta
powinna zostać wywołana przed wysłaniem odpowiedzi do klienta. Jeżeli odpowiedź została już wysłana,
metoda wywołuje wyjątek IllegalStateException. Niewysłana zawartość w buforze odpowiedzi zostaje
automatycznie wyczyszczona przed przekazaniem. Parametry żądania i odpowiedzi muszą być tymi samymi
obiektami, które zostały przekazane metodzie usługowej serwletu wywołującego. Metoda ta została
wprowadzona w Servlet API 2.1.

include()
public abstract void include(ServletRequest zad, ServletResponse odp)
throws ServletException, java.io.IOException
Dołącza zawartość zasobu do aktualnej odpowiedzi. Elementy ścieżki i parametry ServletRequest
pozostają niezmienione w porównaniu z zasobem wywołującym; jeżeli dołączane zasoby wymagają dostępu do
swoich własnych elementów i parametrów, może odczytać je przy pomocy przypisanych do serwera atrybutów
żądania javax.servlet.include.request_uri, javax.servlet.include.context_path,
javax.servlet.include.path_info i javax.servlet.include.query_string. Dołączony
serwlet nie może zmieniać kodu stanu odpowiedzi ani ustawiać nagłówków; każda próba zmiany jest
ignorowana. Parametry żądania i odpowiedzi muszą być tymi samymi obiektami, które zostały przekazane
metodzie usługowej serwletu wywołującego. Metoda został wprowadzona w Servlet API 2.1.
Servlet

Zestawienie
Nazwa interfejsu javax.servlet.Servlet
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez javax.servlet.GenericServlet
Dostępność Servlet API 1.0 i późniejsze

Opis
Wszystkie serwlety implementują interfejs Servlet, czy to bezpośrednio, czy poprzez podklasę GenericServlet
lub HttpServlet. Większość programistów serwletów uważa za łatwiejsze uczynienie serwletu podklasą jednej z
dwóch istniejących klas serwletów niż bezpośrednią implementację tego interfejsu. Interfejs deklaruje
podstawową funkcjonalność serwletów — inicjowanie serwletu, obsługę żądania klienta i niszczenie serwletu.

Deklaracja interfejsu
public interface Servlet {
// Metody
public abstract void destroy();
public abstract ServletConfig getServletConfig();
public abstract String getServletInfo();
public abstract void init(ServletConfig konfig) throws ServletException;
public abstract void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException;
}

Metody

destroy()
public abstract void destroy()
Wywoływana przez kontener serwletów w celu wskazania serwletowi, że został wyłączony. Proszę zobaczyć
pełny opis w GenericServlet.

getServletConfig()
public abstract ServletConfig getServletConfig()
Zwraca obiekt ServletConfig zapamiętany przez metodę init().

getServletInfo()
public abstract String getServletInfo()
Zwraca zdefiniowany przez programistę łańcuch String opisujący serwlet.

init()
public abstract void init(ServletConfig konfig) throws ServletException
Wywoływana przez kontener serwletów w po pierwszym załadowaniu serwletu i przed wywołaniem metody
service() serwletu. Proszę zobaczyć pełny opis w GenericServlet.

service()
public abstract void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException
Wywoływana w celu obsłużenia pojedynczego żądania klienta. Serwlet otrzymuje informację o żądaniu przez
obiekt ServletRequest i wysyła dane z powrotem do klienta przez obiekt ServletResponse.

ServletConfig

Zestawienie
Nazwa interfejsu javax.servlet.ServletConfig
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez javax.servlet.GenericServlet
Dostępność Servlet API 1.0 i późniejsze

Opis
Kontenery serwletów wykorzystują obiekty ServletConfig do przekazywania serwletom informacji o
inicjacji i kontekście. Informacje inicjacji ogólnie składają się z serii parametrów inicjacji i obiektu
ServletContext dostarczającego informacji na temat środowiska serwletu. Serwlet może zaimplementować
ServletConfig w celu ułatwienia dostępu do parametrów inicjacji i informacji o kontekście, jak to robi
GenericServlet.

Deklaracja interfejsu
public interface ServletConfig {
// Metody
public abstract String getInitParameter(String nazwa);
public abstract Enumeration getInitParameterNames();
public abstract ServletContext getServletContext();
public abstract String getServletName(); // Nowość w
2.2

Metody
getInitParameter()
public abstract String getInitParameter(String nazwa)
Zwraca wartość danego parametru inicjacji serwletu lub null, jeżeli nie odnajdzie pasującego parametru.

getInitParameterNames()
public abstract Enumeration getInitParameterNames()
Zwraca nazwy wszystkich parametrów inicjacji serwletu jako Enumeration obiektów String, lub pustą
Enumeration, jeżeli nie występują żądne parametry.

getServletContext()
public abstract ServletContext getServletContext()
Zwraca obiekt ServletContext wywołującego ją serwletu, umożliwiając interakcję z kontenerem serwletów.

getServletName()
public abstract String getServletName()
Zwraca nawę aktualnego egzemplarza serwletu. Nazwa może być dostarczona prze administrację serwera lub
przypisana w deskryptorze aplikacji WWW, lub, w przypadku niezarejestrowanego (i w związku z tym
nienazwanego) egzemplarza serwletu, będzie równa nazwie klasy serwletu. Pochodzi z interfejsu
ServletConfig. Metoda ta została wprowadzona w Servlet API 2.2.

ServletContext

Zestawienie
Nazwa interfejsu javax.servlet.ServletContext
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Interfejs ServletContext definiuje zbiór metod wykorzystywanych do komunikacji z kontenerem serwletów
w sposób niezależny od żądania. Oznacza to odnajdywanie informacji o ścieżce, uzyskiwanie dostępu do innych
serwletów działających na serwerze i zapis w pliku dziennika serwera. Każda aplikacja WWW posiada inny
kontekst serwletów.
Deklaracja interfejsu
public interface ServletContext {
// Metody
public abstract Object getAttribute(String nazwa);
public abstract Enumeration getAttributeNames(); // Nowość w
2.1
public abstract ServletContext getContext(String sciezkauri); // Nowość w
2.1
public abstract String getInitParameter(String nazwa); // Nowość w
2.2
public abstract Enumeration getInitParameterNames(); // Nowość w
2.2
public abstract int getMajorVersion(); // Nowość w
2.1
public abstract String getMimeType(string plik);
public abstract int getMinorVersion(); // Nowość w
2.1
public abstract RequestDispatcher getNamedDispatcher(String nazwa); // Nowość w
2.2
public abstract String getRealPath(String sciezka);
public abstract URL getResource(String sciezka) // Nowość w 2.1
throws MalformedURLException;
public abstract InputStream getResourceAsStream(String sciezka); // Nowość w
2.1
public abstract String getServerInfo();
public abstract Servlet getServlet(String nazwa) // Opuszczona
throws ServletException;
public abstract Enumeration getServletNames(); // Opuszczona
public abstract Enumeration getServlets(); // Opuszczona
public abstract void log(Exception wyjatek, String wiad); // Opuszczona
public abstract void log(String wiad);
public abstract void log(string wiad, Throwable t); // Nowość w
2.1
public abstract void removeAttribute(String nazwa); // Nowość w
2.1
public abstract void setAttribute(String name, Object o); // Nowość w
2.1
}

Metody
getAttribute()
public abstract Object getAttribute(String nazwa)
Zwraca wartość nazwanego atrybutu kontekstu jako Object lub null, jeżeli atrybut nie istnieje. Atrybuty
zależne od serwera mogą zostać automatycznie ustawione przez kontener serwletów w celu dostarczenia
serwletom informacji o zakresie dużo szerszym niż dostarczany przez podstawowy Servlet API. Atrybuty mogą
też być ustawiane programowo przez serwlety jako sposób dzielenia informacji wewnątrz aplikacji WWW
reprezentowanej przez dany kontekst. Nazwy atrybutów powinny być zgodne z tą samą konwencją, co nazwy
pakietów. Nazwy pakietów java.* i sun.* są zarezerwowane przez dział Java Software firmy Sun
Microsystems (dawniej znany jako JavaSoft), a com.sun.* jest zarezerwowany dla użytku firmy Sun
Microsystems. Proszę przejrzeć dokumentację serwera w celu obejrzenia listy wbudowanych atrybutów. Proszę
pamiętać, że serwlety oparte na atrybutach zależnych od serwera nie są przenośne.

getAttributeNames()
public abstract Enumeration getAttributeNames()
Zwraca nazwy wszystkich atrybutów obecnego kontekstu jako Enumeration obiektów String. Zwraca pustą
Enumeration, jeżeli kontekst nie posiada atrybutów. Metoda została wprowadzona w Servlet API 2.1.

getContext()
public abstract ServletContext getContext(String sciezkauri)
Zwraca egzemplarz ServletContext przypisany do danej ścieżki URI. Dana ścieżka musi być bezwzględna
(musi rozpoczynać się od /) i jest interpretowana w oparciu o katalog macierzysty serwera. Metoda ta zapewnia
serwletowi dostęp do kontekstu innego niż własny, umożliwiając przeglądanie danych wewnątrz tego kontekstu
lub otrzymanie RequestDispatcher do zasobów wewnątrz tego kontekstu. W środowisku zabezpieczonym
lub rozproszonym kontener serwletów może zwrócić null dla dowolnej ścieżki. Metoda została wprowadzona
w Servlet API 2.1.

getInitParameter()
public abstract String getInitParameter(String nazwa)
Zwraca wartość danego parametru inicjacji lub null, jeżeli nie odnajdzie pasującego parametru. Metoda ta może
udostępnić przydatne informacje o konfiguracji całej aplikacji WWW. Na przykład, może udostępnić adres
poczty elektronicznej administratora systemu lub nazwę systemu przechowującego dane krytyczne. Parametry
inicjacji kontekstu są przydzielane w deskryptorze aplikacji WWW. Metoda została wprowadzona w Servlet API
2.2.

getInitParameterNames()
public abstract Enumeration getInitParameterNames()
Zwraca nazwy wszystkich parametrów inicjacji danego kontekstu jako Enumeration obiektu String. Zwraca ona
pustą Enumeration, jeżeli kontekst nie posiada żadnych atrybutów. Parametry inicjacji kontekstu są przydzielane
w deskryptorze aplikacji WWW. Metoda została wprowadzona w Servlet API 2.2.

getMajorVersion()
public abstract int getMajorVersion()
Zwraca numer głównej wersji Servlet API obsługiwanego przez kontener serwletów. Na przykład, kontener
implementujący wersję 2.1 zwróci 2. Metoda została wprowadzona w Servlet API 2.1.

getMimeType()
public abstract String getMimeType(string plik)
Zwraca typ MIME danego pliku lub null, jeżeli jest on nieznany. Niektóre implementacje zwracają
text/plain jeżeli plik nie istnieje. Popularne typy MIME to text/html, text/plain, image/gif i
image/jpeg.

getMinorVersion()
public abstract int getMinorVersion()
Zwraca numer pobocznej wersji Servlet API obsługiwanego przez kontener serwletów. Na przykład, kontener
implementujący wersję 2.1 zwróci 1. Metoda została wprowadzona w Servlet API 2.1.

getNamedDispatcher()
public abstract RequestDispatcher getNamedDispatcher(String nazwa)
Zwraca RequestDispatcher, który odsyła działanie do zasobu według nazwy, zamiast według ścieżki.
Pozwala to na rozsyłanie do zasobów, które niekoniecznie są dostępne publicznie. Serwlety (oraz strony JSP)
mogą otrzymać nazwy poprzez administrację serwera lub przez deskryptor aplikacji WWW. Metoda zwraca null,
jeżeli, niezależnie od powodu, kontekst nie może zwrócić rozesłania. Metoda została wprowadzona w Servlet
API 2.2.

getRealPath()
public abstract String get RealPath(String sciezka)
Zwraca prawdziwą ścieżkę systemu plików danej „ścieżki wirtualnej” lub null, jeżeli tłumaczenie nie mogło
zostać przeprowadzone (na przykład kiedy plik znajduje się w zdalnym systemie plików lub jest dostępny jedynie
wewnątrz archiwum .war). Jeżeli dana ścieżka wynosi /, metoda zwraca katalog macierzysty dokumentów
kontekstu. Jeżeli dana ścieżka jest taka sama, jak zwrócona przez getPathInfo(), metoda zwraca tę samą
realną ścieżkę, jaką zwróciłaby przy wywołaniu getPathTranslated(). Nie istnieje odpowiednik CGI.

getResource()
public abstract URL getResource(String sciezka)
Zwraca URL zasobu odwzorowanego w podanej ścieżce. Ścieżka musi rozpoczynać się od /, i jest
interpretowana jako względna do katalogu macierzystego kontekstu. Metoda może zwrócić null, jeżeli
skojarzenie jakiegokolwiek URL-a ze ścieżką nie będzie możliwe. Metoda ta umożliwia kontenerowi
udostępnienie zasobów serwletom z dowolnego źródła. Zasoby mogą zostać odnalezione lokalnie, w zdalnym
systemie plików, bazie danych lub pliku .war. Niektóre kontenery mogą również umożliwiać zapis w obiekcie
URL przy pomocy metod klasy URL. Metoda ta powinna zostać zastosowana kiedy nie wszystkie zasoby to pliki
lokalne — tak jest w przypadku środowiska rozproszonego (w którym kontener serwletów może znajdować się
na innym komputerze niż zasoby) lub kiedy zawartość pochodzi z pliku .war (którego pliki nie są dostępne
bezpośrednio). Zawartość zasobów zwracana jest w formie surowej, tak więc należy pamiętać, że żądanie strony
.jsp zwróci kod źródłowy JSP w celu dołączenia zasobów uruchomieniowych JSP należy zamiennie zastosować
RequestDispatcher. Metoda ta posiada inny cel niż Class.getResource(), która wyszukuje w
oparciu o mechanizm ładowania klas. Metoda ta nie wykorzystuje mechanizmów ładowania klas. Została ona
wprowadzona w Servlet API 2.1.
getResourceAsStream()
public abstract InputStream getResourceAsStream(String sciezka)
Zwraca InputStream w celu odczytania zawartości zasobu odwzorowanego według konkretnej ścieżki.
Ścieżka musi rozpoczynać się / i jest interpretowana według katalogu macierzystego kontekstu. Metoda może
zwracać null, jeżeli ze ścieżką nie można było połączyć żadnego zasobu. Wykorzystanie tej metody jest często
łatwiejsze niż getResource(); jednak podczas wykorzystywania tej metody tracone są metainformacje na
temat zasobów, takie jak wielkość i typ jej zawartości, dostępne przy pomocy getResource(). Metoda
została wprowadzona w Servlet API 2.1.

getServerInfo()
public abstract String getServerInfo()
Zwraca nawę i numer wersji oprogramowania serwera, rozdzielone ukośnikiem (/). Wartość jest równa wartości
zmiennej CGI SERVER_SOFTWARE.

getServlet()
public abstract Servlet getServlet(String nazwa) throws ServletException;
Zwraca null w Servlet API 2.1 i późniejszych. Poprzednio zwracała pobrany serwlet pasujący do podanej
nazwy i URL-u lub null, jeżeli serwlet nie mógł zostać odnaleziony. W API 2.1 metoda została opuszczona i
zdefiniowana tak, by zwracała null, ponieważ bezpośredni dostęp do innego egzemplarza serwletu otwiera zbyt
wiele możliwości dla błędu. Dzieje się tak dlatego, że serwlety mogą zostać zniszczone przez kontener w
dowolnym czasie, tak więc żaden obiekt oprócz kontenera nie powinien posiadać bezpośredniego odwołania do
serwletu. Poza tym, w przypadku serwera obsługującego dystrybucję ładunku, w którym serwlety rozkładane są
na kilku różnych serwerach, niemożliwe może okazać się zwrócenie lokalnego odwołania do serwletu. Zamiast
tego serwlety powinny współpracować poprzez wykorzystanie współdzielonych atrybutów ServletContext.
Technicznie rzecz biorąc, zdefiniowanie metody tak, by zawsze zwracała null, nie usuwa wstecznej
kompatybilności, ponieważ serwer zawsze miał możliwość zwracania null w tej metodzie.

getServletNames()
public abstract Enumeration getServletNames();
Zwraca pustą Enumeration w Servlet API 2.1 i późniejszych. Poprzednio zwracała Enumeration
zawierającą nazwy obiektów serwletów załadowanych do danego kontekstu. Metoda ta została opuszczona i
zdefiniowana do zwracania pustej Enumeration w Servlet API 2.1 z tych samych powodów, dla których
getServlet() została opuszczona i zdefiniowana tak, by zwracała. Metoda ta została wprowadzona w
Servlet API 2.0.

getServlets()
public abstract Enumeration getServlets();
Zwraca pustą Enumeration w Servlet API 2.1 i późniejszych. Poprzednio zwracała Enumeration
zawierającą obiekty Servlet załadowane do danego kontekstu. Metoda ta została opuszczona w Servlet API
2.0 na rzecz getServletNames(). Została zdefiniowana do zwracania pustej Enumeration w Servlet API
2.1 według zachowania metody getServletNames().

log()
public abstract void log(String wiad);
Zapisuje daną wiadomość w dzienniku serwletu. Miejsce zapisu jest zależne od serwera, zazwyczaj jest to plik
dziennika zdarzeń.
public abstract void log(string wiad, Throwable t);
Zapisuje daną wiadomość i ścieżkę stosu obiektu Throwable w dzienniku serwletu. Miejsce zapisu jest zależne
od serwera, zazwyczaj jest to plik dziennika zdarzeń. Metoda została wprowadzona w Servlet API 2.1.
public abstract void log(Exception wyjatek, String wiad);
Zapisuje daną wiadomość i ścieżkę wyjątku Exception w dzienniku serwletu. Miejsce zapisu jest zależne od
serwera. Proszę zauważyć niestandardowe umieszczenie parametru Exception na pozycji pierwszej, a nie
ostatniej. Metoda ta została opuszczona w Servlet API 2.1 na rzecz metody log(String wiad,
Throwable t), w której zastosowano standardowy porządek elementów i która pozwala na zapisanie każdego
obiektu Trowable, nie tylko Exception. Metoda została wprowadzona w Servlet API 2.0.

removeAttribute()
public abstract void removeAttribute(String nazwa);
Usuwa z kontekstu atrybut o danej nazwie. Atrybuty powinny zostać usunięte, kiedy nie są już dłużej potrzebne,
w celu odblokowania miejsca zajmowanego przez nie w pamięci. Metoda została wprowadzona w Servlet API
2.1.

setAttribute()
public abstract void setAttribute(String nazwa, Object o);
Dowiązuje obiekt o danej nazwie do kontekstu serwletu. Każde istniejące dowiązanie o tej samej nazwie zostaje
zastąpione. Metoda została wprowadzona w Servlet API 2.1.

ServletException

Zestawienie
Nazwa klasy javax.servlet.ServletException
Superklasa java.lang.exception
Bezpośrednie podklasy javax.servlet.UnavailableException
Implementowane interfejsy Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Ogólny wyjątek zgłaszany przez serwlety napotykające trudności.

Podsumowanie klasy
public class ServletException extends java.lang.Exception {
// Konstruktory
public ServletException(); // Nowość w
2.0
public ServletException(String wiad);
public ServletException(String wiad, Throwable podstPrzycz); // Nowość w
2.1
public ServletException(Throwable podstPrzycz); // Nowość w
2.1
public Throwable getRootCause(); // Nowość w
2.1
}

Konstruktory
public ServletException()
public ServletException();
public ServletException(String wiad);
public ServletException(String wiad, Throwable podstPrzycz);
public ServletException(Throwable podstPrzycz);
Konstruuje nowy wyjątek ServletException, z opcjonalną opisową wiadomością i opcjonalną
„podstawową przyczyną” wyjątku. Jeżeli wiadomość została określona, może być ona odczytana przy pomocy
wywołania getMessage(); jeżeli określona została podstawowa przyczyna, może ona być odczytana przy
pomocy wywołania getRootCause(). Wiadomości i podstawowe przyczyny są zazwyczaj dołączane do
dzienników serwera i komunikatów o błędach dla użytkowników. Wersje konstruktora pobierające podstawową
przyczynę zostały wprowadzone w Servlet API 2.1.

Metody egzemplarzy
getRootCause()
public Throwable getRootCause();
Zwraca obiekt Throwable, który spowodował wyjątek serwletu lub null, jeżeli nie istnieje podstawowa
przyczyna. Metoda została wprowadzona w Servlet API 2.1.
ServletInputStream

Zestawienie
Nazwa klasy javax.servlet.ServletInputStream
Superklasa java.io.InputStream
Bezpośrednie podklasy Brak
Implementowane interfejsy Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Dostarcza potoku wyjściowego służącego do odczytywania danych binarnych z żądania klienta oraz metody
readLine() służącej do odczytywania danych linia po linii. ServletInputStream jest zwracany przez
metodę getInputStream() ServletRequest. W przypadku serwletów HTTP
ServletInputStream umożliwia dostęp do wysłanych danych POST.

Podsumowanie klasy
public abstract class ServletInputStream extends java.io.InputStream {
// Konstruktory
protected ServletInputStream();

// Metody egzemplarzy
public int readLine(byte b[], int off, int dlug) throws IOException;
}

Konstruktory
ServletInputStream()
protected ServletInputStream();
Domyślny konstruktor nie wykonuje żadnych działań. Serwlet nie powinien nigdy konstruować swojego
własnego ServletInputStream.

Metody egzemplarzy
readLine()
public int readLine(byte b[], int off, int dlug) throws IOException;
Odczytuje bajty z potoku wejściowego do tablicy bajtów b, rozpoczynając od offsetu w tablicy podanego w off.
Zatrzymuje odczytywanie po napotkaniu znaku \n lub po odczytaniu ilości dlug bajtów. Kończący znak \n
jest również odczytywany do bufora. Zwraca ilość odczytanych bajtów lub –1, jeżeli osiągnięto koniec potoku.
Wersja tej klasy dołączona do Servlet API 2.0 zawierała błąd, który powodował ignorowanie parametru dlug,
co dawało wynik w zgłaszaniu wyjątku ArrayIndexOutOfBoundsException podczas odczytywania linii
dłuższej niż bufor. Błąd ten został naprawiony w Servlet API 2.1.

ServletOutputStream

Zestawienie
Nazwa klasy javax.servlet.ServletOutputStream
Superklasa java.io.OutputStream
Bezpośrednie podklasy Brak
Implementowane interfejsy Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Dostarcza strumienia wyjściowego służącego do wysyłania danych binarnych z powrotem do klienta. Serwlet
uzyskuje obiekt ServletOutputStream poprzez metodę getOutpuStream() ServletResponse.
Chociaż zawiera różnorodne metody println() służące do wysyłania tekstu lub HTML,
ServletOutputStream został przewyższony przez PrintWriter. Powinien być wykorzystywany jedynie
do wysyłania danych binarnych. Jeżeli tworzy się podklasę ServletOutputStream, należy dostarczyć
implementacji metody write(int).

Podsumowanie klasy
public abstract class ServletOutputStream extends java.io.OutputStream {
// Konstruktory
protected ServletOutputStream();

// Metody egzemplarzy
public void print(boolean b) throws IOException;
public void print(char c) throws IOException;
public void print(double d) throws IOException;
public void print(float f) throws IOException;
public void print(int i) throws IOException;
public void print(long l) throws IOException;
public void print(String s) throws IOException;
public void println() throws IOException;
public void println(boolean b) throws IOException;
public void println(char c) throws IOException;
public void println(double d) throws IOException;
public void println(float f) throws IOException;
public void println(int i) throws IOException;
public void println(long l) throws IOException;
public void println(String s) throws IOException;
}

Konstruktory
ServletOutputStream()
protected ServletOutputStream();
Domyślny konstruktor nie wykonuje żadnych działań.

Metody egzemplarzy
print()
public void print(boolean b) throws IOException;
public void print(char c) throws IOException;
public void print(double d) throws IOException;
public void print(float f) throws IOException;
public void print(int i) throws IOException;
public void print(long l) throws IOException;
public void print(String s) throws IOException;
Wyświetla klientowi dane informacje, bez kończącego znaku powrotu karetki/końca linii (carriage return/line
feed —CRLF).

println()
public void println() throws IOException;
public void println(boolean b) throws IOException;
public void println(char c) throws IOException;
public void println(double d) throws IOException;
public void println(float f) throws IOException;
public void println(int i) throws IOException;
public void println(long l) throws IOException;
public void println(String s) throws IOException;
Wyświetla klientowi dane informacje, z kończącym CRLF. Metoda bez żadnych parametrów wyświetla po prostu
CRLF.

ServletRequest

Zestawienie
Nazwa interfejsu javax.servlet.ServletRequest
Superinterfejs Brak
Bezpośrednie podinterfejsy javax.servlet.http.HttpServletRequest
Implementowany przez Brak
Dostępność Servlet API 1.0 i późniejsze
Opis
Obiekt ServletRequest zawiera w sobie wszystkie informacje na temat żądania klienta, włączając w to parametry
żądania, atrybut żądania, lokalizacje klienta oraz potok wejściowy służący do odczytywania danych binarnych z
głównej części żądania. Możliwe jest utworzenie podklasy ServletRequest w celu dostarczenia dodatkowych
informacji zależnych od protokołu. Na przykład HttpServletRequest zawiera metody służące do manipulowania
nagłówkami HTTP.

Deklaracja interfejsu
public interface ServletRequest {
// Metody
public abstract Object getAttribute(String nazwa);
public abstract Enumeration getAttributeNames(String nazwa); // Nowość w
2.1
public abstract String getCharacterEncoding(); // Nowość w
2.0
public abstract int getContentLength()
public abstract String getContentType();
public abstract ServletInputStream getInputStream() throws IOException;
public abstract Locale getLocale(); // Nowość w
2.2
public abstract Enumeration getLocales(); // Nowość w
2.2
public abstract String getParameter(String nazwa);
public abstract Enumeration getParameterNames();
public abstract String[] getParameterValues(String nazwa);
public abstract String getProtocol();
public abstract BufferedReader getReader() throws IOException // Nowość w
2.0
public abstract String getRealPath(String sciezka); // Opuszczona
public abstract String getRemoteAddr();
public abstract String getRemoteHost();
public abstract RequestDispatcher getRequestDispatcher(String sciezka) // Nowość
w 2.1
public abstract String getScheme();
public abstract String getServerName();
public abstract int getServerPort();
public abstract boolean isSecure(); // Nowość w
2.2
public abstract void removeAttribute(String nazwa); // Nowość w
2.2
public abstract void setAttribute(String nazwa, Object o); // Nowość w
2.1
}

Metody
getAttribute()
public abstract Object getAttribute(String nazwa)
Zwraca wartość danego atrybutu żądania jako Object lub null, jeżeli atrybut nie istnieje. Atrybuty żądania
zależne od serwera mogą być ustawiane automatycznie przez kontener serwletów w celu dostarczenia serwletom
informacji wykraczających poza zasięg tych dostarczanych przez podstawowy Servlet API. Atrybuty mogą
również zostać automatycznie ustawione przez serwlety, co jest sposobem przekazywania informacji pomiędzy
serwletami podczas wykorzystywania RequestDispatcher. Nazwy atrybutów powinny być zgodne z tą
samą konwencją, co nazwy pakietów. Nazwy pakietów java.* i sun.* są zarezerwowane przez dział Java
Software firmy Sun Microsystems (dawniej znany jako JavaSoft), a com.sun.* jest zarezerwowany dla użytku
firmy Sun Microsystems. Proszę przejrzeć dokumentację serwera w celu obejrzenia listy wbudowanych
atrybutów. Proszę pamiętać, że serwlety oparte na atrybutach zależnych od serwera nie są przenośne.

getAttributeNames()
public abstract Enumeration getAttributeNames()
Zwraca nazwy wszystkich atrybutów obecnego kontekstu jako Enumeration obiektów String. Zwraca pustą
Enumeration, jeżeli kontekst nie posiada atrybutów. Metoda została wprowadzona w Servlet API 2.1.

getCharacterEncoding()
public abstract String getCharacterEncoding()
Zwraca kodowanie potoku wejściowego serwletu lub null, jeżeli nie jest on znany. Metoda został
wprowadzona w Servlet API 2.0.
getContentLength()
public abstract int getContentLength()
Zwraca długość w bajtach zawartości przesyłanej przez potok wejściowy lub –1, jeżeli długość nie jest znana (na
przykład podczas braku danych). Równoważna ze zmienną CGI CONTENT_LENGTH.

getContentType()
public abstract String getContentType()

Zwraca typ zawartości przesyłanej poprzez potok wejściowy lub null, jeżeli typ nie jest znany lub nie
występują żadne dane. Równoważna ze zmienną CGI CONTENT_TYPE.

getInputStream()
public abstract ServletInputStream getInputStream()
throws IOException, IllegalStateException
Pobiera potok wejściowy jako obiekt ServletInputStream. ServletInputStream jest bezpośrednią
podklasą InputStream, i obiekt może być traktowany w identyczny sposób jak zwykły obiekt
InputStream, z dodatkową możliwością efektywnego odczytywania wpisanych danych linia po linii do
tablicy bajtów. Metoda ta powinna być wykorzystywana do odczytywania wpisów binarnych. Zgłasza ona
wyjątek IllegalStateException, jeżeli wcześniej w żądaniu wywołana została metoda getReader().
IllegalStateException nie musi być otwarcie przechwytywany.

getLocale()
public abstract Locale getLocale()
Zwraca lokalizację Locale preferowaną przez klienta, odczytaną z nagłówka żądania klienta
Accept_Language. Jeżeli żądanie klienta nie posiada nagłówka Accept_Language, metoda zwraca
domyślną lokalizację serwera. W celu wykonania dokładniejszego poszukiwania lokalizacji należy wykorzystać
klasę com.oreilly.servlet.LocaleNegotiator. Metoda została wprowadzona w Servlet API 2.2.

getLocales()
public abstract Enumeration getLocales()
Zwraca Enumeration obiektów Locale wskazujących na lokalizacje akceptowane przez klienta, na
podstawie nagłówka Accept-Language, od najbardziej do najmniej preferowanej. Jeżeli żądania klienta nie
zawiera nagłówka Accept_Language, metoda zwraca Enumeration zawierającą jeden Locale,
domyślną lokalizację serwera. W celu wykonania dokładniejszego poszukiwania lokalizacji należy wykorzystać
klasę com.oreilly.servlet.LocaleNegotiator. Metoda została wprowadzona w Servlet API 2.2.

getParameter()
public abstract String getParameter(String nazwa)
Zwraca wartość danego parametru jako łańcuch String. Zwraca null, jeżeli parametr nie istnieje, lub pusty
łańcuch, jeżeli parametr istnieje, ale nie posiada żadnej wartości. Wartość jest zawsze zwracana w swojej
zwykłej, zdekodowanej formie. Jeżeli parametr posiada kilka wartości, należy wykorzystać metodę
getParameterValues(), która zwraca tablicę wartości. Jeżeli metoda jest wywoływana na parametrze
posiadającym kilka wartości, zwracana wartość jest równa pierwszemu elementowi tablicy zwracanej przez
getParameterValues(). Jeżeli informacje na temat parametrów nadeszły w formie zakodowanych danych
POST, mogą być one niedostępne, jeżeli dane POST zostały wcześniej ręcznie odczytane przy pomocy metody
getReader() lub getInputStream(). Metoda ta była przez pewien czas opuszczona na rzecz
getParameterValues(), ale dzięki powszechnym protestom środowiska programistów, została
przywrócona w Servlet API 2.0.

getParameterNames()
public abstract Enumeration getParameterNames()
Zwraca nazwy wszystkich parametrów jako Enumeration obiektów String. Zwraca pustą Enumeration,
jeżeli serwlet nie posiada żadnych parametrów.

getParameterValues()
public abstract String[] getParameterValues(String nazwa)
Zwraca wszystkie wartości danego parametru jako tablicę obiektów String lub null, jeżeli dany parametr nie
istnieje. Pojedyncza wartość jest zwracana jako tablica o długości 1.
getProtocol()
public abstract String getProtocol()
Zwraca nazwę i wersję protokołu wykorzystywanego przez żądanie jako String o wzorze
protokol/wersja-glowna.wersja-poboczna. Równoważna ze zmienną CGI SERVER_PROTOCOL.

getReader()
public abstract BufferedReader getReader()
throws IOException, IllegalStateExeption
Metoda ta odczytuje potok wejściowy jako obiekt BufferedReader, który powinien zostać wykorzystany do
odczytania wprowadzonych danych opartych na znakach, ponieważ mechanizm odczytujący odpowiednio
tłumaczy kodowania. Metoda zgłasza IllegalStateException, jeżeli na tym samym żądaniu poprzednio
wywołano getInputStream(). Zgłasza UnsupportedEncodingException, jeżeli kodowanie
wykorzystywane we wprowadzonych danych nie jest obsługiwane lub znane. Metoda została wprowadzona w
Servlet API 2.0.

getRealPath()
public abstract String getRealPath(String sciezka)
Zwraca bezwzględną ścieżkę systemu plików każdej danej „ścieżki wirtualnej” lub null, jeżeli tłumaczenie nie
mogło zostać przeprowadzone. Jeżeli dana ścieżka wynosi /, zwraca macierzysty katalog dokumentów serwera.
Jeżeli dana ścieżka jest taka sama, jak zwracana przez getPathInfo(), zwraca tę samą ścieżkę, jak byłaby
zwrócona przez getPathTraslated(). Dla tej metody nie istnieje równoważnik CGI. Metoda została
opuszczona w Servlet API 2.1 na rzecz metody getRealPath() w ServletContext.

getRemoteAddr()
public abstract String getRemoteAddr()
Zwraca adres IP komputera klienta jako String. Informacja ta jest odczytywana z portu łączącego serwer z
klientem, tak więc adres zdalny może być adresem serwera proxy. Równoważna zmiennej CGI REMOTE_ADDR.

getRemoteHost()
public abstract String getRemoteHost()
Zwraca nazwę komputera klienta. Informacja ta jest odczytywana z portu łączącego serwer z klientem, tak więc
nazwa może być nazwą serwera proxy. Równoważna zmiennej CGI REMOTE_HOST.

getRequestDispatcher()
public abstract RequestDispatcher getRequestDispatcher(String sciezka)
Zwraca RequestDispatcher posiadający możliwość przekierowywania żądania do danej ścieżki.
RequestDispatcher może być wykorzystany do przekierowania żądania do zasobu lub do dołączenia
zawartość tego zasobu do aktualnej odpowiedzi. Zasób może być dynamiczny lub statyczny. Określona ścieżka
może być względna, chociaż nie może wykraczać poza aktualny kontekst serwletów. Jeżeli ścieżka rozpoczyna
się od /, jest interpretowana jako względna do aktualnego macierzystego katalogu dokumentów. Metoda zwraca
null, jeżeli kontener serwletów nie może zwrócić RequestDispatcher, niezależnie od powodu. Różnica
pomiędzy tą metodą i getRequestDispatcher() w ServletContext jest taka, że ta metoda może
pobierać ścieżkę względną. Metoda została wprowadzona w Servlet API 2.2.

getScheme()
public abstract String getScheme()
Metoda zwraca schemat wykorzystany do wykonania tego żądania. Może być to na przykład http, https i
ftp, jak również jeden z nowszych właściwych Javie schematów takich jak jdbc i rmi.

GetServerName()
public abstract String getServerName()
Zwraca nazwę serwera, który otrzymał żądanie. Jest to atrybut ServletRequest, ponieważ może się
zmieniać dla różnych żądań, co jest związane z różnymi typami zależności klienta od serwera. Podobna do
zmiennej CGI SERVER_NAME.

GetServerPort()
public abstract int getServerPort()
Zwraca numer portu, który otrzymał żądanie, Równoważna zmiennej CGI SERVER_PORT.
isSecure()
public abstract boolean isSecure()
Zwraca odpowiedź, czy żądanie zostało wykonane przy pomocy bezpiecznego kanału, takiego jak HTTPS.
Metoda została wprowadzona w Servlet API 2.2.

removeAtrribute()
public abstract void removeAttribute(String nazwa)
Usuwa z żądania atrybut o danej nazwie. Atrybuty zależne od serwera generalnie nie mogą zostać usunięte.
Metoda została wprowadzona w Servlet API 2.2.

setAtrribute()
public abstract void setAttribute(String nazwa, Object o)
Dowiązuje do aktualnego żądania obiekt o danej nazwie. Wszystkie istniejące dowiązania o tej samej nazwie
zostają usunięte. Atrybuty żądania są najczęściej ustawiane jako sposób przekazywania informacji pomiędzy
serwletami wykorzystującymi RequestDispatcher. Nazwy atrybutów powinny być zgodne z tą samą
konwencją, co nazwy pakietów. Nazwy pakietów java.* i sun.* są zarezerwowane przez dział Java Software
firmy Sun Microsystems (dawniej znany jako JavaSoft), a com.sun.* jest zarezerwowany dla użytku firmy
Sun Microsystems. Metoda została wprowadzona w Servlet API 2.2.

ServletResponse

Zestawienie
Nazwa interfejsu javax.servlet.ServletResponse
Superinterfejs Brak
Bezpośrednie podinterfejsy javax.servlet.http.HttpServletResponse
Implementowany przez Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Serwlety wykorzystują obiekty ServletResponse do wysyłania zakodowanych w MIME danych z powrotem
do klienta. Aby wysłać dane binarne, należy wykorzystać ServletOutputStream zwrócony przez
getOutputStream(). W celu wysłania danych zawierających znaki należy wykorzystać PrintWriter
zwrócony przez getWriter(). Można otwarcie ustawić typ MIME wyświetlanych danych przy pomocy
metody setContentType(), a lokalizację odpowiedzi przy pomocy setLocale(). Należy wykonać te
wywołania przed wywołaniem getWriter(), ponieważ metoda getWriter() sprawdza typ zawartości i
lokalizację w celu określenia kodowania, które należy wykorzystać. Więcej informacji na temat MIME jest
dostępnych w dokumencie RFC 2045 pod adresem http://www.ietf.org/rfc/rfc2045.txt.

Deklaracja interfejsu
public interface ServletResponse {
// Metody
public abstract void flushBuffer() throws IOException; // Nowość w
2.2
public abstract int getBufferSize(); // Nowość w
2.2
public abstract String getCharacterEncoding(); // Nowość w
2.0
public abstract Locale getLocale(); // Nowość w
2.2
public abstract ServletOutputStream getOutputStream()
throws IOException, IllegalStateException;
public abstract PrintWriter getWriter() // Nowość w
2.0
throws IOException, IllegalStateException;
public abstract boolean isCommitted(); // Nowość w
2.2
public abstract void reset() throws IllegalStateException; // Nowość w
2.2
public abstract void setBufferSize(int wielkosc) // Nowość w
2.2
throws IllegalStateException;
public abstract void setContentLength(int dlug);
public abstract void setContentType(String typ);
public abstract void setLocale(Locale lok); // Nowość w
2.2
}

Metody
FlushBuffer()
public abstract void flushBuffer() throws IOException;
Wymusza wyświetlenie klientowi wszystkich danych znajdujących się w buforze. Wywołanie tej metody
automatycznie zatwierdza odpowiedź, co oznacza, że kod stanu oraz nagłówki zostaną zapisane, a metoda
reset() nie będzie już dostępna. Metoda została wprowadzona w Servlet API 2.2.

getBufferSize()
public abstract int getBufferSize();
Zwraca liczbę int informującą o wielkości aktualnego bufora lub 0 w rzadkim przypadku, kiedy buforowanie
nie jest wykorzystywane. Metoda została wprowadzona w Servlet API 2.2.

GetCharacterEncoding()
public abstract String getCharacterEncoding()
Zwraca kodowanie wykorzystywane w aktualnej części głównej MIME. Jest to kodowanie określone przez
przydzielony typ zawartości, lub ISO-8859-1, jeżeli nie określono żadnego kodowania. Metoda została
wprowadzona w Servlet API 2.0.

getLocale()
public abstract Locale getLocale()
Zwraca lokalizację Locale aktualnie przypisaną do odpowiedzi. Metoda została wprowadzona w Servlet API
2.2.

GetOutputStream()
public abstract ServletOutputStream getOutputStream()
throws IOException, IllegalStateException;
Zwraca ServletOutputStream służący do zapisywania binarnych (bit-po-bicie) danych odpowiedzi. Nie
jest wykonywane żadne kodowanie. Zgłasza IllegalStateException, jeżeli na aktualnej odpowiedzi
wywołano wcześniej getWriter().

getWriter()
public abstract PrintWriter getWriter()
throws IOException, IllegalStateException;
Zwraca PrintWriter służący do zapisywania opartych na znakach danych odpowiedzi. Mechanizm
wyświetlający koduje znaki zależnie od jakiegokolwiek kodowania podanego w typie zawartości. Jeżeli w typie
zawartości nie określono żadnego kodowania, jak to się zdarza najczęściej, mechanizm wykorzystuje kodowanie
ISO-8859-1 (Latin-1), odpowiednie dla języków zachodnioeuropejskich. Zgłasza
IllegalStateException, jeżeli na aktualnej odpowiedzi wywołano wcześniej getOutputStream(), a
UnsupportedEncodingException, jeżeli kodowanie potoku wyjściowego nie jest obsługiwane lub znane.
Metoda została wprowadzona w Servlet API 2.2.

isCommitted()
public abstract boolean isCommitted()
Zwraca wartość boolean wskazującą, czy jakakolwiek część odpowiedzi została już wysłana. Jeżeli metoda
zwróci true, jest już za późno, by zmienić kody stanu i nagłówki. Metoda została wprowadzona w Servlet API
2.2.

reset()
public abstract void reset() throws IllegalStateException;
Czyści bufor odpowiedzi, a także aktualnie przypisany kod stanu i nagłówki odpowiedzi. Metoda ta musi zostać
wywołana przed zatwierdzeniem odpowiedzi, inaczej zgłasza IllegalStateException. reset() jest
automatycznie wywoływana przez metody sendError() i sendRedirect(). Metoda została wprowadzona
w Servlet API 2.2.
setBufferSize()
public abstract void setBufferSize(int wielkosc) throws IllegalStateException;
Przekazuje serwerowi minimalną wielkość (w bajtach) bufora odpowiedzi, którą zaakceptuje serwlet. Serwer
może dostarczyć większego bufora niż ten, którego zażądano — na przykład w celu utrzymania buforów jako
bloków o wielkości 8Kb w celu ułatwienia ich ponownego wykorzystania. Większy bufor pozwala na zapisanie
dłuższej zawartości przez jej właściwym wysłaniem, a w związku z tym daje serwletowi więcej czasu na
ustawienie odpowiednich kodów stanu i nagłówków. Mniejszy bufor zmniejsza obciążenie pamięci serwera i
pozwala na szybsze otrzymywanie danych przez klienta. Metoda ta musi zostać wywołana przed zapisaniem
dowolnej zawartości w głównej części odpowiedzi; jeżeli zawartość została zapisana, metoda zgłasza wyjątek
IllegalStateException. Metoda została wprowadzona w Servlet API 2.2.

setContentLength()
public abstract void setContentLength(int dlug)
Ustawia długość zawartości zwracanej przez serwer. W przypadku serwletów HTTP, ustawia nagłówek HTTP
Content-Length. Serwlety HTTP wykorzystuję tę metodę w celu umożliwienia trwałych połączeń i
wspomożenia monitorów postępu klienta. Jej wykorzystanie jest opcjonalne. Jeżeli zawartość odpowiedzi mieści
się całkowicie w wielkości przydzielonego bufora odpowiedzi, serwer może automatycznie ustawić długość
zawartości.

setContentType()
public abstract void setContentType(String typ)
Metoda ustawia typ zawartości odpowiedzi na typ określony. W przypadku serwletów HTTP, ustawia nagłówek
HTTP Content-Type.

setLocale()
public abstract void setLocale(Locale lok)
Ustawia lokalizację odpowiedzi. Serwer modyfikuje nagłówki Content-Language i Content-Type
odpowiednio do danej lokalizacji. Metoda powinna zostać wywołana po ustawieniu Content-Type, a przed
wywołaniem getWriter(). Domyślnie lokalizacja odpowiedzi jest równa domyślnej lokalizacji serwera.
Metoda została wprowadzona w Servlet API 2.2.

SingleThreadModel

Zestawienie
Nazwa interfejsu javax.servlet.SingleThreadModel
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 2.0 i późniejsze

Opis
SingleThreadModel jest interfejsem znaczników nie posiadającym metod. Jeżeli serwlet implementuje ten
interfejs, kontener serwletów upewnia się, że każdy egzemplarz serwletu obsługuje jedynie jedno żądanie usługi
w jednym czasie. Na przykład, kontener serwletów może zaimplementować tę funkcjonalność poprzez
utrzymywanie puli egzemplarzy serwletów i przekierowywanie przychodzących żądań do wolnych serwletów w
puli. Wykorzystanie SingleThreadModel zabezpiecza sam serwlet przed wątków; jednak wykorzystanie
tego interfejsu nie przeciwdziała problemom z synchronizacją, które są powodowane przez serwlety uzyskujące
dostęp do współdzielonych zasobów takich jak statyczne zmienne klas lub zmienne zdalne dla serwletu. Interfejs
ten jest przydatny w bardzo niewielkiej ilości sytuacji.

Deklaracja interfejsu
public interface SingleThreadModel {
}
UnavailableException

Zestawienie
Nazwa klasy javax.servlet.UnavailableException
Superklasa java.io.ServletException
Bezpośrednie podklasy Brak
Implementowane interfejsy Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Serwlet może zgłosić wyjątek UnavailableException w dowolnym czasie w celu wskazania, że nie jest
dostępny w celu zaspokajania żądań klientów. Istnieją dwa typy niedostępności — stała (w której problem nie
może sam się rozwiązać i musi zostać podjęta akcja administracyjna) i tymczasowa (w której problem
najprawdopodobniej sam się rozwiąże po pewnym czasie). Aby oznaczyć serwlet jako czasowo niedostępny
należy podczas konstruowania wyjątku określić czas (w sekundach). Dobrze napisane kontenery serwletów
wykorzystają określony czas w celu utworzenia lepszych komunikatów o błędach dla klienta. Implementacje
serwletów mogą traktować niedostępność tymczasową jako niedostępność stałą.

Podsumowanie klasy
public class UnavailableException extends ServletException {
// Konstruktory
public UnavailableException(int sekundy, Servlet serwlet, String wiad); //
Opuszczona
public UnavailableException(Servlet serwlet, String wiad); // Opuszczona
public UnavailableException(String wiad); // Nowość w
2.2
public UnavailableException(String wiad, int sekundy); // Nowość w
2.2

// Metody egzemplarzy
public Servlet getServlet(); // Opuszczona
public int getUnavailableSeconds();
public boolean isPermanent();
}

Konstruktory
UnavailableException()
public UnavailableException(String wiad)
public UnavailableException(String wiad, int sekundy)
public UnavailableException(Servlet serwlet, String wiad)
public UnavailableException(int sekundy, Servlet serwlet, String wiad)
Konstruuje wyjątek UnavailbleException z daną wyjaśniającą wiadomością. Okres niedostępności może
zostać opcjonalnie określony (w sekundach). Dwa konstruktory z podpisami, które przyjmują parametr Servlet
zostały opuszczone w Servlet API 2.2 na rzecz prostszych i bezpieczniejszych wersji konstruktora nie
przyjmujących egzemplarza serwletu.

Metody egzemplarzy
getServlet()
public Servlet getServlet()
Zwraca serwlet, który zgłosił aktualny wyjątek lub null, jeżeli egzemplarz serwletu nie został dostarczony
konstruktorowi. Metoda została opuszczona w Servlet API 2.2 ze względów bezpieczeństwa.

getUnavailableSeconds()
public int getUnavailableSeconds()
Zwraca ilość sekund, przez które serwlet będzie niedostępny. Niedodatnia liczba sekund wskazuje na
niedostępność stałą. Nie jest wykonywana żadna próba obliczenia czasu określonego od zgłoszenia wyjątku.

isPermanent()
public boolean isPermanent()
Zwraca true, jeżeli serwlet jest niedostępny stale, a w przeciwnym wypadku false.
Dodatek B. Krótki opis HTTP

Servlet API

Pakiet javax.servlet.http umożliwia obsługę serwletów porozumiewających się przy pomocy protokołu
HTTP. Klasy w tym pakiecie są utworzone na podstawie podstawowej funkcjonalności pakietu
javax.servlet i umożliwiają serwletom dostęp do specyficznych dla HTTP własności takich jak kody stanu,
nagłówki żądania i odpowiedzi, sesje i cookies. Rysunek B.1 przedstawia hierarchię klas pakietu
javax.servlet.http.
Rysunek B.1.
Pakiet javax.servlet.http

Cookie

Zestawienie
Nazwa klasy javax.servlet.http.Cookie
Superklasa java.lang.Object
Bezpośrednie podklasy Brak
Implementowane interfejsy java.lang.Cloneable
Dostępność Servlet API 2.0 i późniejsze

Opis
Klasa Cookie dostarcza serwletom łatwego sposobu odczytywania, tworzenia i manipulowania cookies w stylu
HTTP, które umożliwiają serwletom przechowywanie niewielkich ilości danych na komputerze klienta. Cookies
są ogólnie wykorzystywane do śledzenia sesji lub przechowywania niewielkich ilości informacji
konfiguracyjnych specyficznych dla danego klienta. Większa ilość informacji znajduje się w rozdziale 7,
„Śledzenie sesji”.
Serwlet wykorzystuje metodę getCookies() w celu odczytania cookies wysłanych jako część żądania klienta.
Metoda addCookie() HttpServletResponse wysyła do przeglądarki nowe cookie. Ponieważ cookie są
ustawiane przy pomocy nagłówków HTTP, addCookie() musi zostać wywołana przed zatwierdzeniem
odpowiedzi.
Metody getXXX() są rzadko wykorzystywane, ponieważ kiedy cookie zostaje wysłane do serwera, zawiera
jedynie jego nazwę, wartość i wersję, Jeżeli ustawi się atrybut w cookie otrzymanym od klienta, należy dodać go
do odpowiedzi, aby zmiana wywarła efekt, a także należy zadbać o to, by wszystkie atrybuty poza nazwą,
wartością i wersją zostały ponownie ustawione również w cookie.
Niniejsza klasa jest zgodna zarówno ze specyfikacją cookie Netscape, jak i dokumentem RFC 2109.

Podsumowanie klasy
public class Cookie implements java.lang.Cloneable {
// Konstruktory
public Cookie(String nazwa, String wartosc);

// Metody egzemplarzy
public Object clone();
public String getComment;
public String getDomain();
public int getMaxAge();
public String getName();
public String getPath();
public boolean getSecure();
public String getValue();
public int getVersion();
public void setComment(String cel);
public void setDomain(String wzor);
public void setMaxAge(int wygasa);
public void setPath(String uri);
public void setSecure(boolean znacznik);
public void setValue(String nowaWartosc);
public void setVersion(int w);
}

Konstruktory
Cookie()
public Cookie(String nazwa, String wartosc)
Konstruuje nowe cookie o początkowej nazwie i wartości. Zasady nadawania prawidłowych nazw i wartości są
określone w specyfikacji cookie Netscape i dokumencie RFC 2109.

Metody egzemplarzy
clone()
public Object clone()
Omija standardową metodę clone() w celu zwrócenia kopii aktualnego obiektu (duplikatu cookie).

getComment()
public String getComment
Zwraca komentarz związany z cookie. Informacja ta jest dostępna jedynie zaraz po ustawieniu komentarza; kiedy
cookie jest zwracane przez klienta, informacja ta nie zostaje dołączona.

GetDomain()
public String getDomain()
Zwraca ograniczenia domeny związane z aktualnym cookie. Informacja ta jest dostępna jedynie zaraz po
ustawieniu domeny; kiedy cookie jest zwracane przez klienta, informacja ta nie zostaje dołączona.

getMaxAge()
public int getMaxAge()
Zwraca maksymalny wiek dozwolony dla aktualnego cookie. Informacja ta jest dostępna jedynie zaraz po
ustawieniu maksymalnego wieku; kiedy cookie jest zwracane przez klienta, informacja ta nie zostaje dołączona.

getName()
public String getName()
Zwraca nazwę aktualnego cookie.

getPath()
public String getPath()
Zwraca ograniczenia ścieżki związane z aktualnym cookie. Informacja ta jest dostępna jedynie zaraz po
ustawieniu ścieżki; kiedy cookie jest zwracane przez klienta, informacja ta nie zostaje dołączona.

getSecure()
public boolean getSecure()
Zwraca true, jeżeli aktualne cookie wymaga bezpiecznego połączenia, w przeciwnym wypadku false.
Informacja ta jest dostępna jedynie zaraz po ustawieniu znacznika; kiedy cookie jest zwracane przez klienta,
informacja ta nie zostaje dołączona.

getValue()
public String getValue()
Zwraca wartość aktualnego cookie w formie łańcucha.

getVersion()
public int getVersion()
Zwraca wersję aktualnego cookie.

setComment()
public void setComment(String cel)
Ustawia pole komentarza cookie. Komentarz opisuje zamierzone przeznaczenie cookie. Przeglądarka WWW
może wyświetlić ten tekst klientowi. Komentarze nie są obsługiwane przez cookies w wersji 0.

setDomain()
public void setDomain(String wzor)
Określa wzór ograniczenia domeny. Wzór domeny określa serwery, które powinny widzieć cookie. Domyślnie
cookies są zwracane jedynie do komputera, który je zapisał. Określenie wzoru nazwy domeny omija tę zasadę.
Wzór musi rozpoczynać się kropką i zawierać co najmniej dwie kropki. Wzór pasuje jedynie do jednej pozycji
poza początkową kropką. Na przykład wzór .buu.com jest prawidłowy i pasuje do www.buu.com i
wyslij.buu.com, ale nie do www.wyslij.buu.com. Szczegółowe informacje na temat wzorów domen są
dostępne w specyfikacji cookie Netscape i dokumencie RFC 2109.

SetMaxAge()
public void setMaxAge(int wygasa)
Określa maksymalny wiek cookie w sekundach do jego wygaśnięcia. Ujemna wartość wskazuje na działanie
domyślne, czyli na wygaśnięcie cookie z chwilą wyłączenia przeglądarki. Wartość zerowa nakazuje przeglądarce
natychmiastowe usunięcie cookie.

setPath()
public void setPath(String uri)
Określa ścieżkę cookie, która jest podzbiorem URI, do których powinno zostać wysłane cookie. Domyślnie
cookie są wysyłane do strony, która je ustawiła oraz do wszystkich stron w jej katalogu lub podkatalogach. Na
przykład, jeżeli /servlet/CiasteczkowyPotwor ustawiła cookie, domyślną ścieżką jest /servlet. Ścieżka ta
wskazuje, że cookie powinno zostać wysłane do /servlet/Elmo i /servlet/podkat/WielkiPtak — ale nie do aliasu
serwletu /Oskar.html ani do żadnego programu CGI w /cgi-bin. Ścieżka ustawiona na „/” powoduje wysłanie
cookie do wszystkich stron na serwerze. Ścieżka cookie musi być tak skonstruowana, aby uwzględniała serwlet,
który je ustawił.

SetSecure()
public void setSecure(boolean znacznik)
Znacznik bezpieczeństwa wskazuje, czy cookie powinno zostać wysłane jedynie przez kanał bezpieczny, taki jak
SSL. Domyślna wartość wynosi false.

SetValue()
public void setValue(String nowaWartosc)
Przypisuje cookie nową wartość. W przypadku cookie w wersji 0, wartości nie mogą zawierać: pustych miejsc,
klamr, cudzysłowów pojedynczych i podwójnych, nawiasów, znaków równości, przecinków, ukośników, znaków
zapytania, dwukropków i średników. Puste wartości mogą zachowywać się w różny sposób w różnych
przeglądarkach.

SetVersion()
public void setVersion(int w)
Serwlety mogą wysyłać i przyjmować cookies sformatowane tak, aby zgadzały się zarówno trwałymi cookies
Netscape (wersja 0), jak i nowszymi, w pewnym sensie eksperymentalnymi cookies zgodnymi z dokumentem
RFC 2109 (wersja 1). Nowo skonstruowane cookies są domyślnie sformatowane zgodnie z wersją 0 w celu
zmaksymalizowania zasięgu. Podczas wykorzystywania cookies w wersji 1 kontener serwletów może również
wysyłać cookies w stylu wersji 0 o tej samej nazwie i wartości w celu zachowania wstecznej kompatybilności.

HttpServlet

Zestawienie
Nazwa klasy javax.servlet.http.HttpServlet
Superklasa Javax.servlet.GenericServlet
Bezpośrednie podklasy Brak
Implementowane interfejsy java.servlet.Servlet
java.io.Serializable
Dostępność Servlet API 1.0 i późniejsze

Opis
HttpServlet jest abstrakcyjną klasą służącą jako podstawowa klasa dla serwletów HTTP (WWW). Publiczna
metoda service() przekierowuje żądania do specyficznej dla HTTP, chronionej metody service(), która
następnie przekazuje żądania do konkretnych funkcji obsługujących każdy typ wysyłania HTTP — doGet(),
doPost() i tak dalej. Ponieważ domyślna implementacja serwletów HTTP obsługuje przekierowania do tych
metod, to jeżeli ominie się chronioną metodę service(), należy samodzielnie obsłużyć przekierowania, lub
nie wykorzystywać funkcji obsługujących dla metod żądań HTTP.

Podsumowanie klasy
public abstract class HttpServlet extends javax.servlet.GenericServlet
implements javax.servlet.Servlet, java.io.Serializable {
// Konstruktory
public HttpServlet();

// Publiczna metoda egzemplarzy


public void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException;

// Chronione metody egzemplarzy


public void doDelete(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException; // Nowość w
2.0
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException;
public void doOptions(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException; // Nowość w
2.0
public void doPost(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException;
public void doPut(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException; // Nowość w
2.0
public void doTrace(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException; // Nowość w
2.0
public void getLastModified(HttpServletRequest zad);
public void service(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException; // Nowość w
2.0
}
Konstruktory
HttpServlet()
public HttpServlet()
Domyślny konstruktor nie wykonuje żadnych działań. Ponieważ nie można mieć pewności co do sposobu i czasu
ładowania klas, nie poleca się omijania tego konstruktora w celu wykonania czynności startowych. Zamiast tego
należy wykorzystać metodę init().

Publiczne metody egzemplarzy


service()
public void service(ServletRequest zad, ServletResponse odp)
throws ServletException, IOException
Ta metoda service() obsługuje przekierowywanie żądań do chronionej, specyficznej dla HTTP metody
service(). Ogólnie rzecz biorąc nie powinna być omijana.

Chronione metody egzemplarzy


doDelete()
public void doDelete(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP DELETE do
tej metody. Serwlety implementują tę metodę w celu obsługi żądań DELETE. Domyślna implementacja zwraca
błąd HTTP SC_BAD_REQUEST. Metoda została wprowadzona w Servlet API 2.0.

doGet()
public void doGet(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP GET do tej
metody. Serwlety implementują tę metodę w celu obsługi żądań DELETE. Domyślna implementacja zwraca błąd
HTTP SC_BAD_REQUEST.

doOptions()
public void doOptions(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP OPTIONS
do tej metody. Domyślna implementacja określa, które opcje są obsługiwane i zwraca odpowiedni nagłówek. Na
przykład, jeżeli serwlet omija doGet() i doPost(), przeglądarka otrzymuje informację, że obsługiwane są
GET, POST, HEAD, TRACE i OPTIONS. Niemal nigdy nie ma powodu, by omijać tę metodę. Metoda została
wprowadzona w Servlet API 2.0.

doPost()
public void doPost(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP POST do tej
metody. Serwlety implementują tę metodę w celu obsługi żądań POST. Domyślna implementacja zwraca błąd
HTTP SC_BAD_REQUEST.

doPut()
public void doPut(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP PUT do tej
metody. Serwlety implementują tę metodę w celu obsługi żądań PUT. Domyślna implementacja zwraca błąd
HTTP SC_BAD_REQUEST. Większa ilość informacji na temat żądań HTTP PUT jest dostępna w dokumencie
RFC 2068 pod adresem http://www.ietf.org/rfc/rfc2068.txt. Metoda została wprowadzona w Servlet API 2.0.

doTrace()
public void doTrace(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Domyślna implementacja service() w HttpServlet przekierowuje wszystkie żądania HTTP TRACE do
tej metody. Serwlety implementują tę metodę w celu obsługi żądań TRACE. Niemal nigdy nie ma powodu, by
omijać tę metodę. Metoda została wprowadzona w Servlet API 2.0.

getLastModified()
public void getLastModified(HttpServletRequest zad)
Zwraca datę i godzinę (wyrażoną w milisekundach od północy 1 stycznia 1970 GMT) ostatniej modyfikacji
zawartości tworzonej przez serwlet. Wartości ujemne oznaczają, że czas ten nie jest znany. Domyślna
implementacja zwraca –1. Metoda jest wywoływana przez serwery w celu wspomożenia obsługi warunkowych
żądań HTTP GET i zarządzania pamięcią podręczną odpowiedzi. Większa ilość informacji znajduje się w
rozdziale 4, „Pobieranie informacji”.

service()
public void service(HttpServletRequest zad, HttpServletResponse odp)
throws ServletException, IOException
Publiczna metoda service() przekierowuje żądania do tej metody service(). Metod obsługuje
przekierowywanie żądań do doGet(), doPost() i innych funkcji obsługujących w oparciu o typ żądania.
Jeżeli metoda zostanie ominięta, nie są wywoływane żadne funkcje obsługujące.

HttpServletRequest

Zestawienie
Nazwa interfejsu javax.servlet.http.HttpServletRequest
Superinterfejs javax.servlet.ServletRequest
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
HttpServletRequest rozszerza podstawową klasę ServerRequest dostarczając dodatkowej
funkcjonalności serwletom HTTP (WWW). Zawiera obsługę cookies i śledzenia sesji oraz dostęp do informacji z
nagłówków HTTP. HttpServletRequest analizuje również przychodzące dane formularzy HTTP i
przechowuje je jako parametry serwletów. Serwer przekazuje obiekt HttpServletRequest metodzie
usługowej HttpServlet.

Deklaracja interfejsu
public interface HttpServletRequest extends javax.servlet.ServletRequest {
// Metody
public abstract String getAuthType();
public abstract String getContextPath(); // Nowość w
2.2
public abstract Cookie[] getCookies(); // Nowość w
2.0
public abstract long getDateHeader(String nazwa);
public abstract String getHeader(String nazwa);
public abstract Enumeration getHeaderNames();
public abstract Enumeration getHeaders(String nazwa); // Nowość w
2.2
public abstract int getIntHeader(String nazwa);
public abstract String getMethod();
public abstract String getPathInfo();
public abstract String getPathTranslated();
public abstract String getQueryString();
public abstract String getRemoteUser();
public abstract String getRequestedSessionId(); // Nowość w
2.0
public abstract String getRequestURI();
public abstract String getServletPath();
public abstract HttpSession getSession(); // Nowość w
2.1
public abstract HttpSession getSession(boolean tworz); // Nowość w
2.0
public abstract java.security.Principal getUserPrincipal(); // Nowość w
2.2
public abstract boolean isRequestedSessionIdFromCookie(); // Nowość w
2.0
public abstract boolean isRequestedSessionIdFromUrl(); // Opuszczona
public abstract boolean isRequestedSessionIdFromURL(); // Nowość w
2.1
public abstract boolean isRequestedSessionIdValid(); // Nowość w
2.0
public abstract boolean isUserInRole(String rola); // Nowość w
2.2
}

Metody
getAuthType()
public abstract String getAuthType()
Zwraca schemat uwierzytelniania serwletu lub null, jeżeli serwlet nie jest chroniony przez mechanizm kontroli
dostępu. Możliwe schematy to BASIC, DIGEST, FORM i CLIENT-CERT. Równoważna zmiennej CGI
AUTH_TYPE.

getContextPath()
public abstract String getContextPath()
Zwraca część żądanego URI wskazującą na kontekst (aplikację WWW) żądania. Ścieżka kontekstu zawsze
znajduje się na początku URI żądania. Ścieżka rozpoczyna się znakiem /, ale nie kończy się / i jest zwracana
bezpośrednio bez zdekodowania URL. W przypadku serwletów działających w głównym (ROOT) kontekście,
metoda zwraca pusty łańcuch. Metoda została wprowadzona w Servlet API 2.2.

getCookies()
public abstract Cookie[] getCookies()
Zwraca tablicę obiektów Cookie zawierającą wszystkie cookies wysłane przez przeglądarkę jako część żądania
lub null, jeżeli nie wysłano żadnych cookies. Metoda została wprowadzona w Servlet API 2.0.

getDateHeader()
public abstract String getHeader(String nazwa)
Zwraca wartość danego nagłówka jako długą wartość reprezentującą Date (milisekundy od północy 1 stycznia
1970 GMT) lub –1, jeżeli nagłówek nie został wysłany jako część żądania. Nazwa nie zwraca uwagi na wielkość
liter. Zgłasza IllegalArgumentException, jeżeli zostanie wywołana na nagłówku, którego wartość nie
może zostać przekonwertowana na Date. Metoda ta jest przydatna podczas obsługi nagłówków takich jak
Last-Modified i If-Modified-Since.

getHeader()
public abstract String getHeader(String nazwa)
Zwraca wartość danego nagłówka jako łańcuch String lub null, jeżeli nagłówek nie został wysłany jako
część żądania. Nazwa nie zwraca uwagi na wielkość liter. Metoda ta może odczytywać wszystkie typy
nagłówków.

getHeaderNames()
public abstract Enumeration getHeaderNames()
Zwraca nazwy wszystkich nagłówków, do których serwlet może uzyskać dostęp, jako Enumeration obiektów
String lub pustą Enumeration, jeżeli nie występują żądne nagłówki. Niektóre implementacje serwletów
mogą nie pozwalać na dostęp do nagłówków w ten sposób, w którym to przypadku metoda zwróci null.

getHeaders()
public abstract Enumeration getHeaders(String nazwa)
Zwraca wszystkie wartości danego nagłówka jako Enumeration obiektów String. Niektóre nagłówki, takie
jak Accept-Language, mogą być wysyłane klientom jako kilka nagłówków, każdy z inną wartością. Ta
metoda pozwala na odczytanie wszystkich wartości. Jeżeli żądania nie zawiera żadnych nagłówków o danej
nazwie, metoda zwraca pustą Enumeration. Nazwa nie zwraca uwagi na wielkość liter. Metoda została
wprowadzona w Servlet API 2.2.

getIntHeader()
public abstract int getIntHeader(String nazwa)
Zwraca wartość danego nagłówka jako liczbę int lub –1, jeżeli nagłówek nie został wysłany jako część
żądania. Nazwa nie zwraca uwagi na wielkość liter. Zgłasza wyjątek NumberFormatException, jeżeli
zostanie wywołana na nagłówku o wartości, która nie może zostać przekonwertowana na int.

getMethod()
public abstract String getMethod();
Zwraca metodę HTTP wykorzystaną do wysłania żądania. Może to być na przykład GET, POST lub HEAD.
Równoważna zmiennej CGI REQUEST_METHOD. Implementacja service() HttpServlet wykorzystuje tę
metodę podczas przekierowywania żądań.

GetPathInfo()
public abstract String getPathInfo()
Zwraca dodatkowe informacje na temat ścieżki związane z żądaniem lub null, jeżeli informacje te nie
występują. Ścieżka jest zdekodowana przez URL przed zwróceniem. Równoważna zmiennej CGI PATH_INFO.

getPathTranslated()
public abstract String getPathTranslated()
Zwraca dodatkowe informacje na temat ścieżki przetłumaczone na ścieżkę systemu plików lub null, jeżeli
dodatkowe informacje nie występują lub kontener serwletów nie mógł stworzyć prawidłowej ścieżki do pliku (na
przykład kiedy plik znajduje się w zdalnym systemie plików lub może zostać odnaleziony jedynie wewnątrz
archiwum .war). Zwracana ścieżka nie musi koniecznie wskazywać na istniejący plik lub katalog. Wywołanie jest
podobne do zmiennej CGI PATH_TRANSLATED. Jest także równoważne getServletContext().
getRealPath(zad.getPathInfo()).

getQueryString()
public abstract String getQueryString()
Zwraca łańcuch zapytania z URL-a żądania. Wartość jest równoważna wartości zmiennej CGI QUERY_STRING.
Ponieważ HttpServletRequest przetwarza ten łańcuch w zbiór parametrów serwletów dostępnych poprzez
getParameter(), większość serwletów może ignorować tę metodę.

getRemoteUser()
public abstract String getRemoteUser()
Zwraca nazwę użytkownika wykonującego żądanie jako String lub null, jeżeli dostęp do serwletu nie został
ograniczony. Równoważna zmiennej CGI REMOTE_USER. Ogólnie wymaga zalogowania użytkownika przy
pomocy uwierzytelniania HTTP. Nie istnieje porównywalna metoda służąca do bezpośredniego odczytywania
hasła użytkownika zdalnego.

getRequestedSessionId()
public abstract String getRequestedSessionId()
Metoda zwraca identyfikator sesji określony przez klienta. Nie musi być to prawdziwy identyfikator sesji będący
aktualnie w użyciu — na przykład, jeżeli sesja wygasła przez pojawieniem się żądania, serwer tworzy nowy
identyfikator sesji i używa go zamiast poprzedniego. Metoda została wprowadzona w Servlet API 2.0.

getRequestURI()
public abstract String getRequestURI()
Zwraca Universal Resource Identifier (Uniwersalny Identyfikator Zasobów — URI) żądania. Jest to zasób
żądany przez klienta w pierwszej linii jego żądania HTTP, wszystko pomiędzy protokołem i łańcuchem
zapytania. W przypadku zwykłych serwletów HTTP, URI żądania to URI żądania bez schematu, komputera,
portu i łańcucha zapytania, ale z dodatkowymi informacjami na temat ścieżki. Ścieżka jest zwracana
bezpośrednio, bez dekodowania URL. Wczesne wersje Servlet API definiowały i implementowały tę metodę na
różne sposoby. Podczas tworzenia kodu zależnego od tej metody należy upewnić się co do jej wyników w
zależności od różnych wersji Servlet API.

getServletPath()
public abstract String getServletPath()
Zwraca część URI odnoszącą się do serwletu. Ścieżka przechodzi dekodowanie URL przed zwróceniem i nie
zawiera żadnych dodatkowych informacji, ani też łańcucha zapytania. W przypadku dopasowań do rozszerzeń
plików ścieżka nie zawiera tych rozszerzeń. Równoważna zmiennej CGI SCRIPT_NAME.
getSession()
public abstract HttpSession getSession()
public abstract HttpSession getSession(boolean tworz)
Zwraca aktualną sesję związaną z użytkownikiem wykonującym żądanie. Jeżeli użytkownik nie posiada aktualnej
prawidłowej sesji, metoda tworzy nową, jeżeli tworz posiada wartość true lub zwraca null, jeżeli tworz
wynosi false. Wersja bez argumentów posiada tworz ustawioną wewnętrznie na true. Aby zapewnić
prawidłowe utrzymanie sesji, metoda powinna zostać wywołana przynajmniej raz przed przesłaniem w
odpowiedzi dowolnej zawartości. Serwlety nie wykorzystujące śledzenia sesji mogą zignorować tę metodę.
Metoda została wprowadzona w Servlet API 2.0. Wersja bez argumentów została wprowadzona w Servlet API
2.1.

getUserPrincipal()
public abstract java.security.Principal getUserPrincipal()
Zwraca obiekt java.security.Principal zawierający nazwę obecnie uwierzytelnianego użytkownika.
Jeżeli użytkownik nie został uwierzytelniony, metoda zwraca null. Metoda została wprowadzona w Servlet API
2.2.

isRequestedSessionIdFromCookie()
public abstract boolean isRequestedSessionIdFromCookie()
Zwraca true, jeżeli klient przesłał identyfikator sesji przez cookie, w przeciwnym przypadku false. Metoda
została wprowadzona w Servlet API 2.0.

isRequestedSessionIdFromURL()
public abstract boolean isRequestedSessionIdFromUrl()
public abstract boolean isRequestedSessionIdFromURL()
Zwraca true, jeżeli żądany identyfikator sesji został przesłany przez napisany ponownie URL, w przeciwnym
wypadku false. Metoda isRequestedSessionIdFromUrl() została wprowadzona w Servlet API 2.0,
następnie opuszczona w Servlet API 2.1 z wprowadzeniem bardziej standardowo nazwanej metody
isRequestedSessionIdFromURL().

isRequestedSessionIdValid()
public abstract boolean isRequestedSessionIdValid()
Zwraca true, jeżeli sesja żadna przez klienta jest prawidłową sesją i w związku z tym jest sesją obecnie
wykorzystywaną. W przypadku sesji nowych i tych, które wygasły zwraca false. Metoda została wprowadzona
w Servlet API 2.0.

isUserInRole()
public abstract boolean isUserInRole(String rola)
Zwraca wartość boolean wskazującą, czy uwierzytelniony użytkownik posiada określona logiczną „rolę”. Role
i członkostwo w rolach mogą być definiowane przy pomocy deskryptorów, a ich odwzorowanie względem
użytkowników jest wykonywane przy pomocy narzędzi administracyjnych serwera. Jeżeli użytkownik nie został
uwierzytelniony, metoda zawsze zwraca false. Metoda została wprowadzona w Servlet API 2.2.

HttpServletResponse

Zestawienie
Nazwa interfejsu javax.servlet.http.HttpServletResponse
Superinterfejs javax.servlet.ServletResponse
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
HttpServletResponse rozszerza klasę ServletResponse w celu umożliwienia manipulacji danymi
specyficznymi dla protokołu HTTP, włączając w to nagłówki odpowiedzi i kody stanu. Definiuje również pewną
ilość stałych reprezentujących różne kody stanu HTTP i zawiera funkcje wspomagające operacje śledzenia sesji.
Deklaracja interfejsu
public interface HttpServletResponse extends javax.servlet.ServletResponse {
// Stałe
public static final int SC_ACCEPTED;
public static final int SC_BAD_GATEWAY;
public static final int SC_BAD_REQUEST;
public static final int SC_CONFLICT;
public static final int SC_CONTINUE; // Nowość w
2.0
public static final int SC_CREATED;
public static final int SC_EXPECTATION_FAILED; // Nowość w
2.2
public static final int SC_FORBIDDEN;
public static final int SC_GATEWAY_TIMEOUT; // Nowość w
2.0
public static final int SC_GONE; // Nowość w
2.0
public static final int SC_HTTP_VERSION_NOT_SUPPORTED; // Nowość w
2.0
public static final int SC_INTERNAL_SERVER_ERROR;
public static final int SC_LENGTH_REQUIRED; // Nowość w
2.0
public static final int SC_METHOD_NOT_ALLOWED; // Nowość w
2.0
public static final int SC_MOVED_PERMAMENTLY;
public static final int SC_MOVED_TEMPORARILY;
public static final int SC_MULTIPLE_CHOICES; // Nowość w
2.0
public static final int SC_NO_CONTENT;
public static final int SC_NON_AUTHORITATIVE_INFORMATION; // Nowość w
2.0
public static final int SC_NOT_ACCEPTABLE; // Nowość w
2.0
public static final int SC_NOT_FOUND;
public static final int SC_NOT_IMPLEMENTED;
public static final int SC_NOT_MODIFIED;
public static final int SC_OK;
public static final int SC_PARTIAL_CONTENT; // Nowość w
2.0
public static final int SC_PAYMENT_REQUIRED; // Nowość w
2.0
public static final int SC_PRECONDITION_FAILED; // Nowość w
2.0
public static final int SC_PROXY_AUTHENTICATION_REQUIRED; // Nowość w
2.0
public static final int SC_REQUEST_ENTITY_TOO_LARGE; // Nowość w
2.0
public static final int SC_REQUEST_TIMEOUT; // Nowość w
2.0
public static final int SC_REQUEST_URI_TOO_LONG; // Nowość w
2.0
public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE; // Nowość w
2.2
public static final int SC_RESET_CONTENT; // Nowość w
2.0
public static final int SC_SEE_OTHER; // Nowość w
2.0
public static final int SC_SERVICE_UNAVAILABLE;
public static final int SC_SWITCHING_PROTOCOLS; // Nowość w
2.0
public static final int SC_UNAUTHORIZED;
public static final int SC_UNSUPPORTED_MEDIA_TYPE; // Nowość w
2.0
public static final int SC_USE_PROXY; // Nowość w
2.0

// Metody
public abstract void addCookie(Cookie cookie); // Nowość w
2.0
public abstract void addDateHeader(String nazwa, long data); // Nowość w
2.2
public abstract void addHeader(String nazwa, String wartosc); // Nowość w
2.2
public abstract void addIntHeader(String nazwa, int wartosc); // Nowość w
2.2
public abstract boolean containsHeader(String nazwa);
public abstract String encodeRedirectUrl(String url); // Opuszczona
public abstract String encodeRedirectURL(String url); // Nowość w
2.1
public abstract String encodeUrl(String url); // Opuszczona
public abstract String encodeURL(String url); // Nowość w
2.1
public abstract void sendError(int sc)
throws IOException, IllegalStateException;
public abstract void sendError(int sc, String wiad)
throws IOException, IllegalStateException;
public abstract void sendRedirect(String miejsce)
throws IOException, IllegalStateException;
public abstract void setDateHeader(String nazwa, long data);
public abstract void setHeader(String nazwa, String wartosc);
public abstract void setIntHeader(String nazwa, int wartosc);
public abstract void setStatus(int sc);
public abstract void setStatus(int sc, String wiad); //
Opuszczona
}

Stałe
Dodatek D, „Kody stanu HTTP”, zawiera kompletny opis wszystkich kodów stanu SC_XXX.

Metody
addCookie()
public abstract void addCookie(Cookie cookie)
Dodaje do odpowiedzi określone cookie. Dodatkowe cookies mogą być dodawane przy pomocy powtarzanych
wywołań addCookie(). Ponieważ cookies są wysyłane przy pomocy nagłówków HTTP, powinny one zostać
dodane do odpowiedzi przed jej zatwierdzeniem. Od przeglądarek wymaga się przyjmowanie jednocześnie
jedynie 20 cookies na witrynę, 300 na użytkownika, a wielkość każdego cookie może zostać ograniczona do
4096 bajtów.

addDateHeader()
public abstract void addDateHeader(String nazwa, long data)
Dodaje nagłówek o danej nazwie i wartości daty. Metoda przyjmuje datę jako liczbę long reprezentującą ilość
milisekund od północy 1 stycznia 1970 GMT. Metoda pozwala nagłówkom odpowiedzi na posiadanie więcej niż
jednej wartości. Metoda została wprowadzona w Servlet API 2.2.

addHeader()
public abstract void addHeader(String nazwa, String wartosc)
Dodaje nagłówek o danej nazwie i wartości. Metoda pozwala nagłówkom odpowiedzi na posiadanie więcej niż
jednej wartości. Metoda została wprowadzona w Servlet API 2.2.

addIntHeader()
public abstract void addIntHeader(String nazwa, int wartosc)
Dodaje nagłówek o danej nazwie i wartości int. Metoda przyjmuje datę w formie long, reprezentującą ilość
milisekund od północy 1 stycznia 1970 GMT. Metoda pozwala nagłówkom odpowiedzi na posiadanie więcej niż
jednej wartości. Metoda została wprowadzona w Servlet API 2.2.

containsHeader();
public abstract boolean containsHeader(String nazwa)
Zwraca true, jeżeli dany nagłówek został wcześniej ustawiony, w przeciwnym wypadku false.

encodeRedirectURL()
public abstract String encodeRedirectUrl(String url)
public abstract String encodeRedirectURL(String url)
Zwraca określony URL zakodowany (ponownie napisany) tak, aby zawierał identyfikator sesji. Jeżeli kodowanie
nie jest potrzebne lub obsługiwane, metoda pozostawia URL niezmieniony. Zasady wykorzystywane do
podejmowania decyzji czy i kiedy kodować URL są zależne od serwera. Metoda ta może wykorzystywać inne
zasady niż encodeURL(). W celu umożliwienia śledzenia sesji, wszystkie URL-e przekazane metodzie
sendRedirect() powinny przejść prze tę metodę. Metoda encodeRedirectUrl() została
wprowadzona w Servlet API 2.0, po czym opuszczona w Servlet API 2.1 z wprowadzeniem bardziej
standardowo nazwanej metody encodeRedirectURL().
encodeURL()
public abstract String encodeUrl(String url)
public abstract String encodeURL(String url)
Zwraca określony URL zakodowany (ponownie napisany) tak, aby zawierał identyfikator sesji. Jeżeli kodowanie
nie jest potrzebne lub obsługiwane, metoda pozostawia URL niezmieniony. Zasady wykorzystywane do
podejmowania decyzji czy i kiedy kodować URL są zależne od serwera. W celu umożliwienia śledzenia sesji,
wszystkie URL-e wysyłane przez serwlet powinny przejść prze tę metodę. Metoda encodeUrl() została
wprowadzona w Servlet API 2.0, po czym opuszczona w Servlet API 2.1 z wprowadzeniem bardziej
standardowo nazwanej metody encodeURL().

sendError()
public abstract void sendError(int sc)
throws IOException, IllegalStateException
public abstract void sendError(int sc, String wiad)
throws IOException, IllegalStateException
Metody te są podobne do setStatus() poza tym, że są wykorzystywane, kiedy kod stanu wskazuje błąd
podczas obsługi żądania i serwlet chciałby, aby serwer wygenerował odpowiednią stronę błędu. Metoda ta
powinna zostać wywołana przed zatwierdzeniem odpowiedzi, ponieważ w innym przypadku zgłosi wyjątek
IllegalStateException. Metoda ta wykonuje ukryte wyczyszczenie bufora odpowiedzi przed
wygenerowaniem strony błędu. Nagłówki ustawione przed sendError() powinny pozostać niezmienione.

sendRedirect()
public abstract void sendRedirect(String miejsce)
throws IOException, IllegalStateException
Przekierowuje odpowiedź do określonej lokalizacji, automatycznie ustawiając kod stanu i nagłówek Location.
Domyślna implementacja zapisuje również krótka główną część odpowiedzi, która zawiera hiperłącze do nowej
lokacji w celu wspomożenia przeglądarek nie posiadających zdolności przekierowywania. W związku z tym nie
należy tworzyć własnej głównej części odpowiedzi podczas wykorzystywania tej metody. Specyfikacja HTTP
nakazuje, aby wszystkie URL-e przekierowań były bezwzględne; jednak począwszy od Servlet API 2.2 metoda ta
przyjmuje również URL-e względna — serwer przekształca je do formy bezwzględnej (automatycznie dodając
aktualny protokół, serwer i port — ale nie ścieżkę kontekstu; należy wykonać to działanie samodzielne, jeżeli
okaże się konieczne) przed wysłaniem go do klienta. Metoda ta powinna zostać wywołana przed zatwierdzeniem
odpowiedzi, ponieważ w innym przypadku zgłosi wyjątek IllegalStateException. Metoda ta wykonuje
ukryte wyczyszczenie bufora odpowiedzi przed wygenerowaniem strony błędu. Nagłówki ustawione przed
sendRedirect() powinny pozostać niezmienione.

setDateHeader()
public abstract void setDateHeader(String nazwa, long data)
Ustawia wartość danego nagłówka jako String określający konkretną datę i czas. Metoda przyjmuje datę w
formie long, reprezentującą ilość milisekund od północy 1 stycznia 1970 GMT. Jeżeli nagłówek został już
wcześniej ustawiony, nowa wartość nadpisuje wszystkie poprzednie wartości.

setHeader()
public abstract void setHeader(String nazwa, String wartosc)
Ustawia wartość danego nagłówka jako String. Nazwa nie zwraca uwagi na wielkość liter (tak jak we
wszystkich metodach związanych z nagłówkami). Jeżeli nagłówek został już wcześniej ustawiony, nowa wartość
nadpisuje wszystkie poprzednie wartości. Nagłówki zawsze powinny zostać ustawione przed zatwierdzeniem
odpowiedzi.

setIntHeader()
public abstract void setIntHeader(String nazwa, int wartosc)
Ustawia wartość danego nagłówka jako int. Jeżeli nagłówek został już wcześniej ustawiony, nowa wartość
nadpisuje wszystkie poprzednie wartości.

setStatus()
public abstract void setStatus(int sc);
public abstract void setStatus(int sc, String wiad)
Ustawia kod stanu HTTP. Kod może zostać określony przy pomocy wartości liczbowej lub kodów SC_XXX
zdefiniowanych w HttpServletResponse. Jako drugi parametr może zostać określona własna wiadomość o
błędzie protokołu HTTP; jednak nie powinno się wykonywać tego działania ponieważ wersja metody
przyjmująca String została opuszczona w ServletAPI 2.1. Kod stanu powinien zostać ustawiony przed
zatwierdzeniem odpowiedzi, w innym przypadku wywołanie jest ignorowane.
HttpSession

Zestawienie
Nazwa interfejsu javax.servlet.http.HttpSession
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 2.0 i późniejsze

Opis
Interfejs HttpSession dostarcza mechanizmu służącego do przechowywania tymczasowych informacji na
temat odwiedzających witrynę WWW. Dokładne omówienie śledzenia sesji znajduje się w rozdziale 7. Sam
interfejs HttpSession umożliwia serwletom przeglądanie i manipulację informacjami specyficznymi dla
danej sesji, takimi jak czas utworzenia i unikatowy identyfikator sesji. Zawiera również metody służące do
dowiązywania obiektów do sesji w celu późniejszego ich odczytania, umożliwiając „koszykom na zakupy” i
innym aplikacjom przechowywanie danych pomiędzy żądaniami.
Serwlet pobiera obiekt HttpSession przy pomocy metody getSession() HttpServletRequest.
Zachowanie sesji, takie jak ilość czasu, która musi upłynąć przed zniszczeniem sesji, może zostać ustawione
programowo lub przed deskryptor aplikacji WWW.
W przypadku sesji związanych z nierozporoszoną aplikacją WWW, każdy obiekt może zostać dowiązany do
sesji. Obiekty implementujące java.io.Serializable mogą zostać zapisane na dysku w celu uwolnienia
pamięci i przetrwania pomiędzy ponownymi uruchomieniami serwera.
W przypadku sesji związanych z rozproszoną aplikacją WWW, obiekt umieszczony w sesji musi implementować
java.io.Serializable. Jeżeli warunek ten nie zostanie spełniony, serwer może zgłosić
IllegalArgumentException. Serwery wykorzystują przyklejanie sesji w celu efektywnego zarządzania
sesjami w środowisku rozproszonym, w którym często występuje kilka serwerów wspierających. Oznacza to, że
wszystkie żądania będące częścią pojedynczej sesji konkretnego użytkownika są obsługiwane przez tylko jedną
JVM w jednym czasie. Eliminuje to konieczność ciągłego replikowania informacji o sesji pomiędzy serwerami
wspierającymi. Odpowiedzialność za sesję może zostać przekazana innemu serwerowi pomiędzy żądaniami
użytkownika, a w celu umożliwienia przenoszenia sesji wszystkie obiekty w niej umieszczone muszą być
Serializable.

Deklaracja interfejsu
public interface HttpSession {
// Metody
// Większość metod może zgłaszać IllegalStateException
public abstract Object getAttribute(String nazwa); // Nowość w
2.2
public abstract Enumeration getAttributeNames(); // Nowość w
2.2
public abstract long getCreationTime();
public abstract String getID();
public abstract long getLastAccessedTime();
public abstract int getMaxInactiveInterval(); // Nowość w
2.1
public abstract HttpSessionContext getSessionContext(); // Opuszczona
public abstract Object getValue(String nazwa); // Opuszczona
public abstract String[] getValueNames(); // Opuszczona
public abstract void invalidate();
public abstract boolean isNew();
public abstract void putValue(String nazwa, Object wartosc); // Opuszczona
public abstract void removeAttribute(String nazwa); // Nowość w
2.2
public abstract void removeValue(String nazwa); // Opuszczona
public abstract void setAttribute(String nazwa, Object value); // Nowość w
2.2
public abstract void setMaxInactiveInterval(int sekundy); // Nowość w
2.1
}
Metody
getAttribute()
public abstract Object getAttribute(String nazwa)
throws IllegalStateException
Zwraca obiekt dowiązany do sesji pod określoną nazwą lub null, jeżeli nie istnieje pasujące dowiązanie. Zgłasza
IllegalStateException, jeżeli sesja jest nieważna.

getAttributeNames()
public abstract Enumeration getAttributeNames()
throws IllegalStateException
Zwraca Enumeration zawierającą nazwy wszystkich obiektów dowiązanych do aktualnej sesji jako obiekty
String lub pustą Enumeration, jeżeli nie występują żadne dowiązania. Zgłasza
IllegalStateException, jeżeli sesja jest nieważna.

getCreationTime()
public abstract long getCreationTime()
throws IllegalStateException
Zwraca czas utworzenia sesji, jako liczbę long reprezentującą ilość milisekund od północy 1 stycznia 1970
GMT. Zgłasza IllegalStateException, jeżeli sesja jest nieważna.

getId()
public abstract String getID()
throws IllegalStateException
Zwraca niepowtarzalny identyfikator String przypisany do danej sesji. Struktura identyfikatora jest zależna od
implementacji; powinna być po prostu trudna do odgadnięcia dla kogoś innego. Na przykład identyfikator
Tomcata mógłby wyglądać podobnie do awj4qyhsn2. Zgłasza IllegalStateException, jeżeli sesja jest
nieważna.

getLastAccessedTime()
public abstract long getLastAccessedTime()
throws IllegalStateException
Zwraca czas, w którym klient po raz ostatni wysłał żądanie związane z daną sesją (nie włączając w to aktualnego
żądania), jako liczbę long reprezentującą ilość milisekund od północy 1 stycznia 1970 GMT. Zgłasza
IllegalStateException, jeżeli sesja jest nieważna.

getMaxInactiveInterval
public abstract int getMaxInactiveInterval()
throws IllegalStateException
Zwraca czas (w sekundach) jaki musi upłynąć pomiędzy żądaniami, zanim kontener serwletów unieważni sesję.
Ujemny czas oznacza, że sesja nigdy nie ulegnie przedawnieniu. Domyślny czas przedawnienia jest ustawiony
wewnątrz deskryptora aplikacji WWW. Zgłasza IllegalStateException, jeżeli sesja jest nieważna.
Metoda została wprowadzona w Servlet API 2.1.

getSessionContext()
public abstract HttpSessionContext getSessionContext()
Zwraca pusty kontekst ze względów bezpieczeństwa, począwszy od Servlet API 2.1. Opuszczona również od
Servlet API 2.1. Proszę zobaczyć HttpSessionContext w celu uzyskania większej ilości informacji.

getValue()
public abstract Object getValue(String nazwa)
throws IllegalStateException
Zwraca obiekt dowiązany do sesji pod określoną nazwą lub null, jeżeli nie występuje pasujące dowiązanie.
Zgłasza IllegalStateException, jeżeli sesja jest nieważna. Opuszczona w Servlet API 2.2 na rzecz
getAttribute().

getValueNames()
public abstract String[] getValueNames()
throws IllegalStateException
Zwraca tablicę zawierającą nazwy wszystkich obiektów dowiązanych do aktualnej sesji lub pustą (zerowej
długości) tablicę, jeżeli nie występują żadne dowiązania. Zgłasza IllegalStateException, jeżeli sesja jest
nieważna. Opuszczona w Servlet API 2.2 na rzecz getAttributeNames().

invalidate()
public abstract void invalidate()
throws IllegalStateException
Powoduje natychmiastowe unieważnienie sesji. Wszystkie dowiązania obiektów przechowywane w sesji zostają
usunięte. Zgłasza IllegalStateException, jeżeli sesja została już wcześniej unieważniona.

isNew()
public abstract boolean isNew()
throws IllegalStateException
Zwraca odpowiedź, czy sesja jest nowa. Sesja jest uważana za nową, jeżeli została już utworzona przez serwer,
ale klient nie potwierdził jeszcze dołączenia do niej. Na przykład, jeżeli serwer obsługuje jedynie sesje oparte na
cookies, a klient całkowicie zablokował ich użycie, wywołania getSession() zawsze zwracają nowe sesje.
Zgłasza IllegalStateException, jeżeli sesja została już wcześniej unieważniona.

putValue()
public abstract void putValue(String nazwa, Object wartosc)
throws IllegalStateException
Dowiązuje do sesji dany obiekt pod określoną nazwą. Wszystkie istniejące dowiązania o tej samej nazwie zostają
usunięte. Zgłasza IllegalStateException, jeżeli sesja jest nieważna. Opuszczona w Servlet API 2.2 na
rzecz setAttribute().

removeAttribute()
public abstract void removeAttribute(String nazwa)
throws IllegalStateException
Usuwa obiekt dowiązany pod określoną nazwą lub nie wykonuje żadnego działania, jeżeli dowiązanie nie
istnieje. Zgłasza IllegalStateException, jeżeli sesja jest nieważna.

removeValue()
public abstract void removeValue(String nazwa)
throws IllegalStateException
Usuwa obiekt dowiązany pod określoną nazwą lub nie wykonuje żadnego działania, jeżeli dowiązanie nie
istnieje. Zgłasza IllegalStateException, jeżeli sesja jest nieważna. Opuszczona w Servlet API 2.2 na
rzecz removeAttribute().

setAttribute()
public abstract void setAttribute(String nazwa, Object value)
throws IllegalStateException
Dowiązuje do sesji dany obiekt pod określoną nazwą. Wszystkie istniejące dowiązania o tej samej nazwie zostają
usunięte. Zgłasza IllegalStateException, jeżeli sesja jest nieważna. Metoda została wprowadzona w
Servlet API 2.2.

setMaxInactiveInterval()
public abstract void setMaxInactiveInterval(int sekundy)
Określa czas (w sekundach) jaki musi upłynąć pomiędzy żądaniami, zanim kontener serwletów unieważni sesję.
Ujemny czas oznacza, że sesja nie powinna nigdy ulec przedawnieniu. Domyślny czas przedawnienia może
zostać ustawiony wewnątrz deskryptora aplikacji WWW. Zgłasza IllegalStateException, jeżeli sesja
jest nieważna. Metoda została wprowadzona w Servlet API 2.1.

HttpSessionBindingEvent

Zestawienie
Nazwa klasy javax.servlet.http.HttpSessionBindingEvent
Superklasa java.util.EventObject
Bezpośrednie podklasy Brak
Implementowane interfejsy Brak
Dostępność Servlet API 2.0 i późniejsze

Opis
HttpSessionBindingEvent jest przekazywany HttpSessionBindingListener, kiedy obiekt
nasłuchujący jest dowiązywany do sesji, lub kiedy jego dowiązanie zostaje usunięte.

Podsumowanie klas
public class HttpSessionBindingEvent extends java.util.EventObject {
// Konstruktory
public HttpSessionBindingEvent(HttpSession sesja, String nazwa);

// Metody egzemplarzy
public String getName();
public HttpSession getSession();
}

Konstruktory
HttpSessionBindingEvent()
public HttpSessionBindingEvent(HttpSession sesja, String nazwa)
Konstruuje nowy HttpSessionBindingEvent wykorzystując sesję, do której następuje dowiązanie i
nazwę, pod którą dany obiekt zostaje przypisany (jest to ta sama nazwa, która zostaje przekazana metodzie
setAttribute() HttpSession). Programiści serwletów nie powinni nigdy zostać zmuszeni do
wykorzystania tego konstruktora.

Metody egzemplarzy
getName()
public String getName()
Zwraca nazwę, pod którą dany obiekt został przydzielony do sesji.

GetSession()
public HttpSession getSession()
Zwraca sesję, do której dany obiekt jest dowiązywany, lub którego dowiązanie jest usuwane.

HttpSessionBindingListener

Zestawienie
Nazwa interfejsu javax.servlet.http.HttpSessionBindingListener
Superinterfejs java.util.EventListener
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 2.0 i późniejsze

Opis
Obiekt implementujący HttpSessionBindingListener jest informowany poprzez wywołania
valueBound() i valueUnbound(), kiedy zostaje dowiązany (lub jego dowiązanie zostaje usunięte) do
HttpSession. Interfejs ten umożliwia między innymi uporządkowane wyczyszczenie zasobów związanych z
sesją, takich jak połączenia z bazą danych. Proszę zauważyć, że w środowisku rozproszonym wywołanie
valueBound() może nastąpić w innej JVM niż wywołanie valueUnbound().

Deklaracja interfejsu
public interface HttpSessionBindingListener extends java.util.EventListener {
// Metody
public abstract void valueBound(HttpSessionBindingEvent zdarzenie);
public abstract void valueUnbound(HttpSessionBindingEvent zdarzenie);
}
Metody
valueBound()
public abstract void valueBound(HttpSessionBindingEvent zdarzenie)
Wywoływana podczas dowiązywania mechanizmu nasłuchującego do sesji.

valueUnbound()
public abstract void valueUnbound(HttpSessionBindingEvent zdarzenie)
Wywoływana podczas usuwania dowiązania mechanizmu nasłuchującego do sesji (włączając w to zniszczenie
sesji).

HttpSessionContext

Zestawienie
Nazwa interfejsu javax.servlet.http.HttpSessionContext
Superinterfejs Brak
Bezpośrednie podinterfejsy Brak
Implementowany przez Brak
Dostępność Servlet API 2.0 i późniejsze; opuszczony w Servlet API 2.1

Opis
HttpSessionContext jest opuszczona od czasów Servlet API 2.1. Poprzednio klasa ta umożliwiała dostęp
do wszystkich aktywnych w danym momencie sesji w kontenerze serwletów. Tworzyło to potencjalną dziurę w
systemie zabezpieczeń, ponieważ serwlet mógł wykorzystać te klasę w celu wyświetlenia wszystkich
identyfikatorów sesji odnalezionych wewnątrz kontekstu, a informacja ta mogła zostać wykorzystana przez
nieuczciwych klientów do utorowania sobie drogi do innej sesji. Ponieważ możliwość jednoczesnego dostępu do
wszystkich sesji jest nie jest prawie nigdy potrzebna, klasa została opuszczona ze względów bezpieczeństwa.

Deklaracja interfejsu
Public interface HttpSessionContext {
// Metody
public abstract Enumeration getIds(); // Opuszczona
public abstract HttpSession(String idSesji); // Opuszczona
}

Metody
getIds()
public abstract Enumeration getIds()
Opuszczona w Servlet API 2.1. W Servlet API 2.0 zwracała Enumeration zawierającą identyfikatory
wszystkich ważnych w danym momencie sesji, lub pustą Enumeration, jeżeli takie nie występowały.
Identyfikatory sesji zwracane przez getIds() muszą być traktowane jak tajemnica serwera, ponieważ każdy
klient znający identyfikator sesji innego klienta mógłby, przy pomocy utworzonego samodzielnie cookie lub
URL-a dołączyć do sesji drugiego klienta.

getSession()
public abstract HttpSession(String idSesji)
Opuszczona w Servlet API 2.1. W Servlet API 2.0 zwracała sesję związaną z danym identyfikatorem sesji. Lista
ważnych identyfikatorów sesji może zostać odczytana przez metodę getIds().

HttpUtils

Zestawienie
Nazwa klasy javax.servlet.http.HttpUtils
Superklasa java.lang.Object
Bezpośrednie podklasy Brak
Implementowane interfejsy Brak
Dostępność Servlet API 1.0 i późniejsze

Opis
Obiekt służący jako kontener dla kilku potencjalnie przydatnych, zorientowanych na HTTP metod.

Podsumowanie klasy
public class HttpUtils
// Konstruktory
public HttpUtils();

// Metody klasy
public static StringBuffer getRequestURL(HttpServletRequest zad);
public static Hashtable parsePostData(int dlug, ServletInputStream in);
public static Hashtable parseQueryString(String s);
}

Konstruktory
HttpUtils()
public HttpUtils()
Domyślny konstruktor nie wykonuje żadnych działań.

Metody klasy
getRequestURL()
public static StringBuffer getRequestURL(HttpServletRequest zad)
Odtwarza URL żądania w oparciu o informacje dostępne w obiekcie HttpServletRequest. Zwraca
StringBuffer, który zawiera schemat, nazwę serwera, port serwera oraz dodatkowe informacje ścieżki, ale
nie łańcuch zapytania. Odtworzony URL powinien wyglądać niemal identycznie jak URL wykorzystany przez
klienta. Metoda ta może zostać wykorzystana do zgłaszania błędów, przekierowywania oraz tworzenia URL-i. W
przypadku aplikacji, które muszą bezbłędnie zidentyfikować konkretne serwlety. Lepszym wyborem jest
generalnie metoda getRequestURI() HttpServletRequest.

parsePostData()
public static Hashtable parsePostData(int dlug, ServletInputStream in)
Analizuje dlug danych parametrów ServletInputStream (zazwyczaj wysyłanych jako część operacji
POST). Zgłasza IllegalArgumentException, jeżeli dane parametrów okażą się nieprawidłowe.
Większość serwletów wykorzystuje getParameterNames(), getParameter() i
getParameterValues() zamiast tej metody.

parseQueryString()
public static Hashtable parseQueryString(String s)
Zwraca tablicę asocjacyjną Hashtable, w której klucze są nazwami parametrów pobranymi z łańcucha
zapytania, a każda wartość tablicy jest tablicą String zawierającą zdekodowaną wartość/wartości parametru.
Zgłasza IllegalArgumentException, jeżeli łańcuch zapytania okaże się nieprawidłowy. Większość
serwletów wykorzystuje getParameterNames(), getParameter() i getParameterValues()
zamiast tej metody. Użycie obu tych metod nie jest bezpieczne.
Dodatek C. Krótki opis

deskryptorów DTD

Plik Document Type Definition (Definicja Typu Dokumentu — DTD) określa prawidłową zawartość aplikacji w
poprzez plik XML. Niniejszy dodatek przedstawia oficjalny DTD Servlet API 2.2 dla deskryptora aplikacji
WWW. Pliki DTD nie są plikami XML, ale ich składnia nie jest skomplikowana — każdy znacznik
<!ELEMENT> definiuje w nawiasach, jakie elementy potomne może posiadać, jaką ich ilość oraz w jakim
porządku. Znak zapytania (?) po nazwie potomka oznacza, że potomek jest opcjonalny (0 – 1), gwiazdka (*)
oznacza, że może pojawić się dowolna ilość potomków (0 – n), znak plus (+) oznacza, że pewna ilość potomków
musi się pojawić (1 – n), a brak znaku po nazwie oznacza, że potomek jest po prostu konieczny (1). Składnia
(x|y) oznacza x lub y. Elementy potomne musza pojawiać się dokładnie w porządku określonym wewnątrz
nawiasów.
Rysunki od C.1 do C.4 przedstawiają graficznie strukturę elementów Przykład C.1 przedstawia sam DTD. Każdy
znacznik <!ATTLIST> kontroluje dozwolone atrybuty elementu. W niniejszym DTD znacznik ten jest
wykorzystany jedynie w celu dostarczenia domyślnych wartości id. Pozostała część niniejszego dodatku to opis
elementów DTD.

Rysunek C.1.
Struktura elementów deskryptora DTD
Rysunek C.2.

Rysunek C.3.
Rysunek C.4.

Przykład C.1.
Deskryptor DTD
<!ELEMENT web-app (icon?, display-name?, description?, distributable?,
context-param*, servlet*, servlet-mapping*, session-config?,
mime-mapping*, welcome-file-list?, error-page*, taglib*,
resource-ref*, security-constraint*, login-config?,
security-role*, env-entry*, ejb-ref*)>

<!ELEMENT icon (small-icon?, large-icon?)>


<!ELEMENT small-icon (#DANE)>
<!ELEMENT large-icon (#DANE)>
<!ELEMENT display-name (#DANE)>
<!ELEMENT description (#DANE)>

<!ELEMENT distributable EMPTY>

<!ELEMENT context-param (param-name, param-value, description?>


<!ELEMENT param-name (#DANE)>
<!ELEMENT param-value (#DANE)>

<!ELEMENT servlet (icon?, servlet-name, display-name?, description?,


(servlet-class|jsp-file), init-param*, load-on-startup?,
security-role-ref*)>
<!ELEMENT servlet-name (#DANE)>
<!ELEMENT servlet-class (#DANE)>
<!ELEMENT jsp-file (#DANE)>
<!ELEMENT init-param (param-name, param-value, description?>
<!ELEMENT load-on-startup (#DANE)>

<!ELEMENT servlet-mapping (servlet-name, url-pattern)>


<!ELEMENT url-pattern (#DANE)>

<!ELEMENT session-config (session-timeout?)>


<!ELEMENT session-timeout (#DANE)>

<!ELEMENT mime-mapping (extension, mime-type)>


<!ELEMENT extension (#DANE)>
<!ELEMENT mime-type (#DANE)>

<!ELEMENT welcome-file-list (welcome-file+)>


<!ELEMENT welcome-file (#DANE)>

<!ELEMENT taglib (taglib-uri, taglib-location)>


<!ELEMENT taglib-uri (#DANE)>
<!ELEMENT taglib-location (#DANE)>

<!ELEMENT error-page ((error-code | exception-type), location)>


<!ELEMENT error-code (#DANE)>
<!ELEMENT exception-type (#DANE)>
<!ELEMENT location (#DANE)>

<!ELEMENT resource-ref (description?, res-ref-name, res-type, res-auth)>


<!ELEMENT res-ref-name (#DANE)>
<!ELEMENT res-type (#DANE)>
<!ELEMENT res-auth (#DANE)>

<!ELEMENT security-constraint (web-resource-collection+,


auth-constraint?, user-data-constraint?)>
<!ELEMENT web-resource-collection (web-resource-name, description?,
url-pattern*, http-method*)>
<!ELEMENT web-resource-name (#DANE)>
<!ELEMENT http-method (#DANE)>
<!ELEMENT user-data-constraint (description?, transport-guarantee)>
<!ELEMENT transport-guarantee (#DANE)>
<!ELEMENT auth-constraint (description?, role-name*)>
<!ELEMENT role-name (#DANE)>

<!ELEMENT login-config (auth-method?, realm-name?, form-login-config?)>


<!ELEMENT realm-name (#DANE)>
<!ELEMENT form-login-config (form-login-page, form-error-page)>
<!ELEMENT form-login-page (#DANE)>
<!ELEMENT form-error-page (#DANE)>
<!ELEMENT auth-method (#DANE)>

<!ELEMENT security-role (description?, role-name)>


<!ELEMENT security-role-ref (description?, env-entry-name, env-entry-value?
env-entry-type)>
<!ELEMENT env-entry-name (#DANE)>
<!ELEMENT env-entry-value (#DANE)>
<!ELEMENT env-entry-type (#DANE)>

<!ELEMENT ejb-ref (description?, ejb-ref-name, ejb-ref-type, home, remote,


ejb-link?)>
<!ELEMENT ejb-ref-name (#DANE)>
<!ELEMENT ejb-ref-type (#DANE)>
<!ELEMENT home (#DANE)>
<!ELEMENT remote (#DANE)>
<!ELEMENT ejb-link (#DANE)>

<!—Mechanizm identyfikacji umożliwia narzędziom tworzenie w łatwy sposób


specyficznych dla narzędzia odwołań do elementów deskryptora. Pozwala to
narzędziom tworzącym dodatkowe informacje (to znaczy informacje poza standardowymi
informacjami deskryptora) przechowywanie niestandardowych informacji w osobnym
pliku i łatwe odwoływanie się z tych specyficznych dla narzędzia plików do
informacji zawartych w standardowym deskryptorze aplikacji WWW. -->

<!ATTLIST web-app id ID #SUGERUJ>


<!ATTLIST icon id ID #SUGERUJ>
<!ATTLIST small-icon id ID #SUGERUJ>
<!ATTLIST large-icon id ID #SUGERUJ>
<!ATTLIST display-name id ID #SUGERUJ>
<!ATTLIST description id ID #SUGERUJ>
<!ATTLIST distributable id ID #SUGERUJ>
<!ATTLIST context-param id ID #SUGERUJ>
<!ATTLIST param-name id ID #SUGERUJ>
<!ATTLIST param-value id ID #SUGERUJ>
<!ATTLIST servlet id ID #SUGERUJ>
<!ATTLIST servlet-name id ID #SUGERUJ>
<!ATTLIST servlet-class id ID #SUGERUJ>
<!ATTLIST jsp-file id ID #SUGERUJ>
<!ATTLIST init-param id ID #SUGERUJ>
<!ATTLIST load-on-startup id ID #SUGERUJ>
<!ATTLIST servlet-mapping id ID #SUGERUJ>
<!ATTLIST url-pattern id ID #SUGERUJ>
<!ATTLIST session-config id ID #SUGERUJ>
<!ATTLIST session-timeout id ID #SUGERUJ>
<!ATTLIST mime-mapping id ID #SUGERUJ>
<!ATTLIST extension id ID #SUGERUJ>
<!ATTLIST mime-type id ID #SUGERUJ>
<!ATTLIST welcome-file-list id ID #SUGERUJ>
<!ATTLIST welcome-file id ID #SUGERUJ>
<!ATTLIST error-page id ID #SUGERUJ>
<!ATTLIST error-code id ID #SUGERUJ>
<!ATTLIST exception-type id ID #SUGERUJ>
<!ATTLIST location id ID #SUGERUJ>
<!ATTLIST resource-ref id ID #SUGERUJ>
<!ATTLIST res-ref-name id ID #SUGERUJ>
<!ATTLIST res-type id ID #SUGERUJ>
<!ATTLIST res-auth id ID #SUGERUJ>
<!ATTLIST security-constraint id ID #SUGERUJ>
<!ATTLIST web-resource-collection id ID #SUGERUJ>
<!ATTLIST web-resource-name id ID #SUGERUJ>
<!ATTLIST http-method-id id ID #SUGERUJ>
<!ATTLIST user-data-constraint id ID #SUGERUJ>
<!ATTLIST transport-guarantee id ID #SUGERUJ>
<!ATTLIST auth-constraint id ID #SUGERUJ>
<!ATTLIST role-name id ID #SUGERUJ>
<!ATTLIST auth-method id ID #SUGERUJ>
<!ATTLIST basic-auth id ID #SUGERUJ>
<!ATTLIST form-auth id ID #SUGERUJ>
<!ATTLIST form-login-page id ID #SUGERUJ>
<!ATTLIST form-error-page id ID #SUGERUJ>
<!ATTLIST form-login-config id ID #SUGERUJ>
<!ATTLIST realm-name id ID #SUGERUJ>
<!ATTLIST login-config id ID #SUGERUJ>
<!ATTLIST security-role id ID #SUGERUJ>
<!ATTLIST security-role-ref id ID #SUGERUJ>
<!ATTLIST role-link id ID #SUGERUJ>
<!ATTLIST env-entry id ID #SUGERUJ>
<!ATTLIST env-entry-name id ID #SUGERUJ>
<!ATTLIST env-entry-value id ID #SUGERUJ>
<!ATTLIST env-entry-type id ID #SUGERUJ>
<!ATTLIST mutual-auth id ID #SUGERUJ>
<!ATTLIST ejb-ref id ID #SUGERUJ>
<!ATTLIST ejb-ref-name id ID #SUGERUJ>
<!ATTLIST ejb-ref-type id ID #SUGERUJ>
<!ATTLIST home id ID #SUGERUJ>
<!ATTLIST remote id ID #SUGERUJ>
<!ATTLIST ejb-ref id ID #SUGERUJ>

<auth-constraint>

Zestawienie
<!ELEMENT auth-constraint (description?, role-name*)>

Opis
Element <auth-constraint> wskazuje na role użytkowników, które mogą zostać dopuszczone do zbioru
zasobów. Nazwy ról wykorzystane w tym miejscu muszą pojawić się w elemencie <security-role-ref>.
<security-constraint>
<web-resource-collection>
...
</web-resource-collection>
<auth-constraint>
<role-name>menedzer</role-name>
</auth-constraint>
</security-constraint>
<auth-method>

Zestawienie
<!ELEMENT auth-method (#DANE)>

Opis
Element <auth-method> jest wykorzystywany do konfigurowania mechanizmu uwierzytelniającego aplikacji
WWW. Podstawową zasadą dostępu do dowolnych zasobów WWW chronionych przez ograniczenia dostępu
jest uwierzytelnienie użytkownika przy pomocy skonfigurowanego mechanizmu. Prawidłowe wartości tego
elementu to BASIC, DIGEST, FORM i CLIENT-CERT.
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Domyslny</realm-name>
</login-config>

<context-param>

Zestawienie
<!ELEMENT context-param (param-name, param-value, description?>

Opis
Element <context-param> zawiera deklarację parametrów inicjacji kontekstu serwletów aplikacji WWW.
<context-param>
<param-name>rmihost</param-name>
<param-value>localhost</param-value>
</context-param>

<description>

Zestawienie
<!ELEMENT description (#DANE)>

Opis
Element <description> jest wykorzystywany w celu dołączenia tekstu opisującego element przodka.
<description>Nie zapomnij się przywitać!</description>

<display-name>

Zestawienie
<!ELEMENT display-name (#DANE)>

Opis
Element <display-name> zawiera krótką nazwę serwletu, który powinien zostać wyświetlony przez
narzędzia graficzne.
<servlet>
<servlet-name>witaj</servlet-name>
<display-name>WitajSwiecie</display-name>
<servlet-class>WitajSwiecie</servlet-class>
</servlet>

<distributable>

Zestawienie
<!ELEMENT distributable (#DANE)>
Opis
Element <distributable> poprzez swoją obecność w deskryptorze aplikacji WWW wskazuje, że aplikacja
WWW jest zaprogramowana odpowiednio dla rozproszonego kontenera serwletów.
<distributable>
<description>
Wszystkie serwlety i strony JSP są przygotowane do działania rozproszonego
</description>
</distributable>

<ejb-link>

Zestawienie
<!ELEMENT ejb-link (#DANE)>

Opis
Element <ejb-link> jest wykorzystywany w elemencie <ejb-ref> w celu zaznaczenia, że odwołanie
połączone jest z komponentem EJB w pakiecie aplikacji Java 2, Enterprise Edition (J2EE). Wartość elementu
<ejb-link> musi być równa <ejb-name> komponentu w pakiecie aplikacji J2EE.
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
<ejb-link>KabinaElement</ejb-link>
</ejb-ref>

<ejb-ref>

Zestawienie
<!ELEMENT ejb-ref (description?, ejb-ref-name, ejb-ref-type, home, remote,
ejb-link?)>

Opis
Element <ejb-ref> jest wykorzystywany do deklarowania odwołania do komponentu korporacyjnego EJB.
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>

<ejb-ref-name>

Zestawienie
<!ELEMENT ejb-ref-name (#DANE)>

Opis
Element <ejb-ref-name> zawiera nazwę odwołania do EJB. Jest to nazwa JNDI, którą wykorzystuje kod
serwletu w celu pobrania odwołania do komponentu korporacyjnego.
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>
<ejb-ref-type>

Zestawienie
<!ELEMENT ejb-ref-type (#DANE)>

Opis
Element <ejb-ref-type> zawiera typ komponentu. Jego wartość musi wynosić Entity lub Session.44
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>

<env-entry>

Zestawienie
<!ELEMENT env-entry (description?, env-entry-name, env-entry-value?,
env-entry-type)>

Opis
Element <env-entry-type> zawiera deklarację pozycji środowiskowej aplikacji, dostępnej poprzez
wyszukiwanie JNDI w kontekście java:comp/env.
<env-entry>
<description>Wysyłanie kodu PIN pocztą</description>
<env-entry-name>pocztaPIN</env-entry-name>
<env-entry-value>false</env-entry-value>
<env-entry-type>java.lang.Boolean</env-entry-type> <!--PNK-->
</env-entry>

<env-entry-name>

Zestawienie
<!ELEMENT env-entry-name (#DANE)>

Opis
Element <env-entry-name> określa nazwę pozycji środowiskowej aplikacji służącą do poszukiwań.
<env-entry>
<description>Wysyłanie kodu PIN pocztą</description>
<env-entry-name>pocztaPIN</env-entry-name>
<env-entry-value>false</env-entry-value>
<env-entry-type>java.lang.Boolean</env-entry-type> <!--PNK-->
</env-entry>

<env-entry-type>

Zestawienie
<!ELEMENT env-entry-type (#DANE)>

Opis
Element <env-entry-type> zawiera pełną nazwę typu Javy pozycji środowiskowej, który jest oczekiwany
przez kod aplikacji. Prawidłowe wartości <env-entry-type> są następujące: java.lang.Boolean,
java.lang.String, java.lang.Integer, java.lang.Double, and java.lang.Float.

44
Oryginalny komentarz do tego znacznika błędnie deklaruje, że znacznik „zawiera oczekiwany typ klasy Javy EJB, do
którego występuje odwołanie”.
<env-entry>
<description>Wysyłanie kodu PIN pocztą</description>
<env-entry-name>pocztaPIN</env-entry-name>
<env-entry-value>false</env-entry-value>
<env-entry-type>java.lang.Boolean</env-entry-type> <!--PNK-->
</env-entry>

<env-entry-value>

Zestawienie
<!ELEMENT env-entry-value (#DANE)>

Opis
Element <env-entry-value> zawiera domyślną wartość pozycji środowiskowej aplikacji.
<env-entry>
<description>Wysyłanie kodu PIN pocztą</description>
<env-entry-name>pocztaPIN</env-entry-name>
<env-entry-value>false</env-entry-value>
<env-entry-type>java.lang.Boolean</env-entry-type> <!--PNK-->
</env-entry>

<error-code>

Zestawienie
<!ELEMENT error-code (#DANE)>

Opis
Element <error-code> zawiera kod błędu HTTP.
<error-page>
<error-code>400</error-code>
<location>/400.html</location>
</error-page>

<error-page>

Zestawienie
<!ELEMENT error-page ((error-code | exception-type), localization)>

Opis
Element <error-page> zawiera odwzorowanie pomiędzy kodem błędu lub typem wyjątku I ścieżką do
zasobu w aplikacji WWW.
<error-page>
<error-code>400</error-code>
<location>/400.html</location>
</error-page>

<exception-type>

Zestawienie
<!ELEMENT exception-type (#DANE)>

Opis
Element <exception-type> zawiera pełną nazwę klasy typu wyjątku Javy.
<error-page>
<exception-type>javax.servlet.ServletException</exception-type>
<location>/servler/WyswietlBlad</location>
</error-page>
<extension>

Zestawienie
<!ELEMENT exception-type (#DANE)>

Opis
Element <extension> zawiera łańcuch opisujący rozszerzenie pliku.
<mime-mapping>
<extension>wml</extension>
<mime-type>text/vnd.wap.wml</mime-type>
</mime-mapping>

<form-error-page>

Zestawienie
<!ELEMENT form-error-page (#DANE)>

Opis
Element <form-error-page> definiuje lokalizację w aplikacji WWW strony błędu, która jest wyświetlana,
jeżeli logowanie nie powiedzie się.
<form-login-config>
<form-login-page>/stronalog.html</form-login-page>
<form-error-page>/stronablad.html</form-error-page>
</form-login-config>

<form-login-config>

Zestawienie
<!ELEMENT form-login-config (form-login-page, form-error-page)>

Opis
Element <form-login-config> określa strony logowania i błędu, które powinny zostać wykorzystane
podczas logowania opartego na formularzu. Jeżeli logowanie oparte na formularzu nie zostanie zastosowane,
elementy te będą zignorowane.
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/stronalog.html</form-login-page>
<form-error-page>/stronablad.html</form-error-page>
</form-login-config>
</login-config>

<form-login-page>

Zestawienie
<!ELEMENT form-login-page (#DANE)>

Opis
Element <form-login-page> definiuje lokalizację w aplikacji WWW strony logowania.
<form-login-config>
<form-login-page>/stronalog.html</form-login-page>
<form-error-page>/stronablad.html</form-error-page>
</form-login-config>
<home>

Zestawienie
<!ELEMENT home (#DANE)>

Opis
Element <home> zawiera pełną nazwę głównego interfejsu EJB.
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>

<http-method>

Zestawienie
<!ELEMENT http-method (#DANE)>

Opis
Element <http-method> określa metodę HTTP.
<web-resource-collection>
<web-resource-name>TajnaOchrona</web-resource-name>
<url-pattern>/servlet/SerwerPlac</url-pattern>
<url-pattern>/servlet/sekret</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>

<icon>

Zestawienie
<!ELEMENT icon (small-icon?, large-icon?)>

Opis
Element <icon> może zawierać elementy <small-icon> i <large-icon> określające położenie
wewnątrz struktury plików aplikacji WWW obrazków — małego i dużego — wykorzystywanych do
przedstawiania aplikacji WWW w narzędziu graficznym.
<icon>
<small-icon>/obrazki/maly.gif</small-icon>
<large-icon>/obrazki/duzy.gif</large-icon>
</icon>

<init-param>

Zestawienie
<!ELEMENT init-param (param-name, param-value, description?)>

Opis
Element <init-param> zawiera parę nazwa-wartość określającą parametr inicjacji serwletu.
<init-param>
<param-name>klucz</param-name>
<param-value>-915314447111823249</param-value>
</init-param>
<jsp-file>

Zestawienie
<!ELEMENT jsp-file (#DANE)>

Opis
Element <jsp-file> zawiera ścieżkę do pliku JSP wewnątrz aplikacji WWW.
<servlet>
<servlet-name>zamowienie-katalog</servlet-name>
<servlet-class>Katalog</servlet-class>
<jsp-file>/formzamow.jsp</jsp-file>
</servlet>

<large-icon>

Zestawienie
<!ELEMENT large-icon (#DANE)>

Opis
Element <large-icon> określa położenie wewnątrz aplikacji WWW pliku zawierającego duży (32 x 32
piksele) obrazek ikony. Narzędzia przyjmują formaty przynajmniej GIF i JPEG.
<icon>
<small-icon>/obrazki/maly.gif</small-icon>
<large-icon>/obrazki/duzy.gif</large-icon>
</icon>

<load-on-startup>

Zestawienie
<!ELEMENT load-on-startup (#DANE)>

Opis
Element <load-on-startup> wskazuje, że serwlet powinien być ładowany podczas startu aplikacji WWW.
Opcjonalną zawartością tego elementu musi być dodatnia liczba całkowita określająca porządek ładowania
serwletów. Jeżeli nie jest określona żadna wartość lub nie jest ona dodatnią liczbą całkowitą, kontener może
załadować serwlet w dowolnym momencie sekwencji startowej.
<servlet>
<servlet-name>ps</servlet-name>
<servlet-class>PierwszeSzukanie</servlet-class>
<load-on-startup/>
</servlet>

<location>

Zestawienie
<!ELEMENT location (#DANE)>

Opis
Element <location> zawiera położenie zasobu w aplikacji WWW.
<error-page>
<error-code>400</error-code>
<location>/400.html</location>
</error-page>
<login-config>

Zestawienie
<!ELEMENT login-config (auth-method?, realm-name?, form-login-config?)>

Opis
Element <login-config> jest wykorzystywany do konfigurowania metody uwierzytelniania i nazwy obszaru,
które powinny zostać wykorzystane w aplikacji, podobnie jak atrybuty logowania opartego na formularzu, jeżeli
zostanie ono wykorzystane.
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Domyslny</realm-name>
</login-config>

<mime-mapping>

Zestawienie
<!ELEMENT mime-mapping (extension, mime-type)>

Opis
Element <mime-mapping> definiuje odwzorowanie pomiędzy rozszerzeniem i typem MIME.
<mime-mapping>
<extension>wml</extension>
<mime-type>text/vnd.wap.wml</mime-type>
</mime-mapping>

<mime-type>

Zestawienie
<!ELEMENT mime-type (#DANE)>

Opis
Element <mime-type> zawiera zdefiniowany typ MIME.
<mime-mapping>
<extension>wml</extension>
<mime-type>text/vnd.wap.wml</mime-type>
</mime-mapping>

<param-name>

Zestawienie
<!ELEMENT param-name (#DANE)>

Opis
Element <param-name> zawiera nazwę parametru.
<context-param>
<param-name>rmihost</param-name>
<param-value>localhost</param-value>
</context-param>

<param-value>

Zestawienie
<!ELEMENT param-value (#DANE)>
Opis
Element <param-value> zawiera nazwę parametru.
<init-param>
<param-name>klucz</param-name>
<param-value>-915314447111823249</param-value>
</init-param>

<realm-name>

Zestawienie
<!ELEMENT realm-name (#DANE)>

Opis
Element <realm-name> określa nazwę obszaru, która powinna zostać wykorzystana podczas podstawowego
uwierzytelniania TTP.
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Domyslny</realm-name>
</login-config>

<remote>

Zestawienie
<!ELEMENT remote (#DANE)>

Opis
Element <remote> zawiera pełną nazwę zdalnego interfejsu EJB.
<ejb-ref>
<description>Kabiny na statku wycieczkowym</description>
<ejb-ref-name>ejb/KabinaPocz</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.tytan.kabina.KabinaPocz</home>
<remote>com.tytan.kabina.Kabina</remote>
</ejb-ref>

<res-auth>

Zestawienie
<!ELEMENT res-auth (#DANE)>

Opis
Element <auth-res> wskazuje, czy komponent kodu aplikacji przeprowadza przypisywanie zasobów
programowo, czy też kontener przypisuje zasoby w oparciu o ogólną zasadę odwzorowywania określoną przez
programistę. Musi mieć wartość CONTAINER lub SERVLET.
<resource-ref>
<description>Podstawowa baza danych</description>
<res-ref-name>jdbc/podstBD</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>CONTAINER</res-auth>
</resource-ref>

<res-ref-name>

Zestawienie
<!ELEMENT res-ref-name (#DANE)>
Opis
Element <res-ref-name> określa nazwę fabryki zasobów, do której wykonywane jest odwołanie.
<resource-ref>
<description>Podstawowa baza danych</description>
<res-ref-name>jdbc/podstBD</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>CONTAINER</res-auth>
</resource-ref>

<res-type>

Zestawienie
<!ELEMENT res-type (#DANE)>

Opis
Element <res-type> określa typ (klasę Javy) źródła danych/
<resource-ref>
<description>Podstawowa baza danych</description>
<res-ref-name>jdbc/podstBD</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>CONTAINER</res-auth>
</resource-ref>

<resource-ref>

Zestawienie
<!ELEMENT resource-ref (description?, res-ref-name, res-type, res-auth)>

Opis
Element <resource-ref> zawiera deklarację odwołania aplikacji WWW do zasobu zewnętrznego, takiego
jak komponent Enterprise JavaBeans lub obiektu DataSource JDBC.
<resource-ref>
<description>Podstawowa baza danych</description>
<res-ref-name>jdbc/podstBD</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>CONTAINER</res-auth>
</resource-ref>

<role-link>

Zestawienie
<!ELEMENT role-link (#DANE)>

Opis
Element <role-link> jest wykorzystywany do łączenia odwołania do roli bezpieczeństwa ze zdefiniowaną
rolą. Element musi zawierać nazwę jednej z ról bezpieczeństwa określonej w elementach <security-role>.
<security-role-ref>
<role-name>men</role-name>
<role-link>menedzer</role-link>
</security-role-ref>

<role-name>

Zestawienie
<!ELEMENT role-name (#DANE)>
Opis
Element <role-name> zawiera nazwę roli bezpieczeństwa.
<auth-constraint>
<role-name>menedzer</role-name>
</auth-constraint>

<security-constraint>

Zestawienie
<!ELEMENT security-constraint (web-resource-collection+,
auth-constraint?, user-data-constraint?)>

Opis
<security-constraint>
<web-resource-collection>
<web-resource-name>TajnaOchrona</web-resource-name>
<url-pattern>/servlet/SerwerPlac</url-pattern>
<url-pattern>/servlet/sekret</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>menedzer</role-name>
</auth-constraint>
</security-constraint>

<security-role>

Zestawienie
<!ELEMENT security-role (description?, role-name)>

Opis
Element <security-role> zawiera deklarację roli bezpieczeństwa, która jest wykorzystywana w
ograniczeniach dostępu umieszczonych w aplikacji WWW.
<security-role>
<role-name>menedzer</role-name>
</security-role>

<security-role-ref>

Zestawienie
<!ELEMENT security-role-ref (description?, role-name, role-link)>

Opis
Element <security-role-ref> określa alias dla roli bezpieczeństwa tworząc odwzorowanie pomiędzy
nazwą wykorzystywaną przez serwlet i nazwą zastosowaną w deskryptorze.
<security-role-ref>
<role-name>men</role-name>
<role-link>menedzer</role-link>
</security-role-ref>

<servlet>

Zestawienie
<!ELEMENT servlet (icon?, servlet-name, display-name?, description?,
(servlet-class|jsp-file), init-param*, load-on-startup?,
security-role-ref*)>
Opis
Element <servlet> zawiera deklaratywne dane serwletu, takie jak jego zarejestrowana nazwa i plik klasy.
<servlet>
<servlet-name>witaj</servlet-name>
<display-name>witaj</display-name>
<description>Nie zapomnij się przywitać!</description>
<servlet-class>WitajSwiecie</servlet-class>
<init-param>
<param-name>domyslnaNazwa</param-name>
<param-value>Świecie</param-value>
</init-param>
<load-on-startup/>
<security-role-ref>
<role-name>men</role-name>
<role-link>menedzer</role-link>
</security-role-ref>
</servlet>

<servlet-class>

Zestawienie
<!ELEMENT servlet-class (#DANE)>

Opis
<servlet-class> zawiera pełną nazwę klasy serwletu/
<servlet>
<servlet-name>witaj</servlet-name>
<servlet-class>WitajSwiecie</servlet-class>
</servlet>

<servlet-mapping>

Zestawienie
<!ELEMENT servlet-mapping (#DANE)>

Opis
Element <servlet-mapping> definiuje odwzorowanie pomiędzy serwletem i wzorem URL-a.
<servlet-mapping>
<servlet-name>witaj</servlet-name>
<url-pattern>/witaj.html</url-pattern>
</servlet-mapping>

<servlet-name>

Zestawienie
<!ELEMENT servlet-name (#DANE)>

Opis
Element <servlet-name> określa kanoniczną nazwę serwletu.
<servlet>
<servlet-name>witaj</servlet-name>
<servlet-class>WitajSwiecie</servlet-class>
</servlet>

<session-config>

Zestawienie
<!ELEMENT session-config (session-timeout?)>
Opis
Element <session-config> definiuje parametry sesji dla aplikacji WWW.
<session-config>
<session-timeout>60 <!-- minuty --></session-timeout>
</session-config>

<session-timeout>

Zestawienie
<!ELEMENT session-timeout (#DANE)>

Opis
Element <session-timeout> definiuje domyślny czas przedawnienia sesji dla wszystkich sesji utworzonych
w aplikacji WWW. Dany czas przedawnienia musi być określony w pełnych minutach.
<session-config>
<session-timeout>60 <!-- minuty --></session-timeout>
</session-config>

<small-icon>

Zestawienie
<!ELEMENT small-icon (#DANE)>

Opis
Element <small-icon> określa położenie wewnątrz aplikacji WWW pliku zawierającego mały (16 x 16
pikseli) obrazek ikony. Narzędzia przyjmują formaty przynajmniej GIF i JPEG.
<icon>
<small-icon>/obrazki/maly.gif</small-icon>
<large-icon>/obrazki/duzy.gif</large-icon>
</icon>

<taglib>

Zestawienie
<!ELEMENT taglib (taglib-uri, taglib-location)>

Opis
Element <taglib> jest wykorzystywany w celu opisania biblioteki znaczników JSP.
<taglib>
<taglib-uri>/WEB-INF/struts.tld</taglib-uri>
<taglib-location>/WEB-INF/struts.tld</taglib-location>
</taglib>

<taglib-location>

Zestawienie
<!ELEMENT taglib-location (#DANE)>

Opis
Element <taglib-location> zawiera umiejscowienie (jako zasobu względnego do katalogu macierzystego
aplikacji WWW) pliku biblioteki znaczników (TLD).
<taglib>
<taglib-uri>/WEB-INF/struts.tld</taglib-uri>
<taglib-location>/WEB-INF/struts.tld</taglib-location>
</taglib>
<taglib-uri>

Zestawienie
<!ELEMENT taglib-uri (#DANE)>

Opis
Element <taglib-uri> opisuje URI względny do położenia dokumentu web.xml, który identyfikuje
bibliotekę znaczników wykorzystaną w aplikacji WWW.
<taglib>
<taglib-uri>/WEB-INF/struts.tld</taglib-uri>
<taglib-location>/WEB-INF/struts.tld</taglib-location>
</taglib>

<transport-guarantee>

Zestawienie
<!ELEMENT transport-guarantee (#DANE)>

Opis
Element <transport-guarantee> określa sposób komunikacji pomiędzy klientem i serwerem jako NONE,
INTEGRAL lub CONFIDENTIAL. NONE oznacza, że aplikacja nie potrzebuje żadnych gwarancji transportu.
Wartość INTEGRAL oznacza, że dane przesyłane pomiędzy klientem i serwerem powinny być wysyłane w ten
sposób, aby nie mogły zostać zmienione podczas transportu. CONFIDENTIAL oznacza, że aplikacja wymaga
przesyłania danych w ten sposób, aby zawartość transmisji nie mogła zostać zaobserwowana przez żaden inny
program lub egzemplarz programu. W większości przypadków obecność znacznika INTEGRAL lub
CONFIDENTIAL oznacza konieczność wykorzystania SSL.
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>

<url-pattern>

Zestawienie
<!ELEMENT url-pattern (#DANE)>

Opis
Element <url-pattern> zawiera wzór URL-a będącego odwzorowaniem serwletu. Wzór ten musi być
zgodny z zasadami wymienionymi w części 10 specyfikacji Servlet API, wyjaśnionymi w rozdziale 2, „Podstawy
serwletów HTTP”.
<servlet-mapping>
<servlet-name>witaj</servlet-name>
<url-pattern>/witaj.html</url-pattern>
</servlet-mapping>

<user-data-constraint>

Zestawienie
<!ELEMENT user-data-constraint (description?, transport-guarantee)>

Opis
Element <user-data-constraint> jest wykorzystywany w celu wskazania sposobu ochrony danych przesyłanych
pomiędzy klientem i kontenerem.
<security-constraint>
<web-resource-collection>
...
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>

<web-app >

Zestawienie
<!ELEMENT web-app (icon?, display-name?, description?, distributable?,
context-param*, servlet*, servlet-mapping*, session-config?,
mime-mapping*, welcome-file-list?, error-page*, taglib*,
resource-ref*, security-constraint*, login-config?,
security-role*, env-entry*, ejb-ref*)>

Opis
Element <web-app> jest podstawowym elementem deskryptora aplikacji WWW.
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">

<web-app>
<servlet>
<servlet-name>witaj</servlet-name>
<servlet-class>WitajSwiecie</servlet-class>
</servlet>
</web-app>

<web-resource-collection>

Zestawienie
<!ELEMENT web-resource-collection (web-resource-name, description?,
url-pattern*, http-method*)>

Opis
Element <web-resource-collection> jest wykorzystywany w celu zidentyfikowania podzbioru zasobów
i metod HTTP tych zasobów, do których odnoszą się ograniczenia dostępu. Jeżeli nie zostały określone żadne
metody HTTP, ograniczenia dostępu odnoszą się do wszystkich metod HTTP.
<security-constraint>
<web-resource-collection>
<web-resource-name>TajnaOchrona</web-resource-name>
<url-pattern>/servlet/SerwerPlac</url-pattern>
<url-pattern>/servlet/sekret</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
</security-constraint>

<web-resource-name>

Zestawienie
<!ELEMENT web-resource-name (#DANE)>

Opis
<web-resource-name> określa nazwę zbioru zasobów WWW.
<web-resource-collection>
<web-resource-name>TajnaOchrona</web-resource-name>
<url-pattern>/servlet/SerwerPlac</url-pattern>
<url-pattern>/servlet/sekret</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<welcome-file>

Zestawienie
<!ELEMENT welcome-file (#DANE)>

Opis
Element <welcome-file> zawiera nazwę pliku, który powinien zostać wykorzystany jako domyślny plik
powitalny.
<welcome-file-list>
<welcome-file>indeks.html</welcome-file>
<welcome-file>indeks.htm</welcome-file>
</welcome-file-list>

<welcome-file-list>

Zestawienie
<!ELEMENT welcome-file-list (welcome-file+)>

Opis
<welcome-file-list> określa listę plików, które powinien wyszukać kontenener, kiedy przeglądarka
zażąda katalogu, a nie strony WWW lub serwletu.
<welcome-file-list>
<welcome-file>indeks.html</welcome-file>
<welcome-file>indeks.htm</welcome-file>
</welcome-file-list>
Dodatek D. Kody stanu HTTP

Kody stanu HTTP są pogrupowane w sposób przedstawiony w tabeli D.1.


Tabela D.1.
Grupy kodów stanu HTTP
Zakres kodów Znaczenie odpowiedzi
100-199 Informacyjne
200-299 Żądanie klienta pomyślne
300-399 Żądanie klienta przekierowane, konieczne następne działanie
400-499 Żądanie klienta niekompletne
500-599 Błąd serwera
Tabela D.2 wylicza stałe kodów stanu zdefiniowane w interfejsie HttpServletResponse i wykorzystywane
jako parametry metod setStatus() i sendError(). Numer wersji w ostatniej kolumnie odnosi się do
wersji protokołu HTTP, która pierwsza zdefiniowała dany kod stanu. Servlet API 2.0 dodał stałe dla kodów
stanu HTTP w wersji 1.1, jak opisano w proponowanym standardzie RFC 2068. Servlet API 2.2 dodał kody
stanu 416 i 417, jak opisano w próbnym standardzie RFC 2616. Proszę zauważyć, że kody stanu HTTP/1.1
wymagają przeglądarki zgodnej z HTTP/1.1.
Większa ilość informacji na temat HTTP jest dostępna w książce „Web Client Programming” autorstwa Clintona
Wonga (O'Reilly). Najnowsza specyfikacja HTTP/1.1 jest dostępna w dokumencie RFC 2616 pod adresem
http://www.ietf.org/rfc/rfc2616.txt.
Tabela D.2.
Stałe kodów stanu HTTP
Stała Kod Domyślna Znaczenie Wersja
wiadomość HTTP
SC_CONTINUE 100 Continue Serwer otrzymał początkową część 1.1
żądania, klient może kontynuować z
dalszymi częściami.
SC_SWITCHING_PROTOCOLS 101 Switching Serwer chce wypełnić żądanie klienta 1.1
Protocols zmiany protokołu na określony w
nagłówku żądania Upgrade.
Działanie to może zawierać
przełączenie na nowszą wersję HTTP
lub własny synchroniczny kanał
wideo.
SC_OK. 201 OK Żądanie klienta zakończyło się 1.0
sukcesem i odpowiedź serwera
zawiera żądane dane. Jest to domyślny
kod stanu.
SC_CREATED 201 Created Na serwerze został utworzony zasób, 1.0
przypuszczalnie w odpowiedzi na
żądanie klienta. Główna część
odpowiedzi powinna zawierać URL
(-e), pod którym można ten nowy
zasób odnaleźć, z najbardziej
specyficznym URL-em zawartym w
nagłówku Location. Jeżeli zasób
nie może zostać utworzony
natychmiast, powinien zamienne
zostać zwrócony kod stanu
SC_ACCEPTED.
SC_ACCEPTED 202 Accepted Żądanie zostało przyjęte do 1.0
przetwarzania, ale nie zostało jeszcze
ukończone. Serwer powinien opisać
aktualny stan żądania w głównej
części odpowiedzi. Serwer nie jest
zobligowany do działania na ani
kończenia wykonywania żądania.
SC_NON_AUTHORITATIVE_INF 203 Non- Nagłówki odpowiedzi HTTP nadeszły 1.1
ORMATION Authoritative z lokalnego lub niezależnego źródła, a
Information nie z oryginalnego serwera. Zwykłe
serwlety nie mają powodów, by
wykorzystywać ten kod stanu
SC_NO_CONTENT 204 No Content Żądanie powiodło się, ale nie istnieje 1.0
główna część nowej odpowiedzi.
Przeglądarki przyjmujące ten kod
powinny zachować swój aktualny
widok dokumentów. Kod ten jest
przydatny serwletowi, kiedy
otrzymuje on dane z formularza, ale
chce, aby przeglądarka zatrzymała się
na formularzu, w ten sposób unikając
komunikatu o błędzie „Dokument nie
zawiera żadnych danych”.
SC_RESET_CONTENT 205 Reset Content Żądanie powiodło się i przeglądarka 1.1
powinno wyczyścić (ponownie
pobrać) aktualnie przeglądany
dokument. Kod ten jest przydatny
serwletowi, który pobiera dane z
formularza i chce, aby został on
wyświetlony w czystej formie.
SC_PARTIAL_CONTENT 206 Partial Serwer ukończył częściowe żądanie 1.1
Content GET i zwrócił część dokumentu
określoną w nagłówku Range klienta.
SC_MULTIPLE_CHOICES 300 Multiple Żądany URL odnosi się do więcej niż 1.1
Choices jednego zasobu. Na przykład, URL
może odnosić się do dokumentu
przetłumaczonego na wiele języków.
Główna część odpowiedzi powinna
wyjaśniać klientowi opcje w formie
odpowiedniej do typu zawartości
żądania. Serwer może zasugerować
wybór przy pomocy nagłówka
Location.
SC_MOVED_PERMANENTLY 301 Moved Żądany zasób został na stałe 1.0
Permanently przeniesiony do nowej lokacji.
Przyszłe odwołania powinny
wykorzystywać w żądaniach nowy
URL. Nowe umiejscowienie jest
podane w nagłówku Location.
Większość przeglądarek
automatycznie przekierowuje do
nowej lokacji.
SC_MOVED_TEMPORARILY 302 Moved Żądany zasób został czasowo 1.0
Temporarily przeniesiony do innej lokacji, ale
przyszłe żądania powinny dalej
wykorzystywać oryginalny URI w
celu uzyskania dostępu do zasobu.
Nowe umiejscowienie jest podane w
nagłówku Location. Większość
przeglądarek automatycznie
przekierowuje do nowej lokacji.
SC_SEE_OTHER 303 See Other Żądany zasób przetworzył żądanie, ale 1.1
klient powinien pobrać swoją
odpowiedź poprzez wykonanie GET
na URL-u określonym w nagłówku
Location. Kod ten jest przydatny
serwletowi, który chce otrzymywać
dane POST, po czym przekierowywać
klienta do innego zasobu w celu
wygenerowania odpowiedzi.
SC_NOT_MODIFIED 304 Not Modified Żądany dokument nie został 1.0
zmieniony od daty wymienionej w
nagłówku żądania If-Modified-
Since. Zwykłe serwlety nie powinny
być zmuszone do korzystania z tego
kodu stanu. Zamiast tego
implementują one
getLastModified().
SC_USE_PROXY 305 Use Proxy Dostęp do żądanego zasobu musi być 1.1
uzyskiwany poprzez serwer proxy
podany w nagłówku Location.
SC_BAD_REQUEST 400 Bad Request Serwer nie mógł zrozumieć żądania, 1.0
prawdopodobnie z powodu błędu
składni.
SC_UNAUTHORIZED 401 Unauthorized Żądaniu brakuje właściwego 1.0
uwierzytelnienia. Wykorzystywany w
połączeniu z nagłówkami WWW-
Authenticate i
Authorization.
SC_PAYMENT_REQUIRED 402 Payment Zarezerwowany do przyszłego użytku. 1.1
Required Istnieją propozycje, by
wykorzystywać ten kod w połączeniu
z nagłówkiem Charge-To, ale nie stał
się on jeszcze standardem w czasie
oddawania niniejszej książki do
druku.
SC_FORBIDDEN 403 Forbidden Żądanie zostało zrozumiane, ale 1.0
serwer nie chce go wypełnić. Serwer
może wyjaśnić powody swojego
oporu w głównej części odpowiedzi.
SC_NOT_FOUND 404 Not Found Żądany zasób nie mógł zostać 1.0
odnaleziony, lub jest niedostępny.
SC_METHOD_NOT_ALLOWED 405 Method Not Metoda wykorzystywana przez klienta 1.1
Allowed nie jest obsługiwana w tym URL-u.
Metody obsługiwane muszą zostać
wymienione w nagłówku odpowiedzi
Accept.
SC_NOT_ACCEPTABLE 406 Not Żądany zasób istnieje, ale w formacie 1.1
Acceptable nie przyjmowanym przez klienta (jak
wskazano w nagłówku (ach) żądania
Accept.
SC_PROXY_AUTHENTICATION_ 407 Proxy Serwer proxy musi dokonać 1.1
REQUIRED Authenticatio uwierzytelnienia, zanim umożliwi
n Required przejście dalej. Wykorzystywany z
nagłówkiem Proxy-
Authenticate. Zwykłe serwlety
nie powinny być zmuszone do
korzystania z tego kodu stanu.
SC_REQUEST_TIMEOUT 408 Request Klient nie dokończył swojego żądania 1.1
Timeout w czasie, w którym serwer był skłonny
go słuchać.
SC_CONFLICT 409 Conflict Żądanie nie mogło zostać wypełnione, 1.0
ponieważ nastąpił konflikt z innym
żądaniem lub konfiguracją serwera.
Kod ten występuje najczęściej w
przypadku żądań HTTP PUT. W
których plik jest poddawany kontroli
oraz w przypadku konfliktów nowych
wersji z poprzednimi. Serwer może
wysłać opis konfliktu w głównej
części odpowiedzi
SC_GONE 410 Gone Zasób nie jest już dostępny na danym 1.1
serwerze, a żaden alternatywny adres
nie jest znany. Kod ten powinien być
wykorzystywany jedynie w
przypadku, kiedy zasób został trwale
usunięty. Zwykłe serwlety nie
powinny być zmuszone do korzystania
z tego kodu stanu.
SC_LENGTH_REQUIRED 411 Length Serwer nie przyjmie żądania bez 1.1
Required nagłówka Content-Length.
SC_PRECONDITION_FAILED 412 Precondition Wynik wstępnego warunku 1.1
Failed określonego w jednym lub więcej
nagłówku If... wynosi false.
SC_REQUEST_ENTITY_TOO_LA 413 Request Serwer nie przetworzy żądania, 1.1
RGE Entity Too ponieważ zawartość żądania jest zbyt
Large duża. Jeżeli ograniczenie to jest
tymczasowe, serwer może dołączyć
nagłówek Retry-After.
SC_REQUEST_URI_TOO_LONG 414 Request-URI Serwer nie przetworzy żądania, 1.1
Too Long ponieważ URI żądania jest dłuższe,
niż może przyjąć. Może to nastąpić,
kiedy klient przypadkowo
przekonwertował żądanie POST na
GET. Zwykłe serwlety nie powinny
być zmuszone do korzystania z tego
kodu stanu.
SC_UNSUPPORTED_MEDIA_TYP 415 Unsupported Serwer nie przetworzy żądania, 1.1
E Media Type ponieważ jego główna część posiada
format nieobsługiwany przez żądany
zasób.
SC_REQUESTED_RANGE_NOT_S 416 Requested Zakres zawartości żądanej przez 1.1 (RFC
ATISFYABLE Range Not klienta poprzez nagłówek Range 2616)
Satisfiable koliduje z wielkością żądanego
zasobu
SC_EXPECTATION_FAILED 417 Expectation Oczekiwanie podane przez klienta 1.1 (RFC
Failed poprzez nagłówek Expect nie mogło 2616)
zostać spełnione.
SC_INTERNAL_SERVER_ERROR 500 Internal Wewnątrz serwera nastąpił 1.0
Server Error niespodziewany błąd, który
przeszkodziła w wypełnieniu żądania.
SC_NOT_IMPLEMENTED 501 Not Serwer nie obsługuje funkcjonalności 1.0
Implemented potrzebnej do wypełnienia żądania.
SC_BAD_GATEWAY 502 Bad Gateway Serwer działający jako brama lub 1.0
proxy nie otrzymał prawidłowej
odpowiedzi od głównego serwera.
SC_SERVICE_UNAVAILABLE 503 Service Usługa (serwer) jest tymczasowo 1.0
Unavailable niedostępna, ale powinna zostać
przywrócona w przyszłości. Jeżeli
serwer wie, kiedy będzie ponownie
dostępny, może dołączyć nagłówek
Retry-After.
SC_GATEWAY_TIMEOUT 504 Gateway Serwer działający jako brama lub 1.1
Timeout proxy nie otrzymał prawidłowej
odpowiedzi od głównego serwera w
czasie oczekiwania.
SC_HTTP_VERSION_NOT_SUPP 505 HTTP Serwer nie obsługuje wersji protokołu 1.1
ORTED Version Not HTTP wykorzystanej w żądaniu.
Supported Główna część odpowiedzi powinna
określać protokoły obsługiwane przez
serwer. Zwykłe serwlety nie powinny
być zmuszone do korzystania z tego
kodu stanu.
Dodatek E. Encje znakowe

Poniższa tablica wymienia różne kody ucieczkowe Unicode, encje nazwane i numeryczne HTML dla wszystkich
możliwych do wyświetlenia znaków ISO-8859 (Latin-1).
Encje nazwane i numeryczne mogą zostać wykorzystane na stronach HTML; są one konwertowane na symbole
przez przeglądarki WWW. Kody ucieczkowe Unicode mogą zostać wykorzystane w kodzie serwletów; są one
interpretowane przez kompilator Javy. Na przykład, znak funta (£) może zostać osadzony na stronie HTML jako
&#163; lub &pound;. Może również zostać osadzony bezpośrednio w kodzie Javy jako \u00A3.
Proszę zauważyć, że nie każdy znak HTML jest uniwersalnie obsługiwany. Kolumna Obsługa pokazuje poziom
obsługi znaku. Wartość S oznacza, że numeryczne i nazwane wartości encji dla danego symbolu są częścią
standardu HTML. P wskazuje, że wartości encji są proponowanym standardem — nie są częścią standardu
HTML, ale większości przypadków są powszechnie obsługiwane. N w tej kolumnie oznacza, że wartości encji
nie są standardem i w związku z tym ich obsługa jest ograniczona. W przypadku tych symboli najlepiej jest
zastosować kody ucieczkowe Unicode.

Kod Encja Encja nazwana Symbol Opis Obsługa


ucieczkowy numeryczna
Unicode
\u0009 &#009; \t Tabulator poziomy S
\u000A &#010; \n Koniec linii S
\u000D &#013; \r Powrót karetki S
\u0020 &#032; Spacja S
\u0021 &#033; ! Wykrzyknik S
\u0022 &#034; &quot; " Cudzysłów S
\u0023 &#035; # Płotek S
\u0024 &#036; $ Znak dolara S
\u0025 &#037; % Znak procent S
\u0026 &#038; &amp; & Znak łączący S
\u0027 &#039; ' Apostrof S
\u0028 &#040; ( Lewy nawias S
\u0029 &#041; ) Prawy nawias S
\u002A &#042; * Gwiazdka S
\u002B &#043; + Znak dodawania S
\u002C &#044; , Przecinek S
\u002D &#045; - Myślnik S
\u002E &#046; . Kropka S
\u002F &#047; / Ukośnik S
\u0030- &#048;- 0-9 Cyfry 0-9 S
\u0039 &#057
\u003A &#058; : Dwukropek S
\u003B &#059; ; Średnik S
\u003C &#060; &lt; < Znak mniejszości S
\u003D &#061; = Znak równości S
\u003E &#062; &gt; > Znak większości S
\u003F &#063; ? Znak zapytania S
\u0040 &#064; @ „Małpa” S
\u0041- &#065;- A-Z Litery A-Z S
\u005A &#090;
\u005B &#091; [ Lewy nawias kwadratowy S
\u005C &#092; \ Lewy ukośnik S
\u005D &#093; ] Prawy nawias kwadratowy S
\u005E &#094; ^ Karetka S
\u005F &#095; _ Kreska dolna S
\u0060 &#096; ` Akcent grave S
\u0061- &#097;- a-z Litery a-z S
\u007A &#122;
\u007B &#123; { Lewy nawias klamrowy S
\u007C &#124; | Kreska pionowa S
\u007D &#125; } Prawy nawias klamrowy S
\u007E &#126; ~ Tylda S
\u0082 &#130; ‚ Dolny lewy nawias pojedynczy N
\u0083 &#131; f Floren N
\u0084 &#132; „ Lewy dolny nawias podwójny N
\u0085 &#133; … Trzykropek N
\u0086 &#134; † Sztylet N
\u0087 &#135; ‡ Podwójny sztylet N
\u0088 &#136; ˆ circumflex N
\u0089 &#137; ‰ Promil N
\u008A &#138; Š Duże S, caron N
\u008B &#139; ‹ Znak mniejszości N
\u008C &#140; Œ Duże OE, ligatura N
\u0091 &#145; ‘ Lewy pojedynczy cudzysłów N
\u0092 &#146; ’ Prawy pojedynczy cudzysłów N
\u0093 &#147; “ Lewy podwójny cudzysłów N
\u0094 &#148; ” Prawy podwójny cudzysłów N
\u0095 &#149; • Pocisk N
\u0096 &#150; – Krótki myślnik N
\u0097 &#151; — Długi myślnik N
\u0098 &#152; ~ Tylda N
\u0099 &#153; ™ Znak towarowy N
\u009A &#154; š Małe s, caron N
\u009B &#155; › Znak większości N
\u009C &#156; œ Małe oe, ligatura N
\u009F &#159; Ÿ Duże Y, umlaut N
\u00A0 &#160; &nbsp; Spacja niełamiąca P
\u00A1 &#161; &iexcl; ¡ Odwrócony wykrzyknik P
\u00A2 &#162; &cent; ¢ Znak centa P
\u00A3 &#163; &pound; £ Znak funta P
\u00A4 &#164; &curren; ¤ Ogólny znak waluty P
\u00A5 &#165; &yen; ¥ Znak jena P
\u00A6 &#166; &brvbar; ¦ Złamana pionowa kreska P
\u00A7 &#167; &sect; § Paragraf P
\u00A8 &#168; &uml; ¨ Umlaut P
\u00A9 &#169; &copy; © Prawa autorskie P
\u00AA &#170; &ordf; ª Żeński liczebnik porządkowy P
\u00AB &#171; &laquo; « Lewy cudzysłów kątowy P
\u00AC &#172; &not; ¬ Znak zaprzeczenia P
\u00AD &#173; &shy; „Miękki” myślnik P
\u00AE &#174; &reg; ® Zarejestrowany znak towarowy P
\u00AF &#175; &macr; ¯ Akcent macron P
\u00B0 &#176; &deg; ° Znak stopnia P
\u00B1 &#177; &plusmn; ± Plus lub minus P
\u00B2 &#178; &sup2; ² Indeks górny 2 P
\u00B3 &#179; &sup3; ³ Indeks górny 3 P
\u00B4 &#180; &acute; ´ Akcent acute P
\u00B5 &#181; &micro; µ Znak mikro (greckie mi) P
\u00B6 &#182; &para; ¶ Znak akapitu P
\u00B7 &#183; &middot; · Kropka centralna P
\u00B8 &#184; &cedil; ¸ Cedilla P
\u00B9 &#185; &sup1; ¹ Indeks górny 1 P
\u00BA &#186; &ordm; º Męski liczebnik porządkowy P
\u00BB &#187; &raquo; » Prawy cudzysłów kątowy P
\u00BC &#188; &frac14; ¼ Ułamek jedna czwarta P
\u00BD &#189; &frac12; ½ Ułamek jedna druga P
\u00BE &#190; &frac34; ¾ Ułamek trzy czwarte P
\u00BF &#191; &iquest; ¿ Odwrócony znak zapytania P
\u00C0 &#192; &Agrave; À Duże A, akcent grave S
\u00C1 &#193; &Aacute; Á Duże A, akcent acute S
\u00C2 &#194; &Acirc; Â Duże A, akcent circumflex S
\u00C3 &#195; &Atilde; Ă Duże A, tylda S
\u00C4 &#196; &Auml; Ä Duże A, umlaut S
\u00C5 &#197; &Aring; Å Duże A, okrąg S
\u00C6 &#198; &Aelig; Æ Duże A, ligatura S
\u00C7 &#199; &Ccedil; Ç Duże C, cedilla S
\u00C8 &#200; &Egrave; È Duże E, akcent grave S
\u00C9 &#201; &Eacute; É Duże E, akcent acute S
\u00CA &#202; &Ecirc; Ê Duże E, akcent circumflex S
\u00CB &#203; &Euml; Ë Duże E, umlaut S
\u00CC &#204; &Igrave; Ì Duże I, akcent grave S
\u00CD &#205; &Iacute; Í Duże I, akcent acute S
\u00CE &#206; &Icirc; Î Duże I, akcent circumflex S
\u00CF &#207; &Iuml; Ï Duże I, umlaut S
\u00D0 &#208; &ETH; Ð Duże eth, islandzki S
\u00D1 &#209; &Ntilde; Ñ Duże N, tylda S
\u00D2 &#210; &Ograve; Ò Duże O, akcent grave S
\u00D3 &#211; &Oacute; Ó Duże O, akcent acute S
\u00D4 &#212; &Ocirc; Ô Duże O, akcent circumflex S
\u00D5 &#213; &Otilde; Õ Duże O, tylda S
\u00D6 &#214; &Ouml; Ö Duże O, umlaut S
\u00D7 &#215; &times; × Znak mnożenia P
\u00D8 &#216; &Oslash; Ø Duże O, ukośnik S
\u00D9 &#217; &Ugrave; Ù Duże U, akcent grave S
\u00DA &#218; &Uacute; Ú Duże U, akcent acute S
\u00DB &#219; &Ucirc; Û Duże U, akcent circumflex S
\u00DC &#220; &Uuml; Ü Duże U, umlaut S
\u00DD &#221; &Yacute; Ý Duże Y, akcent acute S
\u00DE &#222; &THORN; Þ Duże thorn, islandzki S
\u00DF &#223; &szlig; ß Małe sz, ligatura, niemiecki S
\u00E0 &#224; &agrave; à Małe a, akcent grave S
\u00E1 &#225; &aacute; á Małe a, akcent acute S
\u00E2 &#226; &acirc; â Małe a, akcent circumflex S
\u00E3 &#227; &atilde; ã Małe a, tylda S
\u00E4 &#228; &auml; ä Małe a, umlaut S
\u00E5 &#229; &aring; å Małe a, okrąg S
\u00E6 &#230; &aelig; æ Małe a, ligatura S
\u00E7 &#231; &ccedil; ç Małe c, cedilla S
\u00E8 &#232; &egrave; è Małe e, akcent grave S
\u00E9 &#233; &eacute; é Małe e, akcent acute S
\u00EA &#234; &ecirc; ê Małe e, akcent circumflex S
\u00EB &#235; &euml; ë Małe e, umlaut S
\u00EC &#236; &igrave; ì Małe i, akcent grave S
\u00ED &#237; &iacute; í Małe i, akcent acute S
\u00EE &#238; &icirc; î Małe i, akcent circumflex S
\u00EF &#239; &iuml; ï Małe i, umlaut S
\u00F0 &#240; &eth; ð Małe eth, islandzki S
\u00F1 &#241; &ntilde; ñ Małe n, tylda S
\u00F2 &#242; &ograve; ò Małe o, akcent grave S
\u00F3 &#243; &oacute; ó Małe o, akcent acute S
\u00F4 &#244; &ocirc; ô Małe o, akcent circumflex S
\u00F5 &#245; &otilde; õ Małe o, tylda S
\u00F6 &#246; &ouml; ö Małe o, umlaut S
\u00F7 &#247; &divide; ÷ Znak dzielenia P
\u00F8 &#248; &oslash; ø Małe o, ukośnik S
\u00F9 &#249; &ugrave; ù Małe u, akcent grave S
\u00FA &#250; &uacute; ú Małe u, akcent acute S
\u00FB &#251; &ucirc; û Małe u, akcent circumflex S
\u00FC &#252; &uuml; ü Małe u, umlaut S
\u00FD &#253; &yacute; ý Małe y, akcent acute S-
\u00FE &#254; &thorn; þ Małe thorn, islandzki S
\u00FF &#255; &yuml; ÿ Małe y, umlaut S
Dodatek F. Kodowania

Poniższa tabela zawiera sugerowane kodowania dla dużej ilości języków. Kodowania są wykorzystywane przez
serwlety generujące informacje w wielu językach; określają one, które kodowanie znaków powinien wykorzystać
PrintWriter serwletów. Domyślnie PrintWriter wykorzystuje kodowanie ISO-8859-1 (Latin-1),
właściwe dla większości języków zachodnioeuropejskich. Aby określić alternatywne kodowanie, wartość
kodowania musi zostać przekazana metodzie setContentType() zanim serwlet pobierze swój
PrintWriter, na przykład:
odp.setContentType("text/html; charset=Shift_JIS"); // Kodowanie japońskie
PrintWriter wyj = odp.getWriter; // Zapisuje japoński Shift_JIS
Kodowanie może być również ustawione pośrednio, przy pomocy metody setLocale(), na przykład:
odp.setContentType("text/html");
odp.setLocale(new Locale("ja", "")); // Ustawia kodowanie na Shift_JIS
PrintWriter wyj = odp.getWriter; // Zapisuje japoński Shift_JIS
Metoda setLocale() przypisuje odpowiedzi kodowanie zgodnie z poniższą tabelą. W przypadku, gdy
możliwe jest więcej niż jedno kodowanie, wybierane jest kodowanie umieszczone w tabeli na pierwszej pozycji.
Proszę zauważyć, że nie wszystkie przeglądarki WWW obsługują wszystkie kodowania, lub posiadają czcionki,
dzięki którym istnieje możliwość wyświetlania wszystkich znaków, chociaż wszystkie klienty obsługują
przynajmniej ISO-8859-1. Proszę także pamiętać, że kodowanie UTF-8 może reprezentować wszystkie znaki
Unicode, a w związku z tym może być uznane za właściwe dla wszystkich języków.
Język Kod języka Sugerowane kodowania
angielski en ISO-8859-1
Albański sq ISO-8859-2
Arabski ar ISO-8859-6
białoruski be ISO-8859-5
Bułgarski bg ISO-8859-5
chiński (tradycyjny / Tajwan) zh (kraj TW) Big5
chiński (uproszczony / kontynentalny zh GB2312
Chorwacki hr ISO-8859-2
czeski cs ISO-8859-2
duński da ISO-8859-1
holenderski nl ISO-8859-1
estoński et ISO-8859-1
fiński fi ISO-8859-1
francuski fr ISO-8859-1
grecki el ISO-8859-7
hebrajski he (dawniej iw) ISO-8859-8
hiszpański es ISO-8859-1
islandzki is ISO-8859-1
japoński ja Shift_JIS, ISO-2022-JP, EUC-JP45
kataloński (hiszpański) ca ISO-8859-1

45
Obsługiwany po raz pierwszy w JDK 1.1.6. Poprzednie wersje JDK znają zestaw znaków EUC-JP pod nazwą EUCJIS, tak
więc w celu zapewnienia przenośności można ustawić kodowanie na EUC_JP i samodzielnie skonstruować PrintWriter
EUCJIS.
koreański ko EUC-KR46
litewski lt ISO-8859-2
łotewski lv ISO-8859-2
macedoński mk ISO-8859-5
niemiecki de ISO-8859-1
polski pl ISO-8859-2
portugalski pt ISO-8859-1
rosyjski ru ISO-8859-5, KOI8-R
rumuński ro ISO-8859-2
serbski sr ISO-8859-5, KOI8-R
serbsko-chorwacki sh ISO-8859-5, ISO-8859-2, KOI8-R
słowacki sk ISO-8859-2
słoweński sl ISO-8859-2
szwedzki sv ISO-8859-1
turecki tr ISO-8859-9
ukraiński uk ISO-8859-5, KOI8-R
węgierski hu ISO-8859-2
włoski it ISO-8859-1

46
Obsługiwany po raz pierwszy w JDK 1.1.6. Poprzednie wersje JDK znają zestaw znaków EUC-KR pod nazwą KSC_5601,
tak więc w celu zapewnienia przenośności można ustawić kodowanie na EUC_KR i samodzielnie skonstruować
PrintWriter KSC_5601.

You might also like