Accueil > Non classé > Intégrer GWT dans Liferay : retour d’expérience

Intégrer GWT dans Liferay : retour d’expérience

Logo Liferay largeAu cour d’un projet, il m’a été demandé d’étudier la possibilité d’intégration d’une application GWT dans une portlet Liferay. Je vais vous présenter dans cet article la solution retenue.

 

Une pointe de GWT dans un monde de JSP

Pour rappel, une portlet comporte un ensemble de vues associées à des états. Pour afficher une application GWT dans une portlet, il suffit de l’insérer dans les vues en question sous forme de widget. Dans notre exemple, nous travaillerons sur une vue implémentée en JSP.
Dans une JSP d’une application web ordinaire, nous aurions écrit ceci :

1
2
<script language="javascript" src="myUI/myUI.nocache.js"></script>
<div id="maDivAncrage"></div>

et le code java de notre entryPoint inclurait la ligne de code suivante :

1
RootPanel.get("maDivAncrage").add(monWidget);

Cependant, nous préférerons laisser à Liferay le soin d’importer les fichiers javascript et css dans la page qui abrite la portlet. Pour cela, nous déclarons l’import dans le fichier liferay-portlet.xml :

1
2
3
4
5
6
7
8
9
10
11
<liferay-portlet-app>
    <portlet>
        <portlet-name>myOtherPortlet</portlet-name>
        <icon>/icon.png</icon>
        <instanceable>false</instanceable>
        <header-portlet-css>/css/myCss.css</header-portlet-css>
        <footer-portlet-javascript>/myUI/myUI.nocache.js</footer-portlet-javascript>
        <css-class-wrapper>myOtherPortlet-portlet</css-class-wrapper>
    </portlet>
...
</liferay-portlet-app>

Le fichier JSP de notre vue ne contiendrait plus que la div.

 

Communiquer avec le back (in black) par ACD… non, je veux dire RPC

Notre application GWT est désormais affichable dans une portlet. Nous devons maintenant lui faire afficher des données provenant du back-end et notamment des données associées à l’utilisateur actuellement connecté (donc dépendantes de la session HTTP en cours). Nous avons deux solutions possibles pour permettre au front-end de récupérer ces données :

  • La première solution consiste à inscrire ces données dans la JSP en utilisant des scriptlets ou les taglibs de liferay (liferay-theme) pour ensuite les récupérer par JSNI.
  • La seconde solution est la mise en place d’un service RPC entre le front et le back.

Je ne détaillerais pas plus la première solution qui peut se révéler contraignante si on essaie de transporter des objets complexes et qui risque de faire intervenir du code métier dans la couche de présentation (notamment à cause des potentiels scriptlets).

Nous allons donc mettre en place un service RPC. Cette solution est plus simple, plus standard et par conséquent plus facilement maintenable.
Cependant, un problème va se poser au moment de récupérer la session HTTP. Notre implémentation de service est définie dans une servlet (RemoteServiceServlet) qui vit sa vie indépendamment du portail et n’a donc pas accès à une éventuelle HttpSession.

Pour répondre à ce problème, Liferay nous met à disposition la classe PortalDelegateServlet qui permet de partager (ou déléguer) les sessions du portail avec celles d’une autre servlet (en l’occurrence l’implémentation de notre service RPC). Pour cela, nous devons déclarer la servlet déléguée dans le fichier web.xml de notre portlet de la façon suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
    <init-param>
        <param-name>servlet-class</param-name>
        <param-value>com.excilys.liferay.server.MyServiceImpl</param-value>
    </init-param>
    <init-param>
        <param-name>sub-context</param-name>
        <param-value>mySubServlet</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Ensuite, nous redéfinissons dans la partie cliente de notre service l’url où contacter l’implémentation :

1
2
3
4
5
6
7
8
public class MyUI implements EntryPoint {

    private MyServiceAsync service = GWT.create(MyService.class);

