Développement avec JavaFX

Depuis la version 8 de Java, JavaFX devient le standard officiel pour le développement des interfaces des applications Java au détriment de Swing. La grande nouveauté de JavaFX est de pouvoir structurer nos applications à partir d'un modèle MVC. Je rappelle que ce modèle permet de découpler la Vue grâce à l'outil SceneBuilder et à FXML du Contrôleur qui s'occupe de prendre en compte les événements et de réaliser les traitements de fond au moyen du langage Java. Le Modèle quant à lui ne gère que la persistance des données. Afin de bien réaliser et de bien maîtriser ces différents TPs, je vous invite à bien consulter les différents cours préliminaires.

Développements traités

Premier contact - Hello World !

Pour ce premier projet, nous allons le réaliser uniquement avec du code Java. L'objectif ici est de prendre contact avec les différents composants graphiques qui existent dans la librairie JavaFX. Vous remarquerez que les noms des composants proposés sont les mêmes noms que les composants originaux issus de la bibliothèque AWT le préfixe J de Swing a été supprimé. Mise à part cette histoire de noms, nous avons exactement le même style de programmation que nous avons déjà développé avec Swing.

Cours préliminaires
Description du projet

Ce projet nous permet de souhaiter la bienvenue et de dire bonjour, ceci au travers de deux boutons et d'un label. La disposition des composants est effectuée à l'aide d'un BorderPane et d'un FlowPane. Ce projet est constitué d'un seul fichier qui réalise l'ensemble du traitement.

BienvenueFX.java
package fx;

import javafx.application.Application;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.*;
import javafx.stage.Stage;

public class BienvenueFX extends Application {
   private Button btnBonjour = new Button("Dites 'Bonjour !'");
   private Button btnBienvenue = new Button("Dites 'Bienvenue !'");
   private Label  bienvenue = new Label("Hello Wold !");
   private BorderPane disposition = new BorderPane(bienvenue);
   private FlowPane boutons = new FlowPane(10, 10, btnBienvenue, btnBonjour);
   private Scene scene = new Scene(disposition, 450, 300);
   
   @Override
   public void start(Stage fenêtre) {
      bienvenue.setFont(Font.font(28));
      bienvenue.setTextFill(Color.CHOCOLATE);
      btnBonjour.setOnAction(evt -> { bienvenue.setText("Bonjour à tous !"); });
      btnBienvenue.setOnAction(evt -> { bienvenue.setText("Bienvenue à tout le monde !"); });
      boutons.setAlignment(Pos.CENTER);
      boutons.setPadding(new Insets(10, 0, 10, 0));
      boutons.setBackground(new Background(new BackgroundFill(Color.ALICEBLUE, null, null)));
      disposition.setTop(boutons);
      disposition.setStyle("-fx-background-color: #FAEBD7");
//      disposition.setBackground(new Background(new BackgroundFill(Color.ANTIQUEWHITE, null, null)));
      fenêtre.setTitle("Hello World !");
      fenêtre.setScene(scene);
//      théatre.setResizable(false);
      fenêtre.show();
   }
//
//   public static void main(String[] args) {
//      launch(args);
//   }  
}
Ce développement est relativement simple à mettre en oeuvre puisque nous devons coder dans un seul fichier. Le principe est vraiment identique à Swing avec quelques petites différences toutefois. Comme déjà évoqué, le nom des composants est très similaire mais plus naturel puisque vous n'avez plus à les préfixer de la lettre J. Ce qui est vraiment nouveau, c'est que vous n'êtes plus obligé d'avoir la méthode statique main() dans votre classe principale d'application. Votre classe principale doit hériter de la classe Application et vous devez au minimum redéfinir la méthode start() pour assurer le cycle de vie de votre application. C'est simple à mettre en oeuvre, mais l'inconvénient, c'est que dans votre code, vous avez un mélange entre le traitement relatif à l'aspet visuel qui prend d'ailleurs beaucoup de place et le traitement proprement dit qui gère les actions à réaliser par l'application. Dans les projets qui suivent, nous ferons en sorte de séparer ces deux parties fondamentales en utilisant systématiquement le modèle MVC.
Diagramme UML représentant la structure de l'IHM

Hello World ! avec le modèle MVC

Nous allons reprende le même projet, mais cette fois-ci avec l'architecture MVC. Nous en profiterons pour utiliser le logiciel SceneBuilder qui nous permettra de concevoir l'interface graphique uniquement à l'aide de la souris, avec bien entendu le réglage des différentes propriétés. Cette fois-ci l'arcitecture est un petit peu plus complexe puisque nous devons mettre en oeuvre trois fichiers. Le premier est un fichier au format XML dont l'extension est *.fxml qui représente la Vue de l'application qui peut être entièrement générée par SceneBuilder ou directement en écrivant les balises. Le Contrôleur est assurée par une classe Java qui doit implémenter l'interface Initializable qui est en relation directe grâce à l'annotation @FXML avec les composants définis dans la Vue. Ce Contrôleur assure le traitement à réaliser suivant les événements répertoriés. Enfin, nous retrouvons le fichier correspondant à l'application principale dont le seul but maintenant est de prendre en compte la Vue qui se trouve dans un autre fichier qui elle même se mettra en relation avec le Contrôleur attribut fx:controller de l'élément racine du fichier XML.

Cours préliminaires
Description du projet

Ce projet nous permet de souhaiter la bienvenue et de dire bonjour, ceci au travers de deux boutons et d'un label. La disposition des composants est effectuée à l'aide d'un BorderPane et d'un FlowPane. Ce projet est constitué des trois fichiers définis dans l'introduction.

BienvenueFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class BienvenueFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Bienvenue.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Hello World !");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Bienvenue.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="300.0" prefWidth="450.0" style="-fx-background-color: #FAEBD7;" 
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.BienvenueController">
   <top>
      <FlowPane alignment="CENTER" hgap="10.0" style="-fx-background-color: #F0F8FF;" BorderPane.alignment="CENTER">
         <children>
            <Button onAction="#ditesBienvenue" text="Dites 'Bienvenue !'" />
            <Button onAction="#ditesBonjour" text="Dites 'Bonjour !'" />
         </children>
         <padding>
            <Insets bottom="10.0" top="10.0" />
         </padding>
      </FlowPane>
   </top>
   <center>
      <Label fx:id="bienvenue" text="Hello World !" textFill="CHOCOLATE" BorderPane.alignment="CENTER">
         <font>
            <Font size="28.0" />
         </font>
      </Label>
   </center>
</BorderPane>
BienvenueController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.*;

public class BienvenueController implements Initializable {
   
   @FXML
   private Label bienvenue;
   
   @FXML
   private void ditesBienvenue(ActionEvent event) {
      bienvenue.setText("Bienvenue à tout le monde !");
   }
   
   @FXML
   private void ditesBonjour(ActionEvent event) {
      bienvenue.setText("Bonjour à tous !");
   }   
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      // TODO
   }      
}
Certe, la structure est plus sophistiqué puisque maintenant nous avons besoin de trois fichiers au lieu d'un, mais quelle simplicité au niveau de la conception ! Cette séparation est vraiment bénéfique puisque dans un cas nous nous préoccupons uniquement de l'aspect visuel avec les différents placements, le choix des couleurs, etc. sans nous soucier du traitement à faire. Dans l'autre cas, une fois que les composants sont bien en place nous nous préoccupons que de la partie traitement dont le code devient extrêmement allégé. Le modèle MVC permet un meilleur travail en équipe avec d'un côté le designer qui dispose de ses propres compétences et de l'autre côté le développeur Java qui est plus à même de connaître les subtilités du langage.

Conversion entre les €uros et les Francs

Pour ce projet, nous n'allons conforter nos nouvelles connaissances sans rajouter de nouveaux concepts mais simplement pour nous habituer à ce modèle de conception. Nous réalisons un simple convertisseur monnétaire entre les €uros et les Francs. L'intérêt ici est de prendre contact avec un composant très fréquemment utilisé dans les IHM qui permet de réaliser des saisies. Dans le cas de JavaFX, la classe utilisée est un TextField

Cours préliminaires

Description du projet

Ce projet nous permet de convertir des €uros en Francs uniquement dans ce sens là. Du coup, la zone de saisie correspondant au Francs ne sera pas éditable. Nous en profiterons pour formater correctement les deux monnaies avec un affichage standard français séparation des milliers avec un espace et utilisation de la virgule pour les centimes. Attention, vous devez effectuer la saisie en respectant ce formatage conventionnel, sinon la conversion ne se fait pas.

Un composant de type TextField permet de faire des saisies de texte uniquement. Si vous devez utiliser des nombres entiers, des nombres réels ou comme ici des valeurs monétaires, vous devez réaliser des conversions automatiques afin de manipuler des données en adéquation avec ce que vous souhaitez calculer. C'est une nouveauté par rapport à Swing, JavaFX propose une série de classes qui effectuent automatiquement ces conversions et qui héritent toutes de la classe abstraite StringConverter. Vous disposez des classes IntegerStringConverter, DoubleStringConverter, NumberStringConverter, etc. Nous utiliserons cette dernière classe en corrélation avec les classes de formatage déjà présentes dans Swing, notamment NumberFormat ainsi que DecimalFormat. 
ConversionFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class BienvenueFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Conversion.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Conversion entre les €uros et les francs");
      fenêtre.setResizable(false);
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Conversion.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="84.0" prefWidth="430.0" xmlns="http://javafx.com/javafx/8"
                          xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.ConversionController">
   <children>
      <Button fx:id="button" layoutX="326.0" layoutY="14.0" onAction="#calculerFrancs" text="Conversion" />
      <TextField fx:id="euros" layoutX="14.0" layoutY="14.0" onAction="#calculerFrancs" 
                                         prefHeight="25.0" prefWidth="305.0" promptText="€uros" text="0,00 €" />
      <TextField fx:id="francs" editable="false" layoutX="14.0" layoutY="45.0" 
                                         prefHeight="25.0" prefWidth="305.0" promptText="Francs" text="0,00 F" />
   </children>
</AnchorPane>
ConversionController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.util.converter.NumberStringConverter;

public class ConversionController implements Initializable {
   
   @FXML
   private TextField euros, francs;
   private final double TAUX = 6.55957;
   private NumberStringConverter valeurEnEuro = new NumberStringConverter(NumberFormat.getCurrencyInstance());
   private NumberStringConverter valeurEnFranc = new NumberStringConverter(new DecimalFormat("#,##0.00 F"));
   
   @FXML
   private void calculerFrancs(ActionEvent event) {
      Number valeur = valeurEnEuro.fromString(euros.getText());
      francs.setText(valeurEnFranc.toString(valeur.doubleValue()*TAUX));
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) { }    
}
 L'ossature de ce projet est totalement identique au projet précédent. Une fois que nous avons compris le principe, nous aurons systématiquement le même style de conception quelque soit la difficulté du projet. Le modèle MVC nous aide dans nos différentes recherches. Il peut y avoir plus de fichiers plusieurs Vues et plusieurs Contrôleurs mais le principe de conception demeurera similaire à ce que nous venons de réaliser.

Utilisation des Shape - JavaFX 2D

Durant cette étude, nous allons nous intéresser à l'utilisation de formes toutes faites proposées par JavaFX 2D. Nous disposons d'un ensemble de composants qui sont des formes géométriques, comme des cercles, des rectangles, des ellipses, des polygones, etc. Toutes ces formes sont implémentées au travers de classes spécifiques, comme Circle, Rectangle, Ellipse, Polygon, etc. Toutes ces classes héritent de la classe de base des formes nommée Shape. Cette super-classe hérite elle-même de la classe Node ce qui nous permet d'intégrer n'importe quelle forme dans un panneau quelconque au même titre que les autres composants, comme les boutons, les libellés, etc. en utilisant la même démarche que nous venons d'apprendre.

Cours préliminaires
Description du projet

Le projet consiste à placer deux types de forme, des cercles et des carrés, tout simplement à l'aide de la souris. À chacun des clics de la souris nous plaçons une nouvelle forme suivant le choix déterminé par la sélection d'un des deux boutons radio. Un bouton permet ultérieurement de tout enlever et ainsi de replacer de nouvelles formes à notre convenance.

Afin d'avoir une architecture du projet relativement propre, il est judicieux de proposer un nouveau panneau personnalisé capable de maîtriser tout ces tracés de formes. Il s'agit de créer une nouvelle classe nommée Panneau qui hérite de la classe Pane. Cette dernière est la classe de base de tous les Layouts, comme BorderPane, FlowPane, AnchorPane, etc.
Panneau.java
package fx;

import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;

enum TypeForme {CERCLE, CARRÉ};

public class Panneau extends Pane { 
   private TypeForme type = TypeForme.CERCLE;
   
   public Panneau() {  setOnMouseClicked(evt -> {  tracerForme(evt.getX(), evt.getY()); });  }
   
   public void changer(TypeForme forme) { type = forme; }
   
   public void toutEffacer() { getChildren().clear(); }
   
   private void tracerForme(double x, double y) {
      Shape forme = null;
      switch (type) {
            case CERCLE : forme = new Circle(x, y, 50); break;
            case CARRÉ: forme = new Rectangle(x-50, y-50, 100, 100); break;
      }
      forme.setFill(new Color(0,0,0,0));
      forme.setStroke(Color.CHOCOLATE);
      forme.setStrokeWidth(2);
      getChildren().add(forme);
   }
}
FormesFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class FormesFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Formes.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Placement de formes");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Formes.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="450" prefWidth="600.0" style="-fx-background-color: #FAEBD7;" 
                         xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.FormesController">
   <center>
      <Panneau fx:id="panneau" BorderPane.alignment="CENTER" />
   </center>   
   <bottom>
      <FlowPane alignment="CENTER" hgap="10.0" style="-fx-background-color: #F0F8FF;" BorderPane.alignment="CENTER">
         <padding>
            <Insets bottom="10.0" top="10.0" />
         </padding>
         <effect>
            <DropShadow />
         </effect>
         <children>
            <RadioButton mnemonicParsing="false" onAction="#formeCirculaire" selected="true" text="Cercle">
               <toggleGroup>
                  <ToggleGroup fx:id="formes" />
               </toggleGroup></RadioButton>
            <RadioButton mnemonicParsing="false" onAction="#formeCarrée" text="Carré" toggleGroup="$formes" />
            <Button mnemonicParsing="false" onAction="#toutEffacer" text="Tout effacer">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </Button>
         </children>
      </FlowPane>
   </bottom>
</BorderPane>
FormesController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;

public class FormesController implements Initializable {
   
   @FXML
   private Panneau panneau;
   
   @FXML
   private void formeCirculaire(ActionEvent event) {  panneau.changer(TypeForme.CERCLE); }
   
   @FXML
   private void formeCarrée(ActionEvent event)     {  panneau.changer(TypeForme.CARRÉ);  }   
   
   @FXML
   private void toutEffacer(ActionEvent event)     {  panneau.toutEffacer(); }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {  }         
}
Tout ce situe finalement dans cette classe Panneau. Vu qu'elle enregistre l'ensemble des formes, cette classe peut être considérée comme le Modèle du système au même titre que Formes.fxml représente la Vue, et que FormesController.java, comme son nom l'indique représente le Contrôleur dans l'architecture MVC. Vous remarquez au passage que la gestion événementielle est très similaire à ce que nous avons déjà découvert, c'est juste le nom de la méthode qui diffère et qui est adaptée au type de l'événement, ici setOnMouseClicked().
Modification du projet

Nous reprenons le projet précédent afin de prendre en compte un nouveau type de forme carré à bords arrondis ainsi que la prise en compte de la dimension des formes que nous plaçons. Toutefois, toutes les formes devront systématiquement posséder les mêmes dimensions. Il est alors impératif de redimensionner les formes déjà introduites.

Nous devons modifier notre IHM en rajoutant un nouveau bouton radio et une zone de saisie qui prend en compte la largeur des formes à placer.
Panneau.java
package fx;

import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;

enum TypeForme {CERCLE, CARRÉ, CARRÉARRONDI};

public class Panneau extends Pane { 
   private TypeForme type = TypeForme.CERCLE;
   private double largeur = 100;
   
   public Panneau() {  setOnMouseClicked(evt -> {  tracerForme(evt.getX(), evt.getY()); });  }
   
   public void changer(TypeForme forme) { type = forme; }
   
   public void toutEffacer() { getChildren().clear(); }
   
   private void tracerForme(double x, double y) {
      Shape forme = null;
      switch (type) {
            case CERCLE : 
               forme = new Circle(x, y, largeur/2); break;
            case CARRÉ : 
               forme = new Rectangle(x-50, y-50, largeur, largeur); break;
            case CARRÉARRONDI : 
               forme = new Rectangle(x-50, y-50, largeur, largeur);
               Rectangle rect = (Rectangle) forme;
               rect.setArcHeight(20);
               rect.setArcWidth(20);
               break;
      }
      forme.setFill(new Color(0,0,0,0));
      forme.setStroke(Color.CHOCOLATE);
      forme.setStrokeWidth(2);
      getChildren().add(forme);
   }
   
   public void changer(double largeur)  { 
      this.largeur = largeur; 
      for (Node forme : getChildren()) {
         if (forme instanceof Circle) ((Circle)forme).setRadius(largeur/2);
         else {
            Rectangle rect = (Rectangle) forme;
            rect.setWidth(largeur);
            rect.setHeight(largeur);
         }
      }         
   }
}
FormesFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class FormesFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Formes.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Placement de formes");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Formes.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="450" prefWidth="600.0" style="-fx-background-color: #FAEBD7;" 
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.FormesController">
   <center>
      <Panneau fx:id="panneau" BorderPane.alignment="CENTER" />
   </center>   
   <bottom>
      <FlowPane alignment="CENTER" hgap="10.0" style="-fx-background-color: #F0F8FF;" BorderPane.alignment="CENTER">
         <padding>
            <Insets bottom="10.0" top="10.0" />
         </padding>
         <effect>
            <DropShadow />
         </effect>
         <children>
            <RadioButton mnemonicParsing="false" onAction="#formeCirculaire" selected="true" text="Cercle">
               <toggleGroup>
                  <ToggleGroup fx:id="formes" />
               </toggleGroup></RadioButton>
            <RadioButton mnemonicParsing="false" onAction="#formeCarrée" text="Carré" toggleGroup="$formes" />
            <RadioButton mnemonicParsing="false" onAction="#formeCarréeArrondi" text="Carré arrondi" toggleGroup="$formes" />
            <TextField fx:id="largeur" onAction="#changerLargeur" prefHeight="25.0" prefWidth="100.0" promptText="Largeur" text="100">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </TextField>
            <Button mnemonicParsing="false" onAction="#toutEffacer" text="Tout effacer">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </Button>
         </children>
      </FlowPane>
   </bottom>
</BorderPane>
FormesController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.TextField;

public class FormesController implements Initializable {
   
   @FXML
   private Panneau panneau;
   
   @FXML
   private TextField largeur;
   
   @FXML
   private void formeCirculaire(ActionEvent event) {  
      panneau.changer(TypeForme.CERCLE); 
   }
   
   @FXML
   private void formeCarrée(ActionEvent event)  {  
      panneau.changer(TypeForme.CARRÉ);  
   }   
   
   @FXML
   private void formeCarréeArrondi(ActionEvent event)   {  
      panneau.changer(TypeForme.CARRÉARRONDI);  
   }   
   
   @FXML
   private void changerLargeur(ActionEvent event) {
      panneau.changer(Double.parseDouble(largeur.getText()));
   }
   
   @FXML
   private void toutEffacer(ActionEvent event)  {  
      panneau.toutEffacer(); 
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {  }     
}
Encore une fois, tout ce situe finalement dans cette classe Panneau. Le fait de rajouter un nouveau type de forme ne pose réellement pas de gros problèmes. Là où cela devient plus compliqué, c'est de modifier les formes qui ont déjà été placées dans l'interface. Vous êtes obligés de connaître le type de chaque composant actuellement présent. Cela se fait au moyen du mot réservé instanceof qui permet de retrouver la classe exacte de l'objet stocké, sachant que la méthode getChildren() enregistre des noeuds Node quels qu'ils soient. Vous devez ensuite transtyper vos différents objets afin d'utiliser les méthodes les plus adaptées, comme setRadius(), setWidth() et setHeight(). Cette approche, même si elle fonctionne parfaitement, peut s'avérer très compliqué dès que nous allons rajouter des focntionnalités supplémentaires. Il serait préférable de faire une gestion personnalisé de ses différentes formes, à l'aide d'un tracé spécifique, ce qui se fait en JavaFX avec la notion de Canvas et ContextGraphics. C'est le sujet de l'étude suivante.

JavaFX 2D avec du tracé personnalisé (calque)

Dans cette nouvelle étude, nous allons toujours travailler sur la technologie JavaFX 2D, toutefois, au lieu d'utiliser des composants des noeuds déjà tout fait, comme les clases Circle, Rectangle, etc., nous allons tracer les formes qui nous intéressent sur un dessin, au travers des classes spécifiques, comme Canvas qui représente la feuille de dessin ou le calque suivant notre point de vue. Le canevas peut ensuite être placé dans le panneau au même titre que les autres composants. Le canevas propose un contexte graphique GraphicsContext qui permet de réaliser, à l'aide de méthodes spécifiques, des tracés d'ellipse, de rectangle, de courbes de béziers, etc.

Cours préliminaires
Description du projet

Nous reprenons le projet précédent en prenant en compte cette nouvelle approche. Seul le fichier Panneau.java est modifié. Par contre, un nouveau fichier nommé Forme.java, décrit toutes les formes personnalisés à prendre en compte avec leur affichage respectif.

 
Forme.java
package fx;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

abstract class Forme {
   protected double x, y;
   protected static GraphicsContext calque;
   
   public Forme(double x, double y) {
      this.x = x;
      this.y = y;
      calque.setStroke(Color.CHOCOLATE);
      calque.setLineWidth(2);     
   }   
   
   public abstract void affiche();
   public abstract void changeLargeur(double valeur);
   public static void setCalque(GraphicsContext contexte) { calque = contexte; }
}

class Cercle extends Forme {
   private double rayon;
   
   public Cercle(double x, double y, double rayon) { 
      super(x, y);
      this.rayon = rayon;     
   }

   @Override
   public void affiche() {
       calque.strokeOval(x-rayon, y-rayon, 2*rayon, 2*rayon);
   }

   @Override
   public void changeLargeur(double valeur) {
      rayon = valeur/2;
      affiche();
   }
}

class Carré extends Forme {
   protected double côté;
   
   public Carré(double x, double y, double côté) {
      super(x, y);
      this.côté = côté;
   }

   @Override
   public void affiche() {
      calque.strokeRect(x-côté/2, y-côté/2, côté, côté);
   }

   @Override
   public void changeLargeur(double valeur) {
      côté = valeur;
      affiche();
   }
}

class CarréArrondi extends Carré {
   public CarréArrondi(double x, double y, double côté) {
      super(x, y, côté);    
   } 

   @Override
   public void affiche() {
      calque.strokeRoundRect(x-côté/2, y-côté/2, côté, côté, 20, 20);
   }  
}
Panneau.java
package fx;

import java.util.*;
import javafx.scene.canvas.*;
import javafx.scene.layout.Pane;

enum TypeForme {CERCLE, CARRÉ, CARRÉARRONDI};

public class Panneau extends Pane { 
   private TypeForme type = TypeForme.CERCLE;
   private double largeur = 100;
   private GraphicsContext calque;
   private List<Forme> formes = new ArrayList<>();
   
   public Panneau() {  
      Canvas dessin = new Canvas(1920, 1024);
      calque = dessin.getGraphicsContext2D();  
      Forme.setCalque(calque);
      setOnMouseClicked(evt -> {  tracerForme(evt.getX(), evt.getY()); });  
      getChildren().add(dessin);
   }
   
   public void changer(TypeForme forme)  { type = forme; }
    
   public void toutEffacer() { 
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      formes.clear();
   }     
   
   public void changer(double largeur)  { 
      this.largeur = largeur; 
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      for (Forme forme : formes) forme.changeLargeur(largeur);
   }
   
   private void tracerForme(double x, double y) {
      Forme forme = null;
      switch (type) {
            case CERCLE  : forme = new Cercle(x, y, largeur/2); break;
            case CARRÉ   : forme = new Carré(x, y, largeur); break;
            case CARRÉARRONDI  : forme = new CarréArrondi(x, y, largeur); break;
      }    
      formes.add(forme);
      forme.affiche();
   }
}
FormesFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class FormesFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Formes.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Placement de formes");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Formes.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="450" prefWidth="600.0" style="-fx-background-color: #FAEBD7;" 
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.FormesController">
   <center>
      <Panneau fx:id="panneau" BorderPane.alignment="CENTER" />
   </center>   
   <bottom>
      <FlowPane alignment="CENTER" hgap="10.0" style="-fx-background-color: #F0F8FF;" BorderPane.alignment="CENTER">
         <padding>
            <Insets bottom="10.0" top="10.0" />
         </padding>
         <effect>
            <DropShadow />
         </effect>
         <children>
            <RadioButton mnemonicParsing="false" onAction="#formeCirculaire" selected="true" text="Cercle">
               <toggleGroup>
                  <ToggleGroup fx:id="formes" />
               </toggleGroup></RadioButton>
            <RadioButton mnemonicParsing="false" onAction="#formeCarrée" text="Carré" toggleGroup="$formes" />
            <RadioButton mnemonicParsing="false" onAction="#formeCarréeArrondi" text="Carré arrondi" toggleGroup="$formes" />
            <TextField fx:id="largeur" onAction="#changerLargeur" prefHeight="25.0" prefWidth="100.0" promptText="Largeur" text="100">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </TextField>
            <Button mnemonicParsing="false" onAction="#toutEffacer" text="Tout effacer">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </Button>
         </children>
      </FlowPane>
   </bottom>
</BorderPane>
FormesController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.TextField;

public class FormesController implements Initializable {
   
