Java ( 20 / 36 articles - Voir la liste )

Erreur java.lang.IllegalStateException

Il arrive de rencontrer des IllegalStateException lorsqu'on effectue des redirections dans des servlets.

En général ça se produit quand on a déjà envoyé des données ou qu'on a déjà demandé une redirection dans notre requête HTTP.

Pour éviter cela, il suffit d'utiliser la méthode isCommited() en remplaçant

// ServletResponse response

if (response != null) {
 response.sendRedirect("http://www.google.fr");
 return;
}

par

// ServletResponse response

if (response != null && !response.isCommited()) {
 response.sendRedirect("http://www.google.fr");
 return;
}

Astuce Impossible de trouver mon nouveau skin dans JCMS

Je viens de créer un nouveau skin dans JCMS, mais malgré un redémarrage ou le vidage du work de tomcat, il n'apparaît pas dans la liste de choix du skin de mes portlets.

Par défaut, pour ajouter un nouveau skin il suffit de créer la jsp et de la déclarer dans le fichier plugin.xml. Par exemple :

<types>
    <templates type="AbstractPortletSkinable">
       <template name="newSkin" file="doNewSkin.jsp" usage="box">
             <label xml:lang="en">My new skin</label>
             <label xml:lang="fr">Mon nouveau skin</label>
       </template>
    </templates>
</type>

Il existe cependant un option méconnue dans le CMS : la possibilité d'activer des skins différents pour chaque espace de travail.

Pour cela :

  • Choisissez un espace de travail en back-offfice
  • Allez dans l'Espace d'administration fonctionnelle > Types de publication > Portlet
  • Éditez [Abstract] Portlet Skinable.
  • Cochez les skins à activer dans l'espace de travail

Par défaut dans JCMS, aucun n'est coché. Tous les nouveaux skins sont donc immédiatement disponibles.

Mais à partir du moment où l'un d'eux est coché, vous devrez repasser par cette interface à chaque création de skin pour l'activer manuellement.

Astuce Autoriser plusieurs soumissions de formulaire

Par mesure de sécurité, JCMS limite le nombre de soumissions des formulaires. Il faut attendre environ une minute entre chaque soumission.

Ce comportement peut-être éviter en ajoutant cette classe css sur la balise form du formulaire : noSingleSubmitButton.

Astuce Initialiser une variable de classe

Lorsqu'on utilise des variables statiques, on peut souvent les initialiser directement à leur déclaration :

public class MaClasse {

    public static String myStaticString = "Ma chaîne";
    public static String[] myStaticArray = {"Ma chaîne 1", "Ma chaîne 2"};
}

Mais si vous avez une Collection ou un autre objet en variable de classe, vous ne pourrez pas l'initialiser de cette manière.

Vous pouvez donc déclarer un bloc statique, qui sera exécuté une seule fois, à la première utilisation de la classe. Dans ce bloc, appelez une fonction statique qui se chargera d'initialiser votre variable :

public class MaClasse {

    public static List<String> myStaticList = new ArrayList<String>();

    static {
        MaClasse.initMyStaticList();
    }

    private static void initMyStaticList() {

        MaClasse.myStaticList.add("Ma chaîne 1");
        MaClasse.myStaticList.add("Ma chaîne 2");
    }
}

Astuce Ajouter un espace (ou autre) tous les n caractères

Si vous souhaitez formater un numéro de téléphone, un IBAN, ou n'importe quelle chaîne en y ajoutant régulièrement un séparateur, cette fonction peut vous être utile :

/**
 * Formate une chaîne en ajoutant un séparateur tous les <code>length</code> caractères.
 * 
 * @param string Chaîne à formater
 * @param separator Séparateur à ajouter
 * @param length Taille des groupes de caractères à séparer
 * @return La chaîne formatée
 */
public static String addChar(String string, String separator, int length) {

    if (string != null && string.length() > length) {

        string = string.replaceAll("([^\\s]{" + length + "})", "$0" + separator);
    }

    return string;
}

Astuce Afficher une erreur dans JCMS

Dans JCMS (7 et +) les erreurs sont affichées dans un bloc ressemblant à ça.

Affichage d'une erreur dans JCMS

Transmettre le message

Vous devez tout d'abord transmettre votre message à JCMS.

  • Si vous êtes dans une JSP, utilisez l'une ou l'autre de ces méthodes :
