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.


mercredi 2 octobre 2013

Valider la syntaxe d'un source Java avec Eclipse JDT

Mettre en place un système de vérification syntaxique de code source est initialement tiré d'un besoin d'avoir un smoke test lors de la génération de code : une première étape permettant de valider à minima que mon code généré est OK. Si je sais que la syntaxe de mon fichier généré n'est pas bonne dès la génération alors il ne me sert à rien de l'intégrer dans mon IDE, ou même d'essayer de le compiler.

La solution est apparu en fouillant JDT pour un autre besoin (formater automatiquement du code d'après un formatter Eclipse...) : l'API de parsing AST de JDT fourni tout le nécessaire pour mon besoin, de manière complète et facile à utiliser.

Récupération des librariries Eclipse JDT

Eclipse met à disposition un ensemble de librairies qui peuvent être utilisées en standalone (j'entend par là, hors du contexte d'exécution d'Eclipse, sans lancer toute l'IHM Eclipse.) et donc tout particulièrement utile dans notre cas.
J'utilise les dernières librairies fournis dans Eclipse Kepler 4.3. Ces librairies ne sont pas disponibles dans les dépôts publics Maven (ou du moins, pas complétement ou de manière assez fouillie...). Je conseille donc de télécharger la dernière version d'Eclipse et de récupérer les librairies suivantes dans le répertoire plugins pour les intégrer à ce projet.
J'ai besoin des librairies suivantes  :
  • org.eclipse.core.contenttype_3.4.200.v20130326-1255.jar
  • org.eclipse.core.jobs_3.5.300.v20130429-1813.jar
  • org.eclipse.core.resources_3.8.101.v20130717-0806.jar
  • org.eclipse.core.runtime_3.9.0.v20130326-1255.jar
  • org.eclipse.equinox.common_3.6.200.v20130402-1505.jar
  • org.eclipse.equinox.preferences_3.5.100.v20130422-1538.jar
  • org.eclipse.jdt.core_3.9.1.v20130905-0837.jar
  • org.eclipse.osgi_3.9.1.v20130814-1242.jar

Comprendre l'API JDT et les problèmes de syntaxe

Le Parser AST d'Eclipse peut s'appuyer directement sur un source en String. Pour cela, il faut quand même le convertir en tableau de char.
  char[] sourceArray = source.toCharArray();
Nous aurions pu également tirer parti de la classe Util (org.eclipse.jdt.internal.compiler.util) qui nous propose de convertir directement le contenu d'un fichier sous la forme d'un tableau de char.
   char[] sourceArray = Util.getFileCharContent(file, encoding);
Nous utilisons le JLS 4 qui permet de parser les sources allant des versions Java 1.1 à 1.7.
Il faut, de plus spécifier une option de compilation pour indiquer le niveau de conformité des sources : en version 1.7 dans notre cas.
Le type de source est également indiqué : Nous pouvons parser une expression (K_EXPRESSION), une déclaration (K_STATEMENT), un corps de classe (K_CLASS_BODY_DECLARATIONS) ou, comme dans notre cas, une classe Java complète (K_COMPILATION_UNIT).
  ASTParser parser = ASTParser.newParser(AST.JLS4);
  Map options = new HashMap(1);
  options.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_7);
  parser.setCompilerOptions(options);
  parser.setKind(ASTParser.K_COMPILATION_UNIT);
  parser.setSource(sourceArray);
La source est indiquée au parser grâce à son contenu indiqué dans sourceArray. La méthode createAST() crée un arbre syntaxique que nous pourrions explorer par la suite... mais ce n'est pas l'objet de cet article.
  CompilationUnit unit = (CompilationUnit) parser.createAST(null);
Ce qui nous intéresse ici, c'est de savoir quels sont les problèmes dans notre code source.  Attention à cette étape, seules les erreurs syntaxiques sont remontées (les erreurs de binding, d'imports, etc ne sont pas remonté lors de la remontée dans l'AST). L'analyseur syntaxique fourni par JDT est suffisamment robuste pour nous indiquer toutes les erreurs rencontrées lors du parsing de la classe via la méthode getProblems().
  IProblem[] pbs = unit.getProblems();
Si le tableau est vide, alors de problème de syntaxe. Sinon, les objets IProblem indiquent les portions de code en erreur.
  if(pbs.length > 0) {
      displayProblems(pbs, unit, source, out);
      return false;
   } else {
    return true;
   }
Nous allons donc afficher ces problèmes, pour l'instant en itérant sur chacune des erreurs pour l'afficher, indiquer la ligne de code en erreur et afficher un souligné pour mettre en avant la zone impactée.
La zone en souligné est récupérée depuis le IProblem via les méthodes getSourceStart() et getSourceEnd(). Nous utilisons la méthode utilitaire getColumnNumber() (disponible sur la classe CompilationUnit) pour nous indiquer  l'index de début et de fin par rapport à la ligne courante. Le libellé et le souligné sont affichés par rapport à ces infos.
    
private static void displayProblems(IProblem[] pbs, CompilationUnit unit, String source, PrintStream out) {
    for(IProblem pb : pbs) {
        displayError(pb, out);
        displayLine(source, pb.getSourceLineNumber(), out);
        displayEmphasize(unit.getColumnNumber(pb.getSourceStart()) , unit.getColumnNumber(pb.getSourceEnd()), out);
    }
}

private static void displayError(IProblem pb, PrintStream out) {
    out.append("line ");
    out.append(String.valueOf(pb.getSourceLineNumber()));
    out.append(" : ");
    out.append(pb.getMessage());
    out.append('\n');
}

private static void displayEmphasize(int start, int stop, PrintStream out) {
    int length = stop + 1;
    out.append("  ");
    for(int i=0; i < length; i++) {
        if(i < start || i > stop) {
            out.append(' ');
        } else {
            out.append('-');
        }
    }
    out.append('\n');
}
private static void displayLine(String source, int line, PrintStream out) {
    out.append("  ");
    String[] strlines = source.split("\n");
    out.append( strlines[line - 1 ]);
    out.append('\n');
}

Cas de test sur un exemple concret

J'ai créé un petit cas de tests avec plusieurs erreurs dans le morceaux de code ci-dessous.

package flo.test;
import 1flo.truc.*
import java.util.Date;

   sdf   
public class Toto {
int toto = 123_456;
Bla ex = new Bla();
Truc t ; 
Titi t2; 
private class Titi {}
}

Lors de l'analyse, le morceaux de programme nous remonte les erreurs dans le fichier source et indique les portions qui sont syntaxiquement erronées.

line 2 : Syntax error on token "1f", delete this token
  import 1flo.truc.*
         --
line 2 : Syntax error on token "*", ; expected after this token
  import 1flo.truc.*
                   -
line 5 : Syntax error on token "sdf", delete this token
     sdf   
     ---

Conclusion

L'API Eclipse JDT offre un cadre idéal pour mettre en place rapidement et facilement un système de validation de syntaxe Java (et bien plus encore !). Il n'y a plus qu'à intégrer cette vérification à la suite de notre génération de code pour assurer un premier niveau de validation des sources générées.

 Les sources complètes de la classe sont disponibles sur Github : https://gist.github.com/florentdupont/6799971

Si vous voulez aller plus loin sur l'utilisation de l'AST proposé par Eclipse JDT, je vous conseille l'article de Lars Vogel : Abtract Syntax Tree et Java Model qui explique rapidement les notions de JDT et l'utilisation du Pattern Visitor pour explorer votre arbre. Il regroupe aussi un grand nombre de liens utiles.