   @FXML
   private Panneau panneau;
   
   @FXML
   private TextField largeur;
   
   @FXML
   private void formeCirculaire(ActionEvent event) {  
      panneau.changer(TypeForme.CERCLE); 
   }
   
   @FXML
   private void formeCarrée(ActionEvent event)  {  
      panneau.changer(TypeForme.CARRÉ);  
   }   
   
   @FXML
   private void formeCarréeArrondi(ActionEvent event)   {  
      panneau.changer(TypeForme.CARRÉARRONDI);  
   }   
   
   @FXML
   private void changerLargeur(ActionEvent event) {
      panneau.changer(Double.parseDouble(largeur.getText()));
   }
   
   @FXML
   private void toutEffacer(ActionEvent event)  {  
      panneau.toutEffacer(); 
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {  }     
}
Diagramme des classes du projet

Création d'un nouveau composant - les propriétés

Nous allons reprendre le projet précédent, mais cette fois-ci notre démarche sera différente. Notre objectif sera de créer un nouveau composant qui sera directement exploitable par SceneBuilder avec de nouvelles propriétés tout en conservant ce qui existe. Tous les composants que nous plaçons dans notre fenêtre, pour qu'ils soient visibles et exploitables, doivent tous hériter directement ou indirectement de la classe Control. Notre étude ici consiste à créer un composant capable de réaliser des saisies de valeurs monétaires quelque soit la monnaie avec un formatage adapté à l'écriture française. Puisqu'il s'agit d'une saisie, nous fabriquerons une nouvelle classe Monnaie qui hérite directement de la classe TextField.

Cours préliminaires
Description du projet

Avant de constituer votre projet, consulter bien la notion de propriétés. La première démarche consiste à s'occuper dès le départ de la fabication de ce nouveau composant, dans un fichier à part entière, nommée justement Monnaie.java ce projet va donc être constitué de quatre fichiers. Dans ce nouveau composant, nous allons rajouter deux nouvelles proriétés qui nous permettrons de choisir la monnaie et de récupérer directement la valeur numérique qui permettra par la suite de réaliser les calculs très simplement rajout uniquement du taux de conversion. Enfin, la monnaie sera systématiquement justifiée à droite dans la zone de saisie.

Une fois que le composant est parfaitement constitué, vous pouvez construire le projet afin que l'archive puisse être exploitée directement par la suite par le logiciel SceneBuilder le rajout de composant se fait au travers de l'archive *.jar.

Un composant de type TextField permet de faire des saisies de texte uniquement. Si vous devez utiliser des nombres entiers, des nombres réels ou comme ici des valeurs monétaires, vous devez réaliser des conversions automatiques afin de manipuler des données en adéquation avec ce que vous souhaitez calculer. C'est une nouveauté par rapport à Swing, JavaFX propose une série de classes qui effectuent automatiquement ces conversions et qui héritent toutes de la classe abstraite StringConverter. Vous disposez des classes IntegerStringConverter, DoubleStringConverter, NumberStringConverter, etc. Nous utiliserons cette dernière classe en corrélation avec les classes de formatage déjà présentes dans Swing, notamment NumberFormat ainsi que DecimalFormat. 
Monnaie.java
package fx;

import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.util.converter.CurrencyStringConverter;

public class Monnaie extends TextField {
   private final StringProperty symbole = new SimpleStringProperty("€"); 
   private final DoubleProperty valeur = new SimpleDoubleProperty(0.0);
   private NumberStringConverter valeurMonnaie = new CurrencyStringConverter();

   public Monnaie() {  setAlignment(Pos.CENTER_RIGHT); }
   
   public double getValeur() {      
      setValeur(valeurMonnaie.fromString(getText()).doubleValue());
      return valeur.get(); 
   }

   public void setValeur(double nombre) {
      valeur.set(nombre);
      setText(valeurMonnaie.toString(nombre));
   }

   public String getSymbole() { return symbole.get();  }

   public void setSymbole(String monnaie) {
      symbole.set(monnaie);
      valeurMonnaie = new NumberStringConverter("#,##0.00 "+monnaie);
      setText(valeurMonnaie.toString(valeur.get()));
   }
   
   public DoubleProperty valeurProperty() { return valeur;  }
   public StringProperty symboleProperty() { return symbole;  } 
}
ConversionFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class BienvenueFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Conversion.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Conversion entre les €uros et les francs");
      fenêtre.setResizable(false);
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Conversion.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="84.0" prefWidth="430.0" xmlns="http://javafx.com/javafx/8"
                          xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.ConversionController">
   <children>
      <Button fx:id="button" layoutX="326.0" layoutY="14.0" onAction="#calculerFrancs" text="Conversion" />
      <fx.Monnaie fx:id="euros" layoutX="14.0" layoutY="14.0" onAction="#calculerFrancs" 
                         prefWidth="305.0" promptText="€uros" text="0,00 €" />
      <fx.Monnaie fx:id="francs" editable="false" layoutX="14.0" layoutY="45.0"
                         prefWidth="305.0" promptText="Francs" symbole="F" />
      <Label fx:id="barreEtat" layoutX="14.0" layoutY="81.0" />
   </children>
</AnchorPane>
ConversionController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.*;

public class ConversionController implements Initializable {
   
   @FXML
   private Monnaie euros, francs;
   private final double TAUX = 6.55957;
   
   @FXML
   private Label barreEtat;
   
   @FXML
   private void calculerFrancs(ActionEvent event) {
      francs.setValeur(euros.getValeur()*TAUX);
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      barreEtat.textProperty().bind(euros.textProperty().concat(" donne ").concat(francs.textProperty()));
   }    
}
 L'écriture du contrôleur devient très simple à faire grâce à la conception de ce nouveau composant Monnaie. Nous travaillons directement avec les valeurs numériques des monnaies considérées. Nous en profitons pour mettre en relation des propriétés dans la méthode initialize() afin que la barre d'état soit automatiquement mise à jour par rapport aux calculs proposés. De même, dans le fichier Conversion.fxml nous pouvons utiliser la nouvelle balise <fx.Monnaie /> qui dispose des attributs relatifs aux nouvelle propriétés mises en oeuvre propriété symbole notamment. Cette nouvelle balise dispose bien entendu des attributs propres à la classe TextField grâce à l'héritage que nous avons déjà exploités.
Création d'un nouveau composant de type Label qui change de couleur au survol de la souris

Afin de bien maîtriser ces différents concepts, je vous propose de créer un nouveau composant Texte qui hérite de Label et qui a la particularité de changer de couleur lorsque le curseur de la souris passe au dessus. Deux propiétés doivent être mises en oeuvre pour ce projet, la première, nommée couleurBase, comme son nom l'indique, permet de choisir la couleur du texte par défaut. La deuxième, nommée couleurSurvol, permet de choisir la couleur du texte uniquement lorsque le curseur de la souris passe au dessus du composant. Ces deux propriétés, comme toutes les autres, sont réglables dans SceneBuilder avant de lancer définitivement  l'application. Dans la phase de construction, il faut penser à proposer un texte par défaut, changer éventuellement le style du curseur de la souris et enfin prendre en compte la gestion événementielle.

Exceptionnellement, pour ce projet, nous n'avons pas besoin de contrôleur. Tout se passe au niveau du composant personnalisé.
Texte.java
package texte;

import javafx.beans.property.*;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.paint.*;

public class Texte extends Label {
   private final ObjectProperty<Paint> couleurBase = new ReadOnlyObjectWrapper<>(Color.BLUE);
   private final ObjectProperty<Paint> couleurSurvol = new ReadOnlyObjectWrapper<>(Color.RED);

   public Texte() {
      super("Bienvenue...");
      setCursor(Cursor.HAND);
      setTextFill(couleurBase.get()); 
      setOnMouseEntered(evt -> { setTextFill(couleurSurvol.get()); });
      setOnMouseExited(evt -> { setTextFill(couleurBase.get()); });
   }
   
   public Paint getCouleurBase() { return couleurBase.get(); }   
   public Paint getCouleurSurvol() { return couleurSurvol.get();  }
   
   public void setCouleurSurvol(Paint value) {
      couleurSurvol.set(value); 
   }   

   public void setCouleurBase(Paint value) {
      couleurBase.set(value);
      setTextFill(value);
   }

   public ObjectProperty couleurBaseProperty() { return couleurBase; }
   public ObjectProperty couleurSurvolProperty() { return couleurSurvol; }   
}
TextFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class TexteFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Texte.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Texte.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<?import texte.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>


<AnchorPane id="AnchorPane" prefHeight="200" prefWidth="320" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Texte layoutX="42.0" layoutY="62.0">
         <couleurBase>
            <LinearGradient endX="1.0" endY="1.0">
               <stops>
                  <Stop color="#d5e50bfa" />
                  <Stop color="#917bdbf2" offset="1.0" />
               </stops>
            </LinearGradient>
         </couleurBase>
         <couleurSurvol>
            <RadialGradient centerX="0.5" centerY="0.5" radius="0.5">
               <stops>
                  <Stop color="#ff5100" />
                  <Stop color="WHITE" offset="1.0" />
               </stops>
            </RadialGradient>
         </couleurSurvol>
         <font>
            <Font name="System Bold Italic" size="36.0" />
         </font>
      </Texte>
   </children>
</AnchorPane>

Afficher des images - Image et ImageView

Dans le projet qui suit, nous nous intéressons à l'affichage automatique des images. Pour que cela puisse se faire correctement, nous utilisons deux classes spécifiques qui jouent chacune leur rôle. La première, Image, permet de récupérer des photos à partir des fichiers enregistrés quelque soit leurs formats et de les traiter par la suite à l'aide de méthodes adaptées. La deuxième, ImageView, s'occupe de l'affichage de cette photo. Elle hérite de la classe Control et réagit comme les autres contrôles présents dans l'IHM. Nous exploitons l'objet Image généralememnt dans le contrôleur ou dans le modèle alors que ImageView sera mis en oeuvre au niveau de la vue. Nous profitons de l'occasion pour voir comment utiliser les boîtes de dialogues prédéfinies dans JavaFX.

Cours préliminaires
Description du projet

Dans ce projet, la partie principale de la fenêtre est constituée d'un objet de type ImageView. Cet objet est placé dans un conteneur spécial, ScrollPane, qui permet de rajouter des ascenceurs horizontal et vertical qui seront bien utiles lorsque la photo sera plus grande que la taille de la fenêtre. La partie haute est occupée par une barre d'outils qui dispose d'un bouton qui va activer un sélecteur de fichier classe FileChooser et d'une case à cocher classe CheckBox qui permet de visualiser la photo soit entièrement soit avec un grossissement. Le grossissement sera réglé au travers d'un Slider qui se trouve sur la partie droite de la barre d'outils. Le Slider est accompagné d'un Label qui donne le pourcentage du zoom choisi. Enfin, nous permettons le déplacement de la photo à l'aide de la souris.

PhotosFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class PhotoFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Photo.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Visionneuse");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Photo.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane fx:id="panneau" prefHeight="410.0" prefWidth="554.0" xmlns="http://javafx.com/javafx/8" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="photos.PhotoController">
   <center>
      <ScrollPane fx:id="ascenceur">
         <ImageView fx:id="vignette" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" 
                                     onMousePressed="#pointDeContact" onMouseDragged="#déplacer" />
      </ScrollPane>
   </center>
   <top>
      <ToolBar prefHeight="40.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <items>
          <Button onAction="#chargerPhoto" text="Nouvelle photo" />
          <CheckBox fx:id="zoom" onAction="#actionZoom" prefHeight="18.0" selected="true" text="Zoom" />
          <Slider fx:id="choixZoom" blockIncrement="1.0" min="20.0" onMouseClicked="#réglageZoom" value="100.0" />
          <Label fx:id="valeurZoom" text="100 %" />
        </items>
      </ToolBar>
   </top>
</BorderPane>
PhotoController.java
package photos;

import java.io.*;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;

public class PhotoController implements Initializable {
   private Image photo;
   private FileChooser sélecteurFichier = new FileChooser();
   private double x, y;
   private double largeur;
   
   @FXML private BorderPane panneau;
   @FXML private ImageView vignette;
   @FXML private ScrollPane ascenceur;
   @FXML private CheckBox zoom;   
   @FXML private Slider choixZoom; 
   @FXML private Label valeurZoom;

   @FXML
   private void chargerPhoto() throws IOException {
      File fichier = sélecteurFichier.showOpenDialog(null);
      if (fichier!=null)  {
         photo = new Image(new FileInputStream(fichier));
         vignette.setImage(photo);
         actionZoom();
         ascenceur.setHvalue(0.5);
         ascenceur.setVvalue(0.5);
      }         
   }
   
   @FXML
   private void pointDeContact(MouseEvent souris) {
      x = souris.getX();
      y = souris.getY();
   }
   
   @FXML
   private void déplacer(MouseEvent souris) {
      double offsetX = (souris.getX() - x)/largeur + ascenceur.getHvalue();
      double offsetY = (souris.getY() - y)/largeur + ascenceur.getVvalue();
      ascenceur.setHvalue(offsetX);
      ascenceur.setVvalue(offsetY);
      pointDeContact(souris);
   }
   
   @FXML
   private void actionZoom() {
      if (photo!=null) {
         double memX = ascenceur.getHvalue();
         double memY = ascenceur.getVvalue();
         largeur = zoom.isSelected() ? photo.getWidth()*choixZoom.getValue()/100.0 : panneau.getWidth();
         vignette.setFitWidth(largeur);
         ascenceur.setHvalue(memX);
         ascenceur.setVvalue(memY);
      }
   }
   
   @FXML
   private void réglageZoom() {
      int valeur = (int) choixZoom.getValue();
       valeurZoom.setText(valeur+" %");
       actionZoom();
   }

   @Override
   public void initialize(URL url, ResourceBundle rb) { 
      sélecteurFichier.setTitle("Choisissez votre photo");
      sélecteurFichier.setInitialDirectory(new File("/home/manu/Images"));
      sélecteurFichier.getExtensionFilters().add(new FileChooser.ExtensionFilter("Fichiers images", "*.png", "*.gif ", "*.jpg"));
   }   
}

Création d'une nouvelle interface Dessin

Cette étude va nous permettre de travailler avec la notion d'interface que Java est capable d'implémenter. Nous allons nous servir du projet précédent sur le tracé des différentes formes auquel nous rajoutons le tracé de texte. Dans une interface, nous déclarons l'ensemble des méthodes qui devront être impérativement redéfinies. Toutes les classes qui implémentrons cette interface devront respecter ce contrat. Ainsi, puisqu'il s'agit d'une déclaration, toutes les méthodes de l'interface sont systématiquement abstraite et publique. Les interfaces ne possèdent pas d'attributs. Seules quelques constantes peuvent être intégrées. L'intérêt des interfaces est de mettre en relation des hiérarchies de classes qui à priori n'ont rien en commun si ce n'est les méthodes redéfinies. Lorsque nous passons par des interfaces, comme leurs noms l'indiques, nous ne sommes plus en contact direct avec les classes, nous n'avons pas d'accès aux autres méthodes publiques, ce qui permet de rajouter une protection supplémentaire.

Cours préliminaires
Description du projet

Par rapport au projet précédent, nous rajoutons systématiquement un texte au centre de chacune des formes qui précise les coordonnées de placement.

 
Forme.java
package fx;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

interface Dessin {
   void affiche();
}

abstract class Forme implements Dessin {
   protected double x, y;
   protected static GraphicsContext calque;
   
   public Forme(double x, double y) {
      this.x = x;
      this.y = y;
   }   

   public abstract void changeLargeur(double valeur);
   public static void setCalque(GraphicsContext contexte) { 
      calque = contexte; 
      calque.setStroke(Color.CHOCOLATE);
//      calque.setLineWidth(2);        
   }
}

class Cercle extends Forme {
   private double rayon;
   
   public Cercle(double x, double y, double rayon) { 
      super(x, y);
      this.rayon = rayon;     
   }

   @Override
   public void affiche() {
       calque.strokeOval(x-rayon, y-rayon, 2*rayon, 2*rayon);
   }

   @Override
   public void changeLargeur(double valeur) {
      rayon = valeur/2;
      affiche();
   }
}

class Carré extends Forme {
   protected double côté;
   
   public Carré(double x, double y, double côté) {
      super(x, y);
      this.côté = côté;
   }

   @Override
   public void affiche() {
      calque.strokeRect(x-côté/2, y-côté/2, côté, côté);
   }

   @Override
   public void changeLargeur(double valeur) {
      côté = valeur;
//      affiche();
   }
}

class CarréArrondi extends Carré {
   public CarréArrondi(double x, double y, double côté) {
      super(x, y, côté);    
   } 

   @Override
   public void affiche() {
      calque.strokeRoundRect(x-côté/2, y-côté/2, côté, côté, 20, 20);
   }  
}

class Texte implements Dessin {
   private double x, y;
   private String message;
   protected static GraphicsContext calque;

   public Texte(double x, double y, String message) {
      this.x = x;
      this.y = y;
      this.message = message;
   }
   
   public static void setCalque(GraphicsContext contexte) {
      calque = contexte;
      calque.setFill(Color.CHOCOLATE);
   }

   @Override
   public void affiche() {
      calque.fillText(message, x, y);
   }  
}

class Coordonnées extends Texte {

   public Coordonnées(double x, double y) {
      super(x-33, y+5, "("+(int)x+", "+(int)y+")");
   } 
}
Panneau.java
package fx;

import java.util.*;
import javafx.scene.canvas.*;
import javafx.scene.layout.Pane;

enum TypeForme {CERCLE, CARRÉ, CARRÉARRONDI};

public class Panneau extends Pane { 
   private TypeForme type = TypeForme.CERCLE;
   private double largeur = 100;
   private GraphicsContext calque;
   private List<Dessin> dessins = new ArrayList<>();
   
   public Panneau() {  
      Canvas dessin = new Canvas(1920, 1024);
      calque = dessin.getGraphicsContext2D();  
      Forme.setCalque(calque);
      Texte.setCalque(calque);
      setOnMouseClicked(evt -> {  tracerForme(evt.getX(), evt.getY()); });  
      getChildren().add(dessin);
   }
   
   public void changer(TypeForme forme)  { type = forme; }
    
   public void toutEffacer() { 
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      dessins.clear();
   }     
   
   public void changer(double largeur)  { 
      this.largeur = largeur; 
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      for (Dessin dessin : dessins) {
         if (dessin instanceof Forme)   ((Forme)dessin).changeLargeur(largeur);
         dessin.affiche();
      }
   }
   
   private void tracerForme(double x, double y) {
      Forme forme = null;
      switch (type) {
            case CERCLE           : forme = new Cercle(x, y, largeur/2); break;
            case CARRÉ            : forme = new Carré(x, y, largeur); break;
            case CARRÉARRONDI     : forme = new CarréArrondi(x, y, largeur); break;
      }    
      dessins.add(forme);
      forme.affiche();
      Coordonnées coordonnées = new Coordonnées(x, y);
      dessins.add(coordonnées);
      coordonnées.affiche();      
   }
}
FormesFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class FormesFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Formes.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Placement de formes");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Formes.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="450" prefWidth="600.0" style="-fx-background-color: #FAEBD7;" 
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.FormesController">
   <center>
      <Panneau fx:id="panneau" BorderPane.alignment="CENTER" />
   </center>   
   <bottom>
      <FlowPane alignment="CENTER" hgap="10.0" style="-fx-background-color: #F0F8FF;" BorderPane.alignment="CENTER">
         <padding>
            <Insets bottom="10.0" top="10.0" />
         </padding>
         <effect>
            <DropShadow />
         </effect>
         <children>
            <RadioButton mnemonicParsing="false" onAction="#formeCirculaire" selected="true" text="Cercle">
               <toggleGroup>
                  <ToggleGroup fx:id="formes" />
               </toggleGroup></RadioButton>
            <RadioButton mnemonicParsing="false" onAction="#formeCarrée" text="Carré" toggleGroup="$formes" />
            <RadioButton mnemonicParsing="false" onAction="#formeCarréeArrondi" text="Carré arrondi" toggleGroup="$formes" />
            <TextField fx:id="largeur" onAction="#changerLargeur" prefHeight="25.0" prefWidth="100.0" promptText="Largeur" text="100">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </TextField>
            <Button mnemonicParsing="false" onAction="#toutEffacer" text="Tout effacer">
               <FlowPane.margin>
                  <Insets left="30.0" />
               </FlowPane.margin>
            </Button>
         </children>
      </FlowPane>
   </bottom>
</BorderPane>
FormesController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.*;
import javafx.scene.control.TextField;

public class FormesController implements Initializable {
   
   @FXML
   private Panneau panneau;
   
   @FXML
   private TextField largeur;
   
   @FXML
   private void formeCirculaire(ActionEvent event) {  
      panneau.changer(TypeForme.CERCLE); 
   }
   
   @FXML
   private void formeCarrée(ActionEvent event)  {  
      panneau.changer(TypeForme.CARRÉ);  
   }   
   
   @FXML
   private void formeCarréeArrondi(ActionEvent event)   {  
      panneau.changer(TypeForme.CARRÉARRONDI);  
   }   
   
   @FXML
   private void changerLargeur(ActionEvent event) {
      panneau.changer(Double.parseDouble(largeur.getText()));
   }
   
   @FXML
   private void toutEffacer(ActionEvent event)  {  
      panneau.toutEffacer(); 
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {  }     
}
Diagramme des classes du projet

Enregistrer des dessins

Par rapport au projet précédent, nous allons permettre d'enregistrer l'ensemble du tracé réalisé, de telle sorte qu'ultérieurement il soit possible de récupérer l'ensemble de ces tracés afin de les réafficher sur notre fenêtre. Ce projet nous permet d'utiliser les flux d'objets en Java. Cela nous permet également de voir comment contruire une barre de boutons sous forme d'icônes. Nous profitons de l'occasion pour permettre le changement de couleur de l'ensemble des dessins.

Cours préliminaires
Description du projet

Par rapport au projet précédent, nous plaçons une barre d'outils en lieu et place du panneau de type FlowPane. Chaque bouton doit disposer de l'icône correspondant au traitement souhaité. Cela se fait à l'aide d'un ImageView toutes les icônes doivent être placées dans un répertoire spécifique que vous placez dans l'objet de type Button. Nous en profitons pour placer également des bulles d'aide sur chacun de ces boutons à l'aide de la classe ToolTip.

Nous plaçons également sur cette barre de bouton un ColorPicker qui va nous permettre de choisir la couleur pour l'ensemble des dessins. Par ailleurs, nous devons traiter maintenant une nouvelle forme hexagonale. Enfin, pour terminer, nous allons utiliser des boîtes de dialogue qui vont nous permettre de choisir le nom du fichier ainsi que provoquer une alerte au cas où un problème se pose lors de l'enregistrement ou de la lecture du fichier de dessins.

 
Forme.java
package fx;

import java.io.Serializable;
import javafx.scene.canvas.GraphicsContext;

interface Dessin extends Serializable {
   void affiche();
}

abstract class Forme implements Dessin {
   protected double x, y;
   static GraphicsContext calque;
   
   public Forme(double x, double y) {
      this.x = x;
      this.y = y;
   }   

   public abstract void changeLargeur(double valeur);
}

class Cercle extends Forme {
   private double rayon;
   
   public Cercle(double x, double y, double rayon) { 
      super(x, y);
      this.rayon = rayon;     
   }

   @Override
   public void affiche() {
       calque.strokeOval(x-rayon, y-rayon, 2*rayon, 2*rayon);
   }

   @Override
   public void changeLargeur(double valeur) {
      rayon = valeur/2;
      affiche();
   }
}

class Carré extends Forme {
   protected double côté;
   
   public Carré(double x, double y, double côté) {
      super(x, y);
      this.côté = côté;
   }

   @Override
   public void affiche() {
      calque.strokeRect(x-côté/2, y-côté/2, côté, côté);
   }

   @Override
   public void changeLargeur(double valeur) {
      côté = valeur;
   }
}

class CarréArrondi extends Carré {
   public CarréArrondi(double x, double y, double côté) {
      super(x, y, côté);    
   } 

   @Override
   public void affiche() {
      calque.strokeRoundRect(x-côté/2, y-côté/2, côté, côté, 20, 20);
   }  
}

class Hexagone extends Forme {
   private double largeur;
   private double[] abscisses;
   private double[] ordonnées;
   private final double RACINE = Math.sqrt(3)/4;

   public Hexagone(double x, double y, double largeur) {
      super(x, y);
      changeLargeur(largeur);
   }

