samedi 24 septembre 2011

Java Puzzler - 57 is back !

Detecting a bug and solving it may be very frustrating, but it becomes enjoyable when you try to turn it into a challenge. Once again, I ran into a bug. Once again, I did not see it comming and once again, I will turn this into a puzzler.

No XML Serialization this time, but the same pattern : You will see the code and will be asked to guess what it produces.


Puzzler

The following program tries to compare the method equals and containsAll of java sets and lists. It creates one set and one list with a regex pattern that matches "Mickey" and "mouse". What does the program print ?

public static void main(String... args) {
    System.out.println(prepareList().equals(prepareSet()));
    System.out.println(prepareList().containsAll(prepareSet()));
}

private static List<Pattern> prepareList() {
    List<Pattern> result = new ArrayList<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    return result;
}

private static Set<Pattern> prepareSet() {
    Set<Pattern> result = new HashSet<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    return result;
}

What do we expect ?

This program seems pretty straightforward. It compares two collections containing the same Pattern. If we take a brief look at the javadoc of the List interface, we see that the equals method should return true "if and only if the specified object is also a list, both lists have the same size, and all corresponding pairs of elements in the two lists are equal". We expect the first statement to print false for a Set is not a List. The containsAll method takes a Collection, which is the superinterface of Set and checks if the list "contains all of the elements of the specified collection". Therefore the program should print :

false
true

What do we have ?

If you ran it, you saw a different thing :

false
false

Our first assumption seems correct, but not the second one. What can be wrong ?


Solution

Our program only uses base classes of the Java platform. Set and List classes are used in almost every Java application, so they are hardly arguable. Our only chance is to create a test that will first assert that two identical Pattern does equal.

public static void main(String... args) {
    Pattern original = Pattern.compile("[Mm](ickey|ouse)");
    Pattern copy = Pattern.compile("[Mm](ickey|ouse)");
    System.out.println(original.equals(copy));
}

If you ran this test, you saw that it print false, which means that two identical patterns does not equal.

Indeed, if you look closely to the source code of the Pattern class, you will see that it does not override the equals() method of java.lang.Object, nor does it override the hashCode() method. The following program will therefore print "3", because there is 3 "distinct" patterns in the set.

public static void main(String... args) {
    Set<Pattern> result = new HashSet<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    System.out.println(result.size());
}

We could override hashCode() and equals() with the following implementations to solve the problem. However, to make things worse, the Pattern class is final, so we must create a wrapper and all sort of delegates methods.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return this.toString().equals(o.toString());
}

@Override
public int hashCode() {
    return this.toString().hashCode();
}

The lesson of this puzzle is : when comparing patterns, compare the toString() results instead of the objects themselves.

As i said in my previous article, if you haven't read the book "Java Puzzlers" yet, I urge you to do so. If you have read it, you may have noticed that this puzzler is quite similar with Puzzler #57 - "What's in a Name ?", which also explains the title of this article.

Do know why this implementation choice was made ? Did you run into this bug and find another workaround ? Let's hear about it in the comments.

dimanche 11 septembre 2011

Java Puzzler - Defensive Serialization

It has been a long time since I didn't write on Pingtimeout. Sorry for that :)

Yesterday, while I was adding some features to JTailPlus, I ran into an XML Serialization problem. I had some difficulties to find out what was wrong, and I must admit I didn't expect this kind of error to occur. In this article, I will expose the problem I ran into, and the solution.

First of all, if you don't have read the book "Java Puzzlers" yet, I urge you to do so. It is a must-read for every java developer. It is all about "Traps, Pitfalls, and Corner Cases" of the Java programming language. I enjoyed myself reading the puzzles, even if sometimes I almost tore my hair out.

As this book does, I will present the problem I ran into as a Puzzler. You will see the code and will be asked to guess what it produce. Let's begin.


Puzzler

The following class represents a person by it's first name, last name and children. The library commons-io is used to close streams. In the main method, we serialize and deserialize a person (aka John Jackson). What does the program print ?

public class Person implements Serializable {
    public static final long serialVersionUID = 1L;

    private String firstName;
    private String lastName;
    private List<Person> children;

    public Person() {}

    public Person(String firstName, String lastName, Person... children) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.children = new ArrayList<>();
        Collections.addAll(this.children, children);
    }

    public List<Person> getChildren() { return Collections.unmodifiableList(children); }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    public void setLastName(String lastName) { this.lastName = lastName; }
    public void setChildren(<Person> children) { this.children = children; }

    @Override
    public String toString() {
        String result = firstName + ' ' + lastName;
        if(children != null && children.size() > 0) {
            result += "'s children are " + children;
        }
        return result;
    }

    public static void main(String [] args) {
        FileOutputStream fileOutputStream = null;
        FileInputStream fileInputStream = null;

        Person jim = new Person("Jim", "Jackson");
        Person barbara = new Person("Barbara", "Jackson");
        Person john = new Person("John", "Jackson", jim, barbara);

        try {
            System.out.println(john);

            File temporaryFile = File.createTempFile("XML-Test-", ".xml");
            temporaryFile.deleteOnExit();
            fileOutputStream = new FileOutputStream(temporaryFile);
            XMLEncoder encoder = new XMLEncoder(fileOutputStream);
            encoder.writeObject(john);
            encoder.close();

            fileInputStream = new FileInputStream(temporaryFile);
            XMLDecoder decoder = new XMLDecoder(fileInputStream);
            Person result = (Person) decoder.readObject();
            decoder.close();

            System.out.println(result);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(fileOutputStream);
            IOUtils.closeQuietly(fileInputStream);
        }
    }
}

What do we expect ?

This program does not seem weird. The class Person is a java bean and it is serializable. The main method creates three persons (John Jackson and his two children, Jim and Barbara), John is then serialized to XML with the XMLEncode class and deserialized using the XMLDecoder class. Therefore the program should print :

John Jackson's children are [Jim Jackson, Barbara Jackson]
John Jackson's children are [Jim Jackson, Barbara Jackson]

What do we have ?

If you ran it, you saw a different thing :

John Jackson's children are [Jim Jackson, Barbara Jackson]
java.lang.NullPointerException
Continuing ...
John Jackson

We lost two kids in the process, what can be wrong ?


Solution

The only unusual thing in our program is the use of the class Collections to populate the children attribute and to return an unmodifiable version of the list when the getter is called. We assume that the author of this code did not want anyone but the class Person itself to modify the list "children". Except that, we have a standard Java Bean.

The addAll method seems pretty straightforward : "The behavior of this convenience method is identical to that of c.addAll(Arrays.asList(elements)), but this method is likely to run significantly faster under most implementations.", Javadoc says. Sweet, a fast implementation.

