29 maja 2008

java.io.File.toURI().toURL() oraz słów kilka o Ajaxie w Wicket

Przeglądając ostatnie zmiany w Apache Geronimo natrafiłem na zmianę związaną z java.io.File.toURL(), którą Jason zmigrował do File.toURI().toURL(). Kilkakrotnie już trafiałem na taką konwersję i nigdy nie wiedziałem, jaki jest dokładnie powód. Dzisiaj postanowiłem sprawdzić javadoc dla tych metod i okazało się, że:
  • Java 6 "przyłożyła" adnotację @Deprecated do metody File.toURL() z komentarzem, że metoda nie dbała o poprawość zwracanego URL, który mógł zawierać niedozwolone znaki, np. #, &, {, + czy ?.
  • Jako poprawne wywołanie tej metody wskazano na właśnie File.toURI().toURL()
Dobrze wiedzieć te ciekawostki, aby nie popaść w kłopoty podczas wdrożenia aplikacji, czy podczas egzaminu SCJP 6. Od tej pory nie korzystamy z File.toURL. Nigdy!

W ten sposób wykonanie poniższego kawałka kodu:
 String sciezka = "że#to&nie{powinno+działać?";
File pathFile = new File(sciezka);
System.out.println("1. " + pathFile.toURL());
System.out.println("2. " + pathFile.toURI().toURL());
zwróci:
 1. file:/C:/sandbox/że#to&nie{powinno+działać?
2. file:/C:/sandbox/że%23to&nie%7Bpowinno+działać%3F
Jak można zauważyć pierwszy z URLi jest niepoprawny i próba otwarcia takiego adresu spowoduje wyjątek. Dodatkowo dokumentacja java.net.URL również wskazuje na klasę java.net.URI, właśnie ze względu na brak dbałości o poprawność zwracanych adresów URL. Więcej informacji o niedozwolonych znakach w adresie (ze względu na szczególne ich znaczenie) można znaleźć w RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax w sekcji 2.4.3. Excluded US-ASCII Characters.

W trakcie lektury książki Wicket in Action natrafiłem na wzmianki o wsparciu Ajaxa. Dla ustalenia uwagi moją znajomość JavaScript i innych technologii klienckich określiłbym bliską zeru, więc każdorazowe wsparcie ze strony szkieletów aplikacyjnych w tym zakresie witanych jest przeze mnie z wielkim entuzjazmem. Tak jest z JavaServer Faces, i tak jest z Apache Wicket. Oba rozwiązania mają swoje zalety, gdzie podstawową zaletą JSF podczas porównania z Wicket jest możliwość uruchamiania JSP i jego znaczników, gdzie właśnie to jest podnoszone jako zaleta Wicketa, w którym użycie JSP jest niezwykle utrudnone, jeśli w ogóle w pełni możliwe. Zwróciłem na to uwagę podczas wykorzystania znaczników jpivot dla kostek OLAP, gdzie musiałem zwizualizować kilka takich struktur i jedynym sposobem mogło być wykorzystanie JSF.

Wracając do Wicketa, bo o nim chciałem wspomnieć i jego wsparciu Ajaxa, uruchomienie wstawek ajaksowych sprowadza się do wykorzystania odpowiedników ajaksowanych dla kontrolek akcyjnych - łącze (ang. link) czy przycisk (ang. button), np. org.apache.wicket.ajax.markup.html.AjaxFallbackLink dla org.apache.wicket.markup.html.link.Link (de facto AjaxFallbackLink rozszerza Link) oraz org.apache.wicket.ajax.markup.html.form.AjaxFallbackButton dla org.apache.wicket.markup.html.form.Button. Zaletą stosowania typów AjaxFallback* jest jednoczesna obsługa żądań ajaksowych i nieajaksowych, zlecając Wicketowi decyzję jaką obsługę wybrać do możliwości klienta (przeglądarki).

Dla zniecierpliwionych, podaję 5-minutowy "przepis" na ajaksową aplikację z Wicket. Jedyne co potrzeba, to Apache Maven 2 (dalej m2) oraz Eclipse IDE z wtyczką M2Eclipse, do pracy z projektami kontrolowanymi przez m2.

1. Utworzenie projektu wicket-ajax-demo
 $ mvn archetype:create \
-DarchetypeGroupId=org.apache.wicket \
-DarchetypeArtifactId=wicket-archetype-quickstart \
-DarchetypeVersion=1.4-m1 \
-DgroupId=pl.jaceklaskowski.wicket \
-DartifactId=wicket-ajax-demo
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO] task-segment: [archetype:create] (aggregator-style)
[INFO] ------------------------------------------------------------------------
...
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating OldArchetype: wicket-archetype-quickstart:1.4-m1
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: packageName, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: basedir, Value: c:\projs\sandbox
[INFO] Parameter: package, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: artifactId, Value: wicket-ajax-demo
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] OldArchetype created in dir: c:\projs\sandbox\wicket-ajax-demo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
2. Import projektu do Eclipse