   @Override
   public void changeLargeur(double largeur) {
      this.largeur = largeur;
      abscisses = new double[] {x+largeur/2, x+largeur/4, x-largeur/4, x-largeur/2, x-largeur/4, x+largeur/4};
      ordonnées = new double[] {y, y-RACINE*largeur, y-RACINE*largeur, y, y+RACINE*largeur, y+RACINE*largeur};
   }

   @Override
   public void affiche() {
      calque.strokePolygon(abscisses, ordonnées, 6);
   } 
}

class Texte implements Dessin {
   private double x, y;
   private String message;
   static GraphicsContext calque;

   public Texte(double x, double y, String message) {
      this.x = x;
      this.y = y;
      this.message = message;
   }

   @Override
   public void affiche() {
      calque.fillText(message, x, y);
   }  
}

class Coordonnées extends Texte {

   public Coordonnées(double x, double y) {
      super(x-33, y+5, "("+(int)x+", "+(int)y+")");
   } 
}
Panneau.java
package fx;

import java.io.*;
import java.util.*;
import javafx.scene.canvas.*;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.*;

enum TypeForme {CERCLE, CARRÉ, CARRÉARRONDI, HEXAGONE};

public class Panneau extends Pane { 
   private TypeForme type = TypeForme.CERCLE;
   private double largeur = 100;
   private GraphicsContext calque;
   private List<Dessin> dessins = new ArrayList<>();
   private FileChooser sélecteur = new FileChooser();
   private Alert alerte = new Alert(AlertType.WARNING);
   
   public Panneau() {  
      Canvas dessin = new Canvas(1920, 1024);
      calque = dessin.getGraphicsContext2D();  
      calque.setStroke(Color.CHOCOLATE);
      calque.setFill(Color.CHOCOLATE);
      Forme.calque = calque;
      Texte.calque = calque;
      setOnMouseClicked(evt -> {  tracerForme(evt.getX(), evt.getY()); });  
      getChildren().add(dessin);
      sélecteur.setInitialDirectory(new File("/home/manu/Documents"));
      alerte.setTitle("Problème de fichier");
   }
   
   public void changer(TypeForme forme)  { type = forme; }
   
   public void changer(double largeur)  { 
      this.largeur = largeur; 
      for (Dessin dessin : dessins) if (dessin instanceof Forme)   ((Forme)dessin).changeLargeur(largeur);
      affiche();
   }
   
   public void changer(Color couleur) {
      Forme.calque.setStroke(couleur);
      Texte.calque.setFill(couleur);
      affiche();
   }
   
   private void affiche() {
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      for (Dessin dessin : dessins) dessin.affiche();
   }
   
   private void tracerForme(double x, double y) {
      Forme forme = null;
      switch (type) {
            case CERCLE           : forme = new Cercle(x, y, largeur/2); break;
            case CARRÉ            : forme = new Carré(x, y, largeur); break;
            case CARRÉARRONDI     : forme = new CarréArrondi(x, y, largeur); break;
            case HEXAGONE         : forme = new Hexagone(x, y, largeur); break;
      }    
      dessins.add(forme);
      forme.affiche();
      Coordonnées coordonnées = new Coordonnées(x, y);
      dessins.add(coordonnées);
      coordonnées.affiche();      
   }
       
   public void toutEffacer() { 
      calque.clearRect(0, 0, getWidth(), getHeight()); 
      dessins.clear();
   }     
   
   public void enregistrer() {
      sélecteur.setTitle("Enregistrement des dessins");
      File fichier = sélecteur.showSaveDialog(null);
      if (fichier!=null) {
         try {
            ObjectOutputStream fluxDessins = new ObjectOutputStream(new FileOutputStream(fichier));
            fluxDessins.writeObject(dessins);
            fluxDessins.close();
         } 
         catch (IOException ex) {
            alerte.setHeaderText("Impossible de générer le fichier");
            alerte.setContentText("Le fichier : "+fichier.getName()+" n'a pas été créé");
            alerte.showAndWait();
         }
      }
   }
   
   public void ouvrir() {
      sélecteur.setTitle("Récupérer des dessins");
      File fichier = sélecteur.showOpenDialog(null);
      if (fichier!=null) {
         try {
            ObjectInputStream fluxDessins = new ObjectInputStream(new FileInputStream(fichier));
            dessins = (List<Dessin>) fluxDessins.readObject();
            fluxDessins.close();
            affiche();
         } 
         catch (Exception ex) {
            alerte.setHeaderText("Impossible de lire correctement le fichier");
            alerte.setContentText("("+fichier.getName()+") n'est pas un fichier de dessins");
            alerte.showAndWait();
         } 
      }      
   }
}
FormesFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class FormesFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Formes.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Placement de formes");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Formes.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.*?>
<?import fx.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane prefHeight="420" prefWidth="600" style="-fx-background-color: #FAEBD7;" 
            xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.FormesController">
   <center>
      <Panneau fx:id="panneau" prefHeight="374.0" prefWidth="562.0" BorderPane.alignment="CENTER" />
   </center>
   <top>
      <ToolBar prefHeight="40.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <items>
          <Button mnemonicParsing="false" onAction="#toutEffacer">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/nouveau.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip>
                  <Tooltip text="Supprime définitivement tous les dessins présent sur la feuille" />
               </tooltip>      
            </Button>
            <Button mnemonicParsing="false" onAction="#enregistrer">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/enregistrer.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip>
                  <Tooltip text="Enregistre tous les dessins présent sur la feuille" />
               </tooltip>               
            </Button>
            <Button mnemonicParsing="false" onAction="#ouvrir">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/ouvrir.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip>
                  <Tooltip text="Ouvrir un fichier de dessins et écrase ceux présents sur la feuille" />
               </tooltip>
            </Button>
            <RadioButton onAction="#formeCirculaire" selected="true" text="Cercle">
               <toggleGroup>
                  <ToggleGroup fx:id="formes" />
               </toggleGroup></RadioButton>
            <RadioButton onAction="#formeCarrée" text="Carré" toggleGroup="$formes" />
            <RadioButton onAction="#formeCarréeArrondi" text="Carré arrondi" toggleGroup="$formes" />
            <RadioButton onAction="#formeHexagonale" text="Hexagone" toggleGroup="$formes" />
            <TextField fx:id="largeur" onAction="#changerLargeur" prefHeight="25.0" prefWidth="49.0" text="100" />
            <ColorPicker fx:id="couleur" onAction="#changerCouleur" prefWidth="50.0" />
        </items>
         <effect>
            <DropShadow />
         </effect>
      </ToolBar>
   </top>
</BorderPane>
FormesController.java
package fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.paint.Color;

public class FormesController implements Initializable {
   
   @FXML private Panneau panneau;  
   @FXML private TextField largeur;
   @FXML private ColorPicker couleur;
   
   @FXML private void formeCirculaire()     { panneau.changer(TypeForme.CERCLE); }
   @FXML private void formeCarrée()         { panneau.changer(TypeForme.CARRÉ); }     
   @FXML private void formeCarréeArrondi()  { panneau.changer(TypeForme.CARRÉARRONDI);  } 
   @FXML private void formeHexagonale()     { panneau.changer(TypeForme.HEXAGONE); }
   @FXML private void changerLargeur()      { panneau.changer(Double.parseDouble(largeur.getText())); }   
   @FXML private void toutEffacer()         { panneau.toutEffacer();  }
   @FXML private void enregistrer()         { panneau.enregistrer(); }
   @FXML private void ouvrir()              { panneau.ouvrir(); }
   @FXML private void changerCouleur()      { panneau.changer(couleur.getValue()); }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) { 
      couleur.setValue(Color.CHOCOLATE);
   }     
}
Diagramme des classes du projet

Éditeur HTML

Après avoir pris connaissance des flux en Java, nous allons maintenant les utiliser afin de pouvoir communiquer au travers du réseau. Notre objectif ici est d'ouvrir une socket à l'aide de la classe prévue à cet effet qui se nomme justement Socket. Avec un éditeur simple, nous allons afficher le document HTML de votre choix. Il suffit alors de proposer une URL correspondant au site que vous désirez atteindre. Si l'URL n'est pas correcte, une boîte d'alerte est alors proposée.

Cours préliminaires
Description du projet

HTML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class HTML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("HTML.fxml"));
      Scene scene = new Scene(root);  
      fenêtre.setTitle("Éditeur HTML");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
HTML.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="413.0" prefWidth="534.0" xmlns="http://javafx.com/javafx/8" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.HTMLController">
   <children>
      <TextField fx:id="adresse" layoutX="14.0" layoutY="14.0" prefHeight="25.0" prefWidth="413.0" 
              AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="107.0" AnchorPane.topAnchor="14.0" />
      <Button layoutX="433.0" layoutY="14.0" mnemonicParsing="false" onAction="#soumettre" text="Soumettre" 
              AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0" />
      <TextArea fx:id="document" layoutX="14.0" layoutY="47.0" 
              AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="47.0" />
   </children>
</AnchorPane>
HTMLController.java
package fx;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;

public class HTMLController implements Initializable {
   private Socket service;
   @FXML private TextField adresse;
   @FXML private TextArea document;
   
   @FXML
   private void soumettre() {
      try {
         service = new Socket(adresse.getText(), 80);
         PrintWriter requête = new PrintWriter(service.getOutputStream(), true);
         Scanner réponse = new Scanner(service.getInputStream());
         requête.println("GET / HTTP/1.0\n");
         document.clear();
         while (réponse.hasNextLine()) document.appendText(réponse.nextLine()+"\n");
         service.close();
      } 
      catch (IOException ex) {
         Alert attention = new Alert(Alert.AlertType.WARNING);
         attention.setTitle("Information");
         attention.setHeaderText("Problème d'adresse");
         attention.setContentText("Cette URL n'est pas compatible");
         attention.showAndWait();
      }
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {   }     
}
Modification du projet initial

Nous allons modifier le projet précédent de telle sorte que le document HTML puisse être interprété afin d'avoir une visualisation classique de la page WEB. Cela peut se faire à l'aide d'un objet de type HTTPEditor en lieu et place d'un éditeur basique TextArea. HTTPEditor permet d'avoir un texte enrichi avec la notion de paragraphes, la mise en italique, le mise en gras, la couleur du texte, etc.

HTML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class HTML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("HTML.fxml"));
      Scene scene = new Scene(root);  
      fenêtre.setTitle("Éditeur HTML");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
HTML.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.web.*?>

<AnchorPane prefHeight="413.0" prefWidth="534.0" xmlns="http://javafx.com/javafx/8" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.HTMLController">
   <children>
      <TextField fx:id="adresse" layoutX="14.0" layoutY="14.0" prefHeight="25.0" prefWidth="413.0" 
              AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="107.0" AnchorPane.topAnchor="14.0" />
      <Button layoutX="433.0" layoutY="14.0" mnemonicParsing="false" onAction="#soumettre" text="Soumettre" 
              AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0" />
      <HTMLEditor fx:id="document" layoutX="14.0" layoutY="47.0" 
              AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="47.0" />
   </children>
</AnchorPane>
HTMLController.java
package fx;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;

public class HTMLController implements Initializable {
   private Socket service;
   @FXML private TextField adresse;
   @FXML private HTMLEditor document;
   
   @FXML
   private void soumettre() {
      try {
         service = new Socket(adresse.getText(), 80);
         PrintWriter requête = new PrintWriter(service.getOutputStream(), true);
         Scanner réponse = new Scanner(service.getInputStream());
         requête.println("GET / HTTP/1.0\n");
         StringBuilder html = new StringBuilder();
         while (réponse.hasNextLine()) html.append(réponse.nextLine()+"\n");
         service.close();
         document.setHtmlText(html.toString());
      } 
      catch (IOException ex) {
         Alert attention = new Alert(Alert.AlertType.WARNING);
         attention.setTitle("Information");
         attention.setHeaderText("Problème d'adresse");
         attention.setContentText("Cette URL n'est pas compatible");
         attention.showAndWait();
      }
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {   }     
}
Fabrication d'un navigateur rudimentaire

La version précédente permet de visualiser une page Web avec un bon rendu. Toutefois, il n'est pas possible de naviguer entre différentes pages d'un même site. JavaFX propose des classes de très haut niveau qui vont permettre cette navigation simple avec un rendu classique. La première WebEngine s'occupe du moteur du navigateur et permet de naviguer entre les pages avec la gestion d'un historique, la prise en compte directe des URLs sans passer par l'ouverture explicite d'une socket. La deuxième WebView s'occupe uniquement de l'aspect visuel et affiche automatiquement les pages que la classe WebEngine en association lui propose dans sa navigation.

HTML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class HTML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("HTML.fxml"));
      Scene scene = new Scene(root);  
      fenêtre.setTitle("Navigateur");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
HTML.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.web.*?>

<AnchorPane prefHeight="413.0" prefWidth="534.0" xmlns="http://javafx.com/javafx/8" 
                      xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.HTMLController">
   <children>
      <TextField fx:id="adresse" layoutX="14.0" layoutY="14.0" onAction="#soumettre" prefHeight="25.0" prefWidth="380.0" 
                 AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="140.0" AnchorPane.topAnchor="14.0" />
      <Button layoutX="402.0" layoutY="14.0" mnemonicParsing="false" onAction="#soumettre" 
                 AnchorPane.rightAnchor="98.0" AnchorPane.topAnchor="14.0">        
         <graphic>
            <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
               <image>
                  <Image url="@../icônes/Apply.png" />
               </image>
            </ImageView>
         </graphic>
</Button> <WebView fx:id="navigateur" layoutX="14.0" layoutY="47.0" AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="47.0" /> <Button layoutX="436.0" layoutY="14.0" mnemonicParsing="false" onAction="#back" AnchorPane.rightAnchor="56.0" AnchorPane.topAnchor="14.0"> <graphic> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <image> <Image url="@../icônes/Back.png" /> </image> </ImageView> </graphic> </Button> <Button layoutX="486.0" layoutY="14.0" mnemonicParsing="false" onAction="#forward" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0"> <graphic> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <image> <Image url="@../icônes/Forward.png" /> </image> </ImageView> </graphic> </Button> </children> </AnchorPane>
HTMLController.java
package fx;

import java.net.*;
import java.util.*;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.web.*;

public class HTMLController implements Initializable {
   private WebEngine moteurWeb;
   
   @FXML private TextField adresse;
   @FXML private WebView navigateur;
   
   @FXML private void soumettre() { moteurWeb.load(adresse.getText()); }  
   @FXML private void back()          { moteurWeb.getHistory().go(-1); }
   @FXML private void forward()     { moteurWeb.getHistory().go(1); }
   
   @Override 
   public void initialize(URL url, ResourceBundle rb) { 
      moteurWeb = navigateur.getEngine();
   }     
}

Service monétaire

Dans la section précédente, nous avons vu comment dialoguer au travers du réseau et comment créer une applications cliente capable de communiquer avec un service Web, au travers du protocole HTTP. Dans cette nouvelle rubrique, nous allons créer notre propre service personnalisé qui pemettra de réaliser des conversions monétaires. La mise en oeuvre d'un service se fait très simplement à l'aide d'une classe de haut niveau ServerSocket qui encapsule toutes les difficultés liées à la création d'un service au niveau du système d'exploitation. Avec une telle classe, la création d'un service devient trivial. Côté application serveur, chaque client connecté possèdera sa propre Socket qui permettra d'identifier le client. Comme pour l'application cliente, il faut penser à créer tous les flux nécessaires pour l'échange correcte des données.

Cours préliminaires
Élaboration du service

Dans cette première partie, nous allons mettre en oeuvre l'application côté serveur. Il s'agit de pouvoir convertir entre des €uros et des Francs. Nous allons en profiter pour placer les données importantes du service adresse du serveur et numéro de port dans un fichier de configuration qui sera utile à la fois pour le serveur et aussi pour l'application cliente. Nous réaliserons ce service directement dans le projet relatif à la conversion monétaire que nous avons déjà réalisé. Il suffira de modifier légèrement le code du client pour qu'il soit à même d'utiliser ce service. Cette partie sera traité dans une deuxième partie. L'échange entre le client et le serveur se fera systématiquement sous forme de textes. Dans la requête, vous devez spécifier la monnaie à convertir à la suite de la valeur dans une même requête.

ServiceMonétaire.java
package service;

import java.io.*;
import java.net.*;
import java.util.*;

public class ServiceMonétaire {
   private int port;
   private final double TAUX = 6.55957;
           
   public static void main(String[] args) { new ServiceMonétaire(); }      
   
   private ServiceMonétaire() {
      try {
         lireConfiguration();
         run();
      } 
      catch (IOException ex) {
         System.err.println("Impossible de trouver ou de lire le fichier de configuration");
      }
   }

   private void lireConfiguration() throws IOException {
      Properties configuration = new Properties();
      configuration.load(getClass().getResourceAsStream("../config/connexion.properties"));
      port = Integer.parseInt(configuration.getProperty("port"));
   }

   private void run() {
      try {
         ServerSocket service = new ServerSocket(port);
         System.out.println("Démarrage du service");
         while (true) {
            Socket client = service.accept();
            System.out.println("Requête de "+client.getInetAddress().getHostAddress());
            traitement(client);
            client.close();
         }
      } 
      catch (IOException ex) {
         System.err.println("Ce numéro de service est déjà utilisé");
      }
   }
   
   private void traitement(Socket client) throws IOException {
      Scanner requête = new Scanner(client.getInputStream());
      requête.useLocale(Locale.US);
      PrintWriter réponse = new PrintWriter(client.getOutputStream(), true);
      double valeur = requête.nextDouble();
      String monnaie = requête.next();
      switch (monnaie) {
         case "€" : réponse.println((valeur*TAUX)+" F"); break;
         case "F" : réponse.println((valeur/TAUX)+" €"); break;      
      }
   }
}
Le fichier de configuration est un fichier de propriétés, c'est-à-dire un fichier texte avec des valeurs associées à des clés séparée par le signe =. La classe Properties, prévue à cet effet, permet simplement de récupérer la valeur désirée en spécifiant la clé associée grâce à la méthode getProperty(). Puisque la communication de notre service se fait avec des échanges de textes, il est alors facile de tester le fonctionnement avec un simple client telnet, dont voici une expérience :

Développement de la partie cliente avec JavaFX

Après avoir effectué notre test avec l'utilitaire telnet, nous allons maintenant modifier l'application cliente qui nous avions déjà développée en rajoutant toute la partie communication avec le service. Effectivement, le traitement de la conversion ne se fait plus en local, mais sur un poste distant qui réalise la conversion comme nous venons de le voir dans cette première partie. Sur cette partie cliente, nous devons modifier la vue et le contrôleur.

Ici aussi, nous nous servons du fichier de configuration connexion.properties qui nous indique sur quelle machine se situe le service proposé avec son numéro associé.
Monnaie.java
package fx;

import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.util.converter.CurrencyStringConverter;

public class Monnaie extends TextField {
   private final StringProperty symbole = new SimpleStringProperty("€"); 
   private final DoubleProperty valeur = new SimpleDoubleProperty(0.0);
   private NumberStringConverter valeurMonnaie = new CurrencyStringConverter();

   public Monnaie() {  setAlignment(Pos.CENTER_RIGHT); }
   
   public double getValeur() {      
      setValeur(valeurMonnaie.fromString(getText()).doubleValue());
      return valeur.get(); 
   }

   public void setValeur(double nombre) {
      valeur.set(nombre);
      setText(valeurMonnaie.toString(nombre));
   }

   public String getSymbole() { return symbole.get();  }

   public void setSymbole(String monnaie) {
      symbole.set(monnaie);
      valeurMonnaie = new NumberStringConverter("#,##0.00 "+monnaie);
      setText(valeurMonnaie.toString(valeur.get()));
   }
   
   public DoubleProperty valeurProperty() { return valeur;  }
   public StringProperty symboleProperty() { return symbole;  } 
}
ConversionFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class BienvenueFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Conversion.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Conversion entre les €uros et les francs");
      fenêtre.setResizable(false);
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Conversion.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="81.0" prefWidth="430.0" xmlns="http://javafx.com/javafx/8" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.ConversionController">
   <children>
      <Button layoutX="370.0" layoutY="14.0" onAction="#calculerFrancs" text="-&gt; F" />
      <fx.Monnaie fx:id="euros" layoutX="14.0" layoutY="14.0" onAction="#calculerFrancs" 
                  prefHeight="25.0" prefWidth="349.0" text="0,00 €" />
      <fx.Monnaie fx:id="francs" layoutX="14.0" layoutY="45.0" onAction="#calculerEuros" 
                  prefHeight="25.0" prefWidth="349.0" symbole="F" />
      <Button layoutX="370.0" layoutY="45.0" onAction="#calculerEuros" text="-&gt; €" />
   </children>
</AnchorPane>
ConversionController.java
package fx;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.*;
import javafx.scene.control.*;

public class ConversionController implements Initializable {
   @FXML private Monnaie euros, francs;
   private String adresse;
   private int port;
   private Alert problème;
   
   @FXML
   private void calculerFrancs() {
      francs.setValeur(communication(euros.getValeur()+" €"));
   }
   
   @FXML
   private void calculerEuros() {
      euros.setValeur(communication(francs.getValeur()+" F"));
   }   
   
   private double communication(String monnaie) {
      try {
         Socket service = new Socket(adresse, port);
         PrintWriter requête = new PrintWriter(service.getOutputStream(), true);
         Scanner réponse = new Scanner(service.getInputStream());
         requête.println(monnaie);
         réponse.useLocale(Locale.US);
         return réponse.nextDouble();
      } 
      catch (IOException ex) {
         problème.showAndWait();
         return 0.0;
      }
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      // Configuration boîte d'alerte
      problème = new Alert(Alert.AlertType.ERROR);
      problème.setTitle("Alerte");     
      problème.setHeaderText("Problème de communication avec le service");   
      problème.setContentText("Service hors d'atteinte");
      // Récupération des informations sur la localisation du service
      ResourceBundle config = ResourceBundle.getBundle("config/connexion");
      adresse = config.getString("adresse");
      port = Integer.parseInt(config.getString("port"));
   }    
}
Au lieu de la classe Properties, JavaFX utilise plutôt la classe ResourceBundle. La méthode qui permet de récupérer la valeur désirée s'appelle getString(). Vous devez spécifiez là aussi la clé associée en argument de cette méthode. La méthode initialize() devient importante ici, puisqu'elle permet de configurer correctement la boîte de dialogue et de paramètrer les attributs relatifs à la communication avec le service dans le réseau local.
Développement de la partie cliente avec la librairie QT

Nous allons réutiliser ce service mais cette fois-ci avec un client développé avec la librairie QT. Vous verrez que le principe reste très similaire au client JavaFX sauf en ce qui concerne la gestion du réseau.

Avec JavaFX, lorsque vous utilisez les flux, ceux-ci sont bloquant, c'est-à-dire que lorsque nous demandons une lecture par exemple, tant que l'information n'arrive pas, l'exécution pu processus s'arrête sur cette ligne de code là. Ici, dans ce projet, cela ne pose pas de problème puisque dès que nous nous connectons, nous soumettons notre requête, le service renvoi instantanément la valeur et clôture la session. Finalement pour le client tout se passe très rapidement sans délai apparent. Dans le chaptitre suivant, pour la mise en oeuvre d'un chat, cela posera plus de problème puisque les connexions doivent être permanentes Avec la librairie QT, le principe de communication est très différent. La gestion du réseau se fait sous forme événementielle. Lorsqu'une socket reçoit un message du serveur, un événement adapté est alors sollicité. Grâce à ce principe, nous n'avons plus aucun blocage. Pendant l'attente d'une réponse éventuelle, nous pouvons vaquer à d'autres préoccupations. Par contre, si plusieurs réponses sont possibles, vous devez savoir à tout moment qu'elle est le type de requête que vous avez soumis. Ici aussi, nous nous servons d'un fichier de configuration connexion.ini. Il s'agit ici d'un fichier de type INI dont la structure propose des sections particulières que vous devez spécifier entre crochets ici Localisation. Il existe une classe spécialisée sur la lecture automatique de ce type de fichier, il s'agit de la classe QSetting.

ConversionSocket.pro
QT       += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = conversion
TEMPLATE = app
CONFIG += c++11
SOURCES += main.cpp principal.cpp
HEADERS  += principal.h
FORMS    += principal.ui
DISTFILES += connexion.ini
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include <QtNetwork>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private slots:
  void calculerEuros();
  void calculerFrancs();
  void reception();
private:
  void configuration();
  void soumettre(QString monnaie);
private:
  enum {AUCUN, EURO, FRANC} commande = AUCUN;
  QTcpSocket service;
  QString adresse;
  int port;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  configuration();
  connect(&service, SIGNAL(readyRead()), this, SLOT(reception()));
}

void Principal::configuration()
{
  QSettings config("connexion.ini", QSettings::IniFormat);
  adresse = config.value("Localisation/adresse").toString();
  port = config.value("Localisation/port").toInt();
  barreEtat->showMessage(QString("%1 %2").arg(adresse).arg(port));
}

void Principal::calculerFrancs()
{
  commande = FRANC;
  soumettre(QString("%1 €").arg(euro->value()));
}

void Principal::calculerEuros()
{
  commande = EURO;
  soumettre(QString("%1 F").arg(franc->value()));
}

void Principal::soumettre(QString monnaie)
{
  service.connectToHost(adresse, port);
  QTextStream requete(&service);
  requete << monnaie << endl;
}