// Ajoute un message d'erreur dans la request
setErrorMsg("Le téléchargement du document a échoué", request);

// Ajoute un message d'erreur dans la session
setErrorMsgSession("Le téléchargement du document a échoué", request);
  • Pour faire la même chose dans une classe Java, utilisez l'une de celles-ci :
    // Ajoute un message d'erreur dans la request
    JcmsContext.setErrorMsg("An error occured while saving your content", request);

    // Ajoute un message d'erreur dans la session
    JcmsContext.setErrorMsgSession("An error occured while saving your content", request);

Remarque :

Des méthodes équivalentes existent pour les messages d'information et d'avertissement.

Afficher le message

Une fois le message transmis, il reste à l'afficher. Il suffit pour cela d'inclure ce bout de code :

<%@ include file='/jcore/doMessageBox.jsp' %>

Cette JSP fournie par JCMS, va récupérer tous les messages d'information, d'avertissement ou d'erreur présents en session, dans les paramètres de la request et dans le contexte de la page. Chaque message trouvé est affiché.

Remarque :

Depuis la version 7.1 JCMS utilise Bootstrap. Si vous êtes en version antérieure, le message d'erreur ne ressemblera pas à celui ci-dessus.

Astuce Ordre des champs en ExtraData

Lors de l'affichage des champs en ExtraData, JCMS utilise l'ordre l'alphabétique des clés des propriétés d'ExtraData.

Si votre plugin.prop contient les propriétés suivantes :

extra.Category.jcmsplugin.monplugin.premierchamp
extra.Category.jcmsplugin.monplugin.champ2
extra.Category.jcmsplugin.monplugin.champ3
extra.Category.jcmsplugin.monplugin.dernierchamp

Les champs s'afficheront dans cet ordre en back-office :

  • champ2
  • champ3
  • dernierchamp
  • premierchamp

Astuce Tester si une chaîne est vide ou ne contient que des espaces

Voici une méthode pour tester si une chaîne est vide ou contient uniquement des espaces :

/**
 * Retourne si la chaîne en argument est vide ou contient uniquement des espaces.
 * 
 * @param string Chaîne
 * @return <code>true</code> si la chaîne est nulle, vide, ou s'il ne contient que des caractères espaces (ex: \n \r \s, ...) y compris les espaces insécables.
 */
public static boolean isEmpty(String string) {

    boolean isEmpty = true;

    if (string != null) {

        isEmpty = "".equals(string.replaceAll("[\\s|\\u00A0]+", ""));
    }

    return isEmpty;
}

Remarque :

\u00A0 représente les espaces insécables. Si on utilise uniquement \s dans le remplacement, ils ne seront pas considérés comme des espaces.

Marque-page Un décompilateur Java dans Eclipse

L'application Java Decompiler permet de décompiler un fichier .class simple ou tous ceux présents dans un JAR.

Le site de l'application fournit un plugin Eclipse qui ne fonctionne pas avec les dernières versions (testé avec Eclipse Juno). Plusieurs fork ont été créés et ce site en propose un qui fonctionne : JDEclipse-Realign.

Pour l'installer :

  • Cliquez sur Help > Install New Software... dans le menu d'Eclipse
  • Ajoutez un nouveau site via le bouton Add...
  • Choisissez un nom (ex: JDEclipse-Realign) et cette URL : http://mchr3k-eclipse.appspot.com/
  • Cochez JavaDecompiler Eclipse Plug-in > JD-Eclipse (Realign Edition) et suivez la procédure d'installation

Une fois installé et Eclipse redémarré, vérifiez l'association des fichiers :

  • Windows > Preferences > General > Editors > File Associations
  • Pour *.class without source sélectionnez Class File Editor par défaut

