niedziela, 21 września 2008

Mój ulubiony Exception - java.lang.ArrayIndexOutOfBoundsException

a wszytko zaczęło się od popełnienia przeze mnie aplikacji. JSF, Facelets, Spring, JPA. Nic specjalnego... Poza tym, że na owej aplikacji od czasu do czasu (co kilka dni) wyskakiwał wyjątek java.lang.ArrayIndexOutOfBoundsException, a dokładniej:

09:17:54,784 ERROR [ajp-0.0.0.0-8009-11] (LifecycleDecorator.java:75) - javax.faces.FacesException: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:306)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
at pl.matt.common.LifecycleDecorator.render(LifecycleDecorator.java:71)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at pl.matt.common.filter.ProcessingTimeFilter.doFilter(ProcessingTimeFilter.java:29)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
at org.apache.coyote.ajp.AjpProcessor.process(AjpProcessor.java:437)
at org.apache.coyote.ajp.AjpProtocol$AjpConnectionHandler.process(AjpProtocol.java:381)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.renderkit.RenderKitUtils.buildTypeArrayFromString(RenderKitUtils.java:719)
at com.sun.faces.renderkit.RenderKitUtils.determineContentType(RenderKitUtils.java:572)
at com.sun.faces.renderkit.RenderKitImpl.createResponseWriter(RenderKitImpl.java:219)
at com.sun.facelets.FaceletViewHandler.createResponseWriter(FaceletViewHandler.java:380)
at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:550)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
... 24 more