void Principal::reception()
{
  if (service.canReadLine())
  {
    double monnaie = service.readLine().split(' ')[0].toDouble();
    switch (commande) {
      case FRANC : franc->setValue(monnaie); break;
      case EURO  : euro->setValue(monnaie); break;
      case AUCUN : barreEtat->showMessage("Choisissez votre monnaie");
    }
  }
}
La mise en oeuvre d'une socket se fait avec la classe QTcpSocket. Pour soumettre votre requête, vous devez préciser votre type de flux. Contrairement à Java, c'est beaucoup plus simple, il n'existe que deux types de flux, soit du texte avec QTextStream, soit du binaire avec QDataStream. L'avantage de ces flux, c'est qu'ils ont été construit avec l'utilisation des opérateurs comme avec cout. La réception du résultat se fait de façon événementielle, une méthode un slot doit être automatiquement sollicité dès qu'une information arrive vers le client. La classe QTcpSocket dispose de deux méthodes spécialiées suivant le type de flux choisi, soit readLine(), soit readAll(). Enfin, il est tout à fait possible de fabriquer un service en réseau local avec la librairie QT grâce à la classe QTcpServer. Nous aurions pu également construire le service à partir de la librairie QT. Dans le développement réseau, il est possible de mélanger les différentes technologies, le tout c'est de respecter le protocole d'échange.
Structures logicielle et matérielle

Pour conclure sur ce chapitre, je vous propose un diagramme de déploiement qui nous montre les différents éléments qu'il faut nécessairement avoir pour faire un déploiement approprié sur l'ensemble des machines utilisant ce système client-serveur sur un parc machines en réseau local.

Service Chat

Le service précédent permet de dialoguer entre un seul client et le service. La durée de communication est très courte, juste le temps de renvoyer la réponse associée à la requête proposée. Du coup, nous n'avons pas eu besoin de mettre en place un système multi-tâche. Plusieurs clients peuvent se connecter, la réponse leurs sera donnée instantanément à chaque requête soumise. Il existe des situations toutefois où nous devons maintenir la connexion entre les intervenants, c'est le cas notamment avec le service chat qui met en relation plusieurs clients simultanéments. Dans ce cas de figure, nous sommes obligé de prévoir au niveau du service un ensemble de threads pour chacun des clients connectés qui assureront le multi-service. C'est ce système que nous allons développer dans ce chapitre.

Cours préliminaires
Élaboration du service

Le service est décomposé en deux parties. La première partie concerne le service proprement dit qui attend une demande de connexion de la part des clients. Dès qu'un client se connecte, un thread est automatiquement lancé et le service se tient prêt à attendre une nouvelle connexion. Ce système est ainsi très réactif et peut répondre instantanément à toutes les demandes des clients. La deuxième partie concerne la mise en oeuvre du tread au travers d'une classe qui hérite de la classe Thread. Cette classe permet l'échanges des messages entre les différents clients.

ServiceChat.java
package service;

import java.io.*;
import java.net.*;

public class ServiceChat {
   private ServiceChat() {
      try {
         ServerSocket service = new ServerSocket(9876);
         System.out.println("Service en fonctionnement");
         while (true) {
            Socket client = service.accept();
            new ConnexionClient(client).start();
         }
      } 
      catch (IOException ex) {  System.err.println("Le service existe déjà"); }
   }

   public static void main(String[] args) { new ServiceChat(); }
}
ConnexionClient.java
package service;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;

class ConnexionClient extends Thread {
   private Scanner requête;
   private String login, destinataire, message;
   private static HashMap<String, PrintWriter> clients = new HashMap<>();

   public ConnexionClient(Socket client) throws IOException {
      requête = new Scanner(client.getInputStream());
      PrintWriter réponse = new PrintWriter(client.getOutputStream(), true);
      login = requête.next();
      clients.put(login, réponse);    
      System.out.println(login+" connecté");
      listeDesConnectés(true);
   }  

   @Override
   public void run() {
      while (true) {
         destinataire = requête.next();
         System.out.println(destinataire);
         message = requête.nextLine();
         System.out.println(message);
         if (message.equals(" stop")) break;
         clients.get(destinataire).println(login+" > "+message);
      }     
      System.out.println(login+" déconnecté");   
      clients.get(destinataire).println("stop"); 
      clients.remove(login);  
      listeDesConnectés(false);
   }  
   
   private void listeDesConnectés(boolean connecté) {
      for (Entry<String, PrintWriter> entrée : clients.entrySet()) {
         entrée.getValue().println(login + " vient de se "+(connecté ? "connecter":"déconnecter"));
         entrée.getValue().println("Connectés : "+clients.keySet().toString());     
      }
   }
}
La particularité du thread c'est qu'il est capable de connaître tous les clients déjà connectés, grâce à l'attribut de classe clients attribut statique qui utilise une collection de type Map. Il devient alors possible qu'un client puisse communiquer avec un autre client recensé en passant par ce service. De plus, chaque client est au courant qu'un nouveau client vient de se connecter ou de se déconnecter. Ensuite, à chaque connexion ou déconnexion, chaque client reçoit également la liste de tous les clients encore ou déjà connectés. Toutes ces fonctionnalités sont possibles si chaque client s'identifie dès la connexion. Cette identification doit être bien entendue unique.
Développement de la partie cliente en JavaFX

Comme précédemment, nous proposons un fichier de configuration pour localiser le service avec son numéro de port et nous rajoutons l'identifiant correspondant au prénom de l'utilisateur par exemple.

Dès que l'application démarre, grâce au fichier de configuration précédent, elle doit se connecter automatiquement au service. L'application reçoit alors automatiquement l'ensemble des autres clients déjà connectés. Il peut alors soumettre ses différents messages aux destinaires choisis. Dès que nous clôturons l'application, il faut aussi prévenir le service que l'utilisateur se déconnecte afin que les autres clients soient automatiquement avertis. Il faut dans ces différentes situations proposer un protocole adéquate.

ChatFXML.java
package client;

import javafx.application.Application;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.Stage;

public class ChatFXML extends Application {
   private ChatController contrôleur;
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      FXMLLoader chargeurFXML = new FXMLLoader();
      chargeurFXML.setLocation(getClass().getResource("Chat.fxml"));
      Parent root = chargeurFXML.load();
      contrôleur = chargeurFXML.getController();
      Scene scene = new Scene(root);
      fenêtre.setScene(scene);
      fenêtre.setTitle("Chat");
      fenêtre.show();
   }

   @Override
   public void stop() throws Exception {
      contrôleur.arrêt();
      super.stop();
   }
}
ATTENTION, le code ci-dessus n'est pas classique pour une classe principale d'application. Elle doit faire en sorte que lorsque nous demandons de quitter définitivement l'application, elle soit en relation directe avec le contrôleur qui doit, par son protocole particulier, avertir le service que l'opérateur en cours se déconnecte. Vous remarquez pour cela la redéfinition de la méthode de rappel stop() qui est automatiquement sollicité lorsque l'opérateur clique sur le bouton système de clôture. Avant de quitter définitivement l'application, la méthode arrêt() du contrôleur est d'abord sollicité. Pour permettre cette connexion directe avec le contrôleur, vous devez fabriquer un chargeur personnalisé qui permet d'avoir l'objet contrôleur défini par la vue lorsque celle-ci est préalablement chargée.
Chat.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="380.0" prefWidth="536.0" 
            xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="client.ChatController">
   <children>
      <Button layoutX="468.0" layoutY="14.0" onAction="#soumettre" text="Envoi"
            AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0" />
      <TextField fx:id="message" layoutX="14.0" layoutY="14.0" prefHeight="25.0" prefWidth="312.0" 
            promptText="Message" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="210.0" AnchorPane.topAnchor="14.0" />
      <TextField fx:id="destinataire" layoutX="333.0" layoutY="14.0" prefHeight="25.0" prefWidth="127.0" 
            promptText="Destinataire" AnchorPane.rightAnchor="76.0" AnchorPane.topAnchor="14.0" />
      <TextArea fx:id="zone" layoutX="14.0" layoutY="48.0" prefHeight="322.0" prefWidth="512.0" 
            AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="48.0" />
   </children>
</AnchorPane>
ChatController.java
package client;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.*;
import javafx.scene.control.*;

public class ChatController implements Initializable {  
   @FXML private TextField message;
   @FXML private TextField destinataire;
   @FXML private TextArea zone;
   
   private Scanner réponse;
   private PrintWriter envoi;
   private String identifiant;
   
   @FXML
   private void soumettre() {
      envoi.println(destinataire.getText()+' '+message.getText());
   }
   
   public void arrêt() {
      envoi.println(identifiant+" stop");
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      ResourceBundle config = ResourceBundle.getBundle("config/connexion");
      String adresse = config.getString("adresse");
      int port = Integer.parseInt(config.getString("port"));
      identifiant = config.getString("identifiant");
      try {
         Socket service = new Socket(adresse, port);
         envoi = new PrintWriter(service.getOutputStream(), true);
         envoi.println(identifiant);
         réponse = new Scanner(service.getInputStream());
         zone.appendText(réponse.nextLine()+"\n");
         new Thread() {
            @Override
            public void run() {
               while (true) { 
                  String message = réponse.nextLine();
                  if (message.equals("stop")) break;
                  zone.appendText(message+"\n");
               }
            }
         }.start();
      } 
      catch (IOException ex) {
         Alert problème = new Alert(Alert.AlertType.WARNING);
         problème.setHeaderText("Impossible de se connecter");
         problème.showAndWait();
      }
   }     
}
Au niveau du contrôleur, l'envoi des messages se fait toujours précédé du destinataire, afin que le service puisse connaitre systématiquement à qui envoyer le message. Pour la phase initiale de connexion, le client envoi l'identifiant de l'expéditeur. C'est la première chose qu'il fait. Pour expliquer au service que le client se déconnecte, il envoi dans le message le mot stop. Enfin, le client est constamment en attente de la réception d'un nouveau message venant d'un autre client grâce à une classe anonyme de type Thread.
Ce chat fonctionne parfaitement bien et illustre toutes les possibilités que donne la programmation réseau, notamment avec la connexion permanente entre les différents intervenants. Par contre, actuellement, pour des raisons de sécurité, cette fonctionnalité n'est possible qu'en réseau local. Le port utilisé va être bloqué par le parefeu. Si vous désirez offrir toutes ces fonctionnalités par Internet, vous devez passer par les WebSockets.
Développement de la partie cliente en C++ avec la librairie QT

Le principe des flux que nous utilisons en Java est un système blocant puisque nous sommes constamment en attente d'un message éventuel d'un autre client. Cela pose un problème, nous sommes obligé de prévoir également côté client un système de threads. Par contre, lorsque nous utilisons la librairie QT, nous n'avons pas ce genre de problème puisque la gestion du réseau se fait de façon événementielle. Ainsi, quand un message est reçu, nous pouvons alors le traiter à ce moment là, sinon nous pouvons vaquer à d'autres préoccupations, ce qui est plus facile à gérer.

Ici, nous recevons toujours le même type de message, nous n'avons pas à discriminer la requête en préalable. Du coup, le codage de la réception est très rudimentaire.
connexion.ini
[localisation]
adresse=192.168.1.36
port=9876
identifiant=manu
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include <QtNetwork>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private slots:
  void soumettre();
  void connexion();
  void reception();
private:
  void configuration();
protected:
  void closeEvent(QCloseEvent *cloture);
private:
  QTcpSocket service;
  QString adresse, identifiant;
  QTextStream requete;
  int port;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"
#include <QCloseEvent>

Principal::Principal(QWidget *parent) : QMainWindow(parent), requete(&service)
{
  setupUi(this);
  connect(&service, SIGNAL(readyRead()), this, SLOT(reception()));
  connect(&service, SIGNAL(connected()), this, SLOT(connexion()));
  configuration();
  service.connectToHost(adresse, port);
}

void Principal::configuration()
{
  QSettings config("connexion.ini", QSettings::IniFormat);
  adresse = config.value("localisation/adresse").toString();
  port = config.value("localisation/port").toInt();
  identifiant = config.value("localisation/identifiant").toString();
}

void Principal::connexion()
{
  requete << identifiant << endl;
}

void Principal::soumettre()
{
  requete << destinataire->text() << ' ' << message->text() << endl;
}

void Principal::closeEvent(QCloseEvent *cloture)
{
  requete << identifiant << " stop" << endl;
  cloture->accept();
}

void Principal::reception()
{
  while (service.canReadLine())
  {
    QByteArray texte = service.readLine();
    texte.remove(texte.size()-1, 1);
    zone->append(texte);
  }
}
Il existe une petite différence dans le traitement des chaînes de caractères en Java et en C++ au niveau des flux. Java envoi un \n retour à la ligne suivi d'un \r saut de ligne ce que ne fait pas le C++ qui lui envoi juste un \n. Du coup, dans la zone de l'application cliente, nous avons systématiquement un saut de ligne entre les messages, ce qui n'est pas très agréable. Dans le traitement de la méthode reception(), nous enlevons ce dernier caractère, sinon le code serait très réduit : zone->append(service.readLine()).

Service Web REST - Service monétaire

Si vous devez utiliser vos applications dans le réseau local, l'approche du chapitre précédent convient tout à fait. Vous pouvez atteindre votre service n'importe où au sein du réseau. Puisque nous utilisons des sockets, il est également possible de maintenir votre connexion ouverte le temps que vous voulez. Par contre, si vous désirez que votre service puisse être utilisé ailleurs que dans votre réseau local en passant par le réseau Internet, cette technique devient problématique. Pour des raisons de sécurité, le numéro de service que vous avez choisi 3377 va être bloqué par le parefeu il ne faut surtout pas ouvrir ce port particulier vers l'extérieur. L'une des solutions qui vient alors à l'esprit est de mettre en oeuvre un service web REST qui a l'avantage de prendre les ports classiques 80 ou 8080 qui sont généralement ouverts par le parefeu. Ensuite, la communication se fait au travers du protocole HTTP qui est très fiable. Par contre, cela dépend des situations ici cela ne pose pas de problèmes, mais l'inconvénient c'est que la connexion entre le client et le serveur reste ouverte juste le temps de la réponse en association avec la requête proposée impossibilité d'avoir deux clients qui communiquent entre eux comme pour le service Chat.

Cours préliminaires
Élaboration du service

Avant de réaliser le codage de ce web service, je vous invite fortement à consulter le cours complet sur les Services Web REST proposé en lien ci-dessus. Ce service est très très simple à mettre en oeuvre, ici nous n'utilisons que la méthode GET du protocole HTTP.

Tout se situe au niveau du serveur d'applications GlassFish. Vous devez mettre en place un projet de type application web. Ici, vous n'utilisez plus le Java SE mais Java EE.

Pour que le système soit opérationnel, pensez bien à renseigner le descripteur de déploiement web.xml qui sert de contrôleur.
web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	 version="3.1">
   
    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>
Grâce au système des annotations, même avec un grand nombre de fonctionnalités, le code source est très réduit, beaucoup plus simple à réaliser que l'équivalent élaboré par socket. Vous n'avez plus à vous préoccuper des types de flux ni de la notion de multi-utilisateur donc de multi-tâche. Votre seul soucis, par contre, est de connaître les types mimes et les URLs à prendre en compte.
rest.MonnaieWS.java
package rest;

import javax.ws.rs.*;

@Path("/")
@Produces("text/plain")
public class MonnaieWS {
   private final double TAUX = 6.55927;
   
   @GET
   public String accueil() {
      return "Conversion entre les euros et les francs\nPlacez vos valeurs dans l'URL, comme suit : 15.24F";
   }
   
   @Path("{euro}E")
   @GET
   public String euroFranc(@PathParam("euro") double euro) {
      return (euro * TAUX) + " Francs";
   }
   
   @Path("{franc}F")
   @GET
   public String francEuro(@PathParam("franc") double franc) {
      return (franc / TAUX) + " Euros";
   }   
}
Pour utiliser un service web REST, vous devez systématiquement soumettre une URL qui correspond exactement à la requête souhaitée c'est le principe même de ce type de service. Comme nous ne prenons que la méthode GET du protocole HTTP, il est très facile de tester notre service avec un simple navigateur. Ce service peut être exploiter partout dans le monde. C'est un avantage énorme vu le peu de code à écrire.

Développement de la partie cliente avec JavaFX

Un autre gros avantage d'un service Web, contrairement à une apllication Web qui nécessite systématiquement un navigateur, est qu'il peut également être exploité par une application cliente classique. Il suffit juste de respecter le protocole HTTP. Par ailleurs, il peut s'agir de n'importe quel type de client : PC, tablette, smartphone, informatique embarqué, etc. Pour notre exemple, nous reprenons le projet précédent, avec la seule modification porte sur la connexion réseau du contrôleur.

Nous nous servons encore du fichier de configuration connexion.properties qui nous indique sur quelle machine se situe le service proposé avec son numéro de port réglé à 8080.
Monnaie.java
package fx;

import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.util.converter.CurrencyStringConverter;

public class Monnaie extends TextField {
   private final StringProperty symbole = new SimpleStringProperty("€"); 
   private final DoubleProperty valeur = new SimpleDoubleProperty(0.0);
   private NumberStringConverter valeurMonnaie = new CurrencyStringConverter();

   public Monnaie() {  setAlignment(Pos.CENTER_RIGHT); }
   
   public double getValeur() {      
      setValeur(valeurMonnaie.fromString(getText()).doubleValue());
      return valeur.get(); 
   }

   public void setValeur(double nombre) {
      valeur.set(nombre);
      setText(valeurMonnaie.toString(nombre));
   }

   public String getSymbole() { return symbole.get();  }

   public void setSymbole(String monnaie) {
      symbole.set(monnaie);
      valeurMonnaie = new NumberStringConverter("#,##0.00 "+monnaie);
      setText(valeurMonnaie.toString(valeur.get()));
   }
   
   public DoubleProperty valeurProperty() { return valeur;  }
   public StringProperty symboleProperty() { return symbole;  } 
}
ConversionFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class BienvenueFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Conversion.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Conversion entre les €uros et les francs");
      fenêtre.setResizable(false);
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Conversion.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import fx.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="81.0" prefWidth="430.0" xmlns="http://javafx.com/javafx/8" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.ConversionController">
   <children>
      <Button layoutX="370.0" layoutY="14.0" onAction="#calculerFrancs" text="-&gt; F" />
      <fx.Monnaie fx:id="euros" layoutX="14.0" layoutY="14.0" onAction="#calculerFrancs" 
                  prefHeight="25.0" prefWidth="349.0" text="0,00 €" />
      <fx.Monnaie fx:id="francs" layoutX="14.0" layoutY="45.0" onAction="#calculerEuros" 
                  prefHeight="25.0" prefWidth="349.0" symbole="F" />
      <Button layoutX="370.0" layoutY="45.0" onAction="#calculerEuros" text="-&gt; €" />
   </children>
</AnchorPane>
ConversionController.java
package fx;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.*;
import javafx.scene.control.*;

public class ConversionController implements Initializable {
   @FXML private Monnaie euros, francs;
   private String adresse;
   private int port;
   private Alert problème;
   
   @FXML
   private void calculerFrancs() {
      francs.setValeur(communication(euros.getValeur()+"E"));
   }
   
   @FXML
   private void calculerEuros() {
      euros.setValeur(communication(francs.getValeur()+"F"));
   }   
   
   private double communication(String monnaie) {
      try {
         URL url = new URL("http://"+adresse+":"+port+"/monnaie/"+monnaie);
         HttpURLConnection client = (HttpURLConnection) url.openConnection();
         client.setRequestMethod("GET");
         Scanner réponse = new Scanner(client.getInputStream());
         réponse.useLocale(Locale.US);
         return réponse.nextDouble();
      } 
      catch (IOException ex) {
         problème.showAndWait();
         return 0.0;
      }
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      // Configuration boîte d'alerte
      problème = new Alert(Alert.AlertType.ERROR);
      problème.setTitle("Alerte");     
      problème.setHeaderText("Problème de communication avec le service");   
      problème.setContentText("Service hors d'atteinte");
      // Récupération des informations sur la localisation du service
      ResourceBundle config = ResourceBundle.getBundle("config/connexion");
      adresse = config.getString("adresse");
      port = Integer.parseInt(config.getString("port"));
   }    
}
La classe importante pour communiquer au travers du protocole HTTP est la classe HttpUrlConnection. Il suffit de spécifier la méthode HTTP du protocole que vous souhaitez soumettre au moyen de la méthode setRequestMethod(). Pour l'obtention du résultat, il suffit de gérer les flux d'entrées ou de sortie pour les autres types de méthode HTTP grâce à la méthode getInputStream().
Développement de la partie cliente avec la librairie QT

Là aussi, nous allons nous servir du projet précédent développé en C++ avec la librairie Qt. Pour communiquer avec un Service Web REST, QT propose trois classes pour résoudre toutes les situations possibles :

  1. QNetworkAccessManager : C'est au travers de cette classe que nous établissons la communication avec le service Web distant. Je rappelle que dans la philosophie de QT, la communication réseau se fait au travers d'une gestion événementielle. Ainsi, à chaque fois qu'une requête sera proposée, un événement sera automatiquement lancé dès que la réponse sera prête. Cette classe est également spécialisée dans la communication au travers du protocole HTTP. Ainsi, elle possède des méthodes toutes prêtes pour traduire tous les souhaits de l'utilisateur, grâce notamment aux méthodes : get(), post(), put() et deleteResource() qui font appel aux méthodes respectives du protocole HTTP : GET, POST, PUT et DELETE.
  2. QNetworkRequest : Comme sont nom l'indique, cette classe nous permet d'élaborer les différentes requêtes requises en proposant à chaque fois la bonne URL en adéquation avec ce que souhaite le web service REST. L'objet ainsi créé servira d'argument à l'une des méthodes précédentes - get(), post(), put() et deleteResource() - de la classe QNetworkAccessManager.
  3. QNetworkReply : Cette classe représente la réponse à la requête sollicité par QNetworkRequest. En réalité, l'objet de cette classe est un paramètre d'une méthode SLOT qui sera automatiquement appelée lorsque effectivement une réponse sera reçue du service Web RESTgestion événementielle. Bien entendu, cette classe dispose de méthodes adaptées à la gestion du résultat, avec notamment : la méthode error() qui nous prévient si la réponse a été correctement envoyée, la méthode readAll() qui nous retourne la totalité du contenu espéré, la méthode readLine() qui récupère le texte reçu ligne par ligne, canReadLine() qui teste si il existe encore une ligne de texte à lire, etc.

Nous nous servons encore du fichier de configuration connexion.ini qui nous indique sur quelle machine se situe le service proposé avec son numéro de port réglé à 8080.
ConversionREST.pro
QT       += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
TARGET = ConversionREST
TEMPLATE = app
SOURCES += main.cppprincipal.cpp
HEADERS  += principal.h
FORMS    += principal.ui
DISTFILES += connexion.ini 
connexion.ini
[localisation]
adresse=192.168.1.12
port=8080
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>

#include "ui_principal.h"

class Principal : public QMainWindow, public Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private slots:
  void calculerFrancs();
  void calculerEuros();
  void reception(QNetworkReply *resultat);
private:
  void configuration();
  void soumettre(QString monnaie);
private:
  enum {AUCUN, EURO, FRANC} commande = AUCUN;
  QNetworkAccessManager service;
  QString adresse;
  int port;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"
#include <QSettings>

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(finished(QNetworkReply*)), this, SLOT(reception(QNetworkReply *)));
  configuration();
}

void Principal::configuration()
{
  QSettings config("connexion.ini", QSettings::IniFormat);
  adresse = config.value("localisation/adresse").toString();
  port = config.value("localisation/port").toInt();
}

void Principal::calculerFrancs()
{
  commande = FRANC;
  soumettre(QString("%1E").arg(euro->value()));
}

void Principal::calculerEuros()
{
  commande = EURO;
  soumettre(QString("%1F").arg(franc->value()));
}

void Principal::soumettre(QString monnaie)
{
  QString url(QString("http://%1:%2/monnaie/%3").arg(adresse).arg(port).arg(monnaie));
  service.get(QNetworkRequest(QUrl(url)));
}

void Principal::reception(QNetworkReply *resultat)
{
  if (resultat->error() == QNetworkReply::NoError)
  {
     double monnaie = resultat->readLine().split(' ')[0].toDouble();
     switch (commande) {
       case FRANC : franc->setValue(monnaie); break;
       case EURO  : euro->setValue(monnaie); break;
     }
  }
  delete resultat;
}
La méthode reception() est appelée automatiquement après chaque envoi d'une requête. Elle prend en paramètre un objet de type QNetworkReply qui représente le résultat reçu. Il convient de vérifier systématiquement si l'opération s'est bien déroulée en appelant la méthode error(). Si effectivement la communication a pue être établie, nous pouvons récupérer la totalité de la donnée envoyée par le web service à l'aide de la méthode readAll() ou readLine(). ATTENTION! Une fois que vous avez bien récupéré votre valeur, vous devez systématiquement la supprimer du buffer de réception à l'aide de l'opération delete.

Service Web REST - Archivage de photos