Si vous ouvrez un fichier .class (via F3 ou Ctrl + clic sur le nom d'une classe) vous devriez maintenant voir la source décompilée.

Astuce Utiliser la réflexivité

Instancier un nouvel objet d'une classe

/**
 * Retourne une publication du type en paramètre en remplissant automatiquement ses champs, et après l'avoir créer dans JCMS.
 * 
 * @param clazz La classe du type de contenu souhaité
 * @return une publication du type souhaité
 */
public static <T extends Publication> T fillPublication(Class<T> clazz) {

    Publication publication = null;
    if (Util.notEmpty(clazz)) {

        try {
            publication = clazz.newInstance();

        } catch (InstantiationException e) {

            LOGGER.error("Impossible d'instancier dynamiquement un objet de la classe \"" + clazz.getSimpleName() + "\"", e);

        } catch (IllegalAccessException e) {

            LOGGER.error("Impossible d'instancier dynamiquement un objet de la classe \"" + clazz.getSimpleName() + "\"", e);
        }
    }
    return clazz.cast(publication);
}

Appeler une méthode d'une classe

Publication publication = new Article();
Class<? extends Publication> clazz;

Method method = clazz.getMethod("getName");
String name= (String) method.invoke(publication, null);

L'intérêt de ce code est d'appeler une méthode de l'objet publication sans savoir que c'est un article.

Instancier un tableau d'objets d'une classe

int size = 3;
Objet[] publicationArray = (Publication[]) java.lang.reflect.Array.newInstance(Publication.class, size);

Astuce Modifier la locale utilisée dans les JSTL

Dans les JSTL, la locale permet de déterminer comment formater une date ou un nombre. Par défaut, c'est la JVM qui détermine cette valeur.

Pour modifier cette valeur pour toute une session, il suffit d'utiliser :

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<fmt:setLocale value="fr_FR" scope="session" />

Vous pouvez aussi paramétrer cette valeur pour toute une portlet en ajoutant ces lignes au web.xml, directement sous la racine <web-app> :

<context-param>
    <param-name>javax.servlet.jsp.jstl.fmt.locale</param-name>
    <param-value>fr_FR</param-value>
</context-param>

Astuce Afficher les retours à la ligne en JSTL

Si vous permettez à des contributeurs de saisir un texte avec des retours à la ligne (= textarea), vous aurez sans doute besoin de les afficher ensuite.

Il suffit de remplacer les retours charriots Java (\n) en retour à la ligne HTML (<br />) :

<c:set var="newline" value="<%= \"\n\" %>" />
${fn:replace(myAddress, newline, "<br />")}

Remarque :

Pour pouvoir utiliser la fonction de remplacement de JSTL, vous devez ajouter cet import :

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

Astuce Chiffrer et déchiffrer une chaîne en Java

Si vous avez besoin de chiffrer une chaîne de caractères en Java, vous pouvez utiliser ces deux méthodes :

/**
 * Chiffre la chaîne en argument en utilisant la clé fournie.
 * Ajoute le suffixe à la chaîne chiffrée puis convertit le tout en base64.
 * 
 * @param string Chaîne à chiffrer
 * @param keyString Clé de chiffrement
 * @param suffix Suffixe à ajouter à la chaîne (ex: la date au format yyyyMMdd)
 * @return la chaîne chiffrée + le suffixe, en base64
 * @throws RESTTechnicalException
 */
public static String encrypt(String string, String keyString, String suffix) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {

    String encryptedB64String = null;

    // Chiffrement de la chaîne
    Key key = new SecretKeySpec(keyString.getBytes("UTF-8"), "Blowfish");
    Cipher cipher = Cipher.getInstance("Blowfish");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    byte[] encryptedString = cipher.doFinal(string.getBytes("UTF-8"));

    // Ajout du suffixe
    encryptedString = JavaUtil.mergeArray(encryptedString, suffix.getBytes());

    // Encodage de la chaîne en base 64
    encryptedB64String = Base64.encodeBase64String(encryptedString);

    return encryptedB64String;
}

/**
 * Déchiffre la chaîne en argument en utilisant la clé fournie.
 * 
 * @param b64String Chaîne chiffrée + le suffixe, en base 64
 * @param keyString Clé de déchiffrement
 * @param suffix Suffixe à ajouter à la chaîne (ex: la date au format yyyyMMdd)
 * @return la chaîne déchiffrée
 * @throws RESTTechnicalException
 */
public static String decrypt(String b64String, String keyString, String suffix) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {

    String decryptedString = null;

    // Décodage de la chaîne en base 64
    byte[] string = Base64.decodeBase64(b64String);

    // Suppression du suffixe
    string = JavaUtil.truncateArray(string, suffix.length());

    // Déchiffrement de la chaîne
    Key key         = new SecretKeySpec(keyString.getBytes("UTF-8"), "Blowfish");
    Cipher cipher     = Cipher.getInstance("Blowfish");
    cipher.init(Cipher.DECRYPT_MODE, key);
    decryptedString = new String(cipher.doFinal(string));

    return decryptedString;
}

Explications :

  • La chaîne est (dé)chiffrée grâce à la bibliothèque Cipher de Java, en utilisant la technique de chiffrement Blowfish.
  • Le suffixe en argument est ensuite concaténée à la chaîne chiffrée.
  • La chaîne résultante est finalement convertie en base64.

Remarques :

  • La clé de chiffrement ne doit pas excéder 12 caractères, ou des paramètres doivent être changés dans la JVM.
  • Ces méthodes utilisent la bibliothèque org.apache.commons.codec, qui n'est pas forcément disponible dans le JDK.
  • Le suffixe permet de gérer l'expiration du chiffrement. Par exemple, pour une validité de 24h, il suffit de passer la date au format yyyyMMdd en argument.
  • La conversion en base64 n'est pas indispensable mais permet d'obtenir une chaîne de caractères plus simple et exploitable dans une URL par exemple.
  • Le code des méthodes JavaUtil.truncateArray() et JavaUtil.mergeArray() est disponible ci-dessous :

    /**
     * Tronque le tableau de byte en arguments.
     * 
     * @param array Tableau à tronquer
     * @param maxSize Taille maximale du tableau à conserver
     * @return le tableau tronqué
     */
    public static byte[] truncateArray(byte[] array, int maxSize) {
    
        int newArraySize = Math.min(array.length, maxSize);
        byte[] newArray = new byte[newArraySize];
        System.arraycopy(array, 0, newArray, 0, maxSize);
    
        return newArray;
    }
    
    /**
     * Merge les deux tableaux en arguments.
     * 
     * @param first Premier tableau
     * @param second Second tableau
     * @return
     */
    public static byte[] mergeArray(byte[] first, byte[] second) {
    
        byte[] result = Arrays.copyOf(first, first.length + second.length);
        System.arraycopy(second, 0, result, first.length, second.length);
    
        return result;
    }

Astuce Rétablir le mot de passe admin par défaut

Si vous avez perdu le mot passe administrateur, ou si vous souhaitez le modifier directement dans le store, ajoutez une ligne de la forme suivante à la fin :

<member stamp="j_99999" id="j_2" op="update" author="" mdate="1091526019085" mobile="" name="Admin" password="ISMvKXpXpadDiUoOSoAfww==" />

Cette ligne remplace le mot de passe actuel par admin.

Remarques :

  • Remplacez la valeur de stamp par la même valeur que votre dernière ligne de store, en l'incrémentant.
  • Faites de même pour la date de modification (mdate).

JPlatform 10 (merci Axel pour cette MAJ)

Sur JPlatform 10, le chiffrement des mots de passe a changé. La ligne à ajouter pour le réinitialiser devient :

<member stamp="agi_7217" id="j_2" op="update" mdate="1528122795429" login="admin" name="Admin" password="$2a$10$gbCw7UaAI1kL7PXqodTY9OmzqsdFBufD063oG6ebQT/Zi2PBELX56" />

Erreur Impossible de mapper des sous-classes avec JSON

Si vous utilisez un mapping JSON en Java, vous pouvez rencontrer l'erreur suivante :

org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type

Cette erreur se produit si vous utilisez un Bean global contenant une ou plusieurs classes internes. Exemple :

public class MyGlobalClass {

    private List<SubClass> subClassList;

    public class SubClass {

        private String id;
    }
}

Pour ne pas avoir cette erreur, déclarez vos classes internes statiques dans la classe globale :

public static class SubClass {

    private String id;
}

Astuce Afficher la valeur de tous les champs via la méthode toString()

Lorsque vous créez une nouvelle classe en Java, elle hérite de la méthode toString() de la classe Object. Par défaut, cette méthode n'affiche rien de bien compréhensible.

Vous pouvez la surcharger pour afficher lisiblement les valeurs de tous les champs de votre objet de cette manière :

@Override
public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}