Importuję projekt wicket-ajax-demo do Eclipse z pomocą File > Import > Maven Projects.


3. Podniesienie wersji Wicket do 1.4-m1 w pom.xml

Poprawiam pom.xml o podniesienie wersji Wicket do 1.4-m1.
 <wicket.version>1.4-m1</wicket.version>
Eclipse zatroszczy się pobraniem wymaganych zależności projektowych. Dobrze być wtedy w Sieci, aby mogły być pobrane. Zaleca się skorzystanie z menu Maven > Download Sources, aby Eclipse pobrał źródła zależności, w tym i Wicketa z Sieci, co umożliwi podejrzenie jak to działa bezpośrednio w kodzie.

Podniesienie wersji spowoduje, że Eclipse oznaczy kilka klas jako błędne, co będzie wymagało kolejnego uaktualnienia pom.xml o wersję kompilatora do 1.5 za pomocą konfiguracji wtyczki maven-compiler-plugin.
 <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
Po zmianie najlepiej należy ponownie zaimportować projekt (wcześniej go usuwając) bądź urchomić polecenie mvn eclipse:eclipse z linii poleceń w katalogu projektu i F5 (Refresh) w Eclipse.

4. Modyfikacja strony domowej aplikacji - HomePage.html
 <html>
<head>
<title>Wicket Quickstart Archetype Homepage</title>
</head>
<body>
<strong>Wicket Quickstart Archetype Homepage</strong>
<br/><br/>
<span wicket:id="message">message will be here</span>
<br>
<a href="#" wicket:id="link">Wciśnij mnie</a>, a napis wyżej się zmieni.
</body>
</html>
5. Modyfikacja strony domowej aplikacji - pl.jaceklaskowski.wicket.HomePage

Dodanie wicketowego elementu do strony HTML wymaga odpowiedniej zmiany w klasie strony.
 package pl.jaceklaskowski.wicket;

import java.util.Calendar;
import java.util.Locale;

import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.util.convert.IConverter;

public class HomePage extends WebPage {

private static final long serialVersionUID = 1L;

private Label label;
private Link link;

public HomePage(final PageParameters parameters) {

label = new Label("message", "If you see this message wicket is properly configured and running");
label.setOutputMarkupId(true);
add(label);
add(new AjaxFallbackLink("link") {

@Override
public void onClick(AjaxRequestTarget target) {
label.getModel().setObject("Wcisnieto mnie o " + Calendar.getInstance(new Locale("pl")).getTime());
if (target != null) {
target.addComponent(label);
}
}

});
}
}
Sprawdzenie target != null jest konieczne dla sytuacji, w której żadanie było nieajaksowe (klient nie wspiera Ajaxa).

Konieczne jest również wywołanie metody label.setOutputMarkupId(true), której brak spowoduje:
 java.lang.IllegalArgumentException: cannot update component that does not have setOutputMarkupId property set to true. 
Component: [Component id = message, page = pl.jaceklaskowski.wicket.HomePage, path = 1:message.Label, isVisible = true, isVersioned = true]
at org.apache.wicket.ajax.AjaxRequestTarget.addComponent(AjaxRequestTarget.java:343)
at pl.jaceklaskowski.wicket.HomePage$1.onClick(HomePage.java:31)
at org.apache.wicket.ajax.markup.html.AjaxFallbackLink$1.onEvent(AjaxFallbackLink.java:73)
at org.apache.wicket.ajax.AjaxEventBehavior.respond(AjaxEventBehavior.java:161)
at org.apache.wicket.ajax.AbstractDefaultAjaxBehavior.onRequest(AbstractDefaultAjaxBehavior.java:298)
at org.apache.wicket.request.target.component.listener.BehaviorRequestTarget.processEvents(BehaviorRequestTarget.java:100)
at org.apache.wicket.request.AbstractRequestCycleProcessor.processEvents(AbstractRequestCycleProcessor.java:91)
at org.apache.wicket.RequestCycle.processEventsAndRespond(RequestCycle.java:1174)
at org.apache.wicket.RequestCycle.step(RequestCycle.java:1251)
at org.apache.wicket.RequestCycle.steps(RequestCycle.java:1352)
at org.apache.wicket.RequestCycle.request(RequestCycle.java:499)
at org.apache.wicket.protocol.http.WicketFilter.doGet(WicketFilter.java:375)
at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:199)
...
podczas uruchomienia aplikacji bez tego wywołania.

6. Uruchomienie aplikacji z mvn jetty:run