Maintenant que nous maîtriser bien le principe de fonctionnement de ce type de service, nous allons élargir nos compétences en utilisant les principales méthodes de gestion du protocole HTTP qui permettent : la création, la modification, la récupération et la suppression des ressources, avec respectivement, les méthodes POST, PUT, GET et DELETE. Pour cela, nous allons élaborer un nouveau projet qui permet d'archiver des photos sur le serveur, afin de les retrouver plutard lorsque nous en aurons besoin, le tout en passant par Internet.

Cours préliminaires
Élaboration du service

Comme nous venons de l'évoquer en introduction, ce service utilise les principales méthodes du protocole HTTP.

web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	 version="3.1">
   
    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>
service.stockage.properties
# Répertoire de stockage des photos
répertoire=/home/manu/Applications/Archivage/
service.Archivage.java
package service;

import java.io.*;
import java.util.*;
import javax.ws.rs.*;

@Path("/")
public class Archivage {
   private final String répertoire;
   
   public Archivage() throws IOException {
      Properties config = new Properties();
      config.load(getClass().getResourceAsStream("stockage.properties"));
      répertoire = config.getProperty("répertoire");
      File rep = new File(répertoire);
      if (!rep.exists()) rep.mkdir();     
   }
   
   @GET
   @Produces("text/plain")
   public String listePhotos() { 
      String[] liste = new File(répertoire).list();  
      StringBuilder noms = new StringBuilder();
      for (String nom : liste) noms.append(nom.split(".jpg")[0]+'\n');
      return noms.toString();  
   }
   
   @GET
   @Path("{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom+".jpg");
   }  
   
   @POST
   @Path("{nomFichier}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      FileOutputStream fichier = new FileOutputStream(répertoire+nom+".jpg");
      byte[] octets = new byte[1024];
      int octetsLus = 0;
      while ((octetsLus = flux.read(octets)) > 0)  fichier.write(octets, 0, octetsLus); 
      fichier.close();
   }
   
   @PUT
   @Path("change")
   public void  changerNom(@QueryParam("ancien") String ancien, @QueryParam("nouveau") String nouveau) {
      new File(répertoire+ancien+".jpg").renameTo(new File(répertoire+nouveau+".jpg"));
   }
   
   @DELETE
   public void supprimer(@HeaderParam("nomFichier") String nom) {
      new File(répertoire+nom+".jpg").delete();
   }   
}
Pour tester ces différentes méthodes de l'objet distant en association avec les méthodes du protocole HTTP, nous allons utiliser dans un premier temps un petit plugin, Poster associé au navigateur Firefox, qui est spécialisé dans la communication avec les services Web REST. Là où il sera particulièrement utile c'est pour créer une nouvelle ressource POST, pour modifier une ressource existante PUT ou pour la supprimer définitivement DELETE.

Le résultat à l'issu de ces trois opérations :

Développement de la partie cliente avec JavaFX

Maintenant que nous avons bien vérifié les bonnes fonctionnalités de notre service Web REST, je vous propose de réaliser une application cliente qui permet d'archiver des photos. Cette application permet de récupérer des photos stockées sur le disque local afin de les archiver à distance suivant le besoin. Il est possible également, grâce à cette application, de visualiser les photos déjà archivées sur le serveur. Nous avons la possibilité de modifier le nom des photos ou détruite celles qui ne sont plus d'actualité. Enfin, il est possible de stocker dans le disque local certaines des photos archivées.

Comme nous en avons pris l'habitude, nous nous servons encore d'un fichier de configuration url.properties qui nous précise sur quelle machine se situe le service proposé, avec son application web à utiliser, ainsi que son numéro de port réglé à 8080.
config.url.properties
# URL d'acces au service Web REST d'archivage de photos
URL=http://192.168.1.12:8080/Archivage/
PhotosFXML.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class PhotoFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Photo.fxml"));     
      Scene scene = new Scene(root);
      fenêtre.setTitle("Archivage de photos");
      fenêtre.setScene(scene);
      fenêtre.show();
   }
}
Photo.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane fx:id="panneau" prefHeight="515.0" prefWidth="691.0" xmlns="http://javafx.com/javafx/8" 
                      xmlns:fx="http://javafx.com/fxml/1" fx:controller="photos.PhotoController">
   <center>
         <ImageView fx:id="vignette" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" />
   </center>
   <top>
      <ToolBar prefHeight="40.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <items>
            <Label text="Local" textFill="#3407e3" />
            <Button onAction="#chargerPhoto">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/ouvrir.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Récupère une photo du disque dur local" /></tooltip>               
            </Button>
            <Button onAction="#enregistrer"  mnemonicParsing="false">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/enregistrer.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Sauvegarde la photo sur le disque dur local" /></tooltip>  
            </Button>
            <TextField fx:id="nomFichier" prefHeight="25.0" prefWidth="139.0" />
            <Separator orientation="VERTICAL" prefHeight="16.0" />
            <Label text="Serveur" textFill="#4a02f2" />
            <Button onAction="#archiver"  mnemonicParsing="false">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/Up.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Archive la photo sur le serveur distant" /></tooltip>  
            </Button>
            <Button onAction="#modifier" layoutX="112.0" layoutY="10.0" mnemonicParsing="false">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/Update.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Change le nom de la photo sur le serveur distant" /></tooltip>  
            </Button>
            <Button layoutX="158.0" layoutY="10.0" mnemonicParsing="false" onAction="#listePhotosArchivées">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/Down.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Donne la liste des photos stockées sur le serveur distant" /></tooltip> 
            </Button>
            <Button onAction="#supprimer" layoutX="204.0" layoutY="10.0" mnemonicParsing="false">
               <graphic>
                  <ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
                     <image>
                        <Image url="@../icônes/Delete.png" />
                     </image>
                  </ImageView>
               </graphic>
               <tooltip><Tooltip text="Supprime une photo stockée sur le serveur distant" /></tooltip> 
            </Button>
            <ComboBox fx:id="liste" onAction="#récupérer" prefWidth="150.0" />
        </items>
         <effect>
            <DropShadow />
         </effect>
      </ToolBar>
   </top>
</BorderPane>
PhotoController.java
package photos;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;

public class PhotoController implements Initializable {
   private Image photo;
   private FileChooser sélecteurFichier = new FileChooser();
   private String urlBase;
   private String fichierPhoto;
   
   @FXML private BorderPane panneau;
   @FXML private ImageView vignette;
   @FXML private TextField nomFichier;
   @FXML private ComboBox liste;

   @FXML
   private void chargerPhoto() throws IOException {
      File fichier = sélecteurFichier.showOpenDialog(null);
      if (fichier!=null)  {
         photo = new Image(new FileInputStream(fichier));
         vignette.setImage(photo);
         redimensionner();
         nomFichier.setText(fichier.getName().split(".jpg")[0]);
         fichierPhoto = fichier.getAbsolutePath();
      }         
   }
   
   @FXML
   private void enregistrer() throws IOException {
      sélecteurFichier.setInitialFileName(nomFichier.getText()+".jpg");
      File fichier = sélecteurFichier.showSaveDialog(null);
      if (fichier!=null) {
         URL url = new URL(urlBase+nomFichier.getText());
         HttpURLConnection client = (HttpURLConnection) url.openConnection();
         client.setRequestMethod("GET");
         InputStream stream = client.getInputStream();      
         ByteArrayOutputStream octets = new ByteArrayOutputStream();
         byte[] buffer = new byte[1024]; 
         int octetsLus = 0;
         do {
            octetsLus = stream.read(buffer);
            if (octetsLus > 0) { octets.write(buffer, 0, octetsLus); }
         } 
         while (octetsLus > -1);
         byte[] octetsPhoto =  octets.toByteArray();
         FileOutputStream enregistrement = new FileOutputStream(fichier);
         enregistrement.write(octetsPhoto);
         enregistrement.close();       
      }
   }

   @FXML
   private void listePhotosArchivées() throws IOException {    
      URL url = new URL(urlBase);
      HttpURLConnection client = (HttpURLConnection) url.openConnection();
      client.setRequestMethod("GET"); 
      Scanner réponse = new Scanner(client.getInputStream());  
      liste.getItems().clear();
      while (réponse.hasNextLine()) liste.getItems().add(réponse.nextLine());
      liste.getSelectionModel().select(0); 
   }
   
   @FXML
   private void archiver() throws IOException {
      URL url = new URL(urlBase+nomFichier.getText());
      HttpURLConnection client = (HttpURLConnection) url.openConnection();
      client.setRequestProperty("Content-Type", "image/jpeg");
      client.setDoOutput(true);
      client.setRequestMethod("POST");
      File fichier = new File(fichierPhoto);
      byte[] octets = new byte[(int)fichier.length()];
      FileInputStream fluxOctets = new FileInputStream(fichier);
      fluxOctets.read(octets);
      fluxOctets.close();
      client.getOutputStream().write(octets);
      client.getResponseCode();
      listePhotosArchivées();
   }
   
   @FXML
   private void récupérer() throws IOException {     
      String nom = (String) liste.getValue();
      if (nom!=null) {
         nomFichier.setText(nom);
         URL url = new URL(urlBase+nom);
         HttpURLConnection client = (HttpURLConnection) url.openConnection();
         client.setRequestMethod("GET");
         photo = new Image(client.getInputStream());
         vignette.setImage(photo);
         redimensionner();
      }
   }
   
   @FXML
   private void modifier() throws IOException {
      String modifier = "change?ancien="+(String) liste.getValue()+"&nouveau="+nomFichier.getText();
      URL url = new URL(urlBase+modifier);
      HttpURLConnection client = (HttpURLConnection) url.openConnection();
      client.setRequestMethod("PUT");
      client.getResponseCode();         
      listePhotosArchivées();  
   }
   
   @FXML
   private void supprimer() throws IOException {
      URL url = new URL(urlBase);
      HttpURLConnection client = (HttpURLConnection) url.openConnection();
      client.setRequestProperty("nomFichier", nomFichier.getText());
      client.setRequestMethod("DELETE");
      client.getResponseCode();         
      listePhotosArchivées();      
   }

   public void redimensionner() {
      vignette.setFitWidth(panneau.getWidth());
   }

   @Override
   public void initialize(URL url, ResourceBundle rb) { 
      sélecteurFichier.setTitle("Choisissez votre photo");
      sélecteurFichier.setInitialDirectory(new File("/home/manu/Images"));
      sélecteurFichier.getExtensionFilters().add(new FileChooser.ExtensionFilter("Fichiers images", "*.png", "*.gif ", "*.jpg"));
      ResourceBundle config = ResourceBundle.getBundle("config/url");
      urlBase = config.getString("URL");
      panneau.widthProperty().addListener(e -> redimensionner());
   }   
}
La classe importante pour communiquer au travers du protocole HTTP est toujours la classe HttpUrlConnection. Il suffit de spécifier la méthode HTTP du protocole que vous souhaitez soumettre au moyen de la méthode setRequestMethod(). Lorsque vous soumettez une des méthodes HTTP POST, PUT et DELETE autre que la méthode GET, il est impératif de récupérer la réponse à votre requête au moyen de la méthode getResponseCode() qui sert d'accuser réception. Si vous ne le faite pas, la fonctionnalité n'est pas assurée. Enfin, si vous désirez envoyer un contenu une photo avec la méthode POST, vous devez préciser son type Content-Type et valider cette action à l'aide de la méthode setDoOutput(). Les deux méthodes les plus longues à décrire dans le contrôleur concerne enregistrer() et archiver(). Ceci est dû au fait que la classe Image ne permet pas de récupérer simplement les octets constituant la photo. Du coup, soit nous passons par le fichier du disque sans nous préoccuper du type de fichier pour l'envoyer sur le serveur distant, soit nous récupérons depuis le serveur l'ensemble des octets constituant la photo pour l'enregistrer sur le disque local toujours sans nous préoccuper du type de données.
Développement de la partie cliente avec la librairie QT

Nous allons reprendre les mêmes principes que nous avons évoqué dans la chapitre précédent avec la conversion monétaire en proposant une interface similaire à ce que nous venons de produire avec JavaFX.

Nous nous servons une fois de plus d'un fichier de configuration url.ini qui nous permet d'atteindre correctement le Web service d'archivage de photos, avec son numéro de port réglé à 8080.
Structure logicielle

IHM et Événements

PhotosQtREST.pro
QT       += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += C++11
TARGET = PhotosQtREST
TEMPLATE = app
SOURCES += main.cpp principal.cpp photo.cpp
HEADERS  += principal.h photo.h
FORMS    += principal.ui
DISTFILES += url.ini
RESOURCES += config.qrc 
url.ini
[serveur]
URL=http://192.168.1.12:8080/Archivage/
Photo.h
#ifndef PHOTO_H
#define PHOTO_H

#include <QWidget>
#include <QImage>

class Photo : public QWidget
{
  Q_OBJECT
public:
  explicit Photo(QWidget *parent = 0) {}
protected:
  void paintEvent(QPaintEvent *evt);
signals:
  void envoyerNom(QString message);
private slots:
  void chargerPhoto();
  void enregistrerPhoto();
public:
  void chargerPhoto(const QByteArray &octets);
  QByteArray getOctets() { return octets; }
private:
  QImage photo;
  QByteArray octets;
};

#endif // PHOTO_H 
Photo.cpp
#include "photo.h"
#include <QFileDialog>
#include <QPainter>

void Photo::chargerPhoto()
{
  QString nomFichier = QFileDialog::getOpenFileName(this, "Choix de votre photo","/home/manu/Images","Images (*.jpg *.jpeg)");
  if (!nomFichier.isEmpty())
  {
    QFile fichier(nomFichier);
    fichier.open(QIODevice::ReadOnly);
    octets = fichier.readAll();
    photo.loadFromData(octets);
    update();
    QFileInfo infoFichier(nomFichier);
    envoyerNom(infoFichier.baseName());
  }
}

void Photo::chargerPhoto(const QByteArray &octets)
{
  photo.loadFromData(this->octets = octets);
  update();
}

void Photo::paintEvent(QPaintEvent *evt)
{
  QPainter calque(this);
  if (!photo.isNull()) calque.drawImage(rect(), photo.scaledToWidth(rect().width()), rect());
  calque.setPen(QColor(100, 100, 100));
  calque.drawRect(0, 0, width()-1, height()-1);
}

void Photo::enregistrerPhoto()
{
  if (!photo.isNull())
  {
    QString nomFichier = QFileDialog::getSaveFileName(this, "Sauvegardez la photo");
    if (!nomFichier.isEmpty())
    {
      QFile fichier(nomFichier);
      fichier.open(QIODevice::WriteOnly);
      fichier.write(octets);
    }
  }
} 
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include "photo.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
signals:
  void info(const QString &message);
private slots:
  void listePhotos();
  void restituer();
  void archiver();
  void modifier();
  void supprimer();
  void reception(QNetworkReply *reponse);
private:
  void configuration();
private:
  QString urlBase;
  QNetworkAccessManager service;
  enum {AUCUN, ARCHIVER, LISTE, RESTITUER, MODIFIER, SUPPRIMER} commande = AUCUN;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"
#include <QSettings>

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(finished(QNetworkReply*)), this, SLOT(reception(QNetworkReply*)));
  configuration();
}

void Principal::configuration()
{
  QSettings config("url.ini", QSettings::IniFormat);
  urlBase = config.value("serveur/URL").toString();
}

void Principal::listePhotos()
{
  commande = LISTE;
  service.get(QNetworkRequest(QUrl(urlBase)));
}

void Principal::restituer()
{
  commande = RESTITUER;
  QString url = QString("%1%2").arg(urlBase).arg(nomFichier->text());
  service.get(QNetworkRequest(QUrl(url)));
}

void Principal::archiver()
{
  commande = ARCHIVER;
  QString url = QString("%1%2").arg(urlBase).arg(nomFichier->text());
  QNetworkRequest requete;
  requete.setUrl(QUrl(url));
  requete.setRawHeader("Content-Type", "image/jpeg");
  service.post(requete, photo->getOctets());
}

void Principal::modifier()
{
  commande = MODIFIER;
  QString url = QString("%1change?ancien=%2&nouveau=%3").arg(urlBase).arg(liste->currentText()).arg(nomFichier->text());
  QByteArray vide;
  service.put(QNetworkRequest(QUrl(url)), vide);
}

void Principal::supprimer()
{
  commande = SUPPRIMER;
  QNetworkRequest requete;
  requete.setUrl(QUrl(urlBase));
  requete.setRawHeader("nomFichier", nomFichier->text().toLatin1());
  service.deleteResource(requete);
}

void Principal::reception(QNetworkReply *reponse)
{
  if (reponse->error() == QNetworkReply::NoError)
  {
    switch (commande) {
      case LISTE :
        liste->clear();
        while (reponse->canReadLine()) {
            QString nomPhoto = reponse->readLine();
            nomPhoto.remove("\n");
            liste->addItem(nomPhoto);
          }
        break;
      case RESTITUER :
        photo->chargerPhoto(reponse->readAll());
        break;
      case ARCHIVER :
        info("La photo est archivée dans le serveur");
        listePhotos();
        break;
      case MODIFIER :
        info("Le nom de la photo dans le serveur a été modifié");
        listePhotos();
        break;
      case SUPPRIMER :
        info("La photo a été supprimée du serveur");
        listePhotos();
        break;
    }
  } else info("Problème avec le service d'archivage");
  delete reponse;
}
Dans ce projet, nous disposons d'une classe Photo spécialisé dans l'affichage et le stockage des octets constituant une photo, que ce soit à partir du disque local ou venant du serveur d'archivage.
Structures logicielle et matérielle

Pour conclure sur ce chapitre, je vous propose un diagramme de déploiement qui nous montre les différents éléments qu'il faut nécessairement avoir pour faire un déploiement approprié sur l'ensemble des machines utilisant ce système client-serveur sur un parc machines en réseau local et sur Internet.

Élaboration d'un service autonome en Java sans serveur d'applications

Les services Web REST sont très faciles à implémenter grâce à l'utilisation des annotations. L'inconvénient de cette architecture, c'est qu'il est nécessaire d'avoir un serveur d'applications qui intègre toutes ces fonctionnalités. Il est toutefois possible de développer un service Web REST dans une application java standard autonome sans avoir besoin du serveur d'applications. Dans ce cas de figure, il sera nécessaire d'intégrer dans votre projet un ensemble de librairies qui jouent le rôle de conteneur et qui font parties de la version entreprise édition de java. Ainsi, il sera possible d'intégrer de tels services dans une informatique embarquée, ce qui posserait plus de problèmes si nous devions installer un serveur d'applications à la place.

service.stockage.properties
# Répertoire de stockage des photos
répertoire=/home/pi/Archivage/
uri=http://localhost:8080/Archivage/ 
service.Archivage.java
package service;

import java.io.*;
import java.net.*;
import java.util.Properties;
import javax.ws.rs.*;
import com.sun.net.httpserver.HttpServer;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

@Path("/")
public class Archivage {
   private static String répertoire;
   
   @GET
   @Produces("text/plain")
   public String listePhotos() { 
      String[] liste = new File(répertoire).list();  
      StringBuilder noms = new StringBuilder();
      for (String nom : liste) noms.append(nom.split(".jpg")[0]+'\n');
      return noms.toString();  
   }
   
   @GET
   @Path("{nomFichier}")
   @Produces("image/jpeg")
   public InputStream restituer(@PathParam("nomFichier") String nom) throws FileNotFoundException {
      return new FileInputStream(répertoire+nom+".jpg");
   }  
   
   @POST
   @Path("{nomFichier}")
   @Consumes("image/jpeg")
   public void stocker(@PathParam("nomFichier") String nom, InputStream flux) throws IOException {     
      FileOutputStream fichier = new FileOutputStream(répertoire+nom+".jpg");
      byte[] octets = new byte[1024];
      int octetsLus = 0;
      while ((octetsLus = flux.read(octets)) > 0)  fichier.write(octets, 0, octetsLus); 
      fichier.close();
   }
   
   @PUT
   @Path("change")
   public void changerNom(@QueryParam("ancien") String ancien, @QueryParam("nouveau") String nouveau) {
      new File(répertoire+ancien+".jpg").renameTo(new File(répertoire+nouveau+".jpg"));
   }
   
   @DELETE
   public void supprimer(@HeaderParam("nomFichier") String nom) {
      new File(répertoire+nom+".jpg").delete();
   }   
   
   public static void main(String[] args) throws URISyntaxException, IOException { 
      Properties réglage = new Properties();
      réglage.load(Archivage.class.getResourceAsStream("stockage.properties"));
      répertoire = réglage.getProperty("répertoire");
      File rep = new File(répertoire);
      if (!rep.exists()) rep.mkdir();         
      URI uri = URI.create(réglage.getProperty("uri"));
      ResourceConfig config = new ResourceConfig(Archivage.class);     
      HttpServer service = JdkHttpServerFactory.createHttpServer(uri, config);
   }   
}
Il s'agit d'une application java classique en mode console qui possède la classe principale d'application Archivage avec sa méthode principale main() à l'intérieure de laquelle nous lançons le service Web REST grâce à la classe HttpServer avec les toutes les informations nécessaires au bon fonctionnement : la localisation, le numéro de port, le path de l'URL, et la classe qui implémente le service proprement dit. Mise à part cette particularité, nous retrouvons exactement la même écriture, avec toutes les annotations requises, que nous avions élaboré lors de la conception avec le serveur d'applications. Il est possible ensuite de lancer votre service en ligne de commande sur n'importe quel système qui possède une machine virtuelle Java. Remarquez la présence des archives dans le répertoire lib.

Service et protocole WebSocket - Service Chat

Les services Web REST est une technologie vraiment intéressante puisqu'elle permet de dialoguer par Internet. Elle possède toutefois une limite de taille, dès que le serveur donne sa réponse, le client se trouve déconnecté du serveur. C'est le principe même du protocole HTTP. Dans ce cadre là, il n'est pas possible de prévoir un échange entre clients, puisque le serveur ne peut pas interpeler un client de sa propre initiative. L'idéal serait de pouvoir utiliser l'architecture des sockets avec une connexion permanente, pas seulement en réseau local, mais également au travers d'Internet. C'est là qu'interviennent les services WebSocket qui permettent de fusionner les deux technologies que nous venons d'expérimenter socket au travers du Web. Le protocole websocket commence par établir une connexion en HTTP, sur le même port que le service Web classique le pare-feu est franchi, et demande ensuite un upgrade vers le protocole websocket proprement dit. Une fois que le changement est effectué, la connexion reste établie, et les échanges peuvent se faire comme pour une communication par sockets. Dans ce cadre là, le serveur peut, quand il le désire, envoyer des informations à n'importe lequel de ses clients.

Cours préliminaires
Différentes implémentations

Élaboration du service

La mise en oeuvre d'un Service WebSocket reprend le même principe qu'un Service Web REST, mais de façon plus simple et plus concise. Encore une fois, vous avez besoin du serveur GlassFish. Vous devez créer un projet de type Application Web, mais cette fois-ci, vous n'avez plus besoin du descripteur de déploiement web.xml, une seule classe annotée suffit, avec en son sein des méthodes annotées, dont voici l'architecture :

L’API met à disposition plusieurs types d’annotations
Annotation Rôle
@ServerEndpoint Déclare un Service WebSocket
@ClientEndpoint Déclare un Client qui communique avec un service WebSocket
@OnOpen Défini la méthode de rappel qui gère l'évenement d’ouverture de la connexion
@OnMessage Défini la méthode de rappel qui gère l'évenement de réception d’un message
@OnError Défini la méthode de rappel qui gère l'évenement lors d’une erreur
@OnClose Défini la méthode de rappel qui gère l'évenement de clôture de la connexion
ServiceChat.java 
package service;

import java.io.*;
import java.util.*;
import javax.websocket.*;
import javax.websocket.server.*;

@ServerEndpoint("/{login}")
public class ServiceChat {
   private String login;
   private static HashMap<String, Session> clients = new HashMap<>();

    @OnOpen
    public void ouverture(Session client, @PathParam("login") String login) throws IOException {
       this.login = login;
       clients.put(login, client);
       envoyerATous(login + " vient de se connecter");
       envoyerATous("Connectés : "+clients.keySet().toString());       
    }  
    
    @OnClose
    public void fermeture() throws IOException {
       clients.remove(login);
       envoyerATous(login + " vient de se déconnecter");
       envoyerATous("Connectés : "+clients.keySet().toString()); 
    }    
   
    @OnMessage
    public String chat(String réception) throws IOException{
       Scanner requête = new Scanner(réception);
       String destinataire = requête.next();
       String message = requête.nextLine();
       return envoyerMessage(destinataire, message) ? "Message envoyé" : "Cible inconnue";
    }
    
    private void envoyerATous(String message) throws IOException {
       for (Session client : clients.values())
         if (client.isOpen()) client.getBasicRemote().sendText(message);
    }

    private boolean envoyerMessage(String destinataire, String message) throws IOException {
       if (clients.containsKey(destinataire)) {
          clients.get(destinataire).getBasicRemote().sendText(login + " > " + message);
          return true;
       }
       return false;
    }
}
L'ossature générale du service ressemble à celui que nous avions conçu lors du projet sur les sockets. La différence tient essentiellement aux méthodes de rappel liées à l'ouverture, la fermeture et la transmission des messages. Lors de l'établissement de la connexion, vous devez préciser le login à la fin de l'URL spécifiant le protocole websocket. Le multi-tâche est géré automatiquement du fait qu'un objet est créé à chaque établissement de connexion. Chaque objet représente et suit un client en particulier. L'objet est ensuite automatiquement détruit lorsque le client se déconnecte à son tour. Le point de communication avec le client se fait au travers d'un objet de type Session. Grâce à la Map attribut statique, chaque client connaît l'ensemble des clients déjà connectés. Il est donc facile de transmettre un message à un destinataire répertorié. Il existe un site qui permet de tester simplement et rapidement notre Service Chat dont voici l'adresse : https://www.websocket.org/echo.html.