Grâce à l'introspection et à la librairie org.apache.commons, ces quelques lignes de code fonctionneront pour toutes vos classes.

Astuce Le logger dans JCMS : log4j

Par défaut, JCMS utilise log4j pour gestionnaire de logs.

Utilisation générale de log4j

Voici comment l'utiliser dans une classe :

public class MyClass {

    /** Logger log4j */
    private static final Logger logger = Logger.getLogger(MyClass.class);

    public MyClass() {
        logger.info("Je suis dans le constructeur par défaut de ma classe.");
    }

Explications :

  • Déclarez un nouvel attribut de classe statique qui utilise la méthode getLogger() de log4j.
  • Passez votre classe en argument de cette méthode.
  • Utilisez le logger en appelant ses méthodes debug(), info(), warn(), error() ou fatal(), avec le message en argument, plus éventuellement une exception.

Configuration de log4j

Log4j est configuré via le fichier log4j.xml. Dans JCMS, ce fichier se trouve dans WEB-INF/data/.

Si dans un nouveau projet JCMS vous créez un package pour y mettre vos classes Java, il est probable que les logs ne fonctionnent pas. En effet, log4j n'a pas connaissance de ce nouveau package.

Pour l'en informer, éditez le fichier log4j.xml et ajoutez :

<!-- Logger de classes personnalisées --> 
<logger name="mon.package" additivity="false">
    <level value="DEBUG" />
    <appender-ref ref="JCMS" />
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="LOGFILE" />
    <appender-ref ref="PLUGIN" />
</logger>

Explications :

