jeudi 10 octobre 2013

Formater du code Java avec JDT

Je continue ma série d'article sur l'industrialisation des générateurs de code commencé précédemment.
Dans l'article précédent je présentais comment analyser la syntaxe d'un code source Java afin d'effectuer un premier niveau de validation.
Dans cet article, nous allons voir comment formater son code de manière programmatique (en standalone), conformément à un formateur Eclipse. On va voir comment utiliser Eclipse JDT pour nous aider.

Mise en place du projet

Pour l'utilisation du formateur d'Eclipse, je m'appuie sur les librairies disponibles dans Eclipse Kepler. J'utilise les librairies suivantes :
  • org.eclipse.jdt.core_3.9.1.v20130905-0837.jar
  • org.eclipse.jface_3.9.1.v20130725-1141.jar
  • org.eclipse.jface.text_3.8.101.v20130802-1147.jar
  • org.eclipse.core.runtime_3.9.0.v20130326-1255.jar
  • org.eclipse.equinox.common_3.6.200.v20130402-1505.jar
  • org.eclipse.text_3.5.300.v20130515-1451.jar
  • org.eclipse.osgi_3.9.1.v20130814-1242.jar
  • org.eclipse.core.resources_3.8.101.v20130717-0806.jar
  • org.eclipse.core.jobs_3.5.300.v20130429-1813.jar
  • org.eclipse.core.contenttype_3.4.200.v20130326-1255.jar
  • org.eclipse.equinox.preferences_3.5.100.v20130422-1538.jar

Il est possible de formater son code Java en utilisant l'API directement depuis la ligne de commande. C'est ce qu'explique Peter Friese dans son article Formatting your code using the Eclipse code formatter.
L'idée est d'appeler l'API depuis la ligne de commande :
<path-to-eclipse>\eclipse.exe -vm <path-to-vm>\java.exe -application **org.eclipse.jdt.core.JavaCodeFormatter** -verbose -config <path-to-config-file>\org.eclipse.jdt.core.prefs <path-to-your-source-files>\*.java
C'est une approche qui pourrait répondre à nos besoins, mais dans mon cas, je souhaitais pouvoir exécuter depuis un process Java existant.

Avant de pouvoir appeler le formateur JDT, il faut d'abord récupérer les options de formatage. On a deux possibilités : utiliser les préférences du workspace ou utiliser un formateur XML.

Récupération des options depuis les préférences de workspace

Le fichier org.eclipse.jdt.core.prefs contient les préférences de formatage d'un workspace Eclipse.
Il est disponible dans le répertoire .metadata\.plugins\org.eclipse.core.runtime\.settings de votre workspace.
Il propose des options de formatage sous la forme Clé=Valeur. Ce fichier Properties peut donc être lu facilement avec l'API Properties de Java. L'objet Properties implémente Map<Object, Object>, on peut donc récupérer nos propriétés sous forme de Map très facilement avec un simple cast. L'API de formattage de JDT utilisant un Map<String, String>, cette approche nous simplifie le travail.
(je n'indique ici que le code utile...j'ai intentionnellement supprimé les catch et fermeture de flux pour faciliter la lecture)
private static Map<String, String> readPreferences(String filename) {

    File configFile = new File(filename);
    BufferedInputStream  stream = new BufferedInputStream(new FileInputStream(configFile));
    final Properties formatterOptions = new Properties();
    formatterOptions.load(stream);
    return (Map)formatterOptions;
}

Récupération des options depuis un formateur Eclipse

Pour la récupération d'options depuis un fichier de formateur Eclipse, le travail est un peu plus (mais à peine) compliqué.
Le fichier XML n'est qu'une autre représentation du fichier Properties dans laquelle chaque noeud setting propose le couple [clé,valeur] avec les attributs [id, value].
Un parsing du fichier permet donc d'en sortir une Map<String, String> sans trop de difficultés.

private static Map<String, String> readFormatter(String filename) {
    Map<String, String> options = new TreeMap<String, String>();
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);

    DocumentBuilder builder = factory.newDocumentBuilder();
    org.w3c.dom.Document doc = builder.parse(filename);
    XPathFactory xFactory = XPathFactory.newInstance();
    XPath xpath = xFactory.newXPath();
    XPathExpression expr = xpath.compile("//setting");
    Object result = expr.evaluate(doc, XPathConstants.NODESET);

    NodeList nodes = (NodeList) result;
    for(int i = 0; i < nodes.getLength(); i++) {
        Node id = nodes.item(i).getAttributes().getNamedItem("id");
        Node value = nodes.item(i).getAttributes().getNamedItem("value");
        options.put(id.getNodeValue(), value.getNodeValue());
    }
    return options;
}

Formater le code à partir des options

Une fois les options récupérées sous la forme d'un Map<String, String>, il suffit de récupérer un CodeFormatter avec createCodeFormatter(), puis de formater le code à partir du code source existant. Dans notre cas, nous formatons l'intégralité d'un fichier source (K_COMPILATION_UNIT). Le source formatté est récupéré par une gymastique : le TextEdit est appliqué à un Document, le code source est ensuite récupéré depuis le document avec get() sous la forme d'une chaine de caractère.

CodeFormatter formatter = ToolFactory.createCodeFormatter(options);
TextEdit textEdit = formatter.format(CodeFormatter.K_COMPILATION_UNIT, source, 0, source.length(), 0, null);
IDocument document = new Document(source);
textEdit.apply(document);
String formattedSource = document.get();

Conclusion

L'intérêt de pouvoir formater son code de manière programmatique simplifie le travail lors de la création d'un générateur de code. En effet, il n'y a plus besoin de se soucier de prendre en compte le formatage lors de la conception du générateur de code :ce formatage peut peut être fait en post-génération.
L'intérêt de pouvoir paramétrer son formateur et l'externaliser du générateur est autrement plus intéressant si l'on souhaite proposer plusieurs "saveurs de formatage" (idéal notamment pour les Centre de Service en SSII par exemple), car il permet de générer des sources compatibles aux conventions de codage de chaque client de manière très simple et à moindre coût.

les sources complètes sont disponibles sur Github.


Aucun commentaire:

Enregistrer un commentaire