Voir le flux RSS

bouye

[Actualité] Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1

Noter ce billet
par , 07/04/2015 à 09h23 (2498 Affichages)
Suite aux derniers billets, j'avais en tête de faire quelques chose de plus ludique. Même si au départ j'avais été tenté de faire une interface en HTML5 et du JavaScript histoire d’expérimenter de nouvelle choses, j'ai préféré conserver l'usage du Java des familles. De plus, comme ceci est avant tout destiné à des membres de ma guilde en jeu qui sont actuellement au lycée et qui débutent la programmation (l'année scolaire a débuté mi-février ici à Nouméa), les blog-posts de cette série vont prendre l'apparence de didacticiels. Cependant ils seront directement postés ici plutôt que par le canal habituel de Développez pour éviter trop de lenteurs et délais concernant la publication.

Je ne m’étendrai pas trop sur les bases puisque Développez contient suffisamment de bons didacticiels sur comment débuter en Java tandis que j'essaierai d'aider plus directement les cibles concernées via notre canal audio sur TeamSpeak. N’hésitez pas à poster en commentaires si vous avez des questions spécifiques sur cet article et n'oubliez pas que les forums sont là pour vous aider pour le reste.

Ah oui, une dernière chose : je ne sais pas trop dans quelle direction je vais aller avec cette série concernant Guild Wars 2. Dans un premier temps, je vais faire plusieurs tests sur les endpoint de base de l'API. Par la suite, je ne sais pas trop encore si j'essaierai d’écrire une application plus complète ou si je vais plutôt regarder comment faire des ports pour iOS ou Android. De plus, je pars complètement à l'aveuglette, sur un terrain qui m'est inconnu donc, du moins au début, je n'utiliserai pas de framework particulier ou de toolkit dédié aux app webs. Également, la gestion des erreurs sera assez légère dans les premiers tests.

Prérequis
Il vous faut donc un IDE permettant de compiler du Java (par exemple la dernière version de NetBeans, mais Eclipse ou IntelliJ IDEA feront tout aussi bien l'affaire) ainsi que la dernière version du JDK. Je vous recommande également l'utilisation de SceneBuilder pour tout ce qui est de la manipulation des fichiers FXML. Scenic View est également utile pour déboguer le contenu des scènes.

Pour m'interfacer avec la Web API du jeu, j'ai brièvement contemplé l’idée de créer une couche AJAX en JavaScript qui aurait été invoquée via le scripting engine (voir la JSR 223) de la JVM ou par le composant WebView de JavaFX et puis je me suis dit que c’était inutile de compliquer ainsi les chose et qu'il valait mieux finalement autant rester 100% Java dans la mesure du possible ; c'est là qu'intervient la JSR 353.

La JSR 353 est une spécification définissant l'API Java pour le traitement de données JSON (Java API for JSON Processing ou encore JSON-P). Cette API est généralement disponible uniquement via Java EE et donc il faut d'abord trouver comment l'empaqueter avec Java SE. Il est possible de l’intégrer directement en tant que JAR dans les dépendances de votre projet, soit en téléchargeant l'implémentation de référence depuis la page des téléchargements de la JSR 353 le site du JCP (Java Community Process) et en extrayant le fichier javax.json-1.0.jar qu'elle contient, soit en suivant les instructions pour passer par Maven.

Dans la mesure où nous allons faire des requêtes web, il vous faudra également bien sûr une connexion Internet qui fonctionne et qui ne soit pas filtrée (genre gestionnaire de sécurité empêchant l’accès à des sites de jeux) ou avec un proxy demandant une configuration particulière (hôte, port, authentification) car je ne compte pas aborder ce sujet.

Ces prérequis s'appliqueront tout du long de cette série.

Contexte
Guild Wars 2 est un MMORPG créé par la compagnie américaine ArenaNet et publié en 2012 par sa compagnie mère coréenne NCsoft. Ce jeu n'est disponible que sous Windows et Mac OS X (bien que certains ont réussi à le faire tourner sous Linux). Le jeu est disponible en deux versions :
  • la version americano-européenne gérée directement par ArenaNet, le créateur du jeu ;
  • la version chinoise sortie un peu moins de 2 ans après l'autre version et opérée par Kongzhong.

Les deux versions du jeu sont équivalentes sur les fonctionnalités principales, ArenaNet ayant reporté sur la version US-EU presque toutes les modifications qu'ils ont eux-même développées pour la version chinoise. Bien sûr, cette dernière conserve un peu de censure (pas de représentation des ossements humains, un peu moins de nudité), plus de micro-paiements ici et là, et, évidement, tourne derrière le Grand Parre-feu chinois et est donc réservé aux citoyens de la République Populaire de Chine. Les fonctionnalités qui nous intéressent sont, à ma connaissance, uniquement disponibles pour la version US-EU.

Assez tôt durant la période de présentation du jeu avant sa sortie, les développeurs ont annoncé qu'ils comptaient sortir des application satellites mobiles qui permettraient de s'interfacer avec certaines fonctionnalités du jeu lui-même : gestion de la guilde, gestion des enchères, déroulement des évènements dynamiques en jeu, suivi des batailles dans le mode Monde contre Monde (3 serveurs s'affrontent durant une bataille qui dure 1 semaine). Cependant, ces applications sont restées du domaine du vaporware et ne se sont jamais concrétisées.

En parallèle, pas mal de site web de fan se sont mis en place après la sortie du jeu pour gérer certaines de ces fonctionnalités absentes. Au départ, les données (demandes et offres des enchères, capture des campements, tours ou forteresses) étaient renseignées par les joueurs eux-mêmes mais après quelques temps les développeurs d'ArenaNet ont commencé à publier une Web API qui permet directement d'interroger les serveur pour en retirer des données exploitables. De nos jours, les fansites obtiennent donc désormais leurs informations directement depuis cette source officielle.

Cette API a continué à évoluer avec le temps en fonction des changements dans le jeu lui-même ainsi que des retours des joueurs/utilisateurs et nous en sommes désormais à la version 2 qui permet entre autre de suivre le cours des ventes des objets entre joueurs. L'API ne dispose pas encore d'un module d'authentification donc, pour le moment, on ne peut accéder qu'à des données publiques et pas aux informations liées à un compte en particulier ou aux personnages de ce compte. D'où le fait qu'on ne puisse pas encore participer aux enchères, par exemple.

L'API est décrite sur le wiki du jeu (dont le contenu est peuplé par les joueur) et dispose également d'un espace de discussion sur les forums anglais du jeu (une inscription est requise pour y accéder). De plus la partie de l'API qui est développée en collaboration avec les joueurs est hébergée sur GitHub.

Plusieurs points d’entrée (endpoint) sont disponibles qui permettent d'interroger les serveurs pour en retirer des informations. Nous allons dans un premier temps nous intéresser à l'endpoint Quaggan qui est un point d’entrée de test permettant de récupérer des images de Quaggans...

Des images de qoa... ?
De Quaggans ! Ah, euh, oui, dans l'univers du jeu, les Quaggans sont une race de personnages non-joueurs (PNJ - ou encore NPC, Non-Player Character) qui peuple les lacs et les mers de Tyrie (le monde du jeu). Ce sont des créatures amphibiennes apparentée à des cétacés d'aspect rondouillard pour ne pas dire obèse. Ils sont généralement* dociles et sont donc souvent la proie d'autres bestioles bien plus actives ou dominatrices qu'eux. Ce qui fait que dans pas mal de cartes, des évènements ou des cœurs (des quêtes si vous voulez) tournent autour du fait de devoir aider des villages Quaggans contre les dangers alentours. Ajoutez à cela le fait qu'ils parlent plutôt lentement et s'expriment souvent à la troisième personne ("Quaggan a besoin d'aide !") avec une voix douce ou timide et qu'ils émettent souvent des onomatopées du style "Fooo !", "Cooo !" et "Fuuu !", tout ceci fait qu'ils entre définitivement dans la catégorie "mignon" ou "cute" ce qui fait que le joueur a tendance à s'attacher rapidement à eux. Il n'y a guère que les Skritts et leurs facéties et intellect bizarroïde qui peuvent rivaliser avec eux...

PS : j'ai dit généralement car en fait les Quaggans énervés se montrent bien plus violents (et beaucoup plus moches aussi).

Des Skri... ????
Fooo, laissez tomber. Quaggan est fatigué et ne va pas vous expliquer tout l'univers du jeu !

Reprenons, l'artiste d'origine hawaïenne Kekai Kotaki a plusieurs fois été nommé et primé aux Spectrum Awards pour son travail sur Guild Wars et Guild Wars 2. Il a quitté ArenaNet depuis et a notamment travaillé pour Bungie Studios sur Destiny. Dans le cadre de Guild Wars 2, il a conceptualisé beaucoup de choses (une bonne partie des armes légendaires et des armures du jeu par exemple) dont toute une série de vignette représentant des Quaggans dans des situations diverses. Ces images sont souvent reprises par le site officiel du jeu. Nous allons donc interroger la Web API du jeu de manière à tenter d’accéder à ces images.

Avant d'aller plus loin, je tiens tout de même à préciser le charabia légal habituel : Guild Wars et Guild Wars 2 sont des marques déposées de ArenaNet et NCsoft. De plus, toutes les images restent la propriété de ArenaNet. De même, la Web API utilisée reste la propriété de ArenaNet qui peut décider de la retirer ou d'en interdire l'utilisation comme bon lui semble. Et bien sûr, je ne suis affilié ni avec ArenaNet, ni avec NCsoft.

Cooodage
L'endpoint quaggans permet donc d'obtenir une liste d'identifiants qui permettent en retour d'obtenir les URLs d'images. Comme indiqué sur la page wiki du jeu, si on accède à l'URL https://api.guildwars2.com/v2/quaggans, la requête retournera un tableau JSON via la chaine de texte suivante :

Code JSON : Sélectionner tout - Visualiser dans une fenêtre à part
[ "404", "aloha", "attack", "bear", "bowl", "box", "breakfast", "bubble", "cake", "cheer", "coffee", "construction", "cow", "cry", "elf", "ghost", "girl", "hat", "helmut", "hoodie-down", "hoodie-up", "killerwhale", "knight", "lollipop", "lost", "moving", "party", "present", "quaggan", "rain", "scifi", "seahawks", "sleep", "summer", "vacation"]

Nous pouvons, par exemple, tester le résultat de cette requête en Java en exécutant le code suivant, ce qui nous imprimera la chaine de texte que nous avons vu précédemment :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package test;
 
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.URL;
 
public class Main {
 
    public static void main(String[] args) throws IOException {
        final String basecode = "https://api.guildwars2.com/v2/quaggans";
        final URL url = new URL(basecode);
        try (final InputStream input = url.openStream(); final LineNumberReader reader = new LineNumberReader(new InputStreamReader(input))) {
            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                System.out.println(line);
            }
        }
    }
}

Ici, nous avons créé un nouvel objet de type URL qui nous a permis d'ouvrir un flux d’entrée (InputStream) sur le site distant. Ce flux est en suite passé dans un Reader qui permet de lire son contenu sous forme de lignes de texte. Lignes que nous imprimons sur la console.

Ce tableau contient donc des identifiants sous la forme d'une longue chaine de texte formatée au format JSON ; chaque identifiant permet d'effectuer une seconde requête pour avoir plus d'informations sur l'image en question. Prenons, par exemple, l’identifiant "aloha" ; nous pouvons désormais poster une requête sur l'URL https://api.guildwars2.com/v2/quaggans/aloha. Cette seconde requête retournera un objet JSON, toujours contenu dans une chaîne de texte, mais cette fois-ci sous la forme :

Code JSON : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
{
  "id": "aloha",
  "url": "https://static.staticwars.com/quaggans/aloha.jpg"
}

Nous pouvons tester cela en modifiant la valeur de la variable basecode dans le code posté un peu plus haut. Ici, la chaine de texte définit un objet JSON et non plus d'un tableau. Nous pouvons voir qu'il a deux champs :
  • id - qui contient le code d'identification de cet objet ;
  • url - qui contient l’adresse d'une image.


Comme la syntaxe JSON n'est pas trop compliquée, il n'est pas difficile de se créer un parser pour récupérer ce qui est dans ces chaines de texte. Cependant l'utilisation de l'API JSON-P évite de devoir le faire manuellement.

Nous avons déjà vu comment récupérer le résultat de la requête manuellement ; cependant, étant donné que la JSR 353 accepte directement les flux d’entrée, nous n'aurons plus besoin de manipuler le reader à la main. Maintenant nous allons interpréter les chaines JSON récupérées : je vais commencer par me définir deux méthodes statiques dans une classe utilitaire de manière à pouvoir les réutiliser ailleurs dans mon code. J'en ajouterai probablement d'autres par la suite.

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package test.query;
 
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
 
public final class QueryUtils {
 
    /**
    * Récupère un tableau JSON à l'URL indiquée.
    * @param basecode L'URL source.
    * @return Un objet de type  {@code JsonArray}.
    * @throws IOException En cas d'erreur.
    */
    public static JsonArray queryArray(final String basecode) throws IOException {
        final URL url = new URL(basecode);
        try (final InputStream input = url.openStream(); final JsonReader reader = Json.createReader(input)) {
            return reader.readArray();
        }
    }
 
    /**
    * Récupère un objet JSON à l'URL indiquée.
    * @param basecode L'URL source.
    * @return Un objet de type  {@code JsonArray}.
    * @throws IOException En cas d'erreur.
    */
    public static JsonObject queryObject(final String basecode) throws IOException {
        final URL url = new URL(basecode);
        try (final InputStream input = url.openStream(); final JsonReader reader = Json.createReader(input)) {
            return reader.readObject();
        }
    }
}

Ici, nous ouvrons à nouveau le flux de notre URL mais au lieu de lire son contenu comme nous avons fait précédemment, nous passons cet InputStream à un objet de type JsonReader fourni par l'API JSON-P. Nous invoquons ensuite une méthode spécialisée de ce reader de manière à parser la chaine au format JSON lue sur le flux de l'URL. La première méthode nous retournera un objet de type JsonArray en invoquant readArray(), tandis que la seconde nous retournera un objet de type JsonObject en invoquant readObject(). Il va de soit que des exceptions seront levées si vous invoquez la mauvaise méthode de lecture sur une chaine au format JSON.

Maintenant que nous savons récupérer des objets JSON, je vais ajouter une seconde classe utilitaire qui cette fois-ci est dédiée à faciliter l'utilisation de notre endpoint Quaggans :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package test.query;
 
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonString;
 
import static test.query.QueryUtils.*;
 
public final class QuaggansQuery {
    /**
    * L'URL de base de cet endpoint.
    */
    private static final String basecode = "https://api.guildwars2.com/v2/quaggans"; // NOI18N.
 
    /*
    * Récupère la liste de tous les identifiants des images de Quaggans.
    * @return Une instance de {@code List<String>}.
    * @throws IOException En cas d'erreur.
    */
    public static List<String> list() throws IOException {
        final JsonArray array = queryArray(basecode);
        // On transforme le JsonArray<JsonString> en List<String>.
        final List<String> values = array.getValuesAs(JsonString.class)
                .stream()
                .map(value -> value.getString())
                .collect(Collectors.toList());
        return Collections.unmodifiableList(values);
    }
 
    /**
    * Récupère l'URL de l'image pour un identifiant donné.
    * @param quaggan L'identificateur du Quaggan.
    * @return Une URL sous forme de {@code String}.
    * @throws IOException En cas d'erreur.
    */
    public static String imageURLForId(final String quaggan) throws Exception {
        final JsonObject object = queryObject(basecode + "/" + quaggan); // NOI18N.
        final JsonString url = object.getJsonString("url"); // NOI18N.
        return url.getString();
    }
}

Ces deux méthodes invoquent les méthodes utilitaires que nous avons précédemment définies dans la classe QueryUtils ; d'ailleurs, nous avons réalisé une importation statique pour simplifier l’écriture du code. Ici, dans la méthode list(), nous pouvons voir comment récupérer les valeurs du tableau, qui sont sous forme d'instances de type JsonString pour les mettre dans de simples instances de la classe String. Nous utilisons les flux sur les collections introduits dans le JDK8 pour convertir le tableau JSON en List<String>. Lorsque nous récupérons un objet via la seconde requête, dans la méthode imageURLForId(), nous pouvons accéder uniquement au contenu de son champs url en invoquant la méthode appropriée sur la valeur de type JSonObject lue.

Voilà, finalement, récupérer ces données n'était pas bien complexe et désormais il n'est pas trop difficile de nous construire une petite interface graphique de manière à afficher l'image disponible à l'URL récupérée par la requête. Nous allons donc :
  • charger le FXML contenant l'interface graphique, ce qui aura pour effet de charger le contrôleur ;
  • Dans ce contrôleur, nous allons :
    • récupérer la liste des identifiants des Quaggans ;
    • peupler une boite déroulante avec cette liste ;
    • lorsque l'utilisateur choisit un identifiant dans la liste nous obtenons l'URL de l'image ;
    • une fois l'URL obtenue, nous chargeons l'image ;
    • et nous l'affichons.


Compte tenu du fait que nous faisons des requêtes distantes qui peuvent être soumises à des tas d’aléas (connexion lente ou de mauvaise qualité, connexion qui coupe, site distant qui plante ou hors-ligne, etc.) ou même des données de taille importante (certaines images sont volumineuses), nous allons chercher à faire les accès à la web API dans des tâche de fond pour ne pas bloquer notre interface graphique en monopolisant le JavaFX Application Thread. De plus, nous lancerons un chargement asynchrone de l'image pour cette même raison. Si nous ne procédons pas ainsi, notre interface graphique s'en trouvera bloquée à chaque accès au site distant (récupération de la liste des Ids, récupération d'une URL, chargement de l'image). Je ne vais pas trop m'étaler sur la construction de l'IU mais si vous avez des soucis pour comprendre référez vous à la FAQ JavaFX, à mes cours ou au forum.

Code source
Pour ce premier post, je vous met directement ici, le code complet des différents fichiers de l'application. Mais, par la suite, ça risque de devenir vite assez gros donc je vous mettrai des fichiers ZIP en fin de blog et un lien vers GitHub.com (ou sera placée la version la plus récente du projet) :

Fichier test.QuagganSelector.properties (textes de base en anglais).
Code Properties : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
app.title=Quaggan Selector
choose.your.quaggan=Choose your Quaggan:

Fichier test.QuagganSelector_fr.properties (textes en français).
Code Properties : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
app.title=Sélecteur de Quaggan
choose.your.quaggan=Choisissez votre Quaggan :

Fichier test.QuagganSelector.css (feuille de style de l'application). J'ai ici donné à l'application une apparence similaire à celle du site officiel du jeu.
Code CSS : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.root-pane {
    -fx-background-color: white;
    -fx-background-image: url("corner.png");
    -fx-background-position: top left;
    -fx-background-repeat: no-repeat;
    -fx-padding: 70px 50px 50px 50px;
    -fx-spacing: 10px;
    -fx-font-family: serif;
}
.main-text {
    -fx-font-size: 3.0em;        
}
.combo-box {
    -fx-font-size: 1.4em;   
    -fx-background-color: black, white;
    -fx-background-insets: 0px, 1px;
    -fx-background-radius: 0px;
}

Fichier test.QuagganSelector.fxml (l'interface graphique de l'application).
Code XML : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.scene.image.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
 
<VBox id="rootPane" fx:id="rootPane" styleClass="root-pane" prefHeight="600" prefWidth="400" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="test.controller.QuagganSelectorController">
    <children>
        <TextFlow id="chooseFlow" fx:id="chooseFlow" VBox.vgrow="NEVER">     
            <children>    
                <Text id="chooseLabel" fx:id="chooseLabel" styleClass="main-text" text="%choose.your.quaggan"/>
            </children>
        </TextFlow>
        <ComboBox id="quagganIdCombo" fx:id="quagganIdCombo" maxWidth="1.7976931348623157E308" VBox.vgrow="NEVER" />
        <StackPane VBox.vgrow="ALWAYS">
            <children>
                <ImageView id="quagganImage" fx:id="quagganImage" fitWidth="350.0" pickOnBounds="true" preserveRatio="true" />
                <ProgressIndicator id="progressIndicator" fx:id="progressIndicator" maxHeight="64.0" maxWidth="64.0" />
            </children>
        </StackPane>
    </children>
</VBox>

Fichier test.controller.QuaggaSelectorController.java (le contrôleur du FXML).
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package test.controller;

import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import test.query.QuaggansQuery;

public final class QuagganSelectorController implements Initializable {

    @FXML
    private ComboBox<String> quagganIdCombo;
    @FXML
    private ImageView quagganImage;
    @FXML
    private ProgressIndicator progressIndicator;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        quagganImage.setVisible(false);
        //        
        progressIndicator.setVisible(false);
        progressIndicator.setProgress(0);
        //        
        quagganIdCombo.getSelectionModel().selectedItemProperty().addListener(observable -> {
            final String quagganId = quagganIdCombo.getSelectionModel().getSelectedItem();
            queryQuagganImage(quagganId);
        });
        // Chargement de la liste des Ids des quaggans.
        loadQuagganList();
    }

    /**
     * Charge la liste des Ids des quaggans de manière asynchrone.
     */
    private void loadQuagganList() {
        final Service<ObservableList<String>> query = new Service<ObservableList<String>>() {

            @Override
            protected Task<ObservableList<String>> createTask() {
                return new Task<ObservableList<String>>() {

                    @Override
                    protected ObservableList<String> call() throws Exception {
                        final List<String> quagganList = QuaggansQuery.list();
                        // On transforme la liste en liste observable.
                        final ObservableList<String> observableQuagganList = FXCollections.observableList(quagganList);
                        // On transforme la liste observable en liste observable triée.
                        final SortedList<String> sortedQuagganList = new SortedList<>(observableQuagganList, String::compareTo);
                        return sortedQuagganList;
                    }
                };
            }
        };
        query.setOnSucceeded(workerStateEvent -> {
            final ObservableList<String> guagganList = query.getValue();
            quagganIdCombo.setItems(guagganList);
        });
        query.setOnFailed(workerStateEvent -> {
            System.out.println(query.getException());
        });
        query.start();
    }

    /**
     * Chargement de l'URL de l'image sélectionnée de manière asynchrone.
     * @param quagganId Id à charger.
     */
    private void queryQuagganImage(final String quagganId) {
        if (quagganId == null) {
            return;
        }
        // Avant le début de la requête, on va modifier l'apparence de l'interface.
        quagganImage.setVisible(false);
        quagganImage.setImage(null);
        progressIndicator.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
        progressIndicator.setVisible(true);
        //
        final Service<String> query = new Service<String>() {

            @Override
            protected Task<String> createTask() {
                return new Task<String>() {

                    @Override
                    protected String call() throws Exception {
                        return QuaggansQuery.imageURLForId(quagganId);
                    }
                };
            }
        };
        query.setOnSucceeded(workerStateEvent -> {
            // Lorsque l'URL de l'image a été récupérée, l'image est chargée de manière asynchrone.
            final String quagganURL = query.getValue();
            loadQuagganImage(quagganURL);
        });
        query.setOnFailed(workerStateEvent -> {
            System.out.println(query.getException());
        });
        query.start();
    }

    /**
     * Chargement de l'image sélectionnée de manière asynchrone.
     * @param quagganURL URL de l'image à charger.
     */
    private void loadQuagganImage(final String quagganURL) {
        final Image quagganIcon = new Image(quagganURL, true);
        quagganIcon.progressProperty().addListener(observable -> {
            displayQuagganImage(quagganIcon);
        });
        quagganIcon.exceptionProperty().addListener(observable -> {
            System.out.println(quagganIcon.getException());
        });
        // Si l'image est suffisament petite ou si la connexion est suffisament rapide, l'image est peut-être déjà chargée.
        displayQuagganImage(quagganIcon);
    }

    /**
     * Affiche l'image et restaure l'interface.
     * @param quagganIcon L'image à afficher.
     */
    private void displayQuagganImage(final Image quagganIcon) {
        if (quagganIcon.getProgress() == 1) {
            progressIndicator.setVisible(false);
            progressIndicator.setProgress(0);
            quagganImage.setVisible(true);
            quagganImage.setImage(quagganIcon);
        }
    }
}

Fichier test.QuagganSelector.java (l'application).
Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package test;
 
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
 
public final class QuagganSelector extends Application {
 
    @Override
    public void start(Stage primaryStage) throws IOException {
        // Chargement de l'interface graphique.
        final ResourceBundle bundle = ResourceBundle.getBundle("test.QuagganSelector"); // NOI18N.
        final URL fxmlURL = getClass().getResource("QuagganSelector.fxml"); // NOI18N.
        final FXMLLoader fxmlLoader = new FXMLLoader(fxmlURL, bundle);
        final Parent root = fxmlLoader.load();
        // Affichage de la fenêtre.
        final Scene scene = new Scene(root);
        final URL cssURL = getClass().getResource("QuagganSelector.css"); // NOI18N.
        scene.getStylesheets().add(cssURL.toExternalForm());
        primaryStage.setTitle(bundle.getString("app.title")); // NOI18N.
        primaryStage.setScene(scene);
        primaryStage.show();
    }
 
    public static void main(String[] args) {
        Application.launch(args);
    }
}

Vous trouverez également tous ces fichiers dans l'archive ci-jointe, ce qui vous permettra d'importer ces sources dans un projet créé dans votre IDE favori : QuagganSelector-src.zip
La version la plus récente du projet, au format NetBeans, est accessible sur GitHub.

Voila, notre toute première application permettant d’accéder à la Web API du jeu Guild Wars 2 est désormais terminée. Au lancement, et après sélection d'une image dans la liste déroulante, nous obtenons le résultat suivant :

Nom : QuagganSelector.jpg
Affichages : 711
Taille : 47,2 Ko

Conclusion
Voilà, Quaggan a bien travaillé, Quaggan peut aller se reposer...
Nom : sleep.jpg
Affichages : 669
Taille : 7,2 Ko

Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Viadeo Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Twitter Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Google Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Facebook Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Digg Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Delicious Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog MySpace Envoyer le billet « Utiliser la JSR 353 pour accéder à la Web API de Guild Wars 2 - partie 1 » dans le blog Yahoo

Mis à jour 01/06/2015 à 06h16 par bouye

Catégories
Java , Java , JavaFX , JSON

Commentaires

  1. Avatar de itsmyr4bbit
    • |
    • permalink
    Merci pour ce tuto qui touche à la nouvelle API Json. A la fin de votre article, vous indiquez le projet en pièce jointe, par contre je ne le vois pas (edit, il faut être connecté pour voir l'archive). Je n'ai pas l'habitude d'utiliser java FX et je trouve ça pas si mal en voyant le fichier fxml.
  2. Avatar de bouye
    • |
    • permalink
    Bonjour,
    le fichier QuagganSelector-src.zip apparait en lien en fin de la phrase et je peux le voir même si je vais sur le post en étant pas connecté (avec IE, FireFox et Chrome) donc le problème semble être plus de ton coté. Ceci dit le choix de couleur dans le nouveau thème de Développez est assez discutable, il rend les hyperliens peu visibles.

    EDIT - un didacticiel sur FXML va paraitre sous peu.
  3. Avatar de jaafaresprit
    • |
    • permalink
    Bonsoir,
    Tout d'abord, je vous remercie pour votre excellent tutoriel.
    Tout fois le lien vers le fichier zip apparait comme ce ci
    Vous trouverez également tous ces fichiers dans l'archive ci-jointe, ce qui vous permettra d'importer ces sources dans un projet créé dans votre IDE favori : [ATTACH]173963d1/a/a/a" />
    Merci encore et bon continuation pour la suite.
  4. Avatar de bouye
    • |
    • permalink
    Vraiment bizarre ; sous Windows, j'ai même essayé avec Safari (en plus des 3 autres) et le lien est toujours correct pour moi que je sois identifié ou pas sur la page.
  5. Avatar de bouye
    • |
    • permalink
    Pour ceux qui ont du mal à accéder à l'archive ZIP liée dans l'article, je viens de rajouter un lien vers le repo GitHub ou se trouve la version la plus récente du projet (format NetBeans).