W niniejszym rozdziale:
Języki zachodnioeuropejskie
Hołdowanie lokalnym zwyczajom
Języki spoza Europy Zachodniej
Większa ilość języków
Dynamiczna negocjacja języka
Formularze HTML
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 --> [Author:F&L] , 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ądarki. 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.
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 ñ przedstawia ñ, a ¡ 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ñol</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H3>En Español:</H3>");
wyj.println("¡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, ñ przedstawia ñ, a ¡ 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ñol</TITLE></HEAD>");
wyj.println("<BODY>");
wyj.println("<H3>En Español:</H3>");
wyj.println("¡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 kodowanie. Istnieje nieskończona ilość potencjalnych kodowań. Na szczęście, najczęściej stosuje się jedynie kilkadziesiąt z nich.
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 hebrajskiego.
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:
Wybranie kodowania i ustawienie go dla serwletu.
Wybranie PrintWriter dla tego kodowania.
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.
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ę, 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.
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-1. 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 kodowanie. 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 =
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”.
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.
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.
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.
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.
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.
Ukryte pola formularzy zostały wprowadzone w rozdziale 7, w którym były wykorzystywane do śledzenia sesji.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\0-praca\Java Servlet - programowanie. Wyd. 2\r13-t.doc
Może nie pisz tyle razy pod rząd:światowe,swiatowa,światitd