Sortie fin Octobre, la dernière update de Java 6 offre une multitude de nouveautés orientées utilisateur final qui vous seront présentées dans le Programmez ! 114 du mois de Décembre. Parmi ces nouveautés, une amélioration des performances des applets et une uniformisation du mode déploiement pour les applets et les applications Java Web Start (JWS) à l'aide de fichiers JNLP (Java Network Launching Protocol). Ce dernier point a son importance puisqu'il rend accessible aux applets l'ensemble des API JNLP qui permettent une interaction assez poussée avec le système de l'utilisateur final.
Ce tutoriel a pour objectif de vous présenter de manière plus poussée l'intégration des applets au desktop avec Java 6 Update 10 au travers d'un exemple simple d'applet permettant de lire et d'écrire un fichier situé sur le poste client.
L'applet que nous allons développer aura 2 fonctionnalités principales :
Se voulant graphiquement simple, elle présentera 2 boutons permettant respectivement d'ouvrir un fichier et de sauvegarder un fichier sur le poste client. Le contenu du fichier ouvert étant présenté dans une zone de texte. Cette même zone servant également de référence pour les données qui seront écrites dans le fichier sauvegardé. Enfin, un champ texte non éditable permettra de visualiser le nom du fichier ouvert.
Avant d'attaquer le développement de notre application, attardons nous quelque peu sur sa conception. Bien que notre application soit de taille réduite, il est toujours intéressant de réfléchir en amont du développement au découpage de l'application en terme de classes notamment.
La classe principale de l'application sera la classe Sample et héritera de la classe JApplet du package standard puisque notre application est de type applet. La construction de notre application sera donc lancée depuis la méthode init héritée de JApplet. Afin de pouvoir bénéficier des nouvelles fonctionnalités de drag and drop des applets apportées par l'update 10, nous définirons les méthodes appletRestored, isAppletDragStart et setAppletCloseListener. Encore expérimentales, ces méthodes n'ont pas encore été inclues dans une interface spécifique mais sont d'ores et déjà utilisables. Enfin, le panel principal de l'applet sera défini via la classe ContentPanel.
Cette dernière classe hérite logiquement de la classe JPanel de l'API Java. Elle définit l'ensemble des composants graphiques de l'applet à savoir les 2 boutons d'ouverture et de sauvegarde ainsi que la zone d'affichage textuelle et le champ texte permettant d'afficher le nom du fichier ouvert en lecture. Les 2 actions déclenchées depuis ces 2 boutons seront des implémentations de l'interface ActionListener et seront définies dans les classes OpenFileAction et SaveFileAction. Nécessitant un accès au contenu de la zone textuelle du ContentPanel, elles en auront une instance en tant qu'attribut. En outre, la classe OpenFileAction possèdera une constante entière BUFFER_SIZE qui permettra de positionner la taille du buffer de lecture pour les données du fichier ouvert.
Le diagramme de classes de notre application est présenté à la figure ci-dessous
Afin de réaliser cette applet nous utiliserons l'IDE Eclipse. La mise en place du projet commence par la création d'un projet AppletSample et la définition du package principal de notre application qui est le suivant : fr.test . La figure 2 vient présenter une vision globale de la structure du projet depuis la vue Package Explorer d'Eclipse.
L'application est découpée en 3 packages principaux :
La classe principale de l'applet étant la classe Sample qui se trouve à la racine du package principal. Côté librairies, il vous faut paramétrer votre projet pour faire référence au JRE 6 Update 10 que vous pouvez télécharger à l'adresse suivante http://java.com/fr/download/ pour les retardataires qui n'auraient pas encore fait la mise à jour ;).
L'illustration ci-dessous montre la vue globale de la structure du projet
Afin de pouvoir accéder aux classes de l'API JNLP, il vous faut récupérer deploy.jar et javaws.jar qui se trouvent dans votre répertoire < JAVA_HOME >/jre/lib/. Ces JAR sont nécessaires pour la compilation du projet mais ne seront plus utiles lors du déploiement de l'application. Dans notre projet AppletSample, ils ont été placés dans le répertoire libs.
La classe ContentPanel constitue le panel principal de notre applet. Il s'agit d'une classe assez classique de définition d'IHM à l'aide des composants de l'API Swing. Sa principale difficulté réside dans la gestion du bouton de fermeture de l'applet. Afin de pouvoir interagir avec la fonctionnalité de fermeture de l'applet proposée dans l'update 10 de Java 6, on va dessiner une barre de titre sur notre panel. Cette barre de titre servira à afficher le titre de notre applet mais également à présenter à l'utilisateur une croix rouge utilisée pour restaurer l'applet au sein du navigateur. Ainsi, elle ne sera active que lorsque l'utilisateur aura détaché l'applet du navigateur.
Son code étant assez long et pas forcément très complexe, seul le contenu du constructeur et de la méthode permettant le dessin de la barre de titre et de l'icône de fermeture sont présentés ici :
/**
* Constructeur
*
* @param parent Référence à l'applet
*/
public ContentPanel(final Sample parent) {
super();
setBackground(Color.WHITE);
// Ajout d'un MouseListener sur le panel pour écouter le clic sur l'icône de fermeture
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if(parent != null){
ActionListener closeListener = parent.getCloseListener();
if(closeListener != null) {
if(SwingUtilities.isLeftMouseButton(e) && imageBounds != null && imageBounds.contains(e.getPoint())) {
// Demande fermeture sur le closeListener de l'applet
closeListener.actionPerformed(null);
}
}
}
}
});
// NB : Pour faire des tests en local, on doit initialiser le ServiceManager
// ServiceManager.setServiceManagerStub(new JnlpLookupStub());
// initialisation du panel
initialize();
}
@Override
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
// on récupère la référence à graphics2D
Graphics2D g = (Graphics2D) graphics;
int width = getWidth();
g.setColor(Color.BLUE);
g.fillRect(0, 0, width, TITLE_BAR_HEIGHT);
g.setColor(Color.WHITE);
g.drawString(title, 5, (TITLE_BAR_HEIGHT - 5));
// on dessine l'icône de fermeture
if(closeImage != null){
int imageWidth = closeImage.getIconWidth() + 1;
int imageHeight = closeImage.getIconHeight();
imageBounds = new Rectangle(new Point(getWidth() - imageWidth, 0), new Dimension(imageWidth, Math.max(TITLE_BAR_HEIGHT, imageHeight)));
g.drawImage(closeImage.getImage(), (int)imageBounds.getX(), (int)imageBounds.getY(),
(int)imageBounds.getWidth(), (int)imageBounds.getHeight(), null);
}
}
Le dessin de la barre de titre s'effectue au sein de la méthode paintComponent. Cette dernière permet également l'affichage de l'icône de fermeture de notre applet à l'extrême droite de la barre de titre. Quant au constructeur, il ajoute un MouseListener sur le ContentPanel qui servira à récupérer le click utilisateur et a vérifier s'il est effectué sur l'icône de fermeture. Auquel cas, on exécute la méthode actionPerformed sur le closeListener de la classe Sample de notre projet qui permet de demander la restauration de l'applet au sein du navigateur.
La classe Sample constitue le point d'entrée de notre applet. Ainsi, elle redéfinit la méthode init héritée de la classe JApplet dont elle dérive. Son rôle principal consiste à instancier et à positionner le ContentPanel. En outre, elle permet une interaction avec les méthodes expérimentales introduites par l'update 10. Ces dernières ont pour objectif la gestion du drag and drop de l'applet et sa restauration éventuelle au sein du navigateur le cas échéant. Son code est présenté ci-dessous :
public class Sample extends JApplet{
/**
*
*/
private static final long serialVersionUID = 1L;
/** Panel principal de l'applet */
private ContentPanel contentPanel;
/** Listener de fermeture de l'applet */
private ActionListener closeListener;
@Override
public void init(){
add(getContentPanel());
}
/**
* Accesseur au panel principal
*
* @return
*/
private ContentPanel getContentPanel(){
if(contentPanel == null){
contentPanel = new ContentPanel(this);
}
return contentPanel;
}
/**
* Méthode expérimentale appelée lorsqu'un évènement
* MouseEvent se produit sur l'applet
*
* @param e L'évènement MouseEvent envoyé par l'applet
* @return true si l'applet peut être déplacée, false sinon
*/
public boolean isAppletDragStart(MouseEvent e){
return SwingUtilities.isMiddleMouseButton(e);
}
/**
* Méthode expérimentale permettant de récupérer
* l'ActionListener de fermeture de l'applet
* NB : La mise en place n'est effectuée que lorsque
* l'applet est détachée du navigateur
*
* @param l L'ActionListener de fermeture
*/
public void setAppletCloseListener(ActionListener l) {
closeListener = l;
}
/**
* Méthode expérimentale appelée lorsque l'applet
* est rattachée au navigateur
*
*/
public void appletRestored() {
closeListener = null;
}
/**
* Accesseur au listener de fermeture de l'applet
*
* @return Listener de fermeture de l'applet
*/
public ActionListener getCloseListener(){
return closeListener;
}
}
La méthode isAppletDragStart est appelée lorsqu'un événement souris est enregistré sur l'applet. Par défaut, le drag and drop est réalisable avec la combinaison ALT + click gauche souris. Ici, on précise qu'il est également réalisable en cliquant sur le bouton central de la souris. En outre, la méthode setAppletCloseListener est appelée lorsque l'applet est détachée du navigateur et permet de récupérer le CloseListener associé à l'applet. L'emploi de la méthode actionPerformed de ce dernier permettant alors de restaurer l'applet au sein du navigateur. Ceci entraînant par la suite l'appel à la méthode appletRestored qui nous avertit de cette restauration.
Afin de pouvoir déployer notre applet, il est nécessaire de créer une archive JAR de notre projet. L'export du projet au format JAR se réalise à l'aide d'Eclipse en sélectionnant l'entrée Export du menu contextuel obtenu après un click droit sur notre projet AppletSample. La fenêtre de sélection présentée à la figure 3 s'affiche alors :
Il vous faut sélectionner l'entrée JAR file de la branche Java et ensuite cliquer sur Next. La fenêtre de la figure 4 se présente alors à vous et vous permet de configurer votre archive JAR.
Nous choisissons d'inclure les sources du projet dans le JAR mais nous enlevons les librairies et donc le dossier libs puisque ces dernières sont seulement utiles à la compilation et non à l'exécution de notre applet. Enfin les fichiers de configurations spécifiques à Eclipse (.classpath et .project) ne sont pas inclus dans le JAR. Il vous reste ensuite à choisir la destination du JAR généré et vous pouvez cliquer sur le bouton Finish.
Ceci étant fait vous disposez d'une archive JAR correspondant au contenu de votre projet dans le répertoire de destination que vous avez choisi.
Les applications Java de type client lourd sont considérées comme étant sûres du point de vue de la JVM (Java Virtual Machine) qui les exécute. De ce fait, elles bénéficient d'un accès total aux possibilités offertes par le langage et plus particulièrement en terme d'interactions avec le poste client. A contrario, les applets sont déployées sur le poste client via un client léger et sont donc considérées comme étant potentiellement dangereuses. Afin de garantir la sécurité du poste client, elles sont lancées par la JVM dans un environnement d'exécution restreint. Les restrictions imposées par cet environnement pouvant être définies dans les fichiers suivants :
java.policy pour les applets deployées de manière traditionnelle
javaws.policy pour les applications JWS et les applets déployées à l'aide de fichiers JNLP
Ces 2 fichiers se trouvant dans le répertoire < JAVA_HOME >/jre/lib/security/. Par défaut, il donne toutes les permissions nécessaires à notre applet pour sa bonne exécution. Néanmoins, afin de bénéficier de ces droits à l'exécution notre applet doit être signée sans quoi l'accès aux services JNLP ne sera pas activé.
La signature de l'applet consiste à signer le ou les JAR qui la composent. Dans notre cas, il nous suffira de signer AppletSample.jar que nous avons produit dans la partie 7/ de ce tutoriel. Le déploiement d'un JAR signé sur le poste client va garantir à la JVM que le code envoyé par le serveur est bien celui demandé par le client. Fort heureusement, Sun nous fournit les outils nécessaires à la signature numérique des archives JAR. Contenus dans le dossier /jre/bin/, il s'agit des exécutables keytool et jarsigner. Le premier nommé permet de créer une clé privée qui sera utilisée par le second pour signer notre archive JAR.
La première étape consiste à exécuter la ligne de commande suivante dans le répertoire où se trouve le JAR de notre applet :
keytool -genkey -alias signFiles -keypass programmez -keystore mystore -storepass programmez
Exécutez cette commande et répondez aux questions vous permettant de définir la personne propriétaire de la clé qui sera générée. Une fois que vous avez répondu à ces différentes questions, exécutez la commande suivante toujours dans le même répertoire :
jarsigner -keystore mystore -signedjar sAppletSample.jar AppletSample.jar signFiles
L'exécution de cette commande va permettre la génération de l'archive jar signée sAppletSample.jar que nous allons utiliser par la suite pour le déploiement de notre projet.
Le fichier JNLP permettant le déploiement de notre applet est conçu de la même manière qu'un fichier dédié à une application JWS. La différence notable concerne la balise applet-desc que nous rajoutons au sein de notre fichier pour préciser qu'il s'agit bien du déploiement d'une applet. Le code du fichier JNLP AppletSample.jnlp est présenté ci-dessous :
On commence par définir les informations obligatoires que sont le nom de l'application et le nom du vendeur de cette dernière. On précise que l'application peut être utilisée en mode déconnecté et qu'il est possible de créer un raccourci sur le bureau de notre applet lorsque celle-ci sera déconnectée du navigateur qui aura servi à son déploiement. Notre applet nécessite l'ensemble des permissions de sécurité pour son exécution ce qui est précisé via la balise all-permissions. La partie ressources permet de spécifier que l'applet nécessite une version 6 update 10 de Java sur le poste client et l'archive jar contenant notre applet. Il est important de noter ici que l'on pointe sur l'archive JAR signée sAppletSample.jar que nous avons crée à la partie 8/ de ce tutoriel. En outre, la balise applet-desc permet de définir la classe principale de notre applet via son attribut main-class. Dans notre cas, il s'agit de la classe fr.test.Sample. Enfin, il est important de noter qu'il n'est pas utile de renseigner l'attribut codebase de la balise jnlp comme cela est le cas pour les applications JWS utilisant l'ancien plug-in Java. Désormais, cette information est automatiquement renseignée par le nouveau plug-in Java grâce aux informations présentes dans la page HTML de déploiement.
< ?xml version="1.0" encoding="UTF-8"? >
< jnlp href="AppletSample.jnlp" >
< information >
< title >Applet Sample< /title >
< vendor >Programmez ;)< /vendor >
< offline-allowed / >
< /information >
< shortcut online="false" >
< desktop />
< /shortcut >
< all-permissions/ >
< /security >
< resources >
< j2se version="1.6.0_10"
href="http://java.sun.com/products/autodl/j2se" / >
< !-- Ajout d'extensions éventuelles (vers des Jar ou d'autres JNLP) -- >
< jar href="sAppletSample.jar" / >
< /resources >
< applet-desc name="AppletSample" main-class="fr.test.Sample" width="300" height="300" >
< /applet-desc >
< /jnlp >
Notre fichier JNLP défini, il nous reste une dernière étape dans la mise en place du déploiement de notre applet : il s'agit de la création de la page HTML qui va appeler l'applet. Là encore, nous allons utiliser les nouvelles possibilités offertes par Java 6 Update 10 puisque nous n'allons pas avoir recours à la balise applet traditionnellement utilisée. En lieu et place de cette dernière, nous utiliserons les fonctionnalités offertes par l'objet Javascript deployJava fourni dans cette dernière update.
Cet objet vient faciliter encore un peu plus le déploiement d'applets puisqu'il est désormais possible de demander le lancement d'une applet avec une seule ligne de code ! Pour ce faire, il faut utiliser sa méthode runApplet qui à partir des informations fournies en entrée va se charger de la création de la balise applet configurée comme il faut pour que le chargement de l'applet puisse être réalisé avec succès.
Le code Javascript ci-dessous est à inclure dans une page HTML AppletSample.htm et permet le déploiement de notre applet :
Le lecteur attentif aura sans doute remarqué que certaines informations nécessaires à la configuration de l'applet sont définies à la fois dans la page HTML mais également dans le fichier JNLP présenté à la partie 9/. En particulier, les informations concernant la classe principale de l'applet, sa largeur et sa hauteur.
La documentation du nouveau plug-in Java précise que les informations de hauteur et de largeur utilisées sont celles contenues dans le code HTML. En effet, il est primordial que cette information soit disponible pour le navigateur en charge d'afficher l'applet. Ainsi, les valeurs des balises width et height obligatoires dans la balise applet-desc du fichier JNLP ne sont pas utilisées. De plus, l'information utilisée pour la classe principale de l'application est celle se trouvant dans le fichier JNLP.
Enfin, on notera l'importance de la définition de la valeur de codebase dans la page HTML puisque c'est cette dernière qui sera passée au fichier JNLP et qui permettra le chargement correct de l'applet.
< script src=http://java.com/js/deployJava.js > < /script >
< script >
var attributes = {codebase:'file:///I:/articles/tutoriel_j6u10/deploy/',
code:'fr.test.Sample',
archive:'sAppletSample.jar',
width:300,
height:300} ;
var parameters = {draggable:'true', title:'Programmez !'} ;
var version = '1.6.0_10' ;
deployJava.runApplet(attributes, parameters, version);
< /script >
Le code Javascript présenté à la partie précédente est annoncé comme utilisable dans la documentation de Sun. Cependant, les tests que votre serviteur a pu faire avec ce code n'ont pas été concluants puisqu'il n'a pas été possible d'accéder aux services JNLP depuis l'applet ! L'erreur obtenue en traçant le lancement de l'applet étant la suivante :
java.lang.Exception: JNLPClassLoaderUtil: couldn't find a valid JNLPClassLoaderIf
at com.sun.jnlp.JNLPClassLoaderUtil.getInstance(Unknown Source)
at com.sun.jnlp.JnlpLookupStub.findService(Unknown Source)
at com.sun.jnlp.JnlpLookupStub.access$000(Unknown Source)
at com.sun.jnlp.JnlpLookupStub$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.jnlp.JnlpLookupStub.lookup(Unknown Source)
at javax.jnlp.ServiceManager.lookup(Unknown Source)
at fr.test.actions.OpenFileAction.actionPerformed(OpenFileAction.java:46)
at javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
at javax.swing.DefaultButtonModel.setPressed(Unknown Source)
at javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)
at java.awt.Component.processMouseEvent(Unknown Source)
at javax.swing.JComponent.processMouseEvent(Unknown Source)
at java.awt.Component.processEvent(Unknown Source)
at java.awt.Container.processEvent(Unknown Source)
at java.awt.Component.dispatchEventImpl(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)
at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)
at java.awt.Container.dispatchEventImpl(Unknown Source)
at java.awt.Component.dispatchEvent(Unknown Source)
at java.awt.EventQueue.dispatchEvent(Unknown Source)
at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
at java.awt.EventDispatchThread.run(Unknown Source)
L'erreur indique clairement que JWS n'est pas arrivé à charger les services JNLP. Une recherche rapide sur le site de Sun où son listés les bugs de Java 6 Update 10 me laisse à penser qu'il pourrait s'agir d'un bug identifié mais pas encore traité. A suivre donc ...
Afin de pouvoir lancer notre applet depuis une page HTML, nous reviendrons donc à la méthode traditionnelle de la balise applet qui est présentée dans le code suivant :
< html >
< head >
< title >Applet Sample< /title >
< /head >
< applet width="300" height="300" codebase=file:///I:/articles/tutoriel_j6u10/deploy/ >
< param name="jnlp_href" value="AppletSample.jnlp"/ >
< param name="draggable" value="true" / >
< param name="title" value="Programmez !"/ >
< /applet >
< /body >
< /html >
Ce code va permettre le chargement de l'applet via le fichier JNLP AppletSample.jnlp. Il ne permet cependant pas de préciser quelle version de Java est nécessaire à son exécution. De ce fait, la vérification se fera au lancement de JWS.
Maintenant que tout a été mis en place pour le déploiement de notre applet, il est temps de passer aux tests de déploiement pour vérifier qu'elle fonctionne convenablement. Si vous avez bien suivi le tutoriel jusqu'ici, vous devriez avoir un répertoire avec le contenu suivant :
Le fichier AppletSample_new.htm correspondant à la page HTML utilisant le code Javascript qui semble poser problème comme cela est expliqué à la partie 11/.
Il ne vous reste plus qu'à lancer AppletSample.htm dans votre navigateur favori pour obtenir l'écran suivant :
Cette fenêtre avertit l'utilisateur que l'applet est signée et lui demande s'il souhaite l'exécuter sur son système. En cliquant sur Exécuter, vous obtenez le chargement et l'affichage de notre applet :
Une fois l'applet affichée dans le navigateur il est possible de la détacher de ce dernier en cliquant dessus avec le bouton gauche de la souris et en faisant une action de drag and drop. Cela nous amène à l'affichage suivant:
Une fois l'applet détachée du navigateur, il est possible de la restaurer au sein de ce dernier en cliquant sur l'icône de fermeture en haut à droite de l'applet. Notre applet chargée, il est temps de tester ses fonctionnalités. Commençons tout d'abord par ouvrir un fichier en cliquant sur le bouton Open. On obtient l'affichage suivant :
La fenêtre modale qui s'ouvre permet de sélectionner un fichier présent sur le système du poste client. Ici, on choisit d'afficher le contenu de la page AppletSample.htm ce qui nous donne :
Le contenu du fichier AppletSample.htm s'affiche correctement dans la zone d'affichage textuelle de notre applet. Maintenant, passons à la sauvegarde de données dans un fichier sur le poste client. Nous saisissons du texte et cliquons sur le bouton Save ce qui nous donne à l'écran :
Une fois le FileChooser ouvert, il suffit de sélectionner l'emplacement de destination pour le fichier de sauvegarde et de préciser son nom : save.txt en l'occurrence ici. Le click sur Enregistrer permet de confirmer l'enregistrement et amène à l'écran suivant :
Pour vérifier que la sauvegarde a effectivement réussi, il suffit de regarder le contenu du répertoire de destination :
Ensuite, nous pouvons afficher le contenu de save.txt dans un éditeur de texte :
Maintenant que nous avons testé les fonctionnalités de notre applet et constaté qu'elle fonctionnait correctement, nous allons pouvoir tester les possibilités d'intégration au desktop offertes par Java 6 Update 10. Pour cela, nous allons maintenant fermer le navigateur ayant servi de support au déploiement de l'applet. Suivant votre configuration du nouveau plug-in Java, un message apparaîtra vous demandant de confirmer l'intégration de l'applet au desktop ou bien l'intégration se fera automatiquement. Au final, cela permet d'obtenir un raccourci vers l'applet AppletSample sur le bureau et une entrée dans le menu des programmes du système client comme nous pouvons le voir ci-dessous :
La dernière update de Java 6 apporte un renouveau intéressant aux applets, délaissées depuis un certain temps par les programmeurs Java. Les possibilités offertes par le déploiement via JWS et l'accès aux différents services JNLP ainsi que l'intégration améliorée au desktop devant permettre de replacer le déploiement par applets au premier plan. Enfin, il est important de noter que ces nouveautés s'inscrivent dans la stratégie de Sun de faire de Java FX une solution RIA qui fera le poids face aux poids lourds que sont déjà Silverlight et surtout Flex.