The unmodifiableList method also seem simple : it returns "an unmodifiable view of the specified list. [...] attempts to modify the returned list, whether direct or via its iterator, result in an UnsupportedOperationException. [...] The returned list will be serializable if the specified list is serializable.".

Our initial is is an ArrayList, which is serializable, so the generated list is serializable as well. The console output does not show an UnsupportedOperationException but a NullPointerException. This does not help us very much.


Actually, the bug _is_ caused by the defensive copying design. If we replace the getter by another defensive copying method, the error is still there, John Jackson's kids are still lost :

public List<Person> getChildren() { return new ArrayList<Person>(children); }

The answer lies in the definition of a Java Bean itself. According to wikipedia, the assumed conventions for Java Beans are :

  1. The class must have a public default constructor (no-argument).
  2. The class properties must be accessible using get, set, is (used for boolean properties instead of get), [...] following a standard naming-convention.
  3. The class should be serializable.

The author of this code (me) did not follow the second convention : the "children" attribute is not directly accessible using its appropriate getter, instead, another object is returned. If we replace the getter statement by the following, everything works fine and John Jackson's children are back.

public List<Person> getChildren() { return children; }

The lesson of this puzzle is : be careful when dealing with java beans and defensive copying. The defensive copying design, which can be a valid choice in numerous situations, is inappropriate here.

dimanche 24 avril 2011

Automating application installation - skeleton

In a previous post, we explained how any server and application installation could be automated using IzPack. In this post, we are going to decrypt the smallest configuration that may be used to build an IzPack installer from a Maven project.


Goals

At the end of this tutorial, we will have a project that meets the following requirements :

  • The project is built using Maven
  • It packages an IzPack installer
  • It will have only 2 panels (a "Welcome" panel and a "Installation Complete !" panel)
  • It does not perform any installation operation

Setting up the project

Let's start this tutorial by a simple operation : we need to create a maven project. To do so, we use the command mvn archetype:generate . When prompted, choose the artifact maven-archetype-quickstart, which is the default one.

$ mvn archetype:generate
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO]    task-segment: [archetype:generate] (aggregator-style)
[INFO] ------------------------------------------------------------------------
[...]
Choose a number:  (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) 20: :
[INFO] artifact org.apache.maven.archetypes:maven-archetype-quickstart: checking for updates from central
Define value for groupId: : fr.pingtimeout
Define value for artifactId: : izpack-environment-installer
Define value for version:  1.0-SNAPSHOT: :
Define value for package:  fr.pingtimeout: :
Confirm properties configuration:
groupId: fr.pingtimeout
artifactId: izpack-environment-installer
version: 1.0-SNAPSHOT
package: fr.pingtimeout
 Y: :
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 minute 59 seconds
[INFO] Finished at: Tue Apr 19 17:20:49 CEST 2011
[INFO] Final Memory: 8M/14M
[INFO] ------------------------------------------------------------------------

Modifying the Maven project

The archetype maven-archetype-quickstart is the perfect one for us since it does not come with anything except the basic structure of any project and empty files.

First, we need to delete those files (src/main/java/App.java and src/test/java/AppTest.java). Then, we need to create the src/main/resources/ folder. It will contain the configuration of our installer.

Let's now modify the pom.xml so that we don't have à dependency to an old version of JUnit anymore. Add a new block that will package our installer during the Maven "package" phase. This new block will be parametrised by the following elements :

  • A property defines the version of IzPack we want to use
  • Another property refers to the working directory Maven will use to build the installer
  • The plugin maven-izpack-plugin is executed during the "package" package phase with the previous properties
  • The plugin maven-resources-plugin is used to copy our project resources in the working folder
Here is our new pom.xml :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>fr.pingtimeout</groupId>

 <artifactId>izpack-maven-empty-model</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>pom</packaging>

 <name>izpack-maven-empty-model</name>

 <url>http://www.pingtimeout.fr/</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <izpack.staging.dir>${project.build.directory}/izpack-staging</izpack.staging.dir>

  <izpack.standalone.compiler.version>4.3.2</izpack.standalone.compiler.version>
 </properties>

 <build>
  <plugins>
   <!-- IzPack Installer -->

   <plugin>
    <groupId>org.codehaus.izpack</groupId>
    <artifactId>izpack-maven-plugin</artifactId>
    <version>1.0-alpha-5</version>

    <executions>
     <execution>
      <phase>package</phase>
      <goals>
       <goal>izpack</goal>

      </goals>
      <configuration>
       <izpackBasedir>${izpack.staging.dir}</izpackBasedir>
      </configuration>
     </execution>

    </executions>
    <dependencies>
     <dependency>
      <groupId>org.codehaus.izpack</groupId>
      <artifactId>izpack-standalone-compiler</artifactId>

      <version>${izpack.standalone.compiler.version}</version>
     </dependency>
    </dependencies>
   </plugin>

   <plugin>

    <artifactId>maven-resources-plugin</artifactId>
    <version>2.4.2</version>
    <executions>
     <execution>
      <id>copy-resources</id>

      <phase>validate</phase>
      <goals>
       <goal>copy-resources</goal>
      </goals>
      <configuration>

       <encoding>UTF-8</encoding>
       <outputDirectory>${izpack.staging.dir}</outputDirectory>
       <resources>
        <resource>
         <directory>src/main/resources</directory>

        </resource>
       </resources>
      </configuration>
     </execution>
    </executions>
   </plugin>

  </plugins>
 </build>
</project>

IzPack configuration

The only thing that remains on our Todo list is to configure IzPack so that it builds our empty installer. First, we need an installer descriptor (a file named install.xml in src/main/resources/). This file contains the following elements.

In the info section, we tell IzPack that :

  • Our application is named "IzPack empty Model"
  • The current installer's version is 1.0
  • We do not want an uninstaller to be built
  • Java 1.6 must be installed
  • Our installer is compressed using the Pack200 algorithm

It is available in French and is executed in a resizable window of 640x480 pixels.

In the panels section, we define 2 screens. The first one is a basic welcome screen. The second one is a generic "Installation complete !" screen.

Finally, we define the lists of files that may be copied by the installer (a list is called a "pack"). In this tutorial, there is no such file. However, the IzPack documentation tells us that at least one pack is required, so let's create an empty one.


Building the installer

That's it. The whole Maven project is ready. The only thing we have to do is to package our installer by building our project with the usual mvn package.

$ mvn clean package
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building izpack-maven-empty-model
[INFO]    task-segment: [clean, package]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean {execution: default-clean}]
[INFO] [resources:copy-resources {execution: copy-resources}]
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] [site:attach-descriptor {execution: default-attach-descriptor}]
[INFO] [izpack:izpack {execution: default}]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 11 seconds
[INFO] Finished at: Thu Apr 21 11:54:40 CEST 2011
[INFO] Final Memory: 11M/21M
[INFO] ------------------------------------------------------------------------