Ostatecznie należy sprawdzić poprawność wprowadzonych zmian uruchamiając aplikację z wybranym kontenerem servletów. Jako, że korzystam z m2 stawiam na Jetty, którego uruchomienie i uruchomienie aplikacji webowej sprowadza się do wykonania polecenia mvn clean jetty:run (dodałem clean dla zapewnienia, że klasy zostały skompilowane w odpowiedniej wersji javy).
 $ mvn clean jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ------------------------------------------------------------------------
[INFO] Building quickstart
[INFO] task-segment: [clean, jetty:run]
[INFO] ------------------------------------------------------------------------
...
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: quickstart
[INFO] Webapp source directory = C:\projs\sandbox\wicket-ajax-demo\src\main\webapp
[INFO] web.xml file = C:\projs\sandbox\wicket-ajax-demo\src\main\webapp\WEB-INF\web.xml
[INFO] Classes = C:\projs\sandbox\wicket-ajax-demo\target\classes
2008-05-29 23:09:58.908::INFO: Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /wicket-ajax-demo
[INFO] Tmp directory = determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides = none
[INFO] Webapp directory = C:\projs\sandbox\wicket-ajax-demo\src\main\webapp
[INFO] Starting jetty 6.1.9 ...
2008-05-29 23:09:58.987::INFO: jetty-6.1.9
2008-05-29 23:09:59.112::INFO: No Transaction manager found - if your webapp requires one, please configure one.
INFO - Application - [WicketApplication] init: Wicket core library initializer
...
INFO - WebApplication - [WicketApplication] Started Wicket version 1.4-m1 in development mode
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode. ***
*** ^^^^^^^^^^^ ***
*** Do NOT deploy to your live server(s) without changing this. ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
2008-05-29 23:09:59.924::INFO: Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
Przechodzę na adres http://localhost:8080/wicket-ajax-demo.

Wciśnięcie łącza Wciśnij mnie spowoduje zmianę napisu na zawierający datę wykonania obsługi wciśnięcia.

Niewielka aplikacja, a jak cieszy. I ta prostota Wicketa w kontekście obsługi Ajaxa - zero JavaScripu czy podobnie (!)

Pytanie konkursowe: Jaka jest rola typów AjaxFallback{Link,Button} w Wicket?

Do zobaczenia na JAVArsovii w nadchodzącą sobotę 31.05.2008 w godzinach 9:00-19:00, gdzie podczas mojej prezentacji o Apache Wicket pokaże tą i inne aplikacje webowe z nim na pokładzie. Jutro audycja w Polskim Radio EURO o 9:00. Ciekawym Waszych opinii odnośnie naszego radiowego występu (więcej o nim we wczorajszej notatce 3 dni do JAVArsovii 2008, audycja w Polskim Radiu, @Override i skróty netbeansowe). Wracam do lektury książki Wicket in Action.

1 komentarz:

  1. Większość naszych developerów Javy używa Uniksów. Kilka lat temu testowaliśmy pierwszą werjsę naszego produktu i ku naszemu wielkiemu zaskoczeniu nie działała ona pod Windows (docelowa platforma serwerowa klienta).

    Bardzo długo szukaliśmy przyczyny problemu (chwile zwątpienia w przenośność Javy :) ), aż w końcu okazało się, że problemem nie jest specyfika Windows, lecz spacja w nazwie katalogu. :) Zamiast przyjemnych "bezspacyjnych" katalogów Uniksowych jak /home czy /usr/local, w przypadku Windows mamy "Program Files" albo "Documents and Settings", w dodatku w formach uzależnionych od lokalizacji systemu - zgroza :). Zresztą nie tylko my mieliśmy ten problem - jboss zainstalowany w "c:\Program Files\jboss" też podówczas nie działał.

    Przyczyną błędu okazał się właśnie problem o którym piszesz i w tym miejscu chciałbym uściślić - nie chodzi o to, że pewne znaki w nazwach plików są niedozwolone, co nie jest sprawdzane w metodzie toURL(), lecz o to, że w rezultacie wywołania tej metody znaki te nie są "escapowane" zgodnie z RFC, co jest ewidentnym błędem. Najlepszy przykład stanowi tutaj właśnie spacja - doskonale dozwolona jako element nazwy pliku, nawet przez Microsoft :)

    Nasz konkretny problem związany był z używaniem RMIClassLoader, który standardowo "karmił się" URLami przechowanymi przez nasz własny URLClassLoader. URLClassLoader ładował klasy bez problemu mimo tego błędu, ale RMIClassLoader już nie. A oto właściwy bug report. Sun niestety nie zamierza nic z tym robić, by zachować kompatybilność w dół z kodem który bazuje na tym niepoprawnym działaniu (np. właśnie jak mi się zdaje URLClassLoader) :).

    OdpowiedzUsuń