Développement de la partie cliente en JavaFX

Maintenant que nous avons validé le fonctionnement du service, nous allons réaliser une application cliente qui nous permettra d'échanger nos messages tranquillement depuis Internet. Pour cela, nous reprenons l'application que nous avons déjà mis en oeuvre lors de la communication par Socket auquel nous allons effectuer quelques petits changements, notamment sur le contrôleur. Comme pour l'élaboration du service, nous allons utiliser des annotations qui vont nous permettre de simplifier et d'alléger le code de façon assez significative. Par contre, pour cela, vous devez rajouter une librairie supplémentaire tyrus-standalone-client qui comporte tout ce qu'il faut pour communiquer par le protocole websocket.

Dès que l'application démarre, grâce au fichier de configuration précédent, elle doit se connecter automatiquement au service. L'application reçoit alors automatiquement l'ensemble des autres clients déjà connectés. Nous pouvons alors soumettre nos différents messages aux destinaires choisis.

ChatFXML.java 
package client;

import javafx.application.Application;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.Stage;

public class ChatFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Chat.fxml"));
      Scene scene = new Scene(root);
      fenêtre.setScene(scene);
      fenêtre.setTitle("Chat");
      fenêtre.show();
   }
}
Le code ci-dessus redevient normal. Nous n'avons pas besoin de nous préoccuper de la déconnexion, cela se fait automatiquement.
Chat.fxml 
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="380.0" prefWidth="536.0" 
            xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="client.ChatController">
   <children>
      <Button layoutX="468.0" layoutY="14.0" onAction="#soumettre" text="Envoi"
            AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0" />
      <TextField fx:id="message" layoutX="14.0" layoutY="14.0" prefHeight="25.0" prefWidth="312.0" 
            promptText="Message" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="210.0" AnchorPane.topAnchor="14.0" />
      <TextField fx:id="destinataire" layoutX="333.0" layoutY="14.0" prefHeight="25.0" prefWidth="127.0" 
            promptText="Destinataire" AnchorPane.rightAnchor="76.0" AnchorPane.topAnchor="14.0" />
      <TextArea fx:id="zone" layoutX="14.0" layoutY="48.0" prefHeight="322.0" prefWidth="512.0" 
            AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="48.0" />
   </children>
</AnchorPane>
ChatController.java 
package client;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.fxml.*;
import javafx.scene.control.*;
import javax.websocket.*;
import org.glassfish.tyrus.client.ClientManager;

@ClientEndpoint
public class ChatController implements Initializable {  
   @FXML private TextField message;
   @FXML private TextField destinataire;
   @FXML private TextArea zone;

   private String adresse;
   private Session session;
   
   @FXML
   private void soumettre() throws IOException {
      session.getBasicRemote().sendText(destinataire.getText()+' '+message.getText());
   }
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      try {
         ResourceBundle config = ResourceBundle.getBundle("config/url");
         adresse = config.getString("url");
         ClientManager client = ClientManager.createClient();
         URI uri = URI.create(adresse);
         client.connectToServer(this, uri);
      } 
      catch (IOException | DeploymentException  ex) {
         Alert problème = new Alert(Alert.AlertType.WARNING);
         problème.setHeaderText("Impossible de se connecter");
         problème.showAndWait();
      } 
   }     
   
   @OnOpen
   public void ouverture(Session session) {
      this.session = session;
   }
   
   @OnMessage
   public void réception(String message) {
      zone.appendText(message+'\n');
   }
}
Nous retrouvons les mêmes annotations que pour le service, @OnOpen et @OnMessage, la première nous permet de connaître la session et donc de pouvoir être en permanence en contact avec le service, la deuxième nous permet de récupérer le message venant du serveur et de l'afficher en conséquence. Vous remarquez également la présence de l'annotation @ClientEndpoint qui précise quelle est la classe qui permet de gérer la communication par le protocole websocket. Grâce à cette annotation, notre classe est maintenant capable de gérer le multi-tâche et de travailler de façon événementielle à l'image de ce que nous faisons avec la librairie QT. La connexion au service se fait dès la création du contrôleur au moyen de la classe WebSocketContainer.

Développement de la partie cliente en C++ avec la librairie QT

Nous reprenons l'ossature générale du projet défini avec les sockets. Cette fois-ci nous utilisons une classe spécialisée QWebSocket qui, comme son nom l'indique, encapsule toutes les fonctionnalités propre à ce protocole. Dans ce cadre là, nous n'avons plus à nous préoccuper de la gestion des flux, tout se fait automatiquement. Du coup, le code correspondant est très réduit.

Ici, nous recevons toujours le même type de message, nous n'avons pas à discriminer la requête en préalable. Du coup, le codage de la réception est très rudimentaire.
IHM et Événements 

ChatWebSocket.pro 
QT       += core gui websockets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = ChatWebSocket
TEMPLATE = app
CONFIG += c++11
SOURCES += main.cpp principal.cpp
HEADERS  += principal.h
FORMS    += principal.ui
DISTFILES += url.ini 
url.ini 
[serveur]
url=ws://192.168.1.36:8080/chat/manu
Principal.h 
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include <QWebSocket>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private slots:
  void envoiMessage();
  void receptionMessage(QString texte);
private:
  void configuration();
private:
  QWebSocket service;
  QString url;
};

#endif // PRINCIPAL_H
Principal.cpp 
#include "principal.h"
#include <QSettings>

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
  configuration();
  service.open(QUrl(url));
}

void Principal::configuration()
{
  QSettings config("url.ini", QSettings::IniFormat);
  url = config.value("serveur/url").toString();
}

void Principal::envoiMessage()
{
  QString requete = QString("%1 %2").arg(destinataire->text()).arg(message->text());
  service.sendTextMessage(requete);
}

void Principal::receptionMessage(QString texte)
{
  zone->append(texte);
}
La classe QWebSocket dispose de méthodes adaptées aux différentes situations suivant l'ordre de communication avec le service.
open()
L'établissement de la connexion se fait au travers de cette méthode avec laquelle vous devez spécifier l'URL correspondant au protocole avec l'identifiant du client.
sendTextMessage()
Lorsque vous désirez soumettre un nouveau message au service, vous appelez cette méthode spécifique.
textMessageReceived()
Il s'agit d'un signal qui est soumis lorsque le service envoi lui-même un message au client. Vous devez alors proposer un SLOT adapté qui permettra de récupérer le texte du message et de réagir en conséquence.

Élaboration du service en C++ avec la librairie QT

Dans ce que nous venons de construire, le code du service et le code de la partie cliente sont extrêmement réduits et très simples à mettre en oeuvre.  Le service est très concis puisque c'est le serveur d'applications qui gère la création des différents objets associés à l'ensemble des clients système multi-tâche. Par contre, il est nécessaire d'avoir ce serveur d'applications. Si vous souhaitez réaliser ce type de service sur une informatique embarquée, cela peut poser des problèmes d'installation. Dans ce cas de figure, il est possible de réaliser un service de cette nature grâce à la librairie QT.

La réalisation d'un service WebSocket en C++ se fait au moyen de la classe QWebSocketServer dans un programme classique utilisant les compétences de Qt. Vu que vous n'avez pas de serveur d'applications, vous êtes obligé de gérer le muti-tâche plutôt multi-utilisateur, qui en Qt se fait au moyen de la gestion événementielle. À chaque nouveau client connecté, il faut prévoir un événement pour la réception d'un nouveau message et un événement pour la déconnexion. Du coup, lors de cette déconnexion, il faut supprimer ces événements. Cette gestion alourdi un peu le code, mais elle nous permet de mieux comprendre les différentes interactions ce mode est caché dans le cadre d'un serveur d'applications.
ServiceChat.pro 
QT += core websockets
QT -= gui

TARGET = ServiceChat
CONFIG += console c++11
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp servicechat.cpp
HEADERS += servicechat.h
main.cpp 
#include <QCoreApplication>
#include "servicechat.h"

int main(int argc, char *argv[])
{
  QCoreApplication application(argc, argv);
  ServiceChat chat;
  QObject::connect(&chat, SIGNAL(stop()), &application, SLOT(quit()));
  return application.exec();
}
servicechat.h 
#ifndef SERVICECHAT_H
#define SERVICECHAT_H

#include <QObject>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
using namespace std;

class ServiceChat : public QObject
{
  Q_OBJECT
public:
  explicit ServiceChat(QObject *parent = 0);
private:
  void envoyerATous(const QString &message);
  void listeDesConnectes();
signals:
  void stop();
public slots:
  void nouvelleConnexion();
  void receptionMessage(const QString &texte);
  void deconnexion();
private:
  QWebSocketServer service;
  map<QWebSocket *, QString> connectes;
};

#endif // SERVICECHAT_H
servicechat.cpp 
#include "servicechat.h"

ServiceChat::ServiceChat(QObject *parent) : QObject(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
  if (service.listen(QHostAddress::Any, 8080))
    connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
  else stop();
}

void ServiceChat::nouvelleConnexion()
{
  QWebSocket *client = service.nextPendingConnection();
  QString nom = client->requestUrl().path();
  nom.remove('/');
  client->sendTextMessage("Bonjour "+nom);
  envoyerATous(nom+" vient de se connecter");
  connectes[client] = nom;
  listeDesConnectes();
  connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
  connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}

void ServiceChat::deconnexion()
{
  QWebSocket *client = (QWebSocket *) sender();
  client->deleteLater();
  QString nom = connectes[client];
  connectes.erase(client);
  envoyerATous(nom + " vient de se déconnecter");
  listeDesConnectes();
  disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
  disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}

void ServiceChat::receptionMessage(const QString &texte)
{
  if (texte=="stop") { stop(); return; }
  QWebSocket *client = (QWebSocket *) sender();
  QString expediteur = connectes[client];
  QString destinataire = texte.split(' ')[0];
  QString message = texte.split(destinataire)[1];
  bool trouve = false;
  for (auto &cible : connectes)
    if (cible.second == destinataire) {
      cible.first->sendTextMessage(expediteur+" > "+message);
      client->sendTextMessage("Message envoyé");
      trouve = true; break;
    }
  if (!trouve) client->sendTextMessage("Cible inconnue");
}

void ServiceChat::envoyerATous(const QString &message)
{
  for (auto &client : connectes) client.first->sendTextMessage(message);
}

void ServiceChat::listeDesConnectes()
{
  QString liste = "[";
  for (auto &client : connectes) liste.append(" "+client.second);
  liste.append(" ]");
  envoyerATous("Connectés : "+liste);
}
Voici quelques explications pour mieux comprendre le fonctionnement de ce service
Pas d'IHM
Vu que ce service est prévu pour une informatique embarquée, nous n'avons pas besoin de créer une IHM. Nous disposons toutefois d'un programme principal qui doit permettre la gestion des événements grâce aux modules intégrés de Qt. L'application principale tourne constamment en tâche de fond et attends qu'un arrêt soit proposé toujours sous forme d'événement.
Quitter l'application proprement
Lorsque nous lançons un service sur une informatique embarquée sans environnement graphique, la difficulté est toujours de pouvoir arrêter le service. Ici, nous avons choisi de permettre cet arrêt lorsqu'un client tape le mot stop dans un message.
Un seul objet représentant le service est créé
Puisque nous avons un seul objet représentant l'ensemble du service contrairement au serveur d'applications, il est absolument nécessaire de savoir à tout moment quel client envoi le message ou quel client se déconnecte, ou sous une autre façon de voir, qui a soumis un événement. La classe ServiceChat hérite de la classe QObject qui dispose d'une méthode sender() qui nous renseigne sur la source du signal donc sur l'expéditeur.
Un port par service WebSocket
Malheureusement, la conception d'un service WebSocket avec la librairie Qt ne permet de faire qu'un seul service sur le port désigné. Si vous souhaitez avoir plusieurs services, vous êtes obligé de prendre plusieurs numéros de service, ou alors, le service 8080 doit gérer l'ensemble des services.
URL différente
Dans ce contexte, il n'est pas nécessaire de préciser le type de service comme pour le serveur d'applications où vous devez spécifier l'application web associée. L'URL est donc plus réduite : ws://192.168.1.36:8080/manu par exemple.

Élaboration d'un service autonome en Java sans serveur d'applications

Pour conclure sur ce sujet, vous remarquez que le code que nous venons d'écrire avec la librairie Qt est plus conséquent que celui que nous avions développé en Java. Par contre, l'avantage de la version C++, c'est qu'il n'est pas nécessaire d'avoir un serveur d'applications. En java, cette possibilité existe également. Vous pouvez développer effectivement un service WebSocket dans une application java standard autonome sans avoir besoin du serveur d'applications. Dans ce cas de figure, il sera nécessaire d'intégrer dans votre projet un ensemble de librairies qui gèrent le protocole websocket qui constituent normalement la version entreprise édition de java.

service.Chat.java 
package service;

import java.io.IOException;
import java.util.*;
import javax.websocket.*;
import javax.websocket.server.*;
import org.glassfish.tyrus.server.Server;

@ServerEndpoint("/{login}")
public class Chat {
   private String login;
   private static HashMap<String, Session> clients = new HashMap<>();
   
   public static void main(String[] args) throws DeploymentException {
      Server service = new Server("localhost", 8080, "/chat", null, Chat.class);
      service.start();
      Scanner clavier = new Scanner(System.in);
      clavier.nextLine();        
   }   
   
    @OnOpen
    public void ouverture(Session client, @PathParam("login") String login) throws IOException {
       this.login = login;
       clients.put(login, client);
       envoyerATous(login + " vient de se connecter");
       envoyerATous("Connectés : "+clients.keySet().toString());       
    }  
    
    @OnClose
    public void fermeture() throws IOException {
       clients.remove(login);
       envoyerATous(login + " vient de se déconnecter");
       envoyerATous("Connectés : "+clients.keySet().toString()); 
    }    
   
    @OnMessage
    public String chat(String réception) throws IOException{
       Scanner requête = new Scanner(réception);
       String destinataire = requête.next();
       String message = requête.nextLine();
       return envoyerMessage(destinataire, message) ? "Message envoyé" : "Cible inconnue";
    }
    
    private void envoyerATous(String message) throws IOException {
       for (Session client : clients.values())
         if (client.isOpen()) client.getBasicRemote().sendText(message);
    }

    private boolean envoyerMessage(String destinataire, String message) throws IOException {
       if (clients.containsKey(destinataire)) {
          clients.get(destinataire).getBasicRemote().sendText(login + " > " + message);
          return true;
       }
       return false;
    }   
}
Il s'agit d'une application java classique en mode console qui possède la classe principale d'application Chat avec sa méthode principale main() à l'intérieure de laquelle nous lançons le service WebSocket grâce à la classe Server avec toutes les informations nécessaires au bon fonctionnement : la localisation, le numéro de port, le path de l'URL, et la classe qui implémente le service proprement dit. Mise à part cette particularité, nous retrouvons exactement la même écriture, avec toutes les annotations requises, que nous avions élaboré lors de la conception avec le serveur d'applications. Il est possible ensuite de lancer votre service en ligne de commande sur n'importe quel système qui possède une machine virtuelle Java. Remarquez la présence des archives dans le répertoire lib.

Application cliente sous Android

Android étant codé en Java, il est tout à fait envisageable de prévoir une application client qui utilise le protocole websocket comme nous l'avons fait avec une application cliente en JavaFX. La seule et même condition, c'est d'intégrer l'archive tyrus-standalone-client dans votre projet APK. Pour ce projet, nous utilisons Android Studio qui permet de mettre en place des applications Android de façon simple.

La première démarche à suivre en premier est de placer l'archive tyrus-standalone-client dans le répertoire spécifique LIBS qui se trouve systématiquement dans toute application Android. Ensuite, il est nécessaire que cette archive soit compilée et donc intégrée automatiquement dans l'exécutable final Fichier APK , déployé dans votre système Android. Pour cela, vous devez mettre en place une dépendance supplémentaire à votre projet, soit avec un outil adapté bouton , soit directement dans le fichier de gestion des dépendances.

build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion '23.0.2'
    defaultConfig {
        applicationId "manu.chat"
        minSdkVersion 15
        targetSdkVersion 15
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile files('libs/tyrus-standalone-client-1.12.jar')
}

La suite du développement est tout-à-fait classique dans le cadre d'un projet Android, si ce n'est la particularité d'utiliser des annotations spécifiques similaires à ce que nous avons mis en oeuvre dans l'application cliente en JavaFX.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="manu.chat">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Chat">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest> 
Afin de permettre la communication dans le réseau local ou par Internet, il faut penser à donner l'autorisation nécessaire.
chat.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/destinataire"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_toStartOf="@+id/button"
        android:hint="destinataire" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/message"
        android:layout_below="@+id/destinataire"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true"
        android:hint="message" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Envoi"
        android:id="@+id/button"
        android:layout_above="@+id/message"
        android:layout_alignParentEnd="true"
        android:onClick="soumettre" />

    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ascenseur"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:layout_alignEnd="@+id/message"
        android:layout_below="@+id/message">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/zone"
            android:textIsSelectable="true"
            android:scrollbars = "vertical" />
    </ScrollView>

</RelativeLayout>
La Vue est très rudimentaire. L'objectif ici étant de valider le fonctionnement sans mettre en oeuvre un code considérable. 
manu.chat.Chat.java
package manu.chat;

import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;

import org.glassfish.tyrus.client.ClientManager;

import java.io.IOException;
import java.net.URI;

import javax.websocket.*;

@ClientEndpoint
public class Chat extends AppCompatActivity {
    private EditText destinataire, message;
    private TextView zone;
    private ScrollView ascenseur;
    private Session session;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.chat);
        destinataire = (EditText) findViewById(R.id.destinataire);
        message = (EditText) findViewById(R.id.message);
        zone = (TextView) findViewById(R.id.zone);
        ascenseur = (ScrollView) findViewById(R.id.ascenseur);

        // Autoriser l'utilisation d'internet
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());

        // lancer la connexion au service WebSocket
        ClientManager client = ClientManager.createClient();
        try {
            client.connectToServer(this, URI.create(getString(R.string.uri)));
        }
        catch (Exception e) { zone.setText(e.getMessage()); }
    }

    @OnOpen
    public void ouverture(Session session) {
        this.session = session;
    }

    @OnMessage
    public void reception(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                zone.append(message + "\n\n");
                ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
            }
        });
    }

    public void soumettre(View envoi) throws IOException {
        session.getBasicRemote().sendText(destinataire.getText() + " " + message.getText());
    }

    @Override
    protected void onStop() {
        super.onStop();
        try {
            session.close();
        }
        catch (IOException e) { e.printStackTrace(); }
    }
}
Grâce aux annotations, ce code est très concis. Nous retrouvons globalement la même structure de ce que nous avons déjà  développé dans le Contrôleur de l'application cliente en JavaFX. La petite différence, c'est que nous rajoutons un thread spécifique pour la gestion des messages reçus à l'aide de la méthode runOnUiThread(). Cela permet d'avoir une meilleure fluidité sans bloquer l'IHM du système Android. Dans un projet Android, depuis la version 3, il est également nécessaire, en plus du manifeste, de donner les droits d'accès au réseau. Enfin, il est plus judicieux de placer la description de l'URI dans un fichier de ressource plutôt que directement dans le code source. Sous Android, cela fait dans le document strings.xml
<resources>
    <string name="app_name">Chat</string>
    <string name="uri">ws://192.168.1.13:8080/chat/manu</string>
</resources>

Amélioration du code afin d'avoir une application plus conviviale

En reprenant notre base de travail, je vous propose de rajouter des fonctionnalités supplémentaires sur le projet précédent afin d'obtenir plus de convivialité et que l'utilisation de cette application soit plus intuitive. Par exemple, le choix du destinataire se fera avec une liste déroulante qui se met à jour automatiquement suivant l'ensemble des destinataires présents actuellement sur le réseau. Ensuite, nous allons prévoir une couleur en fonction des messages envoyés et des messages reçus en suivant le même canevas que les applications d'envoi et de réception de SMS.

Le grand changement concerne le rajout de composant de type TextView à la volée. À chaque message envoyé ou reçu, un nouveau TextView est créé et rajouté dans l'interface il est rajouté dans les gestionnaire LinearLayout qui est intégré dans le ScrollView. Deux fonds différents ont été créés pour que visuellement nous fassions la différence entre l'expéditeur et le destinataire. Le deuxième changement concerne l'utilisation d'un Spinner à la place d'un EditText pour choisir son destinataire.

res.drawable.reponse.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <stroke android:width="1dp" android:color="#8888AA" />
    <corners android:radius="3dp" />
    <solid android:color="#EEEEFF" />
    <padding android:bottom="5dp" android:left="10dp"
             android:top="5dp" android:right="10dp"/>
    <size android:width="500dp" />
</shape>
res.drawable.envoi.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <stroke android:width="1dp" android:color="#AA8888" />
    <corners android:radius="3dp" />
    <solid android:color="#FFEEEE" />
    <padding android:bottom="5dp" android:left="10dp"
             android:top="5dp" android:right="10dp"/>
    <size android:width="500dp" />
</shape>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="manu.chat">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Chat">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest> 
 
chat.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <Spinner
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/destinataire"
        android:layout_alignTop="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/button" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/message"
        android:hint="message"
        android:layout_below="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_alignRight="@+id/button" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Envoi"
        android:id="@+id/button"
        android:onClick="soumettre"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true" />

    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ascenseur"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/message"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:id="@+id/zone"
            android:paddingBottom="70dp"/>
    </ScrollView>

</RelativeLayout>

 
manu.chat.Chat.java
package manu.chat;

import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.*;
import android.widget.*;

import org.glassfish.tyrus.client.ClientManager;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;

import javax.websocket.*;

@ClientEndpoint
public class Chat extends AppCompatActivity {
    private Spinner destinataire;
    private EditText message;
    private LinearLayout zone;
    private ScrollView ascenseur;
    private Session session;
    private ArrayAdapter<String> liste;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.chat);
        destinataire = (Spinner) findViewById(R.id.destinataire);
        message = (EditText) findViewById(R.id.message);
        zone = (LinearLayout) findViewById(R.id.zone);
        ascenseur = (ScrollView) findViewById(R.id.ascenseur);

        liste = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, new ArrayList<String>());
        destinataire.setAdapter(liste);

        // Autoriser l'utilisation d'internet
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());

        // lancer la connexion au service WebSocket
        ClientManager client = ClientManager.createClient();
        try {
            client.connectToServer(this, URI.create(getString(R.string.uri)));
        }
        catch (Exception e) { e.getStackTrace(); }
    }

    @OnOpen
    public void ouverture(Session session) {
        this.session = session;
    }

    @OnMessage
    public void reception(final String message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (message.contains("Connectés : [")) {
                    String utilisateurs = message.substring(message.indexOf("[")+1, message.indexOf("]"));
                    liste.clear();
                    liste.addAll(utilisateurs.split(", "));
                }
                if (!message.equals("Message envoyé")) {
                    TextView reponse = new TextView(Chat.this);
                    reponse.setBackground(getDrawable(R.drawable.reponse));
                    reponse.setText(message);
                    zone.addView(reponse);
                    ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
                }
            }
        });
    }

    public void soumettre(View envoi) throws IOException {
        session.getBasicRemote().sendText(destinataire.getSelectedItem().toString() + " " + message.getText());
        TextView moi = new TextView(Chat.this);
        moi.setBackground(getDrawable(R.drawable.envoi));
        moi.setText("moi > " + message.getText());
        zone.addView(moi);
        ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
    }

    @Override
    protected void onStop() {
        super.onStop();
        try {
            session.close();
        }
        catch (IOException e) { e.printStackTrace(); }
    }
}
Dans la méthode de rappel onCreate(), nous rajoutons le mise en oeuvre d'un adaptateur de liste afin de gérer correctement le Spinner. Ensuite, dans la méthode reception(), nous évaluons le message reçu afin de prendre en compte la liste des connectés pour le Spinner à chaque fois qu'elle se présente. Nous bloquons également maintenant le message Message envoyé. Une fois que tout ces critères sont pris en compte, nous créons un nouveau TextView avec le fond spécifique reponse avec comme contenu le message reçu. Pour la méthode soumettre(), là aussi nous créons un nouveau TextView avec comme contenu le message envoyé et le fond spécifique envoi.

WebSocket, JSON et criptage - Service Chat

Nous allons compléter l'étude précédente afin de permettre la mise en oeuvre d'une application cliente en JavaFX plus performante en intégrant des échanges sur le réseau avec des messages formatés en JSON, avec un criptage de l'information. Pour cela nous nous servirons du service déjà mis en oeuvre avec la librairie Qt dans l'étude portant sur le codage du polymorphisme avec le langage C++.

Cours préliminaires
Différentes implémentations

Format JSON