Here are some screenshots of an installer based on this tutorial.

Panel 1 - Welcome

Panel 2 - Installation complete !


Conclusion

This tutorial allows us to set up IzPack pretty quickly in a Maven project. Ideally, it could even be used to create a Maven artifact.

Now, to make full use of IzPack, we need to read the official documentation (which is very complete).


Resources

The resources that were used during this tutorial are available using the following links :

Still not convinced by the power of IzPack or its easy integration in any project ? Tell us about it in the comments !

Automating application installation - Part 1

Any complex system installation can sometimes turn into a complete nightmare, with lots of manual actions and user inputs and an installation notice user-not-so-friendly. In this article, we will explain how such an installation can be simplified using a tool named IzPack.

Introduction

We created a web application which is called hello-world-webapp-1.0.war. We want this application to be deployed in a Tomcat server. The installation will be made on a dedicated server which contains only an operating system and the Java Virtual Machine.

At least, a classic installation notice would contain the following elements :

  • Copy the Tomcat zip archive on the server
  • Extract it
  • Create a folder with the name of our application in tomcat "webapps" directory
  • Copy the application war on the server
  • Extract this war in the newly created folder

Although those operations are pretty simple, they are quite numerous for a basic hello world.


What is "IzPack" ?

IzPack is an open-source tool that allows us to create graphical installers, like InstallShield do, but cross-platform.

How does IzPack work ? Well, it compiles a user defined installation descriptor (a file named install.xml) and generates a graphical installer that does almost everything. We are going to use it to automate every installation step we described.


Setting up the Maven project

First, we need a Maven project which will package our installer, that is :

  • The zip archive of tomcat
  • Our application hello-world-webapp-1.0.war
  • IzPack runtime files

Then, we define some properties in the project (like the version of IzPack and its working directory) :

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <staging.dir>${project.build.directory}/staging</staging.dir>
    <izpack.standalone.compiler.version>4.3.2</izpack.standalone.compiler.version>
</properties>

Finally, we include the plugin izpack-maven-plugin in our project's build lifecycle :

<!-- Used to configure IzPack installer -->
<plugin>
    <groupId>org.codehaus.izpack</groupId>
    <artifactId>izpack-maven-plugin</artifactId>
    <version>1.0-alpha-5</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>izpack</goal>
            </goals>
            <configuration>
                <izpackBasedir>${staging.dir}/izpack</izpackBasedir>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.izpack</groupId>
            <artifactId>izpack-standalone-compiler</artifactId>
            <version>${izpack.standalone.compiler.version}</version>
        </dependency>
    </dependencies>
</plugin>
<!-- Instead of working in the src/ folder, create a staging directory and copy every ressources in this folder -->
<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>2.4.2</version>
    <executions>
        <execution>
            <id>copy-resources</id>
            <phase>validate</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <encoding>UTF-8</encoding>
                <outputDirectory>${staging.dir}</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

IzPack configuration

Even though this configuration is quite basic, we are going to describe how it produces our installer.

Some general application properties are defined :

<info>
    <appname>pingtimeout-sample-installer</appname>
    <appversion>1.0</appversion>
    <authors>
        <author email="pierre@pingtimeout.fr" name="Pierre Laporte" />
    </authors>
    <url>http://www.pingtimeout.fr</url>
    <uninstaller write="no" />
    <javaversion>1.6</javaversion>
    <requiresjdk>yes</requiresjdk>
    <writeinstallationinformation>no</writeinstallationinformation>
    <pack200 />
</info>

Our installer in composed by 6 panels. amongst which the "Welcome" panel and the "Do you agree with the licence ?" panel :

<panels>
    <panel classname="HelloPanel" />
    <panel classname="InfoPanel" />
    <panel classname="LicencePanel" />
    <panel classname="TargetPanel" />
    <panel classname="InstallPanel" />
    <panel classname="SimpleFinishPanel" />
</panels>

Now, the most important part : the packs. The first one contains the Tomcat archive. We want it to be copied in the working directory

<pack name="Copy Apache Tomcat package" preselected="yes" required="yes">
    <description>Copy Apache Tomcat package in $INSTALL_PATH/work/</description>
    <file override="true"
          src="../tomcat/apache-tomcat-7.0.12.zip"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
</pack>

The second pack contains our installation scripts that will do every task we listed (unpacking Tomcat, create a directory, ...). Once these scripts are copied, we want IzPack to execute the "unzip-tomcat.sh" script (which unzips tomcat archive, obviously).

<pack name="Unzip Apache Tomcat" preselected="yes" required="yes">
    <description>Unzip Apache Tomcat in $INSTALL_PATH/work/</description>
    <depends packname="Copy Apache Tomcat package" />
    <file override="true"
          src="../scripts"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
    <executable failure="abort"
                keep="true"
                stage="postinstall"
                targetfile="$INSTALL_PATH/work/scripts/unzip-tomcat.sh">
        <args>
            <arg value="$INSTALL_PATH" />
        </args>
    </executable>
</pack>

The last pack will handle every task related to our "hello-world" application. That is : create a directory in the "webapps" folder and unzip the application war file.

<pack name="Install Hello World application" preselected="yes" required="yes">
    <description>Copy the Hello World WAR in Tomcat webapp directory</description>
    <depends packname="Unzip Apache Tomcat" />
    <file override="true"
          src="../applications/hello-world-webapp-1.0.war"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
    <executable failure="abort"
                keep="true"
                stage="postinstall"
                targetfile="$INSTALL_PATH/work/scripts/deploy-helloworld.sh">
        <args>
            <arg value="$INSTALL_PATH" />
        </args>
    </executable>
</pack>

Compiling the installer

That's it. We can now create our installer compiling our project (with the mvn clean install command).

Here are some screenshots of our installer.

Panel 1 - Welcome

Panel 2 - General informations

Panel 3 - Licence

Panel 4 - Destination folder

Panel 5 - Installation progress

Panel 6 - Installation complete