Hmm... O co chodzi? Co ciekawe, błąd występował zawsze, gdy na stronę wchodziła przeglądarka Yanga WorldSearch Bot v1.1/beta (http://www.yanga.co.uk/). Robi się coraz bardziej interesująco, prawda? Przy wystąpieniu błędu, miałem też ustawione logowanie nagłówków HTTP. Oto one:

user-agent: Yanga WorldSearch Bot v1.1/beta (http://www.yanga.co.uk/)
accept: text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1
accept-charset: utf-8;q=1.0, windows-1251;q=0.8, cp1251;q=0.8, koi8-r;q=0.8, *;q=0.5
accept-encoding: gzip;q=1.0, deflate;q=1.0, identity;q=0.5, *;q=0
content-length: 0


Niby nic specjalnego (no poza przeglądarką) ale sytuacja dziwna. Aplikacja odpalana jest na JBossie 4.2.1.GA, gdzie implementacją JSFów jest implementacja referencyjna SUNa w wersji 1.2.04-p02. Pobrałem źródła, aby podejrzeć 719 linię klasy RenderKitUtils


private final static String CONTENT_TYPE_SUBTYPE_DELIMITER = "/";

//...

// now split type and subtype
if (typeSubType.indexOf(CONTENT_TYPE_SUBTYPE_DELIMITER) >= 0) {
String[] typeSubTypeParts = Util.split(typeSubType.toString(), CONTENT_TYPE_SUBTYPE_DELIMITER);
type = typeSubTypeParts[0].trim();
subtype = typeSubTypeParts[1].trim();
} else {
type = typeSubType.toString();
subtype = "";
}


linia 719 to
subtype = typeSubTypeParts[1].trim();

Klasa parsuje zatem jeden z nagłówków HTTP gdzie spodziewa się dwóch napisów rozdzielonych "/". Od razu moją uwagę zwrócił nagłówek:
accept: text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1 a dokładniej jego część: text/;.

Trop był, trzeba było go jeszcze sprawdzić. Zainstalowałem zatem w FireFoxie wtyczkę Modify Headers która umożliwia... zmodyfikowanie nagłówków HTTP. Skonfigurowałem ją tak, aby zastępowała nagłówek accept wartością text/html;q=1.0, text/plain;q=1.0, text/;q=0.5, */*;q=0.1.



uruchomiłem aplikację, wszedłem na nią Firefoksem i...
bingo. Aplikacja się wywala. Zaglądam w logi:

00:03:13,862 ERROR [LifecycleDecorator] javax.faces.FacesException: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:306)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
at pl.matt.common.LifecycleDecorator.render(LifecycleDecorator.java:71)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at pl.matt.common.filter.ProcessingTimeFilter.doFilter(ProcessingTimeFilter.java:29)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:230)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:179)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:84)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:104)
at org.jboss.web.tomcat.service.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:157)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:241)
at org.apache.coyote.ajp.AjpProcessor.process(AjpProcessor.java:437)
at org.apache.coyote.ajp.AjpProtocol$AjpConnectionHandler.process(AjpProtocol.java:381)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at com.sun.faces.renderkit.RenderKitUtils.buildTypeArrayFromString(RenderKitUtils.java:719)
at com.sun.faces.renderkit.RenderKitUtils.determineContentType(RenderKitUtils.java:572)
at com.sun.faces.renderkit.RenderKitImpl.createResponseWriter(RenderKitImpl.java:219)
at com.sun.facelets.FaceletViewHandler.createResponseWriter(FaceletViewHandler.java:380)
at com.sun.facelets.FaceletViewHandler.renderView(FaceletViewHandler.java:550)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
... 24 more

Jest mój ulubiony Exception.

Tak jak w tenisie stołowym serwis to pół punktu, tak w programowaniu powtórzenie błędu to połowa jego poprawienia. Teraz tylko... no właśnie...

odpaliłem aplikację w trybie odpluskiwania (debug).



Analizując stos wywołań natrafiłem na 203. linię klasy RenderKitImpl:
String[] typeArray = context.getExternalContext().getRequestHeaderValuesMap().get("Accept");

O co chodzi? Przydała się wiedza z zakresu... serwletów! Na obiekcie HttpServletRequest wywołana jest metoda getHeaders(), która zwraca obiekt klasy Enumeration reprezentujący nagłówki HTTP o zadanej nazwie, w tym przypadku "Accept".

Jak teraz podmienić nagłówek w obiekcie HttpServletRequest? Nie da się...
Da się natomiast, przefiltrować żądanie HTTP, podmieniając obiekt je reprezentujący na obiekt innej klasy (oczywiście implementującej interfejs HttpServletRequest). Brzmi skomplikowanie? Chyba nie jest tak źle.

Zatem po kolei:

tworzę klasę filtra:

public class ModifyRequestHeaderFilter implements Filter {

public void init(FilterConfig filterConfig) throws ServletException {
}

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws java.io.IOException,
ServletException {
if (req instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) req;
req = new HttpServletRequestWrapper(request);
}

chain.doFilter(req, res);
}

public void destroy() {
}
}


metoda
chain.doFilter(req, res);
przekazuje żądanie do dalszego przetworzenia, ale wcześniej, żądanie jest opakowywane:

HttpServletRequest request = (HttpServletRequest) req;
req = new HttpServletRequestWrapper(request);

w obiekt mojej klasy, która przeciąża metodę getHeaders() klasy HttpServletRequest.

Klasa HttpServletRequestWrapper rozszerza klasę javax.servlet.http.HttpServletRequestWrapper, która w zasazdie tylko opakowuje żądanie HTTP. Delegując wywołania wszystkich metod do opakowanego obiektu, właśnie po to, żeby można było przeciążyć wybrane z nich. To przy okazji świetny przykład wzorca dekorator .Tworzę zatem swoją klasę HttpServletRequestWrapper która przy żądniu nagłówków o nazwie "Accept", podmienia w ich treści "text/;" na "text/*;".


public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {

private String substitutedHeaderName = "Accept";
private String toReplace = "text/;";
private String replacement = "text/*;";

public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}

@SuppressWarnings( { "unchecked" })
@Override
public Enumeration getHeaders(String name) {
if (substitutedHeaderName.equals(name)) {
Enumeration headers = super.getHeaders(name);
List out = new ArrayList();
while (headers.hasMoreElements()) {
String header = (String) headers.nextElement();
if (header.contains(toReplace)) {
header = header.replaceAll(toReplace, replacement);
}
out.add(header);
}
return new IteratorEnumeration(out.iterator());
}
return super.getHeaders(name);
}

}


Trzeba jeszcze zarejestrować całe rozwiązanie w deskryptorze wdrożenia - pliku web.xml. Filtr konfiguruję tak, aby obsługiwał wszystkie żądania odwołujące się do stron JSF.


<filter>
<filter-name>ModifyRequestHeaderFilter</filter-name>
<filter-class>pl.matt.common.filter.ModifyRequestHeaderFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>ModifyRequestHeaderFilter</filter-name>
<url-pattern>*.jsf</url-pattern>
</filter-mapping>


Restart aplikacji i... działa. Serwlety wiecznie żywe. Mateusz wiecznie zadowolony.

czwartek, 4 września 2008

JBoss Tools - nowa jakość w tworzeniu aplikacji opartych o JSF

Całkiem niedawno, Piotrek Buczek, jeden z moich zafascynowanych Seamem kolegów pokazał mi kilka sztuczek związanych z tą technologią. Oprócz sztuczek, bardzo zresztą widowiskowych uwagę moją przykuło środowisko na jakim pracował. Był to Eclipse 3.3 z wtyczką JBoss Tools. Wsparcie dla Seama - bardzo pierwsza liga, jak to mawia pan Kazimierz. Jak się ma wsparcie dla samych JSFów, zaraz zobaczymy. Zdradzę tylko, że wcale nie gorzej...

Odkąd dostałem olbrzymią emkę za udział w konkursie Ganymede Around the World Contest nie przystoi mi używać innej wersji Eclipse. Na nim zatem zainstaluję wtyczkę JBoss Tools. Z Ganymede działa tylko rozwojowa wersja wtyczki (JBossTools 3.0.0.Alpha1) więc tą właśnie zainstaluję, ale po kolei...

Wtyczkę można pobrać ze strony projektu JBoss Tools lub zainstalować wykorzystując Eclipsowego zarządcę wtyczek. Wybieram to drugie rozwiązanie. Otwieram menu
Help / Software Updates...
i wybieram zakładkę
Avaliable Software.
Dodaję nową stronę o adresie:
http://download.jboss.org/jbosstools/updates/development



Rozwijam nowo utworzony węzeł drzewka, zaznaczam
JBossTools Development Release 3.0.0.Alpha1
i klikam magiczny przycisk
Install



Eclipse warczy, ściąga, restartuje się... i gotowe. Wypada jeszcze tylko skonfigurować w Eclipse JBossa. Odpalam zatem menu
Window / Preferencess. i tam wybieram pozycję
Server / Runtime Environments następnie klikam
Add... i rozwijam gałąź JBoss, a division of Red Hat.


Kilka razy klikam Next wskazuję położenie na dysku JBossa i gotowe.

Czas przyjrzeć się lepiej możliwościom wtyczki.Tworzę nowy projekt JSF (File / New... / Project... / JSF Project).


Na następnej zakładce wybieram wersję JSF 1.2 z Faceletami oraz szablon FaceletsKickStartWithoutLibs, jako, że biblioteki mam w JBossie.




Zobaczmy co oferuje wtyczka. Otwieram plik WebContent/pages/greeting.xhtml. Pierwsze zaskoczenie - oprócz kodu pliku xhtml, mam zgrabny podgląd.



Klikam dowolne miejsce na podglądzie, podświetla mi się odpowiednie miejsce w kodzie.



Ładnie...
Dodaję nowy tag, zmiany na bieżąco widzę w oknie podglądu.



Jak dla mnie, rewelacja...
Czas na zabawy z klawiszem CTRL. Przyciskam go, najeżdżam myszą na pole name wyrażenia #{person.name}. Klikam..,. i już widzę metodę getName() klasy Person, która w pliku faces-config.xml skonfigurowana jest jako ziarno zarządzane JSF.



Klikam w słówko greeting wyrażenia #{msg.greeting}. No i bomba, jestem już przy edycji pliku messages.properties i jego właściwości greeting.



Plik oczywiście jest skonfigurowany odpowiednio w faces-config.xml.
Przejdźmy zatem do tego pliku. Już pierwszy rzut oka, pozwala nam się zorientować, że mamy możliwość graficznej edycji nawigacji w aplikacji JSF.



Sympatycznie. Tu niestety mały minus... Wtyczka automatycznie formatuje zawartość xmlową pliku faces-config.xml, więc nie bardzo można mieć kontrolę nad wcięciami i kolejnością tagów w pliku. Przynajmniej ja nie potrafię tego osiągnąć.

Wrócę jeszcze na moment do pliku WebContent/pages/greeting.xhtml. Oprócz nawigacji z klawiszem CTRL. Wtyczka JBoss Tools oferuje całkiem niezłe podpowiadanie. Zarówno tagów (wystarczy wpisać <f: i nacisnąć CTRL+spację)



jak i właściwości ziaren zarządzanych



oraz wpisów w pliku messages.properties.



Mało? Zobaczmy, co jeszcze oferuje JBoss Tools. Odpalam projekt (na zakładce JBoss Server View klikam Start the server.



Serwer startuje...
Wchodzę na stronę
http://localhost:8080/jsfProject/

otwieram w Eclipsie plik
WebContent/pages/inputname.xhtml. Zauważyliście, że wykorzystywana jest tu, trochę zapomniana funkcja Faceletów, która umożliwia takie konstruowanie stron JSF, aby były one poprawnie wyświetlane w przeglądarce WWW bez uruchamiania ich na serwerze aplikacji?
Zmieniam tytuł Facelets Hello Application na Witaj!. Zapisuję stronę. Przeładowuję ją w przeglądarce... i gotowe, zmiany od razu są widoczne.



Niestety zmiany w kodzie źródłowym, w plikach messages*.properties lub konfiguracja w faces-config.xml wymagają już restartu serwera.



To zapewne tylko niektóre możliwości wtyczki. Jak dla mnie, są one rewelacyjne. Coś czuję, że JBoss Tools dołączy do mojego zestawu ulubionych wtyczek środowiska Eclipse.