JSON est un format léger pour l'échange de données structurées complexes. Il est à l'image des documents XML en moins verbeux. Il est très utile lorsque vous devez transférer toutes les informations relatives à une structure ou à un objet persistant par exemple. Voici ci-dessous un exemple de document JSON représentant un objet de type Personne qui peut disposer de plusieurs numéros de téléphones :

{
   "id" : 51,
   "nom" : "DURAND",
   "prénom" : "Paul",
   "naissance" : 18/11/1969,
   "téléphones" : ["04-45-18-99-77", "06-89-89-87-23", "04-72-33-55-84"]
}
L'ossature du document ressemble à une structure dont le début et la fin sont désignés par des accolades. Chaque élément du document possède une clé et une valeur associée. L'ensemble des éléments sont séparés par des virgules. Pour la définition de la clé et de la valeur, vous devez l'écrire entre guillemets, sauf éventuellement pour les valeurs numériques. Enfin, si une clé possède plusieurs valeurs, vous devez les spécifier entre des crochets séparées par des virgules et toujours écrites entre guillemets. La librairie QT dispose de classes toutes prètes pour générer ou lire des documents au format JSON. Vous avez la classe QJsonDocument qui permet de prendre en compte l'ensemble du document pour sa génération ou sa lecture, la classe QJsonObject qui permettra de créer ou lire la structure globale de l'entité et la classe QJsonArray qui sera capable de générer ou de lire un ensemble de valeurs pour une même clé. La librairie QT stocke l'ensemble des éléments de l'objet de type QJsonObject dans une collection de type map, ce qui correspond bien au principe de l'association clé/valeur. Grâce à ce type de collection, il est très facile de retrouver chacun des éléments en utilisant les crochets. Les valeurs retournées ou prises en compte des éléments sont de type QVariant. Cette classe peut ensuite convertir directement vos valeurs vers des types correspond au langage C++ ou propres à la librairie QT, grâce à des méthodes associées comme toString(), toInt(), toDouble(), toDate(), etc. Java dispose également de classes toutes prètes pour générer ou lire des documents au format JSON. Vous avez la classe JsonWriter qui permet de prendre en compte l'ensemble du document pour sa génération, les classes JsonObjectBuilder et JsonObject qui permettent respectivement de créer ou de lire la structure globale de l'entité et les classes JsonArrayBuilder ainsi que JsonArray qui sont capables de générer ou de lire un ensemble de valeurs pour une même clé.

Service en C++ embarquée dans une raspberry

Pour ce projet, nous n'avons pas besoin de constituer une base de données côté serveur. Il suffit juste de mémoriser l'ensemble des clients actuellement connectés, ceci en temps réel. Chaque client reçoit une notification sur le dernier client qui se connecte ou se déconnecte avec en plus la liste de tous les présents.  Le criptage est présenté dans l'étude du polymorphisme.

MessageriePI.pro
QT       += core websockets
QT       -= gui

TARGET = messagerie
target.path = /home/pi/Travail
INSTALLS += target

CONFIG   += console c++11
CONFIG   -= app_bundle

TEMPLATE = app

SOURCES += main.cpp service.cpp traitement.cpp criptage.cpp
HEADERS += service.h traitement.h criptage.h
criptage.h
#ifndef CRIPTAGE_H
#define CRIPTAGE_H

#include <string>
#include <ctime>
using namespace std;

class Criptage
{
  static int random() { return (clock() % 9) + 1; }
public:
  static string cripter(const string &message);
  static string decripter(const string &texte);
};

#endif // CRIPTAGE_H
criptage.cpp
#include "criptage.h"

string Criptage::cripter(const string &message)
{
  string encodage;
  char lettre = ' ' + random(); // lettre référence pour la clef
  encodage += lettre;
  int chiffre = random(); // chiffre référence pour le criptage
  encodage += lettre + chiffre;
  int nombre = random();  // nombre d'éléments supplémentaires dans la clef
  encodage += lettre + nombre;
  int decalages[nombre]; // suite de nombre servant au calcul de chaque lettre du message
  for (int i=0; i<nombre; i++)
  {
    decalages[i] = random();
    encodage += lettre + decalages[i];
  }
  for (int i=0; i<message.size(); i++) encodage += message[i] - chiffre - decalages[i%nombre];
  // Brassage des lettres afin de mélanger la clé de criptage avec son message cripté
  string melange;
  nombre = encodage.size();
  if (nombre%3 == 0) encodage+=' ';
  for (int i=0, indice=2; i<nombre; i++, indice+=3) melange += encodage[indice%encodage.size()];
  return melange;
}

string Criptage::decripter(const string &texte)
{
// Remettre en place les lettres mélangées (clé de criptage suivi du message cripté) string melange = texte; string message = melange; int nombre = melange.size(); if (nombre%3 == 0) melange+=' '; for (int i=0, indice=2; i<nombre; i++, indice+=3) message[indice%melange.size()] = melange[i]; // Phase de décriptage
string decodage; int clef = 0; char lettre = message[clef++]; // lettre référence pour la clef int chiffre = message[clef++] - lettre; // chiffre référence pour le décriptage int nombre = message[clef++] - lettre; // nombre d'éléments dans la clef int decalages[nombre]; for (int i=0; i<nombre; i++) decalages[i] = message[clef++] - lettre; for (int i=clef; i<message.size(); i++) decodage += message[i] + chiffre + decalages[(i-clef)%nombre]; return decodage; }
service.h
#ifndef SERVICE_H
#define SERVICE_H

#include "criptage.h"
#include <QObject>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
using namespace std;

class Service : public QObject
{
  Q_OBJECT
public:
  explicit Service(QObject *parent, const char *nom, int port);
protected:
  map<QWebSocket *, QString> connectes;
  QWebSocket *client;
  bool criptage = false;
  string message;
private:
  QWebSocketServer service;
signals:
  void arreter();
protected:
  void cripter(const QByteArray &octets)   { message = Criptage::cripter(octets.data()); }
  void decripter(const QByteArray &octets) { message = Criptage::decripter(octets.data()); }
public slots:
  virtual void nouvelleConnexion();
  virtual void receptionMessage(const QString &texte);
  virtual void receptionOctets(const QByteArray &octets);
  virtual void deconnexion();
};

#endif // SERVICE_H
service.cpp
#include "service.h"

Service::Service(QObject *parent, const char *nom, int port) : QObject(parent), service(nom, QWebSocketServer::NonSecureMode, this)
{
  if (service.listen(QHostAddress::Any, port))
    connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
  else arreter();
}

void Service::nouvelleConnexion()
{
  client = service.nextPendingConnection();
  QString nom = client->requestUrl().path();
  nom.remove('/');
  connectes[client] = nom;
  connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
  connect(client, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(receptionOctets(QByteArray)));
  connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}

void Service::receptionMessage(const QString &texte)
{
  if (texte=="stop") { arreter(); return; }
  client = (QWebSocket *) sender();
}

void Service::receptionOctets(const QByteArray &octets)
{
  if (criptage) decripter(octets);
  client = (QWebSocket *) sender();
}

void Service::deconnexion()
{
  QWebSocket *client = (QWebSocket *) sender();
  client->deleteLater();
  disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
  disconnect(client, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(receptionOctets(QByteArray)));
  disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
traitement.h
#ifndef TRAITEMENT_H
#define TRAITEMENT_H

#include "service.h"

class Traitement : public Service
{
public:
  Traitement() : Service(nullptr, "service", 7755) { criptage = true; }
public:
  void receptionOctets(const QByteArray &octets) override;
  void nouvelleConnexion() override;
  void deconnexion() override;
private:
  void envoyerATous(QJsonObject &json);
};

#endif // TRAITEMENT_H
traitement.cpp
#include "traitement.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <iostream>

void Traitement::nouvelleConnexion()
{
  Service::nouvelleConnexion();
  QJsonObject json;  
  json["commande"] = "ouverture";
  json["nom"] = connectes[client];
  envoyerATous(json);
}

void Traitement::deconnexion()
{
  Service::deconnexion();
  QJsonObject json;
  json["commande"] = "fermeture";
  json["nom"] = connectes[client];
  connectes.erase(client);
  envoyerATous(json);
}

void Traitement::envoyerATous(QJsonObject &json)
{
  QJsonArray liste;
  for (auto &chacun : connectes) liste.append(chacun.second);
  json["connectés"] = liste;
  QJsonDocument document(json);
  cout << "Envoi => " << document.toJson(QJsonDocument::Compact).data() << endl << endl;
  cripter(document.toJson(QJsonDocument::Compact));
  for (auto &chacun : connectes) chacun.first->sendBinaryMessage(message.c_str());
}

void Traitement::receptionOctets(const QByteArray &octets)
{
  Service::receptionOctets(octets);
  cout << "Réception => " << message << endl << endl;
  QJsonDocument document = QJsonDocument::fromJson(message.c_str());
  QJsonObject json = document.object();
  QString commande = json["commande"].toString();
  if (commande=="message") {
    bool existe = false;
    QWebSocket *destinataire;
    for (auto &chacun : connectes)
      if (chacun.second==json["destinataire"].toString()) { existe=true; destinataire = chacun.first; break; }
    if (existe) destinataire->sendBinaryMessage(octets);
  }
}

Développement de la partie cliente en JavaFX

Reprenons l'application que nous avons déjà mis en oeuvre lors de  l'étude précédente avec laquelle nous allons effectuer quelques petits changements, notamment sur le contrôleur. Nous devons effectivement rajouter la partie criptage ainsi que le prise en compte du format JSON. La vue change également puisque nous prévoyons cette fois-ci une ComboBox qui nous donnera en temps réel tous les clients connectés. Comme précédemment, nous utilisons des annotations qui vont nous permettre de simplifier et d'alléger le code de façon assez significative. Par contre, pour cela, vous devez rajouter la librairie supplémentaire tyrus-standalone-client qui comporte tout ce qu'il faut pour communiquer par le protocole websocket. Une autre librairie javax.json-1.0.4.jar doit également être rajouter afin d'intégrer dans votre projet l'ensemble des classes nécessaires pour la prise en compte du format JSON.

Dès que l'application démarre, grâce au fichier de configuration précédent, elle doit se connecter automatiquement au service. L'application reçoit alors automatiquement l'ensemble des autres clients déjà connectés. Nous pouvons alors soumettre nos différents messages aux destinaires choisis.

ChatFXML.java
package client;

import javafx.application.Application;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.Stage;

public class ChatFXML extends Application {
   
   @Override
   public void start(Stage fenêtre) throws Exception {
      Parent root = FXMLLoader.load(getClass().getResource("Chat.fxml"));
      Scene scene = new Scene(root);
      fenêtre.setScene(scene);
      fenêtre.setTitle("Messagerie instantanée");
      fenêtre.show();
   }
}
Chat.fxml 
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="380.0" prefWidth="536.0" xmlns="http://javafx.com/javafx/8.0.40" 
xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.ChatController"> <children> <Button layoutX="474.0" layoutY="8.0" onAction="#soumettre" text="Envoi" AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="8.0" /> <TextField fx:id="message" layoutX="14.0" layoutY="46.0" onAction="#soumettre"
prefHeight="26.0" prefWidth="520.0" promptText="Message" AnchorPane.leftAnchor="8.0"
AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="40.0" /> <TextArea fx:id="zone" layoutX="14.0" layoutY="72.0" prefHeight="294.0" prefWidth="514.0"
AnchorPane.bottomAnchor="8.0" AnchorPane.leftAnchor="8.0" AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="74.0" /> <ComboBox fx:id="connectés" layoutX="290.0" layoutY="8.0" prefHeight="26.0" prefWidth="461.0" AnchorPane.leftAnchor="8.0"
AnchorPane.rightAnchor="67.0" AnchorPane.topAnchor="8.0" /> </children> </AnchorPane>
ChatController.java 
package client;

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.*;
import javafx.application.Platform;
import javafx.fxml.*;
import javafx.scene.control.*;
import javax.json.*;
import javax.websocket.*;
import org.glassfish.tyrus.client.ClientManager;

@ClientEndpoint
public class ChatController implements Initializable {  
   @FXML private TextField message;
   @FXML private TextArea zone;
   @FXML private ComboBox<String> connectés;

   private String expéditeur;
   private Session session;
   
   @Override
   public void initialize(URL url, ResourceBundle rb) {
      try {
         ResourceBundle config = ResourceBundle.getBundle("config/url");
         String adresse = config.getString("adresse");
         String port = config.getString("port");
         expéditeur = config.getString("login");
         ClientManager client = ClientManager.createClient();
         URI uri = URI.create("ws://"+adresse+":"+port+"/"+expéditeur);
         client.connectToServer(this, uri);
      } 
      catch (IOException | DeploymentException  ex) {
         Alert problème = new Alert(Alert.AlertType.WARNING);
         problème.setHeaderText("Impossible de se connecter");
         problème.showAndWait();
      } 
   }      
    
   @OnOpen
   public void ouverture(Session session) {
      this.session = session;
   }
     
   @FXML
   private void soumettre() throws IOException, EncodeException {
      zone.appendText("moi > "+message.getText()+ '\n');
      StringWriter texte = new StringWriter();
      JsonWriter json = Json.createWriter(texte);
      JsonObjectBuilder soumettre = Json.createObjectBuilder();    
      soumettre.add("commande", "message");
      soumettre.add("expéditeur", expéditeur);
      soumettre.add("destinataire", connectés.getValue());
      soumettre.add("message", message.getText());
      json.writeObject(soumettre.build());
      json.close();
      String formatUTF8 = new String(texte.toString().getBytes(), Charset.forName("UTF-8"));
      session.getBasicRemote().sendBinary(cripter(formatUTF8.getBytes()));   
      message.selectAll();
   }
   
   @OnMessage
   public void réception(ByteBuffer réception) {
      Platform.runLater(() -> {
         String décriptage = décripter(réception);
         JsonObject json = Json.createReader(new StringReader(décriptage)).readObject();
         String commande = json.getString("commande");
         if (commande.equals("message"))
            zone.appendText(json.getString("expéditeur")+" > "+json.getString("message") + '\n');
         else {        
            JsonArray logins =  json.getJsonArray("connectés");
            zone.appendText("connectés : "+logins.toString()+ '\n');
            connectés.getItems().clear();
            for (int i=0; i<logins.size(); i++) connectés.getItems().add(logins.getString(i));           
            connectés.getSelectionModel().select(0);
         }
      });  
   }
   
   private String décripter(ByteBuffer texte) {
      // Remettre en place les lettres mélangées (clé de criptage suivi du message cripté)
      byte[] mélange = texte.array(); 
      byte[] message = new byte[mélange.length];
      int nombre = mélange.length;
      byte[] ordre;
      int taille = nombre%3 == 0 ? nombre+1 : nombre;
      ordre = Arrays.copyOf(mélange, taille);
      for (int i=0, indice=2; i<nombre; i++, indice+=3)  message[indice%ordre.length] = ordre[i];
      // Phase de décriptage
      int clef = 0;
      byte lettre = message[clef++];               // lettre de référence pour la clef
      int chiffre = message[clef++] - lettre;    // chiffre de référence pour le décriptage
      nombre = message[clef++] - lettre;       // nombre d'éléments dans la clef
      int[] décalages = new int[nombre];
      for (int i=0; i<nombre; i++)  décalages[i] = message[clef++] - lettre;
      byte[] décodage = new byte[message.length - clef];
      for (int i=clef, j=0; i<message.length; i++, j++)  décodage[j] = (byte)(message[i] + chiffre + décalages[(i-clef)%nombre]);
      return new String(décodage, Charset.forName("UTF-8")); // Codage proposé automatiquement par le C++ (serveur)
   }
   
   private ByteBuffer cripter(byte[] message) {
      byte lettre = (byte) (' ' + random()); // lettre de référence pour la clef     
      byte chiffre = (byte) random(); // chiffre référence pour le criptage     
      int nombre = random();  // nombre d'éléments supplémentaires dans la clef
      byte[] encodage = new byte[message.length+3+nombre];
      encodage[0] = lettre;
      encodage[1] = (byte) (lettre + chiffre);
      encodage[2] = (byte) (lettre + nombre);
      int pos = 3;
      int[] décalages = new int[nombre]; // suite de nombre servant au calcul de chaque lettre du message
      for (int i=0; i<nombre; i++) {
        décalages[i] = random();
        encodage[pos++] = (byte) (lettre + décalages[i]);
      }
      for (int i=0; i<message.length; i++) encodage[pos++] = (byte) (message[i] - chiffre - décalages[i%nombre]);
      // Brassage des lettres afin de mélanger la clé de criptage avec son message cripté     
      nombre = encodage.length;
      byte[] mélange = new byte[nombre];
      int taille = nombre%3 == 0 ? nombre+1 : nombre;     
      for (int i=0, indice=2, j=0; i<nombre; i++, indice+=3, j++) mélange[j] = encodage[indice%taille];
      return ByteBuffer.wrap(mélange);
   }
   
   private int random() {
      return (int)(Math.random()*9)+1;
   }
}
Les annotations
Nous retrouvons les mêmes annotations que pour le service, @OnOpen et @OnMessage, la première nous permet de connaître la session et donc de pouvoir être en permanence en contact avec le service, la deuxième nous permet de récupérer le message venant du serveur et de l'afficher en conséquence. Vous remarquez également la présence de l'annotation @ClientEndpoint qui précise quelle est la classe qui permet de gérer la communication par le protocole websocket. Grâce à cette annotation, notre classe est maintenant capable de gérer le multi-tâche et de travailler de façon événementielle à l'image de ce que nous faisons avec la librairie QT. La connexion au service se fait dès la création du contrôleur au moyen de la classe WebSocketContainer.
Criptage et décriptage
Ce code source est relativement conséquent, notamment à cause des deux méthodes spécifiques décripter() et cripter(). Elles doivent suivre scrupuleusement ce qui a été prévu lors de la conception en C++. Justement, le formatage des chaînes de caractères est différent suivant les langages de programmation, surtout entre le C++ et Java. Le C++ manipule des chaînes au format UTF-8. Du coup, dans notre code source en JavaFX, il faut générer ou prendre en compte ce format là et traiter les chaînes plutôt sous forme de suites d'octets.
Méthode de rappel lors de la réception de nouveau message
La méthode réception() est automatiquement sollicité dès qu'un nouveau message venant du serveur apparaît. Cette méthode est annotée @OnMessage. À cause de cette annotation, comme pour la version Android, que nous avons traité dans le chapitre précédent, il est difficile d'interagir avec la vue puisque nous ne sommes plus sur le thread courant d'une application JavaFX standard. Il suffit d'appliquer le même type de remède en invoquant le thread du contôleur au moyen de la classe Platform avec sa méthode statique runLater().

Application cliente sous Android

Pour conclure, je vous propose de fusionner l'application que nous avions déjà faite sous Android avec le code précédent afin d'intégrer également le format JSON ainsi que le criptage automatique des messages envoyés et reçus. Là aussi, nous devons intégrer les archives tyrus-standalone-client et javax.json-1.0.4.jar dans votre projet APK

Comme précédemment, la première démarche à suivre en premier est de placer la deuxième archive javax.json-1.0.4.jar dans le répertoire spécifique LIBS qui se trouve systématiquement dans toute application Android. Ensuite, il est nécessaire que cette archive soit compilée et donc intégrée automatiquement dans l'exécutable final Fichier APK , déployé dans votre système Android. Pour cela, vous devez mettre en place une dépendance supplémentaire à votre projet, soit avec un outil adapté bouton , soit directement dans le fichier de gestion des dépendances.
build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion '23.0.2'
    defaultConfig {
        applicationId "manu.chat"
        minSdkVersion 15
        targetSdkVersion 15
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile files('libs/tyrus-standalone-client-1.12.jar')
    compile files('libs/javax.json-1.0.4.jar') 
}
Le seul changement notable par rapport au projet précédent concerne uniquement le code Java de la classe Chat.

res.values.strings.xml 
<resources>
    <string name="app_name">Messagerie instantanée</string>
    <string name="adresse">192.168.1.33</string>
    <string name="port">7755</string>
    <string name="expediteur">manu</string>
</resources> 
res.drawable.reponse.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <stroke android:width="1dp" android:color="#8888AA" />
    <corners android:radius="3dp" />
    <solid android:color="#EEEEFF" />
    <padding android:bottom="5dp" android:left="10dp"
             android:top="5dp" android:right="10dp"/>
    <size android:width="500dp" />
</shape>
res.drawable.envoi.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <stroke android:width="1dp" android:color="#AA8888" />
    <corners android:radius="3dp" />
    <solid android:color="#FFEEEE" />
    <padding android:bottom="5dp" android:left="10dp"
             android:top="5dp" android:right="10dp"/>
    <size android:width="500dp" />
</shape>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="manu.chat">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".Chat">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest> 
 
chat.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <Spinner
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/destinataire"
        android:layout_alignTop="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/button" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/message"
        android:hint="message"
        android:layout_below="@+id/button"
        android:layout_alignParentLeft="true"
        android:layout_alignRight="@+id/button" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Envoi"
        android:id="@+id/button"
        android:onClick="soumettre"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true" />

    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/ascenseur"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/message"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:id="@+id/zone"
            android:paddingBottom="70dp"/>
    </ScrollView>

</RelativeLayout>
 
manu.chat.Chat.java
package manu.chat;

import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.*;
import android.widget.*;

import org.glassfish.tyrus.client.ClientManager;

import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import java.net.URI;
import java.util.*;

import javax.websocket.*;
import javax.json.*;

@ClientEndpoint
public class Chat extends AppCompatActivity {
    private Spinner destinataire;
    private EditText message;
    private LinearLayout zone;
    private ScrollView ascenseur;
    private Session session;
    private String expediteur;
    private ArrayAdapter<String> liste;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.chat);
        destinataire = (Spinner) findViewById(R.id.destinataire);
        message = (EditText) findViewById(R.id.message);
        zone = (LinearLayout) findViewById(R.id.zone);
        ascenseur = (ScrollView) findViewById(R.id.ascenseur);

        liste = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, new ArrayList<String>());
        destinataire.setAdapter(liste);

        // Autoriser l'utilisation d'internet
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());

        // lancer la connexion au service WebSocket
        ClientManager client = ClientManager.createClient();
        try {
            String adresse = getString(R.string.adresse);
            String port = getString(R.string.port);
            expediteur = getString(R.string.expediteur);
            client.connectToServer(this, URI.create("ws://"+adresse+":"+port+"/"+expediteur));
        }
        catch (Exception e) { e.getStackTrace(); }
    }

    @OnOpen
    public void ouverture(Session session) {
        this.session = session;
    }

    @OnMessage
    public void reception(final ByteBuffer message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String decriptage = decripter(message);
                JsonObject json = Json.createReader(new StringReader(decriptage)).readObject();
                String commande = json.getString("commande");
                if (commande.equals("message")) {
                    TextView reponse = new TextView(Chat.this);
                    reponse.setBackground(getDrawable(R.drawable.reponse));
                    reponse.setText(json.getString("expéditeur") + " > " + json.getString("message"));
                    zone.addView(reponse);
                    ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
                }
                else {
                    JsonArray logins = json.getJsonArray("connectés");
                    liste.clear();
                    for (int i=0; i<logins.size(); i++) liste.add(logins.getString(i));
                }
            }
        });
    }

    public void soumettre(View envoi) throws IOException {
        StringWriter texte = new StringWriter();
        JsonWriter json = Json.createWriter(texte);
        JsonObjectBuilder soumettre = Json.createObjectBuilder();
        soumettre.add("commande", "message");
        soumettre.add("expéditeur", expediteur);
        soumettre.add("destinataire", (String) destinataire.getSelectedItem());
        soumettre.add("message", message.getText().toString());
        json.writeObject(soumettre.build());
        json.close();
        String formatUTF8 = new String(texte.toString().getBytes(), Charset.forName("UTF-8"));
        session.getBasicRemote().sendBinary(cripter(formatUTF8.getBytes()));
        session.getBasicRemote().sendText(destinataire.getSelectedItem().toString() + " " + message.getText());
        TextView moi = new TextView(Chat.this);
        moi.setBackground(getDrawable(R.drawable.envoi));
        moi.setText("moi > " + message.getText());
        zone.addView(moi);
        ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
        message.selectAll();
    }

    @Override
    protected void onStop() {
        super.onStop();
        try {
            session.close();
        }
        catch (IOException e) { e.printStackTrace(); }
    }

    private String decripter(ByteBuffer texte) {
        // Remettre en place les lettres mélangées (clé de criptage suivi du message cripté)
        byte[] mélange = texte.array();
        byte[] message = new byte[mélange.length];
        int nombre = mélange.length;
        byte[] ordre;
        int taille = nombre%3 == 0 ? nombre+1 : nombre;
        ordre = Arrays.copyOf(mélange, taille);
        for (int i=0, indice=2; i<nombre; i++, indice+=3)  message[indice%ordre.length] = ordre[i];
        // Phase de decriptage
        int clef = 0;
        byte lettre = message[clef++];               // lettre de référence pour la clef
        int chiffre = message[clef++] - lettre;    // chiffre de référence pour le decriptage
        nombre = message[clef++] - lettre;       // nombre d'éléments dans la clef
        int[] décalages = new int[nombre];
        for (int i=0; i<nombre; i++)  décalages[i] = message[clef++] - lettre;
        byte[] décodage = new byte[message.length - clef];
        for (int i=clef, j=0; i<message.length; i++, j++)  décodage[j] = (byte)(message[i] + chiffre + décalages[(i-clef)%nombre]);
        return new String(décodage, Charset.forName("UTF-8")); // Codage proposé automatiquement par le C++ (serveur)
    }

    private ByteBuffer cripter(byte[] message) {
        byte lettre = (byte) (' ' + random()); // lettre de référence pour la clef
        byte chiffre = (byte) random(); // chiffre référence pour le criptage
        int nombre = random();  // nombre d'éléments supplémentaires dans la clef
        byte[] encodage = new byte[message.length+3+nombre];
        encodage[0] = lettre;
        encodage[1] = (byte) (lettre + chiffre);
        encodage[2] = (byte) (lettre + nombre);
        int pos = 3;
        int[] décalages = new int[nombre]; // suite de nombre servant au calcul de chaque lettre du message
        for (int i=0; i<nombre; i++) {
            décalages[i] = random();
            encodage[pos++] = (byte) (lettre + décalages[i]);
        }
        for (int i=0; i<message.length; i++) encodage[pos++] = (byte) (message[i] - chiffre - décalages[i%nombre]);
        // Brassage des lettres afin de mélanger la clé de criptage avec son message cripté
        nombre = encodage.length;
        byte[] mélange = new byte[nombre];
        int taille = nombre%3 == 0 ? nombre+1 : nombre;
        for (int i=0, indice=2, j=0; i<nombre; i++, indice+=3, j++) mélange[j] = encodage[indice%taille];
        return ByteBuffer.wrap(mélange);
    }

    private int random() {
        return (int)(Math.random()*9)+1;
    }
}
Dans la méthode de rappel onCreate(), nous rajoutons le mise en oeuvre d'un adaptateur de liste afin de gérer correctement le Spinner. Ensuite, dans la méthode reception(), nous évaluons le message reçu afin de prendre en compte la liste des connectés pour le Spinner à chaque fois qu'elle se présente. Nous bloquons également maintenant le message Message envoyé. Une fois que tout ces critères sont pris en compte, nous créons un nouveau TextView avec le fond spécifique reponse avec comme contenu le message reçu. Pour la méthode soumettre(), là aussi nous créons un nouveau TextView avec comme contenu le message envoyé et le fond spécifique envoi.