  • L'attribut name définit pour quels packages utiliser log4j. (Les sous-packages seront automatiquement logués.)
  • L'attribut value de l'élément <level> permet de déterminer quel niveau de log appliquer. En développement on utilise souvent DEBUG ou INFO alors qu'en production on se contentera de INFO ou WARN. Concrètement, avec le niveau WARN, les méthodes logger.debug() et logger.info() n'auront aucun effet (suivant cet ordre : debug < info < warn < error < fatal).
  • Les <appender> sont les sorties à utiliser : fichier de log, console, ...
  • Vous pouvez déclarer autant de logger que vous le souhaitez dans le fichier log4j.xml.

Astuce Utiliser un logger dans les classes de test

Dans vos classes de test, vous pouvez utiliser le gestionnaire de log slf4j Il s'agit d'une façade qui peut utiliser plusieurs loggers au choix, comme log4j par exemple.

Voici comment utiliser ce gestionnaire :

public class MyClassTest extends JcmsTestCase {

    /** Logger **/
    private static Logger logger;

    /**
     * Initialise un contexte pour une suite de tests, avec des méthodes d'initialisation et de finalisation.
     * 
     * @return une configuration de suite de tests
     */
    public static TestSetup suite() {

        return new TestSetup(new TestSuite(MyClassTest.class)) {

            @BeforeClass
            @Override
            protected void setUp() throws Exception {

                logger = LoggerFactory.getLogger(MyClassTest.class);
                logger.debug("Début des tests unitaires pour la classe " + MyClass.class.getSimpleName() + ".");
            }
            @AfterClass
            @Override
            protected void tearDown() throws Exception {
                logger.debug("Fin des tests unitaires.");
            }
        };
    }

Explications :

  • Déclarez le logger dans votre classe de test.
  • Créez la méthode statique suite(), pour pouvoir initialiser des éléments au début des tests.
  • Utilisez la méthode LoggerFactory.getLogger(), pour initialiser le logger.
  • Utilisez le logger comme avec log4j (ex : logger.info(), logger.debug(), ...).

Remarque :

Pour pouvoir utiliser slf4j avec log4j, vous devez inclure ces deux jars dans votre projet : slf4j-api-1.5.8.jar et slf4j-log4j12-1.5.8.jar.

Astuce Récupérer la version antérieure d'une Data, dans un DataController

Si vous avez besoin de comparer l'ancienne et la nouvelle version d'une Data, dans un DataController (par exemple dans afterWrite()), vous aurez besoin d'utiliser le contexte en paramètre de la méthode.

Pour obtenir cette version, utilisez quelque chose comme :

final Article oldArticle = (Article) context.get("Data.previous");

Astuce Les variables disponibles dans le contexte, pour les DataController

Lorsque vous créez un DataController et que vous surchargez les méthodes beforeWrite(), checkWrite() et afterWrite(), le dernier paramètre de la méthode (la Map), contient tout le contexte disponible.

Dans cette variable il y a notamment :

  • request : L'objet HttpServletRequest de tomcat.
  • response : L'objet HttpServletResponse de tomcat.
  • formHandler : Le FormHandler utilisé lors de l'édition du contenu.

Et dans le cas d'une modification de donnée, il y a également :

  • Data.previous : La dernière version de la Data avant modification.