Once the installation is completed, we may run tomcat using startup.sh so that we can access our application (http://localhost:8080/hello-world/)


Conclusion

IzPack allows us to make the administrator's life simpler, as the installation of our system is now a piece of cake. Some more tutorial will soon be posted on PingTimeout.

We just created an installer capable of installing an application server, managing directories and deploying an application. Using IzPack, we reduce the risk of erroneous user input, and give our installation phase a nicer look.


Resources

The project used in this tutorial, and every related resources can be downloaded using the following links :

Do you have tips about IzPack ? Other installation practices ? Let's talk about it in the comments. :-)

jeudi 21 avril 2011

Automatiser l'installation d'applications - Squelette

Dans un précédent article, nous expliquions comment automatiser l'installation de serveur et d'application en un seul processus via l'outil IzPack. Dans cet article, nous décrypterons le squelette minimal à mettre en place pour utiliser IzPack à l'intérieur d'un projet Maven.


Objectif

A la fin de ce tutorial, nous disposerons d'un code répondant aux critères suivants :

  • Le code sera compilé avec Maven
  • Il embarquera un installeur fait avec IzPack
  • L'installeur aura deux écrans (un écran d'accueil et un écran de fin)
  • L'installeur n'effectuera aucune opération

Mise en place du projet Maven

Commençons par créer le projet Maven que nous utiliserons. Pour cela, nous utiliserons la commande mvn archetype:generate en spécifiant le modèle maven-archetype-quickstart, c'est à dire le modèle par défaut.

$ mvn archetype:generate
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO]    task-segment: [archetype:generate] (aggregator-style)
[INFO] ------------------------------------------------------------------------
[...]
Choose a number:  (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) 20: :
[INFO] artifact org.apache.maven.archetypes:maven-archetype-quickstart: checking for updates from central
Define value for groupId: : fr.pingtimeout
Define value for artifactId: : izpack-environment-installer
Define value for version:  1.0-SNAPSHOT: :
Define value for package:  fr.pingtimeout: :
Confirm properties configuration:
groupId: fr.pingtimeout
artifactId: izpack-environment-installer
version: 1.0-SNAPSHOT
package: fr.pingtimeout
 Y: :
[...]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 minute 59 seconds
[INFO] Finished at: Tue Apr 19 17:20:49 CEST 2011
[INFO] Final Memory: 8M/14M
[INFO] ------------------------------------------------------------------------

Modification du projet Maven

Le modèle maven-archetype-quickstart a ceci d'intéressant qu'il nous crée rapidement la structure Maven classique de tout projet.

Commençons par supprimer les fichiers src/main/java/App.java et src/test/java/AppTest.java. Il nous faut ensuite créer le dossier src/main/resources/ qui contiendra la configuration de IzPack.

Modifions maintenant le pom.xml de manière à ne plus avoir de dépendance vers une ancienne version de JUnit et à packager notre installeur en même temps que le projet. Nous introduisons les éléments suivants :

  • Une propriété définit la version d'IzPack que nous utiliserons
  • Une autre propriété pointe vers le répertoire de travail que Maven utilisera pour construire l'installeur
  • L'appel du plugin maven-izpack-plugin est réalisé pendant la phase "package" avec les propriétés ci-dessus
  • Nous utilisons le plugin maven-resources-plugin pour copier les ressources de notre projet dans le dossier de travail utilisé par Maven
Le nouveau pom.xml est le suivant :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>fr.pingtimeout</groupId>

 <artifactId>izpack-maven-empty-model</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>pom</packaging>

 <name>izpack-maven-empty-model</name>

 <url>http://www.pingtimeout.fr/</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <izpack.staging.dir>${project.build.directory}/izpack-staging</izpack.staging.dir>

  <izpack.standalone.compiler.version>4.3.2</izpack.standalone.compiler.version>
 </properties>

 <build>
  <plugins>
   <!-- IzPack Installer -->

   <plugin>
    <groupId>org.codehaus.izpack</groupId>
    <artifactId>izpack-maven-plugin</artifactId>
    <version>1.0-alpha-5</version>

    <executions>
     <execution>
      <phase>package</phase>
      <goals>
       <goal>izpack</goal>

      </goals>
      <configuration>
       <izpackBasedir>${izpack.staging.dir}</izpackBasedir>
      </configuration>
     </execution>

    </executions>
    <dependencies>
     <dependency>
      <groupId>org.codehaus.izpack</groupId>
      <artifactId>izpack-standalone-compiler</artifactId>

      <version>${izpack.standalone.compiler.version}</version>
     </dependency>
    </dependencies>
   </plugin>

   <plugin>

    <artifactId>maven-resources-plugin</artifactId>
    <version>2.4.2</version>
    <executions>
     <execution>
      <id>copy-resources</id>

      <phase>validate</phase>
      <goals>
       <goal>copy-resources</goal>
      </goals>
      <configuration>

       <encoding>UTF-8</encoding>
       <outputDirectory>${izpack.staging.dir}</outputDirectory>
       <resources>
        <resource>
         <directory>src/main/resources</directory>

        </resource>
       </resources>
      </configuration>
     </execution>
    </executions>
   </plugin>

  </plugins>
 </build>
</project>

Configuration d'IzPack

Il ne reste plus qu'à configurer IzPack pour que ce dernier nous construire l'installeur tant attendu. Pour cela, nous créons le fichier install.xml dans le dossier src/main/resources/.

Dans la section info, nous définissons que :

  • L'application s'appelle "IzPack empty Model"
  • Elle est en version 1.0
  • Nous ne voulons pas de désinstalleur
  • La version 1.6 du JRE est requise
  • Nous utilisons la méthode de compression Pack200

Nous indiquons ensuite que l'installeur est en français et qu'il s'exécute dans une fenêtre redimensionnable de 640x480 pixels

Ensuite, nous créons deux écrans dans la section panels. Le premier est un écran d'accueil générique. Le second est un écran de fin d'installation, également générique.

Enfin, nous définissons la liste des fichiers que IzPack doit traiter. Dans notre cas, il n'y en a aucun. La documentation d'IzPack nous dit cependant que la section packs est requise et qu'au moins un pack doit y être présent. Nous créons donc un pack vide.


Génération de l'installeur

La configuration du projet Maven est maintenant terminée. Il ne nous reste plus qu'à générer notre installeur en compilant simplement le projet maven.

$ mvn clean package
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building izpack-maven-empty-model
[INFO]    task-segment: [clean, package]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean {execution: default-clean}]
[INFO] [resources:copy-resources {execution: copy-resources}]
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] [site:attach-descriptor {execution: default-attach-descriptor}]
[INFO] [izpack:izpack {execution: default}]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 11 seconds
[INFO] Finished at: Thu Apr 21 11:54:40 CEST 2011
[INFO] Final Memory: 11M/21M
[INFO] ------------------------------------------------------------------------

Voici des captures d'écrans d'un installeur généré sur le même modèle.

Ecran 1 - Accueil

Ecran 2 - Fin de l'installation


Conclusion

Ce tutoriel nous permet rapidement de mettre en place, ou d'inclure, Izpack dans un projet Maven. Idéalement, il pourrait d'ailleurs être automatisé à l'aide d'un artefact Maven.

