29 mai 2014

GRAILS and java.lang.NoClassDefFoundError: org/codehaus/jackson/annotate/JacksonAnnotation

Working on a new grails 2.3.5 application, one (and only one) of my developers was stuck running the Grails application "run-app". The error occurs one time on two.
What makes it even more weird is that we all share the same development environnement configuration...


2014-05-28 11:13:24,920 [localhost-startStop-1] ERROR context.GrailsContextLoader  - Error initializing the application: Error creating bean with name 'annotationHandlerMapping': Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: org/codehaus/jackson/annotate/JacksonAnnotation
Message: Error creating bean with name 'annotationHandlerMapping': Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: org/codehaus/jackson/annotate/JacksonAnnotation
    Line | Method
->>  262 | run       in java.util.concurrent.FutureTask
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|   1145 | runWorker in java.util.concurrent.ThreadPoolExecutor
|    615 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^    744 | run       in java.lang.Thread

Caused by NoClassDefFoundError: org/codehaus/jackson/annotate/JacksonAnnotation
->> 3178 | initAnnotationsIfNecessary in java.lang.Class
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|   3137 | getAnnotation in     ''
|   3150 | isAnnotationPresent in     ''
|    262 | run       in java.util.concurrent.FutureTask
|   1145 | runWorker in java.util.concurrent.ThreadPoolExecutor
|    615 | run       in java.util.concurrent.ThreadPoolExecutor$Worker
^    744 | run . . . in java.lang.Thread

Caused by ClassNotFoundException: org.codehaus.jackson.annotate.JacksonAnnotation
->>  175 | findClass in org.codehaus.groovy.tools.RootLoader
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|    425 | loadClass in java.lang.ClassLoader
|    147 | loadClass in org.codehaus.groovy.tools.RootLoader
|    358 | loadClass in java.lang.ClassLoader
|   3178 | initAnnotationsIfNecessary in java.lang.Class
|   3137 | getAnnotation in     ''
|   3150 | isAnnotationPresent in     ''
|    262 | run       in java.util.concurrent.FutureTask
|   1145 | runWorker in java.util.concurrent.ThreadPoolExecutor
|    615 | run       in java.util.concurrent.ThreadPoolExecutor$Worker

No clue in the dependency-report.
Not acceptable to clean and delete $HOME/.grails folder everytime he wants to run the application...

To solve it, just add the dependency !

compile "com.fasterxml.jackson.core:jackson-core:2.1.4"

That's it.

15 mai 2014

Advanced Grails/GORM configuration

Recently I had to setup a Grails 2.3.5 project over a Postgresql Database.

I was quite disappointed on the documentation and example on GORM domain documentation and caching configuration.

Here are my conclusion, relying on a specific Postgresql schema mapping.


Configure Id generation

I wanted to generate my pk according to 2 strategies depending on the domain usage :
  1. Auto increment per table
  2. Sequences
Here is the configuration to apply :
static mapping ={
    id generator: 'sequence', params: [sequence: 'SEQ_NAME', schema: 'schema']
}
static mapping = {
    id generator: 'increment', params: [schema: 'schema']
}


Configure caching

Being used to ehcache configuration region, here is the configuration on my cached domain :

Domain.groovy (change read-write with your need) :
static mapping = {
   cache usage: 'read-write', region: 'myregion'
}

Config.groovy :
grails.cache.config = {
    cache {
        name 'myregion' // max 12h
        timeToIdleSeconds 3600 // expire after an hour without usage
        timeToLiveSeconds 43200 // expire after 12 hours anyway      

        maxElementsInMemory TODO
    }
}
And don't forget to configure the cache in Datasource.groovy:

hibernate {
    cache.use_second_level_cache = true
    cache.use_query_cache = false
}
And in BuildConfig.groovy :
dependencies{
    compile "net.sf.ehcache:ehcache-core:2.4.6"
}

plugins {
    compile ":cache:1.1.1
}

11 mai 2014

[Widget Blogger] Une d'articles aléatoires modifiable

Permier article ! Celui m'ayant décidé à ouvrir un blog :)

Pourquoi ?

Ma femme m'a demandé de modifier pour elle un script blogger permettant d'afficher une liste d'articles aléatoire sur un widget blogger, qu'elle a trouvé sur ce site

Comment on fait ?

Des scripts Javascript sont évalués au chargement de la page pour :
  1. Lister le nombre total de posts à partir de la fonctionnalité REST "feed"
  2. Récupérer de l'information sur chaque post.
Du coup pour lui permettre de rafraichir cette liste, il faut modifier le code pour permettre la récupération à la demande via un bouton...
J'ai aussi pris en compte le cas où le nombre d'articles disponibles est inférieur au total demandé... cas de blog au lancement par exemple :)

Résultat

