Tutoriel sur la migration de code JSF 1 / RF 3.3 vers JSF 2 / RF 4.5

Franck Gasparotto

Cet article propose une manière d'automatiser la réécriture de code lors d'une migration JSF 1.2/Richfaces 3.3 vers JSF 2.2/Richfaces 4.5.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum. Commentez .

- Titre : Tutoriel sur la migration de code JSF 1 / RF 3.3 vers JSF 2 / RF 4.5

- Auteur : Franck Gasparotto

- Parution : 25 mars 2015

- Public visé :

- Licence : Copyright ® 2015 Franck Gasparotto. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Pourquoi migrer ?

- III. Les règles de transformation XSL

- IV. Le programme d'exécution

- V. Autres transformations

- VI. Pour finir

- VII. Remerciements

I - Introduction

Il y a quelques mois, nous avons migré nos applications JSF1.2/RichFaces 3.3 vers les dernières versions JSF2.2/RichFaces 4.5. Remplacer les jars par les nouvelles versions et revoir les fichiers de configuration de l'application n'étaient malheureusement pas suffisant, car dans RF4.5, beaucoup de balises XML et d'attributs ont été renommés, voire supprimés.

La migration se résumait donc à réécrire environ 500 fichiers XHTML dans la nouvelle syntaxe… Devant l'ampleur de la tâche, nous avons réfléchi à une solution moins fastidieuse et un peu plus “informatisée”… et nous nous sommes demandé : n'existe-t-il pas déjà une technologie capable de retranscrire un fichier XML d'une syntaxe à une autre ? XSL/XSLT bien sûr !

II - Pourquoi migrer ?

Avant de rentrer dans le vif du sujet, je vais remettre les choses dans leur contexte et expliquer pourquoi le besoin d'une migration s'est imposé.

Depuis 2009, nous travaillons sur des applications JavaEE métiers développées en JSF 1.2. À l'époque, pour créer des pages réactives, dynamiques en AJAX, il fallait utiliser RichFaces qui est une surcouche à JSF et qui propose notamment une bibliothèque de composants stylisés intégrant l'AJAX (pop-up, tableau paginé/filtrable/triable). Entre temps, les navigateurs web ont évolué et leur interpréteur JavaScript aussi, rendant ainsi certains composants AJAX de RF 3 inutilisables ; ce qui est devenu très gênant pour nos extranets d'entreprise. Pour retrouver une compatibilité avec tous les navigateurs, il fallait donc migrer RF 3 vers RF 4, ce qui impliquait de passer de JSF 1.2 à JSF 2.2.

III - Les règles de transformation XSL

Le gros du travail consistait donc à mettre en place des règles de transformation à partir des guides de migration de RF :

- Rich Panel - Output Components ;

- A4j Components ;

- A4j Common Attributes ;

- Rich Iteration Components ;

- Rich Tree Components ;

- Rich Drag and Drop Components.

La syntaxe XSL permet de réaliser des renommages simples comme des transformations plus complexes impliquant des tests et des expressions régulières. Voici quelques exemples :

- la balise modalPanel devient popupPanel et l'attribut showWhenRendered de modalPanel devient show :

1. <xsl:template match="rich:modalPanel" name="rich:modalPanel">

2. <rich:popupPanel modal="true">

3. <xsl:apply-templates select="@*|node()" />

4. </rich:popupPanel>

5. </xsl:template>

7. <xsl:template match="rich:modalPanel/@showWhenRendered">

8. <xsl:attribute name="show"><xsl:value-of select="." /></xsl:attribute>

9. </xsl:template>

- l'attribut ajaxSingle devient execute=”@this” si l'élément courant ne possède pas déjà un attribut execute, sinon il disparaît :

1. <xsl:template match="@process" name="process" >

2. <xsl:attribute name="execute"><xsl:value-of select="." /></xsl:attribute>

3. </xsl:template>

4. <xsl:template match="@ajaxSingle" >

5. <xsl:if test="..[not(@process)]" >

6. <xsl:attribute name="execute"><xsl:text>@this</xsl:text></xsl:attribute>

7. </xsl:if>

8. </xsl:template>

- l'attribut value de graphicImage est transformé en Trois attributs : library, name et alt :

1. <xsl:template match="h:graphicImage" name="h:graphicImage">

2. <h:graphicImage>

3. <xsl:apply-templates select="@*[name()!='value']" />

4. <xsl:attribute name="library">default</xsl:attribute>

5. <xsl:attribute name="name">

6. <xsl:value-of select="replace(./@value, '/(image/.*)\.([pngif]{3})', '$1.$2')" />

7. </xsl:attribute>

8. <xsl:attribute name="alt">

9. <xsl:if test=".[@value]" >

10. <xsl:analyze-string select="./@value" regex="[-\.a-zA-Z_]+$">

11. <xsl:matching-substring>

