W tym rozdziale:
Aplet
Serwer
Klient
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 port jest zmienną wskaźnikową dla ciągu znaków. Ryzyko 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!");
}
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źć.
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 |
/servlet/Classname |
|
/servlet/registeredName |
|
/servlet/Classname |
|
/servlet/Classname/pathinfo |
|
/servlet/Classname/pathinfo |
|
/servlet/Classname/path%20info |
|
/alias.html |
|
Context/path/servlet/Classname |
a %20 reprezentuje obszar zakodowany
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 |
/servlet/Classname |
|
servlet/registeredName |
|
/servlet/Classname |
|
/servlet/Classname |
|
/servlet/Classname |
|
/servlet/Classname |
|
/alias.html |
|
/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”[Author:PG]
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-Language |
Określa język lub języki, które preferuje klient, używając w tym celu skrótów języka 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: 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. |
Authorization |
Zapewnia uprawnienie klienta do dostępu do żądanego URI, włącznie z nazwą użytkownika i 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.* 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.
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).
* W następnym rozdziale pokażemy jak „powiedzieć” „dostęp nie przyznany” po jedenastej próbie wywołania apletu.
* 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.
* 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.
Wdroż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.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\0-praca\Java Servlet - programowanie. Wyd. 2\r04-04.doc
Do Red.prow: może coś naszego?