Avec l'ajout de jQuery pour faciliter les manipulations, on arrive rapidement à un résultat satisfaisant. Il suffit de remplacer le code du site donné précédemment par celui-ci. Vous pouvez-voir le widget en action sur le côté droit de ce blog :

La partie HTML :
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<div>
    <ul id='random-posts'>
    </ul>
    <button onclick="load_random();return false;">Autres articles</button>
</div>
Et la partie JavaScript qui vient juste après :
<script type='text/javaScript'>//<![CDATA[
    var rdp_numposts = 5;
    var rdp_snippet_length = 150;
    var rdp_info = 'yes';
    var rdp_comment = 'Comments';
    var rdp_disable = 'Comments Disabled';
    var rdp_current = [];
    var rdp_total_posts = 0;
    var rdp_current = new Array(rdp_numposts);

    function totalposts(json) {
        rdp_total_posts = json.feed.openSearch$totalResults.$t
        rdp_numposts = Math.min(rdp_numposts, rdp_total_posts);

        load_random();
    }

    function getvalue() {
        for (var i = 0; i < rdp_numposts; i++) {
            var found = false;
            var rndValue = get_random();
            for (var j = 0; j < rdp_current.length; j++) {
                if (rdp_current[j] == rndValue) {
                    found = true;
                    break
                }
            }
            ;
            if (found) {
                i--
            } else {
                rdp_current[i] = rndValue
            }
        }
    };
    function get_random() {
        var ranNum = 1 + Math.round(Math.random() * (rdp_total_posts - 1));
        return ranNum
    };
    function random_posts(json) {
        for (var i = 0; i < rdp_numposts; i++) {
            var entry = json.feed.entry[i];
            var rdp_posttitle = entry.title.$t;
            if ('content'in entry) {
                var rdp_get_snippet = entry.content.$t
            } else {
                if ('summary'in entry) {
                    var rdp_get_snippet = entry.summary.$t
                } else {
                    var rdp_get_snippet = "";
                }
            }
            ;
            rdp_get_snippet = rdp_get_snippet.replace(/<[^>]*>/g, "");
            if (rdp_get_snippet.length < rdp_snippet_length) {
                var rdp_snippet = rdp_get_snippet
            } else {
                rdp_get_snippet = rdp_get_snippet.substring(0, rdp_snippet_length);
                var space = rdp_get_snippet.lastIndexOf(" ");
                rdp_snippet = rdp_get_snippet.substring(0, space) + "…";
            }
            ;
            for (var j = 0; j < entry.link.length; j++) {
                if ('thr$total'in entry) {
                    var rdp_commentsNum = entry.thr$total.$t + ' ' + rdp_comment
                } else {
                    rdp_commentsNum = rdp_disable
                }
                ;
                if (entry.link[j].rel == 'alternate') {
                    var rdp_posturl = entry.link[j].href;
                    var rdp_postdate = entry.published.$t;
                    if ('media$thumbnail'in entry) {
                        var rdp_thumb = entry.media$thumbnail.url;
                    } else {
                        rdp_thumb = "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHpSBQPjYg2K4BsISoLTPF8B-sGrOmdrb8QA3PUV4UtXnXkuo3p-pMKSmaK6gwiVtBEjXn6tQgvOmgDpqWCbicNSe_CqdUz2g_6uilUVDPLxukX8bDh-06Gh0SD8QCs97nvNmyC42IyeY/s1600/default.jpg";
                    }
                }
            }
            ;

            var html = ('<img alt="' + rdp_posttitle + '" src="' + rdp_thumb + '"/>');
            html += ('<div><a href="' + rdp_posturl + '" rel="nofollow" title="' + rdp_snippet + '">' + rdp_posttitle + '</a></div>');
            if (rdp_info == 'yes') {
                html += ('<span>' + rdp_postdate.substring(8, 10) + '/' + rdp_postdate.substring(5, 7) + '/' + rdp_postdate.substring(0, 4) + ' - ' + rdp_commentsNum) + '</span>';
            }
            html += ('<div style="clear:both"></div>');

            jQuery("#random-posts").append(
                $('<li>').html(html)
            );
        }
    };

    function load_random(){
        rdp_current = new Array(rdp_numposts);
        getvalue();

        jQuery('#random-posts li').fadeOut(500, function(){
            jQuery(this).remove();
        });

        for (var i = 0; i < rdp_numposts; i++) {
            jQuery.ajax('/feeds/posts/default?alt=json-in-script&start-index=' + rdp_current[i] + '&max-results=1&callback=random_posts');
        }
    }

    jQuery.ajax('/feeds/posts/default?alt=json-in-script&max-results=0&callback=totalposts');
//]]>
</script>
 

Aller plus loin... 

Idéalement il faudrait aussi déplacer l'import jQuery dans le modèle HTML, histoire de ne pas créer de duplications au cas où. Mais ca restera déjà bien ainsi !
Fourni par Blogger.