12. <xsl:value-of select="." />

13. </xsl:matching-substring>

14. </xsl:analyze-string>

15. </xsl:if>

16. </xsl:attribute>

17. <xsl:apply-templates select="node()" />

18. </h:graphicImage>

19. </xsl:template>

- la valeur de l'attribut event ne doit plus être préfixée de “on” : onclick -> click :

1. <xsl:template match="a4j:support/@event">

2. <xsl:attribute name="event">

3. <xsl:value-of select="replace( . , 'on(.*)', '$1')" />

4. </xsl:attribute>

5. </xsl:template>

- la donnée json data est accessible via event.data dans oncomplete et non plus via data :

1. <xsl:template match="*/@oncomplete" name="oncomplete">

2. <xsl:attribute name="oncomplete">

3. <xsl:value-of select="replace(., '(.*)data(.*)', '$1event.data$2')" />

4. </xsl:attribute>

5. </xsl:template>

- la valeur de l'attribut direction de toolTip passe en notation camelCase :

1. <!--  Tooltip bottom-right devient bootomRight -->

2. <xsl:template match="rich:toolTip/@direction" >

3. <xsl:attribute name="direction">

4. <xsl:choose>

5. <xsl:when test="contains(., 'right')">

6. <xsl:value-of select="replace( . , '(.*)-right', '$1Right')" />

7. </xsl:when>

8. <xsl:otherwise>

9. <xsl:value-of select="replace( . , '(.*)-left', '$1Left')" />

10. </xsl:otherwise>

11. </xsl:choose>

12. </xsl:attribute>

13. </xsl:template>

- l'ensemble des règles mises bout à bout permet d'obtenir le résultat suivant par exemple :

avant :

1. <rich:modalPanel id="myPopup" showWhenRendered="true" >

3. <f:facet name="header">

4. <h:outputtext value="Titre de la PopUp" ></h:outputtext>

5. </f:facet>

7. <f:facet name="controls">

8. <h:panelgroup>

9. <h:graphicImage id="clsce" value="/images/close.png" styleclass="pointerCursor close" onclick="closePopup()" />

10. </h:panelgroup>

11. </f:facet>

13. <h:form id="fPopup">

14. <a4j:jsfunction id="js1" name="closePopup"

15. ajaxSingle="true"

16. data="#{popupBean.saveBtnOn}"

17. oncomplete="if(data){ Richfaces.showModalPanel('confirmSave'); }else{ Richfaces.hideModalPanel('myPopup'); }" />

19. <a4j:jsfunction id="js2" name="notifyChange"

20. ajaxSingle="true"

21. reRender="cb1" >

22. <f:setPropertyActionListener value="true" target="#{popupBean.saveBtnOn}" />

23. </a4j:jsfunction>

25. <h:selectOneMenu id="som" value="#{popupBean.personneSelected}">

26. <f:selectItem itemValue="true" itemLabel="Personne" />

27. <f:selectItem itemValue="false" itemLabel="Société" />

28. <a4j:support event="onchange" ajaxSingle="true" reRender="pgName" oncomplete="notifyChange()" />

29. </h:selectOneMenu>

31. <h:panelGrid id="pgName" columns="2">

32. <h:outputLabel for="itNom" value="Nom :" rendered="#{popupBean.personneSelected}" />

33. <h:inputText id="itNom" value="#{popupBean.nom}" onchange="notifyChange()" rendered="#{popupBean.personneSelected}" />

35. <h:outputLabel for="itRsoc" value="Raison Sociale :" rendered="#{!popupBean.personneSelected}" />

36. <h:inputText id="itRsoc" value="#{popupBean.rsoc}" onchange="notifyChange()" rendered="#{!popupBean.personneSelected}" />

37. </h:panelGrid>

39. <a4j:commandButton id="cb1" value="Enregistrer"

40. disabled="#{!popupBean.saveBtnOn}"

41. process="pgName"

42. action="#{popupBean.save}"

43. reRender="cb1" />

44. </h:form>

45. </rich:modalPanel>

après :

<rich:popupPanel id="myPopup" show="true">

<f:facet name="header">
<h:outputtext value="Titre de la PopUp"/>
</f:facet>

<f:facet name="controls">
<h:panelgroup>
<h:graphicImage id="clsce" styleclass="pointerCursor close" onclick="closePopup()" library="default" name="images/close.png" alt="close.png"/>
</h:panelgroup>
</f:facet>

<h:form id="fPopup">
<a4j:jsfunction id="js1" name="closePopup"
execute="@this"
data="#{popupBean.saveBtnOn}"
oncomplete="if(event.data){ #{rich:component('confirmSave')}.show() } else { #{rich:component('myPopup')}.hide(); }"/>