    @Override
    public void onModuleLoad() {
        ((ServiceDefTarget) service).setServiceEntryPoint("/delegate/mySubServlet");
        ...

Enfin, dans l’implémentation de notre service RPC, il est possible d’accéder à la requête HTTP qui a permis l’affichage de la portlet grâce à la méthode getThreadLocalRequest de l’api GWT-RPC. Nous pouvons alors récupérer l’utilisateur actuellement connecté en utilisant la classe PortalUtil de l’api de Liferay.
Notre implémentation ressemble à ceci :

1
2
3
4
5
6
7
8
9
public class MyServiceImpl extends RemoteServiceServlet implements MyService {

    @Override
    public String getCurrentUser() {
        String userName = null;
        try {
            User user = PortalUtil.getUser(getThreadLocalRequest());
            userName = user.getFullName();
            ...

 

 

Serializable or IsSerialisable, that is the question


Tant que le service RPC transmet des types simples, la solution décrite plus haut ne pose aucun problème. Malheureusement, si votre service envoie des objets (DTO) plus complexes, c’est une autre histoire.
Le problème est que notre passage par le PortalDelegateServlet fausse le contextPath des requêtes et empêche GWT de charger le fichier définissant la politique de sérialisation. En conséquence, GWT utilise la politique par défaut qui consiste à ne supporter que les classes implémentant IsSerializable.
Pour rappel, indépendamment de l’intégration dans Liferay, un objet DTO transmis par RPC doit implémenter les interfaces IsSerializable de l’API GWT ou Serialisable de java (depuis la version 1.4 de GWT) si la politique de sérialisation est correctement chargée.

Tout ceci ne nous laisse que deux options:

  • Se résigner à utiliser l’interface IsSerializable qui reste spécifique à GWT.
  • Forcer le chargement de la politique de sérialisation en travaillant sur le contextPath manipulé par le service RPC.

Dans notre cas et malgré les recommandations de Google, nous avons préféré implémenter java.io.Serializable (plus standard). Ainsi, pour contourner notre problème, j’ai réimplémenté la méthode doGetSerializationPolicy du service RPC :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MyServiceImpl extends RemoteServiceServlet implements MyService {        
    ...
    @Override
    protected SerializationPolicy doGetSerializationPolicy(
        HttpServletRequest request, String moduleBaseURL, String strongName) {
       
        String contextPath = request.getContextPath();

        String modulePath = null;
        if (moduleBaseURL != null) {
            try {
                modulePath = new URL(moduleBaseURL).getPath();
            } catch (MalformedURLException ex) {
                // log the information, we will default
                this.log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
            }
        }
   
        /*
         * Check that the module path must be in the same web app as the servlet
         * itself. If you need to implement a scheme different than this, override
         * this method.
         */

        if (modulePath == null) {
            String message = "ERROR: The module path requested, "
                + modulePath
                + ", is not in the same web application as this servlet, "
                + contextPath
                + ".  Your module may not be properly configured or your client and server code maybe out of date.";
            this.log(message);
        } else {
            // Strip off the context path from the module base URL. It should be a strict prefix.
            String[] urlParts = modulePath.split("/");
            String portletPath = "";
            if(urlParts.length > 2){
                portletPath = urlParts[1];
            }
         
            // we rebuild the moduleBaseURL because of differences of contextPath due to liferay's DelegatePortalServlet.
            StringBuilder builder = new StringBuilder(moduleBaseURL);
            int hostURLLength = moduleBaseURL.length()-modulePath.length() + contextPath.length();
            builder.delete(hostURLLength, hostURLLength + portletPath.length() + 1 );
         
            moduleBaseURL = builder.toString();
        }
        return super.doGetSerializationPolicy(request, moduleBaseURL, strongName);
    }
}

 

Le fin mot de l’histoire

Intégrer GWT dans une portlet Liferay s’effectue aussi simplement que dans une page JSP. Il faut cependant tenir compte du partage de session du portail, du chargement des fichiers javascripts et css et de l’éventuel accès au contextPath. Notez que la portlet dans laquelle nous avons travaillé n’est pas instanciable et que les solutions énoncées ne seraient pas suffisantes pour faire fonctionner plusieurs instances d’une même portlet GWT dans une même page (notre div d’ancrage étant chargée 2 fois avec le même identifiant). Il existe des solutions pour remédier à cela mais je ne les ai pas mises en pratique.

Share
  1. 23/08/2011 à 15:19 | #1

    Merci pour ce retour d’expérience !

    Par ailleurs, pour préciser les choses : la “serialisation policy” est juste une liste des classes autorisées.

    Pourquoi cette liste ? Pour éviter toute tentative de hack. En effet, avec IsSerialisable, vu que c’est spécifique à GWT, on est certain que les dev veulent que cette classe puisse transiter sur le réseau. Par contre, java.io.Serializable peut être implémentée par des classes qui ne devraient pas être transmises.

    C’est pour ça qu’il faut impérativement une whitelist.

  2. 23/08/2011 à 17:22 | #2

    Wow, bon article et très complet.
    J’ai bossé sur une maquette fin 2009 avec Liferay et Ext-GWT (la version qui est devenue Sencha). Et bien c’était bien compliqué pour un résultat très moyen. Au final on a abandonné la partie Liferay, et le projet s’est transformé en un projet web plus simple, avec non pas des Portlets Java mais plus simplement, des composants JS.
    Nicolas

  3. 23/08/2011 à 17:24 | #3
  1. 12/05/2012 à 17:00 | #1