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éliminairesDescription 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éliminairesDescription 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();
}
}
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();
}
}
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éliminairesDescription 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();
}
}
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();
}
}
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éliminairesDescription 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();
}
}
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éliminairesDescription 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();
}
}
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();
}
}
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éliminairesDescription 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();
}
}
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éliminairesDescription 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();
}
}
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éliminairesDescription 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();
}
}
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éliminairesDescription 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();
}
}
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();
}
}
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();
}
}
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();
}
}
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
permanentesAvec 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.
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(); }
}
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
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
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
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();
}
}
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 :
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.
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.
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
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.
# 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();
}
}
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
#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éliminairesDiffé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
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
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
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
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.
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.
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
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.
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éliminairesDiffé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 :
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.
#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;
}
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();
}
}
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
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éliminairesDescription 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 :
F : Fréquence cardiaque pouls - 1 octet.
O : Taux d'oxygène dans le sang 2 octets : 1 octet partie entière, 1 octet partie décimale.
E : Électrocardiogramme ECG - données sur 8bits échantillonées en 400Hz.
T : Température 2 octets : 1 octet partie entière, 1 octet partie décimale.
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();
}
}
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é.
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.
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.