Pour parfaire notre installeur, il nous faut maintenant nous plonger dans la documentation très complète d'IzPack.


Ressources

Les ressources utilisées dans ce tutoriel sont les suivantes :

Pas encore convaincu de la facilité d'intégration d'un installeur pour un projet Java ? Discutons-en dans les commentaires !

mardi 12 avril 2011

Automatiser l'installation d'applications - Partie 1

La procédure d'installation d'un système d'information complexe prend généralement des allures de cauchemard, avec un grand nombre d'actions relevant de l'utilisateur et une procédure d'installation à l'ergonomie douteuse. Dans cet article, nous verrons comment simplifier une installation à l'aide de l'outil IzPack.


Introduction

Nous disposons d'une application web baptisée hello-world-webapp-1.0.war. Elle doit être déployée dans un serveur Tomcat. Nous l'installons sur une machine dédiée, sur laquelle aucun composant n'est installé à l'exception de l'OS et de la machine virtuelle Java.

La procédure d'installation basique impliquerait les éléments suivants :

  • Copier l'archive zip de Tomcat sur le serveur
  • L'extraire dans un dossier donné
  • Créer un répertoire dans le dossier "webapps" de Tomcat
  • Copier l'application war sur le serveur
  • Extraire l'application dans le dossier nouvellement créé

Bien que les opérations à réaliser soient simples, nous remarquons tout de suite qu'elles sont déjà nombreuses pour un simple hello world.


Présentation de IzPack

IzPack est un logiciel Open-Source permettant de réaliser des installateurs (installers) à la manière d'un InstallShield, mais multi-plateformes.

IzPack fonctionne principalement grace à la compilation d'un installateur personnalisé grace à un fichier install.xml. Nous allons l'utiliser afin d'automatiser toutes les étapes listées précédemment.


Mise en place du projet Maven

Tout d'abord, nous créeons un projet Maven qui aura pour but de packager tout notre système, c'est à dire :

  • L'archive du serveur Tomcat
  • Notre application hello-world-webapp-1.0.war
  • Les fichiers requis par IzPack

Nous définissons les propriétés de notre projet (en particulier la version de IzPack et le dossier de travail) :

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <staging.dir>${project.build.directory}/staging</staging.dir>
    <izpack.standalone.compiler.version>4.3.2</izpack.standalone.compiler.version>
</properties>

Ensuite, il ne reste plus qu'à inclure le plugin izpack-maven-plugin dans le processus de build :

<!-- Used to configure IzPack installer -->
<plugin>
    <groupId>org.codehaus.izpack</groupId>
    <artifactId>izpack-maven-plugin</artifactId>
    <version>1.0-alpha-5</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>izpack</goal>
            </goals>
            <configuration>
                <izpackBasedir>${staging.dir}/izpack</izpackBasedir>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.codehaus.izpack</groupId>
            <artifactId>izpack-standalone-compiler</artifactId>
            <version>${izpack.standalone.compiler.version}</version>
        </dependency>
    </dependencies>
</plugin>
<!-- Instead of working in the src/ folder, create a staging directory and copy every ressources in this folder -->
<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>2.4.2</version>
    <executions>
        <execution>
            <id>copy-resources</id>
            <phase>validate</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <encoding>UTF-8</encoding>
                <outputDirectory>${staging.dir}</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

Configuration d'IzPack

Bien qu'assez standard, nous allons décrire la configuration d'IzPack mise en oeuvre.

Les propriétés générales de l'application sont renseignées :

<info>
    <appname>pingtimeout-sample-installer</appname>
    <appversion>1.0</appversion>
    <authors>
        <author email="pierre@pingtimeout.fr" name="Pierre Laporte" />
    </authors>
    <url>http://www.pingtimeout.fr</url>
    <uninstaller write="no" />
    <javaversion>1.6</javaversion>
    <requiresjdk>yes</requiresjdk>
    <writeinstallationinformation>no</writeinstallationinformation>
    <pack200 />
</info>

Nous disposons de 6 écrans au total, parmi lesquels les écrans d'accueil, d'information et d'affichage de la licence :

<panels>
    <panel classname="HelloPanel" />
    <panel classname="InfoPanel" />
    <panel classname="LicencePanel" />
    <panel classname="TargetPanel" />
    <panel classname="InstallPanel" />
    <panel classname="SimpleFinishPanel" />
</panels>

Nous pouvons maintenant nous intéresser au coeur de l'installateur : les paquets (packs). Premièrement, nous avons un "pack" contenant l'archive du serveur zip Tomcat. Nous la copions dans un dossier de travail.

<pack name="Copy Apache Tomcat package" preselected="yes" required="yes">
    <description>Copy Apache Tomcat package in $INSTALL_PATH/work/</description>
    <file override="true"
          src="../tomcat/apache-tomcat-7.0.12.zip"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
</pack>

Ensuite, nous copions les scripts qui automatiseront l'installation dans ce même dossier de travail. Durant cette phase, nous exécutons le script "unzip-tomcat.sh" qui permet d'extraire le serveur d'applications.

<pack name="Unzip Apache Tomcat" preselected="yes" required="yes">
    <description>Unzip Apache Tomcat in $INSTALL_PATH/work/</description>
    <depends packname="Copy Apache Tomcat package" />
    <file override="true"
          src="../scripts"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
    <executable failure="abort"
                keep="true"
                stage="postinstall"
                targetfile="$INSTALL_PATH/work/scripts/unzip-tomcat.sh">
        <args>
            <arg value="$INSTALL_PATH" />
        </args>
    </executable>
</pack>

Enfin, nous déployons l'application "hello-world" dans un répertoire dédié du serveur Tomcat par le biais du script deploy-helloworld.sh.

<pack name="Install Hello World application" preselected="yes" required="yes">
    <description>Copy the Hello World WAR in Tomcat webapp directory</description>
    <depends packname="Unzip Apache Tomcat" />
    <file override="true"
          src="../applications/hello-world-webapp-1.0.war"
          targetdir="$INSTALL_PATH/work/">
        <os family="Unix" />
    </file>
    <executable failure="abort"
                keep="true"
                stage="postinstall"
                targetfile="$INSTALL_PATH/work/scripts/deploy-helloworld.sh">
        <args>
            <arg value="$INSTALL_PATH" />
        </args>
    </executable>
</pack>

Compilation de l'installateur

C'est terminé. Il ne nous reste qu'à exécuter la commande mvn clean install dans le dossier de notre projet pour que l'installateur soit généré.

Voici les captures d'écran de l'installateur ainsi créé.

Ecran 1 - Accueil

Ecran 2 - Informations générales

Ecran 3 - Licence

Ecran 4 - Dossier de destination