<a4j:jsfunction id="js2" name="notifyChange"
execute="@this"
render="cb1">
<f:setPropertyActionListener value="true" target="#{popupBean.saveBtnOn}"/>
</a4j:jsfunction>

<h:selectOneMenu id="som" value="#{popupBean.personneSelected}">
<f:selectItem itemValue="true" itemLabel="Personne"/>
<f:selectItem itemValue="false" itemLabel="Société"/>
<a4j:ajax event="change" execute="@this" render="pgName" oncomplete="notifyChange()"/>
</h:selectOneMenu>

<h:panelGrid id="pgName" columns="2">
<h:outputLabel for="itNom" value="Nom :" rendered="#{popupBean.personneSelected}"/>
<h:inputText id="itNom" value="#{popupBean.nom}" onchange="notifyChange()" rendered="#{popupBean.personneSelected}"/>

<h:outputLabel for="itRsoc" value="Raison Sociale :" rendered="#{!popupBean.personneSelected}"/>
<h:inputText id="itRsoc" value="#{popupBean.rsoc}" onchange="notifyChange()" rendered="#{!popupBean.personneSelected}"/>
</h:panelGrid>

<a4j:commandButton id="cb1" value="Enregistrer"
disabled="#{!popupBean.saveBtnOn}"
execute="pgName"
action="#{popupBean.save}"
render="cb1"/>
</h:form>
</rich:popupPanel>

Le fichier XSLT complet peut être téléchargé ici.

N.B. Les guides de migration RichFaces recensent ~80 % des règles de transformation, les 20 % restantes ont été ajoutées suite aux tests.

IV - Le programme d'exécution

Pour l'exécution de cette transformation, nous avons simplement créé un projet Java classique sous Eclipse dans le même workspace que le projet à migrer.

Nous utilisons la librairie Saxon comme moteur XSLT afin de bénéficier des fonctions de remplacement de string offert par XSL 2. Du coup, ce jar fait partie du BuildPath du projet.

C'est la classe Main qui fait tout le boulot :

1. package migration;

3. import java.io.File;

4. import java.io.IOException;

5. import java.util.List;

7. import javax.xml.transform.Source;

8. import javax.xml.transform.Transformer;

9. import javax.xml.transform.TransformerException;

10. import javax.xml.transform.TransformerFactory;

11. import javax.xml.transform.stream.StreamResult;

12. import javax.xml.transform.stream.StreamSource;

14. public class Main {

16. private static String PROJECT_PATH = "..\\showcase1";

17. private static String WEB_CONTENT_PATH = PROJECT_PATH + "\\WebContent";

18. private static String WEB_CONTENT_LIB_PATH = WEB_CONTENT_PATH + "\\WEB-INF\\lib";

19. private static String WEB_CONTENT_CSS_PATH = WEB_CONTENT_PATH + "\\css";

21. public static void main(String[] args) throws TransformerException, IOException {

22. System.setProperty("javax.xml.transform.TransformerFactory", "net.sf.saxon.TransformerFactoryImpl");

23. System.out.println("Start");

24. System.out.println(WEB_CONTENT_PATH);

25. /* 1. Màj des jars */

26. System.out.println("Suppression des anciens jars");

27. deleteOldJars();

28. System.out.println("Recopie des nouveaux jars");

29. copyNewJars();

30. System.out.println("DEBUT Traitement de TRANSFORMATION");

31. /* 2. Tranformation css */

32. System.out.println("     DEBUT Fichiers CSS");

33. File[] cssFiles = new File(WEB_CONTENT_CSS_PATH).listFiles();

34. List<StringReplacement> replacements = MigrationUtil.loadReplacementProperties("css_replace.properties");

35. MigrationUtil.executeReplacementsOnFiles(cssFiles, "css", replacements);

36. System.out.println("     FIN Fichiers CSS");

37. /* 3. Tranformation xhtml */

38. System.out.println("     DEBUT Fichiers XHTML");

39. File[] files = new File(WEB_CONTENT_PATH).listFiles();

40. TransformerFactory factory = TransformerFactory.newInstance();

41. Source xslt = new StreamSource(new File("RichFaces3_4.xslt"));

42. Transformer transformer = factory.newTransformer(xslt);

43. xsltTransform(files, transformer);

44. System.out.println("     FIN Fichiers XHTML");

45. System.out.println("FIN Traitement de TRANSFORMATION");

46. }

48. public static void deleteOldJars() {

49. String[] jars = new String[] {

50. "jsf-api.jar",

51. "jsf-facelets.jar",

52. "jsf-impl.jar",

53. "jsf_core.tld",

54. "jstl.jar",

55. "jta.jar",

56. "myfaces-converters11-1.0.0.jar",

57. "myfaces-orchestra-core-1.4.jar",

58. "richfaces-api-3.3.3.Final.jar",

59. "richfaces-impl-3.3.3.Final.jar",

60. "richfaces-ui-3.3.3.Final.jar",

61. "spring.jar",

62. "slf4j-api-1.5.8.jar",

63. "slf4j-log4j12-1.5.0.jar",

64. "javassist.jar",

65. };

66. FileUtil.deleteFilesFromFolder(WEB_CONTENT_LIB_PATH, jars);

67. }

69. public static void copyNewJars() {

70. FileUtil.copyFiles("jars_jsf2", WEB_CONTENT_LIB_PATH);

71. }

73. public static void xsltTransform(File[] files, Transformer transformer) throws IOException, TransformerException {

74. for (File file : files) {

75. if (file.isDirectory()) {

76. xsltTransform(file.listFiles(), transformer); // Recursive.

77. }

78. else {

80. String filename = file.getName();

81. String ext_file = filename.substring(filename.lastIndexOf(".") + 1, filename.length());

82. String ext = "xhtml";

83. String oldFileName = file.getParent() + "\\old_" + filename;

84. File oldName = new File(oldFileName);

85. if (ext.equals(ext_file) && !filename.startsWith("old")) {

86. System.out.println("File: " + file.getAbsolutePath());

87. if (!oldName.exists()) {

88. FileUtil.copyFileUsingFileStreams(file, oldName);

89. System.out.println("Copie réussie");

90. //                        FileUtil.deleteLines(oldFileName, 1, 1);

91. }

92. Source text = new StreamSource(oldName);

93. System.out.println("Debut transformation XSLT");

94. StreamResult result = new StreamResult(file);

95. transformer.transform(text, result);

96. System.out.println("Fin transformation");

97. }

98. }

99. }

100. }

101. }

