JIRACette semaine, j’ai passé un peu de temps au boulot à travailler sur l’extraction de données stockées dans JIRA, en Java. Pour rappel, JIRA est un système de suivi de bugs, un système de gestion des incidents, et un système de gestion de projets, fourni par Atlassian, et c’est bien sûr le système que l’on utilise dans ma boîte. Dans le cadre d’un nouveau projet dont je m’occupe, pour lequel je stocke sous JIRA l’ensemble des tâches à réaliser par toute l’équipe, j’ai besoin d’extraire toutes les informations liées à chacune de ces tâches pour construire en automatique un planning GANTT.

JIRA (Atlassian)

Remarque : il y a bien un plugin officiel pour JIRA orienté “gestion de projet”, il s’agit de Greenhopper, malheureusement il est payant et il n’est pas envisagé aujourd’hui de s’en équiper chez nous.

Atlassian fournit donc plusieurs mécanismes pour interagir avec JIRA (en action - création de nouvelles tâches par ex. - et/ou en extraction). A ma connaissance il y a :

  • l’API XML-RPC ;
  • l’API SOAP ;
  • les flux XML et RSS directement depuis le serveur JIRA ;
  • les Jelly Scripts.

En préambule, il est conseillé de jeter un oeil sur la page How JIRA works.

XML-RPC

XML-RPC est un protocole d’interaction (Remote Procedure Call) permettant d’inteagir par XML avec le système. En lui même, c’est donc juste un ensemble de méthodes à utiliser par des appels XML. Autrement dit utilisable avec n’importe quel langage.

Une documentation JIRA présente ce protocole : JIRA, XML-RPC Overview.