Ecran 5 - Avancement de l'installation

Ecran 6 - Fin de l'installation


Après l'installation, il ne reste plus qu'à exécuter le programme startup.sh de Tomcat pour pouvoir accéder à l'application déployée à l'adresse http://localhost:8080/hello-world/


Conclusion

IzPack permet de faciliter grandement le déploiement de systèmes d'informations. Ceci fera l'objet de quelques tutoriels supplémentaires sur PingTimeout.

Nous venons de réaliser un installateur capable d'installer un serveur d'application ainsi que de déployer une application dans ce serveur nouvellement créé. En procédant ainsi, nous réduisons les risques d'erreur liés à une mauvaise saisie et nous donnons un look beaucoup plus agréable à notre phase d'installation


Ressources

Les ressources utilisées dans ce tutoriel sont les suivantes :

Des astuces sur IzPack ? D'autres méthodes pour installer des applications ? Exprimez-vous dans les commentaires :)

jeudi 24 février 2011

Mettre en place rapidement un serveur mail de test

Introduction

Nos applications on bien souvent besoin d'envoyer des mails. Nous ne disposons pourtant pas toujours d'un serveur de mail disponible immédiatement. Avec Apache James, j'expliquerai comment créer un tel serveur en moins de 15 minutes et comment court-circuiter des adresses mail publiques.


Un mode de communication omniprésent

Qu'il s'agisse d'alertes ou de simples informations, le mail est devenu un élément central dans les modes de communication d'une application. On les retrouve, par exemple, dans les cas suivants :

  • Informer les clients des offres promotionnelles du moment (ex : FNAC, Amazon, ...)
  • Informer l'utilisateur d'une connexion a son compte (ex : OVH)
  • Alerter l'administrateur sur la santé de ses serveurs (ex : la plupart des serveurs UNIX)

Aujourd'hui, les courriers électroniques sont largement utilisés dans de très nombreux domaines. Nous aurions tort de nous en priver...


Contexte

Nous souhaitons tester une application de commerce en ligne.

Notre cahier des charges mentionne l'envoi massif de courriers électroniques pour informer les utilisateurs de bons plans totalement inédits. Nous devons donc disposer d'utilisateurs et de boites mail de test.

Dans un premier temps, nous créerons un nom de domaine de test qui hébergera toutes nos adresses mail : PingtimeoutMailServer.fr.
Dans un second temps, nous tenterons d'usurper le domaine gmail.com de sorte que tous les mails envoyé à destination de GMail soient capturés par notre serveur.

Top chrono.


Choix du serveur de mail

Il existe un grand nombre de serveurs de mail, parmi lesquels ont peut citer Postfix, Microsoft Exchage Server et IBM Lotus Domino.

Dans cette multitude de solutions, on trouve également Apache James, un serveur SMTP et POP3 open-source entièrement écrit en Java, que nous retenons.

Note : ma confiance dans les solutions soutenues par la fondation Apache me pousse à utiliser James dans ce tutoriel. Il est bien évidemment possible d'obtenir le même résultat avec d'autres serveurs.

