Integracja Vaadin + Guice z ICEPush
Wczoraj osiągnąłem niewielki sukces integrując zestaw Vaadin + Guice z IcePush. Celem takiego połączenia jest uzyskanie możliwości aktualizacji UI przez zdarzenia generowane na serwerze, a nie tylko dzięki interakcji po stronie klienta. Jest to o tyle ważne, że w klasycznych rozwiązaniach (nawet AJAX) można aktualizować GUI tylko w wyniku akcji po stronie klienta. Jest to wyjątkowo upierdliwe jeżeli chcemy stworzyć rozwiązanie zbliżone funkcjonalnością do klasycznych okienek.
Dodatkowym problemem jest nietypowy sposób zarządzania modułami przez Google Guice Servlet Extension. Rozwiązanie to działa w oparciu o filtr i w praktyce wymaga wyeliminowania mapowania serwletów z pliku web.xml. Jednocześnie ICEPush wymaga podłączenia specjalnego serwletu, który będzie podtrzymywał połączenie klient-serwer.
Przygotowania i konfiguracja środowiska
Konfiguracja środowiska będzie przebiegać w kilku krokach. Ważną rzeczą jest by dodanie funkcjonalności ICEPush nastąpiło w momencie gdy będzie to już naprawdę niezbędne. Wynika to z konieczności każdorazowego kompilowania kodu za pomocą kompilatora GWT. To powoduje, że proces wydłuża się i to na tyle, że można spokojnie pójść na kawę. Jeżeli jednak chcecie dodać ICEPush już na obecnym etapie to warto zastosować pewien trik, który za chwilę omówię.
Pierwszym krokiem w konfiguracji środowiska jest stworzenie pakietu org.icepush.gwt i umieszczenie w nim pliku ICEpush.gwt.xml.
Listing 1. ICEpush.gwt.xml
<?xml version="1.0" encoding="UTF-8"??><module><inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet"></inherits><inherits name="org.vaadin.artur.icepush.IcepushaddonWidgetset"></inherits><inherits name="pl.koziolekweb.vaadin.guice.widgetset.MyAppWidgetSet"></inherits></module>
Następnie musimy stworzyć pakiet pl.koziolekweb.vaadin.guice.widgetset zawierający plik MyAppWidgetSet.gwt.xml.
Listing 2. MyAppWidgetSet.gwt.xml
<?xml version="1.0" encoding="UTF-8"??><module><inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet"></inherits><inherits name="org.vaadin.artur.icepush.IcepushaddonWidgetset"></inherits><inherits name="org.icepush.gwt.ICEpush"></inherits></module>
Na tym kończy się pierwszy standardowy etap konfiguracji. Drugi etap to edycja pliku pom.xml w celu dodania zależności zarówno do ICEPush jak i pluginów kompilatora GWT oraz managera widgetsetów Vaadin.
Listing 3. pom.xml – pluginy
<plugin><groupid>org.codehaus.mojo</groupid><artifactid>gwt-maven-plugin</artifactid><version>2.1.0-1</version><configuration><webappdirectory>${project.build.directory}/${project.build.finalName}/VAADIN/widgetsets</webappdirectory><extrajvmargs>-Xmx512M -Xss1024k</extrajvmargs><runtarget>test</runtarget><hostedwebapp>${project.build.directory}/${project.build.finalName}</hostedwebapp><noserver>true</noserver><port>8080</port><soyc>false</soyc></configuration><executions><execution><goals><goal>resources</goal><goal>compile</goal></goals></execution></executions></plugin><plugin><groupid>com.vaadin</groupid><artifactid>vaadin-maven-plugin</artifactid><version>1.0.1</version><executions><execution><configuration></configuration><goals><goal>update-widgetset</goal></goals></execution></executions></plugin>
Listing 4. pom.xml – zależności ICEPush
<dependency><groupid>org.vaadin.addons</groupid><artifactid>icepush</artifactid><version>0.2.1</version></dependency><dependency><groupid>org.icepush</groupid><artifactid>icepush</artifactid><version>2.0</version></dependency><dependency><groupid>org.icepush</groupid><artifactid>icepush-gwt</artifactid><version>2.0</version></dependency><dependency><groupid>com.google.gwt</groupid><artifactid>gwt-user</artifactid><version>2.1.1</version><scope>provided</scope></dependency>
Z taką konfiguracją możemy już działać z ICEPush w zwykłej aplikacji Vaadin. Należy co prawda dodać jeszcze odpowiedni serwlet do naszego web.xml, ale o tym na stronie dodatku.
Czas na dodanie konfiguracji Guice. Jest to kolejny przyjemny etap prac konfiguracyjnych. Tu sprawa zaczyna się jednak komplikować, bo i ile konfiguracja w pliku pom.xml
Listing 5. pom.xml – zależności Guice
<dependency><groupid>com.google.inject</groupid><artifactid>guice</artifactid><version>2.0</version></dependency><dependency><groupid>com.google.inject.extensions</groupid><artifactid>guice-servlet</artifactid><version>2.0</version></dependency><dependency><groupid>javax.servlet</groupid><artifactid>servlet-api</artifactid><version>2.5</version></dependency>
to dalej zaczynają się drobne schody.
Po pierwsze Guice przejmuje zarządzanie serwletami od kontenera. To oznacza usunięcie z web.xml mapowania serwletów i zastąpienie ich konfiguracją filtra z Guice.
Listing 6. web.xml – konfiguracja Guice
<?xml version="1.0" encoding="UTF-8"??><web-app id="WebApp_ID" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"><display-name>Vaadin-Guice-ICEPush application</display-name><context-param><description>Vaadin production mode</description><param-name>productionMode</param-name><param-value>false</param-value></context-param><filter><filter-name>guiceFilter</filter-name><filter-class>com.google.inject.servlet.GuiceFilter</filter-class></filter><filter-mapping><filter-name>guiceFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><listener><listener-class>pl.koziolekweb.vaadin.guice.servlet.VaadinGuiceConfiguration</listener-class></listener></web-app>
Następnie należy samodzielnie zaimplementować listener, który utworzy nam injector Guice:
Listing 7. Implementacja listenera Guice
package pl.koziolekweb.vaadin.guice.servlet;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
public class VaadinGuiceConfiguration extends GuiceServletContextListener {
@Override
protected Injector getInjector() {
return Guice.createInjector(new MyServletModule());
}
}
Na koniec trzeba utworzyć moduł Guice:
Listing 8. Implementacja modułu Guice
package pl.koziolekweb.vaadin.guice.servlet;
import java.util.HashMap;
import java.util.Map;
import pl.koziolekweb.vaadin.guice.ApplicationRoot;
import pl.koziolekweb.vaadin.guice.modules.books.model.dao.BookDao;
import pl.koziolekweb.vaadin.guice.modules.books.model.dao.SimpleBookDao;
import pl.koziolekweb.vaadin.guice.modules.books.presenter.ListBookPresenter;
import pl.koziolekweb.vaadin.guice.modules.books.presenter.ListBookPresenterImpl;
import pl.koziolekweb.vaadin.guice.modules.books.view.ListBookView;
import pl.koziolekweb.vaadin.guice.modules.books.view.ListBookViewImpl;
import com.google.inject.servlet.ServletModule;
import com.google.inject.servlet.ServletScopes;
import com.vaadin.Application;
public class MyServletModule extends ServletModule {
@Override
protected void configureServlets() {
Map<String, String> params = new HashMap<String, String>();
serve("/*").with(GuiceApplicationServlet.class, params);
bind(Application.class).to(ApplicationRoot.class).in(ServletScopes.SESSION);
bind(ListBookView.class).to(ListBookViewImpl.class);
bind(BookDao.class).to(SimpleBookDao.class);
bind(ListBookPresenter.class).to(ListBookPresenterImpl.class);
}
}
Na tym etapie należy chwilę się zatrzymać i zastanowić co tu się dzieje. W momencie nadejścia żądania uruchamiany jest filtr guice, który wyszukuje sobie istniejące injectory zawierające ServletModule i w zależności co mu skonfigurujemy w ramach takiego modułu oraz w zależności od ścieżki z żądania zostanie odpalony odpowiedni serwlet. Injectory są trzymane w ramach kontekstu aplikacji, a dodaje je tam implementacja GuiceServletContextListener.
No właśnie tu dochodzimy do najciekawszego elementu czyli spinania Guice z ICEPush. Jeżeli chcemy korzystać z samego Guice w Vaadin to tworzymy własny serwlet rozszerzający AbstractApplicationServlet i tyle.
Listing 9. Implementacja AbstractApplicationServlet dla Guice
package pl.koziolekweb.vaadin.guice.servlet;
import java.io.IOException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.vaadin.Application;
import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
@SuppressWarnings("serial")
@Singleton
public class GuiceApplicationServlet extends AbstractApplicationServlet {
private final Provider<Application> applicationProvider;
@Inject
public GuiceApplicationServlet(Provider<Application> applicationProvider) {
this.applicationProvider = applicationProvider;
}
@Override
protected Class<? extends Application> getApplicationClass() throws ClassNotFoundException {
return Application.class;
}
@Override
protected Application getNewApplication(HttpServletRequest request) throws ServletException {
return applicationProvider.get();
}
}
Jednak jeżeli chcemy dodać obsługę ICEPush to musimy jakość ją włączyć do naszego serwletu. Metoda najprostsza i jednocześnie nie najlepsza to skopiowanie zawartości oryginalnego serwletu do naszego serwletu Guice. Inaczej się niestety nie da. Zatem po zmianach nasz serwlet będzie wyglądał tak:
Listing 10. Implementacja AbstractApplicationServlet dla Guice z ICEPush
package pl.koziolekweb.vaadin.guice.servlet;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.icepush.servlet.MainServlet;
import org.vaadin.artur.icepush.ICEPush;
import org.vaadin.artur.icepush.JavascriptProvider;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.vaadin.Application;
import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
@SuppressWarnings("serial")
@Singleton
public class GuiceApplicationServlet extends AbstractApplicationServlet {
private final Provider<Application> applicationProvider;
private MainServlet ICEPushServlet;
private JavascriptProvider javascriptProvider;
@Inject
public GuiceApplicationServlet(Provider<Application> applicationProvider) {
this.applicationProvider = applicationProvider;
}
@Override
public void destroy() {
super.destroy();
ICEPushServlet.shutdown();
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
try {
super.init(servletConfig);
} catch (ServletException e) {
if (e.getMessage().equals(
"Application not specified in servlet parameters")) {
// Ignore if application is not specified to allow the same
// servlet to be used for only push in portals
} else {
throw e;
}
}
ICEPushServlet = new MainServlet(servletConfig.getServletContext());
try {
javascriptProvider = new JavascriptProvider(getServletContext().getContextPath());
ICEPush.setCodeJavascriptLocation(javascriptProvider.getCodeLocation());
} catch (IOException e) {
throw new ServletException("Error initializing JavascriptProvider",e);
}
}
@Override
protected Class<? extends Application> getApplicationClass()
throws ClassNotFoundException {
return Application.class;
}
@Override
protected Application getNewApplication(HttpServletRequest request)
throws ServletException {
return applicationProvider.get();
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String pathInfo = request.getPathInfo();
if (pathInfo != null
&& pathInfo.equals("/" + javascriptProvider.getCodeName())) {
// Serve icepush.js
serveIcePushCode(request, response);
return;
}
if (request.getRequestURI().endsWith(".icepush")) {
// Push request
try {
ICEPushServlet.service(request, response);
} catch (Exception e) {
throw new ServletException(e);
}
} else {
// Vaadin request
super.service(request, response);
}
}
private void serveIcePushCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String icepushJavscript = javascriptProvider.getJavaScript();
response.setHeader("Content-Type", "text/javascript");
response.getOutputStream().write(icepushJavscript.getBytes());
}
}
Użycie ICEPush oraz sztuczki i kruczki
Jeżeli koniecznie potrzebujesz używać w kodzie ICEPush, ale nie chesz mieć na karku kompilatora GWT można zrobić mały bypass. Po co? Czasami jest tak, że po zakodowaniu modułu ciężko jest dokonywac jakiś zmian. Łatwo za to jest zmienić konfigurację. Co też i zrobimy.
Na początek przygotujemy sobie mały interfejsik, który posłuży nam jako adapter do usługi comet.
Listing 11. Interfejs CometService
package pl.koziolekweb.vaadin.guice;
import com.vaadin.ui.ComponentContainer;
public interface CometService {
public void addToContainer(ComponentContainer componentContainer);
public void push();
}
O ile znaczenie metody push() jest oczywiste, to metoda addToContainer może być trochę tajemnicza. Otoż by móc używać ICEPush należy dodać go do okna aplikacji. My odwrócimy ten schemat dzięki czemu będziemy mieć kontrolę nad tym co dodajemy do aplikacji. Jest to szczególnie wygodne jeżeli chcemy dodać pseudo usługę, bo wtedy można umieścić na stronie info, że ICEPush jest, ale go nie ma.
Zaimplementujmy zatem nasz mock serwis:
Listing 12. Klasa FakeCometService
package pl.koziolekweb.vaadin.guice;
import com.vaadin.ui.ComponentContainer;
import com.vaadin.ui.Label;
public class FakeCometService implements CometService {
public void addToContainer(ComponentContainer componentContainer) {
componentContainer.addComponent(new Label("Fake CometService in use"));
}
public void push() {
System.out.println("call push");
}
}
Pamiętaj by przenieść konfigurację kompilatora GWT do osobnego profilu mavena. Inaczej i tak będziesz pijał kilka kaw ekstra w oczekiwaniu na zakończenie kompilacji.
Oczywiście przyda się też i właściwa implementacja:
Listing 13. Klasa ICEPushCometService
package pl.koziolekweb.vaadin.guice;
import org.vaadin.artur.icepush.ICEPush;
import com.vaadin.ui.ComponentContainer;
public class IcePushCometService implements CometService {
private ICEPush icePush;
public IcePushCometService() {
icePush = new ICEPush();
}
public void addToContainer(ComponentContainer componentContainer) {
componentContainer.addComponent(icePush);
}
public void push() {
icePush.push();
}
}
Użycie jest bardzo proste. Przykładowa konfiguracja modułu:
Listing 13. MyServletModule
package pl.koziolekweb.vaadin.guice.servlet;
import java.util.HashMap;
import java.util.Map;
import pl.koziolekweb.vaadin.guice.ApplicationRoot;
import pl.koziolekweb.vaadin.guice.CometService;
import pl.koziolekweb.vaadin.guice.IcePushCometService;
import pl.koziolekweb.vaadin.guice.modules.books.model.dao.BookDao;
import pl.koziolekweb.vaadin.guice.modules.books.model.dao.SimpleBookDao;
import pl.koziolekweb.vaadin.guice.modules.books.presenter.ListBookPresenter;
import pl.koziolekweb.vaadin.guice.modules.books.presenter.ListBookPresenterImpl;
import pl.koziolekweb.vaadin.guice.modules.books.view.ListBookView;
import pl.koziolekweb.vaadin.guice.modules.books.view.ListBookViewImpl;
import com.google.inject.servlet.ServletModule;
import com.google.inject.servlet.ServletScopes;
import com.vaadin.Application;
public class MyServletModule extends ServletModule {
@Override
protected void configureServlets() {
Map<String, String> params = new HashMap<String, String>();
params.put("widgetset", "pl.koziolekweb.vaadin.guice.widgetset.MyAppWidgetSet");
serve("/*").with(GuiceApplicationServlet.class, params);
bind(Application.class).to(ApplicationRoot.class).in(
ServletScopes.SESSION);
//bind(CometService.class).to(FakeCometService.class);
bind(CometService.class).to(IcePushCometService.class).in(ServletScopes.SESSION);
bind(ListBookView.class).to(ListBookViewImpl.class);
bind(BookDao.class).to(SimpleBookDao.class);
bind(ListBookPresenter.class).to(ListBookPresenterImpl.class);
}
}
Trzeba tylko dodać parametr z nazwą naszego widgetseta do konfiguracji serwletu. Zakres sesyjny dla rzeczywistego serwisu uchroni nas przed zapchaniem łącza w przypadku kilku aktualizacji występujących na raz.
Rozwinięcia twórcze
Mam kilka pomysłów na twórcze rozwinięcie tego rozwiązania. Przede wszystkim można dodać kolejki by serwis pobierał nowe informacje z kolejki i w przypadku dużego natłoku wywołań metody push nie zaspamował łącza. Po drugie integracja z GWTCanvas i tworzenie wykresów w czasie rzeczywistym. Po trzecie moja wersja JBisona, która będzie śledzić http różnych serwerów.