Une implémentation (non-officielle) est par ailleurs disponible en Java et fonctionne parfaitement bien : il s’agit de la librairie Swizzle [http://swizzle.codehaus.org/Home ].

J’avais déjà réalisé un traitement Java avec cette librairie il y a quelques mois, pour récupérer l’ensemble des fiches JIRA liées à une version projet et créer en automatique de nouvelles fiches.

L’utilisation de la librairie Swizzle est cependant très simple, exemple de connexion :

  1. try {
  2. org.codehaus.swizzle.jira.Jira jira = new Jira(url);
  3. jira.login(username, password);
  4. } catch (MalformedURLException e) {
  5. throw new BaseException(e);
  6. } catch (Exception e) {
  7. throw new BaseException(e);
  8. }

Et un exemple de récupération de données sur une recherche libre (il y a bien sûr d’autres méthodes pour récupérer les données) :

  1. Vector v = new Vector();
  2. v.add("CLE_DU_PROJET_JIRA"); // le ou les projets sur lesquels faire la recherche
  3. String critereDeRecherche = ""; // On ramène toutes les fiches JIRA
  4. int nbFichesARamener = 1000; // nombre max de fiches JIRA à récupérer
  5. List issues = jira.getIssuesFromTextSearchWithProject(v, critereDeRecherche, nbFichesARamener);

Ensuite la plupart des informations se retrouvent sur l’objet Issue correspondant. Il y a bien sûr des objets Java pour représenter les Versions JIRA, les filtres, les utilisateurs, le projet JIRA, etc.

Le seul problème vient du fait que toutes les opérations / informations disponibles sous JIRA ne le sont pas au travers de l’API XML-RPC, notamment dans le cas qui m’intéresse, les informations relatives aux sous-tâches et aux champs personnalisés ne sont pas exportées, donc obligé de faire autrement, d’où le switch sur la 2e API proposée par Atlassian, via SOAP.

SOAP

Il s’agit d’une API externe également, avec cette fois une implémentation Java “officielle” fournie par Atlassian (ainsi qu’une implémentation Python, au passage).

La mise en place est moins évidente qu’avec Swizzle, puisqu’il en faut ici :

  • récupérer le projet SOAP client en ligne ;
  • récupérer le fichier WSDL sur le serveur JIRA sur lequel on veut se mapper (fichier décrivant l’ensemble des services disponibles) ;
  • utiliser un profile Maven pour générer à partir de ce fichier WDSL les classes Java (dans “target/generated-classes/*.class”) correspondant aux services disponibles ;
  • toujours avec Maven, mettre à jour le .classpath Eclipse ;
  • pouvoir enfin utiliser les objets natifs Java permettant d’interagir avec JIRA via SOAP ;
  • plus en bonus, mettre en place un packaging sous forme de .jar de ces classes générées pour ne pas avoir à refaire ces manipulations à chaque fois (à faire par soi même) ;

Toutes ces opérations sont détaillées dans la documentation en ligne Atlassian : Creating a SOAP Client pour la présentation générale de l’API SOAP et dans le README du projet exemple pour le détail des manipulations techniques.

S’il n’y a rien de compliqué, c’est moins simple que de simplement ajouter une dépendance comme Swizzle, mais cela a l’avantage de construire une API de services correspondant réellement aux services disponibles sur le serveur JIRA utilisé, quelle que soit la version de ce dernier.

L’usage de ce client Java SOAP est très simple également et ressemble bigrement à l’API XML-RPC, exemple pour la connexion :

  1. String url = "http://jira.domaine.tld/";
  2. String username = "login de servitude";
  3. String password = "password du compte de servitude";
  4. (...)
  5. JiraSoapServiceService jiraSoapServiceLocator = new JiraSoapServiceServiceLocator();
  6. JiraSoapService jiraSoapService = null;
  7. String authToken; // il est important de garder ce auth token, toutes les méthodes en auront besoin ensuite
  8. RemoteType[] remoteTypes; // on va conserver en local la liste des types disponibles au niveau du projet
  9. // pour pouvoir ensuite facilement retrouver à quoi correspondant le type d'une tâche
  10. RemoteStatus[] remoteStatuses // Idem pour les status
  11. try {
  12. if (url == null) {
  13. jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2();
  14. } else {
  15. jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2(url);
  16. log.info("SOAP Session service endpoint at " + url.toExternalForm());
  17. }
  18.  
  19. authToken = getJiraSoapService().login(username, password);
  20. remoteTypes = getJiraSoapService().getIssueTypes(getAuthToken());
  21. remoteStatuses = getJiraSoapService().getStatuses(getAuthToken());
  22. } catch (ServiceException e) {
  23. throw new BaseException("ServiceException during SOAPClient contruction", e);
  24. } catch (RemoteAuthenticationException e) {
  25. throw new BaseException("RemoteAuthenticationException during SOAPClient contruction", e);
  26. } catch (RemoteException e) {
  27. throw new BaseException("RemoteException during SOAPClient contruction", e);
  28. } catch (java.rmi.RemoteException e) {
  29. throw new BaseException("RemoteException during SOAPClient contruction", e);
  30. }

Et pour comparaison, la récupération de données dans JIRA (qui ressemble toujours bigrement à la méthode précédente) :

  1. RemoteIssue[] issues;
  2. int nbFichesARamener = 1000; // nombre max de fiches JIRA à récupérer
  3. try {
  4. issues = getJiraSoapService().getIssuesFromTextSearchWithProject(getAuthToken(), new String[] { project.getKey() }, "", nbFichesARamener);
  5. } catch (RemoteException e) {
  6. throw new BaseException(e);
  7. } catch (java.rmi.RemoteException e) {
  8. throw new BaseException(e);
  9. }

Malheureusement, toujours les mêmes limitations : de nombreuses informations ne sont pas disponibles.

Visiblement je ne suis pas le seul à le déplorer, on trouve de nombreuses issues ouvertes chez Atlassian sur ce sujet … certaines depuis 2005 (voir par ex. ce thread sur Stackoverflow.com et celui-ci dans le forum JIRA et un patch a même été développé).

Donc à ce stade après avoir passé quelques heures à jouer avec le mécanisme SOAP, je n’ai toujours pas moyen de récupérer toutes les informations dont j’ai besoin.

Si les solutions officielles et “propres” fournies par les API JIRA ne fonctionnent pas, il va alors falloir revenir à quelque chose de plus simple : les exports XML.

Bonus : un tutorial très détaillé pour l’utilisation de l’api JIRA SOAP, Vérifier le JIRA avant de faire la release.

XML

Un peu partout dans JIRA, on peut trouver des liens “RSS” et “XML” qui exportent les données JIRA correspondant à la recherche courante. En regardant le contenu, on voit que cette fois il y a bien absolument toutes les informations nécessaires, y compris les champs supplémentaires et les informations sur les sous-tâches (à l’exception notable de la “date de démarrage”, mais je peux m’en passer).

Il suffit donc de mettre en place une récupération par HTTPClient pour récupérer le flux XML, et de le parser (dans mon cas avec la très bonne API simple-xml, qui permet de facilement mapper dans les deux sens du XML vers des beans java par simples annotations) pour récupérer toutes les infos souhaitées. Ca marche mais c’est moins rigoureux, il faut notamment récupérer les informations de type “date” sous une forme textuelle et faire un parsing dessus (autrement dit le format des dates peut changer à la faveur d’une montée de version du serveur, par ex.). Mais vu que je n’ai pas trop le choix …

Il faut bien sûr toujours avoir un compte qui puisse accéder par HTTP aux pages JIRA (dans mon cas, un “compte de service” que j’ai créé uniquement à cet usage, pour ne pas avoir à utiliser mon identifiant réseau propre).

Exemple de récupération du flux XML par HTTPClient :

  1. String host = "jira.domaine.tld";
  2. String username = "login de servitude";
  3. String password = "password du compte de servitude";
  4.  
  5. final GetMethod method = new GetMethod(url);
  6. try {
  7.  
  8. final HttpClient client = new HttpClient();
  9. client.getHostConfiguration().setHost("host", 80, "http");
  10. final Credentials defaultcreds = new UsernamePasswordCredentials(username, password);
  11. client.getState().setCredentials(new AuthScope(host, 80, AuthScope.ANY_REALM), defaultcreds);
  12. method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false));
  13.  
  14. int statusCode = client.executeMethod(method);
  15.  
  16. InputStream responseBody = method.getResponseBodyAsStream();
  17. InputStreamReader reader = new InputStreamReader(responseBody);
  18.  
  19. Serializer serializer = new Persister();
  20. JIRAXMLRss rss = serializer.read(JIRAXMLRss.class, reader);
  21.  
  22. return rss.getChannel().getItems(); // Retourne une liste de beans "maison" alimentés depuis le XML par SimpleXML
  23. } catch (HttpException e) {
  24. throw new BaseException(e);
  25. } catch (IOException e) {
  26. throw new BaseException(e);
  27. } catch (Exception e) {
  28. throw new BaseException(e);
  29. } finally {
  30. method.releaseConnection();
  31. // autres releases ...
  32. }

Mise en place.

Au final - les quelques lignes de code ci-dessus n’étant que des exemples, comme j’avais déjà codé le tout, j’ai fini par garder les 3 implémentations et pouvoir ainsi facilement en switcher selon les usages (je ne détaille pas plus avant, ce post se voulant seulement une présentation des manières d’accéder à JIRA en Java).

Annexes

Jelly scripts

Les scripts Jelly sont des scripts XML exécutables, permettant de réaliser des opérations à partir d’un formalisme XML. C’est un projet Apache, implémenté dans JIRA, mais surtout pour des opérations de type export / import. Je connaissais pour avoir déjà été amené à utiliser ce mécanisme dans le cadre d’une migration Bugzilla vers JIRA lorsque l’on a switché sur ce dernier, mais les fonctions proposées sont plus orientées traitement de masse et semblent ne pas tout couvrir non plus, j’ai donc laissé tomber.

Des exemples de Jelly scripts.

Extension du serveur JIRA.

Comme je n’ai pas la main sur le serveur JIRA (impossible d’y modifier quoi que ce soit de mon côté), je n’ai pas pu déployer cette extension qui aurait corrigé mon problème : Jira Extended Webservice.

C’est un patch qui apporte la gestion des sous-tâches sur l’API SOAP, développé par un utilisateur externe à Atlassian. Voir également le Google Group correspondant.

Bonus

En side point, une présentation vidéo intéressante sur la construction du client lourd JIRA : Killer JIRA CLI.