Téléchargeons donc Apache James v2.3.2 depuis le site officiel (http://james.apache.org/)


Installation et configuration basique

Attention : les opérations de ce tutoriel doivent être exécutées en tant que root. En effet, sous UNIX, il faut disposer des droits du super-utilisateur pour pouvoir utiliser les ports inférieurs à 1024.

1 - Modifier le fichier /etc/hosts pour qu'il contienne la ligne suivante :

127.0.0.1   PingtimeoutMailServer.fr

2 - Créer le dossier /opt/

3 - Extraire le contenu de l'archive james-binary-2.3.2.tar.gz dans le dossier /opt/

4 - Rendre exécutable les fichier .sh du dossier /opt/james-2.3.2/bin/

5 - Puis exécuter le fichier bin/run.sh de James. La console doit afficher ceci :

root@ubuntu:/opt/james-2.3.2# ./bin/run.sh 
Using PHOENIX_HOME:   /opt/james-2.3.2
Using PHOENIX_TMPDIR: /opt/james-2.3.2/temp
Using JAVA_HOME:      /usr/lib/jvm/java-6-sun/
Running Phoenix: 

Phoenix 4.2

James Mail Server 2.3.2
Remote Manager Service started plain:4555
POP3 Service started plain:110
SMTP Service started plain:25
NNTP Service started plain:119
FetchMail Disabled

6 - Stopper James en utilisant le raccourci CTRL+C dans la console

7 - Editer le fichier /opt/james-2.3.2/apps/james/SAR-INF/config.xml. Localiser les lignes suivantes :

<servernames autodetect="true" autodetectIP="true">
 <servername>localhost</servername>
</servernames>
Les modifier pour qu'elles correspondent au listing suivant :
<servernames autodetect="true" autodetectIP="true">
 <servername>localhost</servername>
 <servername>PingtimeoutMailServer.fr</servername>
</servernames>

Localiser la section <dnsserver> et s'assurer que la balise <servers> ne contient aucun élément.

8 - Puis exécuter le fichier bin/run.sh de James pour redémarrer le serveur de mail.

Dans une autre console, créer un utilisateur de test (ici, on lui donne le mot de passe "password")

root@ubuntu:~# telnet localhost 4555
Trying ::1...
Connected to localhost.
Escape character is '^]'.
JAMES Remote Administration Tool 2.3.2
Please enter your login and password
Login id:
root
Password:
root
Welcome root. HELP for a list of commands
adduser ping password
User ping added
quit
Bye
Connection closed by foreign host.

Top. C'est terminé. Le serveur mail est configuré et complètement opérationnel.


Test de réception de mail

Ouvrir Mozilla Thunderbird (ou tout autre client mail de votre choix) puis créer un compte mail avec les paramètres suivants :

Type : Email account
Your Name : ping
Email Address : ping@PingtimeoutMailServer.fr
Type of incoming server : POP
Incoming Server : PingtimeoutMailServer.fr
Incoming User Name : ping
Outgoing Server Name : PingtimeoutMailServer.fr
Outgoing User Name : ping
Account Name : ping@PingtimeoutMailServer.fr

Ensuite, dans une console, envoyer un mail par telnet. Dans ce test, nous utilisons la méthode rustique.

root@ubuntu:~# telnet PingtimeoutMailServer.fr 25
Trying 127.0.0.1...
Connected to PingtimeoutMailServer.fr.
Escape character is '^]'.
220 ubuntu SMTP Server (JAMES SMTP Server 2.3.2) ready Thu, 24 Feb 2011 22:58:34 +0100 (CET)
HELO PingtimeoutMailServer.fr
250 ubuntu Hello PingtimeoutMailServer.fr (localhost [127.0.0.1])
mail from: <toto@AdresseInexistante.nul>
250 2.1.0 Sender <toto@AdresseInexistante.nul> OK
rcpt to: <ping@PingtimeoutMailServer.fr>
250 2.1.5 Recipient <ping@PingtimeoutMailServer.fr> OK
data
354 Ok Send data ending with <CRLF>.<CRLF>

Ceci est un e-mail de test

.
250 2.6.0 Message received
quit
221 2.0.0 ubuntu Service closing transmission channel
Connection closed by foreign host.

Dans Thunderbird, déplier le noeud "ping@PingtimeoutMailServer.fr" puis cliquer sur inbox. Saisir "password" comme mot de passe puis valider. Surprise, notre mail est bien arrivé.


Test d'envoi de mail

Dans Thunderbird, cliquer sur le bouton "Write" puis écrire un mail à destination de ping@PingtimeoutMailServer.fr

Cliquer ensuite sur le bouton "Get Mail". Comme prévu, le mail a bien été envoyé et reçu.

Nous avons donc un serveur d'envoi et de réception de mail qui fonctionne pour des adresses e-mail d'un domaine fantaisiste.


Court-circuiter des adresses mail publiques

Le paramétrage précédent nous a permis de tester l'envoi et la réception de mail depuis des adresses imaginaires.

Nous souhaitons maintenant nous assurer que tout mail que nous envoyons à destination d'un domaine publique (par exemple : gmail.com) soit capté par James comme nous l'avons fait pour le domaine PingtimeoutMailServer.fr.

Rien de plus simple, il suffit de s'assurer que les points suivants sont respectés :

  • Le fichier /etc/hosts du serveur doit rediriger le domaine gmail.com sur l'adresse locale 127.0.0.1
  • Le fichier /opt/james-2.3.2/apps/james/SAR-INF/config.xml doit inclure le domaine gmail.com dans la section servernames
  • Les adresses mail des destinataires doivent avoir été créées dans James

Une fois ces trois actions effectuées, c'est terminé. Toute tentative d'envoi de mail à une adresse en gmail.com sur le serveur James sera captée et conservée en local.


Conclusion

Nous avions bien dit en moins de 15 minutes. C'est chose faite :).

Il ne reste plus qu'à configurer notre serveur d'application pour qu'il utilise notre machine en tant que serveur de mail.

Vous utilisez une solution alternative ? Vous voyez des points à améliorer ? Discutons-en dans les commentaires !

P.L.

jeudi 27 janvier 2011

Projet "1 milliard" - Analyse du périmètre

Dans le cadre du challenge USI 2011, notre défi (que nous acceptons) est de développer une application pouvant accueillir 1 milliard d'utilisateurs et supportant 1 million de connexions simultanées.

Dans cet article, je délimiterai le périmètre fonctionnel de l'application. Cette première analyse permettra de mieux cadrer l'ensemble des fonctionnalités à développer.

Contexte

Le projet "1 milliard" consiste à développer une application de QCM simple. Cette application doit pouvoir accueillir jusqu'à 1 million de joueurs simultanés. Elle doit également fournir une API d'administration permettant de créer des parties, avec un ensemble de questions et les réponses associées, et d'enregistrer tous les joueurs attendus.

Notre application doit disposer de deux façades :

  • Une IHM visualisable dans un navigateur internet
  • Une API REST d'administration

Les organisateurs disposent d'un utilitaire leur permettant de tester l'API de l'application.

Fonctionnalités à développer

Ce paragraphe reprend les exigeances fonctionnelles décrites sur le site du challenge USI en rapportant les exigences à nos éléments techniques.

  • Côté API
    • La fonction "créer un utilisateur" de l'API doit supporter 1 million de requêtes en une heure
    • La fonction "créer une partie" de l'API doit attendre les informations "Questions", "Réponses", "Nombre de joueurs" et "Temps de réponse"
  • Côté API
    • Le serveur accueille tout joueur qui s'authentifie par login/mot de passe.
    • Le serveur attend la connexion de tous les joueurs avant de démarrer la partie.
    • Le serveur transmet à chaque joueur la première question.
    • Le serveur attend que tous les joueurs aient répondu ou que le temps maximum de réponse soit écoulé pour arrêter de recevoir des réponses.
    • Le serveur met à jour les scores des utilisateurs.
    • Le serveur envoie à chaque joueur la bonne réponse à la question, le score du joueur, les 100 premiers joueurs, les 50 joueurs classés avant lui et les 50 joueurs classés après.
    • Le serveur poursuit cette séquence (envoi de question, attente des réponses, calcul du score et envoi du récapitulatif) jusqu'à ce qu'il n'y ait plus de questions.

Choix d'architecture

Les choix d'architectures sont laissés à la discrétion des candidats.

Objectifs

Si les inscriptions ne sont pas encore clôturées, le nombre d'équipes maximum a déjà été atteint. Il n'est plus possible de bénéficier d'une infrastructure de test adaptée à un tel projet.

Je compte toutefois participer en candidat libre au projet. Je posterai sur ce blog mes contributions durant le challenge.

vendredi 14 janvier 2011

Guide de rédaction de journaux applicatifs

Présentation

Écrire des logs, c'est bien. Écrire des bons logs, c'est bien mieux !

Aujourd'hui, nous verrons ce qui caractérise un bon message de log, des exemples de mauvaises utilisations des journaux d'une application et des exemples de messages de log efficaces.


Introduction

Dans cet article, j'utiliserai le plus possible l'exemple suivant :
Un SI propose à ses utilisateurs de s'authentifier par leur login ou leur adresse e-mail.

Note : bien que simpliste, nous verrons que cet exemple constitue un très bon cas d'école.

L'implémentation de référence est celle-ci :

final boolean authentificated;
try {
  if(isMailAddress(username)) {
    authentificateByMail(username, password);
    authentificated = true;
  } else {
    authentificateByUsername (username, password);
    authentificated = true;
  }
} catch (AuthentificationException e) {
  authentificated = false;
} catch (Exception e) {
  e.printStackTrace();
}


Des messages pertinents

Un bon message de log peut avant tout être compris par n'importe quel développeur sans que ce dernier n'ait à lire le code source de l'application.

On trouve cependant beaucoup de messages qui ne reflètent pas un cas métier mais un état d'esprit passager du développeur passé par là. Les sites comme TheDailyWTF recensent nombre de ces exemples.

Dans notre cas, un message de log non-pertinent (impertinent ?) serait celui-ci :

} catch (Exception e) {
  log.error("It's a trap !!");
}

En un sens, cette situation est pire qu'un appel à la méthode printStackTrace() car le message de log nous masque complètement l'exception.