Graphisme 2D - Shapes, Canvas, Charts

Pour clôturer l'ensemble de ces études, nous allons utiliser des composants de hauts niveaux qui permettent de réaliser des tracés de courbes avec des objets de type Chart qui permettent de créer différentes représentations graphiques de données. Nous profiterons de cette étude pour fabriquer une application qui contôle les données biométriques de capteurs fixés sur un patient en perte d'autonomie. Quatre types de capteurs sont visualisés, la température corporelle, le pouls, le taux d'oxygène dans le sang et l'électrocardiogramme.

Cours préliminaires
Description du projet

L'application est en réalité un service sur le port 7777 qui récupère les trames envoyées par les différents capteurs en suivant un protocole bien défini :

<fonction><longueur><données><checksum><fin>

Le code fonction en ASCII est :

longueur correspond au nombre d'octets constituant les données, sachant que la taille maximale des données ne doit pas dépasser 64 octets.

Le code fin est composé des deux octets successifs 0x0A-0x0D.

Dans ce projet, nous avons défini un style CSS associé au composant LineChart afin que la courbe soit la plus fine possible. L'épaisseur ici est réglée à un octet par défaut la courbe est relativement épaisse par rapport à la visualisation des différents axes.
ServeurCollecte.java
package fx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;

public class ServeurCollecte extends Application {

    @Override
    public void start(Stage fenêtre) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("Vue.fxml"));        
        Scene scene = new Scene(root);
        fenêtre.setScene(scene);
        fenêtre.setTitle("Service de collecte");
        fenêtre.show();
    }
}
lignes.css
.chart-series-line {    
    -fx-stroke-width: 1px;
    -fx-effect: null;
}

.default-color0.chart-series-line { -fx-stroke: darkgreen; }
.default-color0.chart-line-symbol { -fx-background-color: darkgreen, white; }
Vue.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.effect.DropShadow?>
<?import javafx.scene.effect.InnerShadow?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane prefHeight="364.0" prefWidth="774.0" xmlns="http://javafx.com/javafx/8.0.65" 
                      xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.Contrôleur">
   <top>
      <ToolBar prefHeight="40.0" prefWidth="200.0" BorderPane.alignment="CENTER">
        <items>
          <ToggleButton fx:id="demarrer" mnemonicParsing="false" onAction="#seConnecter" 
                                   text="Démarrer le service" textFill="#940f03" />
            <Separator orientation="VERTICAL" prefHeight="28.0" prefWidth="30.0" />
            <TextField fx:id="pouls" editable="false" prefHeight="25.0" prefWidth="100.0" promptText="Pouls" />
            <TextField fx:id="oxymètre" editable="false" prefHeight="25.0" prefWidth="140.0" promptText="Oxymètre" />
            <TextField fx:id="température" editable="false" prefHeight="25.0" prefWidth="160.0" promptText="Température" />
            <Separator orientation="VERTICAL" prefHeight="28.0" prefWidth="30.0" />
            <Button mnemonicParsing="false" onAction="#resetCourbe" text="Effacer courbe" />
        </items>
         <effect>
            <DropShadow />
         </effect>
      </ToolBar>
   </top>
   <center>
      <fx.ECG fx:id="electro" prefHeight="371.0" prefWidth="565.0" BorderPane.alignment="CENTER">
         <effect>
            <InnerShadow />
         </effect>
      </fx.ECG>
   </center>
   <bottom>
      <Label fx:id="notification" prefHeight="25.0" 
                  text="Il faut démarrer le service pour capturer les valeurs des capteurs...">
         <BorderPane.margin>
            <Insets left="7.0" right="7.0" />
         </BorderPane.margin>
      </Label>
   </bottom>
</BorderPane>
ECG.java
package fx;

import javafx.scene.chart.*;
import javafx.scene.layout.Pane;

public class ECG extends Pane {
   private NumberAxis axeX = new NumberAxis();
   private NumberAxis axeY = new NumberAxis();
   private LineChart<Number, Number> axes = new LineChart<>(axeX, axeY);
   private XYChart.Series courbe = new XYChart.Series();
   private int index = 0;
   
   public ECG() {
      axeX.setLabel("Temps");
      axeX.setUpperBound(2400);
      axeX.setTickUnit(250);
      axeX.setAutoRanging(false);
      axeY.setLabel("Amplitude");
      axeY.setUpperBound(100);
      axeY.setLowerBound(-50);
      axeY.setTickUnit(25);
      axeY.setAutoRanging(false);
      courbe.setName("Électrocardiogramme");
      axes.setCreateSymbols(false);
      axes.setAnimated(false);
      axes.getStylesheets().add("fx/lignes.css");
      axes.getData().add(courbe);
      getChildren().add(axes);  
   }   

   @Override
   public void resize(double width, double height) {
      super.resize(width, height);
      axes.setPrefSize(width-10, height);
   }
   
   public void ajout(byte[] octets) {
      int[] valeurs = new int[octets[1]];
      for (int i=0; i<octets[1]; i++) valeurs[i] = (octets[i+2]>=0 ? octets[i+2] : octets[i+2]+256)-128;
      for (int i=0; i<valeurs.length; i++) courbe.getData().add(new XYChart.Data(index++, valeurs[i]));
   }
   
   public void razCourbe() {
      index = 0;
      courbe.getData().clear();
   }
}
Contrôleur.java
package fx;

import java.io.*;
import java.net.*;
import java.util.*;
import javafx.application.Platform;
import javafx.fxml.*;
import javafx.scene.control.*;

public class Contrôleur {  
   @FXML private TextField pouls, oxymètre, température;
   @FXML private Label notification;
   @FXML private ToggleButton demarrer;
   @FXML private ECG electro;
   
   private ServerSocket service;
   private Socket client;
   private Scanner requête;
   
   @FXML
   void seConnecter() throws IOException {
      if (demarrer.isSelected())  connexion();
      else {
         service.close();
         if (client!=null) client.close();
         if (requête!=null) requête.close();     
      }
   }
   
   @FXML
   void resetCourbe() { electro.razCourbe(); }
   
   private void connexion() {
      try {
         service = new ServerSocket(7777);
         notification.setText("Service 7777 démarré...\n");
         notification.requestFocus();
         new Thread(() -> {
            try {
               client = service.accept();
               requête = new Scanner(client.getInputStream()); 
               byte[] trame = new byte[70];
               while (client.isConnected() && requête.hasNextLine()) {
                  client.getInputStream().read(trame);                  
                  char code = (char) trame[0];
                  byte nombre = trame[1];
                  byte entiere = nombre>0 ? trame[2] : 0;
                  byte decimale = nombre>1 ? trame[3] : 0;
                  Platform.runLater(() -> {
                     notification.setText("Code("+code+") - Nombre("+nombre+")");
                     switch (code) {
                        case 'F' : pouls.setText("Pouls : "+entiere); break;
                        case 'O' : oxymètre.setText("Oxymètre : "+entiere+","+decimale); break;
                        case 'T' : température.setText("Température : "+entiere+","+decimale); break;
                        case 'E' : electro.ajout(trame); break;
                     }
                  });
               }
            }
            catch (IOException ex) { Platform.runLater(()-> {notification.setText("Communication interrompu...\n");});  }
         }).start();
      } 
      catch (IOException ex) {  Platform.runLater(()-> {notification.setText("Le service existe déjà...\n");});  }  
   }
}
Je rappelle que lorsque nous élaborons une programmation réseau en Java, la méthode utilisée n'est pas événementielle, comme pour la librairie Qt avec le C++. Nous devons constamment attendre, soit la connexion avec un nouveau client ou, dans le cas où ce dernier est connecté, soit l'arrivée de nouvelles trames. Dans ces deux cas, les méthodes sont systématiquement blocantes : d'abord la méthode accept() de la classe ServerSocket et ensuite la méthode hasNextLine() de la classe Scanner. Cette approche est totalement incompatible avec la gestion d'une IHM. C'est pour cette raison que nous avons mis en place un thread particulier qui gère toute la communication réseau. Par ailleurs, lorsque vous devez soumettre de nouvelles valeurs dans votre IHM, il est préférable de lancer un autre thread spécialisé à cet effet au travers de la méthode statique runLater() de la classe Platform.
Simulateur des différents capteurs utilisés codé en C++ avec la librairie QT

Afin de bien comprendre comment sont envoyés les différentes trames issues des capteurs biométriques, je vous propose un simulateur réalisé en C++ à l'aide de la librairie QT. Grâce à cet outil, nous pouvons choisir les fréquences de mesures sur chacun des capteurs en particulier. Pour l'ECG, le contenu d'un fichier pré-enregistré est envoyé successivement avec 64 octets de données pour chacune des trames au maximum cette limitation est dûe au capteur réel utilisé.

CapteursBiometriques.pro
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = CapteursBiometriques
TEMPLATE = app
CONFIG += c++11

SOURCES += main.cpp principal.cpp
HEADERS  += principal.h
FORMS    += principal.ui
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include <QtNetwork>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
protected:
  void timerEvent(QTimerEvent *evt);
private slots:
  void connexion();
  void envoyerECG();
private:
  QTcpSocket service;
  QDataStream requete;
  int tmpPouls, tmpOxy, tmpTemp, tmpECG;
  QByteArray contenuECG;
  int octetslus = 0;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"
#include <unistd.h>

Principal::Principal(QWidget *parent) : QMainWindow(parent), requete(&service)
{
  setupUi(this);
  connect(&service, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(close()));
}

void Principal::connexion()
{
  service.connectToHost(adresse->text(), 7777);
  tmpPouls = startTimer(frePouls->value()*1000);
  tmpOxy = startTimer(freOxy->value()*1000);
  tmpTemp = startTimer(freTemp->value()*1000);
  boutonConnexion->setVisible(false);
  freOxy->setEnabled(false);
  frePouls->setEnabled(false);
  freTemp->setEnabled(false);
}

void Principal::envoyerECG()
{
  ECG->setValue(0);
  QFile fichier("ECG.raw");
  if (fichier.open(QIODevice::ReadOnly)) {
    contenuECG = fichier.readAll();
    octetslus = 0;
    tmpECG = startTimer(160);
  }
}

void Principal::timerEvent(QTimerEvent *evt)
{
  char checksum=0;

  if (evt->timerId()==tmpPouls)
  {
    checksum -= 'F'+1+pouls->value();
    requete << qint8('F') << qint8(1) << qint8(pouls->value())
            << qint8(checksum) << qint8(0x0A) << qint8(0x0D);
  }

  else if (evt->timerId()==tmpOxy)
    {
      char entiere = oxygene->value();
      char decimale = (oxygene->value()-entiere) * 100;
      checksum -= 'O'+2+entiere+decimale;
      requete << qint8('O') << qint8(2) << qint8(entiere) << qint8(decimale)
              << qint8(checksum) << qint8(0x0A) << qint8(0x0D);
    }

  else if (evt->timerId()==tmpTemp)
    {
      char entiere = temperature->value();
      char decimale = (temperature->value()-entiere) * 100;
      checksum -= 'T'+2+entiere+decimale;
      requete << qint8('T') << qint8(2) << qint8(entiere) << qint8(decimale)
              << qint8(checksum) << qint8(0x0A) << qint8(0x0D);
    }

  else if (evt->timerId()==tmpECG)
    {
      int taille = contenuECG.size();
      if (octetslus < taille)
      {
        int nombre = (taille-octetslus) >= 64 ? 64 : taille-octetslus;
        QByteArray octets = contenuECG.mid(octetslus, nombre);
        octetslus+=nombre;
        char checksum = 0 - 'E'+nombre;
        requete << qint8('E') << qint8(nombre);
        for (auto octet : octets) {
          checksum -= octet;
          requete << qint8(octet);
        }
        requete << qint8(checksum) << qint8(0x0A) << qint8(0x0D);
      }
      else killTimer(tmpECG);
      ECG->setValue(octetslus);
    }
  service.flush();
}
Développement du service, cette fois-ci en C++ avec la librairie QT

Puisque nous somme dans le développement C++, je vous propose une alternative au service que nous avons mis en oeuvre en JavaFX. Pour le tracé de l'ECG, nous utilisons une classe spécifiques aux tracés de courbe qui s'appelle QCustomPlot.

CapteursBiometriques.pro
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport

TARGET = ServeurCollecte
TEMPLATE = app

CONFIG += c++11

SOURCES += main.cpp principal.cpp qcustomplot.cpp
HEADERS  += principal.h qcustomplot.h
FORMS    += principal.ui
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include "qcustomplot.h"
#include <QTcpServer>
#include <QTcpSocket>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private:
  QTcpServer service;
  QTcpSocket *client;
private slots:
  void nouvelleConnexion();
  void lectureReseau();
  void razCourbe();
private:
  QVector<double> x, y;
  int index = 0;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
  if (service.listen(QHostAddress::Any, 7777))
    info->showMessage("Service 7777 démarré\n");
  else info->showMessage("ATTENTION, le service n'a pas pu démarré correctement");
  ecg->addGraph();
  ecg->xAxis->setLabel("Temps");
  ecg->yAxis->setLabel("Amplitude");
  ecg->xAxis->setRange(0, 2400);
  ecg->yAxis->setRange(-50, 100);
  ecg->graph(0)->setName("Électrocardiogramme");
  ecg->graph(0)->setData(x, y);
  ecg->graph(0)->setPen(QPen(Qt::darkRed));
  ecg->axisRect()->setBackground(QBrush(QColor(255, 0, 0, 16)));
  ecg->legend->setVisible(true);
  ecg->legend->setBrush(QBrush(QColor(255, 0, 0, 32)));
  ecg->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom|Qt::AlignRight);
  ecg->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
  ecg->plotLayout()->insertRow(0);
  ecg->plotLayout()->addElement(0, 0, new QCPPlotTitle(ecg, "Électrocardiogramme"));
}

void Principal::nouvelleConnexion()
{
  client = service.nextPendingConnection();
  connect(client, SIGNAL(readyRead()), this, SLOT(lectureReseau()));
}

void Principal::lectureReseau()
{ 
  QByteArray octets = client->readAll();
  info->showMessage(QString("Code(%1)").arg(octets[0]));
  switch (octets[0]) {
    case 'F': pouls->setValue(octets[2]); break;
    case 'O': oxymetre->setValue(octets[2]+0.01*octets[3]); break;
    case 'T': temperature->setValue(octets[2]+0.01*octets[3]); break;
    case 'E':
        for (int i=0; i<octets[1]; i++) { 
          x.push_back(index++);
          y.push_back((octets[i+2]>=0 ? octets[i+2] : octets[i+2]+256) - 128);
        }
        ecg->graph(0)->addData(x, y);
        ecg->replot();
      break;
    default : info->showMessage("Non décodé..."); break;
  }
}

void Principal::razCourbe()
{
  index=0;
  x.clear();
  y.clear();
  ecg->graph(0)->clearData();
  ecg->replot();
}
Développement du service, en C++ avec la librairie QT, version améliorée

Je vous propose de faire une petite modification qui permet de prendre plus de compétence de l'objet de type QCustomPlot afin d'alléger la structure.

CapteursBiometriques.pro
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport

TARGET = ServeurCollecte
TEMPLATE = app

CONFIG += c++11

SOURCES += main.cpp principal.cpp qcustomplot.cpp
HEADERS  += principal.h qcustomplot.h
FORMS    += principal.ui
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include "qcustomplot.h"
#include <QTcpServer>
#include <QTcpSocket>

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private:
  QTcpServer service;
  QTcpSocket *client;
private slots:
  void nouvelleConnexion();
  void lectureReseau();
  void razCourbe();
private:
  QVector<double> x, y;
  int index = 0;
  int pouls = 0;
  double oxymetre = 0.0, temperature = 0.0;
  QCPItemText *valeurs;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
  if (service.listen(QHostAddress::Any, 7777))
    info->showMessage("Service 7777 démarré\n");
  else info->showMessage("ATTENTION, le service n'a pas pu démarré correctement");
  ecg->addGraph();
  ecg->xAxis->setLabel("Temps");
  ecg->yAxis->setLabel("Amplitude");
  ecg->xAxis->setRange(0, 2400);
  ecg->yAxis->setRange(-50, 100);
  ecg->graph(0)->setName("Électrocardiogramme");
  ecg->graph(0)->setData(x, y);
  ecg->graph(0)->setPen(QPen(Qt::darkRed));
  ecg->axisRect()->setBackground(QBrush(QColor(255, 0, 0, 16)));
  ecg->legend->setVisible(true);
  ecg->legend->setBrush(QBrush(QColor(255, 0, 0, 32)));
  ecg->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom|Qt::AlignRight);
  ecg->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
  ecg->plotLayout()->insertRow(0);
  ecg->plotLayout()->addElement(0, 0, new QCPPlotTitle(ecg, "Électrocardiogramme"));
  valeurs = new QCPItemText(ecg);
  ecg->addItem(valeurs);
  valeurs->setPositionAlignment(Qt::AlignTop|Qt::AlignLeft);
  valeurs->setTextAlignment(Qt::AlignLeft);
  valeurs->position->setType(QCPItemPosition::ptAxisRectRatio);
  valeurs->position->setCoords(0.02, 0);
  valeurs->setText("O : 0%\nT : 0°\nF : 0");
  valeurs->setFont(QFont(font().family(), 10));
  valeurs->setColor(Qt::darkRed);
}

void Principal::nouvelleConnexion()
{
  client = service.nextPendingConnection();
  connect(client, SIGNAL(readyRead()), this, SLOT(lectureReseau()));
}

void Principal::lectureReseau()
{ 
  QByteArray octets = client->readAll();
  info->showMessage(QString("Code(%1)").arg(octets[0]));
  switch (octets[0]) {
    case 'F': pouls = octets[2]; break;
    case 'O': oxymetre = octets[2]+0.01*octets[3]; break;
    case 'T': temperature = octets[2]+0.01*octets[3]; break;
    case 'E':
        for (int i=0; i<octets[1]; i++) { 
          x.push_back(index++);
          y.push_back((octets[i+2]>=0 ? octets[i+2] : octets[i+2]+256) - 128);
        }
        ecg->graph(0)->addData(x, y);
        ecg->replot();
      break;
    default : info->showMessage("Non décodé..."); break;
  }
  valeurs->setText(QString("O : %2%\nT : %3°\nF : %1")
                   .arg(pouls, 2).arg(oxymetre, 4, 'f', 1).arg(temperature, 4, 'f', 1));
  ecg->replot();
}

void Principal::razCourbe()
{
  index=0;
  x.clear();
  y.clear();
  ecg->graph(0)->clearData();
  ecg->replot();
}
Analyse des trames au travers d'une structure pré-formatée

Je vous propose une dernière petite modification du code précédent afin d'améliorer la syntaxe pour qu'elle soit encore plus lisible. Je rajoute une structure, dont le format correspond au début de la trame des capteurs, avec les différentes parties importantes constituant cette trame, comme le code fonction, le nombre d'octets, la partie entièree et éventuellement, la partie décimale.

CapteursBiometriques.pro
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport

TARGET = ServeurCollecte
TEMPLATE = app

CONFIG += c++11

SOURCES += main.cpp principal.cpp qcustomplot.cpp
HEADERS  += principal.h qcustomplot.h
FORMS    += principal.ui
Principal.h
#ifndef PRINCIPAL_H
#define PRINCIPAL_H

#include "ui_principal.h"
#include "qcustomplot.h"
#include <QTcpServer>
#include <QTcpSocket>

using Octet = unsigned char;

struct Trame
{
  char fonction;
  Octet nombre, partieEntiere, partieDecimale;
  double valeurNumerique() { return partieEntiere + 0.01 * partieDecimale; }
};

class Principal : public QMainWindow, private Ui::Principal
{
  Q_OBJECT

public:
  explicit Principal(QWidget *parent = 0);
private:
  QTcpServer service;
  QTcpSocket *client;
private slots:
  void nouvelleConnexion();
  void lectureReseau();
  void razCourbe();
private:
  QVector<double> x, y;
  int index = 0;
  int pouls = 0;
  double oxymetre = 0.0, temperature = 0.0;
  QCPItemText *valeurs;
};

#endif // PRINCIPAL_H
Principal.cpp
#include "principal.h"

Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
  setupUi(this);
  connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
  if (service.listen(QHostAddress::Any, 7777))
    info->showMessage("Service 7777 démarré\n");
  else info->showMessage("ATTENTION, le service n'a pas pu démarré correctement");
  ecg->addGraph();
  ecg->xAxis->setLabel("Temps");
  ecg->yAxis->setLabel("Amplitude");
  ecg->xAxis->setRange(0, 2400);
  ecg->yAxis->setRange(-50, 100);
  ecg->graph(0)->setName("Électrocardiogramme");
  ecg->graph(0)->setData(x, y);
  ecg->graph(0)->setPen(QPen(Qt::darkRed));
  ecg->axisRect()->setBackground(QBrush(QColor(255, 0, 0, 16)));
  ecg->legend->setVisible(true);
  ecg->legend->setBrush(QBrush(QColor(255, 0, 0, 32)));
  ecg->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom|Qt::AlignRight);
  ecg->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
  ecg->plotLayout()->insertRow(0);
  ecg->plotLayout()->addElement(0, 0, new QCPPlotTitle(ecg, "Électrocardiogramme"));
  valeurs = new QCPItemText(ecg);
  ecg->addItem(valeurs);
  valeurs->setPositionAlignment(Qt::AlignTop|Qt::AlignLeft);
  valeurs->setTextAlignment(Qt::AlignLeft);
  valeurs->position->setType(QCPItemPosition::ptAxisRectRatio);
  valeurs->position->setCoords(0.02, 0);
  valeurs->setText("O : 0%\nT : 0°\nF : 0");
  valeurs->setFont(QFont(font().family(), 10));
  valeurs->setColor(Qt::darkRed);
}

void Principal::nouvelleConnexion()
{
  client = service.nextPendingConnection();
  connect(client, SIGNAL(readyRead()), this, SLOT(lectureReseau()));
}

void Principal::lectureReseau()
{ 
  QByteArray octets = client->readAll();
  Trame* trame = (Trame*) octets.data();

  info->showMessage(QString("Code(%1)").arg(trame->fonction));
  switch (trame->fonction) {
    case 'F': pouls = trame->partieEntiere; break;
    case 'O': oxymetre = trame->valeurNumerique(); break;
    case 'T': temperature = trame->valeurNumerique(); break;
    case 'E':
        for (int i=0; i<trame->nombre; i++) {
          x.push_back(index++);
          y.push_back((octets[i+2]>=0 ? octets[i+2] : octets[i+2]+256) - 128);
        }
        ecg->graph(0)->addData(x, y);
        ecg->replot();
      break;
    default : info->showMessage("Non décodé..."); break;
  }
  valeurs->setText(QString(" O : %2% .\n T : %3°\n F : %1")
                   .arg(pouls, 2).arg(oxymetre, 4, 'f', 1).arg(temperature, 4, 'f', 1));
  ecg->replot();
}

void Principal::razCourbe()
{
  index=0;
  x.clear();
  y.clear();
  ecg->graph(0)->clearData();
  ecg->replot();
}