Les constantes permettent d'indiquer au programme quel est le projet à migrer, et à l'intérieur de ce projet, où se trouvent les libs, les CSS et les fichiers XHTML à transformer.

- Suppression des anciens Jars et recopie des nouveaux (oui, nous n'utilisons pas Maven, cela fera peut-être l'objet d'un futur post : comment réussir sa migration vers Maven).

- Transformation des CSS.

- Transformation des fichiers XHTML.
Le script parcourt tous les dossiers à partir de WebContent,
recopie l'original sous un nom préfixé de « old »,
applique la transformation à partir de « old » et réécrit le fichier.
Cette technique permet de modifier le fichier XSL et de relancer autant de fois qu'on veut le script de transformation.

V - Autres transformations

L'étape 2 de l'algorithme consiste à réécrire les classes CSS de RichFaces que nous avions surchargées.

À partir d'un fichier properties recensant les transformations, nous avons utilisé la fonction Java replaceAll sur toutes les lignes de chaque fichier CSS.

En plus de tous ces automatismes, nous avons également utilisé le bon vieux CTRL+H d'Eclipse afin d'effectuer des remplacements en masse.

- Richfaces.showModalPanel et Richfaces.hideModalPanel n'existent plus.

- Rechercher : Richfaces.(hide|show)ModalPanel(‘([a-zA-Z0-9#{}]*)').

- Remplacer par : #{rich:component(‘$2')}.$1().

- Rechercher : Richfaces.(hide|show)ModalPanel(‘([a-zA-Z0-9#{}]*)').

- Remplacer par : #{rich:component(‘$2')}.$1().

Voilà comment, grâce à un programme JAVA et XSL/XSLT, nous avons pu automatiser la migration de nos projets Richfaces.

Bien sûr, une fois les transformations effectuées, il faut reconfigurer les fichiers XML pour être en adéquation avec JSF 2 :

- web.xml ;

- faces-config.xml.

VI - Pour finir

Enfin, il vous faudra tester l'application ! Eh oui, cette méthode de migration n'est pas magique, mais elle permet au moins de s'abstraire des tâches répétitives pour se concentrer sur la résolution de problèmes spécifiques au passage JSF 1.2 > JSF 2.2. Si vous êtes novice en JSF2 et RF4, vous aurez sûrement quelques surprises en fonction de l'implémentation de JSF utilisée :

- la portée des ui:param dans les facelets est différente ;

- f:setPropertyActionListener n'est plus autorisée dans les a4j:support ;

- application des styles columnClasses dans les « dataTable » dont les colonnes sont cachées/affichées ;

- surcharges des styles des composants Richfaces qui ne fonctionnent plus.

En cas de problème, le forum de Richfaces est d'une grande aide d'ailleurs. Par contre, ils seront plus enclins à répondre avec un bout de code minimal, testé avec la dernière release…

VII - Remerciements

Cet article a été publié avec l'aimable autorisation de la société keeleek et l'article d'origine (migration-jsf1-rf3-vers-jsf2-rf4-gagner-du-temps-reecriture-automatisation/) peut être vu sur le blog/site de Keeleek.

Nous tenons à remercier Mickael Baron pour la mise au gabarit et Malik Seck pour la relecture orthographique.