10 août 2014

Migrating Phaser to CocoonJS

This post to detail my journey on making a Phaser 2.0.7 game work with CocoonJS 2.0.2 (versions ARE importants here).

 

Patch Phaser

Follow this wiki page : https://github.com/photonstorm/phaser/wiki/Phaser-General-Documentation-:-CocoonJS

 

JQuery

So, first of all, JQuery doesn't load  on CocoonJS navigator.
Move on to underscorejs.

 

XML

That may be sound crazy but CocoonJS doesn't provide any XML API. I draw some texts as Phaser bitmapText requiring an XML descriptor parsing.
This code saved my life : https://github.com/videlais/xml-for-cocoonjs

 

Scaling

I never had any issue with scaling during my previous tests (desktop, cordova build on mobile...) but there problems arise : my stage where truncated with black gaps on sides.
In phaser, my game is created using safe height, with the safe width computed based on safewidth/safeheight ratio, then scaling apply

 
this.game = 
    new Phaser.Game(widthRatio, safeHeightheight, 
    Phaser.CANVAS, 'hh_canvas', null, false, false);

hh.game.scale.setScreenSize(true); 
 
With CocoonJs the code is fairly different : full window w&h have to be applied on Game creation, and disabled setScreenSize().

this.game = 
    new Phaser.Game(window.innerWidth*window.devicePixelRatio, window.innerHeight*window.devicePixelRatio, 
    Phaser.CANVAS, 'hh_canvas', null, false, false);   

 

Black background

A fun one that took me a lot of time to figure out the problem.
I render my background using a gradient filled bitmapData : black in CocoonJS
Do NOT set transparent background on new Phaser.Game creation. Works everywhere but in CocoonJS.

 

Last sprite/image not rendered

For an unknown reason, the last element I add to game in "create" function is never renderered.
I just added a dummy.png 1x1 empty image on all my State create() function...

 

Animated texts truncated

One more a time a crazy problem : the last character of my texts, when animated, is not rendered.
Had to add a suffix "_" in case of CocoonJS navigator.
if(navigator.isCocoonJS){
    _text = _text+"_"//add a dummy last character
} 

Hope I saved you some hours I wasted.

I ll try to keep this post updated.

2 août 2014

Grails Offline mode with Maven dependency resolution

Developing with Grails 2.3.5, the maven dependency resolution is almost perfectly integrated.
It depends on the aether core engine to resolve dependencies.

A problem arises when working on offline mode, without any way to check for artifacts repositories.
The application in development runs and gets stuck on "|Configuring classpath" step... for minutes until the application finally starts!
Unfortunately, the configuration grails.offline.mode=true doesn't work with maven resolution, only with the legacy "ivy" dependency resolution.

So here is the trick, after trying loads of configurations failing (-Dsun.net.client.defaultReadTimeout, -Dsun.net.client.defaultConnectTimeout), I ended up adding the following parameters to the JVM :

-Daether.connector.connectTimeout=500 -Daether.connector.requestTimeout=500

Configuring faster timeouts, I can't even notice a latency during application boot :)

Productivity is back !

7 juin 2014

Grails and custom HQL functions and types

Developping database layer with Grails is pretty fun, I like the Closure Criteria notation.

One of my need was to query some Postgresql database columns, columns being crypted. I had to extend GORM Hibernate mapping and query to be able to work with crypting. Grails 2.3.5, Hibernate 4.1.

Mapping Domain property

First job was to map my Domain properties with a custom Hibernate type.


Hibernate UserType :
public class StringEncrypted implements UserType {

    // Here we will manipulate only BINARY persisted types (encrypted data are persisted into binary format)
    static final int SQL_TYPE = Types.BINARY
    static final int[] SQL_TYPES = [ SQL_TYPE ]

    private static boolean initialized = false;

    protected static CipherService cipherService = null;

    /**
     * The class returned by nullSafeGet().
     *
     * @return Class
     */
    @Override
    Class returnedClass() {
        return T.class
    }

    /**
     * Return the SQL type codes for the columns mapped by this type.
     * The codes are defined on java.sql.Types.
     *
     * @return int[] the typecodes
     * @see Types
     */
    @Override
    public final int[] sqlTypes() {
        return (int[]) SQL_TYPES.clone();
    }

    /**
     * Compare two instances of the class mapped by this type for persistence "equality".
     * Equality of the persistent state.
     *
     * @param x
     * @param y
     * @return boolean
     * @throws HibernateException
     */
    @Override
    public final boolean equals(final Object x, final Object y)
            throws HibernateException {
        return x == y || ( x != null && y != null && x.equals( y ) );
    }

    /**
     * Get a hashcode for the instance, consistent with persistence "equality".
     *
     * @param x
     * @return
     * @throws HibernateException
     */
    @Override
    public final int hashCode(final Object x)
            throws HibernateException {
        return x.hashCode();
    }

    /**
     * Retrieve an instance of the mapped class from a JDBC resultset. Implementors should handle possibility of null values.
     *
     * @param rs a JDBC result set
     * @param names the column names
     * @param session
     * @param owner the containing entity
     * @return Object
     * @throws HibernateException
     * @throws SQLException
     */
    @Override
    public Object nullSafeGet(final ResultSet rs, final String[] names,
                              final SessionImplementor session, final Object owner)
            throws HibernateException, SQLException {

        checkInitialization();

        final byte[] message = rs.getBytes(names[0])

        return rs.wasNull() ? null : convertToObject( cipherService.decrypt( message ) )
    }