Ce genre de situations est d'autant plus regrettable que la correction la plus sommaire du message de log prend 2 caractères :

} catch (Exception e) {
  log.error("It's a trap !!",e);
}

Cette correction nous fait passer de "It's a trap !!" à "It's a trap !! java.net.ConnectException : connection refused".

Dit autrement, l'exploitant qui doit analyser le problème passe de "C'est incompréhensible !" à "Ok, le développeur à eu son quart d'heure de folie, en tout cas ça vient de la base de données".


Des messages clairs

L'affichage de la valeur d'une variable dans une fonction ne constitue pas une information a elle seule. Quelle frustration de consulter les logs d'un serveur et de tomber sur un message comme :
valeur=42

Dans l'idée, un tel message permet de deviner vaguement ce qui peut se passer dans l'application. Le problème, c'est que sans aller lire le code de l'application, il est impossible de l'interpréter.

Il est toujours préférable d'associer un contexte a de tels messages et donc d'indiquer les conséquences de la valeur affichée.

Revenons à notre exemple, avec un message de log peu parlant :

final boolean authentificated;
log.debug("Username : {}", username);
try {
  if(isMailAddress(username)) {
    // ...
  } else {
    // ...
  }
}
// ...

Imaginons que la méthode isMailAddress de l'exemple se contente de vérifier la présence d'un @ dans la chaîne passée en paramètre et que, quelques mois plus tard, votre client décide d'accepter les @ dans les logins des utilisateurs.

L'utilisateur 'jimC@rey' ne peut pas s'authentifier sur le système.

Avec notre message de log, nous lisons ceci :
Username = jimC@rey

Cela nous induirait presque en erreur, car ce que nous voyons n'est pas une adresse e-mail. Pourquoi diantre l'application ne se comporte-t-elle pas correctement ?

Modifions notre code pour ajouter un contexte à ce message, c'est à dire la décision qui est prise avec la valeur de "username" :

final boolean authentificated;
log.debug("Username : {}", username);
try {
  if(isMailAddress(username)) {
    log.debug("Authentification by mail address");
    // ...
  } else {
    log.debug("Authentification by username");
    // ...
  }
}
// ...

Nous lisons maintenant ceci :
Username = jimC@rey Authentification by mail address

Désormais, nous savons que l'application détecte la valeur de 'username' comme une adresse e-mail. Nous savons donc où se trouve la source de l'anomalie.


Le bon niveau de journalisation

Les differents niveaux de journalisation (debug, info, warning, error) permettent de donner plus ou moins de précisions sur une situation donnée.

Chaque niveau de log peut être utilisé à la fois pour des messages fonctionnels et techniques.

Typiquement, le niveau debug peut nous donner des informations sur chaque détail d'un traitement métier. Le niveau info nous résume ce traitement en un message. Le niveau warning nous met en évidence des cas fonctionnels maîtrisés, mais qui méritent notre attention. Le niveau error nous indique enfin l'impossibilité de réaliser un traitement et la cause de cette impossibilité.

Toujours dans notre exemple, voici ce que chaque niveau de log pourrait nous dire, au sujet d'un utilisateur 'pingtimeout' qui saisit trois fois un mot de passe incorrect.

debug : "Username : 'pingtimeout', Authentification by username"
info : "Invalid username/password for 'pingtimeout'"
warning : "Maximum connection attempts reached. The user 'pingtimeout' will be disabled for 5 minutes"
Error : "The user 'pingtimeout' is disabled. The authentification cannot be processed."

Il faut toujours garder à l'esprit que la configuration des logs n'est jamais constante. En phase de développement, on peut limiter les logs au niveau INFO et supérieurs, ou bien tous les accepter. En phase de production, on peut limiter les logs au niveau WARNING et supérieurs.

Dans tous les cas, les messages affichés doivent rester cohérents.


Des logs en guise de commentaires

L'un des avantages des logs, face aux commentaires, c'est qu'ils permettent de suivre l'état de l'application en temps réel.

Dans la partie "Des messages clairs", nous avons ajouté des logs indiquant le type d'authentification retenu. D'un point de vue documentation, il aurait été possible de mettre simplement des commentaires :

try {
  if(isMailAddress(username)) {
    // Authentification by mail address
    ...
  } else {
    // Authentification by username
    ...
  }
}

En procédant ainsi, nous n'aurions pas pu diagnostiquer notre anomalie.

Là où l'ajout de logs apporte une information pertinente, on peut se passer de commentaires. Les logs participent à la documentation du code.


Gérer son framework

En cherchant bien (sur TheDailyWtf ou ailleurs), on peut tomber sur des perles dans lesquelles un développeur, visiblement fatigué, ne maitrisait pas son framework de log. Il lui était manifestement difficile de retrouver un message en particulier dans ses journaux. Etaient-ils remplis de log.debug ? Trop verbeux ? Lui seul le sait.

En attendant, pour s'y retrouver, il a donc opté pour l'utilisation de caractères spéciaux :

log.debug("££££££££ Authentification failure !! ££££££££");

L'avantage pour lui, c'est qu'il retrouve très facilement son message grâce à un bon vieux grep. L'inconvénient pour les autres, c'est que les journaux deviennent encore moins lisibles.

Il est primordial de maitriser son framework de log. Changer le niveau de log global est simple. N'activer les traces de debug que pour quelques classes seulement est tout aussi facile.

Dans tous les cas, utiliser des caractères spéciaux pour s'y retrouver est une très mauvaise pratique !


Des éléments réutilisables

Il peut être tentant de retirer les messages de log, pour toutes sortes de raisons (lisibilité du code, idée reçue sur les performances ou juste parce qu'ils "ne seront plus utilisés"). Si vous êtes sujet à cet effet "Mr Propre", résistez !

Des logs correctement écrits ne perturbent pas la lisibilité du code, ils l'améliorent. Ce ne sont pas les responsables des dégradations de performances dont votre système souffre. Enfin, ils serviront toujours.

Si vous avez eu besoin d'un message de log à un moment donné, alors un autre développeur en aura forcement besoin dans le futur. Les messages de log d'une application ne devraient jamais être supprimés, sauf si cette suppression se fait au profit d'une réécriture plus propre.


En conclusion

Tout au long de l'article, nous avons ajouté des messages simples, sans concaténation particulière et qui viennent participer à la documentation du code. Certains messages ont d'ailleurs été ajoutés après le signalement d'un bug pour en permettre un diagnostique.

J'espère que cet article vous permettra d'écrire de meilleurs journaux applicatifs.

Vous n'écrivez pas vos logs de la même manière ? Vous avez également rencontré ces situations ? Parlons-en dans les commentaires :)

P. L.