    /**
     * Write an instance of the mapped class to a prepared statement.
     * Implementors should handle possibility of null values.
     * A multi-column type should be written to parameters starting from index.
     *
     * @param st a JDBC prepared statement
     * @param value the object to write
     * @param index statement parameter index
     * @param session
     * @throws HibernateException
     * @throws SQLException
     */
    @Override
    public void nullSafeSet(final PreparedStatement st, final Object value, final int index,
                            final SessionImplementor session) throws HibernateException, SQLException {

        checkInitialization();
        if (value == null) {
            st.setNull(index, SQL_TYPE);
        } else {
            st.setBytes(index, cipherService.encrypt( convertToByteArray( (T)value ) ) );
        }

    }

    /**
     * Return a deep copy of the persistent state, stopping at entities and at collections.
     * It is not necessary to copy immutable objects, or null values, in which case it is safe to simply return the argument.
     *
     * @param value the object to be cloned, which may be null
     * @return Object a copy
     * @throws HibernateException
     */
    @Override
    public final Object deepCopy(final Object value)
            throws HibernateException {
        return value;
    }

    /**
     * Are objects of this type mutable?
     *
     * @return boolean
     */
    @Override
    public final boolean isMutable() {
        return false;
    }

    /**
     * Transform the object into its cacheable representation. At the very least this method should perform a deep copy if the type is mutable.
     * That may not be enough for some implementations, however; for example, associations must be cached as identifier values. (optional operation)
     *
     * @param value the object to be cached
     * @return a cachable representation of the object
     * @throws HibernateException
     */
    @Override
    public final Serializable disassemble(final Object value)
            throws HibernateException {
        if (value == null) {
            return null;
        }
        return (Serializable) deepCopy(value);
    }

    /**
     * Reconstruct an object from the cacheable representation.
     * At the very least this method should perform a deep copy if the type is mutable. (optional operation)
     *
     * @param cached the object to be cached
     * @param owner the owner of the cached object
     * @return a reconstructed object from the cachable representation
     * @throws HibernateException
     */
    @Override
    public final Object assemble(final Serializable cached, final Object owner)
            throws HibernateException {
        if (cached == null) {
            return null;
        }
        return deepCopy(cached);
    }

    /**
     * During merge, replace the existing (target) value in the entity we are merging to with
     * a new (original) value from the detached entity we are merging.
     * For immutable objects, or null values, it is safe to simply return the first parameter.
     * For mutable objects, it is safe to return a copy of the first parameter.
     * For objects with component values, it might make sense to recursively replace component values.
     *
     * @param original the value from the detached entity being merged
     * @param target the value in the managed entity
     * @param owner
     * @return the value to be merged
     * @throws HibernateException
     */
    @Override
    public final Object replace(final Object original, final Object target, final Object owner)
            throws HibernateException {
        return original;
    }

    /**
     * Charset used for the non-encrypted data
     */
    public static String CHARSET = "UTF-8"

    @Override
    protected String convertToObject(byte[] byteArray) {
        return new String( byteArray, CHARSET )
    }

    @Override
    protected byte[] convertToByteArray(String object) {
        return object.getBytes( CHARSET )
    }

    /**
     * Checks if cipherService is initialized, and initializes it if necessary
     */
    protected static synchronized final void checkInitialization() {
        if (!initialized) {
            cipherService = Holders.getGrailsApplication().mainContext.getBean("cipherService")

            initialized = true;
        }

    }

Then apply this type to any Domain property :
String label
static mapping={
        label type: StringEncrypted
}

From now, the encryption/decryption will be automatically applied on Grails save and find. Magic !

Add decryption function to HQL language

To query with decryption "on the fly" by the databse engine, this requires first that you crypting algorithm is exactly the same between your JVM and your database. Use "AES/CBC/PKCS5Padding" between Java and Postgresql.

In you BootStrap.groovy, add a decrypting function to Hibernate :

def dialect = grailsApplication.mainContext.sessionFactory.dialect

        def decryptString = new SQLFunctionTemplate(BinaryType.INSTANCE, "convert_from(decrypt(?1, convert_to('"+grailsApplication.config.cipherKey+"', 'UTF8'), 'aes'), 'UTF8')"){
            public Type getReturnType(Type columnType, Mapping mapping){
                return StringType.INSTANCE;
            }
        }
        dialect.registerFunction('decryptString', decryptString)

From now, a new HQL function is available : decryptString.

Query

You can now decrypt on the fly in your HQL function :


def res = DemoDomain.executeQuery(
                "FROM DemoDomain \
                WHERE decryptString(label) = :testValue", [testValue : testValue])[0]

Decryption is processed by the database to query on crypted column.

Note

  1. This raises performance issues as the query performs a full table scan... Well sometimes you just haven't the choice but to query this way.
  2. Couldn't find a way to query with add custom function on Closure notation. What a pity.
Thanks to this blog, really help me.

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.